use std::str::FromStr;
use nom::{
branch::alt,
bytes::complete::{tag, take_while_m_n},
character::complete::{char, multispace0, multispace1, one_of},
combinator::{eof, flat_map, map, map_parser, map_res, recognize, value},
error::{Error as NomError, ParseError},
multi::{count, many0, many1, separated_list1},
number::complete::float,
sequence::{delimited, preceded, separated_pair, terminated, tuple},
Finish, IResult, ToUsize,
};
use super::shapes::ExternalImage;
use super::{
draw::{FontCharacteristics, Rgba, Style},
ops::Op,
shapes::{Ellipse, Points, PointsType, Text, TextAlign},
};
fn take_bytes<'a, C: ToUsize, E: ParseError<&'a str>>(
count: C,
) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str, E> {
let count = count.to_usize();
move |input: &str| {
if input.is_char_boundary(count) {
Ok((&input[count..], &input[..count]))
} else {
Err(nom::Err::Error(E::from_error_kind(
input,
nom::error::ErrorKind::Count,
)))
}
}
}
fn decimal(input: &str) -> IResult<&str, &str> {
recognize(many1(terminated(one_of("0123456789"), many0(char('_')))))(input)
}
fn ws<'a, F, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
where
F: FnMut(&'a str) -> IResult<&'a str, O, E>,
{
delimited(multispace0, inner, multispace0)
}
fn tagged<'a, F, O, E: ParseError<&'a str>>(
t: &'static str,
inner: F,
) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
where
F: FnMut(&'a str) -> IResult<&'a str, O, E>,
{
preceded(tuple((tag(t), multispace1)), inner)
}
fn parse_string(input: &str) -> IResult<&str, &str> {
flat_map(map_res(decimal, usize::from_str), |n| {
preceded(tuple((multispace1, tag("-"))), take_bytes(n))
})(input)
}
fn parse_text_align(input: &str) -> IResult<&str, TextAlign> {
alt((
value(TextAlign::Left, tag("-1")),
value(TextAlign::Center, tag("0")),
value(TextAlign::Right, tag("1")),
))(input)
}
fn from_hex(input: &str) -> Result<u8, std::num::ParseIntError> {
u8::from_str_radix(input, 16)
}
fn is_hex_digit(c: char) -> bool {
c.is_ascii_hexdigit()
}
fn hex_primary(input: &str) -> IResult<&str, u8> {
map_res(take_while_m_n(2, 2, is_hex_digit), from_hex)(input)
}
fn hex_color(input: &str) -> IResult<&str, Rgba> {
let (input, _) = tag("#")(input)?;
let (input, (r, g, b)) = tuple((hex_primary, hex_primary, hex_primary))(input)?;
Ok((input, Rgba { r, g, b, a: 0xff }))
}
fn parse_op_draw_shape_ellipse(input: &str) -> IResult<&str, Op> {
let (input, (c, x, y, w, h)) = tuple((
one_of("Ee"),
preceded(multispace1, float),
preceded(multispace1, float),
preceded(multispace1, float),
preceded(multispace1, float),
))(input)?;
let ellip = Ellipse {
filled: c == 'E',
x,
y,
w,
h,
};
Ok((input, ellip.into()))
}
fn parse_op_draw_shape_points(input: &str) -> IResult<&str, Op> {
let (input, (c, points)) = tuple((
terminated(one_of("PpLBb"), multispace1),
flat_map(map_res(decimal, usize::from_str), |n| {
count(
tuple((preceded(multispace1, float), preceded(multispace1, float))),
n,
)
}),
))(input)?;
let points = Points {
filled: c == 'P' || c == 'b',
r#type: match c {
'P' | 'p' => PointsType::Polygon,
'L' => PointsType::Polyline,
'B' | 'b' => PointsType::BSpline,
_ => unreachable!(),
},
points,
};
Ok((input, points.into()))
}
fn parse_op_draw_shape_text(input: &str) -> IResult<&str, Op> {
let (input, (x, y, align, width, text)) = tagged(
"T",
tuple((
terminated(float, multispace1), terminated(float, multispace1), terminated(parse_text_align, multispace1), terminated(float, multispace1), parse_string,
)),
)(input)?;
let text = Text {
x,
y,
align,
width,
text: text.to_owned(),
};
Ok((input, text.into()))
}
fn parse_op_draw_shape(input: &str) -> IResult<&str, Op> {
alt((
parse_op_draw_shape_ellipse,
parse_op_draw_shape_points,
parse_op_draw_shape_text,
))(input)
}
fn parse_op_set_font_characteristics(input: &str) -> IResult<&str, Op> {
tagged(
"t",
map_res(decimal, |value| {
u128::from_str(value).map(|n| FontCharacteristics::from_bits_truncate(n).into())
}),
)(input)
}
fn parse_op_set_color<'a>(
t: &'static str,
op: impl FnMut(Rgba) -> Op,
) -> impl FnMut(&'a str) -> IResult<&'a str, Op> {
map(tagged(t, map_parser(parse_string, hex_color)), op)
}
fn parse_op_set_fill_color(input: &str) -> IResult<&str, Op> {
parse_op_set_color("C", Op::SetFillColor)(input)
}
fn parse_op_set_pen_color(input: &str) -> IResult<&str, Op> {
parse_op_set_color("c", Op::SetPenColor)(input)
}
fn parse_op_set_font(input: &str) -> IResult<&str, Op> {
let (input, (size, name)) =
tagged("F", separated_pair(float, multispace1, parse_string))(input)?;
Ok((
input,
Op::SetFont {
size,
name: name.to_owned(),
},
))
}
fn parse_op_set_style(input: &str) -> IResult<&str, Op> {
let (input, style) = tagged("S", map_res(parse_string, Style::from_str))(input)?;
Ok((input, style.into()))
}
fn parse_op_external_image(input: &str) -> IResult<&str, Op> {
use nom::combinator::{cut, fail};
map(tagged("I", cut(fail)), |_: ()| ExternalImage.into())(input)
}
fn parse_op(input: &str) -> IResult<&str, Op> {
alt((
parse_op_draw_shape,
parse_op_set_font_characteristics,
parse_op_set_fill_color,
parse_op_set_pen_color,
parse_op_set_font,
parse_op_set_style,
parse_op_external_image,
))(input)
}
pub(super) fn parse(input: &str) -> Result<Vec<Op>, NomError<&str>> {
terminated(ws(separated_list1(multispace1, parse_op)), eof)(input)
.finish()
.map(|(rest, ops)| {
assert_eq!(rest, "");
ops
})
}
#[test]
fn test_ellipse() {
assert_eq!(
parse_op_draw_shape_ellipse("e 27 90 18 3"),
Ok((
"",
Ellipse {
filled: false,
x: 27.,
y: 90.,
w: 18.,
h: 3.,
}
.into()
))
)
}
#[test]
fn test_b_spline() {
assert_eq!(
parse_op_draw_shape_points("B 4 27 71.7 27 60.85 27 46.92 27 36.1"),
Ok((
"",
Points {
filled: false,
r#type: PointsType::BSpline,
points: vec![(27., 71.7), (27., 60.85), (27., 46.92), (27., 36.1)]
}
.into()
))
)
}
#[test]
fn test_string_utf8() {
assert_eq!(parse_string("3 -äh"), Ok(("", "äh")))
}