diff --git a/plugins/tiddlywiki/tw2parser/wikitextparser.js b/plugins/tiddlywiki/tw2parser/wikitextparser.js new file mode 100644 index 000000000..a78c22717 --- /dev/null +++ b/plugins/tiddlywiki/tw2parser/wikitextparser.js @@ -0,0 +1,188 @@ +/*\ +title: $:/plugins/tiddlywiki/tw2parser/wikitextparser.js +type: application/javascript +module-type: parser + +Parses a block of tiddlywiki-format wiki text into a parse tree object. This is a transliterated version of the old TiddlyWiki code. The plan is to replace it with a new, mostly backwards compatible parser built in PEGJS. + +A wikitext parse tree is an array of objects with a `type` field that can be `text`,`macro` or the name of an HTML element. + +Text nodes are represented as `{type: "text", value: "A string of text"}`. + +Macro nodes look like this: +` +{type: "macro", name: "view", params: { + one: {type: "eval", value: "2+2"}, + two: {type: "string", value: "twenty two"} +}} +` +HTML nodes look like this: +` +{type: "div", attributes: { + src: "one" + styles: { + "background-color": "#fff", + "color": "#000" + } +}} +` + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Creates a new instance of the wiki text parser with the specified options. The +options are a hashmap of mandatory members as follows: + + wiki: The wiki object to use to parse any cascaded content (eg transclusion) + +Planned: + + enableRules: An array of names of wiki text rules to enable. If not specified, all rules are available + extraRules: An array of additional rule handlers to add + enableMacros: An array of names of macros to enable. If not specified, all macros are available + extraMacros: An array of additional macro handlers to add +*/ + +var WikiTextParser = function(options) { + this.wiki = options.wiki; + this.autoLinkWikiWords = true; +}; + +WikiTextParser.prototype.installRules = function() { + var rules = require("./wikitextrules.js").rules, + pattern = []; + for(var n=0; n this.nextMatch) + this.outputText(this.output,this.nextMatch,ruleMatch.index); + // Set the match parameters for the handler + this.matchStart = ruleMatch.index; + this.matchLength = ruleMatch[0].length; + this.matchText = ruleMatch[0]; + this.nextMatch = this.rulesRegExp.lastIndex; + // Figure out which rule matched and call its handler + var t; + for(t=1; t this.nextMatch) + this.outputText(this.output,this.nextMatch,terminatorMatch.index); + // Set the match parameters + this.matchText = terminatorMatch[1]; + this.matchLength = terminatorMatch[1].length; + this.matchStart = terminatorMatch.index; + this.nextMatch = this.matchStart + this.matchLength; + // Restore the output pointer + this.output = oldOutput; + return; + } + // It must be a rule match; output any text before the match + if(ruleMatch.index > this.nextMatch) + this.outputText(this.output,this.nextMatch,ruleMatch.index); + // Set the match parameters + this.matchStart = ruleMatch.index; + this.matchLength = ruleMatch[0].length; + this.matchText = ruleMatch[0]; + this.nextMatch = this.rulesRegExp.lastIndex; + // Figure out which rule matched and call its handler + var t; + for(t=1; t 0) { + if(!e.attributes) { + e.attributes = {}; + } + if(!e.attributes.style) { + e.attributes.style = {}; + } + for(var t=0; t< styles.length; t++) { + e.attributes.style[styles[t].style] = styles[t].value; + } + } +}; + +var enclosedTextHelper = function(w) { + this.lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) { + var text = lookaheadMatch[1]; + w.output.push($tw.Tree.Element(this.element,null,[$tw.Tree.Text(text)])); + w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length; + } +}; + +var insertMacroCall = function(w,output,name,params,content) { + if(name in w.wiki.macros) { + var macroNode = $tw.Tree.Macro(name,{ + srcParams: params, + content: content, + wiki: w.wiki + }); + w.dependencies.mergeDependencies(macroNode.dependencies); + output.push(macroNode); + } +}; + +var rules = [ +{ + name: "table", + match: "^\\|(?:[^\\n]*)\\|(?:[fhck]?)$", + lookaheadRegExp: /^\|([^\n]*)\|([fhck]?)$/mg, + rowTermRegExp: /(\|(?:[fhck]?)$\n?)/mg, + cellRegExp: /(?:\|([^\n\|]*)\|)|(\|[fhck]?$\n?)/mg, + cellTermRegExp: /((?:\x20*)\|)/mg, + rowTypes: {"c":"caption", "h":"thead", "":"tbody", "f":"tfoot"}, + handler: function(w) + { + var table = $tw.Tree.Element("table",{"class": "table"},[]); + w.output.push(table); + var prevColumns = []; + var currRowType = null; + var rowContainer; + var rowCount = 0; + w.nextMatch = w.matchStart; + this.lookaheadRegExp.lastIndex = w.nextMatch; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + while(lookaheadMatch && lookaheadMatch.index == w.nextMatch) { + var nextRowType = lookaheadMatch[2]; + if(nextRowType == "k") { + table.attributes["class"] = lookaheadMatch[1]; + w.nextMatch += lookaheadMatch[0].length+1; + } else { + if(nextRowType != currRowType) { + rowContainer = $tw.Tree.Element(this.rowTypes[nextRowType],{},[]); + table.children.push(rowContainer); + currRowType = nextRowType; + } + if(currRowType == "c") { + // Caption + w.nextMatch++; + // Move the caption to the first row if it isn't already + if(table.children.length !== 1) { + table.children.pop(); // Take rowContainer out of the children array + table.children.splice(0,0,rowContainer); // Insert it at the bottom + } + rowContainer.attributes.align = rowCount === 0 ? "top" : "bottom"; + w.subWikifyTerm(rowContainer.children,this.rowTermRegExp); + } else { + var theRow = $tw.Tree.Element("tr",{},[]); + theRow.attributes["class"] = rowCount%2 ? "oddRow" : "evenRow"; + rowContainer.children.push(theRow); + this.rowHandler(w,theRow.children,prevColumns); + rowCount++; + } + } + this.lookaheadRegExp.lastIndex = w.nextMatch; + lookaheadMatch = this.lookaheadRegExp.exec(w.source); + } + }, + rowHandler: function(w,e,prevColumns) + { + var col = 0; + var colSpanCount = 1; + var prevCell = null; + this.cellRegExp.lastIndex = w.nextMatch; + var cellMatch = this.cellRegExp.exec(w.source); + while(cellMatch && cellMatch.index == w.nextMatch) { + if(cellMatch[1] == "~") { + // Rowspan + var last = prevColumns[col]; + if(last) { + last.rowSpanCount++; + last.element.attributes.rowspan = last.rowSpanCount; + last.element.attributes.valign = "center"; + if(colSpanCount > 1) { + last.element.attributes.colspan = colSpanCount; + colSpanCount = 1; + } + } + w.nextMatch = this.cellRegExp.lastIndex-1; + } else if(cellMatch[1] == ">") { + // Colspan + colSpanCount++; + w.nextMatch = this.cellRegExp.lastIndex-1; + } else if(cellMatch[2]) { + // End of row + if(prevCell && colSpanCount > 1) { + prevCell.attributes.colspan = colSpanCount; + } + w.nextMatch = this.cellRegExp.lastIndex; + break; + } else { + // Cell + w.nextMatch++; + var styles = inlineCssHelper(w); + var spaceLeft = false; + var chr = w.source.substr(w.nextMatch,1); + while(chr == " ") { + spaceLeft = true; + w.nextMatch++; + chr = w.source.substr(w.nextMatch,1); + } + var cell; + if(chr == "!") { + cell = $tw.Tree.Element("th",{},[]); + e.push(cell); + w.nextMatch++; + } else { + cell = $tw.Tree.Element("td",{},[]); + e.push(cell); + } + prevCell = cell; + prevColumns[col] = {rowSpanCount:1,element:cell}; + if(colSpanCount > 1) { + cell.attributes.colspan = colSpanCount; + colSpanCount = 1; + } + applyCssHelper(cell,styles); + w.subWikifyTerm(cell.children,this.cellTermRegExp); + if(w.matchText.substr(w.matchText.length-2,1) == " ") // spaceRight + cell.attributes.align = spaceLeft ? "center" : "left"; + else if(spaceLeft) + cell.attributes.align = "right"; + w.nextMatch--; + } + col++; + this.cellRegExp.lastIndex = w.nextMatch; + cellMatch = this.cellRegExp.exec(w.source); + } + } +}, + +{ + name: "heading", + match: "^!{1,6}", + termRegExp: /(\n)/mg, + handler: function(w) + { + var e = $tw.Tree.Element("h" + w.matchLength,{},[]); + w.output.push(e); + w.subWikifyTerm(e.children,this.termRegExp); + } +}, + +{ + name: "list", + match: "^(?:[\\*#;:]+)", + lookaheadRegExp: /^(?:(?:(\*)|(#)|(;)|(:))+)/mg, + termRegExp: /(\n)/mg, + handler: function(w) + { + var stack = [w.output]; + var currLevel = 0, currType = null; + var listLevel, listType, itemType, baseType; + w.nextMatch = w.matchStart; + this.lookaheadRegExp.lastIndex = w.nextMatch; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + while(lookaheadMatch && lookaheadMatch.index == w.nextMatch) { + if(lookaheadMatch[1]) { + listType = "ul"; + itemType = "li"; + } else if(lookaheadMatch[2]) { + listType = "ol"; + itemType = "li"; + } else if(lookaheadMatch[3]) { + listType = "dl"; + itemType = "dt"; + } else if(lookaheadMatch[4]) { + listType = "dl"; + itemType = "dd"; + } + if(!baseType) + baseType = listType; + listLevel = lookaheadMatch[0].length; + w.nextMatch += lookaheadMatch[0].length; + var t,e; + if(listLevel > currLevel) { + for(t=currLevel; tlistLevel; t--) + stack.pop(); + } else if(listLevel == currLevel && listType != currType) { + stack.pop(); + e = $tw.Tree.Element(listType,{},[]); + stack[stack.length-1].push(e); + stack.push(e.children); + } + currLevel = listLevel; + currType = listType; + e = $tw.Tree.Element(itemType,{},[]); + stack[stack.length-1].push(e); + w.subWikifyTerm(e.children,this.termRegExp); + this.lookaheadRegExp.lastIndex = w.nextMatch; + lookaheadMatch = this.lookaheadRegExp.exec(w.source); + } + } +}, + +{ + name: "quoteByBlock", + match: "^<<<\\n", + termRegExp: /(^<<<(\n|$))/mg, + element: "blockquote", + handler: function(w) { + var e = $tw.Tree.Element(this.element,{},[]); + w.output.push(e); + w.subWikifyTerm(e.children,this.termRegExp); + } +}, + +{ + name: "quoteByLine", + match: "^>+", + lookaheadRegExp: /^>+/mg, + termRegExp: /(\n)/mg, + element: "blockquote", + handler: function(w) + { + var stack = [w.output]; + var currLevel = 0; + var newLevel = w.matchLength; + var t,matched,e; + do { + if(newLevel > currLevel) { + for(t=currLevel; tnewLevel; t--) + stack.pop(); + } + currLevel = newLevel; + w.subWikifyTerm(stack[stack.length-1],this.termRegExp); + stack[stack.length-1].push($tw.Tree.Element("br")); + this.lookaheadRegExp.lastIndex = w.nextMatch; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + matched = lookaheadMatch && lookaheadMatch.index == w.nextMatch; + if(matched) { + newLevel = lookaheadMatch[0].length; + w.nextMatch += lookaheadMatch[0].length; + } + } while(matched); + } +}, + +{ + name: "rule", + match: "^----+$\\n?|
\\n?", + handler: function(w) + { + w.output.push($tw.Tree.Element("hr")); + } +}, + +{ + name: "monospacedByLine", + match: "^(?:/\\*\\{\\{\\{\\*/|\\{\\{\\{|//\\{\\{\\{|)\\n", + element: "pre", + handler: function(w) + { + switch(w.matchText) { + case "/*{{{*/\n": // CSS + this.lookaheadRegExp = /\/\*\{\{\{\*\/\n*((?:^[^\n]*\n)+?)(\n*^\f*\/\*\}\}\}\*\/$\n?)/mg; + break; + case "{{{\n": // monospaced block + this.lookaheadRegExp = /^\{\{\{\n((?:^[^\n]*\n)+?)(^\f*\}\}\}$\n?)/mg; + break; + case "//{{{\n": // plugin + this.lookaheadRegExp = /^\/\/\{\{\{\n\n*((?:^[^\n]*\n)+?)(\n*^\f*\/\/\}\}\}$\n?)/mg; + break; + case "\n": //template + this.lookaheadRegExp = /\n*((?:^[^\n]*\n)+?)(\n*^\f*$\n?)/mg; + break; + default: + break; + } + enclosedTextHelper.call(this,w); + } +}, + +{ + name: "typedBlock", + match: "^\\$\\$\\$(?:.*)\\n", + lookaheadRegExp: /^\$\$\$(.*)\n((?:^[^\n]*\n)+?)(^\f*\$\$\$$\n?)/mg, + handler: function(w) + { + this.lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) { + // The wikitext parsing infrastructure is horribly unre-entrant + var mimeType = lookaheadMatch[1], + content = lookaheadMatch[2], + oldOutput = w.output, + oldSource = w.source, + oldNextMatch = w.nextMatch, + oldChildren = w.children, + oldDependencies = w.dependencies, + parseTree = w.wiki.parseText(mimeType,content,{defaultType: "text/plain"}).tree; + w.output = oldOutput; + w.source = oldSource; + w.nextMatch = oldNextMatch; + w.children = oldChildren; + w.dependencies = oldDependencies; + w.output.push.apply(w.output,parseTree); + w.nextMatch = this.lookaheadRegExp.lastIndex; + } + } +}, + +{ + name: "wikifyComment", + match: "^(?:/\\*\\*\\*|\n)/mg); + w.subWikifyTerm(w.output,termRegExp); + } +}, + +{ + name: "macro", + match: "<<", + lookaheadRegExp: /<<(?:([!@£\$%\^\&\*\(\)`\~'"\|\\\/;\:\.\,\+\=\-\_\{\}])|([^>\s]+))(?:\s*)((?:[^>]|(?:>(?!>)))*)>>/mg, + handler: function(w) + { + this.lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source), + name = lookaheadMatch[1] || lookaheadMatch[2]; + if(lookaheadMatch && lookaheadMatch.index == w.matchStart && name) { + w.nextMatch = this.lookaheadRegExp.lastIndex; + insertMacroCall(w,w.output,name,lookaheadMatch[3]); + } + } +}, + +{ + name: "prettyLink", + match: "\\[\\[", + lookaheadRegExp: /\[\[(.*?)(?:\|(~)?(.*?))?\]\]/mg, + handler: function(w) + { + this.lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) { + var text = lookaheadMatch[1], + link = text; + if(lookaheadMatch[3]) { + // Pretty bracketted link + link = lookaheadMatch[3]; + } + insertMacroCall(w,w.output,"link",{to: link},[$tw.Tree.Text(text)]); + w.nextMatch = this.lookaheadRegExp.lastIndex; + } + } +}, + +{ + name: "wikiLink", + match: textPrimitives.unWikiLink+"?"+textPrimitives.wikiLink, + handler: function(w) + { + if(w.matchText.substr(0,1) == textPrimitives.unWikiLink) { + w.outputText(w.output,w.matchStart+1,w.nextMatch); + return; + } + if(w.matchStart > 0) { + var preRegExp = new RegExp(textPrimitives.anyLetterStrict,"mg"); + preRegExp.lastIndex = w.matchStart-1; + var preMatch = preRegExp.exec(w.source); + if(preMatch.index == w.matchStart-1) { + w.outputText(w.output,w.matchStart,w.nextMatch); + return; + } + } + if(w.autoLinkWikiWords) { + insertMacroCall(w,w.output,"link",{to: w.matchText},[$tw.Tree.Text(w.source.substring(w.matchStart,w.nextMatch))]); + } else { + w.outputText(w.output,w.matchStart,w.nextMatch); + } + } +}, + +{ + name: "urlLink", + match: textPrimitives.urlPattern, + handler: function(w) + { + insertMacroCall(w,w.output,"link",{to: w.matchText},[$tw.Tree.Text(w.source.substring(w.matchStart,w.nextMatch))]); + } +}, + +{ + name: "image", + match: "\\[[<>]?[Ii][Mm][Gg]\\[", + // [<] sequence below is to avoid lessThan-questionMark sequence so TiddlyWikis can be included in PHP files + lookaheadRegExp: /\[([<]?)(>?)[Ii][Mm][Gg]\[(?:([^\|\]]+)\|)?([^\[\]\|]+)\](?:\[([^\]]*)\])?\]/mg, + handler: function(w) + { + this.lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source), + imageParams = {}, + linkParams = {}; + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) { + if(lookaheadMatch[1]) { + imageParams.alignment = "left"; + } else if(lookaheadMatch[2]) { + imageParams.alignment = "right"; + } + if(lookaheadMatch[3]) { + imageParams.text = lookaheadMatch[3]; + } + imageParams.src = lookaheadMatch[4]; + if(lookaheadMatch[5]) { + linkParams.target = lookaheadMatch[5]; + var linkChildren = []; + insertMacroCall(w,w.output,"link",linkParams,linkChildren); + insertMacroCall(w,linkChildren,"image",imageParams); + } else { + insertMacroCall(w,w.output,"image",imageParams); + } + w.nextMatch = this.lookaheadRegExp.lastIndex; + } + } +}, + +{ + name: "html", + match: "<[Hh][Tt][Mm][Ll]>", + lookaheadRegExp: /<[Hh][Tt][Mm][Ll]>((?:.|\n)*?)<\/[Hh][Tt][Mm][Ll]>/mg, + handler: function(w) + { + this.lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) { + w.output.push($tw.Tree.Element("html",{},[$tw.Tree.Raw(lookaheadMatch[1])])); + w.nextMatch = this.lookaheadRegExp.lastIndex; + } + } +}, + +{ + name: "commentByBlock", + match: "/%", + lookaheadRegExp: /\/%((?:.|\n)*?)%\//mg, + handler: function(w) + { + this.lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) + w.nextMatch = this.lookaheadRegExp.lastIndex; + } +}, + +{ + name: "characterFormat", + match: "''|//|__|\\^\\^|~~|--(?!\\s|$)|\\{\\{\\{|`", + handler: function(w) + { + var e,lookaheadRegExp,lookaheadMatch; + switch(w.matchText) { + case "''": + e = $tw.Tree.Element("strong",null,[]); + w.output.push(e); + w.subWikifyTerm(e.children,/('')/mg); + break; + case "//": + e = $tw.Tree.Element("em",null,[]); + w.output.push(e); + w.subWikifyTerm(e.children,/(\/\/)/mg); + break; + case "__": + e = $tw.Tree.Element("u",null,[]); + w.output.push(e); + w.subWikifyTerm(e.children,/(__)/mg); + break; + case "^^": + e = $tw.Tree.Element("sup",null,[]); + w.output.push(e); + w.subWikifyTerm(e.children,/(\^\^)/mg); + break; + case "~~": + e = $tw.Tree.Element("sub",null,[]); + w.output.push(e); + w.subWikifyTerm(e.children,/(~~)/mg); + break; + case "--": + e = $tw.Tree.Element("strike",null,[]); + w.output.push(e); + w.subWikifyTerm(e.children,/(--)/mg); + break; + case "`": + lookaheadRegExp = /`((?:.|\n)*?)`/mg; + lookaheadRegExp.lastIndex = w.matchStart; + lookaheadMatch = lookaheadRegExp.exec(w.source); + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) { + w.output.push($tw.Tree.Element("code",null,[$tw.Tree.Text(lookaheadMatch[1])])); + w.nextMatch = lookaheadRegExp.lastIndex; + } + break; + case "{{{": + lookaheadRegExp = /\{\{\{((?:.|\n)*?)\}\}\}/mg; + lookaheadRegExp.lastIndex = w.matchStart; + lookaheadMatch = lookaheadRegExp.exec(w.source); + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) { + w.output.push($tw.Tree.Element("code",null,[$tw.Tree.Text(lookaheadMatch[1])])); + w.nextMatch = lookaheadRegExp.lastIndex; + } + break; + } + } +}, + +{ + name: "customFormat", + match: "@@|\\{\\{", + handler: function(w) + { + switch(w.matchText) { + case "@@": + var e = $tw.Tree.Element("span",null,[]); + w.output.push(e); + var styles = inlineCssHelper(w); + if(styles.length === 0) + setAttr(e,"class","marked"); + else + applyCssHelper(e,styles); + w.subWikifyTerm(e.children,/(@@)/mg); + break; + case "{{": + var lookaheadRegExp = /\{\{[\s]*([\-\w]+[\-\s\w]*)[\s]*\{(\n?)/mg; + lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = lookaheadRegExp.exec(w.source); + if(lookaheadMatch) { + w.nextMatch = lookaheadRegExp.lastIndex; + e = $tw.Tree.Element(lookaheadMatch[2] == "\n" ? "div" : "span",{ + "class": lookaheadMatch[1] + },[]); + w.output.push(e); + w.subWikifyTerm(e.children,/(\}\}\})/mg); + } + break; + } + } +}, + +{ + name: "mdash", + match: "--", + handler: function(w) + { + w.output.push($tw.Tree.Entity("—")); + } +}, + +{ + name: "lineBreak", + match: "\\n|
", + handler: function(w) + { + w.output.push($tw.Tree.Element("br")); + } +}, + +{ + name: "rawText", + match: "\"{3}|", + lookaheadRegExp: /(?:\"{3}|)((?:.|\n)*?)(?:\"{3}|<\/nowiki>)/mg, + handler: function(w) + { + this.lookaheadRegExp.lastIndex = w.matchStart; + var lookaheadMatch = this.lookaheadRegExp.exec(w.source); + if(lookaheadMatch && lookaheadMatch.index == w.matchStart) { + w.output.push($tw.Tree.Text(lookaheadMatch[1])); + w.nextMatch = this.lookaheadRegExp.lastIndex; + } + } +}, + +{ + name: "htmlEntitiesEncoding", + match: "&#?[a-zA-Z0-9]{2,8};", + handler: function(w) + { + w.output.push($tw.Tree.Entity(w.matchText)); + } +} + +]; + +exports.rules = rules; + +})();