facet_svg_legacy/
lib.rs

1//! Facet-derived types for SVG parsing and serialization.
2//!
3//! This crate provides strongly-typed SVG elements that can be deserialized
4//! from XML using `facet-xml`.
5//!
6//! # Example
7//!
8//! ```rust
9//! use facet_svg_legacy::Svg;
10//!
11//! let svg_str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
12//!     <rect x="10" y="10" width="80" height="80" fill="blue"/>
13//! </svg>"#;
14//!
15//! let svg: Svg = facet_xml_legacy::from_str(svg_str).unwrap();
16//! ```
17
18use 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
27/// SVG namespace URI
28pub const SVG_NS: &str = "http://www.w3.org/2000/svg";
29
30/// Root SVG element
31#[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    // Note: xmlns is handled by ns_all, not as a separate field
40    #[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/// Any SVG node we care about
51#[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/// SVG group element (`<g>`)
74#[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/// SVG defs element (`<defs>`)
88#[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/// SVG style element (`<style>`)
96#[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/// SVG rect element (`<rect>`)
106#[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/// SVG circle element (`<circle>`)
138#[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/// SVG ellipse element (`<ellipse>`)
164#[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/// SVG line element (`<line>`)
192#[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/// SVG path element (`<path>`)
220#[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/// SVG polygon element (`<polygon>`)
242#[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/// SVG polyline element (`<polyline>`)
264#[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/// SVG text element (`<text>`)
286#[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/// SVG use element (`<use>`)
324#[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/// SVG image element (`<image>`)
340#[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/// SVG title element (`<title>`)
358#[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/// SVG description element (`<desc>`)
366#[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/// SVG symbol element (`<symbol>`)
374#[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
393// Re-export XML utilities for convenience
394pub 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        // These should NOT be None!
411        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        // Simulate C vs Rust precision - C has 3 decimals, Rust has 10
430        // (Without style difference to isolate float tolerance test)
431        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}