1use facet::Facet;
19use facet_xml_legacy as xml;
20
21mod path;
22mod points;
23
24pub use path::{PathCommand, PathData, PathDataProxy};
25pub use points::{Point, Points, PointsProxy};
26
27pub const SVG_NS: &str = "http://www.w3.org/2000/svg";
29
30#[derive(Facet, Debug, Clone, Default)]
32#[facet(
33 xml::ns_all = "http://www.w3.org/2000/svg",
34 rename = "svg",
35 rename_all = "camelCase",
36 skip_all_unless_truthy
37)]
38pub struct Svg {
39 #[facet(xml::attribute)]
41 pub width: Option<String>,
42 #[facet(xml::attribute)]
43 pub height: Option<String>,
44 #[facet(xml::attribute)]
45 pub view_box: Option<String>,
46 #[facet(xml::elements)]
47 pub children: Vec<SvgNode>,
48}
49
50#[derive(Facet, Debug, Clone)]
52#[facet(xml::ns_all = "http://www.w3.org/2000/svg", rename_all = "lowercase")]
53#[repr(u8)]
54pub enum SvgNode {
55 G(Group),
56 Defs(Defs),
57 Style(Style),
58 Rect(Rect),
59 Circle(Circle),
60 Ellipse(Ellipse),
61 Line(Line),
62 Path(Path),
63 Polygon(Polygon),
64 Polyline(Polyline),
65 Text(Text),
66 Use(Use),
67 Image(Image),
68 Title(Title),
69 Desc(Desc),
70 Symbol(Symbol),
71}
72
73#[derive(Facet, Debug, Clone, Default)]
75#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
76pub struct Group {
77 #[facet(xml::attribute)]
78 pub id: Option<String>,
79 #[facet(xml::attribute)]
80 pub class: Option<String>,
81 #[facet(xml::attribute)]
82 pub transform: Option<String>,
83 #[facet(xml::elements)]
84 pub children: Vec<SvgNode>,
85}
86
87#[derive(Facet, Debug, Clone, Default)]
89#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
90pub struct Defs {
91 #[facet(xml::elements)]
92 pub children: Vec<SvgNode>,
93}
94
95#[derive(Facet, Debug, Clone, Default)]
97#[facet(skip_all_unless_truthy)]
98pub struct Style {
99 #[facet(xml::attribute, rename = "type")]
100 pub type_: Option<String>,
101 #[facet(xml::text)]
102 pub content: String,
103}
104
105#[derive(Facet, Debug, Clone, Default)]
107#[facet(
108 xml::ns_all = "http://www.w3.org/2000/svg",
109 rename_all = "kebab-case",
110 skip_all_unless_truthy
111)]
112pub struct Rect {
113 #[facet(xml::attribute)]
114 pub x: Option<f64>,
115 #[facet(xml::attribute)]
116 pub y: Option<f64>,
117 #[facet(xml::attribute)]
118 pub width: Option<f64>,
119 #[facet(xml::attribute)]
120 pub height: Option<f64>,
121 #[facet(xml::attribute)]
122 pub rx: Option<f64>,
123 #[facet(xml::attribute)]
124 pub ry: Option<f64>,
125 #[facet(xml::attribute)]
126 pub fill: Option<String>,
127 #[facet(xml::attribute)]
128 pub stroke: Option<String>,
129 #[facet(xml::attribute)]
130 pub stroke_width: Option<String>,
131 #[facet(xml::attribute)]
132 pub stroke_dasharray: Option<String>,
133 #[facet(xml::attribute)]
134 pub style: String,
135}
136
137#[derive(Facet, Debug, Clone, Default)]
139#[facet(
140 xml::ns_all = "http://www.w3.org/2000/svg",
141 rename_all = "kebab-case",
142 skip_all_unless_truthy
143)]
144pub struct Circle {
145 #[facet(xml::attribute)]
146 pub cx: Option<f64>,
147 #[facet(xml::attribute)]
148 pub cy: Option<f64>,
149 #[facet(xml::attribute)]
150 pub r: Option<f64>,
151 #[facet(xml::attribute)]
152 pub fill: Option<String>,
153 #[facet(xml::attribute)]
154 pub stroke: Option<String>,
155 #[facet(xml::attribute)]
156 pub stroke_width: Option<String>,
157 #[facet(xml::attribute)]
158 pub stroke_dasharray: Option<String>,
159 #[facet(xml::attribute)]
160 pub style: String,
161}
162
163#[derive(Facet, Debug, Clone, Default)]
165#[facet(
166 xml::ns_all = "http://www.w3.org/2000/svg",
167 rename_all = "kebab-case",
168 skip_all_unless_truthy
169)]
170pub struct Ellipse {
171 #[facet(xml::attribute)]
172 pub cx: Option<f64>,
173 #[facet(xml::attribute)]
174 pub cy: Option<f64>,
175 #[facet(xml::attribute)]
176 pub rx: Option<f64>,
177 #[facet(xml::attribute)]
178 pub ry: Option<f64>,
179 #[facet(xml::attribute)]
180 pub fill: Option<String>,
181 #[facet(xml::attribute)]
182 pub stroke: Option<String>,
183 #[facet(xml::attribute)]
184 pub stroke_width: Option<String>,
185 #[facet(xml::attribute)]
186 pub stroke_dasharray: Option<String>,
187 #[facet(xml::attribute)]
188 pub style: String,
189}
190
191#[derive(Facet, Debug, Clone, Default)]
193#[facet(
194 xml::ns_all = "http://www.w3.org/2000/svg",
195 rename_all = "kebab-case",
196 skip_all_unless_truthy
197)]
198pub struct Line {
199 #[facet(xml::attribute)]
200 pub x1: Option<f64>,
201 #[facet(xml::attribute)]
202 pub y1: Option<f64>,
203 #[facet(xml::attribute)]
204 pub x2: Option<f64>,
205 #[facet(xml::attribute)]
206 pub y2: Option<f64>,
207 #[facet(xml::attribute)]
208 pub fill: Option<String>,
209 #[facet(xml::attribute)]
210 pub stroke: Option<String>,
211 #[facet(xml::attribute)]
212 pub stroke_width: Option<String>,
213 #[facet(xml::attribute)]
214 pub stroke_dasharray: Option<String>,
215 #[facet(xml::attribute)]
216 pub style: String,
217}
218
219#[derive(Facet, Debug, Clone, Default)]
221#[facet(
222 xml::ns_all = "http://www.w3.org/2000/svg",
223 rename_all = "kebab-case",
224 skip_all_unless_truthy
225)]
226pub struct Path {
227 #[facet(xml::attribute, proxy = PathDataProxy)]
228 pub d: Option<PathData>,
229 #[facet(xml::attribute)]
230 pub fill: Option<String>,
231 #[facet(xml::attribute)]
232 pub stroke: Option<String>,
233 #[facet(xml::attribute)]
234 pub stroke_width: Option<String>,
235 #[facet(xml::attribute)]
236 pub stroke_dasharray: Option<String>,
237 #[facet(xml::attribute)]
238 pub style: String,
239}
240
241#[derive(Facet, Debug, Clone, Default)]
243#[facet(
244 xml::ns_all = "http://www.w3.org/2000/svg",
245 rename_all = "kebab-case",
246 skip_all_unless_truthy
247)]
248pub struct Polygon {
249 #[facet(xml::attribute, proxy = PointsProxy)]
250 pub points: Points,
251 #[facet(xml::attribute)]
252 pub fill: Option<String>,
253 #[facet(xml::attribute)]
254 pub stroke: Option<String>,
255 #[facet(xml::attribute)]
256 pub stroke_width: Option<String>,
257 #[facet(xml::attribute)]
258 pub stroke_dasharray: Option<String>,
259 #[facet(xml::attribute)]
260 pub style: String,
261}
262
263#[derive(Facet, Debug, Clone, Default)]
265#[facet(
266 xml::ns_all = "http://www.w3.org/2000/svg",
267 rename_all = "kebab-case",
268 skip_all_unless_truthy
269)]
270pub struct Polyline {
271 #[facet(xml::attribute, proxy = PointsProxy)]
272 pub points: Points,
273 #[facet(xml::attribute)]
274 pub fill: Option<String>,
275 #[facet(xml::attribute)]
276 pub stroke: Option<String>,
277 #[facet(xml::attribute)]
278 pub stroke_width: Option<String>,
279 #[facet(xml::attribute)]
280 pub stroke_dasharray: Option<String>,
281 #[facet(xml::attribute)]
282 pub style: String,
283}
284
285#[derive(Facet, Debug, Clone, Default)]
287#[facet(
288 xml::ns_all = "http://www.w3.org/2000/svg",
289 rename_all = "kebab-case",
290 skip_all_unless_truthy
291)]
292pub struct Text {
293 #[facet(xml::attribute)]
294 pub x: Option<f64>,
295 #[facet(xml::attribute)]
296 pub y: Option<f64>,
297 #[facet(xml::attribute)]
298 pub transform: Option<String>,
299 #[facet(xml::attribute)]
300 pub fill: Option<String>,
301 #[facet(xml::attribute)]
302 pub stroke: Option<String>,
303 #[facet(xml::attribute)]
304 pub stroke_width: Option<String>,
305 #[facet(xml::attribute)]
306 pub style: String,
307 #[facet(xml::attribute)]
308 pub font_family: Option<String>,
309 #[facet(xml::attribute)]
310 pub font_style: Option<String>,
311 #[facet(xml::attribute)]
312 pub font_weight: Option<String>,
313 #[facet(xml::attribute)]
314 pub font_size: Option<String>,
315 #[facet(xml::attribute)]
316 pub text_anchor: Option<String>,
317 #[facet(xml::attribute)]
318 pub dominant_baseline: Option<String>,
319 #[facet(xml::text)]
320 pub content: String,
321}
322
323#[derive(Facet, Debug, Clone, Default)]
325#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
326pub struct Use {
327 #[facet(xml::attribute)]
328 pub x: Option<f64>,
329 #[facet(xml::attribute)]
330 pub y: Option<f64>,
331 #[facet(xml::attribute)]
332 pub width: Option<f64>,
333 #[facet(xml::attribute)]
334 pub height: Option<f64>,
335 #[facet(xml::attribute, rename = "xlink:href")]
336 pub href: Option<String>,
337}
338
339#[derive(Facet, Debug, Clone, Default)]
341#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
342pub struct Image {
343 #[facet(xml::attribute)]
344 pub x: Option<f64>,
345 #[facet(xml::attribute)]
346 pub y: Option<f64>,
347 #[facet(xml::attribute)]
348 pub width: Option<f64>,
349 #[facet(xml::attribute)]
350 pub height: Option<f64>,
351 #[facet(xml::attribute)]
352 pub href: Option<String>,
353 #[facet(xml::attribute)]
354 pub style: String,
355}
356
357#[derive(Facet, Debug, Clone, Default)]
359#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
360pub struct Title {
361 #[facet(xml::text)]
362 pub content: String,
363}
364
365#[derive(Facet, Debug, Clone, Default)]
367#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
368pub struct Desc {
369 #[facet(xml::text)]
370 pub content: String,
371}
372
373#[derive(Facet, Debug, Clone, Default)]
375#[facet(
376 xml::ns_all = "http://www.w3.org/2000/svg",
377 rename_all = "camelCase",
378 skip_all_unless_truthy
379)]
380pub struct Symbol {
381 #[facet(xml::attribute)]
382 pub id: Option<String>,
383 #[facet(xml::attribute)]
384 pub view_box: Option<String>,
385 #[facet(xml::attribute)]
386 pub width: Option<String>,
387 #[facet(xml::attribute)]
388 pub height: Option<String>,
389 #[facet(xml::elements)]
390 pub children: Vec<SvgNode>,
391}
392
393pub use facet_xml_legacy;
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_attributes_are_parsed() {
402 let xml = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
403 <path d="M10,10L50,50" stroke="black"/>
404 </svg>"#;
405
406 let svg: Svg = facet_xml_legacy::from_str(xml).unwrap();
407
408 println!("Parsed SVG: {:?}", svg);
409
410 assert!(svg.view_box.is_some(), "viewBox should be parsed");
412
413 if let Some(SvgNode::Path(path)) = svg.children.first() {
414 println!("Parsed Path: {:?}", path);
415 assert!(path.d.is_some(), "path d attribute should be parsed");
416 assert!(
417 path.stroke.is_some(),
418 "path stroke attribute should be parsed"
419 );
420 } else {
421 panic!("Expected a Path element");
422 }
423 }
424
425 #[test]
426 fn test_svg_float_tolerance() {
427 use facet_assert::{SameOptions, SameReport, check_same_with_report};
428
429 let c_svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
432 <path d="M118.239,208.239L226.239,208.239Z"/>
433 </svg>"#;
434
435 let rust_svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
436 <path d="M118.2387401575,208.2387401575L226.2387401575,208.2387401575Z"/>
437 </svg>"#;
438
439 let c: Svg = facet_xml_legacy::from_str(c_svg).unwrap();
440 let rust: Svg = facet_xml_legacy::from_str(rust_svg).unwrap();
441
442 eprintln!("C SVG: {:?}", c);
443 eprintln!("Rust SVG: {:?}", rust);
444
445 let tolerance = 0.002;
446 let options = SameOptions::new().float_tolerance(tolerance);
447
448 let result = check_same_with_report(&c, &rust, options);
449
450 match &result {
451 SameReport::Same => eprintln!("Result: Same"),
452 SameReport::Different(report) => {
453 eprintln!("Result: Different");
454 eprintln!("XML diff:\n{}", report.render_ansi_xml());
455 }
456 SameReport::Opaque { type_name } => eprintln!("Result: Opaque({})", type_name),
457 }
458
459 assert!(
460 matches!(result, SameReport::Same),
461 "SVG values within float tolerance should be considered Same"
462 );
463 }
464}