Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use compact index-state arrays in child matching

Replace set-based candidate tracking with contiguous index arrays and typed active flags in visitChildNodes to reduce hash overhead while preserving matching order and behavior.

+109 -51
+109 -51
src/morphlex.ts
··· 26 26 27 27 type Operation = (typeof Operation)[keyof typeof Operation] 28 28 29 - const candidateNodes: Set<number> = new Set() 30 - const candidateElements: Set<number> = new Set() 31 - const candidateElementsWithIds: Set<number> = new Set() 32 - const unmatchedNodes: Set<number> = new Set() 33 - const unmatchedElements: Set<number> = new Set() 34 - const whitespaceNodes: Set<number> = new Set() 35 - 36 29 type IdSetMap = WeakMap<Node, Set<string>> 37 30 type IdArrayMap = WeakMap<Node, Array<string>> 38 31 ··· 469 462 const fromChildNodes = nodeListToArray(from.childNodes) 470 463 const toChildNodes = nodeListToArray(to.childNodes) 471 464 472 - candidateNodes.clear() 473 - candidateElements.clear() 474 - candidateElementsWithIds.clear() 475 - unmatchedNodes.clear() 476 - unmatchedElements.clear() 477 - whitespaceNodes.clear() 465 + const candidateNodeIndices: Array<number> = [] 466 + const candidateElementIndices: Array<number> = [] 467 + const candidateElementWithIdIndices: Array<number> = [] 468 + const unmatchedNodeIndices: Array<number> = [] 469 + const unmatchedElementIndices: Array<number> = [] 470 + const whitespaceNodeIndices: Array<number> = [] 471 + 472 + const candidateNodeActive = new Uint8Array(fromChildNodes.length) 473 + const candidateElementActive = new Uint8Array(fromChildNodes.length) 474 + const candidateElementWithIdActive = new Uint8Array(fromChildNodes.length) 475 + const unmatchedNodeActive = new Uint8Array(toChildNodes.length) 476 + const unmatchedElementActive = new Uint8Array(toChildNodes.length) 478 477 479 478 const matches: Array<number> = [] 480 479 const op: Array<Operation> = [] ··· 491 490 if (nodeType === ELEMENT_NODE_TYPE) { 492 491 candidateLocalNameMap[i] = (candidate as Element).localName 493 492 if ((candidate as Element).id !== "") { 494 - candidateElementsWithIds.add(i) 493 + candidateElementWithIdActive[i] = 1 494 + candidateElementWithIdIndices.push(i) 495 495 } else { 496 - candidateElements.add(i) 496 + candidateElementActive[i] = 1 497 + candidateElementIndices.push(i) 497 498 } 498 499 } else if (nodeType === TEXT_NODE_TYPE && candidate.textContent?.trim() === "") { 499 - whitespaceNodes.add(i) 500 + whitespaceNodeIndices.push(i) 500 501 } else { 501 - candidateNodes.add(i) 502 + candidateNodeActive[i] = 1 503 + candidateNodeIndices.push(i) 502 504 } 503 505 } 504 506 ··· 509 511 510 512 if (nodeType === ELEMENT_NODE_TYPE) { 511 513 localNameMap[i] = (node as Element).localName 512 - unmatchedElements.add(i) 514 + unmatchedElementActive[i] = 1 515 + unmatchedElementIndices.push(i) 513 516 } else if (nodeType === TEXT_NODE_TYPE && node.textContent?.trim() === "") { 514 517 continue 515 518 } else { 516 - unmatchedNodes.add(i) 519 + unmatchedNodeActive[i] = 1 520 + unmatchedNodeIndices.push(i) 517 521 } 518 522 } 519 523 520 524 // Match elements by isEqualNode 521 - for (const unmatchedIndex of unmatchedElements) { 525 + for (let i = 0; i < unmatchedElementIndices.length; i++) { 526 + const unmatchedIndex = unmatchedElementIndices[i]! 527 + if (!unmatchedElementActive[unmatchedIndex]) continue 528 + 522 529 const localName = localNameMap[unmatchedIndex] 523 530 const element = toChildNodes[unmatchedIndex] as Element 524 531 525 - for (const candidateIndex of candidateElements) { 532 + for (let c = 0; c < candidateElementIndices.length; c++) { 533 + const candidateIndex = candidateElementIndices[c]! 534 + if (!candidateElementActive[candidateIndex]) continue 526 535 if (localName !== candidateLocalNameMap[candidateIndex]) continue 527 536 const candidate = fromChildNodes[candidateIndex] as Element 528 537 529 538 if (candidate.isEqualNode(element)) { 530 539 matches[unmatchedIndex] = candidateIndex 531 540 op[unmatchedIndex] = Operation.EqualNode 532 - candidateElements.delete(candidateIndex) 533 - unmatchedElements.delete(unmatchedIndex) 541 + candidateElementActive[candidateIndex] = 0 542 + unmatchedElementActive[unmatchedIndex] = 0 534 543 break 535 544 } 536 545 } 537 546 } 538 547 539 548 // Match by exact id 540 - for (const unmatchedIndex of unmatchedElements) { 549 + for (let i = 0; i < unmatchedElementIndices.length; i++) { 550 + const unmatchedIndex = unmatchedElementIndices[i]! 551 + if (!unmatchedElementActive[unmatchedIndex]) continue 552 + 541 553 const element = toChildNodes[unmatchedIndex] as Element 542 554 const id = element.id 543 555 544 556 if (id === "") continue 545 557 546 - for (const candidateIndex of candidateElementsWithIds) { 558 + for (let c = 0; c < candidateElementWithIdIndices.length; c++) { 559 + const candidateIndex = candidateElementWithIdIndices[c]! 560 + if (!candidateElementWithIdActive[candidateIndex]) continue 547 561 const candidate = fromChildNodes[candidateIndex] as Element 548 562 549 563 if (localNameMap[unmatchedIndex] === candidateLocalNameMap[candidateIndex] && id === candidate.id) { 550 564 matches[unmatchedIndex] = candidateIndex 551 565 op[unmatchedIndex] = Operation.SameElement 552 - candidateElementsWithIds.delete(candidateIndex) 553 - unmatchedElements.delete(unmatchedIndex) 566 + candidateElementWithIdActive[candidateIndex] = 0 567 + unmatchedElementActive[unmatchedIndex] = 0 554 568 break 555 569 } 556 570 } ··· 558 572 559 573 // Match by idArray (to) against idSet (from) 560 574 // Elements with idSets may not have IDs themselves, so we check candidateElements 561 - for (const unmatchedIndex of unmatchedElements) { 575 + for (let i = 0; i < unmatchedElementIndices.length; i++) { 576 + const unmatchedIndex = unmatchedElementIndices[i]! 577 + if (!unmatchedElementActive[unmatchedIndex]) continue 578 + 562 579 const element = toChildNodes[unmatchedIndex] as Element 563 580 const idArray = this.#idArrayMap.get(element) 564 581 565 582 if (!idArray) continue 566 583 567 - candidateLoop: for (const candidateIndex of candidateElements) { 584 + candidateLoop: for (let c = 0; c < candidateElementIndices.length; c++) { 585 + const candidateIndex = candidateElementIndices[c]! 586 + if (!candidateElementActive[candidateIndex]) continue 587 + 568 588 const candidate = fromChildNodes[candidateIndex] as Element 569 589 570 590 if (localNameMap[unmatchedIndex] === candidateLocalNameMap[candidateIndex]) { ··· 575 595 if (candidateIdSet.has(arrayId)) { 576 596 matches[unmatchedIndex] = candidateIndex 577 597 op[unmatchedIndex] = Operation.SameElement 578 - candidateElements.delete(candidateIndex) 579 - unmatchedElements.delete(unmatchedIndex) 598 + candidateElementActive[candidateIndex] = 0 599 + unmatchedElementActive[unmatchedIndex] = 0 580 600 break candidateLoop 581 601 } 582 602 } ··· 586 606 } 587 607 588 608 // Match by heuristics 589 - for (const unmatchedIndex of unmatchedElements) { 609 + for (let i = 0; i < unmatchedElementIndices.length; i++) { 610 + const unmatchedIndex = unmatchedElementIndices[i]! 611 + if (!unmatchedElementActive[unmatchedIndex]) continue 612 + 590 613 const element = toChildNodes[unmatchedIndex] as Element 591 614 592 615 const name = element.getAttribute("name") 593 616 const href = element.getAttribute("href") 594 617 const src = element.getAttribute("src") 595 618 596 - for (const candidateIndex of candidateElements) { 619 + for (let c = 0; c < candidateElementIndices.length; c++) { 620 + const candidateIndex = candidateElementIndices[c]! 621 + if (!candidateElementActive[candidateIndex]) continue 622 + 597 623 const candidate = fromChildNodes[candidateIndex] as Element 598 624 if ( 599 625 localNameMap[unmatchedIndex] === candidateLocalNameMap[candidateIndex] && ··· 603 629 ) { 604 630 matches[unmatchedIndex] = candidateIndex 605 631 op[unmatchedIndex] = Operation.SameElement 606 - candidateElements.delete(candidateIndex) 607 - unmatchedElements.delete(unmatchedIndex) 632 + candidateElementActive[candidateIndex] = 0 633 + unmatchedElementActive[unmatchedIndex] = 0 608 634 break 609 635 } 610 636 } 611 637 } 612 638 613 639 // Match by tagName 614 - for (const unmatchedIndex of unmatchedElements) { 640 + for (let i = 0; i < unmatchedElementIndices.length; i++) { 641 + const unmatchedIndex = unmatchedElementIndices[i]! 642 + if (!unmatchedElementActive[unmatchedIndex]) continue 643 + 615 644 const element = toChildNodes[unmatchedIndex] as Element 616 645 617 646 const localName = localNameMap[unmatchedIndex] 618 647 619 - for (const candidateIndex of candidateElements) { 648 + for (let c = 0; c < candidateElementIndices.length; c++) { 649 + const candidateIndex = candidateElementIndices[c]! 650 + if (!candidateElementActive[candidateIndex]) continue 651 + 620 652 const candidate = fromChildNodes[candidateIndex] as Element 621 653 const candidateLocalName = candidateLocalNameMap[candidateIndex] 622 654 ··· 627 659 } 628 660 matches[unmatchedIndex] = candidateIndex 629 661 op[unmatchedIndex] = Operation.SameElement 630 - candidateElements.delete(candidateIndex) 631 - unmatchedElements.delete(unmatchedIndex) 662 + candidateElementActive[candidateIndex] = 0 663 + unmatchedElementActive[unmatchedIndex] = 0 632 664 break 633 665 } 634 666 } 635 667 } 636 668 637 669 // Match nodes by isEqualNode (skip whitespace-only text nodes) 638 - for (const unmatchedIndex of unmatchedNodes) { 670 + for (let i = 0; i < unmatchedNodeIndices.length; i++) { 671 + const unmatchedIndex = unmatchedNodeIndices[i]! 672 + if (!unmatchedNodeActive[unmatchedIndex]) continue 673 + 639 674 const node = toChildNodes[unmatchedIndex]! 640 675 641 - for (const candidateIndex of candidateNodes) { 676 + for (let c = 0; c < candidateNodeIndices.length; c++) { 677 + const candidateIndex = candidateNodeIndices[c]! 678 + if (!candidateNodeActive[candidateIndex]) continue 679 + 642 680 const candidate = fromChildNodes[candidateIndex]! 643 681 if (candidate.isEqualNode(node)) { 644 682 matches[unmatchedIndex] = candidateIndex 645 683 op[unmatchedIndex] = Operation.EqualNode 646 - candidateNodes.delete(candidateIndex) 647 - unmatchedNodes.delete(unmatchedIndex) 684 + candidateNodeActive[candidateIndex] = 0 685 + unmatchedNodeActive[unmatchedIndex] = 0 648 686 break 649 687 } 650 688 } 651 689 } 652 690 653 691 // Match by nodeType 654 - for (const unmatchedIndex of unmatchedNodes) { 692 + for (let i = 0; i < unmatchedNodeIndices.length; i++) { 693 + const unmatchedIndex = unmatchedNodeIndices[i]! 694 + if (!unmatchedNodeActive[unmatchedIndex]) continue 695 + 655 696 const nodeType = nodeTypeMap[unmatchedIndex] 656 697 657 - for (const candidateIndex of candidateNodes) { 698 + for (let c = 0; c < candidateNodeIndices.length; c++) { 699 + const candidateIndex = candidateNodeIndices[c]! 700 + if (!candidateNodeActive[candidateIndex]) continue 701 + 658 702 if (nodeType === candidateNodeTypeMap[candidateIndex]) { 659 703 matches[unmatchedIndex] = candidateIndex 660 704 op[unmatchedIndex] = Operation.SameNode 661 - candidateNodes.delete(candidateIndex) 662 - unmatchedNodes.delete(unmatchedIndex) 705 + candidateNodeActive[candidateIndex] = 0 706 + unmatchedNodeActive[unmatchedIndex] = 0 663 707 break 664 708 } 665 709 } 666 710 } 667 711 668 712 // Remove any unmatched candidates first, before calculating LIS and repositioning 669 - for (const i of candidateNodes) this.#removeNode(fromChildNodes[i]!) 670 - for (const i of whitespaceNodes) this.#removeNode(fromChildNodes[i]!) 671 - for (const i of candidateElements) this.#removeNode(fromChildNodes[i]!) 672 - for (const i of candidateElementsWithIds) this.#removeNode(fromChildNodes[i]!) 713 + for (let i = 0; i < candidateNodeIndices.length; i++) { 714 + const candidateIndex = candidateNodeIndices[i]! 715 + if (candidateNodeActive[candidateIndex]) this.#removeNode(fromChildNodes[candidateIndex]!) 716 + } 717 + 718 + for (let i = 0; i < whitespaceNodeIndices.length; i++) { 719 + this.#removeNode(fromChildNodes[whitespaceNodeIndices[i]!]!) 720 + } 721 + 722 + for (let i = 0; i < candidateElementIndices.length; i++) { 723 + const candidateIndex = candidateElementIndices[i]! 724 + if (candidateElementActive[candidateIndex]) this.#removeNode(fromChildNodes[candidateIndex]!) 725 + } 726 + 727 + for (let i = 0; i < candidateElementWithIdIndices.length; i++) { 728 + const candidateIndex = candidateElementWithIdIndices[i]! 729 + if (candidateElementWithIdActive[candidateIndex]) this.#removeNode(fromChildNodes[candidateIndex]!) 730 + } 673 731 674 732 // Find LIS - these nodes don't need to move 675 733 // matches already contains the fromChildNodes indices, so we can use it directly