1pub mod error;
2
3pub use error::*;
4
5use std::collections::BTreeMap;
6use std::fmt;
7use std::str::from_utf8;
8
9use graphitepdf_primitives as P;
10use quick_xml::Reader;
11use quick_xml::XmlVersion;
12use quick_xml::escape::unescape;
13use quick_xml::events::{BytesCData, BytesRef, BytesStart, BytesText, Event};
14
15pub type SvgProps = BTreeMap<String, String>;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum SvgNodeKind {
19 Svg,
20 G,
21 Path,
22 Rect,
23 Circle,
24 Ellipse,
25 Line,
26 Polyline,
27 Polygon,
28 Text,
29 Tspan,
30 Defs,
31 ClipPath,
32 LinearGradient,
33 RadialGradient,
34 Marker,
35 Stop,
36 Image,
37 Use,
38 TextInstance,
39}
40
41impl SvgNodeKind {
42 pub const fn primitive_name(self) -> &'static str {
43 match self {
44 Self::Svg => P::Svg,
45 Self::G => P::G,
46 Self::Path => P::Path,
47 Self::Rect => P::Rect,
48 Self::Circle => P::Circle,
49 Self::Ellipse => P::Ellipse,
50 Self::Line => P::Line,
51 Self::Polyline => P::Polyline,
52 Self::Polygon => P::Polygon,
53 Self::Text => P::Text,
54 Self::Tspan => P::Tspan,
55 Self::Defs => P::Defs,
56 Self::ClipPath => P::ClipPath,
57 Self::LinearGradient => P::LinearGradient,
58 Self::RadialGradient => P::RadialGradient,
59 Self::Marker => P::Marker,
60 Self::Stop => P::Stop,
61 Self::Image => P::Image,
62 Self::Use => P::Use,
63 Self::TextInstance => P::TextInstance,
64 }
65 }
66
67 fn from_tag_name(tag_name: &str) -> Option<Self> {
68 match tag_name {
69 "svg" => Some(Self::Svg),
70 "g" => Some(Self::G),
71 "path" => Some(Self::Path),
72 "rect" => Some(Self::Rect),
73 "circle" => Some(Self::Circle),
74 "ellipse" => Some(Self::Ellipse),
75 "line" => Some(Self::Line),
76 "polyline" => Some(Self::Polyline),
77 "polygon" => Some(Self::Polygon),
78 "text" => Some(Self::Text),
79 "tspan" => Some(Self::Tspan),
80 "defs" => Some(Self::Defs),
81 "clippath" => Some(Self::ClipPath),
82 "lineargradient" => Some(Self::LinearGradient),
83 "radialgradient" => Some(Self::RadialGradient),
84 "marker" => Some(Self::Marker),
85 "stop" => Some(Self::Stop),
86 "image" => Some(Self::Image),
87 "use" => Some(Self::Use),
88 _ => None,
89 }
90 }
91
92 const fn is_text_container(self) -> bool {
93 matches!(self, Self::Text | Self::Tspan)
94 }
95}
96
97impl fmt::Display for SvgNodeKind {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.write_str(self.primitive_name())
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct SvgNode {
105 pub kind: SvgNodeKind,
106 pub r#type: &'static str,
107 pub props: SvgProps,
108 pub children: Vec<SvgNode>,
109 pub value: Option<String>,
110}
111
112impl SvgNode {
113 pub fn new(kind: SvgNodeKind) -> Self {
114 Self {
115 kind,
116 r#type: kind.primitive_name(),
117 props: SvgProps::new(),
118 children: Vec::new(),
119 value: None,
120 }
121 }
122
123 pub fn empty_svg() -> Self {
124 Self::new(SvgNodeKind::Svg)
125 }
126
127 pub fn type_name(&self) -> &'static str {
128 self.r#type
129 }
130
131 fn text_instance(value: String) -> Self {
132 let mut node = Self::new(SvgNodeKind::TextInstance);
133 node.value = Some(value);
134 node
135 }
136}
137
138impl Default for SvgNode {
139 fn default() -> Self {
140 Self::empty_svg()
141 }
142}
143
144#[derive(Debug)]
145struct OpenNode {
146 tag_name: String,
147 node: SvgNode,
148}
149
150fn is_skipped_element(tag_name: &str) -> bool {
151 matches!(
152 tag_name,
153 "script"
154 | "foreignobject"
155 | "filter"
156 | "mask"
157 | "pattern"
158 | "symbol"
159 | "animate"
160 | "animatetransform"
161 | "animatemotion"
162 | "set"
163 )
164}
165
166fn to_camel_case(input: &str) -> String {
167 let mut result = String::with_capacity(input.len());
168 let mut capitalize_next = false;
169
170 for character in input.chars() {
171 if character == '-' || character == ':' {
172 capitalize_next = true;
173 } else if capitalize_next {
174 result.push(character.to_ascii_uppercase());
175 capitalize_next = false;
176 } else {
177 result.push(character);
178 }
179 }
180
181 result
182}
183
184fn parse_style_attribute(style_string: &str) -> SvgProps {
185 let mut props = SvgProps::new();
186
187 for declaration in style_string.split(';') {
188 let declaration = declaration.trim();
189 if declaration.is_empty() {
190 continue;
191 }
192
193 let Some(colon_index) = declaration.find(':') else {
194 continue;
195 };
196
197 let property = declaration[..colon_index].trim();
198 let value = declaration[colon_index + 1..].trim();
199
200 if !property.is_empty() && !value.is_empty() {
201 props.insert(to_camel_case(property), value.to_string());
202 }
203 }
204
205 props
206}
207
208fn convert_attributes(attributes: impl IntoIterator<Item = (String, String)>) -> SvgProps {
209 let mut props = SvgProps::new();
210
211 for (name, value) in attributes {
212 if name == "style" {
213 props.extend(parse_style_attribute(&value));
214 } else {
215 props.insert(to_camel_case(&name), value);
216 }
217 }
218
219 props
220}
221
222fn get_attributes_from_start(event: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
223 let mut attributes = Vec::new();
224
225 for attribute in event.attributes() {
226 let attribute = attribute?;
227 let key = from_utf8(attribute.key.as_ref())?.to_string();
228 let value = attribute
229 .normalized_value(XmlVersion::Implicit1_0)?
230 .into_owned();
231 attributes.push((key, value));
232 }
233
234 Ok(attributes)
235}
236
237fn decode_tag_name(bytes: &[u8]) -> Result<String> {
238 Ok(from_utf8(bytes)?.to_ascii_lowercase())
239}
240
241fn attach_node(root: &mut Option<SvgNode>, stack: &mut [OpenNode], node: SvgNode) {
242 if let Some(parent) = stack.last_mut() {
243 parent.node.children.push(node);
244 } else if root.is_none() {
245 *root = Some(node);
246 }
247}
248
249fn push_text_if_relevant(stack: &mut [OpenNode], text: String) {
250 let Some(parent) = stack.last_mut() else {
251 return;
252 };
253
254 if !parent.node.kind.is_text_container() {
255 return;
256 }
257
258 if text.trim().is_empty() {
259 if let Some(last_child) = parent.node.children.last_mut()
260 && last_child.kind == SvgNodeKind::TextInstance
261 && let Some(existing_value) = last_child.value.as_mut()
262 {
263 existing_value.push_str(&text);
264 }
265 return;
266 }
267
268 if let Some(last_child) = parent.node.children.last_mut()
269 && last_child.kind == SvgNodeKind::TextInstance
270 && let Some(existing_value) = last_child.value.as_mut()
271 {
272 existing_value.push_str(&text);
273 return;
274 }
275
276 parent
277 .node
278 .children
279 .push(SvgNode::text_instance(text.trim_start().to_string()));
280}
281
282fn trim_last_text_instance(node: &mut SvgNode) {
283 if !node.kind.is_text_container() {
284 return;
285 }
286
287 let Some(last_child) = node.children.last_mut() else {
288 return;
289 };
290
291 if last_child.kind != SvgNodeKind::TextInstance {
292 return;
293 }
294
295 let Some(value) = last_child.value.as_mut() else {
296 return;
297 };
298
299 let trimmed = value.trim_end();
300 if trimmed.len() != value.len() {
301 *value = trimmed.to_string();
302 }
303}
304
305fn decode_cdata_text(event: &BytesCData<'_>) -> Result<String> {
306 Ok(event.decode()?.into_owned())
307}
308
309fn decode_text(event: &BytesText<'_>) -> Result<String> {
310 Ok(event.decode()?.into_owned())
311}
312
313fn decode_general_reference(reference: &BytesRef<'_>) -> Result<String> {
314 let decoded = reference.decode()?.into_owned();
315 let escaped = if decoded.starts_with('&') {
316 decoded
317 } else {
318 format!("&{decoded};")
319 };
320
321 Ok(unescape(&escaped)?.into_owned())
322}
323
324fn collapse_stack(mut stack: Vec<OpenNode>, root: Option<SvgNode>) -> SvgNode {
325 if let Some(root) = root {
326 return root;
327 }
328
329 while stack.len() > 1 {
330 let child = stack.pop().expect("stack length checked").node;
331 if let Some(parent) = stack.last_mut() {
332 parent.node.children.push(child);
333 }
334 }
335
336 stack.pop().map(|entry| entry.node).unwrap_or_default()
337}
338
339pub fn try_parse_svg(svg_string: &str) -> Result<SvgNode> {
340 let mut reader = Reader::from_str(svg_string);
341 let mut buffer = Vec::new();
342 let mut stack: Vec<OpenNode> = Vec::new();
343 let mut root: Option<SvgNode> = None;
344 let mut skip_depth = 0usize;
345
346 loop {
347 match reader.read_event_into(&mut buffer)? {
348 Event::Start(event) => {
349 let tag_name = decode_tag_name(event.name().as_ref())?;
350
351 if skip_depth > 0 {
352 skip_depth += 1;
353 buffer.clear();
354 continue;
355 }
356
357 if is_skipped_element(&tag_name) {
358 skip_depth = 1;
359 buffer.clear();
360 continue;
361 }
362
363 let Some(kind) = SvgNodeKind::from_tag_name(&tag_name) else {
364 skip_depth = 1;
365 buffer.clear();
366 continue;
367 };
368
369 if let Some(parent) = stack.last_mut() {
370 trim_last_text_instance(&mut parent.node);
371 }
372
373 let mut node = SvgNode::new(kind);
374 node.props = convert_attributes(get_attributes_from_start(&event)?);
375
376 stack.push(OpenNode { tag_name, node });
377 }
378 Event::Empty(event) => {
379 let tag_name = decode_tag_name(event.name().as_ref())?;
380
381 if skip_depth > 0 || is_skipped_element(&tag_name) {
382 buffer.clear();
383 continue;
384 }
385
386 let Some(kind) = SvgNodeKind::from_tag_name(&tag_name) else {
387 buffer.clear();
388 continue;
389 };
390
391 if let Some(parent) = stack.last_mut() {
392 trim_last_text_instance(&mut parent.node);
393 }
394
395 let mut node = SvgNode::new(kind);
396 node.props = convert_attributes(get_attributes_from_start(&event)?);
397 attach_node(&mut root, &mut stack, node);
398 }
399 Event::End(event) => {
400 let tag_name = decode_tag_name(event.name().as_ref())?;
401
402 if skip_depth > 0 {
403 skip_depth -= 1;
404 buffer.clear();
405 continue;
406 }
407
408 let Some(last_tag_name) = stack.last().map(|entry| entry.tag_name.as_str()) else {
409 buffer.clear();
410 continue;
411 };
412
413 if last_tag_name != tag_name {
414 buffer.clear();
415 continue;
416 }
417
418 if let Some(last) = stack.last_mut() {
419 trim_last_text_instance(&mut last.node);
420 }
421
422 let node = stack.pop().expect("stack is not empty").node;
423 attach_node(&mut root, &mut stack, node);
424 }
425 Event::Text(event) => {
426 if skip_depth == 0 {
427 push_text_if_relevant(&mut stack, decode_text(&event)?);
428 }
429 }
430 Event::CData(event) => {
431 if skip_depth == 0 {
432 push_text_if_relevant(&mut stack, decode_cdata_text(&event)?);
433 }
434 }
435 Event::GeneralRef(reference) => {
436 if skip_depth == 0 {
437 push_text_if_relevant(&mut stack, decode_general_reference(&reference)?);
438 }
439 }
440 Event::Comment(_) | Event::Decl(_) | Event::DocType(_) | Event::PI(_) => {}
441 Event::Eof => break,
442 }
443
444 buffer.clear();
445 }
446
447 Ok(collapse_stack(stack, root))
448}
449
450pub fn parse_svg(svg_string: &str) -> SvgNode {
451 try_parse_svg(svg_string).unwrap_or_default()
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 fn props(entries: &[(&str, &str)]) -> SvgProps {
459 entries
460 .iter()
461 .map(|(name, value)| (String::from(*name), String::from(*value)))
462 .collect()
463 }
464
465 fn node(kind: SvgNodeKind, props_entries: &[(&str, &str)], children: Vec<SvgNode>) -> SvgNode {
466 SvgNode {
467 kind,
468 r#type: kind.primitive_name(),
469 props: props(props_entries),
470 children,
471 value: None,
472 }
473 }
474
475 fn text(value: &str) -> SvgNode {
476 SvgNode {
477 kind: SvgNodeKind::TextInstance,
478 r#type: SvgNodeKind::TextInstance.primitive_name(),
479 props: SvgProps::new(),
480 children: Vec::new(),
481 value: Some(value.to_string()),
482 }
483 }
484
485 #[test]
486 fn parses_dimensions_variants() {
487 let unitless =
488 parse_svg(r#"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="150"></svg>"#);
489 assert_eq!(
490 unitless,
491 node(
492 SvgNodeKind::Svg,
493 &[
494 ("height", "150"),
495 ("width", "200"),
496 ("xmlns", "http://www.w3.org/2000/svg")
497 ],
498 vec![],
499 )
500 );
501
502 let px = parse_svg(
503 r#"<svg xmlns="http://www.w3.org/2000/svg" width="96px" height="48px"></svg>"#,
504 );
505 assert_eq!(px.props.get("width"), Some(&"96px".to_string()));
506 assert_eq!(px.props.get("height"), Some(&"48px".to_string()));
507
508 let pt = parse_svg(
509 r#"<svg xmlns="http://www.w3.org/2000/svg" width="72pt" height="36pt"></svg>"#,
510 );
511 assert_eq!(pt.props.get("width"), Some(&"72pt".to_string()));
512 assert_eq!(pt.props.get("height"), Some(&"36pt".to_string()));
513
514 let inches =
515 parse_svg(r#"<svg xmlns="http://www.w3.org/2000/svg" width="1in" height="2in"></svg>"#);
516 assert_eq!(inches.props.get("width"), Some(&"1in".to_string()));
517 assert_eq!(inches.props.get("height"), Some(&"2in".to_string()));
518
519 let cm = parse_svg(
520 r#"<svg xmlns="http://www.w3.org/2000/svg" width="2.54cm" height="5.08cm"></svg>"#,
521 );
522 assert_eq!(cm.props.get("width"), Some(&"2.54cm".to_string()));
523 assert_eq!(cm.props.get("height"), Some(&"5.08cm".to_string()));
524
525 let mm = parse_svg(
526 r#"<svg xmlns="http://www.w3.org/2000/svg" width="25.4mm" height="50.8mm"></svg>"#,
527 );
528 assert_eq!(mm.props.get("width"), Some(&"25.4mm".to_string()));
529 assert_eq!(mm.props.get("height"), Some(&"50.8mm".to_string()));
530 }
531
532 #[test]
533 fn parses_viewbox_and_missing_dimensions() {
534 let view_box =
535 parse_svg(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 20 300 200"></svg>"#);
536 assert_eq!(
537 view_box.props.get("viewBox"),
538 Some(&"10 20 300 200".to_string())
539 );
540
541 let no_dimensions = parse_svg(r#"<svg xmlns="http://www.w3.org/2000/svg"></svg>"#);
542 assert_eq!(
543 no_dimensions,
544 node(
545 SvgNodeKind::Svg,
546 &[("xmlns", "http://www.w3.org/2000/svg")],
547 vec![],
548 )
549 );
550 }
551
552 #[test]
553 fn maps_basic_shapes() {
554 let svg = parse_svg(
555 r#"
556 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
557 <rect x="10" y="10" width="80" height="80" fill="red"/>
558 <circle cx="50" cy="50" r="25" fill="blue"/>
559 <ellipse cx="50" cy="50" rx="40" ry="20"/>
560 <line x1="0" y1="0" x2="100" y2="100" stroke="black"/>
561 <polyline points="0,0 50,50 100,0"/>
562 <polygon points="50,0 100,100 0,100"/>
563 </svg>"#,
564 );
565
566 assert_eq!(
567 svg,
568 node(
569 SvgNodeKind::Svg,
570 &[
571 ("height", "100"),
572 ("width", "100"),
573 ("xmlns", "http://www.w3.org/2000/svg")
574 ],
575 vec![
576 node(
577 SvgNodeKind::Rect,
578 &[
579 ("fill", "red"),
580 ("height", "80"),
581 ("width", "80"),
582 ("x", "10"),
583 ("y", "10")
584 ],
585 vec![],
586 ),
587 node(
588 SvgNodeKind::Circle,
589 &[("cx", "50"), ("cy", "50"), ("fill", "blue"), ("r", "25")],
590 vec![],
591 ),
592 node(
593 SvgNodeKind::Ellipse,
594 &[("cx", "50"), ("cy", "50"), ("rx", "40"), ("ry", "20")],
595 vec![],
596 ),
597 node(
598 SvgNodeKind::Line,
599 &[
600 ("stroke", "black"),
601 ("x1", "0"),
602 ("x2", "100"),
603 ("y1", "0"),
604 ("y2", "100")
605 ],
606 vec![],
607 ),
608 node(
609 SvgNodeKind::Polyline,
610 &[("points", "0,0 50,50 100,0")],
611 vec![]
612 ),
613 node(
614 SvgNodeKind::Polygon,
615 &[("points", "50,0 100,100 0,100")],
616 vec![]
617 ),
618 ],
619 )
620 );
621 }
622
623 #[test]
624 fn maps_path_gradients_groups_clip_path_and_image() {
625 let path = parse_svg(
626 r#"
627 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
628 <path d="M10 10 H 90 V 90 H 10 Z" fill="none" stroke="black"/>
629 </svg>"#,
630 );
631 assert_eq!(path.children[0].kind, SvgNodeKind::Path);
632
633 let gradients = parse_svg(
634 r#"
635 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
636 <defs>
637 <linearGradient id="lg1" x1="0%" y1="0%" x2="100%" y2="0%">
638 <stop offset="0%" stop-color="red"/>
639 <stop offset="100%" stop-color="blue"/>
640 </linearGradient>
641 <radialGradient id="rg1" cx="50%" cy="50%" r="50%">
642 <stop offset="0%" stop-color="white"/>
643 <stop offset="100%" stop-color="black"/>
644 </radialGradient>
645 </defs>
646 </svg>"#,
647 );
648 assert_eq!(gradients.children[0].kind, SvgNodeKind::Defs);
649 assert_eq!(
650 gradients.children[0].children[0].kind,
651 SvgNodeKind::LinearGradient
652 );
653 assert_eq!(
654 gradients.children[0].children[1].kind,
655 SvgNodeKind::RadialGradient
656 );
657 assert_eq!(
658 gradients.children[0].children[0].children[0]
659 .props
660 .get("stopColor"),
661 Some(&"red".to_string())
662 );
663
664 let groups = parse_svg(
665 r#"
666 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
667 <g transform="translate(10,10)">
668 <g opacity="0.5">
669 <rect width="50" height="50"/>
670 </g>
671 <circle cx="75" cy="75" r="10"/>
672 </g>
673 </svg>"#,
674 );
675 assert_eq!(groups.children[0].kind, SvgNodeKind::G);
676 assert_eq!(groups.children[0].children[0].kind, SvgNodeKind::G);
677 assert_eq!(groups.children[0].children[1].kind, SvgNodeKind::Circle);
678
679 let clip_path = parse_svg(
680 r#"
681 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
682 <defs>
683 <clipPath id="clip1">
684 <rect width="50" height="50"/>
685 </clipPath>
686 </defs>
687 </svg>"#,
688 );
689 assert_eq!(
690 clip_path.children[0].children[0].kind,
691 SvgNodeKind::ClipPath
692 );
693
694 let image = parse_svg(
695 r#"
696 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
697 <image href="photo.png" x="0" y="0" width="100" height="100"/>
698 </svg>"#,
699 );
700 assert_eq!(
701 image.children[0],
702 node(
703 SvgNodeKind::Image,
704 &[
705 ("height", "100"),
706 ("href", "photo.png"),
707 ("width", "100"),
708 ("x", "0"),
709 ("y", "0")
710 ],
711 vec![],
712 )
713 );
714 }
715
716 #[test]
717 fn handles_text_and_tspan_content() {
718 let text_svg = parse_svg(
719 r#"
720 <svg xmlns="http://www.w3.org/2000/svg" width="200" height="50">
721 <text x="10" y="30" font-size="20">Hello World</text>
722 </svg>"#,
723 );
724 assert_eq!(
725 text_svg.children[0],
726 node(
727 SvgNodeKind::Text,
728 &[("fontSize", "20"), ("x", "10"), ("y", "30")],
729 vec![text("Hello World")],
730 )
731 );
732
733 let tspan_svg = parse_svg(
734 r#"
735 <svg xmlns="http://www.w3.org/2000/svg" width="200" height="50">
736 <text x="10" y="30">
737 <tspan fill="red">Red</tspan>
738 <tspan fill="blue">Blue</tspan>
739 </text>
740 </svg>"#,
741 );
742 assert_eq!(
743 tspan_svg.children[0],
744 node(
745 SvgNodeKind::Text,
746 &[("x", "10"), ("y", "30")],
747 vec![
748 node(SvgNodeKind::Tspan, &[("fill", "red")], vec![text("Red")]),
749 node(SvgNodeKind::Tspan, &[("fill", "blue")], vec![text("Blue")]),
750 ],
751 )
752 );
753
754 let non_text = parse_svg(
755 r#"
756 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
757 <rect>some text</rect>
758 </svg>"#,
759 );
760 assert_eq!(non_text.children[0], node(SvgNodeKind::Rect, &[], vec![]));
761 }
762
763 #[test]
764 fn converts_attributes_and_styles() {
765 let camel_case = parse_svg(
766 r#"
767 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
768 <rect stroke-width="2" fill-opacity="0.5" stroke-dasharray="5,3" stroke-linecap="round"/>
769 </svg>"#,
770 );
771 assert_eq!(
772 camel_case.children[0],
773 node(
774 SvgNodeKind::Rect,
775 &[
776 ("fillOpacity", "0.5"),
777 ("strokeDasharray", "5,3"),
778 ("strokeLinecap", "round"),
779 ("strokeWidth", "2"),
780 ],
781 vec![],
782 )
783 );
784
785 let inline_style = parse_svg(
786 r#"
787 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
788 <rect style="fill:red;stroke:blue;stroke-width:2px;opacity:0.8"/>
789 </svg>"#,
790 );
791 assert_eq!(
792 inline_style.children[0],
793 node(
794 SvgNodeKind::Rect,
795 &[
796 ("fill", "red"),
797 ("opacity", "0.8"),
798 ("stroke", "blue"),
799 ("strokeWidth", "2px")
800 ],
801 vec![],
802 )
803 );
804
805 let merged = parse_svg(
806 r#"
807 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
808 <rect fill="green" width="50" height="50" style="stroke:blue;stroke-width:3"/>
809 </svg>"#,
810 );
811 assert_eq!(
812 merged.children[0],
813 node(
814 SvgNodeKind::Rect,
815 &[
816 ("fill", "green"),
817 ("height", "50"),
818 ("stroke", "blue"),
819 ("strokeWidth", "3"),
820 ("width", "50"),
821 ],
822 vec![],
823 )
824 );
825
826 let single_quoted = parse_svg(
827 r#"
828 <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>
829 <rect fill='red' width='50' height='50'/>
830 </svg>"#,
831 );
832 assert_eq!(
833 single_quoted.children[0],
834 node(
835 SvgNodeKind::Rect,
836 &[("fill", "red"), ("height", "50"), ("width", "50")],
837 vec![],
838 )
839 );
840 }
841
842 #[test]
843 fn decodes_entities_and_cdata_in_text() {
844 let entities = parse_svg(
845 r#"
846 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
847 <text x="10" y="30"><hello> & "world"</text>
848 </svg>"#,
849 );
850 assert_eq!(
851 entities.children[0].children,
852 vec![text("<hello> & \"world\"")]
853 );
854
855 let cdata = parse_svg(
856 r#"
857 <svg xmlns="http://www.w3.org/2000/svg" width="200" height="50">
858 <text x="10" y="30"><![CDATA[Some <special> text]]></text>
859 </svg>"#,
860 );
861 assert_eq!(
862 cdata.children[0].children,
863 vec![text("Some <special> text")]
864 );
865 }
866
867 #[test]
868 fn skips_unsupported_and_unknown_elements_like_ts_parser() {
869 let skipped = parse_svg(
870 r#"
871 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
872 <script>alert(1)</script>
873 <rect width="50" height="50"/>
874 <foreignObject><div>hi</div></foreignObject>
875 <circle cx="50" cy="50" r="10"/>
876 <filter id="f1"><feGaussianBlur/></filter>
877 <mask id="m1"><rect/></mask>
878 </svg>"#,
879 );
880 assert_eq!(
881 skipped,
882 node(
883 SvgNodeKind::Svg,
884 &[
885 ("height", "100"),
886 ("width", "100"),
887 ("xmlns", "http://www.w3.org/2000/svg")
888 ],
889 vec![
890 node(
891 SvgNodeKind::Rect,
892 &[("height", "50"), ("width", "50")],
893 vec![]
894 ),
895 node(
896 SvgNodeKind::Circle,
897 &[("cx", "50"), ("cy", "50"), ("r", "10")],
898 vec![]
899 ),
900 ],
901 )
902 );
903
904 let unknown = parse_svg(
905 r#"
906 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
907 <custom-element foo="bar"/>
908 <rect width="50" height="50"/>
909 </svg>"#,
910 );
911 assert_eq!(unknown.children.len(), 1);
912 assert_eq!(unknown.children[0].kind, SvgNodeKind::Rect);
913 }
914
915 #[test]
916 fn parses_use_nodes_and_xlink_references() {
917 let svg = parse_svg(
918 r##"
919 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
920 <defs>
921 <path id="shape" d="M0 0 L10 0 L10 10 Z"/>
922 </defs>
923 <use href="#shape" x="20" y="30"/>
924 <use xlink:href="#shape" transform="translate(40,0)"/>
925 </svg>"##,
926 );
927
928 assert_eq!(svg.children[0].kind, SvgNodeKind::Defs);
929 assert_eq!(svg.children[1].kind, SvgNodeKind::Use);
930 assert_eq!(
931 svg.children[1].props.get("href"),
932 Some(&"#shape".to_string())
933 );
934 assert_eq!(svg.children[1].props.get("x"), Some(&"20".to_string()));
935 assert_eq!(svg.children[2].kind, SvgNodeKind::Use);
936 assert_eq!(
937 svg.children[2].props.get("xlinkHref"),
938 Some(&"#shape".to_string())
939 );
940 }
941
942 #[test]
943 fn ignores_xml_preamble_doctype_and_comments() {
944 let xml_decl = parse_svg(
945 r#"<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="50" height="50"/></svg>"#,
946 );
947 assert_eq!(xml_decl.children[0].kind, SvgNodeKind::Rect);
948
949 let doctype = parse_svg(
950 r#"<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect/></svg>"#,
951 );
952 assert_eq!(doctype.children[0].kind, SvgNodeKind::Rect);
953
954 let comments = parse_svg(
955 r#"
956 <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
957 <!-- this is a comment -->
958 <rect width="50" height="50"/>
959 </svg>"#,
960 );
961 assert_eq!(comments.children[0].kind, SvgNodeKind::Rect);
962 }
963
964 #[test]
965 fn handles_invalid_input_like_ts_parser() {
966 let non_svg_root = parse_svg("<div>not svg</div>");
967 assert_eq!(non_svg_root, SvgNode::empty_svg());
968
969 let empty = parse_svg("");
970 assert_eq!(empty, SvgNode::empty_svg());
971
972 let invalid_view_box = parse_svg(
973 r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="bad"></svg>"#,
974 );
975 assert_eq!(
976 invalid_view_box.props.get("viewBox"),
977 Some(&"bad".to_string())
978 );
979
980 let short_view_box = parse_svg(
981 r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100"></svg>"#,
982 );
983 assert_eq!(
984 short_view_box.props.get("viewBox"),
985 Some(&"0 0 100".to_string())
986 );
987 }
988
989 #[test]
990 fn parses_real_world_like_examples() {
991 let icon = parse_svg(
992 r##"
993 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
994 <defs>
995 <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
996 <stop offset="0%" stop-color="#ff6b6b"/>
997 <stop offset="100%" stop-color="#4ecdc4"/>
998 </linearGradient>
999 </defs>
1000 <g fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1001 <path d="M12 2L2 7l10 5 10-5-10-5z"/>
1002 <path d="M2 17l10 5 10-5"/>
1003 <path d="M2 12l10 5 10-5"/>
1004 </g>
1005 </svg>"##,
1006 );
1007 assert_eq!(icon.children[0].kind, SvgNodeKind::Defs);
1008 assert_eq!(icon.children[1].kind, SvgNodeKind::G);
1009 assert_eq!(icon.children[1].children.len(), 3);
1010 assert_eq!(
1011 icon.children[1].props,
1012 props(&[
1013 ("fill", "none"),
1014 ("strokeLinecap", "round"),
1015 ("strokeLinejoin", "round"),
1016 ("strokeWidth", "2"),
1017 ])
1018 );
1019
1020 let chart = parse_svg(
1021 r##"
1022 <svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 200 100">
1023 <rect width="200" height="100" fill="#f0f0f0"/>
1024 <g transform="translate(20,80)">
1025 <line x1="0" y1="0" x2="160" y2="0" stroke="#ccc"/>
1026 <rect x="0" y="-60" width="30" height="60" style="fill:#4ecdc4;opacity:0.9"/>
1027 <rect x="40" y="-40" width="30" height="40" style="fill:#ff6b6b;opacity:0.9"/>
1028 <rect x="80" y="-75" width="30" height="75" style="fill:#45b7d1;opacity:0.9"/>
1029 </g>
1030 </svg>"##,
1031 );
1032 assert_eq!(chart.children.len(), 2);
1033 assert_eq!(chart.children[0].kind, SvgNodeKind::Rect);
1034 assert_eq!(chart.children[1].kind, SvgNodeKind::G);
1035 assert_eq!(chart.children[1].children.len(), 4);
1036 assert_eq!(
1037 chart.children[1].children[1].props,
1038 props(&[
1039 ("fill", "#4ecdc4"),
1040 ("height", "60"),
1041 ("opacity", "0.9"),
1042 ("width", "30"),
1043 ("x", "0"),
1044 ("y", "-60"),
1045 ])
1046 );
1047 }
1048
1049 #[test]
1050 fn exposes_typed_and_fallible_api() {
1051 let parsed =
1052 try_parse_svg(r#"<svg xmlns="http://www.w3.org/2000/svg"><text>Hello</text></svg>"#)
1053 .expect("valid SVG should parse");
1054
1055 assert_eq!(parsed.kind, SvgNodeKind::Svg);
1056 assert_eq!(parsed.type_name(), P::Svg);
1057 assert_eq!(parsed.children[0].kind, SvgNodeKind::Text);
1058 }
1059}