parseman 0.1.7 → 0.3.0
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 +192 -155
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts +17 -0
- package/dist/combinators/completions.d.ts.map +1 -0
- package/dist/combinators/grammar.d.ts +20 -4
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/guard.d.ts +2 -2
- package/dist/combinators/guard.d.ts.map +1 -1
- package/dist/combinators/keywords.d.ts +22 -0
- package/dist/combinators/keywords.d.ts.map +1 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +24 -0
- package/dist/combinators/node.d.ts.map +1 -0
- package/dist/combinators/parser.d.ts +7 -7
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/probe.d.ts +13 -0
- package/dist/combinators/probe.d.ts.map +1 -0
- package/dist/combinators/recover.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/trivia-skip.d.ts +33 -0
- package/dist/combinators/trivia-skip.d.ts.map +1 -0
- package/dist/combinators/withCtx.d.ts +1 -1
- package/dist/compiler/codegen.d.ts +13 -4
- package/dist/compiler/codegen.d.ts.map +1 -1
- package/dist/cst/grammar.d.ts +33 -2
- package/dist/cst/grammar.d.ts.map +1 -1
- package/dist/cst/incremental.d.ts +4 -2
- package/dist/cst/incremental.d.ts.map +1 -1
- package/dist/cst/trivia-index.d.ts +50 -0
- package/dist/cst/trivia-index.d.ts.map +1 -0
- package/dist/cst/types.d.ts +7 -7
- package/dist/cst/types.d.ts.map +1 -1
- package/dist/functional/doc.d.ts +53 -0
- package/dist/functional/doc.d.ts.map +1 -0
- package/dist/index.cjs +953 -221
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6519 -226
- package/dist/index.js.map +4 -4
- package/dist/plugin/evaluator.d.ts +26 -6
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +1164 -215
- package/dist/plugin/index.cjs.map +4 -4
- package/dist/plugin/index.d.ts +5 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1164 -215
- package/dist/plugin/index.js.map +4 -4
- package/dist/types.d.ts +51 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Write parsers in TypeScript — fast enough to run as-is, and blazing fast when the bundler macro kicks in. Same code either way; no grammar files, no generated output to check in. Drop the plugin in tests or anywhere a bundler isn't around and everything still works.
|
|
8
8
|
|
|
9
|
+
_Note: Not necessarily production-ready! I still have test cases and sample parsers I want to build to more rigorously test the API and performance._
|
|
10
|
+
|
|
9
11
|
## Benchmarks
|
|
10
12
|
|
|
11
13
|
Measured on Apple M2 Pro. Bars show µs per parse — shorter is faster.
|
|
@@ -14,7 +16,11 @@ Measured on Apple M2 Pro. Bars show µs per parse — shorter is faster.
|
|
|
14
16
|
|
|
15
17
|

