fix(js): parse fragments in their insertion context
This commit is contained in:
1 parent
35fa61032b
commit
a2fd4d1735
5 files changed
+59
-46
No files matched your search
@@ -7,4 +7,4 @@ pub mod selector;
|
|||||||
pub mod serialize;
|
pub mod serialize;
|
||||||
|
|
||||||
pub use tree::{Attribute, DomTree, Node, NodeData, NodeId};
|
pub use tree::{Attribute, DomTree, Node, NodeData, NodeId};
|
||||||
pub use tree_sink::{parse_html, parse_fragment};
|
pub use tree_sink::{parse_fragment, parse_fragment_with_context, parse_html};
|
||||||
@@ -243,10 +243,14 @@ pub fn parse_html(html: &str) -> DomTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_fragment(html: &str) -> DomTree {
|
pub fn parse_fragment(html: &str) -> DomTree {
|
||||||
use html5ever::tendril::TendrilSink;
|
|
||||||
use html5ever::{parse_fragment, ParseOpts, QualName};
|
|
||||||
|
|
||||||
let context_name = QualName::new(None, ns!(html), local_name!("body"));
|
let context_name = QualName::new(None, ns!(html), local_name!("body"));
|
||||||
|
parse_fragment_with_context(html, context_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_fragment_with_context(html: &str, context_name: QualName) -> DomTree {
|
||||||
|
use html5ever::tendril::TendrilSink;
|
||||||
|
use html5ever::{parse_fragment, ParseOpts};
|
||||||
|
|
||||||
let tree = DomTree::new();
|
let tree = DomTree::new();
|
||||||
parse_fragment(tree, ParseOpts::default(), context_name, vec![])
|
parse_fragment(tree, ParseOpts::default(), context_name, vec![])
|
||||||
.from_utf8()
|
.from_utf8()
|
||||||
|
|||||||
Vendored
+15
-41
@@ -1227,44 +1227,20 @@ function _isSubmitButton(el) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ancestor chains required to parse table/select children, which the fragment
|
// Parse an HTML string into detached nodes using the actual insertion element
|
||||||
// parser drops when parsed loosely (it uses a fixed <body> context). Each entry
|
// as html5ever's fragment context. This preserves table/select parsing rules,
|
||||||
// is [openTags, closeTags, depth] — depth is how many lastChild hops reach the
|
// comments, text-node order, and foreign-content namespaces without a wrap map.
|
||||||
// container holding the parsed rows/cells/options. Classic jQuery wrapMap.
|
function _parseHTMLFragment(html, context) {
|
||||||
const _wrapMap = {
|
|
||||||
option: ['<select multiple="">', '</select>', 1],
|
|
||||||
optgroup: ['<select multiple="">', '</select>', 1],
|
|
||||||
tr: ['<table><tbody>', '</tbody></table>', 2],
|
|
||||||
td: ['<table><tbody><tr>', '</tr></tbody></table>', 3],
|
|
||||||
th: ['<table><tbody><tr>', '</tr></tbody></table>', 3],
|
|
||||||
tbody: ['<table>', '</table>', 1],
|
|
||||||
thead: ['<table>', '</table>', 1],
|
|
||||||
tfoot: ['<table>', '</table>', 1],
|
|
||||||
caption: ['<table>', '</table>', 1],
|
|
||||||
colgroup: ['<table>', '</table>', 1],
|
|
||||||
col: ['<table><colgroup>', '</colgroup></table>', 2],
|
|
||||||
};
|
|
||||||
// Parse an HTML string into an array of detached nodes. Table/select fragments
|
|
||||||
// are wrapped in the required ancestor chain and dug back out so their children
|
|
||||||
// survive; the wrapper is chosen from the fragment's own leading tag, falling
|
|
||||||
// back to the insertion context element's tag.
|
|
||||||
function _parseHTMLFragment(html, contextTag) {
|
|
||||||
html = String(html == null ? '' : html);
|
html = String(html == null ? '' : html);
|
||||||
const m = /^\s*<\s*([a-zA-Z][a-zA-Z0-9]*)/.exec(html);
|
const ns = context && context.nodeType === 1 ? context.namespaceURI : null;
|
||||||
const lead = m ? m[1].toLowerCase() : '';
|
const tag = context && context.nodeType === 1 ? context.localName : 'body';
|
||||||
const key = _wrapMap[lead] ? lead : (contextTag ? String(contextTag).toLowerCase() : '');
|
const tmp = ns && ns !== 'http://www.w3.org/1999/xhtml'
|
||||||
const wrap = _wrapMap[key];
|
? document.createElementNS(ns, tag)
|
||||||
const tmp = document.createElement('div');
|
: document.createElement(tag);
|
||||||
let container = tmp;
|
|
||||||
if (wrap) {
|
|
||||||
tmp.innerHTML = wrap[0] + html + wrap[1];
|
|
||||||
for (let d = 0; d < wrap[2]; d++) container = container.lastChild || container;
|
|
||||||
} else {
|
|
||||||
tmp.innerHTML = html;
|
tmp.innerHTML = html;
|
||||||
}
|
|
||||||
const out = [];
|
const out = [];
|
||||||
let child;
|
let child;
|
||||||
while ((child = container.firstChild)) out.push(container.removeChild(child));
|
while ((child = tmp.firstChild)) out.push(tmp.removeChild(child));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1526,23 +1502,21 @@ class Element extends Node {
|
|||||||
// table/select fragments keep the right parsing context (_parseHTMLFragment).
|
// table/select fragments keep the right parsing context (_parseHTMLFragment).
|
||||||
const pos = String(position).toLowerCase();
|
const pos = String(position).toLowerCase();
|
||||||
const parent = this.parentNode;
|
const parent = this.parentNode;
|
||||||
const contextTag = (pos === 'beforebegin' || pos === 'afterend')
|
const context = (pos === 'beforebegin' || pos === 'afterend') ? parent : this;
|
||||||
? (parent && parent.nodeType === 1 ? parent.tagName : 'body')
|
|
||||||
: this.tagName;
|
|
||||||
switch (pos) {
|
switch (pos) {
|
||||||
case 'beforebegin':
|
case 'beforebegin':
|
||||||
if (parent) for (const n of _parseHTMLFragment(html, contextTag)) parent.insertBefore(n, this);
|
if (parent) for (const n of _parseHTMLFragment(html, context)) parent.insertBefore(n, this);
|
||||||
break;
|
break;
|
||||||
case 'afterbegin': {
|
case 'afterbegin': {
|
||||||
const first = this.firstChild;
|
const first = this.firstChild;
|
||||||
for (const n of _parseHTMLFragment(html, contextTag)) this.insertBefore(n, first);
|
for (const n of _parseHTMLFragment(html, context)) this.insertBefore(n, first);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'beforeend':
|
case 'beforeend':
|
||||||
for (const n of _parseHTMLFragment(html, contextTag)) this.appendChild(n);
|
for (const n of _parseHTMLFragment(html, context)) this.appendChild(n);
|
||||||
break;
|
break;
|
||||||
case 'afterend':
|
case 'afterend':
|
||||||
if (parent) { const next = this.nextSibling; for (const n of _parseHTMLFragment(html, contextTag)) parent.insertBefore(n, next); }
|
if (parent) { const next = this.nextSibling; for (const n of _parseHTMLFragment(html, context)) parent.insertBefore(n, next); }
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new DOMException(
|
throw new DOMException(
|
||||||
|
|||||||
@@ -391,7 +391,16 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
|||||||
dom.detach(child);
|
dom.detach(child);
|
||||||
}
|
}
|
||||||
if !arg2.is_empty() {
|
if !arg2.is_empty() {
|
||||||
let fragment = obscura_dom::parse_fragment(&arg2);
|
let context_name = dom
|
||||||
|
.with_node(target, |node| match &node.data {
|
||||||
|
NodeData::Element { name, .. } => Some(name.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
let fragment = match context_name {
|
||||||
|
Some(name) => obscura_dom::parse_fragment_with_context(&arg2, name),
|
||||||
|
None => obscura_dom::parse_fragment(&arg2),
|
||||||
|
};
|
||||||
let import_root = fragment.find_body_or_root();
|
let import_root = fragment.find_body_or_root();
|
||||||
dom.import_children_from(target, &fragment, import_root);
|
dom.import_children_from(target, &fragment, import_root);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1346,6 +1346,32 @@ mod tests {
|
|||||||
assert_eq!(out, serde_json::json!("SyntaxError"));
|
assert_eq!(out, serde_json::json!("SyntaxError"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_adjacent_html_keeps_leading_comments_in_table_contexts() {
|
||||||
|
let mut rt = setup_runtime(
|
||||||
|
r#"<html><body><table><tbody id="tb"><tr id="row"></tr></tbody></table></body></html>"#,
|
||||||
|
);
|
||||||
|
let out = rt
|
||||||
|
.evaluate(
|
||||||
|
"(function(){var tb=document.getElementById('tb');tb.insertAdjacentHTML('beforeend','<!--m--><tr><td>v</td></tr>');var row=document.getElementById('row');row.insertAdjacentHTML('beforeend','<!--n--><td>x</td>');return Array.from(tb.childNodes).map(function(n){return n.nodeName}).join('|')+';'+Array.from(row.childNodes).map(function(n){return n.nodeName}).join('|');})()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(out, serde_json::json!("TR|#comment|TR;#comment|TD"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_adjacent_html_uses_the_insertion_element_as_context() {
|
||||||
|
let mut rt = setup_runtime(
|
||||||
|
r#"<html><body><div id="d"></div><table id="table"><tbody id="tb"></tbody></table></body></html>"#,
|
||||||
|
);
|
||||||
|
let out = rt
|
||||||
|
.evaluate(
|
||||||
|
"(function(){var d=document.getElementById('d');d.insertAdjacentHTML('beforeend','<tr><td>v</td></tr>');var table=document.getElementById('table');table.insertAdjacentHTML('beforeend','<tr><td>x</td></tr>');var tb=document.getElementById('tb');tb.insertAdjacentHTML('beforeend','<tr><td>y</td></tr>tail');return d.firstChild.nodeName+':'+d.textContent+';'+table.lastElementChild.tagName+';'+Array.from(tb.childNodes).map(function(n){return n.nodeName+(n.data?':'+n.data:'')}).join('|');})()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(out, serde_json::json!("#text:v;TBODY;TR|#text:tail"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_document_title() {
|
fn test_document_title() {
|
||||||
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");
|
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");
|
||||||
|
|||||||
Reference in new issue
Block a user