clarity-pattern-parser 10.0.2 → 10.0.4

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.
@@ -2,6 +2,11 @@ import { Node } from "../ast/Node";
2
2
  import { Match } from "./CursorHistory";
3
3
  import { ParseError } from "./ParseError";
4
4
  import { Pattern } from "./Pattern";
5
+ export declare class CyclicalParseError extends Error {
6
+ readonly patternId: string;
7
+ readonly patternName: string;
8
+ constructor(patternId: string, patternName: string);
9
+ }
5
10
  export declare class Cursor {
6
11
  private _text;
7
12
  private _index;
@@ -14,7 +14,6 @@ export declare class Not implements Pattern {
14
14
  get parent(): Pattern | null;
15
15
  set parent(pattern: Pattern | null);
16
16
  get children(): Pattern[];
17
- get isOptional(): boolean;
18
17
  constructor(name: string, pattern: Pattern);
19
18
  test(text: string): boolean;
20
19
  exec(text: string, record?: boolean): ParseResult;
@@ -21,8 +21,8 @@ export declare class Repeat implements Pattern {
21
21
  get parent(): Pattern | null;
22
22
  set parent(value: Pattern | null);
23
23
  get children(): Pattern[];
24
- get min(): any;
25
- get max(): any;
24
+ get min(): number;
25
+ get max(): number;
26
26
  constructor(name: string, pattern: Pattern, options?: RepeatOptions);
27
27
  parse(cursor: Cursor): Node | null;
28
28
  exec(text: string): ParseResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-pattern-parser",
3
- "version": "10.0.2",
3
+ "version": "10.0.4",
4
4
  "description": "Parsing Library for Typescript and Javascript.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.esm.js",
package/src/ast/Node.ts CHANGED
@@ -315,6 +315,10 @@ export class Node {
315
315
  return JSON.stringify(this.toCycleFreeObject(), null, space);
316
316
  }
317
317
 
318
+ isEqual(node: Node) {
319
+ return node.toJson(0) === this.toJson(0);
320
+ }
321
+
318
322
  static createValueNode(name: string, value: string) {
319
323
  return new Node("custom-value-node", name, 0, 0, [], value);
320
324
  }
@@ -511,4 +511,38 @@ describe("Grammar", () => {
511
511
  expect(patterns.john).not.toBeNull();
512
512
  expect(patterns.jane).not.toBeNull();
513
513
  });
514
+
515
+ test("Grammar Import", async () => {
516
+ const importExpression = `first-name = "John"`;
517
+ const spaceExpression = `
518
+ use params { custom-space }
519
+ space = custom-space
520
+ `;
521
+ const expression = `
522
+ use params {
523
+ custom-space
524
+ }
525
+ import { first-name } from "first-name.cpat"
526
+ import { space } from "space.cpat" with params {
527
+ custom-space = custom-space
528
+ }
529
+ last-name = "Doe"
530
+ full-name = first-name + space + last-name
531
+ `;
532
+
533
+ const pathMap: Record<string, string> = {
534
+ "space.cpat": spaceExpression,
535
+ "first-name.cpat": importExpression,
536
+ "root.cpat": expression
537
+ };
538
+
539
+ function resolveImport(resource: string) {
540
+ return Promise.resolve({ expression: pathMap[resource], resource });
541
+ }
542
+
543
+ const patterns = await Grammar.import("root.cpat", { resolveImport, params: [new Literal("custom-space", " ")] });
544
+ const fullname = patterns["full-name"] as Pattern;
545
+ const result = fullname.exec("John Doe");
546
+ expect(result?.ast?.value).toBe("John Doe");
547
+ });
514
548
  });
@@ -322,7 +322,7 @@ export class Grammar {
322
322
  const trimDivider = repeatNode.find(n => n.name === "trim-flag") != null;
323
323
  const patterNode = repeatNode.children[1].type === "spaces" ? repeatNode.children[2] : repeatNode.children[1];
324
324
  const pattern = this._buildPattern(patterNode);
325
- const dividerSectionNode = repeatNode.find(n => n.name === "divider-section");
325
+ const dividerSectionNode = repeatNode.find(n => n.name === "repeat-divider-section");
326
326
 
327
327
  const options: RepeatOptions = {
328
328
  min: 1,
@@ -462,7 +462,7 @@ export class Grammar {
462
462
  );
463
463
 
464
464
  const grammar = new Grammar({
465
- params: importedValues,
465
+ params: [...importedValues, ...this._parseContext.paramsByName.values()],
466
466
  originResource: this._originResource,
467
467
  resolveImport: this._resolveImport
468
468
  });
@@ -9,7 +9,7 @@ import { allSpaces, lineSpaces } from "./spaces";
9
9
  import { Optional } from "../../patterns/Optional";
10
10
 
11
11
  const optionalSpaces = new Optional("optional-spaces", allSpaces);
12
- const optionalLineSpaces = new Optional("options-line-spaces", lineSpaces);
12
+ const optionalLineSpaces = new Optional("optional-line-spaces", lineSpaces);
13
13
 
14
14
  const importNameDivider = new Regex("import-name-divider", "(\\s+)?,(\\s+)?");
15
15
  importNameDivider.setTokens([", "]);
@@ -7,7 +7,7 @@ import { Options } from "../../patterns/Options";
7
7
  const patternName = name.clone("pattern-name");
8
8
  patternName.setTokens(["[PATTERN_NAME]"]);
9
9
 
10
- const patterns = new Options("or-patterns", [patternName, anonymousPattern]);
10
+ const patterns = new Options("options-patterns", [patternName, anonymousPattern]);
11
11
  const defaultDivider = new Regex("default-divider", "\\s*[|]\\s*");
12
12
  defaultDivider.setTokens(["|"]);
13
13
 
@@ -8,8 +8,8 @@ import { lineSpaces, spaces } from "./spaces";
8
8
  import { Optional } from "../../patterns/Optional";
9
9
 
10
10
  const optionalSpaces = new Optional("optional-spaces", spaces);
11
- const openBracket = new Literal("open-bracket", "{");
12
- const closeBracket = new Literal("close-bracket", "}");
11
+ const openBracket = new Literal("repeat-open-bracket", "{");
12
+ const closeBracket = new Literal("repeat-close-bracket", "}");
13
13
  const comma = new Literal("comma", ",");
14
14
 
15
15
  const integer = new Regex("integer", "([1-9][0-9]*)|0");
@@ -55,16 +55,16 @@ dividerComma.setTokens([", "]);
55
55
 
56
56
 
57
57
  const patternName = name.clone("pattern-name");
58
- const patterns = new Options("or-patterns", [patternName, anonymousPattern]);
59
- const dividerPattern = patterns.clone("divider-pattern");
60
- const dividerSection = new Sequence("divider-section", [dividerComma, dividerPattern, trimFlag]);
61
- const optionalDividerSection = new Optional("optional-divider-section", dividerSection);
58
+ const repeatPattern = new Options("repeat-pattern", [patternName, anonymousPattern]);
59
+ const repeatDividerPattern = repeatPattern.clone("repeat-divider-pattern");
60
+ const repeatDividerSection = new Sequence("repeat-divider-section", [dividerComma, repeatDividerPattern, trimFlag]);
61
+ const repeatOptionalDividerSection = new Optional("repeat-optional-divider-section", repeatDividerSection);
62
62
 
63
63
  export const repeatLiteral = new Sequence("repeat-literal", [
64
64
  openParen,
65
65
  optionalSpaces,
66
- patterns,
67
- optionalDividerSection,
66
+ repeatPattern,
67
+ repeatOptionalDividerSection,
68
68
  optionalSpaces,
69
69
  closeParen,
70
70
  new Sequence("quantifier-section", [quantifier]),
@@ -118,7 +118,7 @@ describe("AutoComplete", () => {
118
118
 
119
119
  expect(result.ast).toBeNull();
120
120
  expect(result.options).toEqual(expectedOptions);
121
- expect(result.errorAtIndex).toBe(text.length)
121
+ expect(result.errorAtIndex).toBe(text.length);
122
122
  expect(result.isComplete).toBeFalsy();
123
123
  expect(result.cursor).not.toBeNull();
124
124
  });
@@ -48,11 +48,11 @@ export class AutoComplete {
48
48
  errorAtIndex: 0,
49
49
  cursor,
50
50
  ast: null
51
- }
51
+ };
52
52
  }
53
53
 
54
54
  let errorAtIndex = null;
55
- let error = null
55
+ let error = null;
56
56
 
57
57
  const ast = this._pattern.parse(this._cursor);
58
58
  const isComplete = ast?.value === this._text;
@@ -74,7 +74,7 @@ export class AutoComplete {
74
74
  errorAtIndex = startIndex;
75
75
  } else if (!isComplete && this._cursor.hasError && this._cursor.furthestError != null) {
76
76
  errorAtIndex = this._cursor.furthestError.endIndex;
77
- error = this._cursor.furthestError
77
+ error = this._cursor.furthestError;
78
78
 
79
79
  errorAtIndex = options.reduce((errorAtIndex, option) =>
80
80
  Math.max(errorAtIndex, option.startIndex),
@@ -88,7 +88,7 @@ export class AutoComplete {
88
88
  errorAtIndex,
89
89
  cursor: cursor,
90
90
  ast,
91
- }
91
+ };
92
92
 
93
93
  }
94
94
 
@@ -108,7 +108,7 @@ export class AutoComplete {
108
108
  }
109
109
  });
110
110
 
111
- return finalResults
111
+ return finalResults;
112
112
  }
