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, Parser, 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},
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('_'))))).parse(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 Parser<&'a str, Output = O, Error = E>
52where
53    F: Parser<&'a str, Output = O, Error = E>,
54{
55    delimited(multispace0, inner, multispace0)
56}
57
58fn tagged<'a, P, O, E: ParseError<&'a str>>(
59    t: &'static str,
60    inner: P,
61) -> impl Parser<&'a str, Output = O, Error = E>
62where
63    P: Parser<&'a str, Output = O, Error = E>,
64{
65    preceded((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((multispace1, tag("-")), take_bytes(n))
74    })
75    .parse(input)
76}
77
78fn parse_text_align(input: &str) -> IResult<&str, TextAlign> {
79    alt((
80        value(TextAlign::Left, tag("-1")),
81        value(TextAlign::Center, tag("0")),
82        value(TextAlign::Right, tag("1")),
83    ))
84    .parse(input)
85}
86
87fn from_hex(input: &str) -> Result<u8, std::num::ParseIntError> {
88    u8::from_str_radix(input, 16)
89}
90
91fn is_hex_digit(c: char) -> bool {
92    c.is_ascii_hexdigit()
93}
94
95fn hex_primary(input: &str) -> IResult<&str, u8> {
96    map_res(take_while_m_n(2, 2, is_hex_digit), from_hex).parse(input)
97}
98
99fn hex_color(input: &str) -> IResult<&str, Rgba> {
100    let (input, _) = tag("#")(input)?;
101    let (input, (r, g, b)) = (hex_primary, hex_primary, hex_primary).parse(input)?;
102    Ok((input, Rgba { r, g, b, a: 0xff }))
103}
104
105// Op parsers
106
107fn parse_op_draw_shape_ellipse(input: &str) -> IResult<&str, Op> {
108    let (input, (c, x, y, w, h)) = (
109        one_of("Ee"),
110        preceded(multispace1, float),
111        preceded(multispace1, float),
112        preceded(multispace1, float),
113        preceded(multispace1, float),
114    )
115        .parse(input)?;
116    let ellip = Ellipse {
117        filled: c == 'E',
118        x,
119        y,
120        w,
121        h,
122    };
123    Ok((input, ellip.into()))
124}
125
126fn parse_op_draw_shape_points(input: &str) -> IResult<&str, Op> {
127    let (input, (c, points)) = (
128        terminated(one_of("PpLBb"), multispace1),
129        flat_map(map_res(decimal, usize::from_str), |n| {
130            count(
131                (preceded(multispace1, float), preceded(multispace1, float)),
132                n,
133            )
134        }),
135    )
136        .parse(input)?;
137    let points = Points {
138        filled: c == 'P' || c == 'b',
139        r#type: match c {
140            'P' | 'p' => PointsType::Polygon,
141            'L' => PointsType::Polyline,
142            'B' | 'b' => PointsType::BSpline,
143            _ => unreachable!(),
144        },
145        points,
146    };
147    Ok((input, points.into()))
148}
149
150fn parse_op_draw_shape_text(input: &str) -> IResult<&str, Op> {
151    let (input, (x, y, align, width, text)) = tagged(
152        "T",
153        (
154            terminated(float, multispace1),            // x
155            terminated(float, multispace1),            // y
156            terminated(parse_text_align, multispace1), // align
157            terminated(float, multispace1),            // width
158            parse_string,
159        ),
160    )
161    .parse(input)?;
162    let text = Text {
163        x,
164        y,
165        align,
166        width,
167        text: text.to_owned(),
168    };
169    Ok((input, text.into()))
170}
171
172fn parse_op_draw_shape(input: &str) -> IResult<&str, Op> {
173    alt((
174        parse_op_draw_shape_ellipse,
175        parse_op_draw_shape_points,
176        parse_op_draw_shape_text,
177    ))
178    .parse(input)
179}
180
181fn parse_op_set_font_characteristics(input: &str) -> IResult<&str, Op> {
182    tagged(
183        "t",
184        map_res(decimal, |value| {
185            u128::from_str(value).map(|n| FontCharacteristics::from_bits_truncate(n).into())
186        }),
187    )
188    .parse(input)
189}
190
191fn parse_op_set_color<'a>(
192    t: &'static str,
193    op: impl FnMut(Rgba) -> Op,
194) -> impl Parser<&'a str, Output = Op, Error = NomError<&'a str>> {
195    map(tagged(t, map_parser(parse_string, hex_color)), op)
196}
197
198fn parse_op_set_fill_color(input: &str) -> IResult<&str, Op> {
199    parse_op_set_color("C", Op::SetFillColor).parse(input)
200}
201
202fn parse_op_set_pen_color(input: &str) -> IResult<&str, Op> {
203    parse_op_set_color("c", Op::SetPenColor).parse(input)
204}
205
206fn parse_op_set_font(input: &str) -> IResult<&str, Op> {
207    let (input, (size, name)) =
208        tagged("F", separated_pair(float, multispace1, parse_string)).parse(input)?;
209    Ok((
210        input,
211        Op::SetFont {
212            size,
213            name: name.to_owned(),
214        },
215    ))
216}
217
218fn parse_op_set_style(input: &str) -> IResult<&str, Op> {
219    let (input, style) = tagged("S", map_res(parse_string, Style::from_str)).parse(input)?;
220    Ok((input, style.into()))
221}
222
223fn parse_op_external_image(input: &str) -> IResult<&str, Op> {
224    // TODO: implement
225    use nom::combinator::{cut, fail};
226    map(tagged("I", cut(fail())), |_: ()| ExternalImage.into()).parse(input)
227}
228
229fn parse_op(input: &str) -> IResult<&str, Op> {
230    alt((
231        parse_op_draw_shape,
232        parse_op_set_font_characteristics,
233        parse_op_set_fill_color,
234        parse_op_set_pen_color,
235        parse_op_set_font,
236        parse_op_set_style,
237        parse_op_external_image,
238    ))
239    .parse(input)
240}
241
242pub(super) fn parse(input: &str) -> Result<Vec<Op>, NomError<&str>> {
243    terminated(ws(separated_list1(multispace1, parse_op)), eof)
244        .parse(input)
245        .finish()
246        .map(|(rest, ops)| {
247            assert_eq!(rest, "");
248            ops
249        })
250}
251
252#[test]
253fn test_ellipse() {
254    assert_eq!(
255        parse_op_draw_shape_ellipse("e 27 90 18 3"),
256        Ok((
257            "",
258            Ellipse {
259                filled: false,
260                x: 27.,
261                y: 90.,
262                w: 18.,
263                h: 3.,
264            }
265            .into()
266        ))
267    )
268}
269
270#[test]
271fn test_b_spline() {
272    assert_eq!(
273        parse_op_draw_shape_points("B 4 27 71.7 27 60.85 27 46.92 27 36.1"),
274        Ok((
275            "",
276            Points {
277                filled: false,
278                r#type: PointsType::BSpline,
279                points: vec![(27., 71.7), (27., 60.85), (27., 46.92), (27., 36.1)]
280            }
281            .into()
282        ))
283    )
284}
285
286#[test]
287fn test_string_utf8() {
288    assert_eq!(parse_string("3 -äh"), Ok(("", "äh")))
289}