fix(dom): preserve namespaced attribute qualified names
This commit is contained in:
1 parent
1e305135e3
commit
76e5280c32
5 files changed
+96
-27
No files matched your search
@@ -99,8 +99,11 @@ impl DomTree {
|
||||
buf.push_str(tag);
|
||||
for attr in attrs {
|
||||
buf.push(' ');
|
||||
let attr_name = attr.name.local.as_ref();
|
||||
buf.push_str(attr_name);
|
||||
if let Some(prefix) = &attr.name.prefix {
|
||||
buf.push_str(prefix);
|
||||
buf.push(':');
|
||||
}
|
||||
buf.push_str(attr.name.local.as_ref());
|
||||
buf.push_str("=\"");
|
||||
escape_attr(&attr.value, buf);
|
||||
buf.push('"');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use html5ever::{LocalName, Namespace, QualName};
|
||||
use html5ever::{LocalName, Namespace, Prefix, QualName};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
@@ -32,13 +32,24 @@ pub struct Attribute {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// The local-name portion of an attribute name, i.e. the part after any prefix.
|
||||
/// `"xlink:href"` -> `"href"`, `"href"` -> `"href"`. Used for namespace-aware
|
||||
/// attribute lookup, which keys on `(namespace, localName)`.
|
||||
fn attr_local_suffix(name: &str) -> &str {
|
||||
match name.rfind(':') {
|
||||
Some(i) => &name[i + 1..],
|
||||
None => name,
|
||||
impl Attribute {
|
||||
pub fn qualified_name(&self) -> String {
|
||||
match &self.name.prefix {
|
||||
Some(prefix) => format!("{}:{}", prefix, self.name.local),
|
||||
None => self.name.local.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn qualified_name_eq(&self, name: &str) -> bool {
|
||||
match &self.name.prefix {
|
||||
Some(prefix) => {
|
||||
name.len() == prefix.len() + self.name.local.len() + 1
|
||||
&& name.starts_with(prefix.as_ref())
|
||||
&& name.as_bytes().get(prefix.len()) == Some(&b':')
|
||||
&& &name[prefix.len() + 1..] == self.name.local.as_ref()
|
||||
}
|
||||
None => self.name.local.as_ref() == name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +126,7 @@ impl Node {
|
||||
|
||||
pub fn get_attribute(&self, name: &str) -> Option<&str> {
|
||||
self.attrs()?.iter().find_map(|a| {
|
||||
if a.name.local.as_ref() == name {
|
||||
if a.qualified_name_eq(name) {
|
||||
Some(a.value.as_str())
|
||||
} else {
|
||||
None
|
||||
@@ -136,13 +147,10 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
// Read a namespaced attribute by (namespace, localName). The stored local
|
||||
// name may be qualified (e.g. "xlink:href" for setAttributeNS-created attrs)
|
||||
// or a bare local (e.g. "href" for parser-created ones), so compare against
|
||||
// the part after any prefix.
|
||||
// Read a namespaced attribute by (namespace, localName).
|
||||
pub fn get_attribute_ns(&self, ns: &str, local: &str) -> Option<&str> {
|
||||
self.attrs()?.iter().find_map(|a| {
|
||||
if a.name.ns.as_ref() == ns && attr_local_suffix(a.name.local.as_ref()) == local {
|
||||
if a.name.ns.as_ref() == ns && a.name.local.as_ref() == local {
|
||||
Some(a.value.as_str())
|
||||
} else {
|
||||
None
|
||||
@@ -150,21 +158,24 @@ impl Node {
|
||||
})
|
||||
}
|
||||
|
||||
// Set a namespaced attribute. `qualified` is the qualified name
|
||||
// (e.g. "xlink:href"); it is stored as the local name with the namespace
|
||||
// recorded, matching how the non-namespaced path stores names, so
|
||||
// getAttribute(qualified) and serialization keep working unchanged.
|
||||
// Set a namespaced attribute using a proper QualName: prefix and local name
|
||||
// remain separate while qualified-name APIs and serialization reconstruct
|
||||
// `prefix:local` when needed.
|
||||
pub fn set_attribute_ns(&mut self, ns: &str, qualified: &str, value: String) {
|
||||
if let NodeData::Element { attrs, .. } = &mut self.data {
|
||||
let local = attr_local_suffix(qualified);
|
||||
let (prefix, local) = match qualified.split_once(':') {
|
||||
Some((prefix, local)) => (Some(Prefix::from(prefix)), local),
|
||||
None => (None, qualified),
|
||||
};
|
||||
if let Some(attr) = attrs
|
||||
.iter_mut()
|
||||
.find(|a| a.name.ns.as_ref() == ns && attr_local_suffix(a.name.local.as_ref()) == local)
|
||||
.find(|a| a.name.ns.as_ref() == ns && a.name.local.as_ref() == local)
|
||||
{
|
||||
attr.name.prefix = prefix;
|
||||
attr.value = value;
|
||||
} else {
|
||||
attrs.push(Attribute {
|
||||
name: QualName::new(None, Namespace::from(ns), LocalName::from(qualified)),
|
||||
name: QualName::new(prefix, Namespace::from(ns), LocalName::from(local)),
|
||||
value,
|
||||
});
|
||||
}
|
||||
@@ -174,7 +185,7 @@ impl Node {
|
||||
pub fn remove_attribute_ns(&mut self, ns: &str, local: &str) {
|
||||
if let NodeData::Element { attrs, .. } = &mut self.data {
|
||||
attrs.retain(|a| {
|
||||
!(a.name.ns.as_ref() == ns && attr_local_suffix(a.name.local.as_ref()) == local)
|
||||
!(a.name.ns.as_ref() == ns && a.name.local.as_ref() == local)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+22
-1
@@ -1402,7 +1402,12 @@ class Element extends Node {
|
||||
if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev);
|
||||
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('attributes', this._nid, [], [], n);
|
||||
}
|
||||
setAttributeNS(ns, n, v) { _dom("set_attribute_ns", this._nid, String(ns == null ? "" : ns) + "\0" + String(n) + "\0" + String(v)); } // n is the qualified name; stored with its namespace
|
||||
setAttributeNS(ns, n, v) {
|
||||
ns = ns == null || ns === '' ? '' : String(ns);
|
||||
n = String(n);
|
||||
_ns_validateQualifiedName(ns, n);
|
||||
_dom("set_attribute_ns", this._nid, ns + "\0" + n + "\0" + String(v));
|
||||
}
|
||||
removeAttribute(n) { n = _htmlAttrName(this, n); const popoverPrev = (n === "popover") ? this.popover : undefined; _dom("remove_attribute", this._nid, n); if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev); }
|
||||
removeAttributeNS(ns, n) { _dom("remove_attribute_ns", this._nid, String(ns == null ? "" : ns) + "\0" + String(n)); }
|
||||
hasAttribute(n) { return this.getAttribute(n) !== null; }
|
||||
@@ -8423,6 +8428,22 @@ const _ns_isValidXmlName = (name) => {
|
||||
return /^[A-Za-z_:][\w.\-:]*$/.test(name);
|
||||
};
|
||||
|
||||
const _ns_validateQualifiedName = (namespaceURI, qualifiedName) => {
|
||||
const parts = qualifiedName.split(':');
|
||||
if (parts.length > 2 || parts.some((part) => !_ns_isValidXmlName(part))) {
|
||||
throw new DOMException('Invalid attribute name', 'InvalidCharacterError');
|
||||
}
|
||||
const prefix = parts.length === 2 ? parts[0] : null;
|
||||
const XML = 'http://www.w3.org/XML/1998/namespace';
|
||||
const XMLNS = 'http://www.w3.org/2000/xmlns/';
|
||||
if ((prefix && !namespaceURI)
|
||||
|| (prefix === 'xml' && namespaceURI !== XML)
|
||||
|| ((qualifiedName === 'xmlns' || prefix === 'xmlns') && namespaceURI !== XMLNS)
|
||||
|| (namespaceURI === XMLNS && qualifiedName !== 'xmlns' && prefix !== 'xmlns')) {
|
||||
throw new DOMException('The namespace is invalid', 'NamespaceError');
|
||||
}
|
||||
};
|
||||
|
||||
// Document.prototype.createAttribute: create a detached Attr node
|
||||
if (!Document.prototype.createAttribute) {
|
||||
Document.prototype.createAttribute = function(localName) {
|
||||
|
||||
@@ -321,7 +321,7 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
let names: Vec<String> = dom
|
||||
.with_node(NodeId::new(nid), |n| {
|
||||
n.attrs()
|
||||
.map(|a| a.iter().map(|x| x.name.local.as_ref().to_string()).collect())
|
||||
.map(|a| a.iter().map(|x| x.qualified_name()).collect())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
@@ -373,7 +373,7 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
let nid = arg1.parse::<u32>().unwrap_or(0);
|
||||
dom.with_node_mut(NodeId::new(nid), |n| {
|
||||
if let NodeData::Element { attrs, .. } = &mut n.data {
|
||||
attrs.retain(|a| a.name.local.as_ref() != arg2.as_str());
|
||||
attrs.retain(|a| !a.qualified_name_eq(&arg2));
|
||||
}
|
||||
});
|
||||
"true".into()
|
||||
|
||||
@@ -1345,6 +1345,40 @@ mod tests {
|
||||
assert_eq!(v, serde_json::json!("hi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespaced_attribute_keeps_its_qualified_name() {
|
||||
let mut rt = setup_runtime("<html><body></body></html>");
|
||||
let v = rt
|
||||
.evaluate("(function(){var s=document.createElementNS('http://www.w3.org/2000/svg','svg');s.setAttributeNS('http://www.w3.org/1999/xlink','xlink:href','#g');return s.getAttribute('xlink:href')+'|'+s.getAttributeNames()[0]+'|'+s.outerHTML;})()")
|
||||
.unwrap();
|
||||
assert_eq!(v, serde_json::json!("#g|xlink:href|<svg xlink:href=\"#g\"></svg>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsed_xlink_attribute_is_available_through_both_apis() {
|
||||
let mut rt = setup_runtime(
|
||||
r##"<html><body><svg><use id="u" xlink:href="#icon"></use></svg></body></html>"##,
|
||||
);
|
||||
let v = rt
|
||||
.evaluate("(function(){var u=document.getElementById('u');return u.getAttribute('xlink:href')+'|'+u.getAttributeNS('http://www.w3.org/1999/xlink','href')+'|'+u.getAttributeNames().join(',');})()")
|
||||
.unwrap();
|
||||
assert_eq!(v, serde_json::json!("#icon|#icon|id,xlink:href"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_attribute_ns_validates_namespace_constraints() {
|
||||
let mut rt = setup_runtime("<html><body></body></html>");
|
||||
let v = rt
|
||||
.evaluate("(function(){var e=document.createElement('div'),out=[];for(const args of [[null,'x:y'],['urn:test','a:b:c'],['urn:test','xml:lang'],['urn:test','xmlns:x']]){try{e.setAttributeNS(args[0],args[1],'v');out.push('none')}catch(err){out.push(err.name)}}return out.join('|');})()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
v,
|
||||
serde_json::json!(
|
||||
"NamespaceError|InvalidCharacterError|NamespaceError|NamespaceError"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[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