/*\ title: $:/core/modules/wiki.js type: application/javascript module-type: wikimethod Extension methods for the $tw.Wiki object Adds the following properties to the wiki object: * `eventListeners` is an array of {filter: , listener: fn} * `changedTiddlers` is a hashmap describing changes to named tiddlers since wiki change events were last dispatched. Each entry is a hashmap containing two fields: modified: true/false deleted: true/false * `changeCount` is a hashmap by tiddler title containing a numerical index that starts at zero and is incremented each time a tiddler is created changed or deleted * `caches` is a hashmap by tiddler title containing a further hashmap of named cache objects. Caches are automatically cleared when a tiddler is modified or deleted * `macros` is a hashmap by macro name containing an object class inheriting from the Macro tree node \*/ (function(){ /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; /* Get the value of a text reference. Text references can have any of these forms: !! !! - specifies a field of the current tiddlers ## */ exports.getTextReference = function(textRef,defaultText,currTiddlerTitle) { var tr = $tw.utils.parseTextReference(textRef), title = tr.title || currTiddlerTitle; if(tr.field) { var tiddler = this.getTiddler(title); if(tiddler && $tw.utils.hop(tiddler.fields,tr.field)) { return tiddler.fields[tr.field]; } else { return defaultText; } } else if(tr.index) { return this.extractTiddlerDataItem(title,tr.index,defaultText); } else { return this.getTiddlerText(title); } }; exports.setTextReference = function(textRef,value,currTiddlerTitle) { var tr = $tw.utils.parseTextReference(textRef), title,tiddler,fields; // Check if it is a reference to a tiddler if(tr.title && !tr.field) { tiddler = this.getTiddler(tr.title); this.addTiddler(new $tw.Tiddler(tiddler,{title: tr.title,text: value})); // Else check for a field reference } else if(tr.field) { title = tr.title || currTiddlerTitle; tiddler = this.getTiddler(title); if(tiddler) { fields = {}; fields[tr.field] = value; this.addTiddler(new $tw.Tiddler(tiddler,fields)); } } }; exports.deleteTextReference = function(textRef,currTiddlerTitle) { var tr = $tw.utils.parseTextReference(textRef), title,tiddler,fields; // Check if it is a reference to a tiddler if(tr.title && !tr.field) { this.deleteTiddler(tr.title); // Else check for a field reference } else if(tr.field) { title = tr.title || currTiddlerTitle; tiddler = this.getTiddler(title); if(tiddler && $tw.utils.hop(tiddler.fields,tr.field)) { fields = {}; fields[tr.field] = undefined; this.addTiddler(new $tw.Tiddler(tiddler,fields)); } } }; exports.addEventListener = function(filter,listener) { this.eventListeners = this.eventListeners || []; this.eventListeners.push({ filter: filter, listener: listener }); }; exports.removeEventListener = function(filter,listener) { for(var c=this.eventListeners.length-1; c>=0; c--) { var l = this.eventListeners[c]; if(l.filter === filter && l.listener === listener) { this.eventListeners.splice(c,1); } } }; /* Causes a tiddler to be marked as changed, incrementing the change count, and triggers event handlers. This method should be called after the changes it describes have been made to the wiki.tiddlers[] array. title: Title of tiddler isDeleted: defaults to false (meaning the tiddler has been created or modified), true if the tiddler has been created */ exports.touchTiddler = function(title,isDeleted) { // Record the touch in the list of changed tiddlers this.changedTiddlers = this.changedTiddlers || {}; this.changedTiddlers[title] = this.changedTiddlers[title] || []; this.changedTiddlers[title][isDeleted ? "deleted" : "modified"] = true; // Increment the change count this.changeCount = this.changeCount || {}; if($tw.utils.hop(this.changeCount,title)) { this.changeCount[title]++; } else { this.changeCount[title] = 1; } // Trigger events this.eventListeners = this.eventListeners || []; if(!this.eventsTriggered) { var me = this; $tw.utils.nextTick(function() { var changes = me.changedTiddlers; me.changedTiddlers = {}; me.eventsTriggered = false; for(var e=0; e bb) { return 1; } else { return 0; } } }); for(t=0; t b) { return isDescending ? -1 : +1; } else { return 0; } } }); }; exports.forEachTiddler = function(/* [sortField,[excludeTag,]]callback */) { var arg = 0, sortField = arguments.length > 1 ? arguments[arg++] : null, excludeTag = arguments.length > 2 ? arguments[arg++] : null, callback = arguments[arg++], titles = this.getTiddlers(sortField,excludeTag), t, tiddler; for(t=0; t 0) { return tiddler.fields.text.split("\n"); } return []; }; // Return the named cache object for a tiddler. If the cache doesn't exist then the initializer function is invoked to create it exports.getCacheForTiddler = function(title,cacheName,initializer) { this.caches = this.caches || {}; var caches = this.caches[title]; if(caches && caches[cacheName]) { return caches[cacheName]; } else { if(!caches) { caches = {}; this.caches[title] = caches; } caches[cacheName] = initializer(); return caches[cacheName]; } }; // Clear all caches associated with a particular tiddler exports.clearCache = function(title) { this.caches = this.caches || {}; if($tw.utils.hop(this.caches,title)) { delete this.caches[title]; } }; exports.initParsers = function(moduleType) { // Install the new parser modules $tw.wiki.newparsers = {}; var self = this; $tw.modules.forEachModuleOfType("newparser",function(title,module) { for(var f in module) { if($tw.utils.hop(module,f)) { $tw.wiki.newparsers[f] = module[f]; // Store the parser class } } }); }; /* Parse a block of text of a specified MIME type type: content type of text to be parsed text: text options: see below Options include: parseAsInline: if true, the text of the tiddler will be parsed as an inline run */ exports.parseText = function(type,text,options) { options = options || {}; // Select a parser var Parser = this.newparsers[type]; if(!Parser && $tw.config.fileExtensionInfo[type]) { Parser = this.newparsers[$tw.config.fileExtensionInfo[type].type]; } if(!Parser) { Parser = this.newparsers[options.defaultType || "text/vnd.tiddlywiki"]; } if(!Parser) { return null; } // Return the parser instance return new Parser(type,text,{ parseAsInline: options.parseAsInline, wiki: this }); }; /* Parse a tiddler according to its MIME type */ exports.parseTiddler = function(title,options) { options = options || {}; var cacheType = options.parseAsInline ? "newInlineParseTree" : "newBlockParseTree", tiddler = this.getTiddler(title), self = this; return tiddler ? this.getCacheForTiddler(title,cacheType,function() { return self.parseText(tiddler.fields.type,tiddler.fields.text,options); }) : null; }; /* Parse text in a specified format and render it into another format outputType: content type for the output textType: content type of the input text text: input text */ exports.renderText = function(outputType,textType,text) { var parser = this.parseText(textType,text), renderTree = new $tw.WikiRenderTree(parser,{wiki: this}); renderTree.execute(); return renderTree.render(outputType); }; /* Parse text from a tiddler and render it into another format outputType: content type for the output title: title of the tiddler to be rendered */ exports.renderTiddler = function(outputType,title) { var parser = this.parseTiddler(title), renderTree = new $tw.WikiRenderTree(parser,{wiki: this}); renderTree.execute(); return renderTree.render(outputType); }; /* Select the appropriate saver modules and set them up */ exports.initSavers = function(moduleType) { moduleType = moduleType || "saver"; // Instantiate the available savers this.savers = []; var self = this; $tw.modules.forEachModuleOfType(moduleType,function(title,module) { if(module.canSave(self)) { self.savers.push(module.create(self)); } }); // Sort the savers into priority order this.savers.sort(function(a,b) { if(a.info.priority < b.info.priority) { return -1; } else { if(a.info.priority > b.info.priority) { return +1; } else { return 0; } } }); }; /* Invoke the highest priority saver that successfully handles a method */ exports.callSaver = function(method /*, args */ ) { for(var t=this.savers.length-1; t>=0; t--) { var saver = this.savers[t]; if(saver[method].apply(saver,Array.prototype.slice.call(arguments,1))) { return true; } } return false; }; /* Save the wiki contents. Options are: template: the tiddler containing the template to save downloadType: the content type for the saved file */ exports.saveWiki = function(options) { options = options || {}; var template = options.template || "$:/core/templates/tiddlywiki5.template.html", downloadType = options.downloadType || "text/plain"; var text = this.renderTiddler(downloadType,template); this.callSaver("save",text); }; /* Return an array of tiddler titles that match a search string text: The text string to search for options: see below Options available: titles: Hashmap or array of tiddler titles to limit search exclude: An array of tiddler titles to exclude from the search invert: If true returns tiddlers that do not contain the specified string caseSensitive: If true forces a case sensitive search literal: If true, searches for literal string, rather than separate search terms */ exports.search = function(text,options) { options = options || {}; var me = this,t; // Convert the search string into a regexp for each term var terms, searchTermsRegExps, flags = options.caseSensitive ? "" : "i"; if(options.literal) { if(text.length === 0) { return []; } searchTermsRegExps = [new RegExp("(" + $tw.utils.escapeRegExp(text) + ")",flags)]; } else { terms = text.replace(/( +)/g," ").split(" "); searchTermsRegExps = []; if(terms.length === 0) { return []; } for(t=0; t