@bablr/agast-helpers 0.4.0 → 0.5.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
@@ -4,6 +4,8 @@ import {
4
4
  DoctypeTag,
5
5
  OpenNodeTag,
6
6
  CloseNodeTag,
7
+ OpenFragmentTag,
8
+ CloseFragmentTag,
7
9
  ReferenceTag,
8
10
  ShiftTag,
9
11
  GapTag,
@@ -17,9 +19,26 @@ import {
17
19
  } from './symbols.js';
18
20
 
19
21
  const { freeze } = Object;
22
+ const { isArray } = Array;
20
23
 
21
24
  const isObject = (val) => val !== null && typeof value !== 'object';
22
25
 
26
+ function* relatedNodes(properties) {
27
+ for (const value of Object.values(properties)) {
28
+ if (isArray(value)) {
29
+ yield* btree.traverse(value);
30
+ } else {
31
+ yield value;
32
+ }
33
+ }
34
+ }
35
+
36
+ const find = (predicate, iterable) => {
37
+ for (const value of iterable) {
38
+ if (predicate(value)) return value;
39
+ }
40
+ };
41
+
23
42
  export const buildEmbeddedExpression = (expr) => {
24
43
  if (!isObject(expr)) return expr;
25
44
  return freeze({ type: EmbeddedExpression, value: expr });
@@ -35,32 +54,28 @@ export const buildEffect = (value) => {
35
54
 
36
55
  export const buildWriteEffect = (text, options = {}) => {
37
56
  return buildEffect(
38
- buildEmbeddedExpression(
39
- freeze({
40
- verb: 'write',
41
- value: buildEmbeddedExpression(
42
- freeze({ text, options: buildEmbeddedExpression(freeze(options)) }),
43
- ),
44
- }),
45
- ),
57
+ freeze({
58
+ verb: 'write',
59
+ value: buildEmbeddedExpression(
60
+ freeze({ text, options: buildEmbeddedExpression(freeze(options)) }),
61
+ ),
62
+ }),
46
63
  );
47
64
  };
48
65
 
49
66
  export const buildAnsiPushEffect = (spans = '') => {
50
67
  return buildEffect(
51
- buildEmbeddedExpression(
52
- freeze({
53
- verb: 'ansi-push',
54
- value: buildEmbeddedExpression(
55
- freeze({ spans: spans === '' ? freeze([]) : freeze(spans.split(' ')) }),
56
- ),
57
- }),
58
- ),
68
+ freeze({
69
+ verb: 'ansi-push',
70
+ value: buildEmbeddedExpression(
71
+ freeze({ spans: spans === '' ? freeze([]) : freeze(spans.split(' ')) }),
72
+ ),
73
+ }),
59
74
  );
60
75
  };
61
76
 
62
77
  export const buildAnsiPopEffect = () => {
63
- return buildEffect(buildEmbeddedExpression(freeze({ verb: 'ansi-pop', value: undefined })));
78
+ return buildEffect(freeze({ verb: 'ansi-pop', value: undefined }));
64
79
  };
65
80
 
66
81
  export const buildTokenGroup = (tokens) => {
@@ -75,8 +90,8 @@ export const buildBeginningOfStreamToken = () => {
75
90
  return freeze({ type: Symbol.for('@bablr/beginning-of-stream'), value: undefined });
76
91
  };
77
92
 
78
- export const buildReferenceTag = (name, isArray = false) => {
79
- return freeze({ type: ReferenceTag, value: freeze({ name, isArray }) });
93
+ export const buildReferenceTag = (name, isArray = false, hasGap = false) => {
94
+ return freeze({ type: ReferenceTag, value: freeze({ name, isArray, hasGap }) });
80
95
  };
81
96
 
82
97
  export const buildNullTag = () => {
@@ -99,7 +114,7 @@ export const buildEmbeddedNode = (node) => {
99
114
  return freeze({ type: EmbeddedNode, value: node });
100
115
  };
101
116
 
102
- export const buildDoctypeTag = (attributes) => {
117
+ export const buildDoctypeTag = (attributes = {}) => {
103
118
  return freeze({
104
119
  type: DoctypeTag,
105
120
  value: { doctype: 'cstml', version: 0, attributes: freeze(attributes) },
@@ -107,18 +122,10 @@ export const buildDoctypeTag = (attributes) => {
107
122
  };
108
123
 
109
124
  export const buildNodeOpenTag = (flags = {}, language = null, type = null, attributes = {}) => {
110
- let { token, trivia, escape, expression, intrinsic } = flags;
111
-
112
- token = !!token;
113
- trivia = !!trivia;
114
- escape = !!escape;
115
- expression = !!expression;
116
- intrinsic = !!intrinsic;
117
-
118
125
  return freeze({
119
126
  type: OpenNodeTag,
120
127
  value: freeze({
121
- flags: freeze({ token, trivia, escape, intrinsic, expression }),
128
+ flags: freeze(flags),
122
129
  language,
123
130
  type,
124
131
  attributes,
@@ -130,6 +137,23 @@ export const buildNodeCloseTag = (type = null, language = null) => {
130
137
  return freeze({ type: CloseNodeTag, value: freeze({ language, type }) });
131
138
  };
132
139
 
140
+ export const buildFragmentOpenTag = (flags = {}) => {
141
+ return freeze({
142
+ type: OpenFragmentTag,
143
+ value: freeze({
144
+ flags: freeze(flags),
145
+ }),
146
+ });
147
+ };
148
+
149
+ export const buildFragmentCloseTag = (type = null, language = null) => {
150
+ return freeze({ type: CloseFragmentTag, value: freeze({ language, type }) });
151
+ };
152
+
153
+ export const wrapFragment = (node) => {
154
+ return buildFragment([buildReferenceTag('.')], { '.': node });
155
+ };
156
+
133
157
  const isString = (val) => typeof val === 'string';
134
158
 
135
159
  export const buildLiteralTag = (value) => {
@@ -145,7 +169,7 @@ export const buildNodeWithFlags = (
145
169
  properties = {},
146
170
  attributes = {},
147
171
  ) => {
148
- const openTag = buildNodeOpenTag(nodeFlags, language, type, attributes);
172
+ const openTag = buildNodeOpenTag(flags, language, type, attributes);
149
173
  const closeTag = buildNodeCloseTag(type);
150
174
 
151
175
  return freeze({
@@ -158,61 +182,84 @@ export const buildNodeWithFlags = (
158
182
  });
159
183
  };
160
184
 
185
+ const flagsWithGap = new WeakMap();
186
+
187
+ export const getFlagsWithGap = (flags) => flagsWithGap.get(flags);
188
+
161
189
  export const nodeFlags = freeze({
162
190
  token: false,
163
191
  escape: false,
164
192
  trivia: false,
165
- intrinsic: false,
166
193
  expression: false,
194
+ hasGap: false,
167
195
  });
168
196
 
197
+ const hasGap = (flags, children, properties) => {
198
+ return find((node) => node.flags.hasGap, relatedNodes(properties));
199
+ };
200
+
201
+ const getGapFlags = (flags) => {
202
+ let gapFlags = flagsWithGap.get(flags);
203
+ if (!gapFlags) {
204
+ gapFlags = { ...flags, hasGap: true };
205
+ flagsWithGap.set(flags, gapFlags);
206
+ }
207
+ return gapFlags;
208
+ };
209
+
210
+ const getFlags = (flags, children, properties) => {
211
+ if (!hasGap(flags, children, properties)) {
212
+ return flags;
213
+ } else {
214
+ return getGapFlags(flags);
215
+ }
216
+ };
217
+
169
218
  export const buildNode = (language, type, children = [], properties = {}, attributes = {}) => {
170
- return buildNodeWithFlags(nodeFlags, language, type, children, properties, attributes);
219
+ const flags = getFlags(nodeFlags, children, properties);
220
+ return buildNodeWithFlags(flags, language, type, children, properties, attributes);
171
221
  };
172
222
 
173
- export const syntacticFlags = freeze({
174
- token: true,
175
- escape: false,
176
- trivia: false,
177
- intrinsic: false,
178
- expression: false,
179
- });
223
+ export const buildFragmentWithFlags = (flags, children = [], properties = {}, attributes = {}) => {
224
+ const doctypeTag = buildDoctypeTag(attributes);
225
+ const openTag = buildFragmentOpenTag(flags);
226
+ const closeTag = buildFragmentCloseTag();
180
227
 
181
- export const buildSyntacticNode = (language, type, value, attributes = {}) => {
182
- return buildNodeWithFlags(
183
- syntacticFlags,
184
- language,
185
- type,
186
- [buildLiteralTag(value)],
187
- {},
188
- attributes,
189
- );
228
+ return freeze({
229
+ flags,
230
+ children: btree.addAt(
231
+ 0,
232
+ btree.addAt(0, btree.addAt(btree.getSum(children), children, closeTag), openTag),
233
+ doctypeTag,
234
+ ),
235
+ properties: freeze(properties),
236
+ attributes: freeze(attributes),
237
+ });
238
+ };
239
+
240
+ export const buildFragment = (children = [], properties = {}, attributes = {}) => {
241
+ const flags = getFlags(nodeFlags, children, properties);
242
+ return buildFragmentWithFlags(flags, children, properties, attributes);
190
243
  };
191
244
 
192
- export const syntacticIntrinsicFlags = freeze({
245
+ export const syntacticFlags = freeze({
193
246
  token: true,
194
247
  escape: false,
195
248
  trivia: false,
196
- intrinsic: true,
197
249
  expression: false,
250
+ hasGap: false,
198
251
  });
199
- export const buildSyntacticIntrinsicNode = (language, type, value, attributes = {}) => {
200
- return buildNodeWithFlags(
201
- syntacticIntrinsicFlags,
202
- language,
203
- type,
204
- [buildLiteralTag(value)],
205
- {},
206
- attributes,
207
- );
252
+
253
+ export const buildSyntacticNode = (language, type, value) => {
254
+ return buildNodeWithFlags(syntacticFlags, language, type, [buildLiteralTag(value)]);
208
255
  };
209
256
 
210
257
  export const escapeFlags = freeze({
211
258
  token: false,
212
259
  escape: true,
213
260
  trivia: false,
214
- intrinsic: false,
215
261
  expression: false,
262
+ hasGap: false,
216
263
  });
217
264
 
218
265
  export const buildEscapeNode = (
@@ -222,15 +269,16 @@ export const buildEscapeNode = (
222
269
  properties = {},
223
270
  attributes = {},
224
271
  ) => {
225
- return buildNodeWithFlags(escapeFlags, language, type, children, properties, attributes);
272
+ const flags = getFlags(escapeFlags, children, properties);
273
+ return buildNodeWithFlags(flags, language, type, children, properties, attributes);
226
274
  };
227
275
 
228
276
  export const syntacticEscapeFlags = freeze({
229
277
  token: true,
230
278
  escape: true,
231
279
  trivia: false,
232
- intrinsic: false,
233
280
  expression: false,
281
+ hasGap: false,
234
282
  });
235
283
 
236
284
  export const buildSyntacticEscapeNode = (
@@ -247,8 +295,8 @@ export const syntacticTriviaFlags = freeze({
247
295
  token: true,
248
296
  escape: false,
249
297
  trivia: true,
250
- intrinsic: false,
251
298
  expression: false,
299
+ hasGap: false,
252
300
  });
253
301
 
254
302
  export const buildSyntacticTriviaNode = (
@@ -265,8 +313,8 @@ export const triviaFlags = freeze({
265
313
  token: false,
266
314
  escape: false,
267
315
  trivia: true,
268
- intrinsic: false,
269
316
  expression: false,
317
+ hasGap: false,
270
318
  });
271
319
 
272
320
  export const buildTriviaNode = (
@@ -276,7 +324,8 @@ export const buildTriviaNode = (
276
324
  properties = {},
277
325
  attributes = {},
278
326
  ) => {
279
- return buildNodeWithFlags(triviaFlags, language, type, children, properties, attributes);
327
+ const flags = getFlags(triviaFlags, children, properties);
328
+ return buildNodeWithFlags(flags, language, type, children, properties, attributes);
280
329
  };
281
330
 
282
331
  export const buildNullNode = (nullToken = buildNullTag()) => {
@@ -292,7 +341,7 @@ export const buildNullNode = (nullToken = buildNullTag()) => {
292
341
 
293
342
  export const buildGapNode = (gapToken = buildGapTag()) => {
294
343
  return freeze({
295
- flags: nodeFlags,
344
+ flags: getGapFlags(nodeFlags),
296
345
  language: null,
297
346
  type: sym.gap,
298
347
  children: btree.freeze([gapToken]),
package/lib/path.js CHANGED
@@ -1,16 +1,26 @@
1
1
  export const parsePath = (str) => {
2
- const isArray = str.endsWith('[]');
3
- const name = isArray ? str.slice(0, -2) : str;
2
+ let name = str;
3
+ const hasGap = name.endsWith('$');
4
4
 
5
- if (!/^[a-zA-Z]+$/.test(name)) throw new Error();
5
+ if (hasGap) name = name.slice(0, -1);
6
6
 
7
- return { name, isArray };
7
+ const isArray = name.endsWith('[]');
8
+
9
+ if (isArray) name = name.slice(0, -2);
10
+
11
+ if (!/^(\.|[a-zA-Z]+)$/.test(name)) throw new Error();
12
+
13
+ const isRoot = name === '.';
14
+
15
+ return { name, hasGap, isArray, isRoot };
8
16
  };
9
17
 
10
18
  export const printPath = (path) => {
11
19
  if (!path) return null;
12
20
 
13
- const { isArray, name } = path;
21
+ const { isArray, isRoot, hasGap, name } = path;
22
+
23
+ if (isRoot && name !== '.') throw new Error();
14
24
 
15
- return `${name}${isArray ? '[]' : ''}`;
25
+ return `${name}${isArray ? '[]' : ''}${hasGap ? '$' : ''}`;
16
26
  };
package/lib/print.js CHANGED
@@ -2,6 +2,8 @@ import {
2
2
  DoctypeTag,
3
3
  OpenNodeTag,
4
4
  CloseNodeTag,
5
+ OpenFragmentTag,
6
+ CloseFragmentTag,
5
7
  ReferenceTag,
6
8
  ShiftTag,
7
9
  GapTag,
@@ -151,10 +153,11 @@ export const printShiftTag = (tag) => {
151
153
  export const printReferenceTag = (tag) => {
152
154
  if (tag?.type !== ReferenceTag) throw new Error();
153
155
 
154
- const { name, isArray } = tag.value;
156
+ const { name, isArray, hasGap } = tag.value;
155
157
  const pathBraces = isArray ? '[]' : '';
158
+ const gapDollar = hasGap ? '$' : '';
156
159
 
157
- return `${name}${pathBraces}:`;
160
+ return `${name}${pathBraces}${gapDollar}:`;
158
161
  };
159
162
 
160
163
  export const printNullTag = (tag) => {
@@ -192,14 +195,14 @@ export const printLiteralTag = (tag) => {
192
195
 
193
196
  export const printFlags = (flags) => {
194
197
  const hash = flags.trivia ? '#' : '';
195
- const tilde = flags.intrinsic ? '~' : '';
196
198
  const star = flags.token ? '*' : '';
197
199
  const at = flags.escape ? '@' : '';
198
200
  const plus = flags.expression ? '+' : '';
201
+ const dollar = flags.hasGap ? '$' : '';
199
202
 
200
203
  if (flags.escape && flags.trivia) throw new Error('Node cannot be escape and trivia');
201
204
 
202
- return `${hash}${tilde}${star}${at}${plus}`;
205
+ return `${hash}${star}${at}${plus}${dollar}`;
203
206
  };
204
207
 
205
208
  export const printOpenNodeTag = (tag) => {
@@ -213,6 +216,14 @@ export const printOpenNodeTag = (tag) => {
213
216
  return `<${printFlags(flags)}${printTagPath(tagLanguage, type)}${attributesFrag}>`;
214
217
  };
215
218
 
219
+ export const printOpenFragmentTag = (tag) => {
220
+ if (tag?.type !== OpenFragmentTag) throw new Error();
221
+
222
+ const { flags } = tag.value;
223
+
224
+ return `<${printFlags(flags)}>`;
225
+ };
226
+
216
227
  export const printSelfClosingNodeTag = (tag, intrinsicValue) => {
217
228
  if (tag?.type !== OpenNodeTag) throw new Error();
218
229
 
@@ -234,6 +245,12 @@ export const printCloseNodeTag = (tag) => {
234
245
  return `</>`;
235
246
  };
236
247
 
248
+ export const printCloseFragmentTag = (tag) => {
249
+ if (tag?.type !== CloseFragmentTag) throw new Error();
250
+
251
+ return `</>`;
252
+ };
253
+
237
254
  export const printTag = (tag) => {
238
255
  if (!isObject(tag)) throw new Error();
239
256
 
@@ -265,6 +282,12 @@ export const printTag = (tag) => {
265
282
  case CloseNodeTag:
266
283
  return printCloseNodeTag(tag);
267
284
 
285
+ case OpenFragmentTag:
286
+ return printOpenFragmentTag(tag);
287
+
288
+ case CloseFragmentTag:
289
+ return printCloseFragmentTag(tag);
290
+
268
291
  default:
269
292
  throw new Error();
270
293
  }
package/lib/shorthand.js CHANGED
@@ -8,14 +8,15 @@ import {
8
8
  buildNullNode,
9
9
  buildArrayTag,
10
10
  buildNode,
11
+ buildFragment,
11
12
  buildGapNode,
12
13
  buildSyntacticNode,
13
- buildSyntacticIntrinsicNode,
14
14
  buildEscapeNode,
15
15
  buildSyntacticEscapeNode,
16
16
  buildSyntacticTriviaNode,
17
17
  buildTriviaNode,
18
18
  } from './builders.js';
19
+ import { parsePath } from './path.js';
19
20
 
20
21
  export * from './builders.js';
21
22
 
@@ -33,14 +34,9 @@ const stripArray = (val) => {
33
34
  };
34
35
 
35
36
  export const ref = (path) => {
36
- if (isArray(path)) {
37
- const pathIsArray = path[0].endsWith('[]');
38
- const name = pathIsArray ? path[0].slice(0, -2) : path[0];
39
- return buildReferenceTag(name, pathIsArray);
40
- } else {
41
- const { name, isArray: pathIsArray } = path;
42
- return buildReferenceTag(name, pathIsArray);
43
- }
37
+ const { name, isArray: pathIsArray } = parsePath(isArray(path) ? path[0] : path);
38
+
39
+ return buildReferenceTag(name, pathIsArray);
44
40
  };
45
41
 
46
42
  export const lit = (str) => buildLiteralTag(stripArray(str));
@@ -51,9 +47,9 @@ export const embedded = buildEmbeddedNode;
51
47
  export const nodeOpen = buildNodeOpenTag;
52
48
  export const nodeClose = buildNodeCloseTag;
53
49
  export const node = buildNode;
50
+ export const frag = buildFragment;
54
51
  export const g_node = buildGapNode;
55
52
  export const s_node = buildSyntacticNode;
56
- export const s_i_node = buildSyntacticIntrinsicNode;
57
53
  export const e_node = buildEscapeNode;
58
54
  export const s_e_node = buildSyntacticEscapeNode;
59
55
  export const s_t_node = buildSyntacticTriviaNode;
package/lib/stream.js CHANGED
@@ -14,6 +14,8 @@ import {
14
14
  LiteralTag,
15
15
  EmbeddedExpression,
16
16
  TokenGroup,
17
+ OpenFragmentTag,
18
+ CloseFragmentTag,
17
19
  } from './symbols.js';
18
20
 
19
21
  export * from './print.js';
@@ -103,10 +105,6 @@ export class AsyncGenerator {
103
105
  }
104
106
  }
105
107
 
106
- export const isIntrinsicToken = (tag) => {
107
- return tag.type === OpenNodeTag && tag.value.flags.intrinsic && tag.value.flags.token;
108
- };
109
-
110
108
  export class StreamGenerator {
111
109
  constructor(embeddedGenerator) {
112
110
  this.generator = embeddedGenerator;
@@ -161,6 +159,51 @@ export const maybeWait = (maybePromise, callback) => {
161
159
  }
162
160
  };
163
161
 
162
+ function* __isEmpty(tags) {
163
+ const co = new Coroutine(getStreamIterator(tags));
164
+
165
+ for (;;) {
166
+ co.advance();
167
+
168
+ if (co.current instanceof Promise) {
169
+ co.current = yield co.current;
170
+ }
171
+ if (co.done) break;
172
+
173
+ let depth = 0;
174
+
175
+ const tag = co.value;
176
+
177
+ switch (tag.type) {
178
+ case OpenFragmentTag:
179
+ case OpenNodeTag:
180
+ if (tag.value.flags.trivia) {
181
+ ++depth;
182
+ }
183
+
184
+ if (depth === 0 && tag.value.flags.escape) {
185
+ return false;
186
+ }
187
+
188
+ break;
189
+
190
+ case CloseFragmentTag:
191
+ case CloseNodeTag:
192
+ --depth;
193
+ break;
194
+
195
+ case LiteralTag:
196
+ case GapTag:
197
+ return false;
198
+ }
199
+ }
200
+
201
+ return true;
202
+ }
203
+
204
+ export const isEmpty = (tags) =>
205
+ new StreamIterable(__isEmpty(tags))[Symbol.iterator]().next().value;
206
+
164
207
  function* __generateStandardOutput(tags) {
165
208
  const co = new Coroutine(getStreamIterator(tags));
166
209
 
@@ -175,7 +218,7 @@ function* __generateStandardOutput(tags) {
175
218
  const tag = co.value;
176
219
 
177
220
  if (tag.type === 'Effect') {
178
- const effect = getEmbeddedExpression(tag.value);
221
+ const effect = tag.value;
179
222
  if (effect.verb === 'write') {
180
223
  const writeEffect = getEmbeddedExpression(effect.value);
181
224
  if (writeEffect.stream == null || writeEffect.stream === 1) {
@@ -204,7 +247,7 @@ function* __generateAllOutput(tags) {
204
247
  const tag = co.value;
205
248
 
206
249
  if (tag.type === 'Effect') {
207
- const effect = getEmbeddedExpression(tag.value);
250
+ const effect = tag.value;
208
251
  if (effect.verb === 'write') {
209
252
  const writeEffect = getEmbeddedExpression(effect.value);
210
253
  const prevStream = currentStream;
@@ -316,6 +359,10 @@ function* __generateCSTMLStrategy(tags, options) {
316
359
  export const generateCSTMLStrategy = (tags, options = {}) =>
317
360
  new StreamIterable(__generateCSTMLStrategy(tags, options));
318
361
 
362
+ const isToken = (tag) => {
363
+ return tag.value.flags.token;
364
+ };
365
+
319
366
  export const prettyGroupTokens = (tags) => new StreamIterable(__prettyGroupTokens(tags));
320
367
 
321
368
  function* __prettyGroupTokens(tags) {
@@ -334,15 +381,18 @@ function* __prettyGroupTokens(tags) {
334
381
  }
335
382
 
336
383
  const tag = co.value;
337
- const isOpenClose = tag.type === CloseNodeTag || (tag.type === OpenNodeTag && tag.value.type);
384
+ const isOpenClose =
385
+ tag.type === CloseNodeTag ||
386
+ tag.type === OpenNodeTag ||
387
+ tag.type === CloseFragmentTag ||
388
+ tag.type === OpenFragmentTag;
338
389
 
339
390
  if (
340
391
  (tag.type === 'Effect' && tag.value.verb === 'write') ||
341
- [ReferenceTag, DoctypeTag, GapTag, NullTag, ArrayTag, ShiftTag].includes(tag.type) ||
342
- (tag.type === OpenNodeTag && !tag.value.type) ||
343
- (state.open &&
344
- !isIntrinsicToken(state.open) &&
345
- (tag.type === LiteralTag || (tag.type === OpenNodeTag && tag.value.flags.escape)))
392
+ [ReferenceTag, DoctypeTag, GapTag, NullTag, ArrayTag, ShiftTag, OpenFragmentTag].includes(
393
+ tag.type,
394
+ ) ||
395
+ (tag.type === OpenNodeTag && tag.value.flags.escape)
346
396
  ) {
347
397
  state.broken = true;
348
398
 
@@ -350,20 +400,16 @@ function* __prettyGroupTokens(tags) {
350
400
  yield* state.holding;
351
401
  state.holding = [];
352
402
  }
353
-
354
- if (!isOpenClose && tag.type !== 'Effect') {
355
- yield tag;
356
- }
357
- } else if (!isOpenClose && tag.type !== 'Effect') {
403
+ } else if (tag.type === LiteralTag) {
358
404
  state.holding.push(tag);
359
405
  }
360
406
 
361
- if (tag.type === 'Effect') {
407
+ if (!state.holding.length && !isOpenClose) {
362
408
  yield tag;
363
409
  }
364
410
 
365
- if (tag.type === CloseNodeTag) {
366
- if (!state.broken && (isIntrinsicToken(state.open) || state.holding.length === 1)) {
411
+ if (tag.type === CloseNodeTag || tag.type === CloseFragmentTag) {
412
+ if (!state.broken && (isToken(state.open) || state.holding.length === 1)) {
367
413
  state.holding.push(tag);
368
414
  yield buildTokenGroup(state.holding);
369
415
  } else {
@@ -377,8 +423,14 @@ function* __prettyGroupTokens(tags) {
377
423
  state = states.value;
378
424
  }
379
425
 
380
- if (tag.type === OpenNodeTag && tag.value.type) {
381
- states = states.push({ holding: [tag], broken: false, open: tag });
426
+ if (tag.type === OpenNodeTag || tag.type === OpenFragmentTag) {
427
+ if (tag.type === OpenFragmentTag) {
428
+ states = states.push({ holding: [], broken: false, open: tag });
429
+ yield tag;
430
+ } else {
431
+ states = states.push({ holding: [tag], broken: false, open: tag });
432
+ }
433
+
382
434
  state = states.value;
383
435
  }
384
436
  }
@@ -396,6 +448,7 @@ function* __generatePrettyCSTMLStrategy(tags, options) {
396
448
  let indentLevel = 0;
397
449
  let first = true;
398
450
  let inline = false;
451
+ let ref = null;
399
452
 
400
453
  for (;;) {
401
454
  co.advance();
@@ -409,7 +462,7 @@ function* __generatePrettyCSTMLStrategy(tags, options) {
409
462
  const tag = co.value;
410
463
 
411
464
  if (tag.type === 'Effect') {
412
- const effect = getEmbeddedExpression(tag.value);
465
+ const effect = tag.value;
413
466
  if (emitEffects && effect.verb === 'write') {
414
467
  const writeEffect = getEmbeddedExpression(effect.value);
415
468
  yield buildWriteEffect(
@@ -426,6 +479,7 @@ function* __generatePrettyCSTMLStrategy(tags, options) {
426
479
  inline =
427
480
  inlineOption &&
428
481
  inline &&
482
+ ref &&
429
483
  (tag.type === NullTag ||
430
484
  tag.type === GapTag ||
431
485
  tag.type === ArrayTag ||
@@ -435,7 +489,8 @@ function* __generatePrettyCSTMLStrategy(tags, options) {
435
489
  yield buildWriteEffect('\n');
436
490
  }
437
491
 
438
- if (tag.type === CloseNodeTag) {
492
+ if (tag.type === CloseNodeTag || tag.type === CloseFragmentTag) {
493
+ ref = null;
439
494
  if (indentLevel === 0) {
440
495
  throw new Error('imbalanced tag stack');
441
496
  }
@@ -450,6 +505,7 @@ function* __generatePrettyCSTMLStrategy(tags, options) {
450
505
  }
451
506
 
452
507
  if (tag.type === TokenGroup) {
508
+ ref = null;
453
509
  const intrinsicValue = tag.value[0].value.flags.token ? getCooked(tag.value) : null;
454
510
  yield buildWriteEffect(printSelfClosingNodeTag(tag.value[0], intrinsicValue));
455
511
  } else {
@@ -458,9 +514,10 @@ function* __generatePrettyCSTMLStrategy(tags, options) {
458
514
 
459
515
  if (tag.type === ReferenceTag) {
460
516
  inline = true;
517
+ ref = tag;
461
518
  }
462
519
 
463
- if (tag.type === OpenNodeTag) {
520
+ if (tag.type === OpenNodeTag || tag.type === OpenFragmentTag) {
464
521
  indentLevel++;
465
522
  }
466
523
 
@@ -496,6 +553,7 @@ export const getCooked = (tags) => {
496
553
  break;
497
554
  }
498
555
 
556
+ case OpenFragmentTag:
499
557
  case OpenNodeTag: {
500
558
  const { flags, attributes } = tag.value;
501
559
 
@@ -524,6 +582,7 @@ export const getCooked = (tags) => {
524
582
  break;
525
583
  }
526
584
 
585
+ case CloseFragmentTag:
527
586
  case CloseNodeTag: {
528
587
  if (depth === 1) {
529
588
  foundLast = true;
package/lib/symbols.js CHANGED
@@ -9,6 +9,8 @@ export const null_ = Symbol.for('@bablr/null');
9
9
  export const DoctypeTag = Symbol.for('DoctypeTag');
10
10
  export const OpenNodeTag = Symbol.for('OpenNodeTag');
11
11
  export const CloseNodeTag = Symbol.for('CloseNodeTag');
12
+ export const OpenFragmentTag = Symbol.for('OpenFragmentTag');
13
+ export const CloseFragmentTag = Symbol.for('CloseFragmentTag');
12
14
  export const ReferenceTag = Symbol.for('ReferenceTag');
13
15
  export const ShiftTag = Symbol.for('ShiftTag');
14
16
  export const GapTag = Symbol.for('GapTag');
package/lib/template.js CHANGED
@@ -1,12 +1,24 @@
1
1
  import * as t from './builders.js';
2
- import { LiteralTag, EmbeddedNode } from './symbols.js';
2
+ import {
3
+ LiteralTag,
4
+ EmbeddedNode,
5
+ ReferenceTag,
6
+ ArrayTag,
7
+ OpenFragmentTag,
8
+ OpenNodeTag,
9
+ DoctypeTag,
10
+ CloseFragmentTag,
11
+ } from './symbols.js';
3
12
  import * as btree from './btree.js';
13
+ import { getOpenTag, getRoot } from './tree.js';
4
14
 
5
15
  const { isArray } = Array;
6
16
  const { freeze } = Object;
7
17
  const { isFinite } = Number;
8
18
 
9
- export const interpolateArray = (value) => {
19
+ export const interpolateArray = (fragment) => {
20
+ const value = getRoot(fragment);
21
+
10
22
  if (isArray(value)) {
11
23
  if (isFinite(value[0])) {
12
24
  return [...btree.traverse(value)];
@@ -18,21 +30,41 @@ export const interpolateArray = (value) => {
18
30
  }
19
31
  };
20
32
 
21
- export const interpolateArrayChildren = (value, ref, sep) => {
22
- if (isArray(value)) {
23
- const values = value;
24
- const children = [];
25
- let first = true;
26
- for (const _ of values) {
27
- if (!first) children.push(freeze({ ...sep }));
28
- children.push(freeze({ ...ref }));
29
- first = false;
33
+ export function* interpolateFragmentChildren(value, ref) {
34
+ const open = getOpenTag(value);
35
+
36
+ if (open.type === OpenFragmentTag) {
37
+ let currentRef = null;
38
+ for (let child of btree.traverse(value.children)) {
39
+ if (
40
+ child.type === DoctypeTag ||
41
+ child.type === OpenFragmentTag ||
42
+ child.type === CloseFragmentTag
43
+ ) {
44
+ continue;
45
+ }
46
+
47
+ if (child.type === ArrayTag) {
48
+ // if (notAlreadyInitialized) {
49
+ yield child;
50
+ // }
51
+ } else if (child.type === ReferenceTag) {
52
+ currentRef = child;
53
+ if (child.value.name === '.') {
54
+ yield freeze({ ...ref });
55
+ } else {
56
+ yield child;
57
+ }
58
+ } else {
59
+ yield child;
60
+ }
30
61
  }
31
- return children;
62
+ } else if (open.type === OpenNodeTag) {
63
+ yield freeze({ ...ref });
32
64
  } else {
33
- return [freeze({ ...ref })];
65
+ throw new Error();
34
66
  }
35
- };
67
+ }
36
68
 
37
69
  const validateTag = (tag) => {
38
70
  if (!tag || (tag.type !== LiteralTag && tag.type !== EmbeddedNode)) {
package/lib/tree.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Coroutine } from '@bablr/coroutine';
2
2
  import emptyStack from '@iter-tools/imm-stack';
3
- import { nodeFlags, buildDoctypeTag, buildEmbeddedNode } from './builders.js';
3
+ import { nodeFlags, buildEmbeddedNode } from './builders.js';
4
4
  import {
5
5
  printPrettyCSTML as printPrettyCSTMLFromStream,
6
6
  printCSTML as printCSTMLFromStream,
@@ -11,6 +11,8 @@ import {
11
11
  DoctypeTag,
12
12
  OpenNodeTag,
13
13
  CloseNodeTag,
14
+ OpenFragmentTag,
15
+ CloseFragmentTag,
14
16
  ReferenceTag,
15
17
  ShiftTag,
16
18
  GapTag,
@@ -33,11 +35,11 @@ const { isArray } = Array;
33
35
  const { hasOwn, freeze } = Object;
34
36
 
35
37
  export const get = (node, path) => {
36
- const { type, properties } = node;
37
- const { 1: name, 2: index } = /^([^\.]+)(?:\.(\d+))?/.exec(path) || [];
38
+ const { properties } = node;
39
+ const { 1: name, 2: index } = /^([^\.]+|\.)(?:\.(\d+))?/.exec(path) || [];
38
40
 
39
41
  if (!hasOwn(properties, name)) {
40
- throw new Error(`Cannot find {name: ${name}} on node of {type: ${type}}`);
42
+ return null;
41
43
  }
42
44
 
43
45
  if (index != null) {
@@ -88,14 +90,15 @@ function* __treeFromStream(tokens) {
88
90
  continue;
89
91
  }
90
92
 
91
- if (held && tag.type !== 'StartNodeTag' && tag.type !== GapTag) {
93
+ if (held && tag.type !== OpenNodeTag && tag.type !== GapTag) {
92
94
  throw new Error('cannot eat this type of tag while holding');
93
95
  }
94
96
 
95
97
  switch (tag.type) {
96
98
  case LiteralTag:
97
99
  case ReferenceTag:
98
- case CloseNodeTag: {
100
+ case CloseNodeTag:
101
+ case CloseFragmentTag: {
99
102
  break;
100
103
  }
101
104
 
@@ -134,10 +137,7 @@ function* __treeFromStream(tokens) {
134
137
  }
135
138
 
136
139
  case OpenNodeTag: {
137
- const { flags, type } = tag.value;
138
-
139
- const language = type ? tag.value.language : doctype.value.attributes['bablr-language'];
140
- const attributes = type ? tag.value.attributes : doctype.value.attributes;
140
+ const { flags, type, language, attributes } = tag.value;
141
141
 
142
142
  const node = createNode(flags, language, type, [], {}, attributes);
143
143
 
@@ -145,26 +145,48 @@ function* __treeFromStream(tokens) {
145
145
 
146
146
  path = { parent: path, node, depth: (path.depth || -1) + 1 };
147
147
 
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
- }
148
+ if (!parentPath) throw new Error();
154
149
 
155
- const ref = arrayLast(parentNode.children);
156
-
157
- add(parentNode, ref, node);
158
- } else {
159
- parentNode.children.push(buildEmbeddedNode(node));
150
+ const { node: parentNode } = path;
151
+ if (!(flags.escape || flags.trivia)) {
152
+ if (!parentNode.children.length) {
153
+ throw new Error('Nodes must follow references');
160
154
  }
155
+
156
+ const ref = arrayLast(parentNode.children);
157
+
158
+ add(parentNode, ref, node);
161
159
  } else {
162
- rootPath = path;
160
+ parentNode.children.push(buildEmbeddedNode(node));
163
161
  }
164
162
 
165
163
  break;
166
164
  }
167
165
 
166
+ case OpenFragmentTag: {
167
+ const { flags } = tag.value;
168
+
169
+ const language = doctype.value.attributes['bablr-language'];
170
+ const attributes = doctype.value.attributes;
171
+
172
+ const node = freeze({
173
+ flags,
174
+ language,
175
+ type: null,
176
+ children: [],
177
+ properties: {},
178
+ attributes,
179
+ });
180
+
181
+ if (path) throw new Error();
182
+
183
+ path = { parent: null, node, depth: 0 };
184
+
185
+ rootPath = path;
186
+
187
+ break;
188
+ }
189
+
168
190
  default: {
169
191
  throw new Error();
170
192
  }
@@ -193,7 +215,7 @@ function* __treeFromStream(tokens) {
193
215
  return rootPath.node;
194
216
  }
195
217
 
196
- export const treeFromStream = (tags) => new StreamIterable(__treeFromStream(tags));
218
+ export const treeFromStream = (tags) => __treeFromStream(tags);
197
219
 
198
220
  export const treeFromStreamSync = (tokens) => {
199
221
  return evaluateReturnSync(treeFromStream(tokens));
@@ -223,13 +245,64 @@ export const evaluateReturnAsync = async (generator) => {
223
245
 
224
246
  export const streamFromTree = (rootNode) => __streamFromTree(rootNode);
225
247
 
248
+ export const isEmpty = (node) => {
249
+ const { properties } = node;
250
+
251
+ for (const child of btree.traverse(node.children)) {
252
+ switch (child.type) {
253
+ case ReferenceTag: {
254
+ const { name } = child.value;
255
+
256
+ if (properties[name]) {
257
+ const value = properties[name];
258
+
259
+ if (value != null || value.type !== sym.null || (isArray(value) && value.length)) {
260
+ return false;
261
+ }
262
+ }
263
+ break;
264
+ }
265
+
266
+ case EmbeddedNode: {
267
+ if (node.value.flags.escape) {
268
+ return false;
269
+ }
270
+ break;
271
+ }
272
+
273
+ case LiteralTag:
274
+ case GapTag:
275
+ return false;
276
+ }
277
+ }
278
+ return true;
279
+ };
280
+
281
+ const symbolTypeFor = (type) => {
282
+ // prettier-ignore
283
+ switch (type) {
284
+ case NullTag: return sym.null;
285
+ case GapTag: return sym.gap;
286
+ default: throw new Error();
287
+ }
288
+ };
289
+
290
+ export const buildStubNode = (tag) => {
291
+ return freeze({
292
+ flags: nodeFlags,
293
+ language: null,
294
+ type: symbolTypeFor(tag.type),
295
+ children: freeze([tag]),
296
+ properties: freeze({}),
297
+ attributes: freeze({}),
298
+ });
299
+ };
300
+
226
301
  function* __streamFromTree(rootNode) {
227
302
  if (!rootNode || rootNode.type === GapTag) {
228
303
  return rootNode;
229
304
  }
230
305
 
231
- yield buildDoctypeTag(rootNode.attributes);
232
-
233
306
  let stack = emptyStack.push(rootNode);
234
307
  const resolver = new Resolver();
235
308
 
@@ -273,7 +346,8 @@ function* __streamFromTree(rootNode) {
273
346
 
274
347
  case GapTag:
275
348
  case NullTag:
276
- case CloseNodeTag: {
349
+ case CloseNodeTag:
350
+ case CloseFragmentTag: {
277
351
  stack = stack.pop();
278
352
  resolver.advance(tag);
279
353
  yield tag;
@@ -397,8 +471,9 @@ export const printSource = (rootNode) => __printSource(rootNode);
397
471
  export const sourceTextFor = printSource;
398
472
 
399
473
  export const getOpenTag = (node) => {
400
- const tag = btree.getAt(0, node.children);
401
- if (tag && tag.type !== OpenNodeTag) throw new Error();
474
+ const tag = btree.getAt(node.type ? 0 : 1, node.children);
475
+ if (tag.type === NullTag) return null;
476
+ if (tag && tag.type !== OpenNodeTag && tag.type !== OpenFragmentTag) throw new Error();
402
477
  return tag;
403
478
  };
404
479
 
@@ -473,14 +548,8 @@ export const acceptNode = (node, accepted) => {
473
548
  return node;
474
549
  };
475
550
 
476
- export const getRoot = (fragmentNode) => {
477
- if (!fragmentNode) return null;
478
-
479
- for (const tag of btree.traverse(fragmentNode.children)) {
480
- if (tag.type === ReferenceTag) {
481
- return fragmentNode.properties[tag.value.name];
482
- }
483
- }
551
+ export const getRoot = (node) => {
552
+ return node.type == null ? node.properties['.'] : node;
484
553
  };
485
554
 
486
555
  export class Resolver {
@@ -494,6 +563,7 @@ export class Resolver {
494
563
  this.reference = reference;
495
564
  this.popped = popped;
496
565
  this.held = held;
566
+ this.doctype = null;
497
567
  }
498
568
 
499
569
  get idx() {
@@ -552,16 +622,23 @@ export class Resolver {
552
622
  break;
553
623
  }
554
624
 
555
- case OpenNodeTag: {
625
+ case OpenNodeTag:
626
+ case OpenFragmentTag: {
556
627
  const { flags } = tag.value;
557
628
  const isRootNode = states.size === 1;
558
629
 
559
- if (!isRootNode && !this.reference && !(flags.trivia || flags.escape)) {
560
- throw new Error();
561
- }
630
+ if (tag.type === OpenFragmentTag && (!isRootNode || this.reference)) throw new Error();
562
631
 
563
- if (this.reference?.type !== EmbeddedNode && (flags.trivia || flags.escape)) {
564
- this.states = states.push({ properties: new Map(), idx: 0 });
632
+ if (flags.trivia || flags.escape) {
633
+ if (this.reference?.type === ReferenceTag)
634
+ throw new Error('embedded nodes cannot follow references');
635
+ if (this.reference?.type !== EmbeddedNode) {
636
+ this.states = states.push({ properties: new Map(), idx: 0 });
637
+ }
638
+ } else {
639
+ if (!isRootNode && !this.reference) {
640
+ throw new Error();
641
+ }
565
642
  }
566
643
 
567
644
  this.reference = null;
@@ -611,10 +688,21 @@ export class Resolver {
611
688
  break;
612
689
  }
613
690
 
614
- case CloseNodeTag: {
691
+ case CloseNodeTag:
692
+ case CloseFragmentTag: {
615
693
  this.states = states.pop();
616
694
  this.popped = true;
695
+ break;
617
696
  }
697
+
698
+ case DoctypeTag:
699
+ this.doctype = tag;
700
+ break;
701
+ case LiteralTag:
702
+ break;
703
+
704
+ default:
705
+ throw new Error();
618
706
  }
619
707
 
620
708
  return this;
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.4.0",
4
+ "version": "0.5.0",
5
5
  "author": "Conrad Buck<conartist6@gmail.com>",
6
6
  "type": "module",
7
7
  "files": [