Compare commits

..
Author SHA1 Message Date
Théophile DesmedtandGitHub 6680967875 Improve discoverability of the style attribute feature (#9975)
* Improve discoverability of the style attribute feature

* Add missing tiddler  "Style Attributes"
2026-08-21 09:04:34 +02:00
Mario PietschandGitHub f9d5ee0ad7 Add ButtonWidget examples and execution order (#9945)
Document the button widget with runnable test cases and explain how its
attributes combine.

* Add eleven test case tiddlers, listed by title prefix so a new one needs
  no edit to the widget page
* Describe the order in which a button runs body actions, to, message,
  popup, set and actions, whatever order they are written in
* Prefer class over a hardcoded style
2026-08-16 13:17:33 +02:00
Mario PietschandGitHub 68630c939d Docs only Groupedlists examples (#9954)
* Add GroupedLists examples

Document grouped lists with runnable test cases over a shared
bibliography sample.

* Cover plain grouping, search, tabs by first character, both combined,
  and grouping on one field while searching another
* Name the grouped field in one procedure, so the grouping changes in
  one line
* Import it with import-compound, and list the examples by tag so a new
  one needs no page edit
* Rewrite the opening paragraph, which described only the sidebar tabs

* Replace GroupedLists code dumps with examples

Turn the two sidebar tabs the page describes into runnable test cases,
and tighten how the examples handle an empty result.

* Give each new example its own dummy payload, and point the page at the
  live tabs instead of quoting core source
* Carry the values through the filter from the start, so an All tab with
  no matches yields nothing rather than the literal All
* Use the list-empty widget

* use tc-tiny-gap instead of -left -right
2026-08-16 13:16:04 +02:00
superuser-doesandGitHub 7efbd01801 update 5.4.0 release notes: there was no change to default code wrap (#9962) 2026-08-08 11:46:30 +02:00
Théophile DesmedtandGitHub 5143138c16 [DOCS] Update [[Custom Widgets]] with new example for overriding LinkWidget (#9961)
* [DOCS] Update [[Custom Widgets]] with new example for overriding LinkWidget

* fix: non-svg link icon are now sizing properly

* replace HelloThere by list-link in the example for robustness

* Add class to span for better styling in Custom Widgets

* Fix typo in Custom Widgets.tid documentation

* Improve doc by using the proper macro to link widgets

* Change font size style to important for title icon

Updated the font size style for the title icon to be important.

* Update transclude and slot elements in Custom Widgets

Provide a fallback if no body for the link is given

* Enhance link widget to show icons conditionally

Updated the link widget to conditionally display an icon next to the link based on the target tiddler's icon field. Modified the rendering logic to use a conditional statement for the icon display.

* Change transclude height to size in Custom Widgets
2026-08-07 22:05:06 +02:00
Jeremy Ruston ceb0817176 Update Quine instructions 2026-08-06 10:48:53 +01:00
113 changed files with 1669 additions and 1946 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# Default to the current version number for building the plugin library
if [ -z "$TW5_BUILD_VERSION" ]; then
TW5_BUILD_VERSION=v5.5.0.
TW5_BUILD_VERSION=v5.4.1.
fi
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
+5 -33
View File
@@ -217,11 +217,6 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
fileInfo.isEditableFile = true;
fileInfo.originalpath = options.fileInfo.originalpath;
}
// Propagate the pinFilepath flag (so generateTiddlerFilepath can short-circuit
// FileSystemPaths and write back to the original location)
if(options.fileInfo && !!options.fileInfo.pinFilepath) {
fileInfo.pinFilepath = true;
}
// Check if the tiddler has any unsafe fields that can't be expressed in a .tid or .meta file: containing control characters, or leading/trailing whitespace
var hasUnsafeFields = false;
$tw.utils.each(tiddler.getFieldStrings(),function(value,fieldName) {
@@ -324,16 +319,9 @@ exports.generateTiddlerFilepath = function(title,options) {
extension = options.extension || "",
originalpath = (options.fileInfo && options.fileInfo.originalpath) ? options.fileInfo.originalpath : "",
overwrite = options.fileInfo && options.fileInfo.overwrite || false,
pinFilepath = !!(options.fileInfo && options.fileInfo.pinFilepath),
filepath;
// If the tiddler's filepath is pinned via tiddlywiki.files, the originalpath
// wins and the path filters are skipped entirely.
if(pinFilepath && originalpath) {
var pinnedExt = path.extname(originalpath);
filepath = originalpath.substring(0,originalpath.length - pinnedExt.length);
}
// Check if any of the pathFilters applies
if(!filepath && options.pathFilters && options.wiki) {
if(options.pathFilters && options.wiki) {
$tw.utils.each(options.pathFilters,function(filter) {
if(!filepath) {
var source = options.wiki.makeTiddlerIterator([title]),
@@ -348,14 +336,13 @@ exports.generateTiddlerFilepath = function(title,options) {
//Use the originalpath without the extension
var ext = path.extname(originalpath);
filepath = originalpath.substring(0,originalpath.length - ext.length);
// normalise "\" to "/" so the sanitiser keeps subdirectories
// (it strips "\", not "/") instead of flattening them.
filepath = filepath.split(path.sep).join("/");
} else if(!filepath) {
filepath = title;
// Remove any forward or backward slashes so we don't create directories
filepath = filepath.replace(/\/|\\/g,"_");
}
// Replace any Windows control codes
filepath = filepath.replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i,"_$1_");
// Replace any leading spaces with the same number of underscores
filepath = filepath.replace(/^ +/,function (u) { return u.replace(/ /g, "_");});
//If the path does not start with "." or ".." && a path seperator, then
@@ -365,23 +352,8 @@ exports.generateTiddlerFilepath = function(title,options) {
}
// Replace any Unicode control codes
filepath = filepath.replace(/[\x00-\x1f\x80-\x9f]/g,"_");
// Replace any characters that can't be used in cross-platform filenames.
if(!pinFilepath) {
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^|\\/g,"_"));
}
// Per segment (catches "sub/CON"), MUST be after transliterate which can create a
// reserved name. Reserved names are wrapped on every path incl pinned;
// trailing dots/spaces (Windows strips) fixed for generated names, not pinned.
filepath = filepath.split("/").map(function(segment) {
if(segment === "" || segment === "." || segment === "..") {
return segment;
}
segment = segment.replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i,"_$1_");
if(!pinFilepath) {
segment = segment.replace(/[. ]+$/,function(u) { return u.replace(/[. ]/g,"_"); });
}
return segment;
}).join("/");
// Replace any characters that can't be used in cross-platform filenames
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/g,"_"));
// Replace any dots or spaces at the end of the extension with the same number of underscores
extension = extension.replace(/[\. ]+$/, function (u) { return u.replace(/[\. ]/g, "_");});
// Truncate the extension if it is too long
+5 -4
View File
@@ -9,14 +9,15 @@ Serve tiddlers over http
"use strict";
let fs, path, crypto, zlib, URL;
let fs, url, path, querystring, crypto, zlib;
if($tw.node) {
fs = require("fs"),
url = require("url"),
path = require("path"),
querystring = require("querystring"),
crypto = require("crypto"),
zlib = require("zlib");
URL = require("url").URL;
}
/*
@@ -259,8 +260,8 @@ Server.prototype.requestHandler = function(request,response,options) {
state.wiki = options.wiki || self.wiki;
state.boot = options.boot || self.boot;
state.server = self;
state.urlInfo = new URL(request.url, "http://localhost");
state.queryParameters = Object.fromEntries(state.urlInfo.searchParams);
state.urlInfo = url.parse(request.url);
state.queryParameters = querystring.parse(state.urlInfo.query);
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
// Enable CORS
if(this.corsEnable) {
+1 -2
View File
@@ -255,5 +255,4 @@ ViewTemplateTags/Caption: View Template Tags
ViewTemplateTags/Hint: This rule cascade is used by the default view template to dynamically choose the template for displaying the tags area of a tiddler.
WikiInformation/Caption: Wiki Information
WikiInformation/Hint: This page summarises high level information about the configuration of this ~TiddlyWiki. It is designed to enable users to quickly share relevant aspects of the configuration of their ~TiddlyWiki with others, for example when seeking help in one of the forums. No private or personal information is included, and nothing is shared without being explicitly copied and pasted elsewhere
WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki
WikiInformation/Generate/Caption: Click to generate wiki information report
WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki
+1 -1
View File
@@ -78,6 +78,7 @@ class BackgroundActionTracker {
fnProcess: (changes) => {
if(this.hasChanged) {
this.hasChanged = false;
console.log("Processing background action", this.title);
const tiddler = this.wiki.getTiddler(this.title);
let doActions = true;
if(tiddler && tiddler.fields.platforms) {
@@ -88,7 +89,6 @@ class BackgroundActionTracker {
}
}
if(doActions) {
console.log("Processing background action", this.title);
this.wiki.invokeActionString(
this.actions,
null,
+1 -27
View File
@@ -210,29 +210,6 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
this.editShowToolbar = this.wiki.getTiddlerText(ENABLE_TOOLBAR_TITLE,"yes");
this.editShowToolbar = (this.editShowToolbar === "yes") && !!(this.children && this.children.length > 0) && (!this.document.isTiddlyWikiFakeDom);
};
EditTextWidget.prototype.updateDomNodeClasses = function() {
var domNodeClasses = this.engine.domNode.className.split(/\s+/).filter(Boolean),
oldClasses = this.editClass.split(/\s+/).filter(Boolean),
newClasses;
this.editClass = this.getAttribute("class","");
newClasses = this.editClass.split(/\s+/).filter(Boolean);
// Remove classes assigned from the old value of the class attribute
domNodeClasses = domNodeClasses.filter(function(className) {
return !oldClasses.includes(className);
});
// Add new classes from the updated class attribute
domNodeClasses = domNodeClasses.concat(
newClasses.filter(function(className) {
return !domNodeClasses.includes(className);
})
);
this.engine.domNode.className = domNodeClasses.join(" ");
};
/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
@@ -240,7 +217,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
EditTextWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
// Completely rerender if any of our attributes have changed
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedTiddlers["$:/palette"] || changedAttributes.disabled || changedAttributes.fileDrop) {
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedTiddlers["$:/palette"] || changedAttributes.disabled || changedAttributes.fileDrop) {
this.refreshSelf();
return true;
} else if(changedTiddlers[this.editRefreshTitle]) {
@@ -249,9 +226,6 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
var editInfo = this.getEditInfo();
this.updateEditor(editInfo.value,editInfo.type);
}
if(changedAttributes["class"]) {
this.updateDomNodeClasses();
}
this.engine.fixHeight();
if(this.editShowToolbar) {
return this.refreshChildren(changedTiddlers);
@@ -13,15 +13,14 @@ Export our filter prefix function
exports.intersection = function(operationSubFunction) {
return function(results,source,widget) {
if(results.length !== 0) {
const secondRunResults = operationSubFunction(source,widget),
secondRunSet = new Set(secondRunResults),
firstRunResults = results.toArray();
var secondRunResults = operationSubFunction(source,widget);
var firstRunResults = results.toArray();
results.clear();
firstRunResults.forEach((title) => {
if(secondRunSet.has(title)) {
$tw.utils.each(firstRunResults,function(title) {
if(secondRunResults.indexOf(title) !== -1) {
results.push(title);
}
});
}
};
};
};
+20 -15
View File
@@ -13,28 +13,33 @@ Export our filter prefix function
exports.sort = function(operationSubFunction,options) {
return function(results,source,widget) {
if(results.length > 0) {
const suffixes = options.suffixes,
var suffixes = options.suffixes,
sortType = (suffixes[0] && suffixes[0][0]) ? suffixes[0][0] : "string",
invert = suffixes[1] ? suffixes[1].includes("reverse") : false,
isCaseSensitive = suffixes[1] ? suffixes[1].includes("casesensitive") : false,
invert = suffixes[1] ? (suffixes[1].indexOf("reverse") !== -1) : false,
isCaseSensitive = suffixes[1] ? (suffixes[1].indexOf("casesensitive") !== -1) : false,
inputTitles = results.toArray(),
sortKeys = [],
compareFn = $tw.utils.makeCompareFunction(sortType,{defaultType: "string", invert:invert, isCaseSensitive:isCaseSensitive});
results.each((title) => {
const key = operationSubFunction(
options.wiki.makeTiddlerIterator([title]),
widget.makeFakeWidgetWithVariables({
"currentTiddler": "" + title,
"..currentTiddler": widget.getVariable("currentTiddler",{defaultValue:""})
})
);
indexes = new Array(inputTitles.length),
compareFn;
results.each(function(title) {
var key = operationSubFunction(options.wiki.makeTiddlerIterator([title]),widget.makeFakeWidgetWithVariables({
"currentTiddler": "" + title,
"..currentTiddler": widget.getVariable("currentTiddler",{defaultValue:""})
}));
sortKeys.push(key[0] || "");
});
results.clear();
// Prepare an array of indexes to sort
let indexes = Array.from(inputTitles.keys());
indexes.sort((a,b) => compareFn(sortKeys[a],sortKeys[b]));
indexes.forEach((index) => {
for(var t=0; t<inputTitles.length; t++) {
indexes[t] = t;
}
// Sort the indexes
compareFn = $tw.utils.makeCompareFunction(sortType,{defaultType: "string", invert:invert, isCaseSensitive:isCaseSensitive});
indexes = indexes.sort(function(a,b) {
return compareFn(sortKeys[a],sortKeys[b]);
});
// Add to results in correct order
$tw.utils.each(indexes,function(index) {
results.push(inputTitles[index]);
});
}
+1 -1
View File
@@ -296,7 +296,7 @@ exports.compileFilter = function(filterString) {
var varTree = $tw.utils.parseFilterVariable(operand.text);
var resultList = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source});
if((resultList.length > 0 && resultList[0] !== undefined) || resultList.length === 0) {
operand.multiValue = resultList || [];
operand.multiValue = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source}) || [];
operand.value = operand.multiValue[0] || "";
} else {
operand.value = "";
+54 -30
View File
@@ -9,56 +9,69 @@ Filter operators for manipulating the current selection list
"use strict";
/*
Fetch titles from the current list
*/
const prepare_results = (source) => {
const results = [];
source((tiddler,title) => results.push(title));
return results;
};
/*
Order a list
*/
exports.order = function(source,operator,options) {
const results = prepare_results(source);
return operator.operand.toLowerCase() === "reverse" ?
results.reverse() :
results;
var results = [];
if(operator.operand.toLowerCase() === "reverse") {
source(function(tiddler,title) {
results.unshift(title);
});
} else {
source(function(tiddler,title) {
results.push(title);
});
}
return results;
};
/*
Reverse list
*/
exports.reverse = function(source,operator,options) {
return prepare_results(source).reverse();
var results = [];
source(function(tiddler,title) {
results.unshift(title);
});
return results;
};
/*
First entry/entries in list
*/
exports.first = function(source,operator,options) {
const count = $tw.utils.getInt(operator.operand,1);
return prepare_results(source).slice(0,count);
var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
});
return results.slice(0,count);
};
/*
Last entry/entries in list
*/
exports.last = function(source,operator,options) {
const count = $tw.utils.getInt(operator.operand,1);
return count === 0 ?
[] :
prepare_results(source).slice(-count);
var count = $tw.utils.getInt(operator.operand,1),
results = [];
if(count === 0) return results;
source(function(tiddler,title) {
results.push(title);
});
return results.slice(-count);
};
/*
All but the first entry/entries of the list
*/
exports.rest = function(source,operator,options) {
const count = $tw.utils.getInt(operator.operand,1);
return prepare_results(source).slice(count);
var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
});
return results.slice(count);
};
exports.butfirst = exports.rest;
exports.bf = exports.rest;
@@ -67,9 +80,12 @@ exports.bf = exports.rest;
All but the last entry/entries of the list
*/
exports.butlast = function(source,operator,options) {
const count = $tw.utils.getInt(operator.operand,1),
results = prepare_results(source),
index = count === 0 ? results.length : -count;
var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
});
var index = count === 0 ? results.length : -count;
return results.slice(0,index);
};
exports.bl = exports.butlast;
@@ -78,14 +94,22 @@ exports.bl = exports.butlast;
The nth member of the list
*/
exports.nth = function(source,operator,options) {
const count = $tw.utils.getInt(operator.operand,1);
return prepare_results(source).slice(count - 1,count);
var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
});
return results.slice(count - 1,count);
};
/*
The zero based nth member of the list
*/
exports.zth = function(source,operator,options) {
const count = $tw.utils.getInt(operator.operand,0);
return prepare_results(source).slice(count,count + 1);
};
var count = $tw.utils.getInt(operator.operand,0),
results = [];
source(function(tiddler,title) {
results.push(title);
});
return results.slice(count,count + 1);
};
+32 -17
View File
@@ -13,22 +13,37 @@ Filter operator for checking if a title starts with a prefix
Export our filter function
*/
exports.prefix = function(source,operator,options) {
const results = [],
suffixes = (operator.suffixes || [])[0] || [],
caseInsensitive = suffixes.includes("caseinsensitive"),
negate = operator.prefix === "!";
const operand = caseInsensitive ?
operator.operand.toLowerCase() :
operator.operand;
source((tiddler,title) => {
const value = caseInsensitive ? title.toLowerCase() : title,
matches = value.startsWith(operand);
if(negate ? !matches : matches) {
results.push(title);
var results = [],
suffixes = (operator.suffixes || [])[0] || [];
if(suffixes.indexOf("caseinsensitive") !== -1) {
var operand = operator.operand.toLowerCase();
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(title.toLowerCase().substr(0,operand.length) !== operand) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
if(title.toLowerCase().substr(0,operand.length) === operand) {
results.push(title);
}
});
}
});
} else {
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(title.substr(0,operator.operand.length) !== operator.operand) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
if(title.substr(0,operator.operand.length) === operator.operand) {
results.push(title);
}
});
}
}
return results;
};
};
+12 -7
View File
@@ -14,13 +14,13 @@ Export our filter function
*/
exports.sortsub = function(source,operator,options) {
// Compile the subfilter
let filterFn = options.wiki.compileFilter(operator.operand);
var filterFn = options.wiki.compileFilter(operator.operand);
// Collect the input titles and the corresponding sort keys
let inputTitles = [],
var inputTitles = [],
sortKeys = [];
source(function(tiddler,title) {
inputTitles.push(title);
let r = filterFn.call(options.wiki,function(iterator) {
var r = filterFn.call(options.wiki,function(iterator) {
iterator(options.wiki.getTiddler(title),title);
},options.widget.makeFakeWidgetWithVariables({
"currentTiddler": "" + title,
@@ -29,12 +29,17 @@ exports.sortsub = function(source,operator,options) {
sortKeys.push(r[0] || "");
});
// Rather than sorting the titles array, we'll sort the indexes so that we can consult both arrays
let indexes = Array.from(inputTitles.keys());
var indexes = new Array(inputTitles.length);
for(var t=0; t<inputTitles.length; t++) {
indexes[t] = t;
}
// Sort the indexes
let compareFn = $tw.utils.makeCompareFunction(operator.suffix,{defaultType: "string",invert: operator.prefix === "!"});
indexes = indexes.sort((a,b) => compareFn(sortKeys[a],sortKeys[b]));
var compareFn = $tw.utils.makeCompareFunction(operator.suffix,{defaultType: "string",invert: operator.prefix === "!"});
indexes = indexes.sort(function(a,b) {
return compareFn(sortKeys[a],sortKeys[b]);
});
// Make the results array in order
let results = [];
var results = [];
$tw.utils.each(indexes,function(index) {
results.push(inputTitles[index]);
});
+13 -26
View File
@@ -91,35 +91,22 @@ function diffLineWordMode(text1,text2,mode) {
return diffs;
}
exports.makepatches = function(source, operator, options) {
const suffixes = operator.suffixes || [],
[modeArg = [], formatArg = []] = suffixes,
modeSuffix = modeArg[0] || operator.suffix || "",
mode = ["lines", "words"].includes(modeSuffix) ? modeSuffix : "",
isJson = formatArg[0] === "json",
results = [];
source((tiddler, title) => {
if (isJson) {
const diffs = (mode === "lines" || mode === "words")
? diffLineWordMode(title, operator.operand, mode)
: dmp.diffMain(title, operator.operand);
const jsonOutput = diffs.map(([typeCode, text]) => ({
type: typeCode === 1 ? "insert" : (typeCode === -1 ? "delete" : "equal"),
text
}));
results.push(JSON.stringify(jsonOutput));
exports.makepatches = function(source,operator,options) {
var suffix = operator.suffix || "",
result = [];
source(function(tiddler,title) {
let diffs, patches;
if(suffix === "lines" || suffix === "words") {
diffs = diffLineWordMode(title,operator.operand,suffix);
patches = dmp.patchMake(title,diffs);
} else {
const patches = (mode === "lines" || mode === "words")
? dmp.patchMake(title, diffLineWordMode(title, operator.operand, mode))
: dmp.patchMake(title, operator.operand);
results.push(dmp.patchToText(patches));
patches = dmp.patchMake(title,operator.operand);
}
Array.prototype.push.apply(result,[dmp.patchToText(patches)]);
});
return results;
return result;
};
exports.applypatches = makeStringBinaryOperator(
@@ -248,4 +235,4 @@ exports.charcode = function(source,operator,options) {
}
});
return [chars.join("")];
};
};
+4 -5
View File
@@ -13,12 +13,11 @@ Filter operator returning its operand evaluated as a filter
Export our filter function
*/
exports.subfilter = function(source,operator,options) {
const list = options.wiki.filterTiddlers(operator.operand,options.widget,source);
var list = options.wiki.filterTiddlers(operator.operand,options.widget,source);
if(operator.prefix === "!") {
const results = [],
listSet = new Set(list);
source((tiddler,title) => {
if(!listSet.has(title)) {
var results = [];
source(function(tiddler,title) {
if(list.indexOf(title) === -1) {
results.push(title);
}
});
+31 -17
View File
@@ -13,27 +13,41 @@ Filter operator for checking if a title ends with a suffix
Export our filter function
*/
exports.suffix = function(source,operator,options) {
const results = [],
var results = [],
suffixes = (operator.suffixes || [])[0] || [];
if(!operator.operand) {
source((tiddler,title) => {
source(function(tiddler,title) {
results.push(title);
});
return results;
}
const caseInsensitive = suffixes.indexOf("caseinsensitive") !== -1,
negate = operator.prefix === "!",
operand = caseInsensitive ? operator.operand.toLowerCase() : operator.operand;
source((tiddler,title) => {
const value = caseInsensitive ? title.toLowerCase() : title,
matches = value.endsWith(operand);
if(negate ? !matches : matches) {
results.push(title);
} else if(suffixes.indexOf("caseinsensitive") !== -1) {
var operand = operator.operand.toLowerCase();
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(title.toLowerCase().substr(-operand.length) !== operand) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
if(title.toLowerCase().substr(-operand.length) === operand) {
results.push(title);
}
});
}
});
} else {
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(title.substr(-operator.operand.length) !== operator.operand) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
if(title.substr(-operator.operand.length) === operator.operand) {
results.push(title);
}
});
}
}
return results;
};
+5 -4
View File
@@ -22,11 +22,12 @@ exports.tag = function(source,operator,options) {
});
} else {
// Old semantics:
var tiddlers;
if(operator.prefix === "!") {
// Returns a copy of the input if operator.operand is missing
const excludeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
source(function(tiddler,title) {
if(!excludeTagSet.has(title)) {
if(tiddlers.indexOf(title) === -1) {
results.push(title);
}
});
@@ -38,9 +39,9 @@ exports.tag = function(source,operator,options) {
return indexedResults;
}
} else {
const includeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
source(function(tiddler,title) {
if(includeTagSet.has(title)) {
if(tiddlers.indexOf(title) !== -1) {
results.push(title);
}
});
+81 -76
View File
@@ -11,18 +11,20 @@ Extended filter operators to manipulate the current list.
/*
Fetch titles from the current list
*/
const prepare_results = (source) => {
const results = [];
source((tiddler,title) => results.push(title));
*/
var prepare_results = function (source) {
var results = [];
source(function (tiddler, title) {
results.push(title);
});
return results;
};
/*
Moves a number of items from the tail of the current list before the item named in the operand
*/
exports.putbefore = function(source,operator) {
const results = prepare_results(source),
*/
exports.putbefore = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand),
count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
@@ -32,9 +34,9 @@ exports.putbefore = function(source,operator) {
/*
Moves a number of items from the tail of the current list after the item named in the operand
*/
exports.putafter = function(source,operator) {
const results = prepare_results(source),
*/
exports.putafter = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand),
count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
@@ -44,9 +46,9 @@ exports.putafter = function(source,operator) {
/*
Replaces the item named in the operand with a number of items from the tail of the current list
*/
exports.replace = function(source,operator) {
const results = prepare_results(source),
*/
exports.replace = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand),
count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
@@ -56,39 +58,39 @@ exports.replace = function(source,operator) {
/*
Moves a number of items from the tail of the current list to the head of the list
*/
exports.putfirst = function(source,operator) {
const results = prepare_results(source),
*/
exports.putfirst = function (source, operator) {
var results = prepare_results(source),
count = $tw.utils.getInt(operator.suffix,1);
return [...results.slice(-count), ...results.slice(0, -count)];
return results.slice(-count).concat(results.slice(0, -count));
};
/*
Moves a number of items from the head of the current list to the tail of the list
*/
exports.putlast = function(source,operator) {
const results = prepare_results(source),
*/
exports.putlast = function (source, operator) {
var results = prepare_results(source),
count = $tw.utils.getInt(operator.suffix,1);
return [...results.slice(count), ...results.slice(0, count)];
return results.slice(count).concat(results.slice(0, count));
};
/*
Moves the item named in the operand a number of places forward or backward in the list
*/
exports.move = function(source,operator) {
const results = prepare_results(source),
*/
exports.move = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand),
count = $tw.utils.getInt(operator.suffix,1),
marker = results.splice(index, 1),
offset = (index + count) > 0 ? index + count : 0;
offset = (index + count) > 0 ? index + count : 0;
return results.slice(0, offset).concat(marker).concat(results.slice(offset));
};
/*
Returns the items from the current list that are after the item named in the operand
*/
exports.allafter = function(source,operator) {
const results = prepare_results(source),
*/
exports.allafter = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand);
return (index === -1) ? [] :
(operator.suffix) ? results.slice(index) :
@@ -97,9 +99,9 @@ exports.allafter = function(source,operator) {
/*
Returns the items from the current list that are before the item named in the operand
*/
exports.allbefore = function(source,operator) {
const results = prepare_results(source),
*/
exports.allbefore = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand);
return (index === -1) ? [] :
(operator.suffix) ? results.slice(0, index + 1) :
@@ -108,9 +110,9 @@ exports.allbefore = function(source,operator) {
/*
Appends the items listed in the operand array to the tail of the current list
*/
exports.append = function(source,operator) {
const append = $tw.utils.parseStringArray(operator.operand,"true"),
*/
exports.append = function (source, operator) {
var append = $tw.utils.parseStringArray(operator.operand, "true"),
results = prepare_results(source),
count = parseInt(operator.suffix) || append.length;
return (append.length === 0) ? results :
@@ -120,9 +122,9 @@ exports.append = function(source,operator) {
/*
Prepends the items listed in the operand array to the head of the current list
*/
exports.prepend = function(source,operator) {
const prepend = $tw.utils.parseStringArray(operator.operand,"true"),
*/
exports.prepend = function (source, operator) {
var prepend = $tw.utils.parseStringArray(operator.operand, "true"),
results = prepare_results(source),
count = $tw.utils.getInt(operator.suffix,prepend.length);
return (prepend.length === 0) ? results :
@@ -132,19 +134,21 @@ exports.prepend = function(source,operator) {
/*
Returns all items from the current list except the items listed in the operand array
*/
exports.remove = function(source, operator) {
const array = $tw.utils.parseStringArray(operator.operand, "true"),
results = prepare_results(source);
if(array.length === 0) {
return results;
}
const count = parseInt(operator.suffix, 10) || array.length,
targetItems = operator.prefix ? array.slice(-count).reverse() : array.slice(0, count);
for(const item of targetItems) {
const index = results.indexOf(item);
*/
exports.remove = function (source, operator) {
var array = $tw.utils.parseStringArray(operator.operand, "true"),
results = prepare_results(source),
count = parseInt(operator.suffix) || array.length,
p,
len,
index;
len = array.length - 1;
for(p = 0; p < count; ++p) {
if(operator.prefix) {
index = results.indexOf(array[len - p]);
} else {
index = results.indexOf(array[p]);
}
if(index !== -1) {
results.splice(index, 1);
}
@@ -154,38 +158,40 @@ exports.remove = function(source, operator) {
/*
Returns all items from the current list sorted in the order of the items in the operand array
*/
exports.sortby = function(source,operator) {
const results = prepare_results(source);
*/
exports.sortby = function (source, operator) {
var results = prepare_results(source);
if(!results || results.length < 2) {
return results;
}
const lookup = $tw.utils.parseStringArray(operator.operand,"true"),
// The "end" suffix places unlisted titles last, the default is first
unlisted = operator.suffix === "end" ? lookup.length : -1,
position = (title) => {
const index = lookup.indexOf(title);
return index === -1 ? unlisted : index;
};
return results.sort((a,b) => position(a) - position(b));
var lookup = $tw.utils.parseStringArray(operator.operand, "true");
results.sort(function (a, b) {
return lookup.indexOf(a) - lookup.indexOf(b);
});
return results;
};
/*
Removes all duplicate items from the current list
*/
exports.unique = function(source, operator) {
return Array.from(new Set(prepare_results(source)));
*/
exports.unique = function (source, operator) {
var results = prepare_results(source);
var set = results.reduce(function (a, b) {
if(a.indexOf(b) < 0) {
a.push(b);
}
return a;
}, []);
return set;
};
const cycleValueInArray = function(results,operands,stepSize) {
let resultsIndex,
var cycleValueInArray = function(results,operands,stepSize) {
var resultsIndex,
step = stepSize || 1,
i = 0,
opLength = operands.length,
nextOperandIndex;
const opLength = operands.length;
for(; i < opLength; i++) {
for(i; i < opLength; i++) {
resultsIndex = results.indexOf(operands[i]);
if(resultsIndex !== -1) {
break;
@@ -207,19 +213,18 @@ const cycleValueInArray = function(results,operands,stepSize) {
/*
Toggles an item in the current list.
*/
*/
exports.toggle = function(source,operator) {
return cycleValueInArray(prepare_results(source),operator.operands);
};
exports.cycle = function(source,operator) {
const results = prepare_results(source),
operands = operator.operand.length ? $tw.utils.parseStringArray(operator.operand,"true") : [""];
let step = $tw.utils.getInt(operator.operands[1] || "",1);
var results = prepare_results(source),
operands = (operator.operand.length ? $tw.utils.parseStringArray(operator.operand, "true") : [""]),
step = $tw.utils.getInt(operator.operands[1]||"",1);
if(step < 0) {
operands.reverse();
step = Math.abs(step);
}
return cycleValueInArray(results,operands,step);
};
};
+2 -3
View File
@@ -88,11 +88,10 @@ BackSubIndexer.prototype.update = function(updateDescriptor) {
var newTargets = [],
oldTargets = [],
self = this;
// System tiddlers are never indexed as sources, matching the _init() scan
if(updateDescriptor.old.exists && !this.wiki.isSystemTiddler(updateDescriptor.old.tiddler.fields.title)) {
if(updateDescriptor.old.exists) {
oldTargets = this._getTarget(updateDescriptor.old.tiddler);
}
if(updateDescriptor.new.exists && !this.wiki.isSystemTiddler(updateDescriptor.new.tiddler.fields.title)) {
if(updateDescriptor.new.exists) {
newTargets = this._getTarget(updateDescriptor.new.tiddler);
}
+54 -66
View File
@@ -528,79 +528,67 @@ exports.parseAttribute = function(source,pos) {
pos = token.end;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
do {
// Look for a string literal
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
if(stringLiteral) {
pos = stringLiteral.end;
node.type = "string";
node.value = stringLiteral.value;
break;
}
// Look for a string literal
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
if(stringLiteral) {
pos = stringLiteral.end;
node.type = "string";
node.value = stringLiteral.value;
} else {
// Look for a filtered value
var filteredValue = $tw.utils.parseTokenRegExp(source,pos,reFilteredValue);
if(filteredValue) {
pos = filteredValue.end;
node.type = "filtered";
node.filter = filteredValue.match[1];
break;
} else {
// Look for an indirect value
var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
if(indirectValue) {
pos = indirectValue.end;
node.type = "indirect";
node.textReference = indirectValue.match[1];
} else {
// Look for a macro invocation value
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
if(macroInvocation) {
pos = macroInvocation.end;
node.type = "macro";
node.value = macroInvocation;
} else {
// Look for an MVV reference value
var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos);
if(mvvReference) {
pos = mvvReference.end;
node.type = "macro";
node.value = mvvReference;
node.isMVV = true;
} else {
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
if(substitutedValue) {
pos = substitutedValue.end;
node.type = "substituted";
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
} else {
// Look for a unquoted value
var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
if(unquotedValue) {
pos = unquotedValue.end;
node.type = "string";
node.value = unquotedValue.match[1];
} else if(source.charAt(pos) === "<" && source.charAt(pos + 1) === "<" && source.indexOf(">>",pos) !== -1) {
// Value looks like a macro invocation (starts with << with a closing >> ahead) but does not parse as one. Return null so the enclosing tag fails to parse rather than silently binding the attribute to "true" and treating the remainder as further attributes (restores v5.3.8 behaviour)
return null;
} else {
node.type = "string";
node.value = "true";
}
}
}
}
}
}
// Look for an indirect value
var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
if(indirectValue) {
pos = indirectValue.end;
node.type = "indirect";
node.textReference = indirectValue.match[1];
break;
}
// Look for a macro invocation value
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
if(macroInvocation) {
pos = macroInvocation.end;
node.type = "macro";
node.value = macroInvocation;
break;
}
// Look for an MVV reference value
var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos);
if(mvvReference) {
pos = mvvReference.end;
node.type = "macro";
node.value = mvvReference;
node.isMVV = true;
break;
}
// Look for a substituted value
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
if(substitutedValue) {
pos = substitutedValue.end;
node.type = "substituted";
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
break;
}
// Look for a unquoted value
var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
if(unquotedValue) {
pos = unquotedValue.end;
node.type = "string";
node.value = unquotedValue.match[1];
break;
}
if(source.charAt(pos) === "<" && source.charAt(pos + 1) === "<" && source.indexOf(">>",pos) !== -1) {
// Value looks like a macro invocation (starts with << with a closing >> ahead) but does not parse as one. Return null so the enclosing tag fails to parse rather than silently binding the attribute to "true" and treating the remainder as further attributes (restores v5.3.8 behaviour)
return null;
}
node.type = "string";
node.value = "true";
} while(false);
}
} else {
// If there is no equals sign or colon, then this is an attribute with no value, defaulting to "true"
node.type = "string";
@@ -25,7 +25,7 @@ exports.init = function(parser) {
};
exports.parse = function() {
var reEnd = /(^|\r?\n)```$/mg;
var reEnd = /(\r?\n```$)/mg;
var languageStart = this.parser.pos + 3,
languageEnd = languageStart + this.match[1].length;
// Move past the match
+1 -2
View File
@@ -15,8 +15,7 @@ var getCellInfo = function(text, start, length, SEPARATOR) {
var isCellQuoted = text.charAt(start) === QUOTE;
var cellStart = isCellQuoted ? start + 1 : start;
// A quote licenses a separator inside the cell, so only an unquoted cell reads an immediate separator as empty
if(!isCellQuoted && text.charAt(cellStart) === SEPARATOR) {
if(text.charAt(i) === SEPARATOR) {
return [cellStart, cellStart, false];
}
+5 -6
View File
@@ -164,16 +164,15 @@ exports.forceLayout = function(element) {
Pulse an element for debugging purposes
*/
exports.pulseElement = function(element) {
var eventName = $tw.utils.convertEventName("animationEnd");
// Event handler to remove the class at the end
element.addEventListener(eventName,function handler(event) {
element.removeEventListener(eventName,handler,false);
$tw.utils.removeClass(element,"tc-pulse");
element.addEventListener($tw.browser.animationEnd,function handler(event) {
element.removeEventListener($tw.browser.animationEnd,handler,false);
$tw.utils.removeClass(element,"pulse");
},false);
// Apply the pulse class
$tw.utils.removeClass(element,"tc-pulse");
$tw.utils.removeClass(element,"pulse");
$tw.utils.forceLayout(element);
$tw.utils.addClass(element,"tc-pulse");
$tw.utils.addClass(element,"pulse");
};
/*
+2 -13
View File
@@ -115,9 +115,7 @@ var TW_Element = function(tag, namespace) {
this.children = [];
this._style = {}; // Internal style object
this.style = new TW_Style(this); // Proxy for style management
// createElementNS with empty-string or null normalises to null (no namespace) per spec.
// https://dom.spec.whatwg.org/#dom-document-createelementns
this.namespaceURI = namespace !== undefined ? (namespace || null) : "http://www.w3.org/1999/xhtml";
this.namespaceURI = namespace || "http://www.w3.org/1999/xhtml";
};
@@ -208,16 +206,7 @@ TW_Element.prototype.addEventListener = function(type,listener,useCapture) {
Object.defineProperty(TW_Element.prototype, "tagName", {
get: function() {
if(!this.tag) {
return "";
}
// HTML elements report uppercase tagName per DOM spec. Other namespaces
// preserve case. Fakedom only models HTML documents.
// https://dom.spec.whatwg.org/#dom-element-tagname
if(this.namespaceURI === "http://www.w3.org/1999/xhtml") {
return this.tag.toUpperCase();
}
return this.tag;
return this.tag || "";
}
});
+5 -19
View File
@@ -151,24 +151,10 @@ ButtonWidget.prototype.getBoundingClientRect = function() {
};
ButtonWidget.prototype.isSelected = function() {
var currentValue;
if(this.setTitle) {
if(this.setField) {
// The state tiddler usually does not exist until the button is first clicked
var tiddler = this.wiki.getTiddler(this.setTitle);
currentValue = tiddler ? tiddler.getFieldString(this.setField) : undefined;
} else if(this.setIndex) {
currentValue = this.wiki.extractTiddlerDataItem(this.setTitle,this.setIndex);
} else {
currentValue = this.wiki.getTiddlerText(this.setTitle);
}
if(!currentValue) {
currentValue = this.defaultSetValue || this.getVariable("currentTiddler");
}
} else {
currentValue = this.wiki.getTextReference(this.set,this.defaultSetValue,this.getVariable("currentTiddler"));
}
return currentValue === this.setTo;
return this.setTitle ? (this.setField ? this.wiki.getTiddler(this.setTitle).getFieldString(this.setField) === this.setTo :
(this.setIndex ? this.wiki.extractTiddlerDataItem(this.setTitle,this.setIndex) === this.setTo :
this.wiki.getTiddlerText(this.setTitle))) || this.defaultSetValue || this.getVariable("currentTiddler") :
this.wiki.getTextReference(this.set,this.defaultSetValue,this.getVariable("currentTiddler")) === this.setTo;
};
ButtonWidget.prototype.isPoppedUp = function() {
@@ -281,7 +267,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
ButtonWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.tooltip || changedAttributes.actions || changedAttributes.to || changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes.selectedClass || changedAttributes.style || changedAttributes.dragFilter || changedAttributes.dragTiddler || (this.set && changedTiddlers[this.set]) || (this.setTitle && changedTiddlers[this.setTitle]) || (this.popup && changedTiddlers[this.popup]) || (this.popupTitle && changedTiddlers[this.popupTitle]) || changedAttributes.popupAbsCoords || changedAttributes.setTitle || changedAttributes.setField || changedAttributes.setIndex || changedAttributes.popupTitle || changedAttributes.disabled || changedAttributes["default"]) {
if(changedAttributes.tooltip || changedAttributes.actions || changedAttributes.to || changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes.selectedClass || changedAttributes.style || changedAttributes.dragFilter || changedAttributes.dragTiddler || (this.set && changedTiddlers[this.set]) || (this.popup && changedTiddlers[this.popup]) || (this.popupTitle && changedTiddlers[this.popupTitle]) || changedAttributes.popupAbsCoords || changedAttributes.setTitle || changedAttributes.setField || changedAttributes.setIndex || changedAttributes.popupTitle || changedAttributes.disabled || changedAttributes["default"]) {
this.refreshSelf();
return true;
} else {
+1 -1
View File
@@ -22,7 +22,7 @@ Inherit from the base widget class
DiffTextWidget.prototype = new Widget();
DiffTextWidget.prototype.invisibleCharacters = {
"\n": "\n",
"\n": "↩︎\n",
"\r": "⇠",
"\t": "⇥\t"
};
+3 -3
View File
@@ -1,6 +1,6 @@
title: $:/core/stylesheets/custom-properties
\rules only transcludeinline macrocallinline html transcludeblock filteredtranscludeinline
\rules only transcludeinline macrocallinline html transcludeblock
/* Tiddlywiki's CSS properties */
@@ -21,10 +21,10 @@ title: $:/core/stylesheets/custom-properties
--tp-story-left: {{$:/themes/tiddlywiki/vanilla/metrics/storyleft}};
--tp-story-top: {{$:/themes/tiddlywiki/vanilla/metrics/storytop}};
--tp-story-right: {{$:/themes/tiddlywiki/vanilla/metrics/storyright}};
--tp-story-width: {{$:/themes/tiddlywiki/vanilla/metrics/storywidth}};
--tp-story-width: {{$:/themes/tiddlywiki/vanilla/metrics/storyrwidth}};
--tp-tiddler-width: {{$:/themes/tiddlywiki/vanilla/metrics/tiddlerwidth}};
--tp-sidebar-breakpoint: {{$:/themes/tiddlywiki/vanilla/metrics/sidebarbreakpoint}};
--tp-sidebar-width: {{$:/themes/tiddlywiki/vanilla/metrics/sidebarwidth}};
--tp-animation-duration: {{{ [{$:/config/AnimationDuration}addsuffix[ms]] }}};
}
}
+3 -5
View File
@@ -4,15 +4,13 @@ description: create a new journal tiddler
\whitespace trim
\function get-tags() [<textFieldTags>] [<tagsFieldTags>] +[join[ ]]
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
textFieldTags={{$:/config/NewJournal/Tags}}
tagsFieldTags={{$:/config/NewJournal/Tags!!tags}}
journalText={{$:/config/NewJournal/Text}}
>
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} textFieldTags={{$:/config/NewJournal/Tags}} tagsFieldTags={{$:/config/NewJournal/Tags!!tags}} journalText={{$:/config/NewJournal/Text}}>
<$wikify name="journalTitle" text="<$transclude $variable='now' format=<<journalTitleTemplate>>/>">
<$reveal type="nomatch" state=<<journalTitle>> text="">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text={{{ [<journalTitle>get[]] }}}/>
</$reveal>
<$reveal type="match" state=<<journalTitle>> text="">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text=<<journalText>>/>
</$reveal>
</$wikify>
</$let>
+3 -3
View File
@@ -108,10 +108,10 @@ tags: $:/tags/EditTemplate
storeTitle=<<newFieldNameInputTiddler>>
searchListState=<<newFieldNameSelectionTiddler>>
>
<div class=`tc-edit-field-add-name-wrapper ${ [<newFieldNameTiddler>get[text]] :intersection[<storyTiddler>fields[]] :then[[tc-edit-field-exists]] }$`>
<div class="tc-edit-field-add-name-wrapper">
<$transclude $variable="keyboard-driven-input"
cancelPopups="yes"
class="tc-edit-texteditor tc-popup-handle"
class=`tc-edit-texteditor tc-popup-handle ${ [<newFieldNameTiddler>get[text]] :intersection[<storyTiddler>fields[]] :then[[tc-edit-field-exists]] }$`
configTiddlerFilter="[[$:/config/EditMode/fieldname-filter]]"
default=""
focus={{{ [{!!draft.of}is[tiddler]then{$:/config/AutoFocusEdit}match[fields]then[true]] :else[{$:/config/AutoFocus}match[fields]then[true]] :else[[false]] }}}
@@ -178,4 +178,4 @@ tags: $:/tags/EditTemplate
</$let>
</$let>
</div>
</$let>
</$let>
+23 -18
View File
@@ -6,25 +6,30 @@ tags: $:/tags/EditTemplate
\procedure lingo-base() $:/language/EditTemplate/
\procedure tag-body-inner(colour,fallbackTarget,colourA,colourB,icon,tagField:"tags")
<$let foregroundColor=<<contrastcolour target=<<colour>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>> >>
backgroundColor=<<colour>>
>
<span class="tc-tag-label tc-tag-list-item tc-small-gap-right"
data-tag-title=<<currentTiddler>>
style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;`
>
<$transclude tiddler=<<icon>>/>
<$view field="title" format="text"/>
<$button class="tc-btn-invisible tc-remove-tag-button"
style.fill=<<foregroundColor>>
<$wikify name="foregroundColor"
text="""<$macrocall $name="contrastcolour"
target=<<colour>>
fallbackTarget=<<fallbackTarget>>
colourA=<<colourA>>
colourB=<<colourB>>/>
"""
>
<$let backgroundColor=<<colour>> >
<span class="tc-tag-label tc-tag-list-item tc-small-gap-right"
data-tag-title=<<currentTiddler>>
style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;`
>
<$action-listops $tiddler=<<saveTiddler>> $field=<<tagField>> $subfilter="-[{!!title}]"/>
<!-- Touch a temp tiddler so that the tag remove animation fires instantly. Otherwise it's delayed by the typing timeout duration -->
<$action-setfield $tiddler="$:/temp/tags-edit-refresh" text=<<currentTiddler>>/>
{{$:/core/images/close-button}}
</$button>
</span>
</$let>
<$transclude tiddler=<<icon>>/>
<$view field="title" format="text"/>
<$button class="tc-btn-invisible tc-remove-tag-button"
style.fill=<<foregroundColor>>
>
<$action-listops $tiddler=<<saveTiddler>> $field=<<tagField>> $subfilter="-[{!!title}]"/>
{{$:/core/images/close-button}}
</$button>
</span>
</$let>
</$wikify>
\end
\procedure tag-body(colour,palette,icon,tagField:"tags")
+4 -2
View File
@@ -16,13 +16,15 @@ title: $:/core/ui/TagPickerTagTemplate
<$set name="backgroundColor"
value={{{ [<currentTiddler>] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerColourFilter]!is[draft]get[text]] }}}
>
<$let foregroundColor=<<contrastcolour target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>> >> >
<$wikify name="foregroundColor"
text="""<$macrocall $name="contrastcolour" target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>>/>"""
>
<span class="tc-tag-label tc-btn-invisible"
style=<<tag-pill-styles>>
data-tag-title=<<currentTiddler>>
>
{{||$:/core/ui/TiddlerIcon}}<$view field="title" format="text"/>
</span>
</$let>
</$wikify>
</$set>
</$button>
+3 -4
View File
@@ -6,6 +6,7 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
\whitespace trim
\procedure journalButton()
<$button tooltip={{$:/language/Buttons/NewJournalHere/Hint}} aria-label={{$:/language/Buttons/NewJournalHere/Caption}} class=<<tv-config-toolbar-class>>>
<$wikify name="journalTitle" text="""<$transclude $variable="now" format=<<journalTitleTemplate>>/>""">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=`[[$(currentTiddlerTag)$]] $(journalTags)$`/>
<%if [<tv-config-toolbar-icons>match[yes]] %>
{{$:/core/images/new-journal-button}}
@@ -15,11 +16,9 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
<$text text={{$:/language/Buttons/NewJournalHere/Caption}}/>
</span>
<%endif%>
</$wikify>
</$button>
\end
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
journalTags={{$:/config/NewJournal/Tags}}
currentTiddlerTag=<<currentTiddler>>
>
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} journalTags={{$:/config/NewJournal/Tags}} currentTiddlerTag=<<currentTiddler>>>
<<journalButton>>
</$let>
+1 -5
View File
@@ -17,10 +17,6 @@ This page summarises high level information about the configuration of this ~Tid
Drag this link to copy this tool to another wiki
\end intrinsic-lingo-Drag/Caption
\procedure intrinsic-lingo-Generate/Caption()
Click to generate wiki information report
\end intrinsic-lingo-Generate/Caption
\procedure lingo(title,mode:"inline")
<%if [<title>addprefix<lingo-base>is[shadow]] %>
<$transclude $tiddler={{{ [<title>addprefix<lingo-base>] }}} $mode=<<mode>>/>
@@ -109,7 +105,7 @@ Click to generate wiki information report
<$button>
<<display-wiki-info-modal>>
<<lingo title:"Generate/Caption">>
Click to generate wiki information report
</$button>
<$link to="$:/core/ui/ControlPanel/WikiInformation">
+1 -1
View File
@@ -1,6 +1,6 @@
title: $:/config/OfficialPluginLibrary
tags: $:/tags/PluginLibrary
url: https://tiddlywiki.com/library/v5.5.0/index.html
url: https://tiddlywiki.com/library/v5.4.1/index.html
caption: {{$:/language/OfficialPluginLibrary}}
{{$:/language/OfficialPluginLibrary/Hint}}
@@ -1,6 +1,6 @@
title: $:/config/LocalPluginLibrary
tags: $:/tags/PluginLibrary
url: http://127.0.0.1:8080/prerelease/library/v5.5.0/index.html
url: http://127.0.0.1:8080/prerelease/library/v5.4.1/index.html
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease Local)
A locally installed version of the official ~TiddlyWiki plugin library at tiddlywiki.com for testing and debugging. //Requires a local web server to share the library//
@@ -1,6 +1,6 @@
title: $:/config/OfficialPluginLibrary
tags: $:/tags/PluginLibrary
url: https://tiddlywiki.com/prerelease/library/v5.5.0/index.html
url: https://tiddlywiki.com/prerelease/library/v5.4.1/index.html
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease)
The prerelease version of the official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team.
@@ -1,94 +0,0 @@
created: 20260714055110474
modified: 20260714055129225
tags:
title: TiddlyWiki Pre-release Size Comparison
\function bytes.to.mib(size) [abs[]divide[1048576]fixed[2]addsuffix[ MiB]]
\function bytes.to.kib(size) [abs[]divide[1024]fixed[2]addsuffix[ KiB]]
\function format.file.size(size) [<size>abs[]compare:number:gteq[1048576]bytes.to.mib<size>] :else[<size>abs[]compare:number:gteq[1024]bytes.to.kib<size>] :else[<size>addsuffix[ bytes]]
\procedure get-wiki-filesize(filepath)
\procedure completion-get-json()
<!-- Success -->
<$list filter="[<status>compare:number:gteq[200]compare:number:lteq[299]]" variable="ignore">
<$action-log msg="completed" size={{{ [<data>jsonget[size]] }}}/>
<$action-setfield $tiddler=`$:/temp/file-size-comparison/$(filepath)$` text={{{ [<data>jsonget[size]] }}} />
</$list>
\end completion-get-json
<$action-sendmessage
$message="tm-http-request"
url=`https://api.github.com/repos/TiddlyWiki/tiddlywiki.com-gh-pages/contents/$(filepath)$?ref=master`
method="GET"
oncompletion=<<completion-get-json>>
var-filepath=<<filepath>>
/>
\end get-wiki-filesize
\procedure get-wiki-filesizes()
<$list filter="empty.html prerelease/empty.html" variable="filepath">
<$action-log />
<<$transclude $variable="get-wiki-filesize" filepath=<<filepath>> >>
</$list>
\end get-wiki-filesizes
<%if [[$:/temp/file-size-comparison/empty.html]is[tiddler]] [[$:/temp/file-size-comparison/prerelease/empty.html]is[tiddler]] :and[count[]match[2]]%>
<$let delta={{{ [{$:/temp/file-size-comparison/empty.html}subtract{$:/temp/file-size-comparison/prerelease/empty.html}] }}}
message={{{ [<delta>sign[]match[-1]then[Size increased]] :else[<delta>sign[]match[1]then[Size decreased]] :else[[Size has not changed]] }}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="158" height="20" role="img" style.zoom="1.5" aria-label="<$text text=<<message>>/>: <$text text={{{ [format.file.size<delta>] }}} /> bytes">
<title><$text text=<<message>>/>: <$text text={{{ [format.file.size<delta>] }}} /></title>
<filter id="blur">
<feGaussianBlur stdDeviation="16"/>
</filter>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="158" height="20" rx="3"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="93" height="20" fill="#555"/>
<rect x="93" width="65" height="20" fill="#67ac09"/>
<rect width="158" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
<g transform="scale(.1)">
<g aria-hidden="true" fill="#010101">
<text x="475" y="150" fill-opacity=".8" filter="url(#blur)" textLength="830"><$text text=<<message>>/></text>
<text x="475" y="150" fill-opacity=".3" textLength="830"><$text text=<<message>>/></text>
</g>
<text x="475" y="140" textLength="830"><$text text=<<message>>/></text>
</g>
<g transform="scale(.1)">
<g aria-hidden="true" fill="#010101">
<text x="1245" y="150" fill-opacity=".8" filter="url(#blur)" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
<text x="1245" y="150" fill-opacity=".3" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
</g>
<text x="1245" y="140" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
</g>
</g>
</svg>
; Pre-release empty.html
: <$text text={{{ [{$:/temp/file-size-comparison/prerelease/empty.html}] }}} /> bytes
; <$text text={{{ [tag[ReleaseNotes]sort[released]last[]] }}} /> empty.html
: <$text text={{{ [{$:/temp/file-size-comparison/empty.html}] }}} /> bytes
---
</$let>
<%else %>
<$button actions=<<get-wiki-filesizes>> class="tc-btn-big-green">
Compare size of empty.html
</$button>
<%endif%>
@@ -13,8 +13,6 @@ The pre-release is also available as an [[empty wiki|https://tiddlywiki.com/prer
</div>
{{ TiddlyWiki Pre-release Size Comparison }}
<$list filter="[tag[ReleaseNotes]!has[released]!sort[created]]">
<div class="tc-titlebar">
<h2 class="tc-title"><$text text=<<currentTiddler>>/></h2>
@@ -1,12 +0,0 @@
title: ButtonSelection/DefaultSelection
description: The default attribute selects a setTitle button while the state tiddler is missing
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
<$button setTitle="$:/state/demo" setTo="Alpha" default="Beta" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setTo="Beta" default="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button><button aria-checked="true" class=" sel">Beta</button></p>
@@ -1,12 +0,0 @@
title: ButtonSelection/MissingStateTiddler
description: A setField button renders when its state tiddler does not exist
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
<$button setTitle="$:/state/demo" setField="selection" setTo="Alpha" selectedClass="sel">Alpha</$button>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button></p>
@@ -1,15 +0,0 @@
title: ButtonSelection/SetFieldSelection
description: Only the setField button whose setTo matches the state field is selected
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
selection: Beta
+
title: Output
<$button setTitle="$:/state/demo" setField="selection" setTo="Alpha" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setField="selection" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button><button aria-checked="true" class=" sel">Beta</button></p>
@@ -1,17 +0,0 @@
title: ButtonSelection/SetIndexSelection
description: Only the setIndex button whose setTo matches the state index is selected
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
type: application/json
{"selection": "Beta"}
+
title: Output
<$button setTitle="$:/state/demo" setIndex="selection" setTo="Alpha" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setIndex="selection" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button><button aria-checked="true" class=" sel">Beta</button></p>
@@ -1,16 +0,0 @@
title: ButtonSelection/SetSelection
description: Only the set button whose setTo matches the state is selected
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
Alpha
+
title: Output
<$button set="$:/state/demo" setTo="Alpha" selectedClass="sel">Alpha</$button><$button set="$:/state/demo" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="true" class=" sel">Alpha</button><button aria-checked="false" class="">Beta</button></p>
@@ -1,20 +0,0 @@
title: ButtonSelection/SetTitleRefresh
description: The setTitle selection follows the state tiddler when it changes
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
Alpha
+
title: Output
<$button setTitle="$:/state/demo" setTo="Alpha" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: Actions
<$action-setfield $tiddler="$:/state/demo" text="Beta"/>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button><button aria-checked="true" class=" sel">Beta</button></p>
@@ -1,16 +0,0 @@
title: ButtonSelection/SetTitleSelection
description: Only the setTitle button whose setTo matches the state is selected
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
Alpha
+
title: Output
<$button setTitle="$:/state/demo" setTo="Alpha" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="true" class=" sel">Alpha</button><button aria-checked="false" class="">Beta</button></p>
@@ -1,22 +0,0 @@
title: Filters/DiffMergePatch4
description: Tests for diff-merge-patch derived operators
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\whitespace trim
\define text1()
The quick brown fox
\end
\define text2()
The fast brown fox
\end
<$text text={{{ [<text1>makepatches::json<text2>] }}}/>
+
title: ExpectedResult
[{"type":"equal","text":"The "},{"type":"delete","text":"quick"},{"type":"insert","text":"fast"},{"type":"equal","text":" brown fox"}]
@@ -1,14 +0,0 @@
title: Filters/ListOps
description: Test listops operators
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\whitespace trim
(<$text text={{{ =[[E]] =[[A]] =[[B]] =[[C]] =[[C]] =[[D]] =[[C]] +[unique[]join[]] }}}/>)
+
title: ExpectedResult
<p>(EABCD)</p>
@@ -1,162 +0,0 @@
/*\
title: test-back-indexer.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Regression tests for #9916: the back-indexer must never record system
tiddlers as backlink or backtransclude sources, neither when the index is
first built nor when it is incrementally updated.
\*/
"use strict";
describe("Back-indexer system source tests (#9916)", function() {
function setupWiki() {
// Create a wiki with indexers and one primed backlink pair
var wiki = new $tw.Wiki();
wiki.addIndexersToWiki();
wiki.addTiddler({
title: "TestIncoming",
text: ""});
wiki.addTiddler({
title: "TestOutgoing",
text: "A link to [[TestIncoming]]"});
return wiki;
}
it("should never report a system tiddler as a backlink source", function() {
// Browser console: $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") to prime the index,
// then $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"}))
// and run the filter again; $:/temp/demo must not appear.
var wiki = setupWiki();
// The first lookup builds the lazy index; its initial scan skips system tiddlers
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
// The incremental update() must skip them too
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should keep backlinks stable while a linking system tiddler is modified and deleted", function() {
// Browser console: $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") to prime the index,
// then $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"})),
// change its text, $tw.wiki.deleteTiddler("$:/temp/demo"); the filter result never changes.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
// Modify: both the old and the new side of the index update are system tiddlers
wiki.addTiddler({
title: "$:/temp/system-source",
text: "Links to [[TestIncoming]] and [[TestOutgoing]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.deleteTiddler("$:/temp/system-source");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should drop the backlink when its source is renamed to a system title", function() {
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "Demo", text: "[[HelloThere]]"})),
// confirm Demo is in $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]"), then
// $tw.wiki.renameTiddler("Demo","$:/Demo"); neither Demo nor $:/Demo remains a source.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.renameTiddler("TestOutgoing","$:/TestOutgoing");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("");
});
it("should gain the backlink when a linking system tiddler is renamed to a normal title", function() {
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"})),
// then $tw.wiki.renameTiddler("$:/temp/demo","DemoVisible");
// DemoVisible now appears in $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]").
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
wiki.renameTiddler("$:/temp/system-source","VisibleSource");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,VisibleSource");
});
it("should still report normal sources for a system tiddler target", function() {
// Only sources are filtered, targets are not. Browser console:
// $tw.wiki.addTiddler(new $tw.Tiddler({title: "Demo", text: "[[$:/config/NewJournal/Tags]]"}));
// $tw.wiki.filterTiddlers("[[$:/config/NewJournal/Tags]backlinks[]]") contains Demo.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "TestSystemLinker",
text: "A link to [[$:/config/Target]]"});
expect(wiki.filterTiddlers("[[$:/config/Target]backlinks[]]").join(",")).toBe("TestSystemLinker");
});
describe("Adversarial probes", function() {
it("should never index a shadow tiddler as a source, even when revealed by deleting its override", function() {
// Goes red when a refactor of BackSubIndexer.update() checks the tiddler instead of
// the exists flag:
// if(updateDescriptor["new"].tiddler) { ... } // broken: goes red here
// if(updateDescriptor["new"].exists) { ... } // correct: stays green
// After deleteTiddler() on an override, boot.js fills new.tiddler via getTiddler(),
// which falls back to the revealed shadow, while new.exists stays false because
// only the real store counts. The broken variant indexes the shadow's links and
// the final expect fails with "TestOutgoing,ShadowSource".
// The first expect also goes red if _init() ever starts scanning shadows; the
// middle expect pins that a real override IS indexed, so this probe cannot be
// satisfied by indexing nothing at all.
// Browser console: override a plugin shadow with text [[HelloThere]], check it appears in
// $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]"), then $tw.wiki.deleteTiddler(title);
// the title disappears from the filter result even though the shadow still renders.
var wiki = setupWiki();
wiki.addTiddler({
title: "$:/plugins/test/shadow-plugin",
type: "application/json",
"plugin-type": "plugin",
text: JSON.stringify({tiddlers: {
"ShadowSource": {title: "ShadowSource", text: "A shadow link to [[TestIncoming]]"}
}})});
wiki.readPluginInfo();
wiki.registerPluginTiddlers("plugin");
wiki.unpackPluginTiddlers();
// The initial scan sees only real tiddlers, not the shadow
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "ShadowSource",
text: "An overriding link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,ShadowSource");
// Deleting the override reveals the shadow; it must not enter the index
wiki.deleteTiddler("ShadowSource");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should handle hostile titles like __proto__ as source and target", function() {
// Goes red when any title-keyed hashmap on the backlinks path is created like this:
// this.index = {}; // broken: goes red here
// instead of:
// this.index = Object.create(null); // correct: stays green
// this.index = new Map(); // a Map/Set refactor also stays green
// The same applies to the boot tiddler store and to the per-target source maps
// (self.index[target] = ...). On a plain {} the assignment index["__proto__"] = x
// stores no key; it silently replaces the object's prototype. Depending on which
// map regresses, the __proto__ tiddler never registers as a source (second expect),
// or its target entry lands in the shared prototype, where it pollutes every other
// lookup and lookup("__proto__") returns garbage (third expect).
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "__proto__",
// text: "[[HelloThere]]"})); __proto__ appears in
// $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") and
// $tw.wiki.filterTiddlers("[[__proto__]backlinks[]]") lists tiddlers linking to it.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "__proto__",
text: "A link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,__proto__");
wiki.addTiddler({
title: "ProtoLinker",
text: "A link to [[__proto__]]"});
expect(wiki.filterTiddlers("[[__proto__]backlinks[]]").join(",")).toBe("ProtoLinker");
});
});
});
@@ -1,69 +0,0 @@
/*\
title: test-codeblock-parser.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the codeblock wikitext rule (#9047): an empty code block must parse
cleanly, and a closing fence only counts when it stands alone on its line.
\*/
"use strict";
describe("codeblock parser tests (#9047)", function() {
var wiki = new $tw.Wiki();
function parse(text) {
return wiki.parseText("text/vnd.tiddlywiki",text).tree;
}
it("parses an empty code block with a language", function() {
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```bash\n```").tree
// Expected: one codeblock node, code "", language "bash".
var tree = parse("```bash\n```");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("");
expect(tree[0].attributes.language.value).toBe("bash");
});
it("handles CRLF line endings around both fences", function() {
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```bash\r\nfoo\r\n```").tree
// Expected: one codeblock node with code "foo"; neither the CRLF after the
// opening fence nor the one before the closing fence is part of the code.
var tree = parse("```bash\r\nfoo\r\n```");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("foo");
});
it("keeps the content of a block and drops the delimiting newlines", function() {
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```\nfoo\n```").tree
// Expected: one codeblock node with code "foo"; the newlines around the
// delimiter lines are not part of the code.
var tree = parse("```\nfoo\n```");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("foo");
});
it("only closes the block when the fence stands alone on its line", function() {
// A content line ending in ``` is code, not a closing fence, e.g. nested
// markdown fences or template literals quoted inside a code block.
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```\nabc```\ndef\n```").tree
// Expected: one codeblock containing "abc```\ndef"; nothing leaks out as wikitext.
var tree = parse("```\nabc```\ndef\n```");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("abc```\ndef");
});
it("swallows the rest of the tiddler when no closing fence exists", function() {
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```bash\nfoo").tree
// Expected: one codeblock node with code "foo", extending to the end of the text.
var tree = parse("```bash\nfoo");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("foo");
});
});
-41
View File
@@ -1,41 +0,0 @@
/*\
title: test-csv.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the CSV parser.
\*/
"use strict";
describe("CSV tests", function() {
it("parses a simple table", function() {
expect($tw.utils.parseCsvString("a,b,c\n1,2,3")).toEqual([["a","b","c"],["1","2","3"]]);
});
it("parses a table whose first cell reads empty", function() {
expect($tw.utils.parseCsvString(",b,c\n1,2,3")).toEqual([["","b","c"],["1","2","3"]]);
});
it("parses an empty cell in the middle of a row", function() {
expect($tw.utils.parseCsvString("a,,c")).toEqual([["a","","c"]]);
});
it("parses quoted cells", function() {
expect($tw.utils.parseCsvString('a,"b,still b",c')).toEqual([["a","b,still b","c"]]);
});
it("parses a quoted cell holding an escaped quote", function() {
expect($tw.utils.parseCsvString('a,"b ""quoted""",c')).toEqual([["a",'b "quoted"',"c"]]);
});
it("honours a custom separator", function() {
expect($tw.utils.parseCsvString("a;b;c",{separator: ";"})).toEqual([["a","b","c"]]);
});
it("parses a table with a header row into hashmaps", function() {
expect($tw.utils.parseCsvStringWithHeader("a,b\n1,2")).toEqual([{a: "1", b: "2"}]);
});
});
@@ -19,56 +19,6 @@ describe("fakedom tests", function() {
expect($tw.fakeDocument.createTextNode("text").TEXT_NODE).toBe(3);
});
// Per DOM spec, tagName returns the HTML-uppercased qualified name for HTML
// elements. Other namespaces preserve case.
// https://dom.spec.whatwg.org/#dom-element-tagname
var HTML_NS = "http://www.w3.org/1999/xhtml";
var SVG_NS = "http://www.w3.org/2000/svg";
it("tagName uppercases for HTML namespace", function() {
// Default namespace is HTML
expect($tw.fakeDocument.createElement("div").tagName).toBe("DIV");
// The exact predicate the select widget relies on (#9839)
expect($tw.fakeDocument.createElement("optgroup").tagName === "OPTGROUP").toBe(true);
// Already-uppercase input stays uppercase
expect($tw.fakeDocument.createElement("OPTGROUP").tagName).toBe("OPTGROUP");
// Mixed-case input is normalised
expect($tw.fakeDocument.createElement("Div").tagName).toBe("DIV");
// Hyphenated custom-element names uppercase whole tag, hyphens survive
expect($tw.fakeDocument.createElement("my-button").tagName).toBe("MY-BUTTON");
// Empty tag returns empty string
expect($tw.fakeDocument.createElement("").tagName).toBe("");
// Explicit HTML namespace via createElementNS uppercases the same way
expect($tw.fakeDocument.createElementNS(HTML_NS,"Div").tagName).toBe("DIV");
});
it("tagName preserves case for non-HTML namespaces", function() {
// SVG: lowercase preserved
expect($tw.fakeDocument.createElementNS(SVG_NS,"circle").tagName).toBe("circle");
// SVG: camelCase preserved (linearGradient is the canonical example)
expect($tw.fakeDocument.createElementNS(SVG_NS,"linearGradient").tagName).toBe("linearGradient");
// SVG: already-uppercase input is also preserved (NOT lowercased)
expect($tw.fakeDocument.createElementNS(SVG_NS,"DIV").tagName).toBe("DIV");
// Empty namespace string is "no namespace", not HTML. Case preserved.
expect($tw.fakeDocument.createElementNS("","div").tagName).toBe("div");
});
it("tagName reflects current state without mutating it", function() {
// Reading tagName must not overwrite the internal `tag` field
var el = $tw.fakeDocument.createElement("div");
expect(el.tagName).toBe("DIV");
expect(el.tag).toBe("div");
// Idempotent: two reads return identical values
var first = el.tagName, second = el.tagName;
expect(first).toBe(second);
// Dynamic namespace change is reflected. The getter must read current
// state, not a value cached at construction time.
var dynamic = $tw.fakeDocument.createElement("foo");
expect(dynamic.tagName).toBe("FOO");
dynamic.namespaceURI = SVG_NS;
expect(dynamic.tagName).toBe("foo");
});
// Real CSSStyleDeclaration returns undefined for Symbol property keys.
// Without a guard, the TW_Style Proxy throws on Symbol access. This bites
// in practice when Jasmine pretty-prints fakedom elements on failure.
@@ -1,173 +0,0 @@
/*\
title: test-filesystem-adversarial.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Adversarial tests for generateTiddlerFilepath: hostile titles and recorded
originalpaths (path traversal, absolute paths, control codes, reserved device
names, over-long and non-ASCII input) must still yield a filename valid on every
supported OS, without throwing or leaking a forbidden character. Only the
filename is sanitised; the tiddler title itself is never altered.
generateTiddlerFilepath is node-only (utils-node), so reproduce from the CLI, not
the browser. Save as probe.js in the repo root and run `node probe.js`:
var path = require("path");
var $tw = require("./boot/boot.js").TiddlyWiki();
$tw.boot.argv = ["./editions/test"];
$tw.boot.boot(function() {
var dir = path.resolve("/tmp/tw5-probe");
function base(t) { return path.basename($tw.utils.generateTiddlerFilepath(t,{extension: ".tid", directory: dir, fileInfo: {}})); }
console.log(base("con")); // expected: _con_.tid (reserved device name wrapped)
console.log(base("a<b>c")); // expected: a_b_c.tid (forbidden chars replaced)
console.log(base("trailing ")); // expected: trailing_.tid (trailing space replaced)
process.exit(0);
});
Control codes in the specs are built with String.fromCharCode so this source
stays pure ASCII. All path-logic tests use the real-save default overwrite:false.
\*/
"use strict";
if($tw.node) {
var fs = require("fs");
var path = require("path");
describe("generateTiddlerFilepath (adversarial)", function() {
var directory = path.resolve("/tmp/tw5-test-filesystem-adv");
beforeEach(function() {
fs.rmSync(directory,{recursive: true, force: true});
fs.mkdirSync(directory,{recursive: true});
});
afterAll(function() {
fs.rmSync(directory,{recursive: true, force: true});
});
function fromTitle(title) {
return $tw.utils.generateTiddlerFilepath(title,{
extension: ".tid",
directory: directory,
fileInfo: {}
});
}
function fromOriginalpath(originalpath) {
return $tw.utils.generateTiddlerFilepath("plain-title",{
extension: ".tid",
directory: directory,
fileInfo: {originalpath: originalpath}
});
}
// A saved path segment must contain no character Windows forbids in a
// filename and no control code. "/" and "\" are separators between
// segments; "." and ".." are dot segments, not filenames.
function hasControlCode(seg) {
for(var i = 0; i < seg.length; i++) {
if(seg.charCodeAt(i) < 0x20) {
return true;
}
}
return false;
}
function eachSegment(result,callback) {
path.relative(directory,result).split(/[\\/]/).forEach(function(seg) {
if(seg !== "" && seg !== "." && seg !== "..") {
callback(seg);
}
});
}
function expectCleanSegments(result) {
eachSegment(result,function(seg) {
expect(seg).not.toMatch(/[<>:"|?*]/); // Windows-forbidden chars
expect(hasControlCode(seg)).toBe(false); // control codes
expect(seg).not.toMatch(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i); // reserved device name
expect(seg).not.toMatch(/[. ]$/); // trailing dot or space
});
}
// Control codes are built at runtime so the source stays pure ASCII.
var NUL = String.fromCharCode(0), BEL = String.fromCharCode(7), US = String.fromCharCode(0x1f);
// Battery of hostile inputs, fed through both the title and originalpath
// channels.
var hostile = [
"../../../etc/passwd",
"..\\..\\evil",
"a<b>c:d\"e|f?g*h",
"a" + NUL + "b" + BEL + "c" + US + "d",
"con", "PRN", "nul", "COM1", "LPT9",
"a/nul/b",
" leading",
"trailing ",
"dots...",
"café naïve",
"中文テスト",
new Array(400).join("z"),
"<>:\"|?*",
"//..//..//",
"a/b\\c/d",
"",
".",
"..",
"::::",
" "
];
it("never leaks a forbidden character or throws, for any hostile input", function() {
hostile.forEach(function(input) {
[fromTitle, function(t) { return fromOriginalpath(t + ".tid"); }].forEach(function(fn) {
var result;
expect(function() { result = fn(input); }).not.toThrow();
expect(typeof result).toBe("string");
expect(result.length).toBeGreaterThan(0);
expectCleanSegments(result);
});
});
});
it("replaces control codes and NUL in the filename with underscore", function() {
expect(path.basename(fromTitle("a" + NUL + "b" + US + "c"))).toBe("a_b_c.tid");
});
it("wraps a reserved Windows device name used as the whole title", function() {
expect(path.basename(fromTitle("CON"))).toBe("_CON_.tid");
expect(path.basename(fromTitle("nul"))).toBe("_nul_.tid");
expect(path.basename(fromTitle("COM1"))).toBe("_COM1_.tid");
});
it("bounds the filename length for an over-long title", function() {
var result = fromTitle(new Array(400).join("z"));
expect(path.basename(result).length).toBeLessThanOrEqual(200 + ".tid".length);
});
it("transliterates non-ASCII to ASCII in every segment", function() {
expect(path.basename(fromTitle("café"))).toBe("cafe.tid");
eachSegment(fromOriginalpath("café/naïve.tid"),function(seg) {
for(var i = 0; i < seg.length; i++) {
expect(seg.charCodeAt(i)).toBeLessThan(128);
}
});
});
it("gives dots-only titles a valid, distinct, non-empty filename", function() {
// "." and ".." are legal tiddler titles but not legal bare filenames.
// The contract is a non-empty, forbidden-char-free name that is not a
// hidden dotfile, and two different titles must not map to one file.
// The exact fallback encoding is an implementation detail, not asserted.
var dot = path.basename(fromTitle("."));
var dotdot = path.basename(fromTitle(".."));
[dot,dotdot].forEach(function(name) {
expect(name.length).toBeGreaterThan(".tid".length);
expect(name).not.toMatch(/[<>:"|?*]/);
expect(name).not.toMatch(/^\.+/);
});
expect(dot).not.toBe(dotdot);
});
});
}
@@ -1,362 +0,0 @@
/*\
title: test-filesystem.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests for the $:/core-server filesystem utilities: generateTiddlerFilepath,
generateTiddlerFileInfo and generateTiddlerExtension. These are node-only
(module-type utils-node), so they run under `npm test` and are absent in the
browser: reproduce them from the CLI, not the F12 console.
Reproduce a case by hand. Save as probe.js in the repo root, run `node probe.js`,
and swap in the input and expected value from any spec below:
var path = require("path");
var $tw = require("./boot/boot.js").TiddlyWiki();
$tw.boot.argv = ["./editions/test"];
$tw.boot.boot(function() {
var dir = path.resolve("/tmp/tw5-probe");
var r = $tw.utils.generateTiddlerFilepath("a>b",{extension: ".tid", directory: dir, fileInfo: {}});
console.log(path.basename(r)); // expected: a_b.tid
process.exit(0);
});
Reproduce the real client/server save (the retain-original-tiddler-path case).
From the repo root run:
node ./tiddlywiki.js ./editions/tw5.com-server --listen
then in the browser edit a tiddler whose file lives in a subfolder, for example
examples/ButtonWidget/Popup.tid, save, and confirm on disk that the file stays in
examples/ButtonWidget/ rather than being flattened to the tiddlers root.
\*/
"use strict";
if($tw.node) {
var fs = require("fs");
var path = require("path");
describe("generateTiddlerFilepath", function() {
var directory = path.resolve("/tmp/tw5-test-filesystem");
// Real saves use overwrite:false, which runs the uniquifier and probes the
// filesystem. Start every test from an empty directory so the computed
// filenames are deterministic. Only the dedicated overwrite test below
// passes overwrite:true.
beforeEach(function() {
fs.rmSync(directory,{recursive: true, force: true});
fs.mkdirSync(directory,{recursive: true});
});
afterAll(function() {
fs.rmSync(directory,{recursive: true, force: true});
});
// Characters illegal in a filename on at least one supported OS (Windows
// forbids < > : " / \ | ? *). generateTiddlerFilepath replaces each with
// "_" so a tiddler file committed on one OS still checks out on every
// other: an unsanitised "a>b.tid" would break `git checkout` on Windows.
// Only the filename is sanitised; the tiddler title keeps the character.
var forbiddenChars = [
["<","less-than"],
[">","greater-than"],
["~","tilde"],
[":","colon"],
["\"","double-quote"],
["|","pipe"],
["?","question-mark"],
["*","asterisk"],
["^","caret"],
["\\","backslash"]
];
// Title channel: a title is plain text, so its slashes and backslashes are
// not directory separators and every forbidden character is replaced.
function filepathFromTitle(title) {
return $tw.utils.generateTiddlerFilepath(title,{
extension: ".tid",
directory: directory,
fileInfo: {}
});
}
// OTP channel: originalpath ($:/config/OriginalTiddlerPaths) is a real
// recorded relative location. Its separators are preserved; a forbidden
// character inside a segment is still replaced. A distinct title proves the
// filename derives from originalpath, not the title.
function filepathFromOriginalpath(originalpath) {
return $tw.utils.generateTiddlerFilepath("WrongIfUsed",{
extension: ".tid",
directory: directory,
fileInfo: {originalpath: originalpath}
});
}
// FileSystemPaths channel: a $:/config/FileSystemPaths filter builds the
// path from the title, so a backslash leaking in from the title must be
// sanitised. "[[...]]" echoes the literal.
function filepathFromPathFilter(title,filter) {
return $tw.utils.generateTiddlerFilepath(title,{
extension: ".tid",
directory: directory,
wiki: $tw.wiki,
pathFilters: [filter],
fileInfo: {}
});
}
forbiddenChars.forEach(function(entry) {
var char = entry[0], name = entry[1];
it("replaces " + name + " (" + char + ") in a title with underscore", function() {
var result = filepathFromTitle("a" + char + "b");
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe("a_b.tid");
});
});
it("replaces every forbidden char in a dense title", function() {
// Prefix "x" so the result is not all underscores, which would trigger
// the charcode-fallback branch in generateTiddlerFilepath.
var title = "x" + forbiddenChars.map(function(e) { return e[0]; }).join("");
var expected = "x" + forbiddenChars.map(function() { return "_"; }).join("");
var result = filepathFromTitle(title);
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe(expected + ".tid");
});
it("does not overwrite a different tiddler that sanitises to the same filename", function() {
// "a>b" and "a/b" are distinct titles that both sanitise to "a_b.tid".
// With overwrite off (the real-save default) the second file gets a
// uniquifier ("_1") rather than clobbering the first.
fs.writeFileSync(path.resolve(directory,"a_b.tid"),"");
var result = filepathFromTitle("a/b");
expect(path.basename(result)).toBe("a_b_1.tid");
});
it("reuses the target filename when overwrite is set (the --save path)", function() {
// overwrite:true (used by the --save command) writes a specific file
// even if it already exists, so no uniquifier is added. This is the
// only test allowed to pass overwrite:true.
fs.writeFileSync(path.resolve(directory,"a_b.tid"),"");
var result = $tw.utils.generateTiddlerFilepath("a/b",{
extension: ".tid",
directory: directory,
fileInfo: {overwrite: true}
});
expect(path.basename(result)).toBe("a_b.tid");
});
it("sanitises a backslash from a FileSystemPaths filter without making a subdir (#9814)", function() {
// A title such as "Pragma: \define" reaching the filename via a
// FileSystemPaths filter must not act as a Windows path separator.
var result = filepathFromPathFilter("Pragma: \\define","[[Pragma: \\define]]");
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe("Pragma_ _define.tid");
});
it("keeps the subdirectory of a retained originalpath (native separators)", function() {
// Regression: on Windows path.relative yields backslash separators, so
// originalpath is e.g. "examples\ButtonWidget\Popup.tid". They must be
// preserved so the tiddler saves back to its folder rather than being
// flattened to "examples_ButtonWidget_Popup.tid" at the tiddlers root.
var original = ["examples","ButtonWidget","Popup"].join(path.sep) + ".tid";
var result = filepathFromOriginalpath(original);
expect(path.dirname(result)).toBe(path.resolve(directory,"examples","ButtonWidget"));
expect(path.basename(result)).toBe("Popup.tid");
});
it("keeps the subdirectory of a retained originalpath (forward slashes)", function() {
var result = filepathFromOriginalpath("examples/ButtonWidget/Popup.tid");
expect(path.dirname(result)).toBe(path.resolve(directory,"examples","ButtonWidget"));
expect(path.basename(result)).toBe("Popup.tid");
});
it("still sanitises a forbidden char inside an originalpath segment", function() {
// ">" is a legal filename char on Linux and macOS but forbidden on
// Windows. The sanitiser always targets the strictest OS, so a file
// committed on Linux still checks out on Windows: an unsanitised
// "a>b.tid" would break `git checkout` there and brick the clone.
// The ">" is replaced while the "sub/" directory is kept.
var result = filepathFromOriginalpath("sub/a>b.tid");
expect(path.dirname(result)).toBe(path.resolve(directory,"sub"));
expect(path.basename(result)).toBe("a_b.tid");
});
it("replaces trailing dots and spaces in a filename", function() {
// Windows silently strips a trailing dot or space from a filename, so
// they are replaced to keep the name stable across platforms, in a
// title and inside an originalpath segment alike.
expect(path.basename(filepathFromTitle("foo."))).toBe("foo_.tid");
expect(path.basename(filepathFromTitle("foo "))).toBe("foo_.tid");
expect(path.basename(filepathFromOriginalpath("sub/foo..tid"))).toBe("foo_.tid");
});
it("uses a tiddlywiki.files pinned path verbatim, skipping sanitisation", function() {
// A pinFilepath (tiddlywiki.files) path is author-controlled and
// trusted: separators are kept and the forbidden-char sanitiser is
// skipped, so a tilde (which it would otherwise replace) survives.
var result = $tw.utils.generateTiddlerFilepath("WrongIfUsed",{
extension: ".tid",
directory: directory,
fileInfo: {pinFilepath: true, originalpath: "pinned/a~b.tid"}
});
expect(path.dirname(result)).toBe(path.resolve(directory,"pinned"));
expect(path.basename(result)).toBe("a~b.tid");
});
it("falls back to originalpath when no FileSystemPaths filter matches", function() {
// A filter that selects nothing for this tiddler leaves the recorded
// originalpath in charge of the location.
var result = $tw.utils.generateTiddlerFilepath("WrongIfUsed",{
extension: ".tid",
directory: directory,
wiki: $tw.wiki,
pathFilters: ["[tag[__no_such_tag__]]"],
fileInfo: {originalpath: "examples/ButtonWidget/Popup.tid"}
});
expect(path.dirname(result)).toBe(path.resolve(directory,"examples","ButtonWidget"));
expect(path.basename(result)).toBe("Popup.tid");
});
it("replaces leading spaces in a title with underscores", function() {
expect(path.basename(filepathFromTitle(" foo"))).toBe("__foo.tid");
});
it("replaces leading dots so the file is not hidden on unix", function() {
expect(path.basename(filepathFromTitle(".hidden"))).toBe("_hidden.tid");
expect(path.basename(filepathFromTitle("..twodots"))).toBe("__twodots.tid");
});
it("replaces trailing dots or spaces in the extension", function() {
var result = $tw.utils.generateTiddlerFilepath("foo",{
extension: ".tid.",
directory: directory,
fileInfo: {}
});
expect(path.basename(result)).toBe("foo.tid_");
});
it("truncates an over-long extension to 32 characters", function() {
var longExt = "." + new Array(50).join("x");
var result = $tw.utils.generateTiddlerFilepath("foo",{
extension: longExt,
directory: directory,
fileInfo: {}
});
expect(path.basename(result)).toBe("foo" + longExt.substr(0,32));
});
it("does not double the extension when the title already ends in it", function() {
// "notes.tid" with extension ".tid" must yield "notes.tid", not
// "notes.tid.tid".
expect(path.basename(filepathFromTitle("notes.tid"))).toBe("notes.tid");
});
it("keeps its own filename when an existing tiddler is re-saved", function() {
// The tiddler already owns "a_b.tid" (fileInfo.filepath). Re-saving
// reuses it instead of appending a "_1" uniquifier, which would
// otherwise orphan a copy on every save.
var owned = path.resolve(directory,"a_b.tid");
fs.writeFileSync(owned,"");
var result = $tw.utils.generateTiddlerFilepath("a/b",{
extension: ".tid",
directory: directory,
fileInfo: {filepath: owned}
});
expect(path.basename(result)).toBe("a_b.tid");
});
it("URI-encodes the filename after a write error", function() {
// fileInfo.writeError forces the encoded fallback so a retry can write
// a name the filesystem will accept.
var result = $tw.utils.generateTiddlerFilepath("plain",{
extension: ".tid",
directory: directory,
fileInfo: {writeError: true}
});
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).not.toBe("plain.tid");
expect(path.basename(result)).toContain("plain.tid");
});
it("folds a path that escapes all allowed roots back into the directory", function() {
// An originalpath resolving outside directory / wikiTiddlersPath is
// encoded into a single filename inside directory rather than escaping.
var result = $tw.utils.generateTiddlerFilepath("plain",{
extension: ".tid",
directory: directory,
fileInfo: {originalpath: "../../../outside/evil.tid"}
});
expect(path.dirname(result)).toBe(directory);
});
});
describe("generateTiddlerFileInfo", function() {
var directory = path.resolve("/tmp/tw5-test-filesystem");
function fileInfoFor(fields,existing) {
return $tw.utils.generateTiddlerFileInfo(new $tw.Tiddler(fields),{
directory: directory,
wiki: $tw.wiki,
fileInfo: existing || {}
});
}
it("saves a wikitext tiddler as a single .tid file", function() {
var fi = fileInfoFor({title: "Foo", text: "body", type: "text/vnd.tiddlywiki"});
expect(fi.type).toBe("application/x-tiddler");
expect(fi.hasMetaFile).toBe(false);
});
it("saves a non-wikitext tiddler as a body file plus a .meta file", function() {
var fi = fileInfoFor({title: "Foo", text: "body", type: "text/plain"});
expect(fi.type).toBe("text/plain");
expect(fi.hasMetaFile).toBe(true);
});
it("saves a tiddler with an unsafe field value as JSON", function() {
// A field value with leading whitespace cannot round-trip through a
// .tid header, so the whole tiddler is written as JSON.
var fi = fileInfoFor({title: "Foo", text: "body", type: "text/vnd.tiddlywiki", custom: " leading"});
expect(fi.type).toBe("application/json");
expect(fi.hasMetaFile).toBe(false);
});
it("keeps a tiddler with a _canonical_uri as a .tid file", function() {
var fi = fileInfoFor({title: "Img", type: "image/png", _canonical_uri: "images/x.png"});
expect(fi.type).toBe("application/x-tiddler");
});
it("propagates isEditableFile and originalpath from the existing fileInfo", function() {
var fi = fileInfoFor({title: "Foo", text: "b", type: "text/vnd.tiddlywiki"},{isEditableFile: true, originalpath: "sub/Foo.tid"});
expect(fi.isEditableFile).toBe(true);
expect(fi.originalpath).toBe("sub/Foo.tid");
});
});
describe("generateTiddlerExtension", function() {
it("returns the extension from the first matching extFilter", function() {
var ext = $tw.utils.generateTiddlerExtension("Foo",{
extFilters: ["[[Foo]then[.md]]"],
wiki: $tw.wiki
});
expect(ext).toBe(".md");
});
it("returns undefined when no extFilter matches", function() {
var ext = $tw.utils.generateTiddlerExtension("Foo",{
extFilters: ["[tag[__no_such_tag__]then[.md]]"],
wiki: $tw.wiki
});
expect(ext).toBeUndefined();
});
});
}
+1 -11
View File
@@ -940,20 +940,10 @@ describe("Filter tests", function() {
it("should handle the sortby operator", function() {
expect(wiki.filterTiddlers("a b c +[sortby[d e]]").join(",")).toBe("a,b,c");
expect(wiki.filterTiddlers("a b c +[sortby[b c a]]").join(",")).toBe("b,c,a");
// By default titles missing from the reference list sort to the start
expect(wiki.filterTiddlers("aa a b c +[sortby[b c a cc]]").join(",")).toBe("aa,b,c,a");
expect(wiki.filterTiddlers("a bb b c +[sortby[b c a cc]]").join(",")).toBe("bb,b,c,a");
expect(wiki.filterTiddlers("a bb cc b c +[sortby[b c a cc]]").join(",")).toBe("bb,b,c,a,cc");
// The "end" suffix places missing titles after the listed ones
expect(wiki.filterTiddlers("aa a b c +[sortby:end[b c a cc]]").join(",")).toBe("b,c,a,aa");
expect(wiki.filterTiddlers("a bb b c +[sortby:end[b c a cc]]").join(",")).toBe("b,c,a,bb");
expect(wiki.filterTiddlers("a bb cc b c +[sortby:end[b c a cc]]").join(",")).toBe("b,c,a,cc,bb");
// Missing titles keep their input order. Avoid a repeated title here, it would be moved to the end of the input
expect(wiki.filterTiddlers("zz a yy b +[sortby:end[b a]]").join(",")).toBe("b,a,zz,yy");
// Two titles are the shortest input that is sorted rather than returned untouched
expect(wiki.filterTiddlers("a b +[sortby[b a]]").join(",")).toBe("b,a");
expect(wiki.filterTiddlers("a +[sortby:end[b a]]").join(",")).toBe("a");
expect(wiki.filterTiddlers("b a b c +[sortby[]]").join(",")).toBe("a,b,c");
expect(wiki.filterTiddlers("b a b c +[sortby[a b b c]]").join(",")).toBe("a,b,c");
expect(wiki.filterTiddlers("b a b c +[sortby[b a c b]]").join(",")).toBe("b,a,c");
@@ -173,20 +173,5 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}format:json[ ]]")).toEqual(["{\n \"a\": \"one\",\n \"b\": \"\",\n \"c\": 1.618,\n \"d\": {\n \"e\": \"four\",\n \"f\": [\n \"five\",\n \"six\",\n true,\n false,\n null\n ]\n }\n}"]);
});
it("should support the makepatches operator with json output", function() {
expect(wiki.filterTiddlers("[[The quick brown fox]makepatches::json[The fast brown fox]]")).toEqual([
'[{"type":"equal","text":"The "},{"type":"delete","text":"quick"},{"type":"insert","text":"fast"},{"type":"equal","text":" brown fox"}]'
]);
expect(wiki.filterTiddlers("[[The quick brown fox]makepatches:words:json[The fast brown fox]]")).toEqual([
'[{"type":"equal","text":"The "},{"type":"delete","text":"quick "},{"type":"insert","text":"fast "},{"type":"equal","text":"brown fox"}]'
]);
// Safely ignores missing/invalid second suffix and returns a standard patch string instead of JSON
expect(wiki.filterTiddlers("[[The quick brown fox]makepatches:words[The fast brown fox]]")[0]).not.toContain(
'"type":"equal"'
);
});
});
@@ -1,18 +1,22 @@
created: 20150106180000000
modified: 20241204085601176
list: GroupedLists/Example/TypesTab GroupedLists/Example/ByField GroupedLists/Example/WithSearch GroupedLists/Example/WithTabs GroupedLists/Example/TabsWithSearch GroupedLists/Example/SearchAnotherField GroupedLists/Example/RecentTab
modified: 20260729194941115
tags: ListWidget Lists
title: GroupedLists
type: text/vnd.tiddlywiki
The following sidebar tabs give examples of grouped lists created by nesting.
A grouped list is made by nesting one list inside another. The outer list collects each distinct value of a field, and the inner list finds the tiddlers holding that value.
!! [[Types Tab|$:/core/ui/MoreSideBar/Types]]
TiddlyWiki uses the pattern in two of its sidebar tabs. The [[Types Tab|$:/core/ui/MoreSideBar/Types]] groups tiddlers by their `type` field, and sits under ''More'' in the sidebar. The [[Recent Tab|$:/core/ui/SideBar/Recent]] groups them by day, through the <<.mlink timeline>> macro. Both are reproduced as examples below.
For the "Types Tab", the outer list filter as shown below selects each discrete value found in the `type` field. The inner list filter selects all the (non-system) tiddlers with that type.
!! Interactive Examples
<<tw-code "$:/core/ui/MoreSideBar/Types">>
Each example is a runnable test case carrying its own sample data. Where a field is grouped on, it is named once in <<.def groupField>>, so the grouping can be changed without touching anything else.
!! [[Recent Tab|$:/core/ui/SideBar/Recent]]
<<<
<$list filter="[tag[GroupedLists]]">
The list in the "Recent Tab" is generated using the <<.mlink timeline>> macro. Here, the outer list filter selects each discrete day found in the `modified` field, while the inner list filter selects all the tiddlers dated the same day in the `modified` field.
<<tw-code "$:/core/macros/timeline">>
; <$link/>
: {{!!description}}
</$list>
<<<
@@ -0,0 +1,31 @@
title: ButtonWidget/Example/Accessibility
description: Add accessibility and metadata attributes such as aria-label, role, tabindex and data
modified: 20260724235815000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
A button can carry accessibility and metadata attributes that pass straight through to the HTML element. They have no visible effect, so this example shows the markup rather than a result:
* `aria-label` gives an accessible name when the label is only an icon
* `role` overrides the implied ARIA role
* `tabindex` sets the keyboard focus order
* `data-*` attaches custom data attributes for scripts
* `selectedAria` chooses which ARIA state `selectedClass` toggles
Inspect the button in your browser to see the attributes on the element.
+
title: Output
<$wikify name="button-accessibility" mode="inline" output="html"
text="""<$button aria-label="Close"
role="button"
tabindex="0"
data-action="close"
>
×
</$button>
""">
<$text text=<<button-accessibility>>/>
</$wikify>
@@ -0,0 +1,33 @@
title: ButtonWidget/Example/Actions
description: Run one or more ActionWidgets when the button is clicked, using the preferred actions attribute
modified: 20260725015330000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
The `actions` attribute holds one or more ActionWidgets that run when the button is clicked. This is the preferred way to give a button its behaviour, because the whole behaviour sits in one place instead of being spread over several attributes.
A button runs `actions` last, after `to`, `message`, `popup` and `set`. ButtonWidget lists the full order. A single `<$button to="HelloThere">Click Me</$button>` still needs no action string.
The procedure below runs //two// actions in a single click:
* one sets a message
* the other bumps a counter
Click ''Greet'' a few times to watch both update.
+
title: Output
\procedure greetActions()
<$action-setfield $tiddler="$:/state/greeting" text="Hello!"/>
<$action-setfield $tiddler="$:/state/greeting-count"
text={{{ [[$:/state/greeting-count]get[text]] :else[[0]] +[add[1]] }}}
/>
\end
<$button actions=<<greetActions>>>
Greet
</$button>
Message: <$text text={{$:/state/greeting}}/> (clicked <$text text={{{ [[$:/state/greeting-count]get[text]] :else[[0]] }}}/> times)
@@ -0,0 +1,34 @@
title: ButtonWidget/Example/DragAndDrop
description: Make a button draggable with dragTiddler or dragFilter
modified: 20260724235815000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
`dragTiddler` makes the button draggable and sets the single tiddler it carries. `dragFilter` carries a list of tiddlers produced by a filter instead.
Choose one of the two:
* `dragTiddler` is a single tiddler title to drag
* `dragFilter` is a filter whose results are dragged as a list
Drag the button onto the drop area to see the title it carries. Drag and drop needs a desktop browser.
+
title: Output
\procedure recordDrop()
<$action-setfield $tiddler="$:/state/dropped" text=<<actionTiddler>>/>
\end
<$button dragTiddler="HelloThere" class="tc-btn-invisible tc-tiddlylink">
Drag me
</$button>
<$droppable actions=<<recordDrop>>>
<div style="border: 1px dashed #999; padding: 0.5em; margin-top: 0.5em;">
Drop the button here
</div>
</$droppable>
Dropped title: <$text text={{$:/state/dropped}}/>
@@ -0,0 +1,54 @@
title: ButtonWidget/Example/DragFilter
description: Drag a list of tiddlers with dragFilter and receive it with listActions
modified: 20260725001110000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
`dragFilter` makes the button draggable and carries every tiddler the filter returns, where `dragTiddler` carries a single title.
The drop target chooses how it receives that payload:
* `actions` runs once per dragged tiddler, with the title in `<<actionTiddler>>`
* `listActions` runs once for the whole payload, with all titles in `<<actionTiddlerList>>`
This test case carries four payload tiddlers tagged `Concepts`. The button drags all four, so the drop area shows the whole list at once. Drag and drop needs a desktop browser.
+
title: Cascades
tags: Concepts
A cascade is a filter list that is evaluated in turn until one returns a result.
+
title: ColourPalettes
tags: Concepts
A colour palette is a tiddler holding the named colours used by the user interface.
+
title: Commands
tags: Concepts
Commands are the operations run by the Node.js server from the command line.
+
title: Filters
tags: Concepts
A filter is an expression that selects a list of tiddler titles from the wiki.
+
title: Output
\procedure recordDropList()
<$action-setfield $tiddler="$:/state/dropped-list" text=<<actionTiddlerList>>/>
\end
<$button dragFilter="[tag[Concepts]]" class="tc-btn-invisible tc-tiddlylink">
Drag four tiddlers
</$button>
<$droppable listActions=<<recordDropList>>>
<div style="border: 1px dashed #999; padding: 0.5em; margin-top: 0.5em;">
Drop the button here
</div>
</$droppable>
Dropped titles: <$text text={{$:/state/dropped-list}}/>
@@ -0,0 +1,30 @@
title: ButtonWidget/Example/Message
description: Send a widget message with the message and param attributes
modified: 20260724235815000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
The `message` attribute sends a widget message when the button is clicked. The `param` attribute passes a single value with it.
The two attributes work together:
* `message` is the message type to send
* `param` is the value delivered with the message
A `messagecatcher` receives the message here and shows the parameter. Click the button to send it.
+
title: Output
\procedure receiveMessage()
<$action-setfield $tiddler="$:/state/received" text=<<event-param>>/>
\end
<$messagecatcher $tm-sample-message=<<receiveMessage>>>
<$button message="tm-sample-message" param="Hello from param">
Send message
</$button>
</$messagecatcher>
Received param: <$text text={{$:/state/received}}/>
@@ -0,0 +1,42 @@
title: ButtonWidget/Example/NewTiddler
description: Create a tiddler from a template with the tm-new-tiddler message
modified: 20260725002340000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
`tm-new-tiddler` is a message the core already handles, so the button needs no catcher of its own.
* `message` is set to `tm-new-tiddler`
* `param` names a template tiddler, and the new tiddler starts out with that template's fields
This test case carries a `TaskTemplate` payload tiddler holding `tags`, `status` and `priority`. Open the ''TaskTemplate'' tab above to see it.
The message creates a draft and normally opens it for editing. A test case has no story river, so the table lists the drafts instead. Each click adds one row, and the new tiddler takes the next free title because the previous draft still exists.
+
title: TaskTemplate
tags: Task
status: open
priority: normal
Describe the task here.
+
title: Output
<$button message="tm-new-tiddler" param="TaskTemplate">
New task
</$button>
<table>
<tr><th>Click</th><th>New tiddler</th><th>tags</th><th>status</th><th>priority</th></tr>
<$list filter="[has[draft.title]sort[draft.title]]" counter="clickNumber">
<tr>
<td><<clickNumber>></td>
<td><$text text={{!!draft.title}}/></td>
<td><$text text={{!!tags}}/></td>
<td><$text text={{!!status}}/></td>
<td><$text text={{!!priority}}/></td>
</tr>
</$list>
</table>
@@ -0,0 +1,38 @@
created: 20260724134149482
description: Toggle a popup with the popup attribute and a companion reveal widget
modified: 20260725003054000
tags: $:/tags/wiki-test-spec
title: ButtonWidget/Example/Popup
type: text/vnd.tiddlywiki-multiple
title: Narrative
The `popup` attribute names a state tiddler that stores the popup coordinates. A companion `reveal` widget reads that state to show or hide the content.
The button uses these attributes:
* `popup` is the state tiddler for the popup
* `selectedClass` highlights the button while the popup is open
* `popupAbsCoords` writes absolute coordinates when set to `yes`, rather than relative ones
Use `popupTitle` in place of `popup` when the state title should not be read as a text reference. Click the button to toggle the popup.
The `reveal` widget always creates a DOM element of its own: a `div` in block mode, a `span` in inline mode. The `tag` attribute sets that element explicitly. Wrapping the popup content in a `<div>` would add a second element, so this example sets `tag="div"` and puts the `tc-drop-down` class on the widget itself. The popup is then one element instead of two.
+
title: Output
<$button popup="$:/state/popup/demo" selectedClass="tc-selected">
Toggle popup
</$button>
<$reveal type="popup"
tag="div"
state="$:/state/popup/demo"
position="belowleft"
animate="yes"
class="tc-drop-down"
>
This is the popup content.
</$reveal>
@@ -0,0 +1,41 @@
title: ButtonWidget/Example/Presentation
description: Change the rendered element and appearance with tag, class, style and disabled
modified: 20260725003716000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
Several attributes control how the button looks and which element it renders:
* `tag` renders a different HTML element in place of `button`
* `class` adds one or more CSS classes
* `style` sets a full CSS style string
* `disabled` set to `yes` disables the button
Reach for `class` first and keep the rules in a stylesheet. One class restyles every button that uses it, follows the colour palette and stays under the user's control. A hardcoded `style` overrides the theme and has to be repeated at every button, so use it only when no class can do the job.
`style` sets the whole style attribute and replaces anything a `style.*` attribute wrote before it, so do not mix the two.
+
title: Output
<$button tag="a" class="tc-tiddlylink">
tag="a" renders a link element
</$button>
<$button class="tc-btn-big-green">
class uses a rule from the stylesheet
</$button>
<$button disabled="yes">
disabled="yes"
</$button>
<$button style.color="green">
style is the last resort
</$button>
<$button style="font-weight: bold;">
style is the last resort
</$button>
@@ -0,0 +1,52 @@
title: ButtonWidget/Example/SelectedState
description: Highlight the active button with selectedClass, set, setTo and default used together
modified: 20260724235815000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
This example combines four attributes so a row of buttons can show which colour is active:
* `set` and `setTo` write the chosen colour to a state tiddler
* `selectedClass` marks the button whose `setTo` matches the current value of `set`
* `default` gives the value to compare against while the `set` tiddler does not yet exist
Because `default` is `red`, the ''Red'' button starts out selected. Click another colour to move the selection.
+
title: Output
<!-- The inline <style> below is only for this self-contained test/development example.
For production, define styles in a tiddler tagged $:/tags/Stylesheet instead. -->
<style>
.demo-swatch {
padding: 0.2em 0.7em;
}
.demo-selected {
outline: 2px solid #1a73e8;
}
</style>
<$button set="$:/state/colour" setTo="red"
default="red"
class="demo-swatch tc-small-gap-right"
selectedClass="demo-selected"
>
Red
</$button>
<$button set="$:/state/colour" setTo="green"
default="red"
class="demo-swatch tc-small-gap-right"
selectedClass="demo-selected"
>
Green
</$button>
<$button set="$:/state/colour" setTo="blue"
default="red"
class="demo-swatch tc-small-gap-right"
selectedClass="demo-selected"
>
Blue
</$button>
Selected colour: <$text text={{{ [[$:/state/colour]get[text]] :else[[red]] }}}/>
@@ -0,0 +1,24 @@
title: ButtonWidget/Example/SetAndSetTo
description: Assign a value to a tiddler with the set and setTo attributes used together
modified: 20260724235815000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
`set` and `setTo` work as a pair to assign a value when the button is clicked:
* `set` names the storage location, a TextReference
* `setTo` is the value written to it
`set` points at the location //itself//, so it takes no curly brackets, unlike an ordinary transclusion.
Click a colour to write it to the state tiddler shown below.
+
title: Output
<$button set="$:/state/colour" setTo="red">Red</$button>
<$button set="$:/state/colour" setTo="green">Green</$button>
<$button set="$:/state/colour" setTo="blue">Blue</$button>
Selected colour: <$text text={{$:/state/colour}}/>
@@ -0,0 +1,37 @@
title: ButtonWidget/Example/SetField
description: Assign a field or index directly with setTitle, setField and setIndex
modified: 20260724235815000
tags: [[$:/tags/wiki-test-spec]]
type: text/vnd.tiddlywiki-multiple
title: Narrative
The `set` attribute treats its value as a TextReference, which is ambiguous when a title contains `!!` or `##`. The `setTitle` group names the target directly instead.
The attributes work together:
* `setTitle` is the tiddler to change, with no TextReference parsing
* `setField` is the field to write, defaulting to `text`
* `setIndex` writes a data index instead of a field
* `setTo` is the value to assign
Click a button to set the `caption` field, then watch it update below.
+
title: Output
<$button setTitle="$:/state/demo"
setField="caption"
setTo="First"
class="tc-small-gap-right"
>
Set caption to First
</$button>
<$button setTitle="$:/state/demo"
setField="caption"
setTo="Second"
class="tc-small-gap-right"
>
Set caption to Second
</$button>
Caption is now: <$text text={{$:/state/demo!!caption}}/>
@@ -0,0 +1,173 @@
code-body: yes
created: 20260728220000000
description: Bibliography sample data shared by the GroupedLists examples
modified: 20260729180306417
title: GroupedLists/Bibliography
type: text/vnd.tiddlywiki-multiple
title: 3DWiki2011
bibtex-entry-type: InProceedings
bibtex-author: 3D Wiki Collective
bibtex-title: Spatial Notes in Three Dimensions
bibtex-date: 2011
+
title: 3rdWave2014
bibtex-entry-type: Article
bibtex-author: 3rd Wave Research Group
bibtex-title: Note Taking After the Desktop
bibtex-date: 2014
+
title: Aronsson2002
bibtex-entry-type: InProceedings
bibtex-author: Aronsson, Lars
bibtex-title: Operation of a Large Scale, General Purpose Wiki Website
bibtex-date: 2002
+
title: Atkinson2016
bibtex-entry-type: Article
bibtex-author: Atkinson, Paul
bibtex-title: Digital ethnographies
bibtex-date: 2016
+
title: Bakshi2016
bibtex-entry-type: Article
bibtex-author: Bakshi, Divya
bibtex-title: Hypertext and Feminisms: Voicing the Silence
bibtex-date: 2016
+
title: Barker2008
bibtex-entry-type: InProceedings
bibtex-author: Barker, Philip
bibtex-title: Using wikis and weblogs to enhance human performance
bibtex-date: 2008
+
title: Barker2008a
bibtex-entry-type: InProceedings
bibtex-author: Barker, Philip
bibtex-title: Using Wikis for Knowledge Management
bibtex-date: 2008
+
title: Bernstein2016
bibtex-entry-type: Book
bibtex-author: Bernstein, Mark
bibtex-title: Getting Started With Hypertext Narrative
bibtex-date: 2016
+
title: Dalgaard2001
bibtex-entry-type: InProceedings
bibtex-author: Dalgaard, Rune
bibtex-title: Hypertext and the Scholarly Archive: Intertexts, Paratexts and Metatexts at Work
bibtex-date: 2001
+
title: Dickinson2008
bibtex-entry-type: InProceedings
bibtex-author: Dickinson, Anne
bibtex-title: Is the e-Learning Object Create Interactive Accessible e-Learning Accessible?
bibtex-date: 2008
+
title: Finnemann2016
bibtex-entry-type: Article
bibtex-author: Finnemann, Niels Ole
bibtex-title: Hypertext configurations: Genres in networked digital media
bibtex-date: 2016
+
title: Frumkin2005
bibtex-entry-type: Article
bibtex-author: Frumkin, Jeremy
bibtex-title: The wiki and the digital library
bibtex-date: 2005
+
title: Maier2016
bibtex-entry-type: InCollection
bibtex-author: Maier, Carmen Daniela
bibtex-title: Hypertext and Hypermedia
bibtex-date: 2016
+
title: Morris2007
bibtex-entry-type: InProceedings
bibtex-author: Morris, Joseph C.
bibtex-title: DistriWiki: a distributed peer-to-peer wiki network
bibtex-date: 2007
+
title: Ruston2004
bibtex-entry-type: Book
bibtex-author: Ruston, Jeremy
bibtex-title: TiddlyWiki
bibtex-date: 2004
+
title: Rutherford2009
bibtex-entry-type: Thesis
bibtex-author: Rutherford, Jayne
bibtex-title: Graphical Input for TiddlyWiki
bibtex-date: 2009
+
title: Schaffert2006
bibtex-entry-type: InProceedings
bibtex-author: Schaffert, Sebastian
bibtex-title: IkeWiki: A semantic wiki for collaborative knowledge management
bibtex-date: 2006
+
title: Shang2016
bibtex-entry-type: Article
bibtex-author: Shang, Hui-Fang
bibtex-title: Online metacognitive strategies, hypermedia annotations, and motivation on hypertext comprehension
bibtex-date: 2016
+
title: Shang2016a
bibtex-entry-type: Article
bibtex-author: Shang, Hui-Fang
bibtex-title: Exploring demographic and motivational factors associated with hypertext reading by English as a foreign language students
bibtex-date: 2016
+
title: Skiba2005
bibtex-entry-type: Article
bibtex-author: Skiba, Diane J.
bibtex-title: Do your students wiki?
bibtex-date: 2005
+
title: Trentin2009
bibtex-entry-type: Article
bibtex-author: Trentin, Guglielmo
bibtex-title: Using a wiki to evaluate individual contribution to a collaborative learning project
bibtex-date: 2009
+
title: Truman2016
bibtex-entry-type: Article
bibtex-author: Truman, Gail
bibtex-title: Web Archiving Environmental Scan
bibtex-date: 2016
+
title: Wagner2004
bibtex-entry-type: Article
bibtex-author: Wagner, Christian
bibtex-title: Wiki: A technology for conversational knowledge management and group collaboration
bibtex-date: 2004
+
title: Wilson2007
bibtex-entry-type: Article
bibtex-author: Wilson, Tom D.
bibtex-title: Review of TiddlyWiki 2.1.3
bibtex-date: 2007
@@ -0,0 +1,45 @@
created: 20260728220100000
description: Group tiddlers by the value of a field chosen in one place
import-compound: [[GroupedLists/Bibliography]]
modified: 20260729185240642
tags: $:/tags/wiki-test-spec GroupedLists
title: GroupedLists/Example/ByField
type: text/vnd.tiddlywiki-multiple
title: Narrative
`groupField` names the field to group by. It is the only line to change when you want a different grouping.
Two nested lists do the work:
* `f.valuesAll` collects each distinct value of that field, sorted
* `f.tiddlersFor` returns the tiddlers holding one of those values
This test case imports 24 bibliography tiddlers from the `GroupedLists/Bibliography` payload. They came from a ~BibTeX import, so the field is `bibtex-author` rather than `author`.
Barker and Shang each published twice, so their groups list two titles. Set `groupField` to `bibtex-entry-type` to regroup the same data by publication type.
+
title: Output
\whitespace trim
<!-- bibtex-author<value> would make the field name the operator, which cannot be a variable, so f.tiddlersFor compares via get<groupField> -->
<!-- No trailing whitespace after the field name: it becomes part of the name and the list silently renders nothing -->
\procedure groupField() bibtex-author
\function f.valuesAll() [all[tiddlers]!is[system]has<groupField>each<groupField>get<groupField>sort[]]
\function f.tiddlersFor(value) [!is[system]has<groupField>] :filter[get<groupField>match<value>] +[sort[title]]
<$list filter="[f.valuesAll[]]" variable="_value">
<div class="tc-menu-list-item">
<<_value>>
<$list filter="[f.tiddlersFor<_value>]">
<div class="tc-menu-list-subitem">
<$link><$text text={{{ [<currentTiddler>get[bibtex-title]] :else[<currentTiddler>] }}}/></$link>
</div>
</$list>
</div>
</$list>
@@ -0,0 +1,51 @@
title: GroupedLists/Example/RecentTab
created: 20260729190100000
modified: 20260729190100000
description: Group tiddlers by day with the timeline macro, as the sidebar Recent tab does
tags: [[$:/tags/wiki-test-spec]] GroupedLists
type: text/vnd.tiddlywiki-multiple
title: Narrative
The `timeline` macro groups tiddlers by the day found in a date field. The ''Recent'' tab in the sidebar is a single call to it, which is all the Output below is.
The nesting happens inside the macro:
* the outer filter uses `eachday` to yield one tiddler per distinct day
* the inner filter uses `sameday` to collect every tiddler falling on that day
* `dateField` chooses which field to read, so the same macro groups by `created` just as well as by `modified`
The five dummy payload tiddlers below carry hand set `modified` dates. Nothing else here has a date field, so they are exactly what the timeline finds.
Pass `format` to change how each day is headed, and `limit` to cap how many tiddlers are considered.
+
title: Output
\whitespace trim
<$transclude $variable="timeline" format="DDth MMM YYYY"/>
+
title: Alpha
modified: 20260103120000000
Modified on the third, later in the day.
+
title: Beta
modified: 20260103100000000
Modified on the third, earlier in the day.
+
title: Gamma
modified: 20260102120000000
Modified on the second, later in the day.
+
title: Delta
modified: 20260102100000000
Modified on the second, earlier in the day.
+
title: Epsilon
modified: 20260101120000000
Modified on the first.
@@ -0,0 +1,116 @@
created: 20260728224000000
description: Group by one field while the search box filters on another
import-compound: [[GroupedLists/Bibliography]]
modified: 20260729190424885
tags: $:/tags/wiki-test-spec GroupedLists
title: GroupedLists/Example/SearchAnotherField
type: text/vnd.tiddlywiki-multiple
title: Narrative
`searchField` names the field the search box looks at, which no longer has to be the field the list groups by.
Three definitions carry the split:
* `groupField` is `bibtex-entry-type`, so the groups and the tabs are publication types
* `searchField` is `bibtex-author`, so typing still matches people
* `f.tiddlersMatching` narrows the tiddlers first, and the groups are derived from whatever survives
Because the groups are computed after the search, a whole group disappears once none of its entries match. The tab strip is still built from the unfiltered data, so the tabs themselves stay put.
This test case carries the same `_TabButton` and `_TabContent` payload tiddlers as the previous example.
Type `bar` to leave only ''InProceedings'', holding Barker's two papers. The other tabs drop to zero and show the `emptyMessage` of the list.
+
title: Output
\whitespace trim
<!-- Filter operator suffixes cannot be variables, so search:<searchField> is impossible.
f.tiddlersMatching searches the extracted value as a title, which works whichever field it came from. -->
<!-- No trailing whitespace after the field name: it becomes part of the name and the list silently renders nothing -->
\procedure groupField() bibtex-entry-type
\procedure searchField() bibtex-author
\procedure searchLabel() Author
\function f.valuesAll() [all[tiddlers]!is[system]has<groupField>each<groupField>get<groupField>sort[]]
\function f.tiddlersMatching() [all[tiddlers]!is[system]has<groupField>] :filter[get<searchField>search:title{$:/temp/GroupedLists/search-other}]
\function f.valuesFound() [f.tiddlersMatching[]each<groupField>get<groupField>sort[]]
\function f.letters() [f.valuesAll[]] :map[uppercase[]split[]first[]] +[unique[]sort[]]
\function f.valuesFor(letter) [f.valuesFound[]] :filter[uppercase[]split[]first[]match<letter>]
\function f.valuesShown(letter) [f.valuesFound[]] :filter[<letter>match[All]] :else[f.valuesFor<letter>]
\function f.tiddlersFor(value) [f.tiddlersMatching[]] :filter[get<groupField>match<value>] +[sort[title]]
<!-- The inline style below is only for this self-contained example. For production, define styles in a tiddler tagged $:/tags/Stylesheet instead. -->
<style>.demo-group-filter { margin-bottom: 1em; }</style>
<$macrocall
$name="tabs"
tabsList="[[All]] [f.letters[]]"
default="All"
state="$:/state/GroupedLists/tab-other"
buttonTemplate="_TabButton"
template="_TabContent"
/>
+
title: _TabButton
code-body: yes
\whitespace trim
<$text text=<<currentTab>>/><span class="tc-tiny-gap-left">(<$count filter="[f.valuesShown<currentTab>]"/>)</span>
+
title: _TabContent
code-body: yes
\whitespace trim
<!-- tm-focus-selector takes the first match in the document, so .tc-search input would hit the sidebar search. -->
<div class="tc-search demo-group-filter">
<label>
<<searchLabel>>:<$edit-text
tiddler="$:/temp/GroupedLists/search-other"
tag="input"
type="search"
default=""
placeholder={{{ [[Filter by ]addsuffix<searchLabel>] }}}
class="tc-tiny-gap"
/>
</label>
<%if [[$:/temp/GroupedLists/search-other]get[text]minlength[1]] %>
<$button
class="tc-btn-invisible"
tooltip="Clear the filter"
aria-label="Clear the filter"
>
<$action-deletetiddler $tiddler="$:/temp/GroupedLists/search-other"/>
<$action-sendmessage $message="tm-focus-selector" $param=".demo-group-filter input"/>
{{$:/core/images/close-button}}
</$button>
<%endif%>
</div>
<$list filter="[f.valuesShown<currentTab>]" variable="_value">
<$list-empty>
//No matches//
</$list-empty>
<div class="tc-menu-list-item">
<<_value>>
<$list filter="[f.tiddlersFor<_value>]">
<div class="tc-menu-list-subitem">
<$link><$text text={{{ [<currentTiddler>get[bibtex-title]] :else[<currentTiddler>] }}}/></$link>
</div>
</$list>
</div>
</$list>
@@ -0,0 +1,111 @@
created: 20260728220400000
description: Combine the tab strip with a search box inside the tab panel
import-compound: [[GroupedLists/Bibliography]]
modified: 20260729190424885
tags: $:/tags/wiki-test-spec GroupedLists
title: GroupedLists/Example/TabsWithSearch
type: text/vnd.tiddlywiki-multiple
title: Narrative
`_TabContent` holds the search box as well as the list, so the search sits inside the panel and filters within the selected tab.
Which values each definition reads is the whole design:
* `f.letters` reads the //unfiltered// values, so tabs never disappear while you type and the selected tab cannot be stranded on an empty panel
* `f.valuesShown` reads the //filtered// values, so the counts follow the search and a tab showing zero tells you not to look there
This test case carries the same `_TabButton` and `_TabContent` payload tiddlers as the previous example, with the search box added to the panel.
The input is a sibling of the list rather than a child of anything that recomputes, which is what keeps the cursor in the box while you type.
Type `ba` and watch the counts change. Any tab whose count falls to zero shows the `emptyMessage` of the list instead of nothing at all.
+
title: Output
\whitespace trim
<!-- Filter operator suffixes cannot be variables, so search:<groupField> and bibtex-author<value> are impossible.
f.valuesFound searches the collected values as titles, f.tiddlersFor compares via get<groupField>. -->
<!-- No trailing whitespace after the field name: it becomes part of the name and the list silently renders nothing -->
\procedure groupField() bibtex-author
\procedure groupLabel() Author
\function f.valuesAll() [all[tiddlers]!is[system]has<groupField>each<groupField>get<groupField>sort[]]
\function f.valuesFound() [f.valuesAll[]] :filter[search:title{$:/temp/GroupedLists/search}]
\function f.letters() [f.valuesAll[]] :map[uppercase[]split[]first[]] +[unique[]sort[]]
\function f.valuesFor(letter) [f.valuesFound[]] :filter[uppercase[]split[]first[]match<letter>]
\function f.valuesShown(letter) [f.valuesFound[]] :filter[<letter>match[All]] :else[f.valuesFor<letter>]
\function f.tiddlersFor(value) [!is[system]has<groupField>] :filter[get<groupField>match<value>] +[sort[title]]
<!-- The inline style below is only for this self-contained example. For production, define styles in a tiddler tagged $:/tags/Stylesheet instead. -->
<style>.demo-group-filter { margin-bottom: 1em; }</style>
<$macrocall
$name="tabs"
tabsList="[[All]] [f.letters[]]"
default="All"
state="$:/state/GroupedLists/tab-search"
buttonTemplate="_TabButton"
template="_TabContent"
/>
+
title: _TabButton
code-body: yes
\whitespace trim
<$text text=<<currentTab>>/><span class="tc-tiny-gap-left">(<$count filter="[f.valuesShown<currentTab>]"/>)</span>
+
title: _TabContent
code-body: yes
\whitespace trim
<!-- tm-focus-selector takes the first match in the document, so .tc-search input would hit the sidebar search. -->
<div class="tc-search demo-group-filter">
<label>
<<groupLabel>>:<$edit-text
tiddler="$:/temp/GroupedLists/search"
tag="input"
type="search"
default=""
placeholder={{{ [[Filter by ]addsuffix<groupLabel>] }}}
class="tc-tiny-gap"
/>
</label>
<%if [[$:/temp/GroupedLists/search]get[text]minlength[1]] %>
<$button
class="tc-btn-invisible"
tooltip="Clear the filter"
aria-label="Clear the filter"
>
<$action-deletetiddler $tiddler="$:/temp/GroupedLists/search"/>
<$action-sendmessage $message="tm-focus-selector" $param=".demo-group-filter input"/>
{{$:/core/images/close-button}}
</$button>
<%endif%>
</div>
<$list filter="[f.valuesShown<currentTab>]" variable="_value">
<$list-empty>
//No matches//
</$list-empty>
<div class="tc-menu-list-item">
<<_value>>
<$list filter="[f.tiddlersFor<_value>]">
<div class="tc-menu-list-subitem">
<$link><$text text={{{ [<currentTiddler>get[bibtex-title]] :else[<currentTiddler>] }}}/></$link>
</div>
</$list>
</div>
</$list>
@@ -0,0 +1,60 @@
title: GroupedLists/Example/TypesTab
created: 20260729190000000
modified: 20260729190000000
description: Group tiddlers by their type, as the sidebar Types tab does
tags: [[$:/tags/wiki-test-spec]] GroupedLists
type: text/vnd.tiddlywiki-multiple
title: Narrative
`$:/core/Filters/TypedTiddlers` collects one tiddler per distinct type, and the inner list then finds every tiddler sharing that type.
The two lists do different jobs:
* the outer filter ends in `each[type]`, so it yields one representative tiddler per type rather than every tiddler
* the inner filter reads `{!!type}` from that representative, so `currentTiddler` is what carries the type from the outer list to the inner one
This is the code behind the ''Types'' tab in the sidebar, under ''More''. Open that tab to see it running over a real wiki.
The five dummy payload tiddlers below are the only ones here with a `type` field, so they are exactly what the list finds.
+
title: Output
\whitespace trim
<$list filter={{$:/core/Filters/TypedTiddlers!!filter}}>
<div class="tc-menu-list-item">
<$view field="type"/>
<$list filter="[type{!!type}!is[system]sort[title]]">
<div class="tc-menu-list-subitem">
<$link to={{!!title}}><$view field="title"/></$link>
</div>
</$list>
</div>
</$list>
+
title: Alpha
type: image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><circle cx="8" cy="8" r="7"/></svg>
+
title: Beta
type: image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect x="1" y="1" width="14" height="14"/></svg>
+
title: Gamma
type: text/plain
A plain text sample.
+
title: Delta
type: text/plain
Another plain text sample.
+
title: Epsilon
type: application/json
{"sample": true}
@@ -0,0 +1,82 @@
created: 20260728220200000
description: Narrow a grouped list with a search box
import-compound: [[GroupedLists/Bibliography]]
modified: 20260729185240642
tags: $:/tags/wiki-test-spec GroupedLists
title: GroupedLists/Example/WithSearch
type: text/vnd.tiddlywiki-multiple
title: Narrative
`search:title` filters the values collected by `f.valuesAll`, so the grouped list narrows while you type without the tiddlers themselves being searched.
Two definitions are added to the plain grouped list:
* `groupLabel` is the human readable name shown beside the input
* `f.valuesFound` applies the search to the collected values
An empty box is a no operation, so the whole list comes back. The clear button appears only once there is something to clear, and puts the cursor back in the input.
Type `ann` to match Dickinson and Finnemann, `wi` to match several at once, or `zzz` to fall through to the `emptyMessage` of the outer list.
+
title: Output
\whitespace trim
<!-- Filter operator suffixes cannot be variables, so search:<groupField> and bibtex-author<value> are impossible.
f.valuesFound searches the collected values as titles, f.tiddlersFor compares via get<groupField>. -->
<!-- tm-focus-selector takes the first match in the document, so .tc-search input would hit the sidebar search. -->
<!-- No trailing whitespace after the field name: it becomes part of the name and the list silently renders nothing -->
\procedure groupField() bibtex-author
\procedure groupLabel() Author
\function f.valuesAll() [all[tiddlers]!is[system]has<groupField>each<groupField>get<groupField>sort[]]
\function f.valuesFound() [f.valuesAll[]] :filter[search:title{$:/temp/GroupedLists/search}]
\function f.tiddlersFor(value) [!is[system]has<groupField>] :filter[get<groupField>match<value>] +[sort[title]]
<!-- The inline style below is only for this self-contained example. For production, define styles in a tiddler tagged $:/tags/Stylesheet instead. -->
<style>.demo-group-filter { margin-bottom: 1em; }</style>
<div class="tc-search demo-group-filter">
<label>
<<groupLabel>>:<$edit-text
tiddler="$:/temp/GroupedLists/search"
tag="input"
type="search"
default=""
placeholder={{{ [[Filter by ]addsuffix<groupLabel>] }}}
class="tc-tiny-gap"
/>
</label>
<%if [[$:/temp/GroupedLists/search]get[text]minlength[1]] %>
<$button
class="tc-btn-invisible"
tooltip="Clear the filter"
aria-label="Clear the filter"
>
<$action-deletetiddler $tiddler="$:/temp/GroupedLists/search"/>
<$action-sendmessage $message="tm-focus-selector" $param=".demo-group-filter input"/>
{{$:/core/images/close-button}}
</$button>
<%endif%>
</div>
<$list filter="[f.valuesFound[]]" variable="_value">
<$list-empty>
//No matches//
</$list-empty>
<div class="tc-menu-list-item">
<<_value>>
<$list filter="[f.tiddlersFor<_value>]">
<div class="tc-menu-list-subitem">
<$link><$text text={{{ [<currentTiddler>get[bibtex-title]] :else[<currentTiddler>] }}}/></$link>
</div>
</$list>
</div>
</$list>
@@ -0,0 +1,76 @@
created: 20260728220300000
description: Bucket the groups into tabs by first character, with a count per tab
import-compound: [[GroupedLists/Bibliography]]
modified: 20260729190424885
tags: $:/tags/wiki-test-spec GroupedLists
title: GroupedLists/Example/WithTabs
type: text/vnd.tiddlywiki-multiple
title: Narrative
The `tabs` macro takes its tab list from a filter, so here the tabs come out of the data: one per distinct first character, plus an ''All'' tab.
Three definitions feed it:
* `f.letters` collects the distinct first characters
* `f.valuesFor` selects the groups filed under one character
* `f.valuesShown` returns everything for ''All'', one character otherwise
This test case carries two payload tiddlers, `_TabButton` and `_TabContent`, because the `tabs` macro transcludes its caption and its panel by tiddler title and cannot take inline wikitext.
Matching is case insensitive, so `de Vries` and `De Vries` would share a tab. A character that is not a letter gets its own tab, which is why the sample data produces a `3` tab. Each caption carries the number of groups behind it, so no tab is a dead end.
+
title: Output
\whitespace trim
<!-- bibtex-author<value> would make the field name the operator, which cannot be a variable, so f.tiddlersFor compares via get<groupField> -->
<!-- No trailing whitespace after the field name: it becomes part of the name and the list silently renders nothing -->
\procedure groupField() bibtex-author
\function f.valuesAll() [all[tiddlers]!is[system]has<groupField>each<groupField>get<groupField>sort[]]
\function f.letters() [f.valuesAll[]] :map[uppercase[]split[]first[]] +[unique[]sort[]]
\function f.valuesFor(letter) [f.valuesAll[]] :filter[uppercase[]split[]first[]match<letter>]
\function f.valuesShown(letter) [f.valuesAll[]] :filter[<letter>match[All]] :else[f.valuesFor<letter>]
\function f.tiddlersFor(value) [!is[system]has<groupField>] :filter[get<groupField>match<value>] +[sort[title]]
<$macrocall
$name="tabs"
tabsList="[[All]] [f.letters[]]"
default="All"
state="$:/state/GroupedLists/tab"
buttonTemplate="_TabButton"
template="_TabContent"
/>
+
title: _TabButton
code-body: yes
\whitespace trim
<$text text=<<currentTab>>/><span class="tc-tiny-gap-left">(<$count filter="[f.valuesShown<currentTab>]"/>)</span>
+
title: _TabContent
code-body: yes
\whitespace trim
<$list filter="[f.valuesShown<currentTab>]" variable="_value">
<$list-empty>
//No matches//
</$list-empty>
<div class="tc-menu-list-item">
<<_value>>
<$list filter="[f.tiddlersFor<_value>]">
<div class="tc-menu-list-subitem">
<$link><$text text={{{ [<currentTiddler>get[bibtex-title]] :else[<currentTiddler>] }}}/></$link>
</div>
</$list>
</div>
</$list>
@@ -1,5 +1,5 @@
created: 20230304160331362
modified: 20260724082228670
modified: 20230304160332927
tags: [[makepatches Operator]] [[applypatches Operator]] [[Operator Examples]]
title: makepatches and applypatches Operator (Examples)
type: text/vnd.tiddlywiki
@@ -40,12 +40,4 @@ The `lines` mode doesn't work as well in this application:
It is better suited as a very fast algorithm to detect line-wise incremental changes to texts and store only the changes instead of multiple versions of the whole texts.
The `json` suffix outputs a structured JSON array representing the differences instead of a patch string. To use the JSON output with the default character mode, skip the first suffix with a double colon (`::`).
<<.operator-example 8 "[{Hamlet##Shakespeare-old}makepatches::json{Hamlet##Shakespeare-new}]">>
The `json` suffix can also be combined with the `words` or `lines` modes:
<<.operator-example 9 "[{Hamlet##Shakespeare-old}makepatches:words:json{Hamlet##Shakespeare-new}]">>
</div>
@@ -9,4 +9,3 @@ type: text/vnd.tiddlywiki
<<.operator-example 1 "10 6 4 9 3 2 8 +[sortby[1 2 3 4 5 6 7 8 9 10]]">>
<<.operator-example 2 "Friday Tuesday Monday Thursday Sunday +[sortby{Days of the Week!!list}]">>
<<.operator-example 3 "1 Mon 5 Fri 4 Tue Sun 2 +[sortby{Days of the Week!!short}]">>
<<.operator-example 4 "1 Mon 5 Fri 4 Tue Sun 2 +[sortby:end{Days of the Week!!short}]">>
@@ -1,28 +1,23 @@
caption: makepatches
created: 20230304122354967
modified: 20260724081502905
modified: 20230304122400128
op-purpose: returns a set of patches that transform the input to a given string
op-input: a [[selection of titles|Title Selection]]
op-output: a set of patch instructions per input title to be used by the [[applypatches Operator]] to transform the input title(s) into the string <<.place S>>, or a structured JSON array representing the differences if the `json` suffix is used
op-parameter: a string of characters
op-parameter-name: S
op-purpose: returns a set of patches that transform the input to a given string
op-suffix: optional suffixes specifying the diff mode and output format
op-suffix-name: T:F
op-output: a set of patch instructions per input title to be used by the [[applypatches Operator]] to transform the input title(s) into the string <<.place S>>
op-suffix: `lines` to operate in line mode, `words` to operate in word mode. If omitted (default), the algorithm operates in character mode. See notes below.
op-suffix-name: T
tags: [[Filter Operators]] [[String Operators]]
title: makepatches Operator
type: text/vnd.tiddlywiki
<<.from-version "5.2.6">>
The <<.op makepatches>> operator uses up to two suffixes:
* <<.place T>> (diff mode): `lines` to operate in line mode, `words` to operate in word mode. If omitted (default), the algorithm operates in character mode.
* <<.place F>> (output format): <<.from-version "5.5.0">> `json` to output a structured JSON array of differences instead of a standard patch string. To use the JSON output with the default character mode, skip the first suffix with a double colon (e.g., `makepatches::json`).
The difference algorithm operates in character mode by default. This produces the most detailed diff possible. In `words` mode, each word in the input text is transformed into a meta-character, upon which the algorithm then operates. In the default character mode, the filter would find two patches between "ActionWidget" and "Action-Widgets" (the hyphen and the plural s), while in `words` mode, the whole word is found to be changed. In `lines` mode, the meta-character is formed from the whole line, delimited by newline characters, and is found to be changed independent of the number of changes within the line.
The different modes influence the result when the patches are applied to texts other than the original, as well as the runtime.
<<.tip "The calculation in `words` mode is roughly 10 times faster than the default character mode, while `lines` mode can be more than 100 times faster than the default.">>
<<.operator-examples "makepatches and applypatches">>
<<.operator-examples "makepatches and applypatches">>
@@ -1,12 +1,11 @@
caption: sortby
created: 20151017145021839
modified: 20260811120000000
modified: 20151108052142057
op-input: a list of items
op-output: all items sorted by the order list, with unlisted items placed first, or last with the `end` suffix
op-output: all items sorted by lookup list
op-parameter: a list specifying the order in which to sort the current list
op-parameter-name: order
op-purpose: sort the current list in the order of the list referenced in the parameter
op-suffix: <<.from-version "5.5.0">> optional: `end` to place items missing from <<.place order>> after the listed ones. If omitted they are placed first
tags: [[Filter Operators]] [[Order Operators]] [[Listops Operators]]
title: sortby Operator
type: text/vnd.tiddlywiki
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 47 KiB

@@ -1,11 +1,10 @@
title: $:/changenotes/5.4.0/#9107
description: Update configuration defaults
description: Update default sidebar layout
release: 5.4.0
tags: $:/tags/ChangeNote
change-type: enhancement
change-category: usability
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9107
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9107 https://github.com/TiddlyWiki/TiddlyWiki5/issues/9757
github-contributors: Jermolene
* Changed default sidebar layout from ''fixed-fluid'' to ''fluid-fixed''
* Changed ''Wrap long lines in code blocks'' default to ''No''
Changed default sidebar layout from ''fixed-fluid'' to ''fluid-fixed''
@@ -1,8 +0,0 @@
title: $:/changenotes/5.4.0/#9715
change-type: performance
change-category: filters
tags: $:/tags/ChangeNote
github-contributors: linonetwo
release: 5.4.0
description: Optimized tag[] and !tag[] filter operators to use Set for O(1) lookup, matching search:tags[] performance.
github-links: [[https://github.com/Jermolene/TiddlyWiki5/pull/9715]]
@@ -1,12 +0,0 @@
change-category: widget
change-type: enhancement
created: 20260711020921000
description: The diff-text widget shows removed or added newlines with a visible ↲ glyph
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9736
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9736
type: text/vnd.tiddlywiki
* The diff-text widget marks newline characters in a diff with the visible `↲` glyph (U+21B2) instead of an invisible character, so line break changes are recognisable in the rendered diff (fixes [[Issue #9461|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9461]])
@@ -1,12 +0,0 @@
change-category: translation
change-type: enhancement
created: 20260711020729000
description: The "Click to generate wiki info" button in the control panel is now translatable
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9737
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9737
type: text/vnd.tiddlywiki
* The "Click to generate wiki info" button on the control panel's wiki information tab uses a language string instead of hard coded English text, so translators can localise it (fixes [[Issue #9654|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9654]])
@@ -1,13 +0,0 @@
change-category: hackability
change-type: bugfix
created: 20260711181412000
description: Empty code blocks parse cleanly; a closing fence only counts alone on its line
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9739
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9739
type: text/vnd.tiddlywiki
* An empty code block such as ```` ```bash ```` directly followed by the closing fence parses as an empty codeblock instead of swallowing the rest of the tiddler (fixes [[Issue #9047|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9047]])
* A closing fence only ends the block when it stands alone on its line, so code lines ending in three backticks stay inside the block
@@ -1,12 +0,0 @@
change-category: hackability
change-type: bugfix
created: 20260711185128000
description: $tw.utils.pulseElement() works again and the vanilla theme defines the tc-pulse class
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9741
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9741
type: text/vnd.tiddlywiki
* `$tw.utils.pulseElement()` listens for the standard `animationend` event instead of the removed `$tw.browser.animationEnd`, so the pulse animation runs and cleans up its CSS class again, and the vanilla theme defines the missing `tc-pulse` class and animation (fixes [[Issue #6835|https://github.com/TiddlyWiki/TiddlyWiki5/issues/6835]])
@@ -1,12 +0,0 @@
change-category: nodejs
change-type: bugfix
created: 20260711020057000
description: The Node.js server no longer emits the DEP0169 url.parse() deprecation warning
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9742
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9742
type: text/vnd.tiddlywiki
* The server module parses request URLs with the WHATWG `URL` API instead of the deprecated `url.parse()`, so recent Node.js versions no longer print the DEP0169 deprecation warning on startup (fixes [[Issue #9628|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9628]])
@@ -1,12 +0,0 @@
change-category: filters
change-type: enhancement
created: 20260711015656000
description: sortby accepts an optional end suffix to place titles missing from the reference list after the listed ones
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9747
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9747
type: text/vnd.tiddlywiki
* The [[sortby|sortby Operator]] filter operator now accepts an optional `end` suffix that places input titles missing from the reference list after the listed ones. The default is unchanged, so with a list `Alpha Beta Gamma` the input `Delta Epsilon Alpha Beta Gamma` still sorts to `Delta Epsilon Alpha Beta Gamma`, while `sortby:end[...]` sorts it to `Alpha Beta Gamma Delta Epsilon` (addresses [[Issue #8342|https://github.com/TiddlyWiki/TiddlyWiki5/issues/8342]])
@@ -1,14 +0,0 @@
change-category: nodejs
change-type: bugfix
created: 20260711175727000
description: Keep retained subdirectory tiddlers and sanitise file names per path segment
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9815 https://github.com/TiddlyWiki/TiddlyWiki5/pull/9944
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9815
type: text/vnd.tiddlywiki
* A tiddler saved with `retain-original-tiddler-path` keeps its recorded subdirectory instead of being flattened to the tiddlers root on Windows
* File names are sanitised per path segment, so a forbidden character or reserved device name is replaced at every level (for example `sub/CON`), and a title containing a backslash such as `pragma: \define` no longer acts as a Windows path separator (fixes [[Issue #9814|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9814]])
* Tiddler files declared in `tiddlywiki.files` are pinned to their original location and skip the `$:/config/FileSystemPaths` filters and forbidden-character sanitising
@@ -1,10 +0,0 @@
title: $:/changenotes/5.5.0/#9816
description: Replaces some wikify widget with call dynamic syntax
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: enhancement
change-category: internal
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9816
github-contributors: Leilei332
Replaces unnecessary wikify widget usage with call dynamic attribute syntax.
@@ -1,13 +0,0 @@
change-category: internal
change-type: bugfix
created: 20260711014241000
description: fakedom tagName is uppercase for HTML elements, matching the DOM spec
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9843
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9843
type: text/vnd.tiddlywiki
* `$tw.fakeDocument` elements now report `tagName` in uppercase for HTML elements, matching the DOM specification and real browsers; code that compares fakedom `tagName` against lowercase strings must be updated
* Creating an element with an empty or null namespace now produces a plain element without a namespace
@@ -1,10 +0,0 @@
title: $:/changenotes/5.5.0/#9879
description: Fix field-name inputs re-render
tags: $:/tags/ChangeNote
release: 5.5.0
change-type: bugfix
change-category: usability
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9879
github-contributors: BurningTreeC
Fixes the field-name input re-rendering and therefor loosing focus when inserting a field-name that already exists
@@ -1,10 +0,0 @@
title: $:/changenotes/5.4.1/#9889
description: Make tags in Edit Mode animate instantly when removing
tags: $:/tags/ChangeNote
release: 5.5.0
change-type: bugfix
change-category: usability
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9889
github-contributors: BurningTreeC
Fixes an issue where tags of Tiddlers in Edit Mode don't animate instantly when they get removed
@@ -1,10 +0,0 @@
title: $:/changenotes/5.5.0/#9891
description: Move background action log inside platforms check to avoid spurious server-side logs
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: bugfix
change-category: internal
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9891
github-contributors: linonetwo
Background actions with `platforms: browser` were logging on the server even though execution was correctly skipped. The log now only prints when the action actually runs.
@@ -1,12 +0,0 @@
change-category: filters
change-type: bugfix
created: 20260711011920000
description: backlinks[] and backtranscludes[] no longer report edited system tiddlers as sources
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9917
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9917
type: text/vnd.tiddlywiki
* Editing a system tiddler that links or transcludes another tiddler no longer adds it as a [[backlinks|backlinks Operator]] or [[backtranscludes|backtranscludes Operator]] source; the incremental back-index update now skips system tiddlers like the initial scan always did
@@ -1,13 +0,0 @@
title: $:/changenotes/5.5.0/#9919
created: 20260712000000000
modified: 20260712000000000
description: Fix the CSV parser returning every cell empty
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: bugfix
change-category: internal
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9919
github-contributors: joshuafontany
type: text/vnd.tiddlywiki
`getCellInfo` read its loop variable before the loop declared it, so hoisting made it undefined and the parser tested the first character of the whole text rather than the first character of the cell. Any CSV opening with a separator therefore returned every cell empty, and the table rendered blank.
@@ -1,10 +0,0 @@
title: $:/changenotes/5.5.0/#9933
description: Update Chinese translations
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: enhancement
change-category: translation
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9933
github-contributors: BramChen
* Add `WikiInformation/Generate/Caption` in `ControlPanel.multids`
@@ -1,12 +0,0 @@
change-category: filters
change-type: enhancement
created: 20260724080519370
description: The makepatches operator can now output structured JSON
github-contributors: DesignThinkerer
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9940
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9940
type: text/vnd.tiddlywiki
* The [[makepatches|makepatches Operator]] operator now supports a `:json` suffix (e.g., `makepatches::json`) that outputs a structured JSON array of the differences, enabling easier and robust parsing with WikiText
@@ -1,13 +0,0 @@
change-category: filters
change-type: enhancement
created: 20260726124618748
description: Optimizes listops filter operators
github-contributors: saqimtiaz
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9942
modified: 20260726125232992
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9942
type: text/vnd.tiddlywiki
* The listops filter operators have been rewritten using more modern JavaScript (ES2017). The `unique` and `remove` operators have improved performance in certain circumstances.
@@ -1,13 +0,0 @@
change-category: filters
change-type: enhancement
created: 20260726124618748
description: Optimizes filterrun prefixes and select filters using modern JavaScript.
github-contributors: saqimtiaz
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9943
modified: 20260726130207664
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9943
type: text/vnd.tiddlywiki
* The `intersection` and `sort` filterrun prefixes, and the `prefix`, `sortsub`, `subfilter` and `suffix` operators have been optimized using more modern JavaScript (ES2017).
@@ -1,26 +0,0 @@
change-category: widget
change-type: bugfix
created: 20260727171657000
description: The button widget applies selectedClass when the state is addressed with setTitle
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9951
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9951
type: text/vnd.tiddlywiki
* Buttons sharing a state tiddler through `setTitle` highlight the selected one. Only the button whose `setTo` matches the state gets the `selectedClass`
* The highlight follows the state as it changes
* `default` picks the highlighted button while the state tiddler is still missing
* The state is the tiddler text. Use `setField` or `setIndex` to address a field or an index instead
```
<div class="tc-tab-buttons">
<$button setTitle="$:/state/tab" setTo="Alpha" default="Alpha" selectedClass="tc-tab-selected">Alpha</$button>
<$button setTitle="$:/state/tab" setTo="Beta" default="Alpha" selectedClass="tc-tab-selected">Beta</$button>
</div>
```
The ButtonWidget documentation gained two missing rules. `setField` takes preference over `setIndex`. `selectedClass` and `default` apply to `setTitle`, not only to `set`.
Fixed issues: [[#9949|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9949]] and [[#9950|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9950]]

Some files were not shown because too many files have changed in this diff Show More