@bablr/agast-helpers 0.3.1 → 0.4.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/lib/tree.js CHANGED
@@ -1,12 +1,25 @@
1
- import emptyStack from '@iter-tools/imm-stack';
2
1
  import { Coroutine } from '@bablr/coroutine';
3
- import { buildNull, nodeFlags, buildDoctypeTag, buildEmbeddedNode } from './builders.js';
2
+ import emptyStack from '@iter-tools/imm-stack';
3
+ import { nodeFlags, buildDoctypeTag, buildEmbeddedNode } from './builders.js';
4
4
  import {
5
5
  printPrettyCSTML as printPrettyCSTMLFromStream,
6
6
  printCSTML as printCSTMLFromStream,
7
7
  getStreamIterator,
8
8
  StreamIterable,
9
9
  } from './stream.js';
10
+ import {
11
+ DoctypeTag,
12
+ OpenNodeTag,
13
+ CloseNodeTag,
14
+ ReferenceTag,
15
+ ShiftTag,
16
+ GapTag,
17
+ NullTag,
18
+ ArrayTag,
19
+ LiteralTag,
20
+ EmbeddedNode,
21
+ } from './symbols.js';
22
+ import * as btree from './btree.js';
10
23
  import * as sym from './symbols.js';
11
24
  export * from './builders.js';
12
25
  export * from './print.js';
@@ -17,11 +30,6 @@ const isString = (str) => typeof str === 'string';
17
30
 
18
31
  const { isArray } = Array;
19
32
 
20
- const buildFrame = (node) => {
21
- if (!node) throw new Error();
22
- return { node, childrenIdx: -1, resolver: new Resolver(node) };
23
- };
24
-
25
33
  const { hasOwn, freeze } = Object;
26
34
 
27
35
  export const get = (node, path) => {
@@ -33,7 +41,7 @@ export const get = (node, path) => {
33
41
  }
34
42
 
35
43
  if (index != null) {
36
- return properties[name]?.[parseInt(index, 10)];
44
+ return btree.getAt(parseInt(index, 10), properties[name]);
37
45
  } else {
38
46
  return properties[name];
39
47
  }
@@ -46,18 +54,18 @@ export const add = (node, ref, value) => {
46
54
  if (!hasOwn(node.properties, name)) {
47
55
  node.properties[name] = [];
48
56
  }
49
- const array = node.properties[name];
50
57
 
51
- array.push(value);
58
+ node.properties[name] = btree.push(node.properties[name], value);
52
59
  } else {
53
60
  node.properties[name] = value;
54
61
  }
55
62
  };
56
63
 
