first commit
This commit is contained in:
1
cracked_md/.gitignore
vendored
Normal file
1
cracked_md/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
7
cracked_md/Cargo.toml
Normal file
7
cracked_md/Cargo.toml
Normal 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
194
cracked_md/src/ast.rs
Normal 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
76
cracked_md/src/lib.rs
Normal 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
147
cracked_md/src/parser.rs
Normal 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()
|
||||
}]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
45
cracked_md/src/parser/block.rs
Normal file
45
cracked_md/src/parser/block.rs
Normal 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
|
||||
}
|
||||
61
cracked_md/src/parser/inline.rs
Normal file
61
cracked_md/src/parser/inline.rs
Normal 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
|
||||
}
|
||||
4
cracked_md/src/to_html.rs
Normal file
4
cracked_md/src/to_html.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub trait ToHtml {
|
||||
fn to_html(self) -> String;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user