commit c3dd696a3e915bb0399e7e53a8ffa7fd1715f634 Author: David Majda Date: Sun Mar 7 20:41:02 2010 +0100 Initial commit. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2952875 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2010 David Majda + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README b/README new file mode 100644 index 0000000..316b25a --- /dev/null +++ b/README @@ -0,0 +1,4 @@ +PEG.js: Parser Generator for JavaScript +======================================= + +Documentation is being written -- please be patient. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..49d5957 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1 diff --git a/bin/pegjs b/bin/pegjs new file mode 100755 index 0000000..8f63320 --- /dev/null +++ b/bin/pegjs @@ -0,0 +1,4 @@ +#!/bin/sh + +DIR=`dirname "$0"` +java -jar "$DIR/../vendor/rhino/js.jar" "$DIR/pegjs-main.js" "$DIR" "$@" diff --git a/bin/pegjs-main.js b/bin/pegjs-main.js new file mode 100644 index 0000000..e0435cf --- /dev/null +++ b/bin/pegjs-main.js @@ -0,0 +1,160 @@ +importPackage(java.io); +importPackage(java.lang); + +/* + * Rhino does not have __FILE__ or anything similar so we have to pass the + * script path from the outside. + */ +load(arguments[0] + "/../lib/runtime.js"); +load(arguments[0] + "/../lib/compiler.js"); + +var FILE_STDIN = "-"; +var FILE_STDOUT = "-"; + +function readFile(file) { + var f = new BufferedReader(new InputStreamReader( + file === FILE_STDIN ? System["in"] : new FileInputStream(file) + )); + + var result = ""; + var line = ""; + try { + while ((line = f.readLine()) !== null) { + result += line + "\n"; + } + } finally { + f.close(); + } + + return result; +} + +function writeFile(file, text) { + var f = new BufferedWriter(new OutputStreamWriter( + file === FILE_STDOUT ? System.out : new FileOutputStream(file) + )); + + try { + f.write(text); + } finally { + f.close(); + } +} + +function isOption(arg) { + return /-.+/.test(arg); +} + +function printVersion() { + print("PEG.js 0.1"); +} + +function printHelp() { + print("Usage: pegjs [options] [--] [] []"); + print(""); + print("Generates a parser from the PEG grammar specified in the and"); + print("writes it to the . The parser object will be stored in a variable"); + print("named ."); + print(""); + print("If the is omitted, its name is generated by changing the"); + print(" extension to \".js\". If both and are"); + print("omitted, standard input and output are used."); + print(""); + print("Options:"); + print(" -s, --start-rule specify grammar start rule (default: \"start\")"); + print(" -v, --version print version information and exit"); + print(" -h, --help print help and exit"); +} + +function nextArg() { + args.shift(); +} + +function exitSuccess() { + quit(0); +} + +function exitFailure() { + quit(1); +} + +function abort(message) { + System.out.println(message); + exitFailure(); +} + +var startRule = "start"; + +/* + * The trimmed first argument is the script path -- see the beginning of this + * file. + */ +var args = Array.prototype.slice.call(arguments, 1); + +while (args.length > 0 && isOption(args[0])) { + switch (args[0]) { + case "-s": + case "--start": + nextArg(); + if (args.length === 0) { + abort("Missing parameter of the -s/--start option."); + } + startRule = args[0]; + break; + + case "-v": + case "--version": + printVersion(); + exitSuccess(); + break; + + case "-h": + case "--help": + printHelp(); + exitSuccess(); + break; + + case "--": + nextArg(); + break; + + default: + abort("Unknown option: " + args[0] + "."); + } + nextArg(); +} + +if (args.length === 0) { + abort("Too few arguments."); +} +var parserVar = args[0]; +nextArg(); + +switch (args.length) { + case 0: + var inputFile = FILE_STDIN; + var outputFile = FILE_STDOUT; + break; + case 1: + var inputFile = args[0]; + var outputFile = args[0].replace(/\.[^.]*$/, ".js"); + break; + case 2: + var inputFile = args[0]; + var outputFile = args[1]; + break; + default: + abort("Too many arguments."); +} + +var input = readFile(inputFile); +try { + var parser = PEG.buildParser(input, startRule); +} catch (e) { + if (e.line !== undefined && e.column !== undefined) { + abort(e.line + ":" + e.column + ": " + e.message); + } else { + abort(e.message); + } +} +writeFile(outputFile, parserVar + " = " + parser.toSource() + ";\n"); diff --git a/bin/pegjs.bat b/bin/pegjs.bat new file mode 100644 index 0000000..a34138f --- /dev/null +++ b/bin/pegjs.bat @@ -0,0 +1,5 @@ +@echo off + +set DIR_WITH_SLASH=%~dp0 +set DIR=%DIR_WITH_SLASH:~0,-1% +java -jar "%DIR%\..\vendor\rhino\js.jar" "%DIR%\pegjs-main.js" "%DIR%" %* diff --git a/examples/arithmetics.peg b/examples/arithmetics.peg new file mode 100644 index 0000000..71e5bb9 --- /dev/null +++ b/examples/arithmetics.peg @@ -0,0 +1,31 @@ +start : _ expression { return $2; } + +expression : additive + +additive : multiplicative (plus / minus) additive { + if ($2 === "+") { return $1 + $3; } + if ($2 === "-") { return $1 - $3; } + } + / multiplicative + +multiplicative : primary (times / divide) multiplicative { + if ($2 === "*") { return $1 * $3; } + if ($2 === "/") { return $1 / $3; } + } + / primary + +primary : integer + / lparen expression rparen { return $2 } + +integer "integer" + : [0-9]+ _ { return parseInt($1.join("")); } + +plus : "+" _ { return $1; } +minus : "-" _ { return $1; } +times : "*" _ { return $1; } +divide : "/" _ { return $1; } +lparen : "(" _ +rparen : ")" _ + +_ "whitespace" + : [ \t\n\r]* diff --git a/lib/compiler.js b/lib/compiler.js new file mode 100644 index 0000000..2678055 --- /dev/null +++ b/lib/compiler.js @@ -0,0 +1,1325 @@ +/* + * PEG.js compiler. + * + * The runtime.js file must be included before this file. + */ + +(function() { + +/* ===== PEG ===== */ + +/* + * Generates a parser from a specified grammar and start rule and returns it. + * + * The grammar may be either an object or a string. If it is an object, it + * must contain AST of the parsing expressions (i.e. instances of |PEG.Grammar.* + * classes| for the grammar rules in its properties. If it is a string, it is + * parsed using |PEG.grammarParser| to obtain the grammar AST and thus it must + * be in a format that this parser accepts (see the source code for details). + * + * The start rule may be unspecified, in which case "start" is used. + * + * Throws |PEG.Grammar.GrammarError| if the grammar definition is not object nor + * string or if it contains an error. Note that not all errors are detected + * during the generation and some may protrude to the generated parser and cause + * its malfunction. + */ +PEG.buildParser = function(grammar, startRule) { + startRule = startRule || "start"; + + switch (typeof(grammar)) { + case "object": + var ast = grammar; + break; + case "string": + var ast = PEG.grammarParser.parse(grammar); + break; + default: + throw new PEG.Grammar.GrammarError("Grammar must be object or string."); + } + + if (ast[startRule] === undefined) { + throw new PEG.Grammar.GrammarError("Missing \"" + startRule + "\" rule."); + } + + return PEG.Compiler.compileParser(ast, startRule); +}; + +/* ===== PEG.Grammar ===== */ + +/* Namespace with grammar AST nodes. */ + +PEG.Grammar = {}; + +/* ===== PEG.GrammarError ===== */ + +/* Thrown when the grammar contains an error. */ + +PEG.Grammar.GrammarError = function(message) { + this.name = "PEG.Grammar.GrammarError"; + this.message = message; +}; + +PEG.Grammar.GrammarError.prototype = Error.prototype; + +/* ===== PEG.Grammar.* ===== */ + +PEG.Grammar.Rule = function(name, humanName, expression) { + this._name = name; + this._humanName = humanName; + this._expression = expression; +}; + +PEG.Grammar.Rule.prototype = { + getName: function() { return this._name; } +}; + +PEG.Grammar.Literal = function(value) { this._value = value; }; + +PEG.Grammar.Any = function() {}; + +PEG.Grammar.Sequence = function(elements) { this._elements = elements; }; + +PEG.Grammar.Choice = function(alternatives) { + this._alternatives = alternatives; +}; + +PEG.Grammar.ZeroOrMore = function(element) { this._element = element; }; + +PEG.Grammar.NotPredicate = function(expression) { + this._expression = expression; +}; + +PEG.Grammar.RuleRef = function(name) { this._name = name; }; + +PEG.Grammar.Action = function(expression, action) { + this._expression = expression; + this._action = action; +}; + +/* ===== PEG.Compiler ===== */ + +PEG.Compiler = { + /* + * Takes parts of code, interpolates variables inside them and joins them with + * a newline. + * + * Variables are delimited with "${" and "}" and their names must be valid + * identifiers (i.e. they must match [a-zA-Z_][a-zA-Z0-9_]*). Variable values + * are specified as properties of the last parameter (if this is an object, + * otherwise empty variable set is assumed). Undefined variables result in + * throwing |Error|. + * + * There can be a filter specified after the variable name, prefixed with "|". + * The filter name must be a valid identifier. The only recognized filter + * right now is "string", which quotes the variable value as a JavaScript + * string. Unrecognized filters result in throwing |Error|. + * + * If any part has multiple lines and the first line is indented by some + * amount of whitespace (as defined by the /\s+/ JavaScript regular + * expression), second to last lines are indented by the same amount of + * whitespace. This results in nicely indented multiline code in variables + * without making the templates look ugly. + * + * Examples: + * + * PEG.Compiler.formatCode("foo", "bar"); // "foo\nbar" + * PEG.Compiler.formatCode( + * "foo", "${bar}", + * { bar: "baz" } + * ); // "foo\nbaz" + * PEG.Compiler.formatCode("foo", "${bar}"); // throws Error + * PEG.Compiler.formatCode( + * "foo", "${bar|string}", + * { bar: "baz" } + * ); // "foo\n\"baz\"" + * PEG.Compiler.formatCode( + * "foo", "${bar|eeek}", + * { bar: "baz" } + * ); // throws Error + * PEG.Compiler.formatCode( + * "foo", "${bar}", + * { bar: " baz\nqux" } + * ); // "foo\n baz\n qux" + */ + formatCode: function() { + function interpolateVariablesInParts(parts) { + return PEG.ArrayUtils.map(parts, function(part) { + return part.replace( + /\$\{([a-zA-Z_][a-zA-Z0-9_]*)(\|([a-zA-Z_][a-zA-Z0-9_]*))?\}/g, + function(match, name, dummy, filter) { + var value = vars[name]; + if (value === undefined) { + throw new Error("Undefined variable: \"" + name + "\"."); + } + + if (filter !== undefined && filter != "") { // JavaScript engines differ here. + if (filter === "string") { + return PEG.StringUtils.quote(value); + } else { + throw new Error("Unrecognized filter: \"" + filter + "\"."); + } + } else { + return value; + } + } + ); + }); + } + + function indentMultilineParts(parts) { + return PEG.ArrayUtils.map(parts, function(part) { + if (!/\n/.test(part)) { return part; } + + var firstLineWhitespacePrefix = part.match(/^\s*/)[0]; + var lines = part.split("\n"); + var linesIndented = [lines[0]].concat( + PEG.ArrayUtils.map(lines.slice(1), function(line) { + return firstLineWhitespacePrefix + line; + }) + ); + return linesIndented.join("\n"); + }); + } + + var args = Array.prototype.slice.call(arguments); + var vars = args[args.length - 1] instanceof Object ? args.pop() : {}; + + return indentMultilineParts(interpolateVariablesInParts(args)).join("\n"); + }, + + _uniqueIdentifierCounters: {}, + + /* Generates a unique identifier with specified prefix. */ + generateUniqueIdentifier: function(prefix) { + this._uniqueIdentifierCounters[prefix] + = this._uniqueIdentifierCounters[prefix] || 0; + return prefix + this._uniqueIdentifierCounters[prefix]++; + }, + + /* + * Generates a parser from a specified grammar and start rule. + */ + compileParser: function(grammar, startRule) { + var parseFunctionDefinitions = []; + for (var key in grammar) { + parseFunctionDefinitions.push(grammar[key].compile()); + } + + var source = this.formatCode( + "(function(){", + " var result = new PEG.Parser(${startRule|string});", + " ", + " ${parseFunctionDefinitions}", + " ", + " return result;", + "})()", + { + parseFunctionDefinitions: parseFunctionDefinitions.join("\n\n"), + startRule: startRule + } + ); + + var result = eval(source); + result._source = source; + return result; + } +}; + +PEG.Grammar.Rule.prototype.compile = function() { + var resultVar = PEG.Compiler.generateUniqueIdentifier("result"); + + if (this._humanName !== null) { + var setReportMatchFailuresCode = PEG.Compiler.formatCode( + "var savedReportMatchFailures = context.reportMatchFailures;", + "context.reportMatchFailures = false;" + ); + var restoreReportMatchFailuresCode = PEG.Compiler.formatCode( + "context.reportMatchFailures = savedReportMatchFailures;" + ); + var reportMatchFailureCode = PEG.Compiler.formatCode( + "if (context.reportMatchFailures && ${resultVar} === null) {", + " this._matchFailed(new PEG.Parser.NamedRuleMatchFailure(${humanName|string}));", + "}", + { + humanName: this._humanName, + resultVar: resultVar + } + ); + } else { + var setReportMatchFailuresCode = ""; + var restoreReportMatchFailuresCode = ""; + var reportMatchFailureCode = ""; + } + + return PEG.Compiler.formatCode( + "result._parse_${name} = function(context) {", + " this._cache[${name|string}] = this._cache[${name|string}] || [];", + " var cachedResult = this._cache[${name|string}][this._pos];", + " if (cachedResult !== undefined) {", + " this._pos += cachedResult.length;", + " return cachedResult.result;", + " }", + " ", + " var pos = this._pos;", + " ", + " ${setReportMatchFailuresCode}", + " ${code}", + " ${restoreReportMatchFailuresCode}", + " ${reportMatchFailureCode}", + " ", + " this._cache[${name|string}][pos] = {", + " length: this._pos - pos,", + " result: ${resultVar}", + " };", + " return ${resultVar};", + "};", + { + name: this._name, + setReportMatchFailuresCode: setReportMatchFailuresCode, + restoreReportMatchFailuresCode: restoreReportMatchFailuresCode, + reportMatchFailureCode: reportMatchFailureCode, + code: this._expression.compile(resultVar), + resultVar: resultVar + } + ); +}; + +/* + * The contract for all code fragments generated by the following |compile| + * methods is as follows: + * + * * The code fragment should try to match a part of the input starting with the + * position indicated in |this._pos|. That position may point past the end of + * the input. + * + * * If the code fragment matches the input, it advances |this._pos| after the + * matched part of the input and sets variable with a name stored in + * |resultVar| to appropriate value, which is always non-null. + * + * * If the code fragment does not match the input, it does not change + * |this._pos| and it sets a variable with a name stored in |resultVar| to + * |null|. + */ + +PEG.Grammar.Literal.prototype.compile = function(resultVar) { + return PEG.Compiler.formatCode( + "if (this._input.substr(this._pos, ${length}) === ${value|string}) {", + " var ${resultVar} = ${value|string};", + " this._pos += ${length};", + "} else {", + " var ${resultVar} = null;", + " if (context.reportMatchFailures) {", + " this._matchFailed(new PEG.Parser.LiteralMatchFailure(${value|string}));", + " }", + "}", + { + value: this._value, + length: this._value.length, + resultVar: resultVar + } + ); +}; + +PEG.Grammar.Any.prototype.compile = function(resultVar) { + return PEG.Compiler.formatCode( + "if (this._input.length > this._pos) {", + " var ${resultVar} = this._input[this._pos];", + " this._pos++;", + "} else {", + " var ${resultVar} = null;", + " if (context.reportMatchFailures) {", + " this._matchFailed(new PEG.Parser.AnyMatchFailure());", + " }", + "}", + { resultVar: resultVar } + ); +}; + +PEG.Grammar.Sequence.prototype.compile = function(resultVar) { + var savedPosVar = PEG.Compiler.generateUniqueIdentifier("savedPos"); + + var elementResultVars = PEG.ArrayUtils.map(this._elements, function() { + return PEG.Compiler.generateUniqueIdentifier("result") + }); + + var code = PEG.Compiler.formatCode( + "var ${resultVar} = ${elementResultVarArray};", + { + resultVar: resultVar, + elementResultVarArray: "[" + elementResultVars.join(", ") + "]" + } + ); + + for (var i = this._elements.length - 1; i >= 0; i--) { + code = PEG.Compiler.formatCode( + "${elementCode}", + "if (${elementResultVar} !== null) {", + " ${code}", + "} else {", + " var ${resultVar} = null;", + " this._pos = ${savedPosVar};", + "}", + { + elementCode: this._elements[i].compile(elementResultVars[i]), + elementResultVar: elementResultVars[i], + code: code, + savedPosVar: savedPosVar, + resultVar: resultVar + } + ); + } + + return PEG.Compiler.formatCode( + "var ${savedPosVar} = this._pos;", + "${code}", + { + code: code, + savedPosVar: savedPosVar + } + ); +}; + +PEG.Grammar.Choice.prototype.compile = function(resultVar) { + var code = PEG.Compiler.formatCode( + "var ${resultVar} = null;", + { resultVar: resultVar } + ); + + for (var i = this._alternatives.length - 1; i >= 0; i--) { + var alternativeResultVar = PEG.Compiler.generateUniqueIdentifier("result"); + code = PEG.Compiler.formatCode( + "${alternativeCode}", + "if (${alternativeResultVar} !== null) {", + " var ${resultVar} = ${alternativeResultVar};", + "} else {", + " ${code};", + "}", + { + alternativeCode: this._alternatives[i].compile(alternativeResultVar), + alternativeResultVar: alternativeResultVar, + code: code, + resultVar: resultVar + } + ); + } + + return code; +}; + +PEG.Grammar.ZeroOrMore.prototype.compile = function(resultVar) { + var elementResultVar = PEG.Compiler.generateUniqueIdentifier("result"); + + return PEG.Compiler.formatCode( + "var ${resultVar} = [];", + "${elementCode}", + "while (${elementResultVar} !== null) {", + " ${resultVar}.push(${elementResultVar});", + " ${elementCode}", + "}", + { + elementCode: this._element.compile(elementResultVar), + elementResultVar: elementResultVar, + resultVar: resultVar + } + ); +}; + +PEG.Grammar.NotPredicate.prototype.compile = function(resultVar) { + var savedPosVar = PEG.Compiler.generateUniqueIdentifier("savedPos"); + var expressionResultVar = PEG.Compiler.generateUniqueIdentifier("result"); + + return PEG.Compiler.formatCode( + "var ${savedPosVar} = this._pos;", + "${expressionCode}", + "if (${expressionResultVar} === null) {", + " var ${resultVar} = '';", + "} else {", + " var ${resultVar} = null;", + " this._pos = ${savedPosVar};", + "}", + { + expressionCode: this._expression.compile(expressionResultVar), + expressionResultVar: expressionResultVar, + savedPosVar: savedPosVar, + resultVar: resultVar + } + ); +}; + +PEG.Grammar.RuleRef.prototype.compile = function(resultVar) { + return PEG.Compiler.formatCode( + "var ${resultVar} = this.${ruleMethod}(context);", + { + ruleMethod: "_parse_" + this._name, + resultVar: resultVar + } + ); +}; + +PEG.Grammar.Action.prototype.compile = function(resultVar) { + var expressionResultVar = PEG.Compiler.generateUniqueIdentifier("result"); + + if (typeof(this._action) === "function") { + var actionFunction = this._action.toString(); + } else { + var actionCode = this._action.replace( + /\$(\d+)/g, + function(match, digits) { + return PEG.Compiler.formatCode( + "(arguments[${index}])", + { index: parseInt(digits) - 1 } + ); + } + ) + var actionFunction = PEG.Compiler.formatCode( + "function() { ${actionCode} }", + { actionCode: actionCode } + ); + } + + /* + * In case of sequences, we splat their elements into function arguments one + * by one. Example: + * + * start: "a" "b" "c" { alert(arguments.length) } // => "3" + */ + var invokeFunctionName = this._expression instanceof PEG.Grammar.Sequence + ? "apply" + : "call"; + + return PEG.Compiler.formatCode( + "${expressionCode}", + "var ${resultVar} = ${expressionResultVar} !== null", + " ? (${actionFunction}).${invokeFunctionName}(this, ${expressionResultVar})", + " : null;", + { + expressionCode: this._expression.compile(expressionResultVar), + expressionResultVar: expressionResultVar, + actionFunction: actionFunction, + invokeFunctionName: invokeFunctionName, + resultVar: resultVar + } + ); +}; + +/* ===== PEG.grammarParser ===== */ + +var returnFirstArg = function() { return arguments[0]; } +var returnSecondArg = function() { return arguments[1]; } +var returnSecondArgJoined = function() { return arguments[1].join(""); } +var returnFirstArgAndSecondArgJoined = function() { return arguments[0] + arguments[1].join(""); } + +function characterRule(name, ch) { + with (PEG.Grammar) { + return new Rule( + name, + null, + new Action( + new Sequence([new Literal(ch), new RuleRef("__")]), + returnFirstArg + ) + ); + } +} + +/* Bootstrapping is really badly needed. */ + +with (PEG.Grammar) { + PEG.grammarParser = PEG.buildParser({ + grammar: + new Rule( + "grammar", + null, + new Action( + new Sequence([ + new RuleRef("__"), + new RuleRef("rule"), + new ZeroOrMore(new RuleRef("rule")) + ]), + function(dummy, first, rest) { + var rules = [first].concat(rest); + var result = {}; + for (var i = 0; i < rules.length; i++) { + result[rules[i].getName()] = rules[i]; + } + return result; + } + ) + ), + + rule: + new Rule( + "rule", + null, + new Action( + new Sequence([ + new RuleRef("identifier"), + new Choice([new RuleRef("literal"), new Literal("")]), + new RuleRef("colon"), + new RuleRef("expression") + ]), + function(name, humanName, dummy, expression) { + return new PEG.Grammar.Rule( + name, + humanName !== "" ? humanName : null, + expression + ); + } + ) + ), + + expression: + new Rule("expression", null, new RuleRef("choice")), + + choice: + new Rule( + "choice", + null, + new Action( + new Sequence([ + new RuleRef("sequence"), + new ZeroOrMore( + new Sequence([new RuleRef("slash"), new RuleRef("sequence")]) + ) + ]), + function(first, rest) { + return rest.length > 0 + ? new PEG.Grammar.Choice([first].concat(PEG.ArrayUtils.map( + rest, + function(element) { return element[1]; } + ))) + : first; + } + ) + ), + + sequence: + new Rule( + "sequence", + null, + new Choice([ + new Action( + new Sequence([ + new ZeroOrMore(new RuleRef("prefixed")), + new RuleRef("action") + ]), + function(expressions, action) { + return new PEG.Grammar.Action( + expressions.length != 1 + ? new PEG.Grammar.Sequence(expressions) + : expressions[0], + action + ); + } + ), + new Action( + new ZeroOrMore(new RuleRef("prefixed")), + function(expressions) { + return expressions.length != 1 + ? new PEG.Grammar.Sequence(expressions) + : expressions[0]; + } + ) + ]) + ), + + prefixed: + new Rule( + "prefixed", + null, + new Choice([ + new Action( + new Sequence([new RuleRef("and"), new RuleRef("suffixed")]), + function(dummy, expression) { + return new PEG.Grammar.NotPredicate( + new PEG.Grammar.NotPredicate(expression) + ); + } + ), + new Action( + new Sequence([new RuleRef("not"), new RuleRef("suffixed")]), + function(dummy, expression) { + return new PEG.Grammar.NotPredicate(expression); + } + ), + new RuleRef("suffixed") + ]) + ), + + suffixed: + new Rule( + "suffixed", + null, + new Choice([ + new Action( + new Sequence([new RuleRef("primary"), new RuleRef("question")]), + function(expression) { + return new PEG.Grammar.Choice([ + expression, + new PEG.Grammar.Literal("") + ]); + } + ), + new Action( + new Sequence([new RuleRef("primary"), new RuleRef("star")]), + function(expression) { return new PEG.Grammar.ZeroOrMore(expression); } + ), + new Action( + new Sequence([new RuleRef("primary"), new RuleRef("plus")]), + function(expression) { + return new PEG.Grammar.Action( + new PEG.Grammar.Sequence([ + expression, + new PEG.Grammar.ZeroOrMore(expression) + ]), + function(first, rest) { return [first].concat(rest); } + ); + } + ), + new RuleRef("primary") + ]) + ), + + primary: + new Rule( + "primary", + null, + new Choice([ + new Action( + new Sequence([ + new RuleRef("identifier"), + new NotPredicate( + new Sequence([ + new Choice([new RuleRef("literal"), new Literal("")]), + new RuleRef("colon") + ]) + ) + ]), + function(identifier) { return new PEG.Grammar.RuleRef(identifier); } + ), + new Action( + new RuleRef("literal"), + function(literal) { return new PEG.Grammar.Literal(literal); } + ), + new Action( + new RuleRef("dot"), + function() { return new PEG.Grammar.Any(); } + ), + new Action( + new RuleRef("class"), + function(characters) { + return new PEG.Grammar.Choice( + PEG.ArrayUtils.map( + characters.split(""), + function(character) { + return new PEG.Grammar.Literal(character); + } + ) + ); + } + ), + new Action( + new Sequence([ + new RuleRef("lparen"), + new RuleRef("expression"), + new RuleRef("rparen") + ]), + returnSecondArg + ) + ]) + ), + + /* "Lexical" elements */ + + action: + new Rule( + "action", + "action", + new Action( + new Sequence([new RuleRef("braced"), new RuleRef("__")]), + function(braced) { return braced.substr(1, braced.length - 2); } + ) + ), + + braced: + new Rule( + "braced", + null, + new Action( + new Sequence([ + new Literal("{"), + new ZeroOrMore( + new Choice([ + new RuleRef("braced"), + new RuleRef("nonBraceCharacters") + ]) + ), + new Literal("}") + ]), + function(leftBrace, parts, rightBrace) { + return leftBrace + parts.join("") + rightBrace; + } + ) + ), + + nonBraceCharacters: + new Rule( + "nonBraceCharacters", + null, + new Action( + new Sequence([ + new RuleRef("nonBraceCharacter"), + new ZeroOrMore(new RuleRef("nonBraceCharacter")) + ]), + returnFirstArgAndSecondArgJoined + ) + ), + + nonBraceCharacter: + new Rule( + "nonBraceCharacter", + null, + new Action( + new Sequence([ + new NotPredicate(new Choice([new Literal("{"), new Literal("}")])), + new Any() + ]), + returnSecondArg + ) + ), + + colon: characterRule("colon", ":"), + slash: characterRule("slash", "/"), + and: characterRule("and", "&"), + not: characterRule("not", "!"), + question: characterRule("question", "?"), + star: characterRule("star", "*"), + plus: characterRule("plus", "+"), + lparen: characterRule("lparen", "("), + rparen: characterRule("rparen", ")"), + dot: characterRule("dot", "."), + + /* + * Modelled after ECMA-262, 5th ed., 7.6, but much simplified: + * + * * no Unicode escape sequences + * + * * "Unicode combining marks" and "Unicode connection punctuation" can't + * be part of the identifier + * + * * only [a-zA-Z] is considered a "Unicode letter" + * + * * only [0-9] is considered a "Unicode digit" + * + * The simplifications were made just to make the implementation little + * bit easier, there is no "philosophical" reason behind them. + */ + identifier: + new Rule( + "identifier", + "identifier", + new Action( + new Sequence([ + new Choice([ + new RuleRef("letter"), + new Literal("_"), + new Literal("$") + ]), + new ZeroOrMore( + new Choice([ + new RuleRef("letter"), + new RuleRef("digit"), + new Literal("_"), + new Literal("$") + ]) + ), + new RuleRef("__") + ]), + returnFirstArgAndSecondArgJoined + ) + ), + + /* + * Modelled after ECMA-262, 5th ed., 7.8.4. (syntax & semantics, rules only + * vaguely), + */ + literal: + new Rule( + "literal", + "literal", + new Action( + new Sequence([ + new Choice([ + new RuleRef("doubleQuotedLiteral"), + new RuleRef("singleQuotedLiteral") + ]), + new RuleRef("__") + ]), + returnFirstArg + ) + ), + + doubleQuotedLiteral: + new Rule( + "doubleQuotedLiteral", + null, + new Action( + new Sequence([ + new Literal('"'), + new ZeroOrMore(new RuleRef("doubleQuotedCharacter")), + new Literal('"') + ]), + returnSecondArgJoined + ) + ), + + doubleQuotedCharacter: + new Rule( + "doubleQuotedCharacter", + null, + new Choice([ + new RuleRef("simpleDoubleQuotedCharacter"), + new RuleRef("simpleEscapeSequence"), + new RuleRef("zeroEscapeSequence"), + new RuleRef("hexEscapeSequence"), + new RuleRef("unicodeEscapeSequence"), + new RuleRef("eolEscapeSequence") + ]) + ), + + simpleDoubleQuotedCharacter: + new Rule( + "simpleDoubleQuotedCharacter", + null, + new Action( + new Sequence([ + new NotPredicate( + new Choice([ + new Literal('"'), + new Literal("\\"), + new RuleRef("eolChar") + ]) + ), + new Any() + ]), + returnSecondArg + ) + ), + + singleQuotedLiteral: + new Rule( + "singleQuotedLiteral", + null, + new Action( + new Sequence([ + new Literal("'"), + new ZeroOrMore(new RuleRef("singleQuotedCharacter")), + new Literal("'") + ]), + returnSecondArgJoined + ) + ), + + singleQuotedCharacter: + new Rule( + "singleQuotedCharacter", + null, + new Choice([ + new RuleRef("simpleSingleQuotedCharacter"), + new RuleRef("simpleEscapeSequence"), + new RuleRef("zeroEscapeSequence"), + new RuleRef("hexEscapeSequence"), + new RuleRef("unicodeEscapeSequence"), + new RuleRef("eolEscapeSequence") + ]) + ), + + simpleSingleQuotedCharacter: + new Rule( + "simpleSingleQuotedCharacter", + null, + new Action( + new Sequence([ + new NotPredicate( + new Choice([ + new Literal("'"), + new Literal("\\"), + new RuleRef("eolChar") + ]) + ), + new Any() + ]), + returnSecondArg + ) + ), + + "class": + new Rule( + "class", + "character class", + new Action( + new Sequence([ + new Literal("["), + new ZeroOrMore( + new Choice([ + new RuleRef("classCharacterRange"), + new RuleRef("classCharacter") + ]) + ), + new Literal("]"), + new RuleRef("__") + ]), + returnSecondArgJoined + ) + ), + + classCharacterRange: + new Rule( + "classCharacterRange", + null, + new Action( + new Sequence([ + new RuleRef("bracketDelimitedCharacter"), + new Literal("-"), + new RuleRef("bracketDelimitedCharacter") + ]), + function(begin, dummy2, end) { + var beginCharCode = begin.charCodeAt(0); + var endCharCode = end.charCodeAt(0); + if (beginCharCode > endCharCode) { + throw new PEG.Parser.SyntaxError( + "Invalid character range: " + begin + "-" + end + "." + ); + } + + var result = ""; + + for (var charCode = beginCharCode; charCode <= endCharCode; charCode++) { + result += String.fromCharCode(charCode); + } + + return result; + } + ) + ), + + classCharacter: + new Rule("classCharacter", null, new RuleRef("bracketDelimitedCharacter")), + + bracketDelimitedCharacter: + new Rule( + "bracketDelimitedCharacter", + null, + new Choice([ + new RuleRef("simpleBracketDelimitedCharacter"), + new RuleRef("simpleEscapeSequence"), + new RuleRef("zeroEscapeSequence"), + new RuleRef("hexEscapeSequence"), + new RuleRef("unicodeEscapeSequence"), + new RuleRef("eolEscapeSequence") + ]) + ), + + simpleBracketDelimitedCharacter: + new Rule( + "simpleBracketDelimitedCharacter", + null, + new Action( + new Sequence([ + new NotPredicate( + new Choice([ + new Literal(']'), + new Literal("\\"), + new RuleRef("eolChar") + ]) + ), + new Any() + ]), + returnSecondArg + ) + ), + + simpleEscapeSequence: + new Rule( + "simpleEscapeSequence", + null, + new Action( + new Sequence([ + new Literal("\\"), + new NotPredicate( + new Choice([ + new RuleRef("digit"), + new Literal("x"), + new Literal("u"), + new RuleRef("eolChar") + ]) + ), + new Any() + ]), + function(dummy1, dummy2, character) { + return character + .replace("b", "\b") + .replace("f", "\f") + .replace("n", "\n") + .replace("r", "\r") + .replace("t", "\t") + .replace("v", "\v") + } + ) + ), + + zeroEscapeSequence: + new Rule( + "zeroEscapeSequence", + null, + new Action( + new Sequence([ + new Literal("\\0"), + new NotPredicate(new RuleRef("digit")) + ]), + function() { return "\0" } + ) + ), + + hexEscapeSequence: + new Rule( + "hexEscapeSequence", + null, + new Action( + new Sequence([ + new Literal("\\x"), + new RuleRef("hexDigit"), + new RuleRef("hexDigit") + ]), + function(dummy, digit1, digit2) { + return String.fromCharCode(parseInt("0x" + digit1 + digit2)); + } + ) + ), + + unicodeEscapeSequence: + new Rule( + "unicodeEscapeSequence", + null, + new Action( + new Sequence([ + new Literal("\\u"), + new RuleRef("hexDigit"), + new RuleRef("hexDigit"), + new RuleRef("hexDigit"), + new RuleRef("hexDigit") + ]), + function(dummy, digit1, digit2, digit3, digit4) { + return String.fromCharCode(parseInt( + "0x" + digit1 + digit2 + digit3 + digit4 + )); + } + ) + ), + + eolEscapeSequence: + new Rule( + "eolEscapeSequence", + null, + new Action( + new Sequence([new Literal("\\"), new RuleRef("eol")]), + returnSecondArg + ) + ), + + digit: + new Rule( + "digit", + null, + new Choice([ + new Literal("0"), + new Literal("1"), + new Literal("2"), + new Literal("3"), + new Literal("4"), + new Literal("5"), + new Literal("6"), + new Literal("7"), + new Literal("8"), + new Literal("9") + ]) + ), + + hexDigit: + new Rule( + "hexDigit", + null, + new Choice([ + new Literal("0"), + new Literal("1"), + new Literal("2"), + new Literal("3"), + new Literal("4"), + new Literal("5"), + new Literal("6"), + new Literal("7"), + new Literal("8"), + new Literal("9"), + new Literal("a"), + new Literal("b"), + new Literal("c"), + new Literal("d"), + new Literal("e"), + new Literal("f"), + new Literal("A"), + new Literal("B"), + new Literal("C"), + new Literal("D"), + new Literal("E"), + new Literal("F") + ]) + ), + + letter: + new Rule( + "letter", + null, + new Choice([ + new RuleRef("lowerCaseLetter"), + new RuleRef("upperCaseLetter") + ]) + ), + + lowerCaseLetter: + new Rule( + "lowerCaseLetter", + null, + new Choice([ + new Literal("a"), + new Literal("b"), + new Literal("c"), + new Literal("d"), + new Literal("e"), + new Literal("f"), + new Literal("g"), + new Literal("h"), + new Literal("i"), + new Literal("j"), + new Literal("k"), + new Literal("l"), + new Literal("m"), + new Literal("n"), + new Literal("o"), + new Literal("p"), + new Literal("q"), + new Literal("r"), + new Literal("s"), + new Literal("t"), + new Literal("u"), + new Literal("v"), + new Literal("w"), + new Literal("x"), + new Literal("y"), + new Literal("z") + ]) + ), + + upperCaseLetter: + new Rule( + "upperCaseLetter", + null, + new Choice([ + new Literal("A"), + new Literal("B"), + new Literal("C"), + new Literal("D"), + new Literal("E"), + new Literal("F"), + new Literal("G"), + new Literal("H"), + new Literal("I"), + new Literal("J"), + new Literal("K"), + new Literal("L"), + new Literal("M"), + new Literal("N"), + new Literal("O"), + new Literal("P"), + new Literal("Q"), + new Literal("R"), + new Literal("S"), + new Literal("T"), + new Literal("U"), + new Literal("V"), + new Literal("W"), + new Literal("X"), + new Literal("Y"), + new Literal("Z") + ]) + ), + + __: + new Rule( + "__", + null, + new ZeroOrMore( + new Choice([new RuleRef("whitespace"), new RuleRef("eol")]) + ) + ), + + /* Modelled after ECMA-262, 5th ed., 7.3. */ + eol: + new Rule( + "eol", + "end of line", + new Choice([ + new Literal("\n"), + new Literal("\r\n"), + new Literal("\r"), + new Literal("\u2028"), + new Literal("\u2029") + ]) + ), + + eolChar: + new Rule( + "eolChar", + null, + new Choice([ + new Literal("\n"), + new Literal("\r"), + new Literal("\u2028"), + new Literal("\u2029") + ]) + ), + + /* Modelled after ECMA-262, 5th ed., 7.2. */ + whitespace: + new Rule( + "whitespace", + "whitespace", + new Choice([ + new Literal(" "), + new Literal("\t"), + new Literal("\v"), + new Literal("\f"), + new Literal("\xA0"), + new Literal("\uFEFF"), + new Literal("\u1680"), + new Literal("\u180E"), + new Literal("\u2000"), + new Literal("\u2001"), + new Literal("\u2002"), + new Literal("\u2003"), + new Literal("\u2004"), + new Literal("\u2005"), + new Literal("\u2006"), + new Literal("\u2007"), + new Literal("\u2008"), + new Literal("\u2009"), + new Literal("\u200A"), + new Literal("\u202F"), + new Literal("\u205F"), + new Literal("\u3000") + ]) + ), + }, "grammar"); +} + +})(); diff --git a/lib/runtime.js b/lib/runtime.js new file mode 100644 index 0000000..770117a --- /dev/null +++ b/lib/runtime.js @@ -0,0 +1,219 @@ +/* + * PEG.js runtime. + * + * Required by all parsers generated by PEG.js. + */ + +PEG = {}; + +(function() { + +/* ===== PEG.ArrayUtils ===== */ + +/* Array manipulation utility functions. */ + +PEG.ArrayUtils = { + map: function(array, callback) { + var result = []; + var length = array.length; + for (var i = 0; i < length; i++) { + result[i] = callback(array[i]); + } + return result; + } +}; + +/* ===== PEG.StringUtils ===== */ + +/* String manipulation utility functions. */ + +PEG.StringUtils = { + /* + * Surrounds the string with quotes and escapes characters inside so that the + * result is a valid JavaScript string. + */ + quote: function(s) { + /* + * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a string + * literal except for the closing quote character, backslash, carriage + * return, line separator, paragraph separator, and line feed. Any character + * may appear in the form of an escape sequence. + */ + return '"' + s + .replace(/\\/g, '\\\\') // backslash + .replace(/"/g, '\\"') // closing quote character + .replace(/\r/g, '\\r') // carriage return + .replace(/\u2028/g, '\\u2028') // line separator + .replace(/\u2029/g, '\\u2029') // paragraph separator + .replace(/\n/g, '\\n') // line feed + + '"'; + }, + +}; + +/* ===== PEG.Parser ===== */ + +/* Prototype of all parsers generated by PEG.js. */ + +PEG.Parser = function(startRule) { this._startRule = startRule; } + +PEG.Parser.prototype = { + _matchFailed: function(failure) { + if (this._pos > this._rightmostMatchFailuresPos) { + this._rightmostMatchFailuresPos = this._pos; + this._rightmostMatchFailures = []; + } + this._rightmostMatchFailures.push(failure); + }, + + /* + * Parses the input with a generated parser. If the parsing is successfull, + * returns a value explicitly or implicitly specified by the grammar from + * which the parser was generated (see |PEG.buildParser|). If the parsing is + * unsuccessful, throws |PEG.Parser.SyntaxError| describing the error. + */ + parse: function(input) { + var that = this; + + function initialize() { + that._input = input; + that._pos = 0; + that._rightmostMatchFailuresPos = 0; + that._rightmostMatchFailures = []; + that._cache = {}; + } + + function buildErrorMessage() { + function buildExpectedFromMatchFailures(failures) { + switch (failures.length) { + case 0: + return "nothing"; + case 1: + return failures[0].toString(); + default: + return PEG.ArrayUtils.map( + failures.slice(0, failures.length - 1), + function(failure) { return failure.toString(); } + ).join(", ") + + " or " + + failures[failures.length - 1].toString(); + } + } + + if (that._pos === 0) { + var expected = buildExpectedFromMatchFailures( + that._rightmostMatchFailures + ); + var actual = that._rightmostMatchFailuresPos < that._input.length + ? PEG.StringUtils.quote( + that._input.charAt(that._rightmostMatchFailuresPos) + ) + : "end of input"; + } else { + var expected = "end of input"; + var actual = PEG.StringUtils.quote(that._input.charAt(that._pos)); + } + + return "Expected " + expected + " but " + actual + " found."; + } + + function computeErrorPosition() { + /* + * The first idea was to use |String.split| to break the input up to the + * error position along newlines and derive the line and column from + * there. However IE's |split| implementation is so broken that it was + * enough to prevent it. + */ + + var input = that._input; + var pos = that._rightmostMatchFailuresPos; + var line = 1; + var column = 1; + var seenCR = false; + + for (var i = 0; i < pos; i++) { + var ch = input.charAt(i); + if (ch === "\n") { + if (!seenCR) { line++; } + column = 1; + seenCR = false; + } else if (ch === "\r" | ch === "\u2028" || ch === "\u2029") { + line++; + column = 1; + seenCR = true; + } else { + column++; + seenCR = false; + } + } + + return { line: line, column: column }; + } + + initialize(); + + var initialContext = { + reportMatchFailures: true, + }; + + var result = this["_parse_" + this._startRule](initialContext); + if (result === null || this._pos !== input.length) { + var errorPosition = computeErrorPosition(); + throw new PEG.Parser.SyntaxError( + errorPosition.line, + errorPosition.column, + buildErrorMessage() + ); + } + + return result; + }, + + /* Returns the parser source code. */ + toSource: function() { return this._source; } +}; + +/* ===== PEG.Parser.LiteralMatchFailure ===== */ + +/* Stores information about a literal match failure. */ + +PEG.Parser.LiteralMatchFailure = function(value) { this._value = value; }; + +PEG.Parser.LiteralMatchFailure.prototype = { + toString: function() { return PEG.StringUtils.quote(this._value); } +}; + +/* ===== PEG.Parser.AnyMatchFailure ===== */ + +/* Stores information about a failure to match a "." expression. */ + +PEG.Parser.AnyMatchFailure = function() {} + +PEG.Parser.AnyMatchFailure.prototype = { + toString: function() { return "any character"; } +}; + +/* ===== PEG.Parser.NamedRuleMatchFailure ===== */ + +/* Stores information about a failure to match a named rule. */ + +PEG.Parser.NamedRuleMatchFailure = function(humanName) { this._humanName = humanName; } + +PEG.Parser.NamedRuleMatchFailure.prototype = { + toString: function() { return this._humanName; } +}; + +/* ===== PEG.Parser.SyntaxError ===== */ + +/* Thrown when a parser encounters a syntax error. */ + +PEG.Parser.SyntaxError = function(line, column, message) { + this.name = "PEG.Parser.SyntaxError"; + this.line = line; + this.column = column; + this.message = message; +}; + +PEG.Parser.SyntaxError.prototype = Error.prototype; + +})(); diff --git a/test/compiler-test.js b/test/compiler-test.js new file mode 100644 index 0000000..b54a587 --- /dev/null +++ b/test/compiler-test.js @@ -0,0 +1,919 @@ +(function() { + +var global = this; + +/* ===== Helpers ===== */ + +global.throws = function(block, exceptionType) { + var exception = null; + try { + block(); + } catch (e) { + exception = e; + } + + ok( + exception !== null, + exception !== null ? "okay: thrown something" : "failed, nothing thrown" + ); + if (exception !== null) { + ok( + exception instanceof exceptionType, + exception instanceof exceptionType + ? "okay: thrown " + exceptionType.name + : "failed, thrown " + exception.name + " instead of " + exceptionType.name + ); + } + + return exception; +}; + +global.parses = function(parser, input, expected) { + deepEqual(parser.parse(input), expected); +}; + +global.doesNotParse = function(parser, input) { + throws(function() { parser.parse(input); }, PEG.Parser.SyntaxError); +}; + +global.doesNotParseWithMessage = function(parser, input, message) { + var exception = throws( + function() { parser.parse(input); }, + PEG.Parser.SyntaxError + ); + if (exception) { + strictEqual(exception.message, message); + } +}; + +global.doesNotParseWithPos = function(parser, input, line, column) { + var exception = throws( + function() { parser.parse(input); }, + PEG.Parser.SyntaxError + ); + if (exception) { + strictEqual(exception.line, line); + strictEqual(exception.column, column); + } +}; + +global.grammarParserParses = function(input, expected) { + global.parses(PEG.grammarParser, input, expected); +}; + +global.grammarParserDoesNotParse = function(input) { + global.doesNotParse(PEG.grammarParser, input); +} + +/* ===== PEG.Compiler ===== */ + +module("PEG.Compiler"); + +test("formatCode joins parts", function() { + strictEqual(PEG.Compiler.formatCode("foo", "bar"), "foo\nbar"); +}); + +test("formatCode interpolates variables", function() { + strictEqual( + PEG.Compiler.formatCode("foo", "${bar}", { bar: "baz" }), + "foo\nbaz" + ); + + throws(function() { + PEG.Compiler.formatCode("foo", "${bar}"); + }, Error); +}); + +test("formatCode filters variables", function() { + strictEqual( + PEG.Compiler.formatCode("foo", "${bar|string}", { bar: "baz" }), + "foo\n\"baz\"" + ); + + throws(function() { + PEG.Compiler.formatCode("foo", "${bar|eeek}", { bar: "baz" }); + }, Error); +}); + +test("formatCode indents multiline parts", function() { + strictEqual( + PEG.Compiler.formatCode("foo", "${bar}", { bar: " baz\nqux" }), + "foo\n baz\n qux" + ); +}); + +test("generateUniqueIdentifier", function() { + notStrictEqual( + PEG.Compiler.generateUniqueIdentifier("prefix"), + PEG.Compiler.generateUniqueIdentifier("prefix") + ); +}); + +/* ===== PEG ===== */ + +module("PEG"); + +test("buildParser reports invalid grammar object", function() { + throws(function() { PEG.buildParser(42); }, PEG.Grammar.GrammarError); +}); + +test("buildParser reports missing start rule", function() { + throws(function() { PEG.buildParser({}); }, PEG.Grammar.GrammarError); +}); + +test("buildParser allows custom start rule", function() { + var parser = PEG.buildParser('s: "abcd"', "s"); + parses(parser, "abcd", "abcd"); +}); + +/* ===== Generated Parser ===== */ + +module("Generated Parser"); + +test("literals", function() { + var parser = PEG.buildParser('start: "abcd"'); + parses(parser, "abcd", "abcd"); + doesNotParse(parser, ""); + doesNotParse(parser, "abc"); + doesNotParse(parser, "abcde"); + doesNotParse(parser, "efgh"); + + /* + * Test that the parsing position moves forward after successful parsing of + * a literal. + */ + var posTestParser = PEG.buildParser('start: "a" "b"'); + parses(posTestParser, "ab", ["a", "b"]); +}); + +test("anys", function() { + var parser = PEG.buildParser('start: .'); + parses(parser, "a", "a"); + doesNotParse(parser, ""); + doesNotParse(parser, "ab"); + + /* + * Test that the parsing position moves forward after successful parsing of + * an any. + */ + var posTestParser = PEG.buildParser('start: . .'); + parses(posTestParser, "ab", ["a", "b"]); +}); + +test("classes", function() { + var emptyClassParser = PEG.buildParser('start: []'); + doesNotParse(emptyClassParser, ""); + doesNotParse(emptyClassParser, "a"); + doesNotParse(emptyClassParser, "ab"); + + var nonEmptyClassParser = PEG.buildParser('start: [ab-d]'); + parses(nonEmptyClassParser, "a", "a"); + parses(nonEmptyClassParser, "b", "b"); + parses(nonEmptyClassParser, "c", "c"); + parses(nonEmptyClassParser, "d", "d"); + doesNotParse(nonEmptyClassParser, ""); + doesNotParse(nonEmptyClassParser, "ab"); + + /* + * Test that the parsing position moves forward after successful parsing of + * a class. + */ + var posTestParser = PEG.buildParser('start: [ab-d] [ab-d]'); + parses(posTestParser, "ab", ["a", "b"]); +}); + +test("sequences", function() { + var emptySequenceParser = PEG.buildParser('start: '); + parses(emptySequenceParser, "", []); + doesNotParse(emptySequenceParser, "abc"); + + var nonEmptySequenceParser = PEG.buildParser('start: "a" "b" "c"'); + parses(nonEmptySequenceParser, "abc", ["a", "b", "c"]); + doesNotParse(nonEmptySequenceParser, ""); + doesNotParse(nonEmptySequenceParser, "ab"); + doesNotParse(nonEmptySequenceParser, "abcd"); + doesNotParse(nonEmptySequenceParser, "efg"); + + /* + * Test that the parsing position returns after unsuccessful parsing of a + * sequence. + */ + var posTestParser = PEG.buildParser('start: ("a" "b") / "a"'); + parses(posTestParser, "a", "a"); +}); + +test("choices", function() { + var parser = PEG.buildParser('start: "a" / "b" / "c"'); + parses(parser, "a", "a"); + parses(parser, "b", "b"); + parses(parser, "c", "c"); + doesNotParse(parser, ""); + doesNotParse(parser, "ab"); + doesNotParse(parser, "d"); +}); + +test("optional expressions", function() { + var parser = PEG.buildParser('start: "a"?'); + parses(parser, "", ""); + parses(parser, "a", "a"); +}); + +test("zero or more expressions", function() { + var parser = PEG.buildParser('start: "a"*'); + parses(parser, "", []); + parses(parser, "a", ["a"]); + parses(parser, "aaa", ["a", "a", "a"]); +}); + +test("one or more expressions", function() { + var parser = PEG.buildParser('start: "a"+'); + doesNotParse(parser, ""); + parses(parser, "a", ["a"]); + parses(parser, "aaa", ["a", "a", "a"]); +}); + +test("and predicate", function() { + var parser = PEG.buildParser('start: "a" &"b" "b"'); + parses(parser, "ab", ["a", "", "b"]); + doesNotParse(parser, "ac"); + + /* + * Test that the parsing position returns after successful parsing of a + * predicate is not needed, it is implicit in the tests above. + */ +}); + +test("not predicate", function() { + var parser = PEG.buildParser('start: "a" !"b"'); + parses(parser, "a", ["a", ""]); + doesNotParse(parser, "ab"); + + /* + * Test that the parsing position returns after successful parsing of a + * predicate. + */ + var posTestParser = PEG.buildParser('start: "a" !"b" "c"'); + parses(posTestParser, "ac", ["a", "", "c"]); +}); + +test("rule references", function() { + var parser = PEG.buildParser([ + 'start: static / dynamic', + 'static: "C" / "C++" / "Java" / "C#"', + 'dynamic: "Ruby" / "Python" / "JavaScript"' + ].join("\n")); + parses(parser, "Java", "Java"); + parses(parser, "Python", "Python"); +}); + +test("actions", function() { + var singleMatchParser = PEG.buildParser( + 'start: "a" { return Array.prototype.slice.call(arguments).join("").toUpperCase(); }' + ); + parses(singleMatchParser, "a", "A"); + + var multiMatchParser = PEG.buildParser( + 'start: "a" "b" "c" { return Array.prototype.slice.call(arguments).join("").toUpperCase(); }' + ); + parses(multiMatchParser, "abc", "ABC"); + + var innerMatchParser = PEG.buildParser( + 'start: "a" ("b" "c" "d" { return Array.prototype.slice.call(arguments).join("").toUpperCase(); }) "e"' + ); + parses(innerMatchParser, "abcde", ["a", "BCD", "e"]); + + /* Test that the action is not called when its expression does not match. */ + var notAMatchParser = PEG.buildParser( + 'start: "a" { ok(false, "action got called when it should not be"); }' + ); + doesNotParse(notAMatchParser, "b"); + + var variablesParser = PEG.buildParser([ + 'start: "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" {', + ' return [$1, $2, $3, $4, $5, $6, $7, $8, $9, $10].join("").toUpperCase();', + ' }' + ].join("\n")); + parses(variablesParser, "abcdefghij", "ABCDEFGHIJ"); +}); + +test("cache", function() { + /* + * Should trigger a codepath where the cache is used (for the "a" rule). + */ + var parser = PEG.buildParser([ + 'start: (a b) / (a c)', + 'a: "a"', + 'b: "b"', + 'c: "c"' + ].join("\n")); + parses(parser, "ac", ["a", "c"]); +}); + +test("error messages", function() { + var literalParser = PEG.buildParser('start: "abcd"'); + doesNotParseWithMessage( + literalParser, + "", + 'Expected "abcd" but end of input found.' + ); + doesNotParseWithMessage( + literalParser, + "efgh", + 'Expected "abcd" but "e" found.' + ); + doesNotParseWithMessage( + literalParser, + "abcde", + 'Expected end of input but "e" found.' + ); + + var anyParser = PEG.buildParser('start: .'); + doesNotParseWithMessage( + anyParser, + "", + 'Expected any character but end of input found.' + ); + + var namedRuleWithLiteralParser = PEG.buildParser('start "digit": [0-9]'); + doesNotParseWithMessage( + namedRuleWithLiteralParser, + "a", + 'Expected digit but "a" found.' + ); + + var namedRuleWithAnyParser = PEG.buildParser('start "whatever": .'); + doesNotParseWithMessage( + namedRuleWithAnyParser, + "", + 'Expected whatever but end of input found.' + ); + + var namedRuleWithNamedRuleParser = PEG.buildParser([ + 'start "digits": digit+', + 'digit "digit": [0-9]' + ].join("\n")); + doesNotParseWithMessage( + namedRuleWithNamedRuleParser, + "", + 'Expected digits but end of input found.' + ); + + var choiceParser = PEG.buildParser('start: "a" / "b" / "c"'); + doesNotParseWithMessage( + choiceParser, + "def", + 'Expected "a", "b" or "c" but "d" found.' + ); + + var emptyParser = PEG.buildParser('start: '); + doesNotParseWithMessage( + emptyParser, + "something", + 'Expected nothing but "s" found.' + ); +}); + +test("error positions", function() { + var parser = PEG.buildParser([ + 'start: line (("\\r" / "\\n" / "\\u2028" / "\\u2029")+ line)*', + 'line: digit (" "+ digit)*', + 'digit: [0-9]+ { return $1.join(""); }' + ].join("\n")); + + doesNotParseWithPos(parser, "a", 1, 1); + doesNotParseWithPos(parser, "1\n2\n\n3\n\n\n4 5 x", 7, 5); + + /* Non-Unix newlines */ + doesNotParseWithPos(parser, "1\rx", 2, 1); // Old Mac + doesNotParseWithPos(parser, "1\r\nx", 2, 1); // Windows + doesNotParseWithPos(parser, "1\n\rx", 3, 1); // mismatched + + /* Strange newlines */ + doesNotParseWithPos(parser, "1\u2028x", 2, 1); // line separator + doesNotParseWithPos(parser, "1\u2029x", 2, 1); // paragraph separator +}); + +/* + * Following examples are from Wikipedia, see + * http://en.wikipedia.org/w/index.php?title=Parsing_expression_grammar&oldid=335106938. + */ + +test("arithmetics", function() { + /* + * Value ← [0-9]+ / '(' Expr ')' + * Product ← Value (('*' / '/') Value)* + * Sum ← Product (('+' / '-') Product)* + * Expr ← Sum + */ + var parser = PEG.buildParser([ + 'Value : [0-9]+ { return parseInt($1.join("")); }', + ' / "(" Expr ")" { return $2; }', + 'Product : Value (("*" / "/") Value)* {', + ' var result = $1;', + ' for (var i = 0; i < $2.length; i++) {', + ' if ($2[i][0] == "*") { result *= $2[i][1]; }', + ' if ($2[i][0] == "/") { result /= $2[i][1]; }', + ' }', + ' return result;', + ' }', + 'Sum : Product (("+" / "-") Product)* {', + ' var result = $1;', + ' for (var i = 0; i < $2.length; i++) {', + ' if ($2[i][0] == "+") { result += $2[i][1]; }', + ' if ($2[i][0] == "-") { result -= $2[i][1]; }', + ' }', + ' return result;', + ' }', + 'Expr : Sum' + ].join("\n"), "Expr"); + + /* Test "value" rule. */ + parses(parser, "0", 0); + parses(parser, "123", 123); + parses(parser, "(42+43)", 42+43); + + /* Test "product" rule. */ + parses(parser, "42*43", 42*43); + parses(parser, "42*43*44*45", 42*43*44*45); + + /* Test "sum" rule. */ + parses(parser, "42*43+44*45", 42*43+44*45); + parses(parser, "42*43+44*45+46*47+48*49", 42*43+44*45+46*47+48*49); + + /* Test "expr" rule. */ + parses(parser, "42+43", 42+43); + + /* Complex test */ + parses(parser, "(1+2)*(3+4)",(1+2)*(3+4)); +}); + +test("non-context-free language", function() { + /* The following parsing expression grammar describes the classic + * non-context-free language { a^n b^n c^n : n >= 1 }: + * + * S ← &(A c) a+ B !(a/b/c) + * A ← a A? b + * B ← b B? c + */ + var parser = PEG.buildParser([ + 'S: &(A "c") "a"+ B !("a" / "b" / "c") { return $2.join("") + $3; }', + 'A: "a" A? "b" { return $1 + $2 + $3; }', + 'B: "b" B? "c" { return $1 + $2 + $3; }', + ].join("\n"), "S"); + + parses(parser, "abc", "abc"); + parses(parser, "aaabbbccc", "aaabbbccc"); + doesNotParse(parser, "aabbbccc"); + doesNotParse(parser, "aaaabbbccc"); + doesNotParse(parser, "aaabbccc"); + doesNotParse(parser, "aaabbbbccc"); + doesNotParse(parser, "aaabbbcc"); + doesNotParse(parser, "aaabbbcccc"); +}); + +test("nested comments", function() { + /* + * Begin ← "(*" + * End ← "*)" + * C ← Begin N* End + * N ← C / (!Begin !End Z) + * Z ← any single character + */ + var parser = PEG.buildParser([ + 'Begin : "(*"', + 'End : "*)"', + 'C : Begin N* End { return $1 + $2.join("") + $3; }', + 'N : C', + ' / (!Begin !End Z) { return $3; }', + 'Z : .' + ].join("\n"), "C"); + + parses(parser, "(**)", "(**)"); + parses(parser, "(*abc*)", "(*abc*)"); + parses(parser, "(*(**)*)", "(*(**)*)"); + parses( + parser, + "(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)", + "(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)" + ); +}); + +/* ===== Grammar Parser ===== */ + +module("Grammar Parser"); + +with (PEG.Grammar) { + var literalEmpty = new Literal(""); + var literalAbcd = new Literal("abcd"); + var literalEfgh = new Literal("efgh"); + var literalIjkl = new Literal("ijkl"); + + var choice = new Choice([literalAbcd, literalEmpty]); + + var notAbcd = new NotPredicate(literalAbcd); + var notEfgh = new NotPredicate(literalEfgh); + var notIjkl = new NotPredicate(literalIjkl); + + var sequenceEmpty = new Sequence([]); + var sequenceNots = new Sequence([notAbcd, notEfgh, notIjkl]); + var sequenceLiterals = new Sequence([literalAbcd, literalEfgh, literalIjkl]); + + function oneRuleGrammar(expression) { + return { start: new PEG.Grammar.Rule("start", null, expression) }; + } + + var simpleGrammar = oneRuleGrammar(new Literal("abcd")); + + function identifierGrammar(identifier) { + return oneRuleGrammar(new PEG.Grammar.RuleRef(identifier)); + } + + function literalGrammar(literal) { + return oneRuleGrammar(new PEG.Grammar.Literal(literal)); + } + + function classGrammar(chars) { + return oneRuleGrammar(new PEG.Grammar.Choice( + PEG.ArrayUtils.map( + chars.split(""), + function(char) { return new PEG.Grammar.Literal(char); } + ) + )); + } + + var anyGrammar = oneRuleGrammar(new Any()); + + function actionGrammar(action) { + return oneRuleGrammar(new PEG.Grammar.Action(new PEG.Grammar.Literal("a"), action)); + } + + /* Canonical grammar is "a: \"abcd\";\nb: \"efgh\";\nc: \"ijkl\";". */ + test("parses grammar", function() { + grammarParserParses('a: "abcd"', { a: new Rule("a", null, literalAbcd) }); + grammarParserParses( + 'a: "abcd"\nb: "efgh"\nc: "ijkl"', + { + a: new Rule("a", null, literalAbcd), + b: new Rule("b", null, literalEfgh), + c: new Rule("c", null, literalIjkl) + } + ); + }); + + /* Canonical rule is "a: \"abcd\"". */ + test("parses rule", function() { + grammarParserParses( + 'start: "abcd" / "efgh" / "ijkl"', + oneRuleGrammar(new Choice([literalAbcd, literalEfgh, literalIjkl])) + ); + grammarParserParses( + 'start "start rule": "abcd" / "efgh" / "ijkl"', + { + start: + new Rule( + "start", + "start rule", + new Choice([literalAbcd, literalEfgh, literalIjkl]) + ) + } + ); + }); + + /* Canonical expression is "\"abcd\" / \"efgh\" / \"ijkl\"". */ + test("parses expression", function() { + grammarParserParses( + 'start: "abcd" / "efgh" / "ijkl"', + oneRuleGrammar(new Choice([literalAbcd, literalEfgh, literalIjkl])) + ); + }); + + /* Canonical choice is "\"abcd\" / \"efgh\" / \"ijkl\"". */ + test("parses choice", function() { + grammarParserParses( + 'start: "abcd" "efgh" "ijkl"', + oneRuleGrammar(sequenceLiterals) + ); + grammarParserParses( + 'start: "abcd" "efgh" "ijkl" / "abcd" "efgh" "ijkl" / "abcd" "efgh" "ijkl"', + oneRuleGrammar(new Choice([ + sequenceLiterals, + sequenceLiterals, + sequenceLiterals + ])) + ); + }); + + /* Canonical sequence is "\"abcd\" \"efgh\" \"ijkl\"". */ + test("parses sequence", function() { + grammarParserParses( + 'start: { code }', + oneRuleGrammar(new Action(sequenceEmpty, " code ")) + ); + grammarParserParses( + 'start: !"abcd" { code }', + oneRuleGrammar(new Action(notAbcd, " code ")) + ); + grammarParserParses( + 'start: !"abcd" !"efgh" !"ijkl" { code }', + oneRuleGrammar(new Action(sequenceNots, " code ")) + ); + + grammarParserParses('start: ', oneRuleGrammar(sequenceEmpty)); + grammarParserParses('start: !"abcd"', oneRuleGrammar(notAbcd)); + grammarParserParses( + 'start: !"abcd" !"efgh" !"ijkl"', + oneRuleGrammar(sequenceNots) + ); + }); + + /* Canonical prefixed is "!\"abcd\"". */ + test("parses prefixed", function() { + grammarParserParses( + 'start: &"abcd"?', + oneRuleGrammar(new NotPredicate(new NotPredicate(choice))) + ); + grammarParserParses('start: !"abcd"?', oneRuleGrammar(new NotPredicate(choice))); + grammarParserParses('start: "abcd"?', oneRuleGrammar(choice)); + }); + + /* Canonical suffixed is "\"abcd\"?". */ + test("parses suffixed", function() { + grammarParserParses('start: "abcd"?', oneRuleGrammar(choice)); + grammarParserParses('start: "abcd"*', oneRuleGrammar(new ZeroOrMore(literalAbcd))); + grammarParserParses( + 'start: "abcd"+', + oneRuleGrammar(new Action( + new Sequence([literalAbcd, new ZeroOrMore(literalAbcd)]), + function(first, rest) { return [first].concat(rest); } + )) + ); + grammarParserParses('start: "abcd"', literalGrammar("abcd")); + }); + + /* Canonical primary is "\"abcd\"". */ + test("parses primary", function() { + grammarParserParses('start: a', identifierGrammar("a")); + grammarParserParses('start: "abcd"', literalGrammar("abcd")); + grammarParserParses('start: .', anyGrammar); + grammarParserParses('start: [a-d]', classGrammar("abcd")); + grammarParserParses('start: ("abcd")', literalGrammar("abcd")); + }); + + /* Canonical action is "{ code }". */ + test("parses action", function() { + grammarParserParses('start: "a" { code }', actionGrammar(" code ")); + }); + + /* Canonical braced is "{ code }". */ + test("parses braced", function() { + grammarParserParses('start: "a" {}', actionGrammar("")); + grammarParserParses('start: "a" {a}', actionGrammar("a")); + grammarParserParses('start: "a" {{a}}', actionGrammar("{a}")); + grammarParserParses('start: "a" {aaa}', actionGrammar("aaa")); + }); + + /* Trivial character rules are not tested. */ + + /* Canonical identifier is "a". */ + test("parses identifier", function() { + grammarParserParses('start: a', identifierGrammar("a")); + grammarParserParses('start: z', identifierGrammar("z")); + grammarParserParses('start: A', identifierGrammar("A")); + grammarParserParses('start: Z', identifierGrammar("Z")); + grammarParserParses('start: _', identifierGrammar("_")); + grammarParserParses('start: $', identifierGrammar("$")); + grammarParserParses('start: aa', identifierGrammar("aa")); + grammarParserParses('start: az', identifierGrammar("az")); + grammarParserParses('start: aA', identifierGrammar("aA")); + grammarParserParses('start: aZ', identifierGrammar("aZ")); + grammarParserParses('start: a0', identifierGrammar("a0")); + grammarParserParses('start: a9', identifierGrammar("a9")); + grammarParserParses('start: a_', identifierGrammar("a_")); + grammarParserParses('start: a$', identifierGrammar("a$")); + grammarParserParses('start: abcd', identifierGrammar("abcd")); + + grammarParserParses('start: a\n', identifierGrammar("a")); + }); + + /* Canonical literal is "\"abcd\"". */ + test("parses literal", function() { + grammarParserParses('start: "abcd"', literalGrammar("abcd")); + grammarParserParses("start: 'abcd'", literalGrammar("abcd")); + }); + + /* Canonical doubleQuotedLiteral is "\"abcd\"". */ + test("parses doubleQuotedLiteral", function() { + grammarParserParses('start: ""', literalGrammar("")); + grammarParserParses('start: "a"', literalGrammar("a")); + grammarParserParses('start: "abc"', literalGrammar("abc")); + + grammarParserParses('start: "abcd"\n', literalGrammar("abcd")); + }); + + /* Canonical doubleQuotedCharacter is "a". */ + test("parses doubleQuotedCharacter", function() { + grammarParserParses('start: "a"', literalGrammar("a")); + grammarParserParses('start: "\\n"', literalGrammar("\n")); + grammarParserParses('start: "\\0"', literalGrammar("\0")); + grammarParserParses('start: "\\x00"', literalGrammar("\x00")); + grammarParserParses('start: "\\u0120"', literalGrammar("\u0120")); + grammarParserParses('start: "\\\n"', literalGrammar("\n")); + }); + + /* Canonical simpleDoubleQuotedCharacter is "a". */ + test("parses simpleDoubleQuotedCharacter", function() { + grammarParserParses('start: "a"', literalGrammar("a")); + grammarParserParses('start: "\'"', literalGrammar("'")); + grammarParserDoesNotParse('start: """'); + grammarParserDoesNotParse('start: "\\"'); + grammarParserDoesNotParse('start: "\n"'); + grammarParserDoesNotParse('start: "\r"'); + grammarParserDoesNotParse('start: "\u2028"'); + grammarParserDoesNotParse('start: "\u2029"'); + }); + + /* Canonical singleQuotedLiteral is "'abcd'". */ + test("parses singleQuotedLiteral", function() { + grammarParserParses("start: ''", literalGrammar("")); + grammarParserParses("start: 'a'", literalGrammar("a")); + grammarParserParses("start: 'abc'", literalGrammar("abc")); + + grammarParserParses("start: 'abcd'\n", literalGrammar("abcd")); + }); + + /* Canonical singleQuotedCharacter is "a". */ + test("parses singleQuotedCharacter", function() { + grammarParserParses("start: 'a'", literalGrammar("a")); + grammarParserParses("start: '\\n'", literalGrammar("\n")); + grammarParserParses("start: '\\0'", literalGrammar("\0")); + grammarParserParses("start: '\\x00'", literalGrammar("\x00")); + grammarParserParses("start: '\\u0120'", literalGrammar("\u0120")); + grammarParserParses("start: '\\\n'", literalGrammar("\n")); + }); + + /* Canonical simpleSingleQuotedCharacter is "a". */ + test("parses simpleSingleQuotedCharacter", function() { + grammarParserParses("start: 'a'", literalGrammar("a")); + grammarParserParses("start: '\"'", literalGrammar("\"")); + grammarParserDoesNotParse("start: '''"); + grammarParserDoesNotParse("start: '\\'"); + grammarParserDoesNotParse("start: '\n'"); + grammarParserDoesNotParse("start: '\r'"); + grammarParserDoesNotParse("start: '\u2028'"); + grammarParserDoesNotParse("start: '\u2029'"); + }); + + /* Canonical class is "[a-d]". */ + test("parses classCharacterRange", function() { + grammarParserParses("start: []", classGrammar("")); + grammarParserParses("start: [a-d]", classGrammar("abcd")); + grammarParserParses("start: [a]", classGrammar("a")); + grammarParserParses("start: [a-de-hi-l]", classGrammar("abcdefghijkl")); + + grammarParserParses("start: [a-d]\n", classGrammar("abcd")); + }); + + /* Canonical classCharacterRange is "a-d". */ + test("parses classCharacterRange", function() { + grammarParserParses("start: [a-d]", classGrammar("abcd")); + grammarParserParses("start: [a-a]", classGrammar("a")); + grammarParserDoesNotParse("start: [b-a]"); + }); + + /* Canonical classCharacter is "a". */ + test("parses classCharacter", function() { + grammarParserParses("start: [a]", classGrammar("a")); + }); + + /* Canonical bracketDelimitedCharacter is "a". */ + test("parses bracketDelimitedCharacter", function() { + grammarParserParses("start: [a]", classGrammar("a")); + grammarParserParses("start: [\\n]", classGrammar("\n")); + grammarParserParses("start: [\\0]", classGrammar("\0")); + grammarParserParses("start: [\\x00]", classGrammar("\x00")); + grammarParserParses("start: [\\u0120]", classGrammar("\u0120")); + grammarParserParses("start: [\\\n]", classGrammar("\n")); + }); + + /* Canonical simpleBracketDelimiedCharacter is "a". */ + test("parses simpleBracketDelimitedCharacter", function() { + grammarParserParses("start: [a]", classGrammar("a")); + grammarParserParses("start: [[]", classGrammar("[")); + grammarParserDoesNotParse("start: []]"); + grammarParserDoesNotParse("start: [\\]"); + grammarParserDoesNotParse("start: [\n]"); + grammarParserDoesNotParse("start: [\r]"); + grammarParserDoesNotParse("start: [\u2028]"); + grammarParserDoesNotParse("start: [\u2029]"); + }); + + /* Canonical simpleEscapeSequence is "\\n". */ + test("parses simpleEscapeSequence", function() { + grammarParserParses('start: "\\\'"', literalGrammar("'")); + grammarParserParses('start: "\\""', literalGrammar("\"")); + grammarParserParses('start: "\\\\"', literalGrammar("\\")); + grammarParserParses('start: "\\b"', literalGrammar("\b")); + grammarParserParses('start: "\\f"', literalGrammar("\f")); + grammarParserParses('start: "\\n"', literalGrammar("\n")); + grammarParserParses('start: "\\r"', literalGrammar("\r")); + grammarParserParses('start: "\\t"', literalGrammar("\t")); + grammarParserParses('start: "\\v"', literalGrammar("\v")); + + grammarParserParses('start: "\\a"', literalGrammar("a")); + }); + + /* Canonical zeroEscapeSequence is "\\0". */ + test("parses zeroEscapeSequence", function() { + grammarParserParses('start: "\\0"', literalGrammar("\0")); + grammarParserDoesNotParse('start: "\\00"'); + grammarParserDoesNotParse('start: "\\09"'); + }); + + /* Canonical hexEscapeSequence is "\\x00". */ + test("parses hexEscapeSequence", function() { + grammarParserParses('start: "\\x00"', literalGrammar("\x00")); + grammarParserParses('start: "\\x09"', literalGrammar("\x09")); + grammarParserParses('start: "\\x0a"', literalGrammar("\x0a")); + grammarParserParses('start: "\\x0f"', literalGrammar("\x0f")); + grammarParserParses('start: "\\x0A"', literalGrammar("\x0A")); + grammarParserParses('start: "\\x0F"', literalGrammar("\x0F")); + grammarParserDoesNotParse('start: "\\x0"'); + grammarParserParses('start: "\\x000"', literalGrammar("\x000")); + }); + + /* Canonical unicodeEscapeSequence is "\\u0120". */ + test("parses unicodeEscapeSequence", function() { + grammarParserParses('start: "\\u0120"', literalGrammar("\u0120")); + grammarParserParses('start: "\\u0129"', literalGrammar("\u0129")); + grammarParserParses('start: "\\u012a"', literalGrammar("\u012a")); + grammarParserParses('start: "\\u012f"', literalGrammar("\u012f")); + grammarParserParses('start: "\\u012A"', literalGrammar("\u012A")); + grammarParserParses('start: "\\u012F"', literalGrammar("\u012F")); + grammarParserDoesNotParse('start: "\\u012"'); + grammarParserParses('start: "\\u01234"', literalGrammar("\u01234")); + }); + + /* Canonical eolEscapeSequence is "\\\n". */ + test("parses eolEscapeSequence", function() { + grammarParserParses('start: "\\\n"', literalGrammar("\n")); + grammarParserParses('start: "\\\r\n"', literalGrammar("\r\n")); + grammarParserParses('start: "\\\r"', literalGrammar("\r")); + grammarParserParses('start: "\\\u2028"', literalGrammar("\u2028")); + grammarParserParses('start: "\\\u2029"', literalGrammar("\u2029")); + }); + + /* Canonical __ is "\n". */ + test("parses __", function() { + grammarParserParses('start:"abcd"', simpleGrammar); + grammarParserParses('start: "abcd"', simpleGrammar); + grammarParserParses('start:\n"abcd"', simpleGrammar); + grammarParserParses('start: "abcd"', simpleGrammar); + }); + + /* Trivial character class rules are not tested. */ + + /* Canonical eol is "\n". */ + test("parses eol", function() { + grammarParserParses('start:\n"abcd"', simpleGrammar); + grammarParserParses('start:\r\n"abcd"', simpleGrammar); + grammarParserParses('start:\r"abcd"', simpleGrammar); + grammarParserParses('start:\u2028"abcd"', simpleGrammar); + grammarParserParses('start:\u2029"abcd"', simpleGrammar); + }); + + /* Canonical eolChar is "\n". */ + test("parses eolChar", function() { + grammarParserParses('start:\n"abcd"', simpleGrammar); + grammarParserParses('start:\r"abcd"', simpleGrammar); + grammarParserParses('start:\u2028"abcd"', simpleGrammar); + grammarParserParses('start:\u2029"abcd"', simpleGrammar); + }); + + /* Canonical whitespace is " ". */ + test("parses whitespace", function() { + grammarParserParses('start:\t"abcd"', simpleGrammar); + grammarParserParses('start:\v"abcd"', simpleGrammar); + grammarParserParses('start:\f"abcd"', simpleGrammar); + grammarParserParses('start: "abcd"', simpleGrammar); + grammarParserParses('start:\xA0"abcd"', simpleGrammar); + grammarParserParses('start:\uFEFF"abcd"', simpleGrammar); + grammarParserParses('start:\u1680"abcd"', simpleGrammar); + grammarParserParses('start:\u180E"abcd"', simpleGrammar); + grammarParserParses('start:\u2000"abcd"', simpleGrammar); + grammarParserParses('start:\u2001"abcd"', simpleGrammar); + grammarParserParses('start:\u2002"abcd"', simpleGrammar); + grammarParserParses('start:\u2003"abcd"', simpleGrammar); + grammarParserParses('start:\u2004"abcd"', simpleGrammar); + grammarParserParses('start:\u2005"abcd"', simpleGrammar); + grammarParserParses('start:\u2006"abcd"', simpleGrammar); + grammarParserParses('start:\u2007"abcd"', simpleGrammar); + grammarParserParses('start:\u2008"abcd"', simpleGrammar); + grammarParserParses('start:\u2009"abcd"', simpleGrammar); + grammarParserParses('start:\u200A"abcd"', simpleGrammar); + grammarParserParses('start:\u202F"abcd"', simpleGrammar); + grammarParserParses('start:\u205F"abcd"', simpleGrammar); + grammarParserParses('start:\u3000"abcd"', simpleGrammar); + }); +} + +})(); diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..e8fec66 --- /dev/null +++ b/test/index.html @@ -0,0 +1,20 @@ + + + + + PEG.js Test Suite + + + + + + + + +

