diff --git a/Cargo.lock b/Cargo.lock index 13722c2..e74337f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,12 +154,6 @@ dependencies = [ "backtrace", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bigdecimal" version = "0.4.10" @@ -358,13 +352,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "crc32fast" -version = "1.5.0" +name = "countme" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "crossbeam-channel" @@ -375,6 +366,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -484,22 +494,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -788,7 +782,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -801,30 +794,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "indextree" -version = "4.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f80a9409db2176ff13877276660f5149fee32592698d2100afe8b52043d4405" -dependencies = [ - "indextree-macros", -] - -[[package]] -name = "indextree-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85dac6c239acc85fd61934c572292d93adfd2de459d9c032aa22b553506e915" -dependencies = [ - "either", - "itertools", - "proc-macro2", - "quote", - "strum", - "syn", - "thiserror", -] - [[package]] name = "inotify" version = "0.11.1" @@ -857,15 +826,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -926,12 +886,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.183" @@ -944,12 +898,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[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.12.1" @@ -1002,6 +950,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "miette" version = "7.6.0" @@ -1045,7 +1002,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", - "simd-adler32", ] [[package]] @@ -1183,28 +1139,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "onig" -version = "6.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" -dependencies = [ - "bitflags 2.11.0", - "libc", - "once_cell", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "ordered-float" version = "5.3.0" @@ -1222,34 +1156,37 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "jetscii", + "lazy_static", "miette", "notify", "orgize", "pretty_assertions", + "rayon", + "rowan 0.16.1", "serde", + "slugify", "steel-core", "tracing", "tracing-log", "tracing-subscriber", + "walkdir", ] [[package]] name = "orgize" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4998a8f863c00c06e18f83ed3c4de39e01f5a7ee5bddd072f0fb31bacfd7fa" +version = "0.10.0-alpha.10" +source = "git+https://github.com/PoiScript/orgize.git?branch=v0.10#5f26c94dcec2a33b37b1c880ace053b29b5d021e" dependencies = [ "bytecount", + "cfg-if", "chrono", - "indexmap 1.9.3", - "indextree", + "indexmap 2.13.0", "jetscii", - "lazy_static", "memchr", "nom", - "serde", - "serde_indextree", - "syntect", + "rowan 0.15.17", + "tracing", ] [[package]] @@ -1287,25 +1224,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64", - "indexmap 2.13.0", - "quick-xml", - "serde", - "time", -] - [[package]] name = "polling" version = "3.11.0" @@ -1376,15 +1294,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.45" @@ -1457,6 +1366,26 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1474,27 +1403,52 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.10", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rowan" +version = "0.15.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4f1e4a001f863f41ea8d0e6a0c34b356d5b733db50dadab3efef640bafb779b" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "memoffset", + "rustc-hash 1.1.0", + "text-size", +] + +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash 1.1.0", + "text-size", +] + [[package]] name = "rustc-demangle" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -1571,16 +1525,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_indextree" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a70e7d58005454b0ba863a8775df0c4391a8128c3a34974001de556d661bf74f" -dependencies = [ - "indextree", - "serde", -] - [[package]] name = "serde_json" version = "1.0.149" @@ -1618,12 +1562,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - [[package]] name = "sized-chunks" version = "0.6.5" @@ -1640,6 +1578,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slugify" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b8cf203d2088b831d7558f8e5151bfa420c57a34240b28cee29d0ae5f2ac8b" +dependencies = [ + "unidecode", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -1694,7 +1641,7 @@ dependencies = [ "parking_lot", "polling", "rand 0.9.2", - "rustc-hash", + "rustc-hash 2.1.2", "serde", "serde_json", "shared_vector", @@ -1744,7 +1691,7 @@ dependencies = [ "once_cell", "ordered-float", "pretty", - "rustc-hash", + "rustc-hash 2.1.2", "serde", "smallvec", "thin-vec", @@ -1765,27 +1712,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "supports-color" version = "3.0.2" @@ -1829,28 +1755,6 @@ dependencies = [ "syn", ] -[[package]] -name = "syntect" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031" -dependencies = [ - "bincode", - "bitflags 1.3.2", - "flate2", - "fnv", - "lazy_static", - "lazycell", - "onig", - "plist", - "regex-syntax 0.6.29", - "serde", - "serde_derive", - "serde_json", - "walkdir", - "yaml-rust", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -1870,6 +1774,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "textwrap" version = "0.16.2" @@ -1889,26 +1799,6 @@ dependencies = [ "serde", ] -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thread_local" version = "1.1.9" @@ -2061,6 +1951,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unidecode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2333,15 +2229,6 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 65021bf..3f2ce2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,13 @@ serde = { version = "1.0.213", features = ["derive"] } tracing = "0.1.40" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] } -orgize = { version = "0.9.0", features = ["chrono", "indexmap", "syntect"] } +orgize = { git="https://github.com/PoiScript/orgize.git", branch = "v0.10", features = ["chrono", "indexmap", "tracing"] } steel-core = { git="https://github.com/mattwparas/steel.git", branch = "master" } chrono = "0.4.44" notify = "8.2.0" +walkdir = "2.5.0" +jetscii = "0.5.3" +lazy_static = "1.5.0" +rowan = "0.16.1" +slugify = "0.1.0" +rayon = "1.11.0" diff --git a/justfile b/justfile index 4e613e6..00aaf9c 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,4 @@ -file := "-f ~/docs/site/content/" +path := "-p ~/docs/site/" verbose := "-v" # export RUSTC_WRAPPER := "sccache" @@ -11,7 +11,7 @@ build: build-release: cargo build --release run: - cargo run -- {{verbose}} {{file}} + cargo run -- {{verbose}} {{path}} run-release: cargo run --release clean: @@ -22,7 +22,7 @@ bench: export NEXTEST_EXPERIMENTAL_BENCHMARKS=1 cargo nextest bench profile: - samply record cargo run --release -- {{verbose}} {{file}} + samply record cargo run --release -- {{verbose}} {{path}} alias b := build alias r := run diff --git a/src/main.rs b/src/main.rs index 0159bd4..118ab25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +pub mod org; pub mod publish; pub mod watcher; @@ -32,11 +33,10 @@ fn main() -> Result<()> { .parse_lossy(default_directive); tracing_subscriber::FmtSubscriber::builder() - .pretty() - .with_line_number(true) - .with_level(true) .with_env_filter(filter) - .with_target(true) + // .with_line_number(true) + .with_level(true) + .with_target(false) .with_timer(timer) .init(); diff --git a/src/org/exporter.rs b/src/org/exporter.rs index 8b13789..b58564b 100644 --- a/src/org/exporter.rs +++ b/src/org/exporter.rs @@ -1 +1,353 @@ +use orgize::ast::Token; +use rowan::NodeOrToken; +use slugify::slugify; +use std::cmp::min; +use std::fmt; +use std::fmt::Write as _; +use orgize::export::{Container, Event, TraversalContext, Traverser}; +use orgize::{SyntaxElement, SyntaxKind, SyntaxNode}; + +/// A wrapper for escaping sensitive characters in html. +/// +/// ```rust +/// use orgize::export::HtmlEscape as Escape; +/// +/// assert_eq!(format!("{}", Escape("< < <")), "< < <"); +/// assert_eq!( +/// format!("{}", Escape("")), +/// "<script>alert('Hello XSS')</script>" +/// ); +/// ``` +pub struct HtmlEscape>(pub S); + +impl> fmt::Display for HtmlEscape { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut pos = 0; + + let content = self.0.as_ref(); + let bytes = content.as_bytes(); + + while let Some(off) = jetscii::bytes!(b'<', b'>', b'&', b'\'', b'"').find(&bytes[pos..]) { + write!(f, "{}", &content[pos..pos + off])?; + + pos += off + 1; + + match bytes[pos - 1] { + b'<' => write!(f, "<")?, + b'>' => write!(f, ">")?, + b'&' => write!(f, "&")?, + b'\'' => write!(f, "'")?, + b'"' => write!(f, """)?, + _ => {} + } + } + + write!(f, "{}", &content[pos..]) + } +} + +#[derive(Default)] +pub struct OrgHtmlExporter { + output: String, + + in_descriptive_list: Vec, + + table_row: TableRow, +} + +#[derive(Default, PartialEq, Eq)] +enum TableRow { + #[default] + HeaderRule, + Header, + BodyRule, + Body, +} + +impl OrgHtmlExporter { + pub fn push_str(&mut self, s: impl AsRef) { + self.output += s.as_ref(); + } + + pub fn finish(self) -> String { + self.output + } + + /// Render syntax node to html string + /// + /// ```rust + /// use orgize::{Org, ast::Bold, export::HtmlExport, rowan::ast::AstNode}; + /// + /// let org = Org::parse("* /hello/ *world*"); + /// let bold = org.first_node::().unwrap(); + /// let mut html = HtmlExport::default(); + /// html.render(bold.syntax()); + /// assert_eq!(html.finish(), "world"); + /// ``` + pub fn render(&mut self, node: &SyntaxNode) { + let mut ctx = TraversalContext::default(); + self.element(SyntaxElement::Node(node.clone()), &mut ctx); + } +} + +impl Traverser for OrgHtmlExporter { + fn event(&mut self, event: Event, ctx: &mut TraversalContext) { + match event { + Event::Enter(Container::Document(_)) => self.output += "
", + Event::Leave(Container::Document(_)) => self.output += "
", + + Event::Enter(Container::Headline(headline)) => { + let level = min(headline.level() + 1, 6); + let _ = write!(&mut self.output, ""); + let title = headline.title().map(|h| h.to_string()).collect::(); + let _ = write!( + &mut self.output, + "", + slugify!(&title) + ); + for elem in headline.title() { + self.element(elem, ctx); + } + self.output += ""; + let _ = write!(&mut self.output, ""); + } + Event::Leave(Container::Headline(_)) => {} + + Event::Enter(Container::Paragraph(_)) => self.output += "

", + Event::Leave(Container::Paragraph(_)) => self.output += "

", + + Event::Enter(Container::Section(_)) => self.output += "
", + Event::Leave(Container::Section(_)) => self.output += "
", + + Event::Enter(Container::Italic(_)) => self.output += "", + Event::Leave(Container::Italic(_)) => self.output += "", + + Event::Enter(Container::Bold(_)) => self.output += "", + Event::Leave(Container::Bold(_)) => self.output += "", + + Event::Enter(Container::Strike(_)) => self.output += "", + Event::Leave(Container::Strike(_)) => self.output += "", + + Event::Enter(Container::Underline(_)) => self.output += "", + Event::Leave(Container::Underline(_)) => self.output += "", + + Event::Enter(Container::Verbatim(_)) => self.output += "", + Event::Leave(Container::Verbatim(_)) => self.output += "", + + Event::Enter(Container::Code(_)) => self.output += "", + Event::Leave(Container::Code(_)) => self.output += "", + + Event::Enter(Container::SourceBlock(block)) => { + if let Some(language) = block.language() { + let _ = write!( + &mut self.output, + r#"
"#,
+                        HtmlEscape(&language)
+                    );
+                } else {
+                    self.output += r#"
"#
+                }
+            }
+            Event::Leave(Container::SourceBlock(_)) => self.output += "
", + + Event::Enter(Container::QuoteBlock(_)) => self.output += "
", + Event::Leave(Container::QuoteBlock(_)) => self.output += "
", + + Event::Enter(Container::VerseBlock(_)) => self.output += "

