first commit

This commit is contained in:
2025-11-09 01:06:25 +02:00
commit dbaa2feb48
28 changed files with 1551 additions and 0 deletions

1
cracked_md/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

7
cracked_md/Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[package]
name = "cracked_md"
version = "0.1.0"
edition = "2024"
[dependencies]
fstools = { path = "../fstools" }

194
cracked_md/src/ast.rs Normal file
View File

@@ -0,0 +1,194 @@
use crate::to_html::ToHtml;
#[derive(Debug, Clone, PartialEq)]
pub struct Document {
pub blocks: Vec<Block>,
}
impl ToHtml for Document {
fn to_html(self) -> String {
format!(
"<!doctype html><html lang=en><head></head><body>{}</body></html>",
self.blocks.to_html()
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Block {
Paragraph(Vec<Inline>),
Heading {
level: u8,
content: Vec<Inline>,
},
Code {
language: Option<String>,
content: String,
},
List(Vec<ListItem>),
Quote(Vec<Block>),
}
impl ToHtml for Block {
fn to_html(self) -> String {
match self {
Self::Paragraph(content) => format!("<p>{}</p>", content.to_html()),
Self::Heading { level, content } => {
format!("<h{}>{}</h{}>", level, content.to_html(), level)
}
Self::Code {
language: _,
content,
} => {
format!("<pre><code>{}</code></pre>", content)
}
_ => todo!(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ListItem {
pub blocks: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Inline {
Text(String),
Bold(Vec<Inline>),
Italic(Vec<Inline>),
Code(String),
Link { text: Vec<Inline>, href: String },
}
impl ToHtml for Inline {
fn to_html(self) -> String {
match self {
Self::Text(s) => s,
Self::Bold(content) => format!("<b>{}</b>", content.to_html()),
Self::Italic(content) => format!("<i>{}</i>", content.to_html()),
Self::Code(s) => format!("<code>{}</code>", s),
Self::Link { text, href } => format!("<a href=\"{}\">{}</a>", href, text.to_html()),
}
}
}
impl<T> ToHtml for Vec<T>
where
T: ToHtml,
{
fn to_html(self) -> String {
let mut rendered = String::new();
for i in self {
rendered.push_str(&i.to_html());
}
rendered
}
}
// --------------------
// TESTS
// --------------------
#[cfg(test)]
mod unit_test {
use super::*;
#[test]
fn single_header() {
let ast = Document {
blocks: vec![Block::Heading {
level: 1,
content: vec![Inline::Text("Heading 1".to_string())],
}],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1>Heading 1</h1></body></html>"
);
}
#[test]
fn inline_bold_header() {
let ast = Document {
blocks: vec![Block::Heading {
level: 1,
content: vec![
Inline::Bold(vec![Inline::Text("Bold".to_string())]),
Inline::Text(" heading 1".to_string()),
],
}],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> heading 1</h1></body></html>"
);
}
#[test]
fn headings_and_paragraph_nested_code() {
let ast = Document {
blocks: vec![
Block::Heading {
level: 1,
content: vec![
Inline::Bold(vec![Inline::Text("Bold".to_string())]),
Inline::Text(" heading 1".to_string()),
],
},
Block::Heading {
level: 2,
content: vec![Inline::Text("Heading 2".to_string())],
},
Block::Paragraph(vec![
Inline::Text("run ".to_string()),
Inline::Code("sudo rm -rf /".to_string()),
Inline::Text(" on your computer".to_string()),
]),
],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> heading 1</h1><h2>Heading 2</h2><p>run <code>sudo rm -rf /</code> on your computer</p></body></html>"
);
}
}
#[cfg(test)]
mod convert_md_to_html_test {
use crate::parser::parse;
use crate::to_html::ToHtml;
#[test]
fn single_header() {
let md = "# Header 1";
let html = parse(md).to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1>Header 1</h1></body></html>"
);
}
#[test]
fn nested_bold_headers_and_nested_code_paragraph() {
let md = "# *Bold* header 1\n## Header 2\nrun `sudo rm -rf /` on your computer";
let html = parse(md).to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> header 1</h1><h2>Header 2</h2><p>run <code>sudo rm -rf /</code> on your computer</p></body></html>"
);
}
}

76
cracked_md/src/lib.rs Normal file
View File

@@ -0,0 +1,76 @@
#![deny(dead_code, unused_imports)]
use fstools::crawl_fs;
use parser::parse;
use std::{
fmt::Display,
fs::{self, File},
io::Write,
path::PathBuf,
};
use to_html::ToHtml;
pub mod ast;
pub mod parser;
pub mod to_html;
#[derive(Debug)]
pub enum Error {
OutDirIsNotEmpty,
OutDirFileDeleteNotAllowed,
OutDirDirectoryInPlaceOfFile,
FileRead,
DirRead,
FileWrite,
FileCreate,
DirCreate,
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", &self)
}
}
impl std::error::Error for Error {}
type Result<T> = std::result::Result<T, crate::Error>;
pub fn generate(indir: &PathBuf, outdir: &PathBuf, force: bool) -> Result<()> {
let files = crawl_fs(indir);
for path in files {
let fullpath = indir.as_path().join(&path);
// read and parse md file
let content = fs::read_to_string(&fullpath).map_err(|_e| Error::FileRead)?;
let html = parse(&content).to_html();
// write html data to file
let mut newpath = outdir.to_owned();
newpath.push(path);
newpath.set_extension("html");
// check if path exists
if newpath.exists() {
// remove if is file and if force, otherwise error
if newpath.is_file() && force {
fs::remove_file(&newpath).map_err(|_e| Error::OutDirFileDeleteNotAllowed)?;
} else {
Err(Error::OutDirDirectoryInPlaceOfFile)?;
}
}
//println!("About to write file '{}'", newpath.display());
let parent = newpath.parent().ok_or(Error::DirCreate)?;
fs::create_dir_all(parent).map_err(|_e| Error::DirCreate)?;
let mut newfile = File::create_new(newpath).map_err(|_e| Error::FileCreate)?;
newfile
.write(html.as_bytes())
.map_err(|_e| Error::FileWrite)?;
}
Ok(())
}

147
cracked_md/src/parser.rs Normal file
View File

@@ -0,0 +1,147 @@
mod block;
mod inline;
use block::parse_blocks;
use crate::ast::Document;
pub fn parse(s: &str) -> Document {
Document {
blocks: parse_blocks(s),
}
}
#[cfg(test)]
mod test {
use crate::ast::*;
use crate::parser::parse;
#[test]
fn only_paragraph() {
let md = "testing paragraph";
let doc = parse(md);
assert_eq!(
doc,
Document {
blocks: vec![Block::Paragraph(vec![Inline::Text(
"testing paragraph".to_string()
)])]
}
);
}
#[test]
fn different_headers() {
let md = "# Header 1\n## Header 2";
let doc = parse(md);
assert_eq!(
doc,
Document {
blocks: vec![
Block::Heading {
level: 1,
content: vec![Inline::Text("Header 1".to_string())]
},
Block::Heading {
level: 2,
content: vec![Inline::Text("Header 2".to_string())]
},
]
}
);
}
#[test]
fn inline_bold_and_italics() {
let md = "some *bold* and _italic_ text";
let doc = parse(md);
assert_eq!(
doc,
Document {
blocks: vec![Block::Paragraph(vec![
Inline::Text("some ".to_string()),
Inline::Bold(vec![Inline::Text("bold".to_string())]),
Inline::Text(" and ".to_string()),
Inline::Italic(vec![Inline::Text("italic".to_string())]),
Inline::Text(" text".to_string()),
])]
}
);
}
#[test]
fn inline_code() {
let md = "run command `sudo rm -rf /`";
let doc = parse(md);
assert_eq!(
doc,
Document {
blocks: vec![Block::Paragraph(vec![
Inline::Text("run command ".to_string()),
Inline::Code("sudo rm -rf /".to_string()),
])]
}
);
}
#[test]
fn bold_header() {
let md = "# Header is *bold*";
let doc = parse(md);
assert_eq!(
doc,
Document {
blocks: vec![Block::Heading {
level: 1,
content: vec![
Inline::Text("Header is ".to_string()),
Inline::Bold(vec![Inline::Text("bold".to_string())])
]
}]
}
);
}
#[test]
fn anonymous_code_block() {
let md = "```\necho hello\n```";
let doc = parse(md);
assert_eq!(
doc,
Document {
blocks: vec![Block::Code {
language: None,
content: "echo hello\n".to_string()
}]
}
);
}
#[test]
fn rust_code_block() {
let md = "```rust\nfn main() {\n\tprintln!(\"Hello world!\");\n}\n```";
let doc = parse(md);
assert_eq!(
doc,
Document {
blocks: vec![Block::Code {
language: Some("rust".to_string()),
content: "fn main() {\n\tprintln!(\"Hello world!\");\n}\n".to_string()
}]
}
);
}
}

View File

@@ -0,0 +1,45 @@
use crate::ast::Block;
use super::inline::parse_inlines;
pub fn parse_blocks(input: &str) -> Vec<Block> {
let mut blocks = Vec::new();
let mut lines = input.lines().peekable();
while let Some(line) = lines.next() {
if line.starts_with("#") {
let level = line.chars().take_while(|&c| c == '#').count() as u8;
let text = line[level as usize..].trim();
blocks.push(Block::Heading {
level,
content: parse_inlines(text),
});
} else if let Some(quote_body) = line.strip_prefix(">") {
let quote_blocks = parse_blocks(quote_body);
blocks.push(Block::Quote(quote_blocks));
} else if line.starts_with("```") {
let lang_line = line.strip_prefix("```").unwrap().to_string();
let lang = if lang_line.is_empty() {
None
} else {
Some(lang_line)
};
let mut code = String::new();
while lines.peek().is_some() && !lines.peek().unwrap().starts_with("```") {
code.push_str(&format!("{}\n", lines.next().unwrap()));
}
lines.next();
blocks.push(Block::Code {
language: lang,
content: code,
});
} else if line.trim().is_empty() {
continue;
} else {
blocks.push(Block::Paragraph(parse_inlines(line)));
}
}
blocks
}

View File

@@ -0,0 +1,61 @@
use crate::ast::Inline;
pub fn parse_inlines(input: &str) -> Vec<Inline> {
let mut inlines = Vec::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
match c {
'*' => {
let inner = collect_until(&mut chars, '*');
inlines.push(Inline::Bold(parse_inlines(&inner)));
}
'_' => {
let inner = collect_until(&mut chars, '_');
inlines.push(Inline::Italic(parse_inlines(&inner)));
}
'`' => {
let code = collect_until(&mut chars, '`');
inlines.push(Inline::Code(code));
}
'[' => {
let text = collect_until(&mut chars, ']');
if chars.next() == Some('(') {
let href = collect_until(&mut chars, ')');
inlines.push(Inline::Link {
text: parse_inlines(&text),
href,
});
}
}
_ => {
let mut text = String::new();
text.push(c);
while let Some(&nc) = chars.peek() {
if matches!(nc, '*' | '_' | '`' | '[') {
break;
}
text.push(chars.next().unwrap());
}
inlines.push(Inline::Text(text));
}
}
}
inlines
}
fn collect_until<I: Iterator<Item = char>>(
chars: &mut std::iter::Peekable<I>,
end: char,
) -> String {
let mut s = String::new();
while let Some(&c) = chars.peek() {
if c == end {
chars.next();
break;
}
s.push(chars.next().unwrap());
}
s
}

View File

@@ -0,0 +1,4 @@
pub trait ToHtml {
fn to_html(self) -> String;
}