diff --git a/src/ast.rs b/src/ast.rs index 4c241ad..29ac211 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -31,6 +31,6 @@ pub enum Block { Code { content: String, lang: String }, Quote { inner: Box }, Paragraph { inner: Vec }, - UnorderedList { items: Vec }, - OrderedList { items: Vec }, + List { ordered: bool, items: Vec }, + Null, } diff --git a/src/generator/block.rs b/src/generator/block.rs index 0d33b4d..1289690 100644 --- a/src/generator/block.rs +++ b/src/generator/block.rs @@ -21,22 +21,130 @@ impl ToHtml for Block { } Block::Code { content, lang: _ } => format!("
{content}
"), Block::Quote { inner } => format!("
{}
", inner.to_html()), - Block::UnorderedList { items } => { - let mut html = "
    ".to_string(); + Block::List { ordered, items } => { + let tag = if *ordered { "ol" } else { "ul" }; + let mut html = format!("<{}>", tag); for item in items { html.push_str(&format!("
  • {}
  • ", &item.to_html())); } - html.push_str("
"); - html - } - Block::OrderedList { items } => { - let mut html = "
    ".to_string(); - for item in items { - html.push_str(&format!("
  1. {}
  2. ", &item.to_html())); - } - html.push_str("
"); + html.push_str(&format!("", tag)); html } + Block::Null => "".to_string(), } } } + +#[cfg(test)] +mod test { + use crate::{ast::*, generator::ToHtml}; + + #[test] + fn paragraph_to_html() { + let ast = Block::Paragraph { + inner: vec![Inline::Text { + content: "hello".to_string(), + }], + }; + + let html = ast.to_html(); + + assert_eq!(html, "

hello

".to_string()); + } + + #[test] + fn heading_to_html() { + let ast = Block::Heading { + inner: vec![Inline::Text { + content: "hello".to_string(), + }], + level: 2, + }; + + let html = ast.to_html(); + + assert_eq!(html, "

hello

".to_string()); + } + + #[test] + fn code_to_html() { + let ast = Block::Code { + content: "echo 'hello world!'".to_string(), + lang: "bash".to_string(), + }; + + let html = ast.to_html(); + + assert_eq!( + html, + "
echo 'hello world!'
".to_string() + ); + } + + #[test] + fn quote_to_html() { + let ast = Block::Quote { + inner: Box::new(Block::Paragraph { + inner: vec![Inline::Text { + content: "sun tzu".to_string(), + }], + }), + }; + + let html = ast.to_html(); + + assert_eq!(html, "

sun tzu

"); + } + + #[test] + fn ordered_list_to_html() { + let ast = Block::List { + ordered: true, + items: vec![ + Block::Paragraph { + inner: vec![Inline::Text { + content: "item 1".to_string(), + }], + }, + Block::Paragraph { + inner: vec![Inline::Text { + content: "item 2".to_string(), + }], + }, + ], + }; + + let html = ast.to_html(); + + assert_eq!( + html, + "
  1. item 1

  2. item 2

".to_string() + ); + } + + #[test] + fn unordered_list_to_html() { + let ast = Block::List { + ordered: false, + items: vec![ + Block::Paragraph { + inner: vec![Inline::Text { + content: "item 1".to_string(), + }], + }, + Block::Paragraph { + inner: vec![Inline::Text { + content: "item 2".to_string(), + }], + }, + ], + }; + + let html = ast.to_html(); + + assert_eq!( + html, + "
  • item 1

  • item 2

".to_string() + ); + } +} diff --git a/src/generator/file.rs b/src/generator/file.rs index a1f9699..621695a 100644 --- a/src/generator/file.rs +++ b/src/generator/file.rs @@ -7,7 +7,15 @@ use super::{GenerationError, ToHtml, TryToHtml}; impl TryToHtml for PathBuf { fn try_to_html(&self) -> Result { let content_md = fs::read_to_string(self)?; - let (_rem, generated) = blocks(&content_md)?; - Ok(generated.to_html()) + let (rem, generated) = blocks(&content_md)?; + + if !rem.is_empty() { + Err(GenerationError::Termination { + file: self.to_owned(), + remainder: rem.to_string(), + }) + } else { + Ok(generated.to_html()) + } } } diff --git a/src/generator/inline.rs b/src/generator/inline.rs index 6d85385..8ff701a 100644 --- a/src/generator/inline.rs +++ b/src/generator/inline.rs @@ -25,3 +25,74 @@ impl ToHtml for Inline { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn text_to_html() { + let ast = Inline::Text { + content: "hello".to_string(), + }; + + let html = ast.to_html(); + + assert_eq!(html, "hello".to_string()); + } + + #[test] + fn bold_to_html() { + let ast = Inline::Bold { + inner: vec![Inline::Text { + content: "hello".to_string(), + }], + }; + + let html = ast.to_html(); + + assert_eq!(html, "hello".to_string()); + } + + #[test] + fn italic_to_html() { + let ast = Inline::Italic { + inner: vec![Inline::Text { + content: "hello".to_string(), + }], + }; + + let html = ast.to_html(); + + assert_eq!(html, "hello".to_string()); + } + + #[test] + fn code_to_html() { + let ast = Inline::Code { + content: "echo 'hello'".to_string(), + }; + + let html = ast.to_html(); + + assert_eq!(html, "echo 'hello'".to_string()); + } + + #[test] + fn link_to_html() { + let ast = Inline::Link { + inner: vec![Inline::Text { + content: "my webpage".to_string(), + }], + href: crate::ast::Href("https://jlux.dev".to_string()), + }; + + let html = ast.to_html(); + + assert_eq!( + html, + "my webpage".to_string() + ); + //format!("{}", href, inner.to_html()) + } +} diff --git a/src/generator/mod.rs b/src/generator/mod.rs index 8760961..ee96621 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, path::PathBuf}; use crate::parser::MarkdownParseError; @@ -10,6 +10,7 @@ pub mod inline; pub enum GenerationError { IO(std::io::Error), Parse(nom::Err), + Termination { file: PathBuf, remainder: String }, } impl Display for GenerationError { @@ -18,8 +19,13 @@ impl Display for GenerationError { f, "Generation Error: {}", match self { - GenerationError::IO(e) => format!("IO: {e}"), - GenerationError::Parse(e) => format!("Parse: {e}"), + GenerationError::IO(e) => format!("IO error: {e}"), + GenerationError::Parse(e) => format!("Parse error: {e}"), + GenerationError::Termination { file, remainder } => format!( + "Termination error at `{}` before:\n{}", + file.display(), + remainder + ), } ) } diff --git a/src/parser/block.rs b/src/parser/block.rs index 433841a..eb5114e 100644 --- a/src/parser/block.rs +++ b/src/parser/block.rs @@ -15,21 +15,25 @@ pub fn blocks(input: &str) -> IResult<&str, Vec, MarkdownParseError> { } pub fn block(input: &str) -> IResult<&str, Block, MarkdownParseError> { - //alt((heading_block, code_block, quote_block, paragraph_block)).parse(input) terminated( alt(( heading_block, + ordered_list, + unordered_list, code_block, quote_block, paragraph_block, - ordered_list, - unordered_list, + empty_line, )), tag("\n"), ) .parse(input) } +fn empty_line(input: &str) -> IResult<&str, Block, MarkdownParseError> { + tag("").parse(input).map(|(rem, _)| (rem, Block::Null)) +} + fn paragraph_block(input: &str) -> IResult<&str, Block, MarkdownParseError> { (inline) .parse(input) @@ -84,14 +88,26 @@ fn quote_block(input: &str) -> IResult<&str, Block, MarkdownParseError> { fn unordered_list(input: &str) -> IResult<&str, Block, MarkdownParseError> { many1((tag("- "), block)).parse(input).map(|(rem, v)| { let items = v.into_iter().map(|(_, b)| b).collect(); - (rem, Block::UnorderedList { items }) + ( + rem, + Block::List { + ordered: false, + items, + }, + ) }) } fn ordered_list(input: &str) -> IResult<&str, Block, MarkdownParseError> { many1((tag("1. "), block)).parse(input).map(|(rem, v)| { let items = v.into_iter().map(|(_, b)| b).collect(); - (rem, Block::OrderedList { items }) + ( + rem, + Block::List { + ordered: true, + items, + }, + ) }) } @@ -265,7 +281,8 @@ Hello MD assert_eq!(rem, "\n"); assert_eq!( block, - Block::UnorderedList { + Block::List { + ordered: false, items: vec![ Block::Paragraph { inner: vec![Inline::Text { @@ -305,7 +322,8 @@ Hello MD assert_eq!(rem, "\n"); assert_eq!( block, - Block::OrderedList { + Block::List { + ordered: true, items: vec![ Block::Paragraph { inner: vec![Inline::Text { @@ -336,4 +354,73 @@ Hello MD } ); } + + #[test] + fn empty_block() { + let md = "\n"; + + let (rem, ast) = block(md).unwrap(); + + assert_eq!(rem, ""); + assert_eq!(ast, Block::Null); + } + + #[test] + fn complex_1() { + let md = " +# hello + +## second header + +blablabla + +1. hahaha +1. second + +"; + let (rem, ast) = blocks(md).unwrap(); + + assert_eq!(rem, ""); + assert_eq!( + ast, + vec![ + Block::Null, + Block::Heading { + inner: vec![Inline::Text { + content: "hello".to_string() + }], + level: 1 + }, + Block::Null, + Block::Heading { + inner: vec![Inline::Text { + content: "second header".to_string() + }], + level: 2 + }, + Block::Null, + Block::Paragraph { + inner: vec![Inline::Text { + content: "blablabla".to_string() + }] + }, + Block::Null, + Block::List { + ordered: true, + items: vec![ + Block::Paragraph { + inner: vec![Inline::Text { + content: "hahaha".to_string() + }] + }, + Block::Paragraph { + inner: vec![Inline::Text { + content: "second".to_string() + }] + }, + ] + }, + ] + ); + } } diff --git a/src/parser/inline.rs b/src/parser/inline.rs index 16c68b7..542e20a 100644 --- a/src/parser/inline.rs +++ b/src/parser/inline.rs @@ -3,7 +3,7 @@ use nom::{ Parser, branch::alt, bytes::complete::{is_not, tag}, - multi::many0, + multi::many1, sequence::delimited, }; @@ -12,7 +12,7 @@ use crate::ast::{Href, Inline}; use super::MarkdownParseError; pub fn inline(input: &str) -> IResult<&str, Vec, MarkdownParseError> { - many0(alt(( + many1(alt(( text_inline, bold_inline, italic_inline, @@ -184,7 +184,7 @@ mod test { #[test] fn normal_and_nested_bold() { - let md = "some **extra* bold* stuff"; + let md = "some *very *extra* bold* stuff"; let (rem, parsed) = inline(md).unwrap(); assert_eq!(rem, ""); @@ -194,7 +194,11 @@ mod test { Inline::Text { content: "some ".to_string() }, - Inline::Bold { inner: vec![] }, + Inline::Bold { + inner: vec![Inline::Text { + content: "very ".to_string() + }] + }, Inline::Text { content: "extra".to_string() },