xdot/
xdot_parse.rs

1//! `xdot` draw attribute parser without the graph related parts.
2
3use nom::error::Error as NomError;
4
5pub mod draw;
6mod op_parser;
7mod ops;
8pub mod shapes;
9
10pub use self::draw::Pen;
11use self::shapes::Shape;
12
13#[cfg(feature = "pyo3")]
14fn try_into_shape(shape: &pyo3::Bound<'_, pyo3::PyAny>) -> pyo3::PyResult<Shape> {
15    use pyo3::prelude::*;
16
17    if let Ok(ell) = shape.extract::<shapes::Ellipse>() {
18        Ok(ell.into())
19    } else if let Ok(points) = shape.extract::<shapes::Points>() {
20        Ok(points.into())
21    } else if let Ok(text) = shape.extract::<shapes::Text>() {
22        Ok(text.into())
23    } else {
24        Err(pyo3::exceptions::PyTypeError::new_err(format!(
25            "Cannot convert object of type {} to Shape",
26            shape.get_type().name()?
27        )))
28    }
29}
30
31/// A [Shape] together with a [Pen].
32#[derive(Debug, Clone, PartialEq)]
33#[cfg_attr(feature = "pyo3", pyo3::pyclass(eq, module = "xdot_rs"))]
34pub struct ShapeDraw {
35    // #[pyo3(get, set)] not possible with cfg_attr
36    pub pen: Pen,
37    pub shape: Shape,
38}
39#[cfg(feature = "pyo3")]
40#[pyo3::pymethods]
41impl ShapeDraw {
42    #[new]
43    fn new(shape: &pyo3::Bound<'_, pyo3::PyAny>, pen: Pen) -> pyo3::PyResult<Self> {
44        let shape = try_into_shape(shape)?;
45        Ok(ShapeDraw { shape, pen })
46    }
47    #[getter]
48    fn get_pen(&self) -> Pen {
49        self.pen.clone()
50    }
51    #[setter]
52    fn set_pen(&mut self, pen: Pen) {
53        self.pen = pen;
54    }
55    #[getter]
56    fn get_shape<'py>(
57        &self,
58        py: pyo3::Python<'py>,
59    ) -> pyo3::PyResult<pyo3::Bound<'py, pyo3::PyAny>> {
60        use pyo3::IntoPyObjectExt;
61        match &self.shape {
62            Shape::Ellipse(e) => e.clone().into_bound_py_any(py),
63            Shape::Points(p) => p.clone().into_bound_py_any(py),
64            Shape::Text(t) => t.clone().into_bound_py_any(py),
65        }
66    }
67    #[setter]
68    fn set_shape(&mut self, shape: &pyo3::Bound<'_, pyo3::PyAny>) -> pyo3::PyResult<()> {
69        self.shape = try_into_shape(shape)?;
70        Ok(())
71    }
72}
73
74#[cfg(feature = "pyo3")]
75#[test]
76fn cmp_equal() {
77    use super::*;
78    use pyo3::{IntoPyObjectExt, prelude::*};
79
80    pyo3::prepare_freethreaded_python();
81
82    let ellip = shapes::Ellipse {
83        x: 0.,
84        y: 0.,
85        w: 0.,
86        h: 0.,
87        filled: true,
88    };
89    Python::with_gil(|py| {
90        let a = ShapeDraw::new(&ellip.clone().into_bound_py_any(py)?, Pen::default())?;
91        let b = ShapeDraw::new(&ellip.clone().into_bound_py_any(py)?, Pen::default())?;
92        assert!(
93            a.into_bound_py_any(py)?
94                .getattr("__eq__")?
95                .call1((b,))?
96                .extract::<bool>()?
97        );
98        Ok::<(), PyErr>(())
99    })
100    .unwrap();
101}
102
103/// Parse an `xdot` draw attribute (as defined [here](https://graphviz.org/docs/outputs/canon/#xdot)).
104/// Returns a vector of stateless drawing operations defining shape and style of the drawn node, edge, or label.
105pub fn parse(input: &str) -> Result<Vec<ShapeDraw>, NomError<&str>> {
106    use ops::Op::*;
107    let mut pen = Pen::default();
108    let mut shape_draws = vec![];
109    for op in op_parser::parse(input)? {
110        match op {
111            DrawShape(shape) => shape_draws.push(ShapeDraw {
112                pen: pen.clone(),
113                shape,
114            }),
115            SetFontCharacteristics(fc) => pen.font_characteristics = fc,
116            SetFillColor(color) => pen.fill_color = color,
117            SetPenColor(color) => pen.color = color,
118            SetFont { size, name } => {
119                pen.font_size = size;
120                pen.font_name = name;
121            }
122            SetStyle(style) => pen.line_style = style,
123            ExternalImage(_) => todo!("conversion of external image op"),
124        }
125    }
126    Ok(shape_draws)
127}
128
129#[cfg(feature = "pyo3")]
130#[pyo3::pyfunction]
131#[pyo3(name = "parse")]
132pub fn parse_py(input: &str) -> pyo3::PyResult<Vec<ShapeDraw>> {
133    use pyo3::{PyErr, exceptions::PyValueError};
134
135    parse(input).map_err(|e| PyErr::new::<PyValueError, _>(e.to_string()))
136}