~autumnull/flatiron

ae4095b8cf403000413b3bb3c45b1c9d01686a56 — Autumn! 2 years ago 3f6bd7f
Finished table parser
M README.textile => README.textile +1 -1
@@ 26,6 26,6 @@ Find any "lice (bugs)":https://en.wikipedia.org/wiki/Clothes_iron#Hygiene ? I al
h3. TODO

* Parsing
** Tables
** DONE!
* Rendering
** Everything

M SPEC.md => SPEC.md +1 -1
@@ 210,7 210,7 @@ This is *not* supposed to be read as textile!!

[^1]: Delicious! Also note that this document is regrettably written in markdown, in order to avoid issues with textile snippets being rendered in github-flavored textile, but for the record, flatiron-flavored textile makes it easier to write self-documenting textile.

[^2]: Flatirons prefer flat textiles 🙂
[^2]: Flatirons prefer flat textiles 🙂 (Listen you're lucky you even get nested lists, those things were gross to implement)

[^3]: This decision was made in order to avoid collision with hyphens in regular text, since flatiron textile no longer requires that phrase modifiers be surrounded by spaces. The other phrase modifiers were not deemed to be common enough in regular text to warrant replacing.


M samples/textism.textile => samples/textism.textile +3 -3
@@ 39,10 39,10 @@ Well, that went well. How about we insert an ==<a href="http://www.textism.com/"

"This is a link (optional title)":http://www.textism.com

table{border:1px solid black}.
{border:1px solid black}.
|_. this|_. is|_. a|_. header|
<{background:gray}. |\2. this is|{background:red;width:200px}. a|^<>{height:200px}. row|
|this|<>{padding:10px}. is|^. another|(bob#bob). row|
{background:gray}<. |\2. this is|{background:red;width:200px}. a|{height:200px}^<>. row|
|this|{padding:10px}<>. is|^. another|(bob#bob). row|

An image:


M src/parse/block.rs => src/parse/block.rs +6 -37
@@ 1,5 1,6 @@
use crate::parse::{
    attributes::attributes, from_num, list::list, phrase::phrase,
    alignment, attributes::attributes, from_num, list::list, opt_either_order,
    phrase::phrase, table::table,
};
use crate::structs::{Align, Attributes, BlockKind, BlockTag, Indent};
use nom::{


@@ 19,6 20,8 @@ pub fn block(input: &str) -> IResult<&str, BlockTag> {

    if let Ok((rest, list)) = list(input) {
        return Ok((rest, list));
    } else if let Ok((rest, table)) = table(input) {
        return Ok((rest, table));
    }

    let (rest, opt_header) = opt(block_header)(input)?;


@@ 147,34 150,9 @@ fn footnote_modifier(input: &str) -> IResult<&str, BlockKind> {
}

pub fn indent_align(
    mut input: &str,
    input: &str,
) -> IResult<&str, (Option<Indent>, Option<Align>)> {
    let (mut opt_indent, mut opt_align) = (None, None);
    loop {
        if let Ok((rest, i)) = indent(input) {
            if opt_indent.is_none() {
                opt_indent = Some(i);
                input = rest;
            } else {
                return Ok((input, (opt_indent, opt_align)));
            }
        } else if let Ok((rest, a)) = alignment(input) {
            if opt_align.is_none() {
                opt_align = Some(a);
                input = rest;
            } else {
                return Ok((input, (opt_indent, opt_align)));
            }
        } else {
            break;
        }
    }

    if opt_indent.is_some() || opt_align.is_some() {
        Ok((input, (opt_indent, opt_align)))
    } else {
        fail(input)
    }
    opt_either_order(indent, alignment)(input)
}

fn indent(input: &str) -> IResult<&str, Indent> {


@@ 187,15 165,6 @@ fn indent(input: &str) -> IResult<&str, Indent> {
    }
}

fn alignment(input: &str) -> IResult<&str, Align> {
    alt((
        value(Align::Justify, tag("<>")),
        value(Align::Left, tag("<")),
        value(Align::Right, tag(">")),
        value(Align::Center, tag("=")),
    ))(input)
}

pub fn end_of_block(input: &str) -> IResult<&str, &str> {
    alt((
        eof,

M src/parse/image.rs => src/parse/image.rs +7 -7
@@ 1,5 1,5 @@
use crate::parse::{attributes, link::link_suffix};
use crate::structs::{Align, InlineTag};
use crate::parse::{attributes::attributes, link::link_suffix};
use crate::structs::{ImageAlign, InlineTag};
use nom::{
    branch::alt,
    bytes::complete::{escaped_transform, tag},


@@ 15,7 15,7 @@ pub fn image(input: &str) -> IResult<&str, InlineTag> {
            char('!'),
            tuple((
                opt(image_alignment),
                opt(attributes::attributes),
                opt(attributes),
                image_url,
                opt(delimited(char('('), image_alt, char(')'))),
            )),


@@ 43,11 43,11 @@ pub fn image(input: &str) -> IResult<&str, InlineTag> {
    ))
}

fn image_alignment(input: &str) -> IResult<&str, Align> {
fn image_alignment(input: &str) -> IResult<&str, ImageAlign> {
    alt((
        value(Align::Left, tag("<")),
        value(Align::Right, tag(">")),
        value(Align::Center, tag("=")),
        value(ImageAlign::Left, tag("<")),
        value(ImageAlign::Right, tag(">")),
        value(ImageAlign::Center, tag("=")),
    ))(input)
}


M src/parse/list.rs => src/parse/list.rs +10 -6
@@ 1,4 1,8 @@
use crate::parse::{attributes, block, phrase};
use crate::parse::{
    attributes::attributes,
    block::{end_of_block, indent_align},
    phrase::phrase,
};
use crate::structs::{BlockTag, InlineTag, List, ListItem, ListKind};
use nom::{
    branch::alt,


@@ 16,8 20,8 @@ pub fn list(input: &str) -> IResult<&str, BlockTag> {
                value(ListKind::Numeric, char('#')),
                value(ListKind::Bulleted, char('*')),
            )),
            opt(attributes::attributes),
            opt(block::indent_align),
            opt(attributes),
            opt(indent_align),
        )),
        char(' '),
    )(input)?;


@@ 30,7 34,7 @@ pub fn list(input: &str) -> IResult<&str, BlockTag> {

    // collect all items with depth information
    let mut input = rest;
    while let Err(_) = block::end_of_block(input) {
    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));


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

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

fn list_items_to_nested_list(

M src/parse/mod.rs => src/parse/mod.rs +581 -412
@@ 1,10 1,13 @@
use crate::structs::{InlineTag, PhraseKind, Textile};
use crate::structs::{Align, InlineTag, PhraseKind, Textile};
use nom::{
    branch::alt,
    bytes::complete::tag,
    character::complete::line_ending,
    combinator::{fail, value},
    error::ParseError,
    multi::many0,
    sequence::{preceded, terminated},
    IResult,
    IResult, Parser,
};

mod acronym;


@@ 81,11 84,61 @@ fn from_num(input: &str) -> Result<usize, std::num::ParseIntError> {
    usize::from_str_radix(input, 10)
}

pub fn alignment(input: &str) -> IResult<&str, Align> {
    alt((
        value(Align::Justify, tag("<>")),
        value(Align::Left, tag("<")),
        value(Align::Right, tag(">")),
        value(Align::Center, tag("=")),
    ))(input)
}

fn opt_either_order<I, O, P, E, F, G>(
    mut f: F,
    mut g: G,
) -> impl FnMut(I) -> IResult<I, (Option<O>, Option<P>)>
where
    I: Clone,
    F: Parser<I, O, E>,
    G: Parser<I, P, E>,
    E: ParseError<I>,
{
    move |mut input: I| {
        let (mut opt_f, mut opt_g) = (None, None);
        loop {
            if let Ok((rest, i)) = f.parse(input.clone()) {
                if opt_f.is_none() {
                    opt_f = Some(i);
                    input = rest;
                } else {
                    return Ok((input, (opt_f, opt_g)));
                }
            } else if let Ok((rest, a)) = g.parse(input.clone()) {
                if opt_g.is_none() {
                    opt_g = Some(a);
                    input = rest;
                } else {
                    return Ok((input, (opt_f, opt_g)));
                }
            } else {
                break;
            }
        }

        if opt_f.is_some() || opt_g.is_some() {
            Ok((input, (opt_f, opt_g)))
        } else {
            fail(input)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::structs::{
        Attributes, BlockKind, BlockTag, List, ListItem, ListKind,
        Attributes, BlockKind, BlockTag, CellKind, List, ListItem, ListKind,
        TableCell, TableRow, VerticalAlign,
    };

    #[test]


@@ 145,427 198,543 @@ This is a paragraph with some _emphasized text_.";
    fn textile_sample() {
        let input = include_str!("../../samples/textism.textile");
        let result = textile(input);
        assert_eq!(
            result,
            Ok((
                "",
                Textile(vec![
                    BlockTag::Basic {
                        kind: BlockKind::Header(2),
                        indent: None,
                        align: None,
                        attributes: Some(Attributes {
                            class: None,
                            id: None,
                            style: Some(String::from("color:green")),
                            language: None
                        }),
                        content: InlineTag::Plaintext(String::from("This is a title"))
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Header(3),
                        indent: None,
                        align: None,
        let blocks = vec![
            BlockTag::Basic {
                kind: BlockKind::Header(2),
                indent: None,
                align: None,
                attributes: Some(Attributes {
                    class: None,
                    id: None,
                    style: Some(String::from("color:green")),
                    language: None
                }),
                content: InlineTag::Plaintext(String::from("This is a title"))
            },
            BlockTag::Basic {
                kind: BlockKind::Header(3),
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Plaintext(String::from("This is a subhead"))
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: Some(Attributes {
                    class: None,
                    id: None,
                    style: Some(String::from("color:red")),
                    language: None
                }),
                content: InlineTag::Plaintext(String::from("This is some text of dubious character. Isn't the use of \"quotes\" just lazy writing -- and theft of 'intellectual property' besides? I think the time has come to see a block quote."))
            },
            BlockTag::Basic {
                kind: BlockKind::BlockQuote,
                indent: None,
                align: None,
                attributes: Some(Attributes {
                    class: None,
                    id: None,
                    style: None,
                    language: Some(String::from("fr"))
                }),
                content: InlineTag::Plaintext(String::from("This is a block quote. I'll admit it's not the most exciting block quote ever devised."))
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Plaintext(String::from("Simple list:"))
            },
            BlockTag::List{
                indent: None,
                align: None,
                attributes: Some(Attributes {
                    class: None,
                    id: None,
                    style: Some(String::from("color:blue")),
                    language: None
                }),
                content: List {
                    kind: ListKind::Numeric,
                    items: vec![
                        ListItem {
                            content: InlineTag::Plaintext(String::from("one")),
                            sublist: None,
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("two")),
                            sublist: None,
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("three")),
                            sublist: None,
                        },
                    ]
                }
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Plaintext(String::from("Multi-level list:"))
            },
            BlockTag::List{
                indent: None,
                align: None,
                attributes: None,
                content: List {
                    kind: ListKind::Numeric,
                    items: vec![
                        ListItem {
                            content: InlineTag::Plaintext(String::from("one")),
                            sublist: Some(List {
                                kind: ListKind::Numeric,
                                items: vec![
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("aye")),
                                        sublist: None,
                                    },
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("bee")),
                                        sublist: None,
                                    },
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("see")),
                                        sublist: None,
                                    }
                                ],
                            }),
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("two")),
                            sublist: Some(List {
                                kind: ListKind::Numeric,
                                items: vec![
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("x")),
                                        sublist: None,
                                    },
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("y")),
                                        sublist: None,
                                    },
                                ],
                            }),
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("three")),
                            sublist: None,
                        },
                    ]
                }
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Plaintext(String::from("Mixed list:"))
            },
            BlockTag::List{
                indent: None,
                align: None,
                attributes: None,
                content: List {
                    kind: ListKind::Bulleted,
                    items: vec![
                        ListItem {
                            content: InlineTag::Plaintext(String::from("Point one")),
                            sublist: None,
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("Point two")),
                            sublist: Some(List {
                                kind: ListKind::Numeric,
                                items: vec![
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("Step 1")),
                                        sublist: None,
                                    },
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("Step 2")),
                                        sublist: None,
                                    },
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("Step 3")),
                                        sublist: None,
                                    }
                                ],
                            }),
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("Point three")),
                            sublist: Some(List {
                                kind: ListKind::Bulleted,
                                items: vec![
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("Sub point 1")),
                                        sublist: None,
                                    },
                                    ListItem {
                                        content: InlineTag::Plaintext(String::from("Sub point 2")),
                                        sublist: None,
                                    },
                                ],
                            }),
                        },
                    ]
                }
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Phrase {
                    kind: None,
                    attributes: None,
                    content: vec![
                        InlineTag::Plaintext(String::from("Well, that went well. How about we insert an ")),
                        InlineTag::NoTextile(String::from("<a href=\"http://www.textism.com/\" title=\"watch out\">old-fashioned hypertext link</a>")),
                        InlineTag::Plaintext(String::from("? Will the quote marks in the tags get messed up? No!")),
                    ]
                }
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Link {
                    attributes: None,
                    title: Some(String::from("optional title")),
                    url: String::from("http://www.textism.com"),
                    content: Box::new(
                        InlineTag::Plaintext(String::from("This is a link"))
                    )
                }
            },
            BlockTag::Table {
                attributes: Some(Attributes {
                    class: None,
                    id: None,
                    style: Some(String::from("border:1px solid black")),
                    language: None
                }),
                indent: None,
                align: None,
                rows: vec![
                    TableRow {
                        attributes: None,
                        content: InlineTag::Plaintext(String::from("This is a subhead"))
                        h_align: None,
                        v_align: None,
                        cells: vec![
                            TableCell {
                                kind: CellKind::Header,
                                col_span: None,
                                row_span: None,
                                attributes: None,
                                h_align: None,
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("this"))
                            },
                            TableCell {
                                kind: CellKind::Header,
                                col_span: None,
                                row_span: None,
                                attributes: None,
                                h_align: None,
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("is"))
                            },
                            TableCell {
                                kind: CellKind::Header,
                                col_span: None,
                                row_span: None,
                                attributes: None,
                                h_align: None,
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("a"))
                            },
                            TableCell {
                                kind: CellKind::Header,
                                col_span: None,
                                row_span: None,
                                attributes: None,
                                h_align: None,
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("header"))
                            },
                        ]
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: Some(Attributes {
                            class: None,
                            id: None,
                            style: Some(String::from("color:red")),
                            language: None
                        }),
                        content: InlineTag::Plaintext(String::from("This is some text of dubious character. Isn't the use of \"quotes\" just lazy writing -- and theft of 'intellectual property' besides? I think the time has come to see a block quote."))
                    },
                    BlockTag::Basic {
                        kind: BlockKind::BlockQuote,
                        indent: None,
                        align: None,
                        attributes: Some(Attributes {
                    TableRow {
                        attributes: Some(Attributes{
                            class: None,
                            id: None,
                            style: None,
                            language: Some(String::from("fr"))
                            style: Some(String::from("background:gray")),
                            language: None,
                        }),
                        content: InlineTag::Plaintext(String::from("This is a block quote. I'll admit it's not the most exciting block quote ever devised."))
                        h_align: Some(Align::Left),
                        v_align: None,
                        cells: vec![
                            TableCell {
                                kind: CellKind::Data,
                                col_span: Some(2),
                                row_span: None,
                                attributes: None,
                                h_align: None,
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("this is"))
                            },
                            TableCell {
                                kind: CellKind::Data,
                                col_span: None,
                                row_span: None,
                                attributes: Some(Attributes{
                                    class: None,
                                    id: None,
                                    style: Some(String::from("background:red;width:200px")),
                                    language: None,
                                }),
                                h_align: None,
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("a"))
                            },
                            TableCell {
                                kind: CellKind::Data,
                                col_span: None,
                                row_span: None,
                                attributes: Some(Attributes{
                                    class: None,
                                    id: None,
                                    style: Some(String::from("height:200px")),
                                    language: None,
                                }),
                                h_align: Some(Align::Justify),
                                v_align: Some(VerticalAlign::Top),
                                content: InlineTag::Plaintext(String::from("row"))
                            },
                        ]
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                    TableRow {
                        attributes: None,
                        content: InlineTag::Plaintext(String::from("Simple list:"))
                    },
                    BlockTag::List{
                        indent: None,
                        align: None,
                        attributes: Some(Attributes {
                            class: None,
                            id: None,
                            style: Some(String::from("color:blue")),
                            language: None
                        }),
                        content: List {
                            kind: ListKind::Numeric,
                            items: vec![
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("one")),
                                    sublist: None,
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("two")),
                                    sublist: None,
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("three")),
                                    sublist: None,
                                },
                            ]
                        }
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Plaintext(String::from("Multi-level list:"))
                    },
                    BlockTag::List{
                        indent: None,
                        align: None,
                        attributes: None,
                        content: List {
                            kind: ListKind::Numeric,
                            items: vec![
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("one")),
                                    sublist: Some(List {
                                        kind: ListKind::Numeric,
                                        items: vec![
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("aye")),
                                                sublist: None,
                                            },
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("bee")),
                                                sublist: None,
                                            },
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("see")),
                                                sublist: None,
                                            }
                                        ],
                                    }),
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("two")),
                                    sublist: Some(List {
                                        kind: ListKind::Numeric,
                                        items: vec![
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("x")),
                                                sublist: None,
                                            },
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("y")),
                                                sublist: None,
                                            },
                                        ],
                                    }),
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("three")),
                                    sublist: None,
                                },
                            ]
                        }
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Plaintext(String::from("Mixed list:"))
                    },
                    BlockTag::List{
                        indent: None,
                        align: None,
                        attributes: None,
                        content: List {
                            kind: ListKind::Bulleted,
                            items: vec![
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("Point one")),
                                    sublist: None,
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("Point two")),
                                    sublist: Some(List {
                                        kind: ListKind::Numeric,
                                        items: vec![
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("Step 1")),
                                                sublist: None,
                                            },
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("Step 2")),
                                                sublist: None,
                                            },
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("Step 3")),
                                                sublist: None,
                                            }
                                        ],
                                    }),
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("Point three")),
                                    sublist: Some(List {
                                        kind: ListKind::Bulleted,
                                        items: vec![
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("Sub point 1")),
                                                sublist: None,
                                            },
                                            ListItem {
                                                content: InlineTag::Plaintext(String::from("Sub point 2")),
                                                sublist: None,
                                            },
                                        ],
                                    }),
                                },
                            ]
                        }
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Phrase {
                            kind: None,
                            attributes: None,
                            content: vec![
                                InlineTag::Plaintext(String::from("Well, that went well. How about we insert an ")),
                                InlineTag::NoTextile(String::from("<a href=\"http://www.textism.com/\" title=\"watch out\">old-fashioned hypertext link</a>")),
                                InlineTag::Plaintext(String::from("? Will the quote marks in the tags get messed up? No!")),
                            ]
                        }
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Link {
                            attributes: None,
                            title: Some(String::from("optional title")),
                            url: String::from("http://www.textism.com"),
                            content: Box::new(
                                InlineTag::Plaintext(String::from("This is a link"))
                            )
                        }
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Phrase {
                            kind: None,
                            attributes: None,
                            content: vec![
                                InlineTag::Plaintext(String::from("table{border:1px solid black}.")),
                                InlineTag::LineBreak,
                                InlineTag::Plaintext(String::from("|")),
                                InlineTag::Phrase {
                                    kind: Some(PhraseKind::Emphasis),
                                    attributes: None,
                                    content: vec![
                                        InlineTag::Plaintext(String::from(". this|"))
                                    ]
                                },
                                InlineTag::Plaintext(String::from(". is|")),
                                InlineTag::Phrase {
                                    kind: Some(PhraseKind::Emphasis),
                                    attributes: None,
                                    content: vec![
                                        InlineTag::Plaintext(String::from(". a|"))
                                    ]
                                },
                                InlineTag::Plaintext(String::from(". header|")),
                                InlineTag::LineBreak,
                                InlineTag::Plaintext(String::from("<{background:gray}. |\\2. this is|{background:red;width:200px}. a|")),
                                InlineTag::Phrase {
                                    kind: Some(PhraseKind::Superscript),
                                    attributes: None,
                                    content: vec![
                                        InlineTag::Plaintext(String::from("<>{height:200px}. row|")),
                                        InlineTag::LineBreak,
                                        InlineTag::Plaintext(String::from("|this|<>{padding:10px}. is|"))
                                    ]
                                },
                                InlineTag::Plaintext(String::from(". another|(bob#bob). row|"))
                            ]
                        }
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Plaintext(String::from("An image:"))
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Image {
                            attributes: None,
                            align: None,
                            url: String::from("/textist.gif"),
                            alt: Some(String::from("optional alt text"))
                        }
                    },
                    BlockTag::List{
                        indent: None,
                        align: None,
                        attributes: None,
                        content: List {
                            kind: ListKind::Numeric,
                            items: vec![
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("Librarians rule")),
                                    sublist: None,
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("Yes they do")),
                                    sublist: None,
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("But you knew that")),
                                    sublist: None,
                                },
                            ]
                        }
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Phrase {
                            kind: None,
                        h_align: None,
                        v_align: None,
                        cells: vec![
                            TableCell {
                                kind: CellKind::Data,
                                col_span: None,
                                row_span: None,
                                attributes: None,
                                h_align: None,
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("this"))
                            },
                            TableCell {
                                kind: CellKind::Data,
                                col_span: None,
                                row_span: None,
                                attributes: Some(Attributes{
                                    class: None,
                                    id: None,
                                    style: Some(String::from("padding:10px")),
                                    language: None,
                                }),
                                h_align: Some(Align::Justify),
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("is"))
                            },
                            TableCell {
                                kind: CellKind::Data,
                                col_span: None,
                                row_span: None,
                                attributes: None,
                                h_align: None,
                                v_align: Some(VerticalAlign::Top),
                                content: InlineTag::Plaintext(String::from("another"))
                            },
                            TableCell {
                                kind: CellKind::Data,
                                col_span: None,
                                row_span: None,
                                attributes: Some(Attributes{
                                    class: Some(String::from("bob")),
                                    id: Some(String::from("bob")),
                                    style: None,
                                    language: None,
                                }),
                                h_align: None,
                                v_align: None,
                                content: InlineTag::Plaintext(String::from("row"))
                            },
                        ]
                    }
                ],
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Plaintext(String::from("An image:"))
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Image {
                    attributes: None,
                    align: None,
                    url: String::from("/textist.gif"),
                    alt: Some(String::from("optional alt text"))
                }
            },
            BlockTag::List{
                indent: None,
                align: None,
                attributes: None,
                content: List {
                    kind: ListKind::Numeric,
                    items: vec![
                        ListItem {
                            content: InlineTag::Plaintext(String::from("Librarians rule")),
                            sublist: None,
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("Yes they do")),
                            sublist: None,
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("But you knew that")),
                            sublist: None,
                        },
                    ]
                }
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Phrase {
                    kind: None,
                    attributes: None,
                    content: vec![
                        InlineTag::Plaintext(String::from("Some more text of dubious character. Here is a noisome string of ")),
                        InlineTag::Acronym {
                            title: None,
                            content: String::from("CAPITAL")
                        },
                        InlineTag::Plaintext(String::from(" letters. Here is something we want to ")),
                        InlineTag::Phrase {
                            kind: Some(PhraseKind::Emphasis),
                            attributes: None,
                            content: vec![
                                InlineTag::Plaintext(String::from("Some more text of dubious character. Here is a noisome string of ")),
                                InlineTag::Acronym {
                                    title: None,
                                    content: String::from("CAPITAL")
                                },
                                InlineTag::Plaintext(String::from(" letters. Here is something we want to ")),
                                InlineTag::Phrase {
                                    kind: Some(PhraseKind::Emphasis),
                                    attributes: None,
                                    content: vec![
                                        InlineTag::Plaintext(String::from("emphasize"))
                                    ]
                                },
                                InlineTag::Plaintext(String::from(".")),
                                InlineTag::LineBreak,
                                InlineTag::Plaintext(String::from("That was a linebreak. And something to indicate ")),
                                InlineTag::Phrase {
                                    kind: Some(PhraseKind::Strong),
                                    attributes: None,
                                    content: vec![
                                        InlineTag::Plaintext(String::from("strength"))
                                    ]
                                },
                                InlineTag::Plaintext(String::from(". Of course I could use <em>my own ")),
                                InlineTag::Acronym {
                                    title: None,
                                    content: String::from("HTML")
                                },
                                InlineTag::Plaintext(String::from(" tags</em> if I <strong>felt</strong> like it."))
                                InlineTag::Plaintext(String::from("emphasize"))
                            ]
                        }
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Header(3),
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Plaintext(String::from("Coding"))
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Phrase {
                            kind: None,
                        },
                        InlineTag::Plaintext(String::from(".")),
                        InlineTag::LineBreak,
                        InlineTag::Plaintext(String::from("That was a linebreak. And something to indicate ")),
                        InlineTag::Phrase {
                            kind: Some(PhraseKind::Strong),
                            attributes: None,
                            content: vec![
                                InlineTag::Plaintext(String::from("This ")),
                                InlineTag::Code(String::from("is some code, \"isn't it\"")),
                                InlineTag::Plaintext(String::from(". Watch those quote marks! Now for some preformatted text:"))
                            ]
                        }
                    },
                    BlockTag::Preformatted {
                        kind: BlockKind::BlockCode,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: String::from("$text = str_replace(\"<p>%::%</p>\",\"\",$text);\n$text = str_replace(\"%::%</p>\",\"\",$text);\n$text = str_replace(\"%::%\",\"\",$text);")
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Plaintext(String::from("This isn't code."))
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        content: InlineTag::Plaintext(String::from("So you see, my friends:"))
                    },
                    BlockTag::List {
                        indent: None,
                        align: None,
                        attributes: None,
                        content: List {
                            kind: ListKind::Bulleted,
                            items: vec![
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("The time is now")),
                                    sublist: None,
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("The time is not later")),
                                    sublist: None,
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("The time is not yesterday")),
                                    sublist: None,
                                },
                                ListItem {
                                    content: InlineTag::Plaintext(String::from("We must act")),
                                    sublist: None,
                                },
                                InlineTag::Plaintext(String::from("strength"))
                            ]
                        }
                    }
                ])
            ))
        );
                        },
                        InlineTag::Plaintext(String::from(". Of course I could use <em>my own ")),
                        InlineTag::Acronym {
                            title: None,
                            content: String::from("HTML")
                        },
                        InlineTag::Plaintext(String::from(" tags</em> if I <strong>felt</strong> like it."))
                    ]
                }
            },
            BlockTag::Basic {
                kind: BlockKind::Header(3),
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Plaintext(String::from("Coding"))
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Phrase {
                    kind: None,
                    attributes: None,
                    content: vec![
                        InlineTag::Plaintext(String::from("This ")),
                        InlineTag::Code(String::from("is some code, \"isn't it\"")),
                        InlineTag::Plaintext(String::from(". Watch those quote marks! Now for some preformatted text:"))
                    ]
                }
            },
            BlockTag::Preformatted {
                kind: BlockKind::BlockCode,
                indent: None,
                align: None,
                attributes: None,
                content: String::from("$text = str_replace(\"<p>%::%</p>\",\"\",$text);\n$text = str_replace(\"%::%</p>\",\"\",$text);\n$text = str_replace(\"%::%\",\"\",$text);")
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Plaintext(String::from("This isn't code."))
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Plaintext(String::from("So you see, my friends:"))
            },
            BlockTag::List {
                indent: None,
                align: None,
                attributes: None,
                content: List {
                    kind: ListKind::Bulleted,
                    items: vec![
                        ListItem {
                            content: InlineTag::Plaintext(String::from("The time is now")),
                            sublist: None,
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("The time is not later")),
                            sublist: None,
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("The time is not yesterday")),
                            sublist: None,
                        },
                        ListItem {
                            content: InlineTag::Plaintext(String::from("We must act")),
                            sublist: None,
                        },
                    ]
                }
            }
        ];
        match result {
            Ok((_, Textile(v))) => {
                for i in 0..v.len() {
                    assert_eq!(v[i], blocks[i]);
                }
            }
            _ => assert_eq!(result, Ok(("", Textile(vec![])))),
        }
    }
}

