/*\ 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 } // Crypto helper object // Setup crypto var Crypto = function() { var password = null, callSjcl = function(method,inputText) { var outputText; if(!password) { getPassword(); } try { outputText = $tw.crypto.sjcl[method](password,inputText); } catch(ex) { console.log("Crypto error:" + ex); outputText = null; } return outputText; }, getPassword = function() { if($tw.browser) { password = window.prompt("Enter password to decrypt TiddlyWiki"); } }; this.setPassword = function(newPassword) { password = newPassword; }; this.encrypt = function(text) { return callSjcl("encrypt",text); }; this.decrypt = function(text) { return callSjcl("decrypt",text); }; }; $tw.crypto = new Crypto(); $tw.crypto.sjcl = $tw.browser ? window.sjcl : require("./sjcl.js"); // Boot information $tw.boot = {}; // Modules store registers all the modules the system has seen $tw.modules = $tw.modules || {}; $tw.modules.titles = $tw.modules.titles || {}; // hashmap by module title of {fn:, exports:, moduleType:} $tw.modules.types = $tw.modules.types || {}; // hashmap by module type of array of exports // Config object $tw.config = $tw.config || {}; // Constants $tw.config.bootModuleSubDir = $tw.config.bootModuleSubDir || "./modules"; $tw.config.wikiPluginsSubDir = $tw.config.wikiPluginsSubDir || "./plugins"; $tw.config.wikiShadowsSubDir = $tw.config.wikiShadowsSubDir || "./wiki"; $tw.config.wikiTiddlersSubDir = $tw.config.wikiTiddlersSubDir || "./tiddlers"; $tw.config.jsModuleHeaderRegExpString = "^\\/\\*\\\\\\n((?:^[^\\n]*\\n)+?)(^\\\\\\*\\/$\\n?)"; // File extension mappings $tw.config.fileExtensionInfo = { ".tid": {type: "application/x-tiddler"}, ".tiddler": {type: "application/x-tiddler-html-div"}, ".recipe": {type: "application/x-tiddlywiki-recipe"}, ".txt": {type: "text/plain"}, ".css": {type: "text/css"}, ".html": {type: "text/html"}, ".js": {type: "application/javascript"}, ".json": {type: "application/json"}, ".pdf": {type: "application/pdf"}, ".jpg": {type: "image/jpeg"}, ".jpeg": {type: "image/jpeg"}, ".png": {type: "image/png"}, ".gif": {type: "image/gif"}, ".svg": {type: "image/svg+xml"} }; // Content type mappings $tw.config.contentTypeInfo = { "text/x-tiddlywiki": {encoding: "utf8", extension: ".tid"}, "application/x-tiddler": {encoding: "utf8", extension: ".tid"}, "application/x-tiddler-html-div": {encoding: "utf8", extension: ".tiddler"}, "application/x-tiddlywiki-recipe": {encoding: "utf8", extension: ".recipe"}, "text/plain": {encoding: "utf8", extension: ".txt"}, "text/css": {encoding: "utf8", extension: ".css"}, "text/html": {encoding: "utf8", extension: ".html"}, "application/javascript": {encoding: "utf8", extension: ".js"}, "application/json": {encoding: "utf8", extension: ".json"}, "application/pdf": {encoding: "base64", extension: ".pdf"}, "image/jpeg": {encoding: "base64", extension: ".jpg"}, "image/png": {encoding: "base64", extension: ".png"}, "image/gif": {encoding: "base64", extension: ".gif"}, "image/svg+xml": {encoding: "utf8", extension: ".svg"} }; /////////////////////////// Utility functions $tw.utils = $tw.utils || {}; /* 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]"; }; /* Convert "&" to &, "<" to <, ">" to > 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) { text.split(/\r?\n/mg).forEach(function(line) { 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); }; /////////////////////////// Server initialisation var fs, path, vm; if(!$tw.browser) { // Standard node libraries fs = require("fs"); path = require("path"); vm = require("vm"); // System paths and filenames $tw.boot.bootFile = path.basename(module.filename); $tw.boot.bootPath = path.dirname(module.filename); $tw.boot.wikiPath = process.cwd(); // Read package info $tw.packageInfo = JSON.parse(fs.readFileSync($tw.boot.bootPath + "/../package.json")); // Check node version number if($tw.utils.checkVersions($tw.packageInfo.engine.node.substr(2),process.version.substr(1))) { throw "TiddlyWiki5 requires node.js version " + $tw.packageInfo.engine.node; } } /////////////////////////// Module mechanism /* Register the exports of a single module in the $tw.modules.types hashmap */ $tw.modules.registerModuleExports = function(name,moduleType,moduleExports) { if(!(moduleType in $tw.modules.types)) { $tw.modules.types[moduleType] = []; } $tw.modules.types[moduleType].push(moduleExports); }; /* 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 modules = $tw.modules.types[moduleType], results = {}; if(modules) { for(var t=0; t 1) { fields = $tw.utils.parseFields(split[0],fields); fields.text = split.slice(1).join("\n\n"); } else { fields.text = text; } return [fields]; } }); $tw.modules.registerModuleExports("$:/boot/tiddlerdeserializer/txt","tiddlerdeserializer",{ "text/plain": function(text,fields) { fields.text = text; fields.type = "text/plain"; return [fields]; } }); $tw.modules.registerModuleExports("$:/boot/tiddlerdeserializer/html","tiddlerdeserializer",{ "text/html": function(text,fields) { fields.text = text; fields.type = "text/html"; return [fields]; } }); $tw.modules.registerModuleExports("$:/boot/tiddlerdeserializer/json","tiddlerdeserializer",{ "application/json": function(text,fields) { var tiddlers = JSON.parse(text); return tiddlers; } }); // Install the tiddler deserializer modules so they are immediately available $tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules); /////////////////////////// Intermediate initialisation /* Create the wiki store for the app */ $tw.wiki = new $tw.Wiki(); /////////////////////////// Browser definitions if($tw.browser) { /* Execute the module named 'moduleName'. The name can optionally be relative to the module named 'moduleRoot' */ $tw.modules.execute = function(moduleName,moduleRoot) { var name = moduleRoot ? $tw.utils.resolvePath(moduleName,moduleRoot) : moduleName, require = function(modRequire) { return $tw.modules.execute(modRequire,name); }, exports = {}, module = $tw.modules.titles[name]; if(!module) { throw new Error("Cannot find module named '" + moduleName + "' required by module '" + moduleRoot + "', resolved to " + name); } if(module.exports) { return module.exports; } else { module.exports = {}; module.fn(module,module.exports,require); return module.exports; } }; /* Register a deserializer that can extract tiddlers from the DOM */ $tw.modules.registerModuleExports("$:/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) { bundleInfo.tiddlers[bundleTiddlers[t].title.substr(titlePrefix.length)] = bundleTiddlers[t]; } else { throw "The bundle '" + bundleInfo.title + "' cannot contain a tiddler titled '" + bundleTiddlers[t].title + "'"; } } // Save the bundle tiddler return { title: bundleInfo.title, type: "application/json", bundle: "yes", text: JSON.stringify(bundleInfo) }; }; /* Execute the module named 'moduleName'. The name can optionally be relative to the module named 'moduleRoot' */ $tw.modules.execute = function(moduleName,moduleRoot) { var name = moduleRoot ? $tw.utils.resolvePath(moduleName,moduleRoot) : moduleName, module = $tw.modules.titles[name], tiddler = $tw.wiki.getTiddler(name), sandbox = { module: module, exports: {}, console: console, process: process, $tw: $tw, require: function(title) { return $tw.modules.execute(title,name); } }; if(!tiddler || tiddler.fields.type !== "application/javascript") { // If there is no tiddler with that name, let node try to find it return require(moduleName); } // Define the module if it is not defined module = module || { moduleType: tiddler.fields["module-type"] }; $tw.modules.titles[name] = module; // Execute it to get its exports if we haven't already done so if(!module.exports) { try { vm.runInNewContext(tiddler.fields.text,sandbox,tiddler.fields.title); } catch(e) { throw "Error executing boot module " + tiddler.fields.title + ":\n" + e; } module.exports = sandbox.exports; } // Return the exports of the module return module.exports; }; // End of if(!$tw.browser) } /////////////////////////// Final initialisation // Load tiddlers var t; if($tw.browser) { // In the browser, we load tiddlers from certain elements var containerIds = [ "libraryModules", "modules", "bootKernelPrefix", "bootKernel", "styleArea", "storeArea", "shadowArea" ]; for(t=0; t