57
64
  function* __treeFromStream(tokens) {
58
- let nodes = emptyStack;
59
- let rootNode;
65
+ let path = null;
66
+ let rootPath = null;
60
67
  let held = null;
68
+ let doctype = null;
61
69
  const co = new Coroutine(getStreamIterator(tokens));
62
70
 
63
71
  for (;;) {
@@ -69,65 +77,47 @@ function* __treeFromStream(tokens) {
69
77
 
70
78
  if (co.done) break;
71
79
 
72
- const token = co.value;
80
+ const tag = co.value;
73
81
 
74
- if (token.type === 'Effect') {
82
+ if (tag.type === 'Effect') {
75
83
  continue;
76
84
  }
77
85
 
78
- if (token.type === 'DoctypeTag') {
79
- const { attributes } = token.value;
80
-
81
- rootNode = freeze({
82
- flags: nodeFlags,
83
- language: attributes['bablr-language'],
84
- type: null,
85
- children: [],
86
- properties: {},
87
- attributes: freeze(attributes),
88
- });
89
- nodes = nodes.push(rootNode);
86
+ if (tag.type === DoctypeTag) {
87
+ doctype = tag;
90
88
  continue;
91
89
  }
92
90
 
93
- if (!nodes.size) {
94
- throw new Error('imbalanced tag stack');
91
+ if (held && tag.type !== 'StartNodeTag' && tag.type !== GapTag) {
92
+ throw new Error('cannot eat this type of tag while holding');
95
93
  }
96
94
 
97
- switch (token.type) {
98
- case 'Null': {
99
- const node = nodes.value;
100
- const ref = arrayLast(node.children);
101
-
102
- if (ref.type !== 'Reference') throw new Error();
103
-
104
- add(node, ref, null);
105
-
95
+ switch (tag.type) {
96
+ case LiteralTag:
97
+ case ReferenceTag:
98
+ case CloseNodeTag: {
106
99
  break;
107
100
  }
108
101
 
109
- case 'Literal':
110
- case 'Reference': {
111
- nodes.value.children.push(token);
112
- break;
113
- }
102
+ case NullTag:
103
+ case GapTag: {
104
+ const parentNode = path.parent.node;
105
+ const ref = arrayLast(parentNode.children);
106
+ const isGap = tag.type === GapTag;
114
107
 
115
- case 'Gap': {
116
- if (held) {
117
- const node = nodes.value;
118
- const ref = arrayLast(node.children);
108
+ if (ref.type !== ReferenceTag) throw new Error();
119
109
 
120
- if (ref.type !== 'Reference') throw new Error();
110
+ const node = (isGap && held) || createNode(nodeFlags, null, sym.null);
121
111
 
122
- add(node, ref, held);
112
+ held = isGap ? null : held;
113
+ path = { parent: path, node, depth: (path.depth || -1) + 1 };
123
114
 
124
- held = null;
125
- }
115
+ add(parentNode, ref, node);
126
116
  break;
127
117
  }
128
118
 
129
- case 'Shift': {
130
- const { children, properties } = nodes.value;
119
+ case ShiftTag: {
120
+ const { children, properties } = path.node;
131
121
 
132
122
  const ref = arrayLast(children);
133
123
  let node = properties[ref.value.name];
@@ -143,68 +133,67 @@ function* __treeFromStream(tokens) {
143
133
  break;
144
134
  }
145
135
 
146
- case 'OpenNodeTag': {
147
- const { flags, language, type, attributes } = token.value;
148
- const node = nodes.value;
136
+ case OpenNodeTag: {
137
+ const { flags, type } = tag.value;
149
138
 
150
- if (!type) {
151
- break;
152
- }
139
+ const language = type ? tag.value.language : doctype.value.attributes['bablr-language'];
140
+ const attributes = type ? tag.value.attributes : doctype.value.attributes;
153
141
 
154
- const newNode = freeze({
155
- flags,
156
- language,
157
- type,
158
- children: [],
159
- properties: {},
160
- attributes: freeze(attributes),
161
- });
162
-
163
- if (node && !(flags.escape || flags.trivia)) {
164
- if (!node.children.length) {
165
- throw new Error('Nodes must follow references');
166
- }
142
+ const node = createNode(flags, language, type, [], {}, attributes);
143
+
144
+ const parentPath = path;
167
145
 
168
- const ref = arrayLast(node.children);
146
+ path = { parent: path, node, depth: (path.depth || -1) + 1 };
169
147
 
170
- add(node, ref, newNode);
148
+ if (parentPath) {
149
+ const { node: parentNode } = path;
150
+ if (!(flags.escape || flags.trivia)) {
151
+ if (!parentNode.children.length) {
152
+ throw new Error('Nodes must follow references');
153
+ }
154
+
155
+ const ref = arrayLast(parentNode.children);
156
+
157
+ add(parentNode, ref, node);
158
+ } else {
159
+ parentNode.children.push(buildEmbeddedNode(node));
160
+ }
161
+ } else {
162
+ rootPath = path;
171
163
  }
172
164
 
173
- nodes = nodes.push(newNode);
174
165
  break;
175
166
  }
176
167
 
177
- case 'CloseNodeTag': {
178
- const completedNode = nodes.value;
179
- const { flags } = completedNode;
168
+ default: {
169
+ throw new Error();
170
+ }
171
+ }
180
172
 
181
- if (flags.escape || flags.trivia) {
182
- const parentChildren = nodes.prev.value.children;
173
+ path.node.children.push(tag);
183
174
 
184
- parentChildren.push(buildEmbeddedNode(completedNode));
185
- }
175
+ switch (tag.type) {
176
+ case NullTag:
177
+ case GapTag:
178
+ case CloseNodeTag: {
179
+ const completedNode = path.node;
186
180
 
187
- freeze(completedNode.properties);
188
- freeze(completedNode.children);
181
+ finalizeNode(completedNode);
189
182
 
190
- if (!completedNode.type && nodes.size !== 1) {
183
+ if (!completedNode.type && path.depth !== 1) {
191
184
  throw new Error('imbalanced tag stack');
192
185
  }
193
186
 
194
- nodes = nodes.pop();
187
+ path = path.parent;
195
188
  break;
196
189
  }
197
-
198
- default: {
199
- throw new Error();
200
- }
201
190
  }
202
191
  }
203
192
 
204
- return rootNode;
193
+ return rootPath.node;
205
194
  }
206
195
 
207
- export const treeFromStream = (terminals) => new StreamIterable(__treeFromStream(terminals));
196
+ export const treeFromStream = (tags) => new StreamIterable(__treeFromStream(tags));
208
197
 
209
198
  export const treeFromStreamSync = (tokens) => {
210
199
  return evaluateReturnSync(treeFromStream(tokens));
@@ -235,67 +224,73 @@ export const evaluateReturnAsync = async (generator) => {
235
224
  export const streamFromTree = (rootNode) => __streamFromTree(rootNode);
236
225
 
237
226
  function* __streamFromTree(rootNode) {
238
- if (!rootNode || rootNode.type === 'Gap') {
227
+ if (!rootNode || rootNode.type === GapTag) {
239
228
  return rootNode;
240
229
  }
241
230
 
242
231
  yield buildDoctypeTag(rootNode.attributes);
243
232
 
244
- let stack = emptyStack.push(buildFrame(rootNode));
233
+ let stack = emptyStack.push(rootNode);
234
+ const resolver = new Resolver();
245
235
 
246
236
  stack: while (stack.size) {
247
- const frame = stack.value;
248
- const { node, resolver } = frame;
237
+ const node = stack.value;
249
238
  const { children } = node;
250
239
 
251
- while (++frame.childrenIdx < children.length) {
252
- const terminal = children[frame.childrenIdx];
240
+ while (true) {
241
+ const tag = btree.getAt(resolver.idx, children);
253
242
 
254
- switch (terminal.type) {
255
- case 'EmbeddedNode': {
256
- stack = stack.push(buildFrame(terminal.value));
243
+ if (isArray(tag)) {
244
+ throw new Error();
245
+ }
257
246
 
258
- break;
247
+ switch (tag.type) {
248
+ case EmbeddedNode: {
249
+ stack = stack.push(tag.value);
250
+
251
+ resolver.advance(tag);
252
+
253
+ continue stack;
259
254
  }
260
255
 
261
- case 'Reference': {
262
- const resolved = resolver.consume(terminal).get(terminal);
256
+ case ReferenceTag: {
257
+ const resolvedPath = resolver.resolve(tag);
258
+ const resolved = get(stack.value, resolvedPath);
259
+ const { isArray: refIsArray } = tag.value;
263
260
 
264
- yield terminal;
261
+ if (!resolved) throw new Error();
265
262
 
266
- if (terminal.value.isArray && !resolved) {
267
- // TODO evaluate if this is still smart
268
- yield buildNull();
269
- } else {
270
- if (!resolved) throw new Error();
263
+ yield tag;
271
264
 
272
- stack = stack.push(buildFrame(resolved));
273
- }
265
+ resolver.advance(tag);
274
266
 
275
- break;
267
+ if (!refIsArray || !isArray(resolved)) {
268
+ if (isArray(resolved)) throw new Error();
269
+ stack = stack.push(resolved);
270
+ }
271
+ continue stack;
276
272
  }
277
273
 
278
- case 'Null': {
279
- yield terminal;
280
-
281
- break;
274
+ case GapTag:
275
+ case NullTag:
276
+ case CloseNodeTag: {
277
+ stack = stack.pop();
278
+ resolver.advance(tag);
279
+ yield tag;
280
+ continue stack;
282
281
  }
283
282
 
284
283
  default:
285
- yield terminal;
286
- }
287
-
288
- if (terminal.type === 'EmbeddedNode' || terminal.type === 'Reference') {
289
- continue stack;
284
+ resolver.advance(tag);
285
+ yield tag;
286
+ break;
290
287
  }
291
288
  }
292
-
293
- stack = stack.pop();
294
289
  }
295
290
  }
296
291
 
297
292
  export const getCooked = (cookable) => {
298
- if (!cookable || cookable.type === 'Gap') {
293
+ if (!cookable || cookable.type === GapTag) {
299
294
  return '';
300
295
  }
301
296
 
@@ -303,14 +298,17 @@ export const getCooked = (cookable) => {
303
298
 
304
299
  let cooked = '';
305
300
 
306
- for (const terminal of children) {
307
- switch (terminal.type) {
308
- case 'Reference': {
301
+ const openTag = getOpenTag(cookable);
302
+ const closeTag = getCloseTag(cookable);
303
+
304
+ for (const tag of btree.traverse(children)) {
305
+ switch (tag.type) {
306
+ case ReferenceTag: {
309
307
  throw new Error('cookable nodes must not contain other nodes');
310
308
  }
311
309
 
312
- case 'EmbeddedNode': {
313
- const { flags, attributes } = terminal.value;
310
+ case EmbeddedNode: {
311
+ const { flags, attributes } = tag.value;
314
312
 
315
313
  if (!(flags.trivia || (flags.escape && attributes.cooked))) {
316
314
  throw new Error('cookable nodes must not contain other nodes');
@@ -328,8 +326,16 @@ export const getCooked = (cookable) => {
328
326
  break;
329
327
  }
330
328
 
331
- case 'Literal': {
332
- cooked += terminal.value;
329
+ case LiteralTag: {
330
+ cooked += tag.value;
331
+ break;
332
+ }
333
+
334
+ case OpenNodeTag: {
335
+ break;
336
+ }
337
+
338
+ case CloseNodeTag: {
333
339
  break;
334
340
  }
335
341
 
@@ -350,26 +356,35 @@ export const printPrettyCSTML = (rootNode, options = {}) => {
350
356
  return printPrettyCSTMLFromStream(streamFromTree(rootNode), options);
351
357
  };
352
358
 
353
- export const printSource = (node) => {
354
- const resolver = new Resolver(node);
359
+ const __printSource = (rootNode, resolver = new Resolver()) => {
355
360
  let printed = '';
356
361
 
357
- if (!node) return '';
362
+ if (!rootNode) return '';
363
+
364
+ let node = rootNode;
358
365
 
359
366
  if (node instanceof Promise) {
360
367
  printed += '$Promise';
361
368
  } else {
362
- for (const child of node.children) {
363
- if (child.type === 'Literal') {
369
+ for (const child of btree.traverse(node.children)) {
370
+ if (child.type === LiteralTag) {
364
371
  printed += child.value;
365
- } else if (child.type === 'EmbeddedNode') {
366
- printed += printSource(child.value);
367
- } else if (child.type === 'Reference') {
368
- const node_ = resolver.consume(child).get(child);
369
-
370
- if (node_) {
371
- printed += printSource(node_);
372
+ resolver.advance(child);
373
+ } else if (child.type === EmbeddedNode) {
374
+ resolver.advance(child);
375
+ printed += __printSource(child.value, resolver);
376
+ } else if (child.type === ReferenceTag) {
377
+ const resolvedPath = resolver.resolve(child);
378
+ const resolvedNode = get(node, resolvedPath);
379
+
380
+ resolver.advance(child);
381
+ if (resolvedNode) {
382
+ if (!isArray(resolvedNode)) {
383
+ printed += __printSource(resolvedNode, resolver);
384
+ }
372
385
  }
386
+ } else {
387
+ resolver.advance(child);
373
388
  }
374
389
  }
375
390
  }
@@ -377,24 +392,26 @@ export const printSource = (node) => {
377
392
  return printed;
378
393
  };
379
394
 
395
+ export const printSource = (rootNode) => __printSource(rootNode);
396
+
380
397
  export const sourceTextFor = printSource;
381
398
 
382
399
  export const getOpenTag = (node) => {
383
- const tag = node.children[0];
384
- if (tag && tag.type !== 'OpenNodeTag') throw new Error();
400
+ const tag = btree.getAt(0, node.children);
401
+ if (tag && tag.type !== OpenNodeTag) throw new Error();
385
402
  return tag;
386
403
  };
387
404
 
388
405
  export const getCloseTag = (node) => {
389
406
  const { children } = node;
390
- const tag = children[children.length - 1];
391
- if (tag.type !== 'CloseNodeTag') return null;
407
+ const tag = btree.getAt(-1, children);
408
+ if (tag.type !== CloseNodeTag) return null;
392
409
  return tag;
393
410
  };
394
411
 
395
412
  export const getRange = (node) => {
396
413
  const { children } = node;
397
- return children.length ? [children[0], children[children.length - 1]] : null;
414
+ return children.length ? [children[1], children[children.length - 1]] : null;
398
415
  };
399
416
 
400
417
  export const createNode = (openTag) => {
@@ -403,8 +420,14 @@ export const createNode = (openTag) => {
403
420
  };
404
421
 
405
422
  export const finalizeNode = (node) => {
423
+ for (const propertyValue of Object.values(node.properties)) {
424
+ if (isArray(propertyValue)) {
425
+ btree.freeze(propertyValue);
426
+ }
427
+ }
428
+
406
429
  freeze(node);
407
- freeze(node.children);
430
+ btree.freeze(node.children);
408
431
  freeze(node.properties);
409
432
  freeze(node.attributes);
410
433
  return node;
@@ -453,33 +476,145 @@ export const acceptNode = (node, accepted) => {
453
476
  export const getRoot = (fragmentNode) => {
454
477
  if (!fragmentNode) return null;
455
478
 
456
- for (const terminal of fragmentNode.children) {
457
- if (terminal.type === 'Reference') {
458
- if (terminal.value.isArray) throw new Error();
459
- return fragmentNode.properties[terminal.value.name];
479
+ for (const tag of btree.traverse(fragmentNode.children)) {
480
+ if (tag.type === ReferenceTag) {
481
+ return fragmentNode.properties[tag.value.name];
460
482
  }
461
483
  }
462
484
  };
463
485
 
464
486
  export class Resolver {
465
- constructor(node, counters = new Map()) {
466
- this.node = node;
467
- this.counters = counters;
487
+ constructor(
488
+ states = emptyStack.push({ properties: new Map(), idx: 0 }),
489
+ reference = null,
490
+ popped = false,
491
+ held = null,
492
+ ) {
493
+ this.states = states;
494
+ this.reference = reference;
495
+ this.popped = popped;
496
+ this.held = held;
468
497
  }
469
498
 
470
- consume(reference) {
471
- const { name, isArray } = reference.value;
472
- const { counters } = this;
499
+ get idx() {
500
+ return this.states.value.idx;
501
+ }
473
502
 
474
- if (isArray) {
475
- const count = counters.get(name) + 1 || 0;
503
+ get properties() {
504
+ return this.states.value.properties;
505
+ }
506
+
507
+ advance(tag) {
508
+ const { states } = this;
509
+
510
+ ++states.value.idx;
511
+
512
+ this.popped = false;
513
+
514
+ switch (tag.type) {
515
+ case ReferenceTag: {
516
+ const { name, isArray } = tag.value;
517
+ const { properties } = states.value;
518
+
519
+ if (this.reference) throw new Error();
520
+
521
+ this.reference = tag;
522
+
523
+ let state = properties.get(name);
524
+
525
+ if (isArray) {
526
+ if (state && !state.isArray) throw new Error();
527
+
528
+ const { count = -1 } = state || {};
529
+
530
+ state = { count: count + 1, isArray };
531
+ } else if (state) {
532
+ throw new Error(`attempted to consume property {name: ${name}} twice`);
533
+ } else {
534
+ state = { count: 1, isArray: false };
535
+ }
536
+
537
+ properties.set(name, state);
538
+
539
+ if (!isArray || state.count > 0) {
540
+ this.states = states.push({ properties: new Map(), idx: 0 });
541
+ }
542
+
543
+ break;
544
+ }
545
+
546
+ case EmbeddedNode: {
547
+ if (this.reference) throw new Error();
548
+
549
+ this.reference = tag;
550
+
551
+ this.states = states.push({ properties: new Map(), idx: 0 });
552
+ break;
553
+ }
554
+
555
+ case OpenNodeTag: {
556
+ const { flags } = tag.value;
557
+ const isRootNode = states.size === 1;
558
+
559
+ if (!isRootNode && !this.reference && !(flags.trivia || flags.escape)) {
560
+ throw new Error();
561
+ }
562
+
563
+ if (this.reference?.type !== EmbeddedNode && (flags.trivia || flags.escape)) {
564
+ this.states = states.push({ properties: new Map(), idx: 0 });
565
+ }
566
+
567
+ this.reference = null;
568
+ break;
569
+ }
476
570
 
477
- counters.set(name, count);
478
- } else {
479
- if (counters.has(name))
480
- throw new Error(`attempted to consume property {name: ${name}} twice`);
571
+ case ArrayTag: {
572
+ if (!this.reference) throw new Error();
481
573
 
482
- counters.set(name, 1);
574
+ const { name } = this.reference.value;
575
+ const { properties } = states.value;
576
+ const state = properties.get(name);
577
+
578
+ if (!state || !state.isArray || state.count !== 0) throw new Error();
579
+
580
+ properties.set(name, { count: 0, isArray: true });
581
+
582
+ this.reference = null;
583
+ break;
584
+ }
585
+
586
+ case ShiftTag: {
587
+ this.held = this.states.value;
588
+ this.states = this.states.push({ properties: new Map(), idx: 0 });
589
+ this.reference = tag;
590
+
591
+ break;
592
+ }
593
+
594
+ case NullTag: {
595
+ this.states = states.pop();
596
+ this.popped = true;
597
+ this.reference = null;
598
+ break;
599
+ }
600
+
601
+ case GapTag: {
602
+ this.states = states.pop();
603
+
604
+ if (this.held) {
605
+ // this.states = this.states.push(this.held);
606
+ this.held = null;
607
+ }
608
+
609
+ this.popped = true;
610
+ this.reference = null;
611
+ break;
612
+ }
613
+
614
+ case CloseNodeTag: {
615
+ this.states = states.pop();
616
+ this.popped = true;
617
+ }
483
618
  }
484
619
 
485
620
  return this;
@@ -487,30 +622,37 @@ export class Resolver {
487
622
 
488
623
  resolve(reference) {
489
624
  let { name, isArray } = reference.value;
490
- const { counters } = this;
625
+ const { states } = this;
626
+ const state = states.value.properties.get(name);
491
627
  let path = name;
492
628
 
493
629
  if (isArray) {
494
- const count = counters.get(name) || 0;
495
-
496
- path += '.' + count;
630
+ if (state) {
631
+ const count = state?.count || 0;
632
+ path += '.' + count;
633
+ }
497
634
  }
498
635
 
499
636
  return path;
500
637
  }
501
638
 
502
- get(reference) {
503
- if (!this.node) throw new Error('Cannot get from a resolver with no node');
504
-
505
- return get(this.node, this.resolve(reference));
506
- }
507
-
508
639
  branch() {
509
- return new Resolver(this.node, new Map(this.counters));
640
+ const { states, reference, popped, held } = this;
641
+ const { properties, idx } = states.value;
642
+
643
+ return new Resolver(
644
+ states.replace({ properties: new Map(properties), idx }),
645
+ reference,
646
+ popped,
647
+ held,
648
+ );
510
649
  }
511
650
 
512
651
  accept(resolver) {
513
- this.counters = resolver.counters;
652
+ this.states = resolver.states;
653
+ this.reference = resolver.reference;
654
+ this.popped = resolver.popped;
655
+ this.held = resolver.held;
514
656
 
515
657
  return this;
516
658
  }