1#![allow(clippy::comparison_chain)]
5
6use roxmltree::Error;
7
8use super::{AId, Document, EId, NodeId, NodeKind, SvgNode};
9
10const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
11
12pub(crate) fn parse_svg_text_element<'input>(
13 parent: roxmltree::Node<'_, 'input>,
14 parent_id: NodeId,
15 style_sheet: &simplecss::StyleSheet,
16 doc: &mut Document<'input>,
17) -> Result<(), Error> {
18 debug_assert_eq!(parent.tag_name().name(), "text");
19
20 let space = if doc.get(parent_id).has_attribute(AId::Space) {
21 get_xmlspace(doc, parent_id, XmlSpace::Default)
22 } else {
23 if let Some(node) = doc
24 .get(parent_id)
25 .ancestors()
26 .find(|n| n.has_attribute(AId::Space))
27 {
28 get_xmlspace(doc, node.id, XmlSpace::Default)
29 } else {
30 XmlSpace::Default
31 }
32 };
33
34 parse_svg_text_element_impl(parent, parent_id, style_sheet, space, doc)?;
35
36 trim_text_nodes(parent_id, space, doc);
37 Ok(())
38}
39
40fn parse_svg_text_element_impl<'input>(
41 parent: roxmltree::Node<'_, 'input>,
42 parent_id: NodeId,
43 style_sheet: &simplecss::StyleSheet,
44 space: XmlSpace,
45 doc: &mut Document<'input>,
46) -> Result<(), Error> {
47 for node in parent.children() {
48 if node.is_text() {
49 let text = trim_text(node.text().unwrap(), space);
50 doc.append(parent_id, NodeKind::Text(text));
51 continue;
52 }
53
54 let mut tag_name = match super::parse::parse_tag_name(node) {
55 Some(v) => v,
56 None => continue,
57 };
58
59 if tag_name == EId::A {
60 tag_name = EId::Tspan;
62 }
63
64 if !matches!(tag_name, EId::Tspan | EId::Tref | EId::TextPath) {
65 continue;
66 }
67
68 if tag_name == EId::TextPath && parent.tag_name().name() != "text" {
70 continue;
71 }
72
73 let mut is_tref = false;
75 if tag_name == EId::Tref {
76 tag_name = EId::Tspan;
77 is_tref = true;
78 }
79
80 let node_id =
81 super::parse::parse_svg_element(node, parent_id, tag_name, style_sheet, false, doc)?;
82 let space = get_xmlspace(doc, node_id, space);
83
84 if is_tref {
85 let link_value = node
86 .attribute((XLINK_NS, "href"))
87 .or_else(|| node.attribute("href"));
88
89 if let Some(href) = link_value {
90 if let Some(text) = resolve_tref_text(node.document(), href) {
91 let text = trim_text(&text, space);
92 doc.append(node_id, NodeKind::Text(text));
93 }
94 }
95 } else {
96 parse_svg_text_element_impl(node, node_id, style_sheet, space, doc)?;
97 }
98 }
99
100 Ok(())
101}
102
103fn resolve_tref_text(xml: &roxmltree::Document, href: &str) -> Option<String> {
104 let id = svgtypes::IRI::from_str(href).ok()?.0;
105
106 let node = xml.descendants().find(|n| n.attribute("id") == Some(id))?;
108
109 super::parse::parse_tag_name(node)?;
111
112 let text: String = node
120 .descendants()
121 .filter(|n| n.is_text())
122 .filter_map(|n| n.text())
123 .collect();
124 if text.is_empty() {
125 None
126 } else {
127 Some(text)
128 }
129}
130
131#[derive(Clone, Copy, PartialEq, Debug)]
132enum XmlSpace {
133 Default,
134 Preserve,
135}
136
137fn get_xmlspace(doc: &Document, node_id: NodeId, default: XmlSpace) -> XmlSpace {
138 match doc.get(node_id).attribute(AId::Space) {
139 Some("preserve") => XmlSpace::Preserve,
140 Some(_) => XmlSpace::Default,
141 _ => default,
142 }
143}
144
145trait StrTrim {
146 fn remove_first_space(&mut self);
147 fn remove_last_space(&mut self);
148}
149
150impl StrTrim for String {
151 fn remove_first_space(&mut self) {
152 debug_assert_eq!(self.chars().next().unwrap(), ' ');
153 self.drain(0..1);
154 }
155
156 fn remove_last_space(&mut self) {
157 debug_assert_eq!(self.chars().next_back().unwrap(), ' ');
158 self.pop();
159 }
160}
161
162fn trim_text_nodes(text_elem_id: NodeId, xmlspace: XmlSpace, doc: &mut Document) {
169 let mut nodes = Vec::new(); collect_text_nodes(doc.get(text_elem_id), 0, &mut nodes);
171
172 if nodes.len() == 1 {
176 let node_id = nodes[0].0;
179
180 if xmlspace == XmlSpace::Default {
181 if let NodeKind::Text(ref mut text) = doc.nodes[node_id.get_usize()].kind {
182 match text.len() {
183 0 => {} 1 => {
185 if text.as_bytes()[0] == b' ' {
187 text.clear();
188 }
189 }
190 _ => {
191 let c1 = text.as_bytes()[0];
193 let c2 = text.as_bytes()[text.len() - 1];
194
195 if c1 == b' ' {
196 text.remove_first_space();
197 }
198
199 if c2 == b' ' {
200 text.remove_last_space();
201 }
202 }
203 }
204 }
205 } else {
206 }
208 } else if nodes.len() > 1 {
209 let mut i = 0;
218 let len = nodes.len() - 1;
219 let mut last_non_empty: Option<NodeId> = None;
220 while i < len {
221 let (mut node1_id, depth1) = nodes[i];
223 let (node2_id, depth2) = nodes[i + 1];
224
225 if doc.get(node1_id).text().is_empty() {
226 if let Some(n) = last_non_empty {
227 node1_id = n;
228 }
229 }
230
231 let xmlspace1 = get_xmlspace(doc, doc.get(node1_id).parent().unwrap().id, xmlspace);
234 let xmlspace2 = get_xmlspace(doc, doc.get(node2_id).parent().unwrap().id, xmlspace);
235
236 let (c1, c2, c3, c4) = {
239 let text1 = doc.get(node1_id).text();
240 let text2 = doc.get(node2_id).text();
241
242 let bytes1 = text1.as_bytes();
243 let bytes2 = text2.as_bytes();
244
245 let c1 = bytes1.first().cloned();
246 let c2 = bytes1.last().cloned();
247 let c3 = bytes2.first().cloned();
248 let c4 = bytes2.last().cloned();
249
250 (c1, c2, c3, c4)
251 };
252
253 if depth1 < depth2 {
263 if c3 == Some(b' ') {
264 if xmlspace2 == XmlSpace::Default {
265 if let NodeKind::Text(ref mut text) = doc.nodes[node2_id.get_usize()].kind {
266 text.remove_first_space();
267 }
268 }
269 }
270 } else {
271 if c2 == Some(b' ') && c2 == c3 {
272 if xmlspace1 == XmlSpace::Default && xmlspace2 == XmlSpace::Default {
273 if let NodeKind::Text(ref mut text) = doc.nodes[node1_id.get_usize()].kind {
274 text.remove_last_space();
275 }
276 } else {
277 if xmlspace1 == XmlSpace::Preserve && xmlspace2 == XmlSpace::Default {
278 if let NodeKind::Text(ref mut text) =
279 doc.nodes[node2_id.get_usize()].kind
280 {
281 text.remove_first_space();
282 }
283 }
284 }
285 }
286 }
287
288 let is_first = i == 0;
289 let is_last = i == len - 1;
290
291 if is_first
292 && c1 == Some(b' ')
293 && xmlspace1 == XmlSpace::Default
294 && !doc.get(node1_id).text().is_empty()
295 {
296 if let NodeKind::Text(ref mut text) = doc.nodes[node1_id.get_usize()].kind {
298 text.remove_first_space();
299 }
300 } else if is_last
301 && c4 == Some(b' ')
302 && !doc.get(node2_id).text().is_empty()
303 && xmlspace2 == XmlSpace::Default
304 {
305 if let NodeKind::Text(ref mut text) = doc.nodes[node2_id.get_usize()].kind {
308 text.remove_last_space();
309 }
310 }
311
312 if is_last
313 && c2 == Some(b' ')
314 && !doc.get(node1_id).text().is_empty()
315 && doc.get(node2_id).text().is_empty()
316 && doc.get(node1_id).text().ends_with(' ')
317 {
318 if let NodeKind::Text(ref mut text) = doc.nodes[node1_id.get_usize()].kind {
319 text.remove_last_space();
320 }
321 }
322
323 if !doc.get(node1_id).text().trim().is_empty() {
324 last_non_empty = Some(node1_id);
325 }
326
327 i += 1;
328 }
329 }
330
331 }
333
334fn collect_text_nodes(parent: SvgNode, depth: usize, nodes: &mut Vec<(NodeId, usize)>) {
335 for child in parent.children() {
336 if child.is_text() {
337 nodes.push((child.id, depth));
338 } else if child.is_element() {
339 collect_text_nodes(child, depth + 1, nodes);
340 }
341 }
342}
343
344fn trim_text(text: &str, space: XmlSpace) -> String {
345 let mut s = String::with_capacity(text.len());
346
347 let mut prev = '0';
348 for c in text.chars() {
349 let c = match c {
351 '\r' | '\n' | '\t' => ' ',
352 _ => c,
353 };
354
355 if space == XmlSpace::Default && c == ' ' && c == prev {
357 continue;
358 }
359
360 prev = c;
361
362 s.push(c);
363 }
364
365 s
366}