mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-09-17 15:21:22 +00:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c599e64c0 | ||
|
|
2b60da0dcf | ||
|
|
5d3f2685a3 | ||
|
|
3aec096a07 | ||
|
|
ddc59ee77b | ||
|
|
e0e6ab69b1 | ||
|
|
47806feb85 | ||
|
|
5577a3d07b | ||
|
|
d62b75cceb | ||
|
|
30a51be168 | ||
|
|
38fa4486e4 | ||
|
|
52c60be3a0 | ||
|
|
516135e1b2 | ||
|
|
6845f1c47e | ||
|
|
5e5937838b | ||
|
|
6c8ac9b6a7 | ||
|
|
0c60e1fa7a | ||
|
|
b40b6de3ae | ||
|
|
aac08a6252 | ||
|
|
40b353bd0e | ||
|
|
8e65fdc06a | ||
|
|
2f578589a0 | ||
|
|
916df90cc9 | ||
|
|
ea18c85a4c | ||
|
|
abf134afe4 | ||
|
|
45d2ca948a | ||
|
|
94b046b8cb | ||
|
|
c710a56b21 | ||
|
|
e681096127 | ||
|
|
24b433211d | ||
|
|
abf8ffdbca | ||
|
|
97e27d0dad | ||
|
|
855dc2fbb9 | ||
|
|
e1e34f4907 | ||
|
|
7196b9ee9b | ||
|
|
ed71c8262a | ||
|
|
7516d96eed | ||
|
|
4bf7e9d192 | ||
|
|
04cd3081e4 | ||
|
|
d27716c1e0 | ||
|
|
7f7f36d986 | ||
|
|
86a4e18134 | ||
|
|
304858c7c5 | ||
|
|
7ececf9e0f | ||
|
|
521e530e11 | ||
|
|
4c29dae4af | ||
|
|
f5317dc225 | ||
|
|
c952450c2e | ||
|
|
1eb7ec4402 | ||
|
|
869557f7d1 | ||
|
|
c9f1154643 |
+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.4.1.
|
||||
TW5_BUILD_VERSION=v5.5.0.
|
||||
fi
|
||||
|
||||
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
|
||||
|
||||
@@ -217,6 +217,11 @@ 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) {
|
||||
@@ -319,9 +324,16 @@ 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(options.pathFilters && options.wiki) {
|
||||
if(!filepath && options.pathFilters && options.wiki) {
|
||||
$tw.utils.each(options.pathFilters,function(filter) {
|
||||
if(!filepath) {
|
||||
var source = options.wiki.makeTiddlerIterator([title]),
|
||||
@@ -336,13 +348,14 @@ 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
|
||||
@@ -352,8 +365,23 @@ 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
|
||||
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/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 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,15 +9,14 @@ Serve tiddlers over http
|
||||
|
||||
"use strict";
|
||||
|
||||
let fs, url, path, querystring, crypto, zlib;
|
||||
let fs, path, crypto, zlib, URL;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -260,8 +259,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 = url.parse(request.url);
|
||||
state.queryParameters = querystring.parse(state.urlInfo.query);
|
||||
state.urlInfo = new URL(request.url, "http://localhost");
|
||||
state.queryParameters = Object.fromEntries(state.urlInfo.searchParams);
|
||||
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
|
||||
// Enable CORS
|
||||
if(this.corsEnable) {
|
||||
|
||||
@@ -255,4 +255,5 @@ 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/Drag/Caption: Drag this link to copy this tool to another wiki
|
||||
WikiInformation/Generate/Caption: Click to generate wiki information report
|
||||
@@ -78,7 +78,6 @@ 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) {
|
||||
@@ -89,6 +88,7 @@ class BackgroundActionTracker {
|
||||
}
|
||||
}
|
||||
if(doActions) {
|
||||
console.log("Processing background action", this.title);
|
||||
this.wiki.invokeActionString(
|
||||
this.actions,
|
||||
null,
|
||||
|
||||
@@ -210,6 +210,29 @@ 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
|
||||
@@ -217,7 +240,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["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) {
|
||||
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) {
|
||||
this.refreshSelf();
|
||||
return true;
|
||||
} else if(changedTiddlers[this.editRefreshTitle]) {
|
||||
@@ -226,6 +249,9 @@ 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,14 +13,15 @@ Export our filter prefix function
|
||||
exports.intersection = function(operationSubFunction) {
|
||||
return function(results,source,widget) {
|
||||
if(results.length !== 0) {
|
||||
var secondRunResults = operationSubFunction(source,widget);
|
||||
var firstRunResults = results.toArray();
|
||||
const secondRunResults = operationSubFunction(source,widget),
|
||||
secondRunSet = new Set(secondRunResults),
|
||||
firstRunResults = results.toArray();
|
||||
results.clear();
|
||||
$tw.utils.each(firstRunResults,function(title) {
|
||||
if(secondRunResults.indexOf(title) !== -1) {
|
||||
firstRunResults.forEach((title) => {
|
||||
if(secondRunSet.has(title)) {
|
||||
results.push(title);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -13,33 +13,28 @@ Export our filter prefix function
|
||||
exports.sort = function(operationSubFunction,options) {
|
||||
return function(results,source,widget) {
|
||||
if(results.length > 0) {
|
||||
var suffixes = options.suffixes,
|
||||
const suffixes = options.suffixes,
|
||||
sortType = (suffixes[0] && suffixes[0][0]) ? suffixes[0][0] : "string",
|
||||
invert = suffixes[1] ? (suffixes[1].indexOf("reverse") !== -1) : false,
|
||||
isCaseSensitive = suffixes[1] ? (suffixes[1].indexOf("casesensitive") !== -1) : false,
|
||||
invert = suffixes[1] ? suffixes[1].includes("reverse") : false,
|
||||
isCaseSensitive = suffixes[1] ? suffixes[1].includes("casesensitive") : false,
|
||||
inputTitles = results.toArray(),
|
||||
sortKeys = [],
|
||||
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:""})
|
||||
}));
|
||||
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:""})
|
||||
})
|
||||
);
|
||||
sortKeys.push(key[0] || "");
|
||||
});
|
||||
results.clear();
|
||||
// Prepare an array of indexes to sort
|
||||
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) {
|
||||
let indexes = Array.from(inputTitles.keys());
|
||||
indexes.sort((a,b) => compareFn(sortKeys[a],sortKeys[b]));
|
||||
indexes.forEach((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 = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source}) || [];
|
||||
operand.multiValue = resultList || [];
|
||||
operand.value = operand.multiValue[0] || "";
|
||||
} else {
|
||||
operand.value = "";
|
||||
|
||||
@@ -9,69 +9,56 @@ 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) {
|
||||
var results = [];
|
||||
if(operator.operand.toLowerCase() === "reverse") {
|
||||
source(function(tiddler,title) {
|
||||
results.unshift(title);
|
||||
});
|
||||
} else {
|
||||
source(function(tiddler,title) {
|
||||
results.push(title);
|
||||
});
|
||||
}
|
||||
return results;
|
||||
const results = prepare_results(source);
|
||||
return operator.operand.toLowerCase() === "reverse" ?
|
||||
results.reverse() :
|
||||
results;
|
||||
};
|
||||
|
||||
/*
|
||||
Reverse list
|
||||
*/
|
||||
exports.reverse = function(source,operator,options) {
|
||||
var results = [];
|
||||
source(function(tiddler,title) {
|
||||
results.unshift(title);
|
||||
});
|
||||
return results;
|
||||
return prepare_results(source).reverse();
|
||||
};
|
||||
|
||||
/*
|
||||
First entry/entries in list
|
||||
*/
|
||||
exports.first = function(source,operator,options) {
|
||||
var count = $tw.utils.getInt(operator.operand,1),
|
||||
results = [];
|
||||
source(function(tiddler,title) {
|
||||
results.push(title);
|
||||
});
|
||||
return results.slice(0,count);
|
||||
const count = $tw.utils.getInt(operator.operand,1);
|
||||
return prepare_results(source).slice(0,count);
|
||||
};
|
||||
|
||||
/*
|
||||
Last entry/entries in list
|
||||
*/
|
||||
exports.last = function(source,operator,options) {
|
||||
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);
|
||||
const count = $tw.utils.getInt(operator.operand,1);
|
||||
return count === 0 ?
|
||||
[] :
|
||||
prepare_results(source).slice(-count);
|
||||
};
|
||||
|
||||
/*
|
||||
All but the first entry/entries of the list
|
||||
*/
|
||||
exports.rest = function(source,operator,options) {
|
||||
var count = $tw.utils.getInt(operator.operand,1),
|
||||
results = [];
|
||||
source(function(tiddler,title) {
|
||||
results.push(title);
|
||||
});
|
||||
return results.slice(count);
|
||||
const count = $tw.utils.getInt(operator.operand,1);
|
||||
return prepare_results(source).slice(count);
|
||||
};
|
||||
exports.butfirst = exports.rest;
|
||||
exports.bf = exports.rest;
|
||||
@@ -80,12 +67,9 @@ exports.bf = exports.rest;
|
||||
All but the last entry/entries of the list
|
||||
*/
|
||||
exports.butlast = function(source,operator,options) {
|
||||
var count = $tw.utils.getInt(operator.operand,1),
|
||||
results = [];
|
||||
source(function(tiddler,title) {
|
||||
results.push(title);
|
||||
});
|
||||
var index = count === 0 ? results.length : -count;
|
||||
const count = $tw.utils.getInt(operator.operand,1),
|
||||
results = prepare_results(source),
|
||||
index = count === 0 ? results.length : -count;
|
||||
return results.slice(0,index);
|
||||
};
|
||||
exports.bl = exports.butlast;
|
||||
@@ -94,22 +78,14 @@ exports.bl = exports.butlast;
|
||||
The nth member of the list
|
||||
*/
|
||||
exports.nth = function(source,operator,options) {
|
||||
var count = $tw.utils.getInt(operator.operand,1),
|
||||
results = [];
|
||||
source(function(tiddler,title) {
|
||||
results.push(title);
|
||||
});
|
||||
return results.slice(count - 1,count);
|
||||
const count = $tw.utils.getInt(operator.operand,1);
|
||||
return prepare_results(source).slice(count - 1,count);
|
||||
};
|
||||
|
||||
/*
|
||||
The zero based nth member of the list
|
||||
*/
|
||||
exports.zth = function(source,operator,options) {
|
||||
var count = $tw.utils.getInt(operator.operand,0),
|
||||
results = [];
|
||||
source(function(tiddler,title) {
|
||||
results.push(title);
|
||||
});
|
||||
return results.slice(count,count + 1);
|
||||
};
|
||||
const count = $tw.utils.getInt(operator.operand,0);
|
||||
return prepare_results(source).slice(count,count + 1);
|
||||
};
|
||||
@@ -13,37 +13,22 @@ Filter operator for checking if a title starts with a prefix
|
||||
Export our filter function
|
||||
*/
|
||||
exports.prefix = function(source,operator,options) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
} 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
|
||||
var filterFn = options.wiki.compileFilter(operator.operand);
|
||||
let filterFn = options.wiki.compileFilter(operator.operand);
|
||||
// Collect the input titles and the corresponding sort keys
|
||||
var inputTitles = [],
|
||||
let inputTitles = [],
|
||||
sortKeys = [];
|
||||
source(function(tiddler,title) {
|
||||
inputTitles.push(title);
|
||||
var r = filterFn.call(options.wiki,function(iterator) {
|
||||
let r = filterFn.call(options.wiki,function(iterator) {
|
||||
iterator(options.wiki.getTiddler(title),title);
|
||||
},options.widget.makeFakeWidgetWithVariables({
|
||||
"currentTiddler": "" + title,
|
||||
@@ -29,17 +29,12 @@ 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
|
||||
var indexes = new Array(inputTitles.length);
|
||||
for(var t=0; t<inputTitles.length; t++) {
|
||||
indexes[t] = t;
|
||||
}
|
||||
let indexes = Array.from(inputTitles.keys());
|
||||
// Sort the indexes
|
||||
var compareFn = $tw.utils.makeCompareFunction(operator.suffix,{defaultType: "string",invert: operator.prefix === "!"});
|
||||
indexes = indexes.sort(function(a,b) {
|
||||
return compareFn(sortKeys[a],sortKeys[b]);
|
||||
});
|
||||
let compareFn = $tw.utils.makeCompareFunction(operator.suffix,{defaultType: "string",invert: operator.prefix === "!"});
|
||||
indexes = indexes.sort((a,b) => compareFn(sortKeys[a],sortKeys[b]));
|
||||
// Make the results array in order
|
||||
var results = [];
|
||||
let results = [];
|
||||
$tw.utils.each(indexes,function(index) {
|
||||
results.push(inputTitles[index]);
|
||||
});
|
||||
|
||||
@@ -91,22 +91,35 @@ function diffLineWordMode(text1,text2,mode) {
|
||||
return diffs;
|
||||
}
|
||||
|
||||
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);
|
||||
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));
|
||||
} else {
|
||||
patches = dmp.patchMake(title,operator.operand);
|
||||
const patches = (mode === "lines" || mode === "words")
|
||||
? dmp.patchMake(title, diffLineWordMode(title, operator.operand, mode))
|
||||
: dmp.patchMake(title, operator.operand);
|
||||
|
||||
results.push(dmp.patchToText(patches));
|
||||
}
|
||||
Array.prototype.push.apply(result,[dmp.patchToText(patches)]);
|
||||
});
|
||||
|
||||
return result;
|
||||
return results;
|
||||
};
|
||||
|
||||
exports.applypatches = makeStringBinaryOperator(
|
||||
@@ -235,4 +248,4 @@ exports.charcode = function(source,operator,options) {
|
||||
}
|
||||
});
|
||||
return [chars.join("")];
|
||||
};
|
||||
};
|
||||
@@ -13,11 +13,12 @@ Filter operator returning its operand evaluated as a filter
|
||||
Export our filter function
|
||||
*/
|
||||
exports.subfilter = function(source,operator,options) {
|
||||
var list = options.wiki.filterTiddlers(operator.operand,options.widget,source);
|
||||
const list = options.wiki.filterTiddlers(operator.operand,options.widget,source);
|
||||
if(operator.prefix === "!") {
|
||||
var results = [];
|
||||
source(function(tiddler,title) {
|
||||
if(list.indexOf(title) === -1) {
|
||||
const results = [],
|
||||
listSet = new Set(list);
|
||||
source((tiddler,title) => {
|
||||
if(!listSet.has(title)) {
|
||||
results.push(title);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,41 +13,27 @@ Filter operator for checking if a title ends with a suffix
|
||||
Export our filter function
|
||||
*/
|
||||
exports.suffix = function(source,operator,options) {
|
||||
var results = [],
|
||||
const results = [],
|
||||
suffixes = (operator.suffixes || [])[0] || [];
|
||||
|
||||
if(!operator.operand) {
|
||||
source(function(tiddler,title) {
|
||||
source((tiddler,title) => {
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
@@ -22,12 +22,11 @@ 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
|
||||
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
|
||||
const excludeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
|
||||
source(function(tiddler,title) {
|
||||
if(tiddlers.indexOf(title) === -1) {
|
||||
if(!excludeTagSet.has(title)) {
|
||||
results.push(title);
|
||||
}
|
||||
});
|
||||
@@ -39,9 +38,9 @@ exports.tag = function(source,operator,options) {
|
||||
return indexedResults;
|
||||
}
|
||||
} else {
|
||||
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
|
||||
const includeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
|
||||
source(function(tiddler,title) {
|
||||
if(tiddlers.indexOf(title) !== -1) {
|
||||
if(includeTagSet.has(title)) {
|
||||
results.push(title);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,20 +11,18 @@ Extended filter operators to manipulate the current list.
|
||||
|
||||
/*
|
||||
Fetch titles from the current list
|
||||
*/
|
||||
var prepare_results = function (source) {
|
||||
var results = [];
|
||||
source(function (tiddler, title) {
|
||||
results.push(title);
|
||||
});
|
||||
*/
|
||||
const prepare_results = (source) => {
|
||||
const results = [];
|
||||
source((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) {
|
||||
var results = prepare_results(source),
|
||||
*/
|
||||
exports.putbefore = function(source,operator) {
|
||||
const results = prepare_results(source),
|
||||
index = results.indexOf(operator.operand),
|
||||
count = $tw.utils.getInt(operator.suffix,1);
|
||||
return (index === -1) ?
|
||||
@@ -34,9 +32,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) {
|
||||
var results = prepare_results(source),
|
||||
*/
|
||||
exports.putafter = function(source,operator) {
|
||||
const results = prepare_results(source),
|
||||
index = results.indexOf(operator.operand),
|
||||
count = $tw.utils.getInt(operator.suffix,1);
|
||||
return (index === -1) ?
|
||||
@@ -46,9 +44,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) {
|
||||
var results = prepare_results(source),
|
||||
*/
|
||||
exports.replace = function(source,operator) {
|
||||
const results = prepare_results(source),
|
||||
index = results.indexOf(operator.operand),
|
||||
count = $tw.utils.getInt(operator.suffix,1);
|
||||
return (index === -1) ?
|
||||
@@ -58,39 +56,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) {
|
||||
var results = prepare_results(source),
|
||||
*/
|
||||
exports.putfirst = function(source,operator) {
|
||||
const results = prepare_results(source),
|
||||
count = $tw.utils.getInt(operator.suffix,1);
|
||||
return results.slice(-count).concat(results.slice(0, -count));
|
||||
return [...results.slice(-count), ...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) {
|
||||
var results = prepare_results(source),
|
||||
*/
|
||||
exports.putlast = function(source,operator) {
|
||||
const results = prepare_results(source),
|
||||
count = $tw.utils.getInt(operator.suffix,1);
|
||||
return results.slice(count).concat(results.slice(0, count));
|
||||
return [...results.slice(count), ...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) {
|
||||
var results = prepare_results(source),
|
||||
*/
|
||||
exports.move = function(source,operator) {
|
||||
const 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) {
|
||||
var results = prepare_results(source),
|
||||
*/
|
||||
exports.allafter = function(source,operator) {
|
||||
const results = prepare_results(source),
|
||||
index = results.indexOf(operator.operand);
|
||||
return (index === -1) ? [] :
|
||||
(operator.suffix) ? results.slice(index) :
|
||||
@@ -99,9 +97,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) {
|
||||
var results = prepare_results(source),
|
||||
*/
|
||||
exports.allbefore = function(source,operator) {
|
||||
const results = prepare_results(source),
|
||||
index = results.indexOf(operator.operand);
|
||||
return (index === -1) ? [] :
|
||||
(operator.suffix) ? results.slice(0, index + 1) :
|
||||
@@ -110,9 +108,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) {
|
||||
var append = $tw.utils.parseStringArray(operator.operand, "true"),
|
||||
*/
|
||||
exports.append = function(source,operator) {
|
||||
const append = $tw.utils.parseStringArray(operator.operand,"true"),
|
||||
results = prepare_results(source),
|
||||
count = parseInt(operator.suffix) || append.length;
|
||||
return (append.length === 0) ? results :
|
||||
@@ -122,9 +120,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) {
|
||||
var prepend = $tw.utils.parseStringArray(operator.operand, "true"),
|
||||
*/
|
||||
exports.prepend = function(source,operator) {
|
||||
const prepend = $tw.utils.parseStringArray(operator.operand,"true"),
|
||||
results = prepare_results(source),
|
||||
count = $tw.utils.getInt(operator.suffix,prepend.length);
|
||||
return (prepend.length === 0) ? results :
|
||||
@@ -134,21 +132,19 @@ 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) {
|
||||
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]);
|
||||
}
|
||||
*/
|
||||
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);
|
||||
if(index !== -1) {
|
||||
results.splice(index, 1);
|
||||
}
|
||||
@@ -158,40 +154,38 @@ 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) {
|
||||
var results = prepare_results(source);
|
||||
*/
|
||||
exports.sortby = function(source,operator) {
|
||||
const results = prepare_results(source);
|
||||
if(!results || results.length < 2) {
|
||||
return results;
|
||||
}
|
||||
var lookup = $tw.utils.parseStringArray(operator.operand, "true");
|
||||
results.sort(function (a, b) {
|
||||
return lookup.indexOf(a) - lookup.indexOf(b);
|
||||
});
|
||||
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));
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Removes all duplicate items from the current list
|
||||
*/
|
||||
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;
|
||||
*/
|
||||
exports.unique = function(source, operator) {
|
||||
return Array.from(new Set(prepare_results(source)));
|
||||
};
|
||||
|
||||
var cycleValueInArray = function(results,operands,stepSize) {
|
||||
var resultsIndex,
|
||||
const cycleValueInArray = function(results,operands,stepSize) {
|
||||
let resultsIndex,
|
||||
step = stepSize || 1,
|
||||
i = 0,
|
||||
opLength = operands.length,
|
||||
nextOperandIndex;
|
||||
for(i; i < opLength; i++) {
|
||||
const opLength = operands.length;
|
||||
|
||||
for(; i < opLength; i++) {
|
||||
resultsIndex = results.indexOf(operands[i]);
|
||||
if(resultsIndex !== -1) {
|
||||
break;
|
||||
@@ -213,18 +207,19 @@ var 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) {
|
||||
var results = prepare_results(source),
|
||||
operands = (operator.operand.length ? $tw.utils.parseStringArray(operator.operand, "true") : [""]),
|
||||
step = $tw.utils.getInt(operator.operands[1]||"",1);
|
||||
const results = prepare_results(source),
|
||||
operands = operator.operand.length ? $tw.utils.parseStringArray(operator.operand,"true") : [""];
|
||||
let step = $tw.utils.getInt(operator.operands[1] || "",1);
|
||||
|
||||
if(step < 0) {
|
||||
operands.reverse();
|
||||
step = Math.abs(step);
|
||||
}
|
||||
return cycleValueInArray(results,operands,step);
|
||||
};
|
||||
};
|
||||
@@ -88,10 +88,11 @@ BackSubIndexer.prototype.update = function(updateDescriptor) {
|
||||
var newTargets = [],
|
||||
oldTargets = [],
|
||||
self = this;
|
||||
if(updateDescriptor.old.exists) {
|
||||
// System tiddlers are never indexed as sources, matching the _init() scan
|
||||
if(updateDescriptor.old.exists && !this.wiki.isSystemTiddler(updateDescriptor.old.tiddler.fields.title)) {
|
||||
oldTargets = this._getTarget(updateDescriptor.old.tiddler);
|
||||
}
|
||||
if(updateDescriptor.new.exists) {
|
||||
if(updateDescriptor.new.exists && !this.wiki.isSystemTiddler(updateDescriptor.new.tiddler.fields.title)) {
|
||||
newTargets = this._getTarget(updateDescriptor.new.tiddler);
|
||||
}
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ exports.parseMacroInvocationAsTransclusion = function(source,pos) {
|
||||
orderedAttributes: []
|
||||
};
|
||||
// Define our regexps
|
||||
var reVarName = /([^\s>"'=:]+)/y;
|
||||
var reVarName = /([^\s>"'=]+)/y;
|
||||
// Skip whitespace
|
||||
pos = $tw.utils.skipWhiteSpace(source,pos);
|
||||
// Look for a double opening angle bracket
|
||||
@@ -269,7 +269,7 @@ exports.parseMVVReferenceAsTransclusion = function(source,pos) {
|
||||
orderedAttributes: []
|
||||
};
|
||||
// Define our regexps
|
||||
var reVarName = /([^\s>"'=:)]+)/y;
|
||||
var reVarName = /([^\s>"'=)]+)/y;
|
||||
// Skip whitespace
|
||||
pos = $tw.utils.skipWhiteSpace(source,pos);
|
||||
// Look for a double opening parenthesis
|
||||
@@ -528,67 +528,79 @@ exports.parseAttribute = function(source,pos) {
|
||||
pos = token.end;
|
||||
// Skip whitespace
|
||||
pos = $tw.utils.skipWhiteSpace(source,pos);
|
||||
// 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 {
|
||||
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 filtered value
|
||||
var filteredValue = $tw.utils.parseTokenRegExp(source,pos,reFilteredValue);
|
||||
if(filteredValue) {
|
||||
pos = filteredValue.end;
|
||||
node.type = "filtered";
|
||||
node.filter = filteredValue.match[1];
|
||||
} 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -4,7 +4,6 @@ type: application/javascript
|
||||
module-type: startup
|
||||
|
||||
Browser message handling
|
||||
|
||||
\*/
|
||||
|
||||
"use strict";
|
||||
@@ -18,6 +17,20 @@ exports.synchronous = true;
|
||||
/*
|
||||
Load a specified url as an iframe and call the callback when it is loaded. If the url is already loaded then the existing iframe instance is used
|
||||
*/
|
||||
|
||||
// Ensure the callback fires once, whether by PLUGIN-LIBRARY-READY or onload (legacy)
|
||||
|
||||
function flushCallbacks(iframeInfo, err) {
|
||||
if(iframeInfo.status !== "loaded") {
|
||||
iframeInfo.status = err ? "error" : "loaded";
|
||||
saveIFrameInfoTiddler(iframeInfo);
|
||||
var cb;
|
||||
while((cb = iframeInfo.callbacks.shift())) {
|
||||
cb(err, iframeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadIFrame(url,callback) {
|
||||
// Check if iframe already exists
|
||||
var iframeInfo = $tw.browserMessaging.iframeInfoMap[url];
|
||||
@@ -30,7 +43,8 @@ function loadIFrame(url,callback) {
|
||||
iframeInfo = {
|
||||
url: url,
|
||||
status: "loading",
|
||||
domNode: iframe
|
||||
domNode: iframe,
|
||||
callbacks: [callback]
|
||||
};
|
||||
$tw.browserMessaging.iframeInfoMap[url] = iframeInfo;
|
||||
saveIFrameInfoTiddler(iframeInfo);
|
||||
@@ -38,19 +52,18 @@ function loadIFrame(url,callback) {
|
||||
iframe.style.display = "none";
|
||||
iframe.setAttribute("library","true");
|
||||
document.body.appendChild(iframe);
|
||||
// Set up onload
|
||||
|
||||
// Set up onload. Legacy fallback: if PLUGIN-LIBRARY-READY never arrives, onload triggers the flush later
|
||||
iframe.onload = function() {
|
||||
iframeInfo.status = "loaded";
|
||||
saveIFrameInfoTiddler(iframeInfo);
|
||||
callback(null,iframeInfo);
|
||||
flushCallbacks(iframeInfo);
|
||||
};
|
||||
iframe.onerror = function() {
|
||||
callback("Cannot load iframe");
|
||||
flushCallbacks(iframeInfo, "Cannot load iframe");
|
||||
};
|
||||
try {
|
||||
iframe.src = url;
|
||||
} catch(ex) {
|
||||
callback(ex);
|
||||
flushCallbacks(iframeInfo, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +73,7 @@ Unload library iframe for given url
|
||||
*/
|
||||
function unloadIFrame(url){
|
||||
var iframes = document.getElementsByTagName("iframe");
|
||||
for(var t=iframes.length-1; t--; t>=0) {
|
||||
for(var t = iframes.length - 1; t >= 0; t--) {
|
||||
var iframe = iframes[t];
|
||||
if(iframe.getAttribute("library") === "true" &&
|
||||
iframe.getAttribute("src") === url) {
|
||||
@@ -147,6 +160,13 @@ exports.startup = function() {
|
||||
// console.log("browser-messaging: Received message from",event.origin);
|
||||
// console.log("browser-messaging: Message content",event.data);
|
||||
switch(event.data.verb) {
|
||||
case "PLUGIN-LIBRARY-READY":
|
||||
$tw.utils.each($tw.browserMessaging.iframeInfoMap, function(info) {
|
||||
if(info && info.domNode && info.domNode.contentWindow === event.source) {
|
||||
flushCallbacks(info);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "GET-RESPONSE":
|
||||
if(event.data.status.charAt(0) === "2") {
|
||||
if(event.data.cookies) {
|
||||
|
||||
@@ -15,7 +15,8 @@ var getCellInfo = function(text, start, length, SEPARATOR) {
|
||||
var isCellQuoted = text.charAt(start) === QUOTE;
|
||||
var cellStart = isCellQuoted ? start + 1 : start;
|
||||
|
||||
if(text.charAt(i) === SEPARATOR) {
|
||||
// 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) {
|
||||
return [cellStart, cellStart, false];
|
||||
}
|
||||
|
||||
|
||||
@@ -164,15 +164,16 @@ 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($tw.browser.animationEnd,function handler(event) {
|
||||
element.removeEventListener($tw.browser.animationEnd,handler,false);
|
||||
$tw.utils.removeClass(element,"pulse");
|
||||
element.addEventListener(eventName,function handler(event) {
|
||||
element.removeEventListener(eventName,handler,false);
|
||||
$tw.utils.removeClass(element,"tc-pulse");
|
||||
},false);
|
||||
// Apply the pulse class
|
||||
$tw.utils.removeClass(element,"pulse");
|
||||
$tw.utils.removeClass(element,"tc-pulse");
|
||||
$tw.utils.forceLayout(element);
|
||||
$tw.utils.addClass(element,"pulse");
|
||||
$tw.utils.addClass(element,"tc-pulse");
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -115,7 +115,9 @@ var TW_Element = function(tag, namespace) {
|
||||
this.children = [];
|
||||
this._style = {}; // Internal style object
|
||||
this.style = new TW_Style(this); // Proxy for style management
|
||||
this.namespaceURI = namespace || "http://www.w3.org/1999/xhtml";
|
||||
// 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";
|
||||
};
|
||||
|
||||
|
||||
@@ -206,7 +208,16 @@ TW_Element.prototype.addEventListener = function(type,listener,useCapture) {
|
||||
|
||||
Object.defineProperty(TW_Element.prototype, "tagName", {
|
||||
get: function() {
|
||||
return this.tag || "";
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -151,10 +151,24 @@ ButtonWidget.prototype.getBoundingClientRect = function() {
|
||||
};
|
||||
|
||||
ButtonWidget.prototype.isSelected = function() {
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
ButtonWidget.prototype.isPoppedUp = function() {
|
||||
@@ -267,7 +281,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.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.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"]) {
|
||||
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"
|
||||
};
|
||||
|
||||
@@ -99,7 +99,8 @@ LinkWidget.prototype.renderLink = function(parent,nextSibling) {
|
||||
wikiLinkText = this.wiki.filterTiddlers(wikilinkTransformFilter,this,function(iterator) {
|
||||
iterator(self.wiki.getTiddler(self.to),self.to);
|
||||
})[0];
|
||||
} else {
|
||||
}
|
||||
if(!wikiLinkText) {
|
||||
// Expand the tv-wikilink-template variable to construct the href
|
||||
var wikiLinkTemplateMacro = this.getVariable("tv-wikilink-template"),
|
||||
wikiLinkTemplate = wikiLinkTemplateMacro ? wikiLinkTemplateMacro.trim() : "#$uri_encoded$";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
title: $:/core/stylesheets/custom-properties
|
||||
|
||||
\rules only transcludeinline macrocallinline html transcludeblock
|
||||
\rules only transcludeinline macrocallinline html transcludeblock filteredtranscludeinline
|
||||
|
||||
/* 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/storyrwidth}};
|
||||
--tp-story-width: {{$:/themes/tiddlywiki/vanilla/metrics/storywidth}};
|
||||
--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]] }}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
title: $:/core/templates/plain-text
|
||||
|
||||
<$text text=<<text>>/>
|
||||
@@ -0,0 +1,3 @@
|
||||
title: $:/core/templates/rendered-text
|
||||
|
||||
<<text>>
|
||||
@@ -4,13 +4,15 @@ description: create a new journal tiddler
|
||||
|
||||
\whitespace trim
|
||||
\function get-tags() [<textFieldTags>] [<tagsFieldTags>] +[join[ ]]
|
||||
<$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>>/>">
|
||||
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
|
||||
textFieldTags={{$:/config/NewJournal/Tags}}
|
||||
tagsFieldTags={{$:/config/NewJournal/Tags!!tags}}
|
||||
journalText={{$:/config/NewJournal/Text}}
|
||||
>
|
||||
<$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">
|
||||
<div class=`tc-edit-field-add-name-wrapper ${ [<newFieldNameTiddler>get[text]] :intersection[<storyTiddler>fields[]] :then[[tc-edit-field-exists]] }$`>
|
||||
<$transclude $variable="keyboard-driven-input"
|
||||
cancelPopups="yes"
|
||||
class=`tc-edit-texteditor tc-popup-handle ${ [<newFieldNameTiddler>get[text]] :intersection[<storyTiddler>fields[]] :then[[tc-edit-field-exists]] }$`
|
||||
class="tc-edit-texteditor tc-popup-handle"
|
||||
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,30 +6,25 @@ tags: $:/tags/EditTemplate
|
||||
\procedure lingo-base() $:/language/EditTemplate/
|
||||
|
||||
\procedure tag-body-inner(colour,fallbackTarget,colourA,colourB,icon,tagField:"tags")
|
||||
<$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)$;`
|
||||
<$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>>
|
||||
>
|
||||
<$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>
|
||||
<$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>
|
||||
\end
|
||||
|
||||
\procedure tag-body(colour,palette,icon,tagField:"tags")
|
||||
|
||||
@@ -16,15 +16,13 @@ title: $:/core/ui/TagPickerTagTemplate
|
||||
<$set name="backgroundColor"
|
||||
value={{{ [<currentTiddler>] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerColourFilter]!is[draft]get[text]] }}}
|
||||
>
|
||||
<$wikify name="foregroundColor"
|
||||
text="""<$macrocall $name="contrastcolour" target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>>/>"""
|
||||
>
|
||||
<$let foregroundColor=<<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>
|
||||
</$wikify>
|
||||
</$let>
|
||||
</$set>
|
||||
</$button>
|
||||
|
||||
@@ -6,7 +6,6 @@ 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}}
|
||||
@@ -16,9 +15,11 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
|
||||
<$text text={{$:/language/Buttons/NewJournalHere/Caption}}/>
|
||||
</span>
|
||||
<%endif%>
|
||||
</$wikify>
|
||||
</$button>
|
||||
\end
|
||||
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} journalTags={{$:/config/NewJournal/Tags}} currentTiddlerTag=<<currentTiddler>>>
|
||||
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
|
||||
journalTags={{$:/config/NewJournal/Tags}}
|
||||
currentTiddlerTag=<<currentTiddler>>
|
||||
>
|
||||
<<journalButton>>
|
||||
</$let>
|
||||
|
||||
@@ -17,6 +17,10 @@ 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>>/>
|
||||
@@ -105,7 +109,7 @@ Drag this link to copy this tool to another wiki
|
||||
|
||||
<$button>
|
||||
<<display-wiki-info-modal>>
|
||||
Click to generate wiki information report
|
||||
<<lingo title:"Generate/Caption">>
|
||||
</$button>
|
||||
|
||||
<$link to="$:/core/ui/ControlPanel/WikiInformation">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
title: $:/config/OfficialPluginLibrary
|
||||
tags: $:/tags/PluginLibrary
|
||||
url: https://tiddlywiki.com/library/v5.4.1/index.html
|
||||
url: https://tiddlywiki.com/library/v5.5.0/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.4.1/index.html
|
||||
url: http://127.0.0.1:8080/prerelease/library/v5.5.0/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.4.1/index.html
|
||||
url: https://tiddlywiki.com/prerelease/library/v5.5.0/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.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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,6 +13,8 @@ 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>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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>
|
||||
@@ -0,0 +1,12 @@
|
||||
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>
|
||||
@@ -0,0 +1,15 @@
|
||||
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>
|
||||
@@ -0,0 +1,17 @@
|
||||
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>
|
||||
@@ -0,0 +1,16 @@
|
||||
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>
|
||||
@@ -0,0 +1,20 @@
|
||||
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>
|
||||
@@ -0,0 +1,16 @@
|
||||
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>
|
||||
@@ -0,0 +1,22 @@
|
||||
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"}]
|
||||
@@ -0,0 +1,14 @@
|
||||
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>
|
||||
@@ -0,0 +1,15 @@
|
||||
title: MultiValuedVariables/AttributeColonVarName
|
||||
description: ((var)) on a widget attribute may name a variable containing a colon (#10013)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\whitespace trim
|
||||
<$let my:list={{{ [range[2]] }}}>
|
||||
<$text text=((my:list))/>
|
||||
</$let>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>1</p>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
title: Parse/BackCompat/MacrocallColonVarNameAndParam
|
||||
description: A colon-named procedure or macro accepts named parameters with either separator (#10013)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\procedure object:method(p) COLONPROC(<<p>>)
|
||||
\define object:macro(p) COLONMACRO($p$)
|
||||
<<object:method p:"V">>
|
||||
<<object:method p="V">>
|
||||
<<object:macro p:"V">>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>COLONPROC(V)</p><p>COLONPROC(V)</p><p>COLONMACRO(V)</p>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
title: Parse/BackCompat/MacrocallColonVarNameAsValue
|
||||
description: A colon-named procedure or macro may be an =-separated parameter value (#10013)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\procedure object:method() COLONPROC
|
||||
\define object:macro() COLONMACRO
|
||||
\procedure host(a) [a=<<a>>]
|
||||
<<host a=<<object:method>>>>
|
||||
<<host a=<<object:macro>>>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>[a=COLONPROC]</p><p>[a=COLONMACRO]</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/MacrocallColonVarName
|
||||
description: Macro call variable name may contain a colon (#10013)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\procedure object:method() VAL
|
||||
<<object:method>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>VAL</p>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/WidgetAttrMacroColonVarName
|
||||
description: Widget attribute macro value may name a variable containing a colon (#10013)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\procedure object:method() VAL
|
||||
<$text text=<<object:method>>/>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>VAL</p>
|
||||
@@ -0,0 +1,162 @@
|
||||
/*\
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
/*\
|
||||
title: test-browser-messaging.js
|
||||
type: application/javascript
|
||||
tags: [[$:/tags/test-spec]]
|
||||
|
||||
Tests the PLUGIN-LIBRARY-READY handshake and legacy onload fallback.
|
||||
|
||||
\*/
|
||||
|
||||
"use strict";
|
||||
|
||||
describe("Browser Messaging", function() {
|
||||
|
||||
it("should declare itself as a synchronous browser-only startup module", function() {
|
||||
var startup = require("$:/core/modules/browser-messaging.js");
|
||||
expect(startup.name).toBe("browser-messaging");
|
||||
expect(startup.platforms).toEqual(["browser"]);
|
||||
expect(startup.synchronous).toBe(true);
|
||||
expect(typeof startup.startup).toBe("function");
|
||||
});
|
||||
|
||||
// loadIFrame()/flushCallbacks() are private to browser-messaging.js, so the only way
|
||||
// to exercise them for real is through $tw.rootWidget events and real postMessage
|
||||
// traffic with a real iframe, rather than re-implementing their logic here.
|
||||
describe("plugin library iframe handshake", function() {
|
||||
|
||||
var urlsToUnload;
|
||||
|
||||
beforeEach(function() {
|
||||
if(!$tw.browser) { return; }
|
||||
urlsToUnload = [];
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
if(!$tw.browser) { return; }
|
||||
urlsToUnload.forEach(function(url) {
|
||||
// Grab the DOM node before unloading clears the map entry, as a
|
||||
// belt-and-braces removal (unloadIFrame's own removal has an
|
||||
// unrelated off-by-one loop bug for small iframe counts).
|
||||
var info = $tw.browserMessaging.iframeInfoMap[url];
|
||||
$tw.rootWidget.dispatchEvent({type: "tm-unload-plugin-library", paramObject: {url: url}});
|
||||
if(info && info.domNode && info.domNode.parentNode) {
|
||||
info.domNode.parentNode.removeChild(info.domNode);
|
||||
}
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
});
|
||||
|
||||
function makeChildUrl(script) {
|
||||
var blob = new Blob(["<script>" + script + "<\/script>"],{type: "text/html"});
|
||||
var url = URL.createObjectURL(blob);
|
||||
urlsToUnload.push(url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function waitUntil(predicate,done,description) {
|
||||
var attempts = 0;
|
||||
(function check() {
|
||||
if(predicate()) {
|
||||
done();
|
||||
} else if(++attempts > 400) {
|
||||
done.fail("Timed out waiting for: " + description);
|
||||
} else {
|
||||
setTimeout(check,20);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
it("should complete the real GET / GET-RESPONSE round trip once the child signals PLUGIN-LIBRARY-READY", function(done) {
|
||||
if(!$tw.browser) { pending("browser-only: requires a real iframe and postMessage - run in browser"); return; }
|
||||
|
||||
// Mirrors plugins/tiddlywiki/pluginlibrary/libraryserver.js's real handshake and GET handling:
|
||||
// listener registration and the ready postMessage both happen synchronously, before <body> parses,
|
||||
// so onload (the legacy fallback below) cannot win this race for a well-behaved child.
|
||||
var url = makeChildUrl(
|
||||
"window.addEventListener('message',function(e){" +
|
||||
"if(e.data && e.data.verb === 'GET' && e.data.url === 'recipes/library/tiddlers.json'){" +
|
||||
"e.source.postMessage({verb:'GET-RESPONSE',status:'200',cookies:e.data.cookies,url:e.data.url," +
|
||||
"type:'application/json',body:JSON.stringify([{title:'ExamplePlugin',type:'application/json',text:'{}'}])},'*');" +
|
||||
"}});" +
|
||||
"window.parent.postMessage({verb:'PLUGIN-LIBRARY-READY'},'*');"
|
||||
);
|
||||
var expectedTitle = "$:/temp/RemoteAssetInfo/" + url + "/ExamplePlugin";
|
||||
|
||||
$tw.rootWidget.dispatchEvent({
|
||||
type: "tm-load-plugin-library",
|
||||
paramObject: {url: url}
|
||||
});
|
||||
|
||||
waitUntil(function() {
|
||||
return !!$tw.wiki.getTiddler(expectedTitle);
|
||||
},done,"tiddler " + expectedTitle);
|
||||
},10000);
|
||||
|
||||
it("should still resolve via the legacy iframe.onload fallback when the child never sends PLUGIN-LIBRARY-READY", function(done) {
|
||||
if(!$tw.browser) { pending("browser-only: requires a real iframe - run in browser"); return; }
|
||||
|
||||
// A blank document that never sends the handshake exercises the onload-only legacy path.
|
||||
var url = makeChildUrl("");
|
||||
|
||||
$tw.rootWidget.dispatchEvent({
|
||||
type: "tm-load-plugin-library",
|
||||
paramObject: {url: url}
|
||||
});
|
||||
|
||||
waitUntil(function() {
|
||||
var info = $tw.browserMessaging.iframeInfoMap[url];
|
||||
return !!info && info.status === "loaded";
|
||||
},done,"iframe status to become 'loaded' via onload");
|
||||
},10000);
|
||||
|
||||
it("should complete the round trip via PLUGIN-LIBRARY-READY even when the child's own load is aborted and onload can never fire", function(done) {
|
||||
if(!$tw.browser) { pending("browser-only: requires a real iframe and postMessage - run in browser"); return; }
|
||||
|
||||
// window.stop() simulates onload never firing despite the library being ready,
|
||||
// so only the PLUGIN-LIBRARY-READY message can complete this round trip.
|
||||
var url = makeChildUrl(
|
||||
"window.addEventListener('message',function(e){" +
|
||||
"if(e.data && e.data.verb === 'GET' && e.data.url === 'recipes/library/tiddlers.json'){" +
|
||||
"e.source.postMessage({verb:'GET-RESPONSE',status:'200',cookies:e.data.cookies,url:e.data.url," +
|
||||
"type:'application/json',body:JSON.stringify([{title:'ExamplePlugin',type:'application/json',text:'{}'}])},'*');" +
|
||||
"}});" +
|
||||
"window.parent.postMessage({verb:'PLUGIN-LIBRARY-READY'},'*');" +
|
||||
"window.stop();"
|
||||
);
|
||||
var expectedTitle = "$:/temp/RemoteAssetInfo/" + url + "/ExamplePlugin";
|
||||
|
||||
$tw.rootWidget.dispatchEvent({
|
||||
type: "tm-load-plugin-library",
|
||||
paramObject: {url: url}
|
||||
});
|
||||
|
||||
waitUntil(function() {
|
||||
return !!$tw.wiki.getTiddler(expectedTitle);
|
||||
},done,"tiddler " + expectedTitle);
|
||||
},10000);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/*\
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/*\
|
||||
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,6 +19,56 @@ 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.
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*\
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/*\
|
||||
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,10 +940,20 @@ 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,5 +173,20 @@ 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,5 +1,5 @@
|
||||
created: 20160107225427489
|
||||
modified: 20211221102625141
|
||||
modified: 20260907204047567
|
||||
tags: Features
|
||||
title: Modals
|
||||
type: text/vnd.tiddlywiki
|
||||
@@ -19,6 +19,29 @@ Note that the footer and subtitle fields are not limited to plain text, and wiki
|
||||
|
||||
Modals are displayed with the [[WidgetMessage: tm-modal]].
|
||||
|
||||
<$button message="tm-modal" param="SampleWizard">Open demo modal</$button>
|
||||
|
||||
<<.tip """<$macrocall $name=".from-version" version="5.2.4"/> allow using "mask-closable" field""">>
|
||||
|
||||
!! Examples
|
||||
|
||||
<<wikitext-example-without-html """<$button message="tm-modal" param="SampleWizard">Open demo modal</$button>""">>
|
||||
|
||||
<<.from-version "5.5.0">> you can use the template <<.tid $:/core/templates/rendered-text>> to display a modal if you do not want to create a separate tiddler:
|
||||
|
||||
<<wikitext-example-without-html """\procedure text()
|
||||
<style class="headless-modal">
|
||||
.tc-modal:has(.tc-modal-body .headless-modal) .tc-modal-header{display:none}
|
||||
</style>
|
||||
|
||||
!! Motovun Jack
|
||||
|
||||
{{Motovun Jack.jpg}}
|
||||
|
||||
|
||||
<$button message="tm-modal" param="SampleWizard">Open nested modal</$button>
|
||||
|
||||
\end
|
||||
|
||||
<$button>
|
||||
<$action-sendmessage $message="tm-modal" $param="$:/core/templates/rendered-text" text=<<text>>/>
|
||||
Click me!
|
||||
</$button>""">>
|
||||
+9
-1
@@ -1,5 +1,5 @@
|
||||
created: 20230304160331362
|
||||
modified: 20230304160332927
|
||||
modified: 20260724082228670
|
||||
tags: [[makepatches Operator]] [[applypatches Operator]] [[Operator Examples]]
|
||||
title: makepatches and applypatches Operator (Examples)
|
||||
type: text/vnd.tiddlywiki
|
||||
@@ -40,4 +40,12 @@ 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,3 +9,4 @@ 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,23 +1,28 @@
|
||||
caption: makepatches
|
||||
created: 20230304122354967
|
||||
modified: 20230304122400128
|
||||
op-purpose: returns a set of patches that transform the input to a given string
|
||||
modified: 20260724081502905
|
||||
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-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
|
||||
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
|
||||
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">>
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
caption: sortby
|
||||
created: 20151017145021839
|
||||
modified: 20151108052142057
|
||||
modified: 20260811120000000
|
||||
op-input: a list of items
|
||||
op-output: all items sorted by lookup list
|
||||
op-output: all items sorted by the order list, with unlisted items placed first, or last with the `end` suffix
|
||||
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: 47 KiB After Width: | Height: | Size: 50 KiB |
@@ -1,6 +1,6 @@
|
||||
caption: tm-download-file
|
||||
created: 20140811112201235
|
||||
modified: 20230723214745520
|
||||
modified: 20260910134915210
|
||||
tags: Messages
|
||||
title: WidgetMessage: tm-download-file
|
||||
type: text/vnd.tiddlywiki
|
||||
@@ -19,4 +19,41 @@ The following variable names have special behaviour:
|
||||
|
||||
The download file message is usually generated with the ButtonWidget.
|
||||
|
||||
The download file message is handled by the TiddlyWiki core SyncMechanism which invokes the current [[SaverModule|SaverModules]].
|
||||
The download file message is handled by the TiddlyWiki core [[SyncMechanism|https://tiddlywiki.com/dev/#SyncAdaptorModules]] which invokes the current [[SaverModule|https://tiddlywiki.com/dev/#Saver]].
|
||||
|
||||
! Examples
|
||||
|
||||
<<.from-version "5.5.0">> you can use the template <<.tid $:/core/templates/plain-text>> to create a custom download configuration without creating additional tiddlers:
|
||||
|
||||
<<wikitext-example-without-html """\function field.value() [<pluginTitle>get{!!title}]
|
||||
|
||||
\function json.plugin()
|
||||
[<pluginTitle>fields[]]:reduce[<accumulator>jsonset{!!title},<field.value>]=>pluginData
|
||||
"[]"+[jsonset:json[0],<pluginData>]
|
||||
\end
|
||||
|
||||
\define externalPlugin()
|
||||
{
|
||||
const tw = globalThis.$tw ??= Object.create(null);
|
||||
tw.preloadTiddlers = (tw.preloadTiddlers ?? []).concat($(json.plugin)$);
|
||||
}
|
||||
\end
|
||||
|
||||
|
||||
<$let pluginTitle="$:/plugins/tiddlywiki/menubar" text=<<externalPlugin>> >
|
||||
|
||||
<$button tooltip="Export standalone plugin">{{$:/core/images/export-button}} export <<pluginTitle>> as standalone plugin
|
||||
<$action-sendmessage
|
||||
$message="tm-download-file"
|
||||
$param="$:/core/templates/plain-text"
|
||||
text=<<text>>
|
||||
filename={{{ [<pluginTitle>search-replace:g[/],[_]addsuffix[.js]] }}}
|
||||
type="text/javascript"
|
||||
/>
|
||||
</$button>
|
||||
|
||||
You can load the exported plugin by creating a tiddler with the tag <<.tag $:/tags/RawMarkupWikified/TopHead>> containing this:
|
||||
|
||||
```
|
||||
`<script src="URL_OF_YOUR_PLUGIN_HERE" onerror="alert('Error: Cannot load YOUR_PLUGIN located at URL_OF_YOUR_PLUGIN_HERE');"></script>`
|
||||
```""">>
|
||||
@@ -1,6 +1,6 @@
|
||||
caption: tm-notify
|
||||
created: 20140811112304772
|
||||
modified: 20230723220728382
|
||||
modified: 20260907202408431
|
||||
tags: Messages
|
||||
title: WidgetMessage: tm-notify
|
||||
type: text/vnd.tiddlywiki
|
||||
@@ -12,3 +12,28 @@ The notify message briefly displays a specified tiddler as a small alert in the
|
||||
|//{any other params}// |Any other parameters are made available as variables to the notify message. |
|
||||
|
||||
The notify message is handled by the TiddlyWiki core.
|
||||
|
||||
! Examples
|
||||
|
||||
<<.from-version "5.5.0">> you can use the template <<.tid $:/core/templates/plain-text>> to display simple text in a notification:
|
||||
|
||||
<<wikitext-example-without-html """\procedure notify()
|
||||
<$action-sendmessage $message="tm-notify" $param="$:/core/templates/plain-text" text={{!!count}} />
|
||||
\end notify
|
||||
|
||||
<$button set="!!count" setTo={{{ [{!!count}add[1]] }}} tooltip="count" actions="<<notify>>" >
|
||||
Count
|
||||
</$button>""">>
|
||||
|
||||
You can also use the template $:/core/templates/rendered-text to display wikitext:
|
||||
|
||||
<<wikitext-example-without-html """\procedure notify()
|
||||
\procedure template()
|
||||
Current time is <<now>>
|
||||
\end template
|
||||
<$action-sendmessage $message="tm-notify" $param="$:/core/templates/rendered-text" text=<<template>> />
|
||||
\end notify
|
||||
|
||||
<$button actions="<<notify>>" >
|
||||
What time is it?
|
||||
</$button>""">>
|
||||
@@ -0,0 +1,8 @@
|
||||
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]]
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
title: $:/changenotes/5.5.0/#10003
|
||||
description: Fix race condition in plugin library iframe message handling
|
||||
release: 5.5.0
|
||||
tags: $:/tags/ChangeNote
|
||||
change-type: bugfix
|
||||
change-category: plugin
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/10003
|
||||
github-contributors: DesignThinkerer
|
||||
|
||||
Introduce a ''PLUGIN-LIBRARY-READY'' handshake verb for plugin library iframes.
|
||||
This ensures that postMessage commands are not prematurely dropped before the iframe's
|
||||
internal script has fully bound its message listeners.
|
||||
|
||||
Retains iframe.onload as a backward-compatible fallback for legacy or external libraries
|
||||
that do not emit the ready signal.
|
||||
|
||||
Previously, the browser-messaging.js relied entirely on iframe.onload to trigger the initial
|
||||
postMessage call to a plugin library. However, if the iframe's connection or load is aborted
|
||||
after its internal script has already run and bound window.addEventListener("message"), onload
|
||||
never fires at all, so the initial postMessage call is never sent and the request is silently
|
||||
lost with no error. The new handshake eliminates this by having the iframe actively confirm
|
||||
readiness as soon as its listener is bound, independent of whether onload ever fires.
|
||||
@@ -0,0 +1,34 @@
|
||||
title: $:/changenotes/5.5.0/#10014
|
||||
created: 20260906223932000
|
||||
modified: 20260906223932000
|
||||
description: Variable names may contain a colon in call and multi-valued variable syntax
|
||||
release: 5.5.0
|
||||
tags: $:/tags/ChangeNote
|
||||
change-type: bugfix
|
||||
change-category: hackability
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/10014
|
||||
github-contributors: pmario
|
||||
|
||||
A variable name may contain a colon in the `<<...>>` call syntax: standalone, with named parameters using either separator, as a nested call, and as a widget attribute value.
|
||||
|
||||
```
|
||||
\procedure obj:plain() Hello
|
||||
\procedure greet:person(name) Hello <<name>>
|
||||
\procedure obj:host(a) [<<a>>]
|
||||
|
||||
<<obj:plain>>
|
||||
<<greet:person name:"world">>
|
||||
<<greet:person name="world">>
|
||||
<<obj:host a=<<obj:plain>>>>
|
||||
<div data-x=<<obj:plain>> ></div>
|
||||
```
|
||||
|
||||
An `((...))` multi-valued variable reference used as a widget attribute value accepts the same names as the inline syntax:
|
||||
|
||||
```
|
||||
<$let my:list={{{ [range[3]] }}}>
|
||||
<$text text=((my:list))/>
|
||||
</$let>
|
||||
```
|
||||
|
||||
Fixed issue: [[#10013|https://github.com/TiddlyWiki/TiddlyWiki5/issues/10013]]
|
||||
@@ -0,0 +1,12 @@
|
||||
change-category: internal
|
||||
change-type: feature
|
||||
created: 20260906234108933
|
||||
description: Added new $:/core/templates/plain-text core template for rendering raw variables
|
||||
github-contributors: DesignThinkerer
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/10017
|
||||
modified: 20260906234439533
|
||||
release: 5.5.0
|
||||
tags: $:/tags/ChangeNote
|
||||
title: $:/changenotes/5.5.0/#10017
|
||||
|
||||
Added two new core template, `$:/core/templates/plain-text` and `$:/core/templates/rendered-text` that renders respectively a single `text` variable as plain text or wikitext. This allows to export and download dynamic strings via messages like `tm-download-file`, without creating temporary tiddlers in the wiki store.
|
||||
@@ -0,0 +1,12 @@
|
||||
change-category: internal
|
||||
change-type: bugfix
|
||||
created: 20260907175854854
|
||||
description: Fixed a DOM leak when closing the plugin library
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/10018
|
||||
release: 5.5.0
|
||||
tags: $:/tags/ChangeNote
|
||||
title: $:/changenotes/5.5.0/#10018
|
||||
type: text/vnd.tiddlywiki
|
||||
github-contributors: DesignThinkerer
|
||||
|
||||
Fixed a syntax error in the `unloadIFrame` loop that permanently left a hidden `<iframe library="true">` attached to the DOM every time the plugin library modal was closed.
|
||||
@@ -0,0 +1,12 @@
|
||||
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]])
|
||||
@@ -0,0 +1,12 @@
|
||||
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]])
|
||||
@@ -0,0 +1,13 @@
|
||||
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
|
||||
@@ -0,0 +1,12 @@
|
||||
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]])
|
||||
@@ -0,0 +1,12 @@
|
||||
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]])
|
||||
@@ -0,0 +1,12 @@
|
||||
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]])
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
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.
|
||||
@@ -0,0 +1,13 @@
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
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
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -0,0 +1,13 @@
|
||||
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.
|
||||
@@ -0,0 +1,10 @@
|
||||
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`
|
||||
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -0,0 +1,13 @@
|
||||
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.
|
||||
@@ -0,0 +1,13 @@
|
||||
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).
|
||||
@@ -0,0 +1,26 @@
|
||||
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]]
|
||||
@@ -0,0 +1,10 @@
|
||||
title: $:/changenotes/5.4.1/#9958
|
||||
description: Fix typo in CSS custom properties
|
||||
tags: $:/tags/ChangeNote
|
||||
release: 5.5.0
|
||||
change-type: bugfix
|
||||
change-category: usability
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9958
|
||||
github-contributors: BurningTreeC
|
||||
|
||||
Fixes a typo in the CSS custom properties
|
||||
@@ -0,0 +1,14 @@
|
||||
title: $:/changenotes/5.5.0/#9965
|
||||
created: 20260812171534862
|
||||
modified: 20260812171534862
|
||||
tags: $:/tags/ChangeNote
|
||||
change-type: enhancement
|
||||
change-category: usability
|
||||
description: Change of the class attribute of editors does no more completely refresh the widget
|
||||
release: 5.5.0
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9965
|
||||
github-contributors: BurningTreeC
|
||||
type: text/vnd.tiddlywiki
|
||||
|
||||
This pull request changes the way editors refresh when their `class` attribute changes.
|
||||
They did refresh the widget completely. Now they just assign the new classes.
|
||||
@@ -0,0 +1,13 @@
|
||||
caption: 5.5.0
|
||||
created: 20260710103826404
|
||||
modified: 20260710103826404
|
||||
tags: ReleaseNotes
|
||||
title: Release 5.5.0
|
||||
type: text/vnd.tiddlywiki
|
||||
description: Under development
|
||||
|
||||
\procedure release-introduction()
|
||||
Release v5.5.0 is under development.
|
||||
\end release-introduction
|
||||
|
||||
<<releasenote 5.5.0>>
|
||||
@@ -30,12 +30,12 @@ The content of the `<$button>` widget is displayed within the button.
|
||||
|param |The optional parameter to the message |
|
||||
|set |A TextReference to which a new value will be assigned |
|
||||
|setTitle |A title to which a new value will be assigned, ''without'' TextReference. Gets preferred over <<.attr set>> |
|
||||
|setField |A ''field name'' to which the new value will be assigned, if the attribute <<.attr setTitle>> is present. Defaults to the ''text'' field |
|
||||
|setIndex |An ''index'' to which the new value will be assigned, if the attribute <<.attr setTitle>> is present |
|
||||
|setField |A ''field name'' to which the new value will be assigned, if the attribute <<.attr setTitle>> is present. Defaults to the ''text'' field. Takes preference over <<.attr setIndex>> |
|
||||
|setIndex |An ''index'' to which the new value will be assigned, if the attribute <<.attr setTitle>> is present. Ignored if <<.attr setField>> is also present |
|
||||
|setTo |The new value to assign to the TextReference identified in the `set` attribute or the text field / the field specified through <<.attr setField>> / the index specified through <<.attr setIndex>> of the title given through <<.attr setTitle>> |
|
||||
|selectedClass |An optional additional CSS class to be assigned if the popup is triggered or the tiddler specified in <<.attr set>> already has the value specified in <<.attr setTo>> |
|
||||
|selectedClass |An optional additional CSS class to be assigned if the popup is triggered or the state identified by <<.attr set>> or <<.attr setTitle>> already has the value specified in <<.attr setTo>> |
|
||||
|selectedAria |<<.from-version "5.4.0">> An ARIA attribute to be set to `true` or `false` when <<.attr selectedClass>> is defined. Allowed values are `aria-checked` (default), `aria-selected` and `aria-pressed` |
|
||||
|default |Default value if <<.attr set>> tiddler is missing for testing against <<.attr setTo>> to determine <<.attr selectedClass>> |
|
||||
|default |Default value if the state identified by <<.attr set>> or <<.attr setTitle>> is missing for testing against <<.attr setTo>> to determine <<.attr selectedClass>> |
|
||||
|popup |Title of a state tiddler for a popup that is toggled when the button is clicked. See PopupMechanism for details |
|
||||
|popupTitle |Title of a state tiddler for a popup that is toggled when the button is clicked. In difference to the <<.attr popup>> attribute, ''no'' TextReference is used. See PopupMechanism for details |
|
||||
|popupAbsCoords |<<.from-version "5.2.4">> If set to ''yes'' writes absolute coordinates to the tiddler referenced by the <<.attr popup>>. If set to ''no'' (the default) uses relative coordinates. See [[Coordinate Systems]] for details |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user