Merge #1052: Drop XML-forbidden control characters from live-text SVG export (@Silvertree2010)

This commit is contained in:
bflatastic
2026-10-24 09:23:00 +07:00
2 changed files with 37 additions and 1 deletions
+9 -1
View File
@@ -152,8 +152,16 @@ fn blend_style(b: figcraft_color::BlendMode) -> String {
}
}
/// `s` escaped for XML text and attribute values; characters XML 1.0 forbids (control characters
/// other than tab, newline and carriage return, U+FFFE and U+FFFF) are left out.
fn esc(s: &str) -> String {
s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
s.chars()
.filter(|&c| !matches!(c, '\u{0}'..='\u{8}' | '\u{b}' | '\u{c}' | '\u{e}'..='\u{1f}' | '\u{fffe}' | '\u{ffff}'))
.collect::<String>()
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}
fn hex(c: Color) -> String {
+28
View File
@@ -135,6 +135,34 @@ fn title_case_does_not_restart_words_across_fill_runs_in_live_text() {
assert!(svg.contains(">He</tspan>") && svg.contains(">l</tspan>") && svg.contains(">lo</tspan>"), "{svg}");
}
/// Control characters in text (a pasted vertical-tab soft break, U+0001) are not allowed in XML:
/// live-text export must leave them out so the SVG still parses, for plain and per-run spans.
#[test]
fn live_text_export_drops_xml_forbidden_control_characters() {
use figcraft_doc::{StyleRun, TextData};
figcraft_text::scan_system_fonts();
for styled in [false, true] {
let mut d = Document::new("t");
let page = d.first_page().unwrap();
let mut text = TextData::new("line one\u{b}line two\u{1}");
text.style.font_size = 24.0;
if styled {
let mut style = text.style.clone();
style.fills = Some(vec![Paint::solid(Color::from_hex("00FF00").unwrap())]);
text.runs.push(StyleRun { start: 5, end: 12, style });
text.normalize();
}
let id = d.create(page, NodeKind::Text(text), "Text", 0.0, 0.0, 600.0, 40.0).unwrap();
d.get_mut(id).unwrap().fills = vec![Paint::solid(Color::BLACK)];
let svg = export_svg(&d, &[id], &SvgOptions { outline_text: false, ..SvgOptions::default() }).unwrap();
assert!(!svg.contains('\u{b}') && !svg.contains('\u{1}'), "{svg:?}");
assert!(svg.contains("line") && svg.contains("two"), "{svg}");
let mut d2 = Document::new("t2");
let p2 = d2.first_page().unwrap();
assert!(import_svg(&mut d2, svg.as_bytes(), p2, 0.0, 0.0, "x").is_ok(), "{svg:?}");
}
}
#[test]
fn side_strokes_on_rounded_corners_export_as_their_area() {
// border-l-4 rounded-r-lg: the stroke path is the 4 px bar (60 px tall), drawn even-odd.