/ lib / stream.js
stream.js
  1  import { Coroutine } from '@bablr/coroutine';
  2  import emptyStack from '@iter-tools/imm-stack';
  3  import { printSelfClosingNodeTag, printTag } from './print.js';
  4  import { buildTokenGroup, buildWriteEffect } from './builders.js';
  5  import {
  6    DoctypeTag,
  7    OpenNodeTag,
  8    CloseNodeTag,
  9    ReferenceTag,
 10    ShiftTag,
 11    GapTag,
 12    NullTag,
 13    ArrayInitializerTag,
 14    LiteralTag,
 15    EmbeddedObject,
 16    TokenGroup,
 17  } from './symbols.js';
 18  
 19  export * from './print.js';
 20  
 21  const getEmbeddedObject = (obj) => {
 22    if (obj.type !== EmbeddedObject) throw new Error();
 23    return obj.value;
 24  };
 25  
 26  export const getStreamIterator = (obj) => {
 27    return obj[Symbol.for('@@streamIterator')]?.() || obj[Symbol.iterator]?.();
 28  };
 29  
 30  export class SyncGenerator {
 31    constructor(embeddedGenerator) {
 32      if (!embeddedGenerator.next) throw new Error();
 33  
 34      this.generator = embeddedGenerator;
 35    }
 36  
 37    next(value) {
 38      const step = this.generator.next(value);
 39  
 40      if (step instanceof Promise) {
 41        throw new Error('invalid embedded generator');
 42      }
 43  
 44      if (step.done) {
 45        return step;
 46      } else if (step.value instanceof Promise) {
 47        throw new Error('sync generators cannot resolve promises');
 48      } else {
 49        return step;
 50      }
 51    }
 52  
 53    return(value) {
 54      const step = this.generator.return(value);
 55      if (step instanceof Promise) {
 56        throw new Error('invalid embedded generator');
 57      }
 58  
 59      if (step.value instanceof Promise) {
 60        throw new Error('sync generators cannot resolve promises');
 61      }
 62      return step;
 63    }
 64  
 65    [Symbol.iterator]() {
 66      return this;
 67    }
 68  }
 69  
 70  export class AsyncGenerator {
 71    constructor(embeddedGenerator) {
 72      this.generator = embeddedGenerator;
 73    }
 74  
 75    next(value) {
 76      const step = this.generator.next(value);
 77  
 78      if (step instanceof Promise) {
 79        throw new Error('invalid embedded generator');
 80      }
 81  
 82      if (step.done) {
 83        return Promise.resolve(step);
 84      } else if (step.value instanceof Promise) {
 85        return step.value.then((value) => {
 86          return this.next(value);
 87        });
 88      } else {
 89        return Promise.resolve(step);
 90      }
 91    }
 92  
 93    return(value) {
 94      const result = this.generator.return(value);
 95      if (result instanceof Promise) {
 96        throw new Error('sync generators cannot resolve promises');
 97      }
 98      return result;
 99    }
100  
101    [Symbol.asyncIterator]() {
102      return this;
103    }
104  }
105  
106  export class StreamGenerator {
107    constructor(embeddedGenerator) {
108      this.generator = embeddedGenerator;
109    }
110  
111    next(value) {
112      const step = this.generator.next(value);
113  
114      if (step.done) {
115        return step;
116      } else if (step.value instanceof Promise) {
117        return step.value.then((value) => {
118          return this.next(value);
119        });
120      } else {
121        return step;
122      }
123    }
124  
125    return(value) {
126      return this.generator.return(value);
127    }
128  
129    [Symbol.for('@@streamIterator')]() {
130      return this;
131    }
132  }
133  
134  export class StreamIterable {
135    constructor(embeddedStreamIterable) {
136      this.iterable = embeddedStreamIterable;
137    }
138  
139    [Symbol.iterator]() {
140      return new SyncGenerator(this.iterable);
141    }
142  
143    [Symbol.asyncIterator]() {
144      return new AsyncGenerator(this.iterable);
145    }
146  
147    [Symbol.for('@@streamIterator')]() {
148      return new StreamGenerator(this.iterable);
149    }
150  }
151  
152  export const maybeWait = (maybePromise, callback) => {
153    if (maybePromise instanceof Promise) {
154      return maybePromise.then(callback);
155    } else {
156      return callback(maybePromise);
157    }
158  };
159  
160  function* __isEmpty(tags) {
161    const co = new Coroutine(getStreamIterator(tags));
162  
163    for (;;) {
164      co.advance();
165  
166      if (co.current instanceof Promise) {
167        co.current = yield co.current;
168      }
169      if (co.done) break;
170  
171      let depth = 0;
172      let ref = null;
173  
174      const tag = co.value;
175  
176      switch (tag.type) {
177        case ReferenceTag:
178          ref = tag;
179          break;
180  
181        case OpenNodeTag:
182          ++depth;
183  
184          if (depth === 0 && ref.value.name === '@') {
185            return false;
186          }
187  
188          break;
189  
190        case CloseNodeTag:
191          --depth;
192          break;
193  
194        case LiteralTag:
195        case GapTag:
196          return false;
197      }
198    }
199  
200    return true;
201  }
202  
203  export const isEmpty = (tags) =>
204    new StreamIterable(__isEmpty(tags))[Symbol.iterator]().next().value;
205  
206  function* __generateStandardOutput(tags) {
207    const co = new Coroutine(getStreamIterator(tags));
208  
209    for (;;) {
210      co.advance();
211  
212      if (co.current instanceof Promise) {
213        co.current = yield co.current;
214      }
215      if (co.done) break;
216  
217      const tag = co.value;
218  
219      if (tag.type === 'Effect') {
220        const effect = tag.value;
221        if (effect.verb === 'write') {
222          const writeEffect = getEmbeddedObject(effect.value);
223          if (writeEffect.stream == null || writeEffect.stream === 1) {
224            yield* writeEffect.text;
225          }
226        }
227      }
228    }
229  }
230  
231  export const generateStandardOutput = (tags) => new StreamIterable(__generateStandardOutput(tags));
232  
233  function* __generateAllOutput(tags) {
234    const co = new Coroutine(getStreamIterator(tags));
235  
236    let currentStream = null;
237  
238    for (;;) {
239      co.advance();
240  
241      if (co.current instanceof Promise) {
242        co.current = yield co.current;
243      }
244      if (co.done) break;
245  
246      const tag = co.value;
247  
248      if (tag.type === 'Effect') {
249        const effect = tag.value;
250        if (effect.verb === 'write') {
251          const writeEffect = getEmbeddedObject(effect.value);
252          const prevStream = currentStream;
253          currentStream = getEmbeddedObject(writeEffect.options).stream || 1;
254          if (
255            prevStream &&
256            (prevStream !== currentStream || currentStream === 2) &&
257            !writeEffect.text.startsWith('\n')
258          ) {
259            yield* '\n';
260          }
261          yield* writeEffect.text;
262        }
263      }
264    }
265  }
266  
267  export const generateAllOutput = (tags) => new StreamIterable(__generateAllOutput(tags));
268  
269  export const printCSTML = (tags) => {
270    return stringFromStream(generateStandardOutput(generateCSTML(tags)));
271  };
272  
273  function* __emptyStreamIterator() {}
274  
275  export const emptyStreamIterator = () => new StreamIterable(__emptyStreamIterator());
276  
277  export const asyncStringFromStream = async (stream) => {
278    const co = new Coroutine(getStreamIterator(stream));
279    let str = '';
280  
281    for (;;) {
282      co.advance();
283  
284      if (co.current instanceof Promise) {
285        co.current = await co.current;
286      }
287  
288      if (co.done) break;
289  
290      const tag = co.value;
291  
292      str += printTag(tag);
293    }
294  
295    return str;
296  };
297  
298  export const stringFromStream = (stream) => {
299    const co = new Coroutine(stream[Symbol.iterator]());
300    let str = '';
301  
302    for (;;) {
303      co.advance();
304  
305      if (co.done) break;
306  
307      const chr = co.value;
308  
309      str += chr;
310    }
311  
312    return str;
313  };
314  
315  function* __generateCSTML(tags, options) {
316    if (!tags) {
317      yield* '<//>';
318      return;
319    }
320  
321    let prevTag = null;
322  
323    const co = new Coroutine(getStreamIterator(prettyGroupTags(tags)));
324  
325    for (;;) {
326      co.advance();
327  
328      if (co.current instanceof Promise) {
329        co.current = yield co.current;
330      }
331      if (co.done) break;
332  
333      const tag = co.value;
334  
335      if (tag.type === ReferenceTag && prevTag.type === NullTag) {
336        yield* ' ';
337      }
338  
339      if (tag.type === 'Effect') {
340        continue;
341      }
342  
343      if (tag.type === TokenGroup) {
344        const intrinsicValue = getCooked(tag.value);
345        yield* printSelfClosingNodeTag(tag.value[0], intrinsicValue);
346      } else {
347        yield* printTag(tag);
348      }
349  
350      prevTag = tag;
351    }
352  
353    yield* '\n';
354  }
355  
356  export const generateCSTML = (tags, options = {}) =>
357    new StreamIterable(__generateCSTML(tags, options));
358  
359  const isToken = (tag) => {
360    return tag.value.flags.token;
361  };
362  
363  export const prettyGroupTags = (tags) => new StreamIterable(__prettyGroupTags(tags));
364  
365  function* __prettyGroupTags(tags) {
366    let states = emptyStack.push({ holding: [], broken: false, open: null });
367    let state = states.value;
368  
369    const co = new Coroutine(getStreamIterator(tags));
370  
371    let ref = null;
372  
373    for (;;) {
374      co.advance();
375  
376      if (co.done) break;
377  
378      if (co.current instanceof Promise) {
379        co.current = yield co.current;
380      }
381  
382      const tag = co.value;
383      const isOpenClose = tag.type === CloseNodeTag || tag.type === OpenNodeTag;
384  
385      if (tag.type === ReferenceTag) {
386        ref = tag;
387      }
388  
389      if (
390        (tag.type === 'Effect' && tag.value.verb === 'write') ||
391        [ReferenceTag, DoctypeTag, GapTag, NullTag, ArrayInitializerTag, ShiftTag].includes(
392          tag.type,
393        ) ||
394        (tag.type === OpenNodeTag && (!tag.value.type || ref?.value.name === '@'))
395      ) {
396        state.broken = true;
397  
398        if (state.holding.length) {
399          yield* state.holding;
400          state.holding = [];
401        }
402      } else if (tag.type === LiteralTag) {
403        state.holding.push(tag);
404      }
405  
406      if (!state.holding.length && !isOpenClose) {
407        yield tag;
408      }
409  
410      if (tag.type === CloseNodeTag) {
411        if (!state.broken && (isToken(state.open) || state.holding.length === 1)) {
412          state.holding.push(tag);
413          yield buildTokenGroup(state.holding);
414        } else {
415          if (state.holding.length) {
416            yield* state.holding;
417          }
418          yield tag;
419        }
420  
421        states = states.pop();
422        state = states.value;
423      }
424  
425      if (tag.type === OpenNodeTag) {
426        if (!tag.value.type) {
427          states = states.push({ holding: [], broken: false, open: tag });
428          yield tag;
429        } else {
430          states = states.push({ holding: [tag], broken: false, open: tag });
431        }
432  
433        state = states.value;
434      }
435    }
436  }
437  
438  function* __generatePrettyCSTML(tags, options) {
439    let { indent = '  ', inline: inlineOption = true } = options;
440  
441    if (!tags) {
442      yield* '<//>';
443      return;
444    }
445  
446    const co = new Coroutine(getStreamIterator(prettyGroupTags(tags)));
447    let indentLevel = 0;
448    let first = true;
449    let inline = false;
450    let ref = null;
451  
452    for (;;) {
453      co.advance();
454  
455      if (co.done) break;
456  
457      if (co.current instanceof Promise) {
458        co.current = yield co.current;
459      }
460  
461      const tag = co.value;
462  
463      if (tag.type === 'Effect') {
464        continue;
465      }
466  
467      inline =
468        inlineOption &&
469        inline &&
470        ref &&
471        (tag.type === NullTag ||
472          tag.type === GapTag ||
473          tag.type === ArrayInitializerTag ||
474          tag.type === TokenGroup);
475  
476      if (!first && !inline) {
477        yield* '\n';
478      }
479  
480      if (tag.type === CloseNodeTag) {
481        ref = null;
482  
483        indentLevel--;
484      }
485  
486      if (!inline) {
487        yield* indent.repeat(indentLevel);
488      } else {
489        yield* ' ';
490      }
491  
492      if (tag.type === TokenGroup) {
493        ref = null;
494        const intrinsicValue = tag.value[0].value.flags.token ? getCooked(tag.value) : null;
495        yield* printSelfClosingNodeTag(tag.value[0], intrinsicValue);
496      } else {
497        yield* printTag(tag);
498      }
499  
500      if (tag.type === ReferenceTag) {
501        inline = true;
502        ref = tag;
503      }
504  
505      if (tag.type === OpenNodeTag) {
506        indentLevel++;
507      }
508  
509      first = false;
510    }
511  
512    if (indentLevel !== 0) {
513      throw new Error('imbalanced tags');
514    }
515  
516    yield* '\n';
517  }
518  
519  export const generatePrettyCSTML = (tags, options = {}) => {
520    return new StreamIterable(__generatePrettyCSTML(tags, options));
521  };
522  
523  function* __writeCSTMLStrategy(tags) {
524    if (!tags) {
525      yield buildWriteEffect('<//>');
526      return;
527    }
528  
529    let prevTag = null;
530  
531    const co = new Coroutine(getStreamIterator(prettyGroupTags(tags)));
532  
533    for (;;) {
534      co.advance();
535  
536      if (co.current instanceof Promise) {
537        co.current = yield co.current;
538      }
539      if (co.done) break;
540  
541      const tag = co.value;
542  
543      if (tag.type === ReferenceTag && prevTag.type === NullTag) {
544        yield buildWriteEffect(' ');
545      }
546  
547      if (tag.type === 'Effect') {
548        yield tag;
549  
550        continue;
551      }
552  
553      if (tag.type === TokenGroup) {
554        const intrinsicValue = getCooked(tag.value);
555        yield buildWriteEffect(printSelfClosingNodeTag(tag.value[0], intrinsicValue));
556      } else {
557        yield buildWriteEffect(printTag(tag));
558      }
559  
560      prevTag = tag;
561    }
562  
563    yield buildWriteEffect('\n');
564  }
565  
566  export const writeCSTMLStrategy = (tags, options = {}) =>
567    new StreamIterable(__writeCSTMLStrategy(tags, options));
568  
569  function* __writePrettyCSTMLStrategy(tags, options) {
570    let { indent = '  ', emitEffects = false, inline: inlineOption = true } = options;
571  
572    if (!tags) {
573      yield buildWriteEffect('<//>');
574      return;
575    }
576  
577    const co = new Coroutine(getStreamIterator(prettyGroupTags(tags)));
578    let indentLevel = 0;
579    let first = true;
580    let inline = false;
581    let ref = null;
582  
583    for (;;) {
584      co.advance();
585  
586      if (co.done) break;
587  
588      if (co.current instanceof Promise) {
589        co.current = yield co.current;
590      }
591  
592      const tag = co.value;
593  
594      if (tag.type === 'Effect') {
595        const effect = tag.value;
596        if (emitEffects && effect.verb === 'write') {
597          const writeEffect = getEmbeddedObject(effect.value);
598          yield buildWriteEffect(
599            (first ? '' : '\n') + writeEffect.text,
600            getEmbeddedObject(writeEffect.options),
601          );
602  
603          inline = false;
604          first = false;
605        } else {
606          yield tag;
607        }
608        continue;
609      }
610  
611      inline =
612        inlineOption &&
613        inline &&
614        ref &&
615        (tag.type === NullTag ||
616          tag.type === GapTag ||
617          tag.type === ArrayInitializerTag ||
618          tag.type === TokenGroup);
619  
620      if (!first && !inline) {
621        yield buildWriteEffect('\n');
622      }
623  
624      if (tag.type === CloseNodeTag) {
625        ref = null;
626        if (indentLevel === 0) {
627          throw new Error('imbalanced tag stack');
628        }
629  
630        indentLevel--;
631      }
632  
633      if (!inline) {
634        yield buildWriteEffect(indent.repeat(indentLevel));
635      } else {
636        yield buildWriteEffect(' ');
637      }
638  
639      if (tag.type === TokenGroup) {
640        ref = null;
641        const intrinsicValue = tag.value[0].value.flags.token ? getCooked(tag.value) : null;
642        yield buildWriteEffect(printSelfClosingNodeTag(tag.value[0], intrinsicValue));
643      } else {
644        yield buildWriteEffect(printTag(tag));
645      }
646  
647      if (tag.type === ReferenceTag) {
648        inline = true;
649        ref = tag;
650      }
651  
652      if (tag.type === OpenNodeTag) {
653        indentLevel++;
654      }
655  
656      first = false;
657    }
658  
659    yield buildWriteEffect('\n');
660  }
661  
662  export const writePrettyCSTMLStrategy = (tags, options = {}) => {
663    return new StreamIterable(__writePrettyCSTMLStrategy(tags, options));
664  };
665  
666  export const printPrettyCSTML = (tags, options = {}) => {
667    return stringFromStream(generateStandardOutput(generatePrettyCSTML(tags, options)));
668  };
669  
670  export const getCooked = (tags) => {
671    let cooked = '';
672  
673    let first = true;
674    let foundLast = false;
675    let depth = 0;
676    let ref = null;
677  
678    for (const tag of tags) {
679      if (foundLast) throw new Error();
680  
681      switch (tag.type) {
682        case ReferenceTag: {
683          ref = tag;
684          if (depth === 1) {
685            throw new Error('cookable nodes must not contain other nodes');
686          }
687          break;
688        }
689  
690        case OpenNodeTag: {
691          const { flags, attributes } = tag.value;
692  
693          depth++;
694  
695          if (first) {
696            if (flags.token) {
697              break;
698            } else {
699              throw new Error(JSON.stringify(flags));
700            }
701          }
702  
703          if (!(ref.value.name === '#' || (ref.value.name === '@' && attributes.cooked))) {
704            throw new Error('cookable nodes must not contain other nodes');
705          }
706  
707          if (ref.value.name === '@') {
708            const { cooked: cookedValue } = tag.value.attributes;
709  
710            if (!cookedValue) throw new Error('cannot cook string: it contains uncooked escapes');
711  
712            cooked += cookedValue;
713          }
714  
715          break;
716        }
717  
718        case CloseNodeTag: {
719          if (depth === 1) {
720            foundLast = true;
721          }
722          depth--;
723          break;
724        }
725  
726        case LiteralTag: {
727          if (depth === 1) {
728            cooked += tag.value;
729          }
730          break;
731        }
732  
733        default: {
734          throw new Error();
735        }
736      }
737  
738      first = false;
739    }
740  
741    return cooked;
742  };
743  
744  export const printSource = (tags) => {
745    let printed = '';
746  
747    if (!tags) return printed;
748  
749    for (const tag of tags) {
750      if (tag.type === LiteralTag) {
751        printed += tag.value;
752      } else if (tag.type === GapTag) {
753        throw new Error('use generateSourceTextFor');
754      }
755    }
756  
757    return printed;
758  };
759  
760  export function* generateSourceTextFor(tags) {
761    for (const tag of tags) {
762      if (tag.type === LiteralTag) {
763        yield* tag.value;
764      } else if (tag.type === GapTag) {
765        yield null;
766      }
767    }
768  }
769  
770  export const sourceTextFor = printSource;