", + Event::Leave(Container::VerseBlock(_)) => self.output += "

", + + Event::Enter(Container::ExampleBlock(_)) => self.output += "
",
+            Event::Leave(Container::ExampleBlock(_)) => self.output += "
", + + Event::Enter(Container::CenterBlock(_)) => self.output += "
", + Event::Leave(Container::CenterBlock(_)) => self.output += "
", + + Event::Enter(Container::CommentBlock(_)) => self.output += "", + + Event::Enter(Container::Comment(_)) => self.output += "", + + Event::Enter(Container::Subscript(_)) => self.output += "", + Event::Leave(Container::Subscript(_)) => self.output += "", + + Event::Enter(Container::Superscript(_)) => self.output += "", + Event::Leave(Container::Superscript(_)) => self.output += "", + + Event::Enter(Container::List(list)) => { + self.output += if list.is_ordered() { + self.in_descriptive_list.push(false); + "
    " + } else if list.is_descriptive() { + self.in_descriptive_list.push(true); + "
    " + } else { + self.in_descriptive_list.push(false); + "
      " + }; + } + Event::Leave(Container::List(list)) => { + self.output += if list.is_ordered() { + "
" + } else if let Some(true) = self.in_descriptive_list.last() { + "" + } else { + "" + }; + self.in_descriptive_list.pop(); + } + Event::Enter(Container::ListItem(list_item)) => { + if let Some(&true) = self.in_descriptive_list.last() { + self.output += "
"; + for elem in list_item.tag() { + self.element(elem, ctx); + } + self.output += "
"; + } else { + self.output += "
  • "; + } + } + Event::Leave(Container::ListItem(_)) => { + if let Some(&true) = self.in_descriptive_list.last() { + self.output += "
  • "; + } else { + self.output += ""; + } + } + + Event::Enter(Container::OrgTable(table)) => { + self.output += ""; + self.table_row = if table.has_header() { + TableRow::HeaderRule + } else { + TableRow::BodyRule + } + } + Event::Leave(Container::OrgTable(_)) => { + match self.table_row { + TableRow::Body => self.output += "", + TableRow::Header => self.output += "", + _ => {} + } + self.output += "
    "; + } + Event::Enter(Container::OrgTableRow(row)) => { + if row.is_rule() { + match self.table_row { + TableRow::Body => { + self.output += ""; + self.table_row = TableRow::BodyRule; + } + TableRow::Header => { + self.output += ""; + self.table_row = TableRow::BodyRule; + } + _ => {} + } + ctx.skip(); + } else { + match self.table_row { + TableRow::HeaderRule => { + self.table_row = TableRow::Header; + self.output += ""; + } + TableRow::BodyRule => { + self.table_row = TableRow::Body; + self.output += ""; + } + _ => {} + } + self.output += ""; + } + } + Event::Leave(Container::OrgTableRow(row)) => { + if row.is_rule() { + match self.table_row { + TableRow::Body => { + self.output += ""; + self.table_row = TableRow::BodyRule; + } + TableRow::Header => { + self.output += ""; + self.table_row = TableRow::BodyRule; + } + _ => {} + } + ctx.skip(); + } else { + self.output += ""; + } + } + Event::Enter(Container::OrgTableCell(_)) => self.output += "", + Event::Leave(Container::OrgTableCell(_)) => self.output += "", + + Event::Enter(Container::Link(link)) => { + let path = link.path(); + let path = path.trim_start_matches("file:"); + + if link.is_image() { + let _ = write!(&mut self.output, r#""#, HtmlEscape(&path)); + return ctx.skip(); + } + + let _ = write!(&mut self.output, r#""#, HtmlEscape(&path)); + + if !link.has_description() { + let _ = write!(&mut self.output, "{}", HtmlEscape(&path)); + ctx.skip(); + } + } + Event::Leave(Container::Link(_)) => self.output += "", + + Event::Text(text) => { + let _ = write!(&mut self.output, "{}", HtmlEscape(text)); + } + + Event::LineBreak(_) => self.output += "
    ", + + Event::Snippet(snippet) => { + if snippet.backend().eq_ignore_ascii_case("html") { + self.output += &snippet.value(); + } + } + + Event::Rule(_) => self.output += "
    ", + + Event::Timestamp(timestamp) => { + self.output += r#""#; + // for e in timestamp.syntax.children_with_tokens() { + // match e { + // NodeOrToken::Token(t) if t.kind() == SyntaxKind::MINUS2 => { + // self.output += "–"; + // } + // NodeOrToken::Token(t) => { + // self.output += t.text(); + // } + // _ => {} + // } + // } + self.output += r#""#; + } + + Event::LatexFragment(latex) => { + // let _ = write!(&mut self.output, "{}", &latex.syntax); + } + Event::LatexEnvironment(latex) => { + // let _ = write!(&mut self.output, "{}", &latex.syntax); + } + + Event::Enter(Container::Keyword(keyword)) => { + if keyword.key().to_uppercase() == "TITLE" { + self.output += r#"

    "#; + self.output += &keyword.value(); + self.output += r#"

    "#; + } + } + + Event::Entity(entity) => self.output += entity.html(), + + _ => {} + } + } +} diff --git a/src/publish/publish.rs b/src/publish/publish.rs index 4f99271..cf64de2 100644 --- a/src/publish/publish.rs +++ b/src/publish/publish.rs @@ -1,5 +1,15 @@ +use orgize::ast::Keyword; +use rayon::prelude::*; +use std::collections::HashMap; +use std::fs; use std::io::Error as IOError; use std::path::{Path, PathBuf}; +use tracing::{debug, error}; +use walkdir::{DirEntry, Error as WDError, WalkDir}; + +use crate::org::exporter::OrgHtmlExporter; + +#[derive(Debug)] pub struct Project { name: String, base_directory: PathBuf, @@ -19,6 +29,7 @@ pub struct Project { html_validation_link: Option, } +#[derive(Debug)] pub enum Action { Org, Attach, @@ -30,8 +41,122 @@ pub enum Error { ParseError(PathBuf), InternalError, IO(IOError), + WalkDir(WDError), + NotDirectory, } -pub fn build_site(path: impl AsRef) -> Result<(), Error> { - todo!() +pub fn build_site( + base_path: impl AsRef, + publish_path: impl AsRef, +) -> Result<(), Error> { + let base_path = base_path.as_ref().to_path_buf(); + let publish_path = publish_path.as_ref().to_path_buf(); + if !publish_path.exists() { + if let Err(e) = fs::create_dir_all(&publish_path) { + error!("Couldn't create publish directory, check permissions: {e}"); + } + } + if base_path.is_dir() { + WalkDir::new(&base_path) + .follow_links(true) + .into_iter() + .collect::>>() + .into_par_iter() + .filter_map(|res| res.ok()) + .for_each(|entry| { + let path = entry.path(); + let file_name = path.file_name().unwrap_or_default().to_string_lossy(); + if path.is_file() && !path.ends_with("~") && !file_name.starts_with(".#") { + let mut handler = OrgHtmlExporter::default(); + debug!(?path); + if path + .extension() + .iter() + .filter_map(|s| s.to_str()) + .collect::() + == "org" + { + debug!("It ends with org"); + let org_string = fs::read_to_string(path).unwrap_or_default(); + let org = orgize::Org::parse(&org_string); + let mut keywords = org.keywords(); + let keywords = extract_keywords(&mut keywords); + org.traverse(&mut handler); + + let html = build_html(handler.finish(), keywords); + if let Ok(stripped_path) = path.strip_prefix(base_path.clone()) { + let mut final_dest = publish_path.clone(); + final_dest.push(stripped_path); + final_dest.set_extension("html"); + if let Err(e) = fs::write(final_dest, html) { + error!("Error writing html: {e}"); + } + } else { + error!(?path, "This file is not in content directory"); + } + }; + } + }); + + Ok(()) + } else { + Err(Error::NotDirectory) + } +} + +fn build_html(inner_html: String, keywords: HashMap) -> String { + let title = if let Some(title) = keywords.get("TITLE") { + format!("{}", title) + } else { + "My Site".to_string() + }; + let author = if let Some(author) = keywords.get("AUTHOR") { + format!("", author) + } else { + "".to_string() + }; + let description = if let Some(author) = keywords.get("AUTHOR") { + format!("", author) + } else { + "".to_string() + }; + let build_time = chrono::Local::now() + .naive_local() + .format("%Y-%m-%d %H:%M") + .to_string(); + let time_comment = format!("", build_time); + let doctype_html = " +"; + let charset = r#""#; + let style = r#""#; + let viewport = r#""#; + let generator = r#""#; + let mut head = format!("{}\n", doctype_html); + head.push_str(&time_comment); + head.push_str(&charset); + head.push_str(&viewport); + head.push_str(&title); + head.push_str(&description); + head.push_str(&generator); + head.push_str(&author); + head.push_str(&style); + head.push_str(""); + let other_style = r#""#; + format!("{}\n\n{}\n", head, inner_html) +} + +fn extract_keywords(keywords: &mut I) -> HashMap +where + I: Iterator, +{ + let mut map = HashMap::new(); + for keyword in keywords { + map.entry(keyword.key().to_uppercase()) + .or_insert(keyword.value().to_string()); + } + map } diff --git a/src/watcher/mod.rs b/src/watcher/mod.rs index 63a3cf0..6627562 100644 --- a/src/watcher/mod.rs +++ b/src/watcher/mod.rs @@ -1,9 +1,11 @@ use std::path::{Path, PathBuf}; -use notify::{Event, EventKind, Result}; -use tracing::debug; +use notify::{Event, EventKind}; +use tracing::{debug, info}; -pub fn watch(event: Event, path: impl AsRef) -> Result<()> { +use crate::publish::publish::{Error, build_site}; + +pub fn watch(event: Event, path: impl AsRef) -> Result<(), Error> { if event .paths .clone() @@ -30,6 +32,20 @@ pub fn watch(event: Event, path: impl AsRef) -> Result<()> { debug!("file deleted"); } } + let begin = chrono::Local::now(); + let mut base_path = path.as_ref().to_path_buf(); + base_path.push("content"); + let mut publish_path = path.as_ref().to_path_buf(); + publish_path.push("public"); + build_site(base_path, publish_path)?; + let end = chrono::Local::now(); + let elapsed = end - begin; + info!( + "Time to build: {:?} minutes, {:?} seconds, and {:?} nanoseconds", + elapsed.num_minutes(), + elapsed.num_seconds(), + elapsed.num_nanoseconds() + ); } _ => (), }