/*\ title: $:/core/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 along with other elements: # bootprefix.js # # boot.js The module definitions on the browser look like this: $tw.defineModule("MyModule","moduletype",function(module,exports,require) { // Module code inserted here return exports; }); In practice, each module is wrapped in a separate script block. \*/ (function() { /*jslint node: true, browser: true */ /*global modules: false, $tw: false */ "use strict"; /////////////////////////// Setting up $tw // Set up $tw global for the server if(typeof(window) === "undefined") { global.$tw = global.$tw || {}; // No `browser` member for the server exports.$tw = $tw; // Export $tw for when boot.js is required directly in node.js } // Include bootprefix if we're on the server if(!$tw.browser) { require("./bootprefix.js"); } $tw.utils = $tw.utils || {}; $tw.boot = $tw.boot || {}; /////////////////////////// Standard node.js libraries var fs, path, vm; if(!$tw.browser) { fs = require("fs"); path = require("path"); vm = require("vm"); } /////////////////////////// Utility functions /* Log a message */ $tw.utils.log = function(/* args */) { if(console !== undefined && console.log !== undefined) { return Function.apply.call(console.log, console, arguments); } }; /* Display an error and exit */ $tw.utils.error = function(err) { console.error(err); process.exit(1); }; /* Check if an object has a property */ $tw.utils.hop = function(object,property) { return Object.prototype.hasOwnProperty.call(object,property); }; /* Determine if a value is an array */ $tw.utils.isArray = function(value) { return Object.prototype.toString.call(value) == "[object Array]"; }; /* 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 f; if(object) { if($tw.utils.isArray(object)) { for(f=0; f and """ to " */ $tw.utils.htmlDecode = function(s) { return s.toString().replace(/</mg,"<").replace(/>/mg,">").replace(/"/mg,"\"").replace(/&/mg,"&"); }; /* 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 YYYYMMDDHHMM 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()); }; // Parse a date from a YYYYMMDDHHMMSSMMM format string $tw.utils.parseDate = function(value) { if(typeof value === "string") { return new Date(Date.UTC(parseInt(value.substr(0,4),10), 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))); } else if (value instanceof Date) { return value; } else { return null; } }; // Parse a string array from a bracketted list. For example "OneTiddler [[Another Tiddler]] LastOne" $tw.utils.parseStringArray = function(value) { if(typeof value === "string") { var memberRegExp = /(?:\[\[([^\]]+)\]\])|([^\s]+)/mg, results = [], match; do { match = memberRegExp.exec(value); if(match) { results.push(match[1] || match[2]); } } 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 || {}; 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(); fields[field] = value; } } }); return fields; }; /* 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 return sourcepath; } }; /* Returns true if the `actual` version is greater than or equal to the `required` version. Both are in `x.y.z` format. */ $tw.utils.checkVersions = function(required,actual) { var targetVersion = required.split("."), currVersion = actual.split("."), diff = [parseInt(targetVersion[0],10) - parseInt(currVersion[0],10), parseInt(targetVersion[1],10) - parseInt(currVersion[1],10), parseInt(targetVersion[2],10) - parseInt(currVersion[2],10)]; return (diff[0] > 0) || (diff[0] === 0 && diff[1] > 0) || (diff[0] === 0 && diff[1] === 0 && diff[2] > 0); }; /* Register file type information */ $tw.utils.registerFileType = function(type,encoding,extension) { $tw.config.fileExtensionInfo[extension] = {type: type}; $tw.config.contentTypeInfo[type] = {encoding: encoding, extension: extension}; } /* Creates a PasswordPrompt object */ $tw.utils.PasswordPrompt = function() { // Store of pending password prompts this.passwordPrompts = []; // Create the wrapper this.promptWrapper = document.createElement("div"); this.promptWrapper.className = "tw-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 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 submitText = options.submitText || "Login", form = document.createElement("form"), html = ["

" + options.serviceName + "

"]; if(!options.noUserName) { html.push(""); } html.push("", ""); form.className = "form-inline"; form.setAttribute("autocomplete","off"); form.innerHTML = html.join("\n"); 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; } }); // Call the callback if(options.callback(data)) { // Remove the prompt if the callback returned true var i = self.passwordPrompts.indexOf(promptInfo); if(i !== -1) { self.passwordPrompts.splice(i,1); promptInfo.form.parentNode.removeChild(promptInfo.form); self.setWrapperDisplay(); } } else { // Clear the password if the callback returned false $tw.utils.each(form.elements,function(element) { if(element.name === "password") { form.elements[t].value = ""; } }); } event.preventDefault(); return false; },true); // Add the prompt to the list var promptInfo = { serviceName: options.serviceName, callback: options.callback, form: form }; this.passwordPrompts.push(promptInfo); // Make sure the wrapper is displayed 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.browser ? window.sjcl : require("./sjcl.js"), password = null, callSjcl = function(method,inputText) { var outputText; try { outputText = sjcl[method](password,inputText); } catch(ex) { console.log("Crypto error:" + ex); outputText = null; } return outputText; }; this.setPassword = function(newPassword) { password = newPassword; this.updateCryptoStateTiddler(); }; this.updateCryptoStateTiddler = function() { if($tw.wiki && $tw.wiki.addTiddler) { $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/isEncrypted", text: password ? "yes" : "no"})); } }; this.hasPassword = function() { return !!password; } this.encrypt = function(text) { return callSjcl("encrypt",text); }; this.decrypt = function(text) { return callSjcl("decrypt",text); }; }; /////////////////////////// Module mechanism /* 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,object) { 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 = {}; $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 = {}; } $tw.modules.forEachModuleOfType(moduleType,function(title,module) { $tw.utils.each(module,function(element,title,object) { targetObject[title] = module[title]; }); }); return targetObject; }; /* 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 = {}; $tw.modules.forEachModuleOfType(moduleType,function(title,moduleExports) { if(!subType || moduleExports.types[subType]) { var newClass = function() {}; if(baseClass) { newClass.prototype = new baseClass(); newClass.prototype.constructor = baseClass; } $tw.utils.extend(newClass.prototype,moduleExports); classes[moduleExports.name] = newClass; } }); 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 = {}; for(var c=0; c= 1) { fields = $tw.utils.parseFields(split[0],fields); } if(split.length >= 2) { fields.text = split.slice(1).join("\n\n"); } else { fields.text = ""; } return [fields]; } }); $tw.modules.define("$:/boot/tiddlerdeserializer/txt","tiddlerdeserializer",{ "text/plain": function(text,fields) { fields.text = text; fields.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 tiddlers = JSON.parse(text); return tiddlers; } }); /////////////////////////// Browser definitions if($tw.browser) { /* 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 for the password $tw.passwordPrompt.createPrompt({ serviceName: "Enter a password to decrypt this TiddlyWiki", 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 = JSON.parse(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; } } }); } else { // Just invoke the callback straight away if there weren't any encrypted tiddlers callback(); } }; /* Execute the module named 'moduleName'. The name can optionally be relative to the module named 'moduleRoot' */ $tw.modules.execute = function(moduleName,moduleRoot) { /*jslint evil: true */ var name = moduleRoot ? $tw.utils.resolvePath(moduleName,moduleRoot) : moduleName, require = function(modRequire) { return $tw.modules.execute(modRequire,name); }, exports = {}, moduleInfo = $tw.modules.titles[name]; if(!moduleInfo) { $tw.utils.error("Cannot find module named '" + moduleName + "' required by module '" + moduleRoot + "', resolved to " + name); } if(!moduleInfo.exports) { if(typeof moduleInfo.definition === "string") { // String moduleInfo.definition = window["eval"]("(function(module,exports,require) {" + moduleInfo.definition + "})"); moduleInfo.exports = {}; moduleInfo.definition(moduleInfo,moduleInfo.exports,require); } else if(typeof moduleInfo.definition === "function") { // Function moduleInfo.exports = {}; moduleInfo.definition(moduleInfo,moduleInfo.exports,require); } else { // Object moduleInfo.exports = moduleInfo.definition; } } return moduleInfo.exports; }; /* 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 titlePrefix.length) { pluginInfo.tiddlers[pluginTiddlers[t].title.substr(titlePrefix.length)] = pluginTiddlers[t]; } else { console.log("Error extracting plugin: The plugin '" + pluginInfo.title + "' cannot contain a tiddler titled '" + pluginTiddlers[t].title + "'"); } } } } // Save the plugin tiddler return pluginInfo ? { title: pluginInfo.title, type: "application/json", plugin: "yes", text: JSON.stringify(pluginInfo,null,4) } : null; }; /* path: path of wiki directory parentPaths: array of parent paths that we mustn't recurse into */ $tw.loadWikiTiddlers = function(wikiPath,parentPaths) { parentPaths = parentPaths || []; var wikiInfoPath = path.resolve(wikiPath,$tw.config.wikiInfo), wikiInfo = {}, pluginFields; // Bail if we don't have a wiki info file if(!fs.existsSync(wikiInfoPath)) { $tw.utils.error("Missing tiddlywiki.info file at " + wikiPath); } wikiInfo = JSON.parse(fs.readFileSync(wikiInfoPath,"utf8")); // Load any parent wikis if(wikiInfo.includeWikis) { parentPaths = parentPaths.slice(0); parentPaths.push(wikiPath); $tw.utils.each(wikiInfo.includeWikis,function(includedWikiPath) { var resolvedIncludedWikiPath = path.resolve(wikiPath,includedWikiPath); if(parentPaths.indexOf(resolvedIncludedWikiPath) === -1) { $tw.loadWikiTiddlers(resolvedIncludedWikiPath,parentPaths); } else { $tw.utils.error("Cannot recursively include wiki " + resolvedIncludedWikiPath); } }); } // Load any plugins listed in the wiki info file if(wikiInfo.plugins) { var pluginBasePath = path.resolve($tw.boot.corePath,$tw.config.pluginsPath); for(var t=0; t