diff --git a/crates/svg/src/export.rs b/crates/svg/src/export.rs index 85e6e5908..e4846a431 100644 --- a/crates/svg/src/export.rs +++ b/crates/svg/src/export.rs @@ -637,7 +637,7 @@ impl Writer<'_> { .map_or(&[][..], |a| a.classes.as_slice()) .iter() .map(String::as_str) - .filter(|c| !c.is_empty() && c.chars().all(xml_name_char)) + .filter(|c| !c.is_empty() && !c.chars().any(|ch| ch.is_control() || matches!(ch, '\u{fffe}' | '\u{ffff}'))) .collect(); if !classes.is_empty() && !self.tiny() { out.push_str(&format!(" class=\"{}\"", xml_attr_escape(&classes.join(" ")))); diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index d81c71827..b1bb92115 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -718,11 +718,12 @@ type Extras = (Vec<(String, String)>, Vec); const MAX_CLASSES: usize = 64; /// The CSS classes of element `e`, in order and without repeats. Classes our own export -/// generated (`cls-1`, with or without a prefix) aren't kept: export makes them again. +/// generated (`cls-1`, with or without a prefix) aren't kept: export makes them again. A class list +/// is split on SVG's five ASCII whitespace characters only, so a no-break space stays inside a name. fn user_classes(e: XNode) -> Vec { let generated = |c: &str| c.rsplit_once("cls-").is_some_and(|(_, n)| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())); let mut out: Vec = Vec::new(); - for c in e.attribute("class").into_iter().flat_map(str::split_whitespace) { + for c in e.attribute("class").into_iter().flat_map(|v| v.split(['\u{20}', '\t', '\n', '\u{c}', '\r']).filter(|c| !c.is_empty())) { if out.len() < MAX_CLASSES && !generated(c) && !out.iter().any(|k| k == c) { out.push(c.to_string()); } diff --git a/crates/svg/tests/classes.rs b/crates/svg/tests/classes.rs index 679d98dda..88be96429 100644 --- a/crates/svg/tests/classes.rs +++ b/crates/svg/tests/classes.rs @@ -29,3 +29,10 @@ fn imported_classes_survive_every_styling_with_generated_ones() { assert_eq!(class_of(&again, "r"), Some(rect), "{styling:?}"); } } + +#[test] +fn class_names_with_colons_slashes_and_no_break_spaces_are_kept() { + let src = ""; + let out = export(&import(src).unwrap(), &ExportOptions::default()); + assert_eq!(class_of(&out, "r").as_deref(), Some("hover:bg-blue w-1/2 a\u{a0}b")); +}