@tabnas/transduce 0.1.0 → 0.1.2
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 +14 -0
- package/dist/transduce.d.ts +1 -1
- package/dist/transduce.js +1 -1
- package/package.json +3 -1
- package/src/capability.ts +52 -0
- package/src/datum.ts +407 -0
- package/src/error.ts +198 -0
- package/src/event.ts +134 -0
- package/src/guard.ts +112 -0
- package/src/json.ts +82 -0
- package/src/limits.ts +165 -0
- package/src/lines.ts +1247 -0
- package/src/matcher.ts +287 -0
- package/src/parser-source.ts +225 -0
- package/src/route.ts +275 -0
- package/src/rule-events.ts +661 -0
- package/src/scan.ts +83 -0
- package/src/selector.ts +191 -0
- package/src/sink.ts +199 -0
- package/src/source.ts +135 -0
- package/src/table-from-json.ts +296 -0
- package/src/table.ts +213 -0
- package/src/transduce.ts +72 -0
- package/src/tsconfig.json +16 -0
- package/src/value.ts +93 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
2
|
+
|
|
3
|
+
// The rule-event adapter: `JsonEvents/1` from a live tabnas parse.
|
|
4
|
+
//
|
|
5
|
+
// The engine tells a `ruleDone` subscriber about every rule pass, with the
|
|
6
|
+
// rule's node: the value the rule accumulates. Rules pushed or replaced
|
|
7
|
+
// from a rule whose node is a container share that container (the same
|
|
8
|
+
// object), which is the identity the adapter follows; it watches those
|
|
9
|
+
// containers and nothing grammar-specific, which is what lets one adapter
|
|
10
|
+
// serve every grammar. The algorithm is the Rust runtime's, step for step
|
|
11
|
+
// (rs/src/source/rule_events.rs has the full commentary):
|
|
12
|
+
//
|
|
13
|
+
// - A rule whose node is a container at the end of its OPEN pass, a
|
|
14
|
+
// container not already on the frame stack, STARTS a container.
|
|
15
|
+
// - At the end of a CLOSE pass whose node is the top frame's container, a
|
|
16
|
+
// longer container means new entries: the tail of an array, the last
|
|
17
|
+
// members of a map (insertion ordered). A new entry that is the
|
|
18
|
+
// container which completed last was already streamed; any other is
|
|
19
|
+
// walked whole, late.
|
|
20
|
+
// - A key is announced early when the rule stashed it in `u.key` at its
|
|
21
|
+
// OPEN pass; a key the adapter only learns at insertion is announced
|
|
22
|
+
// then.
|
|
23
|
+
// - A repeated member name does not grow the map. When the announcing
|
|
24
|
+
// rule's CLOSE pass leaves the map at its old length the adapter HOLDS
|
|
25
|
+
// what the map has under the name until its next event, which a pass
|
|
26
|
+
// that failed the parse never sends: a held scalar is streamed, the
|
|
27
|
+
// container just streamed is complete already, any other container is a
|
|
28
|
+
// merge of both values and fails the run with `DUPLICATE_MEMBER`.
|
|
29
|
+
// - A map whose names changed after they were streamed (a YAML merge key)
|
|
30
|
+
// fails when its frame ends, with `STREAMABILITY_UNKNOWN`.
|
|
31
|
+
// - The top frame ENDS when a rule at the frame's depth whose node is the
|
|
32
|
+
// frame's container closes, or the rule that started it closes, unless
|
|
33
|
+
// that close REPLACES the rule (`alt.r`).
|
|
34
|
+
// - A root scalar is emitted at the depth-0 rule's close, unless that close
|
|
35
|
+
// replaces the rule. A frame that starts at the root after a root value
|
|
36
|
+
// completed is refused, and so is a parse root that closes for good over
|
|
37
|
+
// a value that is not the one streamed: `STREAMABILITY_UNKNOWN`, before
|
|
38
|
+
// `end`. `end` is not the adapter's to emit: the source sends it after
|
|
39
|
+
// the engine has returned, so a document is complete only when it
|
|
40
|
+
// validated.
|
|
41
|
+
//
|
|
42
|
+
// Number lexemes are best effort: at the close of a rule whose node is a
|
|
43
|
+
// number, the first open token's source text is kept when it is a JSON
|
|
44
|
+
// number that reads as the same value, and attached to that number when it
|
|
45
|
+
// is inserted next.
|
|
46
|
+
//
|
|
47
|
+
// Pruning drops the elements of an array frame from the engine's array
|
|
48
|
+
// after they were emitted, so a document's rows do not pile up in the
|
|
49
|
+
// engine's tree while the transducer streams them. It changes the value
|
|
50
|
+
// the engine returns, which is why only the incremental source, which
|
|
51
|
+
// discards that value, ever asks for it.
|
|
52
|
+
|
|
53
|
+
import { Fail } from './error'
|
|
54
|
+
import { Ev, JsonEvent } from './event'
|
|
55
|
+
import { Guarded } from './guard'
|
|
56
|
+
import { isJsonNumber } from './json'
|
|
57
|
+
import { AbortFlag, Limits, Metrics } from './limits'
|
|
58
|
+
import { Matcher } from './matcher'
|
|
59
|
+
import { Selector } from './selector'
|
|
60
|
+
import { Flow, Sink } from './sink'
|
|
61
|
+
import { Prune, walkValue } from './source'
|
|
62
|
+
import { engineKeys, engineScalar, isEngineContainer, isEngineMap } from './value'
|
|
63
|
+
|
|
64
|
+
// One open container.
|
|
65
|
+
type Frame = {
|
|
66
|
+
cell: object
|
|
67
|
+
array: boolean
|
|
68
|
+
len: number
|
|
69
|
+
ruleI: number
|
|
70
|
+
depth: number
|
|
71
|
+
// The last member name streamed; the one announced early for the next
|
|
72
|
+
// member, when `hasKey`.
|
|
73
|
+
key: string
|
|
74
|
+
hasKey: boolean
|
|
75
|
+
prune: boolean
|
|
76
|
+
// The distinct names streamed into this map, to compare with the map's
|
|
77
|
+
// names when the frame ends.
|
|
78
|
+
names: Set<string>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A member announced early whose rule closed without growing the map, held
|
|
82
|
+
// until the adapter's next event shows that the close stood. `found` is
|
|
83
|
+
// false when the grammar announced a member it never stored.
|
|
84
|
+
type Held = { key: string; found: boolean; value: unknown }
|
|
85
|
+
|
|
86
|
+
// How the run stands, as seen from inside the callbacks.
|
|
87
|
+
export type Status =
|
|
88
|
+
| { type: 'running' }
|
|
89
|
+
// The sink answered `stop`.
|
|
90
|
+
| { type: 'stopped' }
|
|
91
|
+
// The sink or a limit failed; the parse was told to cancel.
|
|
92
|
+
| { type: 'failed'; error: unknown }
|
|
93
|
+
|
|
94
|
+
type PruneState = { type: 'never' } | { type: 'all' } | { type: 'under'; matcher: Matcher }
|
|
95
|
+
|
|
96
|
+
// The adapter's state, behind the engine's subscriber.
|
|
97
|
+
export class Adapter<S extends Sink> {
|
|
98
|
+
private frames: Frame[] = []
|
|
99
|
+
// The container that completed last: the next entry inserted into its
|
|
100
|
+
// parent is this one, already streamed.
|
|
101
|
+
private lastCompleted: object | null = null
|
|
102
|
+
private held: Held | null = null
|
|
103
|
+
private lexeme = ''
|
|
104
|
+
private lexemeValue = 0
|
|
105
|
+
private lexemeReady = false
|
|
106
|
+
readonly sink: Guarded<S>
|
|
107
|
+
status: Status = { type: 'running' }
|
|
108
|
+
private stop: AbortFlag
|
|
109
|
+
private prune: PruneState
|
|
110
|
+
private pruneHit = false
|
|
111
|
+
// A whole root value (a scalar, or the outermost frame) has been emitted.
|
|
112
|
+
private rootDone = false
|
|
113
|
+
// Anything at all has been emitted for the current document.
|
|
114
|
+
private emitted = false
|
|
115
|
+
|
|
116
|
+
constructor(
|
|
117
|
+
sink: S,
|
|
118
|
+
limits: Limits,
|
|
119
|
+
abort: AbortFlag,
|
|
120
|
+
metrics: Metrics,
|
|
121
|
+
prune: Prune,
|
|
122
|
+
stop: AbortFlag,
|
|
123
|
+
) {
|
|
124
|
+
this.sink = new Guarded(sink, limits, abort, metrics)
|
|
125
|
+
this.stop = stop
|
|
126
|
+
switch (prune.type) {
|
|
127
|
+
case 'never':
|
|
128
|
+
this.prune = { type: 'never' }
|
|
129
|
+
break
|
|
130
|
+
case 'all_arrays':
|
|
131
|
+
this.prune = { type: 'all' }
|
|
132
|
+
break
|
|
133
|
+
case 'under': {
|
|
134
|
+
// A selector that names the elements names the array one step up;
|
|
135
|
+
// one that names the array is taken as is.
|
|
136
|
+
const steps = prune.selector.steps
|
|
137
|
+
const last = steps[steps.length - 1]
|
|
138
|
+
const array =
|
|
139
|
+
undefined !== last && 'each_index' === last.type
|
|
140
|
+
? new Selector(steps.slice(0, -1))
|
|
141
|
+
: prune.selector
|
|
142
|
+
this.prune = { type: 'under', matcher: new Matcher([array]) }
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Ready for another document into the same sink: the JSON Lines source
|
|
149
|
+
// parses one value per line with one parser and one subscriber.
|
|
150
|
+
reset(): void {
|
|
151
|
+
this.frames.length = 0
|
|
152
|
+
this.lastCompleted = null
|
|
153
|
+
this.held = null
|
|
154
|
+
this.lexemeReady = false
|
|
155
|
+
this.pruneHit = false
|
|
156
|
+
this.rootDone = false
|
|
157
|
+
this.emitted = false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Whether one whole root value was emitted and every frame closed.
|
|
161
|
+
complete(): boolean {
|
|
162
|
+
return this.rootDone && 0 === this.frames.length
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Whether nothing at all was emitted for the current document, so the
|
|
166
|
+
// value the engine returned can be walked in its place.
|
|
167
|
+
idle(): boolean {
|
|
168
|
+
return !this.emitted
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Emit a whole value through the sink, outside the parse.
|
|
172
|
+
walkWhole(value: unknown): Flow {
|
|
173
|
+
this.emitted = true
|
|
174
|
+
return walkValue(value, this.sink)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Send one event straight to the sink, outside the parse.
|
|
178
|
+
send(ev: JsonEvent): Flow {
|
|
179
|
+
return this.sink.event(ev)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private fail(error: unknown): void {
|
|
183
|
+
this.status = { type: 'failed', error }
|
|
184
|
+
this.stop.abort()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Emit one event. `false` means stop: the sink stopped or failed, and the
|
|
188
|
+
// parse has been told to cancel.
|
|
189
|
+
private emit(ev: JsonEvent): boolean {
|
|
190
|
+
this.emitted = true
|
|
191
|
+
if ('under' === this.prune.type) {
|
|
192
|
+
try {
|
|
193
|
+
const hit = this.prune.matcher.event(ev)
|
|
194
|
+
this.pruneHit = 'start' === hit.kind && 0 < hit.begins
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.fail(err)
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
let flow: Flow
|
|
201
|
+
try {
|
|
202
|
+
flow = this.sink.event(ev)
|
|
203
|
+
} catch (err) {
|
|
204
|
+
this.fail(err)
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
if ('stop' === flow) {
|
|
208
|
+
this.status = { type: 'stopped' }
|
|
209
|
+
this.stop.abort()
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Emit a scalar value, with the remembered lexeme when it is this
|
|
216
|
+
// number's.
|
|
217
|
+
private scalar(value: unknown): boolean {
|
|
218
|
+
const s = engineScalar(value)
|
|
219
|
+
if (undefined === s) return true
|
|
220
|
+
switch (s.type) {
|
|
221
|
+
case 'null':
|
|
222
|
+
return this.emit(Ev.null)
|
|
223
|
+
case 'bool':
|
|
224
|
+
return this.emit(Ev.bool(s.value))
|
|
225
|
+
case 'string':
|
|
226
|
+
return this.emit(Ev.string(s.value))
|
|
227
|
+
case 'number':
|
|
228
|
+
if (this.lexemeReady && this.lexemeValue === s.value) {
|
|
229
|
+
this.lexemeReady = false
|
|
230
|
+
return this.emit(Ev.number(s.value, this.lexeme))
|
|
231
|
+
}
|
|
232
|
+
// A remembered lexeme that is not this number's may be a later
|
|
233
|
+
// entry's of the same batch; the batch's end drops it.
|
|
234
|
+
return this.emit(Ev.number(s.value))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Emit a whole value the adapter did not see being built (late).
|
|
239
|
+
private walk(value: unknown): boolean {
|
|
240
|
+
this.lexemeReady = false
|
|
241
|
+
if (Array.isArray(value)) {
|
|
242
|
+
if (!this.emit(Ev.arrayStart)) return false
|
|
243
|
+
for (const item of value) {
|
|
244
|
+
if (!this.walk(item)) return false
|
|
245
|
+
}
|
|
246
|
+
return this.emit(Ev.arrayEnd)
|
|
247
|
+
}
|
|
248
|
+
if (isEngineMap(value)) {
|
|
249
|
+
if (!this.emit(Ev.objectStart)) return false
|
|
250
|
+
for (const k of engineKeys(value)) {
|
|
251
|
+
if (!this.emit(Ev.key(k)) || !this.walk(value[k])) return false
|
|
252
|
+
}
|
|
253
|
+
return this.emit(Ev.objectEnd)
|
|
254
|
+
}
|
|
255
|
+
return this.scalar(value)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Open a frame. Its `len` starts at zero whatever the container holds:
|
|
259
|
+
// entries present when the adapter first sees a container were never
|
|
260
|
+
// streamed (jsonic's promoted first value), and the next close pass
|
|
261
|
+
// emits them before the new ones.
|
|
262
|
+
private pushFrame(cell: object, array: boolean, rule: any, prune: boolean): void {
|
|
263
|
+
this.frames.push({
|
|
264
|
+
cell,
|
|
265
|
+
array,
|
|
266
|
+
len: 0,
|
|
267
|
+
ruleI: rule.i,
|
|
268
|
+
depth: rule.d,
|
|
269
|
+
key: '',
|
|
270
|
+
hasKey: false,
|
|
271
|
+
prune,
|
|
272
|
+
names: new Set(),
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The subscriber's body.
|
|
277
|
+
onDone(rule: any, done: any): void {
|
|
278
|
+
if ('running' !== this.status.type) return
|
|
279
|
+
try {
|
|
280
|
+
this.step(rule, done)
|
|
281
|
+
} catch (err) {
|
|
282
|
+
// Not a failure the adapter reports itself (those set the status and
|
|
283
|
+
// return): a defect, kept as it is rather than read as the input's.
|
|
284
|
+
this.fail(err)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private step(rule: any, done: any): void {
|
|
289
|
+
// Any further event means the pass that held a member stood.
|
|
290
|
+
if (!this.flushHeld()) return
|
|
291
|
+
if (null != done.alt?.err) return
|
|
292
|
+
if ('o' === done.state) {
|
|
293
|
+
this.opened(rule, rule.node)
|
|
294
|
+
} else {
|
|
295
|
+
const replaces = null != done.alt && '' !== (done.alt.r ?? '')
|
|
296
|
+
this.closed(rule, rule.node, replaces)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private opened(rule: any, cell: unknown): void {
|
|
301
|
+
if (!isEngineContainer(cell)) return
|
|
302
|
+
const node = cell as object
|
|
303
|
+
const array = Array.isArray(node)
|
|
304
|
+
if (!this.frames.some((f) => f.cell === node)) {
|
|
305
|
+
if (0 === this.frames.length && this.rootDone) {
|
|
306
|
+
this.fail(wrappedRoot())
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
if (null !== this.lastCompleted) {
|
|
310
|
+
// The container that completed last was never stored in the one
|
|
311
|
+
// around it, and the grammar is opening another: what left cannot
|
|
312
|
+
// be taken back.
|
|
313
|
+
this.fail(unstoredContainer())
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
const top = this.frames[this.frames.length - 1]
|
|
317
|
+
if (undefined !== top && !top.array && !top.hasKey) {
|
|
318
|
+
// A container opening in a map whose next member has no key yet:
|
|
319
|
+
// its events would leave before the key, which no tree's events do.
|
|
320
|
+
this.fail(valueBeforeKey())
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
this.lexemeReady = false
|
|
324
|
+
if (!this.emit(array ? Ev.arrayStart : Ev.objectStart)) return
|
|
325
|
+
let prune = false
|
|
326
|
+
switch (this.prune.type) {
|
|
327
|
+
case 'all':
|
|
328
|
+
prune = array
|
|
329
|
+
break
|
|
330
|
+
case 'under':
|
|
331
|
+
prune = array && this.pruneHit
|
|
332
|
+
break
|
|
333
|
+
}
|
|
334
|
+
this.pushFrame(node, array, rule, prune)
|
|
335
|
+
} else if (!array) {
|
|
336
|
+
const top = this.frames[this.frames.length - 1]
|
|
337
|
+
if (top.cell === node && !top.hasKey && top.len === containerLen(node)) {
|
|
338
|
+
const key = rule.u?.key
|
|
339
|
+
if ('string' === typeof key) {
|
|
340
|
+
if (!this.emit(Ev.key(key))) return
|
|
341
|
+
top.key = key
|
|
342
|
+
top.hasKey = true
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private closed(rule: any, cell: unknown, replaces: boolean): void {
|
|
349
|
+
// A root scalar's lexeme has to be known before it is emitted below.
|
|
350
|
+
this.rememberLexeme(rule)
|
|
351
|
+
// Whether a whole root value had left before this pass: the pass that
|
|
352
|
+
// completes the root is exempt from the check at the end.
|
|
353
|
+
const wasDone = this.rootDone
|
|
354
|
+
|
|
355
|
+
let pruneFrom = -1
|
|
356
|
+
const topI = this.frames.length - 1
|
|
357
|
+
if (0 <= topI && this.frames[topI].cell === cell && isEngineContainer(cell)) {
|
|
358
|
+
const node = cell as any
|
|
359
|
+
const array = Array.isArray(node)
|
|
360
|
+
const keys = array ? null : engineKeys(node)
|
|
361
|
+
const len = array ? node.length : (keys as string[]).length
|
|
362
|
+
const frame = this.frames[topI]
|
|
363
|
+
const old = frame.len
|
|
364
|
+
if (!array && frame.hasKey && len === old) {
|
|
365
|
+
// The announced member did not grow the map, and the rule that
|
|
366
|
+
// announced it is closing: it replaced or merged an earlier member
|
|
367
|
+
// of the same name, or the pass failed the parse before storing it.
|
|
368
|
+
// Hold what the map has under the name; the next event decides.
|
|
369
|
+
if (rule.u?.key === frame.key) this.holdPending(node, topI)
|
|
370
|
+
}
|
|
371
|
+
if (len > old) {
|
|
372
|
+
for (let i = old; i < len; i++) {
|
|
373
|
+
let value: unknown
|
|
374
|
+
if (array) {
|
|
375
|
+
value = node[i]
|
|
376
|
+
} else {
|
|
377
|
+
const key = (keys as string[])[i]
|
|
378
|
+
value = node[key]
|
|
379
|
+
const top = this.frames[topI]
|
|
380
|
+
if (top.hasKey && top.key !== key) {
|
|
381
|
+
// A member landed ahead of the one announced; that one keeps
|
|
382
|
+
// its place in the stream.
|
|
383
|
+
if (!this.settlePending(node, topI)) return
|
|
384
|
+
}
|
|
385
|
+
const announced = top.hasKey
|
|
386
|
+
top.hasKey = false
|
|
387
|
+
top.names.add(key)
|
|
388
|
+
if (!announced) {
|
|
389
|
+
top.key = key
|
|
390
|
+
if (!this.emit(Ev.key(key))) return
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!this.entry(value)) return
|
|
394
|
+
}
|
|
395
|
+
this.frames[topI].len = len
|
|
396
|
+
this.lexemeReady = false
|
|
397
|
+
if (array && this.frames[topI].prune) pruneFrom = old
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (0 <= pruneFrom) {
|
|
401
|
+
;(cell as unknown[]).length = pruneFrom
|
|
402
|
+
this.frames[this.frames.length - 1].len = pruneFrom
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (replaces) return
|
|
406
|
+
|
|
407
|
+
const top = this.frames[this.frames.length - 1]
|
|
408
|
+
if (undefined !== top && (top.ruleI === rule.i || (top.cell === cell && top.depth === rule.d))) {
|
|
409
|
+
const array = top.array
|
|
410
|
+
// A member held by this very pass goes before the frame ends.
|
|
411
|
+
if (!this.flushHeld()) return
|
|
412
|
+
if (top.hasKey) {
|
|
413
|
+
// The announcing rule never closed on this container: the member is
|
|
414
|
+
// whatever the map holds under the name now.
|
|
415
|
+
if (!this.settlePending(cell, this.frames.length - 1)) return
|
|
416
|
+
}
|
|
417
|
+
if (null !== this.lastCompleted) {
|
|
418
|
+
// A container built inside this one was streamed and never stored
|
|
419
|
+
// (jsonic drops a pair's value inside a list when `list.pair` is
|
|
420
|
+
// off): the frame cannot end as the walk's would.
|
|
421
|
+
this.fail(unstoredContainer())
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
if (!array) {
|
|
425
|
+
const names = isEngineMap(cell) ? engineKeys(cell) : []
|
|
426
|
+
const same = names.length === top.names.size && names.every((k) => top.names.has(k))
|
|
427
|
+
if (!same) {
|
|
428
|
+
this.fail(rewrittenMap())
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
this.frames.pop()
|
|
433
|
+
this.lexemeReady = false
|
|
434
|
+
if (!this.emit(array ? Ev.arrayEnd : Ev.objectEnd)) return
|
|
435
|
+
this.lastCompleted = isEngineContainer(cell) ? (cell as object) : null
|
|
436
|
+
if (0 === this.frames.length) this.rootDone = true
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (0 === rule.d && 0 === this.frames.length && !this.rootDone) {
|
|
440
|
+
if (!isEngineContainer(cell)) {
|
|
441
|
+
if (!this.scalar(cell)) return
|
|
442
|
+
this.rootDone = true
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// The parse root closing for good after the root value left: the node
|
|
447
|
+
// must still hold what was streamed.
|
|
448
|
+
if (wasDone && 0 === rule.d && 0 === this.frames.length) {
|
|
449
|
+
const container = isEngineContainer(cell)
|
|
450
|
+
const streamed =
|
|
451
|
+
null !== this.lastCompleted ? container && cell === this.lastCompleted : !container
|
|
452
|
+
if (!streamed) this.fail(rewrittenRoot())
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Emit one entry that just landed in the top frame's container: a scalar
|
|
457
|
+
// as itself, the container that completed last as nothing (it has been
|
|
458
|
+
// streamed), any other container whole, late.
|
|
459
|
+
private entry(value: unknown): boolean {
|
|
460
|
+
if (null !== this.lastCompleted) {
|
|
461
|
+
if (isEngineContainer(value) && value === this.lastCompleted) {
|
|
462
|
+
this.lastCompleted = null
|
|
463
|
+
return true
|
|
464
|
+
}
|
|
465
|
+
// Something else landed: the container streamed last was never
|
|
466
|
+
// stored, and its events cannot be taken back.
|
|
467
|
+
this.fail(unstoredContainer())
|
|
468
|
+
return false
|
|
469
|
+
}
|
|
470
|
+
if (!isEngineContainer(value)) return this.scalar(value)
|
|
471
|
+
return this.walk(value)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// The member announced on the frame `topI` did not grow its map: stream
|
|
475
|
+
// what the map holds under that name now.
|
|
476
|
+
private settlePending(node: unknown, topI: number): boolean {
|
|
477
|
+
const top = this.frames[topI]
|
|
478
|
+
top.hasKey = false
|
|
479
|
+
const m = member(node, top.key)
|
|
480
|
+
return this.settle(top.key, m.found, m.value)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Like `settlePending`, but the member is held rather than streamed: the
|
|
484
|
+
// announcing rule's close pass may be one the engine reports although an
|
|
485
|
+
// action failed the parse in it.
|
|
486
|
+
private holdPending(node: unknown, topI: number): void {
|
|
487
|
+
const top = this.frames[topI]
|
|
488
|
+
top.hasKey = false
|
|
489
|
+
const m = member(node, top.key)
|
|
490
|
+
this.held = { key: top.key, found: m.found, value: m.value }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Stream the member held at the last pass, if any. `false` means stop.
|
|
494
|
+
private flushHeld(): boolean {
|
|
495
|
+
const held = this.held
|
|
496
|
+
if (null === held) return true
|
|
497
|
+
this.held = null
|
|
498
|
+
return this.settle(held.key, held.found, held.value)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Stream what a map holds under a repeated member's name: a scalar as
|
|
502
|
+
// itself; the container that completed last as nothing; any other
|
|
503
|
+
// container fails the run as a merge; no value at all fails it as a
|
|
504
|
+
// member the grammar announced and never stored.
|
|
505
|
+
private settle(key: string, found: boolean, value: unknown): boolean {
|
|
506
|
+
if (!found) {
|
|
507
|
+
this.fail(
|
|
508
|
+
new Fail(
|
|
509
|
+
'STREAMABILITY_UNKNOWN',
|
|
510
|
+
`the grammar announced member ${JSON.stringify(key)} and never stored it, which the ` +
|
|
511
|
+
`incremental source cannot follow; run it with SourceMode.materialize`,
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
return false
|
|
515
|
+
}
|
|
516
|
+
if (!isEngineContainer(value)) return this.scalar(value)
|
|
517
|
+
if (null !== this.lastCompleted && value === this.lastCompleted) {
|
|
518
|
+
this.lastCompleted = null
|
|
519
|
+
return true
|
|
520
|
+
}
|
|
521
|
+
this.fail(mergedMember(key))
|
|
522
|
+
return false
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Keep the source text of a number rule's first token when it is a JSON
|
|
526
|
+
// number spelling the node's value.
|
|
527
|
+
private rememberLexeme(rule: any): void {
|
|
528
|
+
const node = rule.node
|
|
529
|
+
if ('number' !== typeof node) return
|
|
530
|
+
const token = rule.o?.[0]
|
|
531
|
+
if (null == token) return
|
|
532
|
+
const src = token.src
|
|
533
|
+
if ('string' === typeof src && isJsonNumber(src) && Number(src) === node) {
|
|
534
|
+
this.lexeme = src
|
|
535
|
+
this.lexemeValue = node
|
|
536
|
+
this.lexemeReady = true
|
|
537
|
+
} else {
|
|
538
|
+
this.lexemeReady = false
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function containerLen(node: object): number {
|
|
544
|
+
return Array.isArray(node) ? node.length : engineKeys(node).length
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// The member of a map under `key`.
|
|
548
|
+
function member(node: unknown, key: string): { found: boolean; value: unknown } {
|
|
549
|
+
if (isEngineMap(node) && Object.prototype.hasOwnProperty.call(node, key)) {
|
|
550
|
+
return { found: true, value: node[key] }
|
|
551
|
+
}
|
|
552
|
+
return { found: false, value: undefined }
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// The subscriber and the parse budget on `parser`: the subscriber hands
|
|
556
|
+
// every rule pass to `onDone`, and the budget cancels the parse as soon as
|
|
557
|
+
// `proceed` answers false, while still consulting any budget check the
|
|
558
|
+
// grammar installed at its own cadence. Member order is recorded on the
|
|
559
|
+
// engine's maps (`map.ordered`), which leaves the values' shape alone.
|
|
560
|
+
export function prepare(
|
|
561
|
+
parser: any,
|
|
562
|
+
proceed: () => boolean,
|
|
563
|
+
onDone?: (rule: any, done: any) => void,
|
|
564
|
+
): void {
|
|
565
|
+
const budget = parser.options?.parse?.budget ?? {}
|
|
566
|
+
const prev = 'function' === typeof budget.onCheck ? budget.onCheck : null
|
|
567
|
+
const prevN = prev && 0 < budget.checkEveryN ? budget.checkEveryN : 0
|
|
568
|
+
parser.options({
|
|
569
|
+
map: { ordered: true },
|
|
570
|
+
parse: {
|
|
571
|
+
budget: {
|
|
572
|
+
checkEveryN: 1,
|
|
573
|
+
onCheck: (ctx: any) => {
|
|
574
|
+
if (!proceed()) return false
|
|
575
|
+
if (null !== prev && 0 < prevN && 0 === ctx.kI % prevN) return prev(ctx)
|
|
576
|
+
return true
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
})
|
|
581
|
+
if (onDone) parser.sub({ ruleDone: (rule: any, _ctx: any, done: any) => onDone(rule, done) })
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// The failure for an incremental run whose events did not amount to one
|
|
585
|
+
// whole document.
|
|
586
|
+
export function notStreamable(): Fail {
|
|
587
|
+
return new Fail(
|
|
588
|
+
'STREAMABILITY_UNKNOWN',
|
|
589
|
+
'the grammar did not build its value through rule events the incremental source can ' +
|
|
590
|
+
'follow (the events did not amount to one whole document); run it with ' +
|
|
591
|
+
'SourceMode.materialize',
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// The failure for a container that starts at the root after a root value
|
|
596
|
+
// has completed.
|
|
597
|
+
export function wrappedRoot(): Fail {
|
|
598
|
+
return new Fail(
|
|
599
|
+
'STREAMABILITY_UNKNOWN',
|
|
600
|
+
"the grammar wrapped a value already streamed as the document's root in a list (a YAML " +
|
|
601
|
+
'stream of several documents, a jsonic top-level implicit list); the incremental source ' +
|
|
602
|
+
'cannot take the root back, so run it with SourceMode.materialize',
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// The failure for a parse root whose node no longer holds the value
|
|
607
|
+
// streamed as the document when the root rule closes for good.
|
|
608
|
+
export function rewrittenRoot(): Fail {
|
|
609
|
+
return new Fail(
|
|
610
|
+
'STREAMABILITY_UNKNOWN',
|
|
611
|
+
"the grammar replaced the document's root after the incremental source streamed it (a " +
|
|
612
|
+
'YAML stream of several documents is wrapped in a list when the source ends); the ' +
|
|
613
|
+
'incremental source cannot take the root back, so run it with SourceMode.materialize',
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// The failure for a container the adapter streamed as the grammar built it
|
|
618
|
+
// and the grammar then never stored in the container around it.
|
|
619
|
+
export function unstoredContainer(): Fail {
|
|
620
|
+
return new Fail(
|
|
621
|
+
'STREAMABILITY_UNKNOWN',
|
|
622
|
+
'the grammar built a container the incremental source streamed and then never stored it ' +
|
|
623
|
+
'in the container around it (jsonic drops a pair inside a list when list.pair is off), ' +
|
|
624
|
+
"so the stream would not be the document's; the incremental source cannot follow a " +
|
|
625
|
+
'grammar that builds a container so: run it with SourceMode.materialize',
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// The failure for a container the grammar opens inside a map before the
|
|
630
|
+
// member it belongs to has a key.
|
|
631
|
+
export function valueBeforeKey(): Fail {
|
|
632
|
+
return new Fail(
|
|
633
|
+
'STREAMABILITY_UNKNOWN',
|
|
634
|
+
"the grammar opened a container inside a map before announcing the member's key (it " +
|
|
635
|
+
'builds the value, or a key that is itself a container, in a rule of its own and ' +
|
|
636
|
+
'names the member only when the pair closes); its events would leave before the key, ' +
|
|
637
|
+
"so the stream would not be the document's; the incremental source cannot follow a " +
|
|
638
|
+
'grammar that builds a member so: run it with SourceMode.materialize',
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// The failure for a map whose members the grammar rewrote after the adapter
|
|
643
|
+
// streamed them.
|
|
644
|
+
export function rewrittenMap(): Fail {
|
|
645
|
+
return new Fail(
|
|
646
|
+
'STREAMABILITY_UNKNOWN',
|
|
647
|
+
'the grammar rewrote the members of a map after the incremental source streamed them ' +
|
|
648
|
+
"(a YAML merge key does), so the stream would not be the document's; run it with " +
|
|
649
|
+
'SourceMode.materialize',
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// The failure for a repeated member whose values the grammar merged.
|
|
654
|
+
export function mergedMember(key: string): Fail {
|
|
655
|
+
return new Fail(
|
|
656
|
+
'DUPLICATE_MEMBER',
|
|
657
|
+
`member ${JSON.stringify(key)} appears twice and the grammar merged the two values; the ` +
|
|
658
|
+
'first was already streamed, so the incremental source cannot emit the merged member: ' +
|
|
659
|
+
'run it with SourceMode.materialize',
|
|
660
|
+
)
|
|
661
|
+
}
|