@bablr/agast-helpers 0.2.0 → 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/lib/builders.js CHANGED
@@ -1,5 +1,46 @@
1
+ import * as sym from './symbols.js';
2
+
1
3
  const { freeze } = Object;
2
4
 
5
+ const isObject = (val) => val !== null && typeof value !== 'object';
6
+
7
+ export const buildEmbeddedExpression = (expr) => {
8
+ if (!isObject(expr)) return expr;
9
+ return freeze({ type: 'EmbeddedExpression', value: expr });
10
+ };
11
+
12
+ export const buildEmbeddedTag = (tag) => {
13
+ return freeze({ type: 'EmbeddedTag', value: tag });
14
+ };
15
+
16
+ export const buildEffect = (value) => {
17
+ return freeze({ type: 'Effect', value });
18
+ };
19
+
20
+ export const buildWriteEffect = (text, options = {}) => {
21
+ return buildEffect(
22
+ freeze({ verb: 'write', value: freeze({ text, options: buildEmbeddedExpression(options) }) }),
23
+ );
24
+ };
25
+
26
+ export const buildAnsiPushEffect = (spans = '') => {
27
+ return buildEffect(
28
+ freeze({ verb: 'ansi-push', value: { spans: spans === '' ? [] : spans.split(' ') } }),
29
+ );
30
+ };
31
+
32
+ export const buildAnsiPopEffect = () => {
33
+ return buildEffect(freeze({ verb: 'ansi-pop', value: undefined }));
34
+ };
35
+
36
+ export const buildTokenGroup = (tokens) => {
37
+ return freeze({ type: 'TokenGroup', value: tokens });
38
+ };
39
+
40
+ export const buildCall = (verb, ...args) => {
41
+ return { verb, arguments: args };
42
+ };
43
+
3
44
  export const buildBeginningOfStreamToken = () => {
4
45
  return freeze({ type: Symbol.for('@bablr/beginning-of-stream'), value: undefined });
5
46
  };
@@ -20,8 +61,8 @@ export const buildShift = () => {
20
61
  return freeze({ type: 'Shift', value: undefined });
21
62
  };
22
63
 
23
- export const buildEmbedded = (node) => {
24
- return freeze({ type: 'Embedded', value: node });
64
+ export const buildEmbeddedNode = (node) => {
65
+ return freeze({ type: 'EmbeddedNode', value: node });
25
66
  };
26
67
 
27
68
  export const buildDoctypeTag = (attributes) => {
@@ -31,13 +72,7 @@ export const buildDoctypeTag = (attributes) => {
31
72
  });
32
73
  };
33
74
 
34
- export const buildNodeOpenTag = (
35
- flags = {},
36
- language = null,
37
- type = null,
38
- intrinsicValue = null,
39
- attributes = {},
40
- ) => {
75
+ export const buildNodeOpenTag = (flags = {}, language = null, type = null, attributes = {}) => {
41
76
  let { token, trivia, escape, expression, intrinsic } = flags;
42
77
 
43
78
  token = !!token;
@@ -52,7 +87,6 @@ export const buildNodeOpenTag = (
52
87
  flags: freeze({ token, trivia, escape, intrinsic, expression }),
53
88
  language,
54
89
  type,
55
- intrinsicValue,
56
90
  attributes,
57
91
  }),
58
92
  });
@@ -223,20 +257,13 @@ export const buildTriviaNode = (language, type, children = [], properties = {},
223
257
  attributes: freeze(attributes),
224
258
  });
225
259
 
