Skip to content

Commit 054994e

Browse files
committed
0.0.1 initial attempt
1 parent 793a94a commit 054994e

File tree

3 files changed

+312
-1
lines changed

3 files changed

+312
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
# dom-diff
1+
# dom-diff
2+
utilities for diffing html doms

index.js

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
2+
async function create_dom_diff(html) {
3+
if (!create_dom_diff.parser_p) create_dom_diff.parser_p = new Promise(async (done) => {
4+
const Parser = window.TreeSitter
5+
await Parser.init()
6+
let parser = new Parser()
7+
parser.setLanguage(await Parser.Language.load("https://bloop.monster/tree-sitter-html.wasm"))
8+
done(parser)
9+
})
10+
11+
let parser = await create_dom_diff.parser_p
12+
let tree = parser.parse(html)
13+
14+
return {
15+
patch: (...args) => {
16+
let [start, end, content, start_row_col, end_row_col, end2_row_col] = args
17+
18+
let new_html
19+
let end2
20+
if (args.length == 1) {
21+
new_html = start
22+
let [s, e, e2] = findDiffRange(html, new_html)
23+
start = s
24+
end = e
25+
end2 = e2
26+
content = new_html.slice(s, e2)
27+
} else {
28+
new_html = html.slice(0, start) + content + html.slice(end)
29+
end2 = start + content.length
30+
}
31+
32+
if (!start_row_col) start_row_col = getRowColumn(html, start)
33+
if (!end_row_col) end_row_col = getRowColumn(html, end)
34+
if (!end2_row_col) end2_row_col = getRowColumn(new_html, end2)
35+
36+
let tree_copy = tree.copy()
37+
tree_copy.edit({
38+
startIndex: start,
39+
oldEndIndex: end,
40+
newEndIndex: end2,
41+
startPosition: start_row_col,
42+
oldEndPosition: end_row_col,
43+
newEndPosition: end2_row_col,
44+
})
45+
46+
let new_tree = parser.parse(new_html, tree_copy)
47+
48+
let diff = get_tree_diff(tree, tree_copy, new_tree, html, new_html)
49+
50+
html = new_html
51+
tree = new_tree
52+
53+
return diff
54+
},
55+
}
56+
57+
function get_tree_diff(o, a, b, a_text, b_text) {
58+
let path = []
59+
let diff = []
60+
o = o.rootNode
61+
a = a.rootNode
62+
b = b.rootNode
63+
get_element_diff(o, a, b, path, diff, a_text, b_text)
64+
return diff
65+
}
66+
67+
function get_element_diff(o, a, b, path, diff, a_text, b_text) {
68+
let padding = o.type == "element" ? 1 : 0
69+
70+
let o_children = []
71+
let a_children = []
72+
73+
for (let i = padding; i < o.childCount; i++) {
74+
let oo = o.child(i)
75+
let aa = a.child(i)
76+
if (oo.previousSibling?.endIndex < oo.startIndex) {
77+
o_children.push({
78+
type: "text",
79+
startIndex: oo.previousSibling.endIndex,
80+
text: a_text.slice(oo.previousSibling.endIndex, oo.startIndex),
81+
})
82+
a_children.push({
83+
type: "text",
84+
startIndex: aa.previousSibling.endIndex,
85+
text: b_text.slice(aa.previousSibling.endIndex, aa.startIndex),
86+
})
87+
}
88+
if (i < o.childCount - padding) {
89+
o_children.push(oo)
90+
a_children.push(aa)
91+
}
92+
}
93+
if (!padding) {
94+
let x = o.childCount ? o.child(o.childCount - 1).endIndex : 0
95+
if (x < o.endIndex) {
96+
o_children.push({ type: "text", startIndex: x, text: a_text.slice(x, o.endIndex) })
97+
let y = a.child(a.childCount - 1).endIndex
98+
a_children.push({ type: "text", startIndex: y, text: b_text.slice(y, a.endIndex) })
99+
}
100+
}
101+
102+
let b_children = []
103+
104+
for (let i = padding; i < b.childCount; i++) {
105+
let bb = b.child(i)
106+
if (bb.previousSibling?.endIndex < bb.startIndex) {
107+
b_children.push({
108+
type: "text",
109+
startIndex: bb.previousSibling.endIndex,
110+
text: b_text.slice(bb.previousSibling.endIndex, bb.startIndex),
111+
})
112+
}
113+
if (i < b.childCount - padding) b_children.push(bb)
114+
}
115+
if (!padding) {
116+
let x = b.childCount ? b.child(b.childCount - 1).endIndex : 0
117+
if (x < b.endIndex) {
118+
b_children.push({ type: "text", startIndex: x, text: b_text.slice(x, b.endIndex) })
119+
}
120+
}
121+
122+
o_children = mergeNeighboringNodes(o_children)
123+
for (let i = 0; i < o_children.length; i++) {
124+
let oo = o_children[i]
125+
if (oo.mergedNodes > 1) {
126+
let aa = a_children.splice(i, oo.mergedNodes)
127+
a_children.splice(i, 0, {
128+
type: "text",
129+
startIndex: aa[0].startIndex,
130+
text: aa.map((x) => x.text).join(""),
131+
})
132+
}
133+
}
134+
b_children = mergeNeighboringNodes(b_children)
135+
136+
let i = 0
137+
let j = 0
138+
let path_join = () => path.join("/") + (path.length ? "/" : "")
139+
while (true) {
140+
let oo = o_children[i]
141+
let aa = a_children[i]
142+
let bb = b_children[j]
143+
144+
if (!aa && !bb) break
145+
else if (aa && bb && same_node(oo, aa, bb)) {
146+
if (aa.type == "element") {
147+
path.push(`${aa.type}[${i}]`)
148+
get_element_diff(oo, aa, bb, path, diff, a_text, b_text)
149+
path.pop()
150+
}
151+
i++
152+
j++
153+
} else if (aa && (!bb || aa.startIndex <= bb.startIndex)) {
154+
diff.push({ range: `/${path_join("/")}${aa.type}[${i}]` })
155+
i++
156+
} else {
157+
diff.push({ range: `/${path_join("/")}*[${i}:${i}]`, content: bb.text })
158+
j++
159+
}
160+
}
161+
}
162+
163+
function mergeNeighboringNodes(nodes) {
164+
const mergedNodes = []
165+
let currentText = ""
166+
let currentStartIndex = null
167+
let current_count = 0
168+
169+
for (let i = 0; i < nodes.length; i++) {
170+
const node = nodes[i]
171+
172+
if (node.type === "text" || node.type === "entity") {
173+
if (currentStartIndex === null) {
174+
currentStartIndex = node.startIndex
175+
current_count = 0
176+
}
177+
currentText += node.text
178+
current_count++
179+
} else {
180+
if (currentText !== "") {
181+
mergedNodes.push({ mergedNodes: current_count, type: "text", startIndex: currentStartIndex, text: currentText })
182+
currentText = ""
183+
currentStartIndex = null
184+
}
185+
mergedNodes.push(node)
186+
}
187+
}
188+
189+
if (currentText !== "") {
190+
mergedNodes.push({ mergedNodes: current_count, type: "text", startIndex: currentStartIndex, text: currentText })
191+
}
192+
193+
return mergedNodes
194+
}
195+
196+
function getRowColumn(text, position) {
197+
let row = 0
198+
let column = 0
199+
let currentPosition = 0
200+
201+
for (let i = 0; i < text.length; i++) {
202+
if (i === position) {
203+
break
204+
}
205+
206+
if (text[i] === "\n") {
207+
row++
208+
column = 0
209+
} else {
210+
column++
211+
}
212+
213+
currentPosition++
214+
}
215+
216+
return { row, column }
217+
}
218+
219+
function same_node(o, a, b) {
220+
if (a.type == "text" && b.type == "text") {
221+
return a.startIndex == b.startIndex && o.text == b.text
222+
} else if (a.type == "element" && b.type == "element") {
223+
let o_start_tag = o.child(0)
224+
let a_start_tag = a.child(0)
225+
let b_start_tag = b.child(0)
226+
let o_end_tag = o.child(o.childCount - 1)
227+
let a_end_tag = a.child(a.childCount - 1)
228+
let b_end_tag = b.child(b.childCount - 1)
229+
return (
230+
a_start_tag.startIndex == b_start_tag.startIndex &&
231+
o_start_tag.text == b_start_tag.text &&
232+
a_end_tag.startIndex == b_end_tag.startIndex &&
233+
o_end_tag.text == b_end_tag.text
234+
)
235+
}
236+
}
237+
238+
function findDiffRange(oldText, newText) {
239+
let prefixLength = 0
240+
let oldSuffixStart = oldText.length
241+
let newSuffixStart = newText.length
242+
243+
while (prefixLength < oldSuffixStart && prefixLength < newSuffixStart && oldText[prefixLength] === newText[prefixLength]) {
244+
prefixLength++
245+
}
246+
247+
while (oldSuffixStart > prefixLength && newSuffixStart > prefixLength && oldText[oldSuffixStart - 1] === newText[newSuffixStart - 1]) {
248+
oldSuffixStart--
249+
newSuffixStart--
250+
}
251+
252+
return [prefixLength, oldSuffixStart, newSuffixStart]
253+
}
254+
}
255+
256+
function apply_dom_diff(dom, diff) {
257+
let offsets = new Map()
258+
259+
diff.forEach((change) => {
260+
const [path, newValue] = [change.range, change.content]
261+
const indexes = []
262+
let insert_position = null
263+
path.replace(/\[(\d+)(?::(\d+))?\]/g, (_0, _1, _2) => {
264+
if (_2 != null) {
265+
insert_position = 1 * _2
266+
} else indexes.push(1 * _1)
267+
})
268+
if (insert_position == null) insert_position = indexes.pop()
269+
270+
let node = dom
271+
272+
for (let i = 0; i < indexes.length; i++) {
273+
node = Array.from(node.childNodes)[indexes[i]]
274+
}
275+
276+
const i = insert_position + (offsets.get(node) ?? 0)
277+
278+
if (newValue) {
279+
let newElement = document.createElement("div")
280+
newElement.innerHTML = newValue
281+
newElement = newElement.firstChild
282+
283+
if (i === node.childNodes.length) {
284+
// If the insertion index is equal to the number of child nodes,
285+
// append the new element as the last child
286+
node.appendChild(newElement)
287+
} else {
288+
// Otherwise, insert the new element at the specified index
289+
node.insertBefore(newElement, node.childNodes[i])
290+
}
291+
292+
offsets.set(node, (offsets.get(node) ?? 0) + 1)
293+
} else {
294+
// If newValue is falsy, remove the child node at the specified index
295+
if (i >= node.childNodes.length) throw "bad"
296+
node.removeChild(node.childNodes[i])
297+
offsets.set(node, (offsets.get(node) ?? 0) - 1)
298+
}
299+
})
300+
}

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "@braidjs/dom-diff",
3+
"version": "0.0.1",
4+
"description": "utilities for diffing html doms",
5+
"author": "Braid Working Group",
6+
"repository": "braid-org/dom-diff",
7+
"homepage": "https://braid.org",
8+
"dependencies": {
9+
}
10+
}

0 commit comments

Comments
 (0)