Make SVG export of type with control characters well-formed

Found while testing crates/svg/src/export.rs:1908

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-16 02:21:27 +07:00
committed by bflatastic
co-authored by Claude Haiku 5.5
parent 18c1bb0d55
commit 62ab3bd797
2 changed files with 28 additions and 2 deletions
+16 -2
View File
@@ -166,6 +166,20 @@ fn xml_attr_escape(s: &str) -> String {
xml_escape(s).replace('\t', "&#9;").replace('\n', "&#10;").replace('\r', "&#13;")
}
/// Escape type for XML content. Characters XML 1.0 does not allow are dropped, except the line
/// and page breaks (U+000B, U+000C) other apps paste, which become spaces.
fn xml_text(s: &str) -> String {
let s: String = s
.chars()
.filter_map(|c| match c {
'\u{b}' | '\u{c}' => Some(' '),
c if valid_xml_value(c.encode_utf8(&mut [0; 4])) => Some(c),
_ => None,
})
.collect();
xml_escape(&s)
}
fn valid_xml_value(s: &str) -> bool {
s.chars().all(|c| matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{d7ff}' | '\u{e000}'..='\u{fffd}' | '\u{10000}'..='\u{10ffff}'))
}
@@ -1905,7 +1919,7 @@ impl Writer<'_> {
}
let diff = run_diff(&base.props, self.char_props(st, base.space));
attrs.push_str(&self.attrs(&diff));
let text = xml_escape(&seg.text);
let text = xml_text(&seg.text);
if attrs.is_empty() && fewer {
s.push_str(&text);
} else {
@@ -2010,7 +2024,7 @@ impl Writer<'_> {
for run in &t.runs {
let diff = run_diff(&base, self.char_props(&run.style, space));
let a = self.attrs(&diff);
s.push_str(&format!("<tspan{a}>{}</tspan>", xml_escape(&run.text.replace('\n', " "))));
s.push_str(&format!("<tspan{a}>{}</tspan>", xml_text(&run.text.replace('\n', " "))));
}
s.push_str("</textPath></text>");
self.line(&s);
+12
View File
@@ -386,6 +386,18 @@ fn text_roundtrip_and_escaping() {
assert!(t.xf.translation().to_point().distance(Point::new(20.0, 50.0)) < 0.01);
}
#[test]
fn text_with_control_characters_exports_well_formed_svg() {
// A pasted manual line break (U+000B) or other C0 control character is not allowed in XML 1.0.
let t = TextObject::point(Point::new(20.0, 50.0), "A\u{0}\u{8}B\u{b}C\u{fffe}D", CharStyle::default());
let id = Document::new(200.0, 200.0).alloc_id();
let d = doc_with(vec![Node::new(id, NodeKind::Text(Box::new(t)))]);
let s = export(&d, &ExportOptions::default());
let r = import(&s).unwrap_or_else(|e| panic!("exported SVG doesn't import: {e:?}"));
let NodeKind::Text(t) = &art(&r)[0].kind else { panic!() };
assert_eq!(t.plain_text(), "AB CD");
}
#[test]
fn scaled_text_roundtrips_at_the_size_it_draws() {
// Type scaled with its object (its transform scales it) comes back at the size it draws at,