Introduce refactored wiki parser and renderer

This is a half-way through a big refactoring of the parsing and
rendering infrastructure. The main change is to separate the parse and
render trees, which makes the code a lot cleaner. The new parser isn't
yet functional enough to replace the existing parser so for the moment
you have to manually invoke it with `$tw.testNewParser()` in your
browser console. I really ought to use branches for this kind of
thing...
This commit is contained in:
Jeremy Ruston
2012-12-13 21:34:31 +00:00
parent 916ca8eecf
commit d338a54370
28 changed files with 2837 additions and 33 deletions
@@ -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;
})();
@@ -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<match[0].length; t++) {
listInfo = listTypes[match[0].charAt(t)];
// Remove any stacked up element if we can't re-use it because the list type doesn't match
if(listStack.length > 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;
})();
@@ -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;
})();
@@ -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: &copy;
}}}
\*/
(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;
})();
@@ -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:
{{{
<aside>
This is an HTML5 aside element
</aside>
<_slider target="MyTiddler">
This is a widget invocation
</_slider>
}}}
\*/
(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 = "(</" + (isWidget ? "_" : "") + tagName + ">)",
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;
})();
@@ -0,0 +1,71 @@
/*\
title: $:/core/modules/parsers/wikiparser/rules/run/macrocall.js
type: application/javascript
module-type: wikirunrule
Wiki rule for macro calls
{{{
<<name value value2>>
}}}
\*/
(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;
})();
@@ -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: <string>, attributes: {}, children: []} - an HTML element
{type: "text", text: <string>} - a text node
{type: "entity", value: <string>} - an entity
{type: "raw", html: <string>} - raw HTML
Attributes are stored as hashmaps of the following objects:
{type: "string", value: <string>} - literal string
{type: "array", value: <string array>} - array of strings
{type: "styles", value: <object>} - hashmap of style strings
{type: "indirect", textReference: <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<rules.length; t++) {
var matchPos = rules[t].findNextMatch(startPos);
if(matchPos !== undefined && matchPos <= nextMatchPos) {
nextMatch = rules[t];
nextMatchPos = matchPos;
}
}
return nextMatch;
};
/*
Parse any pragmas at the beginning of a block of parse text
*/
WikiParser.prototype.parsePragmas = function() {
while(true) {
// Skip whitespace
this.skipWhitespace();
// Check for the end of the text
if(this.pos >= 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;
})();