tree.js
1 import { Coroutine } from '@bablr/coroutine'; 2 import emptyStack from '@iter-tools/imm-stack'; 3 import { 4 nodeFlags, 5 buildReferenceTag, 6 buildNullTag, 7 buildOpenNodeTag, 8 buildLiteralTag, 9 buildCloseNodeTag, 10 tokenFlags, 11 } from './builders.js'; 12 import { 13 printPrettyCSTML as printPrettyCSTMLFromStream, 14 printCSTML as printCSTMLFromStream, 15 printSource as printSourceFromStream, 16 getStreamIterator, 17 } from './stream.js'; 18 import { 19 DoctypeTag, 20 OpenNodeTag, 21 CloseNodeTag, 22 ReferenceTag, 23 ShiftTag, 24 GapTag, 25 NullTag, 26 ArrayInitializerTag, 27 LiteralTag, 28 EmbeddedNode, 29 } from './symbols.js'; 30 import * as btree from './btree.js'; 31 export * from './builders.js'; 32 export * from './print.js'; 33 import { 34 add, 35 get, 36 TagPath, 37 Path, 38 isFragmentNode, 39 isNullNode, 40 isGapNode, 41 getOpenTag, 42 getCloseTag, 43 } from './path.js'; 44 45 export { add, get, isFragmentNode, isNullNode, isGapNode, getOpenTag, getCloseTag }; 46 47 export const buildToken = (language, type, value, attributes = {}) => { 48 return treeFromStreamSync([ 49 buildOpenNodeTag(tokenFlags, language, type, attributes), 50 buildLiteralTag(value), 51 buildCloseNodeTag(), 52 ]); 53 }; 54 55 const isString = (str) => typeof str === 'string'; 56 57 const { isArray } = Array; 58 const { freeze } = Object; 59 60 export const parseReference = (str) => { 61 let { 62 1: name, 63 2: isArray, 64 3: index, 65 4: expressionToken, 66 5: hasGapToken, 67 } = /^\s*([.#@]|[a-zA-Z]+)\s*(\[\s*(\d+\s*)?\])?\s*(\+)?(\$)?\s*$/.exec(str); 68 69 let flags = { 70 expression: !!expressionToken, 71 hasGap: !!hasGapToken, 72 }; 73 74 index = index ? parseInt(index, 10) : null; 75 isArray = !!isArray; 76 name = name || null; 77 78 return buildReferenceTag(name, isArray, flags, index); 79 }; 80 81 export const mergeReferences = (outer, inner) => { 82 let { 83 name, 84 isArray, 85 index, 86 flags: { expression, hasGap }, 87 } = outer.value; 88 89 if ( 90 name != null && 91 name !== '.' && 92 inner.value.name != null && 93 inner.value.name !== '.' && 94 name !== inner.value.name 95 ) 96 throw new Error(); 97 98 isArray = isArray || inner.value.isArray; 99 expression = !!(expression || inner.value.flags.expression); 100 hasGap = !!(hasGap || inner.value.flags.hasGap); 101 name = name === '.' ? inner.value.name : name; 102 103 return buildReferenceTag(name, isArray, { expression, hasGap }, index); 104 }; 105 106 export const isEmptyReference = (ref) => { 107 let { name, isArray, flags } = ref.value; 108 return name === '.' && !isArray && !(flags.expression || flags.hasGap); 109 }; 110 111 function* __treeFromStream(tags, options) { 112 let path = null; 113 let rootPath = null; 114 let held = null; 115 let doctype = null; 116 const co = new Coroutine(getStreamIterator(tags)); 117 const expressionsCo = new Coroutine(getStreamIterator(options.expressions || [])); 118 let reference = null; 119 120 for (;;) { 121 co.advance(); 122 123 if (co.current instanceof Promise) { 124 co.current = yield co.current; 125 } 126 127 if (co.done) break; 128 129 const tag = co.value; 130 131 if (tag.type === 'Effect') { 132 continue; 133 } 134 135 if (tag.type === DoctypeTag) { 136 doctype = tag; 137 continue; 138 } 139 140 if (held && tag.type !== OpenNodeTag && tag.type !== GapTag) { 141 throw new Error('cannot eat this type of tag while holding'); 142 } 143 144 let suppressTag = false; 145 146 switch (tag.type) { 147 case LiteralTag: 148 case CloseNodeTag: { 149 break; 150 } 151 152 case ReferenceTag: { 153 reference = tag; 154 suppressTag = true; 155 break; 156 } 157 158 case ArrayInitializerTag: { 159 add(path.node, reference, []); 160 suppressTag = true; 161 reference = null; 162 break; 163 } 164 165 case NullTag: 166 case GapTag: { 167 if (!path) { 168 return buildStubNode(tag); 169 } 170 171 const isGap = tag.type === GapTag; 172 173 if (path.parent && reference.type !== ReferenceTag) throw new Error(); 174 175 let node = createNode(tag); 176 177 if (isGap) { 178 if (held) { 179 node = held; 180 add(path.node, reference, node); 181 suppressTag = true; 182 } else if (!expressionsCo.done) { 183 expressionsCo.advance(); 184 185 let outerReference = reference; 186 187 if (!expressionsCo.done) { 188 node = 189 node == null 190 ? buildStubNode(buildNullTag()) 191 : expressionsCo.value == null 192 ? buildStubNode(buildNullTag()) 193 : expressionsCo.value; 194 suppressTag = true; 195 196 if (isFragmentNode(node)) { 197 const parentNode = path.node; 198 199 let reference; 200 201 for (const tag of btree.traverse(node.children)) { 202 switch (tag.type) { 203 case DoctypeTag: { 204 break; 205 } 206 case OpenNodeTag: 207 case CloseNodeTag: { 208 if (!tag.value.type) { 209 break; 210 } else { 211 throw new Error(); 212 } 213 } 214 215 case ReferenceTag: 216 // combine tags for . 217 218 reference = tag; 219 break; 220 221 case ArrayInitializerTag: { 222 add(parentNode, mergeReferences(outerReference, reference), []); 223 break; 224 } 225 226 case EmbeddedNode: { 227 add(parentNode, mergeReferences(outerReference, reference), tag.value); 228 break; 229 } 230 231 case GapTag: { 232 const resolvedNode = get(reference, node); 233 add(parentNode, mergeReferences(outerReference, reference), resolvedNode); 234 break; 235 } 236 237 case NullTag: { 238 add(parentNode, mergeReferences(outerReference, reference), null); 239 break; 240 } 241 242 default: 243 throw new Error(); 244 } 245 } 246 } else { 247 if (path.node.flags.token) { 248 throw new Error('not implemented'); 249 } 250 add(path.node, reference, node); 251 } 252 } else { 253 if (!path.node.flags.token) { 254 add(path.node, reference, node); 255 } 256 } 257 } 258 } 259 260 reference = null; 261 held = isGap ? null : held; 262 263 if (!path.node.flags.token) { 264 path = { parent: path, node, depth: (path.depth || -1) + 1, arrays: new Set() }; 265 } 266 267 break; 268 } 269 270 // case ShiftTag: { 271 // const { children, properties } = path.node; 272 273 // let property = properties[ref.value.name]; 274 // let node; 275 276 // if (ref.value.isArray) { 277 // ({ node } = btree.getAt(-1, property)); 278 // properties[ref.value.name].pop(); 279 // } else { 280 // ({ node } = property); 281 // properties[ref.value.name] = null; 282 // } 283 284 // held = node; 285 // break; 286 // } 287 288 case OpenNodeTag: { 289 if (path) { 290 const node = createNode(tag); 291 292 if (path) { 293 add(path.node, reference, node); 294 reference = null; 295 } 296 297 path = { parent: path, node, depth: (path ? path.depth : -1) + 1, arrays: new Set() }; 298 } else { 299 const { language, type, flags, attributes } = tag.value; 300 301 const attributes_ = doctype?.value.attributes ?? attributes; 302 const language_ = attributes?.['bablrLanguage'] ?? language; 303 304 const node = { 305 flags, 306 language: language_, 307 type, 308 children: [], 309 properties: {}, 310 attributes: attributes_, 311 }; 312 313 path = { parent: null, node, depth: 0, arrays: new Set() }; 314 315 rootPath = path; 316 } 317 318 break; 319 } 320 321 default: { 322 throw new Error(); 323 } 324 } 325 326 if (!suppressTag) { 327 path.node.children = btree.push(path.node.children, tag); 328 } 329 330 switch (tag.type) { 331 case NullTag: 332 case GapTag: 333 case CloseNodeTag: { 334 const completedNode = path.node; 335 336 if (!(tag.type === GapTag && completedNode.flags.token)) { 337 finalizeNode(completedNode); 338 } 339 340 if (tag.type === GapTag) { 341 if (path && completedNode.type === null && completedNode.flags.token) { 342 break; 343 } 344 } 345 346 path = path.parent; 347 break; 348 } 349 } 350 } 351 352 if (path && path.node.type) { 353 throw new Error('imbalanced tag stack'); 354 } 355 356 return rootPath.node; 357 } 358 359 export const buildNullNode = () => { 360 return treeFromStreamSync([buildNullTag()]); 361 }; 362 363 export const treeFromStream = (tags, options = {}) => __treeFromStream(tags, options); 364 365 export const treeFromStreamSync = (tokens, options = {}) => { 366 return evaluateReturnSync(treeFromStream(tokens, options)); 367 }; 368 369 export const treeFromStreamAsync = async (tokens, options = {}) => { 370 return evaluateReturnAsync(treeFromStream(tokens, options)); 371 }; 372 373 export const evaluateReturnSync = (generator) => { 374 const co = new Coroutine(generator[Symbol.iterator]()); 375 while (!co.done) co.advance(); 376 return co.value; 377 }; 378 379 export const evaluateReturnAsync = async (generator) => { 380 const co = new Coroutine(getStreamIterator(generator)); 381 while (!co.done) { 382 co.advance(); 383 384 if (co.current instanceof Promise) { 385 co.current = await co.current; 386 } 387 } 388 return co.value; 389 }; 390 391 export const streamFromTree = (rootNode, options = {}) => __streamFromTree(rootNode, options); 392 393 export const isEmpty = (node) => { 394 const { properties } = node; 395 396 let ref = null; 397 398 for (const tag of btree.traverse(node.children)) { 399 switch (tag.type) { 400 case ReferenceTag: { 401 const { name } = tag.value; 402 403 ref = tag; 404 405 if (properties[name]) { 406 const property = properties[name]; 407 408 if ( 409 property != null || 410 (isArray(property) && property.length) || 411 !isNullNode(property.node) 412 ) { 413 return false; 414 } 415 } 416 break; 417 } 418 419 case EmbeddedNode: { 420 if (ref.value.name === '@') { 421 return false; 422 } 423 break; 424 } 425 426 case LiteralTag: 427 case GapTag: 428 return false; 429 } 430 } 431 return true; 432 }; 433 434 export const buildStubNode = (tag) => { 435 return freeze({ 436 flags: nodeFlags, 437 language: null, 438 type: null, 439 children: freeze([tag]), 440 properties: freeze({}), 441 attributes: freeze({}), 442 }); 443 }; 444 445 function* __streamFromTree(rootNode, options) { 446 const { unshift = false } = options; 447 if (!rootNode || !btree.getSum(rootNode.children)) return; 448 449 let tagPath = TagPath.from(Path.from(rootNode), 0); 450 451 let count = 0; 452 453 do { 454 if (tagPath.tag.type === OpenNodeTag) count++; 455 if (tagPath.tag.type === CloseNodeTag) count--; 456 457 yield tagPath.tag; 458 } while ((tagPath = unshift ? tagPath.nextUnshifted : tagPath.next)); 459 460 if (count !== 0) throw new Error(); 461 } 462 463 export const getCooked = (cookable) => { 464 if (!cookable || isGapNode(cookable.type)) { 465 return ''; 466 } 467 468 const children = cookable.children || cookable; 469 470 let cooked = ''; 471 472 // const openTag = getOpenTag(cookable); 473 // const closeTag = getCloseTag(cookable); 474 475 let reference = null; 476 477 for (const tag of btree.traverse(children)) { 478 switch (tag.type) { 479 case ReferenceTag: { 480 const { name } = tag.value; 481 482 if (!(name === '#' || name === '@')) { 483 throw new Error('cookable nodes must not contain other nodes'); 484 } 485 486 reference = tag; 487 break; 488 } 489 490 case EmbeddedNode: { 491 const { attributes } = tag.value; 492 493 if (reference.value.name === '@') { 494 const { cooked: cookedValue } = attributes; 495 496 if (!isString(cookedValue)) 497 throw new Error('cannot cook string: it contains uncooked escapes'); 498 499 cooked += cookedValue; 500 } 501 502 break; 503 } 504 505 case LiteralTag: { 506 cooked += tag.value; 507 break; 508 } 509 510 case OpenNodeTag: { 511 break; 512 } 513 514 case CloseNodeTag: { 515 break; 516 } 517 518 default: { 519 throw new Error(); 520 } 521 } 522 } 523 524 return cooked; 525 }; 526 527 export const printCSTML = (rootNode) => { 528 return printCSTMLFromStream(streamFromTree(rootNode)); 529 }; 530 531 export const printPrettyCSTML = (rootNode, options = {}) => { 532 return printPrettyCSTMLFromStream(streamFromTree(rootNode), options); 533 }; 534 535 export const printSource = (rootNode) => { 536 return printSourceFromStream(streamFromTree(rootNode, { unshift: true })); 537 }; 538 539 export const sourceTextFor = printSource; 540 541 export const getRange = (node) => { 542 const { children } = node; 543 return btree.getSum(children) ? [btree.getAt(0, children), btree.getAt(-1, children)] : null; 544 }; 545 546 export const createNode = (openTag) => { 547 if (!openTag || openTag.type === GapTag || openTag.type === NullTag) { 548 return { 549 flags: nodeFlags, 550 language: openTag?.language, 551 type: openTag && ([NullTag, GapTag].includes(openTag.type) ? null : openTag.type), 552 children: [], 553 properties: {}, 554 attributes: openTag?.attributes || {}, 555 }; 556 } else { 557 const { flags, language, type, attributes = {} } = openTag.value || {}; 558 return { flags, language, type, children: [], properties: {}, attributes }; 559 } 560 }; 561 562 export const finalizeNode = (node) => { 563 for (const property of Object.values(node.properties)) { 564 if (isArray(property)) { 565 btree.freeze(property); 566 for (const childProperty of btree.traverse(property)) { 567 freeze(childProperty); 568 if (childProperty.reference.value.flags.expression) { 569 btree.freeze(childProperty.node); 570 } 571 } 572 } else { 573 freeze(property); 574 575 if (property.reference.value.flags.expression) { 576 btree.freeze(property.node); 577 } 578 } 579 } 580 581 freeze(node); 582 btree.freeze(node.children); 583 freeze(node.properties); 584 freeze(node.attributes); 585 return node; 586 }; 587 588 export const notNull = (node) => { 589 return node != null && !isNullNode(node); 590 }; 591 592 export const isNull = (node) => { 593 return node == null || isNullNode(node); 594 }; 595 596 export const branchProperties = (properties) => { 597 const copy = { ...properties }; 598 599 for (const { 0: key, 1: value } of Object.entries(copy)) { 600 if (isArray(value)) { 601 copy[key] = btree.fromValues(value); 602 } 603 } 604 605 return copy; 606 }; 607 608 export const branchNode = (node) => { 609 const { flags, language, type, children, properties, attributes } = node; 610 return { 611 flags, 612 language, 613 type, 614 // if we always use immutable trees we won't need to do this 615 children: btree.fromValues(btree.traverse(children)), 616 properties: branchProperties(properties), 617 attributes: { ...attributes }, 618 }; 619 }; 620 621 export const acceptNode = (node, accepted) => { 622 const { children, properties, attributes } = accepted; 623 node.children = children; 624 node.properties = properties; 625 node.attributes = attributes; 626 return node; 627 }; 628 629 export const getRoot = (node) => { 630 return node == null ? node : isFragmentNode(node) ? node.properties['.'].node : node; 631 }; 632 633 export function* traverseProperties(properties) { 634 for (const value of Object.values(properties)) { 635 if (isArray(value)) { 636 for (let item of btree.traverse(value)) { 637 if (isArray(item.node)) { 638 yield btree.getAt(-1, item.node); 639 } else { 640 yield item.node; 641 } 642 } 643 } else { 644 yield value.node; 645 } 646 } 647 } 648 649 export class Resolver { 650 constructor( 651 states = emptyStack.push({ properties: new Map(), idx: 0 }), 652 reference = null, 653 popped = false, 654 held = null, 655 ) { 656 this.states = states; 657 this.reference = reference; 658 this.popped = popped; 659 this.held = held; 660 this.doctype = null; 661 } 662 663 get idx() { 664 return this.states.value.idx; 665 } 666 667 get properties() { 668 return this.states.value.properties; 669 } 670 671 advance(tag) { 672 const { states } = this; 673 674 ++states.value.idx; 675 676 this.popped = false; 677 678 let hadReference = this.reference; 679 680 switch (tag.type) { 681 case ReferenceTag: { 682 const { name, isArray } = tag.value; 683 const { properties } = states.value; 684 685 if (this.reference) throw new Error(); 686 687 this.reference = tag; 688 689 if (name && name !== '#' && name !== '@') { 690 let state = properties.get(name); 691 692 if (isArray) { 693 if (state && !state.isArray) throw new Error(); 694 695 const { count = -1 } = state || {}; 696 697 state = { count: count + 1, isArray }; 698 } else if (state) { 699 throw new Error(`attempted to consume property {name: ${name}} twice`); 700 } else { 701 state = { count: 1, isArray: false }; 702 } 703 704 properties.set(name, state); 705 } 706 707 break; 708 } 709 710 case EmbeddedNode: { 711 if (!this.reference || !['#', '@'].includes(this.reference.value.name)) throw new Error(); 712 713 // this.states = states.push({ properties: new Map(), idx: 0 }); 714 break; 715 } 716 717 case OpenNodeTag: { 718 const { reference } = this; 719 const { flags } = tag.value; 720 const isRootNode = states.size === 1; 721 722 if (tag.value.type) { 723 this.states = states.push({ properties: new Map(), idx: 0 }); 724 } 725 726 if (!tag.value.type && (!isRootNode || this.reference)) throw new Error(); 727 728 if ( 729 tag.type === OpenNodeTag && 730 ((!reference && !isRootNode) || 731 (reference && 732 reference.type !== ReferenceTag && 733 reference.type !== ShiftTag && 734 reference.type !== OpenNodeTag)) 735 ) { 736 throw new Error('Invalid location for OpenNodeTag'); 737 } 738 739 if (!isRootNode && !reference) { 740 throw new Error(); 741 } 742 743 this.reference = null; 744 break; 745 } 746 747 case ArrayInitializerTag: { 748 if (!this.reference) throw new Error(); 749 750 const { name } = this.reference.value; 751 const { properties } = states.value; 752 const state = properties.get(name); 753 754 if (!state || !state.isArray || state.count !== 0) throw new Error(); 755 756 properties.set(name, { count: 0, isArray: true }); 757 758 this.reference = null; 759 break; 760 } 761 762 case ShiftTag: { 763 this.held = this.states.value; 764 this.states = this.states.push({ properties: new Map(), idx: 0 }); 765 this.reference = tag; 766 767 break; 768 } 769 770 case NullTag: { 771 if (!this.reference) throw new Error(); 772 773 this.popped = true; 774 this.reference = null; 775 break; 776 } 777 778 case GapTag: { 779 // if (!this.reference) throw new Error(); 780 781 if (this.held) { 782 // this.states = this.states.push(this.held); 783 this.held = null; 784 } 785 786 this.popped = true; 787 this.reference = null; 788 break; 789 } 790 791 case CloseNodeTag: { 792 if (this.reference) throw new Error(); 793 794 this.states = states.pop(); 795 this.popped = true; 796 break; 797 } 798 799 case DoctypeTag: 800 this.doctype = tag; 801 break; 802 803 case LiteralTag: 804 break; 805 806 default: 807 throw new Error(); 808 } 809 810 if (hadReference && this.reference) throw new Error(); 811 812 return this; 813 } 814 815 resolve(reference) { 816 let { name, isArray, flags } = reference.value; 817 const { states } = this; 818 const state = states.value.properties.get(name); 819 let index = null; 820 821 if (name === '@' || name === '#') return reference; 822 823 if (isArray && state) { 824 index = state?.count || 0; 825 } 826 827 return buildReferenceTag(name, isArray, flags, index); 828 } 829 830 branch() { 831 const { states, reference, popped, held } = this; 832 const { properties, idx } = states.value; 833 834 return new Resolver( 835 states.replace({ properties: new Map(properties), idx }), 836 reference, 837 popped, 838 held, 839 ); 840 } 841 842 accept(resolver) { 843 this.states = resolver.states; 844 this.reference = resolver.reference; 845 this.popped = resolver.popped; 846 this.held = resolver.held; 847 848 return this; 849 } 850 } 851 852 freeze(Resolver.prototype);