1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-02-21 05:20:01 +00:00

Merge branch 'Jermolene:master' into offline-extjs

This commit is contained in:
cdruan 2021-05-23 16:50:41 -07:00
commit 31583a8d85
153 changed files with 2499 additions and 574 deletions

View File

@ -5,52 +5,52 @@ Optimise the SVGs in ./core/images using SVGO from https://github.com/svg/svgo
Install SVGO with the following command in the root of the repo:
npm install svgo
npm install svgo@2.3.0
*/
"use strict";
var fs = require("fs"),
path = require("path"),
SVGO = require("svgo"),
svgo = new SVGO({
{ optimize } = require("svgo"),
config = {
plugins: [
{cleanupAttrs: true},
{removeDoctype: true},
{removeXMLProcInst: true},
{removeComments: true},
{removeMetadata: true},
{removeTitle: true},
{removeDesc: true},
{removeUselessDefs: true},
{removeEditorsNSData: true},
{removeEmptyAttrs: true},
{removeHiddenElems: true},
{removeEmptyText: true},
{removeEmptyContainers: true},
{removeViewBox: false},
{cleanupEnableBackground: true},
{convertStyleToAttrs: true},
{convertColors: true},
{convertPathData: true},
{convertTransform: true},
{removeUnknownsAndDefaults: true},
{removeNonInheritableGroupAttrs: true},
{removeUselessStrokeAndFill: true},
{removeUnusedNS: true},
{cleanupIDs: true},
{cleanupNumericValues: true},
{moveElemsAttrsToGroup: true},
{moveGroupAttrsToElems: true},
{collapseGroups: true},
{removeRasterImages: false},
{mergePaths: true},
{convertShapeToPath: true},
{sortAttrs: true},
{removeDimensions: false},
{removeAttrs: {attrs: "(stroke|fill)"}}
'cleanupAttrs',
'removeDoctype',
'removeXMLProcInst',
'removeComments',
'removeMetadata',
'removeTitle',
'removeDesc',
'removeUselessDefs',
'removeEditorsNSData',
'removeEmptyAttrs',
'removeHiddenElems',
'removeEmptyText',
'removeEmptyContainers',
// 'removeViewBox',
'cleanupEnableBackground',
'convertStyleToAttrs',
'convertColors',
'convertPathData',
'convertTransform',
'removeUnknownsAndDefaults',
'removeNonInheritableGroupAttrs',
'removeUselessStrokeAndFill',
'removeUnusedNS',
'cleanupIDs',
'cleanupNumericValues',
'moveElemsAttrsToGroup',
'moveGroupAttrsToElems',
'collapseGroups',
// 'removeRasterImages',
'mergePaths',
'convertShapeToPath',
'sortAttrs',
//'removeDimensions',
{name: 'removeAttrs', params: { attrs: '(stroke|fill)' } }
]
});
};
var basepath = "./core/images/",
files = fs.readdirSync(basepath).sort();
@ -66,12 +66,14 @@ files.forEach(function(filename) {
fakeSVG = body.join("\n");
// A hack to make the new-journal-button work
fakeSVG = fakeSVG.replace("<<now \"DD\">>","&lt;&lt;now &quot;DD&quot;&gt;&gt;");
svgo.optimize(fakeSVG, {path: filepath}).then(function(result) {
config.path = filepath;
var result = optimize(fakeSVG,config);
if(result) {
var newSVG = header.join("\n") + "\n\n" + result.data.replace("&lt;&lt;now &quot;DD&quot;&gt;&gt;","<<now \"DD\">>");
fs.writeFileSync(filepath,newSVG);
},function(err) {
} else {
console.log("Error " + err + " with " + filename)
process.exit();
});
};
}
});

View File

@ -2133,6 +2133,7 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
fileInfo = $tw.boot.files[title];
if(fileInfo.isEditableFile) {
relativePath = path.relative($tw.boot.wikiTiddlersPath,fileInfo.filepath);
fileInfo.originalpath = relativePath;
output[title] =
path.sep === "/" ?
relativePath :
@ -2466,16 +2467,29 @@ $tw.boot.executeNextStartupTask = function(callback) {
};
/*
Returns true if we are running on one platforms specified in a task modules `platforms` array
Returns true if we are running on one of the platforms specified in taskModule's
`platforms` array; or if `platforms` property is not defined.
*/
$tw.boot.doesTaskMatchPlatform = function(taskModule) {
var platforms = taskModule.platforms;
if(platforms) {
for(var t=0; t<platforms.length; t++) {
if((platforms[t] === "browser" && !$tw.browser) || (platforms[t] === "node" && !$tw.node)) {
return false;
switch (platforms[t]) {
case "browser":
if ($tw.browser) {
return true;
}
break;
case "node":
if ($tw.node) {
return true;
}
break;
default:
$tw.utils.error("Module " + taskModule.name + ": '" + platforms[t] + "' in export.platforms invalid");
}
}
return false;
}
return true;
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
title: $:/core/images/minus-button
tags: $:/tags/Image
<svg width="22pt" height="22pt" class="tc-image-minus-button tc-image-button" viewBox="0 0 128 128"><path d="M64 0c35.346 0 64 28.654 64 64 0 35.346-28.654 64-64 64-35.346 0-64-28.654-64-64C0 28.654 28.654 0 64 0zm.332 16c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48z"/><rect width="80" height="16" x="24" y="56" rx="8"/></svg>

View File

@ -0,0 +1,4 @@
title: $:/core/images/plus-button
tags: $:/tags/Image
<svg width="22pt" height="22pt" class="tc-image-plus-button tc-image-button" viewBox="0 0 128 128"><path d="M64-.333c35.346 0 64 28.654 64 64 0 35.346-28.654 64-64 64-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64zM64 16c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48z"/><rect width="80" height="16" x="24" y="56" rx="8"/><rect width="16" height="80" x="56" y="24" rx="8"/></svg>

View File

@ -123,12 +123,12 @@ Saving/TiddlySpot/BackupDir: Backup Directory
Saving/TiddlySpot/ControlPanel: ~TiddlySpot Control Panel
Saving/TiddlySpot/Backups: Backups
Saving/TiddlySpot/Caption: ~TiddlySpot Saver
Saving/TiddlySpot/Description: These settings are only used when saving to http://tiddlyspot.com or a compatible remote server
Saving/TiddlySpot/Description: These settings are only used when saving to [[TiddlySpot|http://tiddlyspot.com]], [[TiddlyHost|https://tiddlyhost.com]], or a compatible remote server. See [[here|https://github.com/simonbaird/tiddlyhost/wiki/TiddlySpot-Saver-configuration-for-Tiddlyhost-and-Tiddlyspot]] for information on ~TiddlySpot and ~TiddlyHost saving configuration.
Saving/TiddlySpot/Filename: Upload Filename
Saving/TiddlySpot/Heading: ~TiddlySpot
Saving/TiddlySpot/Hint: //The server URL defaults to `http://<wikiname>.tiddlyspot.com/store.cgi` and can be changed to use a custom server address, e.g. `http://example.com/store.php`.//
Saving/TiddlySpot/Password: Password
Saving/TiddlySpot/ReadOnly: The ~TiddlySpot service is currently only available in read-only form. Please see http://tiddlyspot.com/ for the latest details. The ~TiddlySpot saver can still be used to save to compatible servers.
Saving/TiddlySpot/ReadOnly: Note that [[TiddlySpot|http://tiddlyspot.com]] no longer allows the creation of new sites. For new sites you can use [[TiddlyHost|https://tiddlyhost.com]], a new hosting service which replaces ~TiddlySpot.
Saving/TiddlySpot/ServerURL: Server URL
Saving/TiddlySpot/UploadDir: Upload Directory
Saving/TiddlySpot/UserName: Wiki Name

View File

@ -30,6 +30,7 @@ All parameters are optional with safe defaults, and can be specified in any orde
* ''tls-key'' - pathname of TLS key file (relative to wiki folder)
* ''debug-level'' - optional debug level; set to "debug" to view request details (defaults to "none")
* ''gzip'' - set to "yes" to enable gzip compression for some http endpoints (defaults to "no")
* ''use-browser-cache'' - set to "yes" to allow the browser to cache responses to save bandwith (defaults to "no")
For information on opening up your instance to the entire local network, and possible security concerns, see the WebServer tiddler at TiddlyWiki.com.

View File

@ -1,5 +1,6 @@
title: $:/language/Import/
Editor/Import/Heading: Import images and insert into the editor.
Imported/Hint: The following tiddlers were imported:
Listing/Cancel/Caption: Cancel
Listing/Hint: These tiddlers are ready to import:

View File

@ -24,7 +24,6 @@ Encryption/RepeatPassword: Repeat password
Encryption/PasswordNoMatch: Passwords do not match
Encryption/SetPassword: Set password
Error/Caption: Error
Error/EditConflict: File changed on server
Error/Filter: Filter error
Error/FilterSyntax: Syntax error in filter expression
Error/FilterRunPrefix: Filter Error: Unknown prefix for filter run
@ -32,6 +31,9 @@ Error/IsFilterOperator: Filter Error: Unknown operand for the 'is' filter operat
Error/FormatFilterOperator: Filter Error: Unknown suffix for the 'format' filter operator
Error/LoadingPluginLibrary: Error loading plugin library
Error/NetworkErrorAlert: `<h2>''Network Error''</h2>It looks like the connection to the server has been lost. This may indicate a problem with your network connection. Please attempt to restore network connectivity before continuing.<br><br>''Any unsaved changes will be automatically synchronised when connectivity is restored''.`
Error/PutEditConflict: File changed on server
Error/PutForbidden: Permission denied
Error/PutUnauthorized: Authentication required
Error/RecursiveTransclusion: Recursive transclusion error in transclude widget
Error/RetrievingSkinny: Error retrieving skinny tiddler list
Error/SavingToTWEdit: Error saving to TWEdit

View File

@ -167,10 +167,10 @@ WikiFolderMaker.prototype.saveTiddler = function(directory,tiddler) {
}
var fileInfo = $tw.utils.generateTiddlerFileInfo(tiddler,{
directory: path.resolve(this.wikiFolderPath,directory),
wiki: this.wiki,
pathFilters: pathFilters,
extFilters: extFilters,
originalpath: this.wiki.extractTiddlerDataItem("$:/config/OriginalTiddlerPaths",title, "")
wiki: this.wiki,
fileInfo: {}
});
try {
$tw.utils.saveTiddlerToFileSync(tiddler,fileInfo);

View File

@ -34,7 +34,7 @@ exports.htmlEntities = {quot:34, amp:38, apos:39, lt:60, gt:62, nbsp:160, iexcl:
exports.htmlVoidElements = "area,base,br,col,command,embed,hr,img,input,keygen,link,meta,param,source,track,wbr".split(",");
exports.htmlBlockElements = "address,article,aside,audio,blockquote,canvas,dd,div,dl,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,li,noscript,ol,output,p,pre,section,table,tfoot,ul,video".split(",");
exports.htmlBlockElements = "address,article,aside,audio,blockquote,canvas,dd,details,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,li,nav,ol,p,pre,section,summary,table,tfoot,ul,video".split(",");
exports.htmlUnsafeElements = "script".split(",");

View File

@ -87,7 +87,20 @@ function FramedEngine(options) {
{name: "input",handlerObject: this,handlerMethod: "handleInputEvent"},
{name: "keydown",handlerObject: this.widget,handlerMethod: "handleKeydownEvent"},
{name: "focus",handlerObject: this,handlerMethod: "handleFocusEvent"}
]);
// Add drag and drop event listeners if fileDrop is enabled
if(this.widget.isFileDropEnabled) {
$tw.utils.addEventListeners(this.domNode,[
{name: "dragenter",handlerObject: this.widget,handlerMethod: "handleDragEnterEvent"},
{name: "dragover",handlerObject: this.widget,handlerMethod: "handleDragOverEvent"},
{name: "dragleave",handlerObject: this.widget,handlerMethod: "handleDragLeaveEvent"},
{name: "dragend",handlerObject: this.widget,handlerMethod: "handleDragEndEvent"},
{name: "drop", handlerObject: this.widget,handlerMethod: "handleDropEvent"},
{name: "paste", handlerObject: this.widget,handlerMethod: "handlePasteEvent"},
{name: "click",handlerObject: this.widget,handlerMethod: "handleClickEvent"}
]);
}
// Insert the element into the DOM
this.iframeDoc.body.appendChild(this.domNode);
}

View File

@ -103,7 +103,11 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
var tiddler = this.wiki.getTiddler(this.editTitle);
if(tiddler) {
// If we've got a tiddler, the value to display is the field string value
value = tiddler.getFieldString(this.editField);
if(tiddler.hasField(this.editField)) {
value = tiddler.getFieldString(this.editField);
} else {
value = this.editDefault || "";
}
if(this.editField === "text") {
type = tiddler.fields.type || "text/vnd.tiddlywiki";
}
@ -182,6 +186,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
this.editRefreshTitle = this.getAttribute("refreshTitle");
this.editAutoComplete = this.getAttribute("autocomplete");
this.isDisabled = this.getAttribute("disabled","no");
this.isFileDropEnabled = this.getAttribute("fileDrop","no") === "yes";
// Get the default editor element tag and type
var tag,type;
if(this.editField === "text") {
@ -213,7 +218,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
EditTextWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
// Completely rerender if any of our attributes have changed
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedAttributes.disabled) {
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedAttributes.disabled || changedAttributes.fileDrop) {
this.refreshSelf();
return true;
} else if (changedTiddlers[this.editRefreshTitle]) {
@ -293,19 +298,88 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
Propogate keydown events to our container for the keyboard widgets benefit
*/
EditTextWidget.prototype.propogateKeydownEvent = function(event) {
var newEvent = this.document.createEventObject ? this.document.createEventObject() : this.document.createEvent("Events");
if(newEvent.initEvent) {
newEvent.initEvent("keydown", true, true);
}
newEvent.keyCode = event.keyCode;
newEvent.which = event.which;
newEvent.metaKey = event.metaKey;
newEvent.ctrlKey = event.ctrlKey;
newEvent.altKey = event.altKey;
newEvent.shiftKey = event.shiftKey;
var newEvent = this.cloneEvent(event,["keyCode","which","metaKey","ctrlKey","altKey","shiftKey"]);
return !this.parentDomNode.dispatchEvent(newEvent);
};
EditTextWidget.prototype.cloneEvent = function(event,propertiesToCopy) {
var propertiesToCopy = propertiesToCopy || [],
newEvent = this.document.createEventObject ? this.document.createEventObject() : this.document.createEvent("Events");
if(newEvent.initEvent) {
newEvent.initEvent(event.type, true, true);
}
$tw.utils.each(propertiesToCopy,function(prop){
newEvent[prop] = event[prop];
});
return newEvent;
};
EditTextWidget.prototype.dispatchDOMEvent = function(newEvent) {
var dispatchNode = this.engine.iframeNode || this.engine.parentNode;
return dispatchNode.dispatchEvent(newEvent);
};
/*
Propogate drag and drop events with File data to our container for the dropzone widgets benefit.
If there are no Files, let the browser handle it.
*/
EditTextWidget.prototype.handleDropEvent = function(event) {
if(event.dataTransfer.files.length) {
event.preventDefault();
event.stopPropagation();
this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
}
};
EditTextWidget.prototype.handlePasteEvent = function(event) {
if(event.clipboardData.files.length) {
event.preventDefault();
event.stopPropagation();
this.dispatchDOMEvent(this.cloneEvent(event,["clipboardData"]));
}
};
EditTextWidget.prototype.handleDragEnterEvent = function(event) {
if($tw.utils.dragEventContainsFiles(event)) {
// Ignore excessive events fired by FF when entering and leaving text nodes in a text area.
if( event.relatedTarget && (event.relatedTarget.nodeType === 3 || event.target === event.relatedTarget)) {
return true;
}
event.preventDefault();
return this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
}
return true;
};
EditTextWidget.prototype.handleDragOverEvent = function(event) {
if($tw.utils.dragEventContainsFiles(event)) {
// Call preventDefault() in browsers that default to not allowing drop events on textarea
if($tw.browser.isFirefox || $tw.browser.isIE) {
event.preventDefault();
}
event.dataTransfer.dropEffect = "copy";
return this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
}
return true;
};
EditTextWidget.prototype.handleDragLeaveEvent = function(event) {
// Ignore excessive events fired by FF when entering and leaving text nodes in a text area.
if(event.relatedTarget && ((event.relatedTarget.nodeType === 3) || (event.target === event.relatedTarget))) {
return true;
}
event.preventDefault();
this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
};
EditTextWidget.prototype.handleDragEndEvent = function(event) {
this.dispatchDOMEvent(this.cloneEvent(event));
};
EditTextWidget.prototype.handleClickEvent = function(event) {
return !this.dispatchDOMEvent(this.cloneEvent(event));
};
return EditTextWidget;
}

View File

@ -0,0 +1,23 @@
/*\
title: $:/core/modules/editor/operations/text/insert-text.js
type: application/javascript
module-type: texteditoroperation
Text editor operation insert text at the caret position. If there is a selection it is replaced.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports["insert-text"] = function(event,operation) {
operation.replacement = event.paramObject.text;
operation.cutStart = operation.selStart;
operation.cutEnd = operation.selEnd;
operation.newSelStart = operation.selStart + operation.replacement.length;
operation.newSelEnd = operation.newSelStart;
};
})();

View File

@ -17,10 +17,21 @@ exports.filter = function(operationSubFunction,options) {
return function(results,source,widget) {
if(results.length > 0) {
var resultsToRemove = [];
results.each(function(result) {
var filtered = operationSubFunction(options.wiki.makeTiddlerIterator([result]),widget);
results.each(function(title) {
var filtered = operationSubFunction(options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return widget.getVariable("currentTiddler");
default:
return widget.getVariable(name);
}
}
});
if(filtered.length === 0) {
resultsToRemove.push(result);
resultsToRemove.push(title);
}
});
results.remove(resultsToRemove);

View File

@ -19,23 +19,25 @@ exports.reduce = function(operationSubFunction,options) {
var index = 0;
results.each(function(title) {
var list = operationSubFunction(options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "accumulator":
return "" + accumulator;
case "index":
return "" + index;
case "revIndex":
return "" + (results.length - 1 - index);
case "length":
return "" + results.length;
default:
return widget.getVariable(name);
}
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return widget.getVariable("currentTiddler");
case "accumulator":
return "" + accumulator;
case "index":
return "" + index;
case "revIndex":
return "" + (results.length - 1 - index);
case "length":
return "" + results.length;
default:
return widget.getVariable(name);
}
});
}
});
if(list.length > 0) {
accumulator = "" + list[0];
}

View File

@ -0,0 +1,60 @@
/*\
title: $:/core/modules/filterrunprefixes/sort.js
type: application/javascript
module-type: filterrunprefix
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Export our filter prefix function
*/
exports.sort = function(operationSubFunction,options) {
return function(results,source,widget) {
if(results.length > 0) {
var 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,
inputTitles = results.toArray(),
sortKeys = [],
indexes = new Array(inputTitles.length),
compareFn;
results.each(function(title) {
var key = operationSubFunction(options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return widget.getVariable("currentTiddler");
default:
return widget.getVariable(name);
}
}
});
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) {
results.push(inputTitles[index]);
});
}
}
};
})();

View File

@ -137,7 +137,7 @@ exports.parseFilter = function(filterString) {
p = 0, // Current position in the filter string
match;
var whitespaceRegExp = /(\s+)/mg,
operandRegExp = /((?:\+|\-|~|=|\:(\w+))?)(?:(\[)|(?:"([^"]*)")|(?:'([^']*)')|([^\s\[\]]+))/mg;
operandRegExp = /((?:\+|\-|~|=|\:(\w+)(?:\:([\w\:, ]*))?)?)(?:(\[)|(?:"([^"]*)")|(?:'([^']*)')|([^\s\[\]]+))/mg;
while(p < filterString.length) {
// Skip any whitespace
whitespaceRegExp.lastIndex = p;
@ -162,15 +162,27 @@ exports.parseFilter = function(filterString) {
if(match[2]) {
operation.namedPrefix = match[2];
}
if(match[3]) {
operation.suffixes = [];
$tw.utils.each(match[3].split(":"),function(subsuffix) {
operation.suffixes.push([]);
$tw.utils.each(subsuffix.split(","),function(entry) {
entry = $tw.utils.trim(entry);
if(entry) {
operation.suffixes[operation.suffixes.length -1].push(entry);
}
});
});
}
}
if(match[3]) { // Opening square bracket
if(match[4]) { // Opening square bracket
p = parseFilterOperation(operation.operators,filterString,p);
} else {
p = match.index + match[0].length;
}
if(match[4] || match[5] || match[6]) { // Double quoted string, single quoted string or unquoted title
if(match[5] || match[6] || match[7]) { // Double quoted string, single quoted string or unquoted title
operation.operators.push(
{operator: "title", operands: [{text: match[4] || match[5] || match[6]}]}
{operator: "title", operands: [{text: match[5] || match[6] || match[7]}]}
);
}
results.push(operation);
@ -280,7 +292,7 @@ exports.compileFilter = function(filterString) {
var filterRunPrefixes = self.getFilterRunPrefixes();
// Wrap the operator functions in a wrapper function that depends on the prefix
operationFunctions.push((function() {
var options = {wiki: self};
var options = {wiki: self, suffixes: operation.suffixes || []};
switch(operation.prefix || "") {
case "": // No prefix means that the operation is unioned into the result
return filterRunPrefixes["or"](operationSubFunction, options);
@ -311,6 +323,9 @@ exports.compileFilter = function(filterString) {
} else if(typeof source === "object") { // Array or hashmap
source = self.makeTiddlerIterator(source);
}
if(!widget) {
widget = $tw.rootWidget;
}
var results = new $tw.utils.LinkedList();
$tw.utils.each(operationFunctions,function(operationFunction) {
operationFunction(results,source,widget);

View File

@ -0,0 +1,27 @@
/*\
title: $:/core/modules/filters/deserializers.js
type: application/javascript
module-type: filteroperator
Filter operator for returning the names of the deserializers in this wiki
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Export our filter function
*/
exports.deserializers = function(source,operator,options) {
var results = [];
$tw.utils.each($tw.Wiki.tiddlerDeserializerModules,function(deserializer,type) {
results.push(type);
});
results.sort();
return results;
};
})();

View File

@ -102,7 +102,7 @@ exports.escapecss = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
// escape any character with a special meaning in CSS using CSS.escape()
results.push(CSS.escape(title));
results.push($tw.utils.escapeCSS(title));
});
return results;
};

View File

@ -20,7 +20,18 @@ exports.filter = function(source,operator,options) {
results = [],
target = operator.prefix !== "!";
source(function(tiddler,title) {
var list = filterFn.call(options.wiki,options.wiki.makeTiddlerIterator([title]));
var list = filterFn.call(options.wiki,options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return options.widget.getVariable("currentTiddler");
default:
return options.widget.getVariable(name);
}
}
});
if((list.length > 0) === target) {
results.push(title);
}

View File

@ -5,9 +5,11 @@ module-type: filteroperator
Filter operator that looks up values via a title prefix
[lookup:<field>[<prefix>]]
[lookup:<defaultvalue>:<field OR index>[<prefix>],[<field-name OR index-name>]]
Prepends the prefix to the selected items and returns the specified field value
Prepends the prefix to the selected items and returns the specified
field or index value. If the 2nd suffix does not exist, it defaults to field.
If the second operand is missing it defaults to "text" for fields, and "0" for indexes
\*/
(function(){
@ -20,10 +22,35 @@ Prepends the prefix to the selected items and returns the specified field value
Export our filter function
*/
exports.lookup = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
results.push(options.wiki.getTiddlerText(operator.operand + title) || operator.suffix || '');
});
var results = [],
suffixes = operator.suffixes || [],
defaultSuffix = suffixes[0] ? (suffixes[0][0] || "") : "",
indexSuffix = (suffixes[1] && suffixes[1][0] === "index") ? true : false,
target;
if(operator.operands.length == 2) {
target = operator.operands[1]
} else {
target = indexSuffix ? "0": "text";
}
if(indexSuffix) {
source(function(tiddler,title) {
var targetTitle = operator.operands[0] + title;
var data = options.wiki.extractTiddlerDataItem(targetTitle,target,defaultSuffix);
results.push(data);
});
} else {
source(function(tiddler,title) {
var targetTitle = operator.operands[0] + title;
var targetTiddler = options.wiki.getTiddler(targetTitle);
if(targetTiddler) {
var value = targetTiddler.getFieldString(target);
if(value == "" && defaultSuffix !== "") {
value = defaultSuffix;
}
results.push(value);
}
});
}
return results;
};

View File

@ -125,6 +125,54 @@ exports.minall = makeNumericReducingOperator(
Infinity // Initial value
);
exports.median = makeNumericArrayOperator(
function(values) {
var len = values.length, median;
values.sort();
if(len % 2) {
// Odd, return the middle number
median = values[(len - 1) / 2];
} else {
// Even, return average of two middle numbers
median = (values[len / 2 - 1] + values[len / 2]) / 2;
}
return [median];
}
);
exports.average = makeNumericReducingOperator(
function(accumulator,value) {return accumulator + value},
0, // Initial value
function(finalValue,numberOfValues) {
return finalValue/numberOfValues;
}
);
exports.variance = makeNumericReducingOperator(
function(accumulator,value) {return accumulator + value},
0,
function(finalValue,numberOfValues,originalValues) {
return getVarianceFromArray(originalValues,finalValue/numberOfValues);
}
);
exports["standard-deviation"] = makeNumericReducingOperator(
function(accumulator,value) {return accumulator + value},
0,
function(finalValue,numberOfValues,originalValues) {
var variance = getVarianceFromArray(originalValues,finalValue/numberOfValues);
return Math.sqrt(variance);
}
);
//Calculate the variance of a population of numbers in an array given its mean
function getVarianceFromArray(values,mean) {
var deviationTotal = values.reduce(function(accumulator,value) {
return accumulator + Math.pow(value - mean, 2);
},0);
return deviationTotal/values.length;
};
function makeNumericBinaryOperator(fnCalc) {
return function(source,operator,options) {
var result = [],
@ -134,19 +182,37 @@ function makeNumericBinaryOperator(fnCalc) {
});
return result;
};
}
};
function makeNumericReducingOperator(fnCalc,initialValue) {
function makeNumericReducingOperator(fnCalc,initialValue,fnFinal) {
initialValue = initialValue || 0;
return function(source,operator,options) {
var result = [];
source(function(tiddler,title) {
result.push(title);
result.push($tw.utils.parseNumber(title));
});
return [$tw.utils.stringifyNumber(result.reduce(function(accumulator,currentValue) {
return fnCalc(accumulator,$tw.utils.parseNumber(currentValue));
},initialValue))];
var value = result.reduce(function(accumulator,currentValue) {
return fnCalc(accumulator,currentValue);
},initialValue);
if(fnFinal) {
value = fnFinal(value,result.length,result);
}
return [$tw.utils.stringifyNumber(value)];
};
}
};
function makeNumericArrayOperator(fnCalc) {
return function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
results.push($tw.utils.parseNumber(title));
});
results = fnCalc(results);
$tw.utils.each(results,function(value,index) {
results[index] = $tw.utils.stringifyNumber(value);
});
return results;
};
};
})();

View File

@ -31,6 +31,8 @@ exports.reduce = function(source,operator,options) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return options.widget.getVariable("currentTiddler");
case "accumulator":
return "" + accumulator;
case "index":

View File

@ -27,10 +27,13 @@ exports.sortsub = function(source,operator,options) {
iterator(options.wiki.getTiddler(title),title);
},{
getVariable: function(name) {
if(name === "currentTiddler") {
return title;
} else {
return options.widget.getVariable(name);
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return options.widget.getVariable("currentTiddler");
default:
return options.widget.getVariable(name);
}
}
});

View File

@ -2,6 +2,7 @@
title: $:/core/modules/macros/unusedtitle.js
type: application/javascript
module-type: macro
Macro to return a new title that is unused in the wiki. It can be given a name as a base.
\*/
(function(){
@ -10,25 +11,25 @@ Macro to return a new title that is unused in the wiki. It can be given a name a
/*global $tw: false */
"use strict";
/*
Information about this macro
*/
exports.name = "unusedtitle";
exports.params = [
{name: "baseName"},
{name: "options"}
{name: "separator"},
{name: "template"}
];
/*
Run the macro
*/
exports.run = function(baseName, options) {
exports.run = function(baseName,separator,template) {
separator = separator || " ";
if(!baseName) {
baseName = $tw.language.getString("DefaultNewTiddlerTitle");
}
return this.wiki.generateNewTitle(baseName, options);
// $tw.wiki.generateNewTitle = function(baseTitle,options)
// options.prefix must be a string!
return this.wiki.generateNewTitle(baseName, {"prefix": separator, "template": template});
};
})();

View File

@ -23,10 +23,12 @@ var HtmlParser = function(type,text,options) {
type: "element",
tag: "iframe",
attributes: {
src: {type: "string", value: src},
sandbox: {type: "string", value: ""}
src: {type: "string", value: src}
}
}];
if($tw.wiki.getTiddlerText("$:/config/HtmlParser/DisableSandbox","no") !== "yes") {
this.tree[0].attributes.sandbox = {type: "string", value: $tw.wiki.getTiddlerText("$:/config/HtmlParser/SandboxTokens","")};
}
};
exports["text/html"] = HtmlParser;

View File

@ -36,7 +36,7 @@ exports.parse = function() {
// Move past the pragma invocation
this.parser.pos = this.matchRegExp.lastIndex;
// Parse the filter terminated by a line break
var reMatch = /(.*)(\r?\n)|$/mg;
var reMatch = /(.*)(?:$|\r?\n)/mg;
reMatch.lastIndex = this.parser.pos;
var match = reMatch.exec(this.parser.source);
this.parser.pos = reMatch.lastIndex;

View File

@ -89,9 +89,12 @@ PutSaver.prototype.save = function(text,method,callback) {
if(err) {
// response is textual: "XMLHttpRequest error code: 412"
var status = Number(err.substring(err.indexOf(':') + 2, err.length))
if(status === 412) { // edit conflict
var message = $tw.language.getString("Error/EditConflict");
callback(message);
if(status === 412) { // file changed on server
callback($tw.language.getString("Error/PutEditConflict"));
} else if(status === 401) { // authentication required
callback($tw.language.getString("Error/PutUnauthorized"));
} else if(status === 403) { // permission denied
callback($tw.language.getString("Error/PutForbidden"));
} else {
callback(err); // fail
}

View File

@ -17,9 +17,8 @@ exports.method = "GET";
exports.path = /^\/favicon.ico$/;
exports.handler = function(request,response,state) {
response.writeHead(200, {"Content-Type": "image/x-icon"});
var buffer = state.wiki.getTiddlerText("$:/favicon.ico","");
response.end(buffer,"base64");
state.sendResponse(200,{"Content-Type": "image/x-icon"},buffer,"base64");
};
}());

View File

@ -34,10 +34,7 @@ exports.handler = function(request,response,state) {
content = content;
type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream");
}
response.writeHead(status,{
"Content-Type": type
});
response.end(content);
state.sendResponse(status,{"Content-Type": type},content);
});
};

