SVG import: :not() rules test only the rules indexed for the element

set_by_not_rules tested every rule with a :not() against every element,
while matched_bytes, which charges the import's selector budget, counts
only the rules Index::candidates gives for the element. A style sheet of
many class-keyed :not() rules made the pre-pass test rules x elements
selectors that the budget never saw. It now loops over the same
candidates, which hold every rule that can match the element, so the
properties it returns are unchanged.
This commit is contained in:
dotCooCoo
2026-11-14 20:35:10 -08:00
parent 0c1ac3bdc5
commit 13fa65cf77
2 changed files with 23 additions and 1 deletions
+3 -1
View File
@@ -556,10 +556,12 @@ impl Styles {
}
/// The properties rules with a `:not()` set on `n`, each with its value after the cascade.
/// Only the rules [`Index::candidates`] gives for `n` are tested, as in [`Styles::matched_bytes`].
pub(super) fn set_by_not_rules(&self, n: XNode) -> Vec<(String, String)> {
let declared = self.declared(n);
let mut out: Vec<(String, String)> = Vec::new();
for r in self.rules.iter().filter(|r| r.selector.has_not() && r.selector.matches(n)) {
let rules = self.index.candidates(n).into_iter().filter_map(|i| self.rules.get(i));
for r in rules.filter(|r| r.selector.has_not() && r.selector.matches(n)) {
for (k, ..) in r.decls.iter() {
if !out.iter().any(|(o, _)| o == k)
&& let Some(v) = declared.get(k)
+20
View File
@@ -334,6 +334,26 @@ fn not_selectors() {
);
}
/// The `:not()` rules filed under classes an element lacks are not tested against it, so
/// [`Styles::set_by_not_rules`] tests no more rules than [`Styles::matched_bytes`] counts.
#[test]
fn not_rules_test_only_the_indexed_candidates() {
const N: usize = 2_000;
let rules: String = (0..N).map(|i| format!(".c{i}:not(.skip) {{ fill: #{i:06x} }}\n")).collect();
let rects: String = (0..N).map(|i| format!(r#"<rect id="r{i}" class="c{i}"/>"#)).collect();
let svg = format!(r#"<svg xmlns="http://www.w3.org/2000/svg"><style>{rules}</style>{rects}<rect id="skip" class="c0 skip"/></svg>"#);
with_doc(&svg, |doc, st| {
let before = selector_tests();
for i in 0..N {
assert_eq!(st.set_by_not_rules(by_id(doc, &format!("r{i}"))), [("fill".to_string(), format!("#{i:06x}"))]);
}
assert!(st.set_by_not_rules(by_id(doc, "skip")).is_empty());
// Testing every `:not()` rule against every element would test N × N selectors.
let tests = selector_tests() - before;
assert!(tests <= 3 * N as u64, "finding the :not() rules of {N} elements tested {tests} selectors");
});
}
#[test]
fn decl_comments_are_stripped() {
let d = |k: &str, v: &str| (k.to_string(), v.to_string(), false);