From 5bcc37f32c36bc2ee63400ef8d1de0cdd2390434 Mon Sep 17 00:00:00 2001 From: Araozu Date: Sun, 25 Jun 2023 18:23:17 -0500 Subject: [PATCH] Use config file instead of params. --- .gitignore | 1 + Cargo.lock | 439 +++++++++++++++++++++++++++++++ Cargo.toml | 14 + md-docs.md | 16 ++ src/generator/code.rs | 88 +++++++ src/generator/emphasis.rs | 17 ++ src/generator/heading.rs | 38 +++ src/generator/highlighter/mod.rs | 94 +++++++ src/generator/inline_code.rs | 26 ++ src/generator/list.rs | 47 ++++ src/generator/mod.rs | 59 +++++ src/generator/paragraph.rs | 21 ++ src/generator/root.rs | 25 ++ src/generator/strong.rs | 17 ++ src/generator/text.rs | 13 + src/main.rs | 103 ++++++++ src/pages/md_compiler.rs | 67 +++++ src/pages/mod.rs | 174 ++++++++++++ src/processor.rs | 97 +++++++ src/sidebar/mod.rs | 90 +++++++ src/utils.rs | 66 +++++ 21 files changed, 1512 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 md-docs.md create mode 100644 src/generator/code.rs create mode 100644 src/generator/emphasis.rs create mode 100644 src/generator/heading.rs create mode 100644 src/generator/highlighter/mod.rs create mode 100644 src/generator/inline_code.rs create mode 100644 src/generator/list.rs create mode 100644 src/generator/mod.rs create mode 100644 src/generator/paragraph.rs create mode 100644 src/generator/root.rs create mode 100644 src/generator/strong.rs create mode 100644 src/generator/text.rs create mode 100644 src/main.rs create mode 100644 src/pages/md_compiler.rs create mode 100644 src/pages/mod.rs create mode 100644 src/processor.rs create mode 100644 src/sidebar/mod.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..64db968 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,439 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "clap" +version = "4.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9394150f5b4273a1763355bd1c2ec54cc5a2593f790587bcd6b2c947cfa9211" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "markdown" +version = "1.0.0-alpha.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1bd98c3b68451b0390a289c58c856adb4e2b50cc40507ce2a105d5b00eafc80" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "md-docs" +version = "0.1.0" +dependencies = [ + "clap", + "markdown", + "toml", + "yaml-rust", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-id" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70b6494226b36008c8366c288d77190b3fad2eb4c10533139c1c1f461127f1a" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +dependencies = [ + "memchr", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..424bc6a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "md-docs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# misti = { path = "../compiler"} +clap = { version = "4.2.0", features = ["derive"] } +markdown = "1.0.0-alpha.7" +toml = "0.7.3" +yaml-rust = "0.4.5" + diff --git a/md-docs.md b/md-docs.md new file mode 100644 index 0000000..bd62cb5 --- /dev/null +++ b/md-docs.md @@ -0,0 +1,16 @@ +# MD-DOCS + +All configuration is done via the `md-docs.config.yaml` file. + +## md-docs.config.yaml + +Values: + +```yaml +{ + String input # Path to the input folder + String output # Path to the output folder + + +} +``` diff --git a/src/generator/code.rs b/src/generator/code.rs new file mode 100644 index 0000000..bfe6bc3 --- /dev/null +++ b/src/generator/code.rs @@ -0,0 +1,88 @@ +use markdown::mdast::Code; + +// use super::highlighter::highlight; + +use super::Printable; + +impl Printable for Code { + fn to_html(&self) -> String { + let code = &self.value; // highlight(&self.value); + + if let Some(lang) = &self.lang { + if lang == "nav" { + return generate_nav_html(&self.value); + } + + format!("
{}
", lang, code) + } else { + format!("
{}
", code) + } + } + + fn get_text(&self) -> String { + panic!("Code cannot return its raw text") + } +} + +fn generate_nav_html(data: &String) -> String { + use toml::{Table, Value}; + + let table = data.parse::().unwrap(); + + let previous = match table.get("previous") { + Some(Value::Table(t)) => match (t.get("href"), t.get("title")) { + (Some(Value::String(href)), Some(Value::String(title))) => { + format!( + " + + Previous +
+ {} +
+ ", + href, title + ) + } + _ => panic!("TOML error: `previous` doesn't have a href and title string."), + }, + Some(_) => panic!("TOML error: `previous` is not a table."), + _ => String::from("
"), + }; + + let next = match table.get("next") { + Some(Value::Table(t)) => match (t.get("href"), t.get("title")) { + (Some(Value::String(href)), Some(Value::String(title))) => { + format!( + " + + Next +
+ {} +
+ ", + href, title + ) + } + _ => panic!("TOML error: `next` doesn't have a href and title string."), + }, + Some(_) => panic!("TOML error: `next` is not a table."), + _ => String::from("
"), + }; + + format!( + "
{}{}
", + previous, next + ) +} diff --git a/src/generator/emphasis.rs b/src/generator/emphasis.rs new file mode 100644 index 0000000..53c4e9f --- /dev/null +++ b/src/generator/emphasis.rs @@ -0,0 +1,17 @@ +use markdown::mdast::Emphasis; + +use crate::utils; + +use super::Printable; + +impl Printable for Emphasis { + fn to_html(&self) -> String { + let html = utils::collect_children_html(&self.children); + + format!("{}", html) + } + + fn get_text(&self) -> String { + utils::collect_children_text(&self.children) + } +} diff --git a/src/generator/heading.rs b/src/generator/heading.rs new file mode 100644 index 0000000..44e7409 --- /dev/null +++ b/src/generator/heading.rs @@ -0,0 +1,38 @@ +use markdown::mdast::Heading; + +use crate::utils; + +use super::Printable; + +impl Printable for Heading { + fn to_html(&self) -> String { + let mut result = Vec::::new(); + + for node in &self.children { + result.push(node.to_html()) + } + + let text: String = result.into_iter().collect(); + + if self.depth < 4 { + let html_fragment_text = utils::to_html_fragment(&self.get_text()); + + format!( + "{}", + self.depth, html_fragment_text, html_fragment_text, text, self.depth + ) + } else { + format!("{}", self.depth, text, self.depth) + } + } + + fn get_text(&self) -> String { + let mut result = Vec::::new(); + + for node in &self.children { + result.push(node.get_text()) + } + + result.join("-") + } +} diff --git a/src/generator/highlighter/mod.rs b/src/generator/highlighter/mod.rs new file mode 100644 index 0000000..ce20b77 --- /dev/null +++ b/src/generator/highlighter/mod.rs @@ -0,0 +1,94 @@ +use misti::TokenType; + +#[macro_export] +macro_rules! replace { + ($classes:literal, $token:ident, $offset:ident, $output:ident) => {{ + let start_pos = $token.position; + let end_pos = $token.get_end_position(); + + let range = (start_pos + $offset)..(end_pos + $offset); + let html = format!("{}", $classes, $token.value); + + $offset += 28 + $classes.len(); + + $output.replace_range(range, html.as_str()); + }}; +} + +pub fn highlight(input: &String) -> String { + // The tokens come in order + let tokens = misti::tokenize(&input); + + if tokens.is_err() { + // eprintln!("Found a lexical error processing code.\n{:?}", tokens); + return input.clone(); + } + + let mut output = input.clone(); + // Offset to the position of the tokens in the string, to allow + // several tokens to be highlighted + let mut offset = 0; + + for token in tokens.unwrap() { + match &token.token_type { + TokenType::Datatype => replace!("class-name", token, offset, output), + TokenType::Number => replace!("number", token, offset, output), + TokenType::Identifier if token.value == "true" || token.value == "false" => { + replace!("keyword", token, offset, output) + } + TokenType::String => replace!("string", token, offset, output), + TokenType::Comment => replace!("comment", token, offset, output), + TokenType::VAL | TokenType::VAR => replace!("keyword", token, offset, output), + _ => {} + } + } + + output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_return_simple_string() { + assert_eq!("sample", highlight(&String::from("sample"))) + } + + #[test] + fn should_highlight_datatype() { + assert_eq!( + "Num", + highlight(&String::from("Num")) + ) + } + + #[test] + fn should_highlight_number() { + assert_eq!( + "322", + highlight(&String::from("322")) + ) + } + + #[test] + fn should_highlight_string() { + assert_eq!( + "\"Hello\"", + highlight(&String::from("\"Hello\"")) + ) + } + + #[test] + fn should_highlight_multiple_tokens() { + assert_eq!( + "Str x = 322", + highlight(&String::from("Str x = 322")) + ); + + assert_eq!( + "Str x = \"hello\" 322", + highlight(&String::from("Str x = \"hello\" 322")) + ); + } +} diff --git a/src/generator/inline_code.rs b/src/generator/inline_code.rs new file mode 100644 index 0000000..43e02bf --- /dev/null +++ b/src/generator/inline_code.rs @@ -0,0 +1,26 @@ +use markdown::mdast::InlineCode; + +// use super::highlighter::highlight; +use super::Printable; + +impl Printable for InlineCode { + fn to_html(&self) -> String { + /* + let tokens = misti::tokenize(&self.value); + println!("INLINE CODE ==== tokens ====\n\n{:?}\n\n==== code ====\n\n{}\n\n", tokens, self.value); + + let s = self.value + .replace("<", "<") + .replace(">", ">"); + */ + + format!( + "{}", + &self.value // highlight(&self.value) + ) + } + + fn get_text(&self) -> String { + self.value.clone() + } +} diff --git a/src/generator/list.rs b/src/generator/list.rs new file mode 100644 index 0000000..0a1a161 --- /dev/null +++ b/src/generator/list.rs @@ -0,0 +1,47 @@ +use markdown::mdast::{List, ListItem, Node}; + +use crate::utils; + +use super::Printable; + +impl Printable for List { + fn to_html(&self) -> String { + let mut result = Vec::::new(); + + for node in &self.children { + result.push(format!("
  • {}
  • ", node.to_html())) + } + + let str: String = result.into_iter().collect(); + + if self.ordered { + format!("
      {}
    ", str) + } else { + format!("
      {}
    ", str) + } + } + + fn get_text(&self) -> String { + panic!("List cannot return it's raw text") + } +} + +impl Printable for ListItem { + fn to_html(&self) -> String { + let mut result = Vec::::new(); + + for node in &self.children { + let s = match node { + Node::Paragraph(p) => utils::collect_children_html(&p.children), + _ => panic!("A thing other than Paragraph inside ListItem (?)"), + }; + result.push(format!("{}", s)) + } + + result.into_iter().collect() + } + + fn get_text(&self) -> String { + panic!("ListItem cannot return it's raw text") + } +} diff --git a/src/generator/mod.rs b/src/generator/mod.rs new file mode 100644 index 0000000..9da11ab --- /dev/null +++ b/src/generator/mod.rs @@ -0,0 +1,59 @@ +use markdown::mdast::Node; + +mod code; +mod emphasis; +mod heading; +mod inline_code; +mod list; +mod paragraph; +mod root; +mod strong; +mod text; + +// mod highlighter; + +pub trait Printable { + fn to_html(&self) -> String; + fn get_text(&self) -> String; +} + +impl Printable for Node { + fn to_html(&self) -> String { + match self { + Node::Root(root) => root.to_html(), + Node::Heading(heading) => heading.to_html(), + Node::Text(text) => text.to_html(), + Node::Paragraph(p) => p.to_html(), + Node::ThematicBreak(_) => String::from("
    "), + Node::InlineCode(i) => i.to_html(), + Node::Code(c) => c.to_html(), + Node::Html(h) => h.value.clone(), + Node::Strong(s) => s.to_html(), + Node::Emphasis(e) => e.to_html(), + Node::List(l) => l.to_html(), + Node::ListItem(l) => l.to_html(), + _ => format!( + "
    Not implemented
    {:?}
    ", + self + ), + } + } + + fn get_text(&self) -> String { + match self { + Node::Root(root) => root.get_text(), + Node::Heading(heading) => heading.get_text(), + Node::Text(text) => text.get_text(), + Node::Paragraph(p) => p.get_text(), + Node::ThematicBreak(_) => panic!("
    cannot return its raw text"), + Node::InlineCode(i) => i.get_text(), + Node::Code(c) => c.get_text(), + Node::Html(_) => panic!("Html cannot return its raw text"), + Node::Strong(s) => s.get_text(), + Node::Emphasis(e) => e.get_text(), + Node::List(l) => l.get_text(), + Node::ListItem(l) => l.get_text(), + _ => String::from(""), + } + } +} diff --git a/src/generator/paragraph.rs b/src/generator/paragraph.rs new file mode 100644 index 0000000..fce798f --- /dev/null +++ b/src/generator/paragraph.rs @@ -0,0 +1,21 @@ +use markdown::mdast::Paragraph; + +use super::Printable; + +impl Printable for Paragraph { + fn to_html(&self) -> String { + let mut result = Vec::::new(); + + for node in &self.children { + result.push(node.to_html()) + } + + let text: String = result.into_iter().collect(); + + format!("

    {}

    ", text) + } + + fn get_text(&self) -> String { + panic!("Paragraph cannot return its raw text") + } +} diff --git a/src/generator/root.rs b/src/generator/root.rs new file mode 100644 index 0000000..e378b2a --- /dev/null +++ b/src/generator/root.rs @@ -0,0 +1,25 @@ +use markdown::mdast; + +use super::Printable; + +impl Printable for mdast::Root { + fn to_html(&self) -> String { + let mut result = Vec::::new(); + + for node in &self.children { + result.push(node.to_html()) + } + + result.into_iter().collect() + } + + fn get_text(&self) -> String { + let mut result = Vec::::new(); + + for node in &self.children { + result.push(node.get_text()) + } + + result.join("-") + } +} diff --git a/src/generator/strong.rs b/src/generator/strong.rs new file mode 100644 index 0000000..287b8a1 --- /dev/null +++ b/src/generator/strong.rs @@ -0,0 +1,17 @@ +use markdown::mdast::Strong; + +use crate::utils; + +use super::Printable; + +impl Printable for Strong { + fn to_html(&self) -> String { + let text = utils::collect_children_html(&self.children); + + format!("{}", text) + } + + fn get_text(&self) -> String { + utils::collect_children_text(&self.children) + } +} diff --git a/src/generator/text.rs b/src/generator/text.rs new file mode 100644 index 0000000..424a499 --- /dev/null +++ b/src/generator/text.rs @@ -0,0 +1,13 @@ +use markdown::mdast::Text; + +use super::Printable; + +impl Printable for Text { + fn to_html(&self) -> String { + self.value.clone() + } + + fn get_text(&self) -> String { + self.value.clone() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e3496e1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,103 @@ +use std::{fs, path::Path}; +use yaml_rust::Yaml; + +mod generator; +mod pages; +mod processor; +mod sidebar; +mod utils; + +const CONFIG_NAME: &str = "md-docs.config.yaml"; +const INPUT_KEY: &str = "input"; +const OUTPUT_KEY: &str = "output"; + +/// Creates a `YAML::String` from a `&str` +macro_rules! ystr { + ($str:literal) => { + &Yaml::String(String::from($str)) + }; +} + +fn main() { + let config_file = Path::new(CONFIG_NAME); + if !config_file.is_dir() { + eprintln!("A {} file was not found. Aborting.", CONFIG_NAME); + return; + } + + let config_str = match fs::read(config_file) { + Ok(s) => String::from_utf8(s).expect("md-docs.config.yaml MUST contain valid UTF-8"), + Err(error) => { + println!("Error reading {}.\n{:?}", CONFIG_NAME, error); + return; + } + }; + + let config_yaml = match yaml_rust::YamlLoader::load_from_str(&config_str) { + Ok(y) => { + let document = &y[0]; + let Yaml::Hash(hash) = document + else { + eprintln!("{} doesn't contain a hash as first value.", CONFIG_NAME); + return; + }; + + hash.clone() + } + Err(error) => { + eprintln!("{} doesn't contain valid YAML.\n{:?}", CONFIG_NAME, error); + return; + } + }; + + let input_folder = match config_yaml.get(ystr!("input")) { + Some(Yaml::String(input)) => input, + Some(_) => { + eprintln!("{}'s `{}` key MUST be a string", CONFIG_NAME, INPUT_KEY); + return; + } + None => { + eprintln!("{} MUST have a `{}` key", CONFIG_NAME, INPUT_KEY); + return; + } + }; + + let output_folder = match config_yaml.get(ystr!("output")) { + Some(Yaml::String(input)) => input, + Some(_) => { + eprintln!("{}'s `{}` key MUST be a string", CONFIG_NAME, OUTPUT_KEY); + return; + } + None => { + eprintln!("{} MUST have a `{}` key", CONFIG_NAME, OUTPUT_KEY); + return; + } + }; + + let input_folder = Path::new(input_folder); + let output_folder = Path::new(output_folder); + + match (input_folder.is_dir(), output_folder.is_dir()) { + (true, true) => { + processor::search_config_file(&input_folder, input_folder, output_folder); + } + (false, true) => { + eprintln!( + "{}'s `{}` key is not a valid path to a folder", + CONFIG_NAME, INPUT_KEY + ) + } + (true, false) => { + eprintln!( + "{}'s `{}` key is not a valid path to a folder", + CONFIG_NAME, OUTPUT_KEY + ) + } + (false, false) => { + eprintln!( + "{}'s `{}` and `{}` keys are not valid paths to a folder", + CONFIG_NAME, INPUT_KEY, OUTPUT_KEY + ) + } + } +} diff --git a/src/pages/md_compiler.rs b/src/pages/md_compiler.rs new file mode 100644 index 0000000..5f1d4d6 --- /dev/null +++ b/src/pages/md_compiler.rs @@ -0,0 +1,67 @@ +use std::{ + fs::{self, File}, + io::Write, + path::{Path, PathBuf}, +}; + +use crate::{generator::Printable, sidebar::SidebarGenerator}; + +/// ## Parameters +/// +/// - `file`: Path to the MD file to compile +/// - `input_folder`: Path to the input folder passed as parameter of the program +/// - `output_folder`: Path to the output folder passed as parameter of the program +/// - `file_tree_html`: HTML code of the file tree to be inserted into the generated HTML +pub fn compile(file: &PathBuf, input_folder: &Path, output_folder: &Path, file_tree_html: &String) { + // /home/fernando/misti/docs/markdown + let input_folder = input_folder.canonicalize().unwrap(); + + // /home/fernando/misti/docs/markdown/en/docs/latest/index.md + let input_file = file + .canonicalize() + .expect(format!("Expected file {:?} to exist", file).as_str()); + + // /home/fernando/misti/docs/static + let output_folder = output_folder.canonicalize().unwrap(); + + // en/docs/latests/index.md + let relative_input_file = input_file.strip_prefix(input_folder).unwrap(); + + let mut output_file = output_folder.clone(); + output_file.push(relative_input_file); + output_file.set_extension("html"); + + // + // Read MD from disk + // + let file_content_bytes = fs::read(&input_file).unwrap(); + let markdown_text = String::from_utf8(file_content_bytes).unwrap(); + + // + // Compile MD + // + let md_ast = markdown::to_mdast(&markdown_text, &markdown::ParseOptions::gfm()).unwrap(); + let html_text = md_ast.to_html(); + let sidebar_html = md_ast.generate_sidebar(); + + // Read template.html + let mut template_path = output_folder.clone(); + template_path.push("template.html"); + + let template_contents = fs::read(template_path).unwrap(); + let template_contents = String::from_utf8(template_contents).unwrap(); + + // Insert the markdown, sidebar and file tree into the template + let final_output = template_contents + .replace("{{markdown}}", &html_text) + .replace("{{sidebar}}", &sidebar_html) + .replace("{{pages}}", &file_tree_html); + + // + // Write to disk + // + File::create(&output_file) + .expect(format!("MD: Output file should be valid {:?}", &output_file).as_str()) + .write_all(final_output.as_bytes()) + .unwrap(); +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs new file mode 100644 index 0000000..b8a6a77 --- /dev/null +++ b/src/pages/mod.rs @@ -0,0 +1,174 @@ +use std::path::Path; + +use yaml_rust::Yaml; + +use crate::utils; + +mod md_compiler; + +pub enum Node<'a> { + File(File<'a>), + Folder(Folder<'a>), +} + +pub struct File<'a> { + /// Name of the file + path: &'a String, + /// Display name of the file + name: &'a String, +} + +pub struct Folder<'a> { + /// Name of the folder + path: &'a String, + /// Display name of the folder + name: &'a String, + /// If true, then there MUST be a `File {path: "index"}` in the `children` field + has_index: bool, + /// Sub files or folders + children: Box>>, +} + +/// Creates a `YAML::String` from a `&str` +macro_rules! y_str { + ($str:literal) => { + &Yaml::String(String::from($str)) + }; +} + +pub fn parse_yaml(values: &Yaml) -> Node { + let Yaml::Hash(table) = values + else {panic!("YAML: input MUST be an object")}; + + // Node path + let Yaml::String(path) = table.get(y_str!("path")).expect("YAML: Node MUST have a `path` key") + else { panic!("YAML: `path` MUST be a String") }; + + let Yaml::String(name) = table.get(y_str!("name")).expect("YAML: Node MUST have a `name` key") + else { panic!("YAML: `name` MUST be a String") }; + + let input_data = ( + table.get(y_str!("has_index")), + table.get(y_str!("children")), + ); + + match input_data { + (None, None) => Node::File(File { path, name }), + (has_index, Some(children)) => { + let has_index = match has_index { + Some(Yaml::Boolean(v)) => *v, + Some(_) => panic!("YAML: if key `has_index` exists, it MUST be a Boolean"), + None => false, + }; + + let Yaml::Array(children) = children + else {panic!("YAML: `children` MUST be an Array")}; + + let children_nodes: Vec = children + .into_iter() + .map(|values| parse_yaml(values)) + .collect(); + + Node::Folder(Folder { + path, + name, + has_index, + children: Box::new(children_nodes), + }) + } + _ => { + panic!("YAML: A Node is missing a `name` or `children` key") + } + } +} + +pub fn generate_pages_html(file_tree: &Node, current_path: &Path) -> String { + match file_tree { + Node::File(file) => { + if file.path == "index" { + format!( + "
  • + Index +
  • ", + current_path.to_str().unwrap() + ) + } else if file.path == "" { + String::from("") + } else { + format!( + "
  • + {} +
  • ", + current_path.to_str().unwrap(), + file.path, + file.name + ) + } + } + Node::Folder(folder) => { + let mut new_path = current_path.to_path_buf(); + new_path.push(folder.path); + + let sub_nodes_html: Vec = folder + .children + .iter() + .map(|n| generate_pages_html(n, &new_path)) + .collect(); + + // This is true for the root of the YAML file + if folder.path == "" { + format!("
      {}
    ", sub_nodes_html.join("")) + } else { + format!( + "
  • +
    {}
    +
      {}
    +
  • ", + folder.name, + sub_nodes_html.join("") + ) + } + } + } +} + +pub fn compile_md_to_html( + file_tree: &Node, + current_path: &Path, + input_folder: &Path, + output_folder: &Path, + file_tree_html: &String, +) { + match file_tree { + Node::File(file) if file.path != "" => { + let mut file_path = current_path.canonicalize().unwrap(); + file_path.push(format!("{}.md", file.path)); + + md_compiler::compile(&file_path, input_folder, output_folder, file_tree_html); + } + Node::File(_) => { + panic!("YAML: A file cannot have an empty `path` key") + } + Node::Folder(folder) if folder.path != "" => { + let mut new_path = current_path.canonicalize().unwrap(); + new_path.push(folder.path); + utils::ensure_folder_exists(&new_path, input_folder, output_folder) + .expect("SHOULD be able to create folder"); + + for node in folder.children.iter() { + compile_md_to_html(node, &new_path, input_folder, output_folder, file_tree_html); + } + } + Node::Folder(folder) => { + for node in folder.children.iter() { + compile_md_to_html( + node, + ¤t_path, + input_folder, + output_folder, + file_tree_html, + ); + } + } + } +} diff --git a/src/processor.rs b/src/processor.rs new file mode 100644 index 0000000..d8d6e92 --- /dev/null +++ b/src/processor.rs @@ -0,0 +1,97 @@ +use crate::pages::{compile_md_to_html, generate_pages_html, parse_yaml}; +use crate::utils; +use std::{fs, path::Path}; +use yaml_rust::YamlLoader; + +enum EntryFound { + YamlFile, + OtherFile, + None, +} + +// Traverses the current path searching for a YAML file +pub fn search_config_file(current_path: &Path, input_folder: &Path, output_folder: &Path) { + // Iterate over all the files searching for a YAML file + let result = current_path + .read_dir() + .unwrap() + .fold(&EntryFound::None, |acc, next| { + let p = next.unwrap().path(); + let is_file = p.is_file(); + let ext = p.extension(); + + match (acc, is_file, ext) { + (EntryFound::YamlFile, true, Some(x)) if x == "yaml" => { + panic!("FOUND A SECOND YAML FILE!!!") + } + (EntryFound::YamlFile, _, _) => acc, + (EntryFound::OtherFile, true, Some(x)) if x == "yaml" => &EntryFound::YamlFile, + (EntryFound::None, true, Some(x)) if x == "yaml" => &EntryFound::YamlFile, + (EntryFound::None, true, Some(_)) => &EntryFound::OtherFile, + _ => acc, + } + }); + + match result { + // If a file other than a YAML file is found, panic + EntryFound::OtherFile => panic!( + "Found an orphan file without a YAML parent at {:?}", + current_path + ), + // Process the YAML file + EntryFound::YamlFile => process_yaml(current_path, input_folder, output_folder), + // No files found, recursively read children folders + EntryFound::None => { + for entry in current_path.read_dir().unwrap() { + // Should always succeed, and countain a folder + let x = entry.unwrap(); + let path = x.path(); + + utils::ensure_folder_exists(&path, input_folder, output_folder).unwrap(); + search_config_file(&path, input_folder, output_folder); + } + } + }; +} + +fn process_yaml(current_path: &Path, input_folder: &Path, output_folder: &Path) { + // + // Read YAML file + // + let mut yaml_path = current_path.canonicalize().unwrap(); + yaml_path.push("index.yaml"); + + let yaml_bytes = fs::read(yaml_path).expect("File index.yaml MUST exist"); + let yaml = String::from_utf8(yaml_bytes).expect("YAML index file MUST be valid UTF-8"); + + let yaml_docs = + YamlLoader::load_from_str(yaml.as_str()).expect("YAML file MUST contain valid YAML"); + let yaml = &yaml_docs[0]; + + // + // Parse YAML + // + let file_tree = parse_yaml(&yaml); + + // + // Generate File Tree HTML + // + let tree_html = { + let input_folder = input_folder.canonicalize().unwrap(); + let yaml_folder_temp = current_path.canonicalize().unwrap(); + let web_absolute_path = yaml_folder_temp.strip_prefix(input_folder).unwrap(); + + generate_pages_html(&file_tree, web_absolute_path) + }; + + // + // Compile MD to HTML + // + compile_md_to_html( + &file_tree, + current_path, + input_folder, + output_folder, + &tree_html, + ); +} diff --git a/src/sidebar/mod.rs b/src/sidebar/mod.rs new file mode 100644 index 0000000..3f2b447 --- /dev/null +++ b/src/sidebar/mod.rs @@ -0,0 +1,90 @@ +use markdown::mdast::{Heading, Node}; + +use crate::{generator::Printable, utils}; + +pub trait SidebarGenerator { + fn generate_sidebar(&self) -> String; +} + +impl SidebarGenerator for Node { + fn generate_sidebar(&self) -> String { + match self { + Node::Root(root) => { + let children_nodes = root + .children + .clone() + .into_iter() + .filter_map(|x| match x { + Node::Heading(h) if h.depth <= 3 => Some(h), + _ => None, + }) + .collect(); + + // A top level topic that contains other topics + let topic = extract_topics(&children_nodes, 0, 1); + + match topic { + Some((t, _)) => { + let html: String = t.children.iter().map(|x| x.get_html()).collect(); + format!("
      {}
    ", html) + } + None => String::from("D:"), + } + } + _ => panic!("??"), + } + } +} + +#[derive(Debug)] +struct Topic { + text: String, + children: Box>, +} + +impl Topic { + pub fn get_html(&self) -> String { + let extra = if self.children.len() > 0 { + let children_html: String = self.children.iter().map(|x| x.get_html()).collect(); + + format!("
      {}
    ", children_html) + } else { + String::from("") + }; + + let html_fragment_link = utils::to_html_fragment(&self.text); + format!( + "
  • {}{}
  • ", + html_fragment_link, self.text, extra + ) + } +} + +// Return the next heading and all its children +// current_level: the depth of the heading to match +fn extract_topics<'a>( + headings: &'a Vec, + current_pos: usize, + current_level: u8, +) -> Option<(Topic, usize)> { + match headings.get(current_pos) { + Some(h) if h.depth == current_level => { + let mut new_vec = Vec::new(); + let mut next_pos = current_pos + 1; + + while let Some((topic, next)) = extract_topics(headings, next_pos, current_level + 1) { + new_vec.push(topic); + next_pos = next; + } + + let title = h.get_text(); + let topic = Topic { + text: title, + children: Box::new(new_vec), + }; + + Some((topic, next_pos)) + } + _ => None, + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..ac2720e --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,66 @@ +use std::{fs, path::Path}; + +use markdown::mdast::Node; + +use crate::generator::Printable; + +pub fn to_html_fragment(text: &String) -> String { + text.clone().replace(" ", "-") +} + +pub fn collect_children_html(vec: &Vec) -> String { + let mut result = Vec::::new(); + + for node in vec { + result.push(node.to_html()) + } + + result.into_iter().collect() +} + +pub fn collect_children_text(vec: &Vec) -> String { + let mut result = Vec::::new(); + + for node in vec { + result.push(node.get_text()) + } + + result.join("-") +} + +pub fn ensure_folder_exists( + folder: &Path, + input_folder: &Path, + output_folder: &Path, +) -> Result<(), String> { + // /home/fernando/misti/docs/markdown + let input_folder = input_folder.canonicalize().unwrap(); + + // /home/fernando/misti/docs/static + let output_folder = output_folder.canonicalize().unwrap(); + + // /home/fernando/misti/docs/markdown/en/ + let full_input_folder = folder.canonicalize().unwrap(); + + let relative_new_folder = full_input_folder.strip_prefix(input_folder).unwrap(); + + let mut full_output_folder = output_folder.clone(); + full_output_folder.push(relative_new_folder); + + // println!("Ensuring that folder exists:\n{:?}", full_output_folder); + + // If this is a "top-level" folder, remove all its contents, if it exists + if full_output_folder.is_dir() { + // println!("| Removing..."); + let _ = fs::remove_dir_all(&full_output_folder); + } + + // Create folder + match fs::create_dir(&full_output_folder) { + Ok(_) => { + // println!("| done\n\n"); + Ok(()) + } + Err(_) => Err(format!("Error creating folder {:?}", full_output_folder)), + } +}