oxvg_style/
lib.rs

1use std::{borrow::Borrow, collections::BTreeMap, rc::Rc};
2
3use lightningcss::{declaration, printer, properties, rules, stylesheet, traits::ToCss, values};
4use markup5ever::local_name;
5use oxvg_selectors::{Element, Selector};
6use rcdom::NodeData;
7
8#[derive(Debug)]
9pub enum SVGPaint {
10    Url,
11    Color(values::color::CssColor),
12    ContextFill,
13    ContextStroke,
14    None,
15}
16
17/// Relevant css properties in an owned variant
18#[derive(Debug)]
19pub enum SVGStyle {
20    MarkerMid(Option<String>),
21    Stroke(SVGPaint),
22    StrokeLinecap(properties::svg::StrokeLinecap),
23    StrokeLinejoin(properties::svg::StrokeLinejoin),
24    /// The matched style isn't relevant for SVG optimisation
25    Unsupported,
26}
27
28#[derive(Ord, Eq, PartialEq, PartialOrd, Debug)]
29pub enum SVGStyleID {
30    MarkerMid,
31    Stroke,
32    SrokeLinecap,
33    StrokeLinejoin,
34    Unsupported,
35}
36
37#[derive(Default, Debug)]
38pub struct ComputedStyles {
39    pub inherited: BTreeMap<SVGStyleID, SVGStyle>,
40    pub declarations: BTreeMap<SVGStyleID, (u32, SVGStyle)>,
41    pub inline: BTreeMap<SVGStyleID, SVGStyle>,
42    pub important_declarations: BTreeMap<SVGStyleID, (u32, SVGStyle)>,
43    pub inline_important: BTreeMap<SVGStyleID, SVGStyle>,
44}
45
46/// Gathers stylesheet declarations from the document
47///
48/// # Panics
49/// If the internal selector is invalid
50pub fn root_style(root: &Element) -> String {
51    root.select("style")
52        .expect("`style` should be a valid selector")
53        .map(|s| {
54            s.text_content()
55                .map(|s| s.borrow().to_string())
56                .collect::<Vec<_>>()
57                .join("\n")
58        })
59        .collect::<Vec<_>>()
60        .join("\n")
61}
62
63impl ComputedStyles {
64    pub fn with_all(&mut self, node: &Rc<rcdom::Node>, styles: &[rules::CssRule]) {
65        self.with_inherited(node, styles);
66        self.with_style(node, styles);
67        self.with_attribute(node);
68        self.with_inline_style(node);
69    }
70
71    pub fn with_inherited(&mut self, node: &Rc<rcdom::Node>, styles: &[rules::CssRule]) {
72        let element = Element::new(node.clone());
73        let Some(parent) = element.get_parent() else {
74            return;
75        };
76        let mut inherited = ComputedStyles::default();
77        inherited.with_all(&parent.node, styles);
78        self.inherited = inherited.into_computed();
79    }
80
81    pub fn with_style(&mut self, node: &Rc<rcdom::Node>, styles: &[rules::CssRule]) {
82        styles
83            .iter()
84            .for_each(|s| self.with_nested_style(node, s, "", 0));
85    }
86
87    fn with_nested_style(
88        &mut self,
89        node: &Rc<rcdom::Node>,
90        style: &rules::CssRule,
91        selector: &str,
92        specificity: u32,
93    ) {
94        match style {
95            rules::CssRule::Style(r) => r.selectors.0.iter().for_each(|s| {
96                let Ok(this_selector) = s.to_css_string(printer::PrinterOptions::default()) else {
97                    return;
98                };
99                let selector = format!("{selector}{this_selector}");
100                let Ok(select) = Selector::try_from(selector.as_str()) else {
101                    return;
102                };
103                let element = Element::new(node.clone());
104                if !select.matches_naive(&element) {
105                    return;
106                };
107                let specificity = specificity + s.specificity();
108                self.add_declarations(&r.declarations, specificity);
109            }),
110            rules::CssRule::Container(rules::container::ContainerRule { rules, .. })
111            | rules::CssRule::Media(rules::media::MediaRule { rules, .. }) => {
112                rules
113                    .0
114                    .iter()
115                    .for_each(|r| self.with_nested_style(node, r, selector, specificity));
116            }
117            _ => {}
118        }
119    }
120
121    fn with_attribute(&mut self, node: &Rc<rcdom::Node>) {
122        let node: &rcdom::Node = node.borrow();
123        let NodeData::Element { ref attrs, .. } = node.data else {
124            return;
125        };
126        attrs.borrow().iter().for_each(|a| {
127            let name = &a.name.local;
128            let value = &a.value;
129            let style = format!("{name}:{value}");
130            let Ok(style) =
131                stylesheet::StyleAttribute::parse(&style, stylesheet::ParserOptions::default())
132            else {
133                return;
134            };
135            let property = &style.declarations.declarations[0];
136            let style = SVGStyle::from(property);
137            self.inline.insert(style.id(), style);
138        });
139    }
140
141    pub fn with_inline_style(&mut self, node: &Rc<rcdom::Node>) {
142        let element = Element::new(node.clone());
143        let Some(style) = element.get_attr(&local_name!("style")) else {
144            return;
145        };
146        let Some(style) =
147            stylesheet::StyleAttribute::parse(&style.value, stylesheet::ParserOptions::default())
148                .ok()
149        else {
150            return;
151        };
152        style
153            .declarations
154            .declarations
155            .iter()
156            .map(SVGStyle::from)
157            .for_each(|s| {
158                self.inline.insert(s.id(), s);
159            });
160        style
161            .declarations
162            .important_declarations
163            .iter()
164            .map(SVGStyle::from)
165            .for_each(|s| {
166                self.inline_important.insert(s.id(), s);
167            });
168    }
169
170    pub fn computed<'a>(&'a self) -> BTreeMap<SVGStyleID, &'a SVGStyle> {
171        let mut result = BTreeMap::new();
172        let map = |s: &'a (u32, SVGStyle)| &s.1;
173        let mut insert = |s: &'a SVGStyle| {
174            result.insert(s.id(), s);
175        };
176        self.declarations.values().map(map).for_each(&mut insert);
177        self.inline.values().for_each(&mut insert);
178        self.important_declarations
179            .values()
180            .map(map)
181            .for_each(&mut insert);
182        self.inline_important.values().for_each(insert);
183        result
184    }
185
186    pub fn into_computed(self) -> BTreeMap<SVGStyleID, SVGStyle> {
187        let mut result = BTreeMap::new();
188        let map = |s: (u32, SVGStyle)| s.1;
189        let mut insert = |s: SVGStyle| {
190            result.insert(s.id(), s);
191        };
192        self.declarations
193            .into_values()
194            .map(map)
195            .for_each(&mut insert);
196        self.inline.into_values().for_each(&mut insert);
197        self.important_declarations
198            .into_values()
199            .map(map)
200            .for_each(&mut insert);
201        self.inline_important.into_values().for_each(insert);
202        result
203    }
204
205    fn add_declarations(&mut self, declarations: &declaration::DeclarationBlock, specificity: u32) {
206        Self::set_declarations(
207            &mut self.important_declarations,
208            &declarations.important_declarations,
209            specificity,
210        );
211        Self::set_declarations(
212            &mut self.declarations,
213            &declarations.declarations,
214            specificity,
215        );
216    }
217
218    fn set_declarations(
219        record: &mut BTreeMap<SVGStyleID, (u32, SVGStyle)>,
220        declarations: &[properties::Property],
221        specificity: u32,
222    ) {
223        for d in declarations {
224            let decl = SVGStyle::from(d);
225            let id = decl.id();
226            record.insert(id, (specificity, decl));
227        }
228    }
229}
230
231impl SVGStyle {
232    fn id(&self) -> SVGStyleID {
233        match self {
234            Self::MarkerMid(_) => SVGStyleID::MarkerMid,
235            Self::Stroke(_) => SVGStyleID::Stroke,
236            Self::StrokeLinecap(_) => SVGStyleID::SrokeLinecap,
237            Self::StrokeLinejoin(_) => SVGStyleID::StrokeLinejoin,
238            Self::Unsupported => SVGStyleID::Unsupported,
239        }
240    }
241}
242
243impl From<&properties::Property<'_>> for SVGStyle {
244    fn from(value: &properties::Property) -> Self {
245        match value {
246            properties::Property::MarkerMid(m) => SVGStyle::MarkerMid(match m {
247                lightningcss::properties::svg::Marker::Url(u) => Some(u.url.to_string()),
248                lightningcss::properties::svg::Marker::None => None,
249            }),
250            properties::Property::Stroke(s) => SVGStyle::from(s),
251            properties::Property::StrokeLinecap(s) => SVGStyle::StrokeLinecap(*s),
252            properties::Property::StrokeLinejoin(s) => SVGStyle::StrokeLinejoin(*s),
253            _ => SVGStyle::Unsupported,
254        }
255    }
256}
257
258impl From<&lightningcss::properties::svg::SVGPaint<'_>> for SVGStyle {
259    fn from(value: &lightningcss::properties::svg::SVGPaint) -> Self {
260        Self::Stroke(SVGPaint::from(value))
261    }
262}
263
264impl From<&lightningcss::properties::svg::SVGPaint<'_>> for SVGPaint {
265    fn from(value: &lightningcss::properties::svg::SVGPaint<'_>) -> Self {
266        match value {
267            properties::svg::SVGPaint::Url { .. } => Self::Url,
268            properties::svg::SVGPaint::Color(c) => Self::Color(c.clone()),
269            properties::svg::SVGPaint::ContextFill => Self::ContextFill,
270            properties::svg::SVGPaint::ContextStroke => Self::ContextStroke,
271            properties::svg::SVGPaint::None => Self::None,
272        }
273    }
274}