1mod tracing_macros;
19
20use facet::Facet;
21use facet_xml as xml;
22use facet_xml::to_vec;
23
24mod path;
25mod points;
26
27pub use path::{PathCommand, PathData, PathDataProxy};
28pub use points::{Point, Points, PointsProxy};
29
30pub const SVG_NS: &str = "http://www.w3.org/2000/svg";
32
33pub type Error = facet_xml::DeserializeError<facet_xml::XmlError>;
35
36pub type SerializeError = facet_xml::SerializeError<facet_xml::XmlSerializeError>;
38
39pub fn from_str<T>(s: &str) -> Result<T, Error>
41where
42 T: for<'a> Facet<'a>,
43{
44 facet_xml::from_str(s)
45}
46
47pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError>
49where
50 T: Facet<'facet> + ?Sized,
51{
52 let bytes = to_vec(value)?;
53 Ok(String::from_utf8(bytes).expect("XmlSerializer produces valid UTF-8"))
55}
56
57#[derive(Facet, Debug, Clone, Default)]
59#[facet(
60 xml::ns_all = "http://www.w3.org/2000/svg",
61 rename = "svg",
62 rename_all = "camelCase",
63 skip_all_unless_truthy
64)]
65pub struct Svg {
66 #[facet(xml::attribute)]
68 pub width: Option<String>,
69 #[facet(xml::attribute)]
70 pub height: Option<String>,
71 #[facet(xml::attribute)]
72 pub view_box: Option<String>,
73 #[facet(flatten)]
74 pub children: Vec<SvgNode>,
75}
76
77#[derive(Facet, Debug, Clone)]
79#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
80#[repr(u8)]
81pub enum SvgNode {
82 G(Group),
83 Defs(Defs),
84 Style(Style),
85 Rect(Rect),
86 Circle(Circle),
87 Ellipse(Ellipse),
88 Line(Line),
89 Path(Path),
90 Polygon(Polygon),
91 Polyline(Polyline),
92 Text(Text),
93 Use(Use),
94 Image(Image),
95 Title(Title),
96 Desc(Desc),
97 Symbol(Symbol),
98 Filter(Filter),
99 Marker(Marker),
100 LinearGradient(LinearGradient),
101}
102
103#[derive(Facet, Debug, Clone, Default)]
105#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
106pub struct Group {
107 #[facet(xml::attribute)]
108 pub id: Option<String>,
109 #[facet(xml::attribute)]
110 pub class: Option<String>,
111 #[facet(xml::attribute)]
112 pub transform: Option<String>,
113 #[facet(flatten)]
114 pub children: Vec<SvgNode>,
115}
116
117#[derive(Facet, Debug, Clone, Default)]
119#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
120pub struct Defs {
121 #[facet(flatten)]
122 pub children: Vec<SvgNode>,
123}
124
125#[derive(Facet, Debug, Clone, Default)]
127#[facet(skip_all_unless_truthy)]
128pub struct Style {
129 #[facet(xml::attribute, rename = "type")]
130 pub type_: Option<String>,
131 #[facet(xml::text)]
132 pub content: Option<String>,
133}
134
135#[derive(Facet, Debug, Clone, Default)]
137#[facet(
138 xml::ns_all = "http://www.w3.org/2000/svg",
139 rename_all = "kebab-case",
140 skip_all_unless_truthy
141)]
142pub struct Rect {
143 #[facet(xml::attribute)]
144 pub x: Option<f64>,
145 #[facet(xml::attribute)]
146 pub y: Option<f64>,
147 #[facet(xml::attribute)]
148 pub width: Option<f64>,
149 #[facet(xml::attribute)]
150 pub height: Option<f64>,
151 #[facet(xml::attribute)]
152 pub rx: Option<f64>,
153 #[facet(xml::attribute)]
154 pub ry: Option<f64>,
155 #[facet(xml::attribute)]
156 pub fill: Option<String>,
157 #[facet(xml::attribute)]
158 pub stroke: Option<String>,
159 #[facet(xml::attribute)]
160 pub stroke_width: Option<String>,
161 #[facet(xml::attribute)]
162 pub stroke_dasharray: Option<String>,
163 #[facet(xml::attribute)]
164 pub style: Option<String>,
165}
166
167#[derive(Facet, Debug, Clone, Default)]
169#[facet(
170 xml::ns_all = "http://www.w3.org/2000/svg",
171 rename_all = "kebab-case",
172 skip_all_unless_truthy
173)]
174pub struct Circle {
175 #[facet(xml::attribute)]
176 pub cx: Option<f64>,
177 #[facet(xml::attribute)]
178 pub cy: Option<f64>,
179 #[facet(xml::attribute)]
180 pub r: Option<f64>,
181 #[facet(xml::attribute)]
182 pub fill: Option<String>,
183 #[facet(xml::attribute)]
184 pub stroke: Option<String>,
185 #[facet(xml::attribute)]
186 pub stroke_width: Option<String>,
187 #[facet(xml::attribute)]
188 pub stroke_dasharray: Option<String>,
189 #[facet(xml::attribute)]
190 pub style: Option<String>,
191}
192
193#[derive(Facet, Debug, Clone, Default)]
195#[facet(
196 xml::ns_all = "http://www.w3.org/2000/svg",
197 rename_all = "kebab-case",
198 skip_all_unless_truthy
199)]
200pub struct Ellipse {
201 #[facet(xml::attribute)]
202 pub cx: Option<f64>,
203 #[facet(xml::attribute)]
204 pub cy: Option<f64>,
205 #[facet(xml::attribute)]
206 pub rx: Option<f64>,
207 #[facet(xml::attribute)]
208 pub ry: Option<f64>,
209 #[facet(xml::attribute)]
210 pub fill: Option<String>,
211 #[facet(xml::attribute)]
212 pub stroke: Option<String>,
213 #[facet(xml::attribute)]
214 pub stroke_width: Option<String>,
215 #[facet(xml::attribute)]
216 pub stroke_dasharray: Option<String>,
217 #[facet(xml::attribute)]
218 pub style: Option<String>,
219}
220
221#[derive(Facet, Debug, Clone, Default)]
223#[facet(
224 xml::ns_all = "http://www.w3.org/2000/svg",
225 rename_all = "kebab-case",
226 skip_all_unless_truthy
227)]
228pub struct Line {
229 #[facet(xml::attribute)]
230 pub x1: Option<f64>,
231 #[facet(xml::attribute)]
232 pub y1: Option<f64>,
233 #[facet(xml::attribute)]
234 pub x2: Option<f64>,
235 #[facet(xml::attribute)]
236 pub y2: Option<f64>,
237 #[facet(xml::attribute)]
238 pub fill: Option<String>,
239 #[facet(xml::attribute)]
240 pub stroke: Option<String>,
241 #[facet(xml::attribute)]
242 pub stroke_width: Option<String>,
243 #[facet(xml::attribute)]
244 pub stroke_dasharray: Option<String>,
245 #[facet(xml::attribute)]
246 pub style: Option<String>,
247}
248
249#[derive(Facet, Debug, Clone, Default)]
251#[facet(
252 xml::ns_all = "http://www.w3.org/2000/svg",
253 rename_all = "kebab-case",
254 skip_all_unless_truthy
255)]
256pub struct Path {
257 #[facet(xml::attribute, proxy = PathDataProxy)]
258 pub d: Option<PathData>,
259 #[facet(xml::attribute)]
260 pub fill: Option<String>,
261 #[facet(xml::attribute)]
262 pub stroke: Option<String>,
263 #[facet(xml::attribute)]
264 pub stroke_width: Option<String>,
265 #[facet(xml::attribute)]
266 pub stroke_dasharray: Option<String>,
267 #[facet(xml::attribute)]
268 pub style: Option<String>,
269}
270
271#[derive(Facet, Debug, Clone, Default)]
273#[facet(
274 xml::ns_all = "http://www.w3.org/2000/svg",
275 rename_all = "kebab-case",
276 skip_all_unless_truthy
277)]
278pub struct Polygon {
279 #[facet(xml::attribute, proxy = PointsProxy)]
280 pub points: Points,
281 #[facet(xml::attribute)]
282 pub fill: Option<String>,
283 #[facet(xml::attribute)]
284 pub stroke: Option<String>,
285 #[facet(xml::attribute)]
286 pub stroke_width: Option<String>,
287 #[facet(xml::attribute)]
288 pub stroke_dasharray: Option<String>,
289 #[facet(xml::attribute)]
290 pub style: Option<String>,
291}
292
293#[derive(Facet, Debug, Clone, Default)]
295#[facet(
296 xml::ns_all = "http://www.w3.org/2000/svg",
297 rename_all = "kebab-case",
298 skip_all_unless_truthy
299)]
300pub struct Polyline {
301 #[facet(xml::attribute, proxy = PointsProxy)]
302 pub points: Points,
303 #[facet(xml::attribute)]
304 pub fill: Option<String>,
305 #[facet(xml::attribute)]
306 pub stroke: Option<String>,
307 #[facet(xml::attribute)]
308 pub stroke_width: Option<String>,
309 #[facet(xml::attribute)]
310 pub stroke_dasharray: Option<String>,
311 #[facet(xml::attribute)]
312 pub style: Option<String>,
313}
314
315#[derive(Facet, Debug, Clone, Default)]
317#[facet(
318 xml::ns_all = "http://www.w3.org/2000/svg",
319 rename_all = "kebab-case",
320 skip_all_unless_truthy
321)]
322pub struct Text {
323 #[facet(xml::attribute)]
324 pub x: Option<f64>,
325 #[facet(xml::attribute)]
326 pub y: Option<f64>,
327 #[facet(xml::attribute)]
328 pub transform: Option<String>,
329 #[facet(xml::attribute)]
330 pub fill: Option<String>,
331 #[facet(xml::attribute)]
332 pub stroke: Option<String>,
333 #[facet(xml::attribute)]
334 pub stroke_width: Option<String>,
335 #[facet(xml::attribute)]
336 pub style: Option<String>,
337 #[facet(xml::attribute)]
338 pub font_family: Option<String>,
339 #[facet(xml::attribute)]
340 pub font_style: Option<String>,
341 #[facet(xml::attribute)]
342 pub font_weight: Option<String>,
343 #[facet(xml::attribute)]
344 pub font_size: Option<String>,
345 #[facet(xml::attribute)]
346 pub text_anchor: Option<String>,
347 #[facet(xml::attribute)]
348 pub dominant_baseline: Option<String>,
349 #[facet(xml::text)]
350 pub content: Option<String>,
351}
352
353#[derive(Facet, Debug, Clone, Default)]
355#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
356pub struct Use {
357 #[facet(xml::attribute)]
358 pub x: Option<f64>,
359 #[facet(xml::attribute)]
360 pub y: Option<f64>,
361 #[facet(xml::attribute)]
362 pub width: Option<f64>,
363 #[facet(xml::attribute)]
364 pub height: Option<f64>,
365 #[facet(xml::attribute, rename = "xlink:href")]
366 pub href: Option<String>,
367}
368
369#[derive(Facet, Debug, Clone, Default)]
371#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
372pub struct Image {
373 #[facet(xml::attribute)]
374 pub x: Option<f64>,
375 #[facet(xml::attribute)]
376 pub y: Option<f64>,
377 #[facet(xml::attribute)]
378 pub width: Option<f64>,
379 #[facet(xml::attribute)]
380 pub height: Option<f64>,
381 #[facet(xml::attribute)]
382 pub href: Option<String>,
383 #[facet(xml::attribute)]
384 pub style: Option<String>,
385}
386
387#[derive(Facet, Debug, Clone, Default)]
389#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
390pub struct Title {
391 #[facet(xml::text)]
392 pub content: Option<String>,
393}
394
395#[derive(Facet, Debug, Clone, Default)]
397#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
398pub struct Desc {
399 #[facet(xml::text)]
400 pub content: Option<String>,
401}
402
403#[derive(Facet, Debug, Clone, Default)]
405#[facet(
406 xml::ns_all = "http://www.w3.org/2000/svg",
407 rename_all = "camelCase",
408 skip_all_unless_truthy
409)]
410pub struct Symbol {
411 #[facet(xml::attribute)]
412 pub id: Option<String>,
413 #[facet(xml::attribute)]
414 pub view_box: Option<String>,
415 #[facet(xml::attribute)]
416 pub width: Option<String>,
417 #[facet(xml::attribute)]
418 pub height: Option<String>,
419 #[facet(flatten)]
420 pub children: Vec<SvgNode>,
421}
422
423#[derive(Facet, Debug, Clone, Default)]
425#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
426pub struct Filter {
427 #[facet(xml::attribute)]
428 pub id: Option<String>,
429 #[facet(flatten)]
430 pub children: Vec<FilterPrimitive>,
431}
432
433#[derive(Facet, Debug, Clone)]
435#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
436#[repr(u8)]
437pub enum FilterPrimitive {
438 #[facet(rename = "feGaussianBlur")]
439 FeGaussianBlur(FeGaussianBlur),
440}
441
442#[derive(Facet, Debug, Clone, Default)]
444#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
445pub struct FeGaussianBlur {
446 #[facet(xml::attribute)]
447 pub r#in: Option<String>,
448 #[facet(xml::attribute, rename = "stdDeviation")]
449 pub std_deviation: Option<String>,
450}
451
452#[derive(Facet, Debug, Clone, Default)]
454#[facet(
455 xml::ns_all = "http://www.w3.org/2000/svg",
456 rename_all = "camelCase",
457 skip_all_unless_truthy
458)]
459pub struct Marker {
460 #[facet(xml::attribute)]
461 pub id: Option<String>,
462 #[facet(xml::attribute)]
463 pub marker_width: Option<String>,
464 #[facet(xml::attribute)]
465 pub marker_height: Option<String>,
466 #[facet(xml::attribute, rename = "refX")]
467 pub ref_x: Option<String>,
468 #[facet(xml::attribute, rename = "refY")]
469 pub ref_y: Option<String>,
470 #[facet(xml::attribute)]
471 pub orient: Option<String>,
472 #[facet(flatten)]
473 pub children: Vec<SvgNode>,
474}
475
476#[derive(Facet, Debug, Clone, Default)]
478#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
479pub struct LinearGradient {
480 #[facet(xml::attribute)]
481 pub id: Option<String>,
482 #[facet(xml::attribute)]
483 pub x1: Option<String>,
484 #[facet(xml::attribute)]
485 pub y1: Option<String>,
486 #[facet(xml::attribute)]
487 pub x2: Option<String>,
488 #[facet(xml::attribute)]
489 pub y2: Option<String>,
490 #[facet(xml::elements, rename = "stop")]
491 pub stops: Vec<GradientStop>,
492}
493
494#[derive(Facet, Debug, Clone, Default)]
496#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
497pub struct GradientStop {
498 #[facet(xml::attribute)]
499 pub offset: Option<String>,
500 #[facet(xml::attribute)]
501 pub style: Option<String>,
502 #[facet(xml::attribute, rename = "stop-color")]
503 pub stop_color: Option<String>,
504 #[facet(xml::attribute, rename = "stop-opacity")]
505 pub stop_opacity: Option<String>,
506}
507
508pub use facet_xml;
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 #[test]
516 fn test_attributes_are_parsed() {
517 let xml = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
518 <path d="M10,10L50,50" stroke="black"/>
519 </svg>"#;
520
521 let svg: Svg = from_str(xml).unwrap();
522
523 println!("Parsed SVG: {:?}", svg);
524
525 assert!(svg.view_box.is_some(), "viewBox should be parsed");
527
528 if let Some(SvgNode::Path(path)) = svg.children.first() {
529 println!("Parsed Path: {:?}", path);
530 assert!(path.d.is_some(), "path d attribute should be parsed");
531 assert!(
532 path.stroke.is_some(),
533 "path stroke attribute should be parsed"
534 );
535 } else {
536 panic!("Expected a Path element");
537 }
538 }
539
540 #[test]
541 fn test_svg_float_tolerance() {
542 use facet_assert::{SameOptions, SameReport, check_same_with_report};
543
544 let c_svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
547 <path d="M118.239,208.239L226.239,208.239Z"/>
548 </svg>"#;
549
550 let rust_svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
551 <path d="M118.2387401575,208.2387401575L226.2387401575,208.2387401575Z"/>
552 </svg>"#;
553
554 let c: Svg = from_str(c_svg).unwrap();
555 let rust: Svg = from_str(rust_svg).unwrap();
556
557 eprintln!("C SVG: {:?}", c);
558 eprintln!("Rust SVG: {:?}", rust);
559
560 let tolerance = 0.002;
561 let options = SameOptions::new().float_tolerance(tolerance);
562
563 let result = check_same_with_report(&c, &rust, options);
564
565 match &result {
566 SameReport::Same => eprintln!("Result: Same"),
567 SameReport::Different(report) => {
568 eprintln!("Result: Different");
569 eprintln!("XML diff:\n{}", report.render_ansi_xml());
570 }
571 SameReport::Opaque { type_name } => eprintln!("Result: Opaque({})", type_name),
572 }
573
574 assert!(
575 matches!(result, SameReport::Same),
576 "SVG values within float tolerance should be considered Same"
577 );
578 }
579
580 #[test]
581 fn test_polygon_points_serialization() {
582 let polygon = Polygon {
583 points: crate::points::Points::parse("50,10 90,90 10,90").unwrap(),
584 fill: Some("lime".to_string()),
585 stroke: None,
586 stroke_width: None,
587 stroke_dasharray: None,
588 style: None,
589 };
590
591 let xml = to_string(&polygon).unwrap();
592
593 assert!(
595 xml.contains("points="),
596 "Serialized polygon should contain points attribute, got: {}",
597 xml
598 );
599 assert!(
601 xml.contains(r#"points="50,10 90,90 10,90""#),
602 "Points attribute should have correct value, got: {}",
603 xml
604 );
605 }
606}