parse-fmt-str 2.0.0

parsin format strings since 1997
Documentation
use nom::{
    bytes::complete::{tag, take, take_till, take_while},
    character::complete::{anychar, digit1, one_of},
    combinator::{opt, peek},
    Err, IResult, Needed,
};
use std::num::NonZeroUsize;
use unicode_xid::UnicodeXID;

fn parse_count(input: &str) -> IResult<&str, Count> {
    let error = Err(Err::Error(nom::error::Error {
        input: "",
        code: nom::error::ErrorKind::Fail,
    }));
    if input.is_empty() {
        return error;
    }
    if let (new_input, Some(arg)) = opt(parse_identifier)(input)? {
        if let (new_new_input, Some(_)) = opt(tag("$"))(new_input)? {
            Ok((new_new_input, Count::Parameter(Argument::Identifier(arg))))
        } else {
            Err(Err::Error(nom::error::Error {
                input: new_input,
                code: nom::error::ErrorKind::Fail,
            }))
        }
    } else if let (new_input, Some(count)) = opt(digit1)(input)? {
        Ok((new_input, Count::Integer(count.parse::<usize>().unwrap())))
    } else {
        error
    }
}

fn parse_identifier(input: &str) -> IResult<&str, String> {
    let (car, cdr) = (
        input
            .chars()
            .nth(0)
            .ok_or(Err::Incomplete(Needed::Size(unsafe {
                NonZeroUsize::new_unchecked(1)
            })))?,
        &input[1..],
    );
    if !UnicodeXID::is_xid_start(car) {
        return Err(Err::Error(nom::error::Error {
            input: "",
            code: nom::error::ErrorKind::Fail,
        }));
    }
    let out: IResult<&str, &str> = take_while(UnicodeXID::is_xid_continue)(cdr);
    let (input, cdr) = out?;
    let out = format!("{}{}", car, cdr);
    return Ok((input, out));
}

fn parse_argument(input: &str) -> IResult<&str, Argument> {
    if let (new_input, Some(arg)) = opt(parse_identifier)(input)? {
        Ok((new_input, Argument::Identifier(arg)))
    } else if let (new_input, Some(arg)) = opt(digit1)(input)? {
        Ok((new_input, Argument::Integer(arg.parse::<usize>().unwrap())))
    } else {
        Err(Err::Error(nom::error::Error {
            input: "",
            code: nom::error::ErrorKind::Fail,
        }))
    }
}

pub fn parse_fmt_spec(input: &str) -> IResult<&str, FormatSlot> {
    // NOTE: macros, maybe?
    let (input, arg) = if let (input, Some(ident)) = opt(parse_argument)(input)? {
        (input, Some(ident))
    } else {
        (input, None)
    };
    let (input, fmt_spec) = if let (input, Some(_)) = opt(tag(":"))(input)? {
        let (input, fill) = if let (input, Some(fill)) = opt(anychar)(input)? {
            (input, Some(fill))
        } else {
            (input, None)
        };
        let (input, align) =
            if let (input, Some(align)) = opt(one_of("<^>"))(input)? {
                (
                    input,
                    Some(match align {
                        '<' => Align::Left,
                        '^' => Align::Center,
                        '>' => Align::Right,
                        _ => {
                            unreachable!()
                        }
                    }),
                )
            } else {
                (input, None)
            };
        let (input, sign) = if let (input, Some(sign)) = opt(one_of("+-"))(input)? {
            (
                input,
                Some(match sign {
                    '+' => Sign::Positive,
                    '-' => Sign::Negative,
                    _ => {
                        unreachable!()
                    }
                }),
            )
        } else {
            (input, None)
        };
        let (input, alternate) = if let (input, Some(_)) = opt(tag("#"))(input)? {
            (input, true)
        } else {
            (input, false)
        };
        let (input, pad_with_zeros) =
            if let (input, Some(_)) = opt(tag("0"))(input)? {
                (input, true)
            } else {
                (input, false)
            };
        let (input, width) = if let (input, Some(width)) = opt(parse_count)(input)?
        {
            (input, Some(width))
        } else {
            (input, None)
        };
        let (input, percision) = if let (input, Some(_)) = opt(tag("."))(input)? {
            let input = input;
            if let (input, Some(percision)) = opt(parse_count)(input)? {
                (input, Some(Precision::Count(percision)))
            } else if let (input, Some(_)) = opt(tag("*"))(input)? {
                (input, Some(Precision::SpecifiedPrecision))
            } else {
                (input, None)
            }
        } else {
            (input, None)
        };
        let (input, kind) =
            if let (input, Some(kind)) = opt(one_of("?oxXpbeE"))(input)? {
                match kind {
                    'x' => {
                        if let (input, Some(_)) = opt(tag("?"))(input)? {
                            (input, Type::DebugLowerHex)
                        } else {
                            (input, Type::LowerHex)
                        }
                    }
                    'X' => {
                        if let (input, Some(_)) = opt(tag("?"))(input)? {
                            (input, Type::DebugUpperHex)
                        } else {
                            (input, Type::UpperHex)
                        }
                    }
                    _ => (
                        input,
                        match kind {
                            '?' => Type::Debug,
                            'o' => Type::Octal,
                            'p' => Type::Pointer,
                            'b' => Type::Binary,
                            'e' => Type::LowerExp,
                            'E' => Type::UpperExp,
                            _ => {
                                unreachable!()
                            }
                        },
                    ),
                }
            } else {
                (input, Type::None)
            };
        (
            input,
            Some(FormatSpec {
                fill,
                align,
                sign,
                alternate,
                pad_with_zeros,
                width,
                percision,
                kind,
            }),
        )
    } else {
        (input, None)
    };
    Ok((input, FormatSlot { arg, fmt_spec }))
}

