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;