diff --git a/bin/optimise-svgs.js b/bin/optimise-svgs.js index 28f4f715d..4920ab920 100755 --- a/bin/optimise-svgs.js +++ b/bin/optimise-svgs.js @@ -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">>"); - 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("<<now "DD">>","<>"); fs.writeFileSync(filepath,newSVG); - },function(err) { + } else { console.log("Error " + err + " with " + filename) process.exit(); - }); + }; } }); diff --git a/core/images/minus-button.tid b/core/images/minus-button.tid new file mode 100644 index 000000000..7132ed3e9 --- /dev/null +++ b/core/images/minus-button.tid @@ -0,0 +1,4 @@ +title: $:/core/images/minus-button +tags: $:/tags/Image + + \ No newline at end of file diff --git a/core/images/plus-button.tid b/core/images/plus-button.tid new file mode 100644 index 000000000..b001f3e2f --- /dev/null +++ b/core/images/plus-button.tid @@ -0,0 +1,4 @@ +title: $:/core/images/plus-button +tags: $:/tags/Image + + \ No newline at end of file diff --git a/core/language/en-GB/ControlPanel.multids b/core/language/en-GB/ControlPanel.multids index bdf1cb556..f7c28cbe3 100644 --- a/core/language/en-GB/ControlPanel.multids +++ b/core/language/en-GB/ControlPanel.multids @@ -125,12 +125,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://.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 diff --git a/core/language/en-GB/Help/listen.tid b/core/language/en-GB/Help/listen.tid index db976ce6a..7060a1a48 100644 --- a/core/language/en-GB/Help/listen.tid +++ b/core/language/en-GB/Help/listen.tid @@ -31,6 +31,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. diff --git a/core/language/en-GB/Import.multids b/core/language/en-GB/Import.multids index 3b3308ad5..24b29c3f7 100644 --- a/core/language/en-GB/Import.multids +++ b/core/language/en-GB/Import.multids @@ -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: diff --git a/core/language/en-GB/Misc.multids b/core/language/en-GB/Misc.multids index 81c5758cc..441439640 100644 --- a/core/language/en-GB/Misc.multids +++ b/core/language/en-GB/Misc.multids @@ -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: `

''Network Error''

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.

