/*\ title: $:/boot/boot.js type: application/javascript The main boot kernel for TiddlyWiki. This single file creates a barebones TW environment that is just sufficient to bootstrap the modules containing the main logic of the application. On the server this file is executed directly to boot TiddlyWiki. In the browser, this file is packed into a single HTML file. \*/ 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; } } 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=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); } } 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 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(idx < href.length-1 && href[idx+1] === '#') { // 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 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; } else { return sourcepath; } } }; /* 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"; }; /* Run code globally with specified context variables in scope */ $tw.utils.evalGlobal = function(code,context,filename) { 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(){\n" + code + "\n;})();\nreturn exports;\n})\n"; // Compile the code into a function var fn; if($tw.browser) { fn = window["eval"](code + "\n\n//# sourceURL=" + filename); } else { fn = vm.runInThisContext(code,filename); } // Call the function and return the exports return fn.apply(null,contextValues); }; /* 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) { var sandbox = $tw.utils.extend(Object.create(null),context); vm.runInNewContext(code,sandbox,filename); return sandbox.exports; }; /* 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); } }] })); } 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; }; $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; }; 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; } this.encrypt = function(text,password) { return callSjcl("encrypt",text,password); }; this.decrypt = function(text,password) { return callSjcl("decrypt",text,password); }; }; /////////////////////////// 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(!$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}, 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; } else { // If we don't have a module with that name, let node.js try to find it return require(moduleName); } } // 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); } } else { // line number should be included in e.stack for runtime errors $tw.utils.error("Error executing boot module " + name + ": " + JSON.stringify(e) + "\n\n" + e.stack); } } } // 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=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 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; } }); // 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(); } }; // 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= 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= 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 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