merman_render/svg/pipeline/builtin/
css_override.rs1use 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}