added html generation
All checks were successful
Test the running changes / Test (push) Successful in 40s

This commit is contained in:
2025-12-15 01:19:23 +02:00
parent 30369cfdd3
commit 17185d4420
13 changed files with 392 additions and 133 deletions

123
Cargo.lock generated
View File

@@ -2,10 +2,100 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "marginal"
version = "0.0.1"
dependencies = [
"clap",
"nom",
]
@@ -23,3 +113,36 @@ checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]

View File

@@ -4,4 +4,5 @@ version = "0.0.1"
edition = "2024"
[dependencies]
nom = "8.0.0"
clap = "4.5.53"
nom = { version = "8.0.0", features = ["std"] }

View File

@@ -1,21 +1,4 @@
// Grammar rules:
//
// Markdown ::= Block Markdown | Block
//
// Block ::= (Heading | CodeBlock | Quote | Paragraph) "\n\n"
// Heading ::= "#{1,6}\s" Inline
// CodeBlock ::= "```.*\n" "(.*?\n)*" "```"
// Quote ::= ">" Block
// Paragraph ::= Inline
//
// Inline ::= InlineElem Inline | InlineElem
// InlineElem ::= Bold | Italic | Code | Link | Text
// Bold ::= "\*" Inline "\*"
// Italic ::= "_" Inline "_"
// Code ::= "`" "[.^`]*" "`"
// Link ::= "\[" Inline "\]\(" Href "\)"
// Href ::= "[.^\)]*"
// Text ::= "[.^`*_\[]*"
use std::fmt::Display;
#[derive(Debug, PartialEq)]
pub enum Inline {
@@ -36,34 +19,16 @@ impl Href {
}
}
/*
pub struct Markdown {
block: Block,
rest: Option<Box<Markdown>>,
impl Display for Href {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, PartialEq)]
pub enum Block {
Heading(HeadingBlock),
Code(CodeBlock),
Quote(QuoteBlock),
Paragraph(ParagraphBlock),
Heading { inner: Vec<Inline>, level: u8 },
Code { content: String, lang: String },
Quote { inner: Box<Block> },
Paragraph { inner: Vec<Inline> },
}
pub struct HeadingBlock {
level: u8,
content: Inline,
}
pub struct CodeBlock {
lang: String,
content: String,
}
pub struct QuoteBlock {
content: Box<Block>,
}
pub struct ParagraphBlock {
content: String,
}
*/

26
src/generator/block.rs Normal file
View File

@@ -0,0 +1,26 @@
use crate::ast::Block;
use super::ToHtml;
impl ToHtml for Vec<Block> {
fn to_html(&self) -> String {
let mut html = String::new();
for block in self.iter() {
html.push_str(&block.to_html())
}
html
}
}
impl ToHtml for Block {
fn to_html(&self) -> String {
match self {
Block::Paragraph { inner } => format!("<p>{}</p><br>", inner.to_html()),
Block::Heading { inner, level } => {
format!("<h{}>{}</h{}>", level, inner.to_html(), level)
}
Block::Code { content, lang: _ } => format!("<pre><code>{content}</code></pre>"),
Block::Quote { inner } => format!("<div class=\"quote\">{}</div>", inner.to_html()),
}
}
}

13
src/generator/file.rs Normal file
View File

@@ -0,0 +1,13 @@
use std::{fs, path::PathBuf};
use crate::parser::block::blocks;
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)?;
Ok(generated.to_html())
}
}

27
src/generator/inline.rs Normal file
View File

@@ -0,0 +1,27 @@
use crate::ast::Inline;
use super::ToHtml;
impl ToHtml for Vec<Inline> {
fn to_html(&self) -> String {
let mut html = String::new();
for inline in self.iter() {
html.push_str(&inline.to_html())
}
html
}
}
impl ToHtml for Inline {
fn to_html(&self) -> String {
match self {
Inline::Text { content } => content.to_owned(),
Inline::Bold { inner } => format!("<b>{}</b>", inner.to_html()),
Inline::Italic { inner } => format!("<i>{}</i>", inner.to_html()),
Inline::Code { content } => format!("<code>{content}</code>"),
Inline::Link { inner, href } => {
format!("<a href=\"{}\">{}</a>", href, inner.to_html())
}
}
}
}

56
src/generator/mod.rs Normal file
View File

@@ -0,0 +1,56 @@
use std::fmt::Display;
use crate::parser::MarkdownParseError;
pub mod block;
pub mod file;
pub mod inline;
#[derive(Debug)]
pub enum GenerationError {
IO(std::io::Error),
Parse(nom::Err<MarkdownParseError>),
}
impl Display for GenerationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Generation Error: {}",
match self {
GenerationError::IO(e) => format!("IO: {e}"),
GenerationError::Parse(e) => format!("Parse: {e}"),
}
)
}
}
impl From<std::io::Error> for GenerationError {
fn from(value: std::io::Error) -> Self {
Self::IO(value)
}
}
impl From<nom::Err<MarkdownParseError>> for GenerationError {
fn from(value: nom::Err<MarkdownParseError>) -> Self {
Self::Parse(value)
}
}
impl std::error::Error for GenerationError {}
pub trait ToHtml {
fn to_html(&self) -> String;
}
pub trait TryToHtml {
fn try_to_html(&self) -> Result<String, GenerationError>;
}
/*
impl<T: ToHtml> TryToHtml for T {
fn try_to_html(&self) -> Result<String, GenerationError<impl std::error::Error>> {
Ok::<String, GenerationError<nom::Err<MarkdownParseError>>>(self.to_html())
}
}
*/

