Merge pull request #5279 from Silvertree2010/fix/h5763-svg-export-writes-invalid-xml-wh

Drop invalid XML characters from SVG link URLs and aria-labels
This commit is contained in:
Brandon Thomas
2026-11-03 00:18:21 -05:00
committed by GitHub
4 changed files with 27 additions and 4 deletions
+5 -2
View File
@@ -1,4 +1,4 @@
use super::output::{sanitize_id, valid_xml_value, xml_attr_escape};
use super::output::{sanitize_id, valid_xml_value, xml_attr_escape, xml_attr_valid};
use super::paint::{StrokePlan, has_raster, pixel_gradient};
use super::text::OUTLINE_TOLERANCE;
use super::{
@@ -489,7 +489,10 @@ impl Writer<'_> {
self.layer_nest += layer;
match n.url().filter(|_| n.visible && !self.anonymous) {
Some(url) => {
self.line(&format!("<a xlink:href=\"{}\">", xml_escape(url)));
if !valid_xml_value(url) {
self.warn("Link URLs containing invalid XML characters had those characters removed");
}
self.line(&format!("<a xlink:href=\"{}\">", xml_attr_valid(url)));
self.depth += 1;
self.node_body(n);
self.depth -= 1;
+6
View File
@@ -19,6 +19,12 @@ pub(super) fn xml_text(s: &str) -> String {
xml_escape(&s)
}
/// `s` without the characters XML 1.0 does not allow, ready for a double-quoted attribute.
pub(super) fn xml_attr_valid(s: &str) -> String {
let s: String = s.chars().filter(|c| valid_xml_value(c.encode_utf8(&mut [0; 4]))).collect();
xml_attr_escape(&s)
}
pub(super) 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}'))
}
+2 -2
View File
@@ -1,4 +1,4 @@
use super::output::xml_text;
use super::output::{xml_attr_valid, xml_text};
use super::{
Affine, AppearanceItem, Arc, BTreeSet, BezPath, BlendMode, CharStyle, FillRule, FontDb, FontFace, GradientKind, HashMap, Justify, Node, NodeId,
NodeKind, Paint, PathData, Props, Rect, StrokeAlign, Styling, TextKind, TextObject, Writer, base64_encode, css, css_string, fmt_num,
@@ -163,7 +163,7 @@ impl Writer<'_> {
// Glyphs bent along a path (joined scripts on a curve) can only be outlines: the group
// names its text for screen readers and search.
let label = if lay.glyphs.iter().any(|g| g.bend.is_some()) {
format!(" role=\"img\" aria-label=\"{}\"", xml_escape(&t.plain_text()))
format!(" role=\"img\" aria-label=\"{}\"", xml_attr_valid(&t.plain_text()))
} else {
String::new()
};
+14
View File
@@ -277,3 +277,17 @@ fn invalid_symbol_name_alias_does_not_break_xml() {
}
}
}
#[test]
fn link_url_with_invalid_xml_characters_does_not_break_export() {
let mut doc = import(&svg(r#"<rect id="shape" width="20" height="20"/>"#)).unwrap();
let id = named(&doc, "shape").id;
doc.node_mut(id).unwrap().edit_attrs(|a| a.url = "http://a/\u{1}b\u{ffff}".into());
for styling in styles() {
let out = export_full(&doc, &ExportOptions { styling, ..Default::default() }, None);
let parsed = xml(&out.svg);
let a = parsed.descendants().find(|n| n.has_tag_name("a")).unwrap();
assert_eq!(a.attribute(("http://www.w3.org/1999/xlink", "href")), Some("http://a/b"));
assert!(out.warnings.iter().any(|w| w.contains("invalid XML characters")));
}
}