''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 diff --git a/core/modules/editor/engines/framed.js b/core/modules/editor/engines/framed.js index c70167f6c..724c4e7f3 100644 --- a/core/modules/editor/engines/framed.js +++ b/core/modules/editor/engines/framed.js @@ -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); } diff --git a/core/modules/editor/factory.js b/core/modules/editor/factory.js index f1463fe5c..dd362be16 100644 --- a/core/modules/editor/factory.js +++ b/core/modules/editor/factory.js @@ -186,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") { @@ -217,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]) { @@ -297,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; } diff --git a/core/modules/filterrunprefixes/sort.js b/core/modules/filterrunprefixes/sort.js new file mode 100644 index 000000000..689193fff --- /dev/null +++ b/core/modules/filterrunprefixes/sort.js @@ -0,0 +1,58 @@ +/*\ +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; + default: + return widget.getVariable(name); + } + } + }); + sortKeys.push(key[0] || ""); + }); + results.clear(); + // Prepare an array of indexes to sort + for(var t=0; t 0) === target) { results.push(title); } diff --git a/core/modules/filters/math.js b/core/modules/filters/math.js index f52a8c678..9e767c362 100644 --- a/core/modules/filters/math.js +++ b/core/modules/filters/math.js @@ -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; + }; +}; })(); diff --git a/core/modules/macros/uniquetitle.js b/core/modules/macros/unusedtitle.js similarity index 59% rename from core/modules/macros/uniquetitle.js rename to core/modules/macros/unusedtitle.js index 8531d1ce7..952bd0264 100644 --- a/core/modules/macros/uniquetitle.js +++ b/core/modules/macros/unusedtitle.js @@ -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}); }; })(); diff --git a/core/modules/parsers/htmlparser.js b/core/modules/parsers/htmlparser.js index 39b0c21df..206ab9c78 100644 --- a/core/modules/parsers/htmlparser.js +++ b/core/modules/parsers/htmlparser.js @@ -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; diff --git a/core/modules/savers/put.js b/core/modules/savers/put.js index 771f21c0e..a6c27c891 100644 --- a/core/modules/savers/put.js +++ b/core/modules/savers/put.js @@ -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 } diff --git a/core/modules/server/routes/get-favicon.js b/core/modules/server/routes/get-favicon.js index 79dd1a6be..39a391127 100644 --- a/core/modules/server/routes/get-favicon.js +++ b/core/modules/server/routes/get-favicon.js @@ -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"); }; }()); diff --git a/core/modules/server/routes/get-file.js b/core/modules/server/routes/get-file.js index ac4b01d7e..2588e48f7 100644 --- a/core/modules/server/routes/get-file.js +++ b/core/modules/server/routes/get-file.js @@ -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); }); }; diff --git a/core/modules/server/routes/get-index.js b/core/modules/server/routes/get-index.js index d4431ce64..5a0164f82 100644 --- a/core/modules/server/routes/get-index.js +++ b/core/modules/server/routes/get-index.js @@ -12,38 +12,16 @@ GET / /*global $tw: false */ "use strict"; -var zlib = require("zlib"); - exports.method = "GET"; exports.path = /^\/index.html$/; 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); }; }()); diff --git a/core/modules/server/routes/get-status.js b/core/modules/server/routes/get-status.js index c570c090b..1d7979336 100644 --- a/core/modules/server/routes/get-status.js +++ b/core/modules/server/routes/get-status.js @@ -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"); }; }()); diff --git a/core/modules/server/routes/get-tiddler-html.js b/core/modules/server/routes/get-tiddler-html.js index 4fe440821..1a1b0c5e5 100644 --- a/core/modules/server/routes/get-tiddler-html.js +++ b/core/modules/server/routes/get-tiddler-html.js @@ -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(); diff --git a/core/modules/server/routes/get-tiddler.js b/core/modules/server/routes/get-tiddler.js index e125d7055..4db7be012 100644 --- a/core/modules/server/routes/get-tiddler.js +++ b/core/modules/server/routes/get-tiddler.js @@ -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(); diff --git a/core/modules/server/routes/get-tiddlers-json.js b/core/modules/server/routes/get-tiddlers-json.js index 203900346..6f3f07f6a 100644 --- a/core/modules/server/routes/get-tiddlers-json.js +++ b/core/modules/server/routes/get-tiddlers-json.js @@ -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"); }; }()); diff --git a/core/modules/server/server.js b/core/modules/server/server.js index 9b343bed5..33c306df9 100644 --- a/core/modules/server/server.js +++ b/core/modules/server/server.js @@ -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 = { @@ -75,6 +79,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", @@ -86,7 +155,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) { @@ -196,6 +266,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 diff --git a/core/modules/utils/dom/dragndrop.js b/core/modules/utils/dom/dragndrop.js index ba5d590c3..206846b9f 100644 --- a/core/modules/utils/dom/dragndrop.js +++ b/core/modules/utils/dom/dragndrop.js @@ -205,4 +205,16 @@ function parseJSONTiddlers(json,fallbackTitle) { return data; }; +exports.dragEventContainsFiles = function(event) { + if(event.dataTransfer.types) { + for(var i=0; i b) { @@ -895,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), @@ -910,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); diff --git a/core/modules/widgets/action-listops.js b/core/modules/widgets/action-listops.js index 6da84a3e5..a3448ba71 100644 --- a/core/modules/widgets/action-listops.js +++ b/core/modules/widgets/action-listops.js @@ -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)); diff --git a/core/modules/widgets/action-popup.js b/core/modules/widgets/action-popup.js index af17b3f51..2903532b6 100644 --- a/core/modules/widgets/action-popup.js +++ b/core/modules/widgets/action-popup.js @@ -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); diff --git a/core/modules/widgets/dropzone.js b/core/modules/widgets/dropzone.js index 08af202bb..9c6a16b20 100644 --- a/core/modules/widgets/dropzone.js +++ b/core/modules/widgets/dropzone.js @@ -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 0) { this.refreshSelf(); return true; } diff --git a/core/modules/widgets/fieldmangler.js b/core/modules/widgets/fieldmangler.js index a8b18ffa1..fb249cbf2 100644 --- a/core/modules/widgets/fieldmangler.js +++ b/core/modules/widgets/fieldmangler.js @@ -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; diff --git a/core/modules/widgets/list.js b/core/modules/widgets/list.js index a49bf01ea..17161ca21 100755 --- a/core/modules/widgets/list.js +++ b/core/modules/widgets/list.js @@ -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=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=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(); }; diff --git a/core/modules/widgets/select.js b/core/modules/widgets/select.js index 6efeb588f..b3177d967 100644 --- a/core/modules/widgets/select.js +++ b/core/modules/widgets/select.js @@ -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 diff --git a/core/modules/wiki.js b/core/modules/wiki.js index efe479ece..0fd6b3a4d 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -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; }; @@ -364,12 +374,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 = ""; } diff --git a/core/ui/ControlPanel/Saving/TiddlySpot.tid b/core/ui/ControlPanel/Saving/TiddlySpot.tid index dad217251..cb8c9edf3 100644 --- a/core/ui/ControlPanel/Saving/TiddlySpot.tid +++ b/core/ui/ControlPanel/Saving/TiddlySpot.tid @@ -30,8 +30,6 @@ http://$(userName)$.tiddlyspot.com/$path$/ |<> |<$edit-text tiddler="$:/UploadName" default="" tag="input"/> | |<> |<$password name="upload"/> | -|<> |<> | -|<> |<> | ''<>'' diff --git a/core/ui/EditTemplate/body-editor.tid b/core/ui/EditTemplate/body-editor.tid index f7e8a3acf..4897aff77 100644 --- a/core/ui/EditTemplate/body-editor.tid +++ b/core/ui/EditTemplate/body-editor.tid @@ -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" -/> +/><$list + + filter="[all[shadows+tiddlers]tag[$:/tags/EditorTools]!has[draft.of]]" + +><$list + filter={{!!condition}} + variable="list-condition" +><$transclude/> + diff --git a/core/ui/EditTemplate/body-toolbar-button.tid b/core/ui/EditTemplate/body-toolbar-button.tid index 956bfd634..7390b3e20 100644 --- a/core/ui/EditTemplate/body-toolbar-button.tid +++ b/core/ui/EditTemplate/body-toolbar-button.tid @@ -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=<> actions={{!!actions}} + disabled=<> >match[yes]then[]else{!!shortcuts}] }}} /><><$transclude tiddler=<> field="text" -/><$list +/><$list filter="[all[current]has[dropdown]]" variable="dropdown" @@ -60,24 +61,25 @@ title: $:/core/ui/EditTemplate/body/toolbar/button name="dropdown-state" value=<> -><$button +><$set name=disabled filter={{!!condition-disabled}}><$button popup=<> class="tc-popup-keep tc-btn-invisible $(buttonClasses)$" selectedClass="tc-selected" tooltip=<> actions={{!!actions}} + disabled=<> >match[yes]then[]else{!!shortcuts}] }}} /><><$transclude tiddler=<> field="text" -/><$reveal +/><$reveal state=<> type="popup" diff --git a/core/ui/EditTemplate/body.tid b/core/ui/EditTemplate/body.tid index 8369c82e8..8fdb066ff 100644 --- a/core/ui/EditTemplate/body.tid +++ b/core/ui/EditTemplate/body.tid @@ -5,6 +5,11 @@ tags: $:/tags/EditTemplate \define config-visibility-title() $:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$ \end + +\define importFileActions() +<$action-popup $state=<> $coords="(0,0,0,0)" $floating="yes"/> +\end + <$list filter="[all[current]has[_canonical_uri]]">
@@ -20,9 +25,8 @@ $:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$ <$list filter="[all[current]!has[_canonical_uri]]"> - -<$reveal state="$:/state/showeditpreview" type="match" text="yes"> - +<$vars importTitle=<> importState=<> > +<$dropzone 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=<> ><$reveal state="$:/state/showeditpreview" type="match" text="yes">
<$transclude tiddler="$:/core/ui/EditTemplate/body/editor" mode="inline"/> @@ -38,7 +42,6 @@ $:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$
- <$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"/> - - + + + \ No newline at end of file diff --git a/core/ui/EditTemplate/title.tid b/core/ui/EditTemplate/title.tid index 8972504d2..daac829f6 100644 --- a/core/ui/EditTemplate/title.tid +++ b/core/ui/EditTemplate/title.tid @@ -23,7 +23,7 @@ tags: $:/tags/EditTemplate
-{{$:/core/images/warning}} {{$:/language/EditTemplate/Title/Exists/Prompt}} +{{$:/core/images/warning}} {{$:/language/EditTemplate/Title/Exists/Prompt}}: <$link to={{!!draft.title}} />
diff --git a/core/ui/EditorToolbar/file-import.tid b/core/ui/EditorToolbar/file-import.tid new file mode 100644 index 000000000..742597102 --- /dev/null +++ b/core/ui/EditorToolbar/file-import.tid @@ -0,0 +1,41 @@ +title: $:/core/ui/EditorToolbar/file-import +tags: $:/tags/EditorTools +condition: [!has[type]] [type[text/vnd.tiddlywiki]] + +\define lingo-base() $:/language/Import/ + +\define closePopupActions() +<$action-deletetiddler $filter="[title] [title]"/> +\end + +\define replacement-text-image() [img[$title$]] + +\define replacement-text-file() [[$title$]] + +\define postImportActions() +<$list filter="[links[]] :reduce[get[type]prefix[image]thenelsesearch-replace[$title$],addprefix]" variable="imageTitle"> +<$action-sendmessage + $message="tm-edit-text-operation" + $param="replace-selection" + text=<> +/> + +<> +\end + +\define buttons() +<$button class="tc-btn-invisible" actions=<> ><> +<$button class="tc-btn-invisible" message="tm-perform-import" param=<> actions=<> ><> +\end + +<$reveal type="popup" state=<> tag="div" class="tc-editor-importpopup"> +
+<$list filter="[field:plugin-type[import]]"> +