M src/parse/phrase.rs => src/parse/phrase.rs +4 -6
@@ 1,5 1,5 @@
use crate::parse::block::strip_flatiron_extended;
use crate::parse::{acronym, attributes, from_num};
use crate::parse::{acronym::acronym, attributes::attributes, from_num};
use crate::structs::{InlineTag, PhraseKind};
use nom::{
    branch::alt,


@@ 51,7 51,7 @@ pub fn phrase(input: &str) -> IResult<&str, InlineTag> {
        let mut matched = None;
        if preceding_space {
            // only match acronym if preceded by a space
            if let Ok((rest, tag)) = acronym::acronym(&input[i..]) {
            if let Ok((rest, tag)) = acronym(&input[i..]) {
                matched = Some((rest, tag))
            }
        }


@@ 96,9 96,7 @@ fn tagged_phrase(
    move |mut input: &str| {
        // match opening delimiter
        let (rest, _delimiter) = kind.delimiter(input)?;
        input = rest;
        // only match attributes if phrase is tagged
        let (rest, attributes) = opt(attributes::attributes)(input)?;
        let (rest, attributes) = opt(attributes)(rest)?;
        input = rest;

        let mut content: Vec<InlineTag> = Vec::new();


@@ 128,7 126,7 @@ fn tagged_phrase(
            let mut matched = None;
            if preceding_space {
                // only match acronym if preceded by a space
                if let Ok((rest, tag)) = acronym::acronym(&input[i..]) {
                if let Ok((rest, tag)) = acronym(&input[i..]) {
                    matched = Some((rest, tag))
                }
            }

M src/parse/table.rs => src/parse/table.rs +212 -2
@@ 1,5 1,177 @@
use crate::structs::VerticalAlign;
use nom::{branch::alt, bytes::complete::tag, combinator::value, IResult};
use crate::parse::{
    alignment,
    attributes::attributes,
    block::{end_of_block, indent_align},
    from_num, opt_either_order,
    phrase::phrase,
};
use crate::structs::{
    Align, Attributes, BlockTag, CellKind, Indent, InlineTag, TableCell,
    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 {
            indent,
            align,
            attributes,
            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 {
            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,
            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((


@@ 8,3 180,41 @@ fn vertical_alignment(input: &str) -> IResult<&str, VerticalAlign> {
        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 {
                    indent: None,
                    align: None,
                    attributes: None,
                    rows: vec![TableRow {
                        attributes: None,
                        h_align: None,
                        v_align: None,
                        cells: vec![TableCell {
                            kind: CellKind::Data,
                            col_span: None,
                            row_span: None,
                            attributes: None,
                            h_align: None,
                            v_align: None,
                            content: InlineTag::Plaintext(String::from(
                                "hello"
                            ))
                        }]
                    }]
                }
            ))
        );
    }
}

M src/structs.rs => src/structs.rs +39 -1
@@ 23,6 23,12 @@ pub enum BlockTag {
        attributes: Option<Attributes>,
        content: List,
    },
    Table {
        indent: Option<Indent>,
        align: Option<Align>,
        attributes: Option<Attributes>,
        rows: Vec<TableRow>,
    },
    NoTextile(String),
}



@@ 56,6 62,31 @@ pub enum ListKind {
}

#[derive(Debug, PartialEq)]
pub struct TableRow {
    pub attributes: Option<Attributes>,
    pub h_align: Option<Align>,
    pub v_align: Option<VerticalAlign>,
    pub cells: Vec<TableCell>,
}

#[derive(Debug, PartialEq)]
pub struct TableCell {
    pub kind: CellKind,
    pub col_span: Option<usize>,
    pub row_span: Option<usize>,
    pub attributes: Option<Attributes>,
    pub h_align: Option<Align>,
    pub v_align: Option<VerticalAlign>,
    pub content: InlineTag,
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum CellKind {
    Header,
    Data,
}

#[derive(Debug, PartialEq)]
pub struct Indent {
    pub left: usize,
    pub right: usize,


@@ 93,7 124,7 @@ pub enum InlineTag {
    LineBreak,
    Image {
        attributes: Option<Attributes>,
        align: Option<Align>,
        align: Option<ImageAlign>,
        url: String,
        alt: Option<String>,
    },


@@ 115,6 146,13 @@ pub enum InlineTag {
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ImageAlign {
    Left,
    Right,
    Center,
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum PhraseKind {
    Italic,
    Bold,