Skip to content

Commit 85a6fc6

Browse files
committed
span: migrate to start/end node
1 parent 74b1442 commit 85a6fc6

File tree

2 files changed

+51
-56
lines changed

2 files changed

+51
-56
lines changed

src/html.js

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,31 @@ const flash =
5656
: undefined
5757

5858
class Span {
59+
/**
60+
* @param {Node} parentNode
61+
* @param {Node | null} start
62+
* @param {Node | null} end
63+
*/
5964
constructor(parentNode, start, end) {
65+
assert(start === null || start.parentNode === parentNode)
66+
assert(end === null || end.parentNode === parentNode)
67+
6068
this.parentNode = parentNode
6169
this.start = start
6270
this.end = end
6371
}
72+
6473
deleteContents() {
6574
// optimization for clearing when we own the entire parent.
66-
if (this.start === 0 && this.end >= this.parentNode.childNodes.length) {
67-
if (DEV && this.end !== this.parentNode.childNodes.length) console.warn('end is past the end of the parent')
75+
if (this.parentNode.firstChild === this.start && this.end === null) {
6876
this.parentNode.textContent = ''
69-
this.end = 0
77+
this.start = null
7078
return
7179
}
72-
while (this.end > this.start) {
73-
const node = this.parentNode.childNodes[--this.end]
74-
if (flash && node.getBoundingClientRect) {
80+
for (const node of this) {
81+
if (flash && isElement(node)) {
7582
const box = node.getBoundingClientRect()
76-
const cloned = node.cloneNode(true)
77-
node.remove()
83+
const cloned = /** @type {HTMLElement} */ (node.cloneNode(true))
7884
Object.assign(cloned.style, {
7985
position: 'absolute',
8086
top: box.top + 'px',
@@ -85,35 +91,37 @@ class Span {
8591
})
8692
document.body.appendChild(cloned)
8793
Promise.resolve(flash(cloned, 255, 0, 0)).then(() => cloned.remove())
88-
} else {
89-
node.remove()
9094
}
95+
this.parentNode.removeChild(node)
9196
}
97+
this.start = this.end = this.start?.previousSibling ?? null
9298
}
9399
insertNode(node) {
94-
const length = isDocumentFragment(node) ? node.childNodes.length : 1
95-
this.parentNode.insertBefore(node, this.parentNode.childNodes[this.end] ?? null)
96-
this.end += length
100+
if (isDocumentFragment(node) && node.childNodes.length === 0) return
101+
this.start ??= isDocumentFragment(node) ? node.firstChild : node
102+
this.parentNode.insertBefore(node, this.end)
97103
if (flash) for (const node of this) flash(node, 0, 255, 0)
98104
}
99105
*[Symbol.iterator]() {
100-
for (let i = this.start; i < this.end; i++)
101-
yield (console.log(this.parentNode.childNodes[i]), this.parentNode.childNodes[i])
106+
for (let node = this.start; node !== this.end && node !== null; node = node.nextSibling) yield node
102107
}
103108
extractContents() {
104109
const fragment = document.createDocumentFragment()
105-
while (this.end > this.start) fragment.prepend(this.parentNode.childNodes[--this.end])
110+
for (const node of this) fragment.appendChild(node)
111+
this.start = this.end = null
106112
return fragment
107113
}
108114
get length() {
109-
return this.end - this.start
115+
let length = 0
116+
for (const _ of this) length++
117+
return length
110118
}
111119
}
112120

113121
if (DEV) {
114122
Span.prototype.toString = function () {
115123
let result = ''
116-
for (const node of this) result += node.outerHTML
124+
for (const node of this) result += /** @type {Element} */ (node).outerHTML ?? node
117125
return result
118126
}
119127
}
@@ -143,12 +151,11 @@ export class Root {
143151
}
144152

145153
static appendInto(parent) {
146-
return new Root(new Span(parent, parent.childNodes.length, parent.childNodes.length))
154+
return new Root(new Span(parent, parent.lastChild, null))
147155
}
148156

149157
static replace(node) {
150-
const index = [...node.parentNode.childNodes].indexOf(node)
151-
return new Root(new Span(node.parentNode, index, index + 1))
158+
return new Root(new Span(node.parentNode, node, node.nextSibling))
152159
}
153160

154161
render(value) {
@@ -271,7 +278,7 @@ function compileTemplate(statics) {
271278
// by splitting the text starting from the end, we only have to split the original node.
272279
for (const match of [...node.data.matchAll(DYNAMIC_GLOBAL)].reverse()) {
273280
node.splitText(match.index + match[0].length)
274-
const dyn = new Comment()
281+
const dyn = DEV ? new Comment(`dyn-$${match[1]}`) : new Comment()
275282
node.splitText(match.index).replaceWith(dyn)
276283
nodes.push([dyn, parseInt(match[1])])
277284
}
@@ -399,10 +406,21 @@ class ChildPart {
399406

400407
#span
401408
create(node, value) {
402-
this.#span =
403-
node instanceof Span
404-
? new Span(node.parentNode, node.start + this.#childIndex, node.start + this.#childIndex + 1)
405-
: new Span(node, this.#childIndex, this.#childIndex + 1)
409+
assert(this.#childIndex !== undefined)
410+
411+
if (node instanceof Span) {
412+
let child = node.start
413+
assert(child, 'expected a start node')
414+
for (let i = 0; i < this.#childIndex; i++) {
415+
assert(child.nextSibling !== null)
416+
assert(child.nextSibling !== node.end)
417+
child = child.nextSibling
418+
}
419+
this.#span = new Span(node.parentNode, child, child.nextSibling)
420+
} else {
421+
const child = node.childNodes[this.#childIndex]
422+
this.#span = new Span(node, child, child.nextSibling)
423+
}
406424

407425
this.#childIndex = undefined // we only need this once.
408426

@@ -490,17 +508,9 @@ class ChildPart {
490508
let first = this.#roots[i]
491509
let i1 = i
492510
if (first?._key !== key) {
493-
let i2 = this.#roots.findIndex(root => root?._key === key)
511+
const i2 = this.#roots.findIndex(root => root?._key === key)
494512
if (i2 !== -1) {
495-
let second = this.#roots[i2]
496-
497-
if (second.span.start < first.span.start) {
498-
// first must refer to the lower index.
499-
;[first, second] = [second, first]
500-
;[i1, i2] = [i2, i1]
501-
}
502-
assert(first.span.start < second.span.start)
503-
assert(i1 < i2)
513+
const second = this.#roots[i2]
504514

505515
// swap the contents of the spans
506516
const content1 = second.span.extractContents()
@@ -514,19 +524,12 @@ class ChildPart {
514524
// swap the roots
515525
this.#roots[i1] = second
516526
this.#roots[i2] = first
517-
518-
const difference = second.span.length - first.span.length
519-
for (let j = i1 + 1; j <= i2; j++) {
520-
this.#roots[j].span.start += difference
521-
this.#roots[j].span.end += difference
522-
}
523527
}
524528
}
525529
}
526530

527-
const root = (this.#roots[i++] ??= new Root(new Span(this.#span.parentNode, offset, offset)))
531+
const root = (this.#roots[i++] ??= new Root(new Span(this.#span.parentNodeNode, offset, offset)))
528532
root.render(item)
529-
// console.log(offset, root.span.end)
530533
offset = root.span.end
531534

532535
// TODO: make this a weak relationship, because if key is collected, the comparison will always be false.
@@ -564,8 +567,8 @@ class ChildPart {
564567
if (this.#value != null && value !== null && !(this.#value instanceof Node) && !(value instanceof Node)) {
565568
// we previously rendered a string, and we're rendering a string again.
566569
assert(this.#span.length === 1)
567-
assert(this.#span.parentNode.childNodes[this.#span.start] instanceof Text)
568-
this.#span.parentNode.childNodes[this.#span.start].data = value
570+
assert(this.#span.start instanceof Text)
571+
this.#span.start.data = value
569572
} else {
570573
this.#span.deleteContents()
571574
if (value !== null) this.#span.insertNode(value instanceof Node ? value : new Text(value.toString()))

src/types.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,8 @@ export type Key = string | number | bigint | boolean | symbol | object | null
1515

1616
export declare class Span {
1717
parentNode: Node
18-
start: number
19-
end: number
20-
21-
constructor(parentNode: Node, start: number, end: number)
22-
23-
deleteContents(): void
24-
insertNode(node: Node): void
25-
[Symbol.iterator](): IterableIterator<Node>
26-
extractContents(): DocumentFragment
27-
get length(): number
18+
start: Node | null
19+
end: Node | null
2820
}
2921

3022
export interface Part {

0 commit comments

Comments
 (0)