View File

@@ -1,7 +1,14 @@
use crate::generator::TryToHtml;
use std::path::PathBuf;
mod ast;
mod generator;
mod parser;
fn main() {
let infile = PathBuf::from("test.md");
let html = infile.try_to_html().unwrap();
println!("{}", html);
}

View File

@@ -1,41 +1,33 @@
#![allow(dead_code)]
use crate::{ast::Inline, parser::inline::inline};
use crate::{ast::Block, parser::inline::inline};
use nom::{
IResult, Parser,
bytes::complete::{tag, take_until},
multi::{many_m_n, many1, many0},
sequence::{terminated, delimited},
branch::alt,
bytes::complete::{tag, take_until},
multi::{many_m_n, many0, many1},
sequence::{delimited, terminated},
};
#[derive(Debug, PartialEq)]
pub enum Block {
Heading { inner: Vec<Inline>, level: u8 },
Code { content: String, lang: String },
Quote { inner: Box<Block> },
Paragraph { inner: Vec<Inline> },
}
use super::MarkdownParseError;
pub fn blocks(input: &str) -> IResult<&str, Vec<Block>> {
pub fn blocks(input: &str) -> IResult<&str, Vec<Block>, MarkdownParseError> {
many0(block).parse(input)
}
pub fn block(input: &str) -> IResult<&str, Block> {
pub fn block(input: &str) -> IResult<&str, Block, MarkdownParseError> {
terminated(
alt((heading_block, code_block, quote_block, paragraph_block)),
tag("\n"),
).parse(input)
)
.parse(input)
}
fn paragraph_block(input: &str) -> IResult<&str, Block> {
fn paragraph_block(input: &str) -> IResult<&str, Block, MarkdownParseError> {
(inline)
.parse(input)
.map(|(rem, inl)| (rem, Block::Paragraph { inner: inl }))
}
fn heading_block(input: &str) -> IResult<&str, Block> {
fn heading_block(input: &str) -> IResult<&str, Block, MarkdownParseError> {
(many_m_n(1, 6, tag("#")), many1(tag(" ")), inline)
.parse(input)
.map(|(rem, (head, _, title))| {
@@ -49,7 +41,7 @@ fn heading_block(input: &str) -> IResult<&str, Block> {
})
}
fn code_block(input: &str) -> IResult<&str, Block> {
fn code_block(input: &str) -> IResult<&str, Block, MarkdownParseError> {
delimited(
tag("```"),
(take_until("\n"), tag("\n"), take_until("```\n")),
@@ -67,15 +59,17 @@ fn code_block(input: &str) -> IResult<&str, Block> {
})
}
fn quote_block(input: &str) -> IResult<&str, Block> {
(tag(">"), many0(tag(" ")), block).parse(input).map(|(rem, (_, _, inner))| {
(
rem,
Block::Quote {
inner: Box::new(inner),
},
)
})
fn quote_block(input: &str) -> IResult<&str, Block, MarkdownParseError> {
(tag(">"), many0(tag(" ")), block)
.parse(input)
.map(|(rem, (_, _, inner))| {
(
rem,
Block::Quote {
inner: Box::new(inner),
},
)
})
}
//|-------------------------------------------------------------------------------|
@@ -206,9 +200,9 @@ echo hello
block,
Block::Quote {
inner: Box::new(Block::Paragraph {
inner: vec![
Inline::Text { content: "sun tzu".to_string() }
]
inner: vec![Inline::Text {
content: "sun tzu".to_string()
}]
})
}
);
@@ -216,8 +210,7 @@ echo hello
#[test]
fn heading_and_paragraph() {
let md =
"## Heading
let md = "## Heading
Hello MD
";
let (rem, blocks) = blocks(md).unwrap();
@@ -227,15 +220,15 @@ Hello MD
blocks,
vec![
Block::Heading {
inner: vec![
Inline::Text { content: "Heading".to_string() }
],
inner: vec![Inline::Text {
content: "Heading".to_string()
}],
level: 2
},
Block::Paragraph {
inner: vec![
Inline::Text { content: "Hello MD".to_string() }
]
inner: vec![Inline::Text {
content: "Hello MD".to_string()
}]
}
]
);

View File

@@ -1,23 +1,28 @@
#![allow(dead_code)]
use nom::IResult;
use nom::{
Parser,
branch::alt,
bytes::complete::{is_not, tag},
error::context,
multi::many0,
sequence::delimited,
};
use crate::ast::{Inline, Href};
use crate::ast::{Href, Inline};
use super::MarkdownParseError;
pub fn inline(input: &str) -> IResult<&str, Vec<Inline>> {
many0(alt((text_inline, bold_inline, italic_inline, code_inline, link_inline))).parse(input)
pub fn inline(input: &str) -> IResult<&str, Vec<Inline>, MarkdownParseError> {
many0(alt((
text_inline,
bold_inline,
italic_inline,
code_inline,
link_inline,
)))
.parse(input)
}
fn text_inline(input: &str) -> IResult<&str, Inline> {
fn text_inline(input: &str) -> IResult<&str, Inline, MarkdownParseError> {
is_not("*_`[]\n").parse(input).map(|(rem, con)| {
(
rem,
@@ -28,54 +33,48 @@ fn text_inline(input: &str) -> IResult<&str, Inline> {
})
}
fn bold_inline(input: &str) -> IResult<&str, Inline> {
delimited(
context("opening bold tag", tag("*")),
inline,
context("closing bold tag", tag("*")),
)
fn bold_inline(input: &str) -> IResult<&str, Inline, MarkdownParseError> {
delimited(tag("*"), inline, tag("*"))
.parse(input)
.map(|(rem, inl)| (rem, Inline::Bold { inner: inl }))
}
fn italic_inline(input: &str) -> IResult<&str, Inline> {
delimited(
context("opening italics tag", tag("_")),
inline,
context("closing italics tag", tag("_")),
)
fn italic_inline(input: &str) -> IResult<&str, Inline, MarkdownParseError> {
delimited(tag("_"), inline, tag("_"))
.parse(input)
.map(|(rem, inl)| (rem, Inline::Italic { inner: inl }))
}
fn code_inline(input: &str) -> IResult<&str, Inline> {
delimited(
context("opening code tag", tag("`")),
context("inline code", is_not("`\n")),
context("closing code tag", tag("`")),
)
fn code_inline(input: &str) -> IResult<&str, Inline, MarkdownParseError> {
delimited(tag("`"), is_not("`\n"), tag("`"))
.parse(input)
.map(|(rem, inl)| (rem, Inline::Code { content: inl.to_string() }))
.map(|(rem, inl)| {
(
rem,
Inline::Code {
content: inl.to_string(),
},
)
})
}
fn link_inline(input: &str) -> IResult<&str, Inline> {
fn link_inline(input: &str) -> IResult<&str, Inline, MarkdownParseError> {
(
delimited(
context("opening link tag", tag("[")),
context("link name", inline),
context("closing link tag", tag("]")),
),
delimited(
context("opening href tag", tag("(")),
context("link href", is_not(")\n")),
context("closing href tag", tag(")")),
)
delimited(tag("["), inline, tag("]")),
delimited(tag("("), is_not(")\n"), tag(")")),
)
.parse(input)
.map(|(rem, (name, href))| (rem, Inline::Link { inner: name, href: Href::new(href) }))
.map(|(rem, (name, href))| {
(
rem,
Inline::Link {
inner: name,
href: Href::new(href),
},
)
})
}
//|-------------------------------------------------------------------------------|
//| TESTS TESTS TESTS TESTS TESTS TESTS TESTS TESTS TESTS TESTS TESTS TESTS TESTS |
//|-------------------------------------------------------------------------------|
@@ -254,9 +253,15 @@ mod test {
assert_eq!(
parsed,
vec![
Inline::Text { content: "take some ".to_string() },
Inline::Code { content: "code and *bold* and _italics_".to_string() },
Inline::Text { content: " lmao".to_string() }
Inline::Text {
content: "take some ".to_string()
},
Inline::Code {
content: "code and *bold* and _italics_".to_string()
},
Inline::Text {
content: " lmao".to_string()
}
]
);
}
@@ -273,7 +278,9 @@ mod test {
parsed,
Inline::Link {
inner: vec![
Inline::Text { content: "this link is ".to_string() },
Inline::Text {
content: "this link is ".to_string()
},
Inline::Bold {
inner: vec![Inline::Text {
content: "important".to_string()

View File

@@ -1,3 +1,32 @@
pub mod inline;
pub mod block;
use std::fmt::{Debug, Display};
pub mod block;
pub mod inline;
#[derive(Debug)]
pub struct MarkdownParseError {
kind: nom::error::ErrorKind,
message: String,
}
impl Display for MarkdownParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}, {}", self.kind, self.message)
}
}
impl nom::error::ParseError<&str> for MarkdownParseError {
fn from_error_kind(input: &str, kind: nom::error::ErrorKind) -> Self {
Self {
kind,
message: format!("at: {}", input),
}
}
fn append(input: &str, kind: nom::error::ErrorKind, other: Self) -> Self {
Self {
kind,
message: format!("{}\nat: {}", other, input),
}
}
}

0
test.html Normal file
View File

12
test.md Normal file
View File

@@ -0,0 +1,12 @@
## This is a testing markdown file
```rust
fn main() {
println!("Hello world!");
}
```
We'll see if *this* works...
>A wise man once said...