Merge pull request #507 from mnaza/fix/insert-adjacent-html-495
fix(js): case-insensitive insertAdjacentHTML that keeps table/select fragments
This commit is contained in:
@@ -7,4 +7,4 @@ pub mod selector;
|
||||
pub mod serialize;
|
||||
|
||||
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};
|
||||
|
||||
@@ -246,10 +246,14 @@ pub fn parse_html(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"));
|
||||
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();
|
||||
parse_fragment(tree, ParseOpts::default(), context_name, vec![])
|
||||
.from_utf8()
|
||||
|
||||
44
crates/obscura-js/js/bootstrap.js
vendored
44
crates/obscura-js/js/bootstrap.js
vendored
@@ -24,7 +24,7 @@
|
||||
'_isSpecialScheme', '_applyDocQueryEncoding', '_anchorBase',
|
||||
'_elemHrefURL', '_setElemHrefPart', '_pad', '_daysInMonth',
|
||||
'_isoWeek1Monday', '_inputParseNumber', '_inputFormatNumber',
|
||||
'_htmlAttrName', '_convertNodes', '_elementClassFor', '_wrap', '_wrapEl',
|
||||
'_htmlAttrName', '_convertNodes', '_parseHTMLFragment', '_elementClassFor', '_wrap', '_wrapEl',
|
||||
'_resolveUrl', '_registerIframe', '_base64ToUint8Array',
|
||||
'_bodyToUint8Array', '_arrayBufferFromBytes',
|
||||
'_installWasmStreamingFallback', '_urlParseOp', '_urlSetOp',
|
||||
@@ -1329,6 +1329,23 @@ function _isSubmitButton(el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse an HTML string into detached nodes using the actual insertion element
|
||||
// as html5ever's fragment context. This preserves table/select parsing rules,
|
||||
// comments, text-node order, and foreign-content namespaces without a wrap map.
|
||||
function _parseHTMLFragment(html, context) {
|
||||
html = String(html == null ? '' : html);
|
||||
const ns = context && context.nodeType === 1 ? context.namespaceURI : null;
|
||||
const tag = context && context.nodeType === 1 ? context.localName : 'body';
|
||||
const tmp = ns && ns !== 'http://www.w3.org/1999/xhtml'
|
||||
? document.createElementNS(ns, tag)
|
||||
: document.createElement(tag);
|
||||
tmp.innerHTML = html;
|
||||
const out = [];
|
||||
let child;
|
||||
while ((child = tmp.firstChild)) out.push(tmp.removeChild(child));
|
||||
return out;
|
||||
}
|
||||
|
||||
class Element extends Node {
|
||||
constructor(nid) {
|
||||
super(nid);
|
||||
@@ -1583,20 +1600,33 @@ class Element extends Node {
|
||||
return null;
|
||||
}
|
||||
insertAdjacentHTML(position, html) {
|
||||
// Position is matched ASCII-case-insensitively; an unknown value throws
|
||||
// SyntaxError (both were silent no-ops before). Sibling insertions parse
|
||||
// against the parent's context, child insertions against this element, so
|
||||
// table/select fragments keep the right parsing context (_parseHTMLFragment).
|
||||
const pos = String(position).toLowerCase();
|
||||
const parent = this.parentNode;
|
||||
switch (position) {
|
||||
const context = (pos === 'beforebegin' || pos === 'afterend') ? parent : this;
|
||||
switch (pos) {
|
||||
case 'beforebegin':
|
||||
if (parent) { const tmp = document.createElement('div'); tmp.innerHTML = html; const children = tmp.childNodes; for (let i = 0; i < children.length; i++) parent.insertBefore(children[i], this); }
|
||||
if (parent) for (const n of _parseHTMLFragment(html, context)) parent.insertBefore(n, this);
|
||||
break;
|
||||
case 'afterbegin':
|
||||
{ const tmp = document.createElement('div'); tmp.innerHTML = html; const children = tmp.childNodes; const first = this.firstChild; for (let i = children.length - 1; i >= 0; i--) this.insertBefore(children[i], first); }
|
||||
case 'afterbegin': {
|
||||
const first = this.firstChild;
|
||||
for (const n of _parseHTMLFragment(html, context)) this.insertBefore(n, first);
|
||||
break;
|
||||
}
|
||||
case 'beforeend':
|
||||
{ const tmp = document.createElement('div'); tmp.innerHTML = html; const children = tmp.childNodes; for (let i = 0; i < children.length; i++) this.appendChild(children[i]); }
|
||||
for (const n of _parseHTMLFragment(html, context)) this.appendChild(n);
|
||||
break;
|
||||
case 'afterend':
|
||||
if (parent) { const tmp = document.createElement('div'); tmp.innerHTML = html; const children = tmp.childNodes; const next = this.nextSibling; for (let i = 0; i < children.length; i++) parent.insertBefore(children[i], next); }
|
||||
if (parent) { const next = this.nextSibling; for (const n of _parseHTMLFragment(html, context)) parent.insertBefore(n, next); }
|
||||
break;
|
||||
default:
|
||||
throw new DOMException(
|
||||
"Failed to execute 'insertAdjacentHTML' on 'Element': The value provided ('" + position + "') is not one of 'beforeBegin', 'afterBegin', 'beforeEnd', or 'afterEnd'.",
|
||||
"SyntaxError"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Like insertAdjacentHTML but inserts a Text node instead of parsing markup,
|
||||
|
||||
@@ -391,7 +391,16 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
dom.detach(child);
|
||||
}
|
||||
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();
|
||||
dom.import_children_from(target, &fragment, import_root);
|
||||
}
|
||||
|
||||
@@ -1549,6 +1549,61 @@ mod tests {
|
||||
assert_eq!(out, serde_json::json!("1|OPTION|choice"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_adjacent_html_parses_table_fragments() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<html><body><table id="t"><tbody id="tb"></tbody></table></body></html>"#,
|
||||
);
|
||||
let out = rt
|
||||
.evaluate("(function(){var tb=document.getElementById('tb'); tb.insertAdjacentHTML('beforeend','<tr><td>1</td><td>2</td></tr>'); var tr=tb.firstElementChild; return tr ? (tr.tagName+':'+tr.children.length) : 'NULL';})()")
|
||||
.unwrap();
|
||||
assert_eq!(out, serde_json::json!("TR:2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_adjacent_html_position_is_case_insensitive() {
|
||||
let mut rt = setup_runtime(r#"<html><body><div id="host"><span>base</span></div></body></html>"#);
|
||||
let out = rt
|
||||
.evaluate("(function(){var h=document.getElementById('host'); h.insertAdjacentHTML('BeforeEnd','<b>x</b>'); return h.lastElementChild ? h.lastElementChild.tagName : 'NULL';})()")
|
||||
.unwrap();
|
||||
assert_eq!(out, serde_json::json!("B"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_adjacent_html_rejects_invalid_position() {
|
||||
let mut rt = setup_runtime(r#"<html><body><div id="host"></div></body></html>"#);
|
||||
let out = rt
|
||||
.evaluate("(function(){var h=document.getElementById('host'); try { h.insertAdjacentHTML('nope','<b>x</b>'); return 'no-throw'; } catch(e){ return e.name; }})()")
|
||||
.unwrap();
|
||||
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]
|
||||
fn test_document_title() {
|
||||
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");
|
||||
|
||||
Reference in New Issue
Block a user