mod tracing_macros;
use facet::Facet;
use facet_xml as xml;
use facet_xml::to_vec;
mod path;
mod points;
pub use path::{PathCommand, PathData, PathDataProxy};
pub use points::{Point, Points, PointsProxy};
pub const SVG_NS: &str = "http://www.w3.org/2000/svg";
pub type Error = facet_xml::DeserializeError<facet_xml::XmlError>;
pub type SerializeError = facet_xml::SerializeError<facet_xml::XmlSerializeError>;
pub fn from_str<T>(s: &str) -> Result<T, Error>
where
T: for<'a> Facet<'a>,
{
facet_xml::from_str(s)
}
pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError>
where
T: Facet<'facet> + ?Sized,
{
let bytes = to_vec(value)?;
Ok(String::from_utf8(bytes).expect("XmlSerializer produces valid UTF-8"))
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename = "svg",
rename_all = "camelCase",
skip_all_unless_truthy
)]
pub struct Svg {
#[facet(xml::attribute)]
pub width: Option<String>,
#[facet(xml::attribute)]
pub height: Option<String>,
#[facet(xml::attribute)]
pub view_box: Option<String>,
#[facet(flatten)]
pub children: Vec<SvgNode>,
}
#[derive(Facet, Debug, Clone)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
#[repr(u8)]
pub enum SvgNode {
G(Group),
Defs(Defs),
Style(Style),
Rect(Rect),
Circle(Circle),
Ellipse(Ellipse),
Line(Line),
Path(Path),
Polygon(Polygon),
Polyline(Polyline),
Text(Text),
Use(Use),
Image(Image),
Title(Title),
Desc(Desc),
Symbol(Symbol),
Filter(Filter),
Marker(Marker),
LinearGradient(LinearGradient),
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
pub struct Group {
#[facet(xml::attribute)]
pub id: Option<String>,
#[facet(xml::attribute)]
pub class: Option<String>,
#[facet(xml::attribute)]
pub transform: Option<String>,
#[facet(flatten)]
pub children: Vec<SvgNode>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
pub struct Defs {
#[facet(flatten)]
pub children: Vec<SvgNode>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(skip_all_unless_truthy)]
pub struct Style {
#[facet(xml::attribute, rename = "type")]
pub type_: Option<String>,
#[facet(xml::text)]
pub content: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "kebab-case",
skip_all_unless_truthy
)]
pub struct Rect {
#[facet(xml::attribute)]
pub x: Option<f64>,
#[facet(xml::attribute)]
pub y: Option<f64>,
#[facet(xml::attribute)]
pub width: Option<f64>,
#[facet(xml::attribute)]
pub height: Option<f64>,
#[facet(xml::attribute)]
pub rx: Option<f64>,
#[facet(xml::attribute)]
pub ry: Option<f64>,
#[facet(xml::attribute)]
pub fill: Option<String>,
#[facet(xml::attribute)]
pub stroke: Option<String>,
#[facet(xml::attribute)]
pub stroke_width: Option<String>,
#[facet(xml::attribute)]
pub stroke_dasharray: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "kebab-case",
skip_all_unless_truthy
)]
pub struct Circle {
#[facet(xml::attribute)]
pub cx: Option<f64>,
#[facet(xml::attribute)]
pub cy: Option<f64>,
#[facet(xml::attribute)]
pub r: Option<f64>,
#[facet(xml::attribute)]
pub fill: Option<String>,
#[facet(xml::attribute)]
pub stroke: Option<String>,
#[facet(xml::attribute)]
pub stroke_width: Option<String>,
#[facet(xml::attribute)]
pub stroke_dasharray: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "kebab-case",
skip_all_unless_truthy
)]
pub struct Ellipse {
#[facet(xml::attribute)]
pub cx: Option<f64>,
#[facet(xml::attribute)]
pub cy: Option<f64>,
#[facet(xml::attribute)]
pub rx: Option<f64>,
#[facet(xml::attribute)]
pub ry: Option<f64>,
#[facet(xml::attribute)]
pub fill: Option<String>,
#[facet(xml::attribute)]
pub stroke: Option<String>,
#[facet(xml::attribute)]
pub stroke_width: Option<String>,
#[facet(xml::attribute)]
pub stroke_dasharray: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "kebab-case",
skip_all_unless_truthy
)]
pub struct Line {
#[facet(xml::attribute)]
pub x1: Option<f64>,
#[facet(xml::attribute)]
pub y1: Option<f64>,
#[facet(xml::attribute)]
pub x2: Option<f64>,
#[facet(xml::attribute)]
pub y2: Option<f64>,
#[facet(xml::attribute)]
pub fill: Option<String>,
#[facet(xml::attribute)]
pub stroke: Option<String>,
#[facet(xml::attribute)]
pub stroke_width: Option<String>,
#[facet(xml::attribute)]
pub stroke_dasharray: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "kebab-case",
skip_all_unless_truthy
)]
pub struct Path {
#[facet(xml::attribute, proxy = PathDataProxy)]
pub d: Option<PathData>,
#[facet(xml::attribute)]
pub fill: Option<String>,
#[facet(xml::attribute)]
pub stroke: Option<String>,
#[facet(xml::attribute)]
pub stroke_width: Option<String>,
#[facet(xml::attribute)]
pub stroke_dasharray: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "kebab-case",
skip_all_unless_truthy
)]
pub struct Polygon {
#[facet(xml::attribute, proxy = PointsProxy)]
pub points: Points,
#[facet(xml::attribute)]
pub fill: Option<String>,
#[facet(xml::attribute)]
pub stroke: Option<String>,
#[facet(xml::attribute)]
pub stroke_width: Option<String>,
#[facet(xml::attribute)]
pub stroke_dasharray: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "kebab-case",
skip_all_unless_truthy
)]
pub struct Polyline {
#[facet(xml::attribute, proxy = PointsProxy)]
pub points: Points,
#[facet(xml::attribute)]
pub fill: Option<String>,
#[facet(xml::attribute)]
pub stroke: Option<String>,
#[facet(xml::attribute)]
pub stroke_width: Option<String>,
#[facet(xml::attribute)]
pub stroke_dasharray: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "kebab-case",
skip_all_unless_truthy
)]
pub struct Text {
#[facet(xml::attribute)]
pub x: Option<f64>,
#[facet(xml::attribute)]
pub y: Option<f64>,
#[facet(xml::attribute)]
pub transform: Option<String>,
#[facet(xml::attribute)]
pub fill: Option<String>,
#[facet(xml::attribute)]
pub stroke: Option<String>,
#[facet(xml::attribute)]
pub stroke_width: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
#[facet(xml::attribute)]
pub font_family: Option<String>,
#[facet(xml::attribute)]
pub font_style: Option<String>,
#[facet(xml::attribute)]
pub font_weight: Option<String>,
#[facet(xml::attribute)]
pub font_size: Option<String>,
#[facet(xml::attribute)]
pub text_anchor: Option<String>,
#[facet(xml::attribute)]
pub dominant_baseline: Option<String>,
#[facet(xml::text)]
pub content: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
pub struct Use {
#[facet(xml::attribute)]
pub x: Option<f64>,
#[facet(xml::attribute)]
pub y: Option<f64>,
#[facet(xml::attribute)]
pub width: Option<f64>,
#[facet(xml::attribute)]
pub height: Option<f64>,
#[facet(xml::attribute, rename = "xlink:href")]
pub href: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
pub struct Image {
#[facet(xml::attribute)]
pub x: Option<f64>,
#[facet(xml::attribute)]
pub y: Option<f64>,
#[facet(xml::attribute)]
pub width: Option<f64>,
#[facet(xml::attribute)]
pub height: Option<f64>,
#[facet(xml::attribute)]
pub href: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
pub struct Title {
#[facet(xml::text)]
pub content: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
pub struct Desc {
#[facet(xml::text)]
pub content: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "camelCase",
skip_all_unless_truthy
)]
pub struct Symbol {
#[facet(xml::attribute)]
pub id: Option<String>,
#[facet(xml::attribute)]
pub view_box: Option<String>,
#[facet(xml::attribute)]
pub width: Option<String>,
#[facet(xml::attribute)]
pub height: Option<String>,
#[facet(flatten)]
pub children: Vec<SvgNode>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
pub struct Filter {
#[facet(xml::attribute)]
pub id: Option<String>,
#[facet(flatten)]
pub children: Vec<FilterPrimitive>,
}
#[derive(Facet, Debug, Clone)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg")]
#[repr(u8)]
pub enum FilterPrimitive {
#[facet(rename = "feGaussianBlur")]
FeGaussianBlur(FeGaussianBlur),
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
pub struct FeGaussianBlur {
#[facet(xml::attribute)]
pub r#in: Option<String>,
#[facet(xml::attribute, rename = "stdDeviation")]
pub std_deviation: Option<String>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(
xml::ns_all = "http://www.w3.org/2000/svg",
rename_all = "camelCase",
skip_all_unless_truthy
)]
pub struct Marker {
#[facet(xml::attribute)]
pub id: Option<String>,
#[facet(xml::attribute)]
pub marker_width: Option<String>,
#[facet(xml::attribute)]
pub marker_height: Option<String>,
#[facet(xml::attribute, rename = "refX")]
pub ref_x: Option<String>,
#[facet(xml::attribute, rename = "refY")]
pub ref_y: Option<String>,
#[facet(xml::attribute)]
pub orient: Option<String>,
#[facet(flatten)]
pub children: Vec<SvgNode>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
pub struct LinearGradient {
#[facet(xml::attribute)]
pub id: Option<String>,
#[facet(xml::attribute)]
pub x1: Option<String>,
#[facet(xml::attribute)]
pub y1: Option<String>,
#[facet(xml::attribute)]
pub x2: Option<String>,
#[facet(xml::attribute)]
pub y2: Option<String>,
#[facet(xml::elements, rename = "stop")]
pub stops: Vec<GradientStop>,
}
#[derive(Facet, Debug, Clone, Default)]
#[facet(xml::ns_all = "http://www.w3.org/2000/svg", skip_all_unless_truthy)]
pub struct GradientStop {
#[facet(xml::attribute)]
pub offset: Option<String>,
#[facet(xml::attribute)]
pub style: Option<String>,
#[facet(xml::attribute, rename = "stop-color")]
pub stop_color: Option<String>,
#[facet(xml::attribute, rename = "stop-opacity")]
pub stop_opacity: Option<String>,
}
pub use facet_xml;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_attributes_are_parsed() {
let xml = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M10,10L50,50" stroke="black"/>
</svg>"#;
let svg: Svg = from_str(xml).unwrap();
println!("Parsed SVG: {:?}", svg);
assert!(svg.view_box.is_some(), "viewBox should be parsed");
if let Some(SvgNode::Path(path)) = svg.children.first() {
println!("Parsed Path: {:?}", path);
assert!(path.d.is_some(), "path d attribute should be parsed");
assert!(
path.stroke.is_some(),
"path stroke attribute should be parsed"
);
} else {
panic!("Expected a Path element");
}
}
#[test]
fn test_svg_float_tolerance() {
use facet_assert::{SameOptions, SameReport, check_same_with_report};
let c_svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
<path d="M118.239,208.239L226.239,208.239Z"/>
</svg>"#;
let rust_svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
<path d="M118.2387401575,208.2387401575L226.2387401575,208.2387401575Z"/>
</svg>"#;
let c: Svg = from_str(c_svg).unwrap();
let rust: Svg = from_str(rust_svg).unwrap();
eprintln!("C SVG: {:?}", c);
eprintln!("Rust SVG: {:?}", rust);
let tolerance = 0.002;
let options = SameOptions::new().float_tolerance(tolerance);
let result = check_same_with_report(&c, &rust, options);
match &result {
SameReport::Same => eprintln!("Result: Same"),
SameReport::Different(report) => {
eprintln!("Result: Different");
eprintln!("XML diff:\n{}", report.render_ansi_xml());
}
SameReport::Opaque { type_name } => eprintln!("Result: Opaque({})", type_name),
}
assert!(
matches!(result, SameReport::Same),
"SVG values within float tolerance should be considered Same"
);
}
#[test]
fn test_polygon_points_serialization() {
let polygon = Polygon {
points: crate::points::Points::parse("50,10 90,90 10,90").unwrap(),
fill: Some("lime".to_string()),
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: None,
};
let xml = to_string(&polygon).unwrap();
assert!(
xml.contains("points="),
"Serialized polygon should contain points attribute, got: {}",
xml
);
assert!(
xml.contains(r#"points="50,10 90,90 10,90""#),
"Points attribute should have correct value, got: {}",
xml
);
}
}