<>

+<$tiddler tiddler=<>> +{{||$:/core/ui/ImportListing}} +<> + + +
+ diff --git a/core/wiki/config/EditorEnableImportFilter.tid b/core/wiki/config/EditorEnableImportFilter.tid new file mode 100644 index 000000000..55d068e12 --- /dev/null +++ b/core/wiki/config/EditorEnableImportFilter.tid @@ -0,0 +1,4 @@ +title: $:/config/Editor/EnableImportFilter +type: text/vnd.tiddlywiki + +[all[current]type[text/vnd.tiddlywiki]] [all[current]!has[type]] \ No newline at end of file diff --git a/core/wiki/config/EditorImportContentTypesFilter.tid b/core/wiki/config/EditorImportContentTypesFilter.tid new file mode 100644 index 000000000..27a3c1d88 --- /dev/null +++ b/core/wiki/config/EditorImportContentTypesFilter.tid @@ -0,0 +1,4 @@ +title: $:/config/Editor/ImportContentTypesFilter +type: text/vnd.tiddlywiki + +[prefix[image/]] \ No newline at end of file diff --git a/editions/prerelease/tiddlers/Release 5.1.24.tid b/editions/prerelease/tiddlers/Release 5.1.24.tid index 24c911f12..8d732167f 100644 --- a/editions/prerelease/tiddlers/Release 5.1.24.tid +++ b/editions/prerelease/tiddlers/Release 5.1.24.tid @@ -25,12 +25,31 @@ type: text/vnd.tiddlywiki * <<.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 @@ -40,6 +59,9 @@ type: text/vnd.tiddlywiki * <<.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 @@ -62,6 +84,7 @@ type: text/vnd.tiddlywiki !! [[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 @@ -71,6 +94,11 @@ type: text/vnd.tiddlywiki * <<.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 @@ -90,6 +118,7 @@ type: text/vnd.tiddlywiki * <<.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 + [[@Jermolene|https://github.com/Jermolene]] would like to thank the contributors to this release who have generously given their time to help improve TiddlyWiki: * <]",anchorWidget).join(",")).toBe("one,hasList,TiddlerOne,has filter,$:/TiddlerTwo,Tiddler Three,$:/ShadowPlugin,a fourth tiddler,filter regexp test"); expect(wiki.filterTiddlers("[!sortsub:number]",anchorWidget).join(",")).toBe("filter regexp test,a fourth tiddler,$:/ShadowPlugin,$:/TiddlerTwo,Tiddler Three,TiddlerOne,has filter,hasList,one"); expect(wiki.filterTiddlers("[sortsub:string]",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]",anchorWidget).join(",")).toBe("one,TiddlerOne,hasList,has filter,$:/ShadowPlugin,a fourth tiddler,Tiddler Three,$:/TiddlerTwo,filter regexp test"); expect(wiki.filterTiddlers("[!sortsub:string]",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]",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]",anchorWidget).join(",")).toBe("a1,a2,a3,a10,b1,b3,b10,c1,c9,c11"); }); it("should handle the toggle operator", function() { diff --git a/editions/test/tiddlers/tests/test-prefixes-filter.js b/editions/test/tiddlers/tests/test-prefixes-filter.js index 62f51263f..1c380e347 100644 --- a/editions/test/tiddlers/tests/test-prefixes-filter.js +++ b/editions/test/tiddlers/tests/test-prefixes-filter.js @@ -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]")).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]")).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]")).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]")).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]")).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]").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]").length).toBe(0); expect(wiki.filterTiddlers("[tag[non-existent]] :reduce[get[price]multiply{!!quantity}add] :else[[0]]").join(",")).toBe("0"); + + expect(wiki.filterTiddlers("[tag[non-existent]] :reduce:11,22[get[price]multiply{!!quantity}add] :else[[0]]").join(",")).toBe("0"); + + expect(wiki.filterTiddlers("[tag[non-existent]] :reduce:11[get[price]multiply{!!quantity}add] :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]reduceelse[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]"); 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]",anchorWidget).join(",")).toBe("Red wine,Cheesecake,Chocolate Cake"); - }) + expect(wiki.filterTiddlers("[tag[textexample]filter]",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"); + }); }); })(); \ No newline at end of file diff --git a/editions/test/tiddlers/tests/test-widget.js b/editions/test/tiddlers/tests/test-widget.js index f3d500489..bd766fe7e 100755 --- a/editions/test/tiddlers/tests/test-widget.js +++ b/editions/test/tiddlers/tests/test-widget.js @@ -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=<>/><$text text=<>/><$text text=<>/>"; + 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("

Lemon Squash1yesnoJolly Old World2nonoGolly Gosh3nonoWorldly Old Jelly4noyes

"); + // 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("

Jalapeno Peppers1yesnoLemon Squash2nonoJolly Old World3nonoGolly Gosh4nonoWorldly Old Jelly5noyes

"); + // 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("

Jalapeno Peppers1yesnoLemon Squash2nonoJolly Old World3nonoWorldly Old Jelly4noyes

"); + // 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("

Jalapeno Peppers1yesnoLemon Squash2nonoJolly Old World3nonoSomething4nonoWorldly Old Jelly5noyes

"); + // 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 diff --git a/editions/tw5.com/tiddlers/about/History of TiddlyWiki.tid b/editions/tw5.com/tiddlers/about/History of TiddlyWiki.tid index b5ef197ff..a06939805 100644 --- a/editions/tw5.com/tiddlers/about/History of TiddlyWiki.tid +++ b/editions/tw5.com/tiddlers/about/History of TiddlyWiki.tid @@ -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 diff --git a/editions/tw5.com/tiddlers/community/resources/Hosting TiddlyWiki5 on GoogleDrive.tid b/editions/tw5.com/tiddlers/community/resources/Hosting TiddlyWiki5 on GoogleDrive.tid deleted file mode 100644 index 32a889751..000000000 --- a/editions/tw5.com/tiddlers/community/resources/Hosting TiddlyWiki5 on GoogleDrive.tid +++ /dev/null @@ -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) -<<< diff --git a/editions/tw5.com/tiddlers/concepts/PermaLinks.tid b/editions/tw5.com/tiddlers/concepts/PermaLinks.tid index 5127228d6..40c7a1925 100644 --- a/editions/tw5.com/tiddlers/concepts/PermaLinks.tid +++ b/editions/tw5.com/tiddlers/concepts/PermaLinks.tid @@ -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: -~https://tiddlywiki.com/#:[tags[task]] +~https://tiddlywiki.com/#:[tag[task]] ! About URL encoding diff --git a/editions/tw5.com/tiddlers/concepts/SystemTags.tid b/editions/tw5.com/tiddlers/concepts/SystemTags.tid index 0d0798f33..30dc35aa5 100644 --- a/editions/tw5.com/tiddlers/concepts/SystemTags.tid +++ b/editions/tw5.com/tiddlers/concepts/SystemTags.tid @@ -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 diff --git a/editions/tw5.com/tiddlers/filters/Mathematics Operators.tid b/editions/tw5.com/tiddlers/filters/Mathematics Operators.tid index 2040e0293..a0651ea8c 100644 --- a/editions/tw5.com/tiddlers/filters/Mathematics Operators.tid +++ b/editions/tw5.com/tiddlers/filters/Mathematics Operators.tid @@ -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: diff --git a/editions/tw5.com/tiddlers/filters/average.tid b/editions/tw5.com/tiddlers/filters/average.tid new file mode 100644 index 000000000..36f39be1d --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/average.tid @@ -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">> diff --git a/editions/tw5.com/tiddlers/filters/examples/average Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/average Operator (Examples).tid new file mode 100644 index 000000000..68fb03694 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/average Operator (Examples).tid @@ -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[]]">> diff --git a/editions/tw5.com/tiddlers/filters/examples/median Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/median Operator (Examples).tid new file mode 100644 index 000000000..ed76964b3 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/median Operator (Examples).tid @@ -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[]]">> diff --git a/editions/tw5.com/tiddlers/filters/examples/standard-deviation Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/standard-deviation Operator (Examples).tid new file mode 100644 index 000000000..4290287d8 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/standard-deviation Operator (Examples).tid @@ -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[]]">> diff --git a/editions/tw5.com/tiddlers/filters/examples/variance Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/variance Operator (Examples).tid new file mode 100644 index 000000000..fd0ad07a8 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/variance Operator (Examples).tid @@ -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[]]">> diff --git a/editions/tw5.com/tiddlers/filters/median.tid b/editions/tw5.com/tiddlers/filters/median.tid new file mode 100644 index 000000000..da8b6acc4 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/median.tid @@ -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">> diff --git a/editions/tw5.com/tiddlers/filters/sortsub Operator.tid b/editions/tw5.com/tiddlers/filters/sortsub Operator.tid index 0e48e29bc..451b979c9 100644 --- a/editions/tw5.com/tiddlers/filters/sortsub Operator.tid +++ b/editions/tw5.com/tiddlers/filters/sortsub Operator.tid @@ -1,17 +1,17 @@ +caption: sortsub created: 20200424160155182 -modified: 20200424160155182 +modified: 20210428152533501 +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. @@ -24,6 +24,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]...`. diff --git a/editions/tw5.com/tiddlers/filters/standard-deviation Operator.tid b/editions/tw5.com/tiddlers/filters/standard-deviation Operator.tid new file mode 100644 index 000000000..125c043b1 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/standard-deviation Operator.tid @@ -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">> diff --git a/editions/tw5.com/tiddlers/filters/syntax/Filter Expression.tid b/editions/tw5.com/tiddlers/filters/syntax/Filter Expression.tid index 577ff6181..ec72fbd6a 100644 --- a/editions/tw5.com/tiddlers/filters/syntax/Filter Expression.tid +++ b/editions/tw5.com/tiddlers/filters/syntax/Filter Expression.tid @@ -1,5 +1,5 @@ created: 20150124182421000 -modified: 20201214053032397 +modified: 20210428084144231 tags: [[Filter Syntax]] title: Filter Expression type: text/vnd.tiddlywiki @@ -26,6 +26,8 @@ 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">> @@ -47,7 +49,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[]]` 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! diff --git a/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix (Examples).tid b/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix (Examples).tid index fde0a1557..970bcc434 100644 --- a/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix (Examples).tid +++ b/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix (Examples).tid @@ -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] :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]` 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)]] \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/syntax/Sort Filter Run Prefix (Examples).tid b/editions/tw5.com/tiddlers/filters/syntax/Sort Filter Run Prefix (Examples).tid new file mode 100644 index 000000000..73c95643e --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/syntax/Sort Filter Run Prefix (Examples).tid @@ -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}]">> \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/syntax/Sort Filter Run Prefix.tid b/editions/tw5.com/tiddlers/filters/syntax/Sort Filter Run Prefix.tid new file mode 100644 index 000000000..2fca72716 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/syntax/Sort Filter Run Prefix.tid @@ -0,0 +1,32 @@ +created: 20210428083929749 +modified: 20210428140713422 +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 `:sort` filter run prefix uses an extended syntax that allows for multiple suffixes, some of which are required: + +``` +:sort::[...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)]] \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/variance Operator.tid b/editions/tw5.com/tiddlers/filters/variance Operator.tid new file mode 100644 index 000000000..913eb2943 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/variance Operator.tid @@ -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">> diff --git a/editions/tw5.com/tiddlers/hiddensettings/Hidden Setting HtmlParserDisableSandbox.tid b/editions/tw5.com/tiddlers/hiddensettings/Hidden Setting HtmlParserDisableSandbox.tid new file mode 100644 index 000000000..e17e7998c --- /dev/null +++ b/editions/tw5.com/tiddlers/hiddensettings/Hidden Setting HtmlParserDisableSandbox.tid @@ -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 `