mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-08-23 19:28:54 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6680967875 | ||
|
|
f9d5ee0ad7 | ||
|
|
68630c939d | ||
|
|
7efbd01801 | ||
|
|
5143138c16 | ||
|
|
ceb0817176 |
+1
-1
@@ -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]"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
|
||||
@@ -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("")];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -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 || "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,7 +22,7 @@ Inherit from the base widget class
|
||||
DiffTextWidget.prototype = new Widget();
|
||||
|
||||
DiffTextWidget.prototype.invisibleCharacters = {
|
||||
"\n": "↲\n",
|
||||
"\n": "↩︎\n",
|
||||
"\r": "⇠",
|
||||
"\t": "⇥\t"
|
||||
};
|
||||
|
||||
@@ -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]] }}};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -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
-9
@@ -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>
|
||||
-1
@@ -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">>
|
||||
|
||||
+2
-3
@@ -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 |
+3
-4
@@ -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.
|
||||
-12
@@ -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
Reference in New Issue
Block a user