TiddlyWiki5/plugins/tiddlywiki/railroad/parser.js

452 lines
11 KiB
JavaScript

/*\
title: $:/plugins/tiddlywiki/railroad/parser.js
type: application/javascript
module-type: library
Parser for the source of a railroad diagram.
x y z sequence
<-x y z-> explicit sequence
[:x] optional, normally included
[x] optional, normally omitted
{x} one or more
{x +","} one or more, comma-separated
[{:x}] zero or more, normally included
[{:x +","}] zero or more, comma-separated, normally included
[{x}] zero or more, normally omitted
[{x +","}] zero or more, comma-separated, normally omitted
(x|y|z) alternatives
(x|:y|z) alternatives, normally y
"x" terminal
<"x"> nonterminal
/"blah"/ comment
- dummy
[[x|"tiddler"]] link
{{"tiddler"}} transclusion
"x" can also be written 'x' or """x"""
pragmas:
\arrow yes|no
\debug yes|no
\start single|double|none
\end single|double|none
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var components = require("$:/plugins/tiddlywiki/railroad/components.js").components;
var Parser = function(widget,source,options) {
this.widget = widget;
this.source = source;
this.options = options;
this.tokens = this.tokenise(source);
this.tokenPos = 0;
this.advance();
this.content = this.parseContent();
this.root = new components.Root(this.content);
this.checkFinished();
};
/////////////////////////// Parser dispatch
Parser.prototype.parseContent = function() {
var content = [];
// Parse zero or more components
while(true) {
var component = this.parseComponent();
if(!component) {
break;
}
if(!component.isPragma) {
content.push(component);
}
}
return content;
};
Parser.prototype.parseComponent = function() {
var component = null;
if(this.token) {
if(this.at("string")) {
component = this.parseTerminal();
} else if(this.at("name")) {
component = this.parseName();
} else if(this.at("pragma")) {
component = this.parsePragma();
} else {
switch(this.token.value) {
case "[":
component = this.parseOptional();
break;
case "{":
component = this.parseRepeated();
break;
case "<":
component = this.parseNonterminal();
break;
case "(":
component = this.parseChoice();
break;
case "/":
component = this.parseComment();
break;
case "[[":
component = this.parseLink();
break;
case "{{":
component = this.parseTransclusion();
break;
case "<-":
component = this.parseSequence();
break;
case "-":
component = this.parseDummy();
break;
}
}
}
return component;
};
/////////////////////////// Specific components
Parser.prototype.parseChoice = function() {
// Consume the (
this.advance();
var content = [],
colon = -1;
do {
// Allow at most one branch to be prefixed with a colon
if(colon === -1 && this.eat(":")) {
colon = content.length;
}
// Parse the next branch
content.push(this.parseContent());
} while(this.eat("|"));
// Consume the closing bracket
this.close(")");
// Create a component
return new components.Choice(content,colon === -1 ? 0 : colon);
};
Parser.prototype.parseComment = function() {
// Consume the /
this.advance();
// The comment's content should be in a string literal
var content = this.expectString("after /");
// Consume the closing /
this.close("/");
// Create a component
return new components.Comment(content);
};
Parser.prototype.parseDummy = function() {
// Consume the -
this.advance();
// Create a component
return new components.Dummy();
};
Parser.prototype.parseLink = function() {
// Consume the [[
this.advance();
// Parse the content
var content = this.parseContent();
// Consume the |
this.expect("|");
// Consume the target
var target = this.expectNameOrString("as link target");
// Prepare some attributes for the SVG "a" element to carry
var options = {"data-tw-target": target};
if($tw.utils.isLinkExternal(target)) {
options["data-tw-external"] = true;
}
// Consume the closing ]]
this.close("]]");
// Create a component
return new components.Link(content,options);
};
Parser.prototype.parseName = function() {
// Create a component
var component = new components.Nonterminal(this.token.value);
// Consume the name
this.advance();
return component;
};
Parser.prototype.parseNonterminal = function() {
// Consume the <
this.advance();
// The nonterminal's name should be in a string literal
var content = this.expectString("after <");
// Consume the closing bracket
this.close(">");
// Create a component
return new components.Nonterminal(content);
};
Parser.prototype.parseOptional = function() {
var wantArrow = this.options.arrow;
// Consume the [
this.advance();
// Consume the { if there is one
var repeated = this.eat("{");
// Note whether omission is the normal route
var normal = this.eat(":");
// Parse the content
var content = this.parseContent(),
separator = null;
// Parse the separator if there is one
if(repeated && this.eat("+")) {
separator = this.parseContent();
}
// Consume the closing brackets
if(repeated) {
this.close("}");
}
this.close("]");
// Create a component
return repeated ? new components.OptionalRepeated(content,separator,normal,wantArrow)
: new components.Optional(content,normal);
};
Parser.prototype.parseRepeated = function() {
var wantArrow = this.options.arrow;
// Consume the {
this.advance();
// Parse the content
var content = this.parseContent(),
separator = null;
// Parse the separator if there is one
if(this.eat("+")) {
separator = this.parseContent();
}
// Consume the closing bracket
this.close("}");
// Create a component
return new components.Repeated(content,separator,wantArrow);
};
Parser.prototype.parseSequence = function() {
// Consume the <-
this.advance();
// Parse the content
var content = this.parseContent();
// Consume the closing ->
this.close("->");
// Create a component
return new components.Sequence(content);
};
Parser.prototype.parseTerminal = function() {
var component = new components.Terminal(this.token.value);
// Consume the string literal
this.advance();
return component;
};
Parser.prototype.parseTransclusion = function() {
// Consume the {{
this.advance();
// Consume the text reference
var textRef = this.expectNameOrString("as transclusion source");
// Consume the closing }}
this.close("}}");
// Retrieve the content of the text reference
var source = this.widget.wiki.getTextReference(textRef,"",this.widget.getVariable("currentTiddler"));
// Parse the content
var content = new Parser(this.widget,source).content;
// Create a component
return new components.Transclusion(content);
};
/////////////////////////// Pragmas
Parser.prototype.parsePragma = function() {
// Create a dummy component
var component = { isPragma: true };
// Consume the pragma
var pragma = this.token.value;
this.advance();
// Apply the setting
if(pragma === "arrow") {
this.options.arrow = this.parseYesNo(pragma);
} else if(pragma === "debug") {
this.options.debug = true;
} else if(pragma === "start") {
this.options.start = this.parseTerminusStyle(pragma);
} else if(pragma === "end") {
this.options.end = this.parseTerminusStyle(pragma);
} else {
throw "Invalid pragma";
}
return component;
};
Parser.prototype.parseYesNo = function(pragma) {
return this.parseSetting(["yes","no"],pragma) === "yes";
}
Parser.prototype.parseTerminusStyle = function(pragma) {
return this.parseSetting(["single","double","none"],pragma);
}
Parser.prototype.parseSetting = function(options,pragma) {
if(this.at("name") && options.indexOf(this.token.value) !== -1) {
return this.tokenValueEaten();
}
throw options.join(" or ") + " expected after \\" + pragma;
}
/////////////////////////// Token manipulation
Parser.prototype.advance = function() {
if(this.tokenPos >= this.tokens.length) {
this.token = null;
}
this.token = this.tokens[this.tokenPos++];
};
Parser.prototype.at = function(token) {
return this.token && (this.token.type === token || this.token.type === "token" && this.token.value === token);
};
Parser.prototype.eat = function(token) {
var at = this.at(token);
if(at) {
this.advance();
}
return at;
};
Parser.prototype.tokenValueEaten = function() {
var output = this.token.value;
this.advance();
return output;
};
Parser.prototype.close = function(token) {
if(!this.eat(token)) {
throw "Closing " + token + " expected";
}
};
Parser.prototype.checkFinished = function() {
if(this.token) {
throw "Syntax error at " + this.token.value;
}
};
Parser.prototype.expect = function(token) {
if(!this.eat(token)) {
throw token + " expected";
}
};
Parser.prototype.expectString = function(context,token) {
if(!this.at("string")) {
token = token || "String";
throw token + " expected " + context;
}
return this.tokenValueEaten();
};
Parser.prototype.expectNameOrString = function(context) {
if(this.at("name")) {
return this.tokenValueEaten();
}
return this.expectString(context,"Name or string");
};
/////////////////////////// Tokenisation
Parser.prototype.tokenise = function(source) {
var tokens = [],
pos = 0,
c, s, token;
while(pos < source.length) {
// Initialise this iteration
s = token = null;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Avoid falling off the end of the string
if (pos >= source.length) {
break;
}
// Examine the next character
c = source.charAt(pos);
if("\"'".indexOf(c) !== -1) {
// String literal
token = $tw.utils.parseStringLiteral(source,pos);
if(!token) {
throw "Unterminated string literal";
}
} else if("[]{}".indexOf(c) !== -1) {
// Single or double character
s = source.charAt(pos+1) === c ? c + c : c;
} else if(c === "<") {
// < or <-
s = source.charAt(pos+1) === "-" ? "<-" : "<";
} else if(c === "-") {
// - or ->
s = source.charAt(pos+1) === ">" ? "->" : "-";
} else if("()>+/:|".indexOf(c) !== -1) {
// Single character
s = c;
} else if(c.match(/[a-zA-Z]/)) {
// Name
token = this.readName(source,pos);
} else if(c.match(/\\/)) {
// Pragma
token = this.readPragma(source,pos);
} else {
throw "Syntax error at " + c;
}
// Add our findings to the return array
if(token) {
tokens.push(token);
} else {
token = $tw.utils.parseTokenString(source,pos,s);
tokens.push(token);
}
// Prepare for the next character
pos = token.end;
}
return tokens;
};
Parser.prototype.readName = function(source,pos) {
var re = /([a-zA-Z0-9_.-]+)/g;
re.lastIndex = pos;
var match = re.exec(source);
if(match && match.index === pos) {
return {type: "name", value: match[1], start: pos, end: pos+match[1].length};
} else {
throw "Invalid name";
}
};
Parser.prototype.readPragma = function(source,pos) {
var re = /([a-z]+)/g;
pos++;
re.lastIndex = pos;
var match = re.exec(source);
if(match && match.index === pos) {
return {type: "pragma", value: match[1], start: pos, end: pos+match[1].length};
} else {
throw "Invalid pragma";
}
};
/////////////////////////// Exports
exports.parser = Parser;
})();