use std::collections::HashSet;
use nom::{
branch::alt,
bytes::complete::{tag, take_till1, take_while},
character::complete::{alpha1, alphanumeric1, anychar, digit1},
combinator::{map, map_res, opt, recognize, value},
multi::{many0, many0_count},
sequence::pair,
IResult,
};
use crate::{
fixed_width, precision, Alignment, Argument, ConversionSpec, Flag, FormatFragment,
FormatString, MinFieldWidth, Precision, Primitive, Style,
};
fn named_argument(input: &str) -> IResult<&str, Argument> {
let (input, ident) = recognize(pair(
alt((alpha1, tag("_"))),
many0_count(alt((alphanumeric1, tag("_")))),
))(input)?;
Ok((input, Argument::Named(ident.to_string())))
}
fn positional_argument(input: &str) -> IResult<&str, Argument> {
let (input, index) = map_res(digit1, |val: &str| val.parse::<usize>())(input)?;
Ok((input, Argument::Positional(index)))
}
fn none_argument(input: &str) -> IResult<&str, Argument> {
Ok((input, Argument::None))
}
fn argument(input: &str) -> IResult<&str, Argument> {
alt((named_argument, positional_argument, none_argument))(input)
}
fn explicit_type(input: &str) -> IResult<&str, Style> {
alt((
value(Style::Debug, tag("?")),
value(Style::HexDebug, tag("x?")),
value(Style::UpperHexDebug, tag("X?")),
value(Style::Octal, tag("o")),
value(Style::Hex, tag("x")),
value(Style::UpperHex, tag("X")),
value(Style::Pointer, tag("p")),
value(Style::Binary, tag("b")),
value(Style::Exponential, tag("e")),
value(Style::UpperExponential, tag("E")),
))(input)
}
fn style(input: &str) -> IResult<&str, Style> {
let (input, spec) = explicit_type(input).unwrap_or((input, Style::None));
Ok((input, spec))
}
fn map_flag(value: char) -> Result<Flag, String> {
match value {
'-' => Ok(Flag::LeftJustify),
'+' => Ok(Flag::ForceSign),
'#' => Ok(Flag::AlternateSyntax),
'0' => Ok(Flag::LeadingZeros),
_ => Err(format!("Unsupported flag '{}'", value)),
}
}
fn flags(input: &str) -> IResult<&str, HashSet<Flag>> {
let (input, flags) = many0(map_res(anychar, map_flag))(input)?;
Ok((input, flags.into_iter().collect()))
}
fn map_alignment(value: char) -> Result<Alignment, String> {
match value {
'<' => Ok(Alignment::Left),
'^' => Ok(Alignment::Center),
'>' => Ok(Alignment::Right),
_ => Err(format!("Unsupported alignment '{}'", value)),
}
}
fn bare_alignment(input: &str) -> IResult<&str, Alignment> {
map_res(anychar, map_alignment)(input)
}
fn fill_and_alignment(input: &str) -> IResult<&str, (char, Alignment)> {
let (input, fill) = anychar(input)?;
let (input, alignment) = bare_alignment(input)?;
Ok((input, (fill, alignment)))
}
fn alignment(input: &str) -> IResult<&str, (char, Alignment)> {
if let Ok((input, (fill, alignment))) = fill_and_alignment(input) {
return Ok((input, (fill, alignment)));
}
if let Ok((input, alignment)) = bare_alignment(input) {
return Ok((input, (' ', alignment)));
}
Ok((input, (' ', Alignment::None)))
}
fn format_spec(input: &str) -> IResult<&str, ConversionSpec> {
let (input, _) = tag(":")(input)?;
let (input, (fill, alignment)) = alignment(input)?;
let (input, flags) = flags(input)?;
let (input, width) = opt(fixed_width)(input)?;
let (input, precision) = precision(input)?;
let (input, style) = style(input)?;
Ok((
input,
ConversionSpec {
argument: Argument::None, fill,
alignment,
flags,
min_field_width: width.unwrap_or(MinFieldWidth::None),
precision,
length: None,
primitive: Primitive::Untyped, style,
},
))
}
fn conversion(input: &str) -> IResult<&str, ConversionSpec> {
let (input, _) = tag("{")(input)?;
let (input, argument) = argument(input)?;
let (input, spec) = opt(format_spec)(input)?;
let (input, _) = take_while(|c: char| c.is_whitespace())(input)?;
let (input, _) = tag("}")(input)?;
let mut spec = spec.unwrap_or_else(|| ConversionSpec {
argument: Argument::None,
fill: ' ',
alignment: Alignment::None,
flags: HashSet::new(),
min_field_width: MinFieldWidth::None,
precision: Precision::None,
length: None,
primitive: Primitive::Untyped,
style: Style::None,
});
spec.argument = argument;
Ok((input, spec))
}
fn literal_fragment(input: &str) -> IResult<&str, FormatFragment> {
map(take_till1(|c| c == '{' || c == '}'), |s: &str| {
FormatFragment::Literal(s.to_string())
})(input)
}
fn escape_fragment(input: &str) -> IResult<&str, FormatFragment> {
alt((
map(tag("{{"), |_| FormatFragment::Literal("{".to_string())),
map(tag("}}"), |_| FormatFragment::Literal("}".to_string())),
))(input)
}
fn conversion_fragment(input: &str) -> IResult<&str, FormatFragment> {
map(conversion, FormatFragment::Conversion)(input)
}
fn fragment(input: &str) -> IResult<&str, FormatFragment> {
alt((escape_fragment, conversion_fragment, literal_fragment))(input)
}
pub(crate) fn format_string(input: &str) -> IResult<&str, FormatString> {
let (input, fragments) = many0(fragment)(input)?;
Ok((input, FormatString::from_fragments(&fragments)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_parses_correctly() {
assert_eq!(style(""), Ok(("", Style::None)));
assert_eq!(style("?"), Ok(("", Style::Debug)));
assert_eq!(style("x?"), Ok(("", Style::HexDebug)));
assert_eq!(style("X?"), Ok(("", Style::UpperHexDebug)));
assert_eq!(style("o"), Ok(("", Style::Octal)));
}
#[test]
fn flags_prase_correctly() {
assert_eq!(
flags("0"),
Ok(("", vec![Flag::LeadingZeros].into_iter().collect()))
);
assert_eq!(
flags("-"),
Ok(("", vec![Flag::LeftJustify].into_iter().collect()))
);
assert_eq!(
flags("+"),
Ok(("", vec![Flag::ForceSign].into_iter().collect()))
);
assert_eq!(
flags("#"),
Ok(("", vec![Flag::AlternateSyntax].into_iter().collect()))
);
assert_eq!(
flags("+#0"),
Ok((
"",
vec![Flag::ForceSign, Flag::AlternateSyntax, Flag::LeadingZeros]
.into_iter()
.collect()
))
);
assert_eq!(flags(" "), Ok((" ", HashSet::new())));
}
#[test]
fn alignment_parses_correctly() {
assert_eq!(alignment(""), Ok(("", (' ', Alignment::None))));
assert_eq!(alignment("<"), Ok(("", (' ', Alignment::Left))));
assert_eq!(alignment("^"), Ok(("", (' ', Alignment::Center))));
assert_eq!(alignment(">"), Ok(("", (' ', Alignment::Right))));
assert_eq!(alignment("-<"), Ok(("", ('-', Alignment::Left))));
assert_eq!(alignment("-^"), Ok(("", ('-', Alignment::Center))));
assert_eq!(alignment("->"), Ok(("", ('-', Alignment::Right))));
assert_eq!(alignment("><"), Ok(("", ('>', Alignment::Left))));
assert_eq!(alignment("^^"), Ok(("", ('^', Alignment::Center))));
assert_eq!(alignment("<>"), Ok(("", ('<', Alignment::Right))));
assert_eq!(alignment("1234"), Ok(("1234", (' ', Alignment::None))));
}
#[test]
fn empty_conversion_spec_has_sensible_defaults() {
assert_eq!(
conversion("{}"),
Ok((
"",
ConversionSpec {
argument: Argument::None,
fill: ' ',
alignment: Alignment::None,
flags: HashSet::new(),
min_field_width: MinFieldWidth::None,
precision: Precision::None,
length: None,
primitive: Primitive::Untyped,
style: Style::None,
}
))
);
}
}