#[derive(Debug, PartialEq, Eq)]
enum State {
    Text,
    MaybeFormat,
}

pub fn parse_fmt_str(input: &str) -> Result<FormatString, &str> {
    let mut strings: Vec<String> = vec![];
    let mut slots: Vec<PossibleFormatSlot> = vec![];
    let mut input: String = input.to_string();
    let mut state: State = State::MaybeFormat;
    loop {
        println!(
            "INPUT: {:?}, strings: {:?}, state: {:?}",
            input, strings, state
        );
        if input.is_empty() {
            break;
        }
        let part1: IResult<&str, &str> = peek(tag("{{"))(&input);
        let part2: IResult<&str, &str> = peek(tag("}}"))(&input);
        if !part1.is_err() || !part2.is_err() {
            if state == State::MaybeFormat {
                strings.push("".to_string());
            }
            let out: IResult<&str, &str> = take(1usize)(&input);
            let (input_str, push2str) = out.unwrap();
            let mut input_str = input_str.to_string();
            slots.push(match push2str {
                "{" => PossibleFormatSlot::LeftBrace,
                "}" => PossibleFormatSlot::RightBrace,
                _ => {
                    unreachable!()
                }
            });
            input_str.remove(0);
            input = input_str.to_string();
            state = State::MaybeFormat;
            continue;
        }
        // *cracks knuckles*
        if input.starts_with("{") {
            if state == State::MaybeFormat {
                strings.push("".to_string());
                state = State::Text;
            }
            let out: IResult<&str, &str> = take_till(|chr| chr == '}')(&input);
            let (input_str, format) = out.unwrap();
            let mut input_str = input_str.to_string();
            let mut format = format.to_string();
            if format.is_empty() {
                strings.push(input_str);
                break;
            }
            if !input_str.is_empty() {
                input_str.remove(0);
            }
            format.remove(0);
            if format.is_empty() {
                strings.push(input_str);
                break;
            }
            input = input_str.to_string();
            let next = parse_fmt_spec(&format);
            if next.is_err() {
                return Err("Invalid format string. (slot didn't parse)");
            }
            let (left, slot) = next.unwrap();
            if !left.is_empty() {
                return Err("Invalid format string. (slot had additional data)");
            }
            slots.push(PossibleFormatSlot::FormatSlot(slot));
            state = State::MaybeFormat;
        } else {
            assert_eq!(state, State::MaybeFormat);
            let cloned_input = input.clone();
            let out: IResult<&str, &str> = take_till(|chr| chr == '{' || chr == '}')(&cloned_input);
            let (input_str, push2str) = out.unwrap();
            input = input_str.to_string();
            strings.push(push2str.to_string());
            state = State::Text;
        }
    }
    if state == State::MaybeFormat {
        strings.push("".to_string());
    }
    Ok(FormatString {
        text: strings,
        maybe_fmt: slots,
    })
}

pub type Fill = char;

#[derive(Debug, PartialEq, Eq)]
pub enum Align {
    Left,
    Center,
    Right,
}

#[derive(Debug, PartialEq, Eq)]
pub enum Sign {
    Positive,
    Negative,
}

#[derive(Debug, PartialEq, Eq)]
pub enum Count {
    Parameter(Argument),
    Integer(usize),
}

#[derive(Debug, PartialEq, Eq)]
pub enum Precision {
    Count(Count),
    SpecifiedPrecision,
}

#[derive(Debug, PartialEq, Eq)]
pub enum Type {
    Debug,
    DebugLowerHex,
    DebugUpperHex,
    Octal,
    LowerHex,
    UpperHex,
    Pointer,
    Binary,
    LowerExp,
    UpperExp,
    None,
}

#[derive(Debug, PartialEq, Eq)]
pub struct FormatSpec {
    pub fill: Option<Fill>,
    pub align: Option<Align>,
    pub sign: Option<Sign>,
    pub alternate: bool,
    pub pad_with_zeros: bool,
    pub width: Option<Count>,
    pub percision: Option<Precision>,
    pub kind: Type,
}

#[derive(Debug, PartialEq, Eq)]
pub enum Argument {
    Identifier(String),
    Integer(usize),
}

#[derive(Debug, PartialEq, Eq)]
pub struct FormatSlot {
    pub arg: Option<Argument>,
    pub fmt_spec: Option<FormatSpec>,
}

#[derive(Debug, PartialEq, Eq)]
pub enum PossibleFormatSlot {
    FormatSlot(FormatSlot),
    LeftBrace,
    RightBrace,
}

#[derive(Debug, PartialEq, Eq)]
pub struct FormatString {
    pub text: Vec<String>,
    pub maybe_fmt: Vec<PossibleFormatSlot>,
}