/*\
title: $:/plugins/tiddlywiki/railroad/parser.js
type: application/javascript
module-type: library

Parser for the source of a railroad diagram.

[: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			sequence
<-x y z->		explicit sequence
(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;

})();