208cc33930
Before this commit, generated parser were able to start parsing from any rule. This was nice, but it made rule code inlining impossible. Since this commit, the list of allowed start rules has to be specified explicitly using the |allowedStartRules| option of the |PEG.buildParser| method (or the --allowed-start-rule option on the command-line). These rules will be excluded from inlining when it's implemented.
1049 lines
35 KiB
JavaScript
1049 lines
35 KiB
JavaScript
describe("generated parser", function() {
|
|
function vary(names, block) {
|
|
var values = {
|
|
trackLineAndColumn: [false, true],
|
|
cache: [false, true]
|
|
};
|
|
|
|
function varyStep(names, options) {
|
|
var clonedOptions = {}, key, name, i;
|
|
|
|
if (names.length === 0) {
|
|
/*
|
|
* We have to clone the options so that the block can save them safely
|
|
* (e.g. by capturing in a closure) without the risk that they will be
|
|
* changed later.
|
|
*/
|
|
for (key in options) {
|
|
clonedOptions[key] = options[key];
|
|
}
|
|
|
|
describe(
|
|
"with options " + jasmine.pp(clonedOptions),
|
|
function() { block(clonedOptions); }
|
|
);
|
|
} else {
|
|
name = names[0];
|
|
for (i = 0; i < values[name].length; i++) {
|
|
options[name] = values[name][i];
|
|
varyStep(names.slice(1), options);
|
|
}
|
|
}
|
|
}
|
|
|
|
varyStep(names, {});
|
|
}
|
|
|
|
function varyAll(block) {
|
|
vary(["cache", "trackLineAndColumn"], block);
|
|
}
|
|
|
|
beforeEach(function() {
|
|
this.addMatchers({
|
|
toParse: function(input) {
|
|
var options = arguments.length > 2 ? arguments[1] : {},
|
|
expected = arguments[arguments.length - 1],
|
|
result;
|
|
|
|
try {
|
|
result = this.actual.parse(input, options);
|
|
|
|
this.message = function() {
|
|
return "Expected " + jasmine.pp(input) + " "
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ (this.isNot ? "not " : "")
|
|
+ "to parse as " + jasmine.pp(expected) + ", "
|
|
+ "but it parsed as " + jasmine.pp(result) + ".";
|
|
};
|
|
|
|
return this.env.equals_(result, expected);
|
|
} catch (e) {
|
|
this.message = function() {
|
|
return "Expected " + jasmine.pp(input) + " "
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ "to parse as " + jasmine.pp(expected) + ", "
|
|
+ "but it failed to parse with message "
|
|
+ jasmine.pp(e.message) + ".";
|
|
};
|
|
|
|
return false;
|
|
}
|
|
},
|
|
|
|
toFailToParse: function(input) {
|
|
var options = arguments.length > 2 ? arguments[1] : {},
|
|
details = arguments.length > 1
|
|
? arguments[arguments.length - 1]
|
|
: undefined;
|
|
|
|
/*
|
|
* Extracted into a function just to silence JSHint complaining about
|
|
* creating functions in a loop.
|
|
*/
|
|
function buildKeyMessage(key, value) {
|
|
return function() {
|
|
return "Expected " + jasmine.pp(input) + " "
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ "to fail to parse"
|
|
+ (details ? " with details " + jasmine.pp(details) : "") + ", "
|
|
+ "but " + jasmine.pp(key) + " "
|
|
+ "is " + jasmine.pp(value) + ".";
|
|
};
|
|
}
|
|
|
|
var result, key;
|
|
|
|
try {
|
|
result = this.actual.parse(input, options);
|
|
|
|
this.message = function() {
|
|
return "Expected " + jasmine.pp(input) + " "
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ "to fail to parse"
|
|
+ (details ? " with details " + jasmine.pp(details) : "") + ", "
|
|
+ "but it parsed as " + jasmine.pp(result) + ".";
|
|
};
|
|
|
|
return false;
|
|
} catch (e) {
|
|
if (this.isNot) {
|
|
this.message = function() {
|
|
return "Expected " + jasmine.pp(input)
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ "to parse, "
|
|
+ "but it failed with message "
|
|
+ jasmine.pp(e.message) + ".";
|
|
};
|
|
} else {
|
|
if (details) {
|
|
for (key in details) {
|
|
if (!this.env.equals_(e[key], details[key])) {
|
|
this.message = buildKeyMessage(key, e[key]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("parse", function() {
|
|
var parser = PEG.buildParser([
|
|
'a = "x" { return "a"; }',
|
|
'b = "x" { return "b"; }',
|
|
'c = "x" { return "c"; }'
|
|
].join("\n"), { allowedStartRules: ["b", "c"] });
|
|
|
|
describe("start rule", function() {
|
|
describe("without the |startRule| option", function() {
|
|
it("uses the first allowed rule", function() {
|
|
expect(parser).toParse("x", "b");
|
|
});
|
|
});
|
|
|
|
describe("when the |startRule| option specifies allowed rule", function() {
|
|
it("uses the specified rule", function() {
|
|
expect(parser).toParse("x", { startRule: "b" }, "b");
|
|
expect(parser).toParse("x", { startRule: "c" }, "c");
|
|
});
|
|
});
|
|
|
|
describe("when the |startRule| option specifies disallowed rule", function() {
|
|
it("throws exception", function() {
|
|
expect(parser).toFailToParse("x", { startRule: "a" }, {
|
|
message: "Can't start parsing from rule \"a\"."
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
varyAll(function(options) {
|
|
describe("initializer code", function() {
|
|
it("runs before the parsing begins", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var result = 42; }',
|
|
'start = "a" { return result; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", 42);
|
|
});
|
|
|
|
it("can use options passed to the parser", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var result = options; }',
|
|
'start = "a" { return result; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", { a: 42 }, { a: 42 });
|
|
});
|
|
});
|
|
|
|
describe("rule matching", function() {
|
|
var grammar = [
|
|
'{ var n = 0; }',
|
|
'start = (a "b") / (a "c") { return n; }',
|
|
'a = "a" { n++; }'
|
|
].join("\n");
|
|
|
|
if (options.cache) {
|
|
it("caches rule match results", function() {
|
|
var parser = PEG.buildParser(grammar, options);
|
|
|
|
expect(parser).toParse("ac", 1);
|
|
});
|
|
} else {
|
|
it("does not cache rule match results", function() {
|
|
var parser = PEG.buildParser(grammar, options);
|
|
|
|
expect(parser).toParse("ac", 2);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("named matching", function() {
|
|
var parser = PEG.buildParser('start "start" = "a"');
|
|
|
|
it("delegates to the expression", function() {
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
|
|
it("overwrites expected string on failure", function() {
|
|
expect(parser).toFailToParse("b", { expected: ["start"] });
|
|
});
|
|
});
|
|
|
|
describe("choice matching", function() {
|
|
it("matches correctly", function() {
|
|
var parser = PEG.buildParser('start = "a" / "b" / "c"', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toParse("b", "b");
|
|
expect(parser).toParse("c", "c");
|
|
expect(parser).toFailToParse("d");
|
|
});
|
|
});
|
|
|
|
describe("action code", function() {
|
|
it("tranforms the expression result by returnung a non-|null| value", function() {
|
|
var parser = PEG.buildParser('start = "a" { return 42; }', options);
|
|
|
|
expect(parser).toParse("a", 42);
|
|
});
|
|
|
|
it("causes match failure by returning |null|", function() {
|
|
var parser = PEG.buildParser('start = "a" { return null; }', options);
|
|
|
|
expect(parser).toFailToParse("a");
|
|
});
|
|
|
|
it("is not called when the expression does not match", function() {
|
|
var parser = PEG.buildParser(
|
|
'start = "a" { throw "Boom!"; } / "b"',
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("b", "b");
|
|
});
|
|
|
|
it("can use label variables", function() {
|
|
var parser = PEG.buildParser('start = a:"a" { return a; }', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
|
|
it("can use the |offset| variable to get the current parse position", function() {
|
|
var parser = PEG.buildParser(
|
|
'start = "a" ("b" { return offset; })',
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("ab", ["a", 1]);
|
|
});
|
|
|
|
if (options.trackLineAndColumn) {
|
|
it("can use the |line| and |column| variables to get the current line and column", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var result; }',
|
|
'start = line (nl+ line)* { return result; }',
|
|
'line = thing (" "+ thing)*',
|
|
'thing = digit / mark',
|
|
'digit = [0-9]',
|
|
'mark = "x" { result = [line, column]; }',
|
|
'nl = ("\\r" / "\\n" / "\\u2028" / "\\u2029")'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("1\n2\n\n3\n\n\n4 5 x", [7, 5]);
|
|
|
|
/* Non-Unix newlines */
|
|
expect(parser).toParse("1\rx", [2, 1]); // Old Mac
|
|
expect(parser).toParse("1\r\nx", [2, 1]); // Windows
|
|
expect(parser).toParse("1\n\rx", [3, 1]); // mismatched
|
|
|
|
/* Strange newlines */
|
|
expect(parser).toParse("1\u2028x", [2, 1]); // line separator
|
|
expect(parser).toParse("1\u2029x", [2, 1]); // paragraph separator
|
|
});
|
|
}
|
|
|
|
it("can use variables defined in the initializer", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var v = 42 }',
|
|
'start = "a" { return v; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", 42);
|
|
});
|
|
|
|
it("can use functions defined in the initializer", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ function f() { return 42; } }',
|
|
'start = "a" { return f(); }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", 42);
|
|
});
|
|
|
|
it("can use options passed to the parser", function() {
|
|
var parser = PEG.buildParser(
|
|
'start = "a" { return options; }',
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a", { a: 42 }, { a: 42 });
|
|
});
|
|
|
|
it("does not advance position when the expression matches but the action returns |null|", function() {
|
|
var parser = PEG.buildParser(
|
|
'start = "a" { return null; } / "a"',
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
});
|
|
|
|
describe("sequence matching", function() {
|
|
it("matches empty sequence correctly", function() {
|
|
var parser = PEG.buildParser('start = ', options);
|
|
|
|
expect(parser).toParse("", []);
|
|
});
|
|
|
|
it("matches non-empty sequence correctly", function() {
|
|
var parser = PEG.buildParser('start = "a" "b" "c"', options);
|
|
|
|
expect(parser).toParse("abc", ["a", "b", "c"]);
|
|
});
|
|
|
|
it("does not advance position on failure", function() {
|
|
var parser = PEG.buildParser('start = "a" "b" / "a"', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
});
|
|
|
|
describe("labeled matching", function() {
|
|
it("delegates to the expression", function() {
|
|
var parser = PEG.buildParser('start = a:"a"', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
});
|
|
|
|
describe("simple and matching", function() {
|
|
it("matches correctly", function() {
|
|
var parser = PEG.buildParser('start = &"a" "a"', options);
|
|
|
|
expect(parser).toParse("a", ["", "a"]);
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
|
|
it("does not advance position on success", function() {
|
|
var parser = PEG.buildParser('start = &"a" "a"', options);
|
|
|
|
expect(parser).toParse("a", ["", "a"]);
|
|
});
|
|
|
|
it("does not influence expected strings on failure", function() {
|
|
var parser = PEG.buildParser('start = &"a"', options);
|
|
|
|
expect(parser).toFailToParse("b", { expected: [] });
|
|
});
|
|
});
|
|
|
|
describe("simple not matching", function() {
|
|
it("matches correctly", function() {
|
|
var parser = PEG.buildParser('start = !"a" "b"', options);
|
|
|
|
expect(parser).toParse("b", ["", "b"]);
|
|
expect(parser).toFailToParse("a");
|
|
});
|
|
|
|
it("does not advance position on failure", function() {
|
|
var parser = PEG.buildParser('start = !"a" / "a"', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
|
|
it("does not influence expected strings on failure", function() {
|
|
var parser = PEG.buildParser('start = !"a"', options);
|
|
|
|
expect(parser).toFailToParse("a", { expected: [] });
|
|
});
|
|
});
|
|
|
|
describe("semantic and code", function() {
|
|
it("causes successful match by returning |true|", function() {
|
|
var parser = PEG.buildParser('start = &{ return true; }', options);
|
|
|
|
expect(parser).toParse("", "");
|
|
});
|
|
|
|
it("causes match failure by returning |false|", function() {
|
|
var parser = PEG.buildParser('start = &{ return false; }', options);
|
|
|
|
expect(parser).toFailToParse("");
|
|
});
|
|
|
|
it("can use label variables", function() {
|
|
var parser = PEG.buildParser(
|
|
'start = a:"a" &{ return a === "a"; }',
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a", ["a", ""]);
|
|
});
|
|
|
|
it("can use the |offset| variable to get the current parse position", function() {
|
|
var parser = PEG.buildParser(
|
|
'start = "a" &{ return offset === 1; }',
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a", ["a", ""]);
|
|
});
|
|
|
|
if (options.trackLineAndColumn) {
|
|
it("can use the |line| and |column| variables to get the current line and column", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var result; }',
|
|
'start = line (nl+ line)* { return result; }',
|
|
'line = thing (" "+ thing)*',
|
|
'thing = digit / mark',
|
|
'digit = [0-9]',
|
|
'mark = &{ result = [line, column]; return true; } "x"',
|
|
'nl = ("\\r" / "\\n" / "\\u2028" / "\\u2029")'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("1\n2\n\n3\n\n\n4 5 x", [7, 5]);
|
|
|
|
/* Non-Unix newlines */
|
|
expect(parser).toParse("1\rx", [2, 1]); // Old Mac
|
|
expect(parser).toParse("1\r\nx", [2, 1]); // Windows
|
|
expect(parser).toParse("1\n\rx", [3, 1]); // mismatched
|
|
|
|
/* Strange newlines */
|
|
expect(parser).toParse("1\u2028x", [2, 1]); // line separator
|
|
expect(parser).toParse("1\u2029x", [2, 1]); // paragraph separator
|
|
});
|
|
}
|
|
|
|
it("can use variables defined in the initializer", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var v = 42 }',
|
|
'start = "a" &{ return v === 42; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", ["a", ""]);
|
|
});
|
|
|
|
it("can use functions defined in the initializer", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ function f() { return 42; } }',
|
|
'start = "a" &{ return f() === 42; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", ["a", ""]);
|
|
});
|
|
|
|
it("can use options passed to the parser", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var result; }',
|
|
'start = "a" &{ result = options; return true; } { return result; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", { a: 42 }, { a: 42 });
|
|
});
|
|
});
|
|
|
|
describe("semantic not code", function() {
|
|
it("causes successful match by returning |false|", function() {
|
|
var parser = PEG.buildParser('start = !{ return false; }', options);
|
|
|
|
expect(parser).toParse("", "");
|
|
});
|
|
|
|
it("causes match failure by returning |true|", function() {
|
|
var parser = PEG.buildParser('start = !{ return true; }', options);
|
|
|
|
expect(parser).toFailToParse();
|
|
});
|
|
|
|
it("can use label variables", function() {
|
|
var parser = PEG.buildParser(
|
|
'start = a:"a" !{ return a !== "a"; }',
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a", ["a", ""]);
|
|
});
|
|
|
|
it("can use the |offset| variable to get the current parse position", function() {
|
|
var parser = PEG.buildParser(
|
|
'start = "a" !{ return offset !== 1; }',
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a", ["a", ""]);
|
|
});
|
|
|
|
if (options.trackLineAndColumn) {
|
|
it("can use the |line| and |column| variables to get the current line and column", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var result; }',
|
|
'start = line (nl+ line)* { return result; }',
|
|
'line = thing (" "+ thing)*',
|
|
'thing = digit / mark',
|
|
'digit = [0-9]',
|
|
'mark = !{ result = [line, column]; return false; } "x"',
|
|
'nl = ("\\r" / "\\n" / "\\u2028" / "\\u2029")'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("1\n2\n\n3\n\n\n4 5 x", [7, 5]);
|
|
|
|
/* Non-Unix newlines */
|
|
expect(parser).toParse("1\rx", [2, 1]); // Old Mac
|
|
expect(parser).toParse("1\r\nx", [2, 1]); // Windows
|
|
expect(parser).toParse("1\n\rx", [3, 1]); // mismatched
|
|
|
|
/* Strange newlines */
|
|
expect(parser).toParse("1\u2028x", [2, 1]); // line separator
|
|
expect(parser).toParse("1\u2029x", [2, 1]); // paragraph separator
|
|
});
|
|
}
|
|
|
|
it("can use variables defined in the initializer", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var v = 42 }',
|
|
'start = "a" !{ return v !== 42; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", ["a", ""]);
|
|
});
|
|
|
|
it("can use functions defined in the initializer", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ function f() { return 42; } }',
|
|
'start = "a" !{ return f() !== 42; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", ["a", ""]);
|
|
});
|
|
|
|
it("can use options passed to the parser", function() {
|
|
var parser = PEG.buildParser([
|
|
'{ var result; }',
|
|
'start = "a" !{ result = options; return false; } { return result; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", { a: 42 }, { a: 42 });
|
|
});
|
|
});
|
|
|
|
describe("optional matching", function() {
|
|
it("matches correctly", function() {
|
|
var parser = PEG.buildParser('start = "a"?', options);
|
|
|
|
expect(parser).toParse("", "");
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
});
|
|
|
|
describe("zero or more matching", function() {
|
|
it("matches correctly", function() {
|
|
var parser = PEG.buildParser('start = "a"*', options);
|
|
|
|
expect(parser).toParse("", []);
|
|
expect(parser).toParse("a", ["a"]);
|
|
expect(parser).toParse("aaa", ["a", "a", "a"]);
|
|
});
|
|
});
|
|
|
|
describe("one or more matching", function() {
|
|
it("matches correctly", function() {
|
|
var parser = PEG.buildParser('start = "a"+', options);
|
|
|
|
expect(parser).toFailToParse("");
|
|
expect(parser).toParse("a", ["a"]);
|
|
expect(parser).toParse("aaa", ["a", "a", "a"]);
|
|
});
|
|
});
|
|
|
|
describe("rule reference matching", function() {
|
|
it("follows rule references", function() {
|
|
var parser = PEG.buildParser([
|
|
'start = static / dynamic',
|
|
'static = "C" / "C++" / "Java" / "C#"',
|
|
'dynamic = "Ruby" / "Python" / "JavaScript"'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("Java", "Java");
|
|
expect(parser).toParse("Python", "Python");
|
|
});
|
|
});
|
|
|
|
describe("literal matching", function() {
|
|
it("matches empty literal correctly", function() {
|
|
var parser = PEG.buildParser('start = ""', options);
|
|
|
|
expect(parser).toParse("", "");
|
|
});
|
|
|
|
it("matches one-character literal correctly", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
|
|
it("matches multiple-character literal correctly", function() {
|
|
var parser = PEG.buildParser('start = "abcd"', options);
|
|
|
|
expect(parser).toParse("abcd", "abcd");
|
|
expect(parser).toFailToParse("ebcd");
|
|
expect(parser).toFailToParse("afcd");
|
|
expect(parser).toFailToParse("abgd");
|
|
expect(parser).toFailToParse("abch");
|
|
});
|
|
|
|
it("is case sensitive without the \"i\" flag", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toFailToParse("A");
|
|
});
|
|
|
|
it("is case insensitive with the \"i\" flag", function() {
|
|
var parser = PEG.buildParser('start = "a"i', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toParse("A", "A");
|
|
});
|
|
|
|
it("advances position on success", function() {
|
|
var parser = PEG.buildParser('start = "a" .', options);
|
|
|
|
expect(parser).toParse("ab", ["a", "b"]);
|
|
});
|
|
|
|
it("sets expected string correctly on failure", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("b", { expected: ['"a"'] });
|
|
});
|
|
});
|
|
|
|
describe("class matching", function() {
|
|
it("matches empty class correctly", function() {
|
|
var parser = PEG.buildParser('start = []', options);
|
|
|
|
expect(parser).toFailToParse("a");
|
|
});
|
|
|
|
it("matches class with a character list correctly", function() {
|
|
var parser = PEG.buildParser('start = [abc]', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toParse("b", "b");
|
|
expect(parser).toParse("c", "c");
|
|
expect(parser).toFailToParse("d");
|
|
});
|
|
|
|
it("matches class with a range correctly", function() {
|
|
var parser = PEG.buildParser('start = [a-c]', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toParse("b", "b");
|
|
expect(parser).toParse("c", "c");
|
|
expect(parser).toFailToParse("d");
|
|
});
|
|
|
|
it("matches inverted class correctly", function() {
|
|
var parser = PEG.buildParser('start = [^a]', options);
|
|
|
|
expect(parser).toFailToParse("a");
|
|
expect(parser).toParse("b", "b");
|
|
});
|
|
|
|
it("is case sensitive without the \"i\" flag", function() {
|
|
var parser = PEG.buildParser('start = [a]', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toFailToParse("A");
|
|
});
|
|
|
|
it("is case insensitive with the \"i\" flag", function() {
|
|
var parser = PEG.buildParser('start = [a]i', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toParse("A", "A");
|
|
});
|
|
|
|
it("advances position on success", function() {
|
|
var parser = PEG.buildParser('start = [a] .', options);
|
|
|
|
expect(parser).toParse("ab", ["a", "b"]);
|
|
});
|
|
|
|
it("sets expected string correctly on failure", function() {
|
|
var parser = PEG.buildParser('start = [a]', options);
|
|
|
|
expect(parser).toFailToParse("b", { expected: ["[a]"] });
|
|
});
|
|
});
|
|
|
|
describe("any matching", function() {
|
|
it("matches correctly", function() {
|
|
var parser = PEG.buildParser('start = .', options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
|
|
it("advances position on success", function() {
|
|
var parser = PEG.buildParser('start = . .', options);
|
|
|
|
expect(parser).toParse("ab", ["a", "b"]);
|
|
});
|
|
|
|
it("sets expected string correctly on failure", function() {
|
|
var parser = PEG.buildParser('start = .', options);
|
|
|
|
expect(parser).toFailToParse("", { expected: ['any character'] });
|
|
});
|
|
});
|
|
|
|
describe("error reporting", function() {
|
|
describe("behavior", function() {
|
|
it("reports only the rightmost error", function() {
|
|
var parser = PEG.buildParser('start = "a" "b" / "a" "c" "d"', options);
|
|
|
|
expect(parser).toFailToParse("ace", { offset: 2, expected: ['"d"'] });
|
|
});
|
|
});
|
|
|
|
describe("expected strings reporting", function() {
|
|
it("reports expected strings correctly with no alternative", function() {
|
|
var parser = PEG.buildParser('start = ', options);
|
|
|
|
expect(parser).toFailToParse("a", { expected: [] });
|
|
});
|
|
|
|
it("reports expected strings correctly with one alternative", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("b", { expected: ['"a"'] });
|
|
});
|
|
|
|
it("reports expected strings correctly with multiple alternatives", function() {
|
|
var parser = PEG.buildParser('start = "a" / "b" / "c"', options);
|
|
|
|
expect(parser).toFailToParse("d", {
|
|
expected: ['"a"', '"b"', '"c"']
|
|
});
|
|
});
|
|
|
|
it("removes duplicates from expected strings", function() {
|
|
var parser = PEG.buildParser('start = "a" / "a"', options);
|
|
|
|
expect(parser).toFailToParse("b", { expected: ['"a"'] });
|
|
});
|
|
|
|
it("sorts expected strings", function() {
|
|
var parser = PEG.buildParser('start = "c" / "b" / "a"', options);
|
|
|
|
expect(parser).toFailToParse("d", {
|
|
expected: ['"a"', '"b"', '"c"']
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("found string reporting", function() {
|
|
it("reports found string correctly at the end of input", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("", { found: null });
|
|
});
|
|
|
|
it("reports found string correctly in the middle of input", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("b", { found: "b" });
|
|
});
|
|
});
|
|
|
|
describe("message building", function() {
|
|
it("builds message correctly with no alternative", function() {
|
|
var parser = PEG.buildParser('start = ', options);
|
|
|
|
expect(parser).toFailToParse("a", {
|
|
message: 'Expected end of input but "a" found.'
|
|
});
|
|
});
|
|
|
|
it("builds message correctly with one alternative", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
message: 'Expected "a" but "b" found.'
|
|
});
|
|
});
|
|
|
|
it("builds message correctly with multiple alternatives", function() {
|
|
var parser = PEG.buildParser('start = "a" / "b" / "c"', options);
|
|
|
|
expect(parser).toFailToParse("d", {
|
|
message: 'Expected "a", "b" or "c" but "d" found.'
|
|
});
|
|
});
|
|
|
|
it("builds message correctly at the end of input", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("", {
|
|
message: 'Expected "a" but end of input found.'
|
|
});
|
|
});
|
|
|
|
it("builds message correctly in the middle of input", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
message: 'Expected "a" but "b" found.'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("position reporting", function() {
|
|
it("reports position correctly with invalid input", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("b", { offset: 0, line: 1, column: 1 });
|
|
});
|
|
|
|
it("reports position correctly with trailing input", function() {
|
|
var parser = PEG.buildParser('start = "a"', options);
|
|
|
|
expect(parser).toFailToParse("aa", { offset: 1, line: 1, column: 2});
|
|
});
|
|
|
|
it("reports position correctly in complex cases", function() {
|
|
var parser = PEG.buildParser([
|
|
'start = line (nl+ line)*',
|
|
'line = digit (" "+ digit)*',
|
|
'digit = [0-9]',
|
|
'nl = ("\\r" / "\\n" / "\\u2028" / "\\u2029")'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toFailToParse("1\n2\n\n3\n\n\n4 5 x", {
|
|
offset: 13,
|
|
line: 7,
|
|
column: 5
|
|
});
|
|
|
|
/* Non-Unix newlines */
|
|
expect(parser).toFailToParse("1\rx", { // Old Mac
|
|
offset: 2,
|
|
line: 2,
|
|
column: 1
|
|
});
|
|
expect(parser).toFailToParse("1\r\nx", { // Windows
|
|
offset: 3,
|
|
line: 2,
|
|
column: 1
|
|
});
|
|
expect(parser).toFailToParse("1\n\rx", { // mismatched
|
|
offset: 3,
|
|
line: 3,
|
|
column: 1
|
|
});
|
|
|
|
/* Strange newlines */
|
|
expect(parser).toFailToParse("1\u2028x", { // line separator
|
|
offset: 2,
|
|
line: 2,
|
|
column: 1
|
|
});
|
|
expect(parser).toFailToParse("1\u2029x", { // paragraph separator
|
|
offset: 2,
|
|
line: 2,
|
|
column: 1
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("allowed start rules", function() {
|
|
var grammar = [
|
|
'a = "x"',
|
|
'b = "x"',
|
|
'c = "x"'
|
|
].join("\n");
|
|
|
|
describe("without the |allowedStartRules| option", function() {
|
|
var parser = PEG.buildParser(grammar);
|
|
|
|
it("allows the first rule", function() {
|
|
expect(parser).toParse("x", { startRule: "a" }, "x");
|
|
});
|
|
|
|
it("does not allow any other rules", function() {
|
|
expect(parser).toFailToParse("x", { startRule: "b" }, { });
|
|
expect(parser).toFailToParse("x", { startRule: "c" }, { });
|
|
});
|
|
});
|
|
|
|
describe("with the |allowedStartRules| option", function() {
|
|
var parser = PEG.buildParser(grammar, { allowedStartRules: ["b", "c"] });
|
|
|
|
it("allows the specified rules", function() {
|
|
expect(parser).toParse("x", { startRule: "b" }, "x");
|
|
expect(parser).toParse("x", { startRule: "c" }, "x");
|
|
});
|
|
|
|
it("does not allow any other rules", function() {
|
|
expect(parser).toFailToParse("x", { startRule: "a" }, { });
|
|
});
|
|
});
|
|
});
|
|
|
|
/*
|
|
* Following examples are from Wikipedia, see
|
|
* http://en.wikipedia.org/w/index.php?title=Parsing_expression_grammar&oldid=335106938.
|
|
*/
|
|
describe("complex examples", function() {
|
|
it("handles arithmetics example correctly", function() {
|
|
/*
|
|
* Value ← [0-9]+ / '(' Expr ')'
|
|
* Product ← Value (('*' / '/') Value)*
|
|
* Sum ← Product (('+' / '-') Product)*
|
|
* Expr ← Sum
|
|
*/
|
|
var parser = PEG.buildParser([
|
|
'Expr = Sum',
|
|
'Sum = head:Product tail:(("+" / "-") Product)* {',
|
|
' var result = head, i;',
|
|
' for (i = 0; i < tail.length; i++) {',
|
|
' if (tail[i][0] == "+") { result += tail[i][1]; }',
|
|
' if (tail[i][0] == "-") { result -= tail[i][1]; }',
|
|
' }',
|
|
' return result;',
|
|
' }',
|
|
'Product = head:Value tail:(("*" / "/") Value)* {',
|
|
' var result = head, i;',
|
|
' for (i = 0; i < tail.length; i++) {',
|
|
' if (tail[i][0] == "*") { result *= tail[i][1]; }',
|
|
' if (tail[i][0] == "/") { result /= tail[i][1]; }',
|
|
' }',
|
|
' return result;',
|
|
' }',
|
|
'Value = digits:[0-9]+ { return parseInt(digits.join(""), 10); }',
|
|
' / "(" expr:Expr ")" { return expr; }'
|
|
].join("\n"), options);
|
|
|
|
/* The "value" rule */
|
|
expect(parser).toParse("0", 0);
|
|
expect(parser).toParse("123", 123);
|
|
expect(parser).toParse("(42+43)", 42+43);
|
|
|
|
/* The "product" rule */
|
|
expect(parser).toParse("42", 42);
|
|
expect(parser).toParse("42*43", 42*43);
|
|
expect(parser).toParse("42*43*44*45", 42*43*44*45);
|
|
expect(parser).toParse("42/43", 42/43);
|
|
expect(parser).toParse("42/43/44/45", 42/43/44/45);
|
|
|
|
/* The "sum" rule */
|
|
expect(parser).toParse("42*43", 42*43);
|
|
expect(parser).toParse("42*43+44*45", 42*43+44*45);
|
|
expect(parser).toParse("42*43+44*45+46*47+48*49", 42*43+44*45+46*47+48*49);
|
|
expect(parser).toParse("42*43-44*45", 42*43-44*45);
|
|
expect(parser).toParse("42*43-44*45-46*47-48*49", 42*43-44*45-46*47-48*49);
|
|
|
|
/* The "expr" rule */
|
|
expect(parser).toParse("42+43", 42+43);
|
|
|
|
/* Complex test */
|
|
expect(parser).toParse("(1+2)*(3+4)", (1+2)*(3+4));
|
|
});
|
|
|
|
it("handles non-context-free language correctly", 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:"a"+ B:B !("a" / "b" / "c") { return a.join("") + B; }',
|
|
'A = a:"a" A:A? b:"b" { return a + A + b; }',
|
|
'B = b:"b" B:B? c:"c" { return b + B + c; }'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("abc", "abc");
|
|
expect(parser).toParse("aaabbbccc", "aaabbbccc");
|
|
expect(parser).toFailToParse("aabbbccc");
|
|
expect(parser).toFailToParse("aaaabbbccc");
|
|
expect(parser).toFailToParse("aaabbccc");
|
|
expect(parser).toFailToParse("aaabbbbccc");
|
|
expect(parser).toFailToParse("aaabbbcc");
|
|
expect(parser).toFailToParse("aaabbbcccc");
|
|
});
|
|
|
|
it("handles nested comments example correctly", function() {
|
|
/*
|
|
* Begin ← "(*"
|
|
* End ← "*)"
|
|
* C ← Begin N* End
|
|
* N ← C / (!Begin !End Z)
|
|
* Z ← any single character
|
|
*/
|
|
var parser = PEG.buildParser([
|
|
'C = begin:Begin ns:N* end:End { return begin + ns.join("") + end; }',
|
|
'N = C',
|
|
' / !Begin !End z:Z { return z; }',
|
|
'Z = .',
|
|
'Begin = "(*"',
|
|
'End = "*)"'
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("(**)", "(**)");
|
|
expect(parser).toParse("(*abc*)", "(*abc*)");
|
|
expect(parser).toParse("(*(**)*)", "(*(**)*)");
|
|
expect(parser).toParse(
|
|
"(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)",
|
|
"(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)"
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|