clarity-pattern-parser 11.3.4 → 11.3.6

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/grammar.md ADDED
@@ -0,0 +1,254 @@
1
+ # Clarity Pattern Parser Grammar
2
+
3
+ This document describes the grammar features supported by the Clarity Pattern Parser.
4
+
5
+ ## Basic Patterns
6
+
7
+ ### Literal Strings
8
+ Define literal string patterns using double quotes:
9
+ ```
10
+ name = "John"
11
+ ```
12
+
13
+ Escaped characters are supported in literals:
14
+ - `\n` - newline
15
+ - `\r` - carriage return
16
+ - `\t` - tab
17
+ - `\b` - backspace
18
+ - `\f` - form feed
19
+ - `\v` - vertical tab
20
+ - `\0` - null character
21
+ - `\x00` - hex character
22
+ - `\u0000` - unicode character
23
+ - `\"` - escaped quote
24
+ - `\\` - escaped backslash
25
+
26
+ ### Regular Expressions
27
+ Define regex patterns using forward slashes:
28
+ ```
29
+ name = /\w/
30
+ ```
31
+
32
+ ## Pattern Operators
33
+
34
+ ### Options (|)
35
+ Match one of multiple patterns using the `|` operator. This is used for simple alternatives where order doesn't matter:
36
+ ```
37
+ names = john | jane
38
+ ```
39
+
40
+ ### Expression (|)
41
+ Expression patterns also use the `|` operator but are used for defining operator precedence in expressions. The order of alternatives determines precedence, with earlier alternatives having higher precedence. By default, operators are left-associative.
42
+
43
+ Example of an arithmetic expression grammar:
44
+ ```
45
+ prefix-operators = "+" | "-"
46
+ prefix-expression = prefix-operators + expression
47
+ postfix-operators = "++" | "--"
48
+ postfix-expression = expression + postfix-operators
49
+ add-sub-operators = "+" | "-"
50
+ add-sub-expression = expression + add-sub-operators + expression
51
+ mul-div-operators = "*" | "/"
52
+ mul-div-expression = expression + mul-div-operators + expression
53
+ expression = prefix-expression | mul-div-expression | add-sub-expression | postfix-expression
54
+ ```
55
+
56
+ In this example:
57
+ - `prefix-expression` has highest precedence
58
+ - `mul-div-expression` has next highest precedence
59
+ - `add-sub-expression` has next highest precedence
60
+ - `postfix-expression` has lowest precedence
61
+
62
+ To make an operator right-associative, add the `right` keyword:
63
+ ```
64
+ expression = prefix-expression | mul-div-expression | add-sub-expression right | postfix-expression
65
+ ```
66
+
67
+ ### Sequence (+)
68
+ Concatenate patterns in sequence using the `+` operator:
69
+ ```
70
+ full-name = first-name + space + last-name
71
+ ```
72
+
73
+ ### Optional (?)
74
+ Make a pattern optional using the `?` operator:
75
+ ```
76
+ full-name = first-name + space + middle-name? + last-name
77
+ ```
78
+
79
+ ### Not (!)
80
+ Negative lookahead using the `!` operator:
81
+ ```
82
+ pattern = !excluded-pattern + actual-pattern
83
+ ```
84
+
85
+ ### Take Until (?->|)
86
+ Match all characters until a specific pattern is found:
87
+ ```
88
+ script-text = ?->| "</script"
89
+ ```
90
+
91
+ ## Repetition
92
+
93
+ ### Basic Repeat
94
+ Repeat a pattern one or more times using `+`:
95
+ ```
96
+ digits = (digit)+
97
+ ```
98
+
99
+ ### Zero or More
100
+ Repeat a pattern zero or more times using `*`:
101
+ ```
102
+ digits = (digit)*
103
+ ```
104
+
105
+ ### Bounded Repetition
106
+ Specify exact repetition counts using curly braces:
107
+ - `{n}` - Exactly n times: `(pattern){3}`
108
+ - `{n,}` - At least n times: `(pattern){1,}`
109
+ - `{,n}` - At most n times: `(pattern){,3}`
110
+ - `{n,m}` - Between n and m times: `(pattern){1,3}`
111
+
112
+ ### Repetition with Divider
113
+ Repeat patterns with a divider between occurrences:
114
+ ```
115
+ digits = (digit, comma){3}
116
+ ```
117
+
118
+ Add `trim` keyword to trim the divider from the end:
119
+ ```
120
+ digits = (digit, comma trim)+
121
+ ```
122
+
123
+ ## Imports and Parameters
124
+
125
+ ### Basic Import
126
+ Import patterns from other files:
127
+ ```
128
+ import { pattern-name } from "path/to/file.cpat"
129
+ ```
130
+
131
+ ### Import with Parameters
132
+ Import with custom parameters:
133
+ ```
134
+ import { pattern } from "file.cpat" with params {
135
+ custom-param = "value"
136
+ }
137
+ ```
138
+
139
+ ### Parameter Declaration
140
+ Declare parameters that can be passed to the grammar:
141
+ ```
142
+ use params {
143
+ param-name
144
+ }
145
+ ```
146
+
147
+ ### Default Parameters
148
+ Specify default values for parameters:
149
+ ```
150
+ use params {
151
+ param = default-value
152
+ }
153
+ ```
154
+
155
+ ## Decorators
156
+
157
+ Decorators can be applied to patterns using the `@` syntax:
158
+
159
+ ### Token Decorator
160
+ Specify tokens for a pattern:
161
+ ```
162
+ @tokens([" "])
163
+ spaces = /\s+/
164
+ ```
165
+
166
+ ### Custom Decorators
167
+ Support for custom decorators with various argument types:
168
+ ```
169
+ @decorator() // No arguments
170
+ @decorator(["value"]) // Array argument
171
+ @decorator({"prop": value}) // Object argument
172
+ ```
173
+
174
+ ## Comments
175
+ Add comments using the `#` symbol:
176
+ ```
177
+ # This is a comment
178
+ pattern = "value"
179
+ ```
180
+
181
+ ## Expression Patterns
182
+ Support for recursive expression patterns with optional right association:
183
+ ```
184
+ expression = ternary | variables
185
+ ternary = expression + " ? " + expression + " : " + expression
186
+ ```
187
+
188
+ Add `right` keyword for right association:
189
+ ```
190
+ expression = ternary right | variables
191
+ ```
192
+
193
+ ## Pattern References
194
+ Reference other patterns by name:
195
+ ```
196
+ pattern1 = "value"
197
+ pattern2 = pattern1
198
+ ```
199
+
200
+ ## Pattern Aliasing
201
+ Import patterns with aliases:
202
+ ```
203
+ import { original as alias } from "file.cpat"
204
+ ```
205
+
206
+ ## String Template Patterns
207
+
208
+ Patterns can be defined inline using string templates. This allows for quick pattern definition and testing without creating separate files.
209
+
210
+ ### Basic Example
211
+ ```typescript
212
+ const { fullName } = patterns`
213
+ first-name = "John"
214
+ last-name = "Doe"
215
+ space = /\s+/
216
+ full-name = first-name + space + last-name
217
+ `;
218
+
219
+ const result = fullName.exec("John Doe");
220
+ // result.ast.value will be "John Doe"
221
+ ```
222
+
223
+ ### Complex Example (HTML-like Markup)
224
+ ```typescript
225
+ const { body } = patterns`
226
+ tag-name = /[a-zA-Z_-]+[a-zA-Z0-9_-]*/
227
+ space = /\s+/
228
+ opening-tag = "<" + tag-name + space? + ">"
229
+ closing-tag = "</" + tag-name + space? + ">"
230
+ child = space? + element + space?
231
+ children = (child)*
232
+ element = opening-tag + children + closing-tag
233
+ body = space? + element + space?
234
+ `;
235
+
236
+ const result = body.exec(`
237
+ <div>
238
+ <div></div>
239
+ <div></div>
240
+ </div>
241
+ `, true);
242
+
243
+ // Clean up spaces from the AST
244
+ result?.ast?.findAll(n => n.name.includes("space")).forEach(n => n.remove());
245
+ // result.ast.value will be "<div><div></div><div></div></div>"
246
+ ```
247
+
248
+ ### Key Features
249
+ 1. Patterns are defined using backticks (`)
250
+ 2. Each pattern definition is on a new line
251
+ 3. The `patterns` function returns an object with all defined patterns
252
+ 4. Patterns can be used immediately after definition
253
+ 5. The AST can be manipulated after parsing (e.g., removing spaces)
254
+ 6. The `exec` method can take an optional second parameter to enable debug mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-pattern-parser",
3
- "version": "11.3.4",
3
+ "version": "11.3.6",
4
4
  "description": "Parsing Library for Typescript and Javascript.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.esm.js",
@@ -10,7 +10,28 @@
10
10
  "build": "rollup -c -m",
11
11
  "test": "jest --config=jest.coverage.config.js --coverage"
12
12
  },
13
- "keywords": [],
13
+ "keywords": [
14
+ "parser",
15
+ "ast",
16
+ "pattern-matching",
17
+ "grammar",
18
+ "typescript",
19
+ "javascript",
20
+ "parsing",
21
+ "tree",
22
+ "pattern",
23
+ "syntax",
24
+ "language",
25
+ "compiler",
26
+ "interpreter",
27
+ "lexer",
28
+ "tokenizer",
29
+ "regex",
30
+ "regular-expressions",
31
+ "text-processing",
32
+ "validation",
33
+ "parser-generator"
34
+ ],
14
35
  "devDependencies": {
15
36
  "@types/jest": "^26.0.23",
16
37
  "jest": "^26.6.3",
@@ -1,9 +1,4 @@
1
- import { Context } from "../patterns/Context";
2
- import { Expression } from "../patterns/Expression";
3
- import { Literal } from "../patterns/Literal";
4
1
  import { Pattern } from "../patterns/Pattern";
5
- import { Regex } from "../patterns/Regex";
6
- import { Repeat } from "../patterns/Repeat";
7
2
  import { IVisitor } from "./ivisitor";
8
3
 
9
4
  export class Generator {
@@ -81,7 +81,7 @@ describe("AutoComplete", () => {
81
81
  expect(result.isComplete).toBeFalsy();
82
82
  });
83
83
 
84
- test("Full Pattern Match", () => {
84
+ test("Full Pattern Match Simple", () => {
85
85
  const john = new Literal("john", "John");
86
86
  const space = new Literal("space", " ");
87
87
  const doe = new Literal("doe", "Doe");
@@ -123,6 +123,63 @@ describe("AutoComplete", () => {
123
123
  expect(result.cursor).not.toBeNull();
124
124
  });
125
125
 
126
+ test("Root Regex Pattern suggests customTokens", () => {
127
+ const freeTextPattern = new Regex(
128
+ `free-text`,
129
+ '[(\\w)\\s]+'
130
+ );
131
+
132
+ const customTokensMap:Record<string, string[]> = {
133
+ 'free-text': ['luke',"leia skywalker",'luke skywalker']
134
+ }
135
+ const autoComplete = new AutoComplete(freeTextPattern,{
136
+ customTokens:customTokensMap
137
+ });
138
+ const result = autoComplete.suggestFor("luke");
139
+
140
+ const expected = [
141
+ { text: " skywalker", startIndex: 4 },
142
+ ];
143
+
144
+ expect(result.ast?.value).toBe("luke");
145
+ expect(result.options).toEqual(expected);
146
+ expect(result.errorAtIndex).toBeNull()
147
+ expect(result.isComplete).toBeTruthy();
148
+ expect(result.cursor).not.toBeNull();
149
+
150
+ });
151
+
152
+
153
+ test("Sequence Regex Pattern suggests customTokens", () => {
154
+ const jediLiteral = new Literal("jedi", "jedi ");
155
+ const freeTextPattern = new Regex(
156
+ `free-text`,
157
+ '[(\\w)\\s]+'
158
+ );
159
+ const sequence = new Sequence('sequence', [jediLiteral,freeTextPattern])
160
+
161
+
162
+ const customTokensMap:Record<string, string[]> = {
163
+ 'free-text': ['luke',"leia skywalker",'luke skywalker']
164
+ }
165
+ const autoComplete = new AutoComplete(sequence,{
166
+ customTokens:customTokensMap
167
+ });
168
+ const result = autoComplete.suggestFor("jedi luke sky");
169
+
170
+ const expected = [
171
+ { text: "walker", startIndex: 13 },
172
+ ];
173
+
174
+ expect(result.ast?.value).toBe("jedi luke sky");
175
+ expect(result.options).toEqual(expected);
176
+ expect(result.errorAtIndex).toBeNull()
177
+ expect(result.isComplete).toBeTruthy();
178
+ expect(result.cursor).not.toBeNull();
179
+ });
180
+
181
+
182
+
126
183
  test("Full Pattern Match With Root Repeat", () => {
127
184
  const john = new Literal("john", "John");
128
185
  const space = new Literal("space", " ");
@@ -148,7 +205,7 @@ describe("AutoComplete", () => {
148
205
  expect(result.cursor).not.toBeNull();
149
206
  });
150
207
 
151
- test("Partial", () => {
208
+ test("Partial Simple", () => {
152
209
  const name = new Literal("name", "Name");
153
210
  const autoComplete = new AutoComplete(name);
154
211
  // Use deprecated suggest for code coverage.
@@ -236,6 +293,37 @@ describe("AutoComplete", () => {
236
293
 
237
294
  });
238
295
 
296
+
297
+ test("Options AutoComplete on Root Pattern", () => {
298
+
299
+ const jack = new Literal("first-name", "Jack");
300
+ const john = new Literal("first-name", "John");
301
+
302
+ const names = new Options('names', [jack,john]);
303
+ const divider = new Literal('divider', ', ');
304
+ const repeat = new Repeat('name-list', names, { divider, trimDivider: true });
305
+
306
+ const text = ''
307
+
308
+ const autoCompleteOptions: AutoCompleteOptions = {
309
+ customTokens: {
310
+ 'first-name': ["James"]
311
+ }
312
+ };
313
+ const autoComplete = new AutoComplete(repeat,autoCompleteOptions);
314
+
315
+ const suggestion = autoComplete.suggestFor(text)
316
+
317
+ const expectedOptions = [
318
+ { text: "Jack", startIndex: 0 },
319
+ { text: "John", startIndex: 0 },
320
+ { text: "James", startIndex: 0 },
321
+ ];
322
+
323
+ expect(suggestion.options).toEqual(expectedOptions)
324
+
325
+ })
326
+
239
327
  test("Options AutoComplete On Leaf Pattern", () => {
240
328
  const autoCompleteOptions: AutoCompleteOptions = {
241
329
  greedyPatternNames: ["space"],
@@ -648,4 +736,33 @@ describe("AutoComplete", () => {
648
736
  }
649
737
  ]);
650
738
  });
651
- });
739
+
740
+
741
+ test("Calling AutoComplete -> _getAllOptions Does not mutate customTokensMap", () => {
742
+
743
+ const jediLuke = new Literal(`jedi`, 'luke');
744
+ const names = new Options('names', [jediLuke]);
745
+ const divider = new Literal('divider', ', ');
746
+ const pattern = new Repeat('name-list', names, { divider, trimDivider: true });
747
+
748
+ const customTokensMap:Record<string, string[]> = {
749
+ 'jedi': ["leia"]
750
+ }
751
+
752
+ const copiedCustomTokensMap:Record<string, string[]> = JSON.parse(JSON.stringify(customTokensMap));
753
+
754
+ const autoCompleteOptions: AutoCompleteOptions = {
755
+ greedyPatternNames:['jedi'],
756
+ customTokens: customTokensMap
757
+ };
758
+ const autoComplete = new AutoComplete(pattern,autoCompleteOptions);
759
+
760
+ // provide a non-empty input to trigger the logic flow that hits _getAllOptions
761
+ autoComplete.suggestFor('l')
762
+
763
+ expect(customTokensMap).toEqual(copiedCustomTokensMap)
764
+ })
765
+
766
+ });
767
+
768
+
@@ -159,7 +159,7 @@ export class AutoComplete {
159
159
 
160
160
  private _createSuggestionsFromRoot(): SuggestionOption[] {
161
161
  const suggestions: SuggestionOption[] = [];
162
- const tokens = this._pattern.getTokens();
162
+ const tokens = [...this._pattern.getTokens(),... this._getTokensForPattern(this._pattern)];
163
163
 
164
164
  for (const token of tokens) {
165
165
  if (suggestions.findIndex(s => s.text === token) === -1) {
@@ -175,18 +175,31 @@ export class AutoComplete {
175
175
  return this._createSuggestions(-1, this._getTokensForPattern(this._pattern));
176
176
  }
177
177
 
178
- const leafPattern = match.pattern;
179
- const parent = match.pattern.parent;
180
-
181
- if (parent !== null && match.node != null) {
182
- const patterns = leafPattern.getNextPatterns();
183
-
184
- const tokens = patterns.reduce((acc: string[], pattern) => {
185
- acc.push(...this._getTokensForPattern(pattern));
186
- return acc;
187
- }, []);
188
-
189
- return this._createSuggestions(match.node.lastIndex, tokens);
178
+ if ( match.node != null) {
179
+ const textStartingMatch = this._text.slice(match.node.startIndex,match.node.endIndex)
180
+ const currentPatternsTokens = this._getTokensForPattern(match.pattern);
181
+ /**
182
+ * Compares tokens to current text and extracts remainder tokens
183
+ * - IE. **currentText:** *abc*, **baseToken:** *abcdef*, **trailingToken:** *def*
184
+ */
185
+ const trailingTokens = currentPatternsTokens.reduce<string[]>((acc, token) => {
186
+ if (token.startsWith(textStartingMatch)) {
187
+ const sliced = token.slice(textStartingMatch.length);
188
+ if (sliced !== '') {
189
+ acc.push(sliced);
190
+ }
191
+ }
192
+ return acc;
193
+ }, []);
194
+
195
+ const leafPatterns = match.pattern.getNextPatterns();
196
+ const leafTokens = leafPatterns.reduce((acc: string[], leafPattern) => {
197
+ acc.push(...this._getTokensForPattern(leafPattern));
198
+ return acc;
199
+ }, []);
200
+
201
+ const allTokens = [...trailingTokens,...leafTokens]
202
+ return this._createSuggestions(match.node.lastIndex, allTokens);
190
203
  } else {
191
204
  return [];
192
205
  }
@@ -197,21 +210,21 @@ export class AutoComplete {
197
210
 
198
211
  if (this._options.greedyPatternNames != null && this._options.greedyPatternNames.includes(pattern.name)) {
199
212
  const nextPatterns = pattern.getNextPatterns();
200
- const tokens: string[] = [];
201
-
202
213
  const nextPatternTokens = nextPatterns.reduce((acc: string[], pattern) => {
203
214
  acc.push(...this._getTokensForPattern(pattern));
204
215
  return acc;
205
216
  }, []);
206
-
207
- for (let token of augmentedTokens) {
208
- for (let nextPatternToken of nextPatternTokens) {
209
- tokens.push(token + nextPatternToken);
217
+ // using set to prevent duplicates
218
+ const tokens = new Set<string>();
219
+
220
+ for (const token of augmentedTokens) {
221
+ for (const nextPatternToken of nextPatternTokens) {
222
+ tokens.add(token + nextPatternToken);
210
223
  }
211
224
  }
212
225
 
213
- return tokens;
214
- } else {
226
+ return [...tokens]
227
+ } else {
215
228
  return augmentedTokens;
216
229
  }
217
230
  }
@@ -219,30 +232,39 @@ export class AutoComplete {
219
232
  private _getAugmentedTokens(pattern: Pattern) {
220
233
  const customTokensMap: any = this._options.customTokens || {};
221
234
  const leafPatterns = pattern.getPatterns();
222
- const tokens: string[] = customTokensMap[pattern.name] || [];
223
235
 
224
- leafPatterns.forEach(p => {
225
- const augmentedTokens = customTokensMap[p.name] || [];
226
- tokens.push(...p.getTokens(), ...augmentedTokens);
227
- });
236
+ /** Using Set to
237
+ * - prevent duplicates
238
+ * - prevent mutation of original customTokensMap
239
+ */
240
+ const customTokensForPattern = new Set<string>(customTokensMap[pattern.name] ?? []);
241
+
242
+ for (const lp of leafPatterns) {
243
+ const augmentedTokens = customTokensMap[lp.name] ?? [];
244
+ const lpsCombinedTokens = [...lp.getTokens(), ...augmentedTokens];
228
245
 
229
- return tokens;
246
+ for (const token of lpsCombinedTokens) {
247
+ customTokensForPattern.add(token);
248
+ }
249
+ }
250
+ return [...customTokensForPattern];
230
251
  }
231
252
 
232
253
  private _createSuggestions(lastIndex: number, tokens: string[]): SuggestionOption[] {
233
- let substring = lastIndex === -1 ? "" : this._cursor.getChars(0, lastIndex);
254
+ let textToIndex = lastIndex === -1 ? "" : this._cursor.getChars(0, lastIndex);
234
255
  const suggestionStrings: string[] = [];
235
256
  const options: SuggestionOption[] = [];
236
257
 
237
258
  for (const token of tokens) {
238
- const suggestion = substring + token;
239
- const startsWith = suggestion.startsWith(substring);
259
+ // concatenated for start index identification inside createSuggestion
260
+ const suggestion = textToIndex + token;
240
261
  const alreadyExist = suggestionStrings.includes(suggestion);
241
262
  const isSameAsText = suggestion === this._text;
242
263
 
243
- if (startsWith && !alreadyExist && !isSameAsText) {
264
+ if ( !alreadyExist && !isSameAsText) {
244
265
  suggestionStrings.push(suggestion);
245
- options.push(this._createSuggestion(this._cursor.text, suggestion));
266
+ const suggestionOption = this._createSuggestion(this._cursor.text, suggestion)
267
+ options.push(suggestionOption);
246
268
  }
247
269
  }
248
270
 
@@ -256,12 +278,14 @@ export class AutoComplete {
256
278
  const furthestMatch = findMatchIndex(suggestion, fullText);
257
279
  const text = suggestion.slice(furthestMatch);
258
280
 
259
- return {
281
+ const option:SuggestionOption = {
260
282
  text: text,
261
283
  startIndex: furthestMatch,
262
284
  };
285
+ return option
263
286
  }
264
287
 
288
+
265
289
  static suggestFor(text: string, pattern: Pattern, options?: AutoCompleteOptions) {
266
290
  return new AutoComplete(pattern, options).suggestFor(text);
267
291
  }