diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 4a0f53f..3ce7268 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -584,6 +584,29 @@ const _styleProxy = (decl) => new Proxy(decl, { }, }); +// Clone a single node (no children), used by Node.cloneNode. Elements are built +// with createElement/createElementNS and their content attributes copied, so no +// HTML parsing context is involved and every attribute (including style) is +// preserved. Text/Comment/DocumentFragment map to their factory; anything else +// yields null. +function _shallowCloneNode(node) { + const nt = node.nodeType; + if (nt === 3) return document.createTextNode(node.data != null ? node.data : (node.textContent || "")); + if (nt === 8) return document.createComment(node.data != null ? node.data : (node.nodeValue || "")); + if (nt === 11) return document.createDocumentFragment(); + if (nt !== 1) return null; + const ns = node.namespaceURI; + const el = (ns && ns !== "http://www.w3.org/1999/xhtml") + ? document.createElementNS(ns, node.nodeName) + : document.createElement(node.localName || node.nodeName.toLowerCase()); + const names = node.getAttributeNames ? node.getAttributeNames() : []; + for (const name of names) { + const v = node.getAttribute(name); + if (v !== null) el.setAttribute(name, v); + } + return el; +} + class Node { static ELEMENT_NODE = 1; static ATTRIBUTE_NODE = 2; @@ -774,32 +797,38 @@ class Node { contains(o) { return o ? _dom("contains", this._nid, o._nid) === "true" : false; } hasChildNodes() { return _dom("has_child_nodes", this._nid) === "true"; } cloneNode(deep) { - const t = this.nodeType; - if (t === 1) { - if (deep) { - const wrapper = document.createElement('div'); - wrapper.innerHTML = _domParse("outer_html", this._nid) || ""; - const clone = wrapper.firstChild; - return clone; - } - const el = document.createElement(this.nodeName.toLowerCase()); - const html = _domParse("outer_html", this._nid) || ""; - const attrMatch = html.match(/^<[a-zA-Z][^\s>]*([\s\S]*?)>/); - if (attrMatch && attrMatch[1]) { - const attrStr = attrMatch[1].trim(); - const re = /([a-zA-Z_:][a-zA-Z0-9_.:-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g; - let m; - while ((m = re.exec(attrStr)) !== null) { - const name = m[1]; - const val = m[2] !== undefined ? m[2] : m[3] !== undefined ? m[3] : m[4] || ""; - if (name !== this.nodeName.toLowerCase()) el.setAttribute(name, val); + // Clone structurally via real DOM nodes rather than round-tripping through a + // throwaway