Skip to content

Commit

Permalink
Merge pull request #178 from /issues/177
Browse files Browse the repository at this point in the history
Fixes #177 adds seqObj
  • Loading branch information
Brian Mock committed Jun 27, 2017
2 parents b7c2a23 + b80c2c3 commit 05de493
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 9 deletions.
39 changes: 36 additions & 3 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,14 @@ Returns a parser that doesn't consume any input, and yields `result`.

This is an alias for `Parsimmon.succeed(result)`.

## Parsimmon.formatError(string, error)

Takes the `string` passed to `parser.parse(string)` and the `error` returned from `parser.parse(string)` and turns it into a human readable error message string. Note that there are certainly better ways to format errors, so feel free to write your own.

## Parsimmon.seq(p1, p2, ...pn)

Accepts any number of parsers and returns a new parser that expects them to match in order, yielding an array of all their results.

## Parsimmon.formatError(string, error)

Takes the `string` passed to `parser.parse(string)` and the `error` returned from `parser.parse(string)` and turns it into a human readable error message string. Note that there are certainly better ways to format errors, so feel free to write your own.

## Parsimmon.seqMap(p1, p2, ...pn, function(r1, r2, ...rn))

Expand All @@ -204,6 +205,38 @@ Parsimmon.seqMap(
).parse('a+x')
```

## Parsimmon.seqObj(...args)

Similar to `Parsimmon.seq(...parsers)`, but yields an object of results named based on arguments.

Takes one or more arguments, where each argument is either a parser or a named parser pair (`[stringKey, parser]`).

Requires at least one named parser.

All named parser keys must be unique.

Example:

```js
var _ = Parsimmon.optWhitespace;
var identifier = Parsimmon.regexp(/[a-z_][a-z0-9_]*/i);
var lparen = Parsimmon.string('(');
var rparen = Parsimmon.string(')');
var comma = Parsimmon.string(',');
var functionCall =
Parsimmon.seqObj(
['function', identifier],
lparen,
['arguments', identifier.trim(_).sepBy(comma)],
rparen
);
functionCall.tryParse('foo(bar, baz, quux)');
// => { function: 'foo',
// arguments: [ 'bar', 'baz', 'quux' ] }
```

Tip: Combines well with `.node(name)` for a full-featured AST node.

## Parsimmon.alt(p1, p2, ...pn)

Accepts any number of parsers, yielding the value of the first one that succeeds, backtracking in between.
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## version 1.6.0 (2017-06-26)

* Adds `Parsimmon.seqObj(...args)`

## version 1.5.0 (2017-06-17)

NOTE: Code was completed on 2017-06-17, but due to human error, was not published on npm until 2017-06-26.

* Adds `parser.sepBy(separator)` alias for `Parsimmon.sepBy(parser, separator)`
* Adds `parser.sepBy1(separator)` alias for `Parsimmon.sepBy1(parser, separator)`
* Adds `Parsimmon.range(begin, end)`
Expand Down
10 changes: 6 additions & 4 deletions examples/python-ish.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,16 @@ let Pythonish = P.createLanguage({
// block, and require that every other statement has the same exact string of
// indentation in front of it.
Block: r =>
P.seq(
P.string('block:\n').then(P.regexp(/[ ]+/)),
r.Statement
P.seqObj(
P.string('block:'),
P.string('\n'),
['indent', P.regexp(/[ ]+/)],
['statement', r.Statement]
).chain(args => {
// `.chain` is called after a parser succeeds. It returns the next parser
// to use for parsing. This allows subsequent parsing to be dependent on
// previous text.
let [indent, statement] = args;
let {indent, statement} = args;
let indentSize = indent.length;
let currentSize = indentPeek();
// Indentation must be deeper than the current block context. Otherwise
Expand Down
44 changes: 44 additions & 0 deletions examples/seqobj.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

// Run me with Node to see my output!

let util = require('util');
let P = require('../');

///////////////////////////////////////////////////////////////////////

let Lang = P.createLanguage({

_: () => P.optWhitespace,

LParen: () => P.string('('),
RParen: () => P.string(')'),
Comma: () => P.string(','),
Dot: () => P.string('.'),

Identifier: () => P.letters.node('Identifier'),

MethodCall: r =>
P.seqObj(
['receiver', r.Identifier],
r.Dot.trim(r._),
['method', r.Identifier],
r.LParen,
['arguments', r.Identifier.trim(r._).sepBy(r.Comma)],
r.RParen
).node('MethodCall'),

});

///////////////////////////////////////////////////////////////////////

let text = 'console.log(bar, baz, quux)';

function prettyPrint(x) {
let opts = {depth: null, colors: 'auto'};
let s = util.inspect(x, opts);
console.log(s);
}

let ast = Lang.MethodCall.tryParse(text);
prettyPrint(ast);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parsimmon",
"version": "1.5.0",
"version": "1.6.0",
"description": "A monadic LL(infinity) parser combinator library",
"keywords": [
"parsing",
Expand Down
66 changes: 65 additions & 1 deletion src/parsimmon.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ function isParser(obj) {
return obj instanceof Parsimmon;
}

function isArray(x) {
return {}.toString.call(x) === '[object Array]';
}

function makeSuccess(index, value) {
return {
status: true,
Expand Down Expand Up @@ -107,7 +111,7 @@ function assertParser(p) {

// TODO[ES5]: Switch to Array.isArray eventually.
function assertArray(x) {
if ({}.toString.call(x) !== '[object Array]') {
if (!isArray(x)) {
throw new Error('not an array: ' + x);
}
}
Expand Down Expand Up @@ -203,6 +207,65 @@ function seq() {
});
}

function seqObj() {
var seenKeys = {};
var totalKeys = 0;
var parsers = [].slice.call(arguments);
var numParsers = parsers.length;
for (var j = 0; j < numParsers; j += 1) {
var p = parsers[j];
if (isParser(p)) {
continue;
}
if (isArray(p)) {
var isWellFormed =
p.length === 2 &&
typeof p[0] === 'string' &&
isParser(p[1]);
if (isWellFormed) {
var key = p[0];
if (seenKeys[key]) {
throw new Error('seqObj: duplicate key ' + key);
}
seenKeys[key] = true;
totalKeys++;
continue;
}
}
throw new Error(
'seqObj arguments must be parsers or ' +
'[string, parser] array pairs.'
);
}
if (totalKeys === 0) {
throw new Error('seqObj expects at least one named parser, found zero');
}
return Parsimmon(function(input, i) {
var result;
var accum = {};
for (var j = 0; j < numParsers; j += 1) {
var name;
var parser;
if (isArray(parsers[j])) {
name = parsers[j][0];
parser = parsers[j][1];
} else {
name = null;
parser = parsers[j];
}
result = mergeReplies(parser._(input, i), result);
if (!result.status) {
return result;
}
if (name) {
accum[name] = result.value;
}
i = result.index;
}
return mergeReplies(makeSuccess(i, accum), result);
});
}

function seqMap() {
var args = [].slice.call(arguments);
if (args.length === 0) {
Expand Down Expand Up @@ -720,6 +783,7 @@ Parsimmon.sepBy = sepBy;
Parsimmon.sepBy1 = sepBy1;
Parsimmon.seq = seq;
Parsimmon.seqMap = seqMap;
Parsimmon.seqObj = seqObj;
Parsimmon.string = string;
Parsimmon.succeed = succeed;
Parsimmon.takeWhile = takeWhile;
Expand Down
65 changes: 65 additions & 0 deletions test/core/seqObj.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

suite('Parsimmon.seqObj', function() {

test('does not accept duplicate keys', function() {
assert.throws(function() {
Parsimmon.seqObj(
['a', Parsimmon.of(1)],
['b', Parsimmon.of(2)],
['a', Parsimmon.of(3)]
);
});
});

test('requires at least one key', function() {
assert.throws(function() {
Parsimmon.seqObj();
});
});

test('every key is present in the result object', function() {
var parser = Parsimmon.seqObj(
['a', Parsimmon.of(1)],
['b', Parsimmon.of(2)],
['c', Parsimmon.of(3)]
);
var result = parser.tryParse('');
assert.deepStrictEqual(result, {
a: 1,
b: 2,
c: 3,
});
});

test('every parser is used', function() {
var parser = Parsimmon.seqObj(
Parsimmon.string('a'),
['b', Parsimmon.string('b')],
Parsimmon.string('c'),
['d', Parsimmon.string('d')],
Parsimmon.string('e')
);
var result = parser.tryParse('abcde');
assert.deepStrictEqual(result, {
b: 'b',
d: 'd',
});
});

test('named parser arrays are formed properly', function() {
assert.throws(function() {
Parsimmon.seqObj([]);
});
assert.throws(function() {
Parsimmon.seqObj(['a', Parsimmon.of(1), 'oops extra']);
});
assert.throws(function() {
Parsimmon.seqObj([123, Parsimmon.of(1)]);
});
assert.throws(function() {
Parsimmon.seqObj(['cool', 'potato']);
});
});

});

0 comments on commit 05de493

Please sign in to comment.