226
- export const buildEffect = (value) => {
227
- return freeze({ type: 'Effect', value });
228
- };
229
-
230
- export const buildWriteEffect = (text, options = {}) => {
231
- return buildEffect(freeze({ verb: 'write', value: freeze({ text, options }) }));
232
- };
233
-
234
- export const buildAnsiPushEffect = (spans = '') => {
235
- return buildEffect(
236
- freeze({ verb: 'ansi-push', value: { spans: spans === '' ? [] : spans.split(' ') } }),
237
- );
238
- };
239
-
240
- export const buildAnsiPopEffect = () => {
241
- return buildEffect(freeze({ verb: 'ansi-pop', value: undefined }));
260
+ export const buildNullNode = () => {
261
+ return freeze({
262
+ flags: nodeFlags,
263
+ language: null,
264
+ type: sym.null,
265
+ children: freeze([buildNull()]),
266
+ properties: freeze({}),
267
+ attributes: freeze({}),
268
+ });
242
269
  };
package/lib/print.js CHANGED
@@ -8,6 +8,22 @@ const isFunction = (val) => typeof val === 'function';
8
8
  const when = (condition, value) =>
9
9
  condition ? (isFunction(value) ? value() : value) : { *[Symbol.iterator]() {} };
10
10
 
11
+ export const printCall = (call) => {
12
+ const { verb, arguments: args } = call;
13
+ return `${verb}${printTuple(args)}`;
14
+ };
15
+
16
+ export const printArray = (arr) => `[${arr.map((v) => printExpression(v)).join(', ')}]`;
17
+
18
+ export const printTuple = (tup) => `(${tup.map((v) => printExpression(v)).join(', ')})`;
19
+
20
+ export const printObject = (obj) => {
21
+ const entries = Object.entries(obj);
22
+ return entries.length
23
+ ? `{ ${entries.map(([k, v]) => `${k}: ${printExpression(v)}`).join(', ')} }`
24
+ : '{}';
25
+ };
26
+
11
27
  export const printExpression = (expr) => {
12
28
  if (isString(expr)) {
13
29
  return printString(expr);
@@ -23,14 +39,33 @@ export const printExpression = (expr) => {
23
39
  throw new Error();
24
40
  }
25
41
  } else if (isArray(expr)) {
26
- return `[${expr.map((v) => printExpression(v)).join(', ')}]`;
42
+ return printArray(expr);
27
43
  } else if (typeof expr === 'object') {
28
- return `{${Object.entries(expr).map(([k, v]) => `${k}: ${printExpression(v)}`)}}`;
44
+ return printEmbedded(expr);
29
45
  } else {
30
46
  throw new Error();
31
47
  }
32
48
  };
33
49
 
50
+ export const printEmbedded = (value) => {
51
+ switch (value.type) {
52
+ case 'EmbeddedTag':
53
+ return printTerminal(value.value);
54
+
55
+ case 'EmbeddedExpression': {
56
+ return printObject(value.value);
57
+ }
58
+
59
+ case 'EmbeddedNode': {
60
+ throw new Error('not implemented');
61
+ break;
62
+ }
63
+
64
+ default:
65
+ throw new Error();
66
+ }
67
+ };
68
+
34
69
  export const printAttributes = (attributes) => {
35
70
  return Object.entries(attributes)
36
71
  .map(([k, v]) => (v === true ? k : v === false ? `!${k}` : `${k}=${printExpression(v)}`))
@@ -46,9 +81,10 @@ export const printLanguage = (language) => {
46
81
  };
47
82
 
48
83
  export const printTagPath = (language, type) => {
49
- return [...when(language?.length, () => [printLanguage(language)]), ...when(type, [type])].join(
50
- ':',
51
- );
84
+ return [
85
+ ...when(type && language?.length, () => [printLanguage(language)]),
86
+ ...when(type, [type]),
87
+ ].join(':');
52
88
  };
53
89
 
54
90
  const escapeReplacer = (esc) => {
@@ -121,7 +157,8 @@ export const printDoctypeTag = (terminal) => {
121
157
 
122
158
  let { doctype, version, attributes } = terminal.value;
123
159
 
124
- attributes = attributes ? ` ${printAttributes(attributes)}` : '';
160
+ attributes =
161
+ attributes && Object.values(attributes).length ? ` ${printAttributes(attributes)}` : '';
125
162
 
126
163
  return `<!${version}:${doctype}${attributes}>`;
127
164
  };
@@ -147,17 +184,27 @@ export const printFlags = (flags) => {
147
184
  export const printOpenNodeTag = (terminal) => {
148
185
  if (terminal?.type !== 'OpenNodeTag') throw new Error();
149
186
 
150
- const { flags, language: tagLanguage, type, intrinsicValue, attributes } = terminal.value;
187
+ const { flags, language: tagLanguage, type, attributes } = terminal.value;
188
+
189
+ const printedAttributes = attributes && printAttributes(attributes);
190
+ const attributesFrag = printedAttributes ? ` ${printedAttributes}` : '';
191
+
192
+ return `<${printFlags(flags)}${printTagPath(tagLanguage, type)}${attributesFrag}>`;
193
+ };
194
+
195
+ export const printSelfClosingNodeTag = (terminal, intrinsicValue) => {
196
+ if (terminal?.type !== 'OpenNodeTag') throw new Error();
197
+
198
+ const { flags, language: tagLanguage, type, attributes } = terminal.value;
151
199
 
152
200
  const printedAttributes = attributes && printAttributes(attributes);
153
201
  const attributesFrag = printedAttributes ? ` ${printedAttributes}` : '';
154
202
  const intrinsicFrag = intrinsicValue ? ` ${printString(intrinsicValue)}` : '';
155
- const selfCloser = intrinsicValue ? ' /' : '';
156
203
 
157
204
  return `<${printFlags(flags)}${printTagPath(
158
205
  tagLanguage,
159
206
  type,
160
- )}${intrinsicFrag}${attributesFrag}${selfCloser}>`;
207
+ )}${intrinsicFrag}${attributesFrag} />`;
161
208
  };
162
209
 
163
210
  export const printCloseNodeTag = (terminal) => {
package/lib/shorthand.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  buildReference,
3
3
  buildGap,
4
- buildEmbedded,
4
+ buildEmbeddedNode,
5
5
  buildNodeOpenTag,
6
6
  buildNodeCloseTag,
7
7
  buildLiteral,
@@ -12,6 +12,7 @@ import {
12
12
  buildSyntacticEscapeNode,
13
13
  buildSyntacticTriviaNode,
14
14
  buildTriviaNode,
15
+ buildNullNode,
15
16
  } from './builders.js';
16
17
 
17
18
  export * from './builders.js';
@@ -43,7 +44,7 @@ export const ref = (path) => {
43
44
  export const lit = (str) => buildLiteral(stripArray(str));
44
45
 
45
46
  export const gap = buildGap;
46
- export const embedded = buildEmbedded;
47
+ export const embedded = buildEmbeddedNode;
47
48
  export const nodeOpen = buildNodeOpenTag;
48
49
  export const nodeClose = buildNodeCloseTag;
49
50
  export const node = buildNode;
@@ -53,3 +54,4 @@ export const e_node = buildEscapeNode;
53
54
  export const s_e_node = buildSyntacticEscapeNode;
54
55
  export const s_t_node = buildSyntacticTriviaNode;
55
56
  export const t_node = buildTriviaNode;
57
+ export const null_node = buildNullNode;
package/lib/stream.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Coroutine } from '@bablr/coroutine';
2
- import { printTerminal } from './print.js';
3
- import { buildWriteEffect } from './builders.js';
2
+ import emptyStack from '@iter-tools/imm-stack';
3
+ import { printSelfClosingNodeTag, printTerminal } from './print.js';
4
+ import { buildWriteEffect, buildTokenGroup } from './builders.js';
4
5
  export * from './print.js';
5
6
 
6
7
  export const getStreamIterator = (obj) => {
@@ -83,6 +84,12 @@ export class AsyncGenerator {
83
84
  }
84
85
  }
85
86
 
87
+ export const isIntrinsicToken = (terminal) => {
88
+ return (
89
+ terminal.type === 'OpenNodeTag' && terminal.value.flags.intrinsic && terminal.value.flags.token
90
+ );
91
+ };
92
+
86
93
  export class StreamGenerator {
87
94
  constructor(embeddedGenerator) {
88
95
  this.generator = embeddedGenerator;
@@ -137,45 +144,6 @@ export const maybeWait = (maybePromise, callback) => {
137
144
  }
138
145
  };
139
146
 
140
- function* __generateCSTMLStrategy(terminals) {
141
- if (!terminals) {
142
- yield buildWriteEffect('<//>');
143
- return;
144
- }
145
-
146
- let prevTerminal = null;
147
-
148
- const co = new Coroutine(getStreamIterator(terminals));
149
-
150
- for (;;) {
151
- co.advance();
152
-
153
- if (co.current instanceof Promise) {
154
- co.current = yield co.current;
155
- }
156
- if (co.done) break;
157
-
158
- const terminal = co.value;
159
-
160
- if (terminal.type === 'Reference' && prevTerminal.type === 'Null') {
161
- yield buildWriteEffect(' ');
162
- }
163
-
164
- if (terminal.type !== 'Effect') {
165
- buildWriteEffect(printTerminal(terminal));
166
-
167
- prevTerminal = terminal;
168
- } else {
169
- yield terminal;
170
- }
171
- }
172
-
173
- yield buildWriteEffect('\n');
174
- }
175
-
176
- export const generateCSTMLStrategy = (terminals) =>
177
- new StreamIterable(__generateCSTMLStrategy(terminals));
178
-
179
147
  function* __generateStandardOutput(terminals) {
180
148
  const co = new Coroutine(getStreamIterator(terminals));
181
149
 
@@ -278,17 +246,138 @@ export const stringFromStream = (stream) => {
278
246
  return str;
279
247
  };
280
248
 
281
- function* __generatePrettyCSTMLStrategy(terminals, options) {
282
- const { indent = ' ', emitEffects = false, inline: inlineOption = true } = options;
249
+ function* __generateCSTMLStrategy(terminals, options) {
250
+ let { emitEffects = false, inline: inlineOption = true } = options;
251
+
252
+ if (emitEffects) {
253
+ throw new Error('You must use generatePrettyCSTML with emitEffects');
254
+ }
283
255
 
284
256
  if (!terminals) {
285
257
  yield buildWriteEffect('<//>');
286
258
  return;
287
259
  }
288
260
 
261
+ let prevTerminal = null;
262
+
263
+ const co = new Coroutine(getStreamIterator(prettyGroupTokens(terminals)));
264
+
265
+ for (;;) {
266
+ co.advance();
267
+
268
+ if (co.current instanceof Promise) {
269
+ co.current = yield co.current;
270
+ }
271
+ if (co.done) break;
272
+
273
+ const terminal = co.value;
274
+
275
+ if (terminal.type === 'Reference' && prevTerminal.type === 'Null') {
276
+ yield buildWriteEffect(' ');
277
+ }
278
+
279
+ if (terminal.type === 'Effect') {
280
+ const effect = terminal.value;
281
+ if (emitEffects && effect.verb === 'write') {
282
+ yield buildWriteEffect(effect.value.text, effect.value.options);
283
+ }
284
+ continue;
285
+ }
286
+
287
+ if (terminal.type === 'TokenGroup') {
288
+ const intrinsicValue = getCooked(terminal.value.slice(1, -1));
289
+ yield buildWriteEffect(printSelfClosingNodeTag(terminal.value[0], intrinsicValue));
290
+ } else {
291
+ yield buildWriteEffect(printTerminal(terminal));
292
+ }
293
+
294
+ prevTerminal = terminal;
295
+ }
296
+
297
+ yield buildWriteEffect('\n');
298
+ }
299
+
300
+ export const prettyGroupTokens = (terminals) => new StreamIterable(__prettyGroupTokens(terminals));
301
+
302
+ function* __prettyGroupTokens(terminals) {
303
+ let states = emptyStack.push({ holding: [], broken: false, open: null });
304
+ let state = states.value;
305
+
289
306
  const co = new Coroutine(getStreamIterator(terminals));
307
+
308
+ for (;;) {
309
+ co.advance();
310
+
311
+ if (co.done) break;
312
+
313
+ if (co.current instanceof Promise) {
314
+ co.current = yield co.current;
315
+ }
316
+
317
+ const terminal = co.value;
318
+ const isOpenClose =
319
+ terminal.type === 'CloseNodeTag' || (terminal.type === 'OpenNodeTag' && terminal.value.type);
320
+
321
+ if (
322
+ (terminal.type === 'Effect' && terminal.value.verb === 'write') ||
323
+ ['Reference', 'DoctypeTag', 'Gap', 'Null'].includes(terminal.type) ||
324
+ (terminal.type === 'OpenNodeTag' && !terminal.value.type) ||
325
+ (state.open &&
326
+ !isIntrinsicToken(state.open) &&
327
+ (terminal.type === 'Literal' ||
328
+ (terminal.type === 'OpenNodeTag' && terminal.value.flags.escape)))
329
+ ) {
330
+ state.broken = true;
331
+
332
+ if (state.holding.length) {
333
+ yield* state.holding;
334
+ state.holding = [];
335
+ }
336
+
337
+ if (!isOpenClose) {
338
+ yield terminal;
339
+ }
340
+ } else if (!isOpenClose) {
341
+ state.holding.push(terminal);
342
+ }
343
+
344
+ if (terminal.type === 'CloseNodeTag') {
345
+ if (!state.broken && (isIntrinsicToken(state.open) || state.holding.length === 1)) {
346
+ state.holding.push(terminal);
347
+ yield buildTokenGroup(state.holding);
348
+ } else {
349
+ if (state.holding.length) {
350
+ yield* state.holding;
351
+ }
352
+ yield terminal;
353
+ }
354
+
355
+ states = states.pop();
356
+ state = states.value;
357
+ }
358
+
359
+ if (terminal.type === 'OpenNodeTag' && terminal.value.type) {
360
+ states = states.push({ holding: [terminal], broken: false, open: terminal });
361
+ state = states.value;
362
+ }
363
+ }
364
+ }
365
+
366
+ export const generateCSTMLStrategy = (terminals, options = {}) =>
367
+ new StreamIterable(__generateCSTMLStrategy(terminals, options));
368
+
369
+ function* __generatePrettyCSTMLStrategy(terminals, options) {
370
+ let { indent = ' ', emitEffects = false, inline: inlineOption = true } = options;
371
+
372
+ if (!terminals) {
373
+ yield buildWriteEffect('<//>');
374
+ return;
375
+ }
376
+
377
+ const co = new Coroutine(getStreamIterator(prettyGroupTokens(terminals)));
290
378
  let indentLevel = 0;
291
379
  let first = true;
380
+ let inline = false;
292
381
 
293
382
  for (;;) {
294
383
  co.advance();
@@ -306,18 +395,16 @@ function* __generatePrettyCSTMLStrategy(terminals, options) {
306
395
  if (emitEffects && effect.verb === 'write') {
307
396
  yield buildWriteEffect((first ? '' : '\n') + effect.value.text, effect.value.options);
308
397
 
398
+ inline = false;
309
399
  first = false;
310
400
  }
311
401
  continue;
312
402
  }
313
403
 
314
- const inline =
404
+ inline =
315
405
  inlineOption &&
316
- (terminal.type === 'Null' ||
317
- terminal.type === 'Gap' ||
318
- (terminal.type === 'OpenNodeTag' &&
319
- terminal.value.intrinsicValue &&
320
- terminal.value.flags.intrinsic));
406
+ inline &&
407
+ (terminal.type === 'Null' || terminal.type === 'Gap' || terminal.type === 'TokenGroup');
321
408
 
322
409
  if (!first && !inline) {
323
410
  yield buildWriteEffect('\n');
@@ -336,12 +423,19 @@ function* __generatePrettyCSTMLStrategy(terminals, options) {
336
423
  } else {
337
424
  yield buildWriteEffect(' ');
338
425
  }
339
- yield buildWriteEffect(printTerminal(terminal));
340
426
 
341
- if (
342
- terminal.type === 'OpenNodeTag' &&
343
- (!terminal.value.intrinsicValue || !terminal.value.type)
344
- ) {
427
+ if (terminal.type === 'TokenGroup') {
428
+ const intrinsicValue = getCooked(terminal.value.slice(1, -1));
429
+ yield buildWriteEffect(printSelfClosingNodeTag(terminal.value[0], intrinsicValue));
430
+ } else {
431
+ yield buildWriteEffect(printTerminal(terminal));
432
+ }
433
+
434
+ if (terminal.type === 'Reference') {
435
+ inline = true;
436
+ }
437
+
438
+ if (terminal.type === 'OpenNodeTag') {
345
439
  indentLevel++;
346
440
  }
347
441
 
@@ -408,8 +502,6 @@ export const printSource = (terminals) => {
408
502
  for (const terminal of terminals) {
409
503
  if (terminal.type === 'Literal') {
410
504
  printed += terminal.value;
411
- } else if (terminal.type === 'OpenNodeTag' && terminal.value.intrinsicValue) {
412
- printed += terminal.value.intrinsicValue;
413
505
  }
414
506
  }
415
507
 
@@ -420,21 +512,8 @@ export function* generateSourceTextFor(terminals) {
420
512
  for (const terminal of terminals) {
421
513
  if (terminal.type === 'Literal') {
422
514
  yield* terminal.value;
423
- } else if (terminal.type === 'OpenNodeTag' && terminal.value.intrinsicValue) {
424
- yield* terminal.value.intrinsicValue;
425
515
  }
426
516
  }
427
517
  }
428
518
 
429
519
  export const sourceTextFor = printSource;
430
-
431
- export const startsDocument = (terminal) => {
432
- const { type, value } = terminal;
433
- if ((type === 'OpenNodeTag' && !value.type) || type === 'DoctypeTag') {
434
- return true;
435
- } else if (type === 'OpenNodeTag') {
436
- const { flags } = value;
437
-
438
- return flags.trivia || flags.escape;
439
- }
440
- };
package/lib/symbols.js ADDED
@@ -0,0 +1,9 @@
1
+ export const node = Symbol.for('@bablr/node');
2
+
3
+ export const gap = Symbol.for('@bablr/gap');
4
+
5
+ export const fragment = Symbol.for('@bablr/fragment');
6
+
7
+ export const null_ = Symbol.for('@bablr/null');
8
+
9
+ export { null_ as null };
package/lib/template.js CHANGED
@@ -28,10 +28,10 @@ export const interpolateArrayChildren = (value, ref, sep) => {
28
28
  };
29
29
 
30
30
  const validateTerminal = (term) => {
31
- if (!term || (term.type !== 'Literal' && term.type !== 'Embedded')) {
31
+ if (!term || (term.type !== 'Literal' && term.type !== 'EmbeddedNode')) {
32
32
  throw new Error('Invalid terminal');
33
33
  }
34
- if (term.type === 'Embedded' && !term.value.flags.escape) {
34
+ if (term.type === 'EmbeddedNode' && !term.value.flags.escape) {
35
35
  throw new Error();
36
36
  }
37
37
  };
package/lib/tree.js CHANGED
@@ -1,19 +1,13 @@
1
1
  import emptyStack from '@iter-tools/imm-stack';
2
2
  import { Coroutine } from '@bablr/coroutine';
3
- import {
4
- buildNodeCloseTag,
5
- buildNodeOpenTag,
6
- buildNull,
7
- buildEmbedded,
8
- nodeFlags,
9
- buildDoctypeTag,
10
- } from './builders.js';
3
+ import { buildNull, nodeFlags, buildDoctypeTag, buildEmbeddedNode } from './builders.js';
11
4
  import {
12
5
  printPrettyCSTML as printPrettyCSTMLFromStream,
13
6
  printCSTML as printCSTMLFromStream,
14
7
  getStreamIterator,
15
8
  StreamIterable,
16
9
  } from './stream.js';
10
+ import * as sym from './symbols.js';
17
11
  export * from './builders.js';
18
12
  export * from './print.js';
19
13
 
@@ -21,6 +15,8 @@ const arrayLast = (arr) => arr[arr.length - 1];
21
15
 
22
16
  const isString = (str) => typeof str === 'string';
23
17
 
18
+ const { isArray } = Array;
19
+
24
20
  const buildFrame = (node) => {
25
21
  if (!node) throw new Error();
26
22
  return { node, childrenIdx: -1, resolver: new Resolver(node) };
@@ -28,13 +24,33 @@ const buildFrame = (node) => {
28
24
 
29
25
  const { hasOwn, freeze } = Object;
30
26
 
31
- const get = (node, path) => {
27
+ export const get = (node, path) => {
28
+ const { type, properties } = node;
32
29
  const { 1: name, 2: index } = /^([^\.]+)(?:\.(\d+))?/.exec(path) || [];
33
30
 
31
+ if (!hasOwn(properties, name)) {
32
+ throw new Error(`Cannot find {name: ${name}} on node of {type: ${type}}`);
33
+ }
34
+
34
35
  if (index != null) {
35
- return node.properties[name]?.[parseInt(index, 10)];
36
+ return properties[name]?.[parseInt(index, 10)];
36
37
  } else {
37
- return node.properties[name];
38
+ return properties[name];
39
+ }
40
+ };
41
+
42
+ export const add = (node, ref, value) => {
43
+ const { name, isArray } = ref.value;
44
+
45
+ if (isArray) {
46
+ if (!hasOwn(node.properties, name)) {
47
+ node.properties[name] = [];
48
+ }
49
+ const array = node.properties[name];
50
+
51
+ array.push(value);
52
+ } else {
53
+ node.properties[name] = value;
38
54
  }
39
55
  };
40
56
 
@@ -64,6 +80,7 @@ function* __treeFromStream(tokens) {
64
80
 
65
81
  rootNode = freeze({
66
82
  flags: nodeFlags,
83
+ language: attributes['bablr-language'],
67
84
  type: null,
68
85
  children: [],
69
86
  properties: {},
@@ -79,6 +96,13 @@ function* __treeFromStream(tokens) {
79
96
 
80
97
  switch (token.type) {
81
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
+
82
106
  break;
83
107
  }
84
108
 
@@ -90,19 +114,12 @@ function* __treeFromStream(tokens) {
90
114
 
91
115
  case 'Gap': {
92
116
  if (held) {
93
- const { children, properties } = nodes.value;
94
- const ref = arrayLast(children);
117
+ const node = nodes.value;
118
+ const ref = arrayLast(node.children);
95
119
 
96
120
  if (ref.type !== 'Reference') throw new Error();
97
121
 
98
- if (ref.value.isArray) {
99
- if (!properties[ref.value.name]) {
100
- properties[ref.value.name] = [];
101
- }
102
- properties[ref.value.name].push(held);
103
- } else {
104
- properties[ref.value.name] = held;
105
- }
122
+ add(node, ref, held);
106
123
 
107
124
  held = null;
108
125
  }
@@ -127,12 +144,7 @@ function* __treeFromStream(tokens) {
127
144
  }
128
145
 
129
146
  case 'OpenNodeTag': {
130
- const buildStringTerminals = (str) => {
131
- // do better
132
- return [{ type: 'Literal', value: str }];
133
- };
134
-
135
- const { flags, language, type, intrinsicValue, attributes } = token.value;
147
+ const { flags, language, type, attributes } = token.value;
136
148
  const node = nodes.value;
137
149
 
138
150
  if (!type) {
@@ -143,7 +155,7 @@ function* __treeFromStream(tokens) {
143
155
  flags,
144
156
  language,
145
157
  type,
146
- children: intrinsicValue && flags.intrinsic ? buildStringTerminals(intrinsicValue) : [],
158
+ children: [],
147
159
  properties: {},
148
160
  attributes: freeze(attributes),
149
161
  });
@@ -153,24 +165,9 @@ function* __treeFromStream(tokens) {
153
165
  throw new Error('Nodes must follow references');
154
166
  }
155
167
 
156
- const { name, isArray } = arrayLast(node.children).value;
157
-
158
- if (isArray) {
159
- if (!hasOwn(node.properties, name)) {
160
- node.properties[name] = [];
161
- }
162
- const array = node.properties[name];
168
+ const ref = arrayLast(node.children);
163
169
 
164
- array.push(newNode);
165
- } else {
166
- node.properties[name] = newNode;
167
- }
168
- }
169
-
170
- if (intrinsicValue && flags.intrinsic) {
171
- freeze(newNode.children);
172
- freeze(newNode.properties);
173
- break;
170
+ add(node, ref, newNode);
174
171
  }
175
172
 
176
173
  nodes = nodes.push(newNode);
@@ -184,7 +181,7 @@ function* __treeFromStream(tokens) {
184
181
  if (flags.escape || flags.trivia) {
185
182
  const parentChildren = nodes.prev.value.children;
186
183
 
187
- parentChildren.push(buildEmbedded(completedNode));
184
+ parentChildren.push(buildEmbeddedNode(completedNode));
188
185
  }
189
186
 
190
187
  freeze(completedNode.properties);
@@ -235,75 +232,66 @@ export const evaluateReturnAsync = async (generator) => {
235
232
  return co.value;
236
233
  };
237
234
 
238
- export function* streamFromTree(rootNode) {
235
+ export const streamFromTree = (rootNode) => __streamFromTree(rootNode);
236
+
237
+ function* __streamFromTree(rootNode) {
239
238
  if (!rootNode || rootNode.type === 'Gap') {
240
239
  return rootNode;
241
240
  }
242
241
 
243
242
  yield buildDoctypeTag(rootNode.attributes);
244
- yield buildNodeOpenTag();
245
243
 
246
244
  let stack = emptyStack.push(buildFrame(rootNode));
247
245
 
248
246
  stack: while (stack.size) {
249
247
  const frame = stack.value;
250
248
  const { node, resolver } = frame;
251
- const { flags, language, type, children, attributes } = node;
249
+ const { children } = node;
252
250
 
253
- const intrinsicValue = flags.token && flags.intrinsic ? children[0]?.value : null;
251
+ while (++frame.childrenIdx < children.length) {
252
+ const terminal = children[frame.childrenIdx];
254
253
 
255
- const children_ = intrinsicValue ? [intrinsicValue] : node.children;
254
+ switch (terminal.type) {
255
+ case 'EmbeddedNode': {
256
+ stack = stack.push(buildFrame(terminal.value));
256
257
 
257
- if (frame.childrenIdx === -1 && stack.size > 1) {
258
- yield buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
259
- }
258
+ break;
259
+ }
260
260
 
261
- if (!intrinsicValue) {
262
- while (++frame.childrenIdx < children_.length) {
263
- const terminal = children_[frame.childrenIdx];
261
+ case 'Reference': {
262
+ const resolved = resolver.consume(terminal).get(terminal);
264
263
 
265
- switch (terminal.type) {
266
- case 'Literal':
267
- case 'Gap':
268
- case 'Null': {
269
- yield terminal;
270
- break;
271
- }
264
+ yield terminal;
272
265
 
273
- case 'Embedded': {
274
- stack = stack.push(buildFrame(terminal.value));
275
- continue stack;
276
- }
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();
277
271
 
278
- case 'Reference': {
279
- if (stack.size > 1) {
280
- yield terminal;
281
- }
282
-
283
- const resolved = resolver.consume(terminal).get(terminal);
284
- if (resolved) {
285
- stack = stack.push(buildFrame(resolved));
286
- continue stack;
287
- } else {
288
- yield buildNull();
289
- break;
290
- }
272
+ stack = stack.push(buildFrame(resolved));
291
273
  }
292
274
 
293
- default: {
294
- throw new Error();
295
- }
275
+ break;
276
+ }
277
+
278
+ case 'Null': {
279
+ yield terminal;
280
+
281
+ break;
296
282
  }
283
+
284
+ default:
285
+ yield terminal;
297
286
  }
298
287
 
299
- if (stack.size > 1) {
300
- yield buildNodeCloseTag(type, language);
288
+ if (terminal.type === 'EmbeddedNode' || terminal.type === 'Reference') {
289
+ continue stack;
301
290
  }
302
291
  }
303
292
 
304
293
  stack = stack.pop();
305
294
  }
306
- yield buildNodeCloseTag();
307
295
  }
308
296
 
309
297
  export const getCooked = (cookable) => {
@@ -321,7 +309,7 @@ export const getCooked = (cookable) => {
321
309
  throw new Error('cookable nodes must not contain other nodes');
322
310
  }
323
311
 
324
- case 'Embedded': {
312
+ case 'EmbeddedNode': {
325
313
  const { flags, attributes } = terminal.value;
326
314
 
327
315
  if (!(flags.trivia || (flags.escape && attributes.cooked))) {
@@ -358,26 +346,28 @@ export const printCSTML = (rootNode) => {
358
346
  return printCSTMLFromStream(streamFromTree(rootNode));
359
347
  };
360
348
 
361
- export const printPrettyCSTML = (rootNode, indent = ' ') => {
362
- return printPrettyCSTMLFromStream(streamFromTree(rootNode), indent);
349
+ export const printPrettyCSTML = (rootNode, options = {}) => {
350
+ return printPrettyCSTMLFromStream(streamFromTree(rootNode), options);
363
351
  };
364
352
 
365
353
  export const printSource = (node) => {
366
354
  const resolver = new Resolver(node);
367
355
  let printed = '';
368
356
 
357
+ if (!node) return '';
358
+
369
359
  if (node instanceof Promise) {
370
360
  printed += '$Promise';
371
361
  } else {
372
362
  for (const child of node.children) {
373
363
  if (child.type === 'Literal') {
374
364
  printed += child.value;
375
- } else if (child.type === 'Embedded') {
365
+ } else if (child.type === 'EmbeddedNode') {
376
366
  printed += printSource(child.value);
377
367
  } else if (child.type === 'Reference') {
378
368
  const node_ = resolver.consume(child).get(child);
379
369
 
380
- if (node_ || !child.value.isArray) {
370
+ if (node_) {
381
371
  printed += printSource(node_);
382
372
  }
383
373
  }
@@ -389,6 +379,88 @@ export const printSource = (node) => {
389
379
 
390
380
  export const sourceTextFor = printSource;
391
381
 
382
+ export const getOpenTag = (node) => {
383
+ const tag = node.children[0];
384
+ if (tag && tag.type !== 'OpenNodeTag') throw new Error();
385
+ return tag;
386
+ };
387
+
388
+ export const getCloseTag = (node) => {
389
+ const { children } = node;
390
+ const tag = children[children.length - 1];
391
+ if (tag.type !== 'CloseNodeTag') return null;
392
+ return tag;
393
+ };
394
+
395
+ export const getRange = (node) => {
396
+ const { children } = node;
397
+ return children.length ? [children[0], children[children.length - 1]] : null;
398
+ };
399
+
400
+ export const createNode = (openTag) => {
401
+ const { flags, language, type, attributes } = openTag.value;
402
+ return { flags, language, type, children: [], properties: {}, attributes };
403
+ };
404
+
405
+ export const finalizeNode = (node) => {
406
+ freeze(node);
407
+ freeze(node.children);
408
+ freeze(node.properties);
409
+ freeze(node.attributes);
410
+ return node;
411
+ };
412
+
413
+ export const notNull = (node) => {
414
+ return node && node.type !== sym.null;
415
+ };
416
+
417
+ export const isNull = (node) => {
418
+ return !node || node.type === sym.null;
419
+ };
420
+
421
+ export const branchProperties = (properties) => {
422
+ const copy = { ...properties };
423
+
424
+ for (const { 0: key, 1: value } of Object.entries(copy)) {
425
+ if (isArray(value)) {
426
+ copy[key] = [...value];
427
+ }
428
+ }
429
+
430
+ return copy;
431
+ };
432
+
433
+ export const branchNode = (node) => {
434
+ const { flags, language, type, children, properties, attributes } = node;
435
+ return {
436
+ flags,
437
+ language,
438
+ type,
439
+ children: [...children],
440
+ properties: branchProperties(properties),
441
+ attributes: { ...attributes },
442
+ };
443
+ };
444
+
445
+ export const acceptNode = (node, accepted) => {
446
+ const { children, properties, attributes } = accepted;
447
+ node.children = children;
448
+ node.properties = properties;
449
+ node.attributes = attributes;
450
+ return node;
451
+ };
452
+
453
+ export const getRoot = (fragmentNode) => {
454
+ if (!fragmentNode) return null;
455
+
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];
460
+ }
461
+ }
462
+ };
463
+
392
464
  export class Resolver {
393
465
  constructor(node, counters = new Map()) {
394
466
  this.node = node;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bablr/agast-helpers",
3
3
  "description": "Helper functions for working with agAST trees",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "author": "Conrad Buck<conartist6@gmail.com>",
6
6
  "type": "module",
7
7
  "files": [
@@ -13,6 +13,7 @@
13
13
  "./print": "./lib/print.js",
14
14
  "./shorthand": "./lib/shorthand.js",
15
15
  "./stream": "./lib/stream.js",
16
+ "./symbols": "./lib/symbols.js",
16
17
  "./template": "./lib/template.js",
17
18
  "./tree": "./lib/tree.js"
18
19
  },