/*\ 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 a hashmap by type of arrays of listener functions * `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 * `globalCache` is a hashmap by cache name of cache objects that are cleared whenever any tiddler change occurs \*/ var Widget = require("$:/core/modules/widgets/widget.js").widget; var USER_NAME_TITLE = "$:/status/UserName", TIMESTAMP_DISABLE_TITLE = "$:/config/TimestampDisable"; /* Add available indexers to this wiki */ exports.addIndexersToWiki = function() { var self = this; $tw.utils.each($tw.modules.applyMethods("indexer"),function(Indexer,name) { self.addIndexer(new Indexer(self),name); }); }; /* 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(tr.field === "title") { // Special case so we can return the title of a non-existent tiddler return title || defaultText; } else if(tiddler && $tw.utils.hop(tiddler.fields,tr.field)) { return tiddler.getFieldString(tr.field); } else { return defaultText; } } else if(tr.index) { return this.extractTiddlerDataItem(title,tr.index,defaultText); } else { return this.getTiddlerText(title,defaultText); } }; exports.setTextReference = function(textRef,value,currTiddlerTitle) { var tr = $tw.utils.parseTextReference(textRef), title = tr.title || currTiddlerTitle; this.setText(title,tr.field,tr.index,value); }; exports.setText = function(title,field,index,value,options) { options = options || {}; var creationFields = options.suppressTimestamp ? {} : this.getCreationFields(), modificationFields = options.suppressTimestamp ? {} : this.getModificationFields(); // Check if it is a reference to a tiddler field if(index) { var data = this.getTiddlerData(title,Object.create(null)); if(value !== undefined) { data[index] = value; } else { delete data[index]; } this.setTiddlerData(title,data,{},{suppressTimestamp: options.suppressTimestamp}); } else { var tiddler = this.getTiddler(title), fields = {title: title}; fields[field || "text"] = value; this.addTiddler(new $tw.Tiddler(creationFields,tiddler,fields,modificationFields)); } }; 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 = Object.create(null); fields[tr.field] = undefined; this.addTiddler(new $tw.Tiddler(tiddler,fields,this.getModificationFields())); } } }; exports.addEventListener = function(type,listener) { this.eventListeners = this.eventListeners || {}; this.eventListeners[type] = this.eventListeners[type] || []; this.eventListeners[type].push(listener); }; exports.removeEventListener = function(type,listener) { var listeners = this.eventListeners[type]; if(listeners) { var p = listeners.indexOf(listener); if(p !== -1) { listeners.splice(p,1); } } }; exports.dispatchEvent = function(type /*, args */) { var args = Array.prototype.slice.call(arguments,1), listeners = this.eventListeners[type]; if(listeners) { for(var p=0; p 0) { self.dispatchEvent("change",changes); } }); this.eventsTriggered = true; } }; exports.getSizeOfTiddlerEventQueue = function() { return $tw.utils.count(this.changedTiddlers); }; exports.clearTiddlerEventQueue = function() { this.changedTiddlers = Object.create(null); this.changeCount = Object.create(null); }; exports.getChangeCount = function(title) { this.changeCount = this.changeCount || Object.create(null); if($tw.utils.hop(this.changeCount,title)) { return this.changeCount[title]; } else { return 0; } }; /* Generate an unused title from the specified base options.prefix must be a string */ exports.generateNewTitle = function(baseTitle,options) { options = options || {}; var c = 0, title = baseTitle, template = options.template, prefix = (typeof(options.prefix) === "string") ? options.prefix : " "; if (template) { // "count" is important to avoid an endless loop in while(...)!! template = (/\$count:?(\d+)?\$/i.test(template)) ? template : template + "$count$"; title = $tw.utils.formatTitleString(template,{"base":baseTitle,"separator":prefix,"counter":c}); while(this.tiddlerExists(title) || this.isShadowTiddler(title) || this.findDraft(title)) { title = $tw.utils.formatTitleString(template,{"base":baseTitle,"separator":prefix,"counter":(++c)}); } } else { while(this.tiddlerExists(title) || this.isShadowTiddler(title) || this.findDraft(title)) { title = baseTitle + prefix + (++c); } } return title; }; exports.isSystemTiddler = function(title) { return title && title.indexOf("$:/") === 0; }; exports.isTemporaryTiddler = function(title) { return title && title.indexOf("$:/temp/") === 0; }; exports.isVolatileTiddler = function(title) { return title && title.indexOf("$:/temp/volatile/") === 0; }; exports.isImageTiddler = function(title) { var tiddler = this.getTiddler(title); if(tiddler) { var contentTypeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/vnd.tiddlywiki"]; return !!contentTypeInfo && contentTypeInfo.flags.indexOf("image") !== -1; } else { return null; } }; exports.isBinaryTiddler = function(title) { var tiddler = this.getTiddler(title); if(tiddler) { var contentTypeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/vnd.tiddlywiki"]; return !!contentTypeInfo && contentTypeInfo.encoding === "base64"; } else { return null; } }; /* Like addTiddler() except it will silently reject any plugin tiddlers that are older than the currently loaded version. Returns true if the tiddler was imported */ exports.importTiddler = function(tiddler) { var existingTiddler = this.getTiddler(tiddler.fields.title); // Check if we're dealing with a plugin if(tiddler && tiddler.hasField("plugin-type") && tiddler.hasField("version") && existingTiddler && existingTiddler.hasField("plugin-type") && existingTiddler.hasField("version")) { // Reject the incoming plugin if it is older if(!$tw.utils.checkVersions(tiddler.fields.version,existingTiddler.fields.version)) { return false; } } // Fall through to adding the tiddler this.addTiddler(tiddler); return true; }; /* Return a hashmap of the fields that should be set when a tiddler is created */ exports.getCreationFields = function() { if(this.getTiddlerText(TIMESTAMP_DISABLE_TITLE,"").toLowerCase() !== "yes") { var fields = { created: new Date() }, creator = this.getTiddlerText(USER_NAME_TITLE); if(creator) { fields.creator = creator; } return fields; } else { return {}; } }; /* Return a hashmap of the fields that should be set when a tiddler is modified */ exports.getModificationFields = function() { if(this.getTiddlerText(TIMESTAMP_DISABLE_TITLE,"").toLowerCase() !== "yes") { var fields = Object.create(null), modifier = this.getTiddlerText(USER_NAME_TITLE); fields.modified = new Date(); if(modifier) { fields.modifier = modifier; } return fields; } else { return {}; } }; /* Return a sorted array of tiddler titles. Options include: sortField: field to sort by excludeTag: tag to exclude includeSystem: whether to include system tiddlers (defaults to false) */ exports.getTiddlers = function(options) { options = options || Object.create(null); var self = this, sortField = options.sortField || "title", tiddlers = [], t, titles = []; this.each(function(tiddler,title) { if(options.includeSystem || !self.isSystemTiddler(title)) { if(!options.excludeTag || !tiddler.hasTag(options.excludeTag)) { tiddlers.push(tiddler); } } }); tiddlers.sort(function(a,b) { var aa = a.fields[sortField].toLowerCase() || "", bb = b.fields[sortField].toLowerCase() || ""; if(aa < bb) { return -1; } else { if(aa > bb) { return 1; } else { return 0; } } }); for(t=0; t= 2 ? arguments[arg++] : {}, callback = arguments[arg++], titles = this.getTiddlers(options), t, tiddler; for(t=0; t`) means self-referential transclusion. value = title; } else if(parseTreeNode.attributes.field && parseTreeNode.attributes.field.type === "string") { // Old usage with Empty value (like `<$transclude field='created'/>`) value = title; } // Deduplicate the result. if(value && transcludes.indexOf(value) === -1) { $tw.utils.pushTop(transcludes,value); } } if(parseTreeNode.children) { checkParseTree(parseTreeNode.children,parseTreeNode); } } }; checkParseTree(parseTreeRoot); return transcludes; }; /* Return an array of tiddler titles that are transcluded from the specified tiddler */ exports.getTiddlerTranscludes = function(title) { var self = this; // We'll cache the transcludes so they only get computed if the tiddler changes return this.getCacheForTiddler(title,"transcludes",function() { // Parse the tiddler var parser = self.parseTiddler(title); if(parser) { // this will ignore self-referential transclusions from `title` return self.extractTranscludes(parser.tree,title); } return []; }); }; /* Return an array of tiddler titles that transclude to the specified tiddler */ exports.getTiddlerBacktranscludes = function(targetTitle) { var self = this, backIndexer = this.getIndexer("BackIndexer"), backtranscludes = backIndexer && backIndexer.subIndexers.transclude.lookup(targetTitle); if(!backtranscludes) { backtranscludes = []; } return backtranscludes; }; /* Return a hashmap of tiddler titles that are referenced but not defined. Each value is the number of times the missing tiddler is referenced */ exports.getMissingTitles = function() { var self = this, missing = []; // We should cache the missing tiddler list, even if we recreate it every time any tiddler is modified this.forEachTiddler(function(title,tiddler) { var links = self.getTiddlerLinks(title); $tw.utils.each(links,function(link) { if((!self.tiddlerExists(link) && !self.isShadowTiddler(link)) && missing.indexOf(link) === -1) { missing.push(link); } }); }); return missing; }; exports.getOrphanTitles = function() { var self = this, orphans = this.getTiddlers(); this.forEachTiddler(function(title,tiddler) { var links = self.getTiddlerLinks(title); $tw.utils.each(links,function(link) { var p = orphans.indexOf(link); if(p !== -1) { orphans.splice(p,1); } }); }); return orphans; // Todo }; /* Retrieves a list of the tiddler titles that are tagged with a given tag */ exports.getTiddlersWithTag = function(tag) { // Try to use the indexer var self = this, tagIndexer = this.getIndexer("TagIndexer"), results = tagIndexer && tagIndexer.subIndexers[3].lookup(tag); if(!results) { // If not available, perform a manual scan results = this.getGlobalCache("taglist-" + tag,function() { var tagmap = self.getTagMap(); return self.sortByList(tagmap[tag],tag); }); } return results; }; /* Get a hashmap by tag of arrays of tiddler titles */ exports.getTagMap = function() { var self = this; return this.getGlobalCache("tagmap",function() { var tags = Object.create(null), storeTags = function(tagArray,title) { if(tagArray) { for(var index=0; index= 0) { ++newPos; } } // If a new position is specified, let's move it if (newPos !== -1) { // get its current Pos, and make sure // sure that it's _actually_ in the list // and that it would _actually_ move // (#4275) We don't bother calling // indexOf unless we have a new // position to work with var currPos = titles.indexOf(title); if(currPos >= 0 && newPos !== currPos) { // move it! titles.splice(currPos,1); if(newPos >= currPos) { newPos--; } titles.splice(newPos,0,title); } } } } } var list = this.getTiddlerList(listTitle); if(!array || array.length === 0) { return []; } else { var titles = [], t, title; // First place any entries that are present in the list for(t=0; t} [options.substitutions] - An optional array of substitutions. * * @returns {string} The text with substitutions applied. */ exports.getSubstitutedText = function(text,widget,options) { options = options || {}; text = text || ""; var self = this, substitutions = options.substitutions || [], output; // Evaluate embedded filters and substitute with first result output = text.replace(/\$\{([\S\s]+?)\}\$/g, function(match,filter) { return self.filterTiddlers(filter,widget)[0] || ""; }); // Process any substitutions provided in options $tw.utils.each(substitutions,function(substitute) { output = $tw.utils.replaceString(output,new RegExp("\\$" + $tw.utils.escapeRegExp(substitute.name) + "\\$","mg"),substitute.value); }); // Substitute any variable references with their values return output.replace(/\$\(([^\)\$]+)\)\$/g, function(match,varname) { return widget.getVariable(varname,{defaultValue: ""}) }); }; /** * Create a widget tree for a parse tree. * * @param {Object} parser - The parser object containing the parse tree. * @param {Object} [options] - Options for creating the widget tree. * @param {Document} [options.document] - Optional document to use. * @param {Object} [options.variables] - Hashmap of variables to set. * @param {Widget} [options.parentWidget] - Optional parent widget for the root node. * * @returns {Widget} The root widget of the created widget tree. */ exports.makeWidget = function(parser,options) { options = options || {}; var widgetNode = { type: "widget", children: [] }, currWidgetNode = widgetNode; // Create let variable widget for variables if($tw.utils.count(options.variables) > 0) { var letVariableWidget = { type: "let", attributes: { }, children: [] }; $tw.utils.each(options.variables,function(value,name) { $tw.utils.addAttributeToParseTreeNode(letVariableWidget,name,"" + value); }); currWidgetNode.children = [letVariableWidget]; currWidgetNode = letVariableWidget; } // Add in the supplied parse tree nodes currWidgetNode.children = parser ? parser.tree : []; // Create the widget return new Widget(widgetNode,{ wiki: this, document: options.document || $tw.fakeDocument, parentWidget: options.parentWidget }); }; /* Make a widget tree for transclusion title: target tiddler title options: as for wiki.makeWidget() plus: options.field: optional field to transclude (defaults to "text") options.mode: transclusion mode "inline" or "block" options.recursionMarker : optional flag to set a recursion marker, defaults to "yes" options.children: optional array of children for the transclude widget options.importVariables: optional importvariables filter string for macros to be included options.importPageMacros: optional boolean; if true, equivalent to passing "[[$:/core/ui/PageMacros]] [all[shadows+tiddlers]tag[$:/tags/Macro]!has[draft.of]]" to options.importVariables */ exports.makeTranscludeWidget = function(title,options) { options = options || {}; var parseTreeDiv = {tree: [{ type: "element", tag: "div", children: []}]}, parseTreeImportVariables = { type: "importvariables", attributes: { filter: { name: "filter", type: "string" } }, isBlock: false, children: []}, parseTreeTransclude = { type: "transclude", attributes: { recursionMarker: { name: "recursionMarker", type: "string", value: options.recursionMarker || "yes" }, tiddler: { name: "tiddler", type: "string", value: title } }, isBlock: !options.parseAsInline}; if(options.importVariables || options.importPageMacros) { if(options.importVariables) { parseTreeImportVariables.attributes.filter.value = options.importVariables; } else if(options.importPageMacros) { parseTreeImportVariables.attributes.filter.value = this.getTiddlerText("$:/core/config/GlobalImportFilter"); } parseTreeDiv.tree[0].children.push(parseTreeImportVariables); parseTreeImportVariables.children.push(parseTreeTransclude); } else { parseTreeDiv.tree[0].children.push(parseTreeTransclude); } if(options.field) { parseTreeTransclude.attributes.field = {type: "string", value: options.field}; } if(options.mode) { parseTreeTransclude.attributes.mode = {type: "string", value: options.mode}; } if(options.children) { parseTreeTransclude.children = options.children; } return this.makeWidget(parseTreeDiv,options); }; /* 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 options: see below Options include: variables: hashmap of variables to set parentWidget: optional parent widget for the root node */ exports.renderText = function(outputType,textType,text,options) { options = options || {}; var parser = this.parseText(textType,text,options), widgetNode = this.makeWidget(parser,options); var container = $tw.fakeDocument.createElement("div"); widgetNode.render(container,null); return outputType === "text/html" ? container.innerHTML : container.textContent; }; /* 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 options: see below Options include: variables: hashmap of variables to set parentWidget: optional parent widget for the root node */ exports.renderTiddler = function(outputType,title,options) { options = options || {}; var parser = this.parseTiddler(title,options), widgetNode = this.makeWidget(parser,options); var container = $tw.fakeDocument.createElement("div"); widgetNode.render(container,null); return outputType === "text/html" ? container.innerHTML : (outputType === "text/plain-formatted" ? container.formattedTextContent : container.textContent); }; /* Return an array of tiddler titles that match a search string text: The text string to search for options: see below Options available: source: an iterator function for the source tiddlers, called source(iterator), where iterator is called as iterator(tiddler,title) 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 field: If specified, restricts the search to the specified field, or an array of field names anchored: If true, forces all but regexp searches to be anchored to the start of text excludeField: If true, the field options are inverted to specify the fields that are not to be searched The search mode is determined by the first of these boolean flags to be true literal: searches for literal string whitespace: same as literal except runs of whitespace are treated as a single space regexp: treats the search term as a regular expression words: (default) treats search string as a list of tokens, and matches if all tokens are found, regardless of adjacency or ordering some: treats search string as a list of tokens, and matches if at least ONE token is found */ exports.search = function(text,options) { options = options || {}; var self = this, t, regExpStr="", invert = !!options.invert; // Convert the search string into a regexp for each term var terms, searchTermsRegExps, flags = options.caseSensitive ? "" : "i", anchor = options.anchored ? "^" : ""; if(options.literal) { if(text.length === 0) { searchTermsRegExps = null; } else { searchTermsRegExps = [new RegExp("(" + anchor + $tw.utils.escapeRegExp(text) + ")",flags)]; } } else if(options.whitespace) { terms = []; $tw.utils.each(text.split(/\s+/g),function(term) { if(term) { terms.push($tw.utils.escapeRegExp(term)); } }); searchTermsRegExps = [new RegExp("(" + anchor + terms.join("\\s+") + ")",flags)]; } else if(options.regexp) { try { searchTermsRegExps = [new RegExp("(" + text + ")",flags)]; } catch(e) { searchTermsRegExps = null; console.log("Regexp error parsing /(" + text + ")/" + flags + ": ",e); } } else if(options.some) { terms = text.trim().split(/[^\S\xA0]+/); if(terms.length === 1 && terms[0] === "") { searchTermsRegExps = null; } else { searchTermsRegExps = []; for(t=0; t0 && fieldIndex