Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions matching/src/selectable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,14 @@ impl selectors::Element for CssNodeRef<'_> {
}

fn is_link(&self) -> bool {
&self.0.value().element().unwrap().name.local == "link"
let element = self.0.value().element().unwrap();

element.name.ns == ns!(html)
&& matches!(&*element.name.local, "a" | "area" | "link")
&& element
.attrs
.iter()
.any(|(name, _)| name.ns.is_empty() && &*name.local == "href")
}

fn is_html_slot_element(&self) -> bool {
Expand Down Expand Up @@ -181,14 +188,19 @@ impl selectors::Element for CssNodeRef<'_> {
}

fn is_empty(&self) -> bool {
!self.0.children().any(|x| {
let v = x.value();
v.is_element() || v.is_text()
!self.0.children().any(|child| {
let value = child.value();
value.is_element()
|| value
.text()
.is_some_and(|text| !text.contents.is_empty())
})
}

fn is_root(&self) -> bool {
self.0.value().is_document()
self.0
.parent()
.is_some_and(|parent| parent.value().is_document())
}

fn apply_selector_flags(&self, _flags: selectors::matching::ElementSelectorFlags) {}
Expand Down
84 changes: 84 additions & 0 deletions python/tests/test_rustlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,3 +638,87 @@ def test_iter_submodule():

for name in rl.iter.__all__:
assert getattr(rl.iter, name).__module__ == "markupever._rustlib.iter", name



def test_select_pseudo_classes():
parser = rl.Parser(rl.HtmlOptions())
parser.process(
'<html id="document-root"><head><link id="stylesheet" href="/site.css"></head>'
'<body><a id="anchor" href="#target"></a><a id="plain-anchor"></a>'
'<area id="area" href="#target"></body></html>'
)
parser.finish()
dom = parser.into_dom()

def selected_ids(expression):
return [
_get_attr(node.attrs, "id")[1]
for node in rl.Select(dom.root(), expression)
]

assert selected_ids(":root") == ["document-root"]
assert selected_ids(":link") == ["stylesheet", "anchor", "area"]
assert selected_ids("#plain-anchor:link") == []

body = next(iter(rl.Select(dom.root(), "body")))

empty_text = rl.Element(
dom, rl.QualName("div", "html"), [("id", "empty-text")], False, False
)
comment_only = rl.Element(
dom, rl.QualName("div", "html"), [("id", "comment-only")], False, False
)
whitespace = rl.Element(
dom, rl.QualName("div", "html"), [("id", "whitespace")], False, False
)

for element in (empty_text, comment_only, whitespace):
dom.append(body, element)

dom.append(empty_text, rl.Text(dom, ""))
dom.append(comment_only, rl.Comment(dom, "ignored"))
dom.append(whitespace, rl.Text(dom, " "))

assert selected_ids("#empty-text:empty") == ["empty-text"]
assert selected_ids("#comment-only:empty") == ["comment-only"]
assert selected_ids("#whitespace:empty") == []


def test_unqualified_id_and_class_selectors_ignore_namespaced_attributes():
dom = rl.TreeDom(namespaces={"ns": "urn:example"})
element = rl.Element(
dom,
"item",
[
(rl.QualName("id", "urn:example", "ns"), "namespaced-id"),
("id", "plain-id"),
(rl.QualName("class", "urn:example", "ns"), "namespaced-class"),
("class", "plain-class"),
],
False,
False,
)
dom.append(dom.root(), element)

assert len(list(rl.Select(dom.root(), "#plain-id"))) == 1
assert len(list(rl.Select(dom.root(), "#namespaced-id"))) == 0
assert len(list(rl.Select(dom.root(), ".plain-class"))) == 1
assert len(list(rl.Select(dom.root(), ".namespaced-class"))) == 0
assert len(list(rl.Select(dom.root(), "[ns|id]"))) == 1


def test_detaching_a_node_preserves_namespaces_still_in_use():
dom = rl.TreeDom()
first = rl.Element(dom, rl.QualName("first", "urn:example", "ns"), [], False, False)
second = rl.Element(dom, rl.QualName("second", "urn:example", "ns"), [], False, False)

dom.append(dom.root(), first)
dom.append(dom.root(), second)
assert dom.namespaces() == {"ns": "urn:example"}

dom.detach(first)
assert dom.namespaces() == {"ns": "urn:example"}

dom.detach(second)
assert dom.namespaces() == {}
75 changes: 54 additions & 21 deletions src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ impl PyTreeDom {
Self { dom }
}

#[inline]
fn same_contents(&self, other: &Self) -> bool {
if Arc::ptr_eq(&self.dom, &other.dom) {
return true;
}

// Lock the two DOMs in a stable order so opposite comparisons cannot deadlock.
let self_address = Arc::as_ptr(&self.dom) as usize;
let other_address = Arc::as_ptr(&other.dom) as usize;
let (first, second) = if self_address < other_address {
(self.dom.lock(), other.dom.lock())
} else {
(other.dom.lock(), self.dom.lock())
};

*first == *second
}

#[inline]
fn add_new_namespace(
&self,
Expand Down Expand Up @@ -46,12 +64,41 @@ impl PyTreeDom {
mut lock: ::parking_lot::MutexGuard<'_, ::treedom::IDTreeDOM>,
id: ::treedom::NodeId,
) {
let child = lock.get(id).unwrap();

if let Some(elem) = child.value().element()
&& let Some(prefix) = elem.name.prefix.clone()
let mut detached_prefixes = Vec::new();
{
lock.namespaces_mut().remove(&prefix);
let detached = lock.get(id).unwrap();
if let Some(element) = detached.value().element()
&& let Some(prefix) = element.name.prefix.clone()
{
detached_prefixes.push(prefix);
}

for descendant in detached.descendants() {
if let Some(element) = descendant.value().element()
&& let Some(prefix) = element.name.prefix.clone()
&& !detached_prefixes.contains(&prefix)
{
detached_prefixes.push(prefix);
}
}
}

let active_namespaces = {
let root = lock.root();
root.descendants()
.filter_map(|node| {
let element = node.value().element()?;
Some((element.name.prefix.clone()?, element.name.ns.clone()))
})
.collect::<std::collections::HashMap<_, _>>()
};

for prefix in detached_prefixes {
if let Some(namespace) = active_namespaces.get(&prefix) {
lock.namespaces_mut().insert(prefix, namespace.clone());
} else {
lock.namespaces_mut().remove(&prefix);
}
}
}
}
Expand Down Expand Up @@ -420,29 +467,15 @@ impl PyTreeDom {
Err(_) => return Ok(false),
};

if Arc::ptr_eq(&self_.get().dom, &other.dom) {
Ok(true)
} else {
let t1 = self_.get().dom.lock();
let t2 = other.dom.lock();

Ok(*t1 == *t2)
}
Ok(self_.get().same_contents(&other))
}
pyo3::basic::CompareOp::Ne => {
let other = match other.extract::<pyo3::PyRef<'_, Self>>(self_.py()) {
Ok(o) => o,
Err(_) => return Ok(false),
};

if Arc::ptr_eq(&self_.get().dom, &other.dom) {
Ok(false)
} else {
let t1 = self_.get().dom.lock();
let t2 = other.dom.lock();

Ok(*t1 != *t2)
}
Ok(!self_.get().same_contents(&other))
}
pyo3::basic::CompareOp::Gt => {
crate::nodes::create_richcmp_notimplemented!('>', self_)
Expand Down
4 changes: 2 additions & 2 deletions treedom/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ impl ElementAttributes {
.get_or_init(|| {
self.list
.iter()
.find(|(name, _)| name == "id")
.find(|(name, _)| name.ns.is_empty() && &*name.local == "id")
.map(|(_, value)| value.clone())
})
.as_deref()
Expand All @@ -171,7 +171,7 @@ impl ElementAttributes {
let mut classes = self
.list
.iter()
.filter(|(name, _)| &*name.local == "class")
.filter(|(name, _)| name.ns.is_empty() && &*name.local == "class")
.flat_map(|(_, value)| {
value
.split_ascii_whitespace()
Expand Down
Loading