Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@
"serve": "bun --bun vite --open /benchmark/"
},
"devDependencies": {
"@types/bun": "^1.3.1",
"@typescript/native-preview": "^7.0.0-dev.20251104.1",
"@vitest/browser": "^4.0.6",
"@vitest/browser-playwright": "^4.0.6",
"@vitest/coverage-v8": "^4.0.6",
"@vitest/ui": "^4.0.6",
"@types/bun": "^1.3.2",
"@typescript/native-preview": "^7.0.0-dev.20251109.1",
"@vitest/browser": "^4.0.8",
"@vitest/browser-playwright": "^4.0.8",
"@vitest/coverage-v8": "^4.0.8",
"@vitest/ui": "^4.0.8",
"happy-dom": "^20.0.10",
"oxlint": "^1.25.0",
"oxlint": "^1.26.0",
"oxlint-tsgolint": "^0.4.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"vitest": "^4.0.6"
"vitest": "^4.0.8"
}
}
93 changes: 57 additions & 36 deletions src/morphlex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const unmatchedNodes: Set<number> = new Set()
const unmatchedElements: Set<number> = new Set()
const whitespaceNodes: Set<number> = new Set()

type IdMap = WeakMap<Node, Array<string>>
type IdSetMap = WeakMap<Node, Set<string>>
type IdArrayMap = WeakMap<Node, Array<string>>

/**
* Configuration options for morphing operations.
Expand Down Expand Up @@ -129,7 +130,7 @@ export function morphDocument(from: Document, to: Document | string, options?: O
export function morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode> | string, options: Options = {}): void {
if (typeof to === "string") to = parseFragment(to).childNodes

if (isParentNode(from)) flagDirtyInputs(from)
if (!options.preserveChanges && isParentNode(from)) flagDirtyInputs(from)

new Morph(options).morph(from, to)
}
Expand Down Expand Up @@ -214,7 +215,8 @@ function moveBefore(parent: ParentNode, node: ChildNode, insertionPoint: ChildNo
}

class Morph {
readonly #idMap: IdMap = new WeakMap()
readonly #idArrayMap: IdArrayMap = new WeakMap()
readonly #idSetMap: IdSetMap = new WeakMap()
readonly #options: Options

constructor(options: Options = {}) {
Expand Down Expand Up @@ -284,10 +286,10 @@ class Morph {
}

if (to instanceof NodeList) {
this.#mapIdSetsForEach(to)
this.#mapIdArraysForEach(to)
this.#morphOneToMany(from, to)
} else if (isParentNode(to)) {
this.#mapIdSets(to)
this.#mapIdArrays(to)
this.#morphOneToOne(from, to)
}
}
Expand Down Expand Up @@ -365,9 +367,7 @@ class Morph {
}

// First pass: update/add attributes from reference (iterate forwards)
const toAttributes = to.attributes
for (let i = 0; i < toAttributes.length; i++) {
const { name, value } = toAttributes[i]!
for (const { name, value } of to.attributes) {
if (name === "value") {
if (isInputElement(from) && from.value !== value) {
if (!this.#options.preserveChanges || from.value === from.defaultValue) {
Expand Down Expand Up @@ -400,12 +400,8 @@ class Morph {
}
}

const fromAttrs = from.attributes

// Second pass: remove excess attributes (iterate backwards for efficiency)
for (let i = fromAttrs.length - 1; i >= 0; i--) {
const { name, value } = fromAttrs[i]!

// Second pass: remove excess attributes
for (const { name, value } of from.attributes) {
if (!to.hasAttribute(name)) {
if (name === "selected") {
if (isOptionElement(from) && from.selected) {
Expand Down Expand Up @@ -509,9 +505,9 @@ class Morph {
const element = toChildNodes[unmatchedIndex] as Element

const id = element.id
const idSet = this.#idMap.get(element)
const idArray = this.#idArrayMap.get(element)

if (id === "" && !idSet) continue
if (id === "" && !idArray) continue

candidateLoop: for (const candidateIndex of candidateElements) {
const candidate = fromChildNodes[candidateIndex] as Element
Expand All @@ -525,20 +521,18 @@ class Morph {
break candidateLoop
}

// Match by idSet
if (idSet) {
const candidateIdSet = this.#idMap.get(candidate)
// Match by idArray (to) against idSet (from)
if (idArray) {
const candidateIdSet = this.#idSetMap.get(candidate)
if (candidateIdSet) {
for (let i = 0; i < idSet.length; i++) {
const setId = idSet[i]!
for (let k = 0; k < candidateIdSet.length; k++) {
if (candidateIdSet[k] === setId) {
matches[unmatchedIndex] = candidateIndex
seq[candidateIndex] = unmatchedIndex
candidateElements.delete(candidateIndex)
unmatchedElements.delete(unmatchedIndex)
break candidateLoop
}
for (let i = 0; i < idArray.length; i++) {
const arrayId = idArray[i]!
if (candidateIdSet.has(arrayId)) {
matches[unmatchedIndex] = candidateIndex
seq[candidateIndex] = unmatchedIndex
candidateElements.delete(candidateIndex)
unmatchedElements.delete(unmatchedIndex)
break candidateLoop
}
}
}
Expand Down Expand Up @@ -687,17 +681,41 @@ class Morph {
}
}

#mapIdSetsForEach(nodeList: NodeList): void {
#mapIdArraysForEach(nodeList: NodeList): void {
for (const childNode of nodeList) {
if (isParentNode(childNode)) {
this.#mapIdSets(childNode)
this.#mapIdArrays(childNode)
}
}
}

// For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements.
// For each node with an ID, push that ID into the IdArray on the IdArrayMap, for each of its parent elements.
#mapIdArrays(node: ParentNode): void {
const idArrayMap = this.#idArrayMap

for (const element of node.querySelectorAll("[id]")) {
const id = element.id

if (id === "") continue

let currentElement: Element | null = element

while (currentElement) {
const idArray = idArrayMap.get(currentElement)
if (idArray) {
idArray.push(id)
} else {
idArrayMap.set(currentElement, [id])
}
if (currentElement === node) break
currentElement = currentElement.parentElement
}
}
}

// For each node with an ID, add that ID into the IdSet on the IdSetMap, for each of its parent elements.
#mapIdSets(node: ParentNode): void {
const idMap = this.#idMap
const idSetMap = this.#idSetMap

for (const element of node.querySelectorAll("[id]")) {
const id = element.id
Expand All @@ -707,9 +725,12 @@ class Morph {
let currentElement: Element | null = element

while (currentElement) {
const idSet: Array<string> | undefined = idMap.get(currentElement)
if (idSet) idSet.push(id)
else idMap.set(currentElement, [id])
const idSet = idSetMap.get(currentElement)
if (idSet) {
idSet.add(id)
} else {
idSetMap.set(currentElement, new Set([id]))
}
if (currentElement === node) break
currentElement = currentElement.parentElement
}
Expand Down