Respect quotes and parentheses when splitting SVG CSS declarations

Found while testing crates/svg/src/import/css.rs:21 (parse_decls)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-26 13:43:20 +07:00
committed by bflatastic
co-authored by Claude Haiku 5.5
parent bb3ee0a260
commit 29a98b750a
+44 -1
View File
@@ -18,7 +18,8 @@ pub(super) type XNode<'a, 'i> = roxmltree::Node<'a, 'i>;
/// `name: value` declarations of a block or `style` attribute, with their `!important` flag. The
/// `font` shorthand becomes its longhands, as usvg reads it for the rest of the drawing (#1557).
pub(super) fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
s.split(';')
split_decls(s)
.into_iter()
.filter_map(|d| {
let (k, v) = d.split_once(':')?;
let v = v.trim();
@@ -37,6 +38,36 @@ pub(super) fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
.collect()
}
/// Splits on the `;`s that are outside quoted strings and parentheses (`url(data:…;base64,…)`).
fn split_decls(s: &str) -> Vec<&str> {
let mut parts = Vec::new();
let (mut start, mut depth, mut quote, mut escaped) = (0, 0usize, None::<char>, false);
for (i, c) in s.char_indices() {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if let Some(q) = quote {
if c == q {
quote = None;
}
} else {
match c {
'\'' | '"' => quote = Some(c),
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
';' if depth == 0 => {
parts.push(s.get(start..i).unwrap_or_default());
start = i + 1;
}
_ => {}
}
}
}
parts.push(s.get(start..).unwrap_or_default());
parts
}
/// The longhands of a `font` shorthand: those it leaves out back to their initial values. None
/// when it doesn't parse (it is then ignored, as in CSS).
fn font_longhands(v: &str) -> Vec<(&'static str, String)> {
@@ -467,6 +498,18 @@ mod tests {
assert_eq!(parse_decls("fill: red !importantx"), [("fill".to_string(), "red !importantx".to_string(), false)]);
}
#[test]
fn semicolons_in_quotes_and_parens_do_not_split() {
let d = |k: &str, v: &str| (k.to_string(), v.to_string(), false);
assert_eq!(parse_decls("font-family:'A;B'; fill:red"), [d("font-family", "'A;B'"), d("fill", "red")]);
assert_eq!(parse_decls("font-family:\"A;B\"; fill:red"), [d("font-family", "\"A;B\""), d("fill", "red")]);
assert_eq!(
parse_decls("fill:url(data:image/svg+xml;base64,AAA); stroke:red"),
[d("fill", "url(data:image/svg+xml;base64,AAA)"), d("stroke", "red")]
);
let _ = parse_decls("font-family:'A;B; fill:red");
}
fn with_doc(svg: &str, f: impl FnOnce(&roxmltree::Document, &Styles)) {
let doc = roxmltree::Document::parse(svg).unwrap();
let st = Styles::new(&doc);