/*\ 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 = this.parseTextReference(textRef), title = tr.title || currTiddlerTitle, field = tr.field || "text", tiddler = this.getTiddler(title); if(tiddler && $tw.utils.hop(tiddler.fields,field)) { return tiddler.fields[field]; } else { return defaultText; } }; exports.setTextReference = function(textRef,value,currTiddlerTitle,isShadow) { var tr = this.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}),isShadow); // 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 = this.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)); } } }; /* Parse a text reference into its constituent parts */ exports.parseTextReference = function(textRef,currTiddlerTitle) { // Look for a metadata field separator var pos = textRef.indexOf("!!"); if(pos !== -1) { if(pos === 0) { // Just a field return { field: textRef.substring(2) }; } else { // Field and title return { title: textRef.substring(0,pos), field: textRef.substring(pos + 2) }; } } else { // Otherwise, we've just got a title return { title: textRef }; } }; 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 parser modules moduleType = moduleType || "parser"; $tw.wiki.parsers = {}; var modules = $tw.modules.types[moduleType], n,m,f; if(modules) { for(n=0; n 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: saveEmpty: causes the wiki to be saved without any content 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", renderOptions = {}; renderOptions["with"] = options.saveEmpty ? [undefined,"[!is[shadow]is[shadow]]"] : [undefined,"[!is[shadow]]"]; var text = this.renderTiddler(downloadType,template,renderOptions); 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