From 2d7ef9ae1430485eb2ccaf5df11c51a06b7604a8 Mon Sep 17 00:00:00 2001 From: FSpark <stardust@fspark.me> Date: Mon, 21 Oct 2024 22:45:04 +0800 Subject: [PATCH] upgrade: tw core 5.3.5 --- package.json | 2 +- pnpm-lock.yaml | 11 +- src/override/system/boot/boot.js | 5384 +++++++++-------- .../tiddlywiki/tiddlyweb/tiddlywebadaptor.js | 4 +- 4 files changed, 2718 insertions(+), 2683 deletions(-) diff --git a/package.json b/package.json index 7c55bd5..2768b23 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "FSpark", "license": "MIT", "dependencies": { - "tiddlywiki": "^5.3.3" + "tiddlywiki": "^5.3.5" }, "devDependencies": { "@babel/preset-env": "^7.21.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e865ece..5e1e18a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: tiddlywiki: - specifier: ^5.3.3 - version: 5.3.3 + specifier: ^5.3.5 + version: 5.3.5 devDependencies: '@babel/preset-env': @@ -2209,7 +2209,7 @@ packages: resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} engines: {node: '>= 4.0'} os: [darwin] - deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2 + deprecated: Upgrade to fsevents v2 to mitigate potential security issues requiresBuild: true dependencies: bindings: 1.5.0 @@ -3530,12 +3530,11 @@ packages: xtend: 4.0.2 dev: true - /tiddlywiki@5.3.3: - resolution: {integrity: sha512-PkgVfZNpFFHyMmfFw91igXOJn8Z7IWg3NGXOX5EBqJwzGNeYYOIUg4FqCNsWoqBece20HxtkDue/vTf2jDtdZQ==} + /tiddlywiki@5.3.5: + resolution: {integrity: sha512-8pTmnQdkcHbol9D86Op7OGK4sGDqm19HWT2qgpSxPHfDG0yJ2rSBUTRuOMuh9GoPP0Tcz9+1Pe8A1m6pvd/zYQ==} engines: {node: '>=0.8.2'} hasBin: true dev: false - bundledDependencies: [] /time-stamp@1.1.0: resolution: {integrity: sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==} diff --git a/src/override/system/boot/boot.js b/src/override/system/boot/boot.js index e9a9737..d514cea 100644 --- a/src/override/system/boot/boot.js +++ b/src/override/system/boot/boot.js @@ -10,1307 +10,1369 @@ On the server this file is executed directly to boot TiddlyWiki. In the browser, var _boot = (function($tw) { -/*jslint node: true, browser: true */ -/*global modules: false, $tw: false */ -"use strict"; - -// Include bootprefix if we're not given module data -if(!$tw) { - $tw = require("./bootprefix.js").bootprefix(); -} - -$tw.utils = $tw.utils || Object.create(null); - -/////////////////////////// Standard node.js libraries - -var fs, path, vm; -if($tw.node) { - fs = require("fs"); - path = require("path"); - vm = require("vm"); -} - -/////////////////////////// Utility functions - -$tw.boot.log = function(str) { - $tw.boot.logMessages = $tw.boot.logMessages || []; - $tw.boot.logMessages.push(str); -} - -/* -Check if an object has a property -*/ -$tw.utils.hop = function(object,property) { - return object ? Object.prototype.hasOwnProperty.call(object,property) : false; -}; - -/* -Determine if a value is an array -*/ -$tw.utils.isArray = function(value) { - return Object.prototype.toString.call(value) == "[object Array]"; -}; - -/* -Check if an array is equal by value and by reference. -*/ -$tw.utils.isArrayEqual = function(array1,array2) { - if(array1 === array2) { - return true; - } - array1 = array1 || []; - array2 = array2 || []; - if(array1.length !== array2.length) { - return false; - } - return array1.every(function(value,index) { - return value === array2[index]; - }); -}; - -/* -Add an entry to a sorted array if it doesn't already exist, while maintaining the sort order -*/ -$tw.utils.insertSortedArray = function(array,value) { - var low = 0, high = array.length - 1, mid, cmp; - while(low <= high) { - mid = (low + high) >> 1; - cmp = value.localeCompare(array[mid]); - if(cmp > 0) { - low = mid + 1; - } else if(cmp < 0) { - high = mid - 1; - } else { - return array; + /*jslint node: true, browser: true */ + /*global modules: false, $tw: false */ + "use strict"; + + // Include bootprefix if we're not given module data + if(!$tw) { + $tw = require("./bootprefix.js").bootprefix(); + } + + $tw.utils = $tw.utils || Object.create(null); + + /////////////////////////// Standard node.js libraries + + var fs, path, vm; + if($tw.node) { + fs = require("fs"); + path = require("path"); + vm = require("vm"); + } + + /////////////////////////// Utility functions + + $tw.boot.log = function(str) { + $tw.boot.logMessages = $tw.boot.logMessages || []; + $tw.boot.logMessages.push(str); + } + + /* + Check if an object has a property + */ + $tw.utils.hop = function(object,property) { + return object ? Object.prototype.hasOwnProperty.call(object,property) : false; + }; + + /* + Determine if a value is an array + */ + $tw.utils.isArray = function(value) { + return Object.prototype.toString.call(value) == "[object Array]"; + }; + + /* + Check if an array is equal by value and by reference. + */ + $tw.utils.isArrayEqual = function(array1,array2) { + if(array1 === array2) { + return true; } - } - array.splice(low,0,value); - return array; -}; - -/* -Push entries onto an array, removing them first if they already exist in the array - array: array to modify (assumed to be free of duplicates) - value: a single value to push or an array of values to push -*/ -$tw.utils.pushTop = function(array,value) { - var t,p; - if($tw.utils.isArray(value)) { - // Remove any array entries that are duplicated in the new values - if(value.length !== 0) { - if(array.length !== 0) { - if(value.length < array.length) { - for(t=0; t<value.length; t++) { - p = array.indexOf(value[t]); - if(p !== -1) { - array.splice(p,1); + array1 = array1 || []; + array2 = array2 || []; + if(array1.length !== array2.length) { + return false; + } + return array1.every(function(value,index) { + return value === array2[index]; + }); + }; + + /* + Add an entry to a sorted array if it doesn't already exist, while maintaining the sort order + */ + $tw.utils.insertSortedArray = function(array,value) { + var low = 0, high = array.length - 1, mid, cmp; + while(low <= high) { + mid = (low + high) >> 1; + cmp = value.localeCompare(array[mid]); + if(cmp > 0) { + low = mid + 1; + } else if(cmp < 0) { + high = mid - 1; + } else { + return array; + } + } + array.splice(low,0,value); + return array; + }; + + /* + Push entries onto an array, removing them first if they already exist in the array + array: array to modify (assumed to be free of duplicates) + value: a single value to push or an array of values to push + */ + $tw.utils.pushTop = function(array,value) { + var t,p; + if($tw.utils.isArray(value)) { + // Remove any array entries that are duplicated in the new values + if(value.length !== 0) { + if(array.length !== 0) { + if(value.length < array.length) { + for(t=0; t<value.length; t++) { + p = array.indexOf(value[t]); + if(p !== -1) { + array.splice(p,1); + } } - } - } else { - for(t=array.length-1; t>=0; t--) { - p = value.indexOf(array[t]); - if(p !== -1) { - array.splice(t,1); + } else { + for(t=array.length-1; t>=0; t--) { + p = value.indexOf(array[t]); + if(p !== -1) { + array.splice(t,1); + } } } } + // Push the values on top of the main array + array.push.apply(array,value); } - // Push the values on top of the main array - array.push.apply(array,value); - } - } else { - p = array.indexOf(value); - if(p !== -1) { - array.splice(p,1); - } - array.push(value); - } - return array; -}; - -/* -Determine if a value is a date -*/ -$tw.utils.isDate = function(value) { - return Object.prototype.toString.call(value) === "[object Date]"; -}; - -/* -Iterate through all the own properties of an object or array. Callback is invoked with (element,title,object) -*/ -$tw.utils.each = function(object,callback) { - var next,f,length; - if(object) { - if(Object.prototype.toString.call(object) == "[object Array]") { - for (f=0, length=object.length; f<length; f++) { - next = callback(object[f],f,object); - if(next === false) { - break; - } - } } else { - var keys = Object.keys(object); - for (f=0, length=keys.length; f<length; f++) { - var key = keys[f]; - next = callback(object[key],key,object); - if(next === false) { - break; - } + p = array.indexOf(value); + if(p !== -1) { + array.splice(p,1); } + array.push(value); } - } -}; - -/* -Helper for making DOM elements -tag: tag name -options: see below -Options include: -namespace: defaults to http://www.w3.org/1999/xhtml -attributes: hashmap of attribute values -style: hashmap of styles -text: text to add as a child node -children: array of further child nodes -innerHTML: optional HTML for element -class: class name(s) -document: defaults to current document -eventListeners: array of event listeners (this option won't work until $tw.utils.addEventListeners() has been loaded) -*/ -$tw.utils.domMaker = function(tag,options) { - var doc = options.document || document; - var element = doc.createElementNS(options.namespace || "http://www.w3.org/1999/xhtml",tag); - if(options["class"]) { - element.className = options["class"]; - } - if(options.text) { - element.appendChild(doc.createTextNode(options.text)); - } - $tw.utils.each(options.children,function(child) { - element.appendChild(child); - }); - if(options.innerHTML) { - element.innerHTML = options.innerHTML; - } - $tw.utils.each(options.attributes,function(attribute,name) { - element.setAttribute(name,attribute); - }); - $tw.utils.each(options.style,function(value,name) { - element.style[name] = value; - }); - if(options.eventListeners) { - $tw.utils.addEventListeners(element,options.eventListeners); - } - return element; -}; - -/* -Display an error and exit -*/ -$tw.utils.error = function(err) { - // Prepare the error message - var errHeading = ( $tw.language == undefined ? "Internal JavaScript Error" : $tw.language.getString("InternalJavaScriptError/Title") ), - promptMsg = ( $tw.language == undefined ? "Well, this is embarrassing. It is recommended that you restart TiddlyWiki by refreshing your browser" : $tw.language.getString("InternalJavaScriptError/Hint") ); - // Log the error to the console - console.error($tw.node ? "\x1b[1;31m" + err + "\x1b[0m" : err); - if($tw.browser && !$tw.node) { - // Display an error message to the user - var dm = $tw.utils.domMaker, - heading = dm("h1",{text: errHeading}), - prompt = dm("div",{text: promptMsg, "class": "tc-error-prompt"}), - message = dm("div",{text: err, "class":"tc-error-message"}), - button = dm("div",{children: [dm("button",{text: ( $tw.language == undefined ? "close" : $tw.language.getString("Buttons/Close/Caption") )})], "class": "tc-error-prompt"}), - form = dm("form",{children: [heading,prompt,message,button], "class": "tc-error-form"}); - document.body.insertBefore(form,document.body.firstChild); - form.addEventListener("submit",function(event) { - document.body.removeChild(form); - event.preventDefault(); - return false; - },true); - return null; - } else if(!$tw.browser) { - // Exit if we're under node.js - process.exit(1); - } -}; - -/* -Use our custom error handler if we're in the browser -*/ -if($tw.boot.tasks.trapErrors) { - window.onerror = function(errorMsg,url,lineNumber) { - $tw.utils.error(errorMsg); - return false; + return array; }; -} - -/* -Extend an object with the properties from a list of source objects -*/ -$tw.utils.extend = function(object /*, sourceObjectList */) { - $tw.utils.each(Array.prototype.slice.call(arguments,1),function(source) { - if(source) { - for (var p in source) { - object[p] = source[p]; - } - } - }); - return object; -}; - -/* -Fill in any null or undefined properties of an object with the properties from a list of source objects. Each property that is an object is called recursively -*/ -$tw.utils.deepDefaults = function(object /*, sourceObjectList */) { - $tw.utils.each(Array.prototype.slice.call(arguments,1),function(source) { - if(source) { - for (var p in source) { - if(object[p] === null || object[p] === undefined) { - object[p] = source[p]; + + /* + Determine if a value is a date + */ + $tw.utils.isDate = function(value) { + return Object.prototype.toString.call(value) === "[object Date]"; + }; + + /* + Iterate through all the own properties of an object or array. Callback is invoked with (element,title,object) + */ + $tw.utils.each = function(object,callback) { + var next,f,length; + if(object) { + if(Object.prototype.toString.call(object) == "[object Array]") { + for(f=0, length=object.length; f<length; f++) { + next = callback(object[f],f,object); + if(next === false) { + break; + } } - if(typeof object[p] === "object" && typeof source[p] === "object") { - $tw.utils.deepDefaults(object[p],source[p]); + } else { + var keys = Object.keys(object); + for(f=0, length=keys.length; f<length; f++) { + var key = keys[f]; + next = callback(object[key],key,object); + if(next === false) { + break; + } } } } - }); - return object; -}; - -/* -Convert a URIComponent encoded string to a string safely -*/ -$tw.utils.decodeURIComponentSafe = function(s) { - var v = s; - try { - v = decodeURIComponent(s); - } catch(e) {} - return v; -}; - -/* -Convert a URI encoded string to a string safely -*/ -$tw.utils.decodeURISafe = function(s) { - var v = s; - try { - v = decodeURI(s); - } catch(e) {} - return v; -}; - -/* -Convert "&" to &, " " to nbsp, "<" to <, ">" to > and """ to " -*/ -$tw.utils.htmlDecode = function(s) { - return s.toString().replace(/</mg,"<").replace(/ /mg,"\xA0").replace(/>/mg,">").replace(/"/mg,"\"").replace(/&/mg,"&"); -}; - -/* -Get the browser location.hash. We don't use location.hash because of the way that Firefox auto-urldecodes it (see http://stackoverflow.com/questions/1703552/encoding-of-window-location-hash) -*/ -$tw.utils.getLocationHash = function() { - var href = window.location.href; - var idx = href.indexOf('#'); - if(idx === -1) { - return "#"; - } else if(href.substr(idx + 1,1) === "#" || href.substr(idx + 1,3) === "%23") { - // Special case: ignore location hash if it itself starts with a # - return "#"; - } else { - return href.substring(idx); - } -}; - -/* -Pad a string to a given length with "0"s. Length defaults to 2 -*/ -$tw.utils.pad = function(value,length) { - length = length || 2; - var s = value.toString(); - if(s.length < length) { - s = "000000000000000000000000000".substr(0,length - s.length) + s; - } - return s; -}; - -// Convert a date into UTC YYYYMMDDHHMMSSmmm format -$tw.utils.stringifyDate = function(value) { - return value.getUTCFullYear() + - $tw.utils.pad(value.getUTCMonth() + 1) + - $tw.utils.pad(value.getUTCDate()) + - $tw.utils.pad(value.getUTCHours()) + - $tw.utils.pad(value.getUTCMinutes()) + - $tw.utils.pad(value.getUTCSeconds()) + - $tw.utils.pad(value.getUTCMilliseconds(),3); -}; - -// Parse a date from a UTC YYYYMMDDHHMMSSmmm format string -$tw.utils.parseDate = function(value) { - if(typeof value === "string") { - var negative = 1; - if(value.charAt(0) === "-") { - negative = -1; - value = value.substr(1); - } - var year = parseInt(value.substr(0,4),10) * negative, - d = new Date(Date.UTC(year, - parseInt(value.substr(4,2),10)-1, - parseInt(value.substr(6,2),10), - parseInt(value.substr(8,2)||"00",10), - parseInt(value.substr(10,2)||"00",10), - parseInt(value.substr(12,2)||"00",10), - parseInt(value.substr(14,3)||"000",10))); - d.setUTCFullYear(year); // See https://stackoverflow.com/a/5870822 - return d; - } else if($tw.utils.isDate(value)) { - return value; - } else { - return null; - } -}; - -// Stringify an array of tiddler titles into a list string -$tw.utils.stringifyList = function(value) { - if($tw.utils.isArray(value)) { - var result = new Array(value.length); - for(var t=0, l=value.length; t<l; t++) { - var entry = value[t] || ""; - if(entry.match(/[^\S\xA0]/mg)) { - result[t] = "[[" + entry + "]]"; - } else { - result[t] = entry; - } + }; + + /* + Helper for making DOM elements + tag: tag name + options: see below + Options include: + namespace: defaults to http://www.w3.org/1999/xhtml + attributes: hashmap of attribute values + style: hashmap of styles + text: text to add as a child node + children: array of further child nodes + innerHTML: optional HTML for element + class: class name(s) + document: defaults to current document + eventListeners: array of event listeners (this option won't work until $tw.utils.addEventListeners() has been loaded) + */ + $tw.utils.domMaker = function(tag,options) { + var options = options || {}; + var doc = options.document || document; + var element = doc.createElementNS(options.namespace || "http://www.w3.org/1999/xhtml",tag); + if(options["class"]) { + element.className = options["class"]; + } + if(options.text) { + element.appendChild(doc.createTextNode(options.text)); + } + $tw.utils.each(options.children,function(child) { + element.appendChild(child); + }); + if(options.innerHTML) { + element.innerHTML = options.innerHTML; } - return result.join(" "); - } else { - return value || ""; + $tw.utils.each(options.attributes,function(attribute,name) { + element.setAttribute(name,attribute); + }); + $tw.utils.each(options.style,function(value,name) { + element.style[name] = value; + }); + if(options.eventListeners) { + $tw.utils.addEventListeners(element,options.eventListeners); + } + return element; + }; + + /* + Display an error and exit + */ + $tw.utils.error = function(err) { + // Prepare the error message + var errHeading = ( $tw.language == undefined ? "Internal JavaScript Error" : $tw.language.getString("InternalJavaScriptError/Title") ), + promptMsg = ( $tw.language == undefined ? "Well, this is embarrassing. It is recommended that you restart TiddlyWiki by refreshing your browser" : $tw.language.getString("InternalJavaScriptError/Hint") ); + // Log the error to the console + console.error($tw.node ? "\x1b[1;31m" + err + "\x1b[0m" : err); + if($tw.browser && !$tw.node) { + // Display an error message to the user + var dm = $tw.utils.domMaker, + heading = dm("h1",{text: errHeading}), + prompt = dm("div",{text: promptMsg, "class": "tc-error-prompt"}), + message = dm("div",{text: err, "class":"tc-error-message"}), + closeButton = dm("div",{children: [dm("button",{text: ( $tw.language == undefined ? "close" : $tw.language.getString("Buttons/Close/Caption") )})], "class": "tc-error-prompt"}), + downloadButton = dm("div",{children: [dm("button",{text: ( $tw.language == undefined ? "download tiddlers" : $tw.language.getString("Buttons/EmergencyDownload/Caption") )})], "class": "tc-error-prompt"}), + form = dm("form",{children: [heading,prompt,downloadButton,message,closeButton], "class": "tc-error-form"}); + document.body.insertBefore(form,document.body.firstChild); + downloadButton.addEventListener("click",function(event) { + if($tw && $tw.wiki) { + var tiddlers = []; + $tw.wiki.each(function(tiddler,title) { + tiddlers.push(tiddler.fields); + }); + var link = dm("a"), + text = JSON.stringify(tiddlers); + if(Blob !== undefined) { + var blob = new Blob([text], {type: "text/html"}); + link.setAttribute("href", URL.createObjectURL(blob)); + } else { + link.setAttribute("href","data:text/html," + encodeURIComponent(text)); + } + link.setAttribute("download","emergency-tiddlers-" + (new Date()) + ".json"); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } else { + alert("Emergency tiddler download is not available"); + } + event.preventDefault(); + return false; + },true); + form.addEventListener("submit",function(event) { + document.body.removeChild(form); + event.preventDefault(); + return false; + },true); + return null; + } else if(!$tw.browser) { + // Exit if we're under node.js + process.exit(1); + } + }; + + /* + Use our custom error handler if we're in the browser + */ + if($tw.boot.tasks.trapErrors) { + window.onerror = function(errorMsg,url,lineNumber) { + $tw.utils.error(errorMsg); + return false; + }; } -}; - -// Parse a string array from a bracketted list. For example "OneTiddler [[Another Tiddler]] LastOne" -$tw.utils.parseStringArray = function(value, allowDuplicate) { - if(typeof value === "string") { - var memberRegExp = /(?:^|[^\S\xA0])(?:\[\[(.*?)\]\])(?=[^\S\xA0]|$)|([\S\xA0]+)/mg, - results = [], names = {}, - match; - do { - match = memberRegExp.exec(value); - if(match) { - var item = match[1] || match[2]; - if(item !== undefined && (!$tw.utils.hop(names,item) || allowDuplicate)) { - results.push(item); - names[item] = true; + + /* + Extend an object with the properties from a list of source objects + */ + $tw.utils.extend = function(object /*, sourceObjectList */) { + $tw.utils.each(Array.prototype.slice.call(arguments,1),function(source) { + if(source) { + for(var p in source) { + object[p] = source[p]; } } - } while(match); - return results; - } else if($tw.utils.isArray(value)) { - return value; - } else { - return null; - } -}; - -// Parse a block of name:value fields. The `fields` object is used as the basis for the return value -$tw.utils.parseFields = function(text,fields) { - fields = fields || Object.create(null); - text.split(/\r?\n/mg).forEach(function(line) { - if(line.charAt(0) !== "#") { - var p = line.indexOf(":"); - if(p !== -1) { - var field = line.substr(0, p).trim(), - value = line.substr(p+1).trim(); - if(field) { - fields[field] = value; + }); + return object; + }; + + /* + Fill in any null or undefined properties of an object with the properties from a list of source objects. Each property that is an object is called recursively + */ + $tw.utils.deepDefaults = function(object /*, sourceObjectList */) { + $tw.utils.each(Array.prototype.slice.call(arguments,1),function(source) { + if(source) { + for(var p in source) { + if(object[p] === null || object[p] === undefined) { + object[p] = source[p]; + } + if(typeof object[p] === "object" && typeof source[p] === "object") { + $tw.utils.deepDefaults(object[p],source[p]); + } } } + }); + return object; + }; + + /* + Convert a URIComponent encoded string to a string safely + */ + $tw.utils.decodeURIComponentSafe = function(s) { + var v = s; + try { + v = decodeURIComponent(s); + } catch(e) {} + return v; + }; + + /* + Convert a URI encoded string to a string safely + */ + $tw.utils.decodeURISafe = function(s) { + var v = s; + try { + v = decodeURI(s); + } catch(e) {} + return v; + }; + + /* + Convert "&" to &, " " to nbsp, "<" to <, ">" to > and """ to " + */ + $tw.utils.htmlDecode = function(s) { + return s.toString().replace(/</mg,"<").replace(/ /mg,"\xA0").replace(/>/mg,">").replace(/"/mg,"\"").replace(/&/mg,"&"); + }; + + /* + Get the browser location.hash. We don't use location.hash because of the way that Firefox auto-urldecodes it (see http://stackoverflow.com/questions/1703552/encoding-of-window-location-hash) + */ + $tw.utils.getLocationHash = function() { + var href = window.location.href; + var idx = href.indexOf('#'); + if(idx === -1) { + return "#"; + } else if(href.substr(idx + 1,1) === "#" || href.substr(idx + 1,3) === "%23") { + // Special case: ignore location hash if it itself starts with a # + return "#"; + } else { + return href.substring(idx); } - }); - return fields; -}; - -// Safely parse a string as JSON -$tw.utils.parseJSONSafe = function(text,defaultJSON) { - try { - return JSON.parse(text); - } catch(e) { - if(typeof defaultJSON === "function") { - return defaultJSON(e); + }; + + /* + Pad a string to a given length with "0"s. Length defaults to 2 + */ + $tw.utils.pad = function(value,length) { + length = length || 2; + var s = value.toString(); + if(s.length < length) { + s = "000000000000000000000000000".substr(0,length - s.length) + s; + } + return s; + }; + + // Convert a date into UTC YYYYMMDDHHMMSSmmm format + $tw.utils.stringifyDate = function(value) { + return value.getUTCFullYear() + + $tw.utils.pad(value.getUTCMonth() + 1) + + $tw.utils.pad(value.getUTCDate()) + + $tw.utils.pad(value.getUTCHours()) + + $tw.utils.pad(value.getUTCMinutes()) + + $tw.utils.pad(value.getUTCSeconds()) + + $tw.utils.pad(value.getUTCMilliseconds(),3); + }; + + // Parse a date from a UTC YYYYMMDDHHMMSSmmm format string + $tw.utils.parseDate = function(value) { + if(typeof value === "string") { + var negative = 1; + if(value.charAt(0) === "-") { + negative = -1; + value = value.substr(1); + } + var year = parseInt(value.substr(0,4),10) * negative, + d = new Date(Date.UTC(year, + parseInt(value.substr(4,2),10)-1, + parseInt(value.substr(6,2),10), + parseInt(value.substr(8,2)||"00",10), + parseInt(value.substr(10,2)||"00",10), + parseInt(value.substr(12,2)||"00",10), + parseInt(value.substr(14,3)||"000",10))); + d.setUTCFullYear(year); // See https://stackoverflow.com/a/5870822 + return d; + } else if($tw.utils.isDate(value)) { + return value; } else { - return defaultJSON || {}; + return null; } - } -}; - -/* -Resolves a source filepath delimited with `/` relative to a specified absolute root filepath. -In relative paths, the special folder name `..` refers to immediate parent directory, and the -name `.` refers to the current directory -*/ -$tw.utils.resolvePath = function(sourcepath,rootpath) { - // If the source path starts with ./ or ../ then it is relative to the root - if(sourcepath.substr(0,2) === "./" || sourcepath.substr(0,3) === "../" ) { - var src = sourcepath.split("/"), - root = rootpath.split("/"); - // Remove the filename part of the root - root.splice(root.length-1,1); - // Process the source path bit by bit onto the end of the root path - while(src.length > 0) { - var c = src.shift(); - if(c === "..") { // Slice off the last root entry for a double dot - if(root.length > 0) { - root.splice(root.length-1,1); - } - } else if(c !== ".") { // Ignore dots - root.push(c); // Copy other elements across - } - } - return root.join("/"); - } else { - // If it isn't relative, just return the path - if(rootpath) { - var root = rootpath.split("/"); - // Remove the filename part of the root - root.splice(root.length - 1, 1); - return root.join("/") + "/" + sourcepath; + }; + + // Stringify an array of tiddler titles into a list string + $tw.utils.stringifyList = function(value) { + if($tw.utils.isArray(value)) { + var result = new Array(value.length); + for(var t=0, l=value.length; t<l; t++) { + var entry = value[t] || ""; + if(entry.match(/[^\S\xA0]/mg)) { + result[t] = "[[" + entry + "]]"; + } else { + result[t] = entry; + } + } + return result.join(" "); } else { - return sourcepath; + return value || ""; } - } -}; - -/* -Parse a semantic version string into its constituent parts -- see https://semver.org -*/ -$tw.utils.parseVersion = function(version) { - var match = /^v?((\d+)\.(\d+)\.(\d+))(?:-([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?(?:\+([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?$/.exec(version); - if(match) { - return { - version: match[1], - major: parseInt(match[2],10), - minor: parseInt(match[3],10), - patch: parseInt(match[4],10), - prerelease: match[5], - build: match[6] - }; - } else { - return null; - } -}; - -/* -Returns +1 if the version string A is greater than the version string B, 0 if they are the same, and +1 if B is greater than A. -Missing or malformed version strings are parsed as 0.0.0 -*/ -$tw.utils.compareVersions = function(versionStringA,versionStringB) { - var defaultVersion = { - major: 0, - minor: 0, - patch: 0 - }, - versionA = $tw.utils.parseVersion(versionStringA) || defaultVersion, - versionB = $tw.utils.parseVersion(versionStringB) || defaultVersion, - diff = [ - versionA.major - versionB.major, - versionA.minor - versionB.minor, - versionA.patch - versionB.patch - ]; - if((diff[0] > 0) || (diff[0] === 0 && diff[1] > 0) || (diff[0] === 0 & diff[1] === 0 & diff[2] > 0)) { - return +1; - } else if((diff[0] < 0) || (diff[0] === 0 && diff[1] < 0) || (diff[0] === 0 & diff[1] === 0 & diff[2] < 0)) { - return -1; - } else { - return 0; - } -}; - -/* -Returns true if the version string A is greater than the version string B. Returns true if the versions are the same -*/ -$tw.utils.checkVersions = function(versionStringA,versionStringB) { - return $tw.utils.compareVersions(versionStringA,versionStringB) !== -1; -}; - -/* -Register file type information -options: {flags: flags,deserializerType: deserializerType} - flags:"image" for image types - deserializerType: defaults to type if not specified -*/ -$tw.utils.registerFileType = function(type,encoding,extension,options) { - options = options || {}; - if($tw.utils.isArray(extension)) { - $tw.utils.each(extension,function(extension) { - $tw.config.fileExtensionInfo[extension] = {type: type}; - }); - extension = extension[0]; - } else { - $tw.config.fileExtensionInfo[extension] = {type: type}; - } - $tw.config.contentTypeInfo[type] = {encoding: encoding, extension: extension, flags: options.flags || [], deserializerType: options.deserializerType || type}; -}; - -/* -Given an extension, always access the $tw.config.fileExtensionInfo -using a lowercase extension only. -*/ -$tw.utils.getFileExtensionInfo = function(ext) { - return ext ? $tw.config.fileExtensionInfo[ext.toLowerCase()] : null; -} - -/* -Given an extension, get the correct encoding for that file. -defaults to utf8 -*/ -$tw.utils.getTypeEncoding = function(ext) { - var extensionInfo = $tw.utils.getFileExtensionInfo(ext), - type = extensionInfo ? extensionInfo.type : null, - typeInfo = type ? $tw.config.contentTypeInfo[type] : null; - return typeInfo ? typeInfo.encoding : "utf8"; -}; - -var globalCheck =[ - " Object.defineProperty(Object.prototype, '__temp__', {", - " get: function () { return this; },", - " configurable: true", - " });", - " if(Object.keys(__temp__).length){", - " console.log(\"Warning: Global assignment detected\",Object.keys(__temp__));", - " delete Object.prototype.__temp__;", - " }", - " delete Object.prototype.__temp__;", -].join('\n'); - -/* -Run code globally with specified context variables in scope -*/ -$tw.utils.evalGlobal = function(code,context,filename,sandbox,allowGlobals) { - var contextCopy = $tw.utils.extend(Object.create(null),context); - // Get the context variables as a pair of arrays of names and values - var contextNames = [], contextValues = []; - $tw.utils.each(contextCopy,function(value,name) { - contextNames.push(name); - contextValues.push(value); - }); - // Add the code prologue and epilogue - code = [ - "(function(" + contextNames.join(",") + ") {", - " (function(){" + code + "\n;})();\n", - (!$tw.browser && sandbox && !allowGlobals) ? globalCheck : "", - "\nreturn exports;\n", - "})" - ].join(""); - - // Compile the code into a function - var fn; - if($tw.browser) { - fn = window["eval"](code + "\n\n//# sourceURL=" + filename); - } else { - if(sandbox){ - fn = vm.runInContext(code,sandbox,filename) + }; + + // Parse a string array from a bracketted list. For example "OneTiddler [[Another Tiddler]] LastOne" + $tw.utils.parseStringArray = function(value, allowDuplicate) { + if(typeof value === "string") { + var memberRegExp = /(?:^|[^\S\xA0])(?:\[\[(.*?)\]\])(?=[^\S\xA0]|$)|([\S\xA0]+)/mg, + results = [], names = {}, + match; + do { + match = memberRegExp.exec(value); + if(match) { + var item = match[1] || match[2]; + if(item !== undefined && (!$tw.utils.hop(names,item) || allowDuplicate)) { + results.push(item); + names[item] = true; + } + } + } while(match); + return results; + } else if($tw.utils.isArray(value)) { + return value; } else { - fn = vm.runInThisContext(code,filename); - } - } - // Call the function and return the exports - return fn.apply(null,contextValues); -}; -$tw.utils.sandbox = !$tw.browser ? vm.createContext({}) : undefined; -/* -Run code in a sandbox with only the specified context variables in scope -*/ -$tw.utils.evalSandboxed = $tw.browser ? $tw.utils.evalGlobal : function(code,context,filename,allowGlobals) { - return $tw.utils.evalGlobal( - code,context,filename, - allowGlobals ? vm.createContext({}) : $tw.utils.sandbox, - allowGlobals - ); -}; - -/* -Creates a PasswordPrompt object -*/ -$tw.utils.PasswordPrompt = function() { - // Store of pending password prompts - this.passwordPrompts = []; - // Create the wrapper - this.promptWrapper = $tw.utils.domMaker("div",{"class":"tc-password-wrapper"}); - document.body.appendChild(this.promptWrapper); - // Hide the empty wrapper - this.setWrapperDisplay(); -}; - -/* -Hides or shows the wrapper depending on whether there are any outstanding prompts -*/ -$tw.utils.PasswordPrompt.prototype.setWrapperDisplay = function() { - if(this.passwordPrompts.length) { - this.promptWrapper.style.display = "block"; - } else { - this.promptWrapper.style.display = "none"; - } -}; - -/* -Adds a new password prompt. Options are: -submitText: text to use for submit button (defaults to "Login") -serviceName: text of the human readable service name -noUserName: set true to disable username prompt -canCancel: set true to enable a cancel button (callback called with null) -repeatPassword: set true to prompt for the password twice -callback: function to be called on submission with parameter of object {username:,password:}. Callback must return `true` to remove the password prompt -*/ -$tw.utils.PasswordPrompt.prototype.createPrompt = function(options) { - // Create and add the prompt to the DOM - var self = this, - submitText = options.submitText || "Login", - dm = $tw.utils.domMaker, - children = [dm("h1",{text: options.serviceName})]; - if(!options.noUserName) { - children.push(dm("input",{ - attributes: {type: "text", name: "username", placeholder: $tw.language.getString("Encryption/Username")} - })); - } - children.push(dm("input",{ - attributes: { - type: "password", - name: "password", - placeholder: ( $tw.language == undefined ? "Password" : $tw.language.getString("Encryption/Password") ) - } - })); - if(options.repeatPassword) { - children.push(dm("input",{ - attributes: { - type: "password", - name: "password2", - placeholder: $tw.language.getString("Encryption/RepeatPassword") - } - })); - } - if(options.canCancel) { - children.push(dm("button",{ - text: $tw.language.getString("Encryption/Cancel"), - attributes: { - type: "button" - }, - eventListeners: [{ - name: "click", - handlerFunction: function(event) { - self.removePrompt(promptInfo); - options.callback(null); + return null; + } + }; + + // Parse a block of name:value fields. The `fields` object is used as the basis for the return value + $tw.utils.parseFields = function(text,fields) { + fields = fields || Object.create(null); + text.split(/\r?\n/mg).forEach(function(line) { + if(line.charAt(0) !== "#") { + var p = line.indexOf(":"); + if(p !== -1) { + var field = line.substr(0, p).trim(), + value = line.substr(p+1).trim(); + if(field) { + fields[field] = value; } - }] - })); - } - children.push(dm("button",{ - attributes: {type: "submit"}, - text: submitText - })); - var form = dm("form",{ - attributes: {autocomplete: "off"}, - children: children - }); - this.promptWrapper.appendChild(form); - window.setTimeout(function() { - form.elements[0].focus(); - },10); - // Add a submit event handler - var self = this; - form.addEventListener("submit",function(event) { - // Collect the form data - var data = {},t; - $tw.utils.each(form.elements,function(element) { - if(element.name && element.value) { - data[element.name] = element.value; + } } }); - // Check that the passwords match - if(options.repeatPassword && data.password !== data.password2) { - alert($tw.language.getString("Encryption/PasswordNoMatch")); - } else { - // Call the callback - if(options.callback(data)) { - // Remove the prompt if the callback returned true - self.removePrompt(promptInfo); + return fields; + }; + + // Safely parse a string as JSON + $tw.utils.parseJSONSafe = function(text,defaultJSON) { + try { + return JSON.parse(text); + } catch(e) { + if(typeof defaultJSON === "function") { + return defaultJSON(e); } else { - // Clear the password if the callback returned false - $tw.utils.each(form.elements,function(element) { - if(element.name === "password" || element.name === "password2") { - element.value = ""; - } - }); + return defaultJSON || {}; } } - event.preventDefault(); - return false; - },true); - // Add the prompt to the list - var promptInfo = { - serviceName: options.serviceName, - callback: options.callback, - form: form, - owner: this - }; - this.passwordPrompts.push(promptInfo); - // Make sure the wrapper is displayed - this.setWrapperDisplay(); - return promptInfo; -}; - -$tw.utils.PasswordPrompt.prototype.removePrompt = function(promptInfo) { - var i = this.passwordPrompts.indexOf(promptInfo); - if(i !== -1) { - this.passwordPrompts.splice(i,1); - promptInfo.form.parentNode.removeChild(promptInfo.form); - this.setWrapperDisplay(); - } -} - -/* -Crypto helper object for encrypted content. It maintains the password text in a closure, and provides methods to change -the password, and to encrypt/decrypt a block of text -*/ -$tw.utils.Crypto = function() { - var sjcl = $tw.node ? (global.sjcl || require("./sjcl.js")) : window.sjcl, - currentPassword = null, - callSjcl = function(method,inputText,password) { - password = password || currentPassword; - var outputText; - try { - if(password) { - outputText = sjcl[method](password,inputText); + }; + + /* + Resolves a source filepath delimited with `/` relative to a specified absolute root filepath. + In relative paths, the special folder name `..` refers to immediate parent directory, and the + name `.` refers to the current directory + */ + $tw.utils.resolvePath = function(sourcepath,rootpath) { + // If the source path starts with ./ or ../ then it is relative to the root + if(sourcepath.substr(0,2) === "./" || sourcepath.substr(0,3) === "../" ) { + var src = sourcepath.split("/"), + root = rootpath.split("/"); + // Remove the filename part of the root + root.splice(root.length-1,1); + // Process the source path bit by bit onto the end of the root path + while(src.length > 0) { + var c = src.shift(); + if(c === "..") { // Slice off the last root entry for a double dot + if(root.length > 0) { + root.splice(root.length-1,1); + } + } else if(c !== ".") { // Ignore dots + root.push(c); // Copy other elements across } - } catch(ex) { - console.log("Crypto error:" + ex); - outputText = null; } - return outputText; - }; - this.setPassword = function(newPassword) { - currentPassword = newPassword; - this.updateCryptoStateTiddler(); - }; - this.updateCryptoStateTiddler = function() { - if($tw.wiki) { - var state = currentPassword ? "yes" : "no", - tiddler = $tw.wiki.getTiddler("$:/isEncrypted"); - if(!tiddler || tiddler.fields.text !== state) { - $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/isEncrypted", text: state})); + return root.join("/"); + } else { + // If it isn't relative, just return the path + if(rootpath) { + var root = rootpath.split("/"); + // Remove the filename part of the root + root.splice(root.length - 1, 1); + return root.join("/") + "/" + sourcepath; + } else { + return sourcepath; } } }; - this.hasPassword = function() { - return !!currentPassword; - } - this.encrypt = function(text,password) { - return callSjcl("encrypt",text,password); - }; - this.decrypt = function(text,password) { - return callSjcl("decrypt",text,password); - }; -}; - -$tw.utils.CSE = function () { - var currentPassword = null; - this.setPassword = function(newPassword) { - currentPassword = newPassword; - if($tw.CSE.launched){ - var isRemembered = $tw.wiki.getTiddlerData("$:/plugins/FSpark/TW5-CSE/metaconfig.json") - if(isRemembered && isRemembered["RmbPwd"]==="yes"){ - this.rememberPassword(); - } + + /* + Parse a semantic version string into its constituent parts -- see https://semver.org + */ + $tw.utils.parseVersion = function(version) { + var match = /^v?((\d+)\.(\d+)\.(\d+))(?:-([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?(?:\+([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?$/.exec(version); + if(match) { + return { + version: match[1], + major: parseInt(match[2],10), + minor: parseInt(match[3],10), + patch: parseInt(match[4],10), + prerelease: match[5], + build: match[6] + }; + } else { + return null; } - this.updateCryptoStateTiddler(); }; - this.rememberPassword = function (){ - if(window && window.localStorage){ - window.localStorage.setItem("tw5-cse-pwd", currentPassword); + + /* + Returns +1 if the version string A is greater than the version string B, 0 if they are the same, and +1 if B is greater than A. + Missing or malformed version strings are parsed as 0.0.0 + */ + $tw.utils.compareVersions = function(versionStringA,versionStringB) { + var defaultVersion = { + major: 0, + minor: 0, + patch: 0 + }, + versionA = $tw.utils.parseVersion(versionStringA) || defaultVersion, + versionB = $tw.utils.parseVersion(versionStringB) || defaultVersion, + diff = [ + versionA.major - versionB.major, + versionA.minor - versionB.minor, + versionA.patch - versionB.patch + ]; + if((diff[0] > 0) || (diff[0] === 0 && diff[1] > 0) || (diff[0] === 0 & diff[1] === 0 & diff[2] > 0)) { + return +1; + } else if((diff[0] < 0) || (diff[0] === 0 && diff[1] < 0) || (diff[0] === 0 & diff[1] === 0 & diff[2] < 0)) { + return -1; + } else { + return 0; } }; - this.forgetPassword = function (){ - if(window && window.localStorage){ - window.localStorage.removeItem("tw5-cse-pwd"); - } + + /* + Returns true if the version string A is greater than the version string B. Returns true if the versions are the same + */ + $tw.utils.checkVersions = function(versionStringA,versionStringB) { + return $tw.utils.compareVersions(versionStringA,versionStringB) !== -1; }; - this.updateCryptoStateTiddler = function() { - if($tw.wiki) { - var state = currentPassword ? "yes" : "no", - tiddler = $tw.wiki.getTiddler("$:/isCSEncrypted"); - if(!tiddler || tiddler.fields.text !== state) { - $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/isCSEncrypted", text: state})); - } + + /* + Register file type information + options: {flags: flags,deserializerType: deserializerType} + flags:"image" for image types + deserializerType: defaults to type if not specified + */ + $tw.utils.registerFileType = function(type,encoding,extension,options) { + options = options || {}; + if($tw.utils.isArray(extension)) { + $tw.utils.each(extension,function(extension) { + $tw.config.fileExtensionInfo[extension] = {type: type}; + }); + extension = extension[0]; + } else { + $tw.config.fileExtensionInfo[extension] = {type: type}; } + $tw.config.contentTypeInfo[type] = {encoding: encoding, extension: extension, flags: options.flags || [], deserializerType: options.deserializerType || type}; }; - this.forcePush = function (filter, widget) { - debugger; - filter = filter || $tw.wiki.getTiddlerText('$:/config/TW5-CSE/EncryptFilter',"[all[]!is[system]]") - - widget = widget || $tw.rootWidget - - var filterTiddlers = $tw.wiki.filterTiddlers(filter, widget) - var filterTiddlersLength = filterTiddlers.length - - filterTiddlers.forEach(function (title) { - if($tw.utils.hop($tw.wiki.changeCount, title)) { - $tw.wiki.changeCount[title]++; + + /* + Given an extension, always access the $tw.config.fileExtensionInfo + using a lowercase extension only. + */ + $tw.utils.getFileExtensionInfo = function(ext) { + return ext ? $tw.config.fileExtensionInfo[ext.toLowerCase()] : null; + } + + /* + Given an extension, get the correct encoding for that file. + defaults to utf8 + */ + $tw.utils.getTypeEncoding = function(ext) { + var extensionInfo = $tw.utils.getFileExtensionInfo(ext), + type = extensionInfo ? extensionInfo.type : null, + typeInfo = type ? $tw.config.contentTypeInfo[type] : null; + return typeInfo ? typeInfo.encoding : "utf8"; + }; + + var globalCheck =[ + " Object.defineProperty(Object.prototype, '__temp__', {", + " get: function () { return this; },", + " configurable: true", + " });", + " if(Object.keys(__temp__).length){", + " console.log(\"Warning: Global assignment detected\",Object.keys(__temp__));", + " delete Object.prototype.__temp__;", + " }", + " delete Object.prototype.__temp__;", + ].join('\n'); + + /* + Run code globally with specified context variables in scope + */ + $tw.utils.evalGlobal = function(code,context,filename,sandbox,allowGlobals) { + var contextCopy = $tw.utils.extend(Object.create(null),context); + // Get the context variables as a pair of arrays of names and values + var contextNames = [], contextValues = []; + $tw.utils.each(contextCopy,function(value,name) { + contextNames.push(name); + contextValues.push(value); + }); + // Add the code prologue and epilogue + code = [ + "(function(" + contextNames.join(",") + ") {", + " (function(){" + code + "\n;})();\n", + (!$tw.browser && sandbox && !allowGlobals) ? globalCheck : "", + "\nreturn exports;\n", + "})" + ].join(""); + + // Compile the code into a function + var fn; + if($tw.browser) { + fn = window["eval"](code + "\n\n//# sourceURL=" + filename); + } else { + if(sandbox){ + fn = vm.runInContext(code,sandbox,filename) } else { - $tw.wiki.changeCount[title] = 1; + fn = vm.runInThisContext(code,filename); } - }) - - var id = $tw.wiki.getTiddlerText('$:/temp/CSE-IntervalID') - if(id) clearTimeout(parseInt(id)) - var self = this; - $tw.modal.display("$:/plugins/FSpark/TW5-CSE/ui/PushingModal") - var startTime = Date.now(); - id = setInterval(function() { - if($tw.syncer.isDirty()){ - // Filter out unsynced - filterTiddlers = filterTiddlers.filter(function (title) { - return !$tw.syncer.tiddlerInfo[title] || $tw.wiki.getChangeCount(title) > $tw.syncer.tiddlerInfo[title].changeCount - }) - var syncedTiddlers = filterTiddlersLength - filterTiddlers.length - var percentComplete = (syncedTiddlers * 100 / filterTiddlersLength).toFixed(2) + '%' - - var endTime = Date.now(); - var timeElapsed = (endTime - startTime) / 1000; - var syncSpeed = syncedTiddlers / timeElapsed; - var remainingTiddlersSize = filterTiddlers.length; - var remainingTime = remainingTiddlersSize / syncSpeed; - - var hours = Math.floor(remainingTime / 3600).toString().padStart(2, '0'); - var minutes = Math.floor((remainingTime % 3600) / 60).toString().padStart(2, '0'); - var seconds = Math.floor(remainingTime % 60).toString().padStart(2, '0'); - - $tw.wiki.addTiddler({ - title: "$:/temp/CSENumTasksInProgress", - text: `${syncedTiddlers}/${filterTiddlersLength} ${percentComplete}` }) - $tw.wiki.addTiddler({ - title: "$:/temp/CSESyncEstimatedTimeLeft", - text: `Estimated time left: ${hours}:${minutes}:${seconds}` }) - }else{ - // debugger; - $tw.wiki.addTiddler({title: "$:/state/cse-modal-close", text: "yes"}) - clearTimeout(id) - $tw.wiki.deleteTiddler("$:/temp/CSE-IntervalID") - $tw.wiki.deleteTiddler("$:/temp/CSENumTasksInProgress") - } - }, 500); - $tw.wiki.addTiddler({title: "$:/temp/CSE-IntervalID",text: id.toString()}) - } - this.saveTiddler = function (tiddler, fields) { - debugger; - $tw.wiki.addTiddler( - new $tw.Tiddler( - // $tw.wiki.getModificationFields(), - tiddler, - this.clearNonStandardFields(tiddler), - fields - ) + } + // Call the function and return the exports + return fn.apply(null,contextValues); + }; + $tw.utils.sandbox = !$tw.browser ? vm.createContext({}) : undefined; + /* + Run code in a sandbox with only the specified context variables in scope + */ + $tw.utils.evalSandboxed = $tw.browser ? $tw.utils.evalGlobal : function(code,context,filename,allowGlobals) { + return $tw.utils.evalGlobal( + code,context,filename, + allowGlobals ? vm.createContext({}) : $tw.utils.sandbox, + allowGlobals ); }; - this.encryptFields = function (title, password) { - password = password || currentPassword; - var jsonData = $tw.wiki.getTiddlerAsJson(title); - return $tw.crypto.encrypt(jsonData, password); + + /* + Creates a PasswordPrompt object + */ + $tw.utils.PasswordPrompt = function() { + // Store of pending password prompts + this.passwordPrompts = []; + // Create the wrapper + this.promptWrapper = $tw.utils.domMaker("div",{"class":"tc-password-wrapper"}); + document.body.appendChild(this.promptWrapper); + // Hide the empty wrapper + this.setWrapperDisplay(); }; - - this.decryptFields = function (fields, password) { - password = password || currentPassword; - var JSONfields = $tw.crypto.decrypt(fields.encrypted, password); - if(!!JSONfields) { - return JSON.parse(JSONfields); - } - console.log( - "Error decrypting " + fields.title + ". Probably bad password" - ); - return false; + + /* + Hides or shows the wrapper depending on whether there are any outstanding prompts + */ + $tw.utils.PasswordPrompt.prototype.setWrapperDisplay = function() { + if(this.passwordPrompts.length) { + this.promptWrapper.style.display = "block"; + } else { + this.promptWrapper.style.display = "none"; + } }; - this.clearNonStandardFields = function (tiddler) { - var standardFieldNames = - "title tags modified modifier created creator".split(" "); - var clearFields = {}; - for(var fieldName in tiddler.fields) { - if(standardFieldNames.indexOf(fieldName) === -1) { - clearFields[fieldName] = undefined; + + /* + Adds a new password prompt. Options are: + submitText: text to use for submit button (defaults to "Login") + serviceName: text of the human readable service name + noUserName: set true to disable username prompt + canCancel: set true to enable a cancel button (callback called with null) + repeatPassword: set true to prompt for the password twice + callback: function to be called on submission with parameter of object {username:,password:}. Callback must return `true` to remove the password prompt + */ + $tw.utils.PasswordPrompt.prototype.createPrompt = function(options) { + // Create and add the prompt to the DOM + var self = this, + submitText = options.submitText || "Login", + dm = $tw.utils.domMaker, + children = [dm("h1",{text: options.serviceName})]; + if(!options.noUserName) { + children.push(dm("input",{ + attributes: {type: "text", name: "username", placeholder: $tw.language.getString("Encryption/Username")} + })); + } + children.push(dm("input",{ + attributes: { + type: "password", + name: "password", + placeholder: ( $tw.language == undefined ? "Password" : $tw.language.getString("Encryption/Password") ) } + })); + if(options.repeatPassword) { + children.push(dm("input",{ + attributes: { + type: "password", + name: "password2", + placeholder: $tw.language.getString("Encryption/RepeatPassword") + } + })); + } + if(options.canCancel) { + children.push(dm("button",{ + text: $tw.language.getString("Encryption/Cancel"), + attributes: { + type: "button" + }, + eventListeners: [{ + name: "click", + handlerFunction: function(event) { + self.removePrompt(promptInfo); + options.callback(null); + } + }] + })); } - console.log("Cleared fields " + JSON.stringify(clearFields)); - return clearFields; + children.push(dm("button",{ + attributes: {type: "submit"}, + text: submitText + })); + var form = dm("form",{ + attributes: {autocomplete: "off"}, + children: children + }); + this.promptWrapper.appendChild(form); + window.setTimeout(function() { + form.elements[0].focus(); + },10); + // Add a submit event handler + var self = this; + form.addEventListener("submit",function(event) { + // Collect the form data + var data = {},t; + $tw.utils.each(form.elements,function(element) { + if(element.name && element.value) { + data[element.name] = element.value; + } + }); + // Check that the passwords match + if(options.repeatPassword && data.password !== data.password2) { + alert($tw.language.getString("Encryption/PasswordNoMatch")); + } else { + // Call the callback + if(options.callback(data)) { + // Remove the prompt if the callback returned true + self.removePrompt(promptInfo); + } else { + // Clear the password if the callback returned false + $tw.utils.each(form.elements,function(element) { + if(element.name === "password" || element.name === "password2") { + element.value = ""; + } + }); + } + } + event.preventDefault(); + return false; + },true); + // Add the prompt to the list + var promptInfo = { + serviceName: options.serviceName, + callback: options.callback, + form: form, + owner: this + }; + this.passwordPrompts.push(promptInfo); + // Make sure the wrapper is displayed + this.setWrapperDisplay(); + return promptInfo; }; -} -/////////////////////////// Module mechanism - -/* -Execute the module named 'moduleName'. The name can optionally be relative to the module named 'moduleRoot' -*/ -$tw.modules.execute = function(moduleName,moduleRoot) { - var name = moduleName; - if(moduleName.charAt(0) === ".") { - name = $tw.utils.resolvePath(moduleName,moduleRoot) - } - if(!$tw.modules.titles[name]) { - if($tw.modules.titles[name + ".js"]) { - name = name + ".js"; - } else if($tw.modules.titles[name + "/index.js"]) { - name = name + "/index.js"; - } else if($tw.modules.titles[moduleName]) { - name = moduleName; - } else if($tw.modules.titles[moduleName + ".js"]) { - name = moduleName + ".js"; - } else if($tw.modules.titles[moduleName + "/index.js"]) { - name = moduleName + "/index.js"; + + $tw.utils.PasswordPrompt.prototype.removePrompt = function(promptInfo) { + var i = this.passwordPrompts.indexOf(promptInfo); + if(i !== -1) { + this.passwordPrompts.splice(i,1); + promptInfo.form.parentNode.removeChild(promptInfo.form); + this.setWrapperDisplay(); + } + } + + /* + Crypto helper object for encrypted content. It maintains the password text in a closure, and provides methods to change + the password, and to encrypt/decrypt a block of text + */ + $tw.utils.Crypto = function() { + var sjcl = $tw.node ? (global.sjcl || require("./sjcl.js")) : window.sjcl, + currentPassword = null, + callSjcl = function(method,inputText,password) { + password = password || currentPassword; + var outputText; + try { + if(password) { + outputText = sjcl[method](password,inputText); + } + } catch(ex) { + console.log("Crypto error:" + ex); + outputText = null; + } + return outputText; + }; + $tw.sjcl = sjcl; + this.setPassword = function(newPassword) { + currentPassword = newPassword; + this.updateCryptoStateTiddler(); + }; + this.updateCryptoStateTiddler = function() { + if($tw.wiki) { + var state = currentPassword ? "yes" : "no", + tiddler = $tw.wiki.getTiddler("$:/isEncrypted"); + if(!tiddler || tiddler.fields.text !== state) { + $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/isEncrypted", text: state})); + } + } + }; + this.hasPassword = function() { + return !!currentPassword; } - } - var moduleInfo = $tw.modules.titles[name], - tiddler = $tw.wiki.getTiddler(name), - _exports = {}, - sandbox = { - module: {exports: _exports}, - //moduleInfo: moduleInfo, - exports: _exports, - console: console, - setInterval: setInterval, - clearInterval: clearInterval, - setTimeout: setTimeout, - clearTimeout: clearTimeout, - Buffer: $tw.browser ? undefined : Buffer, - $tw: $tw, - require: function(title) { - return $tw.modules.execute(title, name); + this.encrypt = function(text,password) { + return callSjcl("encrypt",text,password); + }; + this.decrypt = function(text,password) { + return callSjcl("decrypt",text,password); + }; + }; + + $tw.utils.CSE = function () { + var currentPassword = null; + this.setPassword = function(newPassword) { + currentPassword = newPassword; + if($tw.CSE.launched){ + var isRemembered = $tw.wiki.getTiddlerData("$:/plugins/FSpark/TW5-CSE/metaconfig.json") + if(isRemembered && isRemembered["RmbPwd"]==="yes"){ + this.rememberPassword(); + } } + this.updateCryptoStateTiddler(); }; - - Object.defineProperty(sandbox.module, "id", { - value: name, - writable: false, - enumerable: true, - configurable: false - }); - - if(!$tw.browser) { - $tw.utils.extend(sandbox,{ - process: process - }); - } else { - /* - CommonJS optional require.main property: - In a browser we offer a fake main module which points back to the boot function - (Theoretically, this may allow TW to eventually load itself as a module in the browser) - */ - Object.defineProperty(sandbox.require, "main", { - value: (typeof(require) !== "undefined") ? require.main : {TiddlyWiki: _boot}, + this.rememberPassword = function (){ + if(window && window.localStorage){ + window.localStorage.setItem("tw5-cse-pwd", currentPassword); + } + }; + this.forgetPassword = function (){ + if(window && window.localStorage){ + window.localStorage.removeItem("tw5-cse-pwd"); + } + }; + this.updateCryptoStateTiddler = function() { + if($tw.wiki) { + var state = currentPassword ? "yes" : "no", + tiddler = $tw.wiki.getTiddler("$:/isCSEncrypted"); + if(!tiddler || tiddler.fields.text !== state) { + $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/isCSEncrypted", text: state})); + } + } + }; + this.forcePush = function (filter, widget) { + debugger; + filter = filter || $tw.wiki.getTiddlerText('$:/config/TW5-CSE/EncryptFilter',"[all[]!is[system]]") + + widget = widget || $tw.rootWidget + + var filterTiddlers = $tw.wiki.filterTiddlers(filter, widget) + var filterTiddlersLength = filterTiddlers.length + + filterTiddlers.forEach(function (title) { + if($tw.utils.hop($tw.wiki.changeCount, title)) { + $tw.wiki.changeCount[title]++; + } else { + $tw.wiki.changeCount[title] = 1; + } + }) + + var id = $tw.wiki.getTiddlerText('$:/temp/CSE-IntervalID') + if(id) clearTimeout(parseInt(id)) + var self = this; + $tw.modal.display("$:/plugins/FSpark/TW5-CSE/ui/PushingModal") + var startTime = Date.now(); + id = setInterval(function() { + if($tw.syncer.isDirty()){ + // Filter out unsynced + filterTiddlers = filterTiddlers.filter(function (title) { + return !$tw.syncer.tiddlerInfo[title] || $tw.wiki.getChangeCount(title) > $tw.syncer.tiddlerInfo[title].changeCount + }) + var syncedTiddlers = filterTiddlersLength - filterTiddlers.length + var percentComplete = (syncedTiddlers * 100 / filterTiddlersLength).toFixed(2) + '%' + + var endTime = Date.now(); + var timeElapsed = (endTime - startTime) / 1000; + var syncSpeed = syncedTiddlers / timeElapsed; + var remainingTiddlersSize = filterTiddlers.length; + var remainingTime = remainingTiddlersSize / syncSpeed; + + var hours = Math.floor(remainingTime / 3600).toString().padStart(2, '0'); + var minutes = Math.floor((remainingTime % 3600) / 60).toString().padStart(2, '0'); + var seconds = Math.floor(remainingTime % 60).toString().padStart(2, '0'); + + $tw.wiki.addTiddler({ + title: "$:/temp/CSENumTasksInProgress", + text: `${syncedTiddlers}/${filterTiddlersLength} ${percentComplete}` }) + $tw.wiki.addTiddler({ + title: "$:/temp/CSESyncEstimatedTimeLeft", + text: `Estimated time left: ${hours}:${minutes}:${seconds}` }) + }else{ + // debugger; + $tw.wiki.addTiddler({title: "$:/state/cse-modal-close", text: "yes"}) + clearTimeout(id) + $tw.wiki.deleteTiddler("$:/temp/CSE-IntervalID") + $tw.wiki.deleteTiddler("$:/temp/CSENumTasksInProgress") + } + }, 500); + $tw.wiki.addTiddler({title: "$:/temp/CSE-IntervalID",text: id.toString()}) + } + this.saveTiddler = function (tiddler, fields) { + debugger; + $tw.wiki.addTiddler( + new $tw.Tiddler( + // $tw.wiki.getModificationFields(), + tiddler, + this.clearNonStandardFields(tiddler), + fields + ) + ); + }; + this.encryptFields = function (title, password) { + password = password || currentPassword; + var jsonData = $tw.wiki.getTiddlerAsJson(title); + return $tw.crypto.encrypt(jsonData, password); + }; + + this.decryptFields = function (fields, password) { + password = password || currentPassword; + var JSONfields = $tw.crypto.decrypt(fields.encrypted, password); + if(!!JSONfields) { + return JSON.parse(JSONfields); + } + console.log( + "Error decrypting " + fields.title + ". Probably bad password" + ); + return false; + }; + this.clearNonStandardFields = function (tiddler) { + var standardFieldNames = + "title tags modified modifier created creator".split(" "); + var clearFields = {}; + for(var fieldName in tiddler.fields) { + if(standardFieldNames.indexOf(fieldName) === -1) { + clearFields[fieldName] = undefined; + } + } + console.log("Cleared fields " + JSON.stringify(clearFields)); + return clearFields; + }; + } + /////////////////////////// Module mechanism + + /* + Execute the module named 'moduleName'. The name can optionally be relative to the module named 'moduleRoot' + */ + $tw.modules.execute = function(moduleName,moduleRoot) { + var name = moduleName; + if(moduleName.charAt(0) === ".") { + name = $tw.utils.resolvePath(moduleName,moduleRoot) + } + if(!$tw.modules.titles[name]) { + if($tw.modules.titles[name + ".js"]) { + name = name + ".js"; + } else if($tw.modules.titles[name + "/index.js"]) { + name = name + "/index.js"; + } else if($tw.modules.titles[moduleName]) { + name = moduleName; + } else if($tw.modules.titles[moduleName + ".js"]) { + name = moduleName + ".js"; + } else if($tw.modules.titles[moduleName + "/index.js"]) { + name = moduleName + "/index.js"; + } + } + var moduleInfo = $tw.modules.titles[name], + tiddler = $tw.wiki.getTiddler(name), + _exports = {}, + sandbox = { + module: {exports: _exports}, + //moduleInfo: moduleInfo, + exports: _exports, + console: console, + setInterval: setInterval, + clearInterval: clearInterval, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + Buffer: $tw.browser ? undefined : Buffer, + $tw: $tw, + require: function(title) { + return $tw.modules.execute(title, name); + } + }; + + Object.defineProperty(sandbox.module, "id", { + value: name, writable: false, enumerable: true, configurable: false }); - } - if(!moduleInfo) { - // We could not find the module on this path - // Try to defer to browserify etc, or node - var deferredModule; - if($tw.browser) { - if(window.require) { - try { - return window.require(moduleName); - } catch(e) {} - } - throw "Cannot find module named '" + moduleName + "' required by module '" + moduleRoot + "', resolved to " + name; + + if(!$tw.browser) { + $tw.utils.extend(sandbox,{ + process: process + }); } else { - // If we don't have a module with that name, let node.js try to find it - return require(moduleName); + /* + CommonJS optional require.main property: + In a browser we offer a fake main module which points back to the boot function + (Theoretically, this may allow TW to eventually load itself as a module in the browser) + */ + Object.defineProperty(sandbox.require, "main", { + value: (typeof(require) !== "undefined") ? require.main : {TiddlyWiki: _boot}, + writable: false, + enumerable: true, + configurable: false + }); } - } - // Execute the module if we haven't already done so - if(!moduleInfo.exports) { - try { - // Check the type of the definition - if(typeof moduleInfo.definition === "function") { // Function - moduleInfo.exports = _exports; - moduleInfo.definition(moduleInfo,moduleInfo.exports,sandbox.require); - } else if(typeof moduleInfo.definition === "string") { // String - moduleInfo.exports = _exports; - $tw.utils.evalSandboxed(moduleInfo.definition,sandbox,tiddler.fields.title); - if(sandbox.module.exports) { - moduleInfo.exports = sandbox.module.exports; //more codemirror workaround - } - } else { // Object - moduleInfo.exports = moduleInfo.definition; - } - } catch(e) { - if (e instanceof SyntaxError) { - var line = e.lineNumber || e.line; // Firefox || Safari - if (typeof(line) != "undefined" && line !== null) { - $tw.utils.error("Syntax error in boot module " + name + ":" + line + ":\n" + e.stack); - } else if(!$tw.browser) { - // this is the only way to get node.js to display the line at which the syntax error appeared, - // and $tw.utils.error would exit anyway - // cf. https://bugs.chromium.org/p/v8/issues/detail?id=2589 - throw e; - } else { - // Opera: line number is included in e.message - // Chrome/IE: there's currently no way to get the line number - $tw.utils.error("Syntax error in boot module " + name + ": " + e.message + "\n" + e.stack); + if(!moduleInfo) { + // We could not find the module on this path + // Try to defer to browserify etc, or node + var deferredModule; + if($tw.browser) { + if(window.require) { + try { + return window.require(moduleName); + } catch(e) {} } + throw "Cannot find module named '" + moduleName + "' required by module '" + moduleRoot + "', resolved to " + name; } else { - // line number should be included in e.stack for runtime errors - $tw.utils.error("Error executing boot module " + name + ": " + String(e) + "\n\n" + e.stack); + // If we don't have a module with that name, let node.js try to find it + return require(moduleName); } } - } - // Return the exports of the module - return moduleInfo.exports; -}; - -/* -Apply a callback to each module of a particular type - moduleType: type of modules to enumerate - callback: function called as callback(title,moduleExports) for each module -*/ -$tw.modules.forEachModuleOfType = function(moduleType,callback) { - var modules = $tw.modules.types[moduleType]; - $tw.utils.each(modules,function(element,title) { - callback(title,$tw.modules.execute(title)); - }); -}; - -/* -Get all the modules of a particular type in a hashmap by their `name` field -*/ -$tw.modules.getModulesByTypeAsHashmap = function(moduleType,nameField) { - nameField = nameField || "name"; - var results = Object.create(null); - $tw.modules.forEachModuleOfType(moduleType,function(title,module) { - results[module[nameField]] = module; - }); - return results; -}; - -/* -Apply the exports of the modules of a particular type to a target object -*/ -$tw.modules.applyMethods = function(moduleType,targetObject) { - if(!targetObject) { - targetObject = Object.create(null); - } - $tw.modules.forEachModuleOfType(moduleType,function(title,module) { - $tw.utils.each(module,function(element,title,object) { - targetObject[title] = module[title]; - }); - }); - return targetObject; -}; - -/* -Return a class created from a modules. The module should export the properties to be added to those of the optional base class -*/ -$tw.modules.createClassFromModule = function(moduleExports,baseClass) { - var newClass = function() {}; - if(baseClass) { - newClass.prototype = new baseClass(); - newClass.prototype.constructor = baseClass; - } - $tw.utils.extend(newClass.prototype,moduleExports); - return newClass; -}; - -/* -Return an array of classes created from the modules of a specified type. Each module should export the properties to be added to those of the optional base class -*/ -$tw.modules.createClassesFromModules = function(moduleType,subType,baseClass) { - var classes = Object.create(null); - $tw.modules.forEachModuleOfType(moduleType,function(title,moduleExports) { - if(!subType || moduleExports.types[subType]) { - classes[moduleExports.name] = $tw.modules.createClassFromModule(moduleExports,baseClass); - } - }); - return classes; -}; - -/////////////////////////// Barebones tiddler object - -/* -Construct a tiddler object from a hashmap of tiddler fields. If multiple hasmaps are provided they are merged, -taking precedence to the right -*/ -$tw.Tiddler = function(/* [fields,] fields */) { - this.fields = Object.create(null); - this.cache = Object.create(null); - for(var c=0; c<arguments.length; c++) { - var arg = arguments[c], - src = (arg instanceof $tw.Tiddler) ? arg.fields : arg; - for(var t in src) { - if(src[t] === undefined || src[t] === null) { - if(t in this.fields) { - delete this.fields[t]; // If we get a field that's undefined, delete any previous field value + // Execute the module if we haven't already done so + if(!moduleInfo.exports) { + try { + // Check the type of the definition + if(typeof moduleInfo.definition === "function") { // Function + moduleInfo.exports = _exports; + moduleInfo.definition(moduleInfo,moduleInfo.exports,sandbox.require); + } else if(typeof moduleInfo.definition === "string") { // String + moduleInfo.exports = _exports; + $tw.utils.evalSandboxed(moduleInfo.definition,sandbox,tiddler.fields.title); + if(sandbox.module.exports) { + moduleInfo.exports = sandbox.module.exports; //more codemirror workaround + } + } else { // Object + moduleInfo.exports = moduleInfo.definition; } - } else { - // Parse the field with the associated field module (if any) - var fieldModule = $tw.Tiddler.fieldModules[t], - value; - if(fieldModule && fieldModule.parse) { - value = fieldModule.parse.call(this,src[t]); + } catch(e) { + if(e instanceof SyntaxError) { + var line = e.lineNumber || e.line; // Firefox || Safari + if(typeof(line) != "undefined" && line !== null) { + $tw.utils.error("Syntax error in boot module " + name + ":" + line + ":\n" + e.stack); + } else if(!$tw.browser) { + // this is the only way to get node.js to display the line at which the syntax error appeared, + // and $tw.utils.error would exit anyway + // cf. https://bugs.chromium.org/p/v8/issues/detail?id=2589 + throw e; + } else { + // Opera: line number is included in e.message + // Chrome/IE: there's currently no way to get the line number + $tw.utils.error("Syntax error in boot module " + name + ": " + e.message + "\n" + e.stack); + } } else { - value = src[t]; - } - // Freeze the field to keep it immutable - if(value != null && typeof value === "object") { - Object.freeze(value); + // line number should be included in e.stack for runtime errors + $tw.utils.error("Error executing boot module " + name + ": " + String(e) + "\n\n" + e.stack); } - this.fields[t] = value; } } - } - // Freeze the tiddler against modification - Object.freeze(this.fields); - Object.freeze(this); -}; - -$tw.Tiddler.prototype.hasField = function(field) { - return $tw.utils.hop(this.fields,field); -}; - -/* -Compare two tiddlers for equality -tiddler: the tiddler to compare -excludeFields: array of field names to exclude from the comparison -*/ -$tw.Tiddler.prototype.isEqual = function(tiddler,excludeFields) { - if(!(tiddler instanceof $tw.Tiddler)) { - return false; - } - excludeFields = excludeFields || []; - var self = this, - differences = []; // Fields that have differences - // Add to the differences array - function addDifference(fieldName) { - // Check for this field being excluded - if(excludeFields.indexOf(fieldName) === -1) { - // Save the field as a difference - $tw.utils.pushTop(differences,fieldName); - } - } - // Returns true if the two values of this field are equal - function isFieldValueEqual(fieldName) { - var valueA = self.fields[fieldName], - valueB = tiddler.fields[fieldName]; - // Check for identical string values - if(typeof(valueA) === "string" && typeof(valueB) === "string" && valueA === valueB) { - return true; - } - // Check for identical array values - if($tw.utils.isArray(valueA) && $tw.utils.isArray(valueB) && $tw.utils.isArrayEqual(valueA,valueB)) { - return true; + // Return the exports of the module + return moduleInfo.exports; + }; + + /* + Apply a callback to each module of a particular type + moduleType: type of modules to enumerate + callback: function called as callback(title,moduleExports) for each module + */ + $tw.modules.forEachModuleOfType = function(moduleType,callback) { + var modules = $tw.modules.types[moduleType]; + $tw.utils.each(modules,function(element,title) { + callback(title,$tw.modules.execute(title)); + }); + }; + + /* + Get all the modules of a particular type in a hashmap by their `name` field + */ + $tw.modules.getModulesByTypeAsHashmap = function(moduleType,nameField) { + nameField = nameField || "name"; + var results = Object.create(null); + $tw.modules.forEachModuleOfType(moduleType,function(title,module) { + results[module[nameField]] = module; + }); + return results; + }; + + /* + Apply the exports of the modules of a particular type to a target object + */ + $tw.modules.applyMethods = function(moduleType,targetObject) { + if(!targetObject) { + targetObject = Object.create(null); + } + $tw.modules.forEachModuleOfType(moduleType,function(title,module) { + $tw.utils.each(module,function(element,title,object) { + targetObject[title] = module[title]; + }); + }); + return targetObject; + }; + + /* + Return a class created from a modules. The module should export the properties to be added to those of the optional base class + */ + $tw.modules.createClassFromModule = function(moduleExports,baseClass) { + var newClass = function() {}; + if(baseClass) { + newClass.prototype = new baseClass(); + newClass.prototype.constructor = baseClass; + } + $tw.utils.extend(newClass.prototype,moduleExports); + return newClass; + }; + + /* + Return an array of classes created from the modules of a specified type. Each module should export the properties to be added to those of the optional base class + */ + $tw.modules.createClassesFromModules = function(moduleType,subType,baseClass) { + var classes = Object.create(null); + $tw.modules.forEachModuleOfType(moduleType,function(title,moduleExports) { + if(!subType || moduleExports.types[subType]) { + classes[moduleExports.name] = $tw.modules.createClassFromModule(moduleExports,baseClass); + } + }); + return classes; + }; + + /////////////////////////// Barebones tiddler object + + /* + Construct a tiddler object from a hashmap of tiddler fields. If multiple hasmaps are provided they are merged, + taking precedence to the right + */ + $tw.Tiddler = function(/* [fields,] fields */) { + this.fields = Object.create(null); + this.cache = Object.create(null); + for(var c=0; c<arguments.length; c++) { + var arg = arguments[c], + src = (arg instanceof $tw.Tiddler) ? arg.fields : arg; + for(var t in src) { + if(src[t] === undefined || src[t] === null) { + if(t in this.fields) { + delete this.fields[t]; // If we get a field that's undefined, delete any previous field value + } + } else { + // Parse the field with the associated field module (if any) + var fieldModule = $tw.Tiddler.fieldModules[t], + value; + if(fieldModule && fieldModule.parse) { + value = fieldModule.parse.call(this,src[t]); + } else { + value = src[t]; + } + // Freeze the field to keep it immutable + if(value != null && typeof value === "object") { + Object.freeze(value); + } + this.fields[t] = value; + } + } } - // Check for identical date values - if($tw.utils.isDate(valueA) && $tw.utils.isDate(valueB) && valueA.getTime() === valueB.getTime()) { - return true; + // Freeze the tiddler against modification + Object.freeze(this.fields); + Object.freeze(this); + }; + + $tw.Tiddler.prototype.hasField = function(field) { + return $tw.utils.hop(this.fields,field); + }; + + /* + Compare two tiddlers for equality + tiddler: the tiddler to compare + excludeFields: array of field names to exclude from the comparison + */ + $tw.Tiddler.prototype.isEqual = function(tiddler,excludeFields) { + if(!(tiddler instanceof $tw.Tiddler)) { + return false; } - // Otherwise the fields must be different - return false; - } - // Compare our fields - for(var fieldName in this.fields) { - if(!isFieldValueEqual(fieldName)) { - addDifference(fieldName); + excludeFields = excludeFields || []; + var self = this, + differences = []; // Fields that have differences + // Add to the differences array + function addDifference(fieldName) { + // Check for this field being excluded + if(excludeFields.indexOf(fieldName) === -1) { + // Save the field as a difference + $tw.utils.pushTop(differences,fieldName); + } + } + // Returns true if the two values of this field are equal + function isFieldValueEqual(fieldName) { + var valueA = self.fields[fieldName], + valueB = tiddler.fields[fieldName]; + // Check for identical string values + if(typeof(valueA) === "string" && typeof(valueB) === "string" && valueA === valueB) { + return true; + } + // Check for identical array values + if($tw.utils.isArray(valueA) && $tw.utils.isArray(valueB) && $tw.utils.isArrayEqual(valueA,valueB)) { + return true; + } + // Check for identical date values + if($tw.utils.isDate(valueA) && $tw.utils.isDate(valueB) && valueA.getTime() === valueB.getTime()) { + return true; + } + // Otherwise the fields must be different + return false; } - } - // There's a difference for every field in the other tiddler that we don't have - for(fieldName in tiddler.fields) { - if(!(fieldName in this.fields)) { - addDifference(fieldName); + // Compare our fields + for(var fieldName in this.fields) { + if(!isFieldValueEqual(fieldName)) { + addDifference(fieldName); + } } - } - // Return whether there were any differences - return differences.length === 0; -}; - -/* -Register and install the built in tiddler field modules -*/ -$tw.modules.define("$:/boot/tiddlerfields/modified","tiddlerfield",{ - name: "modified", - parse: $tw.utils.parseDate, - stringify: $tw.utils.stringifyDate -}); -$tw.modules.define("$:/boot/tiddlerfields/created","tiddlerfield",{ - name: "created", - parse: $tw.utils.parseDate, - stringify: $tw.utils.stringifyDate -}); -$tw.modules.define("$:/boot/tiddlerfields/color","tiddlerfield",{ - name: "color", - editTag: "input", - editType: "color" -}); -$tw.modules.define("$:/boot/tiddlerfields/tags","tiddlerfield",{ - name: "tags", - parse: $tw.utils.parseStringArray, - stringify: $tw.utils.stringifyList -}); -$tw.modules.define("$:/boot/tiddlerfields/list","tiddlerfield",{ - name: "list", - parse: $tw.utils.parseStringArray, - stringify: $tw.utils.stringifyList -}); - -/////////////////////////// Barebones wiki store - -/* -Wiki constructor. State is stored in private members that only a small number of privileged accessor methods have direct access. Methods added via the prototype have to use these accessors and cannot access the state data directly. -options include: -enableIndexers - Array of indexer names to enable, or null to use all available indexers -*/ -$tw.Wiki = function(options) { - options = options || {}; - var self = this, - tiddlers = Object.create(null), // Hashmap of tiddlers - tiddlerTitles = null, // Array of tiddler titles - getTiddlerTitles = function() { - if(!tiddlerTitles) { - tiddlerTitles = Object.keys(tiddlers).sort(function(a,b) {return a.localeCompare(b);}); - } - return tiddlerTitles; - }, - pluginTiddlers = [], // Array of tiddlers containing registered plugins, ordered by priority - pluginInfo = Object.create(null), // Hashmap of parsed plugin content - shadowTiddlers = Object.create(null), // Hashmap by title of {source:, tiddler:} - shadowTiddlerTitles = null, - getShadowTiddlerTitles = function() { - if(!shadowTiddlerTitles) { - shadowTiddlerTitles = Object.keys(shadowTiddlers).sort(function(a,b) {return a.localeCompare(b);}); - } - return shadowTiddlerTitles; - }, - enableIndexers = options.enableIndexers || null, - indexers = [], - indexersByName = Object.create(null); - - this.addIndexer = function(indexer,name) { - // Bail if this indexer is not enabled - if(enableIndexers && enableIndexers.indexOf(name) === -1) { - return; + // There's a difference for every field in the other tiddler that we don't have + for(fieldName in tiddler.fields) { + if(!(fieldName in this.fields)) { + addDifference(fieldName); + } } - indexers.push(indexer); - indexersByName[name] = indexer; - indexer.init(); - }; - - this.getIndexer = function(name) { - return indexersByName[name] || null; + // Return whether there were any differences + return differences.length === 0; }; - - // Add a tiddler to the store - this.addTiddler = function(tiddler) { - if(!(tiddler instanceof $tw.Tiddler)) { - tiddler = new $tw.Tiddler(tiddler); - } - // Save the tiddler - if(tiddler) { - var title = tiddler.fields.title; - if(title) { -// Uncomment the following line for detailed logs of all tiddler writes -// console.log("Adding",title,tiddler) + + /* + Register and install the built in tiddler field modules + */ + $tw.modules.define("$:/boot/tiddlerfields/modified","tiddlerfield",{ + name: "modified", + parse: $tw.utils.parseDate, + stringify: $tw.utils.stringifyDate + }); + $tw.modules.define("$:/boot/tiddlerfields/created","tiddlerfield",{ + name: "created", + parse: $tw.utils.parseDate, + stringify: $tw.utils.stringifyDate + }); + $tw.modules.define("$:/boot/tiddlerfields/color","tiddlerfield",{ + name: "color", + editTag: "input", + editType: "color" + }); + $tw.modules.define("$:/boot/tiddlerfields/tags","tiddlerfield",{ + name: "tags", + parse: $tw.utils.parseStringArray, + stringify: $tw.utils.stringifyList + }); + $tw.modules.define("$:/boot/tiddlerfields/list","tiddlerfield",{ + name: "list", + parse: $tw.utils.parseStringArray, + stringify: $tw.utils.stringifyList + }); + + /////////////////////////// Barebones wiki store + + /* + Wiki constructor. State is stored in private members that only a small number of privileged accessor methods have direct access. Methods added via the prototype have to use these accessors and cannot access the state data directly. + options include: + enableIndexers - Array of indexer names to enable, or null to use all available indexers + */ + $tw.Wiki = function(options) { + options = options || {}; + var self = this, + tiddlers = Object.create(null), // Hashmap of tiddlers + tiddlerTitles = null, // Array of tiddler titles + getTiddlerTitles = function() { + if(!tiddlerTitles) { + tiddlerTitles = Object.keys(tiddlers).sort(function(a,b) {return a.localeCompare(b);}); + } + return tiddlerTitles; + }, + pluginTiddlers = [], // Array of tiddlers containing registered plugins, ordered by priority + pluginInfo = Object.create(null), // Hashmap of parsed plugin content + shadowTiddlers = Object.create(null), // Hashmap by title of {source:, tiddler:} + shadowTiddlerTitles = null, + getShadowTiddlerTitles = function() { + if(!shadowTiddlerTitles) { + shadowTiddlerTitles = Object.keys(shadowTiddlers).sort(function(a,b) {return a.localeCompare(b);}); + } + return shadowTiddlerTitles; + }, + enableIndexers = options.enableIndexers || null, + indexers = [], + indexersByName = Object.create(null); + + this.addIndexer = function(indexer,name) { + // Bail if this indexer is not enabled + if(enableIndexers && enableIndexers.indexOf(name) === -1) { + return; + } + indexers.push(indexer); + indexersByName[name] = indexer; + indexer.init(); + }; + + this.getIndexer = function(name) { + return indexersByName[name] || null; + }; + + // Add a tiddler to the store + this.addTiddler = function(tiddler) { + if(!(tiddler instanceof $tw.Tiddler)) { + tiddler = new $tw.Tiddler(tiddler); + } + // Save the tiddler + if(tiddler) { + var title = tiddler.fields.title; + if(title) { + // Uncomment the following line for detailed logs of all tiddler writes + // console.log("Adding",title,tiddler) + // Record the old tiddler state + var updateDescriptor = { + old: { + tiddler: this.getTiddler(title), + shadow: this.isShadowTiddler(title), + exists: this.tiddlerExists(title) + } + } + // Save the new tiddler + tiddlers[title] = tiddler; + // Check we've got the title + tiddlerTitles = $tw.utils.insertSortedArray(tiddlerTitles || [],title); + // Record the new tiddler state + updateDescriptor["new"] = { + tiddler: tiddler, + shadow: this.isShadowTiddler(title), + exists: this.tiddlerExists(title) + } + // Update indexes + this.clearCache(title); + this.clearGlobalCache(); + $tw.utils.each(indexers,function(indexer) { + indexer.update(updateDescriptor); + }); + // Queue a change event + this.enqueueTiddlerEvent(title); + } + } + }; + + // Delete a tiddler + this.deleteTiddler = function(title) { + // Uncomment the following line for detailed logs of all tiddler deletions + // console.log("Deleting",title) + if($tw.utils.hop(tiddlers,title)) { // Record the old tiddler state var updateDescriptor = { old: { @@ -1319,13 +1381,18 @@ $tw.Wiki = function(options) { exists: this.tiddlerExists(title) } } - // Save the new tiddler - tiddlers[title] = tiddler; - // Check we've got the title - tiddlerTitles = $tw.utils.insertSortedArray(tiddlerTitles || [],title); + // Delete the tiddler + delete tiddlers[title]; + // Delete it from the list of titles + if(tiddlerTitles) { + var index = tiddlerTitles.indexOf(title); + if(index !== -1) { + tiddlerTitles.splice(index,1); + } + } // Record the new tiddler state updateDescriptor["new"] = { - tiddler: tiddler, + tiddler: this.getTiddler(title), shadow: this.isShadowTiddler(title), exists: this.tiddlerExists(title) } @@ -1336,1568 +1403,1537 @@ $tw.Wiki = function(options) { indexer.update(updateDescriptor); }); // Queue a change event - this.enqueueTiddlerEvent(title); - } - } - }; - - // Delete a tiddler - this.deleteTiddler = function(title) { -// Uncomment the following line for detailed logs of all tiddler deletions -// console.log("Deleting",title) - if($tw.utils.hop(tiddlers,title)) { - // Record the old tiddler state - var updateDescriptor = { - old: { - tiddler: this.getTiddler(title), - shadow: this.isShadowTiddler(title), - exists: this.tiddlerExists(title) - } + this.enqueueTiddlerEvent(title,true); } - // Delete the tiddler - delete tiddlers[title]; - // Delete it from the list of titles - if(tiddlerTitles) { - var index = tiddlerTitles.indexOf(title); - if(index !== -1) { - tiddlerTitles.splice(index,1); + }; + + // Get a tiddler from the store + this.getTiddler = function(title) { + if(title) { + var t = tiddlers[title]; + if(t !== undefined) { + return t; + } else { + var s = shadowTiddlers[title]; + if(s !== undefined) { + return s.tiddler; + } } } - // Record the new tiddler state - updateDescriptor["new"] = { - tiddler: this.getTiddler(title), - shadow: this.isShadowTiddler(title), - exists: this.tiddlerExists(title) + return undefined; + }; + + // Get an array of all tiddler titles + this.allTitles = function() { + return getTiddlerTitles().slice(0); + }; + + // Iterate through all tiddler titles + this.each = function(callback) { + var titles = getTiddlerTitles(), + index,titlesLength,title; + for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { + title = titles[index]; + callback(tiddlers[title],title); } - // Update indexes - this.clearCache(title); - this.clearGlobalCache(); - $tw.utils.each(indexers,function(indexer) { - indexer.update(updateDescriptor); - }); - // Queue a change event - this.enqueueTiddlerEvent(title,true); - } - }; - - // Get a tiddler from the store - this.getTiddler = function(title) { - if(title) { - var t = tiddlers[title]; - if(t !== undefined) { - return t; - } else { - var s = shadowTiddlers[title]; - if(s !== undefined) { - return s.tiddler; + }; + + // Get an array of all shadow tiddler titles + this.allShadowTitles = function() { + return getShadowTiddlerTitles().slice(0); + }; + + // Iterate through all shadow tiddler titles + this.eachShadow = function(callback) { + var titles = getShadowTiddlerTitles(), + index,titlesLength,title; + for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { + title = titles[index]; + if(tiddlers[title]) { + callback(tiddlers[title],title); + } else { + var shadowInfo = shadowTiddlers[title]; + callback(shadowInfo.tiddler,title); } } - } - return undefined; - }; - - // Get an array of all tiddler titles - this.allTitles = function() { - return getTiddlerTitles().slice(0); - }; - - // Iterate through all tiddler titles - this.each = function(callback) { - var titles = getTiddlerTitles(), - index,titlesLength,title; - for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { - title = titles[index]; - callback(tiddlers[title],title); - } - }; - - // Get an array of all shadow tiddler titles - this.allShadowTitles = function() { - return getShadowTiddlerTitles().slice(0); - }; - - // Iterate through all shadow tiddler titles - this.eachShadow = function(callback) { - var titles = getShadowTiddlerTitles(), - index,titlesLength,title; - for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { - title = titles[index]; - if(tiddlers[title]) { + }; + + // Iterate through all tiddlers and then the shadows + this.eachTiddlerPlusShadows = function(callback) { + var index,titlesLength,title, + titles = getTiddlerTitles(); + for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { + title = titles[index]; callback(tiddlers[title],title); - } else { - var shadowInfo = shadowTiddlers[title]; - callback(shadowInfo.tiddler,title); - } - } - }; - - // Iterate through all tiddlers and then the shadows - this.eachTiddlerPlusShadows = function(callback) { - var index,titlesLength,title, - titles = getTiddlerTitles(); - for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { - title = titles[index]; - callback(tiddlers[title],title); - } - titles = getShadowTiddlerTitles(); - for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { - title = titles[index]; - if(!tiddlers[title]) { - var shadowInfo = shadowTiddlers[title]; - callback(shadowInfo.tiddler,title); } - } - }; - - // Iterate through all the shadows and then the tiddlers - this.eachShadowPlusTiddlers = function(callback) { - var index,titlesLength,title, titles = getShadowTiddlerTitles(); - for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { - title = titles[index]; - if(tiddlers[title]) { - callback(tiddlers[title],title); - } else { - var shadowInfo = shadowTiddlers[title]; - callback(shadowInfo.tiddler,title); + for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { + title = titles[index]; + if(!tiddlers[title]) { + var shadowInfo = shadowTiddlers[title]; + callback(shadowInfo.tiddler,title); + } } - } - titles = getTiddlerTitles(); - for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { - title = titles[index]; - if(!shadowTiddlers[title]) { - callback(tiddlers[title],title); + }; + + // Iterate through all the shadows and then the tiddlers + this.eachShadowPlusTiddlers = function(callback) { + var index,titlesLength,title, + titles = getShadowTiddlerTitles(); + for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { + title = titles[index]; + if(tiddlers[title]) { + callback(tiddlers[title],title); + } else { + var shadowInfo = shadowTiddlers[title]; + callback(shadowInfo.tiddler,title); + } } - } - }; - - // Test for the existence of a tiddler (excludes shadow tiddlers) - this.tiddlerExists = function(title) { - return !!$tw.utils.hop(tiddlers,title); - }; - - // Determines if a tiddler is a shadow tiddler, regardless of whether it has been overridden by a real tiddler - this.isShadowTiddler = function(title) { - return $tw.utils.hop(shadowTiddlers,title); - }; - - this.getShadowSource = function(title) { - if($tw.utils.hop(shadowTiddlers,title)) { - return shadowTiddlers[title].source; - } - return null; - }; - - // Get an array of all the currently recognised plugin types - this.getPluginTypes = function() { - var types = []; - $tw.utils.each(pluginTiddlers,function(pluginTiddler) { - var pluginType = pluginTiddler.fields["plugin-type"]; - if(pluginType && types.indexOf(pluginType) === -1) { - types.push(pluginType); + titles = getTiddlerTitles(); + for(index = 0, titlesLength = titles.length; index < titlesLength; index++) { + title = titles[index]; + if(!shadowTiddlers[title]) { + callback(tiddlers[title],title); + } } - }); - return types; - }; - - // Read plugin info for all plugins, or just an array of titles. Returns the number of plugins updated or deleted - this.readPluginInfo = function(titles) { - var results = { - modifiedPlugins: [], - deletedPlugins: [] }; - $tw.utils.each(titles || getTiddlerTitles(),function(title) { - var tiddler = tiddlers[title]; - if(tiddler) { - if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type") && tiddler.fields.text) { - pluginInfo[tiddler.fields.title] = $tw.utils.parseJSONSafe(tiddler.fields.text); - results.modifiedPlugins.push(tiddler.fields.title); + + // Test for the existence of a tiddler (excludes shadow tiddlers) + this.tiddlerExists = function(title) { + return !!$tw.utils.hop(tiddlers,title); + }; + + // Determines if a tiddler is a shadow tiddler, regardless of whether it has been overridden by a real tiddler + this.isShadowTiddler = function(title) { + return $tw.utils.hop(shadowTiddlers,title); + }; + + this.getShadowSource = function(title) { + if($tw.utils.hop(shadowTiddlers,title)) { + return shadowTiddlers[title].source; + } + return null; + }; + + // Get an array of all the currently recognised plugin types + this.getPluginTypes = function() { + var types = []; + $tw.utils.each(pluginTiddlers,function(pluginTiddler) { + var pluginType = pluginTiddler.fields["plugin-type"]; + if(pluginType && types.indexOf(pluginType) === -1) { + types.push(pluginType); + } + }); + return types; + }; + + // Read plugin info for all plugins, or just an array of titles. Returns the number of plugins updated or deleted + this.readPluginInfo = function(titles) { + var results = { + modifiedPlugins: [], + deletedPlugins: [] + }; + $tw.utils.each(titles || getTiddlerTitles(),function(title) { + var tiddler = tiddlers[title]; + if(tiddler) { + if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type") && tiddler.fields.text) { + pluginInfo[tiddler.fields.title] = $tw.utils.parseJSONSafe(tiddler.fields.text); + results.modifiedPlugins.push(tiddler.fields.title); + } + } else { + if(pluginInfo[title]) { + delete pluginInfo[title]; + results.deletedPlugins.push(title); + } } + }); + return results; + }; + + // Get plugin info for a plugin + this.getPluginInfo = function(title) { + return pluginInfo[title]; + }; + + // Register the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting registration to an array of tiddler titles. Return the array of titles affected + this.registerPluginTiddlers = function(pluginType,titles) { + var self = this, + registeredTitles = [], + checkTiddler = function(tiddler,title) { + if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { + var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); + if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { + self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered + pluginTiddlers.push(tiddler); + registeredTitles.push(tiddler.fields.title); + } + } + }; + if(titles) { + $tw.utils.each(titles,function(title) { + checkTiddler(self.getTiddler(title),title); + }); } else { - if(pluginInfo[title]) { - delete pluginInfo[title]; - results.deletedPlugins.push(title); + this.each(function(tiddler,title) { + checkTiddler(tiddler,title); + }); + } + return registeredTitles; + }; + + // Unregister the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting unregistering to an array of tiddler titles. Returns an array of the titles affected + this.unregisterPluginTiddlers = function(pluginType,titles) { + var self = this, + unregisteredTitles = []; + // Remove any previous registered plugins of this type + for(var t=pluginTiddlers.length-1; t>=0; t--) { + var tiddler = pluginTiddlers[t]; + if(tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType) && (!titles || titles.indexOf(tiddler.fields.title) !== -1)) { + unregisteredTitles.push(tiddler.fields.title); + pluginTiddlers.splice(t,1); } } - }); - return results; - }; - - // Get plugin info for a plugin - this.getPluginInfo = function(title) { - return pluginInfo[title]; - }; - - // Register the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting registration to an array of tiddler titles. Return the array of titles affected - this.registerPluginTiddlers = function(pluginType,titles) { - var self = this, - registeredTitles = [], - checkTiddler = function(tiddler,title) { - if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) { - var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); - if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { - self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered - pluginTiddlers.push(tiddler); - registeredTitles.push(tiddler.fields.title); - } + return unregisteredTitles; + }; + + // Unpack the currently registered plugins, creating shadow tiddlers for their constituent tiddlers + this.unpackPluginTiddlers = function() { + var self = this; + // Sort the plugin titles by the `plugin-priority` field + pluginTiddlers.sort(function(a,b) { + if("plugin-priority" in a.fields && "plugin-priority" in b.fields) { + return a.fields["plugin-priority"] - b.fields["plugin-priority"]; + } else if("plugin-priority" in a.fields) { + return -1; + } else if("plugin-priority" in b.fields) { + return +1; + } else if(a.fields.title < b.fields.title) { + return -1; + } else if(a.fields.title === b.fields.title) { + return 0; + } else { + return +1; } - }; - if(titles) { - $tw.utils.each(titles,function(title) { - checkTiddler(self.getTiddler(title),title); }); - } else { - this.each(function(tiddler,title) { - checkTiddler(tiddler,title); + // Now go through the plugins in ascending order and assign the shadows + shadowTiddlers = Object.create(null); + $tw.utils.each(pluginTiddlers,function(tiddler) { + // Extract the constituent tiddlers + if($tw.utils.hop(pluginInfo,tiddler.fields.title)) { + $tw.utils.each(pluginInfo[tiddler.fields.title].tiddlers,function(constituentTiddler,constituentTitle) { + // Save the tiddler object + if(constituentTitle) { + shadowTiddlers[constituentTitle] = { + source: tiddler.fields.title, + tiddler: new $tw.Tiddler(constituentTiddler,{title: constituentTitle}) + }; + } + }); + } + }); + shadowTiddlerTitles = null; + this.clearCache(null); + this.clearGlobalCache(); + $tw.utils.each(indexers,function(indexer) { + indexer.rebuild(); }); + }; + + if(this.addIndexersToWiki) { + this.addIndexersToWiki(); } - return registeredTitles; }; - - // Unregister the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting unregistering to an array of tiddler titles. Returns an array of the titles affected - this.unregisterPluginTiddlers = function(pluginType,titles) { - var self = this, - unregisteredTitles = []; - // Remove any previous registered plugins of this type - for(var t=pluginTiddlers.length-1; t>=0; t--) { - var tiddler = pluginTiddlers[t]; - if(tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType) && (!titles || titles.indexOf(tiddler.fields.title) !== -1)) { - unregisteredTitles.push(tiddler.fields.title); - pluginTiddlers.splice(t,1); - } + + // Dummy methods that will be filled in after boot + $tw.Wiki.prototype.clearCache = + $tw.Wiki.prototype.clearGlobalCache = + $tw.Wiki.prototype.enqueueTiddlerEvent = function() {}; + + // Add an array of tiddlers + $tw.Wiki.prototype.addTiddlers = function(tiddlers) { + for(var t=0; t<tiddlers.length; t++) { + this.addTiddler(tiddlers[t]); } - return unregisteredTitles; }; - - // Unpack the currently registered plugins, creating shadow tiddlers for their constituent tiddlers - this.unpackPluginTiddlers = function() { + + /* + Define all modules stored in ordinary tiddlers + */ + $tw.Wiki.prototype.defineTiddlerModules = function() { + this.each(function(tiddler,title) { + if(tiddler.hasField("module-type")) { + switch(tiddler.fields.type) { + case "application/javascript": + // We only define modules that haven't already been defined, because in the browser modules in system tiddlers are defined in inline script + if(!$tw.utils.hop($tw.modules.titles,tiddler.fields.title)) { + $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],tiddler.fields.text); + } + break; + case "application/json": + $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],$tw.utils.parseJSONSafe(tiddler.fields.text)); + break; + case "application/x-tiddler-dictionary": + $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],$tw.utils.parseFields(tiddler.fields.text)); + break; + } + } + }); + }; + + /* + Register all the module tiddlers that have a module type + */ + $tw.Wiki.prototype.defineShadowModules = function() { var self = this; - // Sort the plugin titles by the `plugin-priority` field - pluginTiddlers.sort(function(a,b) { - if("plugin-priority" in a.fields && "plugin-priority" in b.fields) { - return a.fields["plugin-priority"] - b.fields["plugin-priority"]; - } else if("plugin-priority" in a.fields) { - return -1; - } else if("plugin-priority" in b.fields) { - return +1; - } else if(a.fields.title < b.fields.title) { - return -1; - } else if(a.fields.title === b.fields.title) { - return 0; - } else { - return +1; + this.eachShadow(function(tiddler,title) { + // Don't define the module if it is overidden by an ordinary tiddler + if(!self.tiddlerExists(title) && tiddler.hasField("module-type")) { + // Define the module + $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],tiddler.fields.text); } }); - // Now go through the plugins in ascending order and assign the shadows - shadowTiddlers = Object.create(null); - $tw.utils.each(pluginTiddlers,function(tiddler) { - // Extract the constituent tiddlers - if($tw.utils.hop(pluginInfo,tiddler.fields.title)) { - $tw.utils.each(pluginInfo[tiddler.fields.title].tiddlers,function(constituentTiddler,constituentTitle) { - // Save the tiddler object - if(constituentTitle) { - shadowTiddlers[constituentTitle] = { - source: tiddler.fields.title, - tiddler: new $tw.Tiddler(constituentTiddler,{title: constituentTitle}) - }; - } - }); + }; + + /* + Enable safe mode by deleting any tiddlers that override a shadow tiddler + */ + $tw.Wiki.prototype.processSafeMode = function() { + var self = this, + overrides = []; + // Find the overriding tiddlers + this.each(function(tiddler,title) { + if(self.isShadowTiddler(title)) { + console.log(title); + overrides.push(title); } }); - shadowTiddlerTitles = null; - this.clearCache(null); - this.clearGlobalCache(); - $tw.utils.each(indexers,function(indexer) { - indexer.rebuild(); + // Assemble a report tiddler + var titleReportTiddler = "TiddlyWiki Safe Mode", + report = []; + report.push("TiddlyWiki has been started in [[safe mode|https://tiddlywiki.com/static/SafeMode.html]]. All plugins are temporarily disabled. Most customisations have been disabled by renaming the following tiddlers:") + // Delete the overrides + overrides.forEach(function(title) { + var tiddler = self.getTiddler(title), + newTitle = "SAFE: " + title; + self.deleteTiddler(title); + self.addTiddler(new $tw.Tiddler(tiddler, {title: newTitle})); + report.push("* [[" + title + "|" + newTitle + "]]"); }); + report.push() + this.addTiddler(new $tw.Tiddler({title: titleReportTiddler, text: report.join("\n\n")})); + // Set $:/DefaultTiddlers to point to our report + this.addTiddler(new $tw.Tiddler({title: "$:/DefaultTiddlers", text: "[[" + titleReportTiddler + "]]"})); }; - - if(this.addIndexersToWiki) { - this.addIndexersToWiki(); - } -}; - -// Dummy methods that will be filled in after boot -$tw.Wiki.prototype.clearCache = -$tw.Wiki.prototype.clearGlobalCache = -$tw.Wiki.prototype.enqueueTiddlerEvent = function() {}; - -// Add an array of tiddlers -$tw.Wiki.prototype.addTiddlers = function(tiddlers) { - for(var t=0; t<tiddlers.length; t++) { - this.addTiddler(tiddlers[t]); - } -}; - -/* -Define all modules stored in ordinary tiddlers -*/ -$tw.Wiki.prototype.defineTiddlerModules = function() { - this.each(function(tiddler,title) { - if(tiddler.hasField("module-type")) { - switch (tiddler.fields.type) { - case "application/javascript": - // We only define modules that haven't already been defined, because in the browser modules in system tiddlers are defined in inline script - if(!$tw.utils.hop($tw.modules.titles,tiddler.fields.title)) { - $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],tiddler.fields.text); - } - break; - case "application/json": - $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],$tw.utils.parseJSONSafe(tiddler.fields.text)); - break; - case "application/x-tiddler-dictionary": - $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],$tw.utils.parseFields(tiddler.fields.text)); - break; - } + + /* + Extracts tiddlers from a typed block of text, specifying default field values + */ + $tw.Wiki.prototype.deserializeTiddlers = function(type,text,srcFields,options) { + srcFields = srcFields || Object.create(null); + options = options || {}; + var deserializer = $tw.Wiki.tiddlerDeserializerModules[options.deserializer], + fields = Object.create(null); + if(!deserializer) { + deserializer = $tw.Wiki.tiddlerDeserializerModules[type]; + } + if(!deserializer && $tw.utils.getFileExtensionInfo(type)) { + // If we didn't find the serializer, try converting it from an extension to a content type + type = $tw.utils.getFileExtensionInfo(type).type; + deserializer = $tw.Wiki.tiddlerDeserializerModules[type]; + } + if(!deserializer && $tw.config.contentTypeInfo[type]) { + // see if this type has a different deserializer registered with it + type = $tw.config.contentTypeInfo[type].deserializerType; + deserializer = $tw.Wiki.tiddlerDeserializerModules[type]; + } + if(!deserializer) { + // If we still don't have a deserializer, treat it as plain text + deserializer = $tw.Wiki.tiddlerDeserializerModules["text/plain"]; + } + for(var f in srcFields) { + fields[f] = srcFields[f]; + } + if(deserializer) { + return deserializer.call(this,text,fields,type); + } else { + // Return a raw tiddler for unknown types + fields.text = text; + return [fields]; } + }; + + /* + Register the built in tiddler deserializer modules + */ + var deserializeHeaderComment = function(text,fields) { + var headerCommentRegExp = new RegExp($tw.config.jsModuleHeaderRegExpString,"mg"), + match = headerCommentRegExp.exec(text); + fields.text = text; + if(match) { + fields = $tw.utils.parseFields(match[1].split(/\r?\n\r?\n/mg)[0],fields); + } + return [fields]; + }; + $tw.modules.define("$:/boot/tiddlerdeserializer/js","tiddlerdeserializer",{ + "application/javascript": deserializeHeaderComment }); -}; - -/* -Register all the module tiddlers that have a module type -*/ -$tw.Wiki.prototype.defineShadowModules = function() { - var self = this; - this.eachShadow(function(tiddler,title) { - // Don't define the module if it is overidden by an ordinary tiddler - if(!self.tiddlerExists(title) && tiddler.hasField("module-type")) { - // Define the module - $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],tiddler.fields.text); - } + $tw.modules.define("$:/boot/tiddlerdeserializer/css","tiddlerdeserializer",{ + "text/css": deserializeHeaderComment }); -}; - -/* -Enable safe mode by deleting any tiddlers that override a shadow tiddler -*/ -$tw.Wiki.prototype.processSafeMode = function() { - var self = this, - overrides = []; - // Find the overriding tiddlers - this.each(function(tiddler,title) { - if(self.isShadowTiddler(title)) { - console.log(title); - overrides.push(title); + $tw.modules.define("$:/boot/tiddlerdeserializer/tid","tiddlerdeserializer",{ + "application/x-tiddler": function(text,fields) { + var split = text.split(/\r?\n\r?\n/mg); + if(split.length >= 1) { + fields = $tw.utils.parseFields(split[0],fields); + } + if(split.length >= 2) { + fields.text = split.slice(1).join("\n\n"); + } + return [fields]; } }); - // Assemble a report tiddler - var titleReportTiddler = "TiddlyWiki Safe Mode", - report = []; - report.push("TiddlyWiki has been started in [[safe mode|https://tiddlywiki.com/static/SafeMode.html]]. All plugins are temporarily disabled. Most customisations have been disabled by renaming the following tiddlers:") - // Delete the overrides - overrides.forEach(function(title) { - var tiddler = self.getTiddler(title), - newTitle = "SAFE: " + title; - self.deleteTiddler(title); - self.addTiddler(new $tw.Tiddler(tiddler, {title: newTitle})); - report.push("* [[" + title + "|" + newTitle + "]]"); - }); - report.push() - this.addTiddler(new $tw.Tiddler({title: titleReportTiddler, text: report.join("\n\n")})); - // Set $:/DefaultTiddlers to point to our report - this.addTiddler(new $tw.Tiddler({title: "$:/DefaultTiddlers", text: "[[" + titleReportTiddler + "]]"})); -}; - -/* -Extracts tiddlers from a typed block of text, specifying default field values -*/ -$tw.Wiki.prototype.deserializeTiddlers = function(type,text,srcFields,options) { - srcFields = srcFields || Object.create(null); - options = options || {}; - var deserializer = $tw.Wiki.tiddlerDeserializerModules[options.deserializer], - fields = Object.create(null); - if(!deserializer) { - deserializer = $tw.Wiki.tiddlerDeserializerModules[type]; - } - if(!deserializer && $tw.utils.getFileExtensionInfo(type)) { - // If we didn't find the serializer, try converting it from an extension to a content type - type = $tw.utils.getFileExtensionInfo(type).type; - deserializer = $tw.Wiki.tiddlerDeserializerModules[type]; - } - if(!deserializer && $tw.config.contentTypeInfo[type]) { - // see if this type has a different deserializer registered with it - type = $tw.config.contentTypeInfo[type].deserializerType; - deserializer = $tw.Wiki.tiddlerDeserializerModules[type]; - } - if(!deserializer) { - // If we still don't have a deserializer, treat it as plain text - deserializer = $tw.Wiki.tiddlerDeserializerModules["text/plain"]; - } - for(var f in srcFields) { - fields[f] = srcFields[f]; - } - if(deserializer) { - return deserializer.call(this,text,fields,type); - } else { - // Return a raw tiddler for unknown types - fields.text = text; - return [fields]; - } -}; - -/* -Register the built in tiddler deserializer modules -*/ -var deserializeHeaderComment = function(text,fields) { - var headerCommentRegExp = new RegExp($tw.config.jsModuleHeaderRegExpString,"mg"), - match = headerCommentRegExp.exec(text); - fields.text = text; - if(match) { - fields = $tw.utils.parseFields(match[1].split(/\r?\n\r?\n/mg)[0],fields); - } - return [fields]; - }; -$tw.modules.define("$:/boot/tiddlerdeserializer/js","tiddlerdeserializer",{ - "application/javascript": deserializeHeaderComment -}); -$tw.modules.define("$:/boot/tiddlerdeserializer/css","tiddlerdeserializer",{ - "text/css": deserializeHeaderComment -}); -$tw.modules.define("$:/boot/tiddlerdeserializer/tid","tiddlerdeserializer",{ - "application/x-tiddler": function(text,fields) { - var split = text.split(/\r?\n\r?\n/mg); - if(split.length >= 1) { - fields = $tw.utils.parseFields(split[0],fields); - } - if(split.length >= 2) { - fields.text = split.slice(1).join("\n\n"); - } - return [fields]; - } -}); -$tw.modules.define("$:/boot/tiddlerdeserializer/tids","tiddlerdeserializer",{ - "application/x-tiddlers": function(text,fields) { - var titles = [], - tiddlers = [], - match = /\r?\n\r?\n/mg.exec(text); - if(match) { - fields = $tw.utils.parseFields(text.substr(0,match.index),fields); - var lines = text.substr(match.index + match[0].length).split(/\r?\n/mg); - for(var t=0; t<lines.length; t++) { - var line = lines[t]; - if(line.charAt(0) !== "#") { - var colonPos= line.indexOf(":"); - if(colonPos !== -1) { - var tiddler = $tw.utils.extend(Object.create(null),fields); - tiddler.title = (tiddler.title || "") + line.substr(0,colonPos).trim(); - if(titles.indexOf(tiddler.title) !== -1) { - console.log("Warning: .multids file contains multiple definitions for " + tiddler.title); + $tw.modules.define("$:/boot/tiddlerdeserializer/tids","tiddlerdeserializer",{ + "application/x-tiddlers": function(text,fields) { + var titles = [], + tiddlers = [], + match = /\r?\n\r?\n/mg.exec(text); + if(match) { + fields = $tw.utils.parseFields(text.substr(0,match.index),fields); + var lines = text.substr(match.index + match[0].length).split(/\r?\n/mg); + for(var t=0; t<lines.length; t++) { + var line = lines[t]; + if(line.charAt(0) !== "#") { + var colonPos= line.indexOf(":"); + if(colonPos !== -1) { + var tiddler = $tw.utils.extend(Object.create(null),fields); + tiddler.title = (tiddler.title || "") + line.substr(0,colonPos).trim(); + if(titles.indexOf(tiddler.title) !== -1) { + console.log("Warning: .multids file contains multiple definitions for " + tiddler.title); + } + titles.push(tiddler.title); + tiddler.text = line.substr(colonPos + 2).trim(); + tiddlers.push(tiddler); } - titles.push(tiddler.title); - tiddler.text = line.substr(colonPos + 2).trim(); - tiddlers.push(tiddler); } } } + return tiddlers; } - return tiddlers; - } -}); -$tw.modules.define("$:/boot/tiddlerdeserializer/txt","tiddlerdeserializer",{ - "text/plain": function(text,fields,type) { - fields.text = text; - fields.type = type || "text/plain"; - return [fields]; - } -}); -$tw.modules.define("$:/boot/tiddlerdeserializer/html","tiddlerdeserializer",{ - "text/html": function(text,fields) { - fields.text = text; - fields.type = "text/html"; - return [fields]; - } -}); -$tw.modules.define("$:/boot/tiddlerdeserializer/json","tiddlerdeserializer",{ - "application/json": function(text,fields) { - var isTiddlerValid = function(data) { - // Not valid if it's not an object with a title property - if(typeof(data) !== "object" || !$tw.utils.hop(data,"title")) { - return false; - } - for(var f in data) { - if($tw.utils.hop(data,f)) { - // Check field name doesn't contain control characters - if(typeof(data[f]) !== "string" || /[\x00-\x1F]/.test(f)) { - return false; + }); + $tw.modules.define("$:/boot/tiddlerdeserializer/txt","tiddlerdeserializer",{ + "text/plain": function(text,fields,type) { + fields.text = text; + fields.type = type || "text/plain"; + return [fields]; + } + }); + $tw.modules.define("$:/boot/tiddlerdeserializer/html","tiddlerdeserializer",{ + "text/html": function(text,fields) { + fields.text = text; + fields.type = "text/html"; + return [fields]; + } + }); + $tw.modules.define("$:/boot/tiddlerdeserializer/json","tiddlerdeserializer",{ + "application/json": function(text,fields) { + var isTiddlerValid = function(data) { + // Not valid if it's not an object with a title property + if(typeof(data) !== "object" || !$tw.utils.hop(data,"title")) { + return false; + } + for(var f in data) { + if($tw.utils.hop(data,f)) { + // Check field name doesn't contain control characters + if(typeof(data[f]) !== "string" || /[\x00-\x1F]/.test(f)) { + return false; + } } } - } - return true; - }, - isTiddlerArrayValid = function(data) { - for(var t=0; t<data.length; t++) { - if(!isTiddlerValid(data[t])) { - return false; + return true; + }, + isTiddlerArrayValid = function(data) { + for(var t=0; t<data.length; t++) { + if(!isTiddlerValid(data[t])) { + return false; + } } - } - return true; - }, - data = $tw.utils.parseJSONSafe(text); - if($tw.utils.isArray(data) && isTiddlerArrayValid(data)) { - if($tw.CSE.launched) { - for(var t = 0; t < data.length; t++) { - if($tw.utils.hop(data[t], "encrypted")) { - var decryptedFields = $tw.CSE.decryptFields(data[t]) - if(decryptedFields) { - if($tw.utils.hop(data[t], "revision") && $tw.utils.hop(data[t], "bag")) - decryptedFields = $tw.utils.extend(decryptedFields, { - revision: data[t].revision, - bag: data[t].bag - }) - data[t] = decryptedFields + return true; + }, + data = $tw.utils.parseJSONSafe(text); + if($tw.utils.isArray(data) && isTiddlerArrayValid(data)) { + if($tw.CSE.launched) { + for(var t = 0; t < data.length; t++) { + if($tw.utils.hop(data[t], "encrypted")) { + var decryptedFields = $tw.CSE.decryptFields(data[t]) + if(decryptedFields) { + if($tw.utils.hop(data[t], "revision") && $tw.utils.hop(data[t], "bag")) + decryptedFields = $tw.utils.extend(decryptedFields, { + revision: data[t].revision, + bag: data[t].bag + }) + data[t] = decryptedFields + } } } } + return data; + } else if(isTiddlerValid(data)) { + return [data]; + } else { + // Plain JSON file + fields.text = text; + fields.type = "application/json"; + return [fields]; } - return data; - } else if(isTiddlerValid(data)) { - return [data]; - } else { - // Plain JSON file - fields.text = text; - fields.type = "application/json"; - return [fields]; } - } -}); - -/////////////////////////// Browser definitions - -if($tw.browser && !$tw.node) { - -/* -Decrypt any tiddlers stored within the element with the ID "encryptedArea". The function is asynchronous to allow the user to be prompted for a password - callback: function to be called the decryption is complete -*/ -$tw.boot.decryptEncryptedTiddlers = function(callback) { - var encryptedArea = document.getElementById("encryptedStoreArea"); - if(encryptedArea) { - var encryptedText = encryptedArea.innerHTML, - prompt = "Enter a password to decrypt this TiddlyWiki"; - // Prompt for the password - if($tw.utils.hop($tw.boot,"encryptionPrompts")) { - prompt = $tw.boot.encryptionPrompts.decrypt; - } - $tw.passwordPrompt.createPrompt({ - serviceName: prompt, - noUserName: true, - submitText: "Decrypt", - callback: function(data) { - // Attempt to decrypt the tiddlers - $tw.crypto.setPassword(data.password); - var decryptedText = $tw.crypto.decrypt(encryptedText); - if(decryptedText) { - var json = $tw.utils.parseJSONSafe(decryptedText); - for(var title in json) { - $tw.preloadTiddler(json[title]); + }); + + /////////////////////////// Browser definitions + + if($tw.browser && !$tw.node) { + + /* + Decrypt any tiddlers stored within the element with the ID "encryptedArea". The function is asynchronous to allow the user to be prompted for a password + callback: function to be called the decryption is complete + */ + $tw.boot.decryptEncryptedTiddlers = function(callback) { + var encryptedArea = document.getElementById("encryptedStoreArea"); + if(encryptedArea) { + var encryptedText = encryptedArea.innerHTML, + prompt = "Enter a password to decrypt this TiddlyWiki"; + // Prompt for the password + if($tw.utils.hop($tw.boot,"encryptionPrompts")) { + prompt = $tw.boot.encryptionPrompts.decrypt; + } + $tw.passwordPrompt.createPrompt({ + serviceName: prompt, + noUserName: true, + submitText: "Decrypt", + callback: function(data) { + // Attempt to decrypt the tiddlers + $tw.crypto.setPassword(data.password); + var decryptedText = $tw.crypto.decrypt(encryptedText); + if(decryptedText) { + var json = $tw.utils.parseJSONSafe(decryptedText); + for(var title in json) { + $tw.preloadTiddler(json[title]); + } + // Call the callback + callback(); + // Exit and remove the password prompt + return true; + } else { + // We didn't decrypt everything, so continue to prompt for password + return false; } - // Call the callback + } + }); + } else { + var CSEncryptedMetaArea = document.getElementById("CSEncryptedMetaArea"); + if(CSEncryptedMetaArea){ + var CSEncryptedText = CSEncryptedMetaArea.innerHTML, + prompt = "CSE: Enter a password to decrypt this TiddlyWiki"; + var CSEncryptedInfo = JSON.parse(CSEncryptedText) + + console.log(CSEncryptedMetaArea) + var pwdCallback = function (data) { + $tw.CSE.setPassword(data.password); + $tw.CSE.launched = true callback(); - // Exit and remove the password prompt return true; + } + if(CSEncryptedInfo && CSEncryptedInfo.config && CSEncryptedInfo.config.RmbPwd === "yes" && window.localStorage.getItem("tw5-cse-pwd")){ + pwdCallback({password:window.localStorage.getItem("tw5-cse-pwd")}) } else { - // We didn't decrypt everything, so continue to prompt for password - return false; + $tw.passwordPrompt.createPrompt({ + serviceName: prompt, + noUserName: true, + submitText: "Decrypt", + callback: pwdCallback + }); } - } - }); - } else { - var CSEncryptedMetaArea = document.getElementById("CSEncryptedMetaArea"); - if(CSEncryptedMetaArea){ - var CSEncryptedText = CSEncryptedMetaArea.innerHTML, - prompt = "CSE: Enter a password to decrypt this TiddlyWiki"; - var CSEncryptedInfo = JSON.parse(CSEncryptedText) - - console.log(CSEncryptedMetaArea) - var pwdCallback = function (data) { - $tw.CSE.setPassword(data.password); - $tw.CSE.launched = true - callback(); - return true; - } - if(CSEncryptedInfo && CSEncryptedInfo.config && CSEncryptedInfo.config.RmbPwd === "yes" && window.localStorage.getItem("tw5-cse-pwd")){ - pwdCallback({password:window.localStorage.getItem("tw5-cse-pwd")}) } else { - $tw.passwordPrompt.createPrompt({ - serviceName: prompt, - noUserName: true, - submitText: "Decrypt", - callback: pwdCallback - }); + // Just invoke the callback straight away if there weren't any encrypted tiddlers + callback(); + } + } + }; + + /* + Register a deserializer that can extract tiddlers from the DOM + */ + $tw.modules.define("$:/boot/tiddlerdeserializer/dom","tiddlerdeserializer",{ + "(DOM)": function(node) { + var extractTextTiddlers = function(node) { + var e = node.firstChild; + while(e && e.nodeName.toLowerCase() !== "pre") { + e = e.nextSibling; + } + var title = node.getAttribute ? node.getAttribute("title") : null; + if(e && title) { + var attrs = node.attributes, + tiddler = { + text: $tw.utils.htmlDecode(e.innerHTML) + }; + for(var i=attrs.length-1; i >= 0; i--) { + tiddler[attrs[i].name] = attrs[i].value; + } + return [tiddler]; + } else { + return null; + } + }, + extractModuleTiddlers = function(node) { + if(node.hasAttribute && node.hasAttribute("data-tiddler-title")) { + var text = node.innerHTML, + s = text.indexOf("{"), + e = text.lastIndexOf("}"); + if(node.hasAttribute("data-module") && s !== -1 && e !== -1) { + text = text.substring(s+1,e); + } + var fields = {text: text}, + attributes = node.attributes; + for(var a=0; a<attributes.length; a++) { + if(attributes[a].nodeName.substr(0,13) === "data-tiddler-") { + fields[attributes[a].nodeName.substr(13)] = attributes[a].value; + } + } + return [fields]; + } else { + return null; + } + }, + t,result = []; + if(node) { + var type = (node.getAttribute && node.getAttribute("type")) || null; + if(type) { + // A new-style container with an explicit deserialization type + result = $tw.wiki.deserializeTiddlers(type,node.textContent); + } else { + // An old-style container of classic DIV-based tiddlers + for(t = 0; t < node.childNodes.length; t++) { + var childNode = node.childNodes[t], + tiddlers = extractTextTiddlers(childNode); + tiddlers = tiddlers || extractModuleTiddlers(childNode); + if(tiddlers) { + result.push.apply(result,tiddlers); + } + } + } + } + return result; + } + }); + + $tw.loadTiddlersBrowser = function() { + // In the browser, we load tiddlers from certain elements + var containerSelectors = [ + // IDs for old-style v5.1.x tiddler stores + "#libraryModules", + "#modules", + "#bootKernelPrefix", + "#bootKernel", + "#styleArea", + "#storeArea", + "#systemArea", + // Classes for new-style v5.2.x JSON tiddler stores + "script.tiddlywiki-tiddler-store" + ]; + for(var t=0; t<containerSelectors.length; t++) { + var nodes = document.querySelectorAll(containerSelectors[t]); + for(var n=0; n<nodes.length; n++) { + $tw.wiki.addTiddlers($tw.wiki.deserializeTiddlers("(DOM)",nodes[n])); } + } + }; + + } else { + + /////////////////////////// Server definitions + + /* + Get any encrypted tiddlers + */ + $tw.boot.decryptEncryptedTiddlers = function(callback) { + // Storing encrypted tiddlers on the server isn't supported yet + callback(); + }; + + } // End of if($tw.browser && !$tw.node) + + /////////////////////////// Node definitions + + if($tw.node) { + + /* + Load the tiddlers contained in a particular file (and optionally extract fields from the accompanying .meta file) returned as {filepath:,type:,tiddlers:[],hasMetaFile:} + */ + $tw.loadTiddlersFromFile = function(filepath,fields) { + var ext = path.extname(filepath), + extensionInfo = $tw.utils.getFileExtensionInfo(ext), + type = extensionInfo ? extensionInfo.type : null, + typeInfo = type ? $tw.config.contentTypeInfo[type] : null, + data = fs.readFileSync(filepath,typeInfo ? typeInfo.encoding : "utf8"), + tiddlers = $tw.wiki.deserializeTiddlers(ext,data,fields), + metadata = $tw.loadMetadataForFile(filepath); + if(metadata) { + if(type === "application/json") { + tiddlers = [{text: data, type: "application/json"}]; + } + tiddlers = [$tw.utils.extend({},tiddlers[0],metadata)]; + } + return {filepath: filepath, type: type, tiddlers: tiddlers, hasMetaFile: !!metadata}; + }; + + /* + Load the metadata fields in the .meta file corresponding to a particular file + */ + $tw.loadMetadataForFile = function(filepath) { + var metafilename = filepath + ".meta"; + if(fs.existsSync(metafilename)) { + return $tw.utils.parseFields(fs.readFileSync(metafilename,"utf8") || ""); } else { - // Just invoke the callback straight away if there weren't any encrypted tiddlers - callback(); + return null; } - } -}; - -/* -Register a deserializer that can extract tiddlers from the DOM -*/ -$tw.modules.define("$:/boot/tiddlerdeserializer/dom","tiddlerdeserializer",{ - "(DOM)": function(node) { - var extractTextTiddlers = function(node) { - var e = node.firstChild; - while(e && e.nodeName.toLowerCase() !== "pre") { - e = e.nextSibling; - } - var title = node.getAttribute ? node.getAttribute("title") : null; - if(e && title) { - var attrs = node.attributes, - tiddler = { - text: $tw.utils.htmlDecode(e.innerHTML) - }; - for(var i=attrs.length-1; i >= 0; i--) { - tiddler[attrs[i].name] = attrs[i].value; - } - return [tiddler]; + }; + + /* + A default set of files for TiddlyWiki to ignore during load. + This matches what NPM ignores, and adds "*.meta" to ignore tiddler + metadata files. + */ + $tw.boot.excludeRegExp = /^\.DS_Store$|^.*\.meta$|^\..*\.swp$|^\._.*$|^\.git$|^\.github$|^\.vscode$|^\.hg$|^\.lock-wscript$|^\.svn$|^\.wafpickle-.*$|^CVS$|^npm-debug\.log$/; + + /* + Load all the tiddlers recursively from a directory, including honouring `tiddlywiki.files` files for drawing in external files. Returns an array of {filepath:,type:,tiddlers: [{..fields...}],hasMetaFile:}. Note that no file information is returned for externally loaded tiddlers, just the `tiddlers` property. + */ + $tw.loadTiddlersFromPath = function(filepath,excludeRegExp) { + excludeRegExp = excludeRegExp || $tw.boot.excludeRegExp; + var tiddlers = []; + if(fs.existsSync(filepath)) { + var stat = fs.statSync(filepath); + if(stat.isDirectory()) { + var files = fs.readdirSync(filepath); + // Look for a tiddlywiki.files file + if(files.indexOf("tiddlywiki.files") !== -1) { + Array.prototype.push.apply(tiddlers,$tw.loadTiddlersFromSpecification(filepath,excludeRegExp)); } else { - return null; + // If not, read all the files in the directory + $tw.utils.each(files,function(file) { + if(!excludeRegExp.test(file) && file !== "plugin.info") { + tiddlers.push.apply(tiddlers,$tw.loadTiddlersFromPath(filepath + path.sep + file,excludeRegExp)); + } + }); } - }, - extractModuleTiddlers = function(node) { - if(node.hasAttribute && node.hasAttribute("data-tiddler-title")) { - var text = node.innerHTML, - s = text.indexOf("{"), - e = text.lastIndexOf("}"); - if(node.hasAttribute("data-module") && s !== -1 && e !== -1) { - text = text.substring(s+1,e); + } else if(stat.isFile()) { + tiddlers.push($tw.loadTiddlersFromFile(filepath,{title: filepath})); + } + } + return tiddlers; + }; + + /* + Load all the tiddlers defined by a `tiddlywiki.files` specification file + filepath: pathname of the directory containing the specification file + */ + $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) { + var tiddlers = []; + // Read the specification + var filesInfo = $tw.utils.parseJSONSafe(fs.readFileSync(filepath + path.sep + "tiddlywiki.files","utf8")); + // Helper to process a file + var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath) { + var extInfo = $tw.config.fileExtensionInfo[path.extname(filename)], + type = (extInfo || {}).type || fields.type || "text/plain", + typeInfo = $tw.config.contentTypeInfo[type] || {}, + pathname = path.resolve(filepath,filename), + text = fs.readFileSync(pathname,typeInfo.encoding || "utf8"), + metadata = $tw.loadMetadataForFile(pathname) || {}, + fileTiddlers; + if(isTiddlerFile) { + fileTiddlers = $tw.wiki.deserializeTiddlers(path.extname(pathname),text,metadata) || []; + } else { + fileTiddlers = [$tw.utils.extend({text: text},metadata)]; + } + var combinedFields = $tw.utils.extend({},fields,metadata); + $tw.utils.each(fileTiddlers,function(tiddler) { + $tw.utils.each(combinedFields,function(fieldInfo,name) { + if(typeof fieldInfo === "string" || $tw.utils.isArray(fieldInfo)) { + tiddler[name] = fieldInfo; + } else { + var value = tiddler[name]; + switch(fieldInfo.source) { + case "subdirectories": + value = path.relative(rootPath, filename).split(path.sep).slice(0, -1); + break; + case "filepath": + value = path.relative(rootPath, filename).split(path.sep).join('/'); + break; + case "filename": + value = path.basename(filename); + break; + case "filename-uri-decoded": + value = $tw.utils.decodeURIComponentSafe(path.basename(filename)); + break; + case "basename": + value = path.basename(filename,path.extname(filename)); + break; + case "basename-uri-decoded": + value = $tw.utils.decodeURIComponentSafe(path.basename(filename,path.extname(filename))); + break; + case "extname": + value = path.extname(filename); + break; + case "created": + value = new Date(fs.statSync(pathname).birthtime); + break; + case "modified": + value = new Date(fs.statSync(pathname).mtime); + break; + } + if(fieldInfo.prefix) { + value = fieldInfo.prefix + value; + } + if(fieldInfo.suffix) { + value = value + fieldInfo.suffix; + } + tiddler[name] = value; } - var fields = {text: text}, - attributes = node.attributes; - for(var a=0; a<attributes.length; a++) { - if(attributes[a].nodeName.substr(0,13) === "data-tiddler-") { - fields[attributes[a].nodeName.substr(13)] = attributes[a].value; + }); + }); + if(isEditableFile) { + tiddlers.push({filepath: pathname, hasMetaFile: !!metadata && !isTiddlerFile, isEditableFile: true, tiddlers: fileTiddlers}); + } else { + tiddlers.push({tiddlers: fileTiddlers}); + } + }; + // Helper to recursively search subdirectories + var getAllFiles = function(dirPath, recurse, arrayOfFiles) { + recurse = recurse || false; + arrayOfFiles = arrayOfFiles || []; + var files = fs.readdirSync(dirPath); + files.forEach(function(file) { + if(recurse && fs.statSync(dirPath + path.sep + file).isDirectory()) { + arrayOfFiles = getAllFiles(dirPath + path.sep + file, recurse, arrayOfFiles); + } else if(fs.statSync(dirPath + path.sep + file).isFile()){ + arrayOfFiles.push(path.join(dirPath, path.sep, file)); + } + }); + return arrayOfFiles; + } + // Process the listed tiddlers + $tw.utils.each(filesInfo.tiddlers,function(tidInfo) { + if(tidInfo.prefix && tidInfo.suffix) { + tidInfo.fields.text = {prefix: tidInfo.prefix,suffix: tidInfo.suffix}; + } else if(tidInfo.prefix) { + tidInfo.fields.text = {prefix: tidInfo.prefix}; + } else if(tidInfo.suffix) { + tidInfo.fields.text = {suffix: tidInfo.suffix}; + } + processFile(tidInfo.file,tidInfo.isTiddlerFile,tidInfo.fields); + }); + // Process any listed directories + $tw.utils.each(filesInfo.directories,function(dirSpec) { + // Read literal directories directly + if(typeof dirSpec === "string") { + var pathname = path.resolve(filepath,dirSpec); + if(fs.existsSync(pathname) && fs.statSync(pathname).isDirectory()) { + tiddlers.push.apply(tiddlers,$tw.loadTiddlersFromPath(pathname,excludeRegExp)); + } + } else { + // Process directory specifier + var dirPath = path.resolve(filepath,dirSpec.path); + if(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) { + var files = getAllFiles(dirPath, dirSpec.searchSubdirectories), + fileRegExp = new RegExp(dirSpec.filesRegExp || "^.*$"), + metaRegExp = /^.*\.meta$/; + for(var t=0; t<files.length; t++) { + var thisPath = path.relative(filepath, files[t]), + filename = path.basename(thisPath); + if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) { + processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile,dirSpec.path); } } - return [fields]; } else { - return null; + console.log("Warning: a directory in a tiddlywiki.files file does not exist."); + console.log("dirPath: " + dirPath); + console.log("tiddlywiki.files location: " + filepath); } - }, - t,result = []; - if(node) { - var type = (node.getAttribute && node.getAttribute("type")) || null; - if(type) { - // A new-style container with an explicit deserialization type - result = $tw.wiki.deserializeTiddlers(type,node.textContent); - } else { - // An old-style container of classic DIV-based tiddlers - for(t = 0; t < node.childNodes.length; t++) { - var childNode = node.childNodes[t], - tiddlers = extractTextTiddlers(childNode); - tiddlers = tiddlers || extractModuleTiddlers(childNode); - if(tiddlers) { - result.push.apply(result,tiddlers); + } + }); + return tiddlers; + }; + + /* + Load the tiddlers from a plugin folder, and package them up into a proper JSON plugin tiddler + */ + $tw.loadPluginFolder = function(filepath,excludeRegExp) { + excludeRegExp = excludeRegExp || $tw.boot.excludeRegExp; + var infoPath = filepath + path.sep + "plugin.info"; + if(fs.existsSync(filepath) && fs.statSync(filepath).isDirectory()) { + // Read the plugin information + if(!fs.existsSync(infoPath) || !fs.statSync(infoPath).isFile()) { + console.log("Warning: missing plugin.info file in " + filepath); + return null; + } + var pluginInfo = $tw.utils.parseJSONSafe(fs.readFileSync(infoPath,"utf8"),function() {return null;}); + if(!pluginInfo) { + console.log("warning: invalid JSON in plugin.info file at " + infoPath); + pluginInfo = {}; + } + // Read the plugin files + var pluginFiles = $tw.loadTiddlersFromPath(filepath,excludeRegExp); + // Save the plugin tiddlers into the plugin info + pluginInfo.tiddlers = pluginInfo.tiddlers || Object.create(null); + for(var f=0; f<pluginFiles.length; f++) { + var tiddlers = pluginFiles[f].tiddlers; + for(var t=0; t<tiddlers.length; t++) { + var tiddler= tiddlers[t]; + if(tiddler.title) { + pluginInfo.tiddlers[tiddler.title] = tiddler; } } } + // Give the plugin the same version number as the core if it doesn't have one + if(!("version" in pluginInfo)) { + pluginInfo.version = $tw.packageInfo.version; + } + // Use "plugin" as the plugin-type if we don't have one + if(!("plugin-type" in pluginInfo)) { + pluginInfo["plugin-type"] = "plugin"; + } + pluginInfo.dependents = pluginInfo.dependents || []; + pluginInfo.type = "application/json"; + // Set plugin text + pluginInfo.text = JSON.stringify({tiddlers: pluginInfo.tiddlers}); + delete pluginInfo.tiddlers; + // Deserialise array fields (currently required for the dependents field) + for(var field in pluginInfo) { + if($tw.utils.isArray(pluginInfo[field])) { + pluginInfo[field] = $tw.utils.stringifyList(pluginInfo[field]); + } + } + return pluginInfo; + } else { + return null; } - return result; - } -}); - -$tw.loadTiddlersBrowser = function() { - // In the browser, we load tiddlers from certain elements - var containerSelectors = [ - // IDs for old-style v5.1.x tiddler stores - "#libraryModules", - "#modules", - "#bootKernelPrefix", - "#bootKernel", - "#styleArea", - "#storeArea", - "#systemArea", - // Classes for new-style v5.2.x JSON tiddler stores - "script.tiddlywiki-tiddler-store" - ]; - for(var t=0; t<containerSelectors.length; t++) { - var nodes = document.querySelectorAll(containerSelectors[t]); - for(var n=0; n<nodes.length; n++) { - $tw.wiki.addTiddlers($tw.wiki.deserializeTiddlers("(DOM)",nodes[n])); - } - } -}; - -} else { - -/////////////////////////// Server definitions - -/* -Get any encrypted tiddlers -*/ -$tw.boot.decryptEncryptedTiddlers = function(callback) { - // Storing encrypted tiddlers on the server isn't supported yet - callback(); -}; - -} // End of if($tw.browser && !$tw.node) - -/////////////////////////// Node definitions - -if($tw.node) { - -/* -Load the tiddlers contained in a particular file (and optionally extract fields from the accompanying .meta file) returned as {filepath:,type:,tiddlers:[],hasMetaFile:} -*/ -$tw.loadTiddlersFromFile = function(filepath,fields) { - var ext = path.extname(filepath), - extensionInfo = $tw.utils.getFileExtensionInfo(ext), - type = extensionInfo ? extensionInfo.type : null, - typeInfo = type ? $tw.config.contentTypeInfo[type] : null, - data = fs.readFileSync(filepath,typeInfo ? typeInfo.encoding : "utf8"), - tiddlers = $tw.wiki.deserializeTiddlers(ext,data,fields), - metadata = $tw.loadMetadataForFile(filepath); - if(metadata) { - if(type === "application/json") { - tiddlers = [{text: data, type: "application/json"}]; - } - tiddlers = [$tw.utils.extend({},tiddlers[0],metadata)]; - } - return {filepath: filepath, type: type, tiddlers: tiddlers, hasMetaFile: !!metadata}; -}; - -/* -Load the metadata fields in the .meta file corresponding to a particular file -*/ -$tw.loadMetadataForFile = function(filepath) { - var metafilename = filepath + ".meta"; - if(fs.existsSync(metafilename)) { - return $tw.utils.parseFields(fs.readFileSync(metafilename,"utf8") || ""); - } else { + }; + + /* + name: Name of the plugin to find + paths: array of file paths to search for it + Returns the path of the plugin folder + */ + $tw.findLibraryItem = function(name,paths) { + var pathIndex = 0; + do { + var pluginPath = path.resolve(paths[pathIndex],"./" + name) + if(fs.existsSync(pluginPath) && fs.statSync(pluginPath).isDirectory()) { + return pluginPath; + } + } while(++pathIndex < paths.length); return null; - } -}; - -/* -A default set of files for TiddlyWiki to ignore during load. -This matches what NPM ignores, and adds "*.meta" to ignore tiddler -metadata files. -*/ -$tw.boot.excludeRegExp = /^\.DS_Store$|^.*\.meta$|^\..*\.swp$|^\._.*$|^\.git$|^\.github$|^\.vscode$|^\.hg$|^\.lock-wscript$|^\.svn$|^\.wafpickle-.*$|^CVS$|^npm-debug\.log$/; - -/* -Load all the tiddlers recursively from a directory, including honouring `tiddlywiki.files` files for drawing in external files. Returns an array of {filepath:,type:,tiddlers: [{..fields...}],hasMetaFile:}. Note that no file information is returned for externally loaded tiddlers, just the `tiddlers` property. -*/ -$tw.loadTiddlersFromPath = function(filepath,excludeRegExp) { - excludeRegExp = excludeRegExp || $tw.boot.excludeRegExp; - var tiddlers = []; - if(fs.existsSync(filepath)) { - var stat = fs.statSync(filepath); - if(stat.isDirectory()) { - var files = fs.readdirSync(filepath); - // Look for a tiddlywiki.files file - if(files.indexOf("tiddlywiki.files") !== -1) { - Array.prototype.push.apply(tiddlers,$tw.loadTiddlersFromSpecification(filepath,excludeRegExp)); - } else { - // If not, read all the files in the directory - $tw.utils.each(files,function(file) { - if(!excludeRegExp.test(file) && file !== "plugin.info") { - tiddlers.push.apply(tiddlers,$tw.loadTiddlersFromPath(filepath + path.sep + file,excludeRegExp)); + }; + + /* + name: Name of the plugin to load + paths: array of file paths to search for it + */ + $tw.loadPlugin = function(name,paths) { + var pluginPath = $tw.findLibraryItem(name,paths); + if(pluginPath) { + var pluginFields = $tw.loadPluginFolder(pluginPath); + if(pluginFields) { + $tw.wiki.addTiddler(pluginFields); + return; + } + } + console.log("Warning: Cannot find plugin '" + name + "'"); + }; + + /* + libraryPath: Path of library folder for these plugins (relative to core path) + envVar: Environment variable name for these plugins + Returns an array of search paths + */ + $tw.getLibraryItemSearchPaths = function(libraryPath,envVar) { + var pluginPaths = [path.resolve($tw.boot.corePath,libraryPath)], + env; + if(envVar) { + env = process.env[envVar]; + if(env) { + env.split(path.delimiter).map(function(item) { + if(item) { + pluginPaths.push(item); } }); } - } else if(stat.isFile()) { - tiddlers.push($tw.loadTiddlersFromFile(filepath,{title: filepath})); } - } - return tiddlers; -}; - -/* -Load all the tiddlers defined by a `tiddlywiki.files` specification file -filepath: pathname of the directory containing the specification file -*/ -$tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) { - var tiddlers = []; - // Read the specification - var filesInfo = $tw.utils.parseJSONSafe(fs.readFileSync(filepath + path.sep + "tiddlywiki.files","utf8")); - // Helper to process a file - var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath) { - var extInfo = $tw.config.fileExtensionInfo[path.extname(filename)], - type = (extInfo || {}).type || fields.type || "text/plain", - typeInfo = $tw.config.contentTypeInfo[type] || {}, - pathname = path.resolve(filepath,filename), - text = fs.readFileSync(pathname,typeInfo.encoding || "utf8"), - metadata = $tw.loadMetadataForFile(pathname) || {}, - fileTiddlers; - if(isTiddlerFile) { - fileTiddlers = $tw.wiki.deserializeTiddlers(path.extname(pathname),text,metadata) || []; + return pluginPaths; + }; + + /* + plugins: Array of names of plugins (eg, "tiddlywiki/filesystemadaptor") + libraryPath: Path of library folder for these plugins (relative to core path) + envVar: Environment variable name for these plugins + */ + $tw.loadPlugins = function(plugins,libraryPath,envVar) { + if(plugins) { + var pluginPaths = $tw.getLibraryItemSearchPaths(libraryPath,envVar); + for(var t=0; t<plugins.length; t++) { + $tw.loadPlugin(plugins[t],pluginPaths); + } + } + }; + + /* + path: path of wiki directory + options: + parentPaths: array of parent paths that we mustn't recurse into + readOnly: true if the tiddler file paths should not be retained + */ + $tw.loadWikiTiddlers = function(wikiPath,options) { + options = options || {}; + var parentPaths = options.parentPaths || [], + wikiInfoPath = path.resolve(wikiPath,$tw.config.wikiInfo), + wikiInfo, + pluginFields; + // Bail if we don't have a wiki info file + if(fs.existsSync(wikiInfoPath)) { + wikiInfo = $tw.utils.parseJSONSafe(fs.readFileSync(wikiInfoPath,"utf8"),function() {return null;}); + if(!wikiInfo) { + console.log("warning: invalid JSON in tiddlywiki.info file at " + wikiInfoPath); + wikiInfo = {}; + } } else { - fileTiddlers = [$tw.utils.extend({text: text},metadata)]; + return null; } - var combinedFields = $tw.utils.extend({},fields,metadata); - $tw.utils.each(fileTiddlers,function(tiddler) { - $tw.utils.each(combinedFields,function(fieldInfo,name) { - if(typeof fieldInfo === "string" || $tw.utils.isArray(fieldInfo)) { - tiddler[name] = fieldInfo; + // Save the path to the tiddlers folder for the filesystemadaptor + var config = wikiInfo.config || {}; + if($tw.boot.wikiPath == wikiPath) { + $tw.boot.wikiTiddlersPath = path.resolve($tw.boot.wikiPath,config["default-tiddler-location"] || $tw.config.wikiTiddlersSubDir); + } + // Load any parent wikis + if(wikiInfo.includeWikis) { + parentPaths = parentPaths.slice(0); + parentPaths.push(wikiPath); + $tw.utils.each(wikiInfo.includeWikis,function(info) { + if(typeof info === "string") { + info = {path: info}; + } + var resolvedIncludedWikiPath = path.resolve(wikiPath,info.path); + if(parentPaths.indexOf(resolvedIncludedWikiPath) === -1) { + var subWikiInfo = $tw.loadWikiTiddlers(resolvedIncludedWikiPath,{ + parentPaths: parentPaths, + readOnly: info["read-only"] + }); + // Merge the build targets + wikiInfo.build = $tw.utils.extend([],subWikiInfo.build,wikiInfo.build); } else { - var value = tiddler[name]; - switch(fieldInfo.source) { - case "subdirectories": - value = path.relative(rootPath, filename).split('/').slice(0, -1); - break; - case "filepath": - value = path.relative(rootPath, filename); - break; - case "filename": - value = path.basename(filename); - break; - case "filename-uri-decoded": - value = $tw.utils.decodeURIComponentSafe(path.basename(filename)); - break; - case "basename": - value = path.basename(filename,path.extname(filename)); - break; - case "basename-uri-decoded": - value = $tw.utils.decodeURIComponentSafe(path.basename(filename,path.extname(filename))); - break; - case "extname": - value = path.extname(filename); - break; - case "created": - value = new Date(fs.statSync(pathname).birthtime); - break; - case "modified": - value = new Date(fs.statSync(pathname).mtime); - break; - } - if(fieldInfo.prefix) { - value = fieldInfo.prefix + value; - } - if(fieldInfo.suffix) { - value = value + fieldInfo.suffix; - } - tiddler[name] = value; + $tw.utils.error("Cannot recursively include wiki " + resolvedIncludedWikiPath); } }); - }); - if(isEditableFile) { - tiddlers.push({filepath: pathname, hasMetaFile: !!metadata && !isTiddlerFile, isEditableFile: true, tiddlers: fileTiddlers}); - } else { - tiddlers.push({tiddlers: fileTiddlers}); } - }; - // Helper to recursively search subdirectories - var getAllFiles = function(dirPath, recurse, arrayOfFiles) { - recurse = recurse || false; - arrayOfFiles = arrayOfFiles || []; - var files = fs.readdirSync(dirPath); - files.forEach(function(file) { - if (recurse && fs.statSync(dirPath + path.sep + file).isDirectory()) { - arrayOfFiles = getAllFiles(dirPath + path.sep + file, recurse, arrayOfFiles); - } else if(fs.statSync(dirPath + path.sep + file).isFile()){ - arrayOfFiles.push(path.join(dirPath, path.sep, file)); + // Load any plugins, themes and languages listed in the wiki info file + $tw.loadPlugins(wikiInfo.plugins,$tw.config.pluginsPath,$tw.config.pluginsEnvVar); + $tw.loadPlugins(wikiInfo.themes,$tw.config.themesPath,$tw.config.themesEnvVar); + $tw.loadPlugins(wikiInfo.languages,$tw.config.languagesPath,$tw.config.languagesEnvVar); + // Load the wiki files, registering them as writable + var resolvedWikiPath = path.resolve(wikiPath,$tw.config.wikiTiddlersSubDir); + $tw.utils.each($tw.loadTiddlersFromPath(resolvedWikiPath),function(tiddlerFile) { + if(!options.readOnly && tiddlerFile.filepath) { + $tw.utils.each(tiddlerFile.tiddlers,function(tiddler) { + $tw.boot.files[tiddler.title] = { + filepath: tiddlerFile.filepath, + type: tiddlerFile.type, + hasMetaFile: tiddlerFile.hasMetaFile, + isEditableFile: config["retain-original-tiddler-path"] || tiddlerFile.isEditableFile || tiddlerFile.filepath.indexOf($tw.boot.wikiTiddlersPath) !== 0 + }; + }); } + $tw.wiki.addTiddlers(tiddlerFile.tiddlers); }); - return arrayOfFiles; - } - // Process the listed tiddlers - $tw.utils.each(filesInfo.tiddlers,function(tidInfo) { - if(tidInfo.prefix && tidInfo.suffix) { - tidInfo.fields.text = {prefix: tidInfo.prefix,suffix: tidInfo.suffix}; - } else if(tidInfo.prefix) { - tidInfo.fields.text = {prefix: tidInfo.prefix}; - } else if(tidInfo.suffix) { - tidInfo.fields.text = {suffix: tidInfo.suffix}; - } - processFile(tidInfo.file,tidInfo.isTiddlerFile,tidInfo.fields); - }); - // Process any listed directories - $tw.utils.each(filesInfo.directories,function(dirSpec) { - // Read literal directories directly - if(typeof dirSpec === "string") { - var pathname = path.resolve(filepath,dirSpec); - if(fs.existsSync(pathname) && fs.statSync(pathname).isDirectory()) { - tiddlers.push.apply(tiddlers,$tw.loadTiddlersFromPath(pathname,excludeRegExp)); - } - } else { - // Process directory specifier - var dirPath = path.resolve(filepath,dirSpec.path); - if(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) { - var files = getAllFiles(dirPath, dirSpec.searchSubdirectories), - fileRegExp = new RegExp(dirSpec.filesRegExp || "^.*$"), - metaRegExp = /^.*\.meta$/; - for(var t=0; t<files.length; t++) { - var thisPath = path.relative(filepath, files[t]), - filename = path.basename(thisPath); - if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) { - processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile,dirSpec.path); - } + if($tw.boot.wikiPath == wikiPath) { + // Save the original tiddler file locations if requested + var output = {}, relativePath, fileInfo; + for(var title in $tw.boot.files) { + fileInfo = $tw.boot.files[title]; + if(fileInfo.isEditableFile) { + relativePath = path.relative($tw.boot.wikiTiddlersPath,fileInfo.filepath); + fileInfo.originalpath = relativePath; + output[title] = + path.sep === "/" ? + relativePath : + relativePath.split(path.sep).join("/"); } - } else { - console.log("Warning: a directory in a tiddlywiki.files file does not exist."); - console.log("dirPath: " + dirPath); - console.log("tiddlywiki.files location: " + filepath); + } + if(Object.keys(output).length > 0){ + $tw.wiki.addTiddler({title: "$:/config/OriginalTiddlerPaths", type: "application/json", text: JSON.stringify(output)}); } } - }); - return tiddlers; -}; - -/* -Load the tiddlers from a plugin folder, and package them up into a proper JSON plugin tiddler -*/ -$tw.loadPluginFolder = function(filepath,excludeRegExp) { - excludeRegExp = excludeRegExp || $tw.boot.excludeRegExp; - var infoPath = filepath + path.sep + "plugin.info"; - if(fs.existsSync(filepath) && fs.statSync(filepath).isDirectory()) { - // Read the plugin information - if(!fs.existsSync(infoPath) || !fs.statSync(infoPath).isFile()) { - console.log("Warning: missing plugin.info file in " + filepath); - return null; - } - var pluginInfo = $tw.utils.parseJSONSafe(fs.readFileSync(infoPath,"utf8"),function() {return null;}); - if(!pluginInfo) { - console.log("warning: invalid JSON in plugin.info file at " + infoPath); - pluginInfo = {}; - } - // Read the plugin files - var pluginFiles = $tw.loadTiddlersFromPath(filepath,excludeRegExp); - // Save the plugin tiddlers into the plugin info - pluginInfo.tiddlers = pluginInfo.tiddlers || Object.create(null); - for(var f=0; f<pluginFiles.length; f++) { - var tiddlers = pluginFiles[f].tiddlers; - for(var t=0; t<tiddlers.length; t++) { - var tiddler= tiddlers[t]; - if(tiddler.title) { - pluginInfo.tiddlers[tiddler.title] = tiddler; + // Load any plugins within the wiki folder + var wikiPluginsPath = path.resolve(wikiPath,$tw.config.wikiPluginsSubDir); + if(fs.existsSync(wikiPluginsPath)) { + var pluginFolders = fs.readdirSync(wikiPluginsPath); + for(var t=0; t<pluginFolders.length; t++) { + pluginFields = $tw.loadPluginFolder(path.resolve(wikiPluginsPath,"./" + pluginFolders[t])); + if(pluginFields) { + $tw.wiki.addTiddler(pluginFields); } } } - // Give the plugin the same version number as the core if it doesn't have one - if(!("version" in pluginInfo)) { - pluginInfo.version = $tw.packageInfo.version; - } - // Use "plugin" as the plugin-type if we don't have one - if(!("plugin-type" in pluginInfo)) { - pluginInfo["plugin-type"] = "plugin"; - } - pluginInfo.dependents = pluginInfo.dependents || []; - pluginInfo.type = "application/json"; - // Set plugin text - pluginInfo.text = JSON.stringify({tiddlers: pluginInfo.tiddlers}); - delete pluginInfo.tiddlers; - // Deserialise array fields (currently required for the dependents field) - for(var field in pluginInfo) { - if($tw.utils.isArray(pluginInfo[field])) { - pluginInfo[field] = $tw.utils.stringifyList(pluginInfo[field]); + // Load any themes within the wiki folder + var wikiThemesPath = path.resolve(wikiPath,$tw.config.wikiThemesSubDir); + if(fs.existsSync(wikiThemesPath)) { + var themeFolders = fs.readdirSync(wikiThemesPath); + for(var t=0; t<themeFolders.length; t++) { + pluginFields = $tw.loadPluginFolder(path.resolve(wikiThemesPath,"./" + themeFolders[t])); + if(pluginFields) { + $tw.wiki.addTiddler(pluginFields); + } } } - return pluginInfo; - } else { - return null; - } -}; - -/* -name: Name of the plugin to find -paths: array of file paths to search for it -Returns the path of the plugin folder -*/ -$tw.findLibraryItem = function(name,paths) { - var pathIndex = 0; - do { - var pluginPath = path.resolve(paths[pathIndex],"./" + name) - if(fs.existsSync(pluginPath) && fs.statSync(pluginPath).isDirectory()) { - return pluginPath; - } - } while(++pathIndex < paths.length); - return null; -}; - -/* -name: Name of the plugin to load -paths: array of file paths to search for it -*/ -$tw.loadPlugin = function(name,paths) { - var pluginPath = $tw.findLibraryItem(name,paths); - if(pluginPath) { - var pluginFields = $tw.loadPluginFolder(pluginPath); - if(pluginFields) { - $tw.wiki.addTiddler(pluginFields); - return; - } - } - console.log("Warning: Cannot find plugin '" + name + "'"); -}; - -/* -libraryPath: Path of library folder for these plugins (relative to core path) -envVar: Environment variable name for these plugins -Returns an array of search paths -*/ -$tw.getLibraryItemSearchPaths = function(libraryPath,envVar) { - var pluginPaths = [path.resolve($tw.boot.corePath,libraryPath)], - env = process.env[envVar]; - if(env) { - env.split(path.delimiter).map(function(item) { - if(item) { - pluginPaths.push(item); + // Load any languages within the wiki folder + var wikiLanguagesPath = path.resolve(wikiPath,$tw.config.wikiLanguagesSubDir); + if(fs.existsSync(wikiLanguagesPath)) { + var languageFolders = fs.readdirSync(wikiLanguagesPath); + for(var t=0; t<languageFolders.length; t++) { + pluginFields = $tw.loadPluginFolder(path.resolve(wikiLanguagesPath,"./" + languageFolders[t])); + if(pluginFields) { + $tw.wiki.addTiddler(pluginFields); + } } - }); - } - return pluginPaths; -}; - -/* -plugins: Array of names of plugins (eg, "tiddlywiki/filesystemadaptor") -libraryPath: Path of library folder for these plugins (relative to core path) -envVar: Environment variable name for these plugins -*/ -$tw.loadPlugins = function(plugins,libraryPath,envVar) { - if(plugins) { - var pluginPaths = $tw.getLibraryItemSearchPaths(libraryPath,envVar); - for(var t=0; t<plugins.length; t++) { - $tw.loadPlugin(plugins[t],pluginPaths); - } - } -}; - -/* -path: path of wiki directory -options: - parentPaths: array of parent paths that we mustn't recurse into - readOnly: true if the tiddler file paths should not be retained -*/ -$tw.loadWikiTiddlers = function(wikiPath,options) { - options = options || {}; - var parentPaths = options.parentPaths || [], - wikiInfoPath = path.resolve(wikiPath,$tw.config.wikiInfo), - wikiInfo, - pluginFields; - // Bail if we don't have a wiki info file - if(fs.existsSync(wikiInfoPath)) { - wikiInfo = $tw.utils.parseJSONSafe(fs.readFileSync(wikiInfoPath,"utf8"),function() {return null;}); - if(!wikiInfo) { - console.log("warning: invalid JSON in tiddlywiki.info file at " + wikiInfoPath); - wikiInfo = {}; } - } else { - return null; - } - // Save the path to the tiddlers folder for the filesystemadaptor - var config = wikiInfo.config || {}; - if($tw.boot.wikiPath == wikiPath) { - $tw.boot.wikiTiddlersPath = path.resolve($tw.boot.wikiPath,config["default-tiddler-location"] || $tw.config.wikiTiddlersSubDir); - } - // Load any parent wikis - if(wikiInfo.includeWikis) { - parentPaths = parentPaths.slice(0); - parentPaths.push(wikiPath); - $tw.utils.each(wikiInfo.includeWikis,function(info) { - if(typeof info === "string") { - info = {path: info}; - } - var resolvedIncludedWikiPath = path.resolve(wikiPath,info.path); - if(parentPaths.indexOf(resolvedIncludedWikiPath) === -1) { - var subWikiInfo = $tw.loadWikiTiddlers(resolvedIncludedWikiPath,{ - parentPaths: parentPaths, - readOnly: info["read-only"] - }); - // Merge the build targets - wikiInfo.build = $tw.utils.extend([],subWikiInfo.build,wikiInfo.build); + return wikiInfo; + }; + + $tw.loadTiddlersNode = function() { + // Load the boot tiddlers + $tw.utils.each($tw.loadTiddlersFromPath($tw.boot.bootPath),function(tiddlerFile) { + $tw.wiki.addTiddlers(tiddlerFile.tiddlers); + }); + // Load the core tiddlers + $tw.wiki.addTiddler($tw.loadPluginFolder($tw.boot.corePath)); + // Load any extra plugins + $tw.utils.each($tw.boot.extraPlugins,function(name) { + if(name.charAt(0) === "+") { // Relative path to plugin + var pluginFields = $tw.loadPluginFolder(name.substring(1)); + if(pluginFields) { + $tw.wiki.addTiddler(pluginFields); + } } else { - $tw.utils.error("Cannot recursively include wiki " + resolvedIncludedWikiPath); + var parts = name.split("/"), + type = parts[0]; + if(parts.length === 3 && ["plugins","themes","languages"].indexOf(type) !== -1) { + $tw.loadPlugins([parts[1] + "/" + parts[2]],$tw.config[type + "Path"],$tw.config[type + "EnvVar"]); + } } }); - } - // Load any plugins, themes and languages listed in the wiki info file - $tw.loadPlugins(wikiInfo.plugins,$tw.config.pluginsPath,$tw.config.pluginsEnvVar); - $tw.loadPlugins(wikiInfo.themes,$tw.config.themesPath,$tw.config.themesEnvVar); - $tw.loadPlugins(wikiInfo.languages,$tw.config.languagesPath,$tw.config.languagesEnvVar); - // Load the wiki files, registering them as writable - var resolvedWikiPath = path.resolve(wikiPath,$tw.config.wikiTiddlersSubDir); - $tw.utils.each($tw.loadTiddlersFromPath(resolvedWikiPath),function(tiddlerFile) { - if(!options.readOnly && tiddlerFile.filepath) { - $tw.utils.each(tiddlerFile.tiddlers,function(tiddler) { - $tw.boot.files[tiddler.title] = { - filepath: tiddlerFile.filepath, - type: tiddlerFile.type, - hasMetaFile: tiddlerFile.hasMetaFile, - isEditableFile: config["retain-original-tiddler-path"] || tiddlerFile.isEditableFile || tiddlerFile.filepath.indexOf($tw.boot.wikiTiddlersPath) !== 0 - }; - }); - } - $tw.wiki.addTiddlers(tiddlerFile.tiddlers); - }); - if ($tw.boot.wikiPath == wikiPath) { - // Save the original tiddler file locations if requested - var output = {}, relativePath, fileInfo; - for(var title in $tw.boot.files) { - fileInfo = $tw.boot.files[title]; - if(fileInfo.isEditableFile) { - relativePath = path.relative($tw.boot.wikiTiddlersPath,fileInfo.filepath); - fileInfo.originalpath = relativePath; - output[title] = - path.sep === "/" ? - relativePath : - relativePath.split(path.sep).join("/"); - } - } - if(Object.keys(output).length > 0){ - $tw.wiki.addTiddler({title: "$:/config/OriginalTiddlerPaths", type: "application/json", text: JSON.stringify(output)}); - } - } - // Load any plugins within the wiki folder - var wikiPluginsPath = path.resolve(wikiPath,$tw.config.wikiPluginsSubDir); - if(fs.existsSync(wikiPluginsPath)) { - var pluginFolders = fs.readdirSync(wikiPluginsPath); - for(var t=0; t<pluginFolders.length; t++) { - pluginFields = $tw.loadPluginFolder(path.resolve(wikiPluginsPath,"./" + pluginFolders[t])); - if(pluginFields) { - $tw.wiki.addTiddler(pluginFields); - } + // Load the tiddlers from the wiki directory + if($tw.boot.wikiPath) { + $tw.boot.wikiInfo = $tw.loadWikiTiddlers($tw.boot.wikiPath); } - } - // Load any themes within the wiki folder - var wikiThemesPath = path.resolve(wikiPath,$tw.config.wikiThemesSubDir); - if(fs.existsSync(wikiThemesPath)) { - var themeFolders = fs.readdirSync(wikiThemesPath); - for(var t=0; t<themeFolders.length; t++) { - pluginFields = $tw.loadPluginFolder(path.resolve(wikiThemesPath,"./" + themeFolders[t])); - if(pluginFields) { - $tw.wiki.addTiddler(pluginFields); + }; + + // End of if($tw.node) + } + + /////////////////////////// Main startup function called once tiddlers have been decrypted + + /* + Startup TiddlyWiki + */ + $tw.boot.initStartup = function(options) { + // Get the URL hash and check for safe mode + $tw.locationHash = "#"; + if($tw.browser && !$tw.node) { + if(location.hash === "#:safe") { + $tw.safeMode = true; + } else { + $tw.locationHash = $tw.utils.getLocationHash(); } } - } - // Load any languages within the wiki folder - var wikiLanguagesPath = path.resolve(wikiPath,$tw.config.wikiLanguagesSubDir); - if(fs.existsSync(wikiLanguagesPath)) { - var languageFolders = fs.readdirSync(wikiLanguagesPath); - for(var t=0; t<languageFolders.length; t++) { - pluginFields = $tw.loadPluginFolder(path.resolve(wikiLanguagesPath,"./" + languageFolders[t])); - if(pluginFields) { - $tw.wiki.addTiddler(pluginFields); + // Initialise some more $tw properties + $tw.utils.deepDefaults($tw,{ + modules: { // Information about each module + titles: Object.create(null), // hashmap by module title of {fn:, exports:, moduleType:} + types: {} // hashmap by module type of hashmap of exports + }, + config: { // Configuration overridables + pluginsPath: "../plugins/", + themesPath: "../themes/", + languagesPath: "../languages/", + editionsPath: "../editions/", + wikiInfo: "./tiddlywiki.info", + wikiPluginsSubDir: "./plugins", + wikiThemesSubDir: "./themes", + wikiLanguagesSubDir: "./languages", + wikiTiddlersSubDir: "./tiddlers", + wikiOutputSubDir: "./output", + jsModuleHeaderRegExpString: "^\\/\\*\\\\(?:\\r?\\n)((?:^[^\\r\\n]*(?:\\r?\\n))+?)(^\\\\\\*\\/$(?:\\r?\\n)?)", + fileExtensionInfo: Object.create(null), // Map file extension to {type:} + contentTypeInfo: Object.create(null), // Map type to {encoding:,extension:} + pluginsEnvVar: "TIDDLYWIKI_PLUGIN_PATH", + themesEnvVar: "TIDDLYWIKI_THEME_PATH", + languagesEnvVar: "TIDDLYWIKI_LANGUAGE_PATH", + editionsEnvVar: "TIDDLYWIKI_EDITION_PATH" + }, + log: {}, // Log flags + unloadTasks: [] + }); + if(!$tw.boot.tasks.readBrowserTiddlers) { + // For writable tiddler files, a hashmap of title to {filepath:,type:,hasMetaFile:} + $tw.boot.files = Object.create(null); + // System paths and filenames + $tw.boot.bootPath = options.bootPath || path.dirname(module.filename); + $tw.boot.corePath = path.resolve($tw.boot.bootPath,"../core"); + // If there's no arguments then default to `--help` + if($tw.boot.argv.length === 0) { + $tw.boot.argv = ["--help"]; + } + // Parse any extra plugin references + $tw.boot.extraPlugins = $tw.boot.extraPlugins || []; + while($tw.boot.argv[0] && $tw.boot.argv[0].indexOf("+") === 0) { + $tw.boot.extraPlugins.push($tw.boot.argv[0].substring(1)); + $tw.boot.argv.splice(0,1); + } + // If the first command line argument doesn't start with `--` then we + // interpret it as the path to the wiki folder, which will otherwise default + // to the current folder + if($tw.boot.argv[0] && $tw.boot.argv[0].indexOf("--") !== 0) { + $tw.boot.wikiPath = $tw.boot.argv[0]; + $tw.boot.argv = $tw.boot.argv.slice(1); + } else { + $tw.boot.wikiPath = process.cwd(); + } + // Read package info + $tw.packageInfo = $tw.packageInfo || require("../package.json"); + // Check node version number + if(!$tw.utils.checkVersions(process.version.substr(1),$tw.packageInfo.engines.node.substr(2))) { + $tw.utils.error("TiddlyWiki5 requires node.js version " + $tw.packageInfo.engines.node); + } + } + // Add file extension information + $tw.utils.registerFileType("text/vnd.tiddlywiki","utf8",".tid"); + $tw.utils.registerFileType("application/x-tiddler","utf8",".tid"); + $tw.utils.registerFileType("application/x-tiddlers","utf8",".multids"); + $tw.utils.registerFileType("application/x-tiddler-html-div","utf8",".tiddler"); + $tw.utils.registerFileType("text/vnd.tiddlywiki2-recipe","utf8",".recipe"); + $tw.utils.registerFileType("text/plain","utf8",".txt"); + $tw.utils.registerFileType("text/css","utf8",".css"); + $tw.utils.registerFileType("text/html","utf8",[".html",".htm"]); + $tw.utils.registerFileType("application/hta","utf16le",".hta",{deserializerType:"text/html"}); + $tw.utils.registerFileType("application/javascript","utf8",".js"); + $tw.utils.registerFileType("application/json","utf8",".json"); + $tw.utils.registerFileType("application/pdf","base64",".pdf",{flags:["image"]}); + $tw.utils.registerFileType("application/zip","base64",".zip"); + $tw.utils.registerFileType("application/x-zip-compressed","base64",".zip"); + $tw.utils.registerFileType("image/jpeg","base64",[".jpg",".jpeg"],{flags:["image"]}); + $tw.utils.registerFileType("image/jpg","base64",[".jpg",".jpeg"],{flags:["image"]}); + $tw.utils.registerFileType("image/png","base64",".png",{flags:["image"]}); + $tw.utils.registerFileType("image/gif","base64",".gif",{flags:["image"]}); + $tw.utils.registerFileType("image/webp","base64",".webp",{flags:["image"]}); + $tw.utils.registerFileType("image/heic","base64",".heic",{flags:["image"]}); + $tw.utils.registerFileType("image/heif","base64",".heif",{flags:["image"]}); + $tw.utils.registerFileType("image/svg+xml","utf8",".svg",{flags:["image"]}); + $tw.utils.registerFileType("image/vnd.microsoft.icon","base64",".ico",{flags:["image"]}); + $tw.utils.registerFileType("image/x-icon","base64",".ico",{flags:["image"]}); + $tw.utils.registerFileType("application/wasm","base64",".wasm"); + $tw.utils.registerFileType("application/font-woff","base64",".woff"); + $tw.utils.registerFileType("application/x-font-ttf","base64",".woff"); + $tw.utils.registerFileType("application/font-woff2","base64",".woff2"); + $tw.utils.registerFileType("audio/ogg","base64",".ogg"); + $tw.utils.registerFileType("audio/mp4","base64",[".mp4",".m4a"]); + $tw.utils.registerFileType("video/ogg","base64",[".ogm",".ogv",".ogg"]); + $tw.utils.registerFileType("video/webm","base64",".webm"); + $tw.utils.registerFileType("video/mp4","base64",".mp4"); + $tw.utils.registerFileType("audio/mp3","base64",".mp3"); + $tw.utils.registerFileType("audio/mpeg","base64",[".mp3",".m2a",".mp2",".mpa",".mpg",".mpga"]); + $tw.utils.registerFileType("text/markdown","utf8",[".md",".markdown"],{deserializerType:"text/x-markdown"}); + $tw.utils.registerFileType("text/x-markdown","utf8",[".md",".markdown"]); + $tw.utils.registerFileType("application/enex+xml","utf8",".enex"); + $tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.wordprocessingml.document","base64",".docx"); + $tw.utils.registerFileType("application/msword","base64",".doc"); + $tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","base64",".xlsx"); + $tw.utils.registerFileType("application/excel","base64",".xls"); + $tw.utils.registerFileType("application/vnd.ms-excel","base64",".xls"); + $tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.presentationml.presentation","base64",".pptx"); + $tw.utils.registerFileType("application/mspowerpoint","base64",".ppt"); + $tw.utils.registerFileType("text/x-bibtex","utf8",".bib",{deserializerType:"application/x-bibtex"}); + $tw.utils.registerFileType("application/x-bibtex","utf8",".bib"); + $tw.utils.registerFileType("application/epub+zip","base64",".epub"); + $tw.utils.registerFileType("application/octet-stream","base64",".octet-stream"); + // Create the wiki store for the app + $tw.wiki = new $tw.Wiki($tw.safeMode && {enableIndexers: []}); + // Install built in tiddler fields modules + $tw.Tiddler.fieldModules = $tw.modules.getModulesByTypeAsHashmap("tiddlerfield"); + // Install the tiddler deserializer modules + $tw.Wiki.tiddlerDeserializerModules = Object.create(null); + $tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules); + // Call unload handlers in the browser + if($tw.browser) { + window.onbeforeunload = function(event) { + event = event || {}; + var result; + $tw.utils.each($tw.unloadTasks,function(task) { + var r = task(event); + if(r) { + result = r; + } + }); + return result; } } - } - return wikiInfo; -}; - -$tw.loadTiddlersNode = function() { - // Load the boot tiddlers - $tw.utils.each($tw.loadTiddlersFromPath($tw.boot.bootPath),function(tiddlerFile) { - $tw.wiki.addTiddlers(tiddlerFile.tiddlers); - }); - // Load the core tiddlers - $tw.wiki.addTiddler($tw.loadPluginFolder($tw.boot.corePath)); - // Load any extra plugins - $tw.utils.each($tw.boot.extraPlugins,function(name) { - if(name.charAt(0) === "+") { // Relative path to plugin - var pluginFields = $tw.loadPluginFolder(name.substring(1)); - if(pluginFields) { - $tw.wiki.addTiddler(pluginFields); - } + }; + $tw.boot.loadStartup = function(options){ + + // Load tiddlers + if($tw.boot.tasks.readBrowserTiddlers) { + $tw.loadTiddlersBrowser(); } else { - var parts = name.split("/"), - type = parts[0]; - if(parts.length === 3 && ["plugins","themes","languages"].indexOf(type) !== -1) { - $tw.loadPlugins([parts[1] + "/" + parts[2]],$tw.config[type + "Path"],$tw.config[type + "EnvVar"]); + $tw.loadTiddlersNode(); + } + // Load any preloaded tiddlers + if($tw.preloadTiddlers) { + $tw.wiki.addTiddlers($tw.preloadTiddlers); + } + // Give hooks a chance to modify the store + $tw.hooks.invokeHook("th-boot-tiddlers-loaded"); + } + $tw.boot.execStartup = function(options){ + // Unpack plugin tiddlers + $tw.wiki.readPluginInfo(); + $tw.wiki.registerPluginTiddlers("plugin",$tw.safeMode ? ["$:/core"] : undefined); + $tw.wiki.unpackPluginTiddlers(); + // Process "safe mode" + if($tw.safeMode) { + $tw.wiki.processSafeMode(); + } + // Register typed modules from the tiddlers we've just loaded + $tw.wiki.defineTiddlerModules(); + // And any modules within plugins + $tw.wiki.defineShadowModules(); + // Make sure the crypto state tiddler is up to date + if($tw.crypto) { + $tw.crypto.updateCryptoStateTiddler(); + } + // Gather up any startup modules + $tw.boot.remainingStartupModules = []; // Array of startup modules + $tw.modules.forEachModuleOfType("startup",function(title,module) { + if(module.startup) { + $tw.boot.remainingStartupModules.push(module); } - } - }); - // Load the tiddlers from the wiki directory - if($tw.boot.wikiPath) { - $tw.boot.wikiInfo = $tw.loadWikiTiddlers($tw.boot.wikiPath); - } -}; - -// End of if($tw.node) -} - -/////////////////////////// Main startup function called once tiddlers have been decrypted - -/* -Startup TiddlyWiki -*/ -$tw.boot.initStartup = function(options) { - // Get the URL hash and check for safe mode - $tw.locationHash = "#"; - if($tw.browser && !$tw.node) { - if(location.hash === "#:safe") { - $tw.safeMode = true; - } else { - $tw.locationHash = $tw.utils.getLocationHash(); - } - } - // Initialise some more $tw properties - $tw.utils.deepDefaults($tw,{ - modules: { // Information about each module - titles: Object.create(null), // hashmap by module title of {fn:, exports:, moduleType:} - types: {} // hashmap by module type of hashmap of exports - }, - config: { // Configuration overridables - pluginsPath: "../plugins/", - themesPath: "../themes/", - languagesPath: "../languages/", - editionsPath: "../editions/", - wikiInfo: "./tiddlywiki.info", - wikiPluginsSubDir: "./plugins", - wikiThemesSubDir: "./themes", - wikiLanguagesSubDir: "./languages", - wikiTiddlersSubDir: "./tiddlers", - wikiOutputSubDir: "./output", - jsModuleHeaderRegExpString: "^\\/\\*\\\\(?:\\r?\\n)((?:^[^\\r\\n]*(?:\\r?\\n))+?)(^\\\\\\*\\/$(?:\\r?\\n)?)", - fileExtensionInfo: Object.create(null), // Map file extension to {type:} - contentTypeInfo: Object.create(null), // Map type to {encoding:,extension:} - pluginsEnvVar: "TIDDLYWIKI_PLUGIN_PATH", - themesEnvVar: "TIDDLYWIKI_THEME_PATH", - languagesEnvVar: "TIDDLYWIKI_LANGUAGE_PATH", - editionsEnvVar: "TIDDLYWIKI_EDITION_PATH" - }, - log: {}, // Log flags - unloadTasks: [] - }); - if(!$tw.boot.tasks.readBrowserTiddlers) { - // For writable tiddler files, a hashmap of title to {filepath:,type:,hasMetaFile:} - $tw.boot.files = Object.create(null); - // System paths and filenames - $tw.boot.bootPath = options.bootPath || path.dirname(module.filename); - $tw.boot.corePath = path.resolve($tw.boot.bootPath,"../core"); - // If there's no arguments then default to `--help` - if($tw.boot.argv.length === 0) { - $tw.boot.argv = ["--help"]; - } - // Parse any extra plugin references - $tw.boot.extraPlugins = $tw.boot.extraPlugins || []; - while($tw.boot.argv[0] && $tw.boot.argv[0].indexOf("+") === 0) { - $tw.boot.extraPlugins.push($tw.boot.argv[0].substring(1)); - $tw.boot.argv.splice(0,1); - } - // If the first command line argument doesn't start with `--` then we - // interpret it as the path to the wiki folder, which will otherwise default - // to the current folder - if($tw.boot.argv[0] && $tw.boot.argv[0].indexOf("--") !== 0) { - $tw.boot.wikiPath = $tw.boot.argv[0]; - $tw.boot.argv = $tw.boot.argv.slice(1); - } else { - $tw.boot.wikiPath = process.cwd(); - } - // Read package info - $tw.packageInfo = $tw.packageInfo || require("../package.json"); - // Check node version number - if(!$tw.utils.checkVersions(process.version.substr(1),$tw.packageInfo.engines.node.substr(2))) { - $tw.utils.error("TiddlyWiki5 requires node.js version " + $tw.packageInfo.engines.node); - } - } - // Add file extension information - $tw.utils.registerFileType("text/vnd.tiddlywiki","utf8",".tid"); - $tw.utils.registerFileType("application/x-tiddler","utf8",".tid"); - $tw.utils.registerFileType("application/x-tiddlers","utf8",".multids"); - $tw.utils.registerFileType("application/x-tiddler-html-div","utf8",".tiddler"); - $tw.utils.registerFileType("text/vnd.tiddlywiki2-recipe","utf8",".recipe"); - $tw.utils.registerFileType("text/plain","utf8",".txt"); - $tw.utils.registerFileType("text/css","utf8",".css"); - $tw.utils.registerFileType("text/html","utf8",[".html",".htm"]); - $tw.utils.registerFileType("application/hta","utf16le",".hta",{deserializerType:"text/html"}); - $tw.utils.registerFileType("application/javascript","utf8",".js"); - $tw.utils.registerFileType("application/json","utf8",".json"); - $tw.utils.registerFileType("application/pdf","base64",".pdf",{flags:["image"]}); - $tw.utils.registerFileType("application/zip","base64",".zip"); - $tw.utils.registerFileType("application/x-zip-compressed","base64",".zip"); - $tw.utils.registerFileType("image/jpeg","base64",[".jpg",".jpeg"],{flags:["image"]}); - $tw.utils.registerFileType("image/jpg","base64",[".jpg",".jpeg"],{flags:["image"]}); - $tw.utils.registerFileType("image/png","base64",".png",{flags:["image"]}); - $tw.utils.registerFileType("image/gif","base64",".gif",{flags:["image"]}); - $tw.utils.registerFileType("image/webp","base64",".webp",{flags:["image"]}); - $tw.utils.registerFileType("image/heic","base64",".heic",{flags:["image"]}); - $tw.utils.registerFileType("image/heif","base64",".heif",{flags:["image"]}); - $tw.utils.registerFileType("image/svg+xml","utf8",".svg",{flags:["image"]}); - $tw.utils.registerFileType("image/vnd.microsoft.icon","base64",".ico",{flags:["image"]}); - $tw.utils.registerFileType("image/x-icon","base64",".ico",{flags:["image"]}); - $tw.utils.registerFileType("application/font-woff","base64",".woff"); - $tw.utils.registerFileType("application/x-font-ttf","base64",".woff"); - $tw.utils.registerFileType("application/font-woff2","base64",".woff2"); - $tw.utils.registerFileType("audio/ogg","base64",".ogg"); - $tw.utils.registerFileType("audio/mp4","base64",[".mp4",".m4a"]); - $tw.utils.registerFileType("video/ogg","base64",[".ogm",".ogv",".ogg"]); - $tw.utils.registerFileType("video/webm","base64",".webm"); - $tw.utils.registerFileType("video/mp4","base64",".mp4"); - $tw.utils.registerFileType("audio/mp3","base64",".mp3"); - $tw.utils.registerFileType("audio/mpeg","base64",[".mp3",".m2a",".mp2",".mpa",".mpg",".mpga"]); - $tw.utils.registerFileType("text/markdown","utf8",[".md",".markdown"],{deserializerType:"text/x-markdown"}); - $tw.utils.registerFileType("text/x-markdown","utf8",[".md",".markdown"]); - $tw.utils.registerFileType("application/enex+xml","utf8",".enex"); - $tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.wordprocessingml.document","base64",".docx"); - $tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","base64",".xlsx"); - $tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.presentationml.presentation","base64",".pptx"); - $tw.utils.registerFileType("text/x-bibtex","utf8",".bib",{deserializerType:"application/x-bibtex"}); - $tw.utils.registerFileType("application/x-bibtex","utf8",".bib"); - $tw.utils.registerFileType("application/epub+zip","base64",".epub"); - $tw.utils.registerFileType("application/octet-stream","base64",".octet-stream"); - // Create the wiki store for the app - $tw.wiki = new $tw.Wiki($tw.safeMode && {enableIndexers: []}); - // Install built in tiddler fields modules - $tw.Tiddler.fieldModules = $tw.modules.getModulesByTypeAsHashmap("tiddlerfield"); - // Install the tiddler deserializer modules - $tw.Wiki.tiddlerDeserializerModules = Object.create(null); - $tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules); - // Call unload handlers in the browser - if($tw.browser) { - window.onbeforeunload = function(event) { - event = event || {}; - var result; - $tw.utils.each($tw.unloadTasks,function(task) { - var r = task(event); - if(r) { - result = r; - } - }); - return result; - } - } -}; -$tw.boot.loadStartup = function(options){ - - // Load tiddlers - if($tw.boot.tasks.readBrowserTiddlers) { - $tw.loadTiddlersBrowser(); - } else { - $tw.loadTiddlersNode(); - } - // Load any preloaded tiddlers - if($tw.preloadTiddlers) { - $tw.wiki.addTiddlers($tw.preloadTiddlers); - } - // Give hooks a chance to modify the store - $tw.hooks.invokeHook("th-boot-tiddlers-loaded"); -} -$tw.boot.execStartup = function(options){ - // Unpack plugin tiddlers - $tw.wiki.readPluginInfo(); - $tw.wiki.registerPluginTiddlers("plugin",$tw.safeMode ? ["$:/core"] : undefined); - $tw.wiki.unpackPluginTiddlers(); - // Process "safe mode" - if($tw.safeMode) { - $tw.wiki.processSafeMode(); - } - // Register typed modules from the tiddlers we've just loaded - $tw.wiki.defineTiddlerModules(); - // And any modules within plugins - $tw.wiki.defineShadowModules(); - // Make sure the crypto state tiddler is up to date - if($tw.crypto) { - $tw.crypto.updateCryptoStateTiddler(); - } - // Gather up any startup modules - $tw.boot.remainingStartupModules = []; // Array of startup modules - $tw.modules.forEachModuleOfType("startup",function(title,module) { - if(module.startup) { - $tw.boot.remainingStartupModules.push(module); - } - }); - // Keep track of the startup tasks that have been executed - $tw.boot.executedStartupModules = Object.create(null); - $tw.boot.disabledStartupModules = $tw.boot.disabledStartupModules || []; - // Repeatedly execute the next eligible task - $tw.boot.executeNextStartupTask(options.callback); -} -/* -Startup TiddlyWiki -*/ -$tw.boot.startup = function(options) { - options = options || {}; - // Get the URL hash and check for safe mode - $tw.boot.initStartup(options); - $tw.boot.loadStartup(options); - $tw.boot.execStartup(options); -}; - -/* -Add another unload task -*/ -$tw.addUnloadTask = function(task) { - if($tw.unloadTasks.indexOf(task) === -1) { - $tw.unloadTasks.push(task); - } -} - -/* -Execute the remaining eligible startup tasks -*/ -$tw.boot.executeNextStartupTask = function(callback) { - // Find the next eligible task - var taskIndex = 0, task, - asyncTaskCallback = function() { - if(task.name) { - $tw.boot.executedStartupModules[task.name] = true; - } - return $tw.boot.executeNextStartupTask(callback); - }; - while(taskIndex < $tw.boot.remainingStartupModules.length) { - task = $tw.boot.remainingStartupModules[taskIndex]; - if($tw.boot.isStartupTaskEligible(task)) { - // Remove this task from the list - $tw.boot.remainingStartupModules.splice(taskIndex,1); - // Assemble log message - var s = ["Startup task:",task.name]; - if(task.platforms) { - s.push("platforms:",task.platforms.join(",")); - } - if(task.after) { - s.push("after:",task.after.join(",")); - } - if(task.before) { - s.push("before:",task.before.join(",")); - } - $tw.boot.log(s.join(" ")); - // Execute task - if(!$tw.utils.hop(task,"synchronous") || task.synchronous) { - task.startup(); + }); + // Keep track of the startup tasks that have been executed + $tw.boot.executedStartupModules = Object.create(null); + $tw.boot.disabledStartupModules = $tw.boot.disabledStartupModules || []; + // Repeatedly execute the next eligible task + $tw.boot.executeNextStartupTask(options.callback); + } + /* + Startup TiddlyWiki + */ + $tw.boot.startup = function(options) { + options = options || {}; + // Get the URL hash and check for safe mode + $tw.boot.initStartup(options); + $tw.boot.loadStartup(options); + $tw.boot.execStartup(options); + }; + + /* + Add another unload task + */ + $tw.addUnloadTask = function(task) { + if($tw.unloadTasks.indexOf(task) === -1) { + $tw.unloadTasks.push(task); + } + } + + /* + Execute the remaining eligible startup tasks + */ + $tw.boot.executeNextStartupTask = function(callback) { + // Find the next eligible task + var taskIndex = 0, task, + asyncTaskCallback = function() { if(task.name) { $tw.boot.executedStartupModules[task.name] = true; } return $tw.boot.executeNextStartupTask(callback); - } else { - task.startup(asyncTaskCallback); - return true; - } - } - taskIndex++; - } - if(typeof callback === 'function') { - callback(); - } - return false; -}; - -/* -Returns true if we are running on one of the platforms specified in taskModule's -`platforms` array; or if `platforms` property is not defined. -*/ -$tw.boot.doesTaskMatchPlatform = function(taskModule) { - var platforms = taskModule.platforms; - if(platforms) { - for(var t=0; t<platforms.length; t++) { - switch (platforms[t]) { - case "browser": - if ($tw.browser) { - return true; - } - break; - case "node": - if ($tw.node) { - return true; + }; + while(taskIndex < $tw.boot.remainingStartupModules.length) { + task = $tw.boot.remainingStartupModules[taskIndex]; + if($tw.boot.isStartupTaskEligible(task)) { + // Remove this task from the list + $tw.boot.remainingStartupModules.splice(taskIndex,1); + // Assemble log message + var s = ["Startup task:",task.name]; + if(task.platforms) { + s.push("platforms:",task.platforms.join(",")); + } + if(task.after) { + s.push("after:",task.after.join(",")); + } + if(task.before) { + s.push("before:",task.before.join(",")); + } + $tw.boot.log(s.join(" ")); + // Execute task + if(!$tw.utils.hop(task,"synchronous") || task.synchronous) { + task.startup(); + if(task.name) { + $tw.boot.executedStartupModules[task.name] = true; } - break; - default: - $tw.utils.error("Module " + taskModule.name + ": '" + platforms[t] + "' in export.platforms invalid"); + return $tw.boot.executeNextStartupTask(callback); + } else { + task.startup(asyncTaskCallback); + return true; + } } + taskIndex++; + } + if(typeof callback === 'function') { + callback(); } return false; - } - return true; -}; - -$tw.boot.isStartupTaskEligible = function(taskModule) { - var t; - // Check that the platform is correct - if(!$tw.boot.doesTaskMatchPlatform(taskModule)) { - return false; - } - var name = taskModule.name, - remaining = $tw.boot.remainingStartupModules; - if(name) { - // Fail if this module is disabled - if($tw.boot.disabledStartupModules.indexOf(name) !== -1) { + }; + + /* + Returns true if we are running on one of the platforms specified in taskModule's + `platforms` array; or if `platforms` property is not defined. + */ + $tw.boot.doesTaskMatchPlatform = function(taskModule) { + var platforms = taskModule.platforms; + if(platforms) { + for(var t=0; t<platforms.length; t++) { + switch(platforms[t]) { + case "browser": + if($tw.browser) { + return true; + } + break; + case "node": + if($tw.node) { + return true; + } + break; + default: + $tw.utils.error("Module " + taskModule.name + ": '" + platforms[t] + "' in export.platforms invalid"); + } + } return false; } - // Check that no other outstanding tasks must be executed before this one - for(t=0; t<remaining.length; t++) { - var task = remaining[t]; - if(task.before && task.before.indexOf(name) !== -1) { - if($tw.boot.doesTaskMatchPlatform(task) && (!task.name || $tw.boot.disabledStartupModules.indexOf(task.name) === -1)) { - return false; + return true; + }; + + $tw.boot.isStartupTaskEligible = function(taskModule) { + var t; + // Check that the platform is correct + if(!$tw.boot.doesTaskMatchPlatform(taskModule)) { + return false; + } + var name = taskModule.name, + remaining = $tw.boot.remainingStartupModules; + if(name) { + // Fail if this module is disabled + if($tw.boot.disabledStartupModules.indexOf(name) !== -1) { + return false; + } + // Check that no other outstanding tasks must be executed before this one + for(t=0; t<remaining.length; t++) { + var task = remaining[t]; + if(task.before && task.before.indexOf(name) !== -1) { + if($tw.boot.doesTaskMatchPlatform(task) && (!task.name || $tw.boot.disabledStartupModules.indexOf(task.name) === -1)) { + return false; + } } } } - } - // Check that all of the tasks that we must be performed after has been done - var after = taskModule.after; - if(after) { - for(t=0; t<after.length; t++) { - if(!$tw.boot.executedStartupModules[after[t]]) { - return false; + // Check that all of the tasks that we must be performed after has been done + var after = taskModule.after; + if(after) { + for(t=0; t<after.length; t++) { + if(!$tw.boot.executedStartupModules[after[t]]) { + return false; + } } } - } - return true; -}; - -/* -Global Hooks mechanism which allows plugins to modify default functionality -*/ -$tw.hooks = $tw.hooks || { names: {}}; - -/* -Add hooks to the hashmap -*/ -$tw.hooks.addHook = function(hookName,definition) { - if($tw.utils.hop($tw.hooks.names,hookName)) { - $tw.hooks.names[hookName].push(definition); - } - else { - $tw.hooks.names[hookName] = [definition]; - } -}; - -/* -Delete hooks from the hashmap -*/ -$tw.hooks.removeHook = function(hookName,definition) { - if($tw.utils.hop($tw.hooks.names,hookName)) { - var p = $tw.hooks.names[hookName].indexOf(definition); - if(p !== -1) { - $tw.hooks.names[hookName].splice(p, 1); + return true; + }; + + /* + Global Hooks mechanism which allows plugins to modify default functionality + */ + $tw.hooks = $tw.hooks || { names: {}}; + + /* + Add hooks to the hashmap + */ + $tw.hooks.addHook = function(hookName,definition) { + if($tw.utils.hop($tw.hooks.names,hookName)) { + $tw.hooks.names[hookName].push(definition); + } + else { + $tw.hooks.names[hookName] = [definition]; } - } -}; - -/* -Invoke the hook by key -*/ -$tw.hooks.invokeHook = function(hookName /*, value,... */) { - var args = Array.prototype.slice.call(arguments,1); - if($tw.utils.hop($tw.hooks.names,hookName)) { - for (var i = 0; i < $tw.hooks.names[hookName].length; i++) { - args[0] = $tw.hooks.names[hookName][i].apply(null,args); + }; + + /* + Delete hooks from the hashmap + */ + $tw.hooks.removeHook = function(hookName,definition) { + if($tw.utils.hop($tw.hooks.names,hookName)) { + var p = $tw.hooks.names[hookName].indexOf(definition); + if(p !== -1) { + $tw.hooks.names[hookName].splice(p, 1); + } } - } - return args[0]; -}; - -/////////////////////////// Main boot function to decrypt tiddlers and then startup - -$tw.boot.boot = function(callback) { - // Initialise crypto object - $tw.crypto = new $tw.utils.Crypto(); - $tw.CSE = new $tw.utils.CSE(); - // Initialise password prompter - if($tw.browser && !$tw.node) { - $tw.passwordPrompt = new $tw.utils.PasswordPrompt(); - } - // Preload any encrypted tiddlers - $tw.boot.decryptEncryptedTiddlers(function() { - // Startup - $tw.boot.startup({callback: callback}); + }; + + /* + Invoke the hook by key + */ + $tw.hooks.invokeHook = function(hookName /*, value,... */) { + var args = Array.prototype.slice.call(arguments,1); + if($tw.utils.hop($tw.hooks.names,hookName)) { + for(var i = 0; i < $tw.hooks.names[hookName].length; i++) { + args[0] = $tw.hooks.names[hookName][i].apply(null,args); + } + } + return args[0]; + }; + + /////////////////////////// Main boot function to decrypt tiddlers and then startup + + $tw.boot.boot = function(callback) { + // Initialise crypto object + $tw.crypto = new $tw.utils.Crypto(); + $tw.CSE = new $tw.utils.CSE(); + // Initialise password prompter + if($tw.browser && !$tw.node) { + $tw.passwordPrompt = new $tw.utils.PasswordPrompt(); + } + // Preload any encrypted tiddlers + $tw.boot.decryptEncryptedTiddlers(function() { + // Startup + $tw.boot.startup({callback: callback}); + }); + }; + + /////////////////////////// Autoboot in the browser + + if($tw.browser && !$tw.boot.suppressBoot) { + $tw.boot.boot(); + } + + return $tw; + }); -}; - -/////////////////////////// Autoboot in the browser - -if($tw.browser && !$tw.boot.suppressBoot) { - $tw.boot.boot(); -} - -return $tw; - -}); - -if(typeof(exports) !== "undefined") { - exports.TiddlyWiki = _boot; -} else { - _boot(window.$tw); -} -//# sourceURL=$:/boot/boot.js + + if(typeof(exports) !== "undefined") { + exports.TiddlyWiki = _boot; + } else { + _boot(window.$tw); + } + //# sourceURL=$:/boot/boot.js + \ No newline at end of file diff --git a/src/override/system/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js b/src/override/system/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js index d53aa94..ec5591d 100644 --- a/src/override/system/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js +++ b/src/override/system/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js @@ -84,7 +84,7 @@ TiddlyWebAdaptor.prototype.getStatus = function(callback) { var json = null; try { json = JSON.parse(data); - } catch (e) { + } catch(e) { } if(json) { self.logger.log("Status:",data); @@ -164,7 +164,7 @@ TiddlyWebAdaptor.prototype.getCsrfToken = function() { var regex = /^(?:.*; )?csrf_token=([^(;|$)]*)(?:;|$)/, match = regex.exec(document.cookie), csrf = null; - if (match && (match.length === 2)) { + if(match && (match.length === 2)) { csrf = match[1]; } return csrf;