Compare commits
4 Commits
b7ca4ac6e3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| af63f069ab | |||
| 99b1205cec | |||
| 47cb8d2cc2 | |||
| e04beb51ee |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "marginal"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2024"
|
||||
publish = ["gitea"]
|
||||
|
||||
|
||||
@@ -31,6 +31,6 @@ pub enum Block {
|
||||
Code { content: String, lang: String },
|
||||
Quote { inner: Box<Block> },
|
||||
Paragraph { inner: Vec<Inline> },
|
||||
UnorderedList { items: Vec<Block> },
|
||||
OrderedList { items: Vec<Block> },
|
||||
List { ordered: bool, items: Vec<Block> },
|
||||
Null,
|
||||
}
|
||||
|
||||
@@ -21,22 +21,130 @@ impl ToHtml for Block {
|
||||
}
|
||||
Block::Code { content, lang: _ } => format!("<pre><code>{content}</code></pre>"),
|
||||
Block::Quote { inner } => format!("<div class=\"quote\">{}</div>", inner.to_html()),
|
||||
Block::UnorderedList { items } => {
|
||||
let mut html = "<ul>".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!("<li>{}</li>", &item.to_html()));
|
||||
}
|
||||
html.push_str("</ul>");
|
||||
html
|
||||
}
|
||||
Block::OrderedList { items } => {
|
||||
let mut html = "<ol>".to_string();
|
||||
for item in items {
|
||||
html.push_str(&format!("<li>{}</li>", &item.to_html()));
|
||||
}
|
||||
html.push_str("</ol>");
|
||||
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, "<p>hello</p>".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, "<h2>hello</h2>".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,
|
||||
"<pre><code>echo 'hello world!'</code></pre>".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, "<div class=\"quote\"><p>sun tzu</p></div>");
|
||||
}
|
||||
|
||||
#[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,
|
||||
"<ol><li><p>item 1</p></li><li><p>item 2</p></li></ol>".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,
|
||||
"<ul><li><p>item 1</p></li><li><p>item 2</p></li></ul>".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,15 @@ use super::{GenerationError, ToHtml, TryToHtml};
|
||||
impl TryToHtml for PathBuf {
|
||||
fn try_to_html(&self) -> Result<String, GenerationError> {
|
||||
let content_md = fs::read_to_string(self)?;
|
||||
let (_rem, generated) = blocks(&content_md)?;
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "<b>hello</b>".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, "<i>hello</i>".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_to_html() {
|
||||
let ast = Inline::Code {
|
||||
content: "echo 'hello'".to_string(),
|
||||
};
|
||||
|
||||
let html = ast.to_html();
|
||||
|
||||
assert_eq!(html, "<code>echo 'hello'</code>".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,
|
||||
"<a href=\"https://jlux.dev\">my webpage</a>".to_string()
|
||||
);
|
||||
//format!("<a href=\"{}\">{}</a>", href, inner.to_html())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MarkdownParseError>),
|
||||
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
|
||||
),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,21 +15,25 @@ pub fn blocks(input: &str) -> IResult<&str, Vec<Block>, 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()
|
||||
}]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Inline>, 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()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user