|
|
16
18
|
|
|
17
|
-
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
Parséman has three modes — **interpreter** (zero setup, works anywhere), **macro build** (compiled by the bundler plugin at build time, zero runtime cost), and **`.compile()`** (optional runtime JIT). Most production use lands on one of the first two. The initialization section only shows parsers with a nonzero setup cost: `.compile()` costs 85–680 µs depending on grammar size; Chevrotain always costs 950–1,300 µs. Parsers not listed there start for free.
|
|
22
|
+
|
|
23
|
+
On JSON, Parséman macro beats Peggy at small and medium sizes; Peggy pulls ahead by ~19% at 12 kB — it's been doing this a while. On CSV and GraphQL, where the grammar is non-recursive or fully inlineable, Parséman macro is the clear winner — CSV macro is now ~6× faster than Peggy on the 500-row fixture. For CST-building grammars (`node()` rules with trivia capture), the interpreter runs ~2× faster than Chevrotain on the JSON CST fixture while building an equivalent tree.
|
|
18
24
|
|
|
19
25
|
---
|
|
20
26
|
|
|
@@ -106,7 +112,7 @@ If `with { type: 'macro' }` is stripped (older bundlers, test runners), the attr
|
|
|
106
112
|
|
|
107
113
|
### What gets compiled
|
|
108
114
|
|
|
109
|
-
|
|
115
|
+
Combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `oneOrMore`, `optional`, `sepBy`, `transform`, `skip`, `not`, `scanTo`, `balanced` — plus `rules()` factories (including mutually recursive ones), `parser({ trivia })` wrappers, and `node()` rules (CST capture and all). A full grammar built as a `rules()` factory of `node()` rules compiles end to end: each rule becomes an independently-callable function, terminal/trivia capture is emitted inline, and every `build`/`transform` callback is inlined with its source span. Grammars with no `node()` emit zero capture code, so they compile byte-identically. Parsers that close over external variables the evaluator can't resolve stay as-is — the plugin compiles what it can and quietly leaves the rest alone.
|
|
110
116
|
|
|
111
117
|
---
|
|
112
118
|
|
|
@@ -115,6 +121,8 @@ Pure combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `one
|
|
|
115
121
|
| Combinator | Description |
|
|
116
122
|
|---|---|
|
|
117
123
|
| `literal(s, opts?)` | Exact string match. `opts.caseInsensitive` for locale-aware comparison. |
|
|
124
|
+
| `word(s, boundary?)` | Single keyword with automatic word-boundary guard. |
|
|
125
|
+
| `wordContext(boundary?)` | Returns a keyword factory with a baked-in boundary (e.g. CSS `A-Za-z0-9_-`). |
|
|
118
126
|
| `regex(pattern)` | Match a regex at the current position. Patterns are optimized via `regexp-tree`. |
|
|
119
127
|
| `sequence(...parsers)` | Match all in order; returns a tuple `[v1, v2, ...]`. Skips trivia between terms when trivia is set. |
|
|
120
128
|
| `choice(...parsers)` | Ordered alternatives (PEG — first match wins). Disjoint first chars → O(1) dispatch. |
|
|
@@ -124,29 +132,53 @@ Pure combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `one
|
|
|
124
132
|
| `sepBy(parser, sep)` | Zero or more `parser` separated by `sep`. |
|
|
125
133
|
| `transform(parser, fn)` | Map the result: `fn(value, span) → newValue`. |
|
|
126
134
|
| `skip(main, skipped)` | Match `main` then `skipped`; return `main`'s value. |
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
135
|
+
| `rules(factory)` | Named grammar rules — no forward declarations needed, handles mutual recursion. |
|
|
136
|
+
| `node(type, parser, build)` | CST/AST rule: captures `parser`'s terminals into `children`/`rawChildren` and trivia offsets into a flat `triviaLog`, then calls `build(children, rawChildren, span, triviaLog)`. |
|
|
137
|
+
| `ref<T>()` | Low-level forward declaration slot (use `rules()` in most cases). |
|
|
129
138
|
| `not(parser)` | Negative lookahead — succeeds (consuming nothing) when `parser` fails. |
|
|
130
139
|
| `guard(predicate)` | Succeeds only when `predicate(ctx)` returns true; used for context-sensitive rules. |
|
|
131
140
|
| `withCtx(extra, parser)` | Merge `extra` into the user context for the duration of `parser`. |
|
|
132
141
|
| `recover(parser, sentinel)` | On failure, skip input until `sentinel` matches; returns a `CSTError` node. |
|
|
133
|
-
| `scanTo(sentinel,
|
|
134
|
-
| `balanced(open, close)` | Match a balanced
|
|
142
|
+
| `scanTo(sentinel, opts?)` | Scan forward until `sentinel` matches (sentinel not consumed). Pass `opts.skip` to treat certain patterns as opaque blobs that may contain the sentinel character. Pass `opts.orEOF: true` to succeed at end-of-input. |
|
|
143
|
+
| `balanced(open, close, opts?)` | Match a single balanced delimited region — e.g. `(…)` or `[…]` — including the delimiters. Primarily used as an element of `scanTo`'s `opts.skip` list. |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Performance: collapse opaque shapes into one regex
|
|
148
|
+
|
|
149
|
+
The single biggest grammar-level perf lever is **the number of combinator boundaries on the hot path**. Every `sequence` / `regex` / `oneOrMore` step is a function call plus a result-object allocation plus — in a `node()` rule — a leaf push. Fewer, fatter combinators beat many thin ones.
|
|
150
|
+
|
|
151
|
+
Measured on a repeated 3-shape group (`name1 1px #111 …`, ~29 KB), parsing the same content three ways:
|
|
152
|
+
|
|
153
|
+
| Approach | Interpreted | Compiled |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `oneOrMore(sequence(ident, sp, num, sp, hex, sp))` | 0.289 ms | 0.167 ms |
|
|
156
|
+
| same, with **inline** `regex(…)` instead of shared refs | 0.281 ms | 0.183 ms |
|
|
157
|
+
| **one** `regex(/…ident…num…hex…/)` per group | **0.055 ms** | **0.042 ms** |
|
|
158
|
+
|
|
159
|
+
Two takeaways:
|
|
160
|
+
|
|
161
|
+
- **Shared combinator ref vs inline `regex(…)` literal makes no difference.** Both produce the identical runtime structure (one `regex` combinator either way), and the compiler inlines a `regex` test at every use site regardless of sharing — there's no function-call indirection to eliminate (that only exists for `ref()` / `rules()` entries). Factor out shared terminals for readability; it costs nothing.
|
|
162
|
+
- **Collapsing a fixed multi-token shape into a single `regex` is 4–5× faster** in both the interpreter and compiled output, because it erases the per-step call + allocation overhead. `compile()` is a real but smaller win (~1.7×) and **stacks** with collapsing.
|
|
163
|
+
|
|
164
|
+
**When to collapse:** only where the CST treats the group as opaque text — a dimension `\d+px`, a hex color, an `nth` expression, a simple ident-run. A single regex yields **one leaf**, not structured sub-nodes.
|
|
165
|
+
|
|
166
|
+
**When NOT to collapse:** keep the parts as separate combinators wherever the builder needs them as distinct CST children — for per-field source spans (`fieldSpans`/`valueSpans`), for trivia recovered *between* the parts, or for distinct typed nodes. Correctness first; collapse only the genuinely opaque runs.
|
|
135
167
|
|
|
136
168
|
---
|
|
137
169
|
|
|
138
170
|
## Whitespace and comment skipping
|
|
139
171
|
|
|
140
|
-
|
|
172
|
+
Wrap your root combinator with `parser()` to declare trivia — whitespace and comments to skip between tokens. This bakes the setting into the combinator tree so all modes (interpreter, `compile()`, macro build) behave identically:
|
|
141
173
|
|
|
142
174
|
```ts
|
|
143
|
-
import { regex, sepBy, literal
|
|
175
|
+
import { parser, regex, trivia, sepBy, literal } from 'parseman'
|
|
144
176
|
|
|
145
|
-
const ws = regex(/\s*/)
|
|
177
|
+
const ws = trivia(regex(/\s*/))
|
|
146
178
|
const word = regex(/[a-z]+/)
|
|
147
|
-
const list = sepBy(word, literal(','))
|
|
179
|
+
const list = parser({ trivia: ws }, sepBy(word, literal(',')))
|
|
148
180
|
|
|
149
|
-
parse(
|
|
181
|
+
list.parse('foo , bar , baz')
|
|
150
182
|
// { ok: true, value: ['foo', 'bar', 'baz'], ... }
|
|
151
183
|
```
|
|
152
184
|
|
|
@@ -155,15 +187,7 @@ Multiple trivia types — whitespace and comments — combine with `choice()` an
|
|
|
155
187
|
```ts
|
|
156
188
|
const lineComment = sequence(literal('//'), regex(/[^\n]*/))
|
|
157
189
|
const blockComment = sequence(literal('/*'), scanTo(literal('*/'), []))
|
|
158
|
-
const
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Use `grammar(opts, root)` instead of the `parse()` trivia option when you want trivia only for a subtree within a larger parse:
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
import { grammar } from 'parseman'
|
|
165
|
-
|
|
166
|
-
const jsonValue = grammar({ trivia: ws }, choice(object, array, str, num, bool, nil))
|
|
190
|
+
const ws = trivia(many(choice(regex(/\s+/), lineComment, blockComment)))
|
|
167
191
|
```
|
|
168
192
|
|
|
169
193
|
---
|
|
@@ -172,7 +196,20 @@ const jsonValue = grammar({ trivia: ws }, choice(object, array, str, num, bool,
|
|
|
172
196
|
|
|
173
197
|
`choice()` uses PEG ordered-choice semantics: first match wins. **Order matters.**
|
|
174
198
|
|
|
175
|
-
For keywords — where `if` should not match the prefix of `ifdef` — use `
|
|
199
|
+
For keywords — where `if` should not match the prefix of `ifdef` — use `word()` or `wordContext()`:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const kw = wordContext() // bakes in a default identifier boundary once
|
|
203
|
+
|
|
204
|
+
const token = choice(
|
|
205
|
+
kw('if'),
|
|
206
|
+
kw('else'),
|
|
207
|
+
kw('return'),
|
|
208
|
+
regex(/[a-zA-Z_]\w*/), // ident fallback
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or build the boundary guard by hand with `not()`:
|
|
176
213
|
|
|
177
214
|
```ts
|
|
178
215
|
const wordChar = regex(/\w/)
|
|
@@ -196,20 +233,20 @@ const op = choice(literal('instanceof'), literal('in'), literal('if'))
|
|
|
196
233
|
|
|
197
234
|
---
|
|
198
235
|
|
|
199
|
-
##
|
|
236
|
+
## Named and recursive rules
|
|
200
237
|
|
|
201
|
-
Use `
|
|
238
|
+
Use `rules()` when your combinators need to reference each other by name. Pass a factory that receives all rule names as ready-to-use references and returns the definitions:
|
|
202
239
|
|
|
203
240
|
```ts
|
|
204
|
-
import { parser, choice, sequence, literal, sepBy, transform, regex } from 'parseman'
|
|
241
|
+
import { rules, parser, choice, sequence, literal, sepBy, transform, trivia, regex } from 'parseman'
|
|
205
242
|
import type { Combinator } from 'parseman'
|
|
206
243
|
|
|
207
244
|
type JSON = null | boolean | number | string | JSON[] | Record<string, JSON>
|
|
208
245
|
|
|
209
|
-
const ws = regex(/[ \t\n\r]*/)
|
|
246
|
+
const ws = trivia(regex(/[ \t\n\r]*/))
|
|
210
247
|
|
|
211
|
-
const { value } =
|
|
212
|
-
const comma =
|
|
248
|
+
const { value } = rules<{ value: Combinator<JSON> }>(g => {
|
|
249
|
+
const comma = literal(',')
|
|
213
250
|
|
|
214
251
|
const array = transform(
|
|
215
252
|
sequence(literal('['), sepBy(g.value, comma), literal(']')),
|
|
@@ -225,151 +262,92 @@ const { value } = parser<{ value: Combinator<JSON> }>(g => {
|
|
|
225
262
|
)
|
|
226
263
|
|
|
227
264
|
return {
|
|
228
|
-
value:
|
|
229
|
-
{ trivia: ws },
|
|
230
|
-
choice(object, array, jsonString, jsonNumber, jsonBool, jsonNull)
|
|
231
|
-
) as Combinator<JSON>,
|
|
265
|
+
value: choice(object, array, jsonString, jsonNumber, jsonBool, jsonNull) as Combinator<JSON>,
|
|
232
266
|
}
|
|
233
267
|
})
|
|
268
|
+
|
|
269
|
+
export const jsonParser = parser({ trivia: ws }, value)
|
|
270
|
+
jsonParser.parse('{ "a": 1 }')
|
|
234
271
|
```
|
|
235
272
|
|
|
236
|
-
`g.value` is a
|
|
273
|
+
`g.value` is a reference that works anywhere inside the factory regardless of order. Local helpers (`comma`, `pair`, `object`) that don't need to be cross-referenced can be plain `const`. Only put a rule in the returned object if other rules need to reach it as `g.xxx`.
|
|
237
274
|
|
|
238
|
-
> **Macro
|
|
275
|
+
> **Macro and `rules()`:** The plugin fully compiles `rules()` factories, including recursive ones. It emits mutually recursive named functions (`_pf0` etc.) so the cycle is broken. Add `with { type: 'macro' }` to your import and the entire grammar — recursive rules included — is inlined at build time. Both binding forms compile: `const { value } = rules(...)` (each rule becomes a top-level function) and `const grammar = rules(...)` (an object literal of compiled rules, so `grammar.value(...)` works).
|
|
276
|
+
>
|
|
277
|
+
> If the plugin meets a macro-imported declaration it can't compile statically (it closes over a runtime value, or isn't a recognized combinator shape), it leaves that declaration for the interpreter, strips the `with { type: 'macro' }` attribute so the import stays valid, and emits a build **warning** (`[parseman] file:line — …`) pointing at it — so a silent fallback never goes unnoticed.
|
|
239
278
|
|
|
240
279
|
### `ref<T>()` — low-level forward declaration
|
|
241
280
|
|
|
242
|
-
`
|
|
281
|
+
`rules()` handles forward references automatically. `ref<T>()` is the lower-level primitive it uses internally, exposed for cases where you need a single forward slot outside of a `rules()` call:
|
|
243
282
|
|
|
244
283
|
```ts
|
|
245
284
|
const value = ref<JSON>()
|
|
246
285
|
// ... build parsers that use value ...
|
|
247
|
-
value.define(
|
|
286
|
+
value.define(choice(object, array, str, num, bool, nil))
|
|
248
287
|
```
|
|
249
288
|
|
|
250
289
|
---
|
|
251
290
|
|
|
252
|
-
##
|
|
291
|
+
## Grammars that build an AST
|
|
253
292
|
|
|
254
|
-
For grammars that
|
|
293
|
+
For grammars that produce a typed CST/AST, support incremental re-parsing, or care about trivia, wrap each rule in `node(type, parser, build)`. parseman captures the rule's terminals into `children` / `rawChildren` and records trivia as flat `[start, end, insertIdx, …]` triples in `triviaLog`, then hands all four to `build(children, rawChildren, span, triviaLog)`. **Capture is the library's job:** you don't wrap terminals to recover their spans, and you don't reconstruct trivia — it's collected as the parser runs, in both the interpreter and the compiled build. Use `buildTriviaIndex(tree, input)` to turn `triviaLog` into a before/after lookup table.
|
|
255
294
|
|
|
256
295
|
```ts
|
|
257
|
-
import {
|
|
258
|
-
import type {
|
|
259
|
-
|
|
260
|
-
class ExprParser extends Parser {
|
|
261
|
-
ws = regex(/\s*/)
|
|
262
|
-
digits = regex(/[0-9]+/)
|
|
263
|
-
ident = regex(/[a-zA-Z_]\w*/)
|
|
264
|
-
|
|
265
|
-
// Plain initializer when no cross-reference needed
|
|
266
|
-
Str = sequence(literal('"'), regex(/[^"]*/), literal('"'))
|
|
267
|
-
|
|
268
|
-
// Thunk form for forward / mutual references
|
|
269
|
-
Num = (g: Refs<ExprParser>) => g.digits
|
|
270
|
-
Id = (g: Refs<ExprParser>) => g.ident
|
|
271
|
-
Add = (g: Refs<ExprParser>) => sequence(g.Num, many(sequence(literal('+'), g.Num)))
|
|
272
|
-
Expr = (g: Refs<ExprParser>) => choice(g.Add, g.Num, g.Id)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const expr = new ExprParser()
|
|
276
|
-
const r = parse(expr.rule('Expr'), '1+2+3')
|
|
277
|
-
// r.value is a CSTNode { _tag: 'node', type: 'Expr', span, children, savedContext }
|
|
278
|
-
```
|
|
296
|
+
import { rules, parser, node, regex, literal, sequence, many, trivia } from 'parseman'
|
|
297
|
+
import type { Combinator } from 'parseman'
|
|
279
298
|
|
|
280
|
-
|
|
299
|
+
// Any node shape works as long as it satisfies NodeLike (see below).
|
|
300
|
+
type N = { _tag: 'node'; type: string; span: { start: number; end: number }; state: unknown; children: unknown[] }
|
|
301
|
+
const ws = trivia(regex(/\s+/))
|
|
281
302
|
|
|
282
|
-
|
|
303
|
+
export const { Expr, Num } = rules<{ Expr: Combinator<N>; Num: Combinator<N> }>(g => {
|
|
304
|
+
const num = node('Num', regex(/[0-9]+/),
|
|
305
|
+
(children, raw, span, _triviaLog) => ({ _tag: 'node', type: 'Num', span, state: null, children: [...children] }))
|
|
306
|
+
const expr = node('Expr', parser({ trivia: ws }, sequence(g.Num, many(sequence(literal('+'), g.Num)))),
|
|
307
|
+
(children, raw, span, _triviaLog) => ({ _tag: 'node', type: 'Expr', span, state: null, children: [...children] }))
|
|
308
|
+
return { Expr: expr, Num: num }
|
|
309
|
+
})
|
|
283
310
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// replace just the ident rule; everything else stays
|
|
287
|
-
ident = regex(/[a-zA-Z_$][\w$]*/)
|
|
288
|
-
}
|
|
311
|
+
Expr.parse('1 + 2 + 3', 0, { trackLines: false })
|
|
312
|
+
// value is a Node whose children are the captured Num sub-nodes and '+' leaves
|
|
289
313
|
```
|
|
290
314
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
```ts
|
|
296
|
-
import type { CSTLeaf, CSTError, CSTRawChild, Span } from 'parseman'
|
|
297
|
-
|
|
298
|
-
type MyNode = { _tag: 'node'; type: string; span: Span; savedContext: unknown; children: MyNode[]; text: string }
|
|
299
|
-
|
|
300
|
-
class MyParser extends Parser<MyNode> {
|
|
301
|
-
// ... rules ...
|
|
315
|
+
- **`children`** — structural items in source order: spanned `CSTLeaf` terminals (`{ _tag:'leaf', value, span }`) and sub-nodes (whatever a nested `node()`'s `build` returned). A `build` that returns a bare string is recorded by the parent as a spanned leaf, so single-item "collapsing" rules keep their source span.
|
|
316
|
+
- **`rawChildren`** — structural children only (same items as `children`, without trivia tokens).
|
|
317
|
+
- **`triviaLog`** — flat `[start, end, insertIdx, …]` triples for whitespace/comments consumed between terms. `insertIdx` is the `rawChildren` index before which the trivia was consumed. Pass the tree to `buildTriviaIndex(tree, input)` for a `before`/`after` map of trivia tokens — useful for whitespace-sensitive syntax (e.g. CSS `div p` vs `div.p`).
|
|
302
318
|
|
|
303
|
-
|
|
304
|
-
type: string,
|
|
305
|
-
span: Span,
|
|
306
|
-
children: ReadonlyArray<MyNode | CSTLeaf | CSTError>,
|
|
307
|
-
savedContext: unknown,
|
|
308
|
-
rawChildren: ReadonlyArray<CSTRawChild>,
|
|
309
|
-
): MyNode {
|
|
310
|
-
return { _tag: 'node', type, span, savedContext, children: children as MyNode[], text: '...' }
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
```
|
|
319
|
+
Each rule returned from the factory is independently callable — `Expr`, `Num` above are the **rule registry** incremental re-parsing needs. Wrap a rule's inner parser in `parser({ trivia })` so trivia-skipping is baked in regardless of which rule you start from; the macro compiles the wrapper (and all capture) away to flat JS.
|
|
314
320
|
|
|
315
|
-
`
|
|
316
|
-
|
|
317
|
-
### Whitespace-sensitive rules with `rawChildren`
|
|
318
|
-
|
|
319
|
-
When whitespace is semantically meaningful (e.g. CSS where `div p` is a descendant combinator but `div+p` is adjacent), inspect `rawChildren` inside `buildNode`:
|
|
320
|
-
|
|
321
|
-
```ts
|
|
322
|
-
import type { CSTTrivia } from 'parseman'
|
|
323
|
-
|
|
324
|
-
class CssParser extends Parser<SelectorNode> {
|
|
325
|
-
ident = regex(/[a-zA-Z-]+/)
|
|
326
|
-
Selector = (g: Refs<CssParser>) => sequence(g.ident, g.ident)
|
|
327
|
-
|
|
328
|
-
protected buildNode(type, span, children, savedContext, rawChildren) {
|
|
329
|
-
if (type === 'Selector') {
|
|
330
|
-
// rawChildren: [Ident("div"), CSTTrivia(" "), Ident("p")]
|
|
331
|
-
const hasDescendant = rawChildren.some(c => c._tag === 'trivia')
|
|
332
|
-
}
|
|
333
|
-
return ...
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Trivia is set on parse() — the whitespace skip happens globally
|
|
338
|
-
parse(css.rule('Stylesheet'), src, { trivia: many(choice(regex(/\s+/), comment)) })
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
`CSTTrivia` nodes only appear in `rawChildren`, never in `children`. Zero-length trivia matches (e.g. `\s*` at a non-whitespace position) are not emitted.
|
|
321
|
+
> `transform(p, fn)` is still the tool for plain value-mapping (no children/trivia). `node()` is for CST/AST rules — it adds the capture `transform` doesn't. Both compile under the macro.
|
|
342
322
|
|
|
343
323
|
### Incremental re-parsing
|
|
344
324
|
|
|
345
|
-
`
|
|
325
|
+
`makeFunctionalDoc(registry, rootRule, input, opts?)` wraps a parse in a document that re-parses incrementally on edits. The `registry` is the object `rules()` returns (rule name → parser fn); the parser functions stay stateless, all incremental state lives in the doc.
|
|
346
326
|
|
|
347
327
|
```ts
|
|
348
|
-
|
|
328
|
+
import { makeFunctionalDoc } from 'parseman'
|
|
349
329
|
|
|
350
|
-
const
|
|
351
|
-
doc
|
|
330
|
+
const registry = { Expr, Num } // straight from rules()
|
|
331
|
+
let doc = makeFunctionalDoc(registry, 'Expr', src)
|
|
332
|
+
doc.tree // your Node root, or null on failure
|
|
352
333
|
doc.errors // ParseFail[], empty on success
|
|
353
334
|
doc.input // the source string
|
|
354
335
|
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
|
|
336
|
+
// edit(from, to, replacement) — two byte offsets into the OLD text + the
|
|
337
|
+
// replacement. "Select from→to, type replacement" — the three things every
|
|
338
|
+
// editor knows on each keystroke. Returns a new doc; the old one is untouched.
|
|
339
|
+
doc = doc.edit(changeStart, changeStart + changeLength, newText)
|
|
358
340
|
```
|
|
359
341
|
|
|
360
|
-
`edit(
|
|
361
|
-
|
|
362
|
-
Context-sensitive grammars work correctly: each CST node records a `ctx.user` snapshot at parse time (`savedContext`), so re-parsing resumes from the exact same state. Solid enough for a language server.
|
|
342
|
+
`edit()` finds the smallest node containing the change, re-parses just that rule from its start offset using the node's saved `state`, and grafts the result back in when the new span end lands where the edit's delta predicts. Nodes unaffected by the edit are shared by reference between old and new docs. For class-instance ASTs that can't be shallow-spread, pass `opts.rebuild(node, children)` to control how a parent is reconstructed when a child is replaced.
|
|
363
343
|
|
|
364
|
-
**In an IDE extension**,
|
|
344
|
+
**In an IDE extension**, keep one registry per language and one doc per open document. Each keystroke gives you the changed range as byte offsets — pass them straight to `edit()`:
|
|
365
345
|
|
|
366
346
|
```ts
|
|
367
|
-
|
|
368
|
-
const parser = new CSSParser()
|
|
369
|
-
const docs = new Map<string, ParseDoc<CSTNode>>()
|
|
347
|
+
const docs = new Map<string, ReturnType<typeof makeFunctionalDoc<Node>>>()
|
|
370
348
|
|
|
371
|
-
vscode.workspace.onDidOpenTextDocument(
|
|
372
|
-
docs.set(
|
|
349
|
+
vscode.workspace.onDidOpenTextDocument(d => {
|
|
350
|
+
docs.set(d.uri.toString(), makeFunctionalDoc(registry, 'Stylesheet', d.getText()))
|
|
373
351
|
})
|
|
374
352
|
|
|
375
353
|
vscode.workspace.onDidChangeTextDocument(event => {
|
|
@@ -379,7 +357,23 @@ vscode.workspace.onDidChangeTextDocument(event => {
|
|
|
379
357
|
doc = doc.edit(change.rangeOffset, change.rangeOffset + change.rangeLength, change.text)
|
|
380
358
|
}
|
|
381
359
|
docs.set(uri, doc)
|
|
382
|
-
// walk doc.tree
|
|
360
|
+
// walk doc.tree for diagnostics, folding ranges, semantic tokens, etc.
|
|
361
|
+
})
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Inheritance — by composition
|
|
365
|
+
|
|
366
|
+
Share rules across grammar variants by composition: factor common rules into helpers, and let each variant swap the ones it needs. Because every rule is a plain value, a variant is just different pieces passed into the same `rules()` shape — and it all stays macro-compilable.
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
// shared building blocks
|
|
370
|
+
const makeIdent = () => regex(/[a-zA-Z_]\w*/)
|
|
371
|
+
|
|
372
|
+
export const { Expr } = rules<{ Expr: Combinator<Node> }>(g => {
|
|
373
|
+
const ident = makeIdent() // base
|
|
374
|
+
// a JSX variant would use regex(/[a-zA-Z_$][\w$]*/) here instead
|
|
375
|
+
/* ...rules referencing ident... */
|
|
376
|
+
return { Expr: /* ... */ }
|
|
383
377
|
})
|
|
384
378
|
```
|
|
385
379
|
|
|
@@ -387,28 +381,26 @@ vscode.workspace.onDidChangeTextDocument(event => {
|
|
|
387
381
|
|
|
388
382
|
## Context-sensitive parsing
|
|
389
383
|
|
|
390
|
-
`withCtx` and `guard` implement context-sensitive rules without mutating shared state.
|
|
384
|
+
`withCtx` and `guard` implement context-sensitive rules without mutating shared state, and they compose into `rules()` like any other combinator.
|
|
391
385
|
|
|
392
|
-
`withCtx(extra, parser)` merges `extra` into the user context for the duration of `parser`. `guard(predicate)` succeeds only when `predicate(ctx)` returns true,
|
|
386
|
+
`withCtx(extra, parser)` merges `extra` into the user context for the duration of `parser`. `guard(predicate)` succeeds only when `predicate(ctx)` returns true, gating a rule behind runtime context.
|
|
393
387
|
|
|
394
388
|
```ts
|
|
395
|
-
import { withCtx, guard, many, sequence, choice, literal, regex } from 'parseman'
|
|
389
|
+
import { rules, withCtx, guard, many, sequence, choice, literal, regex, trivia, parser } from 'parseman'
|
|
390
|
+
import type { Combinator } from 'parseman'
|
|
396
391
|
|
|
397
|
-
|
|
398
|
-
ws = regex(/\s*/)
|
|
392
|
+
const ws = trivia(regex(/\s*/))
|
|
399
393
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
guard((ctx: { inFn?: boolean }) => ctx.inFn === true),
|
|
403
|
-
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
Program = (g: Refs<LangParser>) => many(g.Body)
|
|
408
|
-
}
|
|
394
|
+
export const { Program } = rules<{ Program: Combinator<unknown> }>(g => {
|
|
395
|
+
const expr = regex(/[a-z]+/)
|
|
396
|
+
const ret = sequence(guard((ctx: { inFn?: boolean }) => ctx.inFn === true), literal('return'))
|
|
397
|
+
const stmt = choice(ret, expr)
|
|
398
|
+
const body = withCtx({ inFn: true }, many(sequence(stmt, ws)))
|
|
399
|
+
return { Program: parser({ trivia: ws }, many(body)) }
|
|
400
|
+
})
|
|
409
401
|
```
|
|
410
402
|
|
|
411
|
-
`
|
|
403
|
+
`return` is only reachable inside a body because `guard` rejects it when `inFn` is not set. Incremental `edit()` replays the correct context because each node records the `ctx.state` snapshot (`node.state`) active when it was parsed.
|
|
412
404
|
|
|
413
405
|
---
|
|
414
406
|
|
|
@@ -426,7 +418,35 @@ const stmt = recover(g.Stmt, literal(';'))
|
|
|
426
418
|
const block = scanTo(literal('}'), [balanced(literal('('), literal(')')), balanced(literal('['), literal(']'))])
|
|
427
419
|
```
|
|
428
420
|
|
|
429
|
-
`scanTo
|
|
421
|
+
### `scanTo` vs `balanced` — when to use each
|
|
422
|
+
|
|
423
|
+
Both are **position arithmetic**: pure cursor-advance with zero CST allocation. Neither pushes terminals into the enclosing `node()`'s child list; only the final scanned span appears as a single leaf.
|
|
424
|
+
|
|
425
|
+
**`scanTo(sentinel, opts?)`** — use when you need to consume an open-ended region whose boundary is a specific token. It scans character-by-character, stopping the moment `sentinel` matches. Pass `opts.skip` to declare patterns that should be treated as opaque blobs so their content never accidentally looks like the sentinel:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
// Consume a CSS at-rule prelude up to '{' or ';',
|
|
429
|
+
// but don't stop inside parentheses or strings.
|
|
430
|
+
const prelude = scanTo(choice(literal('{'), literal(';')), {
|
|
431
|
+
skip: [balanced('(', ')'), singleStr, doubleStr],
|
|
432
|
+
})
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**`balanced(open, close, opts?)`** — use when you want to match a single self-contained delimited region and get its full text back (delimiters included). This is the natural building block for `scanTo`'s `skip` list:
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
const parenGroup = balanced('(', ')') // matches (a + b) including the parens
|
|
439
|
+
const bracketExpr = balanced('[', ']') // matches [0] including the brackets
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
`balanced` can itself accept a `skip` list for deeply nested structures:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
// Match (…) allowing strings inside to contain unbalanced parens
|
|
446
|
+
const parenWithStrings = balanced('(', ')', { skip: [singleStr, doubleStr] })
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**The key difference**: `scanTo` scans *until* a boundary; `balanced` matches *across* a known delimited region. Use `balanced` inside `scanTo`'s `skip` to prevent the scanner from stopping at a sentinel that appears inside a nested structure.
|
|
430
450
|
|
|
431
451
|
---
|
|
432
452
|
|
|
@@ -479,18 +499,35 @@ type Span = {
|
|
|
479
499
|
}
|
|
480
500
|
```
|
|
481
501
|
|
|
482
|
-
###
|
|
502
|
+
### Node and document types
|
|
503
|
+
|
|
504
|
+
Any AST your `transform` callbacks produce participates in incremental re-parsing as long as it satisfies `NodeLike` — that's the whole contract:
|
|
483
505
|
|
|
484
506
|
```ts
|
|
485
|
-
type
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
507
|
+
type NodeLike = {
|
|
508
|
+
readonly _tag: 'node'
|
|
509
|
+
readonly type: string // the rule name — used as the registry key on re-parse
|
|
510
|
+
readonly span: Span
|
|
511
|
+
readonly state: unknown // ctx.state snapshot at parse time; replayed on edit
|
|
512
|
+
readonly children: ReadonlyArray<{ readonly _tag: string }>
|
|
513
|
+
}
|
|
489
514
|
|
|
490
|
-
|
|
491
|
-
type
|
|
515
|
+
// makeFunctionalDoc<N>(registry, rootRule, input, opts?) → FunctionalDoc<N>
|
|
516
|
+
type Registry<N> = Record<string, (input: string, pos: number, ctx: ParseContext) => ParseResult<N>>
|
|
517
|
+
interface FunctionalDoc<N extends NodeLike> {
|
|
518
|
+
readonly tree: N | null
|
|
519
|
+
readonly errors: ParseFail[]
|
|
520
|
+
readonly input: string
|
|
521
|
+
edit(from: number, to: number, replacement: string): FunctionalDoc<N>
|
|
522
|
+
}
|
|
523
|
+
type FunctionalDocOptions<N> = {
|
|
524
|
+
state?: unknown // initial ctx.state for the root parse
|
|
525
|
+
rebuild?: (node: N, children: ReadonlyArray<unknown>) => N // override for class-instance ASTs
|
|
526
|
+
}
|
|
492
527
|
```
|
|
493
528
|
|
|
529
|
+
`children` only needs items carrying a `_tag` so traversal can tell sub-nodes (`_tag: 'node'`) from anything else. The `type` string must match the rule name in the registry so `edit()` can re-parse the right rule.
|
|
530
|
+
|
|
494
531
|
---
|
|
495
532
|
|
|
496
533
|
## Developing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"choice.d.ts","sourceRoot":"","sources":["../../src/combinators/choice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAC+B,QAAQ,EAClD,MAAM,aAAa,CAAA;AAGpB,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;AACvH,KAAK,SAAS,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI;KACrE,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAChC,CAAC,MAAM,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAEjD,wBAAgB,MAAM,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EACxH,GAAG,IAAI,EAAE,CAAC,GACT,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"choice.d.ts","sourceRoot":"","sources":["../../src/combinators/choice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAC+B,QAAQ,EAClD,MAAM,aAAa,CAAA;AAGpB,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;AACvH,KAAK,SAAS,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI;KACrE,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAChC,CAAC,MAAM,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAEjD,wBAAgB,MAAM,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EACxH,GAAG,IAAI,EAAE,CAAC,GACT,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAiI1B;AAwFD,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAKzE;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKhG"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Combinator } from '../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the set of expected tokens at the given cursor offset.
|
|
4
|
+
* Useful for implementing IDE completions.
|
|
5
|
+
*
|
|
6
|
+
* Runs the parser on input truncated at `offset` with a probe that tracks the
|
|
7
|
+
* highest-position failure seen, even when sepBy/many backtrack past the cursor.
|
|
8
|
+
* Returns the expected tokens from that deepest failure.
|
|
9
|
+
*
|
|
10
|
+
* Returns an empty array when the input up to `offset` parses completely
|
|
11
|
+
* with no failures at or before the cursor.
|
|
12
|
+
*
|
|
13
|
+
* The returned strings use the same labels as ParseFail.expected:
|
|
14
|
+
* quoted literals like `"\"{\""` and regex patterns like `"/[0-9]+/"`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function completionsAt(parser: Combinator<unknown>, input: string, offset: number): string[];
|
|
17
|
+
//# sourceMappingURL=completions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/combinators/completions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,aAAa,CAAA;AAEtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,MAAM,EAAE,CAYV"}
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import type { Combinator, ParseResult } from '../types.ts';
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult } from '../types.ts';
|
|
2
2
|
export type ParseOptions = {
|
|
3
|
-
trivia?: Combinator<unknown>;
|
|
4
3
|
trackLines?: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Enable error recovery. When true, recover() nodes collect their ParseErrors
|
|
6
|
+
* into a side-channel array rather than (only) embedding them in the value tree.
|
|
7
|
+
* The returned ParseOk will have an `errors` field listing all recovered errors.
|
|
8
|
+
* Top-level parse failures (where no recover() node caught the error) still
|
|
9
|
+
* return ParseFail as usual.
|
|
10
|
+
*/
|
|
11
|
+
recover?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type ParserOptions = ParseOptions & {
|
|
14
|
+
trivia?: Combinator<unknown>;
|
|
15
|
+
/** Record consumed trivia as CSTTrivia tokens in rawChildren. Default: skip. */
|
|
16
|
+
captureTrivia?: boolean;
|
|
5
17
|
};
|
|
6
|
-
export
|
|
7
|
-
|
|
18
|
+
export interface ParsemanParser<T> extends Combinator<T> {
|
|
19
|
+
parse(input: string): ParseResult<T>;
|
|
20
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T>;
|
|
21
|
+
}
|
|
22
|
+
export declare function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanParser<T>;
|
|
23
|
+
export declare function parse<T>(combinator: Combinator<T>, input: string, opts?: ParseOptions): ParseResult<T>;
|
|
8
24
|
//# sourceMappingURL=grammar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/combinators/grammar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/combinators/grammar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGxE,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IACzC,MAAM,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAC5B,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,CAAA;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,UAAU,CAAC,CAAC,CAAC;IACtD,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;IACpC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;CACrE;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CA6BrF;AAED,wBAAgB,KAAK,CAAC,CAAC,EACrB,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,YAAiB,GACtB,WAAW,CAAC,CAAC,CAAC,CAehB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Combinator } from '../types.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Zero-width assertion: succeeds (consuming nothing) only when `predicate`
|
|
4
|
-
* returns true for `ctx.
|
|
4
|
+
* returns true for `ctx.state`. Fails otherwise.
|
|
5
5
|
*
|
|
6
6
|
* Intended for use inside sequence() to gate subsequent parsing on runtime
|
|
7
7
|
* context set with withCtx().
|
|
@@ -11,5 +11,5 @@ import type { Combinator } from '../types.ts';
|
|
|
11
11
|
* literal('return'), optional(expr)
|
|
12
12
|
* )
|
|
13
13
|
*/
|
|
14
|
-
export declare function guard(predicate: (
|
|
14
|
+
export declare function guard(predicate: (state: unknown) => boolean): Combinator<null>;
|
|
15
15
|
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/combinators/guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAA;AAEpF;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,SAAS,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/combinators/guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAA;AAEpF;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAgB9E"}
|