parser-combinators 1.2.2 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  ## Parser Combinators
2
2
 
3
3
  [![CI](https://github.com/michalusio/Parser/actions/workflows/CI.yml/badge.svg)](https://github.com/michalusio/Parser/actions/workflows/CI.yml)
4
- [![CodeQL](https://github.com/michalusio/Parser/actions/workflows/codeql-analysis.yml/badge.svg?branch=master)](https://github.com/michalusio/Parser/actions/workflows/codeql-analysis.yml)
5
4
  [![codecov](https://codecov.io/gh/michalusio/Parser/branch/master/graph/badge.svg?token=gtTR0hPodY)](https://codecov.io/gh/michalusio/Parser)
6
5
 
7
6
  ![NPM](https://img.shields.io/npm/l/parser-combinators)
package/dist/parser.js CHANGED
@@ -12,7 +12,7 @@ const types_1 = require("./types");
12
12
  function ParseText(text, parser, path = '') {
13
13
  const res = parser({ text, path, index: 0 });
14
14
  if ((0, types_1.isFailure)(res)) {
15
- throw new types_1.ParseError(`Parse error, expected ${[...res.history].pop()} at char ${res.ctx.index}`, text, res.ctx.index, res.history);
15
+ throw new types_1.ParseError(`Parse error, expected ${res.history[res.history.length - 1]} at char ${res.ctx.index}`, text, res.ctx.index, res.history);
16
16
  }
17
17
  if (res.ctx.index !== text.length) {
18
18
  throw new types_1.ParseError(`Parse error, expected end of text at char ${res.ctx.index}`, text, res.ctx.index, []);
@@ -3,22 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.any = any;
4
4
  const types_1 = require("../types");
5
5
  const anyString_1 = require("./anyString");
6
- const utilities_1 = require("./utilities");
7
- const optimizableTypes = ['str', 'stri', 'anyString'];
6
+ const optimizations_1 = require("./optimizations");
8
7
  function any(...parsers) {
9
8
  let marker = {};
10
- if ((0, utilities_1.shouldPerformFusions)() && parsers.every(p => 'parserType' in p && typeof p.parserType === 'string' && optimizableTypes.includes(p.parserType))) {
11
- const optimizableParsers = parsers;
12
- const matches = optimizableParsers.flatMap(p => {
13
- switch (p.parserType) {
14
- case 'anyString':
15
- return p.matches;
16
- case 'str':
17
- return [[p.match, false]];
18
- case 'stri':
19
- return [[p.match, true]];
20
- }
21
- });
9
+ if ((0, optimizations_1.shouldPerformFusions)() && (0, optimizations_1.allStringParsers)(parsers)) {
10
+ const matches = parsers.flatMap(p => p.matches);
22
11
  // Not fusing if not enough matches
23
12
  // We do pass the matches forward though, because a parser above may want to fuse
24
13
  if (matches.length > 10) {
@@ -33,8 +22,9 @@ function any(...parsers) {
33
22
  for (const parser of parsers) {
34
23
  const res = parser(ctx);
35
24
  if ((0, types_1.isFailure)(res)) {
36
- if (res.history.includes('surely')) {
37
- return (0, types_1.failure)(res.ctx, res.expected, res.history.filter(h => h !== 'surely'));
25
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
26
+ if (surelyIndex >= 0) {
27
+ return (0, types_1.failure)(res.ctx, res.expected, ['any', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
38
28
  }
39
29
  expected.push(res);
40
30
  }
@@ -1,15 +1,12 @@
1
1
  import { Parser } from "../types";
2
- import { StrIParser, StrParser } from "./str";
3
- type AnyStringParser<T> = Parser<T> & {
2
+ export type AnyStringParser<T> = Parser<T> & {
4
3
  parserType: 'anyString';
5
4
  /**
6
5
  * if flag is true, match case-insensitive
7
6
  */
8
- matches: (readonly [string, boolean])[];
7
+ matches: (readonly [string, boolean, (v: string) => unknown])[];
9
8
  };
10
- export type OptimizableStrParser<T> = AnyStringParser<T> | StrParser<T> | StrIParser<T>;
11
9
  /**
12
10
  * Optimization for `any(str(), str(), ...)` which replaces the parser tree with one parser which tries all strings together
13
11
  */
14
- export declare function anyString<T extends string>(matches: (readonly [string, boolean])[]): AnyStringParser<T>;
15
- export {};
12
+ export declare function anyString<T>(matches: (readonly [string, boolean, (v: string) => unknown])[]): AnyStringParser<T>;
@@ -12,7 +12,7 @@ function anyString(matches) {
12
12
  tree ??= createSearchTree(matches);
13
13
  const result = searchThroughTree(tree, ctx.text, ctx.index);
14
14
  if (result) {
15
- return (0, types_1.success)({ ...ctx, index: result.end }, matches[result.matchIndex][0]);
15
+ return (0, types_1.success)({ ...ctx, index: result.end }, matches[result.matchIndex][2](matches[result.matchIndex][0]));
16
16
  }
17
17
  else {
18
18
  return (0, types_1.failure)(ctx, lastMatchInQuotes, ['any', lastMatchInQuotes]);
@@ -27,7 +27,9 @@ function createSearchTree(matches) {
27
27
  function addMatchToTree(node, match, idIndex, charIndex = 0) {
28
28
  if (charIndex >= match[0].length) {
29
29
  if (node.matchIndex != undefined) {
30
- node.matchIndex = Math.min(node.matchIndex, idIndex);
30
+ if (node.matchIndex > idIndex) {
31
+ node.matchIndex = idIndex;
32
+ }
31
33
  }
32
34
  else {
33
35
  node.matchIndex = idIndex;
@@ -2,19 +2,23 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.between = between;
4
4
  const types_1 = require("../types");
5
- const seq_1 = require("./seq");
6
5
  /** Parses a sequence of three parsers.
7
6
  * @returns A parser returning only the middle parser's result.
8
7
  */
9
8
  function between(left, parser, right) {
10
- const sequence = (0, seq_1.seq)(left, parser, right);
11
9
  return (ctx) => {
12
- const res = sequence(ctx);
13
- if ((0, types_1.isFailure)(res)) {
14
- const newHistory = [...res.history];
15
- newHistory.splice(0, 1);
16
- return (0, types_1.failure)(res.ctx, res.expected, ['between', ...newHistory]);
10
+ const resLeft = left(ctx);
11
+ if ((0, types_1.isFailure)(resLeft)) {
12
+ return resLeft;
17
13
  }
18
- return { ...res, value: res.value[1] };
14
+ const resParse = parser(resLeft.ctx);
15
+ if ((0, types_1.isFailure)(resParse)) {
16
+ return resParse;
17
+ }
18
+ const resRight = right(resParse.ctx);
19
+ if ((0, types_1.isFailure)(resRight)) {
20
+ return resRight;
21
+ }
22
+ return (0, types_1.success)(resRight.ctx, resParse.value);
19
23
  };
20
24
  }
@@ -1,4 +1,4 @@
1
- import { Parser } from '../types';
1
+ import { Parser } from "../types";
2
2
  /** Parses zero or more occurences of the given parser.
3
3
  * @returns A parser returning an array of many parses.
4
4
  */
@@ -6,7 +6,7 @@ export declare function many<T>(parser: Parser<T>): Parser<T[]>;
6
6
  /** Parses zero or more occurences of the given parser, separated with the separator parser.
7
7
  * @returns A parser returning an array of many parses, omitting the separator.
8
8
  */
9
- export declare function zeroOrMany<T, V>(item: Parser<T>, separator: Parser<V>): Parser<T[]>;
9
+ export declare function zeroOrMany<T, V>(item: Parser<T>, separator?: Parser<V> | undefined): Parser<T[]>;
10
10
  /** Parses one or more occurences of the given parser, separated with the separator parser.
11
11
  * @returns A parser returning an array of many parses, omitting the separator.
12
12
  */
@@ -5,9 +5,6 @@ exports.zeroOrMany = zeroOrMany;
5
5
  exports.oneOrMany = oneOrMany;
6
6
  exports.oneOrManyRed = oneOrManyRed;
7
7
  const types_1 = require("../types");
8
- const map_1 = require("./map");
9
- const opt_1 = require("./opt");
10
- const seq_1 = require("./seq");
11
8
  /** Parses zero or more occurences of the given parser.
12
9
  * @returns A parser returning an array of many parses.
13
10
  */
@@ -17,6 +14,10 @@ function many(parser) {
17
14
  while (true) {
18
15
  const res = parser(ctx);
19
16
  if ((0, types_1.isFailure)(res)) {
17
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
18
+ if (surelyIndex >= 0) {
19
+ return (0, types_1.failure)(res.ctx, res.expected, ['many', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
20
+ }
20
21
  return (0, types_1.success)(ctx, results);
21
22
  }
22
23
  ctx = res.ctx;
@@ -27,35 +28,136 @@ function many(parser) {
27
28
  /** Parses zero or more occurences of the given parser, separated with the separator parser.
28
29
  * @returns A parser returning an array of many parses, omitting the separator.
29
30
  */
30
- function zeroOrMany(item, separator) {
31
- return (0, map_1.map)((0, opt_1.opt)(oneOrMany(item, separator)), t => t ?? []);
31
+ function zeroOrMany(item, separator = undefined) {
32
+ if (separator) {
33
+ return (ctx) => {
34
+ const results = [];
35
+ const res = item(ctx);
36
+ if ((0, types_1.isFailure)(res)) {
37
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
38
+ if (surelyIndex >= 0) {
39
+ return (0, types_1.failure)(res.ctx, res.expected, ['zeroOrMany', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
40
+ }
41
+ return (0, types_1.success)(ctx, results);
42
+ }
43
+ ctx = res.ctx;
44
+ results.push(res.value);
45
+ while (true) {
46
+ const resSep = separator(ctx);
47
+ if ((0, types_1.isFailure)(resSep)) {
48
+ const surelyIndex = resSep.history.findIndex(h => h === 'surely');
49
+ if (surelyIndex >= 0) {
50
+ return (0, types_1.failure)(resSep.ctx, resSep.expected, ['zeroOrMany', ...resSep.history.slice(0, surelyIndex), ...resSep.history.slice(surelyIndex + 1)]);
51
+ }
52
+ return (0, types_1.success)(ctx, results);
53
+ }
54
+ const res = item(resSep.ctx);
55
+ if ((0, types_1.isFailure)(res)) {
56
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
57
+ if (surelyIndex >= 0) {
58
+ return (0, types_1.failure)(res.ctx, res.expected, ['zeroOrMany', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
59
+ }
60
+ return (0, types_1.success)(ctx, results);
61
+ }
62
+ ctx = res.ctx;
63
+ results.push(res.value);
64
+ }
65
+ };
66
+ }
67
+ else
68
+ return many(item);
32
69
  }
33
70
  /** Parses one or more occurences of the given parser, separated with the separator parser.
34
71
  * @returns A parser returning an array of many parses, omitting the separator.
35
72
  */
36
73
  function oneOrMany(item, separator = undefined) {
37
- const sequencer = (0, map_1.map)((0, seq_1.seq)(item, many(separator
38
- ? (0, map_1.map)((0, seq_1.seq)(separator, item), ([, t]) => t)
39
- : item)), ([t, ts]) => ([t, ...ts]));
40
- return (ctx) => {
41
- const res = sequencer(ctx);
42
- if ((0, types_1.isFailure)(res)) {
43
- const newHistory = [...res.history];
44
- newHistory.splice(0, 2);
45
- return (0, types_1.failure)(res.ctx, res.expected, ['oneOrMany', ...newHistory]);
46
- }
47
- return res;
48
- };
74
+ if (separator) {
75
+ return (ctx) => {
76
+ const results = [];
77
+ const res = item(ctx);
78
+ if ((0, types_1.isFailure)(res))
79
+ return res;
80
+ ctx = res.ctx;
81
+ results.push(res.value);
82
+ while (true) {
83
+ const resSep = separator(ctx);
84
+ if ((0, types_1.isFailure)(resSep)) {
85
+ const surelyIndex = resSep.history.findIndex(h => h === 'surely');
86
+ if (surelyIndex >= 0) {
87
+ return (0, types_1.failure)(resSep.ctx, resSep.expected, ['oneOrMany', ...resSep.history.slice(0, surelyIndex), ...resSep.history.slice(surelyIndex + 1)]);
88
+ }
89
+ return (0, types_1.success)(ctx, results);
90
+ }
91
+ const res = item(resSep.ctx);
92
+ if ((0, types_1.isFailure)(res)) {
93
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
94
+ if (surelyIndex >= 0) {
95
+ return (0, types_1.failure)(res.ctx, res.expected, ['oneOrMany', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
96
+ }
97
+ return (0, types_1.success)(ctx, results);
98
+ }
99
+ ctx = res.ctx;
100
+ results.push(res.value);
101
+ }
102
+ };
103
+ }
104
+ else {
105
+ return (ctx) => {
106
+ const results = [];
107
+ const res = item(ctx);
108
+ if ((0, types_1.isFailure)(res))
109
+ return res;
110
+ ctx = res.ctx;
111
+ results.push(res.value);
112
+ while (true) {
113
+ const res = item(ctx);
114
+ if ((0, types_1.isFailure)(res)) {
115
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
116
+ if (surelyIndex >= 0) {
117
+ return (0, types_1.failure)(res.ctx, res.expected, ['oneOrMany', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
118
+ }
119
+ return (0, types_1.success)(ctx, results);
120
+ }
121
+ ctx = res.ctx;
122
+ results.push(res.value);
123
+ }
124
+ };
125
+ }
49
126
  }
50
127
  /** Parses one or more occurences of the given parser, separated with the separator parser.
51
128
  * @returns A parser returning the result of many parses, reduced using the `reducer` function passed in.
52
129
  */
53
130
  function oneOrManyRed(item, separator, reducer) {
54
- return (0, map_1.map)((0, map_1.map)((0, seq_1.seq)((0, map_1.map)(item, (x) => [null, x]), many((0, seq_1.seq)(separator, item))), ([t, ts]) => ([t, ...ts])), ts => {
55
- let result = ts[0][1];
56
- for (let i = 1; i < ts.length; i++) {
57
- result = reducer(result, ts[i][1], ts[i][0]);
131
+ return (ctx) => {
132
+ const res = item(ctx);
133
+ if ((0, types_1.isFailure)(res))
134
+ return res;
135
+ ctx = res.ctx;
136
+ let result = res.value;
137
+ while (true) {
138
+ const resSep = separator(ctx);
139
+ if ((0, types_1.isFailure)(resSep)) {
140
+ const surelyIndex = resSep.history.findIndex(h => h === 'surely');
141
+ if (surelyIndex >= 0) {
142
+ return (0, types_1.failure)(resSep.ctx, resSep.expected, ['oneOrManyRed', ...resSep.history.slice(0, surelyIndex), ...resSep.history.slice(surelyIndex + 1)]);
143
+ }
144
+ return (0, types_1.success)(ctx, result);
145
+ }
146
+ const res = item(resSep.ctx);
147
+ if ((0, types_1.isFailure)(res)) {
148
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
149
+ if (surelyIndex >= 0) {
150
+ return (0, types_1.failure)(res.ctx, res.expected, ['oneOrManyRed', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
151
+ }
152
+ return (0, types_1.success)(ctx, result);
153
+ }
154
+ ctx = res.ctx;
155
+ try {
156
+ result = reducer(result, res.value, resSep.value);
157
+ }
158
+ catch {
159
+ return (0, types_1.failure)(res.ctx, 'Error while reducing', ['oneOrManyRed']);
160
+ }
58
161
  }
59
- return result;
60
- });
162
+ };
61
163
  }
@@ -2,11 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.map = map;
4
4
  const types_1 = require("../types");
5
+ const optimizations_1 = require("./optimizations");
5
6
  /** Parses the input using the given parser, then maps the output.
6
7
  * @returns A parser that parses the same input, but has output mapped using the mapping function.
7
8
  */
8
9
  function map(parser, mapper) {
9
- return (ctx) => {
10
+ let marker = {};
11
+ if ((0, optimizations_1.shouldPerformFusions)() && (0, optimizations_1.isStringParser)(parser)) {
12
+ marker = { parserType: 'anyString', matches: parser.matches.map(m => ([m[0], m[1], (v) => mapper(m[2](v))])) };
13
+ }
14
+ return Object.assign((ctx) => {
10
15
  const res = parser(ctx);
11
16
  if ((0, types_1.isFailure)(res))
12
17
  return (0, types_1.failure)(res.ctx, res.expected, ['map', ...res.history]);
@@ -17,5 +22,5 @@ function map(parser, mapper) {
17
22
  catch {
18
23
  return (0, types_1.failure)(res.ctx, 'Error while mapping', ['map']);
19
24
  }
20
- };
25
+ }, marker);
21
26
  }
@@ -0,0 +1,11 @@
1
+ import { Parser } from "../types";
2
+ import { AnyStringParser } from "./anyString";
3
+ export declare const shouldPerformFusions: () => boolean;
4
+ /**
5
+ * Disables all parser combinator fusions.
6
+ *
7
+ * Should probably be used only for testing performance.
8
+ */
9
+ export declare const toggleFusions: (value: boolean) => void;
10
+ export declare function isStringParser<T>(parser: Parser<T>): parser is AnyStringParser<T>;
11
+ export declare function allStringParsers<T>(parsers: Parser<T>[]): parsers is AnyStringParser<T>[];
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toggleFusions = exports.shouldPerformFusions = void 0;
4
+ exports.isStringParser = isStringParser;
5
+ exports.allStringParsers = allStringParsers;
6
+ const shouldPerformFusions = () => performFusions;
7
+ exports.shouldPerformFusions = shouldPerformFusions;
8
+ let performFusions = true;
9
+ /**
10
+ * Disables all parser combinator fusions.
11
+ *
12
+ * Should probably be used only for testing performance.
13
+ */
14
+ const toggleFusions = (value) => {
15
+ performFusions = value;
16
+ };
17
+ exports.toggleFusions = toggleFusions;
18
+ function isStringParser(parser) {
19
+ return 'parserType' in parser && typeof parser.parserType === 'string' && parser.parserType === 'anyString';
20
+ }
21
+ function allStringParsers(parsers) {
22
+ return parsers.every(isStringParser);
23
+ }
@@ -1,17 +1,9 @@
1
1
  import { Parser } from '../types';
2
- export type StrParser<T> = Parser<T> & {
3
- parserType: 'str';
4
- match: T;
5
- };
6
2
  /** Parses a string and returns it as a result of the parse.
7
3
  * @returns A parser parsing a given string, case-sensitive.
8
4
  */
9
- export declare function str<T extends string>(match: T): StrParser<T>;
10
- export type StrIParser<T> = Parser<T> & {
11
- parserType: 'stri';
12
- match: T;
13
- };
5
+ export declare function str<T extends string>(match: T): Parser<T>;
14
6
  /** Parses a string case-insensitively and returns it as a result of the parse.
15
7
  * @returns A parser parsing a given string, case-insensitive.
16
8
  */
17
- export declare function stri<T extends string>(match: T): StrIParser<Lowercase<T>>;
9
+ export declare function stri<T extends string>(match: T): Parser<Lowercase<T>>;
@@ -15,7 +15,7 @@ function str(match) {
15
15
  else {
16
16
  return (0, types_1.failure)(ctx, inQuotes, [inQuotes]);
17
17
  }
18
- }, { parserType: 'str', match });
18
+ }, { parserType: 'anyString', matches: [[match, false, (v) => v]] });
19
19
  }
20
20
  const collator = new Intl.Collator('en', { sensitivity: 'accent' });
21
21
  /** Parses a string case-insensitively and returns it as a result of the parse.
@@ -32,5 +32,5 @@ function stri(match) {
32
32
  else {
33
33
  return (0, types_1.failure)(ctx, inQuotes, [inQuotes]);
34
34
  }
35
- }, { parserType: 'stri', match: lowercase });
35
+ }, { parserType: 'anyString', matches: [[lowercase, true, (v) => v]] });
36
36
  }
@@ -1,11 +1,4 @@
1
1
  import { Parser, Token } from '../types';
2
- export declare const shouldPerformFusions: () => boolean;
3
- /**
4
- * Disables all parser combinator fusions.
5
- *
6
- * Should probably be used only for testing performance.
7
- */
8
- export declare const toggleFusions: (value: boolean) => void;
9
2
  /** Allows to make a condition on the result of the parsing function.
10
3
  * @returns A parser returning the same but also performing a given check on the result.
11
4
  */
@@ -31,3 +24,7 @@ export declare function token<T>(parser: Parser<T>): Parser<Token<T>>;
31
24
  * Checks whether the parser parses successfully, but doesn't move the cursor forward
32
25
  */
33
26
  export declare function lookaround<T>(parser: Parser<T>): Parser<void>;
27
+ /**
28
+ * Turns a function creating a parser into a parser. Useful for recursive grammars.
29
+ */
30
+ export declare function lazy<T>(parserGetter: () => Parser<T>): Parser<T>;
@@ -1,25 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toggleFusions = exports.shouldPerformFusions = void 0;
4
3
  exports.ref = ref;
5
4
  exports.expect = expect;
6
5
  exports.expectErase = expectErase;
7
6
  exports.surely = surely;
8
7
  exports.token = token;
9
8
  exports.lookaround = lookaround;
9
+ exports.lazy = lazy;
10
10
  const types_1 = require("../types");
11
- const shouldPerformFusions = () => performFusions;
12
- exports.shouldPerformFusions = shouldPerformFusions;
13
- let performFusions = true;
14
- /**
15
- * Disables all parser combinator fusions.
16
- *
17
- * Should probably be used only for testing performance.
18
- */
19
- const toggleFusions = (value) => {
20
- performFusions = value;
21
- };
22
- exports.toggleFusions = toggleFusions;
23
11
  /** Allows to make a condition on the result of the parsing function.
24
12
  * @returns A parser returning the same but also performing a given check on the result.
25
13
  */
@@ -94,3 +82,13 @@ function lookaround(parser) {
94
82
  return (0, types_1.failure)(ctx, result.expected, ['lookaround', ...result.history]);
95
83
  };
96
84
  }
85
+ /**
86
+ * Turns a function creating a parser into a parser. Useful for recursive grammars.
87
+ */
88
+ function lazy(parserGetter) {
89
+ let parser;
90
+ return (ctx) => {
91
+ parser ??= parserGetter();
92
+ return parser(ctx);
93
+ };
94
+ }
@@ -9,7 +9,7 @@ import { isFailure, ParseError } from './types';
9
9
  export function ParseText(text, parser, path = '') {
10
10
  const res = parser({ text, path, index: 0 });
11
11
  if (isFailure(res)) {
12
- throw new ParseError(`Parse error, expected ${[...res.history].pop()} at char ${res.ctx.index}`, text, res.ctx.index, res.history);
12
+ throw new ParseError(`Parse error, expected ${res.history[res.history.length - 1]} at char ${res.ctx.index}`, text, res.ctx.index, res.history);
13
13
  }
14
14
  if (res.ctx.index !== text.length) {
15
15
  throw new ParseError(`Parse error, expected end of text at char ${res.ctx.index}`, text, res.ctx.index, []);
@@ -1,21 +1,10 @@
1
1
  import { failure, isFailure } from '../types';
2
2
  import { anyString } from './anyString';
3
- import { shouldPerformFusions } from './utilities';
4
- const optimizableTypes = ['str', 'stri', 'anyString'];
3
+ import { shouldPerformFusions, allStringParsers } from './optimizations';
5
4
  export function any(...parsers) {
6
5
  let marker = {};
7
- if (shouldPerformFusions() && parsers.every(p => 'parserType' in p && typeof p.parserType === 'string' && optimizableTypes.includes(p.parserType))) {
8
- const optimizableParsers = parsers;
9
- const matches = optimizableParsers.flatMap(p => {
10
- switch (p.parserType) {
11
- case 'anyString':
12
- return p.matches;
13
- case 'str':
14
- return [[p.match, false]];
15
- case 'stri':
16
- return [[p.match, true]];
17
- }
18
- });
6
+ if (shouldPerformFusions() && allStringParsers(parsers)) {
7
+ const matches = parsers.flatMap(p => p.matches);
19
8
  // Not fusing if not enough matches
20
9
  // We do pass the matches forward though, because a parser above may want to fuse
21
10
  if (matches.length > 10) {
@@ -30,8 +19,9 @@ export function any(...parsers) {
30
19
  for (const parser of parsers) {
31
20
  const res = parser(ctx);
32
21
  if (isFailure(res)) {
33
- if (res.history.includes('surely')) {
34
- return failure(res.ctx, res.expected, res.history.filter(h => h !== 'surely'));
22
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
23
+ if (surelyIndex >= 0) {
24
+ return failure(res.ctx, res.expected, ['any', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
35
25
  }
36
26
  expected.push(res);
37
27
  }
@@ -1,15 +1,12 @@
1
1
  import { Parser } from "../types";
2
- import { StrIParser, StrParser } from "./str";
3
- type AnyStringParser<T> = Parser<T> & {
2
+ export type AnyStringParser<T> = Parser<T> & {
4
3
  parserType: 'anyString';
5
4
  /**
6
5
  * if flag is true, match case-insensitive
7
6
  */
8
- matches: (readonly [string, boolean])[];
7
+ matches: (readonly [string, boolean, (v: string) => unknown])[];
9
8
  };
10
- export type OptimizableStrParser<T> = AnyStringParser<T> | StrParser<T> | StrIParser<T>;
11
9
  /**
12
10
  * Optimization for `any(str(), str(), ...)` which replaces the parser tree with one parser which tries all strings together
13
11
  */
14
- export declare function anyString<T extends string>(matches: (readonly [string, boolean])[]): AnyStringParser<T>;
15
- export {};
12
+ export declare function anyString<T>(matches: (readonly [string, boolean, (v: string) => unknown])[]): AnyStringParser<T>;
@@ -9,7 +9,7 @@ export function anyString(matches) {
9
9
  tree ??= createSearchTree(matches);
10
10
  const result = searchThroughTree(tree, ctx.text, ctx.index);
11
11
  if (result) {
12
- return success({ ...ctx, index: result.end }, matches[result.matchIndex][0]);
12
+ return success({ ...ctx, index: result.end }, matches[result.matchIndex][2](matches[result.matchIndex][0]));
13
13
  }
14
14
  else {
15
15
  return failure(ctx, lastMatchInQuotes, ['any', lastMatchInQuotes]);
@@ -24,7 +24,9 @@ function createSearchTree(matches) {
24
24
  function addMatchToTree(node, match, idIndex, charIndex = 0) {
25
25
  if (charIndex >= match[0].length) {
26
26
  if (node.matchIndex != undefined) {
27
- node.matchIndex = Math.min(node.matchIndex, idIndex);
27
+ if (node.matchIndex > idIndex) {
28
+ node.matchIndex = idIndex;
29
+ }
28
30
  }
29
31
  else {
30
32
  node.matchIndex = idIndex;
@@ -1,17 +1,21 @@
1
- import { failure, isFailure } from '../types';
2
- import { seq } from './seq';
1
+ import { isFailure, success } from '../types';
3
2
  /** Parses a sequence of three parsers.
4
3
  * @returns A parser returning only the middle parser's result.
5
4
  */
6
5
  export function between(left, parser, right) {
7
- const sequence = seq(left, parser, right);
8
6
  return (ctx) => {
9
- const res = sequence(ctx);
10
- if (isFailure(res)) {
11
- const newHistory = [...res.history];
12
- newHistory.splice(0, 1);
13
- return failure(res.ctx, res.expected, ['between', ...newHistory]);
7
+ const resLeft = left(ctx);
8
+ if (isFailure(resLeft)) {
9
+ return resLeft;
14
10
  }
15
- return { ...res, value: res.value[1] };
11
+ const resParse = parser(resLeft.ctx);
12
+ if (isFailure(resParse)) {
13
+ return resParse;
14
+ }
15
+ const resRight = right(resParse.ctx);
16
+ if (isFailure(resRight)) {
17
+ return resRight;
18
+ }
19
+ return success(resRight.ctx, resParse.value);
16
20
  };
17
21
  }
@@ -1,4 +1,4 @@
1
- import { Parser } from '../types';
1
+ import { Parser } from "../types";
2
2
  /** Parses zero or more occurences of the given parser.
3
3
  * @returns A parser returning an array of many parses.
4
4
  */
@@ -6,7 +6,7 @@ export declare function many<T>(parser: Parser<T>): Parser<T[]>;
6
6
  /** Parses zero or more occurences of the given parser, separated with the separator parser.
7
7
  * @returns A parser returning an array of many parses, omitting the separator.
8
8
  */
9
- export declare function zeroOrMany<T, V>(item: Parser<T>, separator: Parser<V>): Parser<T[]>;
9
+ export declare function zeroOrMany<T, V>(item: Parser<T>, separator?: Parser<V> | undefined): Parser<T[]>;
10
10
  /** Parses one or more occurences of the given parser, separated with the separator parser.
11
11
  * @returns A parser returning an array of many parses, omitting the separator.
12
12
  */
@@ -1,7 +1,4 @@
1
- import { failure, isFailure, success } from '../types';
2
- import { map } from './map';
3
- import { opt } from './opt';
4
- import { seq } from './seq';
1
+ import { failure, isFailure, success } from "../types";
5
2
  /** Parses zero or more occurences of the given parser.
6
3
  * @returns A parser returning an array of many parses.
7
4
  */
@@ -11,6 +8,10 @@ export function many(parser) {
11
8
  while (true) {
12
9
  const res = parser(ctx);
13
10
  if (isFailure(res)) {
11
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
12
+ if (surelyIndex >= 0) {
13
+ return failure(res.ctx, res.expected, ['many', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
14
+ }
14
15
  return success(ctx, results);
15
16
  }
16
17
  ctx = res.ctx;
@@ -21,35 +22,136 @@ export function many(parser) {
21
22
  /** Parses zero or more occurences of the given parser, separated with the separator parser.
22
23
  * @returns A parser returning an array of many parses, omitting the separator.
23
24
  */
24
- export function zeroOrMany(item, separator) {
25
- return map(opt(oneOrMany(item, separator)), t => t ?? []);
25
+ export function zeroOrMany(item, separator = undefined) {
26
+ if (separator) {
27
+ return (ctx) => {
28
+ const results = [];
29
+ const res = item(ctx);
30
+ if (isFailure(res)) {
31
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
32
+ if (surelyIndex >= 0) {
33
+ return failure(res.ctx, res.expected, ['zeroOrMany', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
34
+ }
35
+ return success(ctx, results);
36
+ }
37
+ ctx = res.ctx;
38
+ results.push(res.value);
39
+ while (true) {
40
+ const resSep = separator(ctx);
41
+ if (isFailure(resSep)) {
42
+ const surelyIndex = resSep.history.findIndex(h => h === 'surely');
43
+ if (surelyIndex >= 0) {
44
+ return failure(resSep.ctx, resSep.expected, ['zeroOrMany', ...resSep.history.slice(0, surelyIndex), ...resSep.history.slice(surelyIndex + 1)]);
45
+ }
46
+ return success(ctx, results);
47
+ }
48
+ const res = item(resSep.ctx);
49
+ if (isFailure(res)) {
50
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
51
+ if (surelyIndex >= 0) {
52
+ return failure(res.ctx, res.expected, ['zeroOrMany', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
53
+ }
54
+ return success(ctx, results);
55
+ }
56
+ ctx = res.ctx;
57
+ results.push(res.value);
58
+ }
59
+ };
60
+ }
61
+ else
62
+ return many(item);
26
63
  }
27
64
  /** Parses one or more occurences of the given parser, separated with the separator parser.
28
65
  * @returns A parser returning an array of many parses, omitting the separator.
29
66
  */
30
67
  export function oneOrMany(item, separator = undefined) {
31
- const sequencer = map(seq(item, many(separator
32
- ? map(seq(separator, item), ([, t]) => t)
33
- : item)), ([t, ts]) => ([t, ...ts]));
34
- return (ctx) => {
35
- const res = sequencer(ctx);
36
- if (isFailure(res)) {
37
- const newHistory = [...res.history];
38
- newHistory.splice(0, 2);
39
- return failure(res.ctx, res.expected, ['oneOrMany', ...newHistory]);
40
- }
41
- return res;
42
- };
68
+ if (separator) {
69
+ return (ctx) => {
70
+ const results = [];
71
+ const res = item(ctx);
72
+ if (isFailure(res))
73
+ return res;
74
+ ctx = res.ctx;
75
+ results.push(res.value);
76
+ while (true) {
77
+ const resSep = separator(ctx);
78
+ if (isFailure(resSep)) {
79
+ const surelyIndex = resSep.history.findIndex(h => h === 'surely');
80
+ if (surelyIndex >= 0) {
81
+ return failure(resSep.ctx, resSep.expected, ['oneOrMany', ...resSep.history.slice(0, surelyIndex), ...resSep.history.slice(surelyIndex + 1)]);
82
+ }
83
+ return success(ctx, results);
84
+ }
85
+ const res = item(resSep.ctx);
86
+ if (isFailure(res)) {
87
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
88
+ if (surelyIndex >= 0) {
89
+ return failure(res.ctx, res.expected, ['oneOrMany', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
90
+ }
91
+ return success(ctx, results);
92
+ }
93
+ ctx = res.ctx;
94
+ results.push(res.value);
95
+ }
96
+ };
97
+ }
98
+ else {
99
+ return (ctx) => {
100
+ const results = [];
101
+ const res = item(ctx);
102
+ if (isFailure(res))
103
+ return res;
104
+ ctx = res.ctx;
105
+ results.push(res.value);
106
+ while (true) {
107
+ const res = item(ctx);
108
+ if (isFailure(res)) {
109
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
110
+ if (surelyIndex >= 0) {
111
+ return failure(res.ctx, res.expected, ['oneOrMany', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
112
+ }
113
+ return success(ctx, results);
114
+ }
115
+ ctx = res.ctx;
116
+ results.push(res.value);
117
+ }
118
+ };
119
+ }
43
120
  }
44
121
  /** Parses one or more occurences of the given parser, separated with the separator parser.
45
122
  * @returns A parser returning the result of many parses, reduced using the `reducer` function passed in.
46
123
  */
47
124
  export function oneOrManyRed(item, separator, reducer) {
48
- return map(map(seq(map(item, (x) => [null, x]), many(seq(separator, item))), ([t, ts]) => ([t, ...ts])), ts => {
49
- let result = ts[0][1];
50
- for (let i = 1; i < ts.length; i++) {
51
- result = reducer(result, ts[i][1], ts[i][0]);
125
+ return (ctx) => {
126
+ const res = item(ctx);
127
+ if (isFailure(res))
128
+ return res;
129
+ ctx = res.ctx;
130
+ let result = res.value;
131
+ while (true) {
132
+ const resSep = separator(ctx);
133
+ if (isFailure(resSep)) {
134
+ const surelyIndex = resSep.history.findIndex(h => h === 'surely');
135
+ if (surelyIndex >= 0) {
136
+ return failure(resSep.ctx, resSep.expected, ['oneOrManyRed', ...resSep.history.slice(0, surelyIndex), ...resSep.history.slice(surelyIndex + 1)]);
137
+ }
138
+ return success(ctx, result);
139
+ }
140
+ const res = item(resSep.ctx);
141
+ if (isFailure(res)) {
142
+ const surelyIndex = res.history.findIndex(h => h === 'surely');
143
+ if (surelyIndex >= 0) {
144
+ return failure(res.ctx, res.expected, ['oneOrManyRed', ...res.history.slice(0, surelyIndex), ...res.history.slice(surelyIndex + 1)]);
145
+ }
146
+ return success(ctx, result);
147
+ }
148
+ ctx = res.ctx;
149
+ try {
150
+ result = reducer(result, res.value, resSep.value);
151
+ }
152
+ catch {
153
+ return failure(res.ctx, 'Error while reducing', ['oneOrManyRed']);
154
+ }
52
155
  }
53
- return result;
54
- });
156
+ };
55
157
  }
@@ -1,9 +1,14 @@
1
1
  import { failure, isFailure, success } from '../types';
2
+ import { isStringParser, shouldPerformFusions } from './optimizations';
2
3
  /** Parses the input using the given parser, then maps the output.
3
4
  * @returns A parser that parses the same input, but has output mapped using the mapping function.
4
5
  */
5
6
  export function map(parser, mapper) {
6
- return (ctx) => {
7
+ let marker = {};
8
+ if (shouldPerformFusions() && isStringParser(parser)) {
9
+ marker = { parserType: 'anyString', matches: parser.matches.map(m => ([m[0], m[1], (v) => mapper(m[2](v))])) };
10
+ }
11
+ return Object.assign((ctx) => {
7
12
  const res = parser(ctx);
8
13
  if (isFailure(res))
9
14
  return failure(res.ctx, res.expected, ['map', ...res.history]);
@@ -14,5 +19,5 @@ export function map(parser, mapper) {
14
19
  catch {
15
20
  return failure(res.ctx, 'Error while mapping', ['map']);
16
21
  }
17
- };
22
+ }, marker);
18
23
  }
@@ -0,0 +1,11 @@
1
+ import { Parser } from "../types";
2
+ import { AnyStringParser } from "./anyString";
3
+ export declare const shouldPerformFusions: () => boolean;
4
+ /**
5
+ * Disables all parser combinator fusions.
6
+ *
7
+ * Should probably be used only for testing performance.
8
+ */
9
+ export declare const toggleFusions: (value: boolean) => void;
10
+ export declare function isStringParser<T>(parser: Parser<T>): parser is AnyStringParser<T>;
11
+ export declare function allStringParsers<T>(parsers: Parser<T>[]): parsers is AnyStringParser<T>[];
@@ -0,0 +1,16 @@
1
+ export const shouldPerformFusions = () => performFusions;
2
+ let performFusions = true;
3
+ /**
4
+ * Disables all parser combinator fusions.
5
+ *
6
+ * Should probably be used only for testing performance.
7
+ */
8
+ export const toggleFusions = (value) => {
9
+ performFusions = value;
10
+ };
11
+ export function isStringParser(parser) {
12
+ return 'parserType' in parser && typeof parser.parserType === 'string' && parser.parserType === 'anyString';
13
+ }
14
+ export function allStringParsers(parsers) {
15
+ return parsers.every(isStringParser);
16
+ }
@@ -1,17 +1,9 @@
1
1
  import { Parser } from '../types';
2
- export type StrParser<T> = Parser<T> & {
3
- parserType: 'str';
4
- match: T;
5
- };
6
2
  /** Parses a string and returns it as a result of the parse.
7
3
  * @returns A parser parsing a given string, case-sensitive.
8
4
  */
9
- export declare function str<T extends string>(match: T): StrParser<T>;
10
- export type StrIParser<T> = Parser<T> & {
11
- parserType: 'stri';
12
- match: T;
13
- };
5
+ export declare function str<T extends string>(match: T): Parser<T>;
14
6
  /** Parses a string case-insensitively and returns it as a result of the parse.
15
7
  * @returns A parser parsing a given string, case-insensitive.
16
8
  */
17
- export declare function stri<T extends string>(match: T): StrIParser<Lowercase<T>>;
9
+ export declare function stri<T extends string>(match: T): Parser<Lowercase<T>>;
@@ -11,7 +11,7 @@ export function str(match) {
11
11
  else {
12
12
  return failure(ctx, inQuotes, [inQuotes]);
13
13
  }
14
- }, { parserType: 'str', match });
14
+ }, { parserType: 'anyString', matches: [[match, false, (v) => v]] });
15
15
  }
16
16
  const collator = new Intl.Collator('en', { sensitivity: 'accent' });
17
17
  /** Parses a string case-insensitively and returns it as a result of the parse.
@@ -28,5 +28,5 @@ export function stri(match) {
28
28
  else {
29
29
  return failure(ctx, inQuotes, [inQuotes]);
30
30
  }
31
- }, { parserType: 'stri', match: lowercase });
31
+ }, { parserType: 'anyString', matches: [[lowercase, true, (v) => v]] });
32
32
  }
@@ -1,11 +1,4 @@
1
1
  import { Parser, Token } from '../types';
2
- export declare const shouldPerformFusions: () => boolean;
3
- /**
4
- * Disables all parser combinator fusions.
5
- *
6
- * Should probably be used only for testing performance.
7
- */
8
- export declare const toggleFusions: (value: boolean) => void;
9
2
  /** Allows to make a condition on the result of the parsing function.
10
3
  * @returns A parser returning the same but also performing a given check on the result.
11
4
  */
@@ -31,3 +24,7 @@ export declare function token<T>(parser: Parser<T>): Parser<Token<T>>;
31
24
  * Checks whether the parser parses successfully, but doesn't move the cursor forward
32
25
  */
33
26
  export declare function lookaround<T>(parser: Parser<T>): Parser<void>;
27
+ /**
28
+ * Turns a function creating a parser into a parser. Useful for recursive grammars.
29
+ */
30
+ export declare function lazy<T>(parserGetter: () => Parser<T>): Parser<T>;
@@ -1,14 +1,4 @@
1
1
  import { failure, isFailure, success } from '../types';
2
- export const shouldPerformFusions = () => performFusions;
3
- let performFusions = true;
4
- /**
5
- * Disables all parser combinator fusions.
6
- *
7
- * Should probably be used only for testing performance.
8
- */
9
- export const toggleFusions = (value) => {
10
- performFusions = value;
11
- };
12
2
  /** Allows to make a condition on the result of the parsing function.
13
3
  * @returns A parser returning the same but also performing a given check on the result.
14
4
  */
@@ -83,3 +73,13 @@ export function lookaround(parser) {
83
73
  return failure(ctx, result.expected, ['lookaround', ...result.history]);
84
74
  };
85
75
  }
76
+ /**
77
+ * Turns a function creating a parser into a parser. Useful for recursive grammars.
78
+ */
79
+ export function lazy(parserGetter) {
80
+ let parser;
81
+ return (ctx) => {
82
+ parser ??= parserGetter();
83
+ return parser(ctx);
84
+ };
85
+ }
package/package.json CHANGED
@@ -1,57 +1,61 @@
1
- {
2
- "name": "parser-combinators",
3
- "version": "1.2.2",
4
- "license": "ISC",
5
- "maintainers": [
6
- "Micha_i"
7
- ],
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/michalusio/Parser.git"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/michalusio/Parser/issues"
14
- },
15
- "homepage": "https://github.com/michalusio/Parser#readme",
16
- "description": "A library of parser combinators, with which you can create your own parsers. The library will be continuously improved in time.",
17
- "scripts": {
18
- "start": "tsc && node .",
19
- "lint": "eslint src --ext .ts",
20
- "build": "tsc",
21
- "build-esm": "tsc --project tsconfig.esm.json",
22
- "bundle": "npm run lint && npm run build && npm run build-esm",
23
- "test:mutate": "stryker run",
24
- "test:unit": "mocha",
25
- "test": "nyc -e '.ts' --r html -r lcov -r text npm run test:unit"
26
- },
27
- "author": "Micha_i <isalski.michal@gmail.com> (https://github.com/michalusio)",
28
- "main": "./dist/index.js",
29
- "types": "./dist/index.d.ts",
30
- "type": "commonjs",
31
- "engines": {
32
- "node": ">=18"
33
- },
34
- "devDependencies": {
35
- "@stryker-mutator/core": "^9.4.0",
36
- "@stryker-mutator/mocha-runner": "^9.4.0",
37
- "@stryker-mutator/typescript-checker": "^9.4.0",
38
- "@types/mocha": "10.0.10",
39
- "@types/node": "25.0.9",
40
- "cross-env": "10.1.0",
41
- "esbuild": "0.27.2",
42
- "eslint": "9.39.2",
43
- "mocha": "11.7.5",
44
- "nyc": "17.1.0",
45
- "ts-node": "10.9.2",
46
- "typescript": "5.9.3",
47
- "typescript-eslint": "8.53.0"
48
- },
49
- "keywords": [
50
- "parser",
51
- "combinator",
52
- "parser-combinator",
53
- "parsing",
54
- "combining",
55
- "functional"
56
- ]
57
- }
1
+ {
2
+ "name": "parser-combinators",
3
+ "version": "1.2.5",
4
+ "license": "ISC",
5
+ "maintainers": [
6
+ "Micha_i"
7
+ ],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/michalusio/Parser.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/michalusio/Parser/issues"
14
+ },
15
+ "homepage": "https://github.com/michalusio/Parser#readme",
16
+ "description": "A library of parser combinators, with which you can create your own parsers. The library will be continuously improved in time.",
17
+ "scripts": {
18
+ "start": "tsc && node .",
19
+ "lint": "eslint src --ext .ts",
20
+ "build": "tsc",
21
+ "build-esm": "tsc --project tsconfig.esm.json",
22
+ "bundle": "npm run lint && npm run build && npm run build-esm",
23
+ "test:mutate": "stryker run",
24
+ "test:unit": "mocha",
25
+ "test": "nyc -e '.ts' --r html -r lcov -r text npm run test:unit"
26
+ },
27
+ "author": "Micha_i <isalski.michal@gmail.com> (https://github.com/michalusio)",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "type": "commonjs",
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "devDependencies": {
35
+ "@stryker-mutator/core": "^9.4.0",
36
+ "@stryker-mutator/mocha-runner": "^9.4.0",
37
+ "@stryker-mutator/typescript-checker": "^9.4.0",
38
+ "@types/mocha": "10.0.10",
39
+ "@types/node": "25.0.9",
40
+ "cross-env": "10.1.0",
41
+ "esbuild": "0.27.2",
42
+ "eslint": "9.39.2",
43
+ "mocha": "11.7.5",
44
+ "nyc": "17.1.0",
45
+ "ts-node": "10.9.2",
46
+ "typescript": "5.9.3",
47
+ "typescript-eslint": "8.53.0"
48
+ },
49
+ "keywords": [
50
+ "parser",
51
+ "combinator",
52
+ "parser-combinator",
53
+ "parsing",
54
+ "combining",
55
+ "functional"
56
+ ],
57
+ "prettier": {
58
+ "tabWidth": 4,
59
+ "useTabs": false
60
+ }
61
+ }