PEG.js Test Suite

+

+
+

+
    + + diff --git a/test/runtime-test.js b/test/runtime-test.js new file mode 100644 index 0000000..263f1d3 --- /dev/null +++ b/test/runtime-test.js @@ -0,0 +1,27 @@ +(function() { + +/* ===== PEG.ArrayUtils ===== */ + +module("PEG.ArrayUtils"); + +test("map", function() { + function square(x) { return x * x; } + + deepEqual(PEG.ArrayUtils.map([], square), []); + deepEqual(PEG.ArrayUtils.map([1, 2, 3], square), [1, 4, 9]); +}); + +/* ===== PEG.StringUtils ===== */ + +module("PEG.StringUtils"); + +test("quote", function() { + strictEqual(PEG.StringUtils.quote(""), '""'); + strictEqual(PEG.StringUtils.quote("abcd"), '"abcd"'); + strictEqual( + PEG.StringUtils.quote("\"\\\r\u2028\u2029\n\"\\\r\u2028\u2029\n"), + '"\\\"\\\\\\r\\u2028\\u2029\\n\\\"\\\\\\r\\u2028\\u2029\\n"' + ); +}); + +})(); diff --git a/vendor/qunit/qunit.css b/vendor/qunit/qunit.css new file mode 100644 index 0000000..5714bf4 --- /dev/null +++ b/vendor/qunit/qunit.css @@ -0,0 +1,119 @@ + +ol#qunit-tests { + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + margin:0; + padding:0; + list-style-position:inside; + + font-size: smaller; +} +ol#qunit-tests li{ + padding:0.4em 0.5em 0.4em 2.5em; + border-bottom:1px solid #fff; + font-size:small; + list-style-position:inside; +} +ol#qunit-tests li ol{ + box-shadow: inset 0px 2px 13px #999; + -moz-box-shadow: inset 0px 2px 13px #999; + -webkit-box-shadow: inset 0px 2px 13px #999; + margin-top:0.5em; + margin-left:0; + padding:0.5em; + background-color:#fff; + border-radius:15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; +} +ol#qunit-tests li li{ + border-bottom:none; + margin:0.5em; + background-color:#fff; + list-style-position: inside; + padding:0.4em 0.5em 0.4em 0.5em; +} + +ol#qunit-tests li li.pass{ + border-left:26px solid #C6E746; + background-color:#fff; + color:#5E740B; + } +ol#qunit-tests li li.fail{ + border-left:26px solid #EE5757; + background-color:#fff; + color:#710909; +} +ol#qunit-tests li.pass{ + background-color:#D2E0E6; + color:#528CE0; +} +ol#qunit-tests li.fail{ + background-color:#EE5757; + color:#000; +} +ol#qunit-tests li strong { + cursor:pointer; +} +h1#qunit-header{ + background-color:#0d3349; + margin:0; + padding:0.5em 0 0.5em 1em; + color:#fff; + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + border-top-right-radius:15px; + border-top-left-radius:15px; + -moz-border-radius-topright:15px; + -moz-border-radius-topleft:15px; + -webkit-border-top-right-radius:15px; + -webkit-border-top-left-radius:15px; + text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; +} +h2#qunit-banner{ + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + height:5px; + margin:0; + padding:0; +} +h2#qunit-banner.qunit-pass{ + background-color:#C6E746; +} +h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { + background-color:#EE5757; +} +#qunit-testrunner-toolbar { + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + padding:0; + /*width:80%;*/ + padding:0em 0 0.5em 2em; + font-size: small; +} +h2#qunit-userAgent { + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + background-color:#2b81af; + margin:0; + padding:0; + color:#fff; + font-size: small; + padding:0.5em 0 0.5em 2.5em; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} +p#qunit-testresult{ + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + margin:0; + font-size: small; + color:#2b81af; + border-bottom-right-radius:15px; + border-bottom-left-radius:15px; + -moz-border-radius-bottomright:15px; + -moz-border-radius-bottomleft:15px; + -webkit-border-bottom-right-radius:15px; + -webkit-border-bottom-left-radius:15px; + background-color:#D2E0E6; + padding:0.5em 0.5em 0.5em 2.5em; +} +strong b.fail{ + color:#710909; + } +strong b.pass{ + color:#5E740B; + } diff --git a/vendor/qunit/qunit.js b/vendor/qunit/qunit.js new file mode 100644 index 0000000..3eda4f9 --- /dev/null +++ b/vendor/qunit/qunit.js @@ -0,0 +1,1062 @@ +/* + * QUnit - A JavaScript Unit Testing Framework + * + * http://docs.jquery.com/QUnit + * + * Copyright (c) 2009 John Resig, Jörn Zaefferer + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + */ + +(function(window) { + +var QUnit = { + + // Initialize the configuration options + init: function() { + config = { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: +new Date, + blocking: false, + autorun: false, + assertions: [], + filters: [], + queue: [] + }; + + var tests = id("qunit-tests"), + banner = id("qunit-banner"), + result = id("qunit-testresult"); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + }, + + // call on start of module test to prepend name to all tests + module: function(name, testEnvironment) { + config.currentModule = name; + + synchronize(function() { + if ( config.currentModule ) { + QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); + } + + config.currentModule = name; + config.moduleTestEnvironment = testEnvironment; + config.moduleStats = { all: 0, bad: 0 }; + + QUnit.moduleStart( name, testEnvironment ); + }); + }, + + asyncTest: function(testName, expected, callback) { + if ( arguments.length === 2 ) { + callback = expected; + expected = 0; + } + + QUnit.test(testName, expected, callback, true); + }, + + test: function(testName, expected, callback, async) { + var name = testName, testEnvironment, testEnvironmentArg; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + // is 2nd argument a testEnvironment? + if ( expected && typeof expected === 'object') { + testEnvironmentArg = expected; + expected = null; + } + + if ( config.currentModule ) { + name = config.currentModule + " module: " + name; + } + + if ( !validTest(name) ) { + return; + } + + synchronize(function() { + QUnit.testStart( testName ); + + testEnvironment = extend({ + setup: function() {}, + teardown: function() {} + }, config.moduleTestEnvironment); + if (testEnvironmentArg) { + extend(testEnvironment,testEnvironmentArg); + } + + // allow utility functions to access the current test environment + QUnit.current_testEnvironment = testEnvironment; + + config.assertions = []; + config.expected = expected; + + try { + if ( !config.pollution ) { + saveGlobal(); + } + + testEnvironment.setup.call(testEnvironment); + } catch(e) { + QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); + } + + if ( async ) { + QUnit.stop(); + } + + try { + callback.call(testEnvironment); + } catch(e) { + fail("Test " + name + " died, exception and test follows", e, callback); + QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); + // else next test will carry the responsibility + saveGlobal(); + + // Restart the tests if they're blocking + if ( config.blocking ) { + start(); + } + } + }); + + synchronize(function() { + try { + checkPollution(); + testEnvironment.teardown.call(testEnvironment); + } catch(e) { + QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); + } + + try { + QUnit.reset(); + } catch(e) { + fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); + } + + if ( config.expected && config.expected != config.assertions.length ) { + QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); + } + + var good = 0, bad = 0, + tests = id("qunit-tests"); + + config.stats.all += config.assertions.length; + config.moduleStats.all += config.assertions.length; + + if ( tests ) { + var ol = document.createElement("ol"); + ol.style.display = "none"; + + for ( var i = 0; i < config.assertions.length; i++ ) { + var assertion = config.assertions[i]; + + var li = document.createElement("li"); + li.className = assertion.result ? "pass" : "fail"; + li.appendChild(document.createTextNode(assertion.message || "(no message)")); + ol.appendChild( li ); + + if ( assertion.result ) { + good++; + } else { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + + var b = document.createElement("strong"); + b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; + + addEvent(b, "click", function() { + var next = b.nextSibling, display = next.style.display; + next.style.display = display === "none" ? "block" : "none"; + }); + + addEvent(b, "dblclick", function(e) { + var target = e && e.target ? e.target : window.event.srcElement; + if ( target.nodeName.toLowerCase() === "strong" ) { + var text = "", node = target.firstChild; + + while ( node.nodeType === 3 ) { + text += node.nodeValue; + node = node.nextSibling; + } + + text = text.replace(/(^\s*|\s*$)/g, ""); + + if ( window.location ) { + window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text); + } + } + }); + + var li = document.createElement("li"); + li.className = bad ? "fail" : "pass"; + li.appendChild( b ); + li.appendChild( ol ); + tests.appendChild( li ); + + if ( bad ) { + var toolbar = id("qunit-testrunner-toolbar"); + if ( toolbar ) { + toolbar.style.display = "block"; + id("qunit-filter-pass").disabled = null; + id("qunit-filter-missing").disabled = null; + } + } + + } else { + for ( var i = 0; i < config.assertions.length; i++ ) { + if ( !config.assertions[i].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + } + + QUnit.testDone( testName, bad, config.assertions.length ); + + if ( !window.setTimeout && !config.queue.length ) { + done(); + } + }); + + if ( window.setTimeout && !config.doneTimer ) { + config.doneTimer = window.setTimeout(function(){ + if ( !config.queue.length ) { + done(); + } else { + synchronize( done ); + } + }, 13); + } + }, + + /** + * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. + */ + expect: function(asserts) { + config.expected = asserts; + }, + + /** + * Asserts true. + * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); + */ + ok: function(a, msg) { + QUnit.log(a, msg); + + config.assertions.push({ + result: !!a, + message: msg + }); + }, + + /** + * Checks that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * + * Prefered to ok( actual == expected, message ) + * + * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); + * + * @param Object actual + * @param Object expected + * @param String message (optional) + */ + equal: function(actual, expected, message) { + push(expected == actual, actual, expected, message); + }, + + notEqual: function(actual, expected, message) { + push(expected != actual, actual, expected, message); + }, + + deepEqual: function(a, b, message) { + push(QUnit.equiv(a, b), a, b, message); + }, + + notDeepEqual: function(a, b, message) { + push(!QUnit.equiv(a, b), a, b, message); + }, + + strictEqual: function(actual, expected, message) { + push(expected === actual, actual, expected, message); + }, + + notStrictEqual: function(actual, expected, message) { + push(expected !== actual, actual, expected, message); + }, + + start: function() { + // A slight delay, to avoid any current callbacks + if ( window.setTimeout ) { + window.setTimeout(function() { + if ( config.timeout ) { + clearTimeout(config.timeout); + } + + config.blocking = false; + process(); + }, 13); + } else { + config.blocking = false; + process(); + } + }, + + stop: function(timeout) { + config.blocking = true; + + if ( timeout && window.setTimeout ) { + config.timeout = window.setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + QUnit.start(); + }, timeout); + } + }, + + /** + * Resets the test setup. Useful for tests that modify the DOM. + */ + reset: function() { + if ( window.jQuery ) { + jQuery("#main").html( config.fixture ); + jQuery.event.global = {}; + jQuery.ajaxSettings = extend({}, config.ajaxSettings); + } + }, + + /** + * Trigger an event on an element. + * + * @example triggerEvent( document.body, "click" ); + * + * @param DOMElement elem + * @param String type + */ + triggerEvent: function( elem, type, event ) { + if ( document.createEvent ) { + event = document.createEvent("MouseEvents"); + event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + elem.dispatchEvent( event ); + + } else if ( elem.fireEvent ) { + elem.fireEvent("on"+type); + } + }, + + // Safe object type checking + is: function( type, obj ) { + return Object.prototype.toString.call( obj ) === "[object "+ type +"]"; + }, + + // Logging callbacks + done: function(failures, total) {}, + log: function(result, message) {}, + testStart: function(name) {}, + testDone: function(name, failures, total) {}, + moduleStart: function(name, testEnvironment) {}, + moduleDone: function(name, failures, total) {} +}; + +// Backwards compatibility, deprecated +QUnit.equals = QUnit.equal; +QUnit.same = QUnit.deepEqual; + +// Maintain internal state +var config = { + // The queue of tests to run + queue: [], + + // block until document ready + blocking: true +}; + +// Load paramaters +(function() { + var location = window.location || { search: "", protocol: "file:" }, + GETParams = location.search.slice(1).split('&'); + + for ( var i = 0; i < GETParams.length; i++ ) { + GETParams[i] = decodeURIComponent( GETParams[i] ); + if ( GETParams[i] === "noglobals" ) { + GETParams.splice( i, 1 ); + i--; + config.noglobals = true; + } else if ( GETParams[i].search('=') > -1 ) { + GETParams.splice( i, 1 ); + i--; + } + } + + // restrict modules/tests by get parameters + config.filters = GETParams; + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = !!(location.protocol === 'file:'); +})(); + +// Expose the API as global variables, unless an 'exports' +// object exists, in that case we assume we're in CommonJS +if ( typeof exports === "undefined" || typeof require === "undefined" ) { + extend(window, QUnit); + window.QUnit = QUnit; +} else { + extend(exports, QUnit); + exports.QUnit = QUnit; +} + +if ( typeof document === "undefined" || document.readyState === "complete" ) { + config.autorun = true; +} + +addEvent(window, "load", function() { + // Initialize the config, saving the execution queue + var oldconfig = extend({}, config); + QUnit.init(); + extend(config, oldconfig); + + config.blocking = false; + + var userAgent = id("qunit-userAgent"); + if ( userAgent ) { + userAgent.innerHTML = navigator.userAgent; + } + + var toolbar = id("qunit-testrunner-toolbar"); + if ( toolbar ) { + toolbar.style.display = "none"; + + var filter = document.createElement("input"); + filter.type = "checkbox"; + filter.id = "qunit-filter-pass"; + filter.disabled = true; + addEvent( filter, "click", function() { + var li = document.getElementsByTagName("li"); + for ( var i = 0; i < li.length; i++ ) { + if ( li[i].className.indexOf("pass") > -1 ) { + li[i].style.display = filter.checked ? "none" : ""; + } + } + }); + toolbar.appendChild( filter ); + + var label = document.createElement("label"); + label.setAttribute("for", "qunit-filter-pass"); + label.innerHTML = "Hide passed tests"; + toolbar.appendChild( label ); + + var missing = document.createElement("input"); + missing.type = "checkbox"; + missing.id = "qunit-filter-missing"; + missing.disabled = true; + addEvent( missing, "click", function() { + var li = document.getElementsByTagName("li"); + for ( var i = 0; i < li.length; i++ ) { + if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { + li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; + } + } + }); + toolbar.appendChild( missing ); + + label = document.createElement("label"); + label.setAttribute("for", "qunit-filter-missing"); + label.innerHTML = "Hide missing tests (untested code is broken code)"; + toolbar.appendChild( label ); + } + + var main = id('main'); + if ( main ) { + config.fixture = main.innerHTML; + } + + if ( window.jQuery ) { + config.ajaxSettings = window.jQuery.ajaxSettings; + } + + QUnit.start(); +}); + +function done() { + if ( config.doneTimer && window.clearTimeout ) { + window.clearTimeout( config.doneTimer ); + config.doneTimer = null; + } + + if ( config.queue.length ) { + config.doneTimer = window.setTimeout(function(){ + if ( !config.queue.length ) { + done(); + } else { + synchronize( done ); + } + }, 13); + + return; + } + + config.autorun = true; + + // Log the last module results + if ( config.currentModule ) { + QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); + } + + var banner = id("qunit-banner"), + tests = id("qunit-tests"), + html = ['Tests completed in ', + +new Date - config.started, ' milliseconds.
    ', + '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); + + if ( banner ) { + banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); + } + + if ( tests ) { + var result = id("qunit-testresult"); + + if ( !result ) { + result = document.createElement("p"); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests.nextSibling ); + } + + result.innerHTML = html; + } + + QUnit.done( config.stats.bad, config.stats.all ); +} + +function validTest( name ) { + var i = config.filters.length, + run = false; + + if ( !i ) { + return true; + } + + while ( i-- ) { + var filter = config.filters[i], + not = filter.charAt(0) == '!'; + + if ( not ) { + filter = filter.slice(1); + } + + if ( name.indexOf(filter) !== -1 ) { + return !not; + } + + if ( not ) { + run = true; + } + } + + return run; +} + +function push(result, actual, expected, message) { + message = message || (result ? "okay" : "failed"); + QUnit.ok( result, result ? message + ": " + expected : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) ); +} + +function synchronize( callback ) { + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process(); + } +} + +function process() { + while ( config.queue.length && !config.blocking ) { + config.queue.shift()(); + } +} + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + config.pollution.push( key ); + } + } +} + +function checkPollution( name ) { + var old = config.pollution; + saveGlobal(); + + var newGlobals = diff( old, config.pollution ); + if ( newGlobals.length > 0 ) { + ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); + config.expected++; + } + + var deletedGlobals = diff( config.pollution, old ); + if ( deletedGlobals.length > 0 ) { + ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); + config.expected++; + } +} + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var result = a.slice(); + for ( var i = 0; i < result.length; i++ ) { + for ( var j = 0; j < b.length; j++ ) { + if ( result[i] === b[j] ) { + result.splice(i, 1); + i--; + break; + } + } + } + return result; +} + +function fail(message, exception, callback) { + if ( typeof console !== "undefined" && console.error && console.warn ) { + console.error(message); + console.error(exception); + console.warn(callback.toString()); + + } else if ( window.opera && opera.postError ) { + opera.postError(message, exception, callback.toString); + } +} + +function extend(a, b) { + for ( var prop in b ) { + a[prop] = b[prop]; + } + + return a; +} + +function addEvent(elem, type, fn) { + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, fn ); + } else { + fn(); + } +} + +function id(name) { + return !!(typeof document !== "undefined" && document && document.getElementById) && + document.getElementById( name ); +} + +// Test for equality any JavaScript type. +// Discussions and reference: http://philrathe.com/articles/equiv +// Test suites: http://philrathe.com/tests/equiv +// Author: Philippe Rathé +QUnit.equiv = function () { + + var innerEquiv; // the real equiv function + var callers = []; // stack to decide between skip/abort functions + var parents = []; // stack to avoiding loops from circular referencing + + + // Determine what is o. + function hoozit(o) { + if (QUnit.is("String", o)) { + return "string"; + + } else if (QUnit.is("Boolean", o)) { + return "boolean"; + + } else if (QUnit.is("Number", o)) { + + if (isNaN(o)) { + return "nan"; + } else { + return "number"; + } + + } else if (typeof o === "undefined") { + return "undefined"; + + // consider: typeof null === object + } else if (o === null) { + return "null"; + + // consider: typeof [] === object + } else if (QUnit.is( "Array", o)) { + return "array"; + + // consider: typeof new Date() === object + } else if (QUnit.is( "Date", o)) { + return "date"; + + // consider: /./ instanceof Object; + // /./ instanceof RegExp; + // typeof /./ === "function"; // => false in IE and Opera, + // true in FF and Safari + } else if (QUnit.is( "RegExp", o)) { + return "regexp"; + + } else if (typeof o === "object") { + return "object"; + + } else if (QUnit.is( "Function", o)) { + return "function"; + } else { + return undefined; + } + } + + // Call the o related callback with the given arguments. + function bindCallbacks(o, callbacks, args) { + var prop = hoozit(o); + if (prop) { + if (hoozit(callbacks[prop]) === "function") { + return callbacks[prop].apply(callbacks, args); + } else { + return callbacks[prop]; // or undefined + } + } + } + + var callbacks = function () { + + // for string, boolean, number and null + function useStrictEquality(b, a) { + if (b instanceof a.constructor || a instanceof b.constructor) { + // to catch short annotaion VS 'new' annotation of a declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function (b) { + return isNaN(b); + }, + + "date": function (b, a) { + return hoozit(b) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function (b, a) { + return hoozit(b) === "regexp" && + a.source === b.source && // the regex itself + a.global === b.global && // and its modifers (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function () { + var caller = callers[callers.length - 1]; + return caller !== Object && + typeof caller !== "undefined"; + }, + + "array": function (b, a) { + var i, j, loop; + var len; + + // b could be an object literal here + if ( ! (hoozit(b) === "array")) { + return false; + } + + len = a.length; + if (len !== b.length) { // safe and faster + return false; + } + + //track reference to avoid circular references + parents.push(a); + for (i = 0; i < len; i++) { + loop = false; + for(j=0;j' : '\n' : this.HTML ? ' ' : ' '; + }, + indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing + if ( !this.multiline ) + return ''; + var chr = this.indentChar; + if ( this.HTML ) + chr = chr.replace(/\t/g,' ').replace(/ /g,' '); + return Array( this._depth_ + (extra||0) ).join(chr); + }, + up:function( a ) { + this._depth_ += a || 1; + }, + down:function( a ) { + this._depth_ -= a || 1; + }, + setParser:function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote:quote, + literal:literal, + join:join, + // + _depth_: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers:{ + window: '[Window]', + document: '[Document]', + error:'[ERROR]', //when no parser is found, shouldn't happen + unknown: '[Unknown]', + 'null':'null', + undefined:'undefined', + 'function':function( fn ) { + var ret = 'function', + name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE + if ( name ) + ret += ' ' + name; + ret += '('; + + ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); + return join( ret, this.parse(fn,'functionCode'), '}' ); + }, + array: array, + nodelist: array, + arguments: array, + object:function( map ) { + var ret = [ ]; + this.up(); + for ( var key in map ) + ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); + this.down(); + return join( '{', ret, '}' ); + }, + node:function( node ) { + var open = this.HTML ? '<' : '<', + close = this.HTML ? '>' : '>'; + + var tag = node.nodeName.toLowerCase(), + ret = open + tag; + + for ( var a in this.DOMAttrs ) { + var val = node[this.DOMAttrs[a]]; + if ( val ) + ret += ' ' + a + '=' + this.parse( val, 'attribute' ); + } + return ret + close + open + '/' + tag + close; + }, + functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function + var l = fn.length; + if ( !l ) return ''; + + var args = Array(l); + while ( l-- ) + args[l] = String.fromCharCode(97+l);//97 is 'a' + return ' ' + args.join(', ') + ' '; + }, + key:quote, //object calls it internally, the key part of an item in a map + functionCode:'[code]', //function calls it internally, it's the content of the function + attribute:quote, //node calls it internally, it's an html attribute value + string:quote, + date:quote, + regexp:literal, //regex + number:literal, + 'boolean':literal + }, + DOMAttrs:{//attributes to dump from nodes, name=>realName + id:'id', + name:'name', + 'class':'className' + }, + HTML:true,//if true, entities are escaped ( <, >, \t, space and \n ) + indentChar:' ',//indentation unit + multiline:true //if true, items in a collection, are separated by a \n, else just a space. + }; + + return jsDump; +})(); + +})(this); diff --git a/vendor/rhino/js.jar b/vendor/rhino/js.jar new file mode 100644 index 0000000..2369f99 Binary files /dev/null and b/vendor/rhino/js.jar differ