clarity-pattern-parser 11.5.4 → 11.6.1
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/architecture.md +300 -0
- package/dist/grammar/Grammar.d.ts +24 -25
- package/dist/grammar/patterns/grammar.d.ts +99 -0
- package/dist/grammar/patterns/grammar.test.d.ts +1 -0
- package/dist/index.browser.js +2314 -2592
- package/dist/index.browser.js.map +1 -1
- package/dist/index.esm.js +2314 -2592
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2314 -2592
- package/dist/index.js.map +1 -1
- package/grammar-guide.md +836 -0
- package/package.json +1 -1
- package/src/grammar/Grammar.test.ts +419 -1
- package/src/grammar/Grammar.ts +483 -515
- package/src/grammar/patterns/grammar.test.ts +276 -0
- package/src/grammar/patterns/grammar.ts +113 -37
- package/src/grammar/patterns.ts +6 -6
- package/src/grammar/patterns/anonymousPattern.ts +0 -23
- package/src/grammar/patterns/body.ts +0 -22
- package/src/grammar/patterns/comment.ts +0 -4
- package/src/grammar/patterns/decoratorStatement.ts +0 -85
- package/src/grammar/patterns/import.ts +0 -88
- package/src/grammar/patterns/literal.ts +0 -4
- package/src/grammar/patterns/literals.ts +0 -21
- package/src/grammar/patterns/name.ts +0 -3
- package/src/grammar/patterns/optionsLiteral.ts +0 -25
- package/src/grammar/patterns/pattern.ts +0 -29
- package/src/grammar/patterns/regexLiteral.ts +0 -5
- package/src/grammar/patterns/repeatLiteral.ts +0 -71
- package/src/grammar/patterns/sequenceLiteral.ts +0 -24
- package/src/grammar/patterns/spaces.ts +0 -16
- package/src/grammar/patterns/statement.ts +0 -22
- package/src/grammar/patterns/takeUtilLiteral.ts +0 -20
- package/src/grammar/spec.md +0 -331
- package/src/grammar_v2/patterns/Grammar.ts +0 -170
- package/src/grammar_v2/patterns/patterns/cpat.cpat +0 -91
- package/src/grammar_v2/patterns/patterns/grammar.test.ts +0 -218
- package/src/grammar_v2/patterns/patterns/grammar.ts +0 -103
package/grammar-guide.md
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
# Clarity Pattern Parser - Grammar Guide
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [Quick Start](#quick-start)
|
|
6
|
+
2. [Grammar Syntax Reference](#grammar-syntax-reference)
|
|
7
|
+
3. [Pattern Types](#pattern-types)
|
|
8
|
+
4. [AST Output Reference](#ast-output-reference)
|
|
9
|
+
5. [Expression Patterns (Operator Precedence)](#expression-patterns)
|
|
10
|
+
6. [Imports and Parameters](#imports-and-parameters)
|
|
11
|
+
7. [Decorators](#decorators)
|
|
12
|
+
8. [Complete Examples](#complete-examples)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Using the Tagged Template Literal
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { patterns } from "clarity-pattern-parser";
|
|
22
|
+
|
|
23
|
+
const { fullName } = patterns`
|
|
24
|
+
first-name = /[A-Z][a-z]+/
|
|
25
|
+
last-name = /[A-Z][a-z]+/
|
|
26
|
+
space = " "
|
|
27
|
+
full-name = first-name + space + last-name
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const result = fullName.exec("John Doe");
|
|
31
|
+
|
|
32
|
+
// result.ast is the AST Node (or null on failure)
|
|
33
|
+
// result.cursor contains error info on failure
|
|
34
|
+
console.log(result.ast?.value); // "John Doe"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Using the Grammar Class
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { Grammar } from "clarity-pattern-parser";
|
|
41
|
+
|
|
42
|
+
const patterns = Grammar.parseString(`
|
|
43
|
+
digit = /\\d/
|
|
44
|
+
digits = (digit)+
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
const result = patterns["digits"].exec("123");
|
|
48
|
+
console.log(result.ast?.value); // "123"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> **Note**: In the tagged template literal, kebab-case names are auto-converted to camelCase for JavaScript access. `full-name` becomes `fullName`. In the `Grammar` class, names stay as-is.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Grammar Syntax Reference
|
|
56
|
+
|
|
57
|
+
### Comments
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
# This is a comment (must be on its own line)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Assignments
|
|
64
|
+
|
|
65
|
+
Every pattern is defined as a named assignment:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
pattern-name = <pattern-definition>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Names must match: `/[a-zA-Z_-]+[a-zA-Z0-9_-]*/`
|
|
72
|
+
|
|
73
|
+
### Literals
|
|
74
|
+
|
|
75
|
+
Double-quoted exact string match:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
greeting = "Hello"
|
|
79
|
+
newline = "\n"
|
|
80
|
+
quote = "\""
|
|
81
|
+
backslash = "\\"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Supported escape sequences: `\n`, `\r`, `\t`, `\b`, `\f`, `\v`, `\0`, `\xHH`, `\uHHHH`, `\"`, `\\`
|
|
85
|
+
|
|
86
|
+
### Regex
|
|
87
|
+
|
|
88
|
+
Forward-slash delimited regular expressions:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
digits = /\d+/
|
|
92
|
+
word = /[a-zA-Z_]\w*/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- Do NOT include `^` or `$` anchors (they are added internally)
|
|
96
|
+
- Uses `gu` flags (global, unicode)
|
|
97
|
+
- Regex patterns match greedily from the current cursor position
|
|
98
|
+
|
|
99
|
+
### Sequence (AND)
|
|
100
|
+
|
|
101
|
+
Concatenation with `+` operator. All parts must match in order:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
full-name = first-name + " " + last-name
|
|
105
|
+
greeting = "Hello" + space + name + "!"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Options (OR)
|
|
109
|
+
|
|
110
|
+
Alternatives with `|` operator:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
# Non-greedy (first match wins):
|
|
114
|
+
name = "John" | "Jane" | "Bob"
|
|
115
|
+
|
|
116
|
+
# Greedy (longest match wins) using <|>:
|
|
117
|
+
token = identifier <|> keyword <|> operator
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The `|` operator tries alternatives in order and returns the first match. The `<|>` operator tries all alternatives and returns the longest match.
|
|
121
|
+
|
|
122
|
+
**Important**: When options contain self-referencing patterns (recursion), the parser automatically converts them into an `Expression` pattern for proper operator precedence handling. See [Expression Patterns](#expression-patterns).
|
|
123
|
+
|
|
124
|
+
### Repeat
|
|
125
|
+
|
|
126
|
+
Repetition with bounds and optional dividers:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
# One or more:
|
|
130
|
+
digits = (digit)+
|
|
131
|
+
|
|
132
|
+
# Zero or more:
|
|
133
|
+
spaces = (space)*
|
|
134
|
+
|
|
135
|
+
# Exact count:
|
|
136
|
+
triple = (digit){3}
|
|
137
|
+
|
|
138
|
+
# Range (min, max):
|
|
139
|
+
few-digits = (digit){2,5}
|
|
140
|
+
|
|
141
|
+
# Min only (2 or more):
|
|
142
|
+
many = (digit){2,}
|
|
143
|
+
|
|
144
|
+
# Max only (0 to 3):
|
|
145
|
+
some = (digit){,3}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### With Divider
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
# Comma-separated digits:
|
|
152
|
+
csv-digits = (digit, comma)+
|
|
153
|
+
|
|
154
|
+
# With trailing divider trimming:
|
|
155
|
+
items = (item, comma trim)+
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The `trim` flag allows a trailing divider at the end (but doesn't require it).
|
|
159
|
+
|
|
160
|
+
### Optional
|
|
161
|
+
|
|
162
|
+
Append `?` to make a pattern optional in a sequence:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
middle-name-section = middle-name + space
|
|
166
|
+
full-name = first-name + space + middle-name-section? + last-name
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Not (Negative Lookahead)
|
|
170
|
+
|
|
171
|
+
Prefix with `!` in a sequence to assert a pattern does NOT match:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
# Match any first-name except "Jack":
|
|
175
|
+
full-name = !jack + first-name + space + last-name
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`!` does not consume input. It only asserts that the pattern doesn't match at the current position.
|
|
179
|
+
|
|
180
|
+
### Take Until
|
|
181
|
+
|
|
182
|
+
Consume all characters until a terminating pattern:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
script-content = ?->| "</script"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Matches everything from the current position up to (but not including) the terminator.
|
|
189
|
+
|
|
190
|
+
### Alias
|
|
191
|
+
|
|
192
|
+
Reference an existing pattern under a new name:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
word = /\w+/
|
|
196
|
+
identifier = word # alias: same regex, new name "identifier"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Anonymous Patterns
|
|
200
|
+
|
|
201
|
+
Inline unnamed patterns in sequences and repeats:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
# Inline literals and regex — fine, they get their value as the AST node name:
|
|
205
|
+
greeting = "Hello" + " " + /\w+/
|
|
206
|
+
|
|
207
|
+
# Complex inline patterns — avoid this:
|
|
208
|
+
items = ("item-a" <|> "item-b" <|> (thing)+)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Recommendation: Only use literals and regex inline.** Inline sequences, options, and repeats produce anonymous AST nodes that are hard to query or work with. If you need to find or walk specific parts of the result, give them names.
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
# Avoid — anonymous nodes you can only navigate by index:
|
|
215
|
+
pairs = ("(" + /\w+/ + ")")+
|
|
216
|
+
|
|
217
|
+
# Prefer — every piece is addressable by name:
|
|
218
|
+
open = "("
|
|
219
|
+
close = ")"
|
|
220
|
+
word = /\w+/
|
|
221
|
+
pair = open + word + close
|
|
222
|
+
pairs = (pair)+
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
With named patterns, you can use `ast.find(n => n.name === "word")` or `ast.findAll(n => n.name === "pair")` to pull out exactly what you need. With anonymous inline patterns, you're stuck counting children by position.
|
|
226
|
+
|
|
227
|
+
Inline literals (`"text"`) and regex (`/pattern/`) are the exception — they use their content as the node name (`"Hello"` becomes a node named `Hello`, `/\d+/` becomes a node named `\d+`), so they remain useful inline.
|
|
228
|
+
|
|
229
|
+
### Export Names
|
|
230
|
+
|
|
231
|
+
Bare names at the end of a grammar re-export an imported pattern:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
import { value } from "other.cpat"
|
|
235
|
+
value # re-exports "value" as a top-level pattern
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Pattern Types
|
|
241
|
+
|
|
242
|
+
Each pattern type creates nodes with a specific `type` field:
|
|
243
|
+
|
|
244
|
+
| Grammar Syntax | Pattern Class | Node `type` |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `"text"` | `Literal` | `"literal"` |
|
|
247
|
+
| `/regex/` | `Regex` | `"regex"` |
|
|
248
|
+
| `a + b` | `Sequence` | `"sequence"` |
|
|
249
|
+
| `a \| b` | `Options` | `"options"` |
|
|
250
|
+
| `a \| b` (with recursion) | `Expression` | `"expression"` |
|
|
251
|
+
| `(a)+` | `Repeat` | `"infinite-repeat"` or `"finite-repeat"` |
|
|
252
|
+
| `a?` | `Optional` | `"optional"` |
|
|
253
|
+
| `!a` | `Not` | `"not"` |
|
|
254
|
+
| `?->\| a` | `TakeUntil` | `"take-until"` |
|
|
255
|
+
| `alias = other` | Cloned pattern | Same as original |
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## AST Output Reference
|
|
260
|
+
|
|
261
|
+
### Node Structure
|
|
262
|
+
|
|
263
|
+
Every successful parse returns a `Node` (or `null` on failure):
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
interface ParseResult {
|
|
267
|
+
ast: Node | null; // The AST root node, or null if parsing failed
|
|
268
|
+
cursor: Cursor; // Cursor with error info if ast is null
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A `Node` has:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
node.type // string - pattern type ("literal", "regex", "sequence", "expression", etc.)
|
|
276
|
+
node.name // string - the pattern name you defined in the grammar
|
|
277
|
+
node.value // string - the matched text (leaf nodes store directly; composite nodes concatenate children)
|
|
278
|
+
node.startIndex // number - 0-based inclusive start position in source text
|
|
279
|
+
node.endIndex // number - 0-based exclusive end position in source text
|
|
280
|
+
node.children // readonly Node[] - child nodes (empty for leaf nodes)
|
|
281
|
+
node.parent // Node | null - parent node reference
|
|
282
|
+
node.isLeaf // boolean - true if no children
|
|
283
|
+
node.hasChildren // boolean - true if has children
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Serialization
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Cycle-free plain object (safe for JSON):
|
|
290
|
+
node.toCycleFreeObject()
|
|
291
|
+
// Returns: { id, type, name, value, startIndex, endIndex, children: [...] }
|
|
292
|
+
|
|
293
|
+
// JSON string:
|
|
294
|
+
node.toJson(2) // pretty-printed with 2-space indent
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Output by Pattern Type
|
|
298
|
+
|
|
299
|
+
#### Literal
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
greeting = "Hello"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Parsing `"Hello"`:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
Node {
|
|
309
|
+
type: "literal"
|
|
310
|
+
name: "greeting"
|
|
311
|
+
value: "Hello"
|
|
312
|
+
startIndex: 0
|
|
313
|
+
endIndex: 5
|
|
314
|
+
children: [] # Always empty - leaf node
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### Regex
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
digits = /\d+/
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Parsing `"42"`:
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
Node {
|
|
328
|
+
type: "regex"
|
|
329
|
+
name: "digits"
|
|
330
|
+
value: "42"
|
|
331
|
+
startIndex: 0
|
|
332
|
+
endIndex: 2
|
|
333
|
+
children: [] # Always empty - leaf node
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Sequence
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
first-name = /[A-Z][a-z]+/
|
|
341
|
+
space = " "
|
|
342
|
+
last-name = /[A-Z][a-z]+/
|
|
343
|
+
full-name = first-name + space + last-name
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Parsing `"John Doe"`:
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
Node {
|
|
350
|
+
type: "sequence"
|
|
351
|
+
name: "full-name"
|
|
352
|
+
value: "John Doe"
|
|
353
|
+
startIndex: 0
|
|
354
|
+
endIndex: 8
|
|
355
|
+
children: [
|
|
356
|
+
Node { type: "regex", name: "first-name", value: "John", startIndex: 0, endIndex: 4 }
|
|
357
|
+
Node { type: "literal", name: "space", value: " ", startIndex: 4, endIndex: 5 }
|
|
358
|
+
Node { type: "regex", name: "last-name", value: "Doe", startIndex: 5, endIndex: 8 }
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Key**: Sequence nodes contain one child per matched sub-pattern. Optional patterns that don't match are excluded from children.
|
|
364
|
+
|
|
365
|
+
#### Options
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
name = "John" | "Jane"
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Parsing `"Jane"`:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
Node {
|
|
375
|
+
type: "literal" # The winning alternative's type, NOT "options"
|
|
376
|
+
name: "Jane" # The winning alternative's name
|
|
377
|
+
value: "Jane"
|
|
378
|
+
startIndex: 0
|
|
379
|
+
endIndex: 4
|
|
380
|
+
children: []
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Key**: Options do NOT wrap the result. The node returned is the matched alternative directly.
|
|
385
|
+
|
|
386
|
+
#### Repeat
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
digit = /\d/
|
|
390
|
+
digits = (digit)+
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Parsing `"123"`:
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
Node {
|
|
397
|
+
type: "infinite-repeat"
|
|
398
|
+
name: "digits"
|
|
399
|
+
value: "123"
|
|
400
|
+
startIndex: 0
|
|
401
|
+
endIndex: 3
|
|
402
|
+
children: [
|
|
403
|
+
Node { type: "regex", name: "digit", value: "1", startIndex: 0, endIndex: 1 }
|
|
404
|
+
Node { type: "regex", name: "digit", value: "2", startIndex: 1, endIndex: 2 }
|
|
405
|
+
Node { type: "regex", name: "digit", value: "3", startIndex: 2, endIndex: 3 }
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### Repeat with Divider
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
digit = /\d+/
|
|
414
|
+
comma = ","
|
|
415
|
+
csv = (digit, comma)+
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Parsing `"1,2,3"`:
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
Node {
|
|
422
|
+
type: "infinite-repeat"
|
|
423
|
+
name: "csv"
|
|
424
|
+
value: "1,2,3"
|
|
425
|
+
children: [
|
|
426
|
+
Node { type: "regex", name: "digit", value: "1" }
|
|
427
|
+
Node { type: "literal", name: "comma", value: "," }
|
|
428
|
+
Node { type: "regex", name: "digit", value: "2" }
|
|
429
|
+
Node { type: "literal", name: "comma", value: "," }
|
|
430
|
+
Node { type: "regex", name: "digit", value: "3" }
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Key**: Divider nodes are interleaved between the repeated pattern nodes. With `trim`, a trailing divider is allowed but included if present.
|
|
436
|
+
|
|
437
|
+
#### Optional (in Sequence)
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
middle = /[A-Z]\./
|
|
441
|
+
full = first + " " + middle? + " "? + last
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
If `middle` matches, it appears as a child. If it doesn't match, it's simply absent from the children array. No wrapper node is created.
|
|
445
|
+
|
|
446
|
+
#### Expression (Operator Precedence)
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
integer = /\d+/
|
|
450
|
+
add-expr = expr + " + " + expr
|
|
451
|
+
mul-expr = expr + " * " + expr
|
|
452
|
+
expr = mul-expr | add-expr | integer
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Parsing `"1 + 2 * 3"`:
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
Node {
|
|
459
|
+
type: "expression"
|
|
460
|
+
name: "add-expr"
|
|
461
|
+
value: "1 + 2 * 3"
|
|
462
|
+
children: [
|
|
463
|
+
Node { type: "regex", name: "integer", value: "1" }
|
|
464
|
+
Node { type: "literal", name: " + ", value: " + " }
|
|
465
|
+
Node {
|
|
466
|
+
type: "expression"
|
|
467
|
+
name: "mul-expr"
|
|
468
|
+
value: "2 * 3"
|
|
469
|
+
children: [
|
|
470
|
+
Node { type: "regex", name: "integer", value: "2" }
|
|
471
|
+
Node { type: "literal", name: " * ", value: " * " }
|
|
472
|
+
Node { type: "regex", name: "integer", value: "3" }
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**Key**: The Expression pattern builds a proper precedence tree. Infix nodes have type `"expression"` and the name of the matched operator pattern. Children include the left operand, operator token(s), and right operand.
|
|
480
|
+
|
|
481
|
+
#### Prefix/Postfix Expression
|
|
482
|
+
|
|
483
|
+
```
|
|
484
|
+
integer = /\d+/
|
|
485
|
+
unary = "-" + expr
|
|
486
|
+
postfix = expr + "++"
|
|
487
|
+
binary = expr + " + " + expr
|
|
488
|
+
expr = postfix | unary | binary | integer
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Parsing `"-10++"`:
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
Node {
|
|
495
|
+
type: "expression"
|
|
496
|
+
name: "unary"
|
|
497
|
+
value: "-10++"
|
|
498
|
+
children: [
|
|
499
|
+
Node { type: "literal", name: "-", value: "-" }
|
|
500
|
+
Node {
|
|
501
|
+
type: "expression"
|
|
502
|
+
name: "postfix"
|
|
503
|
+
value: "10++"
|
|
504
|
+
children: [
|
|
505
|
+
Node { type: "regex", name: "integer", value: "10" }
|
|
506
|
+
Node { type: "literal", name: "++", value: "++" }
|
|
507
|
+
]
|
|
508
|
+
}
|
|
509
|
+
]
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### Take Until
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
content = ?->| "</script"
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Parsing `"function(){}</script"`:
|
|
520
|
+
|
|
521
|
+
```
|
|
522
|
+
Node {
|
|
523
|
+
type: "take-until"
|
|
524
|
+
name: "content"
|
|
525
|
+
value: "function(){}"
|
|
526
|
+
startIndex: 0
|
|
527
|
+
endIndex: 12
|
|
528
|
+
children: []
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Key**: Consumes everything BEFORE the terminator. The terminator itself is NOT included.
|
|
533
|
+
|
|
534
|
+
### Traversal and Manipulation
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
const result = pattern.exec("some text");
|
|
538
|
+
const ast = result.ast;
|
|
539
|
+
|
|
540
|
+
// Find first node by predicate:
|
|
541
|
+
const nameNode = ast.find(n => n.name === "first-name");
|
|
542
|
+
|
|
543
|
+
// Find all nodes by predicate:
|
|
544
|
+
const allNames = ast.findAll(n => n.name.includes("name"));
|
|
545
|
+
|
|
546
|
+
// Walk tree (depth-first, children first):
|
|
547
|
+
ast.walkUp(node => {
|
|
548
|
+
console.log(node.name, node.value);
|
|
549
|
+
// Return false to stop walking
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Walk tree (depth-first, parent first):
|
|
553
|
+
ast.walkDown(node => {
|
|
554
|
+
console.log(node.name, node.value);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Walk breadth-first:
|
|
558
|
+
ast.walkBreadthFirst(node => { /* ... */ });
|
|
559
|
+
|
|
560
|
+
// Get leaf nodes only:
|
|
561
|
+
const leaves = ast.flatten();
|
|
562
|
+
|
|
563
|
+
// Remove whitespace nodes:
|
|
564
|
+
ast.findAll(n => n.name === "space").forEach(n => n.remove());
|
|
565
|
+
|
|
566
|
+
// Collapse node to single value (removes children):
|
|
567
|
+
node.compact();
|
|
568
|
+
|
|
569
|
+
// Transform with visitor pattern:
|
|
570
|
+
ast.transform({
|
|
571
|
+
"space": (node) => { node.remove(); return node; },
|
|
572
|
+
"name": (node) => { /* transform */ return node; }
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Navigate siblings:
|
|
576
|
+
const next = node.nextSibling();
|
|
577
|
+
const prev = node.previousSibling();
|
|
578
|
+
|
|
579
|
+
// Find ancestor:
|
|
580
|
+
const parent = node.findAncestor(n => n.name === "expression");
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
## Expression Patterns
|
|
586
|
+
|
|
587
|
+
Expression patterns handle operator precedence and associativity automatically. They're created implicitly when an options pattern (`|`) contains self-referencing alternatives.
|
|
588
|
+
|
|
589
|
+
### How Precedence Works
|
|
590
|
+
|
|
591
|
+
Precedence is determined by **declaration order** - patterns listed first have **higher precedence** (bind tighter):
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
integer = /\d+/
|
|
595
|
+
mul-expr = expr + " * " + expr # Higher precedence (listed first)
|
|
596
|
+
add-expr = expr + " + " + expr # Lower precedence (listed second)
|
|
597
|
+
expr = mul-expr | add-expr | integer
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
`2 + 3 * 4` parses as `2 + (3 * 4)` because `mul-expr` has higher precedence.
|
|
601
|
+
|
|
602
|
+
### Auto-Classification
|
|
603
|
+
|
|
604
|
+
The Expression pattern examines each alternative's structure:
|
|
605
|
+
|
|
606
|
+
| Structure | Classification | Example |
|
|
607
|
+
|---|---|---|
|
|
608
|
+
| No self-reference | **Atom** | `integer`, `"(" + expr + ")"` |
|
|
609
|
+
| Self-ref at end only | **Prefix** | `"-" + expr` |
|
|
610
|
+
| Self-ref at start only | **Postfix** | `expr + "++"` |
|
|
611
|
+
| Self-ref at both ends | **Infix** | `expr + " + " + expr` |
|
|
612
|
+
|
|
613
|
+
"Self-reference" means the pattern references the expression pattern itself (by name).
|
|
614
|
+
|
|
615
|
+
### Right Associativity
|
|
616
|
+
|
|
617
|
+
By default, infix operators are left-associative. Use the `right` keyword for right-associativity:
|
|
618
|
+
|
|
619
|
+
```
|
|
620
|
+
ternary = expr + " ? " + expr + " : " + expr
|
|
621
|
+
expr = ternary right | integer
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
`a ? b : c ? d : e` parses as `a ? b : (c ? d : e)` instead of `(a ? b : c) ? d : e`.
|
|
625
|
+
|
|
626
|
+
### Complete Expression Example
|
|
627
|
+
|
|
628
|
+
```
|
|
629
|
+
integer = /\d+/
|
|
630
|
+
variable = /[a-z]/
|
|
631
|
+
open-paren = "("
|
|
632
|
+
close-paren = ")"
|
|
633
|
+
|
|
634
|
+
group = open-paren + expr + close-paren
|
|
635
|
+
mul-expr = expr + " * " + expr
|
|
636
|
+
div-expr = expr + " / " + expr
|
|
637
|
+
add-expr = expr + " + " + expr
|
|
638
|
+
sub-expr = expr + " - " + expr
|
|
639
|
+
neg-expr = "-" + expr
|
|
640
|
+
post-inc = expr + "++"
|
|
641
|
+
|
|
642
|
+
expr = mul-expr | div-expr | add-expr | sub-expr | neg-expr | post-inc | group | integer | variable
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Imports and Parameters
|
|
648
|
+
|
|
649
|
+
### Importing Patterns
|
|
650
|
+
|
|
651
|
+
```
|
|
652
|
+
import { pattern-name } from "path/to/grammar.cpat"
|
|
653
|
+
import { old-name as new-name } from "grammar.cpat"
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Multiple imports:
|
|
657
|
+
|
|
658
|
+
```
|
|
659
|
+
import { alpha, beta } from "greek.cpat"
|
|
660
|
+
import { one, two, three } from "numbers.cpat"
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Parameterized Grammars
|
|
664
|
+
|
|
665
|
+
Define parameters in the imported grammar:
|
|
666
|
+
|
|
667
|
+
```
|
|
668
|
+
# divider.cpat
|
|
669
|
+
use params { separator }
|
|
670
|
+
items = (item, separator)+
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Pass values when importing:
|
|
674
|
+
|
|
675
|
+
```
|
|
676
|
+
import { items } from "divider.cpat" with params {
|
|
677
|
+
separator = ","
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
Parameters with defaults:
|
|
682
|
+
|
|
683
|
+
```
|
|
684
|
+
use params {
|
|
685
|
+
separator = default-separator
|
|
686
|
+
}
|
|
687
|
+
default-separator = ","
|
|
688
|
+
items = (item, separator)+
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
If a param is supplied, it overrides the default. Otherwise the default is used.
|
|
692
|
+
|
|
693
|
+
### Providing resolveImport
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
const patterns = await Grammar.parse(grammarString, {
|
|
697
|
+
resolveImport: async (resource, originResource) => {
|
|
698
|
+
const content = await fs.readFile(resolve(originResource, resource), "utf-8");
|
|
699
|
+
return { expression: content, resource };
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// Synchronous version:
|
|
704
|
+
const patterns = Grammar.parseString(grammarString, {
|
|
705
|
+
resolveImportSync: (resource, originResource) => {
|
|
706
|
+
const content = fs.readFileSync(resolve(originResource, resource), "utf-8");
|
|
707
|
+
return { expression: content, resource };
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## Decorators
|
|
715
|
+
|
|
716
|
+
Decorators modify patterns before they're finalized:
|
|
717
|
+
|
|
718
|
+
```
|
|
719
|
+
# Method decorator with args:
|
|
720
|
+
@tokens([" ", "\t"])
|
|
721
|
+
whitespace = /\s+/
|
|
722
|
+
|
|
723
|
+
# Method decorator without args:
|
|
724
|
+
@tokens()
|
|
725
|
+
spaces = /\s+/
|
|
726
|
+
|
|
727
|
+
# Name decorator (no parens):
|
|
728
|
+
@myDecorator
|
|
729
|
+
pattern = /\w+/
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### Built-in: @tokens
|
|
733
|
+
|
|
734
|
+
Sets the token hints used by intellisense:
|
|
735
|
+
|
|
736
|
+
```
|
|
737
|
+
@tokens(["(", ")"])
|
|
738
|
+
parens = /[()]/
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Custom Decorators
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
import { createPatternsTemplate } from "clarity-pattern-parser";
|
|
745
|
+
|
|
746
|
+
const patterns = createPatternsTemplate({
|
|
747
|
+
decorators: {
|
|
748
|
+
myDecorator: (pattern, arg) => {
|
|
749
|
+
// Modify the pattern
|
|
750
|
+
console.log(`Decorated: ${pattern.name}`, arg);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const { result } = patterns`
|
|
756
|
+
@myDecorator({"key": "value"})
|
|
757
|
+
result = /\w+/
|
|
758
|
+
`;
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Decorator arguments are JSON-parsed. Supported types: arrays, objects, strings, numbers, booleans, null.
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## Complete Examples
|
|
766
|
+
|
|
767
|
+
### JSON Parser
|
|
768
|
+
|
|
769
|
+
```
|
|
770
|
+
true-value = "true"
|
|
771
|
+
false-value = "false"
|
|
772
|
+
null-value = "null"
|
|
773
|
+
comma = ","
|
|
774
|
+
colon = ":"
|
|
775
|
+
open-bracket = "["
|
|
776
|
+
close-bracket = "]"
|
|
777
|
+
open-brace = "{"
|
|
778
|
+
close-brace = "}"
|
|
779
|
+
|
|
780
|
+
spaces = /\s*/
|
|
781
|
+
string = /"([^"\\]|\\.)*"/
|
|
782
|
+
number = /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/
|
|
783
|
+
|
|
784
|
+
value = string | number | true-value | false-value | null-value | array | object
|
|
785
|
+
|
|
786
|
+
array-items = (value, /\s*,\s*/ trim)*
|
|
787
|
+
array = open-bracket + spaces + array-items + spaces + close-bracket
|
|
788
|
+
|
|
789
|
+
pair = spaces + string + spaces + colon + spaces + value + spaces
|
|
790
|
+
pairs = (pair, comma trim)*
|
|
791
|
+
object = open-brace + spaces + pairs + spaces + close-brace
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Simple Markup
|
|
795
|
+
|
|
796
|
+
```
|
|
797
|
+
tag-name = /[a-zA-Z_-]+[a-zA-Z0-9_-]*/
|
|
798
|
+
space = /\s+/
|
|
799
|
+
opening-tag = "<" + tag-name + space? + ">"
|
|
800
|
+
closing-tag = "</" + tag-name + space? + ">"
|
|
801
|
+
child = space? + element + space?
|
|
802
|
+
children = (child)*
|
|
803
|
+
element = opening-tag + children + closing-tag
|
|
804
|
+
body = space? + element + space?
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### Math Expression
|
|
808
|
+
|
|
809
|
+
```
|
|
810
|
+
integer = /\d+/
|
|
811
|
+
operator = "+" | "-" | "*" | "/"
|
|
812
|
+
unary-operator = "+" | "-"
|
|
813
|
+
postfix-operator = "++" | "--"
|
|
814
|
+
|
|
815
|
+
binary-expr = expr + operator + expr
|
|
816
|
+
unary-expr = unary-operator + expr
|
|
817
|
+
postfix-expr = expr + postfix-operator
|
|
818
|
+
|
|
819
|
+
expr = postfix-expr | unary-expr | binary-expr | integer
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### Recursive Array
|
|
823
|
+
|
|
824
|
+
```
|
|
825
|
+
digit = /\d+/
|
|
826
|
+
divider = /\s*,\s*/
|
|
827
|
+
open-bracket = "["
|
|
828
|
+
close-bracket = "]"
|
|
829
|
+
spaces = /\s+/
|
|
830
|
+
|
|
831
|
+
items = digit | array
|
|
832
|
+
array-items = (items, divider trim)*
|
|
833
|
+
array = open-bracket + spaces? + array-items + spaces? + close-bracket
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Parsing `"[1, [2, 3], []]"` produces a nested AST matching the recursive structure.
|