View File

@ -12,38 +12,16 @@ GET /
/*global $tw: false */
"use strict";
var zlib = require("zlib");
exports.method = "GET";
exports.path = /^\/$/;
exports.handler = function(request,response,state) {
var acceptEncoding = request.headers["accept-encoding"];
if(!acceptEncoding) {
acceptEncoding = "";
}
var text = state.wiki.renderTiddler(state.server.get("root-render-type"),state.server.get("root-tiddler")),
responseHeaders = {
"Content-Type": state.server.get("root-serve-type")
};
/*
If the gzip=yes flag for `listen` is set, check if the user agent permits
compression. If so, compress our response. Note that we use the synchronous
functions from zlib to stay in the imperative style. The current `Server`
doesn't depend on this, and we may just as well use the async versions.
*/
if(state.server.enableGzip) {
if (/\bdeflate\b/.test(acceptEncoding)) {
responseHeaders["Content-Encoding"] = "deflate";
text = zlib.deflateSync(text);
} else if (/\bgzip\b/.test(acceptEncoding)) {
responseHeaders["Content-Encoding"] = "gzip";
text = zlib.gzipSync(text);
}
}
response.writeHead(200,responseHeaders);
response.end(text);
state.sendResponse(200,responseHeaders,text);
};
}());

View File

@ -25,8 +25,9 @@ exports.handler = function(request,response,state) {
response.end();
} else {
// Redirect to the root wiki if login worked
var location = ($tw.syncadaptor && $tw.syncadaptor.host)? $tw.syncadaptor.host: "/";
response.writeHead(302,{
Location: "/"
Location: location
});
response.end();
}

View File

@ -17,7 +17,6 @@ exports.method = "GET";
exports.path = /^\/status$/;
exports.handler = function(request,response,state) {
response.writeHead(200, {"Content-Type": "application/json"});
var text = JSON.stringify({
username: state.authenticatedUsername || state.server.get("anon-username") || "",
anonymous: !state.authenticatedUsername,
@ -28,7 +27,7 @@ exports.handler = function(request,response,state) {
},
tiddlywiki_version: $tw.version
});
response.end(text,"utf8");
state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
};
}());

View File

@ -32,9 +32,9 @@ exports.handler = function(request,response,state) {
renderTemplate = renderTemplate || state.server.get("tiddler-render-template");
}
var text = state.wiki.renderTiddler(renderType,renderTemplate,{parseAsInline: true, variables: {currentTiddler: title}});
// Naughty not to set a content-type, but it's the easiest way to ensure the browser will see HTML pages as HTML, and accept plain text tiddlers as CSS or JS
response.writeHead(200);
response.end(text,"utf8");
state.sendResponse(200,{},text,"utf8");
} else {
response.writeHead(404);
response.end();

View File

@ -36,8 +36,7 @@ exports.handler = function(request,response,state) {
tiddlerFields.revision = state.wiki.getChangeCount(title);
tiddlerFields.bag = "default";
tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki";
response.writeHead(200, {"Content-Type": "application/json"});
response.end(JSON.stringify(tiddlerFields),"utf8");
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8");
} else {
response.writeHead(404);
response.end();

View File

@ -33,7 +33,6 @@ exports.handler = function(request,response,state) {
}
var excludeFields = (state.queryParameters.exclude || "text").split(","),
titles = state.wiki.filterTiddlers(filter);
response.writeHead(200, {"Content-Type": "application/json"});
var tiddlers = [];
$tw.utils.each(titles,function(title) {
var tiddler = state.wiki.getTiddler(title);
@ -45,7 +44,7 @@ exports.handler = function(request,response,state) {
}
});
var text = JSON.stringify(tiddlers);
response.end(text,"utf8");
state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
};
}());

View File

