Skip to content

Commit 7c8cbf2

Browse files
committed
feat: parse element innerhtml fragments
1 parent 1e0fe5a commit 7c8cbf2

3 files changed

Lines changed: 119 additions & 2 deletions

File tree

crates/rom-runtime/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ mod tests_fragments;
2323
#[cfg(test)]
2424
mod tests_history;
2525
#[cfg(test)]
26+
mod tests_innerhtml;
27+
#[cfg(test)]
2628
mod tests_layout_observers;
2729
#[cfg(test)]
2830
mod tests_messaging;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use crate::{RomRuntime, RuntimeConfig};
2+
3+
#[test]
4+
fn supports_innerhtml_fragment_parsing_and_serialization() {
5+
let runtime = RomRuntime::new(RuntimeConfig::default()).unwrap();
6+
let result = runtime
7+
.eval_async_as_string(
8+
r##"
9+
(async () => {
10+
const host = document.createElement("div");
11+
host.innerHTML = '<span class="greeting">Hello</span><strong data-kind="accent">world</strong>';
12+
13+
const first = host.firstChild;
14+
const second = host.lastChild;
15+
const initial = {
16+
childCount: host.childNodes.length,
17+
firstTag: first.tagName,
18+
firstClass: first.className,
19+
firstText: first.textContent,
20+
secondTag: second.tagName,
21+
secondAttr: second.getAttribute("data-kind"),
22+
secondText: second.textContent,
23+
serialized: host.innerHTML,
24+
};
25+
26+
host.innerHTML = '<em data-state="ready">updated</em>';
27+
28+
return JSON.stringify({
29+
initial,
30+
replaced: {
31+
childCount: host.childNodes.length,
32+
firstTag: host.firstChild.tagName,
33+
attr: host.firstChild.getAttribute("data-state"),
34+
text: host.textContent,
35+
serialized: host.innerHTML,
36+
},
37+
});
38+
})()
39+
"##,
40+
)
41+
.unwrap();
42+
43+
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
44+
assert_eq!(value["initial"]["childCount"], 2);
45+
assert_eq!(value["initial"]["firstTag"], "SPAN");
46+
assert_eq!(value["initial"]["firstClass"], "greeting");
47+
assert_eq!(value["initial"]["firstText"], "Hello");
48+
assert_eq!(value["initial"]["secondTag"], "STRONG");
49+
assert_eq!(value["initial"]["secondAttr"], "accent");
50+
assert_eq!(value["initial"]["secondText"], "world");
51+
assert_eq!(
52+
value["initial"]["serialized"],
53+
"<span class=\"greeting\">Hello</span><strong data-kind=\"accent\">world</strong>"
54+
);
55+
56+
assert_eq!(value["replaced"]["childCount"], 1);
57+
assert_eq!(value["replaced"]["firstTag"], "EM");
58+
assert_eq!(value["replaced"]["attr"], "ready");
59+
assert_eq!(value["replaced"]["text"], "updated");
60+
assert_eq!(
61+
value["replaced"]["serialized"],
62+
"<em data-state=\"ready\">updated</em>"
63+
);
64+
}

crates/rom-webapi/src/js/bootstrap_dom.js

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,51 @@
1616
}
1717
}
1818

19+
function escapeHtmlText(value) {
20+
return String(value)
21+
.replace(/&/g, "&amp;")
22+
.replace(/</g, "&lt;")
23+
.replace(/>/g, "&gt;");
24+
}
25+
26+
function escapeHtmlAttribute(value) {
27+
return escapeHtmlText(value).replace(/"/g, "&quot;");
28+
}
29+
30+
function serializeNodeToHtml(node) {
31+
if (!node) {
32+
return "";
33+
}
34+
35+
if (node.nodeType === 3) {
36+
return escapeHtmlText(node.textContent ?? "");
37+
}
38+
39+
if (node.nodeType === 11 || node.nodeType === 9) {
40+
return node.childNodes.map((child) => serializeNodeToHtml(child)).join("");
41+
}
42+
43+
if (node.nodeType !== 1) {
44+
return "";
45+
}
46+
47+
const tagName = node.tagName.toLowerCase();
48+
const attributes = Array.from(node.attributes.entries())
49+
.map(([name, value]) => ` ${name}="${escapeHtmlAttribute(value)}"`)
50+
.join("");
51+
const content = node.childNodes.map((child) => serializeNodeToHtml(child)).join("");
52+
return `<${tagName}${attributes}>${content}</${tagName}>`;
53+
}
54+
55+
function parseHtmlFragment(source) {
56+
const fragmentDocument = new Document();
57+
const parsed = parseMarkup(fragmentDocument, String(source ?? ""), false);
58+
if (parsed.error) {
59+
return [fragmentDocument.createTextNode(String(source ?? "").replace(/<[^>]*>/g, ""))];
60+
}
61+
return parsed.nodes;
62+
}
63+
1964
class Node extends EventTarget {
2065
constructor(nodeType, nodeName) {
2166
super();
@@ -242,11 +287,17 @@
242287
}
243288

244289
get innerHTML() {
245-
return this.textContent;
290+
return this.childNodes.map((child) => serializeNodeToHtml(child)).join("");
246291
}
247292

248293
set innerHTML(value) {
249-
this.textContent = value;
294+
while (this.childNodes.length > 0) {
295+
this.removeChild(this.childNodes[this.childNodes.length - 1]);
296+
}
297+
298+
for (const node of parseHtmlFragment(value)) {
299+
this.appendChild(node);
300+
}
250301
}
251302

252303
querySelector(selector) {

0 commit comments

Comments
 (0)