flatiron 1.0.5

A parser and HTML renderer for the Textile markup language
Documentation
use crate::parse::{
    alignment,
    attributes::attributes,
    block::{end_of_block, indent_align},
    from_num, opt_either_order,
    phrase::phrase,
};
use crate::structs::{
    Align, Attributes, BlockHeader, BlockTag, CellKind, Indent, InlineTag,
    TableCell, TableHeader, TableRow, VerticalAlign,
};
use nom::{
    branch::alt,
    bytes::complete::{escaped_transform, tag, take_while1},
    character::complete::{char, line_ending, none_of},
    combinator::{complete, fail, map_res, opt, value},
    multi::{many1, many_till},
    sequence::{preceded, terminated, tuple},
    IResult,
};

pub fn table(input: &str) -> IResult<&str, BlockTag> {
    let (rest, (opt_header, rows)) =
        tuple((opt(table_head), table_body))(input)?;
    let (attributes, align, indent) = opt_header.unwrap_or((None, None, None));
    Ok((
        rest,
        BlockTag::Table {
            header: BlockHeader {
                attributes,
                indent,
                align,
            },
            rows,
        },
    ))
}

fn table_head(
    input: &str,
) -> IResult<&str, (Option<Attributes>, Option<Align>, Option<Indent>)> {
    let (rest, (attributes, indent_align)) = terminated(
        tuple((opt(attributes), opt(indent_align))),
        tuple((char('.'), line_ending)),
    )(input)?;
    let (indent, align) = indent_align.unwrap_or((None, None));
    Ok((rest, (attributes, align, indent)))
}

fn table_body(input: &str) -> IResult<&str, Vec<TableRow>> {
    many1(table_row)(input)
}

fn table_row(input: &str) -> IResult<&str, TableRow> {
    let (rest, (opt_header, cells)) = tuple((opt(row_head), row_body))(input)?;
    let (attributes, h_align, v_align) =
        opt_header.unwrap_or((None, None, None));
    Ok((
        rest,
        TableRow {
            header: TableHeader {
                attributes,
                h_align,
                v_align,
            },
            cells,
        },
    ))
}

fn row_head(
    input: &str,
) -> IResult<&str, (Option<Attributes>, Option<Align>, Option<VerticalAlign>)> {
    let (rest, (attributes, aligns)) = terminated(
        tuple((opt(attributes), opt(table_aligns))),
        tag(". "),
    )(input)?;
    let (h_align, v_align) = aligns.unwrap_or((None, None));
    Ok((rest, (attributes, h_align, v_align)))
}

fn row_body(input: &str) -> IResult<&str, Vec<TableCell>> {
    let (rest, (cells, _end)) = many_till(
        table_cell,
        tuple((char('|'), alt((line_ending, end_of_block)))),
    )(input)?;
    Ok((rest, cells))
}

fn table_cell(input: &str) -> IResult<&str, TableCell> {
    let (rest, (opt_header, content)) =
        preceded(char('|'), tuple((opt(cell_head), cell_body)))(input)?;
    let (kind, col_span, row_span, attributes, h_align, v_align) =
        opt_header.unwrap_or((CellKind::Data, None, None, None, None, None));
    Ok((
        rest,
        TableCell {
            kind,
            col_span,
            row_span,
            header: TableHeader {
                attributes,
                h_align,
                v_align,
            },
            content,
        },
    ))
}

fn cell_head(
    input: &str,
) -> IResult<
    &str,
    (
        CellKind,
        Option<usize>,
        Option<usize>,
        Option<Attributes>,
        Option<Align>,
        Option<VerticalAlign>,
    ),
> {
    let (rest, (kind, spans, attributes, aligns)) = terminated(
        tuple((
            alt((
                value(CellKind::Header, char('_')),
                value(CellKind::Data, tag("")),
            )),
            opt(cell_spans),
            opt(attributes),
            opt(table_aligns),
        )),
        tag(". "),
    )(input)?;
    let (h_align, v_align) = aligns.unwrap_or((None, None));
    let (col_span, row_span) = spans.unwrap_or((None, None));
    Ok((
        rest,
        (kind, col_span, row_span, attributes, h_align, v_align),
    ))
}

fn cell_body(input: &str) -> IResult<&str, InlineTag> {
    let (rest, body) = escaped_transform(
        none_of("\\|"),
        '\\',
        alt((value("\\", tag("\\")), value("|", tag("|")))),
    )(input)?;
    let phrase_result = complete(phrase)(&*body);
    if let Ok((_, content)) = phrase_result {
        Ok((rest, content))
    } else {
        fail(input)
    }
}

fn table_aligns(
    input: &str,
) -> IResult<&str, (Option<Align>, Option<VerticalAlign>)> {
    opt_either_order(alignment, vertical_alignment)(input)
}

fn cell_spans(input: &str) -> IResult<&str, (Option<usize>, Option<usize>)> {
    opt_either_order(col_span, row_span)(input)
}

fn col_span(input: &str) -> IResult<&str, usize> {
    preceded(
        char('\\'),
        map_res(take_while1(|c: char| c.is_ascii_digit()), from_num),
    )(input)
}

fn row_span(input: &str) -> IResult<&str, usize> {
    preceded(
        char('/'),
        map_res(take_while1(|c: char| c.is_ascii_digit()), from_num),
    )(input)
}

fn vertical_alignment(input: &str) -> IResult<&str, VerticalAlign> {
    alt((
        value(VerticalAlign::Top, tag("^")),
        value(VerticalAlign::Middle, tag("-")),
        value(VerticalAlign::Bottom, tag("~")),
    ))(input)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn table_basic() {
        let input = "|hello|";
        let result = table(input);
        assert_eq!(
            result,
            Ok((
                "",
                BlockTag::Table {
                    header: BlockHeader {
                        attributes: None,
                        indent: None,
                        align: None,
                    },
                    rows: vec![TableRow {
                        header: TableHeader {
                            attributes: None,
                            h_align: None,
                            v_align: None,
                        },
                        cells: vec![TableCell {
                            kind: CellKind::Data,
                            col_span: None,
                            row_span: None,
                            header: TableHeader {
                                attributes: None,
                                h_align: None,
                                v_align: None,
                            },
                            content: InlineTag::Plaintext(String::from(
                                "hello"
                            ))
                        }]
                    }]
                }
            ))
        );
    }
}