113
113
 
114
114
  private _getOptionsFromErrors() {
@@ -159,7 +159,7 @@ export class AutoComplete {
159
159
  }
160
160
 
161
161
  private _getTokensForPattern(pattern: Pattern) {
162
- const augmentedTokens = this._getAugmentedTokens(pattern)
162
+ const augmentedTokens = this._getAugmentedTokens(pattern);
163
163
 
164
164
  if (this._options.greedyPatternNames != null && this._options.greedyPatternNames.includes(pattern.name)) {
165
165
  const nextPatterns = pattern.getNextPatterns();
@@ -225,7 +225,7 @@ export class AutoComplete {
225
225
  return {
226
226
  text: text,
227
227
  startIndex: furthestMatch,
228
- }
228
+ };
229
229
  }
230
230
 
231
231
  static suggestFor(text: string, pattern: Pattern, options?: AutoCompleteOptions) {
@@ -3,6 +3,17 @@ import { CursorHistory, Match, Trace } from "./CursorHistory";
3
3
  import { ParseError } from "./ParseError";
4
4
  import { Pattern } from "./Pattern";
5
5
 
6
+ export class CyclicalParseError extends Error {
7
+ readonly patternId: string;
8
+ readonly patternName: string;
9
+
10
+ constructor(patternId: string, patternName: string) {
11
+ super("Cyclical Parse Error");
12
+ this.patternId = patternId;
13
+ this.patternName = patternName;
14
+ }
15
+ }
16
+
6
17
  export class Cursor {
7
18
  private _text: string;
8
19
  private _index: number;
@@ -145,16 +156,14 @@ export class Cursor {
145
156
  }
146
157
 
147
158
  startParseWith(pattern: Pattern) {
148
- const patternName = pattern.name;
149
-
150
159
  const trace = {
151
160
  pattern,
152
161
  cursorIndex: this.index
153
162
  };
154
163
 
155
- const hasCycle = this._stackTrace.find(t => t.pattern.id === pattern.id && this.index === t.cursorIndex);
164
+ const hasCycle = this._stackTrace.filter(t => t.pattern.id === pattern.id && this.index === t.cursorIndex).length > 1;
156
165
  if (hasCycle) {
157
- throw new Error(`Cyclical Pattern: ${this._stackTrace.map(t => `${t.pattern.name}#${t.pattern.id}{${t.cursorIndex}}`).join(" -> ")} -> ${patternName}#${pattern.id}{${this.index}}.`);
166
+ throw new CyclicalParseError(pattern.id, pattern.name);
158
167
  }
159
168
 
160
169
  this._history.pushStackTrace(trace);
@@ -478,4 +478,10 @@ describe("BoundedRepeat", () => {
478
478
  expect(comma).toBe(numbers.children[1]);
479
479
  });
480
480
 
481
+ test("Trim Trailing Complex Delimiter Pattern", () => {
482
+ const numbers = new FiniteRepeat("numbers", new Regex("number", "\\d"), { divider: new Sequence("comma", [new Literal(",", ","), new Literal("space", " ")]), trimDivider: true, max: 3 });
483
+ const result = numbers.parse(new Cursor("1, 2,"));
484
+ expect(result?.value).toBe("1, 2");
485
+ });
486
+
481
487
  });
@@ -117,7 +117,7 @@ export class FiniteRepeat implements Pattern {
117
117
  }
118
118
 
119
119
  if (this._trimDivider && this._hasDivider) {
120
- const isDividerLastMatch = cursor.leafMatch.pattern?.id === this.children[1].id;
120
+ const isDividerLastMatch = this.children.length > 1 && nodes[nodes.length - 1].name === this.children[1].name;
121
121
  if (isDividerLastMatch) {
122
122
  const node = nodes.pop() as Node;
123
123
  cursor.moveTo(node.firstIndex);
@@ -264,5 +264,11 @@ describe("InfiniteRepeat", () => {
264
264
  expect(result.cursor.hasError).toBeTruthy();
265
265
  });
266
266
 
267
+ test("Trim Trailing Complex Delimiter Pattern", () => {
268
+ const numbers = new InfiniteRepeat("numbers", new Regex("number", "\\d"), { divider: new Sequence("comma", [new Literal(",", ","), new Literal("space", " ")]), trimDivider: true });
269
+ const result = numbers.parse(new Cursor("1, 2,"));
270
+ expect(result?.value).toBe("1, 2");
271
+ });
272
+
267
273
 
268
274
  });
@@ -235,7 +235,7 @@ export class InfiniteRepeat implements Pattern {
235
235
  if (
236
236
  hasDivider &&
237
237
  this._trimDivider &&
238
- cursor.leafMatch.pattern === this._divider
238
+ this._nodes[this._nodes.length - 1].name === this._divider?.name
239
239
  ) {
240
240
  const dividerNode = this._nodes.pop() as Node;
241
241
  cursor.moveTo(dividerNode.firstIndex);
@@ -53,7 +53,6 @@ describe("Not", () => {
53
53
  expect(notA.name).toBe("not-a");
54
54
  expect(notA.parent).toBeNull();
55
55
  expect(notA.children[0].name).toBe("a");
56
- expect(notA.isOptional).toBeFalsy();
57
56
  });
58
57
 
59
58
  test("Not A Not", () => {
@@ -36,10 +36,6 @@ export class Not implements Pattern {
36
36
  return this._children;
37
37
  }
38
38
 
39
- get isOptional(): boolean {
40
- return false;
41
- }
42
-
43
39
  constructor(name: string, pattern: Pattern) {
44
40
  this._id = `not-${idIndex++}`;
45
41
  this._type = "not";
@@ -5,6 +5,8 @@ import { Options } from "./Options";
5
5
  import { Sequence } from "./Sequence";
6
6
  import { Pattern } from "./Pattern";
7
7
  import { Optional } from "./Optional";
8
+ import { Regex } from "./Regex";
9
+ import { Reference } from "./Reference";
8
10
 
9
11
  describe("Options", () => {
10
12
  test("Empty Options", () => {
@@ -271,4 +273,26 @@ describe("Options", () => {
271
273
  const result = firstName.exec("Jane");
272
274
  expect(result.ast?.value).toBe("Jane");
273
275
  });
276
+
277
+ test("Cyclical Error Recorvery", () => {
278
+ const john = new Literal("john", "John");
279
+ const jane = new Literal("jane", "Jane");
280
+ const names = new Options("names", [john, jane]);
281
+ const questionMark = new Literal("?", "?");
282
+ const colon = new Literal(":", ":");
283
+ const space = new Regex("space", "\\s+");
284
+ const expressionReference = new Reference("expression");
285
+ const ternary = new Sequence("ternary", [expressionReference, space, questionMark, space, expressionReference, space, colon, space, expressionReference]);
286
+ const expression = new Options("expression", [names, ternary], true);
287
+
288
+ let result = expression.exec("John");
289
+ expect(result.ast?.toString()).toBe("John");
290
+
291
+ result = expression.exec("John ? Jane : John");
292
+ expect(result.ast?.toString()).toBe("John ? Jane : John");
293
+
294
+
295
+ result = expression.exec("John ? Jane : John ? Jane : John");
296
+ expect(result.ast?.toString()).toBe("John ? Jane : John ? Jane : John");
297
+ });
274
298
  });
@@ -84,6 +84,7 @@ export class Options implements Pattern {
84
84
 
85
85
  parse(cursor: Cursor): Node | null {
86
86
  cursor.startParseWith(this);
87
+
87
88
  this._firstIndex = cursor.index;
88
89
 
89
90
  const node = this._tryToParse(cursor);
@@ -106,7 +107,18 @@ export class Options implements Pattern {
106
107
 
107
108
  for (const pattern of this._children) {
108
109
  cursor.moveTo(this._firstIndex);
109
- const result = pattern.parse(cursor);
110
+ let result = null;
111
+
112
+ try {
113
+ result = pattern.parse(cursor);
114
+ } catch (error: any) {
115
+ if (error.patternId === this._id) {
116
+ continue;
117
+ } else {
118
+ cursor.endParse();
119
+ throw error;
120
+ }
121
+ }
110
122
 
111
123
  if (this._isGreedy) {
112
124
  results.push(result);
@@ -53,11 +53,11 @@ export class Repeat implements Pattern {
53
53
  }
54
54
 
55
55
  get min() {
56
- return (this.children[0] as any).min;
56
+ return this._options.min;
57
57
  }
58
58
 
59
59
  get max() {
60
- return (this.children[0] as any).max || Infinity;
60
+ return this._options.max;
61
61
  }
62
62
 
63
63
  constructor(name: string, pattern: Pattern, options: RepeatOptions = {}) {
@@ -3,6 +3,7 @@ import { Sequence } from "./Sequence";
3
3
  import { Literal } from "./Literal";
4
4
  import { Node } from "../ast/Node";
5
5
  import { Optional } from "./Optional";
6
+ import { Pattern } from "./Pattern";
6
7
 
7
8
  describe("Sequence", () => {
8
9
  test("No Patterns", () => {
@@ -310,9 +311,9 @@ describe("Sequence", () => {
310
311
 
311
312
  const sequenceClone = parent.find(p => p.name === "sequence");
312
313
  const nextPatterns = sequenceClone?.getNextPatterns() || [];
313
- const b = parent.find(p => p.name === "b")
314
+ const b = parent.find(p => p.name === "b") as Pattern;
314
315
 
315
- expect(nextPatterns[0]).toBe(b);
316
+ expect(nextPatterns[0].isEqual(b)).toBeTruthy();
316
317
  });
317
318
 
318
319
  test("Get Next Patterns With Null Parent", () => {
@@ -212,7 +212,7 @@ export class Sequence implements Pattern {
212
212
  for (const child of this._children) {
213
213
  tokens.push(...child.getTokens());
214
214
 
215
- if (child.type !== "optional") {
215
+ if (child.type !== "optional" && child.type !== "not") {
216
216
  break;
217
217
  }
218
218
  }
@@ -243,7 +243,7 @@ export class Sequence implements Pattern {
243
243
  for (const child of this._children) {
244
244
  patterns.push(...child.getPatterns());
245
245
 
246
- if (child.type !== "optional") {
246
+ if (child.type !== "optional" && child.type !== "not") {
247
247
  break;
248
248
  }
249
249
  }