diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index 289693e8..231963e3 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -476,6 +476,9 @@ pub(super) struct Edits { list: Vec<(std::ops::Range, String)>, /// The ids made up so far, by element. ids: HashMap, + /// Declarations added to elements' `style`s, by element: (where they go, whether the element + /// has a `style` attribute, the declarations, XML-escaped). + styles: HashMap, } impl Edits { @@ -505,9 +508,24 @@ impl Edits { } } + /// Add the declaration `decl` (XML-escaped already) to the `style` of element `n` (in `svg`), + /// after the ones already there (so it wins over them and over style sheet rules). + fn style(&mut self, svg: &str, n: XNode, decl: &str) { + let (_, _, decls) = + self.styles.entry(n.id()).or_insert_with(|| match n.attributes().find(|a| a.name() == "style" && a.namespace().is_none()) { + Some(a) => (a.range_value().end, true, String::new()), + None => (tag_name_end(svg, n), false, String::new()), + }); + decls.push(';'); + decls.push_str(decl); + } + /// `svg` with the edits made; overlapping ones and ranges off character boundaries are /// skipped. fn apply(mut self, svg: &str) -> Cow<'_, str> { + for (at, has_style, decls) in std::mem::take(&mut self.styles).into_values() { + self.insert(at, if has_style { decls } else { format!(" style=\"{}\"", &decls[1..]) }); + } if self.list.is_empty() { return svg.into(); } @@ -534,12 +552,15 @@ impl Edits { /// to come back hidden; one something links to (a `` template) stays as it is; /// * shapes with a non-scaling stroke get an id when they have none, recorded ([`vector_effect`]); /// * linked files are read ([`files`]); -/// * percentage lengths in a `` with a `viewBox` become numbers ([`symbol_units`]). +/// * percentage lengths in a `` with a `viewBox` become numbers ([`symbol_units`]); +/// * what style sheet rules with a `:not()` set goes in `style` attributes ([`not_rules`]). fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) { let mut found = Found::default(); // (`:` for prefixed elements such as ``.) let css_transform = has_css_transform(svg); + let not = svg.contains(":not("); if !css_transform + && !not && !["(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) { } } let (display, non_scaling) = (svg.contains("display"), svg.contains(vector_effect::NON_SCALING)); - if display || non_scaling { + if display || non_scaling || not { let css = css::Styles::new(&xml); + // First: an undisplayed object shown for usvg is shown after these. + if not && css.has_not_rules() { + not_rules(svg, &xml, &css, &mut edits); + } if display { hidden_objects(svg, &xml, &css, &mut edits, &mut found.hidden); } @@ -594,6 +619,18 @@ fn has_css_transform(svg: &str) -> bool { svg.match_indices("transform").any(|(i, t)| svg.get(i + t.len()..).is_some_and(|s| s.trim_start().starts_with(':'))) } +/// usvg's style sheets drop rules with a `:not()` selector (#1557). The properties such rules set +/// on an element go in its `style` attribute, with the value that wins the cascade there (which +/// can be another rule's or the attribute's own), so usvg reads what CSS would. +fn not_rules(svg: &str, xml: &roxmltree::Document, css: &css::Styles, edits: &mut Edits) { + for n in xml.descendants().filter(XNode::is_element) { + for (name, value) in css.set_by_not_rules(n) { + let value = if name == "transform" { css_transform::to_svg(&value).unwrap_or(value) } else { value }; + edits.style(svg, n, &crate::xml_escape(&format!("{name}:{value}"))); + } + } +} + /// The elements that are objects (what [`hidden_objects`] shows). const OBJECTS: &[&str] = &["g", "a", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon", "image", "text", "use"]; @@ -615,10 +652,7 @@ fn hidden_objects(svg: &str, xml: &roxmltree::Document, css: &css::Styles, edits } ids.insert(edits.id(svg, n)); // A `style` declaration wins over the attribute and style sheet rules. - match n.attributes().find(|a| a.name() == "style" && a.namespace().is_none()) { - Some(a) => edits.insert(a.range_value().end, ";display:inline".to_string()), - None => edits.insert(tag_name_end(svg, n), " style=\"display:inline\"".to_string()), - } + edits.style(svg, n, "display:inline"); } } diff --git a/crates/svg/src/import/css.rs b/crates/svg/src/import/css.rs index 172e93b1..5c87742e 100644 --- a/crates/svg/src/import/css.rs +++ b/crates/svg/src/import/css.rs @@ -1,9 +1,9 @@ //! The style cascade for the `` pre-pass: ` + + xx + "#, + |doc, st| { + assert_eq!(st.own(by_id(doc, "r0"), "fill").as_deref(), Some("green")); + assert_eq!(st.own(by_id(doc, "r1"), "fill").as_deref(), Some("red")); + assert_eq!(st.own(by_id(doc, "t0"), "stroke").as_deref(), Some("blue")); + assert_eq!(st.own(by_id(doc, "t1"), "stroke"), None); + assert_eq!(st.own(by_id(doc, "r2"), "stroke"), None); + assert_eq!(st.own(by_id(doc, "r2"), "opacity").as_deref(), Some("0.5")); + assert_eq!(st.own(by_id(doc, "r0"), "opacity"), None); + assert!(st.has_not_rules()); + assert_eq!(st.set_by_not_rules(by_id(doc, "r0")), [("fill".to_string(), "green".to_string())]); + assert!(st.set_by_not_rules(by_id(doc, "r1")).is_empty()); + }, + ); + } } diff --git a/crates/svg/tests/import_details.rs b/crates/svg/tests/import_details.rs index 9929aff9..abd21972 100644 --- a/crates/svg/tests/import_details.rs +++ b/crates/svg/tests/import_details.rs @@ -203,6 +203,50 @@ fn css_transforms_move_objects() { assert!(near(b[2], -20.0, 0.0, 0.0, 50.0), "{:?}", b[2]); } +/// #1557: style sheet rules with a `:not()` selector apply, with CSS precedence: against other +/// rules, the `style` attribute, `!important`, and for a shape, a group (inherited) and a hidden +/// object. +#[test] +fn not_selectors_apply() { + let d = import_with( + &svg(r##" + + + + + + + + "##), + &ImportOptions::default(), + ) + .unwrap() + .0; + let im = render_artboard(&d); + let rgb = |x: u32, y: u32| { + let p = im.pixel(x, y); + [p[0], p[1], p[2]] + }; + assert_eq!(rgb(10, 10), [0, 255, 0], "rect:not(.keep) beats the fill attribute"); + assert_eq!(rgb(30, 10), [0, 0, 255], "#b is more specific"); + assert_eq!(rgb(50, 10), [255, 0, 0], ".keep is left alone"); + assert_eq!(rgb(70, 10), [0, 255, 255], "inherited from the group"); + assert_eq!(rgb(90, 10), [255, 0, 0], "the style attribute wins"); + assert_eq!(rgb(110, 10), [0, 255, 0], "!important beats the style attribute"); + assert_eq!(rgb(10, 90), [255, 255, 255], "moved by the CSS transform"); + assert_eq!(rgb(110, 90), [0, 255, 0]); + let hidden: Vec<&Node> = art(&d).into_iter().filter(|n| !n.visible).collect(); + assert_eq!(hidden.len(), 1, "display: none from a :not() rule comes in as a hidden object"); +} + /// #1561: percentages in a `` with a `viewBox` are of the `viewBox`, so the symbol draws as /// the same one written with numbers. #[test]