~autumnull/flatiron

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

* Parsing
** DONE!
* Rendering
** Everything
** Punctuation transforms

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

An image:

!images/flatiron.png(optional alt text)!
!=images/flatiron.png(optional alt text)!

# Librarians rule
# Yes they do

M src/lib.rs => src/lib.rs +3 -7
@@ 1,14 1,10 @@
#![allow(dead_code)]
use nom::combinator::complete;

mod parse;
mod render;
mod structs;

pub fn convert(input: String) -> Result<String, &'static str> {
    let parse_result = complete(parse::textile)(&input);
    match parse_result {
        Ok((_, s)) => Ok(format!("{}", s)),
        Err(_) => Err("Could not parse textile!"),
    }
pub fn convert(input: String) -> String {
    let (_, textile) = complete(parse::textile)(&input).unwrap();
    format!("{}", textile)
}

M src/main.rs => src/main.rs +1 -1
@@ 4,6 4,6 @@ use std::fs;
fn main() {
    let textile = fs::read_to_string("samples/textism.textile")
        .expect("Something went wrong while reading the file");
    let html = convert(textile).unwrap();
    let html = convert(textile);
    print!("{}", html);
}

M src/parse/block.rs => src/parse/block.rs +108 -54
@@ 2,7 2,9 @@ use crate::parse::{
    alignment, attributes::attributes, from_num, list::list, opt_either_order,
    phrase::phrase, table::table,
};
use crate::structs::{Align, Attributes, BlockKind, BlockTag, Indent};
use crate::structs::{
    Align, Attributes, BlockHeader, BlockKind, BlockTag, Indent,
};
use nom::{
    branch::alt,
    bytes::complete::{escaped_transform, tag, take_while1},


@@ 56,18 58,46 @@ pub fn block(input: &str) -> IResult<&str, BlockTag> {
    let (rest, _) = alt((eof, end_of_block))(&rest[i..])?;

    match kind {
        BlockKind::Paragraph
        | BlockKind::Header(_)
        | BlockKind::Footnote(_)
        | BlockKind::BlockQuote => {
        BlockKind::Paragraph | BlockKind::Header(_) | BlockKind::BlockQuote => {
            let (_, content) = complete(phrase)(body)?;
            Ok((
                rest,
                BlockTag::Basic {
                    kind,
                    header: BlockHeader {
                        indent,
                        align,
                        attributes,
                    },
                    content,
                },
            ))
        }
        BlockKind::Footnote(n) => {
            let mut attributes = attributes.unwrap_or(Attributes {
                class: None,
                id: None,
                style: None,
                language: None,
            });
            attributes.class = match attributes.class {
                Some(s) => Some(format!("footnote {}", s)),
                None => Some(String::from("footnote")),
            };
            attributes.id = match attributes.id {
                Some(s) => Some(format!("fn{} {}", n, s)),
                None => Some(format!("fn{}", n)),
            };
            let (_, content) = complete(phrase)(body)?;
            Ok((
                rest,
                BlockTag::Basic {
                    kind,
                    indent,
                    align,
                    attributes,
                    header: BlockHeader {
                        indent,
                        align,
                        attributes: Some(attributes),
                    },
                    content,
                },
            ))


@@ 81,9 111,11 @@ pub fn block(input: &str) -> IResult<&str, BlockTag> {
                rest,
                BlockTag::Preformatted {
                    kind,
                    indent,
                    align,
                    attributes,
                    header: BlockHeader {
                        indent,
                        align,
                        attributes,
                    },
                    content,
                },
            ))


@@ 195,9 227,11 @@ mod tests {
                "",
                BlockTag::Basic {
                    kind: BlockKind::Paragraph,
                    indent: None,
                    align: None,
                    attributes: None,
                    header: BlockHeader {
                        indent: None,
                        align: None,
                        attributes: None,
                    },
                    content: InlineTag::Plaintext(String::from(
                        "Untagged paragraph"
                    ))


@@ 216,9 250,11 @@ mod tests {
                "",
                BlockTag::Basic {
                    kind: BlockKind::Paragraph,
                    indent: None,
                    align: None,
                    attributes: None,
                    header: BlockHeader {
                        indent: None,
                        align: None,
                        attributes: None,
                    },
                    content: InlineTag::Plaintext(String::from(
                        "Tagged paragraph"
                    ))


@@ 237,9 273,11 @@ mod tests {
                "",
                BlockTag::Preformatted {
                    kind: BlockKind::Preformatted,
                    indent: None,
                    align: None,
                    attributes: None,
                    header: BlockHeader {
                        indent: None,
                        align: None,
                        attributes: None,
                    },
                    content: String::from("Preformatted _paragraph_")
                }
            ))


@@ 270,9 308,11 @@ mod tests {
                "p. Hell if I know.",
                BlockTag::Basic {
                    kind: BlockKind::Paragraph,
                    indent: None,
                    align: None,
                    attributes: None,
                    header: BlockHeader {
                        indent: None,
                        align: None,
                        attributes: None,
                    },
                    content: InlineTag::Phrase {
                        kind: None,
                        attributes: None,


@@ 302,14 342,16 @@ mod tests {
                "",
                BlockTag::Basic {
                    kind: BlockKind::Header(2),
                    indent: None,
                    align: None,
                    attributes: Some(Attributes {
                        class: Some(String::from("class")),
                        id: None,
                        style: Some(String::from("color:green")),
                        language: None,
                    }),
                    header: BlockHeader {
                        indent: None,
                        align: None,
                        attributes: Some(Attributes {
                            class: Some(String::from("class")),
                            id: None,
                            style: Some(String::from("color:green")),
                            language: None,
                        }),
                    },
                    content: InlineTag::Plaintext(String::from(
                        "This is a title"
                    ))


@@ 328,9 370,11 @@ mod tests {
                "",
                BlockTag::Basic {
                    kind: BlockKind::Header(2),
                    indent: None,
                    align: Some(Align::Center),
                    attributes: None,
                    header: BlockHeader {
                        indent: None,
                        align: Some(Align::Center),
                        attributes: None,
                    },
                    content: InlineTag::Plaintext(String::from(
                        "This is a title"
                    ))


@@ 349,9 393,11 @@ mod tests {
                "",
                BlockTag::Basic {
                    kind: BlockKind::Header(2),
                    indent: Some(Indent { left: 1, right: 1 }),
                    align: None,
                    attributes: None,
                    header: BlockHeader {
                        indent: Some(Indent { left: 1, right: 1 }),
                        align: None,
                        attributes: None,
                    },
                    content: InlineTag::Plaintext(String::from(
                        "This is a title"
                    ))


@@ 370,9 416,11 @@ mod tests {
                "",
                BlockTag::Basic {
                    kind: BlockKind::Paragraph,
                    indent: Some(Indent { left: 0, right: 2 }),
                    align: Some(Align::Right),
                    attributes: None,
                    header: BlockHeader {
                        indent: Some(Indent { left: 0, right: 2 }),
                        align: Some(Align::Right),
                        attributes: None,
                    },
                    content: InlineTag::Plaintext(String::from("I am a fish!"))
                }
            ))


@@ 389,9 437,11 @@ mod tests {
                "",
                BlockTag::Basic {
                    kind: BlockKind::Paragraph,
                    indent: Some(Indent { left: 0, right: 2 }),
                    align: Some(Align::Right),
                    attributes: None,
                    header: BlockHeader {
                        indent: Some(Indent { left: 0, right: 2 }),
                        align: Some(Align::Right),
                        attributes: None,
                    },
                    content: InlineTag::Plaintext(String::from(
                        "I am a transmitter!"
                    ))


@@ 410,14 460,16 @@ mod tests {
                "",
                BlockTag::Basic {
                    kind: BlockKind::Paragraph,
                    indent: Some(Indent { left: 1, right: 1 }),
                    align: Some(Align::Right),
                    attributes: Some(Attributes {
                        class: Some(String::from("greeting")),
                        id: None,
                        style: Some(String::from("color:green")),
                        language: Some(String::from("fr")),
                    }),
                    header: BlockHeader {
                        indent: Some(Indent { left: 1, right: 1 }),
                        align: Some(Align::Right),
                        attributes: Some(Attributes {
                            class: Some(String::from("greeting")),
                            id: None,
                            style: Some(String::from("color:green")),
                            language: Some(String::from("fr")),
                        }),
                    },
                    content: InlineTag::Plaintext(String::from("Salut!"))
                }
            ))


@@ 445,9 497,11 @@ mod tests {
                "",
                BlockTag::Preformatted {
                    kind: BlockKind::BlockCode,
                    indent: None,
                    align: None,
                    attributes: None,
                    header: BlockHeader {
                        indent: None,
                        align: None,
                        attributes: None,
                    },
                    content: String::from(
                        "This is supposed to be a block of textile code.


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


@@ 24,8 24,7 @@ pub fn image(input: &str) -> IResult<&str, InlineTag> {
        opt(link_suffix),
    ))(input)?;
    let img = InlineTag::Image {
        attributes,
        align,
        header: ImageHeader { attributes, align },
        url,
        alt,
    };


@@ 93,8 92,10 @@ mod tests {
            Ok((
                "",
                InlineTag::Image {
                    attributes: None,
                    align: None,
                    header: ImageHeader {
                        attributes: None,
                        align: None,
                    },
                    url: String::from(
                        "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png",
                    ),


@@ 114,8 115,8 @@ mod tests {
            Ok((
                "",
                InlineTag::Image {
                    attributes: None,
                    align: None,
                    header: ImageHeader {attributes: None,
                    align: None,},
                    url: String::from(
                        "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png",
                    ),


@@ 135,8 136,10 @@ mod tests {
            Ok((
                "",
                InlineTag::Image {
                    attributes: None,
                    align: None,
                    header: ImageHeader {
                        attributes: None,
                        align: None,
                    },
                    url: String::from(
                        "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png(parentheses)",
                    ),


@@ 156,8 159,8 @@ mod tests {
            Ok((
                "",
                InlineTag::Image {
                    attributes: None,
                    align: None,
                    header: ImageHeader {attributes: None,
                    align: None,},
                    url: String::from(
                        "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!!",
                    ),


@@ 187,8 190,8 @@ mod tests {
                    title: None,
                    url: String::from("https://github.com/autumnull/flatiron"),
                    content: Box::new(InlineTag::Image {
                        attributes: None,
                        align: None,
                        header: ImageHeader {attributes: None,
                        align: None,},
                        url: String::from("https://github.com/autumnull/flatiron/raw/main/images/flatiron.png"),
                        alt: None,
                    })


@@ 210,8 213,8 @@ mod tests {
                    title: None,
                    url: String::from("https://github.com/autumnull/flatiron"),
                    content: Box::new(InlineTag::Image {
                        attributes: None,
                        align: None,
                        header: ImageHeader {attributes: None,
                        align: None,},
                        url: String::from("https://github.com/autumnull/flatiron/raw/main/images/flatiron.png"),
                        alt: None,
                    })

M src/parse/list.rs => src/parse/list.rs +8 -4
@@ 3,7 3,9 @@ use crate::parse::{
    block::{end_of_block, indent_align},
    phrase::phrase,
};
use crate::structs::{BlockTag, InlineTag, List, ListItem, ListKind};
use crate::structs::{
    BlockHeader, BlockTag, InlineTag, List, ListItem, ListKind,
};
use nom::{
    branch::alt,
    character::complete::{char, line_ending},


@@ 46,9 48,11 @@ pub fn list(input: &str) -> IResult<&str, BlockTag> {
        Ok(l) => Ok((
            input,
            BlockTag::List {
                indent,
                align,
                attributes,
                header: BlockHeader {
                    attributes,
                    indent,
                    align,
                },
                content: l,
            },
        )),

M src/parse/mod.rs => src/parse/mod.rs +205 -158
@@ 137,8 137,9 @@ where
mod tests {
    use super::*;
    use crate::structs::{
        Attributes, BlockKind, BlockTag, CellKind, List, ListItem, ListKind,
        TableCell, TableRow, VerticalAlign,
        Attributes, BlockHeader, BlockKind, BlockTag, CellKind, ImageAlign,
        ImageHeader, List, ListItem, ListKind, TableCell, TableHeader,
        TableRow, VerticalAlign,
    };

    #[test]


@@ 154,23 155,27 @@ This is a paragraph with some _emphasized text_.";
                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,
                        }),
                        header: BlockHeader {
                            attributes: Some(Attributes {
                                class: None,
                                id: None,
                                style: Some(String::from("color:green")),
                                language: None,
                            }),
                            indent: None,
                            align: None,
                        },
                        content: InlineTag::Plaintext(String::from(
                            "This is a title"
                        ))
                    },
                    BlockTag::Basic {
                        kind: BlockKind::Paragraph,
                        indent: None,
                        align: None,
                        attributes: None,
                        header: BlockHeader {
                            attributes: None,
                            indent: None,
                            align: None,
                        },
                        content: InlineTag::Phrase {
                            kind: None,
                            attributes: None,


@@ 201,63 206,71 @@ This is a paragraph with some _emphasized text_.";
        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
                }),
                header: BlockHeader {
                    attributes: Some(Attributes {
                        class: None,
                        id: None,
                        style: Some(String::from("color:green")),
                        language: None
                    }),
                    indent: None,
                    align: None,
                },
                content: InlineTag::Plaintext(String::from("This is a title"))
            },
            BlockTag::Basic {
                kind: BlockKind::Header(3),
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: 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
                }),
                header: BlockHeader {
                    attributes: Some(Attributes {
                        class: None,
                        id: None,
                        style: Some(String::from("color:red")),
                        language: None
                    }),
                    indent: None,
                    align: 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"))
                }),
                header: BlockHeader {
                    attributes: Some(Attributes {
                        class: None,
                        id: None,
                        style: None,
                        language: Some(String::from("fr"))
                    }),
                    indent: None,
                    align: None,
                },
                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,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: 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
                }),
                header: BlockHeader {
                    attributes: Some(Attributes {
                        class: None,
                        id: None,
                        style: Some(String::from("color:blue")),
                        language: None
                    }),
                    indent: None,
                    align: None,
                },
                content: List {
                    kind: ListKind::Numeric,
                    items: vec![


@@ 278,15 291,15 @@ This is a paragraph with some _emphasized text_.";
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Plaintext(String::from("Multi-level list:"))
            },
            BlockTag::List{
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: List {
                    kind: ListKind::Numeric,
                    items: vec![


@@ 335,15 348,15 @@ This is a paragraph with some _emphasized text_.";
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Plaintext(String::from("Mixed list:"))
            },
            BlockTag::List{
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: List {
                    kind: ListKind::Bulleted,
                    items: vec![


@@ 392,9 405,9 @@ This is a paragraph with some _emphasized text_.";
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Phrase {
                    kind: None,
                    attributes: None,


@@ 407,9 420,9 @@ This is a paragraph with some _emphasized text_.";
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Link {
                    attributes: None,
                    title: Some(String::from("optional title")),


@@ 420,156 433,186 @@ This is a paragraph with some _emphasized text_.";
                }
            },
            BlockTag::Table {
                attributes: Some(Attributes {
                    class: None,
                    id: None,
                    style: Some(String::from("border:1px solid black")),
                    language: None
                }),
                indent: None,
                align: None,
                header: BlockHeader {
                    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,
                        h_align: None,
                        v_align: None,
                        header: TableHeader {
                            attributes: None,
                            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,
                                header: TableHeader {
                                    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,
                                header: TableHeader {
                                    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,
                                header: TableHeader {
                                    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,
                                header: TableHeader {
                                    attributes: None,
                                    h_align: None,
                                    v_align: None,
                                },
                                content: InlineTag::Plaintext(String::from("header"))
                            },
                        ]
                    },
                    TableRow {
                        attributes: Some(Attributes{
                            class: None,
                            id: None,
                            style: Some(String::from("background:gray")),
                            language: None,
                        }),
                        h_align: Some(Align::Left),
                        v_align: None,
                        header: TableHeader {
                            attributes: Some(Attributes{
                                class: None,
                                id: None,
                                style: Some(String::from("background:gray")),
                                language: None,
                            }),
                            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,
                                header: TableHeader {
                                    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,
                                header: TableHeader {
                                    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),
                                header: TableHeader {
                                    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"))
                            },
                        ]
                    },
                    TableRow {
                        attributes: None,
                        h_align: None,
                        v_align: None,
                        header: TableHeader {
                            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,
                                header: TableHeader {
                                    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,
                                header: TableHeader {
                                    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),
                                header: TableHeader {
                                    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,
                                header: TableHeader {
                                    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"))
                            },
                        ]


@@ 578,27 621,31 @@ This is a paragraph with some _emphasized text_.";
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Plaintext(String::from("An image:"))
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                indent: None,
                align: None,
                attributes: None,
                content: InlineTag::Image {
                header: BlockHeader {
                    attributes: None,
                    indent: None,
                    align: None,
                },
                content: InlineTag::Image {
                    header: ImageHeader {
                        attributes: None,
                        align: Some(ImageAlign::Center),
                    },
                    url: String::from("images/flatiron.png"),
                    alt: Some(String::from("optional alt text"))
                }
            },
            BlockTag::List{
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: List {
                    kind: ListKind::Numeric,
                    items: vec![


@@ 619,9 666,9 @@ This is a paragraph with some _emphasized text_.";
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Phrase {
                    kind: None,
                    attributes: None,


@@ 660,16 707,16 @@ This is a paragraph with some _emphasized text_.";
            },
            BlockTag::Basic {
                kind: BlockKind::Header(3),
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Plaintext(String::from("Coding"))
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Phrase {
                    kind: None,
                    attributes: None,


@@ 682,29 729,29 @@ This is a paragraph with some _emphasized text_.";
            },
            BlockTag::Preformatted {
                kind: BlockKind::BlockCode,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: 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,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Plaintext(String::from("This isn't code."))
            },
            BlockTag::Basic {
                kind: BlockKind::Paragraph,
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: InlineTag::Plaintext(String::from("So you see, my friends:"))
            },
            BlockTag::List {
                header: BlockHeader {attributes: None,
                indent: None,
                align: None,
                attributes: None,
                align: None,},
                content: List {
                    kind: ListKind::Bulleted,
                    items: vec![

M src/parse/phrase.rs => src/parse/phrase.rs +5 -3
@@ 202,7 202,7 @@ pub fn footnote_ref(input: &str) -> IResult<&str, InlineTag> {
#[cfg(test)]
mod tests {
    use super::*;
    use crate::structs::Attributes;
    use crate::structs::{Attributes, ImageHeader};

    #[test]
    fn empty_phrase() {


@@ 263,8 263,10 @@ mod tests {
                    content: vec![
                        InlineTag::Plaintext(String::from("Textist: ")),
                        InlineTag::Image {
                            attributes: None,
                            align: None,
                            header: ImageHeader {
                                attributes: None,
                                align: None,
                            },
                            url: String::from("/common/textist.gif"),
                            alt: Some(String::from("Textist")),
                        }

M src/parse/table.rs => src/parse/table.rs +32 -20
@@ 6,8 6,8 @@ use crate::parse::{
    phrase::phrase,
};
use crate::structs::{
    Align, Attributes, BlockTag, CellKind, Indent, InlineTag, TableCell,
    TableRow, VerticalAlign,
    Align, Attributes, BlockHeader, BlockTag, CellKind, Indent, InlineTag,
    TableCell, TableHeader, TableRow, VerticalAlign,
};
use nom::{
    branch::alt,


@@ 26,9 26,11 @@ pub fn table(input: &str) -> IResult<&str, BlockTag> {
    Ok((
        rest,
        BlockTag::Table {
            indent,
            align,
            attributes,
            header: BlockHeader {
                attributes,
                indent,
                align,
            },
            rows,
        },
    ))


@@ 56,9 58,11 @@ fn table_row(input: &str) -> IResult<&str, TableRow> {
    Ok((
        rest,
        TableRow {
            attributes,
            h_align,
            v_align,
            header: TableHeader {
                attributes,
                h_align,
                v_align,
            },
            cells,
        },
    ))


@@ 94,9 98,11 @@ fn table_cell(input: &str) -> IResult<&str, TableCell> {
            kind,
            col_span,
            row_span,
            attributes,
            h_align,
            v_align,
            header: TableHeader {
                attributes,
                h_align,
                v_align,
            },
            content,
        },
    ))


@@ 194,20 200,26 @@ mod tests {
            Ok((
                "",
                BlockTag::Table {
                    indent: None,
                    align: None,
                    attributes: None,
                    rows: vec![TableRow {
                    header: BlockHeader {
                        attributes: None,
                        h_align: None,
                        v_align: 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,
                            attributes: None,
                            h_align: None,
                            v_align: None,
                            header: TableHeader {
                                attributes: None,
                                h_align: None,
                                v_align: None,
                            },
                            content: InlineTag::Plaintext(String::from(
                                "hello"
                            ))

M src/render.rs => src/render.rs +269 -54
@@ 21,53 21,59 @@ impl fmt::Display for BlockTag {
        match self {
            BlockTag::Basic {
                kind,
                indent,
                align,
                attributes,
                header,
                content,
            } => match kind {
                BlockKind::Paragraph => write!(f, "<p>{}</p>", content),
                BlockKind::BlockQuote => write!(f, "<blockquote>{}</blockquote>", content),
                BlockKind::Header(n) => {
                    write!(f, "<h{}>{}</h{}>", n, content, n)
                BlockKind::Footnote(n) => {
                    write!(
                        f,
                        "<p{}><sup>{}</sup>{}<a href=\"#fnr{}\">&21A9;</a></p>",
                        header, n, content, n
                    )
                }
                BlockKind::Footnote(n) => write!(
                    f,
                    "<p id=\"fn{}\" class=\"footnote\"><sup>{}</sup>{}<a href=\"#fnr{}\">&21A9;</a></p>",
                    n, n, content, n
                ),
                _ => Err(fmt::Error)
                BlockKind::Paragraph
                | BlockKind::BlockQuote
                | BlockKind::Header(_) => {
                    write!(f, "<{}{}>{}</{}>", kind, header, content, kind)
                }
                _ => Err(fmt::Error),
            },
            BlockTag::Preformatted {
                kind,
                indent,
                align,
                attributes,
                header,
                content,
            } => match kind {
                BlockKind::Preformatted => write!(f, "<pre>{}</pre>", html_escape(content)),
                BlockKind::BlockCode => write!(f, "<pre><code>{}</code></pre>", html_escape(content)),
                _ => Err(fmt::Error) // other kinds should not be possible here
                BlockKind::Preformatted => {
                    write!(f, "<pre{}>{}</pre>", header, html_escape(content))
                }
                BlockKind::BlockCode => write!(
                    f,
                    "<pre{}><code>{}</code></pre>",
                    header,
                    html_escape(content)
                ),
                _ => Err(fmt::Error), // other kinds should not be possible here
            },
            BlockTag::List {
                indent,
                align,
                attributes,
                content,
            } => write!(f, "{}", content),
            BlockTag::Table {
                indent,
                align,
                attributes,
                rows,
            } => {
                write!(f, "<table>\n")?;
            BlockTag::List { header, content } => {
                let tag_name = match content.kind {
                    ListKind::Numeric => "ol",
                    ListKind::Bulleted => "ul",
                };
                write!(f, "<{}{}>\n", tag_name, header)?;
                for item in content.items.iter() {
                    write!(f, "{}\n", item)?;
                }
                write!(f, "</{}>", tag_name)?;
                Ok(())
            }
            BlockTag::Table { header, rows } => {
                write!(f, "<table {}>\n", header)?;
                for row in rows {
                    write!(f, "{}\n", row);
                    write!(f, "{}\n", row)?;
                }
                write!(f, "</table>");
                write!(f, "</table>")?;
                Ok(())
            },
            }
            BlockTag::NoTextile(s) => {
                write!(f, "{}", s)
            }


@@ 75,6 81,84 @@ impl fmt::Display for BlockTag {
    }
}

impl fmt::Display for BlockKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BlockKind::Paragraph => write!(f, "p"),
            BlockKind::Header(n) => write!(f, "h{}", n),
            BlockKind::BlockQuote => write!(f, "blockquote"),
            _ => Err(fmt::Error), // should never be called
        }
    }
}

impl fmt::Display for BlockHeader {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.indent.is_none() && self.align.is_none() {
            match &self.attributes {
                Some(attributes) => write!(f, "{}", attributes),
                None => Ok(()),
            }
        } else {
            let indent_style = match &self.indent {
                Some(indent) => format!("{}", indent),
                None => String::new(),
            };
            let align_style = match &self.align {
                Some(align) => format!("{}", align),
                None => String::new(),
            };
            match &self.attributes {
                Some(attributes) => {
                    if let Some(s) = &attributes.class {
                        write!(f, " class=\"{}\"", s)?;
                    }
                    if let Some(s) = &attributes.id {
                        write!(f, " id=\"{}\"", s)?;
                    }
                    if let Some(s) = &attributes.style {
                        write!(
                            f,
                            " style=\"{}{} {}\"",
                            indent_style, align_style, s
                        )?;
                    }
                    if let Some(s) = &attributes.language {
                        write!(f, " lang=\"{}\"", s)?;
                    }
                }
                None => {
                    write!(f, " style=\"{}{}\"", indent_style, align_style)?;
                }
            }
            Ok(())
        }
    }
}

impl fmt::Display for Indent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.left > 0 {
            write!(f, "padding-left: {}em; ", self.left)?;
        }
        if self.right > 0 {
            write!(f, "padding-left: {}em; ", self.right)?;
        }
        Ok(())
    }
}

impl fmt::Display for Align {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Align::Left => write!(f, "text-align: left;"),
            Align::Right => write!(f, "text-align: right;"),
            Align::Center => write!(f, "text-align: center;"),
            Align::Justify => write!(f, "text-align: justify;"),
        }
    }
}

impl fmt::Display for InlineTag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {


@@ 97,24 181,33 @@ impl fmt::Display for InlineTag {
                title,
                url,
                content,
            } => match title {
                Some(title) => write!(
                    f,
                    "<a href=\"{}\" title=\"{}\">{}</a>",
                    url, title, content
                ),
                None => write!(f, "<a href=\"{}\">{}</a>", url, content),
            },
            InlineTag::Image {
                attributes,
                align,
                url,
                alt,
            } => match alt {
            } => {
                let attrs = match attributes {
                    Some(attributes) => format!("{}", attributes),
                    None => format!(""),
                };
                match title {
                    Some(title) => write!(
                        f,
                        "<a{} href=\"{}\" title=\"{}\">{}</a>",
                        attrs, url, title, content
                    ),
                    None => write!(
                        f,
                        "<a{} href=\"{}\">{}</a>",
                        attrs, url, content
                    ),
                }
            }
            InlineTag::Image { header, url, alt } => match alt {
                Some(alt) => {
                    write!(f, "<img src=\"{}\" alt=\"{}\" />", url, alt)
                    write!(
                        f,
                        "<img{} src=\"{}\" alt=\"{}\" />",
                        header, url, alt
                    )
                }
                None => write!(f, "<img src=\"{}\" />", url),
                None => write!(f, "<img {} src=\"{}\" />", header, url),
            },
            InlineTag::Phrase {
                kind,


@@ 122,7 215,10 @@ impl fmt::Display for InlineTag {
                content,
            } => match kind {
                Some(k) => {
                    write!(f, "<{}>", k)?;
                    match attributes {
                        Some(attributes) => write!(f, "<{}{}>", k, attributes)?,
                        None => write!(f, "<{}>", k)?,
                    };
                    for phrase in content {
                        write!(f, "{}", phrase)?;
                    }


@@ 158,6 254,63 @@ impl fmt::Display for PhraseKind {
    }
}

impl fmt::Display for Attributes {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(s) = &self.class {
            write!(f, " class=\"{}\"", s)?;
        }
        if let Some(s) = &self.id {
            write!(f, " id=\"{}\"", s)?;
        }
        if let Some(s) = &self.style {
            write!(f, " style=\"{}\"", s)?;
        }
        if let Some(s) = &self.language {
            write!(f, " lang=\"{}\"", s)?;
        }
        Ok(())
    }
}

impl fmt::Display for ImageHeader {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.align {
            Some(align) => match &self.attributes {
                Some(attributes) => {
                    if let Some(s) = &attributes.class {
                        write!(f, " class=\"{}\"", s)?;
                    }
                    if let Some(s) = &attributes.id {
                        write!(f, " id=\"{}\"", s)?;
                    }
                    if let Some(s) = &attributes.style {
                        write!(f, " style=\"{} {}\"", align, s)?;
                    }
                    if let Some(s) = &attributes.language {
                        write!(f, " lang=\"{}\"", s)?;
                    }
                    Ok(())
                }
                None => write!(f, " style=\"{}\"", align),
            },
            None => match &self.attributes {
                Some(attributes) => write!(f, "{}", attributes),
                None => Ok(()),
            },
        }
    }
}

impl fmt::Display for ImageAlign {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ImageAlign::Left => write!(f, "float: left;"),
            ImageAlign::Right => write!(f, "float: right;"),
            ImageAlign::Center => write!(f, "display: block; margin: auto;"),
        }
    }
}

impl fmt::Display for List {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let tag_name = match self.kind {


@@ 186,7 339,7 @@ impl fmt::Display for ListItem {

impl fmt::Display for TableRow {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "<tr>\n")?;
        write!(f, "<tr{}>\n", self.header)?;
        for cell in &self.cells {
            write!(f, "{}\n", cell)?;
        }


@@ 201,7 354,69 @@ impl fmt::Display for TableCell {
            CellKind::Header => "th",
            CellKind::Data => "td",
        };
        write!(f, "<{}>{}</{}>", tag_name, self.content, tag_name)?;
        let colspan = match self.col_span {
            Some(n) => format!(" colspan={}", n),
            None => String::new(),
        };
        let rowspan = match self.row_span {
            Some(n) => format!(" rowspan={}", n),
            None => String::new(),
        };
        write!(
            f,
            "<{}{}{}{}>{}</{}>",
            tag_name, colspan, rowspan, self.header, self.content, tag_name
        )?;
        Ok(())
    }
}

impl fmt::Display for TableHeader {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.h_align.is_none() && self.v_align.is_none() {
            match &self.attributes {
                Some(attributes) => write!(f, "{}", attributes),
                None => Ok(()),
            }
        } else {
            let h_align = match &self.h_align {
                Some(align) => format!("{}", align),
                None => String::new(),
            };
            let v_align = match &self.v_align {
                Some(align) => format!("{}", align),
                None => String::new(),
            };
            match &self.attributes {
                Some(attributes) => {
                    if let Some(s) = &attributes.class {
                        write!(f, " class=\"{}\"", s)?;
                    }
                    if let Some(s) = &attributes.id {
                        write!(f, " id=\"{}\"", s)?;
                    }
                    if let Some(s) = &attributes.style {
                        write!(f, " style=\"{}{} {}\"", h_align, v_align, s)?;
                    }
                    if let Some(s) = &attributes.language {
                        write!(f, " lang=\"{}\"", s)?;
                    }
                }
                None => {
                    write!(f, " style=\"{}{}\"", h_align, v_align)?;
                }
            }
            Ok(())
        }
    }
}

impl fmt::Display for VerticalAlign {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            VerticalAlign::Top => write!(f, "vertical-align: top;"),
            VerticalAlign::Middle => write!(f, "vertical-align: middle;"),
            VerticalAlign::Bottom => write!(f, "vertical-align: bottom;"),
        }
    }
}

M src/structs.rs => src/structs.rs +25 -18
@@ 5,28 5,20 @@ pub struct Textile(pub Vec<BlockTag>);
pub enum BlockTag {
    Basic {
        kind: BlockKind,
        indent: Option<Indent>,
        align: Option<Align>,
        attributes: Option<Attributes>,
        header: BlockHeader,
        content: InlineTag,
    },
    Preformatted {
        kind: BlockKind,
        indent: Option<Indent>,
        align: Option<Align>,
        attributes: Option<Attributes>,
        header: BlockHeader,
        content: String,
    },
    List {
        indent: Option<Indent>,
        align: Option<Align>,
        attributes: Option<Attributes>,
        header: BlockHeader,
        content: List,
    },
    Table {
        indent: Option<Indent>,
        align: Option<Align>,
        attributes: Option<Attributes>,
        header: BlockHeader,
        rows: Vec<TableRow>,
    },
    NoTextile(String),


@@ 44,6 36,13 @@ pub enum BlockKind {
}

#[derive(Debug, PartialEq)]
pub struct BlockHeader {
    pub attributes: Option<Attributes>,
    pub indent: Option<Indent>,
    pub align: Option<Align>,
}

#[derive(Debug, PartialEq)]
pub struct List {
    pub kind: ListKind,
    pub items: Vec<ListItem>,


@@ 63,10 62,15 @@ pub enum ListKind {

#[derive(Debug, PartialEq)]
pub struct TableRow {
    pub header: TableHeader,
    pub cells: Vec<TableCell>,
}

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

#[derive(Debug, PartialEq)]


@@ 74,9 78,7 @@ 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 header: TableHeader,
    pub content: InlineTag,
}



@@ 123,8 125,7 @@ pub enum InlineTag {
    FootnoteRef(usize),
    LineBreak,
    Image {
        attributes: Option<Attributes>,
        align: Option<ImageAlign>,
        header: ImageHeader,
        url: String,
        alt: Option<String>,
    },


@@ 145,6 146,12 @@ pub enum InlineTag {
    },
}

#[derive(Debug, PartialEq)]
pub struct ImageHeader {
    pub attributes: Option<Attributes>,
    pub align: Option<ImageAlign>,
}

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