Skip to main content

merman_render/svg/pipeline/builtin/
css_override.rs

1use crate::Result;
2use std::borrow::Cow;
3
4use crate::svg::pipeline::{SvgPostprocessContext, SvgPostprocessor};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum CssOverridePolicy {
8    #[default]
9    Preserve,
10    StripExistingImportant,
11}
12
13#[derive(Debug, Clone, Copy, Default)]
14pub struct CssOverridePostprocessor {
15    policy: CssOverridePolicy,
16}
17
18impl CssOverridePostprocessor {
19    pub fn new(policy: CssOverridePolicy) -> Self {
20        Self { policy }
21    }
22
23    pub fn strip_existing_important() -> Self {
24        Self::new(CssOverridePolicy::StripExistingImportant)
25    }
26
27    pub fn policy(&self) -> CssOverridePolicy {
28        self.policy
29    }
30}
31
32impl SvgPostprocessor for CssOverridePostprocessor {
33    fn name(&self) -> &'static str {
34        "css-override"
35    }
36
37    fn process<'a>(
38        &self,
39        svg: Cow<'a, str>,
40        _ctx: &SvgPostprocessContext<'_>,
41    ) -> Result<Cow<'a, str>> {
42        match self.policy {
43            CssOverridePolicy::Preserve => Ok(svg),
44            CssOverridePolicy::StripExistingImportant => {
45                Ok(Cow::Owned(strip_css_important(svg.as_ref())))
46            }
47        }
48    }
49}
50
51pub(crate) fn strip_css_important(svg: &str) -> String {
52    let mut out = String::with_capacity(svg.len());
53    let mut copied_until = 0usize;
54    let mut search_from = 0usize;
55    let mut stripped = false;
56
57    while let Some(rel) = svg[search_from..].find('!') {
58        let bang = search_from + rel;
59        if let Some((start, end)) = css_important_match_bounds_at_bang(svg, bang) {
60            out.push_str(&svg[copied_until..start]);
61            copied_until = end;
62            search_from = end;
63            stripped = true;
64            continue;
65        }
66
67        search_from = bang + 1;
68    }
69
70    if !stripped {
71        return svg.to_string();
72    }
73
74    out.push_str(&svg[copied_until..]);
75    out
76}
77
78fn css_important_match_bounds_at_bang(svg: &str, bang: usize) -> Option<(usize, usize)> {
79    let marker_end = bang + "!important".len();
80    if !svg
81        .get(bang..marker_end)?
82        .eq_ignore_ascii_case("!important")
83    {
84        return None;
85    }
86
87    if let Some(next) = svg.get(marker_end..).and_then(|tail| tail.chars().next()) {
88        if is_css_regex_word_char(next) {
89            return None;
90        }
91    }
92
93    let start = svg[..bang]
94        .char_indices()
95        .rev()
96        .find(|(_, ch)| !ch.is_whitespace())
97        .map(|(idx, ch)| idx + ch.len_utf8())
98        .unwrap_or(0);
99
100    Some((start, marker_end))
101}
102
103fn is_css_regex_word_char(ch: char) -> bool {
104    ch == '_' || ch.is_alphanumeric()
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::svg::pipeline::SvgPipeline;
111
112    #[test]
113    fn css_override_strips_important_only_when_requested() {
114        let svg = r#"<svg><style>.node{fill:red !important;}</style></svg>"#;
115
116        let preserve = SvgPipeline::parity()
117            .with_postprocessor(CssOverridePostprocessor::new(CssOverridePolicy::Preserve))
118            .process_to_string(svg)
119            .unwrap();
120        let strip = SvgPipeline::parity()
121            .with_postprocessor(CssOverridePostprocessor::strip_existing_important())
122            .process_to_string(svg)
123            .unwrap();
124
125        assert!(preserve.contains("!important"));
126        assert!(!strip.contains("!important"));
127    }
128
129    #[test]
130    fn css_override_important_scanner_preserves_regex_boundaries() {
131        assert_eq!(
132            strip_css_important(
133                ".a{fill:red\t!important;stroke:blue !IMPORTANT;color:green !importantfoo;}"
134            ),
135            ".a{fill:red;stroke:blue;color:green !importantfoo;}"
136        );
137        assert_eq!(
138            strip_css_important(".a{fill:red!important-border;color:blue !importanté;}"),
139            ".a{fill:red-border;color:blue !importanté;}"
140        );
141    }
142}