mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-06-17 10:48:52 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31f242a42b | |||
| ea84baa5a3 | |||
| a4e4d36bf6 | |||
| 3ed481b2e2 | |||
| 27c60ff58d | |||
| 748ef8aa8d | |||
| 9cfa5a29fb | |||
| 5ea43ce212 | |||
| df6bbbdedf | |||
| 37a461323e | |||
| b29da7baac | |||
| 75b54457ed | |||
| 51f322c3c6 | |||
| 853af2d848 | |||
| 2c1cb33081 | |||
| 1d8131704c | |||
| a1a191b504 | |||
| 34d013ca3d | |||
| 1e06098d20 | |||
| b378f3f462 | |||
| b20b578183 | |||
| 4b046884b1 | |||
| f272f718fa |
+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.0
|
||||
TW5_BUILD_VERSION=v5.5.0
|
||||
fi
|
||||
|
||||
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
|
||||
|
||||
@@ -14,6 +14,8 @@ var _boot = (function($tw) {
|
||||
|
||||
"use strict";
|
||||
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-start"); }
|
||||
|
||||
// Include bootprefix if we're not given module data
|
||||
if(!$tw) {
|
||||
$tw = require("./bootprefix.js").bootprefix();
|
||||
@@ -1158,6 +1160,30 @@ $tw.Wiki = function(options) {
|
||||
pluginTiddlers = [], // Array of tiddlers containing registered plugins, ordered by priority
|
||||
pluginInfo = Object.create(null), // Hashmap of parsed plugin content
|
||||
shadowTiddlers = Object.create(null), // Hashmap by title of {source:, tiddler:}
|
||||
systemTiddlerTitles = null, // Array of system tiddler titles (starting with "$:/")
|
||||
nonSystemTiddlerTitles = null, // Array of non-system tiddler titles
|
||||
partitionTiddlerTitles = function() {
|
||||
if(systemTiddlerTitles === null) {
|
||||
systemTiddlerTitles = [];
|
||||
nonSystemTiddlerTitles = [];
|
||||
var titles = getTiddlerTitles();
|
||||
for(var i = 0, length = titles.length; i < length; i++) {
|
||||
if(titles[i].indexOf("$:/") === 0) {
|
||||
systemTiddlerTitles.push(titles[i]);
|
||||
} else {
|
||||
nonSystemTiddlerTitles.push(titles[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSystemTiddlerTitles = function() {
|
||||
partitionTiddlerTitles();
|
||||
return systemTiddlerTitles;
|
||||
},
|
||||
getNonSystemTiddlerTitles = function() {
|
||||
partitionTiddlerTitles();
|
||||
return nonSystemTiddlerTitles;
|
||||
},
|
||||
shadowTiddlerTitles = null,
|
||||
getShadowTiddlerTitles = function() {
|
||||
if(!shadowTiddlerTitles) {
|
||||
@@ -1206,6 +1232,14 @@ $tw.Wiki = function(options) {
|
||||
tiddlers[title] = tiddler;
|
||||
// Check we've got the title
|
||||
tiddlerTitles = $tw.utils.insertSortedArray(tiddlerTitles || [],title);
|
||||
// Maintain system/non-system partitions
|
||||
if(systemTiddlerTitles !== null) {
|
||||
if(title.indexOf("$:/") === 0) {
|
||||
$tw.utils.insertSortedArray(systemTiddlerTitles,title);
|
||||
} else {
|
||||
$tw.utils.insertSortedArray(nonSystemTiddlerTitles,title);
|
||||
}
|
||||
}
|
||||
// Record the new tiddler state
|
||||
updateDescriptor["new"] = {
|
||||
tiddler: tiddler,
|
||||
@@ -1246,6 +1280,14 @@ $tw.Wiki = function(options) {
|
||||
tiddlerTitles.splice(index,1);
|
||||
}
|
||||
}
|
||||
// Delete from system/non-system partitions
|
||||
if(systemTiddlerTitles !== null) {
|
||||
var partitionArray = title.indexOf("$:/") === 0 ? systemTiddlerTitles : nonSystemTiddlerTitles;
|
||||
var partitionIndex = partitionArray.indexOf(title);
|
||||
if(partitionIndex !== -1) {
|
||||
partitionArray.splice(partitionIndex,1);
|
||||
}
|
||||
}
|
||||
// Record the new tiddler state
|
||||
updateDescriptor["new"] = {
|
||||
tiddler: this.getTiddler(title),
|
||||
@@ -1284,6 +1326,16 @@ $tw.Wiki = function(options) {
|
||||
return getTiddlerTitles().slice(0);
|
||||
};
|
||||
|
||||
// Get an array of all system tiddler titles (returns cached array; do not mutate)
|
||||
this.allSystemTitles = function() {
|
||||
return getSystemTiddlerTitles();
|
||||
};
|
||||
|
||||
// Get an array of all non-system tiddler titles (returns cached array; do not mutate)
|
||||
this.allNonSystemTitles = function() {
|
||||
return getNonSystemTiddlerTitles();
|
||||
};
|
||||
|
||||
// Iterate through all tiddler titles
|
||||
this.each = function(callback) {
|
||||
var titles = getTiddlerTitles(),
|
||||
@@ -2539,7 +2591,9 @@ $tw.boot.loadStartup = function(options){
|
||||
|
||||
// Load tiddlers
|
||||
if($tw.boot.tasks.readBrowserTiddlers) {
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-store-read-start"); }
|
||||
$tw.loadTiddlersBrowser();
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-store-read-end"); }
|
||||
} else {
|
||||
$tw.loadTiddlersNode();
|
||||
}
|
||||
@@ -2551,6 +2605,7 @@ $tw.boot.loadStartup = function(options){
|
||||
$tw.hooks.invokeHook("th-boot-tiddlers-loaded");
|
||||
};
|
||||
$tw.boot.execStartup = function(options){
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-exec-start"); }
|
||||
// Unpack plugin tiddlers
|
||||
$tw.wiki.readPluginInfo();
|
||||
$tw.wiki.registerPluginTiddlers("plugin",$tw.safeMode ? ["$:/core"] : undefined);
|
||||
@@ -2578,6 +2633,7 @@ $tw.boot.execStartup = function(options){
|
||||
$tw.boot.executedStartupModules = Object.create(null);
|
||||
$tw.boot.disabledStartupModules = $tw.boot.disabledStartupModules || [];
|
||||
// Repeatedly execute the next eligible task
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-startup-modules-start"); }
|
||||
$tw.boot.executeNextStartupTask(options.callback);
|
||||
};
|
||||
/*
|
||||
@@ -2645,6 +2701,7 @@ $tw.boot.executeNextStartupTask = function(callback) {
|
||||
}
|
||||
taskIndex++;
|
||||
}
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-boot-complete"); }
|
||||
if(typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
|
||||
+4
-1
@@ -14,6 +14,8 @@ See Boot.js for further details of the boot process.
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-bootprefix-start"); }
|
||||
|
||||
var _bootprefix = (function($tw) {
|
||||
|
||||
"use strict";
|
||||
@@ -23,7 +25,7 @@ $tw.boot = $tw.boot || Object.create(null);
|
||||
|
||||
// Config
|
||||
$tw.config = $tw.config || Object.create(null);
|
||||
$tw.config.maxEditFileSize = 100 * 1024 * 1024; // 100MB
|
||||
$tw.config.maxEditFileSize = 200 * 1024 * 1024; // 200MB
|
||||
|
||||
// Detect platforms
|
||||
if(!("browser" in $tw)) {
|
||||
@@ -121,6 +123,7 @@ return $tw;
|
||||
if(typeof(exports) === "undefined") {
|
||||
// Set up $tw global for the browser
|
||||
window.$tw = _bootprefix(window.$tw);
|
||||
if(typeof performance !== "undefined") { performance.mark("tw-bootprefix-end"); }
|
||||
} else {
|
||||
// Export functionality as a module
|
||||
exports.bootprefix = _bootprefix;
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+34
-17
@@ -261,6 +261,7 @@ exports.compileFilter = function(filterString) {
|
||||
var filterOperators = this.getFilterOperators();
|
||||
// Assemble array of functions, one for each operation
|
||||
var operationFunctions = [];
|
||||
var operationSubFunctions = []; // Unwrapped sub-functions for fast path
|
||||
// Step through the operations
|
||||
var self = this;
|
||||
$tw.utils.each(filterParseTree,function(operation) {
|
||||
@@ -289,20 +290,24 @@ exports.compileFilter = function(filterString) {
|
||||
operand.value = self.getTextReference(operand.text,"",currTiddlerTitle);
|
||||
operand.multiValue = [operand.value];
|
||||
} else if(operand.variable) {
|
||||
var varTree = $tw.utils.parseFilterVariable(operand.text);
|
||||
operand.value = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source})[0] || "";
|
||||
if(!operand._varTree) {
|
||||
operand._varTree = $tw.utils.parseFilterVariable(operand.text);
|
||||
}
|
||||
operand.value = widgetClass.evaluateVariable(widget,operand._varTree.name,{params: operand._varTree.params, source: source})[0] || "";
|
||||
operand.multiValue = [operand.value];
|
||||
} else if(operand.multiValuedVariable) {
|
||||
var varTree = $tw.utils.parseFilterVariable(operand.text);
|
||||
var resultList = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source});
|
||||
if(!operand._varTree) {
|
||||
operand._varTree = $tw.utils.parseFilterVariable(operand.text);
|
||||
}
|
||||
var resultList = widgetClass.evaluateVariable(widget,operand._varTree.name,{params: operand._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 = widgetClass.evaluateVariable(widget,operand._varTree.name,{params: operand._varTree.params, source: source}) || [];
|
||||
operand.value = operand.multiValue[0] || "";
|
||||
} else {
|
||||
operand.value = "";
|
||||
operand.multiValue = [];
|
||||
}
|
||||
operand.isMultiValueOperand = true;
|
||||
operand.isMultiValueOperand = true;
|
||||
} else {
|
||||
operand.value = operand.text;
|
||||
operand.multiValue = [operand.value];
|
||||
@@ -343,6 +348,7 @@ exports.compileFilter = function(filterString) {
|
||||
return resultArray;
|
||||
}
|
||||
};
|
||||
operationSubFunctions.push(operationSubFunction);
|
||||
var filterRunPrefixes = self.getFilterRunPrefixes();
|
||||
// Wrap the operator functions in a wrapper function that depends on the prefix
|
||||
operationFunctions.push((function() {
|
||||
@@ -372,6 +378,10 @@ exports.compileFilter = function(filterString) {
|
||||
}
|
||||
})());
|
||||
});
|
||||
// Detect single "or" run for fast path (bypass LinkedList)
|
||||
var isSingleOrRun = filterParseTree.length === 1 &&
|
||||
(!filterParseTree[0].prefix || filterParseTree[0].prefix === "");
|
||||
var singleOrSubFunction = isSingleOrRun ? operationSubFunctions[0] : null;
|
||||
// Return a function that applies the operations to a source iterator of tiddler titles
|
||||
var fnMeasured = $tw.perf.measure("filter: " + filterString,function filterFunction(source,widget) {
|
||||
if(!source) {
|
||||
@@ -382,23 +392,30 @@ exports.compileFilter = function(filterString) {
|
||||
if(!widget) {
|
||||
widget = $tw.rootWidget;
|
||||
}
|
||||
var results = new $tw.utils.LinkedList();
|
||||
self.filterRecursionCount = (self.filterRecursionCount || 0) + 1;
|
||||
var resultArray;
|
||||
if(self.filterRecursionCount < MAX_FILTER_DEPTH) {
|
||||
$tw.utils.each(operationFunctions,function(operationFunction) {
|
||||
var operationResult = operationFunction(results,source,widget);
|
||||
if(operationResult) {
|
||||
if(operationResult.variables) {
|
||||
// If the filter run prefix has returned variables, create a new fake widget with those variables
|
||||
widget = widget.makeFakeWidgetWithVariables(operationResult.variables);
|
||||
if(singleOrSubFunction) {
|
||||
// Fast path: single "or" run, return array directly without LinkedList
|
||||
resultArray = singleOrSubFunction(source,widget);
|
||||
} else {
|
||||
var results = new $tw.utils.LinkedList();
|
||||
$tw.utils.each(operationFunctions,function(operationFunction) {
|
||||
var operationResult = operationFunction(results,source,widget);
|
||||
if(operationResult) {
|
||||
if(operationResult.variables) {
|
||||
// If the filter run prefix has returned variables, create a new fake widget with those variables
|
||||
widget = widget.makeFakeWidgetWithVariables(operationResult.variables);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
resultArray = results.toArray();
|
||||
}
|
||||
} else {
|
||||
results.push("/**-- Excessive filter recursion --**/");
|
||||
resultArray = ["/**-- Excessive filter recursion --**/"];
|
||||
}
|
||||
self.filterRecursionCount = self.filterRecursionCount - 1;
|
||||
return results.toArray();
|
||||
return resultArray;
|
||||
});
|
||||
if(this.filterCacheCount >= 2000) {
|
||||
// To prevent memory leak, we maintain an upper limit for cache size.
|
||||
|
||||
@@ -13,6 +13,18 @@ Filter function for [is[shadow]]
|
||||
Export our filter function
|
||||
*/
|
||||
exports.shadow = function(source,prefix,options) {
|
||||
// Fast path: when source is wiki.each (all real tiddlers), use shadow title list
|
||||
if(source === options.wiki.each && prefix !== "!") {
|
||||
// Return real tiddlers that are also shadow tiddlers (overridden shadows)
|
||||
var results = [],
|
||||
shadowTitles = options.wiki.allShadowTitles();
|
||||
for(var i = 0, len = shadowTitles.length; i < len; i++) {
|
||||
if(options.wiki.tiddlerExists(shadowTitles[i])) {
|
||||
results.push(shadowTitles[i]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
var results = [];
|
||||
if(prefix === "!") {
|
||||
source(function(tiddler,title) {
|
||||
|
||||
@@ -13,6 +13,14 @@ Filter function for [is[system]]
|
||||
Export our filter function
|
||||
*/
|
||||
exports.system = function(source,prefix,options) {
|
||||
// Fast path: when iterating all tiddlers, use pre-partitioned arrays
|
||||
if(source === options.wiki.each) {
|
||||
if(prefix === "!") {
|
||||
return options.wiki.allNonSystemTitles();
|
||||
} else {
|
||||
return options.wiki.allSystemTitles();
|
||||
}
|
||||
}
|
||||
var results = [];
|
||||
if(prefix === "!") {
|
||||
source(function(tiddler,title) {
|
||||
|
||||
@@ -13,6 +13,13 @@ Filter function for [is[tiddler]]
|
||||
Export our filter function
|
||||
*/
|
||||
exports.tiddler = function(source,prefix,options) {
|
||||
// Fast path: wiki.each only iterates real tiddlers, all of which exist
|
||||
if(source === options.wiki.each) {
|
||||
if(prefix === "!") {
|
||||
return []; // No real tiddler fails tiddlerExists
|
||||
}
|
||||
return source; // Return iterator directly; all real tiddlers pass
|
||||
}
|
||||
var results = [];
|
||||
if(prefix === "!") {
|
||||
source(function(tiddler,title) {
|
||||
|
||||
@@ -13,6 +13,21 @@ Filter operator returning all the tags of the selected tiddlers
|
||||
Export our filter function
|
||||
*/
|
||||
exports.tags = function(source,operator,options) {
|
||||
// Fast path: cache result when iterating all tiddlers
|
||||
if(source === options.wiki.each) {
|
||||
return options.wiki.getGlobalCache("filter-tags-all-tiddlers",function() {
|
||||
var tags = {};
|
||||
source(function(tiddler,title) {
|
||||
var t, length;
|
||||
if(tiddler && tiddler.fields.tags) {
|
||||
for(t=0, length=tiddler.fields.tags.length; t<length; t++) {
|
||||
tags[tiddler.fields.tags[t]] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return Object.keys(tags);
|
||||
});
|
||||
}
|
||||
var tags = {};
|
||||
source(function(tiddler,title) {
|
||||
var t, length;
|
||||
|
||||
@@ -67,7 +67,57 @@ TagSubIndexer.prototype.rebuild = function() {
|
||||
};
|
||||
|
||||
TagSubIndexer.prototype.update = function(updateDescriptor) {
|
||||
this.index = null;
|
||||
// If the index hasn't been built yet, no update needed
|
||||
if(this.index === null) {
|
||||
return;
|
||||
}
|
||||
// Determine whether the old/new tiddler is visible to this iterator
|
||||
var oldVisible = this._isVisible(updateDescriptor.old),
|
||||
newVisible = this._isVisible(updateDescriptor["new"]),
|
||||
self = this;
|
||||
// Remove old tags from index
|
||||
if(oldVisible && updateDescriptor.old.tiddler) {
|
||||
var oldTitle = updateDescriptor.old.tiddler.fields.title,
|
||||
oldTags = updateDescriptor.old.tiddler.fields.tags || [];
|
||||
$tw.utils.each(oldTags,function(tag) {
|
||||
if(self.index[tag]) {
|
||||
var idx = self.index[tag].titles.indexOf(oldTitle);
|
||||
if(idx !== -1) {
|
||||
self.index[tag].titles.splice(idx,1);
|
||||
if(self.index[tag].titles.length === 0) {
|
||||
delete self.index[tag];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add new tags to index
|
||||
if(newVisible && updateDescriptor["new"].tiddler) {
|
||||
var newTitle = updateDescriptor["new"].tiddler.fields.title,
|
||||
newTags = updateDescriptor["new"].tiddler.fields.tags || [];
|
||||
$tw.utils.each(newTags,function(tag) {
|
||||
if(!self.index[tag]) {
|
||||
self.index[tag] = {isSorted: false, titles: [newTitle]};
|
||||
} else if(self.index[tag].titles.indexOf(newTitle) === -1) {
|
||||
self.index[tag].titles.push(newTitle);
|
||||
self.index[tag].isSorted = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Determine whether a tiddler described by a descriptor is visible to this sub-indexer's iterator
|
||||
*/
|
||||
TagSubIndexer.prototype._isVisible = function(descriptor) {
|
||||
if(this.iteratorMethod === "each") {
|
||||
return descriptor.exists;
|
||||
} else if(this.iteratorMethod === "eachShadow") {
|
||||
return descriptor.shadow;
|
||||
} else {
|
||||
// eachTiddlerPlusShadows and eachShadowPlusTiddlers both visit all tiddlers and shadows
|
||||
return descriptor.exists || descriptor.shadow;
|
||||
}
|
||||
};
|
||||
|
||||
TagSubIndexer.prototype.lookup = function(tag) {
|
||||
|
||||
@@ -327,7 +327,7 @@ exports.parseMacroParameterAsAttribute = function(source,pos) {
|
||||
// Define our regexps
|
||||
var reAttributeName = /([^\/\s>"'`=:]+)/y,
|
||||
reStrictIdentifier = /^[A-Za-z0-9\-_]+$/,
|
||||
reUnquotedAttribute = /((?:(?:>(?!>))|[^\s>"'])+)/y,
|
||||
reUnquotedAttribute = /(?!<<)((?:(?:>(?!>))|[^\s>"'])+)/y,
|
||||
reFilteredValue = /\{\{\{([\S\s]+?)\}\}\}/y,
|
||||
reIndirectValue = /\{\{([^\}]+)\}\}/y,
|
||||
reSubstitutedValue = /(?:```([\s\S]*?)```|`([^`]|[\S\s]*?)`)/y;
|
||||
@@ -576,6 +576,9 @@ exports.parseAttribute = function(source,pos) {
|
||||
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";
|
||||
|
||||
@@ -59,14 +59,48 @@ LinkedList.prototype.push = function(/* values */) {
|
||||
LinkedList.prototype.pushTop = function(value) {
|
||||
var t;
|
||||
if($tw.utils.isArray(value)) {
|
||||
for(t=0; t<value.length; t++) {
|
||||
_assertString(value[t]);
|
||||
}
|
||||
for(t=0; t<value.length; t++) {
|
||||
_removeOne(this,value[t]);
|
||||
}
|
||||
for(t=0; t<value.length; t++) {
|
||||
_linkToEnd(this,value[t]);
|
||||
if(this.length === 0) {
|
||||
// Fast path for empty list: skip removal pass
|
||||
for(t = 0; t < value.length; t++) {
|
||||
_assertString(value[t]);
|
||||
}
|
||||
var prev = null,
|
||||
useInline = true;
|
||||
for(t = 0; t < value.length; t++) {
|
||||
if(useInline) {
|
||||
var v = value[t];
|
||||
var old = this.next.get(v);
|
||||
if(old !== undefined) {
|
||||
// Duplicate found: switch to _linkToEnd for this and all remaining elements
|
||||
useInline = false;
|
||||
_linkToEnd(this,v);
|
||||
} else {
|
||||
// Inline the common case of _linkToEnd for new unique values
|
||||
this.next.set(v,null);
|
||||
this.prev.set(v,prev);
|
||||
if(prev !== null) {
|
||||
this.next.set(prev,v);
|
||||
} else {
|
||||
this.next.set(null,v);
|
||||
}
|
||||
this.prev.set(null,v);
|
||||
this.length++;
|
||||
prev = v;
|
||||
}
|
||||
} else {
|
||||
_linkToEnd(this,value[t]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for(t=0; t<value.length; t++) {
|
||||
_assertString(value[t]);
|
||||
}
|
||||
for(t=0; t<value.length; t++) {
|
||||
_removeOne(this,value[t]);
|
||||
}
|
||||
for(t=0; t<value.length; t++) {
|
||||
_linkToEnd(this,value[t]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_assertString(value);
|
||||
|
||||
+33
-6
@@ -1400,15 +1400,15 @@ exports.search = function(text,options) {
|
||||
fields.push("text");
|
||||
}
|
||||
// Function to check a given tiddler for the search term
|
||||
var searchTiddler = function(title) {
|
||||
var searchTiddler = function(tiddler,title) {
|
||||
if(!searchTermsRegExps) {
|
||||
return true;
|
||||
}
|
||||
var notYetFound = searchTermsRegExps.slice();
|
||||
|
||||
var tiddler = self.getTiddler(title);
|
||||
if(!tiddler) {
|
||||
tiddler = new $tw.Tiddler({title: title, text: "", type: "text/vnd.tiddlywiki"});
|
||||
tiddler = self.getTiddler(title);
|
||||
if(!tiddler) {
|
||||
tiddler = new $tw.Tiddler({title: title, text: "", type: "text/vnd.tiddlywiki"});
|
||||
}
|
||||
}
|
||||
var contentTypeInfo = $tw.config.contentTypeInfo[tiddler.fields.type] || $tw.config.contentTypeInfo["text/vnd.tiddlywiki"],
|
||||
searchFields;
|
||||
@@ -1424,6 +1424,33 @@ exports.search = function(text,options) {
|
||||
} else {
|
||||
searchFields = fields;
|
||||
}
|
||||
// Fast path for single search term (avoids array slice/splice per tiddler)
|
||||
if(searchTermsRegExps.length === 1) {
|
||||
var singleRegExp = searchTermsRegExps[0];
|
||||
for(var fieldIndex=0; fieldIndex<searchFields.length; fieldIndex++) {
|
||||
var fieldName = searchFields[fieldIndex];
|
||||
if(fieldName === "text" && contentTypeInfo.encoding !== "utf8") {
|
||||
continue;
|
||||
}
|
||||
var str = tiddler.fields[fieldName];
|
||||
if(str) {
|
||||
if($tw.utils.isArray(str)) {
|
||||
for(var s=0; s<str.length; s++) {
|
||||
if(singleRegExp.test(str[s])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
str = tiddler.getFieldString(fieldName);
|
||||
if(singleRegExp.test(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
var notYetFound = searchTermsRegExps.slice();
|
||||
for(var fieldIndex=0; notYetFound.length>0 && fieldIndex<searchFields.length; fieldIndex++) {
|
||||
// Don't search the text field if the content type is binary
|
||||
var fieldName = searchFields[fieldIndex];
|
||||
@@ -1463,7 +1490,7 @@ exports.search = function(text,options) {
|
||||
var results = [],
|
||||
source = options.source || this.each;
|
||||
source(function(tiddler,title) {
|
||||
if(searchTiddler(title) !== invert) {
|
||||
if(searchTiddler(tiddler,title) !== invert) {
|
||||
results.push(title);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
title: $:/config/OfficialPluginLibrary
|
||||
tags: $:/tags/PluginLibrary
|
||||
url: https://tiddlywiki.com/library/v5.4.0/index.html
|
||||
url: https://tiddlywiki.com/library/v5.5.0/index.html
|
||||
caption: {{$:/language/OfficialPluginLibrary}}
|
||||
|
||||
{{$:/language/OfficialPluginLibrary/Hint}}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "Performance measurement edition",
|
||||
"plugins": [
|
||||
"tiddlywiki/performance"
|
||||
],
|
||||
"themes": [
|
||||
"tiddlywiki/vanilla",
|
||||
"tiddlywiki/snowwhite"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
title: $:/config/LocalPluginLibrary
|
||||
tags: $:/tags/PluginLibrary
|
||||
url: http://127.0.0.1:8080/prerelease/library/v5.4.0/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.0/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.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"tiddlywiki/jszip",
|
||||
"tiddlywiki/confetti",
|
||||
"tiddlywiki/tour",
|
||||
"tiddlywiki/performance",
|
||||
"tiddlywiki/dom-to-image"
|
||||
],
|
||||
"themes": [
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/MacrocallColonNamed
|
||||
description: Macrocall named-parameter syntax using colon continues to work
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<<x d:"hi">>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=hi</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/MacrocallColonNonStrict
|
||||
description: Colon-separator requires a strict identifier name; otherwise value is treated as positional (fix #9788)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<<x $:/a:"v">>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=<a class="tc-tiddlylink tc-tiddlylink-missing" href="#%24%3A%2Fa">$:/a</a>:</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/MacrocallEqualsNoValue
|
||||
description: Macrocall named parameter with =-separator but no value fails to parse the macro call
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<<x d=>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p><<x d="true">></x></p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/MacrocallNamedEquals
|
||||
description: Macrocall attribute with =-separator (restored compact call syntax, v5.3.9+)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<<x d="hello">>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=hello</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/MacrocallFilteredValue
|
||||
description: Macrocall attribute value can be a filtered value {{{...}}}
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<<x d={{{[[hi]]}}}>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=hi</p>
|
||||
@@ -0,0 +1,17 @@
|
||||
title: Parse/BackCompat/MacrocallIndirectValue
|
||||
description: Macrocall attribute value can be an indirect value {{tid!!field}}
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<<x d={{Other!!foo}}>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=FROM-OTHER</p>
|
||||
+
|
||||
title: Other
|
||||
foo: FROM-OTHER
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
title: Parse/BackCompat/MacrocallMvvValue
|
||||
description: Macrocall attribute value can be an MVV reference ((v))
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define d() DEFD
|
||||
\define x(d) got=$d$
|
||||
<<x d=((d))>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=DEFD</p>
|
||||
@@ -0,0 +1,14 @@
|
||||
title: Parse/BackCompat/MacrocallNestedMacroValue
|
||||
description: Macrocall attribute value can be a nested macro invocation
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define d() DEFD
|
||||
\define x(d) got=$d$
|
||||
<<x d=<<d>>>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=DEFD</p>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/MacrocallSubstitutedValue
|
||||
description: Macrocall attribute value can be a substituted value `...`
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<<x d=`hi`>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=hi</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/MacrocallPositionalGt
|
||||
description: Bare > (not followed by >) is permitted in unquoted macrocall positional params
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<<x foo>val>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=foo>val</p>
|
||||
@@ -0,0 +1,17 @@
|
||||
title: Parse/BackCompat/MalformedMultiLineTerminator
|
||||
description: When << value looks like a macro invocation but fails to parse, the enclosing widget tag must fail cleanly so the remainder of the document is not silently reinterpreted (the v5.4 strict parser previously collapsed to attribute="true" and swallowed subsequent lines as stray attributes)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define d() DEFD
|
||||
\define x(d) got=$d$
|
||||
<$macrocall $name="x" d=<<d> />
|
||||
<$macrocall $name="x" d=<<d> />
|
||||
>>
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p><$macrocall $name="x" d=<<d> />
|
||||
<$macrocall $name="x" d=DEFD</d></p>
|
||||
@@ -0,0 +1,14 @@
|
||||
title: Parse/BackCompat/QuotedGtInNestedMacro
|
||||
description: Nested macro invocation in widget attribute can contain > in quoted inner attribute values
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define d() DEFD
|
||||
\define x(d) got=$d$
|
||||
<$macrocall $name="x" d=<<d b="x>y">> />
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=DEFD</p>
|
||||
@@ -0,0 +1,14 @@
|
||||
title: Parse/BackCompat/MalformedNoTerminator
|
||||
description: Widget attribute value starting with << but with no >> ahead falls through to "true" (matches v5.3.8)
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define d() DEFD
|
||||
\define x(d) got=$d$
|
||||
<$macrocall $name="x" d=<<d> />
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=true</p>
|
||||
@@ -0,0 +1,14 @@
|
||||
title: Parse/BackCompat/ValidNestedMacro
|
||||
description: Widget attribute value with well-formed nested macro invocation
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define d() DEFD
|
||||
\define x(d) got=$d$
|
||||
<$macrocall $name="x" d=<<d>> />
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=DEFD</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/WidgetAttrEqualsNoValue
|
||||
description: Widget attribute with trailing equals and no value falls through to "true"
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<$macrocall $name="x" d= />
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=true</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/WidgetAttrMalformedMvv
|
||||
description: Widget attribute value with unclosed ((v) falls through to a string literal
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<$macrocall $name="x" d=((d) />
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=((d)</p>
|
||||
@@ -0,0 +1,14 @@
|
||||
title: Parse/BackCompat/WidgetAttrMvvValue
|
||||
description: Widget attribute value can be an MVV reference ((v))
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define d() DEFD
|
||||
\define x(d) got=$d$
|
||||
<$macrocall $name="x" d=((d)) />
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=DEFD</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Parse/BackCompat/WidgetAttrSubstitutedValue
|
||||
description: Widget attribute value can be a substituted value `...`
|
||||
type: text/vnd.tiddlywiki-multiple
|
||||
tags: [[$:/tags/wiki-test-spec]]
|
||||
|
||||
title: Output
|
||||
|
||||
\define x(d) got=$d$
|
||||
<$macrocall $name="x" d=`hi` />
|
||||
+
|
||||
title: ExpectedResult
|
||||
|
||||
<p>got=hi</p>
|
||||
@@ -468,5 +468,9 @@ describe("WikiText parser tests", function() {
|
||||
|
||||
expect(parse(wikitext)).toEqual(expectedParseTree);
|
||||
});
|
||||
});
|
||||
|
||||
it("should reject unquoted macro parameter values that start with <<", function() {
|
||||
var attribute = $tw.utils.parseMacroParameterAsAttribute("d=<<d> />",0);
|
||||
expect(attribute).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ title: TiddlyWiki Archive
|
||||
5.1.20 5.1.21 5.1.22 5.1.23
|
||||
5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7
|
||||
5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8
|
||||
5.4.0
|
||||
\end
|
||||
|
||||
Older versions of TiddlyWiki are available in the [[archive|https://github.com/TiddlyWiki/tiddlywiki.com-gh-pages/tree/master/archive]]:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
created: 20130822170200000
|
||||
icon: $:/core/icon
|
||||
list: [[A Gentle Guide to TiddlyWiki]] [[Discover TiddlyWiki]] [[Some of the things you can do with TiddlyWiki]] [[Ten reasons to switch to TiddlyWiki]] Examples [[What happened to the original TiddlyWiki?]]
|
||||
modified: 20250807084952911
|
||||
modified: 20260420192600833
|
||||
tags: Welcome
|
||||
title: HelloThere
|
||||
type: text/vnd.tiddlywiki
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
created: 20150414070451144
|
||||
list: [[HelloThumbnail - Donations]] [[HelloThumbnail - Newsletter]] [[HelloThumbnail - Community Survey 2025]] [[HelloThumbnail - Introduction Video]] [[HelloThumbnail - Grok TiddlyWiki]] [[HelloThumbnail - Latest Version]] [[HelloThumbnail - MultiWikiServer]] [[HelloThumbnail - Twenty Years of TiddlyWiki]] [[HelloThumbnail - TiddlyWiki Privacy]] [[HelloThumbnail - Marketplace]] [[HelloThumbnail - Intertwingled Innovations]] [[HelloThumbnail - TiddlyWikiLinks]]
|
||||
list: [[HelloThumbnail - Latest Version]] [[HelloThumbnail - Donations]] [[HelloThumbnail - Newsletter]] [[HelloThumbnail - Community Survey 2025]] [[HelloThumbnail - Introduction Video]] [[HelloThumbnail - Grok TiddlyWiki]] [[HelloThumbnail - MultiWikiServer]] [[HelloThumbnail - Twenty Years of TiddlyWiki]] [[HelloThumbnail - TiddlyWiki Privacy]] [[HelloThumbnail - Marketplace]] [[HelloThumbnail - Intertwingled Innovations]] [[HelloThumbnail - TiddlyWikiLinks]]
|
||||
modified: 20150414070948246
|
||||
title: HelloThumbnail
|
||||
type: text/vnd.tiddlywiki
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 19 KiB |
@@ -2,7 +2,7 @@ change-category: hackability
|
||||
change-type: enhancement
|
||||
created: 20260228212206750
|
||||
description: Allows modular relinking behavior for plugin support
|
||||
github-contributors: flibbles
|
||||
github-contributors: Flibbles
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9703
|
||||
modified: 20260228212206750
|
||||
release: 5.4.0
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
title: $:/changenotes/5.4.0/#9782
|
||||
description: Update Greek translation
|
||||
release: 5.4.0
|
||||
tags: $:/tags/ChangeNote
|
||||
change-type: enhancement
|
||||
change-category: translation
|
||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9782
|
||||
github-contributors: superuser-does
|
||||
|
||||
* Translated all new text introduced in 5.4.0
|
||||
* Changed tab captions which weren't proper nouns from __T__itle __C__ase to __S__entence __c__ase
|
||||
* Lowercased export dialog options to match that section of the UI, like in the original English version
|
||||
* Other minor changes and corrections
|
||||
@@ -1,10 +1,11 @@
|
||||
caption: 5.4.0
|
||||
created: 20250901000000000
|
||||
modified: 20250901000000000
|
||||
created: 20260420192600833
|
||||
description: Multi Valued Variables, Nested Procedure Calls, Background Actions, New Wikitext Serializer, Bugfixes and much more
|
||||
modified: 20260420192600833
|
||||
released: 20260420192600833
|
||||
tags: ReleaseNotes
|
||||
title: Release 5.4.0
|
||||
type: text/vnd.tiddlywiki
|
||||
description: Under development
|
||||
|
||||
\procedure release-introduction()
|
||||
Release v5.4.0 deliberately and forensically loosens backwards compatibility to clear the path for significant new features and fundamental improvements to be made in the future.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
caption: 5.5.0
|
||||
created: 20260420195917090
|
||||
modified: 20260420195917090
|
||||
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>>
|
||||
@@ -1,13 +1,13 @@
|
||||
title: Multi-Valued Variables
|
||||
created: 20250307212252946
|
||||
modified: 20250307212252946
|
||||
tags: Concepts Variables
|
||||
title: Multi-Valued Variables
|
||||
|
||||
<<.from-version "5.4.0">> In ordinary usage, [[variables|Variables]] contain a single snippet of text. With the introduction of multi-valued variables. it is now possible to store a list of multiple values in a single variable. When accessed in the usual way, only the first value is returned, but using round brackets instead of angle brackets around the variable name allows access to the complete list of the values. This makes multi-valued variables largely invisible unless you specifically need to use them.
|
||||
|
||||
! Setting Multi-Valued Variables
|
||||
|
||||
Generally, all the methods for setting variables implicitly set multi-valued variables, with the exception of the [[Set Widget|Set Widget]].
|
||||
Generally, all the methods for setting variables implicitly set multi-valued variables, with the exception of the [[Set Widget|SetWidget]].
|
||||
|
||||
!! LetWidget
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ Listing/Preview/TextRaw: Text - roh
|
||||
Listing/Preview/Fields: Felder
|
||||
Listing/Preview/Diff: Diff - Text
|
||||
Listing/Preview/DiffFields: Diff - Felder
|
||||
Listing/ImportOptions/Caption: Import Optionen
|
||||
Listing/ImportOptions/NoMatch: Keine Optionen aktiv für diese Files.
|
||||
Listing/Rename/Tooltip: Tiddler vorm Importieren umbenennen
|
||||
Listing/Rename/Prompt: Umbenennen in:
|
||||
Listing/Rename/ConfirmRename : Tiddler umbenennen
|
||||
@@ -31,4 +33,4 @@ Upgrader/System/Alert: Sie sind dabei einen Tiddler zu importieren, der einen "C
|
||||
Upgrader/ThemeTweaks/Created: Migrieren der "theme tweaks" von: <$text text=<<from>>/>.
|
||||
Upgrader/Tiddler/Disabled: Deaktivierter Tiddler.
|
||||
Upgrader/Tiddler/Selected: Ausgewählter Tiddler.
|
||||
Upgrader/Tiddler/Unselected: Auswahl aufgehoben.
|
||||
Upgrader/Tiddler/Unselected: Auswahl aufgehoben.
|
||||
|
||||
@@ -9,10 +9,10 @@ Advanced/ShadowInfo/NotShadow/Hint: Der Tiddler: <$link to=<<infoTiddler>>><$tex
|
||||
Advanced/ShadowInfo/Shadow/Hint: Der Tiddler: <$link to=<<infoTiddler>>><$text text=<<infoTiddler>>/></$link> ist ein Schatten-Tiddler.
|
||||
Advanced/ShadowInfo/Shadow/Source: Er ist definiert im Plugin: <$link to=<<pluginTiddler>>><$text text=<<pluginTiddler>>/></$link>.
|
||||
Advanced/ShadowInfo/OverriddenShadow/Hint: Der originale Schatten-Tiddler wurde durch diesen Tiddler überschrieben. Wenn Sie diesen Tiddler löschen, wird der originale Schatten-Tiddler wieder aktiv. Erstellen Sie vorher eventuell eine Sicherungskopie!
|
||||
Advanced/CascadeInfo/Heading: Kascade Details
|
||||
Advanced/CascadeInfo/Hint: ViewTemplate Kaskade - Filter Segmente getagged: <<tag "$:/tags/ViewTemplate">>.
|
||||
Advanced/CascadeInfo/Heading: Kaskade Details
|
||||
Advanced/CascadeInfo/Hint: ViewTemplate Kaskade - Filter Segmente getagged: <<tag "$:/tags/ViewTemplate">> sind aktiv.
|
||||
Advanced/CascadeInfo/Detail/View: Ansicht
|
||||
Advanced/CascadeInfo/Detail/ActiveCascadeFilter: Filter - Aktive Kascade
|
||||
Advanced/CascadeInfo/Detail/ActiveCascadeFilter: Filter - Aktive Kaskade
|
||||
Advanced/CascadeInfo/Detail/Template: Template
|
||||
Fields/Caption: Felder
|
||||
List/Caption: Liste
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "tiddlywiki",
|
||||
"version": "5.4.0-prerelease",
|
||||
"version": "5.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tiddlywiki",
|
||||
"version": "5.4.0-prerelease",
|
||||
"version": "5.4.0",
|
||||
"license": "BSD",
|
||||
"bin": {
|
||||
"tiddlywiki": "tiddlywiki.js"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tiddlywiki",
|
||||
"preferGlobal": true,
|
||||
"version": "5.4.0-prerelease",
|
||||
"version": "5.5.0-prerelease",
|
||||
"author": "Jeremy Ruston <jeremy@jermolene.com>",
|
||||
"description": "a non-linear personal web notebook",
|
||||
"contributors": [
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/performance/perf-replay-command.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Command to replay a recorded performance timeline against the current wiki
|
||||
|
||||
Usage: --perf-replay <timeline.json> [--no-coalesce]
|
||||
|
||||
Loads the wiki (use --load before this command), builds a widget tree
|
||||
using fakeDocument, then replays the recorded store modifications
|
||||
batch by batch, measuring refresh performance for each batch.
|
||||
|
||||
\*/
|
||||
|
||||
"use strict";
|
||||
|
||||
exports.info = {
|
||||
name: "perf-replay",
|
||||
synchronous: false
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
var self = this,
|
||||
fs = require("fs"),
|
||||
path = require("path"),
|
||||
wiki = this.commander.wiki,
|
||||
widget = require("$:/core/modules/widgets/widget.js");
|
||||
// Parse parameters
|
||||
if(this.params.length < 1) {
|
||||
return "Missing timeline filename. Usage: --perf-replay <timeline.json>";
|
||||
}
|
||||
var timelinePath = this.params[0];
|
||||
// Load timeline
|
||||
var timelineData;
|
||||
try {
|
||||
timelineData = JSON.parse(fs.readFileSync(timelinePath,"utf8"));
|
||||
} catch(e) {
|
||||
return "Error reading timeline file: " + e.message;
|
||||
}
|
||||
if(!Array.isArray(timelineData) || timelineData.length === 0) {
|
||||
return "Timeline file is empty or invalid";
|
||||
}
|
||||
// Count tiddlers in wiki
|
||||
var tiddlerCount = 0;
|
||||
wiki.each(function() { tiddlerCount++; });
|
||||
// Build a widget tree against fakeDocument (mirroring what render.js does in the browser)
|
||||
var PAGE_TEMPLATE_TITLE = "$:/core/ui/RootTemplate";
|
||||
// Create root widget
|
||||
var rootWidget = new widget.widget({
|
||||
type: "widget",
|
||||
children: []
|
||||
},{
|
||||
wiki: wiki,
|
||||
document: $tw.fakeDocument
|
||||
});
|
||||
// Enable performance instrumentation
|
||||
var perf = new $tw.Performance(true);
|
||||
// Wrap filter execution with perf measurement
|
||||
var origCompileFilter = wiki.compileFilter;
|
||||
var filterInvocations = 0;
|
||||
wiki.compileFilter = function(filterString) {
|
||||
var compiledFilter = origCompileFilter.call(wiki,filterString);
|
||||
return perf.measure("filter: " + filterString.substring(0,80),function(source,widget) {
|
||||
filterInvocations++;
|
||||
return compiledFilter.call(this,source,widget);
|
||||
});
|
||||
};
|
||||
// Re-initialise parsers so filters get wrapped
|
||||
wiki.clearCache(null);
|
||||
// Build and render the page widget tree
|
||||
var pageWidgetNode = wiki.makeTranscludeWidget(PAGE_TEMPLATE_TITLE,{
|
||||
document: $tw.fakeDocument,
|
||||
parentWidget: rootWidget,
|
||||
recursionMarker: "no"
|
||||
});
|
||||
var pageContainer = $tw.fakeDocument.createElement("div");
|
||||
var renderStart = $tw.utils.timer();
|
||||
pageWidgetNode.render(pageContainer,null);
|
||||
var renderTime = $tw.utils.timer(renderStart);
|
||||
// Link root widget
|
||||
rootWidget.domNodes = [pageContainer];
|
||||
rootWidget.children = [pageWidgetNode];
|
||||
// Group timeline events by batch
|
||||
var batches = [];
|
||||
var currentBatch = null;
|
||||
$tw.utils.each(timelineData,function(event) {
|
||||
if(!currentBatch || event.batch !== currentBatch.batchId) {
|
||||
currentBatch = {batchId: event.batch, events: []};
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
currentBatch.events.push(event);
|
||||
});
|
||||
// Replay each batch and measure refresh
|
||||
var results = [],
|
||||
totalRefreshTime = 0,
|
||||
totalFilterInvocations = 0;
|
||||
self.commander.streams.output.write("\nPerformance Timeline Replay\n");
|
||||
self.commander.streams.output.write("==========================\n");
|
||||
self.commander.streams.output.write("Wiki: " + tiddlerCount + " tiddlers\n");
|
||||
self.commander.streams.output.write("Timeline: " + timelineData.length + " operations in " + batches.length + " batches\n");
|
||||
self.commander.streams.output.write("Initial render: " + renderTime.toFixed(2) + "ms\n\n");
|
||||
self.commander.streams.output.write(padRight("Batch",8) + padRight("Ops",6) + padRight("Changed",10) +
|
||||
padRight("Refresh(ms)",14) + padRight("Filters",10) + "Tiddlers Changed\n");
|
||||
self.commander.streams.output.write(padRight("-----",8) + padRight("---",6) + padRight("-------",10) +
|
||||
padRight("-----------",14) + padRight("-------",10) + "----------------\n");
|
||||
$tw.utils.each(batches,function(batch,index) {
|
||||
// Apply all operations in this batch directly (bypassing the intercepted addTiddler
|
||||
// to avoid re-recording)
|
||||
var changedTiddlers = Object.create(null);
|
||||
$tw.utils.each(batch.events,function(event) {
|
||||
if(event.op === "add") {
|
||||
wiki.addTiddler(new $tw.Tiddler(event.fields));
|
||||
changedTiddlers[event.title] = {modified: true};
|
||||
} else if(event.op === "delete") {
|
||||
wiki.deleteTiddler(event.title);
|
||||
changedTiddlers[event.title] = {deleted: true};
|
||||
}
|
||||
});
|
||||
// Measure refresh
|
||||
filterInvocations = 0;
|
||||
var refreshStart = $tw.utils.timer();
|
||||
pageWidgetNode.refresh(changedTiddlers);
|
||||
var refreshTime = $tw.utils.timer(refreshStart);
|
||||
totalRefreshTime += refreshTime;
|
||||
totalFilterInvocations += filterInvocations;
|
||||
var changedTitles = Object.keys(changedTiddlers);
|
||||
var titlesDisplay = changedTitles.slice(0,3).join(", ");
|
||||
if(changedTitles.length > 3) {
|
||||
titlesDisplay += " (+" + (changedTitles.length - 3) + " more)";
|
||||
}
|
||||
results.push({
|
||||
batch: index + 1,
|
||||
ops: batch.events.length,
|
||||
changed: changedTitles.length,
|
||||
refreshMs: refreshTime,
|
||||
filters: filterInvocations,
|
||||
tiddlers: changedTitles
|
||||
});
|
||||
self.commander.streams.output.write(
|
||||
padRight(String(index + 1),8) +
|
||||
padRight(String(batch.events.length),6) +
|
||||
padRight(String(changedTitles.length),10) +
|
||||
padRight(refreshTime.toFixed(2),14) +
|
||||
padRight(String(filterInvocations),10) +
|
||||
titlesDisplay + "\n"
|
||||
);
|
||||
});
|
||||
// Summary statistics
|
||||
var refreshTimes = results.map(function(r) { return r.refreshMs; }).sort(function(a,b) { return a - b; });
|
||||
var p50 = percentile(refreshTimes,50);
|
||||
var p95 = percentile(refreshTimes,95);
|
||||
var p99 = percentile(refreshTimes,99);
|
||||
var maxRefresh = refreshTimes[refreshTimes.length - 1] || 0;
|
||||
var meanRefresh = batches.length > 0 ? totalRefreshTime / batches.length : 0;
|
||||
self.commander.streams.output.write("\nSummary\n");
|
||||
self.commander.streams.output.write("-------\n");
|
||||
self.commander.streams.output.write("Initial render: " + renderTime.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("Total refresh time: " + totalRefreshTime.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("Mean refresh: " + meanRefresh.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("P50 refresh: " + p50.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("P95 refresh: " + p95.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("P99 refresh: " + p99.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("Max refresh: " + maxRefresh.toFixed(2) + "ms\n");
|
||||
self.commander.streams.output.write("Total filters run: " + totalFilterInvocations + "\n");
|
||||
// Output filter breakdown
|
||||
self.commander.streams.output.write("\nTop Filter Execution Times\n");
|
||||
self.commander.streams.output.write("--------------------------\n");
|
||||
var measures = perf.measures;
|
||||
var orderedMeasures = Object.keys(measures).sort(function(a,b) {
|
||||
return measures[b].time - measures[a].time;
|
||||
}).slice(0,20);
|
||||
$tw.utils.each(orderedMeasures,function(name) {
|
||||
var m = measures[name];
|
||||
self.commander.streams.output.write(
|
||||
padRight(m.time.toFixed(2) + "ms",14) +
|
||||
padRight(String(m.invocations) + "x",10) +
|
||||
padRight((m.time / m.invocations).toFixed(3) + "ms avg",16) +
|
||||
name + "\n"
|
||||
);
|
||||
});
|
||||
// Write JSON results
|
||||
var jsonResultPath = timelinePath.replace(/\.json$/,"") + "-results.json";
|
||||
try {
|
||||
fs.writeFileSync(jsonResultPath,JSON.stringify({
|
||||
wiki: {tiddlerCount: tiddlerCount},
|
||||
timeline: {operations: timelineData.length, batches: batches.length},
|
||||
initialRender: renderTime,
|
||||
summary: {
|
||||
totalRefreshTime: totalRefreshTime,
|
||||
meanRefresh: meanRefresh,
|
||||
p50: p50,
|
||||
p95: p95,
|
||||
p99: p99,
|
||||
maxRefresh: maxRefresh,
|
||||
totalFilterInvocations: totalFilterInvocations
|
||||
},
|
||||
batches: results,
|
||||
topFilters: orderedMeasures.map(function(name) {
|
||||
return {name: name, time: measures[name].time, invocations: measures[name].invocations};
|
||||
})
|
||||
},null,"\t"),"utf8");
|
||||
self.commander.streams.output.write("\nDetailed results written to: " + jsonResultPath + "\n");
|
||||
} catch(e) {
|
||||
self.commander.streams.output.write("\nWarning: Could not write results file: " + e.message + "\n");
|
||||
}
|
||||
self.callback(null);
|
||||
return null;
|
||||
};
|
||||
|
||||
function percentile(sortedArray,p) {
|
||||
if(sortedArray.length === 0) return 0;
|
||||
var index = Math.ceil(sortedArray.length * p / 100) - 1;
|
||||
return sortedArray[Math.max(0,index)];
|
||||
}
|
||||
|
||||
function padRight(str,width) {
|
||||
while(str.length < width) {
|
||||
str += " ";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
exports.Command = Command;
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "$:/plugins/tiddlywiki/performance",
|
||||
"name": "Performance",
|
||||
"description": "Record and replay wiki store modifications for performance testing",
|
||||
"list": "readme ui",
|
||||
"stability": "STABILITY_1_EXPERIMENTAL"
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
title: $:/plugins/tiddlywiki/performance/readme
|
||||
|
||||
! Performance Testing Plugin
|
||||
|
||||
This plugin provides a framework for measuring the performance of TiddlyWiki's refresh cycle — the process that updates the display when tiddlers are modified.
|
||||
|
||||
The idea is to capture a realistic workload by recording store modifications while a user interacts with a wiki in the browser, and then replaying those modifications under Node.js where the refresh cycle can be precisely measured in isolation.
|
||||
|
||||
!! Motivation
|
||||
|
||||
An important motivation for this framework is to enable LLMs to iteratively optimise TiddlyWiki's performance. The workflow is:
|
||||
|
||||
# An LLM makes a change to the TiddlyWiki codebase (e.g. optimising a filter operator, caching a computation, or restructuring a widget's refresh logic)
|
||||
# The LLM runs `--perf-replay` against a recorded timeline to measure the impact
|
||||
# The LLM reads the JSON results file to determine whether the change improved, regressed, or had no effect on performance
|
||||
# The LLM iterates: tries another approach, measures again, and converges on the best solution
|
||||
|
||||
This tight edit-measure-iterate loop works because `--perf-replay` runs entirely under Node.js with no browser required, produces machine-readable JSON output, and completes in seconds.
|
||||
|
||||
!! How It Works
|
||||
|
||||
The framework has two parts:
|
||||
|
||||
!!! 1. Recording (Browser)
|
||||
|
||||
The plugin intercepts `wiki.addTiddler()` and `wiki.deleteTiddler()` to capture every store modification as it happens. Each operation is recorded with:
|
||||
|
||||
* A sequence number and high-resolution timestamp
|
||||
* The full tiddler fields (so the exact state can be recreated)
|
||||
* A batch identifier that tracks TiddlyWiki's change batching via `$tw.utils.nextTick()`
|
||||
|
||||
The batch tracking is important because TiddlyWiki groups multiple store changes that occur in the same tick into a single refresh cycle. The recorder preserves these batch boundaries so that playback triggers the same pattern of refreshes.
|
||||
|
||||
!!! 2. Playback (Node.js)
|
||||
|
||||
The `--perf-replay` command loads a wiki and builds the full widget tree using TiddlyWiki's `$tw.fakeDocument` — the lightweight DOM implementation used for server-side rendering. It then replays the recorded timeline batch by batch, calling `widgetNode.refresh(changedTiddlers)` after each batch and measuring how long it takes.
|
||||
|
||||
This means we are measuring TiddlyWiki's own refresh logic (widget tree traversal, filter evaluation, DOM diffing) in isolation from browser layout and paint. This is intentional — it lets us identify performance bottlenecks within TiddlyWiki itself, independent of which browser is being used.
|
||||
|
||||
!! Why Store-Level Recording?
|
||||
|
||||
An alternative would be to record DOM events (clicks, keystrokes) and replay them in a headless browser. Store-level recording was chosen instead because:
|
||||
|
||||
* The refresh cycle responds to ''store changes'', not DOM events — store modifications are the natural input
|
||||
* Store changes are fully deterministic and reproducible
|
||||
* No DOM dependency means playback works in pure Node.js with no headless browser to install
|
||||
* A headless browser would add its own overhead, making measurements less precise
|
||||
|
||||
!! Recording
|
||||
|
||||
# Include this plugin in your wiki
|
||||
# Open the Control Panel and find the "Performance Testing Recorder" tab
|
||||
# Click "Start Recording"
|
||||
# Interact with the wiki — open tiddlers, edit, type, navigate, switch tabs
|
||||
# Click "Stop Recording"
|
||||
# Download the `timeline.json` file
|
||||
|
||||
!!! Draft Coalescing
|
||||
|
||||
When editing a tiddler, TiddlyWiki writes to draft tiddlers on every keystroke. By default, the recorder coalesces rapid draft updates within the same batch, keeping only the last update. This produces a more compact timeline that focuses on the refresh-relevant changes.
|
||||
|
||||
Uncheck "Coalesce rapid draft updates" to record every individual keystroke. This is useful when you specifically want to measure the performance impact of rapid typing.
|
||||
|
||||
!! Playback
|
||||
|
||||
```
|
||||
tiddlywiki editions/performance --load mywiki.html --perf-replay timeline.json
|
||||
```
|
||||
|
||||
Or from any edition that includes this plugin:
|
||||
|
||||
```
|
||||
tiddlywiki myedition --perf-replay timeline.json
|
||||
```
|
||||
|
||||
Playback runs at full speed with no delays between batches. The recorded timestamps are preserved in the timeline for reference but are not used for pacing.
|
||||
|
||||
!! What Gets Measured
|
||||
|
||||
* ''Initial render time'' — the time to build and render the full widget tree from scratch
|
||||
* ''Refresh time per batch'' — the time `widgetNode.refresh(changedTiddlers)` takes for each batch of store modifications
|
||||
* ''Filter execution'' — individual filter timings and invocation counts, showing which filters are the most expensive
|
||||
* ''Statistical summary'' — mean, P50, P95, P99, and maximum refresh times across all batches
|
||||
|
||||
!! Output
|
||||
|
||||
The command produces two forms of output:
|
||||
|
||||
!!! Text Report (stdout)
|
||||
|
||||
A human-readable table printed to the console showing per-batch timings, a summary with percentile statistics, and a breakdown of the most expensive filter executions.
|
||||
|
||||
!!! JSON Results File
|
||||
|
||||
A `<timeline-name>-results.json` file is written alongside the input timeline. This is the primary output for automated consumption. The file contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"wiki": {
|
||||
"tiddlerCount": 2076
|
||||
},
|
||||
"timeline": {
|
||||
"operations": 156,
|
||||
"batches": 42
|
||||
},
|
||||
"initialRender": 55.46,
|
||||
"summary": {
|
||||
"totalRefreshTime": 234.5,
|
||||
"meanRefresh": 5.58,
|
||||
"p50": 4.12,
|
||||
"p95": 18.7,
|
||||
"p99": 31.2,
|
||||
"maxRefresh": 31.2,
|
||||
"totalFilterInvocations": 4821
|
||||
},
|
||||
"batches": [
|
||||
{
|
||||
"batch": 1,
|
||||
"ops": 1,
|
||||
"changed": 1,
|
||||
"refreshMs": 12.3,
|
||||
"filters": 293,
|
||||
"tiddlers": ["$:/StoryList"]
|
||||
}
|
||||
],
|
||||
"topFilters": [
|
||||
{
|
||||
"name": "filter: [subfilter{$:/core/config/GlobalImportFilter}]",
|
||||
"time": 5.65,
|
||||
"invocations": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All times are in milliseconds. The key fields for automated analysis:
|
||||
|
||||
* `summary.totalRefreshTime` — the single most important number: total time spent in refresh across all batches
|
||||
* `summary.meanRefresh` — average refresh time per batch
|
||||
* `summary.p95` / `summary.p99` — tail latency indicators
|
||||
* `initialRender` — time to build the widget tree from scratch (measures startup cost)
|
||||
* `batches[].refreshMs` — per-batch breakdown, useful for identifying which user actions are expensive
|
||||
* `topFilters[]` — the most expensive filters by total execution time, useful for identifying optimisation targets
|
||||
|
||||
!! Example: LLM Optimisation Workflow
|
||||
|
||||
An LLM optimising TiddlyWiki performance would follow this pattern:
|
||||
|
||||
!!! Step 1: Establish baseline
|
||||
|
||||
```
|
||||
node ./tiddlywiki.js editions/performance --load mywiki.html --perf-replay timeline.json
|
||||
```
|
||||
|
||||
Read `timeline-results.json` and note the baseline `summary.totalRefreshTime`.
|
||||
|
||||
!!! Step 2: Make a change
|
||||
|
||||
Edit a source file (e.g. optimise a filter operator in `core/modules/filters/`).
|
||||
|
||||
!!! Step 3: Measure impact
|
||||
|
||||
Run the same `--perf-replay` command again and read the new `timeline-results.json`.
|
||||
|
||||
!!! Step 4: Compare
|
||||
|
||||
Compare `summary.totalRefreshTime` and `summary.p95` between baseline and new results. If improved, keep the change. If regressed, revert and try a different approach.
|
||||
|
||||
!!! Step 5: Iterate
|
||||
|
||||
Repeat steps 2-4 until the target metric is optimised.
|
||||
|
||||
The JSON results file makes step 4 straightforward — an LLM can read two JSON files and compare numeric fields directly without parsing tabular text output.
|
||||
|
||||
!! Timeline Format
|
||||
|
||||
The timeline is a JSON array of operations:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"seq": 0,
|
||||
"t": 123.45,
|
||||
"batch": 0,
|
||||
"op": "add",
|
||||
"title": "$:/StoryList",
|
||||
"isDraft": false,
|
||||
"fields": {
|
||||
"title": "$:/StoryList",
|
||||
"list": "GettingStarted",
|
||||
"text": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
* `seq` — sequential operation number
|
||||
* `t` — milliseconds since recording started
|
||||
* `batch` — batch identifier (operations in the same batch trigger a single refresh)
|
||||
* `op` — `"add"` or `"delete"`
|
||||
* `isDraft` — whether this is a draft tiddler (used for coalescing)
|
||||
* `fields` — complete tiddler fields (null for delete operations)
|
||||
@@ -0,0 +1,143 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/performance/recorder.js
|
||||
type: application/javascript
|
||||
module-type: startup
|
||||
|
||||
Store modification recorder for performance testing.
|
||||
Intercepts wiki.addTiddler() and wiki.deleteTiddler() to capture
|
||||
a timeline of all store modifications with batch boundary tracking.
|
||||
|
||||
\*/
|
||||
|
||||
"use strict";
|
||||
|
||||
exports.name = "perf-recorder";
|
||||
exports.platforms = ["browser"];
|
||||
exports.after = ["load-modules"];
|
||||
exports.synchronous = true;
|
||||
|
||||
exports.startup = function() {
|
||||
var STATE_TIDDLER = "$:/state/performance/recording",
|
||||
TIMELINE_TIDDLER = "$:/temp/performance/timeline",
|
||||
COALESCE_CONFIG = "$:/config/performance/coalesce-drafts",
|
||||
timeline = [],
|
||||
seq = 0,
|
||||
startTime = null,
|
||||
recording = false,
|
||||
batchId = 0,
|
||||
currentBatch = 0,
|
||||
origNextTick = $tw.utils.nextTick,
|
||||
origAddTiddler = $tw.wiki.addTiddler.bind($tw.wiki),
|
||||
origDeleteTiddler = $tw.wiki.deleteTiddler.bind($tw.wiki);
|
||||
|
||||
// Patch nextTick to track batch boundaries
|
||||
$tw.utils.nextTick = function(fn) {
|
||||
origNextTick(function() {
|
||||
if(recording) {
|
||||
currentBatch = ++batchId;
|
||||
}
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
// Patch addTiddler
|
||||
$tw.wiki.addTiddler = function(tiddler) {
|
||||
if(recording) {
|
||||
if(!(tiddler instanceof $tw.Tiddler)) {
|
||||
tiddler = new $tw.Tiddler(tiddler);
|
||||
}
|
||||
var title = tiddler.fields.title;
|
||||
// Skip our own state/timeline tiddlers
|
||||
if(title !== STATE_TIDDLER && title !== TIMELINE_TIDDLER) {
|
||||
timeline.push({
|
||||
seq: seq++,
|
||||
t: $tw.utils.timer(startTime),
|
||||
batch: currentBatch,
|
||||
op: "add",
|
||||
title: title,
|
||||
isDraft: tiddler.hasField("draft.of"),
|
||||
fields: tiddler.getFieldStrings()
|
||||
});
|
||||
}
|
||||
}
|
||||
return origAddTiddler.apply(null,arguments);
|
||||
};
|
||||
|
||||
// Patch deleteTiddler
|
||||
$tw.wiki.deleteTiddler = function(title) {
|
||||
if(recording) {
|
||||
if(title !== STATE_TIDDLER && title !== TIMELINE_TIDDLER) {
|
||||
timeline.push({
|
||||
seq: seq++,
|
||||
t: $tw.utils.timer(startTime),
|
||||
batch: currentBatch,
|
||||
op: "delete",
|
||||
title: title,
|
||||
isDraft: false,
|
||||
fields: null
|
||||
});
|
||||
}
|
||||
}
|
||||
return origDeleteTiddler.apply(null,arguments);
|
||||
};
|
||||
|
||||
// Listen for recording state changes
|
||||
$tw.wiki.addEventListener("change",function(changes) {
|
||||
if(STATE_TIDDLER in changes) {
|
||||
var state = $tw.wiki.getTiddlerText(STATE_TIDDLER,"").trim();
|
||||
if(state === "yes" && !recording) {
|
||||
// Start recording
|
||||
timeline = [];
|
||||
seq = 0;
|
||||
batchId = 0;
|
||||
currentBatch = 0;
|
||||
startTime = $tw.utils.timer();
|
||||
recording = true;
|
||||
console.log("performance: Recording started");
|
||||
} else if(state !== "yes" && recording) {
|
||||
// Stop recording and save timeline
|
||||
recording = false;
|
||||
var coalesce = $tw.wiki.getTiddlerText(COALESCE_CONFIG,"yes").trim() === "yes";
|
||||
var output = coalesce ? coalesceDrafts(timeline) : timeline;
|
||||
origAddTiddler(new $tw.Tiddler({
|
||||
title: TIMELINE_TIDDLER,
|
||||
type: "application/json",
|
||||
text: JSON.stringify(output,null,"\t")
|
||||
}));
|
||||
console.log("performance: Recording stopped. " + timeline.length + " operations captured" +
|
||||
(coalesce ? " (" + output.length + " after coalescing drafts)" : ""));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Coalesce rapid draft tiddler updates within the same batch.
|
||||
Keeps only the last update for each draft tiddler per batch.
|
||||
*/
|
||||
function coalesceDrafts(events) {
|
||||
var result = [],
|
||||
i = 0;
|
||||
while(i < events.length) {
|
||||
var event = events[i];
|
||||
if(event.isDraft && event.op === "add") {
|
||||
// Look ahead for later updates to this same draft in the same batch
|
||||
var lastIndex = i;
|
||||
for(var j = i + 1; j < events.length; j++) {
|
||||
if(events[j].batch !== event.batch) {
|
||||
break;
|
||||
}
|
||||
if(events[j].title === event.title && events[j].op === "add") {
|
||||
lastIndex = j;
|
||||
}
|
||||
}
|
||||
// Keep only the last one, but fix its seq to maintain ordering
|
||||
result.push(events[lastIndex]);
|
||||
i = lastIndex + 1;
|
||||
} else {
|
||||
result.push(event);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
title: $:/plugins/tiddlywiki/performance/ui
|
||||
caption: Recorder
|
||||
|
||||
! Performance Testing Recorder
|
||||
|
||||
<$let
|
||||
state="$:/state/performance/recording"
|
||||
timeline="$:/temp/performance/timeline"
|
||||
coalesceConfig="$:/config/performance/coalesce-drafts"
|
||||
>
|
||||
|
||||
!! Recording
|
||||
|
||||
<$reveal state=<<state>> type="nomatch" text="yes">
|
||||
<$button set=<<state>> setTo="yes" class="tc-btn-big-green">Start Recording</$button>
|
||||
</$reveal>
|
||||
|
||||
<$reveal state=<<state>> type="match" text="yes">
|
||||
<$button set=<<state>> setTo="no" class="tc-btn-big-green" style="background: #d33;">Stop Recording</$button>
|
||||
//Recording in progress...//
|
||||
</$reveal>
|
||||
|
||||
!! Options
|
||||
|
||||
<$checkbox tiddler=<<coalesceConfig>> field="text" checked="yes" unchecked="no" default="yes"> Coalesce rapid draft updates</$checkbox>
|
||||
|
||||
!! Timeline
|
||||
|
||||
<$reveal type="nomatch" state=<<timeline>> text="">
|
||||
|
||||
<$let timelineText={{$(timeline)$}}>
|
||||
<$vars count={{{ [<timeline>get[text]jsonextract[]] +[count[]] }}}>
|
||||
Timeline contains <<count>> operations.
|
||||
</$vars>
|
||||
|
||||
<$button>
|
||||
<$action-sendmessage $message="tm-download-file" $param=<<timeline>> filename="timeline.json"/>
|
||||
Download timeline.json
|
||||
</$button>
|
||||
|
||||
</$let>
|
||||
</$reveal>
|
||||
|
||||
<$reveal type="match" state=<<timeline>> text="">
|
||||
No timeline recorded yet. Click "Start Recording", interact with the wiki, then click "Stop Recording".
|
||||
</$reveal>
|
||||
|
||||
</$let>
|
||||
@@ -1,9 +1,6 @@
|
||||
<h1 class="">Welcome</h1><p>Welcome to <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a>, a non-linear personal web notebook that anyone can use and keep forever, independently of any corporation.</p><p>TiddlyWiki is a complete interactive wiki in JavaScript. It can be used as a single HTML file in the browser or as a powerful Node.js application. It is highly customisable: the entire user interface is itself implemented in hackable <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/WikiText.html">WikiText</a>.</p><h2 class="">Demo</h2><p>Learn more and see it in action at <a class="tc-tiddlylink-external" href="https://tiddlywiki.com/" rel="noopener noreferrer" target="_blank">https://tiddlywiki.com/</a></p><h2 class="">Developer Documentation</h2><p>Developer documentation is in progress at <a class="tc-tiddlylink-external" href="https://tiddlywiki.com/dev/" rel="noopener noreferrer" target="_blank">https://tiddlywiki.com/dev/</a></p><h2 class="">Pull Request Previews</h2><p>Pull request previews courtesy of <a class="tc-tiddlylink-external" href="https://netlify.com" rel="noopener noreferrer" target="_blank">Netlify</a></p><p><a href="https://www.netlify.com" rel="noopener noreferrer" target="_blank"><img alt="Deploys by Netlify" src="https://www.netlify.com/v3/img/components/netlify-light.svg"></a></p><h1 class="">Join the Community</h1><p>
|
||||
<h2 class="">Official Forums</h2><h3 class=""><a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/" rel="noopener noreferrer" target="_blank">https://talk.tiddlywiki.org/</a></h3><blockquote class="tc-quote"><p>The new official forum for talking about TiddlyWiki: requests for help, <a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/c/announcements/20" rel="noopener noreferrer" target="_blank">announcements</a> of new releases and plugins, debating new features, or just sharing experiences. You can participate via the associated website, or subscribe via email.</p><p><strong>talk.tiddlywiki.org</strong> is a community run service that we host and maintain ourselves. The modest running costs are covered by community contributions.
|
||||
</p></blockquote><h4 class="">Google Groups</h4><blockquote class="tc-quote"><p>For the convenience of existing users, we also continue to operate the original TiddlyWiki group (hosted on Google Groups since 2005): <a class="tc-tiddlylink-external" href="https://groups.google.com/group/TiddlyWiki" rel="noopener noreferrer" target="_blank">https://groups.google.com/group/TiddlyWiki</a>
|
||||
</p></blockquote><h2 class="">Developer Forums</h2><h2 class=""><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/graphs/contributors" rel="noopener noreferrer" target="_blank">GitHub Stats</a></h2><p>There are several resources for developers to learn more about <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> and to discuss and contribute to its development.</p><blockquote><div><img class=" tc-image-loading" src="https://repobeats.axiom.co/api/embed/b92b1b363e2b5f26837ae573a60d39b4248b50a0.svg"></div></blockquote><ul><li><a class="tc-tiddlylink-external" href="https://tiddlywiki.com/dev" rel="noopener noreferrer" target="_blank">tiddlywiki.com/dev</a> is the official developer documentation</li><li>Get involved in the <a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5" rel="noopener noreferrer" target="_blank">development on GitHub</a></li><li><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/discussions" rel="noopener noreferrer" target="_blank">GitHub Discussions</a> are for Q&A and open-ended discussion</li><li><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/issues" rel="noopener noreferrer" target="_blank">GitHub Issues</a> are for raising bug reports and proposing specific, actionable new ideas</li><li>The older TiddlyWikiDev Google Group is now closed in favour of <a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/" rel="noopener noreferrer" target="_blank">Talk TiddlyWiki</a> and <a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/discussions" rel="noopener noreferrer" target="_blank">GitHub Discussions</a> <ul><li>It remains a useful archive: <a class="tc-tiddlylink-external" href="https://groups.google.com/group/TiddlyWikiDev" rel="noopener noreferrer" target="_blank">https://groups.google.com/group/TiddlyWikiDev</a><ul><li>An enhanced group search facility is available on <a class="tc-tiddlylink-external" href="https://www.mail-archive.com/tiddlywikidev@googlegroups.com/" rel="noopener noreferrer" target="_blank">mail-archive.com</a></li></ul></li></ul></li></ul><h2 class="">Other Forums</h2><ul><li><a class="tc-tiddlylink-external" href="https://www.reddit.com/r/TiddlyWiki5/" rel="noopener noreferrer" target="_blank">TiddlyWiki Subreddit</a></li><li>Chat on Discord at <a class="tc-tiddlylink-external" href="https://discord.gg/HFFZVQ8" rel="noopener noreferrer" target="_blank">https://discord.gg/HFFZVQ8</a></li></ul><h3 class="">Documentation</h3><p>There is also a discussion group specifically for discussing <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> documentation improvement initiatives: <a class="tc-tiddlylink-external" href="https://groups.google.com/group/tiddlywikidocs" rel="noopener noreferrer" target="_blank">https://groups.google.com/group/tiddlywikidocs</a>
|
||||
</p>
|
||||
</p><hr><h1 class="">Installing TiddlyWiki on Node.js</h1><p>TiddlyWiki is a <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/SingleFileApplication.html">SingleFileApplication</a>, which is easy to use. For advanced users and developers there is a possibility to use a Node.js client / server configuration. This configuration is also used to build the TiddlyWiki <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/SinglePageApplication.html">SinglePageApplication</a></p><ol><li>Install <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Node.js.html">Node.js</a><ul><li>Linux: <blockquote><div><em>Debian/Ubuntu</em>:<br><code>apt install nodejs</code><br>May need to be followed up by:<br><code>apt install npm</code></div><div><em>Arch Linux</em><br><code>yay -S tiddlywiki</code> <br>(installs node and tiddlywiki)</div></blockquote></li><li>Mac<blockquote><div><code>brew install node</code></div></blockquote></li><li>Android<blockquote><div><a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Serving%2520TW5%2520from%2520Android.html">Termux for Android</a></div></blockquote></li><li>Other <blockquote><div>See <a class="tc-tiddlylink-external" href="http://nodejs.org" rel="noopener noreferrer" target="_blank">http://nodejs.org</a></div></blockquote></li></ul></li><li>Open a command line terminal and type:<blockquote><div><code>npm install -g tiddlywiki</code></div><div>If it fails with an error you may need to re-run the command as an administrator:</div><div><code>sudo npm install -g tiddlywiki</code> (Mac/Linux)</div></blockquote></li><li>Ensure TiddlyWiki is installed by typing:<blockquote><div><code>tiddlywiki --version</code></div></blockquote><ul><li>In response, you should see <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> report its current version (eg "5.3.8". You may also see other debugging information reported.)</li></ul></li><li>Try it out:<ol><li><code>tiddlywiki mynewwiki --init server</code> to create a folder for a new wiki that includes server-related components</li><li><code>tiddlywiki mynewwiki --listen</code> to start <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a></li><li>Visit <a class="tc-tiddlylink-external" href="http://127.0.0.1:8080/" rel="noopener noreferrer" target="_blank">http://127.0.0.1:8080/</a> in your browser</li><li>Try editing and creating tiddlers</li></ol></li><li>Optionally, make an offline copy:<ul><li>click the <span class="doc-icon"><svg class="tc-image-save-button-dynamic tc-image-button" height="22pt" viewBox="0 0 128 128" width="22pt">
|
||||
<h2 class="">User forums</h2><h3 class="">Talk TiddlyWiki</h3><p>As the official TiddlyWiki forum, Talk TiddlyWiki is a place to talk about TiddlyWiki: requests for help, <a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/c/announcements/20" rel="noopener noreferrer" target="_blank">announcements</a> of new releases and plugins, debating new features, or just sharing experiences. You can participate via the associated website, or subscribe via email.</p><p><a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/" rel="noopener noreferrer" target="_blank">https://talk.tiddlywiki.org/</a></p><h3 class="">Google Groups</h3><p>For the convenience of existing users, we also continue to operate the original TiddlyWiki group (hosted on Google Groups since 2005): <a class="tc-tiddlylink-external" href="https://groups.google.com/group/TiddlyWiki" rel="noopener noreferrer" target="_blank">https://groups.google.com/group/TiddlyWiki</a></p><h2 class="">Developer forums</h2><ul><li><a class="tc-tiddlylink-external" href="https://tiddlywiki.com/dev" rel="noopener noreferrer" target="_blank">tiddlywiki.com/dev</a> is the official developer documentation</li><li>Get involved in the <a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5" rel="noopener noreferrer" target="_blank">development on GitHub</a></li><li><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/discussions" rel="noopener noreferrer" target="_blank">GitHub Discussions</a> are for Q&A and open-ended discussion</li><li><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/issues" rel="noopener noreferrer" target="_blank">GitHub Issues</a> are for raising bug reports and proposing specific, actionable new ideas</li><li>See <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Contributing.html">Contributing</a> for guidelines on how to contribute to the project.</li></ul><h2 class="">Other forums</h2><ul><li><a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> Subreddit: <a class="tc-tiddlylink-external" href="https://www.reddit.com/r/TiddlyWiki5/" rel="noopener noreferrer" target="_blank">/r/TiddlyWiki5</a></li><li>Chat on Discord at <a class="tc-tiddlylink-external" href="https://discord.gg/HFFZVQ8" rel="noopener noreferrer" target="_blank">https://discord.gg/HFFZVQ8</a></li></ul>
|
||||
</p><hr><h1 class="">Installing TiddlyWiki on Node.js</h1><p>TiddlyWiki is a <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/SingleFileApplication.html">SingleFileApplication</a>, which is easy to use. For advanced users and developers there is a possibility to use a Node.js client / server configuration. This configuration is also used to build the TiddlyWiki <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/SinglePageApplication.html">SinglePageApplication</a></p><ol><li>Install <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Node.js.html">Node.js</a><ul><li>Linux: <blockquote><div><em>Debian/Ubuntu</em>:<br><code>apt install nodejs</code><br>May need to be followed up by:<br><code>apt install npm</code></div><div><em>Arch Linux</em><br><code>yay -S tiddlywiki</code> <br>(installs node and tiddlywiki)</div></blockquote></li><li>Mac<blockquote><div><code>brew install node</code></div></blockquote></li><li>Android<blockquote><div><a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Serving%2520TW5%2520from%2520Android.html">Termux for Android</a></div></blockquote></li><li>Other <blockquote><div>See <a class="tc-tiddlylink-external" href="http://nodejs.org" rel="noopener noreferrer" target="_blank">http://nodejs.org</a></div></blockquote></li></ul></li><li>Open a command line terminal and type:<blockquote><div><code>npm install -g tiddlywiki</code></div><div>If it fails with an error you may need to re-run the command as an administrator:</div><div><code>sudo npm install -g tiddlywiki</code> (Mac/Linux)</div></blockquote></li><li>Ensure TiddlyWiki is installed by typing:<blockquote><div><code>tiddlywiki --version</code></div></blockquote><ul><li>In response, you should see <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> report its current version (eg "5.4.0". You may also see other debugging information reported.)</li></ul></li><li>Try it out:<ol><li><code>tiddlywiki mynewwiki --init server</code> to create a folder for a new wiki that includes server-related components</li><li><code>tiddlywiki mynewwiki --listen</code> to start <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a></li><li>Visit <a class="tc-tiddlylink-external" href="http://127.0.0.1:8080/" rel="noopener noreferrer" target="_blank">http://127.0.0.1:8080/</a> in your browser</li><li>Try editing and creating tiddlers</li></ol></li><li>Optionally, make an offline copy:<ul><li>click the <span class="doc-icon"><svg class="tc-image-save-button-dynamic tc-image-button" height="22pt" viewBox="0 0 128 128" width="22pt">
|
||||
<g class="tc-image-save-button-dynamic-clean">
|
||||
<path d="M120.783 34.33c4.641 8.862 7.266 18.948 7.266 29.646 0 35.347-28.653 64-64 64-35.346 0-64-28.653-64-64 0-35.346 28.654-64 64-64 18.808 0 35.72 8.113 47.43 21.03l2.68-2.68c3.13-3.13 8.197-3.132 11.321-.008 3.118 3.118 3.121 8.193-.007 11.32l-4.69 4.691zm-12.058 12.058a47.876 47.876 0 013.324 17.588c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48c14.39 0 27.3 6.332 36.098 16.362L58.941 73.544 41.976 56.578c-3.127-3.127-8.201-3.123-11.32-.005-3.123 3.124-3.119 8.194.006 11.319l22.617 22.617a7.992 7.992 0 005.659 2.347c2.05 0 4.101-.783 5.667-2.349l44.12-44.12z" fill-rule="evenodd"></path>
|
||||
</g>
|
||||
|
||||
Reference in New Issue
Block a user