alle new
This commit is contained in:
erik committed 2026-10-01 06:46:51 +02:00
1 parent da64472251
commit 5b20adee48
1605 files changed
+396517 -1674

No files matched your search

Generated Vendored
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2013, Michael Williamson
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Generated Vendored
+248
View File
@@ -0,0 +1,248 @@
# lop -- parsing library for JavaScript
lop is a library to create parsers using parser combinators with helpful errors.
```javascript
function parse(tokens) {
var parser = lop.Parser();
return parser.parseTokens(expressionRule, tokens);
}
// This rule is wrapped inside lop.rule to defer evaluation until
// the rule is used -- otherwise, it would reference integerRule
// and ifRule, which don't exist yet.
var expressionRule = lop.rule(function() {
return rules.firstOf("expression",
integerRule,
ifRule
);
});
var integerRule = rules.then(
rules.tokenOfType("integer"),
function(value) {
return new IntegerNode(parseInt(value, 10));
}
);
var ifRule = rules.sequence(
rules.token("keyword", "if"),
rules.sequence.cut(),
rules.sequence.capture(expressionRule),
rules.token("keyword", "then"),
rules.sequence.capture(expressionRule),
rules.token("keyword", "else"),
rules.sequence.capture(expressionRule)
).map(function(condition, trueBranch, falseBranch) {
return new IfNode(condition, trueBranch, falseBranch);
});
```
lop tries to provide helpful errors where possible. For instance, in `ifRule`
as defined above, there is a cut following the keyword `if`. Before the cut,
if we fail to match the input, we can backtrack -- in this case, we backtrack
and see if another form of expression might match the input. However, after the
cut, we prevent backtracking. Once we've see the keyword `if`, there's no doubt
about which sort of expression this is, so if parsing fails later in this rule,
there's no point in backtracking. This allows informative error messages to be
generated: if we try to parse the string `"if 1 42 else 12"`, we get the error:
Error: File: /tmp/lop-example
Line number: 1
Character number: 6:
Expected keyword "then"
but got integer "42"
## Tokens
When using a parser built with lop, the input is an array of tokens. A token can be any value so long as it has the property `source`, which must be a `StringSourceRange`.
### Regex tokeniser
The easiest way to create a tokeniser is using lop's regex tokeniser.
A regex tokeniser can be constructed by calling `new lop.RegexTokeniser(rules)`,
where `rules` is a list of token rules.
A token rule should have a `name` property that uniquely identifies that rule,
and a `regex` property that is an instance of `RegExp` describing the token.
Calling `tokenise` with a string will return a list of tokens.
Each token has three properties:
* `type`
* `value`
* `source`
The tokeniser will apply the regex from each rule in order at the current position.
The current position is initially zero, the start of the string.
The first rule with a matching regex is used to produce a token,
with the token's `value` being the first capture of the regex,
or `undefined` if the regex does not define any capture groups.
The current position is incremented to the index of the first character unmatched by the regex.
If no rule matches at the current position,
a single character `unrecognisedCharacter` token is produced,
and the current position is incremented by one.
For instance, to create a simple tokeniser that generates a stream of words tokens separated by whitespace tokens.
```javascript
var lop = require("lop");
var rules = [
{
name: "identifier",
regex: /(\s+)/
},
{
name: "whitespace",
regex: /(\S+)/
}
];
var tokeniser = new lop.RegexTokeniser(rules);
tokeniser.tokenise(input);
```
### Custom tokenisers
You can also create your own tokeniser.
For instance, to create a simple tokeniser that generates a stream of words tokens separated by whitespace tokens:
```javascript
var StringSource = require("lop").StringSource;
function tokeniseString(string) {
return tokenise(new StringSource(string, "raw string"));
}
function tokenise(source) {
var string = source.asString();
var whitespaceRegex = /(\s+)/g;
var result;
var start = 0;
var parts = [];
while ((result = whitespaceRegex.exec(source)) !== null) {
parts.push({
type: "word",
value: string.substring(start, result.index),
source: source.range(start, result.index)
});
parts.push({
type: "whitespace",
value: result[1],
source: source.range(result.index, whitespaceRegex.lastIndex)
});
start = whitespaceRegex.lastIndex;
}
parts.push({
type: "word",
value: string.substring(start),
source: source.range(start, string.length)
});
parts.push({
type: "end",
source: source.range(string.length, string.length)
});
return parts.filter(function(part) {
return part.type !== "word" || part.value !== "";
});
}
```
lop also defines its own notion of a token. Each instance of `lop.Token` has a type, name, and source, similarly to most of the tokens that would be created by the token above. For instance, instead of:
{
type: "word",
value: value,
source: source
}
you could use:
new Token("word", value, source)
The main advantage of using `lop.Token` is that you can then use the rules `lop.rules.token` and `lop.rules.tokenOfType` (described later). If you don't use `lop.Token`, you must define your own atomic rules, but you can use the other rules without any modifications.
## Parser
To parse an array of tokens, you can call the method `parseTokens` on `lop.Parser`, passing in the parsing rule and the array of tokens. For instance, assuming we already have a `tokenise` function (the one above would do fine):
```javascript
function parseSentence(source) {
var tokens = tokenise(source);
var parser = new lop.Parser();
var parseResult = parser.parseTokens(sentenceRule, tokens);
if (!parseResult.isSuccess()) {
throw new Error("Failed to parse: " + describeFailure(parseResult));
}
return parseResult.value();
}
function describeFailure(parseResult) {
return parseResult.errors().map(describeError).join("\n");
function describeError(error) {
return error.describe();
}
}
```
The result of parsing can be success, failure, or error. While failure indicates
that the rule didn't match the input tokens, error indicates that the input
was invalid in some way. In general, rules will backtrack when they
encounter a failure, but will completely abort when they encounter an error.
Each of these results has a number of methods:
```javascript
result.isSuccess() // true for success, false otherwise
result.isFailure() // true for failure, false otherwise
result.isError() // true for error, false otherwise
result.value() // if success, the value that was parsed
result.remaining() // if success, the tokens that weren't consumed by parsing
result.source() // the StringSourceRange containing the consumed tokens
result.errors() // if failure or error, an array of descriptions of the failure/error
```
The final question is then: how do we define rules for the parser, such as the currently undefined `sentenceRule`?
## Rules
Each rule in lop accepts an iterator over tokens, and returns a result, as
described in the previous section.
### lop.rules.token(*tokenType*, *value*)
Success if the next token has type `tokenType` and value `value`, failure
otherwise. Value on success is the value of the token.
### lop.rules.tokenOfType(*tokenType*)
Success if the next token has type `tokenType`, failure otherwise. Value on
success is the value of the token.
### lop.rules.firstOf(*name*, *subRules*)
Tries each rule in `subRules` on the input tokens in turn. We return the result
from the first sub-rule that returns success or error. In other words, return the
result from the first sub-rule that doesn't return failure. If all sub-rules return
failure, this rule returns failure.
### lop.rules.then(*subRule*, *func*)
Try `subRule` on the input tokens, and if successful, map over the result. For
instance:
```javascript
lop.rules.then(
lop.rules.tokenOfType("integer"),
function(tokenValue) {
return parseInt(tokenValue, 10);
}
)
```
### lop.rules.optional(*subRule*)
Try `subRule` on the input tokens. If the sub-rule is successful with the value
`value`, then return success with the value `options.some(value)`. If the sub-rule fails, return
success with the value `options.none`. If the sub-rules errors, return that error.
Generated Vendored
+18
View File
@@ -0,0 +1,18 @@
exports.Parser = require("./lib/parser").Parser;
exports.rules = require("./lib/rules");
exports.errors = require("./lib/errors");
exports.results = require("./lib/parsing-results");
exports.StringSource = require("./lib/StringSource");
exports.Token = require("./lib/Token");
exports.bottomUp = require("./lib/bottom-up");
exports.RegexTokeniser = require("./lib/regex-tokeniser").RegexTokeniser;
exports.rule = function(ruleBuilder) {
var rule;
return function(input) {
if (!rule) {
rule = ruleBuilder();
}
return rule(input);
};
};
Generated Vendored Executable
+53
View File
@@ -0,0 +1,53 @@
var StringSource = module.exports = function(string, description) {
var self = {
asString: function() {
return string;
},
range: function(startIndex, endIndex) {
return new StringSourceRange(string, description, startIndex, endIndex);
}
};
return self;
};
var StringSourceRange = function(string, description, startIndex, endIndex) {
this._string = string;
this._description = description;
this._startIndex = startIndex;
this._endIndex = endIndex;
};
StringSourceRange.prototype.to = function(otherRange) {
// TODO: Assert that tokens are the same across both iterators
return new StringSourceRange(this._string, this._description, this._startIndex, otherRange._endIndex);
};
StringSourceRange.prototype.describe = function() {
var position = this._position();
var description = this._description ? this._description + "\n" : "";
return description + "Line number: " + position.lineNumber + "\nCharacter number: " + position.characterNumber;
};
StringSourceRange.prototype.lineNumber = function() {
return this._position().lineNumber;
};
StringSourceRange.prototype.characterNumber = function() {
return this._position().characterNumber;
};
StringSourceRange.prototype._position = function() {
var self = this;
var index = 0;
var nextNewLine = function() {
return self._string.indexOf("\n", index);
};
var lineNumber = 1;
while (nextNewLine() !== -1 && nextNewLine() < this._startIndex) {
index = nextNewLine() + 1;
lineNumber += 1;
}
var characterNumber = this._startIndex - index + 1;
return {lineNumber: lineNumber, characterNumber: characterNumber};
};
Generated Vendored Executable
+7
View File
@@ -0,0 +1,7 @@
module.exports = function(name, value, source) {
this.name = name;
this.value = value;
if (source) {
this.source = source;
}
};
+28
View File
@@ -0,0 +1,28 @@
var TokenIterator = module.exports = function(tokens, startIndex) {
this._tokens = tokens;
this._startIndex = startIndex || 0;
};
TokenIterator.prototype.head = function() {
return this._tokens[this._startIndex];
};
TokenIterator.prototype.tail = function(startIndex) {
return new TokenIterator(this._tokens, this._startIndex + 1);
};
TokenIterator.prototype.toArray = function() {
return this._tokens.slice(this._startIndex);
};
TokenIterator.prototype.end = function() {
return this._tokens[this._tokens.length - 1];
};
// TODO: doesn't need to be a method, can be a separate function,
// which simplifies implementation of the TokenIterator interface
TokenIterator.prototype.to = function(end) {
var start = this.head().source;
var endToken = end.head() || end.end();
return start.to(endToken.source);
};
+128
View File
@@ -0,0 +1,128 @@
var rules = require("./rules");
var results = require("./parsing-results");
exports.parser = function(name, prefixRules, infixRuleBuilders) {
var self = {
rule: rule,
leftAssociative: leftAssociative,
rightAssociative: rightAssociative
};
var infixRules = new InfixRules(infixRuleBuilders.map(createInfixRule));
var prefixRule = rules.firstOf(name, prefixRules);
function createInfixRule(infixRuleBuilder) {
return {
name: infixRuleBuilder.name,
rule: lazyRule(infixRuleBuilder.ruleBuilder.bind(null, self))
};
}
function rule() {
return createRule(infixRules);
}
function leftAssociative(name) {
return createRule(infixRules.untilExclusive(name));
}
function rightAssociative(name) {
return createRule(infixRules.untilInclusive(name));
}
function createRule(infixRules) {
return apply.bind(null, infixRules);
}
function apply(infixRules, tokens) {
var leftResult = prefixRule(tokens);
if (leftResult.isSuccess()) {
return infixRules.apply(leftResult);
} else {
return leftResult;
}
}
return self;
};
function InfixRules(infixRules) {
function untilExclusive(name) {
return new InfixRules(infixRules.slice(0, ruleNames().indexOf(name)));
}
function untilInclusive(name) {
return new InfixRules(infixRules.slice(0, ruleNames().indexOf(name) + 1));
}
function ruleNames() {
return infixRules.map(function(rule) {
return rule.name;
});
}
function apply(leftResult) {
var currentResult;
var source;
while (true) {
currentResult = applyToTokens(leftResult.remaining());
if (currentResult.isSuccess()) {
source = leftResult.source().to(currentResult.source());
leftResult = results.success(
currentResult.value()(leftResult.value(), source),
currentResult.remaining(),
source
)
} else if (currentResult.isFailure()) {
return leftResult;
} else {
return currentResult;
}
}
}
function applyToTokens(tokens) {
return rules.firstOf("infix", infixRules.map(function(infix) {
return infix.rule;
}))(tokens);
}
return {
apply: apply,
untilExclusive: untilExclusive,
untilInclusive: untilInclusive
}
}
exports.infix = function(name, ruleBuilder) {
function map(func) {
return exports.infix(name, function(parser) {
var rule = ruleBuilder(parser);
return function(tokens) {
var result = rule(tokens);
return result.map(function(right) {
return function(left, source) {
return func(left, right, source);
};
});
};
});
}
return {
name: name,
ruleBuilder: ruleBuilder,
map: map
};
}
// TODO: move into a sensible place and remove duplication
var lazyRule = function(ruleBuilder) {
var rule;
return function(input) {
if (!rule) {
rule = ruleBuilder();
}
return rule(input);
};
};
+22
View File
@@ -0,0 +1,22 @@
exports.error = function(options) {
return new Error(options);
};
var Error = function(options) {
this.expected = options.expected;
this.actual = options.actual;
this._location = options.location;
};
Error.prototype.describe = function() {
var locationDescription = this._location ? this._location.describe() + ":\n" : "";
return locationDescription + "Expected " + this.expected + "\nbut got " + this.actual;
};
Error.prototype.lineNumber = function() {
return this._location.lineNumber();
};
Error.prototype.characterNumber = function() {
return this._location.characterNumber();
};
+81
View File
@@ -0,0 +1,81 @@
var fromArray = exports.fromArray = function(array) {
var index = 0;
var hasNext = function() {
return index < array.length;
};
return new LazyIterator({
hasNext: hasNext,
next: function() {
if (!hasNext()) {
throw new Error("No more elements");
} else {
return array[index++];
}
}
});
};
var LazyIterator = function(iterator) {
this._iterator = iterator;
};
LazyIterator.prototype.map = function(func) {
var iterator = this._iterator;
return new LazyIterator({
hasNext: function() {
return iterator.hasNext();
},
next: function() {
return func(iterator.next());
}
});
};
LazyIterator.prototype.filter = function(condition) {
var iterator = this._iterator;
var moved = false;
var hasNext = false;
var next;
var moveIfNecessary = function() {
if (moved) {
return;
}
moved = true;
hasNext = false;
while (iterator.hasNext() && !hasNext) {
next = iterator.next();
hasNext = condition(next);
}
};
return new LazyIterator({
hasNext: function() {
moveIfNecessary();
return hasNext;
},
next: function() {
moveIfNecessary();
var toReturn = next;
moved = false;
return toReturn;
}
});
};
LazyIterator.prototype.first = function() {
var iterator = this._iterator;
if (this._iterator.hasNext()) {
return iterator.next();
} else {
return null;
}
};
LazyIterator.prototype.toArray = function() {
var result = [];
while (this._iterator.hasNext()) {
result.push(this._iterator.next());
}
return result;
};
+11
View File
@@ -0,0 +1,11 @@
var TokenIterator = require("./TokenIterator");
exports.Parser = function(options) {
var parseTokens = function(parser, tokens) {
return parser(new TokenIterator(tokens));
};
return {
parseTokens: parseTokens
};
};
+103
View File
@@ -0,0 +1,103 @@
module.exports = {
failure: function(errors, remaining) {
if (errors.length < 1) {
throw new Error("Failure must have errors");
}
return new Result({
status: "failure",
remaining: remaining,
errors: errors
});
},
error: function(errors, remaining) {
if (errors.length < 1) {
throw new Error("Failure must have errors");
}
return new Result({
status: "error",
remaining: remaining,
errors: errors
});
},
success: function(value, remaining, source) {
return new Result({
status: "success",
value: value,
source: source,
remaining: remaining,
errors: []
});
},
cut: function(remaining) {
return new Result({
status: "cut",
remaining: remaining,
errors: []
});
}
};
var Result = function(options) {
this._value = options.value;
this._status = options.status;
this._hasValue = options.value !== undefined;
this._remaining = options.remaining;
this._source = options.source;
this._errors = options.errors;
};
Result.prototype.map = function(func) {
if (this._hasValue) {
return new Result({
value: func(this._value, this._source),
status: this._status,
remaining: this._remaining,
source: this._source,
errors: this._errors
});
} else {
return this;
}
};
Result.prototype.changeRemaining = function(remaining) {
return new Result({
value: this._value,
status: this._status,
remaining: remaining,
source: this._source,
errors: this._errors
});
};
Result.prototype.isSuccess = function() {
return this._status === "success" || this._status === "cut";
};
Result.prototype.isFailure = function() {
return this._status === "failure";
};
Result.prototype.isError = function() {
return this._status === "error";
};
Result.prototype.isCut = function() {
return this._status === "cut";
};
Result.prototype.value = function() {
return this._value;
};
Result.prototype.remaining = function() {
return this._remaining;
};
Result.prototype.source = function() {
return this._source;
};
Result.prototype.errors = function() {
return this._errors;
};
+70
View File
@@ -0,0 +1,70 @@
var Token = require("./Token");
var StringSource = require("./StringSource");
exports.RegexTokeniser = RegexTokeniser;
function RegexTokeniser(rules) {
rules = rules.map(function(rule) {
return {
name: rule.name,
regex: new RegExp(rule.regex.source, "g")
};
});
function tokenise(input, description) {
var source = new StringSource(input, description);
var index = 0;
var tokens = [];
while (index < input.length) {
var result = readNextToken(input, index, source);
index = result.endIndex;
tokens.push(result.token);
}
tokens.push(endToken(input, source));
return tokens;
}
function readNextToken(string, startIndex, source) {
for (var i = 0; i < rules.length; i++) {
var regex = rules[i].regex;
regex.lastIndex = startIndex;
var result = regex.exec(string);
if (result) {
var endIndex = startIndex + result[0].length;
if (result.index === startIndex && endIndex > startIndex) {
var value = result[1];
var token = new Token(
rules[i].name,
value,
source.range(startIndex, endIndex)
);
return {token: token, endIndex: endIndex};
}
}
}
var endIndex = startIndex + 1;
var token = new Token(
"unrecognisedCharacter",
string.substring(startIndex, endIndex),
source.range(startIndex, endIndex)
);
return {token: token, endIndex: endIndex};
}
function endToken(input, source) {
return new Token(
"end",
null,
source.range(input.length, input.length)
);
}
return {
tokenise: tokenise
}
}
+313
View File
@@ -0,0 +1,313 @@
var _ = require("underscore");
var options = require("option");
var results = require("./parsing-results");
var errors = require("./errors");
var lazyIterators = require("./lazy-iterators");
exports.token = function(tokenType, value) {
var matchValue = value !== undefined;
return function(input) {
var token = input.head();
if (token && token.name === tokenType && (!matchValue || token.value === value)) {
return results.success(token.value, input.tail(), token.source);
} else {
var expected = describeToken({name: tokenType, value: value});
return describeTokenMismatch(input, expected);
}
};
};
exports.tokenOfType = function(tokenType) {
return exports.token(tokenType);
};
exports.firstOf = function(name, parsers) {
if (!_.isArray(parsers)) {
parsers = Array.prototype.slice.call(arguments, 1);
}
return function(input) {
return lazyIterators
.fromArray(parsers)
.map(function(parser) {
return parser(input);
})
.filter(function(result) {
return result.isSuccess() || result.isError();
})
.first() || describeTokenMismatch(input, name);
};
};
exports.then = function(parser, func) {
return function(input) {
var result = parser(input);
if (!result.map) {
console.log(result);
}
return result.map(func);
};
};
exports.sequence = function() {
var parsers = Array.prototype.slice.call(arguments, 0);
var rule = function(input) {
var result = _.foldl(parsers, function(memo, parser) {
var result = memo.result;
var hasCut = memo.hasCut;
if (!result.isSuccess()) {
return {result: result, hasCut: hasCut};
}
var subResult = parser(result.remaining());
if (subResult.isCut()) {
return {result: result, hasCut: true};
} else if (subResult.isSuccess()) {
var values;
if (parser.isCaptured) {
values = result.value().withValue(parser, subResult.value());
} else {
values = result.value();
}
var remaining = subResult.remaining();
var source = input.to(remaining);
return {
result: results.success(values, remaining, source),
hasCut: hasCut
};
} else if (hasCut) {
return {result: results.error(subResult.errors(), subResult.remaining()), hasCut: hasCut};
} else {
return {result: subResult, hasCut: hasCut};
}
}, {result: results.success(new SequenceValues(), input), hasCut: false}).result;
var source = input.to(result.remaining());
return result.map(function(values) {
return values.withValue(exports.sequence.source, source);
});
};
rule.head = function() {
var firstCapture = _.find(parsers, isCapturedRule);
return exports.then(
rule,
exports.sequence.extract(firstCapture)
);
};
rule.map = function(func) {
return exports.then(
rule,
function(result) {
return func.apply(this, result.toArray());
}
);
};
function isCapturedRule(subRule) {
return subRule.isCaptured;
}
return rule;
};
var SequenceValues = function(values, valuesArray) {
this._values = values || {};
this._valuesArray = valuesArray || [];
};
SequenceValues.prototype.withValue = function(rule, value) {
if (rule.captureName && rule.captureName in this._values) {
throw new Error("Cannot add second value for capture \"" + rule.captureName + "\"");
} else {
var newValues = _.clone(this._values);
newValues[rule.captureName] = value;
var newValuesArray = this._valuesArray.concat([value]);
return new SequenceValues(newValues, newValuesArray);
}
};
SequenceValues.prototype.get = function(rule) {
if (rule.captureName in this._values) {
return this._values[rule.captureName];
} else {
throw new Error("No value for capture \"" + rule.captureName + "\"");
}
};
SequenceValues.prototype.toArray = function() {
return this._valuesArray;
};
exports.sequence.capture = function(rule, name) {
var captureRule = function() {
return rule.apply(this, arguments);
};
captureRule.captureName = name;
captureRule.isCaptured = true;
return captureRule;
};
exports.sequence.extract = function(rule) {
return function(result) {
return result.get(rule);
};
};
exports.sequence.applyValues = function(func) {
// TODO: check captureName doesn't conflict with source or other captures
var rules = Array.prototype.slice.call(arguments, 1);
return function(result) {
var values = rules.map(function(rule) {
return result.get(rule);
});
return func.apply(this, values);
};
};
exports.sequence.source = {
captureName: "☃source☃"
};
exports.sequence.cut = function() {
return function(input) {
return results.cut(input);
};
};
exports.optional = function(rule) {
return function(input) {
var result = rule(input);
if (result.isSuccess()) {
return result.map(options.some);
} else if (result.isFailure()) {
return results.success(options.none, input);
} else {
return result;
}
};
};
exports.zeroOrMoreWithSeparator = function(rule, separator) {
return repeatedWithSeparator(rule, separator, false);
};
exports.oneOrMoreWithSeparator = function(rule, separator) {
return repeatedWithSeparator(rule, separator, true);
};
var zeroOrMore = exports.zeroOrMore = function(rule) {
return function(input) {
var values = [];
var result;
while ((result = rule(input)) && result.isSuccess()) {
input = result.remaining();
values.push(result.value());
}
if (result.isError()) {
return result;
} else {
return results.success(values, input);
}
};
};
exports.oneOrMore = function(rule) {
return exports.oneOrMoreWithSeparator(rule, noOpRule);
};
function noOpRule(input) {
return results.success(null, input);
}
var repeatedWithSeparator = function(rule, separator, isOneOrMore) {
return function(input) {
var result = rule(input);
if (result.isSuccess()) {
var mainRule = exports.sequence.capture(rule, "main");
var remainingRule = zeroOrMore(exports.then(
exports.sequence(separator, mainRule),
exports.sequence.extract(mainRule)
));
var remainingResult = remainingRule(result.remaining());
return results.success([result.value()].concat(remainingResult.value()), remainingResult.remaining());
} else if (isOneOrMore || result.isError()) {
return result;
} else {
return results.success([], input);
}
};
};
exports.leftAssociative = function(leftRule, rightRule, func) {
var rights;
if (func) {
rights = [{func: func, rule: rightRule}];
} else {
rights = rightRule;
}
rights = rights.map(function(right) {
return exports.then(right.rule, function(rightValue) {
return function(leftValue, source) {
return right.func(leftValue, rightValue, source);
};
});
});
var repeatedRule = exports.firstOf.apply(null, ["rules"].concat(rights));
return function(input) {
var start = input;
var leftResult = leftRule(input);
if (!leftResult.isSuccess()) {
return leftResult;
}
var repeatedResult = repeatedRule(leftResult.remaining());
while (repeatedResult.isSuccess()) {
var remaining = repeatedResult.remaining();
var source = start.to(repeatedResult.remaining());
var right = repeatedResult.value();
leftResult = results.success(
right(leftResult.value(), source),
remaining,
source
);
repeatedResult = repeatedRule(leftResult.remaining());
}
if (repeatedResult.isError()) {
return repeatedResult;
}
return leftResult;
};
};
exports.leftAssociative.firstOf = function() {
return Array.prototype.slice.call(arguments, 0);
};
exports.nonConsuming = function(rule) {
return function(input) {
return rule(input).changeRemaining(input);
};
};
var describeToken = function(token) {
if (token.value) {
return token.name + " \"" + token.value + "\"";
} else {
return token.name;
}
};
function describeTokenMismatch(input, expected) {
var error;
var token = input.head();
if (token) {
error = errors.error({
expected: expected,
actual: describeToken(token),
location: token.source
});
} else {
error = errors.error({
expected: expected,
actual: "end of tokens"
});
}
return results.failure([error], input);
}
+69
View File
@@ -0,0 +1,69 @@
var _ = require("underscore");
var duck = require("duck");
var Token = require("./Token");
var assertIsSuccess = exports.assertIsSuccess = function(test, result, options) {
test.ok(result.isSuccess(), result.errors().map(function(error) {
return error.describe();
}).join("\n\n"));
options = options || {};
if (!options.remaining) {
options.remaining = [new Token("end", null)];
}
assertResultExtras(test, result, options);
};
var assertIsSuccessWithValue = exports.assertIsSuccessWithValue = function(test, result, value, source) {
assertIsSuccess(test, result, {
value: value,
source: source
});
};
var assertIsFailure = exports.assertIsFailure = function(test, result, options) {
test.deepEqual(result.isSuccess(), false);
assertResultExtras(test, result, options);
};
var assertRemaining = exports.assertRemaining = function(test, result, expectedRemaining) {
var actualRemaining = result.remaining().toArray();
assertThat(test, actualRemaining, duck.isArray(expectedRemaining.map(duck.hasProperties)));
};
var assertErrors = exports.assertErrors = function(test, result, expectedErrors) {
test.deepEqual(result.errors(), expectedErrors);
};
var assertIsFailureWithRemaining = exports.assertIsFailureWithRemaining = function(test, result, expectedRemaining) {
assertIsFailure(test, result);
assertRemaining(test, result, expectedRemaining);
};
var assertIsError = exports.assertIsError = function(test, result, options) {
test.ok(result.isError());
assertResultExtras(test, result, options);
};
var assertResultExtras = function(test, result, options) {
options = options || {};
if (options.remaining) {
assertRemaining(test, result, options.remaining);
}
if (options.errors) {
assertErrors(test, result, options.errors);
}
if (options.source) {
test.deepEqual(result.source(), options.source);
}
if (options.value) {
assertThat(test, result.value(), duck.is(options.value));
}
}
var assertThat = function(test, value, matcher) {
var message = "Expected " + matcher.describeSelf() +
"\nbut " + matcher.describeMismatch(value);
test.ok(matcher.matches(value), message);
};
Generated Vendored
+4
View File
@@ -0,0 +1,4 @@
.PHONY: test
test:
node_modules/.bin/nodeunit `find test -name \*.test.js -type f`
+24
View File
@@ -0,0 +1,24 @@
{
"name": "lop",
"version": "0.4.2",
"author": "Michael Williamson <mike@zwobble.org>",
"description": "Create parsers using parser combinators with helpful error messages",
"keywords": [
"parse",
"parser",
"combinator"
],
"repository": {
"type": "git",
"url": "https://github.com/mwilliamson/lop.git"
},
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
},
"devDependencies": {
"nodeunit": "~0.8.0"
},
"license": "BSD-2-Clause"
}
Generated Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
var StringSource = require("../lib/StringSource");
exports.stringSourceRangeDescriptionIncludesLineAndCharacterNumber = function(test) {
test.equal(
"Line number: 1\nCharacter number: 3",
new StringSource("blah").range(2, 3).describe()
);
test.equal(
"Line number: 3\nCharacter number: 5",
new StringSource("one\ntwo\nthree\nfour").range(12, 15).describe()
);
test.done();
};
exports.stringSourceRangeDescriptionIncludesDescriptionIfProvided = function(test) {
test.equal(
"File: /some/file\nLine number: 1\nCharacter number: 3",
new StringSource("blah", "File: /some/file").range(2, 3).describe()
);
test.done();
};
Generated Vendored Executable
+17
View File
@@ -0,0 +1,17 @@
var TokenIterator = require("../lib/TokenIterator");
var Token = require("../lib/Token");
var StringSource = require("../lib/StringSource");
exports.canCreateSourceRangeToIteratorBeyondEnd = function(test) {
var source = function(startIndex, endIndex) {
return new StringSource("blah").range(startIndex, endIndex);
};
var startIterator = new TokenIterator([
new Token("identifier", "blah", source(0, 4)),
new Token("end", null, source(4, 4))
]);
var endIterator = startIterator.tail().tail();
var range = startIterator.to(endIterator);
test.deepEqual(source(0, 4), range);
test.done();
};
Generated Vendored Executable
+50
View File
@@ -0,0 +1,50 @@
var Token = require("../lib/Token");
var StringSource = require("../lib/StringSource");
var Tokeniser = module.exports = function(options) {
var keywords = options.keywords;
var tokenise = function(input) {
var source = new StringSource(input);
var createToken = function(startIndex, endIndex) {
var value = input.substring(startIndex, endIndex);
var tokenType = keywords.indexOf(value) === -1 ? "identifier" : "keyword";
return new Token(
tokenType,
value,
source.range(startIndex, endIndex)
);
};
var position = 0;
var tokens = [];
var done = input === "";
while (!done) {
var nextWhitespace = indexOfRegex(input, /\s/, position);
if (nextWhitespace === -1) {
done = true;
tokens.push(createToken(position, input.length));
} else {
tokens.push(createToken(position, nextWhitespace));
position = indexOfRegex(input, /\S/, nextWhitespace);
}
}
tokens.push(new Token("end", null, source.range(input.length, input.length)));
return tokens;
};
var indexOfRegex = function(string, regex, startIndex) {
startIndex = startIndex || 0;
var index = string.substring(startIndex).search(regex);
if (index === -1) {
return -1;
} else {
return index + startIndex;
}
};
return {
tokenise: tokenise
};
};
Generated Vendored Executable
+38
View File
@@ -0,0 +1,38 @@
var Tokeniser = require("./Tokeniser");
var Token = require("../lib/Token");
var StringSource = require("../lib/StringSource");
exports.stringIsSingleIdentifier = stringIsTokenisedTo("blah", [
new Token("identifier", "blah", stringSourceRange("blah", 0, 4)),
new Token("end", null, stringSourceRange("blah", 4, 4))
]);
exports.identifiersAreSeparatedByWhitespace = stringIsTokenisedTo("one two", [
new Token("identifier", "one", stringSourceRange("one two", 0, 3)),
new Token("identifier", "two", stringSourceRange("one two", 4, 7)),
new Token("end", null, stringSourceRange("one two", 7, 7))
]);
exports.canDetectKeywords = stringIsTokenisedTo("true", [
new Token("keyword", "true", stringSourceRange("true", 0, 4)),
new Token("end", null, stringSourceRange("true", 4, 4))
]);
exports.emptyStringIsTokenisedToSingleEndToken = stringIsTokenisedTo("", [
new Token("end", null, stringSourceRange("", 0, 0))
]);
function stringIsTokenisedTo(input, expected) {
return function(test) {
test.deepEqual(expected, tokenise(input));
test.done();
};
};
function stringSourceRange(string, startIndex, endIndex) {
return new StringSource(string).range(startIndex, endIndex);
};
function tokenise(input) {
return new Tokeniser({keywords: ["true"]}).tokenise(input);
};
+187
View File
@@ -0,0 +1,187 @@
var rules = require("../lib/rules");
var bottomUp = require("../lib/bottom-up");
var testing = require("../lib/testing");
var TokenIterator = require("../lib/TokenIterator");
var errors = require("../lib/errors");
var results = require("../lib/parsing-results");
var StringSource = require("../lib/StringSource");
var assertIsSuccess = testing.assertIsSuccess;
var assertIsSuccessWithValue = testing.assertIsSuccessWithValue;
var assertIsFailure = testing.assertIsFailure;
var assertIsFailureWithRemaining = testing.assertIsFailureWithRemaining;
var assertIsError = testing.assertIsError;
var Tokeniser = require("./Tokeniser");
var Token = require("../lib/Token");
var source = function(string, startIndex, endIndex) {
return new StringSource(string).range(startIndex, endIndex);
};
var token = function(tokenType, value, source) {
return new Token(tokenType, value, source);
};
var partialCallRule = bottomUp.infix("call", function(parser) {
return rules.sequence(
rules.token("symbol", "("),
rules.sequence.capture(parser.rule()),
rules.token("symbol", ")")
).head();
}).map(function(left, arg) {
return [left, arg];
});
var partialAddRule = bottomUp.infix("add", function(parser) {
return rules.sequence(
rules.token("symbol", "+"),
rules.sequence.capture(parser.leftAssociative("add"))
).head();
}).map(function(left, right) {
return ["+", left, right];
});
var partialMultiplyRule = bottomUp.infix("multiply", function(parser) {
return rules.sequence(
rules.token("symbol", "*"),
rules.sequence.capture(parser.leftAssociative("multiply"))
).head();
}).map(function(left, right) {
return ["*", left, right];
});
var partialPowerRule = bottomUp.infix("power", function(parser) {
return rules.sequence(
rules.token("symbol", "^"),
rules.sequence.capture(parser.rightAssociative("power"))
).head();
}).map(function(left, right) {
return ["^", left, right];
});
exports.canParsePrefixExpression = function(test) {
var rule = bottomUp.parser("expression",
[rules.tokenOfType("identifier")],
[]
).rule();
var result = parse(rule, [
token("identifier", "blah", source("blah", 0, 4)),
token("end", null, source("blah", 4, 4))
]);
assertIsSuccess(test, result, {
value: "blah",
source: source("blah", 0, 4)
});
test.done();
};
exports.canParseSimpleInfixExpression = function(test) {
var rule = bottomUp.parser("expression",
[rules.tokenOfType("identifier")],
[partialCallRule]
).rule();
var result = parse(rule, [
token("identifier", "print", source("print(name)", 0, 5)),
token("symbol", "(", source("print(name)", 5, 6)),
token("identifier", "name", source("print(name)", 6, 10)),
token("symbol", ")", source("print(name)", 10, 11)),
token("end", null, source("print(name)", 11, 11))
]);
assertIsSuccess(test, result, {
value: ["print", "name"],
source: source("print(name)", 0, 11)
});
test.done();
};
exports.parsingStopsIfPrefixRuleFails = function(test) {
var rule = bottomUp.parser("expression",
[rules.tokenOfType("identifier")],
[partialCallRule]
).rule();
var result = parse(rule, [
token("symbol", "(", source("(name)", 0, 1)),
token("identifier", "name", source("(name)", 1, 5)),
token("symbol", ")", source("(name)", 5, 6)),
token("end", null, source("(name)", 6, 6))
]);
assertIsFailure(test, result, {
remaining: [
token("symbol", "(", source("(name)", 0, 1)),
token("identifier", "name", source("(name)", 1, 5)),
token("symbol", ")", source("(name)", 5, 6)),
token("end", null, source("(name)", 6, 6))
],
errors: [errors.error({
expected: "expression",
actual: "symbol \"(\"",
location: source("(name)", 0, 1)
})]
});
test.done();
};
exports.canParseExpressionWithTwoLeftAssociativeOperators = function(test) {
var expressionParser = bottomUp.parser("expression",
[rules.tokenOfType("number")],
[
partialMultiplyRule,
partialAddRule
]
);
var rule = expressionParser.rule();
var result = parse(rule, [
token("number", "1", source("1 * 2 * 3 + 4 * 5", 0, 1)),
token("symbol", "*", source("1 * 2 * 3 + 4 * 5", 2, 3)),
token("number", "2", source("1 * 2 * 3 + 4 * 5", 4, 5)),
token("symbol", "*", source("1 * 2 * 3 + 4 * 5", 6, 7)),
token("number", "3", source("1 * 2 * 3 + 4 * 5", 8, 9)),
token("symbol", "+", source("1 * 2 * 3 + 4 * 5", 10, 11)),
token("number", "4", source("1 * 2 * 3 + 4 * 5", 12, 13)),
token("symbol", "*", source("1 * 2 * 3 + 4 * 5", 14, 15)),
token("number", "5", source("1 * 2 * 3 + 4 * 5", 16, 17)),
token("end", null, source("1 * 2 * 3 + 4 * 5", 17, 17))
]);
assertIsSuccess(test, result, {
value: ["+", ["*", ["*", "1", "2"], "3"], ["*", "4", "5"]],
source: source("1 * 2 * 3 + 4 * 5", 0, 17)
});
test.done();
};
exports.canParseExpressionWithRightAssociativeOperators = function(test) {
var expressionParser = bottomUp.parser("expression",
[rules.tokenOfType("number")],
[
partialPowerRule,
partialAddRule
]
);
var rule = expressionParser.rule();
var result = parse(rule, [
token("number", "1", source("1 ^ 2 ^ 3 + 4 ^ 5", 0, 1)),
token("symbol", "^", source("1 ^ 2 ^ 3 + 4 ^ 5", 2, 3)),
token("number", "2", source("1 ^ 2 ^ 3 + 4 ^ 5", 4, 5)),
token("symbol", "^", source("1 ^ 2 ^ 3 + 4 ^ 5", 6, 7)),
token("number", "3", source("1 ^ 2 ^ 3 + 4 ^ 5", 8, 9)),
token("symbol", "+", source("1 ^ 2 ^ 3 + 4 ^ 5", 10, 11)),
token("number", "4", source("1 ^ 2 ^ 3 + 4 ^ 5", 12, 13)),
token("symbol", "^", source("1 ^ 2 ^ 3 + 4 ^ 5", 14, 15)),
token("number", "5", source("1 ^ 2 ^ 3 + 4 ^ 5", 16, 17)),
token("end", null, source("1 ^ 2 ^ 3 + 4 ^ 5", 17, 17))
]);
assertIsSuccess(test, result, {
value: ["+", ["^", "1", ["^", "2", "3"]], ["^", "4", "5"]],
source: source("1 ^ 2 ^ 3 + 4 ^ 5", 0, 17)
});
test.done();
};
var parse = function(parser, tokens) {
return parser(new TokenIterator(tokens));
};
Generated Vendored Executable
+36
View File
@@ -0,0 +1,36 @@
var errors = require("../lib/errors");
var StringSource = require("../lib/StringSource");
exports.errorDescriptionIncludesLocationAndActualValueAndExpectedValue = function(test) {
var error = errors.error({
expected: "Nothing",
actual: "Something",
location: {
describe: function() {
return "Here"
}
}
});
test.equal("Here:\nExpected Nothing\nbut got Something", error.describe());
test.done();
};
exports.canDescribeErrorWithoutLocation = function(test) {
var error = errors.error({
expected: "Nothing",
actual: "Something"
});
test.equal("Expected Nothing\nbut got Something", error.describe());
test.done();
};
exports.canGetPositionFromError = function(test) {
var error = errors.error({
expected: "Nothing",
actual: "Something",
location: new StringSource("abc\ndef\nghi\n", "").range(6, 8)
});
test.equal(2, error.lineNumber());
test.equal(3, error.characterNumber());
test.done();
};
+53
View File
@@ -0,0 +1,53 @@
var lazyIterators = require("../lib/lazy-iterators");
exports.convertingArrayToIteratorAndBack = function(test) {
var original = ["apple", "banana", "coconut"];
var iterator = lazyIterators.fromArray(original);
var reconstituted = iterator.toArray();
test.deepEqual(original, reconstituted);
test.done();
};
exports.mapAppliesFunctionToEveryElement = function(test) {
var original = [1, 2, 3];
var squared = lazyIterators.fromArray(original).map(function(i) {
return i * i;
}).toArray();
test.deepEqual([1, 4, 9], squared);
test.done();
};
exports.mappingFunctionIsOnlyCalledWhenNecessary = function(test) {
var original = [1, 2, 3];
var faultySquared = lazyIterators.fromArray(original).map(function(i) {
if (i === 1) {
return i;
} else {
throw new Error("Can only calculate the square of one");
}
}).first();
test.deepEqual(1, faultySquared);
test.done();
};
exports.filterRetainsElementsThatSatisfyPredicate = function(test) {
var original = [1, 2, 3, 4];
var even = lazyIterators.fromArray(original).filter(function(i) {
return i % 2 === 0;
}).toArray();
test.deepEqual([2, 4], even);
test.done();
};
exports.firstReturnsFirstElementOfArray = function(test) {
var original = [1, 2, 3, 4];
var first = lazyIterators.fromArray(original).first();
test.deepEqual(1, first);
test.done();
};
exports.firstIsNullIfArrayIsEmpty = function(test) {
var first = lazyIterators.fromArray([]).first();
test.same(null, first);
test.done();
};
+27
View File
@@ -0,0 +1,27 @@
var lop = require("../");
var Parser = lop.Parser;
var rules = lop.rules;
var testing = require("../lib/testing");
var Tokeniser = require("./Tokeniser");
exports.canParseUsingParser = function(test) {
var tokens = new Tokeniser({keywords: []}).tokenise("! blah");
var name = rules.sequence.capture(rules.token("identifier"), "name");
var rule = rules.sequence(
rules.token("identifier", "!"),
name
);
var parser = new Parser();
var result = parser.parseTokens(rule, tokens);
testing.assertIsSuccess(test, result);
test.deepEqual(result.value().get(name), "blah");
test.done();
};
var parseString = function(parser, string) {
return parser(new TokenIterator(tokens));
};
+114
View File
@@ -0,0 +1,114 @@
var RegexTokeniser = require("../lib/regex-tokeniser").RegexTokeniser;
var Token = require("../lib/Token");
var StringSource = require("../lib/StringSource");
exports.emptyStringIsTokenisedToEndToken = stringIsTokenisedTo("", [
endToken("")
]);
exports.canMatchSingleToken = stringIsTokenisedTo("blah", [
new Token("identifier", "blah", stringSourceRange("blah", 0, 4)),
endToken("blah")
]);
exports.canMatchMultipleTokens = stringIsTokenisedTo("a.btn", [
new Token("identifier", "a", stringSourceRange("a.btn", 0, 1)),
new Token("dot", ".", stringSourceRange("a.btn", 1, 2)),
new Token("identifier", "btn", stringSourceRange("a.btn", 2, 5)),
endToken("a.btn")
]);
exports.unrecognisedCharactersAreTokenised = stringIsTokenisedTo("!btn", [
new Token("unrecognisedCharacter", "!", stringSourceRange("!btn", 0, 1)),
new Token("identifier", "btn", stringSourceRange("!btn", 1, 4)),
endToken("!btn")
]);
exports.firstMatchingRuleIsUsed = stringIsTokenisedTo(":", [
new Token("colon1", ":", stringSourceRange(":", 0, 1)),
endToken(":")
]);
exports.valuesOfZeroLengthAreIgnored = function(test) {
var expectedTokens = [
new Token("unrecognisedCharacter", "!", stringSourceRange("!btn", 0, 1)),
new Token("identifier", "btn", stringSourceRange("!btn", 1, 4)),
endToken("!btn")
];
var rules = [
{
name: "identifier",
regex: /([a-z]*)/
}
];
var tokeniser = new RegexTokeniser(rules);
test.deepEqual(expectedTokens, tokeniser.tokenise("!btn"));
test.done();
};
exports.tokenValueIsFirstCaptureOfRegex = stringIsTokenisedTo('"a"', [
new Token("string", "a", stringSourceRange('"a"', 0, 3)),
endToken('"a"')
]);
exports.tokenWithNoCaptureHasUndefinedValue = function(test) {
var expectedTokens = [
new Token("bang", undefined, stringSourceRange("!", 0, 1)),
endToken("!")
];
var rules = [
{
name: "bang",
regex: /!/
}
];
var tokeniser = new RegexTokeniser(rules);
test.deepEqual(expectedTokens, tokeniser.tokenise("!"));
test.done();
};
function endToken(input) {
var source = stringSourceRange(input, input.length, input.length);
return new Token("end", null, source);
}
function stringIsTokenisedTo(input, expected) {
return function(test) {
test.deepEqual(expected, tokenise(input));
test.done();
};
};
function stringSourceRange(string, startIndex, endIndex) {
return new StringSource(string).range(startIndex, endIndex);
};
function tokenise(input) {
var rules = [
{
name: "identifier",
regex: /([a-z]+)/
},
{
name: "dot",
regex: /(\.)/
},
{
name: "colon1",
regex: /(:)/
},
{
name: "colon2",
regex: /(:)/
},
{
name: "string",
regex: /"([a-z]*)"/
}
];
var tokeniser = new RegexTokeniser(rules);
return tokeniser.tokenise(input);
};
+8
View File
@@ -0,0 +1,8 @@
var results = require("../lib/parsing-results");
exports.errorIsThrownIfCreatingFailureWithoutAnyErrors = function(test) {
test.throws(function() {
results.failure([]);
});
test.done();
};
+617
View File
@@ -0,0 +1,617 @@
var options = require("option");
var rules = require("../lib/rules");
var testing = require("../lib/testing");
var TokenIterator = require("../lib/TokenIterator");
var errors = require("../lib/errors");
var results = require("../lib/parsing-results");
var StringSource = require("../lib/StringSource");
var assertIsSuccess = testing.assertIsSuccess;
var assertIsSuccessWithValue = testing.assertIsSuccessWithValue;
var assertIsFailure = testing.assertIsFailure;
var assertIsFailureWithRemaining = testing.assertIsFailureWithRemaining;
var assertIsError = testing.assertIsError;
var Tokeniser = require("./Tokeniser");
var Token = require("../lib/Token");
var stringSourceRange = function(string, startIndex, endIndex) {
return new StringSource(string).range(startIndex, endIndex);
};
var token = function(tokenType, value, source) {
return new Token(tokenType, value, source);
};
var keyword = function(value) {
return rules.token("keyword", value);
};
var identifier = function(value) {
return rules.token("identifier", value);
};
exports.tokenRuleFailsIfInputIsEmpty = function(test) {
var tokens = [];
var rule = rules.token("keyword", "true");
var result = rule(new TokenIterator(tokens));
assertIsFailure(test, result, {
remaining: [],
errors: [errors.error({
expected: "keyword \"true\"",
actual: "end of tokens"
})]
});
test.done();
};
exports.tokenRuleConsumeTokenWhenTokenIsOfCorrectType = function(test) {
var parser = rules.token("keyword", "true");
var result = parseString(parser, "true");
assertIsSuccess(test, result, {
value: "true",
source: stringSourceRange("true", 0, 4)
});
test.done();
};
exports.parsingTokenFailsIfTokenIsOfWrongType = function(test) {
var parser = rules.token("keyword", "true");
var result = parseString(parser, "blah");
assertIsFailure(test, result, {
remaining: [
token("identifier", "blah", stringSourceRange("blah", 0, 4)),
token("end", null, stringSourceRange("blah", 4, 4))
],
errors: [errors.error({
expected: "keyword \"true\"",
actual: "identifier \"blah\"",
location: stringSourceRange("blah", 0, 4)
})]
});
test.done();
};
exports.parsingTokenFailsIfTokenIsOfWrongValue = function(test) {
var parser = rules.token("keyword", "true");
var result = parseString(parser, "false");
assertIsFailure(test, result, {
remaining: [
token("keyword", "false", stringSourceRange("false", 0, 5)),
token("end", null, stringSourceRange("false", 5, 5))
],
errors: [errors.error({
expected: "keyword \"true\"",
actual: "keyword \"false\"",
location: stringSourceRange("false", 0, 5)
})]
});
test.done();
};
exports.anyValueIsAcceptedIfValueOfTokenIsNotSpecified = function(test) {
var parser = rules.token("keyword");
var result = parseString(parser, "true");
assertIsSuccess(test, result, {
value: "true",
source: stringSourceRange("true", 0, 4)
});
test.done();
};
exports.firstSuccessIsReturnedByFirstOf = function(test) {
var trueParser = keyword("true");
var falseParser = keyword("false");
var evilParser = function() {
throw new Error("Hahaha!");
};
var result = parseString(rules.firstOf("Boolean", trueParser, falseParser, evilParser), "false");
assertIsSuccessWithValue(test, result, "false");
test.done();
};
exports.firstOfFailsIfNoParsersMatch = function(test) {
var trueParser = keyword("true");
var falseParser = keyword("false");
var result = parseString(rules.firstOf("Boolean", trueParser, falseParser), "blah");
assertIsFailure(test, result, {
remaining:[
identifier("blah", stringSourceRange("blah", 0, 4)),
token("end", null, stringSourceRange("blah", 4, 4))
],
errors: [errors.error({
expected: "Boolean",
actual: "identifier \"blah\"",
location: stringSourceRange("blah", 0, 4)
})]
});
test.done();
};
exports.firstOfReturnsErrorIfSubRuleReturnsErrorEvenIfLaterRuleSucceeds = function(test) {
var trueParser = rules.sequence(rules.sequence.cut(), keyword("true"));
var falseParser = keyword("false");
var result = parseString(rules.firstOf("Boolean", trueParser, falseParser), "false");
assertIsError(test, result, {
remaining:[
keyword("false", stringSourceRange("false", 0, 5)),
token("end", null, stringSourceRange("false", 5, 5))
],
errors: [errors.error({
expected: "keyword \"true\"",
actual: "keyword \"false\"",
location: stringSourceRange("false", 0, 5)
})]
});
test.done();
};
exports.thenReturnsFailureIfOriginalResultIsFailure = function(test) {
var parser = rules.then(keyword("true"), function() { return true; });
var result = parseString(parser, "blah");
assertIsFailure(test, result, {
remaining:[
identifier("blah", stringSourceRange("blah", 0, 4)),
token("end", null, stringSourceRange("blah", 4, 4))
],
errors: [errors.error({
expected: "keyword \"true\"",
actual: "identifier \"blah\"",
location: stringSourceRange("blah", 0, 4)
})]
});
test.done();
};
exports.thenMapsOverValueIfOriginalResultIsSuccess = function(test) {
var parser = rules.then(keyword("true"), function() { return true; });
var result = parseString(parser, "true");
assertIsSuccessWithValue(test, result, true);
test.done();
};
exports.sequenceSucceedsIfSubParsersCanBeAppliedInOrder = function(test) {
var parser = rules.sequence(identifier("one"), identifier("two"));
var result = parseString(parser, "one two");
assertIsSuccess(test, result, {
source: stringSourceRange("one two", 0, 7)
});
test.done();
};
exports.sequenceFailIfSubParserFails = function(test) {
var parser = rules.sequence(identifier("("), identifier(")"));
var result = parseString(parser, "(");
assertIsFailure(test, result, {
remaining:[token("end", null, stringSourceRange("(", 1, 1))],
errors: [errors.error({
expected: "identifier \")\"",
actual: "end",
location: stringSourceRange("(", 1, 1)
})]
});
test.done();
};
exports.sequenceFailIfSubParserFailsAndFinalParserSucceeds = function(test) {
var parser = rules.sequence(identifier("("), identifier(")"));
var result = parseString(parser, ")");
assertIsFailure(test, result, {
remaining:[
identifier(")", stringSourceRange(")", 0, 1)),
token("end", null, stringSourceRange(")", 1, 1))
],
errors: [errors.error({
expected: "identifier \"(\"",
actual: "identifier \")\"",
location: stringSourceRange(")", 0, 1)
})]
});
test.done();
};
exports.sequenceReturnsMapOfCapturedValues = function(test) {
var name = rules.sequence.capture(identifier(), "name");
var parser = rules.sequence(identifier("("), name, identifier(")"));
var result = parseString(parser, "( bob )");
assertIsSuccess(test, result);
test.deepEqual(result.value().get(name), "bob");
test.done();
};
exports.failureInSubRuleInSequenceBeforeCutCausesSequenceToFail = function(test) {
var parser = rules.sequence(identifier("("), rules.sequence.cut(), identifier(), identifier(")"));
var result = parseString(parser, "bob");
assertIsFailure(test, result);
test.done();
};
exports.failureInSubRuleInSequenceAfterCutCausesError = function(test) {
var parser = rules.sequence(identifier("("), rules.sequence.cut(), identifier(), identifier(")"));
var result = parseString(parser, "( true");
assertIsError(test, result, {
remaining: [
token("keyword", "true", stringSourceRange("( true", 2, 6)),
token("end", null, stringSourceRange("( true", 6, 6))
],
errors: [errors.error({
expected: "identifier",
actual: "keyword \"true\"",
location: stringSourceRange("( true", 2, 6)
})]
});
test.done();
};
exports.canPullSingleValueOutOfCapturedValuesUsingExtract = function(test) {
var name = rules.sequence.capture(identifier(), "name");
var parser = rules.then(
rules.sequence(identifier("("), name, identifier(")")),
rules.sequence.extract(name)
);
var result = parseString(parser, "( bob )");
assertIsSuccessWithValue(test, result, "bob");
test.done();
};
exports.canPullSingleValueOutOfCapturedValuesUsingHeadOnSequenceRule = function(test) {
var name = rules.sequence.capture(identifier(), "name");
var parser =
rules.sequence(identifier("("), name, identifier(")"))
.head();
var result = parseString(parser, "( bob )");
assertIsSuccessWithValue(test, result, "bob");
test.done();
};
exports.canApplyValuesFromSequenceToFunction = function(test) {
var firstName = rules.sequence.capture(identifier(), "firstName");
var secondName = rules.sequence.capture(identifier(), "secondName");
var parser = rules.then(
rules.sequence(
secondName,
identifier(","),
firstName
),
rules.sequence.applyValues(function(firstName, secondName) {
return {first: firstName, second: secondName};
}, firstName, secondName)
);
var result = parseString(parser, "Bobertson , Bob");
assertIsSuccessWithValue(test, result, {first: "Bob", second: "Bobertson"});
test.done();
};
exports.canApplyValuesAndSourceFromSequenceToFunctionUsingMapOnSequenceRule = function(test) {
var firstName = rules.sequence.capture(identifier(), "firstName");
var secondName = rules.sequence.capture(identifier());
var parser = rules.sequence(
secondName,
identifier(","),
firstName
).map(function(secondName, firstName, source) {
return {first: firstName, second: secondName, source: source};
});
var result = parseString(parser, "Bobertson , Bob");
assertIsSuccessWithValue(test, result, {
first: "Bob",
second: "Bobertson",
source: stringSourceRange("Bobertson , Bob", 0, 15)
});
test.done();
};
exports.canApplyValuesWithSourceFromSequenceToFunction = function(test) {
var firstName = rules.sequence.capture(identifier(), "firstName");
var secondName = rules.sequence.capture(identifier(), "secondName");
var parser = rules.then(
rules.sequence(
secondName,
identifier(","),
firstName
),
rules.sequence.applyValues(function(firstName, secondName, source) {
return {first: firstName, second: secondName, source: source};
}, firstName, secondName, rules.sequence.source)
);
var result = parseString(parser, "Bobertson , Bob");
assertIsSuccessWithValue(test, result, {
first: "Bob",
second: "Bobertson",
source: stringSourceRange("Bobertson , Bob", 0, 15)
});
test.done();
};
exports.exceptionIfTryingToReadAValueThatHasntBeenCaptured = function(test) {
var name = rules.sequence.capture(identifier(), "name");
var parser = rules.sequence(identifier("("), identifier(")"));
var result = parseString(parser, "( )");
assertIsSuccess(test, result);
try {
result.value().get(name);
test.ok(false, "Expected exception");
} catch (error) {
test.equal(error.message, "No value for capture \"name\"");
}
test.done();
};
exports.exceptionIfTryingToCaptureValueWithUsedName = function(test) {
var firstName = rules.sequence.capture(identifier(), "name");
var secondName = rules.sequence.capture(identifier(), "name");
var parser = rules.sequence(secondName, identifier(","), firstName);
try {
parseString(parser, "Bobertson , Bob")
test.ok(false, "Expected exception");
} catch (error) {
test.equal(error.message, "Cannot add second value for capture \"name\"");
}
test.done();
};
exports.optionalRuleDoesNothingIfValueDoesNotMatch = function(test) {
var parser = rules.optional(identifier("("));
var result = parseString(parser, "");
assertIsSuccess(test, result);
test.deepEqual(result.value(), options.none);
test.done();
};
exports.optionalRuleConsumesInputIfPossible = function(test) {
var parser = rules.optional(identifier("("));
var result = parseString(parser, "(");
assertIsSuccess(test, result);
test.deepEqual(result.value(), options.some("("));
test.done();
};
exports.optionalRulePreservesErrors = function(test) {
var error = results.error([errors.error({
expected: "something",
actual: "something else"
})]);
var parser = rules.optional(function(input) {
return error;
});
var result = parseString(parser, "");
test.deepEqual(result, error);
test.done();
};
exports.zeroOrMoreWithSeparatorParsesEmptyStringAndReturnsEmptyArray = function(test) {
var parser = rules.zeroOrMoreWithSeparator(identifier(), identifier(","));
var result = parseString(parser, "");
assertIsSuccessWithValue(test, result, []);
test.done();
};
exports.zeroOrMoreWithSeparatorParsesSingleInstanceOfRuleAndReturnsSingleElementArray = function(test) {
var parser = rules.zeroOrMoreWithSeparator(identifier(), identifier(","));
var result = parseString(parser, "blah");
assertIsSuccessWithValue(test, result, ["blah"]);
test.done();
};
exports.zeroOrMoreWithSeparatorParsesMultipleInstanceOfRuleAndReturnsArray = function(test) {
var parser = rules.zeroOrMoreWithSeparator(identifier(), identifier(","));
var result = parseString(parser, "apple , banana , coconut");
assertIsSuccessWithValue(test, result, ["apple", "banana", "coconut"]);
test.done();
};
exports.zeroOrMoreWithSeparatorDoesNotConsumeFinalSeparatorIfItIsNotFollowedByMainRule = function(test) {
var parser = rules.zeroOrMoreWithSeparator(identifier(), identifier(","));
var result = parseString(parser, "apple , banana ,");
assertIsSuccess(test, result, {
remaining: [
token("identifier", ",", stringSourceRange("apple , banana ,", 15, 16)),
token("end", null, stringSourceRange("apple , banana ,", 16, 16))
],
});
test.done();
};
exports.zeroOrMoreReturnsErrorIfFirstUseOfRuleReturnsError = function(test) {
var parser = rules.zeroOrMoreWithSeparator(
rules.sequence(identifier(), rules.sequence.cut(), identifier()),
identifier(",")
);
var result = parseString(parser, "apple");
assertIsError(test, result);
test.done();
};
exports.zeroOrMoreParsesEmptyStringAndReturnsEmptyArray = function(test) {
var parser = rules.zeroOrMore(identifier());
var result = parseString(parser, "");
assertIsSuccessWithValue(test, result, []);
test.done();
};
exports.zeroOrMoreParsesSingleInstanceOfRuleAndReturnsSingleElementArray = function(test) {
var parser = rules.zeroOrMore(identifier());
var result = parseString(parser, "blah");
assertIsSuccessWithValue(test, result, ["blah"]);
test.done();
};
exports.zeroOrMoreParsesMultipleInstanceOfRuleAndReturnsArray = function(test) {
var parser = rules.zeroOrMore(identifier());
var result = parseString(parser, "( , )");
assertIsSuccessWithValue(test, result, ["(", ",", ")"]);
test.done();
};
exports.zeroOrMoreReturnsErrorIfSubRuleReturnsError = function(test) {
var parser = rules.zeroOrMore(
rules.sequence(identifier(), rules.sequence.cut(), identifier(";"))
);
var result = parseString(parser, "blah");
assertIsError(test, result, {
remaining:[
token("end", null, stringSourceRange("blah", 4, 4))
],
errors: [errors.error({
expected: "identifier \";\"",
actual: "end",
location: stringSourceRange("blah", 4, 4)
})]
});
test.done();
};
exports.oneOrMoreWithSeparatorFailsOnEmptyString = function(test) {
var parser = rules.oneOrMoreWithSeparator(identifier(), identifier(","));
var result = parseString(parser, "");
assertIsFailure(test, result, {
remaining:[
token("end", null, stringSourceRange("", 0, 0))
],
errors: [errors.error({
expected: "identifier",
actual: "end",
location: stringSourceRange("", 0, 0)
})]
});
test.done();
};
exports.oneOrMoreWithSeparatorParsesSingleInstanceOfRuleAndReturnsSingleElementArray = function(test) {
var parser = rules.oneOrMoreWithSeparator(identifier(), identifier(","));
var result = parseString(parser, "blah");
assertIsSuccessWithValue(test, result, ["blah"]);
test.done();
};
exports.oneOrMoreWithSeparatorParsesMultipleInstanceOfRuleAndReturnsArray = function(test) {
var parser = rules.oneOrMoreWithSeparator(identifier(), identifier(","));
var result = parseString(parser, "apple , banana , coconut");
assertIsSuccessWithValue(test, result, ["apple", "banana", "coconut"]);
test.done();
};
exports.oneOrMoreFailsOnEmptyString = function(test) {
var parser = rules.oneOrMore(identifier());
var result = parseString(parser, "");
assertIsFailure(test, result, {
remaining:[
token("end", null, stringSourceRange("", 0, 0))
],
errors: [errors.error({
expected: "identifier",
actual: "end",
location: stringSourceRange("", 0, 0)
})]
});
test.done();
};
exports.oneOrMoreParsesSingleInstanceOfRuleAndReturnsSingleElementArray = function(test) {
var parser = rules.oneOrMore(identifier());
var result = parseString(parser, "blah");
assertIsSuccessWithValue(test, result, ["blah"]);
test.done();
};
exports.oneOrMoreParsesMultipleInstanceOfRuleAndReturnsArray = function(test) {
var parser = rules.oneOrMore(identifier());
var result = parseString(parser, "apple banana coconut");
assertIsSuccessWithValue(test, result, ["apple", "banana", "coconut"]);
test.done();
};
exports.leftAssociativeConsumesNothingIfLeftHandSideDoesntMatch = function(test) {
var parser = rules.leftAssociative(
keyword(),
identifier("+"),
function(left, right) {
return [left, right];
}
);
var result = parseString(parser, "+ +");
assertIsFailure(test, result, {
remaining:[
token("identifier", "+", stringSourceRange("+ +", 0, 1)),
token("identifier", "+", stringSourceRange("+ +", 2, 3)),
token("end", null, stringSourceRange("+ +", 3, 3))
],
errors: [errors.error({
expected: "keyword",
actual: "identifier \"+\"",
location: stringSourceRange("+ +", 0, 1)
})]
});
test.done();
};
exports.leftAssociativeReturnsValueOfLeftHandSideIfRightHandSideDoesntMatch = function(test) {
var parser = rules.leftAssociative(
identifier(),
identifier("+"),
function(left, right) {
return [left, right];
}
);
var result = parseString(parser, "apple");
assertIsSuccessWithValue(test, result, "apple");
test.done();
};
exports.leftAssociativeAllowsLeftAssociativeRules = function(test) {
var parser = rules.leftAssociative(
identifier(),
identifier("+"),
function(left, right) {
return [left, right];
}
);
var result = parseString(parser, "apple + +");
assertIsSuccessWithValue(test, result, [["apple", "+"], "+"]);
test.done();
};
exports.leftAssociativeCanHaveMultipleChoicesForRight = function(test) {
var parser = rules.leftAssociative(
identifier(),
rules.leftAssociative.firstOf(
{rule: identifier("+"), func: function(left, right) { return [left, right]; }},
{rule: identifier(","), func: function(left, right) { return [left]; }}
)
);
var result = parseString(parser, "apple + ,");
assertIsSuccessWithValue(test, result, [["apple", "+"]]);
test.done();
};
exports.leftAssociativeReturnsErrorIfRightHandSideReturnsError = function(test) {
var parser = rules.leftAssociative(
identifier(),
rules.leftAssociative.firstOf(
{rule: rules.sequence(rules.sequence.cut(), identifier("+")), func: function() {}}
)
);
var result = parseString(parser, "apple");
assertIsError(test, result);
test.done();
};
exports.nonConsumingRuleDoesNotConsumeInput = function(test) {
var parser = rules.nonConsuming(rules.token("keyword", "true"));
var result = parseString(parser, "true");
assertIsSuccess(test, result, {
value: "true",
source: stringSourceRange("true", 0, 4),
remaining: [token("keyword", "true"), token("end", null)]
});
test.done();
};
var parseString = function(parser, string) {
var keywords = ["true", "false"];
var tokens = new Tokeniser({keywords: keywords}).tokenise(string);
return parser(new TokenIterator(tokens));
};
var parseTokens = function(parser, tokens) {
return parser(new TokenIterator(tokens));
};