All new
alle new
This commit is contained in:
1 parent
da64472251
commit
5b20adee48
1605 files changed
+396517
-1674
No files matched your search
+22
@@ -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.
|
||||
+248
@@ -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.
|
||||
+18
@@ -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);
|
||||
};
|
||||
};
|
||||
+53
@@ -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};
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
module.exports = function(name, value, source) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
if (source) {
|
||||
this.source = source;
|
||||
}
|
||||
};
|
||||
+28
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
.PHONY: test
|
||||
|
||||
test:
|
||||
node_modules/.bin/nodeunit `find test -name \*.test.js -type f`
|
||||
+24
@@ -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"
|
||||
}
|
||||
+21
@@ -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();
|
||||
};
|
||||
+17
@@ -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();
|
||||
};
|
||||
+50
@@ -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
|
||||
};
|
||||
};
|
||||
+38
@@ -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
@@ -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));
|
||||
};
|
||||
+36
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
||||
var results = require("../lib/parsing-results");
|
||||
|
||||
exports.errorIsThrownIfCreatingFailureWithoutAnyErrors = function(test) {
|
||||
test.throws(function() {
|
||||
results.failure([]);
|
||||
});
|
||||
test.done();
|
||||
};
|
||||
+617
@@ -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));
|
||||
};
|
||||
Reference in new issue
Block a user