@ -17,7 +17,9 @@ if($tw.node) {
fs = require("fs"),
url = require("url"),
path = require("path"),
querystring = require("querystring");
querystring = require("querystring"),
crypto = require("crypto"),
zlib = require("zlib");
}
/*
@ -47,6 +49,8 @@ function Server(options) {
this.csrfDisable = this.get("csrf-disable") === "yes";
// Initialize Gzip compression
this.enableGzip = this.get("gzip") === "yes";
// Initialize browser-caching
this.enableBrowserCache = this.get("use-browser-cache") === "yes";
// Initialise authorization
var authorizedUserName = (this.get("username") && this.get("password")) ? this.get("username") : "(anon)";
this.authorizationPrincipals = {
@ -78,6 +82,71 @@ function Server(options) {
this.transport = require(this.protocol);
}
/*
Send a response to the client. This method checks if the response must be sent
or if the client alrady has the data cached. If that's the case only a 304
response will be transmitted and the browser will use the cached data.
Only requests with status code 200 are considdered for caching.
request: request instance passed to the handler
response: response instance passed to the handler
statusCode: stauts code to send to the browser
headers: response headers (they will be augmented with an `Etag` header)
data: the data to send (passed to the end method of the response instance)
encoding: the encoding of the data to send (passed to the end method of the response instance)
*/
function sendResponse(request,response,statusCode,headers,data,encoding) {
if(this.enableBrowserCache && (statusCode == 200)) {
var hash = crypto.createHash('md5');
// Put everything into the hash that could change and invalidate the data that
// the browser already stored. The headers the data and the encoding.
hash.update(data);
hash.update(JSON.stringify(headers));
if(encoding) {
hash.update(encoding);
}
var contentDigest = hash.digest("hex");
// RFC 7232 section 2.3 mandates for the etag to be enclosed in quotes
headers["Etag"] = '"' + contentDigest + '"';
headers["Cache-Control"] = "max-age=0, must-revalidate";
// Check if any of the hashes contained within the if-none-match header
// matches the current hash.
// If one matches, do not send the data but tell the browser to use the
// cached data.
// We do not implement "*" as it makes no sense here.
var ifNoneMatch = request.headers["if-none-match"];
if(ifNoneMatch) {
var matchParts = ifNoneMatch.split(",").map(function(etag) {
return etag.replace(/^[ "]+|[ "]+$/g, "");
});
if(matchParts.indexOf(contentDigest) != -1) {
response.writeHead(304,headers);
response.end();
return;
}
}
}
/*
If the gzip=yes is set, check if the user agent permits compression. If so,
compress our response if the raw data is bigger than 2k. Compressing less
data is inefficient. Note that we use the synchronous functions from zlib
to stay in the imperative style. The current `Server` doesn't depend on
this, and we may just as well use the async versions.
*/
if(this.enableGzip && (data.length > 2048)) {
var acceptEncoding = request.headers["accept-encoding"] || "";
if(/\bdeflate\b/.test(acceptEncoding)) {
headers["Content-Encoding"] = "deflate";
data = zlib.deflateSync(data);
} else if(/\bgzip\b/.test(acceptEncoding)) {
headers["Content-Encoding"] = "gzip";
data = zlib.gzipSync(data);
}
}
response.writeHead(statusCode,headers);
response.end(data,encoding);
}
Server.prototype.defaultVariables = {
port: "8080",
host: "127.0.0.1",
@ -89,7 +158,8 @@ Server.prototype.defaultVariables = {
"system-tiddler-render-type": "text/plain",
"system-tiddler-render-template": "$:/core/templates/wikified-tiddler",
"debug-level": "none",
"gzip": "no"
"gzip": "no",
"use-browser-cache": "no"
};
Server.prototype.get = function(name) {
@ -167,6 +237,7 @@ Server.prototype.requestHandler = function(request,response,options) {
state.urlInfo = url.parse(request.url);
state.queryParameters = querystring.parse(state.urlInfo.query);
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
state.sendResponse = sendResponse.bind(self,request,response);
// Get the principals authorized to access this resource
var authorizationType = this.methodMappings[request.method] || "readers";
// Check for the CSRF header if this is a write

View File

@ -82,7 +82,7 @@ exports.startup = function() {
var onlyThrottledTiddlersHaveChanged = true;
for(var title in changes) {
var tiddler = $tw.wiki.getTiddler(title);
if(!tiddler || !(tiddler.hasField("draft.of") || tiddler.hasField("throttle.refresh"))) {
if(!$tw.wiki.isVolatileTiddler(title) && (!tiddler || !(tiddler.hasField("draft.of") || tiddler.hasField("throttle.refresh")))) {
onlyThrottledTiddlersHaveChanged = false;
}
}

View File

@ -22,6 +22,10 @@ exports.domContains = function(a,b) {
!!(a.compareDocumentPosition(b) & 16);
};
exports.domMatchesSelector = function(node,selector) {
return node.matches ? node.matches(selector) : node.msMatchesSelector(selector);
};
exports.removeChildren = function(node) {
while(node.hasChildNodes()) {
node.removeChild(node.firstChild);

View File

@ -205,4 +205,16 @@ function parseJSONTiddlers(json,fallbackTitle) {
return data;
};
exports.dragEventContainsFiles = function(event) {
if(event.dataTransfer.types) {
for(var i=0; i<event.dataTransfer.types.length; i++) {
if(event.dataTransfer.types[i] === "Files") {
return true;
break;
}
}
}
return false;
};
})();

View File

@ -1,33 +1,28 @@
/*\
title: $:/core/modules/startup/CSSescape.js
title: $:/core/modules/utils/escapecss.js
type: application/javascript
module-type: startup
module-type: utils
Polyfill for CSS.escape()
Provides CSS.escape() functionality.
\*/
(function(root,factory){
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
/*global $tw: false, window: false */
"use strict";
// Export name and synchronous status
exports.name = "css-escape";
exports.platforms = ["browser"];
exports.after = ["startup"];
exports.synchronous = true;
/*! https://mths.be/cssescape v1.5.1 by @mathias | MIT license */
// https://github.com/umdjs/umd/blob/master/returnExports.js
exports.startup = function() {factory(root);};
}(typeof global != 'undefined' ? global : this, function(root) {
if (root.CSS && root.CSS.escape) {
return;
exports.escapeCSS = (function() {
// use browser's native CSS.escape() function if available
if ($tw.browser && window.CSS && window.CSS.escape) {
return window.CSS.escape;
}
// https://drafts.csswg.org/cssom/#serialize-an-identifier
var cssEscape = function(value) {
// otherwise, a utility method is provided
// see also https://drafts.csswg.org/cssom/#serialize-an-identifier
/*! https://mths.be/cssescape v1.5.1 by @mathias | MIT license */
return function(value) {
if (arguments.length == 0) {
throw new TypeError('`CSS.escape` requires an argument.');
}
@ -104,11 +99,6 @@ exports.startup = function() {factory(root);};
}
return result;
};
})();
if (!root.CSS) {
root.CSS = {};
}
Object.getPrototypeOf(root.CSS).escape = cssEscape;
}));
})();

View File

@ -213,13 +213,13 @@ Options include:
extFilters: optional array of filters to be used to generate the base path
wiki: optional wiki for evaluating the pathFilters,
fileInfo: an existing fileInfo to check against
originalpath: a preferred filepath if no pathFilters match
*/
exports.generateTiddlerFileInfo = function(tiddler,options) {
var fileInfo = {}, metaExt;
// Propagate the isEditableFile flag
if(options.fileInfo) {
fileInfo.isEditableFile = options.fileInfo.isEditableFile || false;
if(options.fileInfo && !!options.fileInfo.isEditableFile) {
fileInfo.isEditableFile = true;
fileInfo.originalpath = options.fileInfo.originalpath;
}
// Check if the tiddler has any unsafe fields that can't be expressed in a .tid or .meta file: containing control characters, or leading/trailing whitespace
var hasUnsafeFields = false;
@ -247,7 +247,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
fileInfo.hasMetaFile = true;
}
if(options.extFilters) {
// Check for extension override
// Check for extension overrides
metaExt = $tw.utils.generateTiddlerExtension(tiddler.fields.title,{
extFilters: options.extFilters,
wiki: options.wiki
@ -279,8 +279,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
directory: options.directory,
pathFilters: options.pathFilters,
wiki: options.wiki,
fileInfo: options.fileInfo,
originalpath: options.originalpath
fileInfo: options.fileInfo
});
return fileInfo;
};
@ -292,8 +291,7 @@ Options include:
wiki: optional wiki for evaluating the extFilters
*/
exports.generateTiddlerExtension = function(title,options) {
var self = this,
extension;
var extension;
// Check if any of the extFilters applies
if(options.extFilters && options.wiki) {
$tw.utils.each(options.extFilters,function(filter) {
@ -319,11 +317,10 @@ Options include:
fileInfo: an existing fileInfo object to check against
*/
exports.generateTiddlerFilepath = function(title,options) {
var self = this,
directory = options.directory || "",
var directory = options.directory || "",
extension = options.extension || "",
originalpath = options.originalpath || "",
filepath;
originalpath = (options.fileInfo && options.fileInfo.originalpath) ? options.fileInfo.originalpath : "",
filepath;
// Check if any of the pathFilters applies
if(options.pathFilters && options.wiki) {
$tw.utils.each(options.pathFilters,function(filter) {
@ -336,7 +333,7 @@ exports.generateTiddlerFilepath = function(title,options) {
}
});
}
if(!filepath && originalpath !== "") {
if(!filepath && !!originalpath) {
//Use the originalpath without the extension
var ext = path.extname(originalpath);
filepath = originalpath.substring(0,originalpath.length - ext.length);
@ -345,27 +342,35 @@ exports.generateTiddlerFilepath = function(title,options) {
// Remove any forward or backward slashes so we don't create directories
filepath = filepath.replace(/\/|\\/g,"_");
}
//If the path does not start with "." or ".." and a path seperator, then
// 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
if(!/^\.{1,2}[/\\]/g.test(filepath)) {
// Don't let the filename start with any dots because such files are invisible on *nix
filepath = filepath.replace(/^\.+/g,"_");
filepath = filepath.replace(/^\.+/g,function (u) { return u.replace(/\./g, "_")});
}
// Replace any Unicode control codes
filepath = filepath.replace(/[\x00-\x1f\x80-\x9f]/g,"_");
// Replace any characters that can't be used in cross-platform filenames
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/g,"_"));
// Replace any 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
if(extension.length > 32) {
extension = extension.substr(0,32);
}
// If the filepath already ends in the extension then remove it
if(filepath.substring(filepath.length - extension.length) === extension) {
filepath = filepath.substring(0,filepath.length - extension.length);
}
// Remove any characters that can't be used in cross-platform filenames
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/g,"_"));
// Truncate the filename if it is too long
if(filepath.length > 200) {
filepath = filepath.substr(0,200);
}
// Truncate the extension if it is too long
if(extension.length > 32) {
extension = extension.substr(0,32);
}
// If the resulting filename is blank (eg because the title is just punctuation characters)
if(!filepath) {
// If the resulting filename is blank (eg because the title is just punctuation)
if(!filepath || /^_+$/g.test(filepath)) {
// ...then just use the character codes of the title
filepath = "";
$tw.utils.each(title.split(""),function(char) {
@ -386,14 +391,15 @@ exports.generateTiddlerFilepath = function(title,options) {
count++;
} while(fs.existsSync(fullPath));
// If the last write failed with an error, or if path does not start with:
// the resolved options.directory, the resolved wikiPath directory, or the wikiTiddlersPath directory,
// then encodeURIComponent() and resolve to tiddler directory
var writePath = $tw.hooks.invokeHook("th-make-tiddler-path",fullPath),
// the resolved options.directory, the resolved wikiPath directory, the wikiTiddlersPath directory,
// or the 'originalpath' directory, then encodeURIComponent() and resolve to tiddler directory.
var writePath = $tw.hooks.invokeHook("th-make-tiddler-path",fullPath,fullPath),
encode = (options.fileInfo || {writeError: false}).writeError == true;
if(!encode) {
encode = !(fullPath.indexOf(path.resolve(directory)) == 0 ||
fullPath.indexOf(path.resolve($tw.boot.wikiPath)) == 0 ||
fullPath.indexOf($tw.boot.wikiTiddlersPath) == 0);
encode = !(writePath.indexOf($tw.boot.wikiTiddlersPath) == 0 ||
writePath.indexOf(path.resolve(directory)) == 0 ||
writePath.indexOf(path.resolve($tw.boot.wikiPath)) == 0 ||
writePath.indexOf(path.resolve($tw.boot.wikiTiddlersPath,originalpath)) == 0 );
}
if(encode) {
writePath = path.resolve(directory,encodeURIComponent(fullPath));
@ -413,7 +419,7 @@ exports.saveTiddlerToFile = function(tiddler,fileInfo,callback) {
if(fileInfo.hasMetaFile) {
// Save the tiddler as a separate body and meta file
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
fs.writeFile(fileInfo.filepath,tiddler.fields.text,typeInfo.encoding,function(err) {
fs.writeFile(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding,function(err) {
if(err) {
return callback(err);
}
@ -455,7 +461,7 @@ exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
if(fileInfo.hasMetaFile) {
// Save the tiddler as a separate body and meta file
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
fs.writeFileSync(fileInfo.filepath,tiddler.fields.text,typeInfo.encoding);
fs.writeFileSync(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding);
fs.writeFileSync(fileInfo.filepath + ".meta",tiddler.getFieldStringBlock({exclude: ["text","bag"]}),"utf8");
} else {
// Save the tiddler as a self contained templated file
@ -465,6 +471,7 @@ exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
fs.writeFileSync(fileInfo.filepath,JSON.stringify([tiddler.getFieldStrings({exclude: ["bag"]})],null,$tw.config.preferences.jsonSpaces),"utf8");
}
}
return fileInfo;
};
/*

View File

@ -294,6 +294,47 @@ exports.slowInSlowOut = function(t) {
return (1 - ((Math.cos(t * Math.PI) + 1) / 2));
};
exports.formatTitleString = function(template,options) {
var base = options.base || "",
separator = options.separator || "",
counter = options.counter || "";
var result = "",
t = template,
matches = [
[/^\$basename\$/i, function() {
return base;
}],
[/^\$count:(\d+)\$/i, function(match) {
return $tw.utils.pad(counter,match[1]);
}],
[/^\$separator\$/i, function() {
return separator;
}],
[/^\$count\$/i, function() {
return counter + "";
}]
];
while(t.length){
var matchString = "";
$tw.utils.each(matches, function(m) {
var match = m[0].exec(t);
if(match) {
matchString = m[1].call(null,match);
t = t.substr(match[0].length);
return false;
}
});
if(matchString) {
result += matchString;
} else {
result += t.charAt(0);
t = t.substr(1);
}
}
result = result.replace(/\\(.)/g,"$1");
return result;
};
exports.formatDateString = function(date,template) {
var result = "",
t = template,
@ -515,6 +556,15 @@ exports.htmlEncode = function(s) {
}
};
// Converts like htmlEncode, but forgets the double quote for brevity
exports.htmlTextEncode = function(s) {
if(s) {
return s.toString().replace(/&/mg,"&amp;").replace(/</mg,"&lt;").replace(/>/mg,"&gt;");
} else {
return "";
}
};
// Converts all HTML entities to their character equivalents
exports.entityDecode = function(s) {
var converter = String.fromCodePoint || String.fromCharCode,
@ -867,7 +917,9 @@ exports.stringifyNumber = function(num) {
exports.makeCompareFunction = function(type,options) {
options = options || {};
var gt = options.invert ? -1 : +1,
// set isCaseSensitive to true if not defined in options
var isCaseSensitive = (options.isCaseSensitive === false) ? false : true,
gt = options.invert ? -1 : +1,
lt = options.invert ? +1 : -1,
compare = function(a,b) {
if(a > b) {
@ -886,7 +938,11 @@ exports.makeCompareFunction = function(type,options) {
return compare($tw.utils.parseInt(a),$tw.utils.parseInt(b));
},
"string": function(a,b) {
return compare("" + a,"" +b);
if(!isCaseSensitive) {
a = a.toLowerCase();
b = b.toLowerCase();
}
return compare("" + a,"" + b);
},
"date": function(a,b) {
var dateA = $tw.utils.parseDate(a),
@ -901,6 +957,13 @@ exports.makeCompareFunction = function(type,options) {
},
"version": function(a,b) {
return $tw.utils.compareVersions(a,b);
},
"alphanumeric": function(a,b) {
if(!isCaseSensitive) {
a = a.toLowerCase();
b = b.toLowerCase();
}
return options.invert ? b.localeCompare(a,undefined,{numeric: true,sensitivity: "base"}) : a.localeCompare(b,undefined,{numeric: true,sensitivity: "base"});
}
};
return (types[type] || types[options.defaultType] || types.number);

View File

@ -44,9 +44,7 @@ ActionListopsWidget.prototype.execute = function() {
*/
ActionListopsWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.$tiddler || changedAttributes.$filter ||
changedAttributes.$subfilter || changedAttributes.$field ||
changedAttributes.$index || changedAttributes.$tags) {
if($tw.utils.count(changedAttributes) > 0) {
this.refreshSelf();
return true;
}
@ -60,12 +58,10 @@ ActionListopsWidget.prototype.invokeAction = function(triggeringWidget,
//Apply the specified filters to the lists
var field = this.listField,
index,
type = "!!",
list = this.listField;
if(this.listIndex) {
field = undefined;
index = this.listIndex;
type = "##";
list = this.listIndex;
}
if(this.filter) {
@ -74,15 +70,14 @@ ActionListopsWidget.prototype.invokeAction = function(triggeringWidget,
.filterTiddlers(this.filter, this)));
}
if(this.subfilter) {
var subfilter = "[list[" + this.target + type + list + "]] " + this.subfilter;
this.wiki.setText(this.target, field, index, $tw.utils.stringifyList(
this.wiki
.filterTiddlers(subfilter, this)));
var inputList = this.wiki.getTiddlerList(this.target,field,index),
subfilter = $tw.utils.stringifyList(inputList) + " " + this.subfilter;
this.wiki.setText(this.target, field, index, $tw.utils.stringifyList(this.wiki.filterTiddlers(subfilter,this)));
}
if(this.filtertags) {
var tiddler = this.wiki.getTiddler(this.target),
oldtags = tiddler ? (tiddler.fields.tags || []).slice(0) : [],
tagfilter = "[list[" + this.target + "!!tags]] " + this.filtertags,
tagfilter = $tw.utils.stringifyList(oldtags) + " " + this.filtertags,
newtags = this.wiki.filterTiddlers(tagfilter,this);
if($tw.utils.stringifyList(oldtags.sort()) !== $tw.utils.stringifyList(newtags.sort())) {
this.wiki.setText(this.target,"tags",undefined,$tw.utils.stringifyList(newtags));

View File

@ -37,6 +37,7 @@ Compute the internal state of the widget
ActionPopupWidget.prototype.execute = function() {
this.actionState = this.getAttribute("$state");
this.actionCoords = this.getAttribute("$coords");
this.floating = this.getAttribute("$floating","no") === "yes";
};
/*
@ -68,7 +69,8 @@ ActionPopupWidget.prototype.invokeAction = function(triggeringWidget,event) {
height: parseFloat(match[4])
},
title: this.actionState,
wiki: this.wiki
wiki: this.wiki,
floating: this.floating
});
} else {
$tw.popup.cancel(0);

View File

@ -12,6 +12,8 @@ Dropzone widget
/*global $tw: false */
"use strict";
var IMPORT_TITLE = "$:/Import";
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var DropZoneWidget = function(parseTreeNode,options) {
@ -35,6 +37,7 @@ DropZoneWidget.prototype.render = function(parent,nextSibling) {
this.execute();
// Create element
var domNode = this.document.createElement("div");
this.domNode = domNode;
domNode.className = this.dropzoneClass || "tc-dropzone";
// Add event handlers
if(this.dropzoneEnable) {
@ -45,10 +48,8 @@ DropZoneWidget.prototype.render = function(parent,nextSibling) {
{name: "drop", handlerObject: this, handlerMethod: "handleDropEvent"},
{name: "paste", handlerObject: this, handlerMethod: "handlePasteEvent"},
{name: "dragend", handlerObject: this, handlerMethod: "handleDragEndEvent"}
]);
]);
}
domNode.addEventListener("click",function (event) {
},false);
// Insert element
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
@ -57,12 +58,46 @@ DropZoneWidget.prototype.render = function(parent,nextSibling) {
this.currentlyEntered = [];
};
// Handler for transient event listeners added when the dropzone has an active drag in progress
DropZoneWidget.prototype.handleEvent = function(event) {
if(event.type === "click") {
if(this.currentlyEntered.length) {
this.resetState();
}
} else if(event.type === "dragenter") {
if(event.target && event.target !== this.domNode && !$tw.utils.domContains(this.domNode,event.target)) {
this.resetState();
}
} else if(event.type === "dragleave") {
// Check if drag left the window
if(event.relatedTarget === null || (event.relatedTarget && event.relatedTarget.nodeName === "HTML")) {
this.resetState();
}
}
};
// Reset the state of the dropzone after a drag has ended
DropZoneWidget.prototype.resetState = function() {
$tw.utils.removeClass(this.domNode,"tc-dragover");
this.currentlyEntered = [];
this.document.body.removeEventListener("click",this,true);
this.document.body.removeEventListener("dragenter",this,true);
this.document.body.removeEventListener("dragleave",this,true);
this.dragInProgress = false;
};
DropZoneWidget.prototype.enterDrag = function(event) {
if(this.currentlyEntered.indexOf(event.target) === -1) {
this.currentlyEntered.push(event.target);
}
// If we're entering for the first time we need to apply highlighting
$tw.utils.addClass(this.domNodes[0],"tc-dragover");
if(!this.dragInProgress) {
this.dragInProgress = true;
// If we're entering for the first time we need to apply highlighting
$tw.utils.addClass(this.domNodes[0],"tc-dragover");
this.document.body.addEventListener("click",this,true);
this.document.body.addEventListener("dragenter",this,true);
this.document.body.addEventListener("dragleave",this,true);
}
};
DropZoneWidget.prototype.leaveDrag = function(event) {
@ -72,15 +107,17 @@ DropZoneWidget.prototype.leaveDrag = function(event) {
}
// Remove highlighting if we're leaving externally
if(this.currentlyEntered.length === 0) {
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
this.resetState();
}
};
DropZoneWidget.prototype.handleDragEnterEvent = function(event) {
// Check for this window being the source of the drag
if($tw.dragInProgress) {
return false;
}
if(this.filesOnly && !$tw.utils.dragEventContainsFiles(event)) {
return false;
}
this.enterDrag(event);
// Tell the browser that we're ready to handle the drop
event.preventDefault();
@ -99,7 +136,10 @@ DropZoneWidget.prototype.handleDragOverEvent = function(event) {
}
// Tell the browser that we're still interested in the drop
event.preventDefault();
event.dataTransfer.dropEffect = "copy"; // Explicitly show this is a copy
// Check if this is a synthetic event, IE does not allow accessing dropEffect outside of original event handler
if(event.isTrusted) {
event.dataTransfer.dropEffect = "copy"; // Explicitly show this is a copy
}
};
DropZoneWidget.prototype.handleDragLeaveEvent = function(event) {
@ -107,13 +147,41 @@ DropZoneWidget.prototype.handleDragLeaveEvent = function(event) {
};
DropZoneWidget.prototype.handleDragEndEvent = function(event) {
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
this.resetState();
};
DropZoneWidget.prototype.filterByContentTypes = function(tiddlerFieldsArray) {
var filteredTypes,
filtered = [],
types = [];
$tw.utils.each(tiddlerFieldsArray,function(tiddlerFields) {
types.push(tiddlerFields.type || "");
});
filteredTypes = this.wiki.filterTiddlers(this.contentTypesFilter,this,this.wiki.makeTiddlerIterator(types));
$tw.utils.each(tiddlerFieldsArray,function(tiddlerFields) {
if(filteredTypes.indexOf(tiddlerFields.type) !== -1) {
filtered.push(tiddlerFields);
}
});
return filtered;
};
DropZoneWidget.prototype.readFileCallback = function(tiddlerFieldsArray) {
if(this.contentTypesFilter) {
tiddlerFieldsArray = this.filterByContentTypes(tiddlerFieldsArray);
}
if(tiddlerFieldsArray.length) {
this.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray), autoOpenOnImport: this.autoOpenOnImport, importTitle: this.importTitle});
if(this.actions) {
this.invokeActionString(this.actions,this,event,{importTitle: this.importTitle});
}
}
};
DropZoneWidget.prototype.handleDropEvent = function(event) {
var self = this,
readFileCallback = function(tiddlerFieldsArray) {
self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray), autoOpenOnImport: self.autoOpenOnImport, importTitle: self.importTitle});
self.readFileCallback(tiddlerFieldsArray);
};
this.leaveDrag(event);
// Check for being over a TEXTAREA or INPUT
@ -127,7 +195,7 @@ DropZoneWidget.prototype.handleDropEvent = function(event) {
var self = this,
dataTransfer = event.dataTransfer;
// Remove highlighting
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
this.resetState();
// Import any files in the drop
var numFiles = 0;
if(dataTransfer.files) {
@ -138,7 +206,23 @@ DropZoneWidget.prototype.handleDropEvent = function(event) {
}
// Try to import the various data types we understand
if(numFiles === 0) {
$tw.utils.importDataTransfer(dataTransfer,this.wiki.generateNewTitle("Untitled"),readFileCallback);
var fallbackTitle = self.wiki.generateNewTitle("Untitled");
//Use the deserializer specified if any
if(this.dropzoneDeserializer) {
for(var t= 0; t<dataTransfer.items.length; t++) {
var item = dataTransfer.items[t];
if(item.kind === "string") {
item.getAsString(function(str){
var tiddlerFields = self.wiki.deserializeTiddlers(null,str,{title: fallbackTitle},{deserializer:self.dropzoneDeserializer});
if(tiddlerFields && tiddlerFields.length) {
readFileCallback(tiddlerFields);
}
})
}
}
} else {
$tw.utils.importDataTransfer(dataTransfer,fallbackTitle,readFileCallback);
}
}
// Tell the browser that we handled the drop
event.preventDefault();
@ -149,7 +233,7 @@ DropZoneWidget.prototype.handleDropEvent = function(event) {
DropZoneWidget.prototype.handlePasteEvent = function(event) {
var self = this,
readFileCallback = function(tiddlerFieldsArray) {
self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray), autoOpenOnImport: self.autoOpenOnImport, importTitle: self.importTitle});
self.readFileCallback(tiddlerFieldsArray);
};
// Let the browser handle it if we're in a textarea or input box
if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) == -1 && !event.target.isContentEditable) {
@ -166,17 +250,26 @@ DropZoneWidget.prototype.handlePasteEvent = function(event) {
});
} else if(item.kind === "string") {
// Create tiddlers from string items
var type = item.type;
var tiddlerFields,
type = item.type;
item.getAsString(function(str) {
var tiddlerFields = {
title: self.wiki.generateNewTitle("Untitled"),
text: str,
type: type
};
if($tw.log.IMPORT) {
console.log("Importing string '" + str + "', type: '" + type + "'");
// Use the deserializer specified if any
if(self.dropzoneDeserializer) {
tiddlerFields = self.wiki.deserializeTiddlers(null,str,{title: self.wiki.generateNewTitle("Untitled")},{deserializer:self.dropzoneDeserializer});
if(tiddlerFields && tiddlerFields.length) {
readFileCallback(tiddlerFields);
}
} else {
tiddlerFields = {
title: self.wiki.generateNewTitle("Untitled"),
text: str,
type: type
};
if($tw.log.IMPORT) {
console.log("Importing string '" + str + "', type: '" + type + "'");
}
readFileCallback([tiddlerFields]);
}
self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify([tiddlerFields]), autoOpenOnImport: self.autoOpenOnImport, importTitle: self.importTitle});
});
}
}
@ -194,7 +287,10 @@ DropZoneWidget.prototype.execute = function() {
this.dropzoneDeserializer = this.getAttribute("deserializer");
this.dropzoneEnable = (this.getAttribute("enable") || "yes") === "yes";
this.autoOpenOnImport = this.getAttribute("autoOpenOnImport");
this.importTitle = this.getAttribute("importTitle");
this.importTitle = this.getAttribute("importTitle",IMPORT_TITLE);
this.actions = this.getAttribute("actions");
this.contentTypesFilter = this.getAttribute("contentTypesFilter");
this.filesOnly = this.getAttribute("filesOnly","no") === "yes";
// Make child widgets
this.makeChildWidgets();
};
@ -204,7 +300,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
DropZoneWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.enable || changedAttributes.autoOpenOnImport || changedAttributes.importTitle || changedAttributes.deserializer || changedAttributes.class) {
if($tw.utils.count(changedAttributes) > 0) {
this.refreshSelf();
return true;
}

View File

@ -47,38 +47,46 @@ EventWidget.prototype.render = function(parent,nextSibling) {
domNode.addEventListener(type,function(event) {
var selector = self.getAttribute("selector"),
actions = self.getAttribute("actions-"+type),
stopPropagation = self.getAttribute("stopPropagation","onaction"),
selectedNode = event.target,
selectedNodeRect,
catcherNodeRect,
variables = {};
// Firefox can fire dragover and dragenter events on text nodes instead of their parents
if(selectedNode.nodeType === 3) {
selectedNode = selectedNode.parentNode;
}
if(selector) {
// Search ancestors for a node that matches the selector
while(!selectedNode.matches(selector) && selectedNode !== domNode) {
while(!$tw.utils.domMatchesSelector(selectedNode,selector) && selectedNode !== domNode) {
selectedNode = selectedNode.parentNode;
}
// If we found one, copy the attributes as variables, otherwise exit
if(selectedNode.matches(selector)) {
$tw.utils.each(selectedNode.attributes,function(attribute) {
variables["dom-" + attribute.name] = attribute.value.toString();
});
//Add a variable with a popup coordinate string for the selected node
variables["tv-popup-coords"] = "(" + selectedNode.offsetLeft + "," + selectedNode.offsetTop +"," + selectedNode.offsetWidth + "," + selectedNode.offsetHeight + ")";
//Add variables for offset of selected node
variables["tv-selectednode-posx"] = selectedNode.offsetLeft.toString();
variables["tv-selectednode-posy"] = selectedNode.offsetTop.toString();
variables["tv-selectednode-width"] = selectedNode.offsetWidth.toString();
variables["tv-selectednode-height"] = selectedNode.offsetHeight.toString();
if($tw.utils.domMatchesSelector(selectedNode,selector)) {
// Only set up variables if we have actions to invoke
if(actions) {
$tw.utils.each(selectedNode.attributes,function(attribute) {
variables["dom-" + attribute.name] = attribute.value.toString();
});
//Add a variable with a popup coordinate string for the selected node
variables["tv-popup-coords"] = "(" + selectedNode.offsetLeft + "," + selectedNode.offsetTop +"," + selectedNode.offsetWidth + "," + selectedNode.offsetHeight + ")";
//Add variables for event X and Y position relative to selected node
selectedNodeRect = selectedNode.getBoundingClientRect();
variables["event-fromselected-posx"] = (event.clientX - selectedNodeRect.left).toString();
variables["event-fromselected-posy"] = (event.clientY - selectedNodeRect.top).toString();
//Add variables for offset of selected node
variables["tv-selectednode-posx"] = selectedNode.offsetLeft.toString();
variables["tv-selectednode-posy"] = selectedNode.offsetTop.toString();
variables["tv-selectednode-width"] = selectedNode.offsetWidth.toString();
variables["tv-selectednode-height"] = selectedNode.offsetHeight.toString();
//Add variables for event X and Y position relative to event catcher node
catcherNodeRect = self.domNode.getBoundingClientRect();
variables["event-fromcatcher-posx"] = (event.clientX - catcherNodeRect.left).toString();
variables["event-fromcatcher-posy"] = (event.clientY - catcherNodeRect.top).toString();
//Add variables for event X and Y position relative to selected node
selectedNodeRect = selectedNode.getBoundingClientRect();
variables["event-fromselected-posx"] = (event.clientX - selectedNodeRect.left).toString();
variables["event-fromselected-posy"] = (event.clientY - selectedNodeRect.top).toString();
//Add variables for event X and Y position relative to event catcher node
catcherNodeRect = self.domNode.getBoundingClientRect();
variables["event-fromcatcher-posx"] = (event.clientX - catcherNodeRect.left).toString();
variables["event-fromcatcher-posy"] = (event.clientY - catcherNodeRect.top).toString();
}
} else {
return false;
}
@ -106,6 +114,8 @@ EventWidget.prototype.render = function(parent,nextSibling) {
variables["event-detail"] = event.detail.toString();
}
self.invokeActionString(actions,self,event,variables);
}
if((actions && stopPropagation === "onaction") || stopPropagation === "always") {
event.preventDefault();
event.stopPropagation();
return true;

View File

@ -67,7 +67,7 @@ FieldManglerWidget.prototype.handleRemoveFieldEvent = function(event) {
deletion = {};
deletion[event.param] = undefined;
this.wiki.addTiddler(new $tw.Tiddler(tiddler,deletion));
return true;
return false;
};
FieldManglerWidget.prototype.handleAddFieldEvent = function(event) {
@ -105,7 +105,7 @@ FieldManglerWidget.prototype.handleAddFieldEvent = function(event) {
}
}
this.wiki.addTiddler(new $tw.Tiddler(tiddler,addition));
return true;
return false;
};
FieldManglerWidget.prototype.handleRemoveTagEvent = function(event) {
@ -122,7 +122,7 @@ FieldManglerWidget.prototype.handleRemoveTagEvent = function(event) {
this.wiki.addTiddler(new $tw.Tiddler(tiddler,modification));
}
}
return true;
return false;
};
FieldManglerWidget.prototype.handleAddTagEvent = function(event) {
@ -140,7 +140,7 @@ FieldManglerWidget.prototype.handleAddTagEvent = function(event) {
tag.push(event.param.trim());
this.wiki.addTiddler(new $tw.Tiddler({title: this.mangleTitle, tags: tag},modification));
}
return true;
return false;
};
exports.fieldmangler = FieldManglerWidget;

View File

@ -61,6 +61,7 @@ ListWidget.prototype.execute = function() {
this.template = this.getAttribute("template");
this.editTemplate = this.getAttribute("editTemplate");
this.variableName = this.getAttribute("variable","currentTiddler");
this.counterName = this.getAttribute("counter");
this.storyViewName = this.getAttribute("storyview");
this.historyTitle = this.getAttribute("history");
// Compose the list elements
@ -72,7 +73,7 @@ ListWidget.prototype.execute = function() {
members = this.getEmptyMessage();
} else {
$tw.utils.each(this.list,function(title,index) {
members.push(self.makeItemTemplate(title));
members.push(self.makeItemTemplate(title,index));
});
}
// Construct the child widgets
@ -105,7 +106,7 @@ ListWidget.prototype.getEmptyMessage = function() {
/*
Compose the template for a list item
*/
ListWidget.prototype.makeItemTemplate = function(title) {
ListWidget.prototype.makeItemTemplate = function(title,index) {
// Check if the tiddler is a draft
var tiddler = this.wiki.getTiddler(title),
isDraft = tiddler && tiddler.hasField("draft.of"),
@ -128,7 +129,14 @@ ListWidget.prototype.makeItemTemplate = function(title) {
}
}
// Return the list item
return {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree};
var parseTreeNode = {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree};
if(this.counterName) {
parseTreeNode.counter = (index + 1).toString();
parseTreeNode.counterName = this.counterName;
parseTreeNode.isFirst = index === 0;
parseTreeNode.isLast = index === this.list.length - 1;
}
return parseTreeNode;
};
/*
@ -142,7 +150,7 @@ ListWidget.prototype.refresh = function(changedTiddlers) {
this.storyview.refreshStart(changedTiddlers,changedAttributes);
}
// Completely refresh if any of our attributes have changed
if(changedAttributes.filter || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) {
if(changedAttributes.filter || changedAttributes.variable || changedAttributes.counter || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) {
this.refreshSelf();
result = true;
} else {
@ -211,23 +219,41 @@ ListWidget.prototype.handleListChanges = function(changedTiddlers) {
this.removeChildDomNodes();
this.children = [];
}
// Cycle through the list, inserting and removing list items as needed
var hasRefreshed = false;
for(var t=0; t<this.list.length; t++) {
var index = this.findListItem(t,this.list[t]);
if(index === undefined) {
// The list item must be inserted
this.insertListItem(t,this.list[t]);
hasRefreshed = true;
} else {
// There are intervening list items that must be removed
for(var n=index-1; n>=t; n--) {
this.removeListItem(n);
// If we are providing an counter variable then we must refresh the items, otherwise we can rearrange them
var hasRefreshed = false,t;
if(this.counterName) {
// Cycle through the list and remove and re-insert the first item that has changed, and all the remaining items
for(t=0; t<this.list.length; t++) {
if(hasRefreshed || !this.children[t] || this.children[t].parseTreeNode.itemTitle !== this.list[t]) {
if(this.children[t]) {
this.removeListItem(t);
}
this.insertListItem(t,this.list[t]);
hasRefreshed = true;
} else {
// Refresh the item we're reusing
var refreshed = this.children[t].refresh(changedTiddlers);
hasRefreshed = hasRefreshed || refreshed;
}
}
} else {
// Cycle through the list, inserting and removing list items as needed
for(t=0; t<this.list.length; t++) {
var index = this.findListItem(t,this.list[t]);
if(index === undefined) {
// The list item must be inserted
this.insertListItem(t,this.list[t]);
hasRefreshed = true;
} else {
// There are intervening list items that must be removed
for(var n=index-1; n>=t; n--) {
this.removeListItem(n);
hasRefreshed = true;
}
// Refresh the item we're reusing
var refreshed = this.children[t].refresh(changedTiddlers);
hasRefreshed = hasRefreshed || refreshed;
}
// Refresh the item we're reusing
var refreshed = this.children[t].refresh(changedTiddlers);
hasRefreshed = hasRefreshed || refreshed;
}
}
// Remove any left over items
@ -257,7 +283,7 @@ Insert a new list item at the specified index
*/
ListWidget.prototype.insertListItem = function(index,title) {
// Create, insert and render the new child widgets
var widget = this.makeChildWidget(this.makeItemTemplate(title));
var widget = this.makeChildWidget(this.makeItemTemplate(title,index));
widget.parentDomNode = this.parentDomNode; // Hack to enable findNextSiblingDomNode() to work
this.children.splice(index,0,widget);
var nextSibling = widget.findNextSiblingDomNode();
@ -311,6 +337,11 @@ Compute the internal state of the widget
ListItemWidget.prototype.execute = function() {
// Set the current list item title
this.setVariable(this.parseTreeNode.variableName,this.parseTreeNode.itemTitle);
if(this.parseTreeNode.counterName) {
this.setVariable(this.parseTreeNode.counterName,this.parseTreeNode.counter);
this.setVariable(this.parseTreeNode.counterName + "-first",this.parseTreeNode.isFirst ? "yes" : "no");
this.setVariable(this.parseTreeNode.counterName + "-last",this.parseTreeNode.isLast ? "yes" : "no");
}
// Construct the child widgets
this.makeChildWidgets();
};

View File

@ -170,7 +170,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
SelectWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
// If we're using a different tiddler/field/index then completely refresh ourselves
if(changedAttributes.selectTitle || changedAttributes.selectField || changedAttributes.selectIndex || changedAttributes.selectTooltip) {
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes.tooltip) {
this.refreshSelf();
return true;
// If the target tiddler value has changed, just update setting and refresh the children

View File

@ -75,17 +75,8 @@ TranscludeWidget.prototype.execute = function() {
]}];
}
}
// Assign any variables set via attributes starting with $
var variables = Object.create(null);
$tw.utils.each(this.attributes,function(attribute,name) {
if(name.charAt(0) === "$") {
variables[name.substr(1)] = attribute;
}
});
// Construct the child widgets
this.makeChildWidgets(parseTreeNodes,{
variables: variables
});
this.makeChildWidgets(parseTreeNodes);
};
/*
@ -112,7 +103,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
TranscludeWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if($tw.utils.count(changedAttributes) || changedTiddlers[this.transcludeTitle]) {
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedTiddlers[this.transcludeTitle]) {
this.refreshSelf();
return true;
} else {

View File

@ -65,6 +65,9 @@ ViewWidget.prototype.execute = function() {
case "htmlencoded":
this.text = this.getValueAsHtmlEncoded();
break;
case "htmltextencoded":
this.text = this.getValueAsHtmlTextEncoded();
break;
case "urlencoded":
this.text = this.getValueAsUrlEncoded();
break;
@ -160,6 +163,10 @@ ViewWidget.prototype.getValueAsHtmlEncoded = function() {
return $tw.utils.htmlEncode(this.getValueAsText());
};
ViewWidget.prototype.getValueAsHtmlTextEncoded = function() {
return $tw.utils.htmlTextEncode(this.getValueAsText());
};
ViewWidget.prototype.getValueAsUrlEncoded = function() {
return encodeURIComponent(this.getValueAsText());
};

View File

@ -190,15 +190,25 @@ exports.getChangeCount = function(title) {
/*
Generate an unused title from the specified base
options.prefix must be a string
*/
exports.generateNewTitle = function(baseTitle,options) {
options = options || {};
var c = 0,
title = baseTitle;
while(this.tiddlerExists(title) || this.isShadowTiddler(title) || this.findDraft(title)) {
title = baseTitle +
(options.prefix || " ") +
(++c);
title = baseTitle,
template = options.template,
prefix = (typeof(options.prefix) === "string") ? options.prefix : " ";
if (template) {
// "count" is important to avoid an endless loop in while(...)!!
template = (/\$count:?(\d+)?\$/i.test(template)) ? template : template + "$count$";
title = $tw.utils.formatTitleString(template,{"base":baseTitle,"separator":prefix,"counter":c});
while(this.tiddlerExists(title) || this.isShadowTiddler(title) || this.findDraft(title)) {
title = $tw.utils.formatTitleString(template,{"base":baseTitle,"separator":prefix,"counter":(++c)});
}
} else {
while(this.tiddlerExists(title) || this.isShadowTiddler(title) || this.findDraft(title)) {
title = baseTitle + prefix + (++c);
}
}
return title;
};
@ -211,6 +221,10 @@ exports.isTemporaryTiddler = function(title) {
return title && title.indexOf("$:/temp/") === 0;
};
exports.isVolatileTiddler = function(title) {
return title && title.indexOf("$:/temp/volatile/") === 0;
};
exports.isImageTiddler = function(title) {
var tiddler = this.getTiddler(title);
if(tiddler) {
@ -364,12 +378,12 @@ exports.sortTiddlers = function(titles,sortField,isDescending,isCaseSensitive,is
var tiddlerA = self.getTiddler(a),
tiddlerB = self.getTiddler(b);
if(tiddlerA) {
a = tiddlerA.fields[sortField] || "";
a = tiddlerA.getFieldString(sortField) || "";
} else {
a = "";
}
if(tiddlerB) {
b = tiddlerB.fields[sortField] || "";
b = tiddlerB.getFieldString(sortField) || "";
} else {
b = "";
}
@ -1495,6 +1509,13 @@ exports.invokeUpgraders = function(titles,tiddlers) {
// Determine whether a plugin by title is dynamically loadable
exports.doesPluginRequireReload = function(title) {
var tiddler = this.getTiddler(title);
if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"]) {
if(tiddler.fields["plugin-type"] === "import") {
// The import plugin never requires reloading
return false;
}
}
return this.doesPluginInfoRequireReload(this.getPluginInfo(title) || this.getTiddlerDataCached(title));
};
@ -1539,4 +1560,3 @@ exports.slugify = function(title,options) {
};
})();

View File

@ -5,5 +5,5 @@ title: $:/core/templates/html-div-tiddler
This template is used for saving tiddlers as an HTML DIV tag with attributes representing the tiddler fields.
-->`<div`<$fields template=' $name$="$encoded_value$"'></$fields>`>
<pre>`<$view field="text" format="htmlencoded" />`</pre>
<pre>`<$view field="text" format="htmltextencoded" />`</pre>
</div>`

View File

@ -30,8 +30,6 @@ http://$(userName)$.tiddlyspot.com/$path$/
|<<lingo UserName>> |<$edit-text tiddler="$:/UploadName" default="" tag="input"/> |
|<<lingo Password>> |<$password name="upload"/> |
|<<lingo Backups>> |<<siteLink backup>> |
|<<lingo ControlPanel>> |<<siteLink controlpanel>> |
''<<lingo Advanced/Heading>>''

View File

@ -8,6 +8,7 @@ title: $:/core/ui/EditTemplate/body/editor
tabindex={{$:/config/EditTabIndex}}
focus={{{ [{$:/config/AutoFocus}match[text]then[true]] ~[[false]] }}}
cancelPopups="yes"
fileDrop={{{ [{$:/config/DragAndDrop/Enable}match[no]] :else[subfilter{$:/config/Editor/EnableImportFilter}then[yes]else[no]] }}}
><$set
@ -30,4 +31,12 @@ title: $:/core/ui/EditTemplate/body/editor
tiddler="$:/core/ui/EditTemplate/body/toolbar/button"
mode="inline"
/></$reveal></$list></$set></$edit>
/></$reveal></$list><$list
filter="[all[shadows+tiddlers]tag[$:/tags/EditorTools]!has[draft.of]]"
><$list
filter={{!!condition}}
variable="list-condition"
><$transclude/>
</$list></$list></$set></$edit>

View File

@ -35,22 +35,23 @@ title: $:/core/ui/EditTemplate/body/toolbar/button
filter="[all[current]!has[dropdown]]"
variable="no-dropdown"
><$button
><$set name=disabled filter={{!!condition-disabled}}><$button
class="tc-btn-invisible $(buttonClasses)$"
tooltip=<<tooltip-text>>
actions={{!!actions}}
disabled=<<disabled>>
><span
data-tw-keyboard-shortcut={{!!shortcuts}}
data-tw-keyboard-shortcut={{{ [<disabled>match[yes]then[]else{!!shortcuts}] }}}
/><<toolbar-button-icon>><$transclude
tiddler=<<currentTiddler>>
field="text"
/></$button></$list><$list
/></$button></$set></$list><$list
filter="[all[current]has[dropdown]]"
variable="dropdown"
@ -60,24 +61,25 @@ title: $:/core/ui/EditTemplate/body/toolbar/button
name="dropdown-state"
value=<<qualify "$:/state/EditorToolbarDropdown">>
><$button
><$set name=disabled filter={{!!condition-disabled}}><$button
popup=<<dropdown-state>>
class="tc-popup-keep tc-btn-invisible $(buttonClasses)$"
selectedClass="tc-selected"
tooltip=<<tooltip-text>>
actions={{!!actions}}
disabled=<<disabled>>
><span
data-tw-keyboard-shortcut={{!!shortcuts}}
data-tw-keyboard-shortcut={{{ [<disabled>match[yes]then[]else{!!shortcuts}] }}}
/><<toolbar-button-icon>><$transclude
tiddler=<<currentTiddler>>
field="text"
/></$button><$reveal
/></$button></$set><$reveal
state=<<dropdown-state>>
type="popup"

View File

@ -5,6 +5,11 @@ tags: $:/tags/EditTemplate
\define config-visibility-title()
$:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$
\end
\define importFileActions()
<$action-popup $state=<<importState>> $coords="(0,0,0,0)" $floating="yes"/>
\end
<$list filter="[all[current]has[_canonical_uri]]">
<div class="tc-message-box">
@ -20,9 +25,8 @@ $:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$
</$list>
<$list filter="[all[current]!has[_canonical_uri]]">
<$reveal state="$:/state/showeditpreview" type="match" text="yes">
<$vars importTitle=<<qualify $:/ImportImage>> importState=<<qualify $:/state/ImportImage>> >
<$dropzone importTitle=<<importTitle>> autoOpenOnImport="no" contentTypesFilter={{$:/config/Editor/ImportContentTypesFilter}} class="tc-dropzone-editor" enable={{{ [{$:/config/DragAndDrop/Enable}match[no]] :else[subfilter{$:/config/Editor/EnableImportFilter}then[yes]else[no]] }}} filesOnly="yes" actions=<<importFileActions>> ><$reveal state="$:/state/showeditpreview" type="match" text="yes">
<div class="tc-tiddler-preview">
<$transclude tiddler="$:/core/ui/EditTemplate/body/editor" mode="inline"/>
@ -38,7 +42,6 @@ $:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$
</div>
</div>
</$reveal>
<$reveal state="$:/state/showeditpreview" type="nomatch" text="yes">
@ -46,5 +49,6 @@ $:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$
<$transclude tiddler="$:/core/ui/EditTemplate/body/editor" mode="inline"/>
</$reveal>
</$list>
</$dropzone>
</$vars>
</$list>

View File

@ -84,7 +84,7 @@ $value={{{ [<newFieldValueTiddler>get[text]] }}}/>
<$fieldmangler>
<div class="tc-edit-field-add">
<em class="tc-edit tc-big-gap-right">
<em class="tc-edit tc-small-gap-right">
<<lingo Fields/Add/Prompt>>
</em>
<$vars refreshTitle=<<qualify "$:/temp/fieldname/refresh">> storeTitle=<<newFieldNameInputTiddler>> searchListState=<<newFieldNameSelectionTiddler>>>

View File

@ -23,7 +23,7 @@ tags: $:/tags/EditTemplate
<div class="tc-message-box">
{{$:/core/images/warning}} {{$:/language/EditTemplate/Title/Exists/Prompt}}
{{$:/core/images/warning}} {{$:/language/EditTemplate/Title/Exists/Prompt}}: <$link to={{!!draft.title}} />
</div>

View File

@ -7,7 +7,7 @@ first-search-filter: [all[shadows+tiddlers]prefix[$:/language/Docs/Types/]sort[d
\whitespace trim
<$set name="refreshTitle" value=<<qualify "$:/temp/type-search/refresh">>>
<div class="tc-edit-type-selector-wrapper">
<em class="tc-edit tc-big-gap-right"><<lingo Type/Prompt>></em>
<em class="tc-edit tc-small-gap-right"><<lingo Type/Prompt>></em>
<div class="tc-type-selector-dropdown-wrapper">
<div class="tc-type-selector"><$fieldmangler>
<$macrocall $name="keyboard-driven-input" tiddler=<<currentTiddler>> storeTitle=<<typeInputTiddler>> refreshTitle=<<refreshTitle>> selectionStateTitle=<<typeSelectionTiddler>> field="type" tag="input" default="" placeholder={{$:/language/EditTemplate/Type/Placeholder}} focusPopup=<<qualify "$:/state/popup/type-dropdown">> class="tc-edit-typeeditor tc-edit-texteditor tc-popup-handle" tabindex={{$:/config/EditTabIndex}} focus={{{ [{$:/config/AutoFocus}match[type]then[true]] ~[[false]] }}} cancelPopups="yes" configTiddlerFilter="[[$:/core/ui/EditTemplate/type]]" inputCancelActions=<<input-cancel-actions>>/><$button popup=<<qualify "$:/state/popup/type-dropdown">> class="tc-btn-invisible tc-btn-dropdown tc-small-gap" tooltip={{$:/language/EditTemplate/Type/Dropdown/Hint}} aria-label={{$:/language/EditTemplate/Type/Dropdown/Caption}}>{{$:/core/images/down-arrow}}</$button><$button message="tm-remove-field" param="type" class="tc-btn-invisible tc-btn-icon" tooltip={{$:/language/EditTemplate/Type/Delete/Hint}} aria-label={{$:/language/EditTemplate/Type/Delete/Caption}}>{{$:/core/images/delete-button}}<$action-deletetiddler $filter="[<storeTitle>] [<refreshTitle>] [<selectionStateTitle>]"/></$button>

View File

@ -0,0 +1,41 @@
title: $:/core/ui/EditorToolbar/file-import
tags: $:/tags/EditorTools
condition: [<targetTiddler>!has[type]] [<targetTiddler>type[text/vnd.tiddlywiki]]
\define lingo-base() $:/language/Import/
\define closePopupActions()
<$action-deletetiddler $filter="[title<importState>] [title<importTitle>]"/>
\end
\define replacement-text-image() [img[$title$]]
\define replacement-text-file() [[$title$]]
\define postImportActions()
<$list filter="[<importTitle>links[]] :reduce[get[type]prefix[image]then<replacement-text-image>else<replacement-text-file>search-replace[$title$],<currentTiddler>addprefix<accumulator>]" variable="imageTitle">
<$action-sendmessage
$message="tm-edit-text-operation"
$param="insert-text"
text=<<imageTitle>>
/>
</$list>
<<closePopupActions>>
\end
\define buttons()
<$button class="tc-btn-invisible" actions=<<closePopupActions>> ><<lingo Listing/Cancel/Caption>></$button>
<$button class="tc-btn-invisible" message="tm-perform-import" param=<<importTitle>> actions=<<postImportActions>> ><<lingo Listing/Import/Caption>></$button>
\end
<$reveal type="popup" state=<<importState>> tag="div" class="tc-editor-importpopup">
<div class="tc-editor-import">
<$list filter="[<importTitle>field:plugin-type[import]]">
<h2><<lingo Editor/Import/Heading>></h2>
<$tiddler tiddler=<<importTitle>>>
{{||$:/core/ui/ImportListing}}
<<buttons>>
</$tiddler>
</$list>
</div>
</$reveal>

View File

@ -0,0 +1,4 @@
title: $:/config/Editor/EnableImportFilter
type: text/vnd.tiddlywiki
[all[current]type[text/vnd.tiddlywiki]] [all[current]!has[type]]

View File

@ -0,0 +1,4 @@
title: $:/config/Editor/ImportContentTypesFilter
type: text/vnd.tiddlywiki
[prefix[image/]]

View File

@ -7,9 +7,9 @@ $:/core/images/storyview-$(storyview)$
<div class="tc-chooser tc-viewswitcher">
<$list filter="[storyviews[]]" variable="storyview">
<$set name="cls" filter="[<storyview>prefix{$:/view}]" value="tc-chooser-item tc-chosen" emptyValue="tc-chooser-item"><div class=<<cls>>>
<$link to=<<storyview>>><$transclude tiddler=<<icon>>/><$text text=<<storyview>>/></$link>
<$button tag="a" class="tc-tiddlylink tc-btn-invisible" to=<<storyview>>><$transclude tiddler=<<icon>>/><$text text=<<storyview>>/></$button>
</div>
</$set>
</$list>
</div>
</$linkcatcher>
</$linkcatcher>

View File

@ -11,16 +11,140 @@ type: text/vnd.tiddlywiki
//[[See GitHub for detailed change history of this release|https://github.com/Jermolene/TiddlyWiki5/compare/v5.1.23...master]]//
<div class="doc-plain-list">
! Performance Improvements
! Improvements
* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/5380">> (and again [[here|https://github.com/Jermolene/TiddlyWiki5/pull/5488]]) the efficiency of the linked list implementation
* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/5362">> [[all Operator]] to use new linked list implementation
* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/5369">> [[links Operator]] to use new linked list implementation
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5383">> unneeded escaping of double quotes in tiddler DIVs inside single file wikis (saving about 10% from the size of empty.html)
*
! Usability Improvements
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/a360adbba924d222c5b55709133c18890c04398d">> dropzone size when story river is empty
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5326">> fill colour for "remove tag" button
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5370">> page title so that the separating em-dash is only used if the site subtitle is present
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5397">> broken aria-label in $:/PaletteManager
* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/5451">> macro calls to use the same parser as that used for widget attributes
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/commit/89546b3357b0696a7047e6915bd6cd137b589de6">> a hidden setting to control sandboxing of tiddlers of type `text/html`
* <<.link-badge-updated "https://github.com/Jermolene/TiddlyWiki5/commit/caec6bc3fea9155eb2b0aae64d577c565dd7b088">> SVG optimiser script
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/commit/c325380231a8c592a6e51d4498c1e6c3a241b539">> plus/minus SVG icons: <<.icon $:/core/images/plus-button>> and <<.icon $:/core/images/minus-button>>
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/5294">> support for [[dynamic toolbar buttons|How to create dynamic editor toolbar buttons]]
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/5612">> [[average Operator]], [[median Operator]], [[variance Operator]] and [[standard-deviation Operator]] for calculating the arithmetic mean of a list of numbers
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/commit/cf56a17f28f1e44dcb62c5e161be4ac29e27c3f2">> unusedtitle macro to use the prefix parameter
! Hackability Improvements
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/commit/9eda02868f21e9dd1733ffe26352bd7ac96285b4">> new MessageCatcherWidget
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/commit/d25e540dd2f0decf61c52fdc665a28a5dfeda93f">> support for `image/vnd.microsoft.icon` content type
! Widget Improvements
* <<.link-badge-modified "https://github.com/Jermolene/TiddlyWiki5/commit/b9647b2c48152dac069a1099a0822b32375a66cf">> [[FieldManglerWidget]] to ensure it doesn't propogate events that it traps
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/5597">> [[DropzoneWidget]] to optionally invoke actions after the `tm-import-tiddlers` message has been sent, and to specify an optional `contentTypesFilter` which determines which content types are accepted by the dropzone.
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/5611">> [[ListWidget]] with `index` attribute and here (and [[here| https://github.com/Jermolene/TiddlyWiki5/commit/4a99e0cc7d4a6b9e7071c0b2a9a0f63c3c7d2492]])
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5635">> [[SelectWidget]] refreshing
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/5252">> support for suffixes to filter run prefixes
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/5653">> :sort filter run prefix
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5644">> [[ActionListopsWidget]] bug by avoiding stitching together filter expressions for the original list values
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/commit/07caa16e8714afe9a64eb202375e4a2f95da1508">> [[DropzoneWidget]] to also use the specified deserializer for strings either dropped or pasted on to the dropzone
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/44df6fe52f79bee88357afb4fc3d6f4800aa6dde">> issue with widget not being available to filter operator
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/commit/3f986861538a3cc5c3c6da578b45d0d9138a6b2b">> [[ActionPopupWidget]] to create floating popups that must be manually cleared
! Client-server Improvements
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/e96a54c7531a2d9e07745e27d2015d8d7d09588f">> crash running in client server configuration when 'etag' header is missing
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5423">> blank favicon when using lazily loaded images
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/issues/4461">> web server issue with custom path prefix and basic authentication
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/issues/5366">> crash on Node.js with ActionSetFieldWidget when type field is given a value upon new tiddler creation
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5329">> issue with tiddler titles that already end in the required extension
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5465">> several consistency issues with the filesystem plugin
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/issues/5483">> issue with encoding of $:/config/OriginalTiddlerPaths outside the wiki folder
* <<.link-badge-updated "https://github.com/Jermolene/TiddlyWiki5/pull/5628">> the TiddlySpot Saver settings form
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/5638">> 401 and 403 error messages for PUT saver
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/d8ac00a10856b1b64311b8e0496344d5b0c1b987">> fixed crash if browser doesn't support Server Sent Events
! Plugin Improvements
!! [[XLSX Utilities Plugin]]
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/issues/5400">> crash with the XLSX Utils plugin
!! [[KaTeX Plugin]]
* <<.link-badge-updated "https://github.com/Jermolene/TiddlyWiki5/commit/f2aba29d94cddcff6d7c188c4aa0b668995d8002">> to KaTeX v0.12.0
!! [[Freelinks Plugin]]
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/commit/0ed32fded996826a59832d7a7555bb16c4a57864">> the Freelinks plugin with a filter to determine which tiddlers can be the targets of freelinks
!! [[Menubar Plugin]]
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/discussions/5533">> Menu plugin to support optional ''dropdown-position'' field
!! [[BibTeX Plugin]]
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/issues/5581">> BibTeX Plugin to report errors more sensibly
* <<.link-badge-modified "https://github.com/Jermolene/TiddlyWiki5/commit/953fb9f237ad78e409c03d4b29b9854d8abf6cdf">> BibTex Plugin to force fieldnames to be lowercase
! Developer Experience Improvements
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/5279">> support for [[server sent events|https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events]]
* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/5382">> the widget subclassing mechanism to work with widgets that add event listeners in their constructor
* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/5377">> the Jasmine test suite output
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/commit/9f9ce6595b08032a602981f82940ca113cff8211">> wikitext parser with a subclassing mechanism
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/commit/ef76349c37662e9706acfffc2c2edb51a920183d">> added support for ''utils-browser'' modules
! Translation improvements
* <<.link-badge-improved>> Chinese translations
! Other Bug Fixes
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5376">> issue with [[lookup Operator]] returning "undefined" under some circumstances
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/issues/5396">> crash with unterminated wikitext comments
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5453">> tiddler info area content bleeding on close animation
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5452">> inline/block widget parsing glitch
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5401">> runaway regexp when parsing filters
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5440">> right margin of tag pill when used outside of the tags wrapper
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/5455">> upload saver to optionally work without a username or password
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/5e4430dbf9ff66d9a18fbdf3005abcd716efc07d">> RadioWidget to refresh selectively, and to use the ''checked'' attribute correctly
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5542">> "invert" option of `wiki.search()` method
* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/commit/e157d16b724172f752da0ff714847e0c0ca9664d">> ''data-tag-title'' attribute to tag pills
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/7b1a0c6e6a8bd2d3badf8766af0cd3f5f7ac5ec8">> ES5 compatibility issue
* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/commit/a38dc1730010c6a2b6a011aff4818bbc67c04055">> RenderCommand to allow multiple variables to be passed
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/226df2ad7d2978d3d7400d94767a0503e495cf98">> exporting of tiddlers that begin and end with double quotes
* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/commit/d56e8764a1f02a214df5da1cc95191be2da2491b">> accessibility of button widget when controlling a popup
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/d6ea369f5ef9d3092a360a4286a99902df37782b">> EditTextWidget to use default text for missing fields
* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/5552">> css-escape-polyfill to work under Node.js
! Contributors
[[@Jermolene|https://github.com/Jermolene]] would like to thank the contributors to this release who have generously given their time to help improve TiddlyWiki:
* <<contributor Jermolene>>
</div>
* <<contributor Arlen22
* <<contributor BlueGreenMagick>>
* <<contributor BramChen>>
* <<contributor BurningTreeC>>
* <<contributor cdruan>>
* <<contributor clutterstack>>
* <<contributor CodaCodr>>
* <<contributor dixonge>>
* <<contributor donmor>>
* <<contributor FlashSystems>>
* <<contributor flibbles>>
* <<contributor hoelzro>>
* <<contributor jeremyredhead>>
* <<contributor joebordes>>
* <<contributor joshuafontany>>
* <<contributor kookma>>
* <<contributor laomaiweng>>
* <<contributor leehawk787>>
* <<contributor morosanuae>>
* <<contributor neumark>>
* <<contributor NicolasPetton>>
* <<contributor OdinJorna>>
* <<contributor pmario>>
* <<contributor saqimtias>>
* <<contributor simonbaird>>
* <<contributor slaymaker1907>>
* <<contributor twMat>>

View File

@ -750,6 +750,7 @@ function runTests(wiki) {
rootWidget.setVariable("sort1","[length[]]");
rootWidget.setVariable("sort2","[get[text]else[]length[]]");
rootWidget.setVariable("sort3","[{!!value}divide{!!cost}]");
rootWidget.setVariable("sort4","[{!!title}]");
expect(wiki.filterTiddlers("[sortsub:number<sort1>]",anchorWidget).join(",")).toBe("one,hasList,TiddlerOne,has filter,$:/TiddlerTwo,Tiddler Three,$:/ShadowPlugin,a fourth tiddler,filter regexp test");
expect(wiki.filterTiddlers("[!sortsub:number<sort1>]",anchorWidget).join(",")).toBe("filter regexp test,a fourth tiddler,$:/ShadowPlugin,$:/TiddlerTwo,Tiddler Three,TiddlerOne,has filter,hasList,one");
expect(wiki.filterTiddlers("[sortsub:string<sort1>]",anchorWidget).join(",")).toBe("TiddlerOne,has filter,$:/TiddlerTwo,Tiddler Three,$:/ShadowPlugin,a fourth tiddler,filter regexp test,one,hasList");
@ -759,6 +760,7 @@ function runTests(wiki) {
expect(wiki.filterTiddlers("[sortsub:string<sort2>]",anchorWidget).join(",")).toBe("one,TiddlerOne,hasList,has filter,$:/ShadowPlugin,a fourth tiddler,Tiddler Three,$:/TiddlerTwo,filter regexp test");
expect(wiki.filterTiddlers("[!sortsub:string<sort2>]",anchorWidget).join(",")).toBe("filter regexp test,$:/TiddlerTwo,Tiddler Three,a fourth tiddler,$:/ShadowPlugin,has filter,hasList,TiddlerOne,one");
expect(wiki.filterTiddlers("[[TiddlerOne]] [[$:/TiddlerTwo]] [[Tiddler Three]] [[a fourth tiddler]] +[!sortsub:number<sort3>]",anchorWidget).join(",")).toBe("$:/TiddlerTwo,Tiddler Three,TiddlerOne,a fourth tiddler");
expect(wiki.filterTiddlers("a1 a10 a2 a3 b10 b3 b1 c9 c11 c1 +[sortsub:alphanumeric<sort4>]",anchorWidget).join(",")).toBe("a1,a2,a3,a10,b1,b3,b10,c1,c9,c11");
});
it("should handle the toggle operator", function() {
@ -796,6 +798,21 @@ function runTests(wiki) {
expect(wiki.filterTiddlers("[[12]pad[9],[abc]]").join(",")).toBe("abcabca12");
expect(wiki.filterTiddlers("[[12]pad:suffix[9],[abc]]").join(",")).toBe("12abcabca");
});
it("should handle the escapecss operator", function() {
expect(wiki.filterTiddlers("[[Hello There]escapecss[]]").join(",")).toBe("Hello\\ There");
expect(wiki.filterTiddlers('\'"Reveal.js" by Devin Weaver[1]\' +[escapecss[]]').join(",")).toBe('\\"Reveal\\.js\\"\\ by\\ Devin\\ Weaver\\[1\\]');
expect(wiki.filterTiddlers(".foo#bar (){} '--a' 0 \0 +[escapecss[]]").join(",")).toBe("\\.foo\\#bar,\\(\\)\\{\\},--a,\\30 ,\ufffd");
expect(wiki.filterTiddlers("'' +[escapecss[]]").join(",")).toBe("");
expect(wiki.filterTiddlers("1234 +[escapecss[]]").join(",")).toBe("\\31 234");
expect(wiki.filterTiddlers("'-25' +[escapecss[]]").join(",")).toBe("-\\32 5");
expect(wiki.filterTiddlers("'-' +[escapecss[]]").join(",")).toBe("\\-");
});
it("should handle the deserializers operator", function() {
expect(wiki.filterTiddlers("[deserializers[]]").join(",")).toBe("application/javascript,application/json,application/x-tiddler,application/x-tiddler-html-div,application/x-tiddlers,text/css,text/html,text/plain");
});
}
});

View File

@ -18,6 +18,199 @@ describe("general filter prefix tests", function() {
var results = wiki.filterTiddlers("[tag[A]] :nonexistent[tag[B]]");
expect(results).toEqual(["Filter Error: Unknown prefix for filter run"]);
});
// Test filter run prefix parsing
it("should parse filter run prefix suffixes", function() {
// two runs, one with a named prefix but no suffix
expect($tw.wiki.parseFilter("[[Sparkling water]tags[]] :intersection[[Red wine]tags[]]")).toEqual(
[
{
"prefix": "",
"operators": [
{
"operator": "title",
"operands": [
{
"text": "Sparkling water"
}
]
},
{
"operator": "tags",
"operands": [
{
"text": ""
}
]
}
]
},
{
"prefix": ":intersection",
"operators": [
{
"operator": "title",
"operands": [
{
"text": "Red wine"
}
]
},
{
"operator": "tags",
"operands": [
{
"text": ""
}
]
}
],
"namedPrefix": "intersection"
}
]
);
// named prefix with no suffix
expect($tw.wiki.parseFilter(":reduce[multiply<accumulator>]")).toEqual(
[
{
"prefix": ":reduce",
"operators": [
{
"operator": "multiply",
"operands": [
{
"variable": true,
"text": "accumulator"
}
]
}
],
"namedPrefix": "reduce"
}
]
);
//named prefix with one simple suffix
expect($tw.wiki.parseFilter(":reduce:1[multiply<accumulator>]")).toEqual(
[
{
"prefix": ":reduce:1",
"operators": [
{
"operator": "multiply",
"operands": [
{
"variable": true,
"text": "accumulator"
}
]
}
],
"namedPrefix": "reduce",
"suffixes": [
[
"1"
]
]
}
]
);
//named prefix with two simple suffixes
expect($tw.wiki.parseFilter(":reduce:1:hello[multiply<accumulator>]")).toEqual(
[
{
"prefix": ":reduce:1:hello",
"operators": [
{
"operator": "multiply",
"operands": [
{
"variable": true,
"text": "accumulator"
}
]
}
],
"namedPrefix": "reduce",
"suffixes": [
[
"1"
],
[
"hello",
]
]
}
]
);
//named prefix with two rich (comma separated) suffixes
expect($tw.wiki.parseFilter(":reduce:1,one:hello,there[multiply<accumulator>]")).toEqual(
[
{
"prefix": ":reduce:1,one:hello,there",
"operators": [
{
"operator": "multiply",
"operands": [
{
"variable": true,
"text": "accumulator"
}
]
}
],
"namedPrefix": "reduce",
"suffixes": [
[
"1",
"one"
],
[
"hello",
"there"
]
]
}
]
);
// suffixes with spaces
expect($tw.wiki.parseFilter(":reduce: 1, one:hello, there [multiply<accumulator>]")).toEqual(
[
{
"prefix": ":reduce: 1, one:hello, there ",
"operators": [
{
"operator": "multiply",
"operands": [
{
"variable": true,
"text": "accumulator"
}
]
}
],
"namedPrefix": "reduce",
"suffixes": [
[
"1",
"one"
],
[
"hello",
"there"
]
]
}
]
);
});
});
describe("'reduce' and 'intersection' filter prefix tests", function() {
@ -60,7 +253,7 @@ describe("'reduce' and 'intersection' filter prefix tests", function() {
wiki.addTiddler({
title: "Red wine",
tags: ["drinks", "wine", "textexample"],
text: "This is some more text"
text: "This is some more text!"
});
wiki.addTiddler({
title: "Cheesecake",
@ -72,6 +265,26 @@ describe("'reduce' and 'intersection' filter prefix tests", function() {
tags: ["cakes", "food", "textexample"],
text: "This is even more text"
});
wiki.addTiddler({
title: "Persian love cake",
tags: ["cakes"],
text: "An amazing cake worth the effort to make"
});
wiki.addTiddler({
title: "cheesecake",
tags: ["cakes"],
text: "Everyone likes cheescake"
});
wiki.addTiddler({
title: "chocolate cake",
tags: ["cakes"],
text: "lower case chocolate cake"
});
wiki.addTiddler({
title: "Pound cake",
tags: ["cakes","with tea"],
text: "Does anyone eat pound cake?"
});
it("should handle the :reduce filter prefix", function() {
expect(wiki.filterTiddlers("[tag[shopping]] :reduce[get[quantity]add<accumulator>]").join(",")).toBe("22");
@ -80,6 +293,10 @@ describe("'reduce' and 'intersection' filter prefix tests", function() {
// Empty input should become empty output
expect(wiki.filterTiddlers("[tag[non-existent]] :reduce[get[price]multiply{!!quantity}add<accumulator>]").length).toBe(0);
expect(wiki.filterTiddlers("[tag[non-existent]] :reduce[get[price]multiply{!!quantity}add<accumulator>] :else[[0]]").join(",")).toBe("0");
expect(wiki.filterTiddlers("[tag[non-existent]] :reduce:11,22[get[price]multiply{!!quantity}add<accumulator>] :else[[0]]").join(",")).toBe("0");
expect(wiki.filterTiddlers("[tag[non-existent]] :reduce:11[get[price]multiply{!!quantity}add<accumulator>] :else[[0]]").join(",")).toBe("0");
});
it("should handle the reduce operator", function() {
@ -101,6 +318,28 @@ describe("'reduce' and 'intersection' filter prefix tests", function() {
expect(wiki.filterTiddlers("[tag[non-existent]reduce<add-price>else[0]]",anchorWidget).join(",")).toBe("0");
});
it("should handle the average operator", function() {
expect(wiki.filterTiddlers("[tag[shopping]get[price]average[]]").join(",")).toBe("2.3575");
expect(parseFloat(wiki.filterTiddlers("[tag[food]get[price]average[]]").join(","))).toBeCloseTo(3.155);
});
it("should handle the median operator", function() {
expect(parseFloat(wiki.filterTiddlers("[tag[shopping]get[price]median[]]").join(","))).toBeCloseTo(1.99);
expect(parseFloat(wiki.filterTiddlers("[tag[food]get[price]median[]]").join(","))).toBeCloseTo(3.155);
});
it("should handle the variance operator", function() {
expect(parseFloat(wiki.filterTiddlers("[tag[shopping]get[price]variance[]]").join(","))).toBeCloseTo(2.92);
expect(parseFloat(wiki.filterTiddlers("[tag[food]get[price]variance[]]").join(","))).toBeCloseTo(3.367);
expect(wiki.filterTiddlers(" +[variance[]]").toString()).toBe("NaN");
});
it("should handle the standard-deviation operator", function() {
expect(parseFloat(wiki.filterTiddlers("[tag[shopping]get[price]standard-deviation[]]").join(","))).toBeCloseTo(1.71);
expect(parseFloat(wiki.filterTiddlers("[tag[food]get[price]standard-deviation[]]").join(","))).toBeCloseTo(1.835);
expect(wiki.filterTiddlers(" +[standard-deviation[]]").toString()).toBe("NaN");
});
it("should handle the :intersection prefix", function() {
expect(wiki.filterTiddlers("[[Sparkling water]tags[]] :intersection[[Red wine]tags[]]").join(",")).toBe("drinks,textexample");
expect(wiki.filterTiddlers("[[Brownies]tags[]] :intersection[[Chocolate Cake]tags[]]").join(",")).toBe("food");
@ -116,11 +355,25 @@ describe("'reduce' and 'intersection' filter prefix tests", function() {
rootWidget.makeChildWidgets();
var anchorWidget = rootWidget.children[0];
rootWidget.setVariable("larger-than-18","[get[text]length[]compare:integer:gteq[18]]");
rootWidget.setVariable("nr","18");
rootWidget.setVariable("larger-than-18-with-var","[get[text]length[]compare:integer:gteq<nr>]");
expect(wiki.filterTiddlers("[tag[textexample]] :filter[get[text]length[]compare:integer:gteq[18]]",anchorWidget).join(",")).toBe("Red wine,Cheesecake,Chocolate Cake");
expect(wiki.filterTiddlers("[tag[textexample]]",anchorWidget).join(",")).toBe("Sparkling water,Red wine,Cheesecake,Chocolate Cake");
expect(wiki.filterTiddlers("[tag[textexample]filter<larger-than-18>]",anchorWidget).join(",")).toBe("Red wine,Cheesecake,Chocolate Cake");
})
expect(wiki.filterTiddlers("[tag[textexample]filter<larger-than-18-with-var>]",anchorWidget).join(",")).toBe("Red wine,Cheesecake,Chocolate Cake");
});
it("should handle the :sort prefix", function() {
expect(wiki.filterTiddlers("a1 a10 a2 a3 b10 b3 b1 c9 c11 c1 :sort:alphanumeric[{!!title}]").join(",")).toBe("a1,a2,a3,a10,b1,b3,b10,c1,c9,c11");
expect(wiki.filterTiddlers("a1 a10 a2 a3 b10 b3 b1 c9 c11 c1 :sort:alphanumeric:reverse[{!!title}]").join(",")).toBe("c11,c9,c1,b10,b3,b1,a10,a3,a2,a1");
expect(wiki.filterTiddlers("[tag[shopping]] :sort:number:[get[price]]").join(",")).toBe("Milk,Chick Peas,Rice Pudding,Brownies");
expect(wiki.filterTiddlers("[tag[textexample]] :sort:number:[get[text]length[]]").join(",")).toBe("Sparkling water,Chocolate Cake,Red wine,Cheesecake");
expect(wiki.filterTiddlers("[tag[textexample]] :sort:number:reverse[get[text]length[]]").join(",")).toBe("Cheesecake,Red wine,Chocolate Cake,Sparkling water");
expect(wiki.filterTiddlers("[tag[notatag]] :sort:number[get[price]]").join(",")).toBe("");
expect(wiki.filterTiddlers("[tag[cakes]] :sort:string[{!!title}]").join(",")).toBe("Cheesecake,cheesecake,Chocolate Cake,chocolate cake,Persian love cake,Pound cake");
expect(wiki.filterTiddlers("[tag[cakes]] :sort:string:casesensitive[{!!title}]").join(",")).toBe("Cheesecake,Chocolate Cake,Persian love cake,Pound cake,cheesecake,chocolate cake");
expect(wiki.filterTiddlers("[tag[cakes]] :sort:string:casesensitive,reverse[{!!title}]").join(",")).toBe("chocolate cake,cheesecake,Pound cake,Persian love cake,Chocolate Cake,Cheesecake");
});
});
})();

View File

@ -350,6 +350,123 @@ describe("Widget module", function() {
expect(wrapper.children[0].children[4].sequenceNumber).toBe(5);
});
it("should deal with the list widget using a counter variable", function() {
var wiki = new $tw.Wiki();
// Add some tiddlers
wiki.addTiddlers([
{title: "TiddlerOne", text: "Jolly Old World"},
{title: "TiddlerTwo", text: "Worldly Old Jelly"},
{title: "TiddlerThree", text: "Golly Gosh"},
{title: "TiddlerFour", text: "Lemon Squash"}
]);
// Construct the widget node
var text = "<$list counter='index'><$view field='text'/><$text text=<<index>>/><$text text=<<index-first>>/><$text text=<<index-last>>/></$list>";
var widgetNode = createWidgetNode(parseText(text,wiki),wiki);
// Render the widget node to the DOM
var wrapper = renderWidgetNode(widgetNode);
// Test the rendering
expect(wrapper.innerHTML).toBe("<p>Lemon Squash1yesnoJolly Old World2nonoGolly Gosh3nonoWorldly Old Jelly4noyes</p>");
// Test the sequence numbers in the DOM
expect(wrapper.sequenceNumber).toBe(0);
expect(wrapper.children[0].sequenceNumber).toBe(1);
expect(wrapper.children[0].children[0].sequenceNumber).toBe(2);
expect(wrapper.children[0].children[1].sequenceNumber).toBe(3);
expect(wrapper.children[0].children[2].sequenceNumber).toBe(4);
expect(wrapper.children[0].children[3].sequenceNumber).toBe(5);
expect(wrapper.children[0].children[4].sequenceNumber).toBe(6);
expect(wrapper.children[0].children[5].sequenceNumber).toBe(7);
expect(wrapper.children[0].children[6].sequenceNumber).toBe(8);
expect(wrapper.children[0].children[7].sequenceNumber).toBe(9);
expect(wrapper.children[0].children[8].sequenceNumber).toBe(10);
expect(wrapper.children[0].children[9].sequenceNumber).toBe(11);
expect(wrapper.children[0].children[10].sequenceNumber).toBe(12);
expect(wrapper.children[0].children[11].sequenceNumber).toBe(13);
expect(wrapper.children[0].children[12].sequenceNumber).toBe(14);
expect(wrapper.children[0].children[13].sequenceNumber).toBe(15);
expect(wrapper.children[0].children[14].sequenceNumber).toBe(16);
expect(wrapper.children[0].children[15].sequenceNumber).toBe(17);
// Add another tiddler
wiki.addTiddler({title: "TiddlerFive", text: "Jalapeno Peppers"});
// Refresh
refreshWidgetNode(widgetNode,wrapper,["TiddlerFive"]);
// Test the refreshing
expect(wrapper.innerHTML).toBe("<p>Jalapeno Peppers1yesnoLemon Squash2nonoJolly Old World3nonoGolly Gosh4nonoWorldly Old Jelly5noyes</p>");
// Test the sequence numbers in the DOM
expect(wrapper.sequenceNumber).toBe(0);
expect(wrapper.children[0].sequenceNumber).toBe(1);
expect(wrapper.children[0].children[0].sequenceNumber).toBe(18);
expect(wrapper.children[0].children[1].sequenceNumber).toBe(19);
expect(wrapper.children[0].children[2].sequenceNumber).toBe(20);
expect(wrapper.children[0].children[3].sequenceNumber).toBe(21);
expect(wrapper.children[0].children[4].sequenceNumber).toBe(22);
expect(wrapper.children[0].children[5].sequenceNumber).toBe(23);
expect(wrapper.children[0].children[6].sequenceNumber).toBe(24);
expect(wrapper.children[0].children[7].sequenceNumber).toBe(25);
expect(wrapper.children[0].children[8].sequenceNumber).toBe(26);
expect(wrapper.children[0].children[9].sequenceNumber).toBe(27);
expect(wrapper.children[0].children[10].sequenceNumber).toBe(28);
expect(wrapper.children[0].children[11].sequenceNumber).toBe(29);
expect(wrapper.children[0].children[12].sequenceNumber).toBe(30);
expect(wrapper.children[0].children[13].sequenceNumber).toBe(31);
expect(wrapper.children[0].children[14].sequenceNumber).toBe(32);
expect(wrapper.children[0].children[15].sequenceNumber).toBe(33);
expect(wrapper.children[0].children[16].sequenceNumber).toBe(34);
expect(wrapper.children[0].children[17].sequenceNumber).toBe(35);
expect(wrapper.children[0].children[18].sequenceNumber).toBe(36);
expect(wrapper.children[0].children[19].sequenceNumber).toBe(37);
// Remove a tiddler
wiki.deleteTiddler("TiddlerThree");
// Refresh
refreshWidgetNode(widgetNode,wrapper,["TiddlerThree"]);
// Test the refreshing
expect(wrapper.innerHTML).toBe("<p>Jalapeno Peppers1yesnoLemon Squash2nonoJolly Old World3nonoWorldly Old Jelly4noyes</p>");
// Test the sequence numbers in the DOM
expect(wrapper.sequenceNumber).toBe(0);
expect(wrapper.children[0].sequenceNumber).toBe(1);
expect(wrapper.children[0].children[0].sequenceNumber).toBe(18);
expect(wrapper.children[0].children[1].sequenceNumber).toBe(19);
expect(wrapper.children[0].children[2].sequenceNumber).toBe(20);
expect(wrapper.children[0].children[3].sequenceNumber).toBe(21);
expect(wrapper.children[0].children[4].sequenceNumber).toBe(22);
expect(wrapper.children[0].children[5].sequenceNumber).toBe(23);
expect(wrapper.children[0].children[6].sequenceNumber).toBe(24);
expect(wrapper.children[0].children[7].sequenceNumber).toBe(25);
expect(wrapper.children[0].children[8].sequenceNumber).toBe(26);
expect(wrapper.children[0].children[9].sequenceNumber).toBe(27);
expect(wrapper.children[0].children[10].sequenceNumber).toBe(28);
expect(wrapper.children[0].children[11].sequenceNumber).toBe(29);
expect(wrapper.children[0].children[12].sequenceNumber).toBe(38);
expect(wrapper.children[0].children[13].sequenceNumber).toBe(39);
expect(wrapper.children[0].children[14].sequenceNumber).toBe(40);
expect(wrapper.children[0].children[15].sequenceNumber).toBe(41);
// Add it back a tiddler
wiki.addTiddler({title: "TiddlerThree", text: "Something"});
// Refresh
refreshWidgetNode(widgetNode,wrapper,["TiddlerThree"]);
// Test the refreshing
expect(wrapper.innerHTML).toBe("<p>Jalapeno Peppers1yesnoLemon Squash2nonoJolly Old World3nonoSomething4nonoWorldly Old Jelly5noyes</p>");
// Test the sequence numbers in the DOM
expect(wrapper.sequenceNumber).toBe(0);
expect(wrapper.children[0].sequenceNumber).toBe(1);
expect(wrapper.children[0].children[0].sequenceNumber).toBe(18);
expect(wrapper.children[0].children[1].sequenceNumber).toBe(19);
expect(wrapper.children[0].children[2].sequenceNumber).toBe(20);
expect(wrapper.children[0].children[3].sequenceNumber).toBe(21);
expect(wrapper.children[0].children[4].sequenceNumber).toBe(22);
expect(wrapper.children[0].children[5].sequenceNumber).toBe(23);
expect(wrapper.children[0].children[6].sequenceNumber).toBe(24);
expect(wrapper.children[0].children[7].sequenceNumber).toBe(25);
expect(wrapper.children[0].children[8].sequenceNumber).toBe(26);
expect(wrapper.children[0].children[9].sequenceNumber).toBe(27);
expect(wrapper.children[0].children[10].sequenceNumber).toBe(28);
expect(wrapper.children[0].children[11].sequenceNumber).toBe(29);
expect(wrapper.children[0].children[12].sequenceNumber).toBe(42);
expect(wrapper.children[0].children[13].sequenceNumber).toBe(43);
expect(wrapper.children[0].children[14].sequenceNumber).toBe(44);
expect(wrapper.children[0].children[15].sequenceNumber).toBe(45);
});
it("should deal with the list widget followed by other widgets", function() {
var wiki = new $tw.Wiki();
// Add some tiddlers
@ -585,6 +702,19 @@ describe("Widget module", function() {
expect(wrapper.innerHTML).toBe("<p>Don't forget me.</p>");
});
/** Special case. \import should parse correctly, even if it's
* the only line in the tiddler. Technically doesn't cause a
* visual difference, but may affect plugins if it doesn't.
*/
it("should work when import pragma is standalone", function() {
var wiki = new $tw.Wiki();
var text = "\\import [prefix[XXX]]";
var parseTreeNode = parseText(text,wiki);
// Test the resulting parse tree node, since there is no
// rendering which may expose a problem.
expect(parseTreeNode.children[0].attributes.filter.value).toBe('[prefix[XXX]]');
});
/** This test reproduces issue #4504.
*
* The importvariable widget was creating redundant copies into

View File

@ -6,7 +6,7 @@ type: text/vnd.tiddlywiki
//These are personal reflections on the history and development of TiddlyWiki from JeremyRuston.//
//There is also a [[podcast|https://changelog.com/196/]] discussing TiddlyWiki's backstory.//
//There is also a [[podcast from 2016|https://changelog.com/podcast/196]] discussing TiddlyWiki's backstory as well as a [[recording from 2021|https://twit.tv/shows/floss-weekly/episodes/620]].//
! Origins of TiddlyWiki

View File

@ -1,17 +1,59 @@
created: 20131101111400000
modified: 20190115165616599
modified: 20210402095728684
tags: Community
title: Contributing
type: text/vnd.tiddlywiki
We welcome contributions to the code and documentation of TiddlyWiki in several ways:
Here we focus on contributions via GitHub Pull Requests but there are many other ways that anyone can help the TiddlyWiki project, such as [[reporting bugs|ReportingBugs]] or helping to [[improve our documentation|Improving TiddlyWiki Documentation]].
* ReportingBugs
* Helping to [[improve our documentation|Improving TiddlyWiki Documentation]]
* Contributing to the code via [[GitHub|https://github.com/Jermolene/TiddlyWiki5]]
** See https://tiddlywiki.com/dev for more details
! Rules for Pull Requests
There are other ways to [[help TiddlyWiki|HelpingTiddlyWiki]] too.
PRs must meet these minimum requirements before they can be considered for merging:
* The material in the PR must be free of licensing restrictions. Which means that either:
** The author must hold the copyright in all of the material themselves
** The material must be licensed under a license compatible with TiddlyWiki's BSD license
* The author must sign the Contributors License Agreement (see below)
* Each PR should only make a single feature change
* The title of the PR should be 50 characters or less
* The title of the PR should be capitalised, and should not end with a period
* The title of the PR should be written in the imperative mood. See below
* Adequate explanation in the body of the PR for the motivation and implementation of the change. Focus on the //why// and //what//, rather than the //how//
* PRs must be self-contained. Although they can link to material elsewhere, everything needed to understand the intention of the PR should be included
* Documentation as appropriate for end-users or developers
* Observe the coding style
* Read the developers documentation
* Please open a consultation issue prior to investing time in making a large PR
!! Imperative Mood for PR Titles
The "imperative mood" means written as if giving a command or instruction. See [[this post|https://chris.beams.io/posts/git-commit/#imperative]] for more details, but the gist is that the title of the PR should make sense when used to complete the sentence "If applied, this commit will...". So for example, these are good PR titles:
* If applied, this commit will //update the contributing guidelines//
* If applied, this commit will //change css-escape-polyfill to a $tw.utils method//
* If applied, this commit will //make it easier to subclass the wikitext parser with a custom rule set//
These a poorly worded PR titles:
* ~~If applied, this commit will //edit text widgets should use default text for missing fields//~~
* ~~If applied, this commit will //signing the CLA//~~
* ~~If applied, this commit will //don't crash if options.event is missing//~~
PR titles may also include a short prefix to indicate the subsystem to which they apply. For example:
* //Menu plugin: Include menu text in aerial rotator//
! Commenting on Pull Requests
One of the principles of open source is that many pairs of eyes on the code can improve quality. So, we welcome comments and critiques of pending PRs. [[Conventional Comments|https://conventionalcomments.org]] has some techcniques to help make comments as constructive and actionable as possible. Notably, they recommend prefixing a comment with a label to clarify the intention:
|praise |Praises highlight something positive. Try to leave at least one of these comments per review. Do not leave false praise (which can actually be damaging). Do look for something to sincerely praise |
|nitpick |Nitpicks are small, trivial, but necessary changes. Distinguishing nitpick comments significantly helps direct the reader's attention to comments requiring more involvement |
|suggestion |Suggestions are specific requests to improve the subject under review. It is assumed that we all want to do what's best, so these comments are never dismissed as “mere suggestions”, but are taken seriously |
|issue |Issues represent user-facing problems. If possible, it's great to follow this kind of comment with a suggestion |
|question |Questions are appropriate if you have a potential concern but are not quite sure if it's relevant or not. Asking the author for clarification or investigation can lead to a quick resolution |
|thought |Thoughts represent an idea that popped up from reviewing. These comments are non-blocking by nature, but they are extremely valuable and can lead to more focused initiatives and mentoring opportunities |
|chore |Chores are simple tasks that must be done before the subject can be “officially” accepted. Usually, these comments reference some common process. Try to leave a link to the process description so that the reader knows how to resolve the chore |
! Contributor License Agreement
@ -24,9 +66,3 @@ There are other ways to [[help TiddlyWiki|HelpingTiddlyWiki]] too.
---
//The CLA documents used for this project were created using [[Harmony Project Templates|http://www.harmonyagreements.org]]. "HA-CLA-I-LIST Version 1.0" for "CLA-individual" and "HA-CLA-E-LIST Version 1.0" for "CLA-entity".//
!! Remarks
''If you do not own the copyright in the entire work of authorship'':
In this case, please clearly state so and provide links and any additional information that clarify under which license the rest of the code is distributed.

View File

@ -1,14 +0,0 @@
created: 20140315085406905
modified: 20210106151027120
tags: Tutorials
title: "Hosting TiddlyWiki5 on GoogleDrive" by Tony Ching
type: text/vnd.tiddlywiki
url: https://googledrive.com/host/0B51gSXixfJ2Qb0I4R2M4MWJVMlU
Tony Ching's quick guide for sharing TiddlyWiki with Google Drive.
{{!!url}}
<<<
Anyway your self-contained a non-linear personal web notebook can be hosted on Google Drive, a free cloud service from Google.com. Because TiddlyWiki5 now supports the Stanford Javascript Crypto Library (SJCL), you can encrypt your content from prying eyes (excluding the NSA of course)
<<<

View File

@ -1,5 +1,5 @@
created: 20140502213500000
modified: 20160622111355787
modified: 20210406131243532
tags: Features Concepts
title: PermaLinks
type: text/vnd.tiddlywiki
@ -32,7 +32,7 @@ If the target tiddler isn't present in the story list then it is automatically i
It is also possible to specify a story filter without specifying a target tiddler for navigation:
<a href="https://tiddlywiki.com/#:[tags[task]]" rel="noopener noreferrer">~https://tiddlywiki.com/#:[tags[task]]</a>
<a href="https://tiddlywiki.com/#:[tag[task]]" rel="noopener noreferrer">~https://tiddlywiki.com/#:[tag[task]]</a>
! About URL encoding

View File

@ -1,6 +1,6 @@
created: 20130822080600000
list: [[SystemTag: $:/tags/AboveStory]] [[SystemTag: $:/tags/AdvancedSearch]] [[SystemTag: $:/tags/AdvancedSearch/FilterButton]] [[SystemTag: $:/tags/Alert]] [[SystemTag: $:/tags/BelowStory]] [[SystemTag: $:/tags/ControlPanel]] [[SystemTag: $:/tags/ControlPanel/Advanced]] [[SystemTag: $:/tags/ControlPanel/Appearance]] [[SystemTag: $:/tags/ControlPanel/Info]] [[SystemTag: $:/tags/ControlPanel/Saving]] [[SystemTag: $:/tags/ControlPanel/Settings]] [[SystemTag: $:/tags/ControlPanel/Toolbars]] [[SystemTag: $:/tags/EditorToolbar]] [[SystemTag: $:/tags/EditPreview]] [[SystemTag: $:/tags/EditTemplate]] [[SystemTag: $:/tags/EditToolbar]] [[SystemTag: $:/tags/Exporter]] [[SystemTag: $:/tags/Filter]] [[SystemTag: $:/tags/Image]] [[SystemTag: $:/tags/ImportPreview]] [[SystemTag: $:/tags/KeyboardShortcut]] [[SystemTag: $:/tags/Layout]] [[SystemTag: $:/tags/Macro]] [[SystemTag: $:/tags/Macro/View]] [[SystemTag: $:/tags/Manager/ItemMain]] [[SystemTag: $:/tags/Manager/ItemSidebar]] [[SystemTag: $:/tags/MoreSideBar]] [[SystemTag: $:/tags/MoreSideBar/Plugins]] [[SystemTag: $:/tags/PageControls]] [[SystemTag: $:/tags/PageTemplate]] [[SystemTag: $:/tags/Palette]] [[SystemTag: $:/tags/PluginLibrary]] [[SystemTag: $:/tags/RawMarkup]] [[SystemTag: $:/tags/RawMarkupWikified]] [[SystemTag: $:/tags/RawMarkupWikified/BottomBody]] [[SystemTag: $:/tags/RawMarkupWikified/TopBody]] [[SystemTag: $:/tags/RawMarkupWikified/TopHead]] [[SystemTag: $:/tags/RawStaticContent]] [[SystemTag: $:/tags/RemoteAssetInfo]] [[SystemTag: $:/tags/SearchResults]] [[SystemTag: $:/tags/ServerConnection]] [[SystemTag: $:/tags/SideBar]] [[SystemTag: $:/tags/SideBarSegment]] [[SystemTag: $:/tags/StartupAction]] [[SystemTag: $:/tags/StartupAction/Browser]] [[SystemTag: $:/tags/StartupAction/Node]] [[SystemTag: $:/tags/Stylesheet]] [[SystemTag: $:/tags/TagDropdown]] [[SystemTag: $:/tags/TextEditor/Snippet]] [[SystemTag: $:/tags/TiddlerInfo]] [[SystemTag: $:/tags/TiddlerInfo/Advanced]] [[SystemTag: $:/tags/TiddlerInfoSegment]] [[SystemTag: $:/tags/ToolbarButtonStyle]] [[SystemTag: $:/tags/TopLeftBar]] [[SystemTag: $:/tags/TopRightBar]] [[SystemTag: $:/tags/ViewTemplate]] [[SystemTag: $:/tags/ViewToolbar]]
modified: 20201123192434277
list: [[SystemTag: $:/tags/AboveStory]] [[SystemTag: $:/tags/AdvancedSearch]] [[SystemTag: $:/tags/AdvancedSearch/FilterButton]] [[SystemTag: $:/tags/Alert]] [[SystemTag: $:/tags/BelowStory]] [[SystemTag: $:/tags/ControlPanel]] [[SystemTag: $:/tags/ControlPanel/Advanced]] [[SystemTag: $:/tags/ControlPanel/Appearance]] [[SystemTag: $:/tags/ControlPanel/Info]] [[SystemTag: $:/tags/ControlPanel/Saving]] [[SystemTag: $:/tags/ControlPanel/Settings]] [[SystemTag: $:/tags/ControlPanel/Toolbars]] [[SystemTag: $:/tags/EditorTools]] [[SystemTag: $:/tags/EditorToolbar]] [[SystemTag: $:/tags/EditPreview]] [[SystemTag: $:/tags/EditTemplate]] [[SystemTag: $:/tags/EditToolbar]] [[SystemTag: $:/tags/Exporter]] [[SystemTag: $:/tags/Filter]] [[SystemTag: $:/tags/Image]] [[SystemTag: $:/tags/ImportPreview]] [[SystemTag: $:/tags/KeyboardShortcut]] [[SystemTag: $:/tags/Layout]] [[SystemTag: $:/tags/Macro]] [[SystemTag: $:/tags/Macro/View]] [[SystemTag: $:/tags/Manager/ItemMain]] [[SystemTag: $:/tags/Manager/ItemSidebar]] [[SystemTag: $:/tags/MoreSideBar]] [[SystemTag: $:/tags/MoreSideBar/Plugins]] [[SystemTag: $:/tags/PageControls]] [[SystemTag: $:/tags/PageTemplate]] [[SystemTag: $:/tags/Palette]] [[SystemTag: $:/tags/PluginLibrary]] [[SystemTag: $:/tags/RawMarkup]] [[SystemTag: $:/tags/RawMarkupWikified]] [[SystemTag: $:/tags/RawMarkupWikified/BottomBody]] [[SystemTag: $:/tags/RawMarkupWikified/TopBody]] [[SystemTag: $:/tags/RawMarkupWikified/TopHead]] [[SystemTag: $:/tags/RawStaticContent]] [[SystemTag: $:/tags/RemoteAssetInfo]] [[SystemTag: $:/tags/SearchResults]] [[SystemTag: $:/tags/ServerConnection]] [[SystemTag: $:/tags/SideBar]] [[SystemTag: $:/tags/SideBarSegment]] [[SystemTag: $:/tags/StartupAction]] [[SystemTag: $:/tags/StartupAction/Browser]] [[SystemTag: $:/tags/StartupAction/Node]] [[SystemTag: $:/tags/Stylesheet]] [[SystemTag: $:/tags/TagDropdown]] [[SystemTag: $:/tags/TextEditor/Snippet]] [[SystemTag: $:/tags/TiddlerInfo]] [[SystemTag: $:/tags/TiddlerInfo/Advanced]] [[SystemTag: $:/tags/TiddlerInfoSegment]] [[SystemTag: $:/tags/ToolbarButtonStyle]] [[SystemTag: $:/tags/TopLeftBar]] [[SystemTag: $:/tags/TopRightBar]] [[SystemTag: $:/tags/ViewTemplate]] [[SystemTag: $:/tags/ViewToolbar]]
modified: 20210519160636964
tags: Reference Concepts
title: SystemTags
type: text/vnd.tiddlywiki

View File

@ -1,5 +1,5 @@
created: 20190206140446821
modified: 20190611155838557
modified: 20210417090408263
tags: Filters
title: Mathematics Operators
type: text/vnd.tiddlywiki
@ -26,6 +26,7 @@ The mathematics operators take three different forms:
* ''Reducing operators'' apply an operation to all of the numbers in the input list, returning a single result (e.g. sum, product)
** <<.inline-operator-example "=1 =2 =3 =4 +[sum[]]">>
** <<.inline-operator-example "=1 =2 =3 =4 +[product[]]">>
** <<.inline-operator-example "=1 =2 =3 =4 +[average[]]">>
Operators can be combined:

View File

@ -0,0 +1,13 @@
caption: average
created: 20210417090137714
modified: 20210426131553482
op-input: a [[selection of titles|Title Selection]]
op-output: the arithmetic mean of the input as numbers
op-purpose: treating each input title as a number, compute their arithmetic mean
tags: [[Reducing Mathematics Operators]] [[Filter Operators]] [[Mathematics Operators]]
title: average Operator
type: text/vnd.tiddlywiki
<<.from-version "5.1.24">> See [[Mathematics Operators]] for an overview.
<<.operator-examples "average">>

View File

@ -0,0 +1,14 @@
caption: deserializers
created: 20210506115203172
from-version: 5.1.24
modified: 20210506130322593
op-input: ignored
op-output: the title of each available deserializer
op-parameter: none
tags: [[Filter Operators]] [[Special Operators]]
title: deserializers Operator
type: text/vnd.tiddlywiki
<<.tip "You can specify a specific deserializer for a DropzoneWidget to use">>
<<.operator-examples "deserializers">>

View File

@ -0,0 +1,10 @@
created: 20210426130837644
modified: 20210426131553546
tags: [[Operator Examples]] [[average Operator]]
title: average Operator (Examples)
type: text/vnd.tiddlywiki
<<.operator-example 1 "=1 =3 =4 =5 +[average[]]">>
Note that if there is no input the operator returns `NaN`
<<.operator-example 2 "[tag[NotATiddler]get[price]] +[average[]]">>

View File

@ -0,0 +1,7 @@
created: 20210506115345021
modified: 20210506115402052
tags: [[Operator Examples]] [[deserializer Operator]]
title: deserializers Operator (Examples)
type: text/vnd.tiddlywiki
<<.operator-example 1 "[deserializers[]]">>

View File

@ -1,7 +1,10 @@
created: 20170907144257037
modified: 20170907144559822
modified: 20201224034837935
title: lookup Operator (Examples)
type: text/vnd.tiddlywiki
<<.operator-example 1 "[all[shadows+tiddlers]tag[$:/tags/PageControls]lookup[$:/config/PageControlButtons/Visibility/]]" "Retrieve the visibility status of each page control button">>
<<.operator-example 2 "[all[shadows+tiddlers]tag[$:/tags/PageControls]lookup:show[$:/config/PageControlButtons/Visibility/]]" "Retrieve the visibility status of each page control button, this time with a default value">>
<<.operator-example 3 "[all[tiddlers]has[plugin-type]removeprefix[$:/plugins/tiddlywiki/]lookup:missing-description:field[$:/plugins/tiddlywiki/],[description]]" "Retrieve the description of all plugin-tiddlers that are in the `$:/plugins/tiddlywiki/` namespace.">>
<<.operator-example 4 "OriginalTiddlerPaths +[lookup:missing-index:index[$:/config/],[HelloThere]]" "Lookup the original tiddler path on disk for the [[Hello There]] tiddler.">>
<<.operator-example 5 "OriginalTiddlerPaths +[lookup:missing-index:index[$:/config/],[MissingTiddler]]" "Lookup the original tiddler path on disk for the [[MissingTiddler]] tiddler.">>

View File

@ -0,0 +1,10 @@
created: 20210426131042769
modified: 20210426131553560
tags: [[Operator Examples]] [[median Operator]]
title: median Operator (Examples)
type: text/vnd.tiddlywiki
<<.operator-example 1 "=1 =3 =4 =5 +[median[]]">>
Note that if there is no input the operator returns `NaN`
<<.operator-example 2 "[title[NotATiddler]get[price]] +[median[]]">>

View File

@ -0,0 +1,10 @@
created: 20210426130306824
modified: 20210426131553553
tags: [[Operator Examples]] [[standard-deviation Operator]]
title: standard-deviation Operator (Examples)
type: text/vnd.tiddlywiki
<<.operator-example 1 "=1 =3 =4 =5 +[standard-deviation[]]">>
Note that if there is no input the operator returns `NaN`
<<.operator-example 2 "[title[NotATiddler]get[price]] +[standard-deviation[]]">>

View File

@ -0,0 +1,10 @@
created: 20210426130620777
modified: 20210426131553522
tags: [[Operator Examples]] [[variance Operator]]
title: variance Operator (Examples)
type: text/vnd.tiddlywiki
<<.operator-example 1 "1 3 4 5 +[variance[]]">>
Note that if there is no input the operator returns `NaN`
<<.operator-example 2 "[title[NotATiddler]is[tiddler]get[price]] +[variance[]]">>

View File

@ -1,6 +1,6 @@
caption: filter
created: 20200929174420821
modified: 20201027185144953
modified: 20210522162551921
op-input: a [[selection of titles|Title Selection]] passed as input to the filter
op-neg-input: a [[selection of titles|Title Selection]] passed as input to the filter
op-neg-output: those input titles that <<.em "do not">> pass the filter <<.place S>>
@ -28,6 +28,8 @@ Simple filter operations can be concatenated together directly (eg `[tag[HelloTh
</$vars>
```
Note that within the subfilter, the "currentTiddler" variable is set to the title of the tiddler being processed. The value of currentTiddler outside the subfilter is available in the variable "..currentTiddler". <<.from-version "5.1.24">>
<<.tip "Compare with the similar [[subfilter|subfilter Operator]] operator which runs a subfilter and directly returns the results">>
<<.tip "Compare with the analagous named filter run prefix `:filter`">>

View File

@ -1,24 +1,40 @@
caption: lookup
created: 20170907103639431
modified: 20170907144703051
modified: 20210116081305739
op-input: a [[selection of titles|Title Selection]]
op-output: the lookup values corresponding to each input title
op-parameter: prefix applied to input titles to yield title of lookup tiddler from which value is retrieved
op-parameter-name: P
op-purpose: applies a prefix to each input title to yield the title of a tiddler from which the final value is retrieved
op-suffix: the default value to be used for missing lookups
op-suffix-name: D
op-output: the lookup values corresponding to each lookup tiddler
op-parameter: prefix applied to input titles to yield title of lookup tiddler from which value is retrieved. Now accepts 1 or 2 parameters, see below for details
op-parameter-name: P, T
op-purpose: applies a prefix to each input title to yield the title of a tiddler from which the final value is retrieved. With a single parameter, the default field is "text" and the default index is "0". If a second parameter is provided, that becomes the target field or index.
op-suffix: the default value to be used for missing lookups. This operator can now accept a second suffix, see below for details
op-suffix-name: D, I
tags: [[Filter Operators]]
title: lookup Operator
type: text/vnd.tiddlywiki
<<.from-version "5.1.15">>
The action of this operator is as follows:
The action of this operator is as follows with 1 parameter:
* Apply the specified prefix to each input tiddler title, yielding a new list of tiddler titles
* Transclude the value of each of those tiddlers
** Substitute the default value for missing or empty tiddlers
* Transclude the value of the `text` field each of those tiddlers
** Substitute the default value for missing or empty values
* Return the list of values
<<.from-version "5.1.24">>
The action of this operator is as follows with 2 parameters:
If there are two parameters provided, use the second parameter as the target field or index.
<<.note """If there is only one parameter given, the filter checks for a second suffix equal to "index". If this suffix is found, the default target index is "0".
In all other cases, the default target field is "text".""">>
Then:
* Apply the specified prefix to each input tiddler title, yielding a new list of tiddler titles
* Transclude the value of the target field or index
** Substitute the default value for missing or empty values
* Return the list of values
<<.operator-examples "lookup">>

View File

@ -0,0 +1,13 @@
caption: median
created: 20210417090137714
modified: 20210426131553507
op-input: a [[selection of titles|Title Selection]]
op-output: the median of the input numbers
op-purpose: treating each input title as a number, compute their median value
tags: [[Filter Operators]] [[Mathematics Operators]]
title: median Operator
type: text/vnd.tiddlywiki
<<.from-version "5.1.24">> See [[Mathematics Operators]] for an overview.
<<.operator-examples "median">>

View File

@ -1,6 +1,6 @@
caption: reduce
created: 20201004154131193
modified: 20201208185109549
modified: 20210522162536854
op-input: a [[selection of titles|Title Selection]] passed as input to the filter
op-output: the final result of running the subfilter <<.place S>>
op-parameter: a [[filter expression|Filter Expression]]. Optional second parameter for initial value for accumulator
@ -18,6 +18,7 @@ The following variables are available within the subfilter:
* ''accumulator'' - the result of the previous subfilter run
* ''currentTiddler'' - the input title
* ''..currentTiddler'' - the value of the variable `currentTiddler` outside the subfilter. <<.from-version "5.1.24">>
* ''index'' - the numeric index of the current list item (with zero being the first item in the list)
* ''revIndex'' - the reverse numeric index of the current list item (with zero being the last item in the list)
* ''length'' - the total length of the input list

View File

@ -1,21 +1,23 @@
caption: sortsub
created: 20200424160155182
modified: 20200424160155182
modified: 20210522162521222
op-input: a [[selection of titles|Title Selection]]
op-neg-output: the input, sorted into reverse order by the result of evaluating subfilter <<.param S>>
op-output: the input, sorted into ascending order by the result of evaluating subfilter <<.param S>>
op-parameter: a subfilter to be evaluated
op-parameter-name: S
op-purpose: sort the input by the result of evaluating a subfilter for each item
op-suffix: the type used for the comparison (string, number, integer, date, version), defaulting to string
op-suffix-name: T
tags: [[Filter Operators]] [[Field Operators]] [[Order Operators]] [[Negatable Operators]]
title: sortsub Operator
type: text/vnd.tiddlywiki
caption: sortsub
op-purpose: sort the input by the result of evaluating a subfilter for each item
op-input: a [[selection of titles|Title Selection]]
op-parameter: a subfilter to be evaluated
op-parameter-name: S
op-suffix: the type used for the comparison (string, number, integer, date, version), defaulting to string
op-suffix-name: T
op-output: the input, sorted into ascending order by the result of evaluating subfilter <<.param S>>
op-neg-output: the input, sorted into reverse order by the result of evaluating subfilter <<.param S>>
Each item in the list of input titles is passed to the subfilter in turn. The subfilter transforms the input titles into the form needed for sorting. For example, the subfilter `[length[]]` transforms each input title in the number representing its length, and thus sorts the input titles according to their length.
Note that within the subfilter, the "currentTiddler" variable is set to the title of the tiddler being processed. This permits subfilters like `[{!!value}divide{!!cost}]` to be used for computation.
Note that within the subfilter, the "currentTiddler" variable is set to the title of the tiddler being processed. This permits subfilters like `[{!!value}divide{!!cost}]` to be used for computation.
The value of currentTiddler outside the subfilter is available in the variable "..currentTiddler". <<.from-version "5.1.24">>
The suffix <<.place T>> determines how the items are compared and can be:
@ -24,6 +26,7 @@ The suffix <<.place T>> determines how the items are compared and can be:
* "integer" - invalid integers are interpreted as zero
* "date" - invalid dates are interpreted as 1st January 1970
* "version" - invalid versions are interpreted as "v0.0.0"
* "alphanumeric" - treat items as alphanumerics <<.from-version "5.1.24">>
Note that subfilters should return the same number of items that they are passed. Any missing entries will be treated as zero or the empty string. In particular, when retrieving the value of a field with the [[get Operator]] it is helpful to guard against a missing field value using the [[else Operator]]. For example `[get[myfield]else[default-value]...`.

View File

@ -0,0 +1,15 @@
caption: standard-deviation
created: 20210426130150358
modified: 20210426131553530
op-input: a [[selection of titles|Title Selection]]
op-output: the standard-deviation of the input as numbers
op-purpose: treating each input title as a number, compute their standard-deviation
tags: [[Reducing Mathematics Operators]] [[Filter Operators]] [[Mathematics Operators]]
title: standard-deviation Operator
type: text/vnd.tiddlywiki
<<.from-version "5.1.24">> See [[Mathematics Operators]] for an overview.
<<.tip """ The `standard-deviation` operator treats the input as a complete population and not a sample""">>
<<.operator-examples "standard-deviation">>

View File

@ -1,5 +1,5 @@
created: 20150124182421000
modified: 20201214053032397
modified: 20210522162642994
tags: [[Filter Syntax]]
title: Filter Expression
type: text/vnd.tiddlywiki
@ -26,11 +26,15 @@ If a run has:
* named prefix `:intersection` replaces all filter output so far with titles that are present in the output of this run, as well as the output from previous runs. Forms the input for the next run. <<.from-version "5.1.23">>
* named prefix `:reduce` replaces all filter output so far with a single item by repeatedly applying a formula to each input title. A typical use is to add up the values in a given field of each input title. <<.from-version "5.1.23">>
** [[Examples|Filter Run Prefix (Examples)]]
* named prefix `:sort` sorts all filter output so far by applying this run to each input title and sorting according to that output. <<.from-version "5.1.24">>
** See [[Sort Filter Run Prefix]].
<<.tip "Compare named filter run prefix `:filter` with [[filter Operator]] which applies a subfilter to every input title, removing the titles that return an empty result from the subfilter">>
<<.tip "Compare named filter run prefix `:reduce` with [[reduce Operator]] which is used to used to flatten a list of items down to a single item by repeatedly applying a subfilter.">>
<<.tip """Within the filter runs prefixed with `:reduce`, `:sort` and `:filter`, the "currentTiddler" variable is set to the title of the tiddler being processed. The value of currentTiddler outside the subfilter is available in the variable "..currentTiddler".<<.from-version "5.1.24">>""" >>
In technical / logical terms:
|!Run |!Equivalent named prefix |!Interpretation |!Output |
@ -47,7 +51,7 @@ The input of a run is normally a list of all the non-[[shadow|ShadowTiddlers]] t
|Prefix|Input|h
|`-`, `~`, `=`, `:intersection` or none| <$link to="all Operator">`[all[]]`</$link> tiddler titles, unless otherwise determined by the first [[filter operator|Filter Operators]]|
|`+`, `:filter`, `:reduce`|the filter output of all previous runs so far|
|`+`, `:filter`, `:reduce`,`:sort`|the filter output of all previous runs so far|
Precisely because of varying inputs, be aware that both prefixes `-` and `+` do not behave inverse to one another!

View File

@ -1,6 +1,6 @@
created: 20201117073343969
modified: 20201208185546667
tags: [[Filter Syntax]]
modified: 20210428084013109
tags: [[Filter Syntax]] [[Filter Run Prefix Examples]]
title: Filter Run Prefix (Examples)
type: text/vnd.tiddlywiki
@ -44,3 +44,7 @@ Specifying a default value when input is empty:
`[tag[non-existent]] :reduce[get[price]multiply{!!quantity}add<accumulator>] :else[[0]]`
<$macrocall $name=".tip" _="""Unlike the [[reduce Operator]], the `:reduce` prefix cannot specify an initial value for the accumulator, so its initial value will always be empty (which is treated as 0 by mathematical operators). So `=1 =2 =3 :reduce[multiply<accumulator>]` will produce 0, not 6. If you need to specify an initial accumulator value, use the [[reduce Operator]]."""/>
!! `:sort` examples
See [[Sort Filter Run Prefix (Examples)]]

View File

@ -0,0 +1,33 @@
created: 20210428074912172
modified: 20210428085746041
tags: [[Filter Syntax]] [[Sort Filter Run Prefix]] [[Filter Run Prefix Examples]]
title: Sort Filter Run Prefix (Examples)
type: text/vnd.tiddlywiki
Sort by title length:
<<.operator-example 1 "[all[tiddlers]] :sort:number[length[]] +[limit[10]]">>
Sort by title length reversed:
<<.operator-example 2 "[all[tiddlers]] :sort:number:reverse[length[]] +[limit[10]]">>
Sort by text length:
<<.operator-example 3 "[all[tiddlers]] :sort:number[get[text]length[]] +[limit[10]]">>
Sort by newest of modified dates:
<<.operator-example 4 "[tag[Field Operators]] :sort:date[get[modified]else[19700101]] +[limit[10]]">>
Sort by title:
<<.operator-example 5 "[tag[Field Operators]] :sort:string:casesensitive[get[caption]] +[limit[10]]">>
Sort by title in reverse order:
<<.operator-example 6 "[tag[Field Operators]] :sort:string:casesensitive,reverse[get[caption]] +[limit[10]]">>
Sort as text with case sensitivity:
<<.operator-example 7 "Apple Banana Orange Grapefruit guava DragonFruit Kiwi apple orange :sort:string:casesensitive[{!!title}]">>
Sort as text ignoring case:
<<.operator-example 8 "Apple Banana Orange Grapefruit guava DragonFruit Kiwi apple orange :sort:string:caseinsensitive[{!!title}]">>

View File

@ -0,0 +1,32 @@
created: 20210428083929749
modified: 20210522162628946
tags: [[Filter Syntax]] [[Filter Run Prefix]]
title: Sort Filter Run Prefix
type: text/vnd.tiddlywiki
<<.from-version "5.1.24">>
|''purpose'' |sort the input titles by the result of evaluating this filter run for each item |
|''input'' |all titles from previous filter runs |
|''suffix'' |the `:sort` filter run prefix uses a rich suffix, see below for details |
|''output''|the sorted result of previous filter runs |
Each input title from previous runs is passed to this run in turn. The filter run transforms the input titles into the form needed for sorting. For example, the filter run `[length[]]` transforms each input title in to the number representing its length, and thus sorts the input titles according to their length.
Note that within the filter run, the "currentTiddler" variable is set to the title of the tiddler being processed. This permits filter runs like `:sort:number[{!!value}divide{!!cost}]` to be used for computation. The value of currentTiddler outside the run is available in the variable "..currentTiddler".
The `:sort` filter run prefix uses an extended syntax that allows for multiple suffixes, some of which are required:
```
:sort:<type>:<flaglist>[...filter run...]
```
* ''type'': Required. Determines how the items are compared and can be any of: ''string'', ''alphanumeric'', ''number'', ''integer'', ''version'' or ''date''.
* ''flaglist'': comma separated list of the following flags:
** ''casesensitive'' or ''caseinsensitive'' (required for types `string` and `alphanumeric`).
** ''reverse'' to invert the order of the filter run (optional).
Note that filter runs used with the `:sort` prefix should return the same number of items that they are passed. Any missing entries will be treated as zero or the empty string. In particular, when retrieving the value of a field with the [[get Operator]] it is helpful to guard against a missing field value using the [[else Operator]]. For example `[get[myfield]else[default-value]...`.
[[Examples|Sort Filter Run Prefix (Examples)]]

View File

@ -0,0 +1,15 @@
caption: variance
created: 20210426130029500
modified: 20210426131553539
op-input: a [[selection of titles|Title Selection]]
op-output: the variance of the input as numbers
op-purpose: treating each input title as a number, compute their variance
tags: [[Reducing Mathematics Operators]] [[Filter Operators]] [[Mathematics Operators]]
title: variance Operator
type: text/vnd.tiddlywiki
<<.from-version "5.1.24">> See [[Mathematics Operators]] for an overview.
<<.tip """ The `standard-deviation` operator treats the input as a complete population and not a sample""">>
<<.operator-examples "variance">>

View File

@ -0,0 +1,13 @@
created: 20210411100148461
modified: 20210411100148461
tags: [[Hidden Settings]]
title: Hidden Setting: HTML Parser Sandbox
type: text/vnd.tiddlywiki
<<.from-version "5.1.24">> By default, tiddlers with the type `text/html` are displayed in an iframe with the [[sandbox attribute|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox]] set to the empty string. This causes all security restrictions to be applied, disabling many features such as JavaScript, downloads and external file references. This is the safest setting.
To globally disable the sandbox, set the tiddler $:/config/HtmlParser/DisableSandbox to `yes`. This will mean that the code in the iframe has full access to TiddlyWiki's internals, which means that a malicious HTML page could exfiltrate data from a private wiki.
To keep the sandbox but control which restrictions are applied, ensure that $:/config/HtmlParser/DisableSandbox is not set to `yes`, and then set $:/config/HtmlParser/SandboxTokens to the desired list of tokens [[from the MDN documentation|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox]].
Note that these are global settings. To control the sandboxing on an individual tiddler basis will require a custom `<iframe>` to be used.

View File

@ -1,10 +1,10 @@
created: 20200315143638556
modified: 20200315143638556
modified: 20210519155433742
tags: [[Hidden Settings]]
title: Hidden Setting: Disable Drag and Drop
type: text/vnd.tiddlywiki
<.from-version "5.1.22">> To disable all the drag and drop operations that are built into the core, set the following tiddler to "no":
<<.from-version "5.1.22">> To disable all the drag and drop operations that are built into the core, set the following tiddler to "no":
$:/config/DragAndDrop/Enable

View File

@ -0,0 +1,12 @@
created: 20210519155447339
modified: 20210519160010708
tags: [[Hidden Settings]]
title: Hidden Setting: Enable File Import in Editor
type: text/vnd.tiddlywiki
<<.from-version "5.1.24">>
$:/config/Editor/EnableImportFilter
This filter determines whether dragging and dropping files in the editor works for a given tiddler or not. A non-empty result enables drag and drop in the editor for that tiddler.
This filter is used in such a manner that it respects the global drag and drop setting.

View File

@ -0,0 +1,11 @@
created: 20210519155910219
modified: 20210519160221219
tags: [[Hidden Settings]]
title: Hidden Settings: Import Content Types for Editor
type: text/vnd.tiddlywiki
<<.from-version "5.1.24">>
$:/config/Editor/ImportContentTypesFilter
This filter determines which `contentTypes` can be imported by dragging and dropping into the editor. It used by a DropzoneWidget wrapped around the editor, for the `contentTypesFilter` attribute.

View File

@ -0,0 +1,126 @@
created: 20201216182347597
modified: 20201217193318816
tags:
title: How to create dynamic editor toolbar buttons
type: text/vnd.tiddlywiki
\define disabledFilterExample()`[[$(tempBoldDisabled)$]get[state-disabled]else[no]]`
\define remove-shortcuts()
<$action-deletetiddler $tiddler=<<shortcutInfo>>/>
<$action-deletetiddler $tiddler=<<shortcutConfigMac>>/>
<$action-deletetiddler $tiddler=<<shortcutConfigNotMac>>/>
<$action-deletetiddler $tiddler=<<tempBold>>/>
<$action-deletetiddler $tiddler=<<tempBoldDisabled>>/>
\end
\define create-shortcut-tiddlers()
<$action-createtiddler $basetitle=<<shortcutInfo>>
$template="$:/config/ShortcutInfo/bold"
$overwrite="yes"
/>
<$action-navigate $to=<<shortcutInfo>>/>
<$action-createtiddler $basetitle=<<shortcutConfigMac>>
$template="$:/config/shortcuts-mac/bold"
$overwrite="yes"
text="meta-shift-X"
/>
<$action-navigate $to=<<shortcutConfigMac>>/>
<$action-createtiddler $basetitle=<<shortcutConfigNotMac>>
$template="$:/config/shortcuts-not-mac/bold"
$overwrite="yes"
text="ctrl-shift-X"
/>
<$action-navigate $to=<<shortcutConfigNotMac>>/>
\end
\define clone-bold()
<$action-createtiddler $basetitle=<<tempBold>>
$template="$:/core/ui/EditorToolbar/bold"
$overwrite="yes"
icon="$:/core/images/spiral"
shortcuts="((temp-bold))"
condition-disabled="[[$(tempBoldDisabled)$]get[state-disabled]else[no]]"
/>
<$action-sendmessage $message="tm-edit-tiddler" $param=<<tempBold>>/>
\end
\define clone-button-bold()
<$button actions=<<clone-bold>> >Create a Temporary Bold Button</$button>
\end
\define toggle-bold()
<$action-listops $tiddler=<<tempBoldDisabled>> $field="state-disabled" $subfilter="+[toggle[yes],[no]]" />
\end
<!-- $vars is needed don't remove it! -->
<$vars tempBold="$:/temp/bold" tempBoldDisabled="$:/temp/bold/disabled" shortcutInfo="$:/config/ShortcutInfo/temp-bold" shortcutConfigMac="$:/config/shortcuts-mac/temp-bold" shortcutConfigNotMac="$:/config/shortcuts-not-mac/temp-bold">
!! Create a New Toolbar Button
The easiest way to create new editor toolbar button is to clone and open one.
><<clone-button-bold>>
This tiddler contains all the necessary elements that are important for toolbar buttons.
<<<
; text
: We don't discuss the text field details in this howto
; caption
: The caption field is used to display the shortcut name in the $:/ControlPanel : Keyboard Shortcuts tab
; condition
: A filter, that defines the button visibility state
; condition-disabled <<.from-version "5.1.23">>
: A ''filter'', that allows us to define the "disabled" attribute for buttons. eg: <<disabledFilterExample>>
: This condition ''must'' return "no", if the "state tiddler" or "state field" doesn't exist! So there has to be a `else[no]` element in the filter.
; description
: Is used as the button tooltip
; icon
: Assigns the button icon. We use `$:/core/images/spiral` {{$:/core/images/spiral}} here.
; shortcuts
: This is the [[Keyboard Shortcut Descriptor]] eg: `((temp-bold))`
<<<
!! Disabled State
You can use any "state tiddler" to define the button disabled state. It's important, that the ''condition-disabled'' field is a ''filter''. For our example we use the `state-disabled` field from tiddler: <<tempBoldDisabled>>
The easiest way to test filters is with the $:/AdvancedSearch : Filter tab
> The temporary "bold button" is disabled: ''"{{{ [<tempBoldDisabled>get[state-disabled]else[no]] }}}"'' ... You can see the button in the editor toolbar if you edit any tiddler!
> <$button actions=<<toggle-bold>>>Toggle Temporary Bold Button Visisbility</$button>
!! Create Keyboard Shortcuts for New Button
For our ~HowTo we use the ''Keyboard Shortcut Descriptor'': `((temp-bold))`
To create a valid shortcut configuration we need 2 tiddlers:
>[[$:/config/ShortcutInfo/|$:/config/ShortcutInfo/temp-bold]]''temp-bold'' .. and
>[[$:/config/shortcuts/|$:/config/shortcuts/temp-bold]]''temp-bold''
> <$button actions=<<create-shortcut-tiddlers>>>Create Shortcut Tiddlers</$button>
!!
!! Clean up the Configuration Tiddlers
If you don't need the config tiddlers anymore you can
><$button actions=<<remove-shortcuts>>>Remove the Shortcut Configuration</$button>
</$vars><!-- needed don't remove! -->

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