mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-08-06 19:08:53 +00:00
Merge branch 'master' into colour-improvements
This commit is contained in:
@@ -348,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
|
||||
@@ -368,6 +369,19 @@ exports.generateTiddlerFilepath = function(title,options) {
|
||||
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
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ exports.compileFilter = function(filterString,options) {
|
||||
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("")];
|
||||
};
|
||||
};
|
||||
@@ -17,9 +17,10 @@ exports.subfilter = function(source,operator,options) {
|
||||
defaultFilterRunPrefix = (suffixes[0] && suffixes[0][0]) || options.defaultFilterRunPrefix || "or";
|
||||
var list = options.wiki.filterTiddlers(operator.operand,options.widget,source,{defaultFilterRunPrefix});
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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,32 @@ 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");
|
||||
return results.sort((a,b) => lookup.indexOf(a) - lookup.indexOf(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 +201,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);
|
||||
};
|
||||
};
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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,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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -3,25 +3,62 @@ title: test-filesystem.js
|
||||
type: application/javascript
|
||||
tags: [[$:/tags/test-spec]]
|
||||
|
||||
Tests for $:/core-server filesystem utilities.
|
||||
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");
|
||||
|
||||
// Characters stripped by the cross-platform-filename regex at
|
||||
// core-server/filesystem.js:356. Each entry is [char, description].
|
||||
// The forbidden set includes all characters disallowed by Windows
|
||||
// (< > : " | ? *), backslash (directory separator on Windows),
|
||||
// tilde (legacy 8.3 short-name marker), and caret (disallowed in
|
||||
// some shell/FS contexts).
|
||||
// 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"],
|
||||
@@ -35,49 +72,291 @@ if($tw.node) {
|
||||
["\\","backslash"]
|
||||
];
|
||||
|
||||
// Use originalpath so we exercise the line-356 regex directly.
|
||||
// The title-branch at line ~342 pre-strips "/" and "\" before the
|
||||
// main regex runs, which would mask the backslash case.
|
||||
function filepathFromOriginalpath(title,extension) {
|
||||
// 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: extension,
|
||||
extension: ".tid",
|
||||
directory: directory,
|
||||
fileInfo: {
|
||||
overwrite: true,
|
||||
originalpath: title + extension
|
||||
}
|
||||
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("should replace " + name + " (" + char + ") with underscore", function() {
|
||||
var result = filepathFromOriginalpath("a" + char + "b",".tid");
|
||||
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("should replace every forbidden char in a dense string", function() {
|
||||
// Prefix with "x" so the sanitized result isn't all underscores,
|
||||
// which would trigger the charcode-fallback branch at line ~371.
|
||||
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 = filepathFromOriginalpath(title,".tid");
|
||||
var result = filepathFromTitle(title);
|
||||
expect(path.dirname(result)).toBe(directory);
|
||||
expect(path.basename(result)).toBe(expected + ".tid");
|
||||
});
|
||||
|
||||
it("should sanitize real-world 'Pragma: \\define' title without creating a subdirectory", function() {
|
||||
// Issue for editions/tw5.com titles like "Pragma: \define".
|
||||
// Before the fix, backslash survived sanitization and path.resolve
|
||||
// treated it as a separator on Windows, producing a "Pragma_ "
|
||||
// subdir containing "define.tid".
|
||||
var result = filepathFromOriginalpath("Pragma: \\define",".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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -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"'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
+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,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">>
|
||||
@@ -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]])
|
||||
@@ -1,13 +1,14 @@
|
||||
change-category: nodejs
|
||||
change-type: bugfix
|
||||
created: 20260711175727000
|
||||
description: Tiddler titles containing a backslash no longer break generated file paths
|
||||
description: Keep retained subdirectory tiddlers and sanitise file names per path segment
|
||||
github-contributors: pmario
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9815
|
||||
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
|
||||
|
||||
* Titles containing a backslash, such as `pragma: \define`, get the backslash replaced like other forbidden filename characters instead of it acting 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 filename sanitising
|
||||
* 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,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
|
||||
@@ -5,25 +5,27 @@ created: 20131129101027725
|
||||
delivery: App
|
||||
description: iPad/iPhone app for working with TiddlyWiki
|
||||
method: save
|
||||
modified: 20201007205336209
|
||||
modified: 20260421210013537
|
||||
tags: Saving iOS [[Standalone App]]
|
||||
title: Saving on iPad/iPhone
|
||||
type: text/vnd.tiddlywiki
|
||||
|
||||
The iPad/iPhone app ''Quine 2'' makes it possible to view, edit and then save changes to TiddlyWiki5 on iOS. [[Download it here|https://apps.apple.com/us/app/quine-2/id1450128957]].
|
||||
The iPad/iPhone app ''Quine'' makes it possible to view, edit and then save changes to TiddlyWiki5 on iOS.
|
||||
|
||||
Currently in its third major version, rewritten for iOS and iPadOS 26.0.
|
||||
|
||||
[[Download it here|https://apps.apple.com/us/app/TiddlyWiki51664603]].
|
||||
|
||||
Instructions for use:
|
||||
|
||||
# Open Quine 2
|
||||
# Tap the + toolbar button to create and open a new TiddlyWiki
|
||||
# Open Quine
|
||||
# From the file list tap the + toolbar button, or the "New Wiki" button, to create and open a new TiddlyWiki
|
||||
# From the file list tap an existing TiddlyWiki file to open it
|
||||
# Edit the TiddlyWiki as normal, and save as normal using either Autosave or the TiddlyWiki save button <<.icon $:/core/images/save-button-dynamic>>
|
||||
# Tap the left hand "Documents" toolbar button to close an open TiddlyWiki
|
||||
# Tap the left hand "<" toolbar button to close an open TiddlyWiki, returning to the file list
|
||||
|
||||
*Quine 2 works natively in iOS with the local file system and the iCloud file system
|
||||
*Quine 2 also allows you to open, edit and save TiddlyWiki files stored with cloud file providers
|
||||
*Quine 2 allows you to follow embedded WikiText links and canonical links to external files for cloud-like file providers which support "folder level sharing".
|
||||
**This includes the apps "Secure Shellfish" and "Working Copy". Most providers, though, do not allow apps like Quine 2 to access linked files this way.
|
||||
** If you wish to enable such links for "well behaved" file providers, toggle "on" the "Enable folder selection for out-of-sandbox links" setting in iOS Settings for Quine 2
|
||||
*Quine works natively in iOS with the local file system and the iCloud file system
|
||||
*Quine also allows you to open, edit and save TiddlyWiki files stored with other cloud file providers
|
||||
*Quine allows you to follow embedded WikiText links and canonical links to external files on cloud-like file providers which support "folder level sharing".
|
||||
|
||||
//Note that Quine is published independently of TiddlyWiki//
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Generated
+4
-2
@@ -6,7 +6,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tiddlywiki",
|
||||
"version": "5.4.1",
|
||||
"version": "5.5.0-prerelease",
|
||||
"license": "BSD",
|
||||
"bin": {
|
||||
"tiddlywiki": "tiddlywiki.js"
|
||||
@@ -298,7 +298,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1242,6 +1242,17 @@ button.tc-btn-invisible.tc-remove-tag-button {
|
||||
100% {background-position: 250% 0, 250% 0}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: .3; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.tc-pulse {
|
||||
background: lightgreen;
|
||||
animation: pulse 0.8s ease-out;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.tc-titlebar h2 {
|
||||
font-size: 1em;
|
||||
display: inline;
|
||||
|
||||
Reference in New Issue
Block a user