use crate::table::{Column, ColumnAlign};
use nom::bytes::complete::{is_not, take_till, take_until};
use nom::error::convert_error;
use nom::error::{ParseError, VerboseError};
use nom::multi::{many0, many1};
use nom::IResult;
use nom::{
branch::alt,
bytes::complete::{tag, take_while},
combinator::{all_consuming, map, opt, value},
error::context,
sequence::{delimited, preceded, separated_pair, terminated, tuple},
};
type Res<T, U> = IResult<T, U, VerboseError<T>>;
fn ws<'a, E: ParseError<&'a str>>(i: &'a str) -> IResult<&'a str, (), E> {
let chars = " \n\t\r";
value((), take_while(move |c| chars.contains(c)))(i)
}
fn eol<'a, E: ParseError<&'a str>>(i: &'a str) -> IResult<&'a str, (), E> {
value((), alt((tag("\n\r"), tag("\r\n"), tag("\n"))))(i)
}
fn comment<'a, E: ParseError<&'a str>>(i: &'a str) -> IResult<&'a str, (), E> {
value(
(), many1(preceded(ws, delimited(tag("#"), is_not("\n\r"), opt(eol)))),
)(i)
}
fn sp<'a, E: ParseError<&'a str>>(i: &'a str) -> IResult<&'a str, (), E> {
value(
(), alt((comment, ws)),
)(i)
}
fn column_align(txt: &str) -> Res<&str, ColumnAlign> {
preceded(
sp,
alt((
value(ColumnAlign::Left, tag("<")),
value(ColumnAlign::Center, tag("^")),
value(ColumnAlign::Right, tag(">")),
)),
)(txt)
}
pub fn column(txt: &str) -> Res<&str, Column> {
let (rest, (align, (head, templ))) = context(
"column definition",
preceded(
sp,
tuple((
opt(column_align),
preceded(
ws,
separated_pair(
map(take_until("=>"), str::trim),
tag("=>"),
map(delimited(ws, take_till(|c| c == '\n'), ws), str::trim),
),
),
)),
),
)(txt)?;
Ok((rest, Column::new(head, templ, align)))
}
pub fn parse_table(txt: &str) -> Res<&str, Vec<Column>> {
context(
"table file",
all_consuming(terminated(preceded(sp, many0(column)), sp)),
)(txt)
}
pub fn parse_table_complete(txt: &str) -> Result<Vec<Column>, String> {
let (_rest, val) = match parse_table(&format!("{}\n", txt)) {
Ok(v) => v,
Err(e) => {
let er = match e {
nom::Err::Error(er) | nom::Err::Failure(er) => er,
nom::Err::Incomplete(_er) => panic!("shouldn't happen"),
};
return Err(convert_error(txt, er));
}
};
Ok(val)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case("field=> test {here}", Column::new("field", "test {here}", None), "")]
#[case(
"<Field 1 =>{here} is {more_test?\"default\"} 2.4",
Column::new(
"Field 1",
"{here} is {more_test?\"default\"} 2.4",
Some(ColumnAlign::Left)
),
""
)]
#[case(
"#new field\n < field => test {here}\n",
Column::new("field", "test {here}", Some(ColumnAlign::Left)),
""
)]
#[case::hash(
"Correlation => #image(\"corrs/{NAME}.png\", height: 1in)\n",
Column::new("Correlation", "#image(\"corrs/{NAME}.png\", height: 1in)", None),
""
)]
fn column_test(#[case] txt: &str, #[case] value: Column, #[case] reminder: &str) {
let (rest, n) = column(txt).unwrap();
assert_eq!(rest, reminder);
assert_eq!(n, value);
}
#[rstest]
#[case(
"field=> test {here}",
vec![Column::new("field", "test {here}", Some(ColumnAlign::Center))],
""
)]
#[case(
"<Field 1 =>{here} is {more_test?\"default\"} 2.4",
vec![Column::new(
"Field 1",
"{here} is {more_test?\"default\"} 2.4",
Some(ColumnAlign::Left)
)],
""
)]
#[case(
"#new field\n < field => test {here}\n",
vec![Column::new("field", "test {here}", Some(ColumnAlign::Left))],
""
)]
#[case(
"#new field\n < field => test {here}\n# an co\n\n<Field 1 =>{here} is {more_test?\"default\"} 2.4",
vec![Column::new("field", "test {here}", Some(ColumnAlign::Left)),
Column::new(
"Field 1",
"{here} is {more_test?\"default\"} 2.4",
Some(ColumnAlign::Left)
)],
""
)]
fn parse_table_test(#[case] txt: &str, #[case] value: Vec<Column>, #[case] reminder: &str) {
let (rest, n) = parse_table(txt).unwrap();
assert_eq!(rest, reminder);
assert_eq!(n, value);
}
}