diff --git a/core/modules/parsers/wikiparser/rules/block/heading.js b/core/modules/parsers/wikiparser/rules/block/heading.js new file mode 100644 index 000000000..e05da0338 --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/block/heading.js @@ -0,0 +1,57 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/block/heading.js +type: application/javascript +module-type: wikiblockrule + +Wiki text block rule for headings + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var HeadingRule = function(parser,startPos) { + // Save state + this.parser = parser; + // Regexp to match + this.reMatch = /(!{1,6})/mg; + // Get the first match + this.matchIndex = startPos-1; + this.findNextMatch(startPos); +}; + +HeadingRule.prototype.findNextMatch = function(startPos) { + if(this.matchIndex !== undefined && startPos > this.matchIndex) { + this.reMatch.lastIndex = startPos; + this.match = this.reMatch.exec(this.parser.source); + this.matchIndex = this.match ? this.match.index : undefined; + } + return this.matchIndex; +}; + +/* +Parse the most recent match +*/ +HeadingRule.prototype.parse = function() { + // Get all the details of the match + var headingLevel = this.match[1].length; + // Move past the !s + this.parser.pos = this.reMatch.lastIndex; + // Parse the heading + var classedRun = this.parser.parseClassedRun(/(\r?\n)/mg); + // Return the heading + return [{ + type: "element", + tag: "h" + this.match[1].length, + attributes: { + "class": {type: "string", value: classedRun["class"]} + }, + children: classedRun.tree + }]; +}; + +exports.HeadingRule = HeadingRule; + +})(); diff --git a/core/modules/parsers/wikiparser/rules/block/list.js b/core/modules/parsers/wikiparser/rules/block/list.js new file mode 100644 index 000000000..27834f154 --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/block/list.js @@ -0,0 +1,141 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/block/list.js +type: application/javascript +module-type: wikiblockrule + +Wiki text block rule for lists. For example: + +{{{ +* This is an unordered list +* It has two items + +# This is a numbered list +## With a subitem +# And a third item + +; This is a term that is being defined +: This is the definition of that term +}}} + +Note that lists can be nested arbitrarily: + +{{{ +#** One +#* Two +#** Three +#**** Four +#**# Five +#**## Six +## Seven +### Eight +## Nine +}}} + +A CSS class can be applied to a list item as follows: + +{{{ +* List item one +*.active List item two has the class `active` +* List item three +}}} + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var ListRule = function(parser,startPos) { + // Save state + this.parser = parser; + // Regexp to match + this.reMatch = /([\\*#;:]+)/mg; + // Get the first match + this.matchIndex = startPos-1; + this.findNextMatch(startPos); +}; + +ListRule.prototype.findNextMatch = function(startPos) { + if(this.matchIndex !== undefined && startPos > this.matchIndex) { + this.reMatch.lastIndex = startPos; + this.match = this.reMatch.exec(this.parser.source); + this.matchIndex = this.match ? this.match.index : undefined; + } + return this.matchIndex; +}; + +var listTypes = { + "*": {listTag: "ul", itemTag: "li"}, + "#": {listTag: "ol", itemTag: "li"}, + ";": {listTag: "dl", itemTag: "dt"}, + ":": {listTag: "dl", itemTag: "dd"} +}; + +/* +Parse the most recent match +*/ +ListRule.prototype.parse = function() { + // Array of parse tree nodes for the previous row of the list + var listStack = []; + // Cycle through the items in the list + while(true) { + // Match the list marker + var reMatch = /(^[\*#;:]+)/mg; + reMatch.lastIndex = this.parser.pos; + var match = reMatch.exec(this.parser.source); + if(!match || match.index !== this.parser.pos) { + break; + } + // Check whether the list type of the top level matches + var listInfo = listTypes[match[0].charAt(0)]; + if(listStack.length > 0 && listStack[0].tag !== listInfo.listTag) { + break; + } + // Move past the list marker + this.parser.pos = match.index + match[0].length; + // Walk through the list markers for the current row + for(var t=0; t t && listStack[t].tag !== listInfo.listTag) { + listStack.splice(t,listStack.length - t); + } + // Construct the list element or reuse the previous one at this level + if(listStack.length <= t) { + var listElement = {type: "element", tag: listInfo.listTag, children: [ + {type: "element", tag: listInfo.itemTag, children: []} + ]}; + // Link this list element into the last child item of the parent list item + if(t) { + var prevListItem = listStack[t-1].children[listStack[t-1].children.length-1]; + prevListItem.children.push(listElement); + } + // Save this element in the stack + listStack[t] = listElement; + } else if(t === (match[0].length - 1)) { + listStack[t].children.push({type: "element", tag: listInfo.itemTag, children: []}); + } + } + if(listStack.length > match[0].length) { + listStack.splice(match[0].length,listStack.length - match[0].length); + } + // Process the body of the list item into the last list item + var lastListChildren = listStack[listStack.length-1].children, + lastListItem = lastListChildren[lastListChildren.length-1], + classedRun = this.parser.parseClassedRun(/(\r?\n)/mg); + lastListItem.children.push.apply(lastListItem.children,classedRun.tree); + if(classedRun["class"]) { + lastListItem.attributes = lastListItem.attributes || {}; + lastListItem.attributes["class"] = {type: "string", value: classedRun["class"]}; + } + // Consume any whitespace following the list item + this.parser.skipWhitespace(); + }; + // Return the root element of the list + return [listStack[0]]; +}; + +exports.ListRule = ListRule; + +})(); diff --git a/core/modules/parsers/wikiparser/rules/pragma/macrodef.js b/core/modules/parsers/wikiparser/rules/pragma/macrodef.js new file mode 100644 index 000000000..11ca07dae --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/pragma/macrodef.js @@ -0,0 +1,98 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/pragma/macrodef.js +type: application/javascript +module-type: wikipragmarule + +Wiki pragma rule for macro definitions + +{{{ +/define name(param:defaultvalue,param2:defaultvalue) +definition text, including $param$ markers +/end +}}} + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Instantiate parse rule +*/ +var MacroDefRule = function(parser,startPos) { + // Save state + this.parser = parser; + // Regexp to match + this.reMatch = /^\\define\s*([^(\s]+)\(\s*([^)]*)\)(\r?\n)?/mg; + // Get the first match + this.matchIndex = startPos-1; + this.findNextMatch(startPos); +}; + +MacroDefRule.prototype.findNextMatch = function(startPos) { + if(this.matchIndex !== undefined && startPos > this.matchIndex) { + this.reMatch.lastIndex = startPos; + this.match = this.reMatch.exec(this.parser.source); + this.matchIndex = this.match ? this.match.index : undefined; + } + return this.matchIndex; +}; + +/* +Parse the most recent match +*/ +MacroDefRule.prototype.parse = function() { + // Move past the macro name and parameters + this.parser.pos = this.reMatch.lastIndex; + // Parse the parameters + var paramString = this.match[2], + params = []; + if(paramString !== "") { + var reParam = /\s*([A-Za-z0-9\-_]+)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|\[\[([^\]]*)\]\]|([^"'\s]+)))?/mg, + paramMatch = reParam.exec(paramString); + while(paramMatch) { + // Save the parameter details + var paramInfo = {name: paramMatch[1]}, + defaultValue = paramMatch[2] || paramMatch[3] || paramMatch[4] || paramMatch[5]; + if(defaultValue) { + paramInfo["default"] = defaultValue; + } + params.push(paramInfo); + // Look for the next parameter + paramMatch = reParam.exec(paramString); + } + } + // Is this a multiline definition? + var reEnd; + if(this.match[3]) { + // If so, the end of the body is marked with \end + reEnd = /(\r?\n\\end\r?\n)/mg; + } else { + // Otherwise, the end of the definition is marked by the end of the line + reEnd = /(\r?\n)/mg; + } + // Find the end of the definition + reEnd.lastIndex = this.parser.pos; + var text, + endMatch = reEnd.exec(this.parser.source); + if(endMatch) { + text = this.parser.source.substring(this.parser.pos,endMatch.index).trim(); + this.parser.pos = endMatch.index + endMatch[0].length; + } else { + // We didn't find the end of the definition, so we'll make it blank + text = ""; + } + // Save the macro definition + this.parser.macroDefinitions[this.match[1]] = { + type: "textmacro", + name: this.match[1], + params: params, + text: text + }; +}; + +exports.MacroDefRule = MacroDefRule; + +})(); diff --git a/core/modules/parsers/wikiparser/rules/run/entity.js b/core/modules/parsers/wikiparser/rules/run/entity.js new file mode 100644 index 000000000..6d02c65a7 --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/run/entity.js @@ -0,0 +1,52 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/run/entity.js +type: application/javascript +module-type: wikirunrule + +Wiki text run rule for HTML entities. For example: + +{{{ + This is a copyright symbol: © +}}} + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var EntityRule = function(parser,startPos) { + // Save state + this.parser = parser; + // Regexp to match + this.reMatch = /(&#?[a-zA-Z0-9]{2,8};)/mg; + // Get the first match + this.matchIndex = startPos-1; + this.findNextMatch(startPos); +}; + +EntityRule.prototype.findNextMatch = function(startPos) { + if(this.matchIndex !== undefined && startPos > this.matchIndex) { + this.reMatch.lastIndex = startPos; + this.match = this.reMatch.exec(this.parser.source); + this.matchIndex = this.match ? this.match.index : undefined; + } + return this.matchIndex; +}; + +/* +Parse the most recent match +*/ +EntityRule.prototype.parse = function() { + // Get all the details of the match + var entityString = this.match[1]; + // Move past the macro call + this.parser.pos = this.reMatch.lastIndex; + // Return the entity + return [{type: "entity", entity: this.match[0]}]; +}; + +exports.EntityRule = EntityRule; + +})(); diff --git a/core/modules/parsers/wikiparser/rules/run/html.js b/core/modules/parsers/wikiparser/rules/run/html.js new file mode 100644 index 000000000..8270867da --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/run/html.js @@ -0,0 +1,113 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/run/html.js +type: application/javascript +module-type: wikirunrule + +Wiki rule for HTML elements and widgets. For example: + +{{{ + + +<_slider target="MyTiddler"> +This is a widget invocation + + +}}} + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var voidElements = "area,base,br,col,command,embed,hr,img,input,keygen,link,meta,param,source,track,wbr".split(","); + +var HtmlRule = function(parser,startPos) { + // Save state + this.parser = parser; + // Regexp to match + this.reMatch = /<(_)?([A-Za-z]+)(\s*[^>]*?)(\/)?>/mg; + // Get the first match + this.matchIndex = startPos-1; + this.findNextMatch(startPos); +}; + +HtmlRule.prototype.findNextMatch = function(startPos) { + if(this.matchIndex !== undefined && startPos > this.matchIndex) { + this.reMatch.lastIndex = startPos; + this.match = this.reMatch.exec(this.parser.source); + this.matchIndex = this.match ? this.match.index : undefined; + } + return this.matchIndex; +}; + +/* +Parse the most recent match +*/ +HtmlRule.prototype.parse = function() { + // Get all the details of the match in case this parser is called recursively + var isWidget = !!this.match[1], + tagName = this.match[2], + attributeString = this.match[3], + isSelfClosing = !!this.match[4]; + // Move past the tag name and parameters + this.parser.pos = this.reMatch.lastIndex; + var reLineBreak = /(\r?\n)/mg, + reAttr = /\s*([A-Za-z\-_]+)(?:\s*=\s*(?:("[^"]*")|('[^']*')|(\{\{[^\}]*\}\})|([^"'\s]+)))?/mg, + isBlock; + // Process the attributes + var attrMatch = reAttr.exec(attributeString), + attributes = {}; + while(attrMatch) { + var name = attrMatch[1], + value; + if(attrMatch[2]) { // Double quoted + value = {type: "string", value: attrMatch[2].substring(1,attrMatch[2].length-1)}; + } else if(attrMatch[3]) { // Single quoted + value = {type: "string", value: attrMatch[3].substring(1,attrMatch[3].length-1)}; + } else if(attrMatch[4]) { // Double curly brace quoted + value = {type: "indirect", textReference: attrMatch[4].substr(2,attrMatch[4].length-4)}; + } else if(attrMatch[5]) { // Unquoted + value = {type: "string", value: attrMatch[5]}; + } else { // Valueless + value = {type: "string", value: "true"}; // TODO: We should have a way of indicating we want an attribute without a value + } + attributes[name] = value; + attrMatch = reAttr.exec(attributeString); + } + // Check for a line break immediate after the opening tag + reLineBreak.lastIndex = this.parser.pos; + var lineBreakMatch = reLineBreak.exec(this.parser.source); + if(lineBreakMatch && lineBreakMatch.index === this.parser.pos) { + this.parser.pos = lineBreakMatch.index + lineBreakMatch[0].length; + isBlock = true; + } else { + isBlock = false; + } + if(!isSelfClosing && (isWidget || voidElements.indexOf(tagName) === -1)) { + var reEndString = "()", + reEnd = new RegExp(reEndString,"mg"), + content; + if(isBlock) { + content = this.parser.parseBlocks(reEndString); + } else { + content = this.parser.parseRun(reEnd); + } + reEnd.lastIndex = this.parser.pos; + var endMatch = reEnd.exec(this.parser.source); + if(endMatch && endMatch.index === this.parser.pos) { + this.parser.pos = endMatch.index + endMatch[0].length; + } + } else { + content = []; + } + var element = {type: isWidget ? "widget" : "element", tag: tagName, isBlock: isBlock, attributes: attributes, children: content}; + return [element]; +}; + +exports.HtmlRule = HtmlRule; + +})(); diff --git a/core/modules/parsers/wikiparser/rules/run/macrocall.js b/core/modules/parsers/wikiparser/rules/run/macrocall.js new file mode 100644 index 000000000..f9c45d46c --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/run/macrocall.js @@ -0,0 +1,71 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/run/macrocall.js +type: application/javascript +module-type: wikirunrule + +Wiki rule for macro calls + +{{{ +<> +}}} + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var MacroCallRule = function(parser,startPos) { + // Save state + this.parser = parser; + // Regexp to match + this.reMatch = /<<([^\s>]+)\s*([\s\S]*?)>>/mg; + // Get the first match + this.matchIndex = startPos-1; + this.findNextMatch(startPos); +}; + +MacroCallRule.prototype.findNextMatch = function(startPos) { + if(this.matchIndex !== undefined && startPos > this.matchIndex) { + this.reMatch.lastIndex = startPos; + this.match = this.reMatch.exec(this.parser.source); + this.matchIndex = this.match ? this.match.index : undefined; + } + return this.matchIndex; +}; + +/* +Parse the most recent match +*/ +MacroCallRule.prototype.parse = function() { + // Get all the details of the match + var macroName = this.match[1], + paramString = this.match[2]; + // Move past the macro call + this.parser.pos = this.reMatch.lastIndex; + var params = [], + reParam = /\s*(?:([A-Za-z0-9\-_]+)\s*:)?(?:\s*(?:"([^"]*)"|'([^']*)'|\[\[([^\]]*)\]\]|([^"'\s]+)))/mg, + paramMatch = reParam.exec(paramString); + while(paramMatch) { + // Process this parameter + var paramInfo = { + value: paramMatch[2] || paramMatch[3] || paramMatch[4] || paramMatch[5] + }; + if(paramMatch[1]) { + paramInfo.name = paramMatch[1]; + } + params.push(paramInfo); + // Find the next match + paramMatch = reParam.exec(paramString); + } + return [{ + type: "macrocall", + name: macroName, + params: params + }]; +}; + +exports.MacroCallRule = MacroCallRule; + +})(); diff --git a/core/modules/parsers/wikiparser/wikiparser.js b/core/modules/parsers/wikiparser/wikiparser.js new file mode 100644 index 000000000..af63ed004 --- /dev/null +++ b/core/modules/parsers/wikiparser/wikiparser.js @@ -0,0 +1,296 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/wikiparser.js +type: application/javascript +module-type: global + +The wiki text parser processes blocks of source text into a parse tree. + +The parse tree is made up of nested arrays of these JavaScript objects: + + {type: "element", tag: , attributes: {}, children: []} - an HTML element + {type: "text", text: } - a text node + {type: "entity", value: } - an entity + {type: "raw", html: } - raw HTML + +Attributes are stored as hashmaps of the following objects: + + {type: "string", value: } - literal string + {type: "array", value: } - array of strings + {type: "styles", value: } - hashmap of style strings + {type: "indirect", textReference: } - indirect through a text reference + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var WikiParser = function(vocabulary,type,text,options) { + this.wiki = options.wiki; + this.vocabulary = vocabulary; + // Save the parse text + this.type = type || "text/vnd.tiddlywiki"; + this.source = text || ""; + this.sourceLength = this.source.length; + // Set current parse position + this.pos = 0; + // Initialise the things that pragma rules can change + this.macroDefinitions = {}; // Hash map of macro definitions + // Instantiate the pragma parse rules + this.pragmaRules = this.instantiateRules(this.vocabulary.pragmaRuleClasses,0); + // Parse any pragmas + this.parsePragmas(); + // Instantiate the parser block and run rules + this.blockRules = this.instantiateRules(this.vocabulary.blockRuleClasses,this.pos); + this.runRules = this.instantiateRules(this.vocabulary.runRuleClasses,this.pos); + // Parse the text into runs or blocks + if(this.type === "text/vnd.tiddlywiki-run") { + this.tree = this.parseRun(); + } else { + this.tree = this.parseBlocks(); + } +}; + +/* +Instantiate an array of parse rules +*/ +WikiParser.prototype.instantiateRules = function(classes,startPos) { + var rules = [], + self = this; + $tw.utils.each(classes,function(RuleClass) { + // Instantiate the rule + var rule = new RuleClass(self,startPos); + // Only save the rule if there is at least one match + if(rule.matchIndex !== undefined) { + rules.push(rule); + } + }); + return rules; +}; + +/* +Skip any whitespace at the current position. Options are: + treatNewlinesAsNonWhitespace: true if newlines are NOT to be treated as whitespace +*/ +WikiParser.prototype.skipWhitespace = function(options) { + options = options || {}; + var whitespaceRegExp = options.treatNewlinesAsNonWhitespace ? /([^\S\n]+)/mg : /(\s+)/mg; + whitespaceRegExp.lastIndex = this.pos; + var whitespaceMatch = whitespaceRegExp.exec(this.source); + if(whitespaceMatch && whitespaceMatch.index === this.pos) { + this.pos = whitespaceRegExp.lastIndex; + } +}; + +/* +Get the next match out of an array of parse rule instances +*/ +WikiParser.prototype.findNextMatch = function(rules,startPos) { + var nextMatch = undefined, + nextMatchPos = this.sourceLength; + for(var t=0; t= this.sourceLength) { + return; + } + // Check if we've arrived at a pragma rule match + var nextMatch = this.findNextMatch(this.pragmaRules,this.pos); + // If not, just exit + if(!nextMatch || nextMatch.matchIndex !== this.pos) { + return; + } + // Process the pragma rule + nextMatch.parse(); + } +}; + +/* +Parse a block from the current position + terminatorRegExpString: optional regular expression string that identifies the end of plain paragraphs. Must not include capturing parenthesis +*/ +WikiParser.prototype.parseBlock = function(terminatorRegExpString) { + var terminatorRegExp = terminatorRegExpString ? new RegExp("(" + terminatorRegExpString + "|\\r?\\n\\r?\\n)","mg") : /(\r?\n\r?\n)/mg; + this.skipWhitespace(); + if(this.pos >= this.sourceLength) { + return []; + } + // Look for a block rule that applies at the current position + var nextMatch = this.findNextMatch(this.blockRules,this.pos); + if(nextMatch && nextMatch.matchIndex === this.pos) { + return nextMatch.parse(); + } + // Treat it as a paragraph if we didn't find a block rule + return [{type: "element", tag: "p", children: this.parseRun(terminatorRegExp)}]; +}; + +/* +Parse a series of blocks of text until a terminating regexp is encountered or the end of the text + terminatorRegExpString: terminating regular expression +*/ +WikiParser.prototype.parseBlocks = function(terminatorRegExpString) { + if(terminatorRegExpString) { + return this.parseBlocksTerminated(terminatorRegExpString); + } else { + return this.parseBlocksUnterminated(); + } +}; + +/* +Parse a block from the current position to the end of the text +*/ +WikiParser.prototype.parseBlocksUnterminated = function() { + var tree = []; + while(this.pos < this.sourceLength) { + tree.push.apply(tree,this.parseBlock()); + } + return tree; +}; + +/* +Parse blocks of text until a terminating regexp is encountered +*/ +WikiParser.prototype.parseBlocksTerminated = function(terminatorRegExpString) { + var terminatorRegExp = new RegExp("(" + terminatorRegExpString + ")","mg"), + tree = []; + // Skip any whitespace + this.skipWhitespace(); + // Check if we've got the end marker + terminatorRegExp.lastIndex = this.pos; + var match = terminatorRegExp.exec(this.source); + // Parse the text into blocks + while(this.pos < this.sourceLength && !(match && match.index === this.pos)) { + var blocks = this.parseBlock(terminatorRegExpString); + tree.push.apply(tree,blocks); + // Skip any whitespace + this.skipWhitespace(); + // Check if we've got the end marker + terminatorRegExp.lastIndex = this.pos; + match = terminatorRegExp.exec(this.source); + } + if(match && match.index === this.pos) { + this.pos = match.index + match[0].length; + } + return tree; +}; + +/* +Parse a run of text at the current position + terminatorRegExp: a regexp at which to stop the run +*/ +WikiParser.prototype.parseRun = function(terminatorRegExp) { + if(terminatorRegExp) { + return this.parseRunTerminated(terminatorRegExp); + } else { + return this.parseRunUnterminated(); + } +}; + +WikiParser.prototype.parseRunUnterminated = function() { + var tree = []; + // Find the next occurrence of a runrule + var nextMatch = this.findNextMatch(this.runRules,this.pos); + // Loop around the matches until we've reached the end of the text + while(this.pos < this.sourceLength && nextMatch) { + // Process the text preceding the run rule + if(nextMatch.matchIndex > this.pos) { + tree.push({type: "text", text: this.source.substring(this.pos,nextMatch.matchIndex)}); + this.pos = nextMatch.matchIndex; + } + // Process the run rule + tree.push.apply(tree,nextMatch.parse()); + // Look for the next run rule + nextMatch = this.findNextMatch(this.runRules,this.pos); + } + // Process the remaining text + if(this.pos < this.sourceLength) { + tree.push({type: "text", text: this.source.substr(this.pos)}); + } + this.pos = this.sourceLength; + return tree; +}; + +WikiParser.prototype.parseRunTerminated = function(terminatorRegExp) { + var tree = []; + // Find the next occurrence of the terminator + terminatorRegExp.lastIndex = this.pos; + var terminatorMatch = terminatorRegExp.exec(this.source); + // Find the next occurrence of a runrule + var runRuleMatch = this.findNextMatch(this.runRules,this.pos); + // Loop around until we've reached the end of the text + while(this.pos < this.sourceLength && (terminatorMatch || runRuleMatch)) { + // Return if we've found the terminator, and it precedes any run rule match + if(terminatorMatch) { + if(!runRuleMatch || runRuleMatch.matchIndex >= terminatorMatch.index) { + if(terminatorMatch.index > this.pos) { + tree.push({type: "text", text: this.source.substring(this.pos,terminatorMatch.index)}); + } + this.pos = terminatorMatch.index; + return tree; + } + } + // Process any run rule, along with the text preceding it + if(runRuleMatch) { + // Preceding text + if(runRuleMatch.matchIndex > this.pos) { + tree.push({type: "text", text: this.source.substring(this.pos,runRuleMatch.matchIndex)}); + this.pos = runRuleMatch.matchIndex; + } + // Process the run rule + tree.push.apply(tree,runRuleMatch.parse()); + // Look for the next run rule + runRuleMatch = this.findNextMatch(this.runRules,this.pos); + // Look for the next terminator match + terminatorRegExp.lastIndex = this.pos; + terminatorMatch = terminatorRegExp.exec(this.source); + } + } + // Process the remaining text + if(this.pos < this.sourceLength) { + tree.push({type: "text", text: this.source.substr(this.pos)}); + } + this.pos = this.sourceLength; + return tree; +}; + +/* +Parse a run of text preceded by zero or more class specifiers `.classname` +*/ +WikiParser.prototype.parseClassedRun = function(terminatorRegExp) { + var classRegExp = /\.([^\s\.]+)/mg, + classNames = []; + classRegExp.lastIndex = this.pos; + var match = classRegExp.exec(this.source); + while(match && match.index === this.pos) { + this.pos = match.index + match[0].length; + classNames.push(match[1]); + var match = classRegExp.exec(this.source); + } + this.skipWhitespace({treatNewlinesAsNonWhitespace: true}); + var tree = this.parseRun(terminatorRegExp); + return { + "class": classNames.join(" "), + tree: tree + }; +}; + +exports.WikiParser = WikiParser; + +})(); + diff --git a/core/modules/rendertree/renderers/element.js b/core/modules/rendertree/renderers/element.js new file mode 100644 index 000000000..22f604aa2 --- /dev/null +++ b/core/modules/rendertree/renderers/element.js @@ -0,0 +1,151 @@ +/*\ +title: $:/core/modules/rendertree/renderers/element.js +type: application/javascript +module-type: wikirenderer + +Element renderer + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Element renderer +*/ +var ElementRenderer = function(renderTree,renderContext,parseTreeNode) { + // Store state information + this.renderTree = renderTree; + this.renderContext = renderContext; + this.parseTreeNode = parseTreeNode; + // Compute our dependencies + this.dependencies = {}; + var self = this; + $tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) { + if(attribute.type === "indirect") { + var tr = $tw.utils.parseTextReference(attribute.textReference); + if(tr.title) { + self.dependencies[tr.title] = true; + } else { + self.dependencies[renderContext.tiddlerTitle] = true; + } + } + }); + // Compute our attributes + this.computeAttributes(); + // Create the renderers for the child nodes + this.children = this.renderTree.createRenderers(this.renderContext,this.parseTreeNode.children); +}; + +ElementRenderer.prototype.computeAttributes = function() { + this.attributes = {}; + var self = this; + $tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) { + if(attribute.type === "indirect") { + self.attributes[name] = self.renderTree.wiki.getTextReference(attribute.textReference,self.renderContext.tiddlerTitle); + } else { // String attribute + self.attributes[name] = attribute.value; + } + }); +}; + +ElementRenderer.prototype.render = function(type) { + var isHtml = type === "text/html", + output = [],attr,a,v; + if(isHtml) { + output.push("<",this.parseTreeNode.tag); + if(this.attributes) { + attr = []; + for(a in this.attributes) { + attr.push(a); + } + attr.sort(); + for(a=0; a"); + } + if(this.children && this.children.length > 0) { + $tw.utils.each(this.children,function(node) { + if(node.render) { + output.push(node.render(type)); + } + }); + if(isHtml) { + output.push(""); + } + } + return output.join(""); +}; + +ElementRenderer.prototype.renderInDom = function() { + // Create the element + this.domNode = document.createElement(this.parseTreeNode.tag); + // Assign the attributes + this.assignAttributes(); + // Render any child nodes + var self = this; + $tw.utils.each(this.children,function(node) { + if(node.renderInDom) { + self.domNode.appendChild(node.renderInDom()); + } + }); + // Assign any specified event handlers + $tw.utils.addEventListeners(this.domNode,this.parseTreeNode.events); + // Return the dom node + return this.domNode; +}; + +ElementRenderer.prototype.assignAttributes = function() { + var self = this; + $tw.utils.each(this.attributes,function(v,a) { + if(v !== undefined) { + if($tw.utils.isArray(v)) { // Ahem, could there be arrays other than className? + self.domNode.className = v.join(" "); + } else if (typeof v === "object") { // ...or objects other than style? + for(var p in v) { + self.domNode.style[$tw.utils.unHyphenateCss(p)] = v[p]; + } + } else { + self.domNode.setAttribute(a,v); + } + } + }); +}; + +ElementRenderer.prototype.refreshInDom = function(changes) { + // Check if any of our dependencies have changed + if($tw.utils.checkDependencies(this.dependencies,changes)) { + // Update our attributes + this.computeAttributes(); + this.assignAttributes(); + } + // Refresh any child nodes + $tw.utils.each(this.children,function(node) { + if(node.refreshInDom) { + node.refreshInDom(changes); + } + }); +}; + +exports.element = ElementRenderer + +})(); diff --git a/core/modules/rendertree/renderers/entity.js b/core/modules/rendertree/renderers/entity.js new file mode 100644 index 000000000..038ab37a3 --- /dev/null +++ b/core/modules/rendertree/renderers/entity.js @@ -0,0 +1,35 @@ +/*\ +title: $:/core/modules/rendertree/renderers/entity.js +type: application/javascript +module-type: wikirenderer + +Entity renderer + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Entity renderer +*/ +var EntityRenderer = function(renderTree,renderContext,parseTreeNode) { + // Store state information + this.renderTree = renderTree; + this.renderContext = renderContext; + this.parseTreeNode = parseTreeNode; +}; + +EntityRenderer.prototype.render = function(type) { + return type === "text/html" ? this.parseTreeNode.entity : $tw.utils.entityDecode(this.parseTreeNode.entity); +}; + +EntityRenderer.prototype.renderInDom = function() { + return document.createTextNode($tw.utils.entityDecode(this.parseTreeNode.entity)); +}; + +exports.entity = EntityRenderer + +})(); diff --git a/core/modules/rendertree/renderers/macrocall.js b/core/modules/rendertree/renderers/macrocall.js new file mode 100644 index 000000000..08ad33432 --- /dev/null +++ b/core/modules/rendertree/renderers/macrocall.js @@ -0,0 +1,101 @@ +/*\ +title: $:/core/modules/rendertree/renderers/macrocall.js +type: application/javascript +module-type: wikirenderer + +Macro call renderer + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Macro call renderer +*/ +var MacroCallRenderer = function(renderTree,renderContext,parseTreeNode) { + // Store state information + this.renderTree = renderTree; + this.renderContext = renderContext; + this.parseTreeNode = parseTreeNode; + // Find the macro definition + var macro,childTree; + if($tw.utils.hop(this.renderTree.parser.macroDefinitions,this.parseTreeNode.name)) { + macro = this.renderTree.parser.macroDefinitions[this.parseTreeNode.name]; + } + // Insert an error message if we couldn't find the macro + if(!macro) { + childTree = [{type: "text", text: "<>"}]; + } else { + // Substitute the macro parameters + var text = this.substituteParameters(macro.text,this.parseTreeNode,macro); + // Parse the text + childTree = this.renderTree.wiki.new_parseText("text/vnd.tiddlywiki",text).tree; + } + // Create the renderers for the child nodes + this.children = this.renderTree.createRenderers(this.renderContext,childTree); +}; + +/* +Expand the parameters in a block of text +*/ +MacroCallRenderer.prototype.substituteParameters = function(text,macroCallParseTreeNode,macroDefinition) { + var nextAnonParameter = 0; // Next candidate anonymous parameter in macro call + // Step through each of the parameters in the macro definition + for(var p=0; p 0) { + while(macroCallParseTreeNode.params[nextAnonParameter].name && nextAnonParameter < macroCallParseTreeNode.params.length-1) { + nextAnonParameter++; + } + if(!macroCallParseTreeNode.params[nextAnonParameter].name) { + paramValue = macroCallParseTreeNode.params[nextAnonParameter].value; + nextAnonParameter++; + } + } + // If we've still not got a value, use the default, if any + paramValue = paramValue || paramInfo["default"] || ""; + // Replace any instances of this parameter + text = text.replace(new RegExp("\\$" + $tw.utils.escapeRegExp(paramInfo.name) + "\\$","mg"),paramValue); + } + return text; +}; + +MacroCallRenderer.prototype.render = function(type) { + var output = []; + $tw.utils.each(this.children,function(node) { + if(node.render) { + output.push(node.render(type)); + } + }); + return output.join(""); +}; + +MacroCallRenderer.prototype.renderInDom = function() { + // Create the element + this.domNode = document.createElement("macrocall"); + this.domNode.setAttribute("data-macro-name",this.parseTreeNode.name); + // Render any child nodes + var self = this; + $tw.utils.each(this.children,function(node,index) { + if(node.renderInDom) { + self.domNode.appendChild(node.renderInDom()); + } + }); + // Return the dom node + return this.domNode; +}; + +exports.macrocall = MacroCallRenderer + +})(); diff --git a/core/modules/rendertree/renderers/raw.js b/core/modules/rendertree/renderers/raw.js new file mode 100644 index 000000000..ecf0cb811 --- /dev/null +++ b/core/modules/rendertree/renderers/raw.js @@ -0,0 +1,37 @@ +/*\ +title: $:/core/modules/rendertree/renderers/raw.js +type: application/javascript +module-type: wikirenderer + +Raw HTML renderer + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Raw HTML renderer +*/ +var RawRenderer = function(renderTree,renderContext,parseTreeNode) { + // Store state information + this.renderTree = renderTree; + this.renderContext = renderContext; + this.parseTreeNode = parseTreeNode; +}; + +RawRenderer.prototype.render = function(type) { + return this.parseTreeNode.html; +}; + +RawRenderer.prototype.renderInDom = function() { + var domNode = document.createElement("div"); + domNode.innerHTML = this.parseTreeNode.html; + return domNode; +}; + +exports.raw = RawRenderer + +})(); diff --git a/core/modules/rendertree/renderers/text.js b/core/modules/rendertree/renderers/text.js new file mode 100644 index 000000000..b7e2a13cd --- /dev/null +++ b/core/modules/rendertree/renderers/text.js @@ -0,0 +1,35 @@ +/*\ +title: $:/core/modules/rendertree/renderers/text.js +type: application/javascript +module-type: wikirenderer + +Text renderer + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Text renderer +*/ +var TextRenderer = function(renderTree,renderContext,parseTreeNode) { + // Store state information + this.renderTree = renderTree; + this.renderContext = renderContext; + this.parseTreeNode = parseTreeNode; +}; + +TextRenderer.prototype.render = function(type) { + return type === "text/html" ? $tw.utils.htmlEncode(this.parseTreeNode.text) : this.parseTreeNode.text; +}; + +TextRenderer.prototype.renderInDom = function() { + return document.createTextNode(this.parseTreeNode.text); +}; + +exports.text = TextRenderer + +})(); diff --git a/core/modules/rendertree/renderers/widget.js b/core/modules/rendertree/renderers/widget.js new file mode 100644 index 000000000..7591d5af7 --- /dev/null +++ b/core/modules/rendertree/renderers/widget.js @@ -0,0 +1,151 @@ +/*\ +title: $:/core/modules/rendertree/renderers/widget.js +type: application/javascript +module-type: wikirenderer + +Widget renderer. + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Widget renderer +*/ +var WidgetRenderer = function(renderTree,renderContext,parseTreeNode) { + // Store state information + this.renderTree = renderTree; + this.renderContext = renderContext; + this.parseTreeNode = parseTreeNode; + // Compute the default dependencies + this.dependencies = {}; + var self = this; + $tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) { + if(attribute.type === "indirect") { + var tr = self.renderTree.wiki.parseTextReference(attribute.textReference); + if(tr.title) { + self.dependencies[tr.title] = true; + } else { + self.dependencies[renderContext.tiddlerTitle] = true; + } + } + }); + // Compute our attributes + this.attributes = {}; + this.computeAttributes(); + // Create the widget object + var WidgetClass = this.renderTree.parser.vocabulary.widgetClasses[this.parseTreeNode.tag]; + if(WidgetClass) { + this.widget = new WidgetClass(this); + } else { + // Error if we couldn't find the widget + this.children = this.renderTree.createRenderers(this.renderContext,[ + {type: "text", text: "Unknown widget type '" + this.parseTreeNode.tag + "'"} + ]); + } +}; + +WidgetRenderer.prototype.computeAttributes = function() { + var changedAttributes = {}; + var self = this; + $tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) { + if(attribute.type === "indirect") { + var value = self.renderTree.wiki.getTextReference(attribute.textReference,self.renderContext.tiddlerTitle); + if(self.attributes[name] !== value) { + self.attributes[name] = value; + changedAttributes[name] = true; + } + } else { // String attribute + if(self.attributes[name] !== attribute.value) { + self.attributes[name] = attribute.value; + changedAttributes[name] = true; + } + } + }); + return changedAttributes; +}; + +WidgetRenderer.prototype.hasAttribute = function(name) { + return $tw.utils.hop(this.attributes,name); +}; + +WidgetRenderer.prototype.getAttribute = function(name,defaultValue) { + if($tw.utils.hop(this.attributes,name)) { + return this.attributes[name]; + } else { + return defaultValue; + } +}; + +WidgetRenderer.prototype.render = function(type) { + // Render the widget if we've got one + if(this.widget && this.widget.render) { + return this.widget.render(type); + } +}; + +WidgetRenderer.prototype.renderInDom = function() { + // Create the wrapper element + this.domNode = document.createElement("widget"); + this.domNode.setAttribute("data-widget-type",this.parseTreeNode.tag); + this.domNode.setAttribute("data-widget-attr",JSON.stringify(this.attributes)); + // Render the widget if we've got one + if(this.widget && this.widget.renderInDom) { + this.widget.renderInDom(this.domNode); + } + // Return the dom node + return this.domNode; +}; + +WidgetRenderer.prototype.refreshInDom = function(changedTiddlers) { + // Refresh if the widget cleared the depencies hashmap to indicate that it should always be refreshed, or if any of our dependencies have changed + if(!this.dependencies || $tw.utils.checkDependencies(this.dependencies,changedTiddlers)) { + // Update our attributes + var changedAttributes = this.computeAttributes(); + // Refresh the widget + if(this.widget && this.widget.refreshInDom) { + this.widget.refreshInDom(changedAttributes,changedTiddlers); + return; + } + } + // If the widget itself didn't need refreshing, just refresh any child nodes + var self = this; + $tw.utils.each(this.children,function(node,index) { + if(node.refreshInDom) { + node.refreshInDom(changedTiddlers); + } + }); +}; + +WidgetRenderer.prototype.getContextTiddlerTitle = function() { + return this.renderContext ? this.renderContext.tiddlerTitle : undefined; +}; + +/* +Check for render context recursion by returning true if the members of a proposed new render context are already present in the render context chain +*/ +WidgetRenderer.prototype.checkContextRecursion = function(newRenderContext) { + var context = this.renderContext; + while(context) { + var match = true; + for(var member in newRenderContext) { + if($tw.utils.hop(newRenderContext,member)) { + if(newRenderContext[member] && newRenderContext[member] !== context[member]) { + match = false; + } + } + } + if(match) { + return true; + } + context = context.parentContext; + } + return false; +}; + +exports.widget = WidgetRenderer + +})(); diff --git a/core/modules/rendertree/wikirendertree.js b/core/modules/rendertree/wikirendertree.js new file mode 100644 index 000000000..2c4e5c189 --- /dev/null +++ b/core/modules/rendertree/wikirendertree.js @@ -0,0 +1,91 @@ +/*\ +title: $:/core/modules/rendertree/wikirendertree.js +type: application/javascript +module-type: global + +Wiki text render tree + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Create a render tree object for a parse tree +*/ +var WikiRenderTree = function(parser,options) { + this.parser = parser; + this.wiki = options.wiki; +}; + +/* +Generate the full render tree for this parse tree + renderContext: see below +An renderContext consists of these fields: + tiddlerTitle: title of the tiddler providing the context + parentContext: reference back to previous context in the stack +*/ +WikiRenderTree.prototype.execute = function(renderContext) { + this.rendererTree = this.createRenderers(renderContext,this.parser.tree); +}; + +/* +Create an array of renderers for an array of parse tree nodes +*/ +WikiRenderTree.prototype.createRenderers = function(renderContext,parseTreeNodes) { + var rendererNodes = []; + for(var t=0; t 0) { + // Use our content as the template + templateTree = this.renderer.parseTreeNode.children; + } else { + // Use default content + templateTree = [{ + type: "widget", + tag: "view", + attributes: { + field: {type: "string", value: "title"}, + format: {type: "string", value: "link"} + } + }]; + } + } + // Create the tiddler macro + return { + type: "widget", + tag: "transclude", + attributes: { + target: {type: "string", value: title}, + template: {type: "string", value: template} + }, + children: templateTree + }; +}; + +/* +Remove a list element from the list, along with the attendant DOM nodes +*/ +ListWidget.prototype.removeListElement = function(index) { + // Get the list element + var listElement = this.children[0].children[index]; + // Remove the DOM node + listElement.domNode.parentNode.removeChild(listElement.domNode); + // Then delete the actual renderer node + this.children[0].children.splice(index,1); +}; + +/* +Return the index of the list element that corresponds to a particular title +startIndex: index to start search (use zero to search from the top) +title: tiddler title to seach for +*/ +ListWidget.prototype.findListElementByTitle = function(startIndex,title) { + while(startIndex < this.children[0].children.length) { + if(this.children[0].children[startIndex].children[0].attributes.target === title) { + return startIndex; + } + startIndex++; + } + return undefined; +}; + +ListWidget.prototype.render = function(type) { + var output = []; + $tw.utils.each(this.children,function(node) { + if(node.render) { + output.push(node.render(type)); + } + }); + return output.join(""); +}; + +ListWidget.prototype.renderInDom = function(parentElement) { + this.parentElement = parentElement; + // Render any child nodes + $tw.utils.each(this.children,function(node) { + if(node.renderInDom) { + parentElement.appendChild(node.renderInDom()); + } + }); +}; + +ListWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) { + // Reexecute the widget if any of our attributes have changed + if(changedAttributes.itemClass || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.type || changedAttributes.filter || changedAttributes.template) { + // Remove old child nodes + $tw.utils.removeChildren(this.parentElement); + // Regenerate and render children + this.generateChildNodes(); + var self = this; + $tw.utils.each(this.children,function(node) { + if(node.renderInDom) { + self.parentElement.appendChild(node.renderInDom()); + } + }); + } else { + // Handle any changes to the list, and refresh any nodes we're reusing + this.handleListChanges(changedTiddlers); + } +}; + +ListWidget.prototype.handleListChanges = function(changedTiddlers) { + var t, + prevListLength = this.list.length, + frame = this.children[0]; + // Get the list of tiddlers, having saved the previous length + this.getTiddlerList(); + // Check if the list is empty + if(this.list.length === 0) { + // Check if it was empty before + if(prevListLength === 0) { + // If so, just refresh the empty message + $tw.utils.each(this.children,function(node) { + if(node.refreshInDom) { + node.refreshInDom(changedTiddlers); + } + }); + return; + } else { + // If the list wasn't empty before, empty it + for(t=prevListLength-1; t>=0; t--) { + this.removeListElement(t); + } + // Insert the empty message + frame.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[this.getEmptyMessage()]); + $tw.utils.each(frame.children,function(node) { + if(node.renderInDom) { + frame.domNode.appendChild(node.renderInDom()); + } + }); + return; + } + } else { + // If it is not empty now, but was empty previously, then remove the empty message + if(prevListLength === 0) { + this.removeListElement(0); + } + } + // Step through the list and adjust our child list elements appropriately + for(t=0; t=t; n--) { + this.removeListElement(n); + } + // Refresh the node we're reusing + frame.children[t].refreshInDom(changedTiddlers); + } + } + // Remove any left over elements + for(t=frame.children.length-1; t>=this.list.length; t--) { + this.removeListElement(t); + } +}; + +exports.list = ListWidget; + +})(); diff --git a/core/modules/widgets/navigator.js b/core/modules/widgets/navigator.js new file mode 100644 index 000000000..6a2d222b7 --- /dev/null +++ b/core/modules/widgets/navigator.js @@ -0,0 +1,241 @@ +/*\ +title: $:/core/modules/widget/navigator.js +type: application/javascript +module-type: widget + +Implements the navigator widget. + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var NavigatorWidget = function(renderer) { + // Save state + this.renderer = renderer; + // Generate child nodes + this.generateChildNodes(); +}; + +NavigatorWidget.prototype.generateChildNodes = function() { + // We'll manage our own dependencies + this.renderer.dependencies = undefined; + // Get our parameters + this.storyTitle = this.renderer.getAttribute("story"); + this.historyTitle = this.renderer.getAttribute("history"); + // Render our children + this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,this.renderer.parseTreeNode.children); +}; + +NavigatorWidget.prototype.render = function(type) { + var output = []; + $tw.utils.each(this.children,function(node) { + if(node.render) { + output.push(node.render(type)); + } + }); + return output.join(""); +}; + +NavigatorWidget.prototype.renderInDom = function(parentElement) { + this.parentElement = parentElement; + // Render any child nodes + $tw.utils.each(this.children,function(node) { + if(node.renderInDom) { + parentElement.appendChild(node.renderInDom()); + } + }); + // Attach our event handlers + $tw.utils.addEventListeners(this.renderer.domNode,[ + {name: "tw-navigate", handlerObject: this, handlerMethod: "handleNavigateEvent"}, + {name: "tw-EditTiddler", handlerObject: this, handlerMethod: "handleEditTiddlerEvent"}, + {name: "tw-SaveTiddler", handlerObject: this, handlerMethod: "handleSaveTiddlerEvent"}, + {name: "tw-close", handlerObject: this, handlerMethod: "handleCloseTiddlerEvent"}, + {name: "tw-NewTiddler", handlerObject: this, handlerMethod: "handleNewTiddlerEvent"} + ]); +}; + +NavigatorWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) { + // We don't need to refresh ourselves, so just refresh any child nodes + $tw.utils.each(this.children,function(node) { + if(node.refreshInDom) { + node.refreshInDom(changedTiddlers); + } + }); +}; + +NavigatorWidget.prototype.getStoryList = function() { + var text = this.renderer.renderTree.wiki.getTextReference(this.storyTitle,""); + if(text && text.length > 0) { + this.storyList = text.split("\n"); + } else { + this.storyList = []; + } +}; + +NavigatorWidget.prototype.saveStoryList = function() { + var storyTiddler = this.renderer.renderTree.wiki.getTiddler(this.storyTitle); + this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler({ + title: this.storyTitle + },storyTiddler,{text: this.storyList.join("\n")})); +}; + +NavigatorWidget.prototype.findTitleInStory = function(title,defaultIndex) { + for(var t=0; t +}}} + +This will render the tiddler Foo within the current tiddler. If the tiddler Foo includes +the view widget (or other widget that reference the fields of the current tiddler), then the +fields of the tiddler Foo will be accessed. + +If you want to transclude the tiddler as a template, so that the fields referenced by the view +widget are those of the tiddler doing the transcluding, then you can instead specify the tiddler +as a template: + +{{{ +<_transclude template="Foo"/> +}}} + +The effect is the same as the previous example: the text of the tiddler Foo is rendered. The +difference is that the view widget will access the fields of the tiddler doing the transcluding. + +The `target` and `template` attributes may be combined: + +{{{ +<_transclude template="Bar" target="Foo"/> +}}} + +Here, the text of the tiddler `Bar` will be transcluded, with the widgets within it accessing the fields +of the tiddler `Foo`. + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var TranscludeWidget = function(renderer) { + // Save state + this.renderer = renderer; + // Generate child nodes + this.generateChildNodes(); +}; + +TranscludeWidget.prototype.generateChildNodes = function() { + var tr, templateParseTree, templateTiddler; + // We'll manage our own dependencies + this.renderer.dependencies = undefined; + // Get the render target details + this.targetTitle = this.renderer.getAttribute("target",this.renderer.getContextTiddlerTitle()); + // Get the render tree for the template + this.templateTitle = undefined; + if(this.renderer.parseTreeNode.children && this.renderer.parseTreeNode.children.length > 0) { + // Use the child nodes as the template if we've got them + templateParseTree = this.renderer.parseTreeNode.children; + } else { + this.templateTitle = this.renderer.getAttribute("template",this.targetTitle); + // Check for recursion + if(this.renderer.checkContextRecursion({ + tiddlerTitle: this.targetTitle, + templateTitle: this.templateTitle + })) { + templateParseTree = [{type: "text", text: "Tiddler recursion error in transclude widget"}]; + } else { + var parser = this.renderer.renderTree.wiki.new_parseTiddler(this.templateTitle); + templateParseTree = parser ? parser.tree : []; + } + } + // Set up the attributes for the wrapper element + var classes = []; + if(!this.renderer.renderTree.wiki.tiddlerExists(this.targetTitle)) { + $tw.utils.pushTop(classes,"tw-tiddler-missing"); + } + // Create the renderers for the wrapper and the children + var newRenderContext = { + tiddlerTitle: this.targetTitle, + templateTitle: this.templateTitle, + parentContext: this.renderer.renderContext + }; + this.children = this.renderer.renderTree.createRenderers(newRenderContext,[{ + type: "element", + tag: this.renderer.parseTreeNode.isBlock ? "div" : "span", + attributes: { + "class": {type: "string", value: classes.join(" ")} + }, + children: templateParseTree + }]); +}; + +TranscludeWidget.prototype.render = function(type) { + var output = []; + $tw.utils.each(this.children,function(node) { + if(node.render) { + output.push(node.render(type)); + } + }); + return output.join(""); +}; + +TranscludeWidget.prototype.renderInDom = function(parentElement) { + this.parentElement = parentElement; + // Render any child nodes + $tw.utils.each(this.children,function(node) { + if(node.renderInDom) { + parentElement.appendChild(node.renderInDom()); + } + }); +}; + +TranscludeWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) { + // Set the class for missing tiddlers + if(this.targetTitle) { + $tw.utils.toggleClass(this.children[0].domNode,"tw-tiddler-missing",!this.renderer.renderTree.wiki.tiddlerExists(this.targetTitle)); + } + // Check if any of our attributes have changed, or if a tiddler we're interested in has changed + if(changedAttributes.target || changedAttributes.template || (this.targetTitle && changedTiddlers[this.targetTitle]) || (this.templateTitle && changedTiddlers[this.templateTitle])) { + // Remove old child nodes + $tw.utils.removeChildren(this.parentElement); + // Regenerate and render children + this.generateChildNodes(); + var self = this; + $tw.utils.each(this.children,function(node) { + if(node.renderInDom) { + self.parentElement.appendChild(node.renderInDom()); + } + }); + } else { + // We don't need to refresh ourselves, so just refresh any child nodes + $tw.utils.each(this.children,function(node) { + if(node.refreshInDom) { + node.refreshInDom(changedTiddlers); + } + }); + } +}; + +exports.transclude = TranscludeWidget; + +})(); diff --git a/core/modules/widgets/view/view.js b/core/modules/widgets/view/view.js new file mode 100644 index 000000000..b42f1a4fd --- /dev/null +++ b/core/modules/widgets/view/view.js @@ -0,0 +1,141 @@ +/*\ +title: $:/core/modules/widgets/view.js +type: application/javascript +module-type: widget + +The view widget displays a tiddler field. + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Define the "text" viewer here so that it is always available +*/ +var TextViewer = function(viewWidget,tiddler,field,value) { + this.viewWidget = viewWidget; + this.tiddler = tiddler; + this.field = field; + this.value = value; +}; + +TextViewer.prototype.render = function() { + // Get the value as a string + if(this.field !== "text" && this.tiddler) { + this.value = this.tiddler.getFieldString(this.field); + } + var value = ""; + if(this.value !== undefined && this.value !== null) { + value = this.value; + } + return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,[{ + type: "text", + text: value + }]); +}; + +// We'll cache the available field viewers here +var fieldViewers = undefined; + +var ViewWidget = function(renderer) { + // Save state + this.renderer = renderer; + // Initialise the field viewers if they've not been done already + if(!fieldViewers) { + fieldViewers = {text: TextViewer}; // Start with the built-in text viewer + $tw.modules.applyMethods("newfieldviewer",fieldViewers); + } + // Generate child nodes + this.generateChildNodes(); +}; + +ViewWidget.prototype.generateChildNodes = function() { + // We'll manage our own dependencies + this.renderer.dependencies = undefined; + // Get parameters from our attributes + this.tiddlerTitle = this.renderer.getAttribute("tiddler",this.renderer.getContextTiddlerTitle()); + this.fieldName = this.renderer.getAttribute("field","text"); + this.format = this.renderer.getAttribute("format","text"); + // Get the value to display + var tiddler = this.renderer.renderTree.wiki.getTiddler(this.tiddlerTitle), + value; + if(tiddler) { + if(this.fieldName === "text") { + // Calling getTiddlerText() triggers lazy loading of skinny tiddlers + value = this.renderer.renderTree.wiki.getTiddlerText(this.tiddlerTitle); + } else { + value = tiddler.fields[this.fieldName]; + } + } else { // Use a special value if the tiddler is missing + switch(this.fieldName) { + case "title": + value = this.tiddlerTitle; + break; + case "modified": + case "created": + value = new Date(); + break; + default: + value = ""; + break; + } + } + // Choose the viewer to use + var Viewer = fieldViewers.text; + if($tw.utils.hop(fieldViewers,this.format)) { + Viewer = fieldViewers[this.format]; + } + this.viewer = new Viewer(this,tiddler,this.fieldName,value); + // Ask the viewer to create the children + this.children = this.viewer.render(); +}; + +ViewWidget.prototype.render = function(type) { + var output = []; + $tw.utils.each(this.children,function(node) { + if(node.render) { + output.push(node.render(type)); + } + }); + return output.join(""); +}; + +ViewWidget.prototype.renderInDom = function(parentElement) { + this.parentElement = parentElement; + // Render any child nodes + $tw.utils.each(this.children,function(node) { + if(node.renderInDom) { + parentElement.appendChild(node.renderInDom()); + } + }); +}; + +ViewWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) { + // Check if any of our attributes have changed, or if a tiddler we're interested in has changed + if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.format || (this.tiddlerTitle && changedTiddlers[this.tiddlerTitle])) { + // Remove old child nodes + $tw.utils.removeChildren(this.parentElement); + // Regenerate and render children + this.generateChildNodes(); + var self = this; + $tw.utils.each(this.children,function(node) { + if(node.renderInDom) { + self.parentElement.appendChild(node.renderInDom()); + } + }); + } else { + // We don't need to refresh ourselves, so just refresh any child nodes + $tw.utils.each(this.children,function(node) { + if(node.refreshInDom) { + node.refreshInDom(changedTiddlers); + } + }); + } +}; + +exports.view = ViewWidget; + +})(); diff --git a/core/modules/widgets/view/viewers/date.js b/core/modules/widgets/view/viewers/date.js new file mode 100644 index 000000000..327ea4d92 --- /dev/null +++ b/core/modules/widgets/view/viewers/date.js @@ -0,0 +1,36 @@ +/*\ +title: $:/core/modules/widgets/view/viewers/date.js +type: application/javascript +module-type: newfieldviewer + +A viewer for viewing tiddler fields as a date + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var DateViewer = function(viewWidget,tiddler,field,value) { + this.viewWidget = viewWidget; + this.tiddler = tiddler; + this.field = field; + this.value = value; +}; + +DateViewer.prototype.render = function() { + var template = this.viewWidget.renderer.getAttribute("template","DD MMM YYYY"), + value = ""; + if(this.value !== undefined) { + value = $tw.utils.formatDateString(this.value,template); + } + return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,[{ + type: "text", + text: value + }]); +}; + +exports.date = DateViewer; + +})(); diff --git a/core/modules/widgets/view/viewers/link.js b/core/modules/widgets/view/viewers/link.js new file mode 100644 index 000000000..77d1cc6cd --- /dev/null +++ b/core/modules/widgets/view/viewers/link.js @@ -0,0 +1,44 @@ +/*\ +title: $:/core/modules/widgets/view/viewers/link.js +type: application/javascript +module-type: newfieldviewer + +A viewer for viewing tiddler fields as a link + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var LinkViewer = function(viewWidget,tiddler,field,value) { + this.viewWidget = viewWidget; + this.tiddler = tiddler; + this.field = field; + this.value = value; +}; + +LinkViewer.prototype.render = function() { + var parseTree = []; + if(this.value === undefined) { + parseTree.push({type: "text", text: ""}); + } else { + parseTree.push({ + type: "widget", + tag: "link", + attributes: { + to: {type: "string", value: this.value} + }, + children: [{ + type: "text", + text: this.value + }] + }) + } + return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,parseTree); +}; + +exports.link = LinkViewer; + +})(); diff --git a/core/modules/widgets/view/viewers/wikified.js b/core/modules/widgets/view/viewers/wikified.js new file mode 100644 index 000000000..c142705a5 --- /dev/null +++ b/core/modules/widgets/view/viewers/wikified.js @@ -0,0 +1,41 @@ +/*\ +title: $:/core/modules/widgets/view/viewers/wikified.js +type: application/javascript +module-type: newfieldviewer + +A viewer for viewing tiddler fields as wikified text + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var WikifiedViewer = function(viewWidget,tiddler,field,value) { + this.viewWidget = viewWidget; + this.tiddler = tiddler; + this.field = field; + this.value = value; +}; + +WikifiedViewer.prototype.render = function() { + var parseTree; + // If we're viewing the text field of a tiddler then we'll transclude it + if(this.tiddler && this.field === "text") { + parseTree = [{ + type: "widget", + tag: "transclude", + attributes: { + target: {type: "string", value: this.tiddler.fields.title} + } + }]; + } else { + parseTree = this.viewWidget.renderer.renderTree.wiki.new_parseText("text/vnd.tiddlywiki",this.value).tree; + } + return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,parseTree); +}; + +exports.wikified = WikifiedViewer; + +})(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index a0314a704..31d8c6b02 100644 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -32,7 +32,7 @@ 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), + var tr = $tw.utils.parseTextReference(textRef), title = tr.title || currTiddlerTitle, field = tr.field || "text", tiddler = this.getTiddler(title); @@ -44,7 +44,7 @@ exports.getTextReference = function(textRef,defaultText,currTiddlerTitle) { }; exports.setTextReference = function(textRef,value,currTiddlerTitle) { - var tr = this.parseTextReference(textRef), + var tr = $tw.utils.parseTextReference(textRef), title,tiddler,fields; // Check if it is a reference to a tiddler if(tr.title && !tr.field) { @@ -63,7 +63,7 @@ exports.setTextReference = function(textRef,value,currTiddlerTitle) { }; exports.deleteTextReference = function(textRef,currTiddlerTitle) { - var tr = this.parseTextReference(textRef), + var tr = $tw.utils.parseTextReference(textRef), title,tiddler,fields; // Check if it is a reference to a tiddler if(tr.title && !tr.field) { @@ -80,33 +80,6 @@ exports.deleteTextReference = function(textRef,currTiddlerTitle) { } }; -/* -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({ @@ -184,9 +157,8 @@ exports.addTiddler = function(tiddler) { if(!(tiddler instanceof $tw.Tiddler)) { tiddler = new $tw.Tiddler(tiddler); } - // Get the title, and the current tiddler with that title - var title = tiddler.fields.title, - prevTiddler = this.tiddlers[title]; + // Get the title + var title = tiddler.fields.title; // Save the tiddler this.tiddlers[title] = tiddler; this.clearCache(title); @@ -384,6 +356,54 @@ exports.clearCache = function(title) { } }; +exports.new_initParsers = function() { + // Create a default vocabulary + this.vocabulary = new $tw.WikiVocabulary({wiki: this}); +}; + +/* +Parse a block of text of a specified MIME type +*/ +exports.new_parseText = function(type,text) { + return this.vocabulary.parseText(type,text); +}; + +/* +Parse a tiddler according to its MIME type +*/ +exports.new_parseTiddler = function(title,options) { + var tiddler = this.getTiddler(title), + self = this; + return tiddler ? this.getCacheForTiddler(title,"newParseTree",function() { + return self.new_parseText(tiddler.fields.type,tiddler.fields.text); + }) : 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.new_renderText = function(outputType,textType,text) { + var parser = this.new_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.new_renderTiddler = function(outputType,title) { + var parser = this.new_parseTiddler(title), + renderTree = new $tw.WikiRenderTree(parser,{wiki: this}); + renderTree.execute(); + return renderTree.render(outputType); +}; + exports.initParsers = function(moduleType) { // Install the parser modules moduleType = moduleType || "parser"; diff --git a/core/modules/wikivocabulary.js b/core/modules/wikivocabulary.js new file mode 100644 index 000000000..e7fe1ec1e --- /dev/null +++ b/core/modules/wikivocabulary.js @@ -0,0 +1,32 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/wikivocabulary.js +type: application/javascript +module-type: global + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var WikiVocabulary = function(options) { + this.wiki = options.wiki; + // Hashmaps of the various parse rule classes + this.pragmaRuleClasses = $tw.modules.applyMethods("wikipragmarule"); + this.blockRuleClasses = $tw.modules.applyMethods("wikiblockrule"); + this.runRuleClasses = $tw.modules.applyMethods("wikirunrule"); + // Hashmap of the various renderer classes + this.rendererClasses = $tw.modules.applyMethods("wikirenderer"); + // Hashmap of the available widgets + this.widgetClasses = $tw.modules.applyMethods("widget"); +}; + +WikiVocabulary.prototype.parseText = function(type,text) { + return new $tw.WikiParser(this,type,text,{wiki: this.wiki}); +}; + +exports.WikiVocabulary = WikiVocabulary; + +})(); + diff --git a/core/templates/NewPageTemplate.tid b/core/templates/NewPageTemplate.tid new file mode 100644 index 000000000..83e086e38 --- /dev/null +++ b/core/templates/NewPageTemplate.tid @@ -0,0 +1,33 @@ +title: $:/templates/NewPageTemplate + +\define coolmacro(p:ridiculously) This is my $p$ cool macro! +\define me(one two) +some
thing +\end +\define another(first:default second third:default3) that is + +* This +*.disabled Is a +* List!! + +<_navigator story="$:/StoryList" history="$:/HistoryList"> + +<_link to="JeremyRuston" hover="HelloThere"> +Go to it! + + +! Heading1 +!!.myclass Heading2 +!!! Heading3 +!!!! Heading4 + + +
+
+
+<_list filter="[list[$:/StoryList]]" history="$:/HistoryList" template="$:/templates/NewViewTemplate" editTemplate="$:/templates/EditTemplate" listview=classic itemClass="tw-tiddler-frame"/> +
+
+
+ + diff --git a/core/templates/NewViewTemplate.tid b/core/templates/NewViewTemplate.tid new file mode 100644 index 000000000..1e1a23b7e --- /dev/null +++ b/core/templates/NewViewTemplate.tid @@ -0,0 +1,16 @@ +title: $:/templates/NewViewTemplate +modifier: JeremyRuston + + + <_view field="title"/> + <_button popup="HelloThere">close + + +
+ <_view field="modifier" format="link"/> + <_view field="modified" format="date"/> +
+ +
+ <_view field="text" format="wikified"/> +