mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-10-06 08:01:42 +00:00
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:
@@ -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: ©
|
||||
}}}
|
||||
|
||||
\*/
|
||||
(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;
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user