use nom::branch::alt;
use nom::bytes::complete::{tag, take, take_till1, take_while};
use nom::multi::many0;
use nom::IResult;
use std::cmp::Ordering;
use std::str;
#[derive(PartialEq, Eq, Debug)]
pub enum Padding {
None,
Left(usize),
Right(usize),
Center(usize),
}
#[derive(PartialEq, Eq, Debug)]
pub enum Specifier<'a> {
Spacing(char),
Format(char, Padding),
Text(&'a str),
Conditional(char, Vec<Specifier<'a>>, Option<Vec<Specifier<'a>>>),
}
fn escaped_percent_sign(input: &str) -> IResult<&str, Specifier> {
tag("%%")(input).map(|result| (result.0, Specifier::Text(&result.1[0..1])))
}
fn spacing(input: &str) -> IResult<&str, Specifier> {
let (input, _) = tag("%>")(input)?;
let (input, c) = take(1usize)(input)?;
let chr = c.chars().next().unwrap();
Ok((input, Specifier::Spacing(chr)))
}
fn center_format(input: &str) -> IResult<&str, Specifier> {
let (input, _) = tag("%=")(input)?;
let (input, width) = take_while(|chr: char| chr.is_ascii() && (chr.is_numeric()))(input)?;
let (input, format) = take(1usize)(input)?;
let format = format.chars().next().unwrap();
let width = width.parse::<usize>().unwrap_or(0);
Ok((input, Specifier::Format(format, Padding::Center(width))))
}
fn padded_format(input: &str) -> IResult<&str, Specifier> {
let (input, _) = tag("%")(input)?;
let (input, width) =
take_while(|chr: char| chr.is_ascii() && (chr.is_numeric() || chr == '-'))(input)?;
let (input, format) = take(1usize)(input)?;
let format = format.chars().next().unwrap();
let width = width.parse::<isize>().unwrap_or(0);
let padding = match width.cmp(&0isize) {
Ordering::Equal => Padding::None,
Ordering::Greater => Padding::Left(width.unsigned_abs()),
Ordering::Less => Padding::Right(width.unsigned_abs()),
};
Ok((input, Specifier::Format(format, padding)))
}
fn text_outside_conditional(input: &str) -> IResult<&str, Specifier> {
let (input, text) = take_till1(|chr: char| chr == '%')(input)?;
Ok((input, Specifier::Text(text)))
}
fn text_inside_conditional(input: &str) -> IResult<&str, Specifier> {
let (input, text) = take_till1(|chr: char| chr == '%' || chr == '&' || chr == '?')(input)?;
Ok((input, Specifier::Text(text)))
}
fn conditional(input: &str) -> IResult<&str, Specifier> {
let start_tag = tag("%?");
let condition = take(1usize);
let then_tag = tag("?");
let then_branch = conditional_branch;
let else_tag = tag("&");
let end_tag = tag("?");
let some_else_branch = |input| {
let (input, _) = else_tag(input)?;
let (input, els) = conditional_branch(input)?;
let (input, _) = end_tag(input)?;
Ok((input, Some(els)))
};
let none_else_branch = |input| {
let (input, _) = end_tag(input)?;
Ok((input, None))
};
let mut else_branch = alt((some_else_branch, none_else_branch));
let (input, _) = start_tag(input)?;
let (input, cond) = condition(input)?;
let (input, _) = then_tag(input)?;
let (input, then) = then_branch(input)?;
let (input, els) = else_branch(input)?;
let cond = cond.chars().next().unwrap();
Ok((input, Specifier::Conditional(cond, then, els)))
}
fn conditional_branch(input: &str) -> IResult<&str, Vec<Specifier>> {
let alternatives = (
escaped_percent_sign,
spacing,
center_format,
padded_format,
text_inside_conditional,
);
many0(alt(alternatives))(input)
}
fn parser(input: &str) -> IResult<&str, Vec<Specifier>> {
let alternatives = (
conditional,
escaped_percent_sign,
spacing,
center_format,
padded_format,
text_outside_conditional,
);
many0(alt(alternatives))(input)
}
fn sanitize(mut input: Vec<Specifier>) -> Vec<Specifier> {
input.retain(|s| {
if let Specifier::Format(c, ref _b) = *s {
c.is_ascii()
} else {
true
}
});
input
}
pub fn parse(input: &str) -> Vec<Specifier> {
match parser(input) {
Ok((_leftovers, ast)) => sanitize(ast),
Err(_) => vec![Specifier::Text("")],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t_parses_formats_without_specifiers() {
let input = "Hello, world!";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
assert_eq!(result, vec![Specifier::Text("Hello, world!")]);
}
#[test]
fn t_replaces_double_percent_with_a_single_percent() {
let input = "%%";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
assert_eq!(result, vec![Specifier::Text("%")]);
}
#[test]
fn t_parses_sequences_of_specifiers() {
let input = "100%% pure Ceylon tea";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![
Specifier::Text("100"),
Specifier::Text("%"),
Specifier::Text(" pure Ceylon tea"),
];
assert_eq!(result, expected);
}
#[test]
fn t_parses_formats_with_letters() {
let input = "%t (%a)";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![
Specifier::Format('t', Padding::None),
Specifier::Text(" ("),
Specifier::Format('a', Padding::None),
Specifier::Text(")"),
];
assert_eq!(result, expected);
}
#[test]
fn t_parses_formats_with_positive_padding() {
let input = "%8a%4b%13x";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![
Specifier::Format('a', Padding::Left(8)),
Specifier::Format('b', Padding::Left(4)),
Specifier::Format('x', Padding::Left(13)),
];
assert_eq!(result, expected);
}
#[test]
fn t_parses_formats_with_negative_padding() {
let input = "%-8a%-4b%-13x";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![
Specifier::Format('a', Padding::Right(8)),
Specifier::Format('b', Padding::Right(4)),
Specifier::Format('x', Padding::Right(13)),
];
assert_eq!(result, expected);
}
#[test]
fn t_parses_spacing_format() {
let input = "%-8a%>m%4b%> %-13x";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![
Specifier::Format('a', Padding::Right(8)),
Specifier::Spacing('m'),
Specifier::Format('b', Padding::Left(4)),
Specifier::Spacing(' '),
Specifier::Format('x', Padding::Right(13)),
];
assert_eq!(result, expected);
}
#[test]
fn t_parses_conditionals() {
let input = "%?x?success&failure?";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![Specifier::Conditional(
'x',
vec![Specifier::Text("success")],
Some(vec![Specifier::Text("failure")]),
)];
assert_eq!(result, expected);
}
#[test]
fn t_parses_conditionals_without_else_branch() {
let input = "%?x?success?";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![Specifier::Conditional(
'x',
vec![Specifier::Text("success")],
None,
)];
assert_eq!(result, expected);
}
#[test]
fn t_parses_conditionals_with_empty_then_branch() {
let input = "%?x??";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![Specifier::Conditional('x', vec![], None)];
assert_eq!(result, expected);
}
#[test]
fn t_parses_conditionals_with_empty_then_nonempty_else_branches() {
let input = "%?x?&nonempty?";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![Specifier::Conditional(
'x',
vec![],
Some(vec![Specifier::Text("nonempty")]),
)];
assert_eq!(result, expected);
}
#[test]
fn t_parses_conditionals_with_empty_then_and_else_branches() {
let input = "%?x?&?";
let (leftovers, result) = parser(input).unwrap();
assert_eq!(leftovers, "");
let expected = vec![Specifier::Conditional('x', vec![], Some(vec![]))];
assert_eq!(result, expected);
}
}