bnto_vector/optimize/
xml_passes.rs1use roxmltree::{Attribute, Document, ExpandedName};
7
8pub fn collect_editor_prefixes(doc: &Document, editor_uris: &[&str]) -> Vec<String> {
14 let svg = match doc.root().first_element_child() {
15 Some(el) => el,
16 None => return Vec::new(),
17 };
18
19 let mut prefixes = Vec::new();
20 for ns in svg.namespaces() {
21 if let Some(prefix) = ns.name()
22 && editor_uris.contains(&ns.uri())
23 {
24 prefixes.push(prefix.to_string());
25 }
26 }
27 prefixes
28}
29
30pub fn is_editor_element(tag: ExpandedName, editor_prefixes: &[String]) -> bool {
32 let ns = match tag.namespace() {
33 Some(ns) => ns,
34 None => return false,
35 };
36
37 if ns == "http://www.w3.org/2000/svg" || ns == "http://www.w3.org/1999/xlink" {
39 return false;
40 }
41
42 let _ = editor_prefixes; true
47}
48
49pub fn is_collapsible_container(tag_name: &str) -> bool {
51 matches!(
52 tag_name,
53 "g" | "defs" | "symbol" | "marker" | "clipPath" | "mask" | "pattern"
54 )
55}
56
57pub fn has_no_meaningful_attributes(node: &roxmltree::Node, editor_prefixes: &[String]) -> bool {
63 for attr in node.attributes() {
64 if is_editor_attribute(&attr, editor_prefixes) {
65 continue;
66 }
67 if attr.value().is_empty() {
68 continue;
69 }
70 return false;
72 }
73 true
74}
75
76pub fn is_editor_attribute(attr: &Attribute, editor_prefixes: &[String]) -> bool {
78 if let Some(ns) = attr.namespace()
80 && ns != "http://www.w3.org/2000/svg"
81 && ns != "http://www.w3.org/1999/xlink"
82 && ns != "http://www.w3.org/XML/1998/namespace"
83 {
84 return true;
85 }
86
87 if let Some(prefix) = attr.name().split(':').next()
89 && attr.name().contains(':')
90 && editor_prefixes.contains(&prefix.to_string())
91 {
92 return true;
93 }
94
95 false
96}
97
98pub fn minify_text(text: &str) -> String {
100 let trimmed = text.trim();
101 if trimmed.is_empty() {
102 return String::new();
103 }
104
105 let mut result = String::with_capacity(trimmed.len());
107 let mut prev_was_space = false;
108 for ch in trimmed.chars() {
109 if ch.is_whitespace() {
110 if !prev_was_space {
111 result.push(' ');
112 prev_was_space = true;
113 }
114 } else {
115 result.push(ch);
116 prev_was_space = false;
117 }
118 }
119 result
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_collect_editor_prefixes_finds_inkscape() {
128 let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"><rect/></svg>"#;
129 let doc = Document::parse(svg).unwrap();
130 let uris = &["http://www.inkscape.org/namespaces/inkscape"];
131 let prefixes = collect_editor_prefixes(&doc, uris);
132 assert_eq!(prefixes, vec!["inkscape"]);
133 }
134
135 #[test]
136 fn test_collect_editor_prefixes_ignores_svg_namespace() {
137 let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>"#;
138 let doc = Document::parse(svg).unwrap();
139 let uris = &["http://www.inkscape.org/namespaces/inkscape"];
140 let prefixes = collect_editor_prefixes(&doc, uris);
141 assert!(prefixes.is_empty());
142 }
143
144 #[test]
145 fn test_collapsible_containers() {
146 assert!(is_collapsible_container("g"));
147 assert!(is_collapsible_container("defs"));
148 assert!(is_collapsible_container("symbol"));
149 assert!(!is_collapsible_container("rect"));
150 assert!(!is_collapsible_container("svg"));
151 assert!(!is_collapsible_container("text"));
152 }
153
154 #[test]
155 fn test_minify_text_collapses_whitespace() {
156 assert_eq!(minify_text(" hello world "), "hello world");
157 assert_eq!(minify_text("\n \t "), "");
158 assert_eq!(minify_text("no change"), "no change");
159 assert_eq!(minify_text(" a b c "), "a b c");
160 }
161
162 #[test]
163 fn test_minify_text_empty_string() {
164 assert_eq!(minify_text(""), "");
165 assert_eq!(minify_text(" "), "");
166 }
167}