flatiron 1.0.5

A parser and HTML renderer for the Textile markup language
Documentation
use crate::parse::{
    attributes::attributes,
    block::{end_of_block, indent_align},
    phrase::phrase,
};
use crate::structs::{
    BlockHeader, BlockTag, InlineTag, List, ListItem, ListKind,
};
use nom::{
    branch::alt,
    character::complete::{char, line_ending},
    combinator::{fail, map, opt, value},
    multi::many1_count,
    sequence::{delimited, terminated, tuple},
    IResult,
};

pub fn list(input: &str) -> IResult<&str, BlockTag> {
    let (rest, (kind, attributes, opt_indent_align)) = terminated(
        tuple((
            alt((
                value(ListKind::Numeric, char('#')),
                value(ListKind::Bulleted, char('*')),
            )),
            opt(attributes),
            opt(indent_align),
        )),
        char(' '),
    )(input)?;
    let (indent, align) = opt_indent_align.unwrap_or((None, None));

    let mut all_items: Vec<(ListKind, usize, InlineTag)> = Vec::new();
    // get first item
    let (rest, first_content) = list_item_content(rest)?;
    all_items.push((kind, 1, first_content));

    // collect all items with depth information
    let mut input = rest;
    while let Err(_) = end_of_block(input) {
        let (rest, (item_kind, depth)) = list_item_head(input)?;
        let (rest, content) = list_item_content(rest)?;
        all_items.push((item_kind, depth, content));
        input = rest;
    }

    // convert flat items list to nested list
    match list_items_to_nested_list(&mut all_items) {
        Ok(l) => Ok((
            input,
            BlockTag::List {
                header: BlockHeader {
                    attributes,
                    indent,
                    align,
                },
                content: l,
            },
        )),
        Err(_) => fail(input),
    }
}

fn list_item_head(input: &str) -> IResult<&str, (ListKind, usize)> {
    delimited(
        line_ending,
        alt((
            map(many1_count(char('#')), |n| (ListKind::Numeric, n)),
            map(many1_count(char('*')), |n| (ListKind::Bulleted, n)),
        )),
        char(' '),
    )(input)
}

fn list_item_content(input: &str) -> IResult<&str, InlineTag> {
    let mut i = 0;
    while let Err(_) = end_list_item(&input[i..]) {
        i += 1;
        while !input.is_char_boundary(i) {
            i += 1;
        }
    }
    let (_, content) = phrase(&input[..i])?;
    Ok((&input[i..], content))
}

fn end_list_item(input: &str) -> IResult<&str, &str> {
    alt((value("", list_item_head), end_of_block))(input)
}

fn list_items_to_nested_list(
    list: &mut Vec<(ListKind, usize, InlineTag)>,
) -> Result<List, String> {
    let mut items: Vec<ListItem> = Vec::new();
    let kind = list[0].0;
    let depth = list[0].1;
    while !list.is_empty() {
        if list[0].1 > depth {
            // unwrap should be safe because depth can only change after 1st item
            items.last_mut().unwrap().sublist =
                Some(list_items_to_nested_list(list)?);
        } else if list[0].1 < depth {
            break;
        } else if list[0].0 != kind {
            return Err(format!(
                "List uses mixed markers on same level: {:?}",
                list[0].2
            ));
        } else {
            let (_, _, content) = list.remove(0);
            items.push(ListItem {
                content,
                sublist: None,
            })
        }
    }
    Ok(List { kind, items })
}