diff --git a/crates/svg/src/export.rs b/crates/svg/src/export.rs index 661b6381e..1f9f79ac0 100644 --- a/crates/svg/src/export.rs +++ b/crates/svg/src/export.rs @@ -135,12 +135,12 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix: } if !doc.title.is_empty() { out.push_str(&w.indent(1)); - out.push_str(&format!("{}{nl}", xml_escape(&doc.title))); + out.push_str(&format!("{}{nl}", xml_text(&doc.title))); } // File Info's description. if !doc.metadata.description.trim().is_empty() { out.push_str(&w.indent(1)); - out.push_str(&format!("{}{nl}", xml_escape(&doc.metadata.description))); + out.push_str(&format!("{}{nl}", xml_text(&doc.metadata.description))); } // Window › SVG Interactivity › Link JavaScript Files. for file in &doc.svg_scripts { diff --git a/crates/svg/tests/title_control_chars.rs b/crates/svg/tests/title_control_chars.rs new file mode 100644 index 000000000..c7f842c0b --- /dev/null +++ b/crates/svg/tests/title_control_chars.rs @@ -0,0 +1,26 @@ +// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness). +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +use vectorcraft_doc::Document; +use vectorcraft_svg::{ExportOptions, export, import}; + +#[test] +fn title_and_description_control_chars_are_filtered() { + let mut d = Document::new(200.0, 200.0); + d.title = "Poster\u{b}v2".into(); + d.metadata.description = "a\u{1}b".into(); + let svg = export(&d, &ExportOptions::default()); + assert!(import(&svg).is_ok(), "export wrote invalid XML: {svg}"); + assert!(svg.contains("Poster v2")); + assert!(svg.contains("ab")); +} + +#[test] +fn title_and_description_markup_is_escaped() { + let mut d = Document::new(200.0, 200.0); + d.title = "A & ".into(); + d.metadata.description = "x & y".into(); + let svg = export(&d, &ExportOptions::default()); + assert!(import(&svg).is_ok(), "export wrote invalid XML: {svg}"); + assert!(svg.contains("A & <B>")); + assert!(svg.contains("x & y")); +}