xdot/xdot_parse/
op_parser.rs

1//! Stateless parser extracting `xdot` operations.
2//! See the [syntax documentation](https://graphviz.org/docs/outputs/canon/#xdot) for details.
3use std::str::FromStr;
4
5use nom::{
6    Finish, IResult, ToUsize,
7    branch::alt,
8    bytes::complete::{tag, take_while_m_n},
9    character::complete::{char, multispace0, multispace1, one_of},
10    combinator::{eof, flat_map, map, map_parser, map_res, recognize, value},
11    error::{Error as NomError, ParseError},
12    multi::{count, many0, many1, separated_list1},
13    number::complete::float,
14    sequence::{delimited, preceded, separated_pair, terminated, tuple},
15};
16
17use super::shapes::ExternalImage;
18
19use super::{
20    draw::{FontCharacteristics, Rgba, Style},
21    ops::Op,
22    shapes::{Ellipse, Points, PointsType, Text, TextAlign},
23};
24
25// Combinators
26
27/// Take `count` bytes and throw an error if they don’t match a char boundary
28fn take_bytes<'a, C: ToUsize, E: ParseError<&'a str>>(
29    count: C,
30) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str, E> {
31    let count = count.to_usize();
32    move |input: &str| {
33        if input.is_char_boundary(count) {
34            Ok((&input[count..], &input[..count]))
35        } else {
36            // TODO: better error
37            Err(nom::Err::Error(E::from_error_kind(
38                input,
39                nom::error::ErrorKind::Count,
40            )))
41        }
42    }
43}
44
45fn decimal(input: &str) -> IResult<&str, &str> {
46    recognize(many1(terminated(one_of("0123456789"), many0(char('_')))))(input)
47}
48
49/// A combinator that takes a parser `inner` and produces a parser that also consumes both leading and
50/// trailing whitespace, returning the output of `inner`.
51fn ws<'a, F, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
52where
53    F: FnMut(&'a str) -> IResult<&'a str, O, E>,
54{
55    delimited(multispace0, inner, multispace0)
56}
57
58fn tagged<'a, F, O, E: ParseError<&'a str>>(
59    t: &'static str,
60    inner: F,
61) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
62where
63    F: FnMut(&'a str) -> IResult<&'a str, O, E>,
64{
65    preceded(tuple((tag(t), multispace1)), inner)
66}
67
68// Data type parsers
69
70/// Parse xdot’s “n -b₁b₂...bₙ” pattern
71fn parse_string(input: &str) -> IResult<&str, &str> {
72    flat_map(map_res(decimal, usize::from_str), |n| {
73        preceded(tuple((multispace1, tag("-"))), take_bytes(n))
74    })(input)
75}
76
77fn parse_text_align(input: &str) -> IResult<&str, TextAlign> {
78    alt((
79        value(TextAlign::Left, tag("-1")),
80        value(TextAlign::Center, tag("0")),
81        value(TextAlign::Right, tag("1")),
82    ))(input)
83}
84
85fn from_hex(input: &str) -> Result<u8, std::num::ParseIntError> {
86    u8::from_str_radix(input, 16)
87}
88
89fn is_hex_digit(c: char) -> bool {
90    c.is_ascii_hexdigit()
91}
92
93fn hex_primary(input: &str) -> IResult<&str, u8> {
94    map_res(take_while_m_n(2, 2, is_hex_digit), from_hex)(input)
95}
96
97fn hex_color(input: &str) -> IResult<&str, Rgba> {
98    let (input, _) = tag("#")(input)?;
99    let (input, (r, g, b)) = tuple((hex_primary, hex_primary, hex_primary))(input)?;
100    Ok((input, Rgba { r, g, b, a: 0xff }))
101}
102
103// Op parsers
104
105fn parse_op_draw_shape_ellipse(input: &str) -> IResult<&str, Op> {
106    let (input, (c, x, y, w, h)) = tuple((
107        one_of("Ee"),
108        preceded(multispace1, float),
109        preceded(multispace1, float),
110        preceded(multispace1, float),
111        preceded(multispace1, float),
112    ))(input)?;
113    let ellip = Ellipse {
114        filled: c == 'E',
115        x,
116        y,
117        w,
118        h,
119    };
120    Ok((input, ellip.into()))
121}
122
123fn parse_op_draw_shape_points(input: &str) -> IResult<&str, Op> {
124    let (input, (c, points)) = tuple((
125        terminated(one_of("PpLBb"), multispace1),
126        flat_map(map_res(decimal, usize::from_str), |n| {
127            count(
128                tuple((preceded(multispace1, float), preceded(multispace1, float))),
129                n,
130            )
131        }),
132    ))(input)?;
133    let points = Points {
134        filled: c == 'P' || c == 'b',
135        r#type: match c {
136            'P' | 'p' => PointsType::Polygon,
137            'L' => PointsType::Polyline,
138            'B' | 'b' => PointsType::BSpline,
139            _ => unreachable!(),
140        },
141        points,
142    };
143    Ok((input, points.into()))
144}
145
146fn parse_op_draw_shape_text(input: &str) -> IResult<&str, Op> {
147    let (input, (x, y, align, width, text)) = tagged(
148        "T",
149        tuple((
150            terminated(float, multispace1),            // x
151            terminated(float, multispace1),            // y
152            terminated(parse_text_align, multispace1), // align
153            terminated(float, multispace1),            // width
154            parse_string,
155        )),
156    )(input)?;
157    let text = Text {
158        x,
159        y,
160        align,
161        width,
162        text: text.to_owned(),
163    };
164    Ok((input, text.into()))
165}
166
167fn parse_op_draw_shape(input: &str) -> IResult<&str, Op> {
168    alt((
169        parse_op_draw_shape_ellipse,
170        parse_op_draw_shape_points,
171        parse_op_draw_shape_text,
172    ))(input)
173}
174
175fn parse_op_set_font_characteristics(input: &str) -> IResult<&str, Op> {
176    tagged(
177        "t",
178        map_res(decimal, |value| {
179            u128::from_str(value).map(|n| FontCharacteristics::from_bits_truncate(n).into())
180        }),
181    )(input)
182}
183
184fn parse_op_set_color<'a>(
185    t: &'static str,
186    op: impl FnMut(Rgba) -> Op,
187) -> impl FnMut(&'a str) -> IResult<&'a str, Op> {
188    map(tagged(t, map_parser(parse_string, hex_color)), op)
189}
190
191fn parse_op_set_fill_color(input: &str) -> IResult<&str, Op> {
192    parse_op_set_color("C", Op::SetFillColor)(input)
193}
194
195fn parse_op_set_pen_color(input: &str) -> IResult<&str, Op> {
196    parse_op_set_color("c", Op::SetPenColor)(input)
197}
198
199fn parse_op_set_font(input: &str) -> IResult<&str, Op> {
200    let (input, (size, name)) =
201        tagged("F", separated_pair(float, multispace1, parse_string))(input)?;
202    Ok((
203        input,
204        Op::SetFont {
205            size,
206            name: name.to_owned(),
207        },
208    ))
209}
210
211fn parse_op_set_style(input: &str) -> IResult<&str, Op> {
212    let (input, style) = tagged("S", map_res(parse_string, Style::from_str))(input)?;
213    Ok((input, style.into()))
214}
215
216fn parse_op_external_image(input: &str) -> IResult<&str, Op> {
217    // TODO: implement
218    use nom::combinator::{cut, fail};
219    map(tagged("I", cut(fail)), |_: ()| ExternalImage.into())(input)
220}
221
222fn parse_op(input: &str) -> IResult<&str, Op> {
223    alt((
224        parse_op_draw_shape,
225        parse_op_set_font_characteristics,
226        parse_op_set_fill_color,
227        parse_op_set_pen_color,
228        parse_op_set_font,
229        parse_op_set_style,
230        parse_op_external_image,
231    ))(input)
232}
233
234pub(super) fn parse(input: &str) -> Result<Vec<Op>, NomError<&str>> {
235    terminated(ws(separated_list1(multispace1, parse_op)), eof)(input)
236        .finish()
237        .map(|(rest, ops)| {
238            assert_eq!(rest, "");
239            ops
240        })
241}
242
243#[test]
244fn test_ellipse() {
245    assert_eq!(
246        parse_op_draw_shape_ellipse("e 27 90 18 3"),
247        Ok((
248            "",
249            Ellipse {
250                filled: false,
251                x: 27.,
252                y: 90.,
253                w: 18.,
254                h: 3.,
255            }
256            .into()
257        ))
258    )
259}
260
261#[test]
262fn test_b_spline() {
263    assert_eq!(
264        parse_op_draw_shape_points("B 4 27 71.7 27 60.85 27 46.92 27 36.1"),
265        Ok((
266            "",
267            Points {
268                filled: false,
269                r#type: PointsType::BSpline,
270                points: vec![(27., 71.7), (27., 60.85), (27., 46.92), (27., 36.1)]
271            }
272            .into()
273        ))
274    )
275}
276
277#[test]
278fn test_string_utf8() {
279    assert_eq!(parse_string("3 -äh"), Ok(("", "äh")))
280}