Skip to content

Commit 05de493

Browse files
author
Brian Mock
authored
Merge pull request #178 from /issues/177
Fixes #177 adds seqObj
2 parents b7c2a23 + b80c2c3 commit 05de493

File tree

7 files changed

+223
-9
lines changed

7 files changed

+223
-9
lines changed

API.md

+36-3
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,14 @@ Returns a parser that doesn't consume any input, and yields `result`.
178178

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

181+
## Parsimmon.formatError(string, error)
182+
183+
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.
184+
181185
## Parsimmon.seq(p1, p2, ...pn)
182186

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

185-
## Parsimmon.formatError(string, error)
186-
187-
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.
188189

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

@@ -204,6 +205,38 @@ Parsimmon.seqMap(
204205
).parse('a+x')
205206
```
206207

208+
## Parsimmon.seqObj(...args)
209+
210+
Similar to `Parsimmon.seq(...parsers)`, but yields an object of results named based on arguments.
211+
212+
Takes one or more arguments, where each argument is either a parser or a named parser pair (`[stringKey, parser]`).
213+
214+
Requires at least one named parser.
215+
216+
All named parser keys must be unique.
217+
218+
Example:
219+
220+
```js
221+
var _ = Parsimmon.optWhitespace;
222+
var identifier = Parsimmon.regexp(/[a-z_][a-z0-9_]*/i);
223+
var lparen = Parsimmon.string('(');
224+
var rparen = Parsimmon.string(')');
225+
var comma = Parsimmon.string(',');
226+
var functionCall =
227+
Parsimmon.seqObj(
228+
['function', identifier],
229+
lparen,
230+
['arguments', identifier.trim(_).sepBy(comma)],
231+
rparen
232+
);
233+
functionCall.tryParse('foo(bar, baz, quux)');
234+
// => { function: 'foo',
235+
// arguments: [ 'bar', 'baz', 'quux' ] }
236+
```
237+
238+
Tip: Combines well with `.node(name)` for a full-featured AST node.
239+
207240
## Parsimmon.alt(p1, p2, ...pn)
208241

209242
Accepts any number of parsers, yielding the value of the first one that succeeds, backtracking in between.

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
## version 1.6.0 (2017-06-26)
2+
3+
* Adds `Parsimmon.seqObj(...args)`
4+
15
## version 1.5.0 (2017-06-17)
26

7+
NOTE: Code was completed on 2017-06-17, but due to human error, was not published on npm until 2017-06-26.
8+
39
* Adds `parser.sepBy(separator)` alias for `Parsimmon.sepBy(parser, separator)`
410
* Adds `parser.sepBy1(separator)` alias for `Parsimmon.sepBy1(parser, separator)`
511
* Adds `Parsimmon.range(begin, end)`

examples/python-ish.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,16 @@ let Pythonish = P.createLanguage({
5151
// block, and require that every other statement has the same exact string of
5252
// indentation in front of it.
5353
Block: r =>
54-
P.seq(
55-
P.string('block:\n').then(P.regexp(/[ ]+/)),
56-
r.Statement
54+
P.seqObj(
55+
P.string('block:'),
56+
P.string('\n'),
57+
['indent', P.regexp(/[ ]+/)],
58+
['statement', r.Statement]
5759
).chain(args => {
5860
// `.chain` is called after a parser succeeds. It returns the next parser
5961
// to use for parsing. This allows subsequent parsing to be dependent on
6062
// previous text.
61-
let [indent, statement] = args;
63+
let {indent, statement} = args;
6264
let indentSize = indent.length;
6365
let currentSize = indentPeek();
6466
// Indentation must be deeper than the current block context. Otherwise

examples/seqobj.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
// Run me with Node to see my output!
4+
5+
let util = require('util');
6+
let P = require('../');
7+
8+
///////////////////////////////////////////////////////////////////////
9+
10+
let Lang = P.createLanguage({
11+
12+
_: () => P.optWhitespace,
13+
14+
LParen: () => P.string('('),
15+
RParen: () => P.string(')'),
16+
Comma: () => P.string(','),
17+
Dot: () => P.string('.'),
18+
19+
Identifier: () => P.letters.node('Identifier'),
20+
21+
MethodCall: r =>
22+
P.seqObj(
23+
['receiver', r.Identifier],
24+
r.Dot.trim(r._),
25+
['method', r.Identifier],
26+
r.LParen,
27+
['arguments', r.Identifier.trim(r._).sepBy(r.Comma)],
28+
r.RParen
29+
).node('MethodCall'),
30+
31+
});
32+
33+
///////////////////////////////////////////////////////////////////////
34+
35+
let text = 'console.log(bar, baz, quux)';
36+
37+
function prettyPrint(x) {
38+
let opts = {depth: null, colors: 'auto'};
39+
let s = util.inspect(x, opts);
40+
console.log(s);
41+
}
42+
43+
let ast = Lang.MethodCall.tryParse(text);
44+
prettyPrint(ast);

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parsimmon",
3-
"version": "1.5.0",
3+
"version": "1.6.0",
44
"description": "A monadic LL(infinity) parser combinator library",
55
"keywords": [
66
"parsing",

src/parsimmon.js

+65-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ function isParser(obj) {
1515
return obj instanceof Parsimmon;
1616
}
1717

18+
function isArray(x) {
19+
return {}.toString.call(x) === '[object Array]';
20+
}
21+
1822
function makeSuccess(index, value) {
1923
return {
2024
status: true,
@@ -107,7 +111,7 @@ function assertParser(p) {
107111

108112
// TODO[ES5]: Switch to Array.isArray eventually.
109113
function assertArray(x) {
110-
if ({}.toString.call(x) !== '[object Array]') {
114+
if (!isArray(x)) {
111115
throw new Error('not an array: ' + x);
112116
}
113117
}
@@ -203,6 +207,65 @@ function seq() {
203207
});
204208
}
205209

210+
function seqObj() {
211+
var seenKeys = {};
212+
var totalKeys = 0;
213+
var parsers = [].slice.call(arguments);
214+
var numParsers = parsers.length;
215+
for (var j = 0; j < numParsers; j += 1) {
216+
var p = parsers[j];
217+
if (isParser(p)) {
218+
continue;
219+
}
220+
if (isArray(p)) {
221+
var isWellFormed =
222+
p.length === 2 &&
223+
typeof p[0] === 'string' &&
224+
isParser(p[1]);
225+
if (isWellFormed) {
226+
var key = p[0];
227+
if (seenKeys[key]) {
228+
throw new Error('seqObj: duplicate key ' + key);
229+
}
230+
seenKeys[key] = true;
231+
totalKeys++;
232+
continue;
233+
}
234+
}
235+
throw new Error(
236+
'seqObj arguments must be parsers or ' +
237+
'[string, parser] array pairs.'
238+
);
239+
}
240+
if (totalKeys === 0) {
241+
throw new Error('seqObj expects at least one named parser, found zero');
242+
}
243+
return Parsimmon(function(input, i) {
244+
var result;
245+
var accum = {};
246+
for (var j = 0; j < numParsers; j += 1) {
247+
var name;
248+
var parser;
249+
if (isArray(parsers[j])) {
250+
name = parsers[j][0];
251+
parser = parsers[j][1];
252+
} else {
253+
name = null;
254+
parser = parsers[j];
255+
}
256+
result = mergeReplies(parser._(input, i), result);
257+
if (!result.status) {
258+
return result;
259+
}
260+
if (name) {
261+
accum[name] = result.value;
262+
}
263+
i = result.index;
264+
}
265+
return mergeReplies(makeSuccess(i, accum), result);
266+
});
267+
}
268+
206269
function seqMap() {
207270
var args = [].slice.call(arguments);
208271
if (args.length === 0) {
@@ -720,6 +783,7 @@ Parsimmon.sepBy = sepBy;
720783
Parsimmon.sepBy1 = sepBy1;
721784
Parsimmon.seq = seq;
722785
Parsimmon.seqMap = seqMap;
786+
Parsimmon.seqObj = seqObj;
723787
Parsimmon.string = string;
724788
Parsimmon.succeed = succeed;
725789
Parsimmon.takeWhile = takeWhile;

test/core/seqObj.test.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
3+
suite('Parsimmon.seqObj', function() {
4+
5+
test('does not accept duplicate keys', function() {
6+
assert.throws(function() {
7+
Parsimmon.seqObj(
8+
['a', Parsimmon.of(1)],
9+
['b', Parsimmon.of(2)],
10+
['a', Parsimmon.of(3)]
11+
);
12+
});
13+
});
14+
15+
test('requires at least one key', function() {
16+
assert.throws(function() {
17+
Parsimmon.seqObj();
18+
});
19+
});
20+
21+
test('every key is present in the result object', function() {
22+
var parser = Parsimmon.seqObj(
23+
['a', Parsimmon.of(1)],
24+
['b', Parsimmon.of(2)],
25+
['c', Parsimmon.of(3)]
26+
);
27+
var result = parser.tryParse('');
28+
assert.deepStrictEqual(result, {
29+
a: 1,
30+
b: 2,
31+
c: 3,
32+
});
33+
});
34+
35+
test('every parser is used', function() {
36+
var parser = Parsimmon.seqObj(
37+
Parsimmon.string('a'),
38+
['b', Parsimmon.string('b')],
39+
Parsimmon.string('c'),
40+
['d', Parsimmon.string('d')],
41+
Parsimmon.string('e')
42+
);
43+
var result = parser.tryParse('abcde');
44+
assert.deepStrictEqual(result, {
45+
b: 'b',
46+
d: 'd',
47+
});
48+
});
49+
50+
test('named parser arrays are formed properly', function() {
51+
assert.throws(function() {
52+
Parsimmon.seqObj([]);
53+
});
54+
assert.throws(function() {
55+
Parsimmon.seqObj(['a', Parsimmon.of(1), 'oops extra']);
56+
});
57+
assert.throws(function() {
58+
Parsimmon.seqObj([123, Parsimmon.of(1)]);
59+
});
60+
assert.throws(function() {
61+
Parsimmon.seqObj(['cool', 'potato']);
62+
});
63+
});
64+
65+
});

0 commit comments

Comments
 (0)