diff --git a/Cargo.lock b/Cargo.lock index f606ef73..021c7c68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.0.4" @@ -125,6 +137,12 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cc" version = "1.0.83" @@ -221,6 +239,40 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.29", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "diff" version = "0.1.13" @@ -237,6 +289,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + [[package]] name = "env_logger" version = "0.10.0" @@ -277,6 +350,25 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -287,6 +379,26 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "handlebars" version = "4.3.7" @@ -307,6 +419,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "humantime" version = "2.1.0" @@ -400,12 +526,54 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "mdbook" version = "0.4.34" @@ -437,15 +605,21 @@ name = "mdbook-i18n-helpers" version = "0.2.2" dependencies = [ "anyhow", + "ego-tree", + "markup5ever", + "markup5ever_rcdom", "mdbook", "polib", "pretty_assertions", "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", + "scraper", "semver", + "serde", "serde_json", "tempfile", + "thiserror", ] [[package]] @@ -454,6 +628,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + [[package]] name = "normpath" version = "1.1.1" @@ -489,6 +669,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "pest" version = "2.7.2" @@ -519,7 +722,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -533,6 +736,60 @@ dependencies = [ "sha2", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "polib" version = "0.2.0" @@ -542,6 +799,18 @@ dependencies = [ "linereader", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -552,6 +821,12 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.66" @@ -590,6 +865,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -661,6 +966,48 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a930e03325234c18c7071fd2b60118307e025d6fff3e12745ffbf63a3d29c" +dependencies = [ + "ahash", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors", + "smallvec", + "tendril", +] + +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags 2.4.0", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "semver" version = "1.0.18" @@ -684,7 +1031,7 @@ checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -698,6 +1045,15 @@ dependencies = [ "serde", ] +[[package]] +name = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha2" version = "0.10.7" @@ -715,12 +1071,67 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.29" @@ -745,6 +1156,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -781,7 +1203,7 @@ checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -826,6 +1248,18 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.1" @@ -838,6 +1272,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -859,7 +1299,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -881,7 +1321,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -998,6 +1438,17 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "xml5ever" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" +dependencies = [ + "log", + "mac", + "markup5ever", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 898c91d6..fb87253e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,13 +11,19 @@ description = "Plugins for a mdbook translation workflow based on Gettext." [dependencies] anyhow = "1.0.68" +ego-tree = "0.6.2" +markup5ever = "0.11.0" +markup5ever_rcdom = "0.2.0" mdbook = { version = "0.4.25", default-features = false } polib = "0.2.0" pulldown-cmark = { version = "0.9.2", default-features = false } pulldown-cmark-to-cmark = "10.0.4" regex = "1.9.4" +scraper = "0.17.1" semver = "1.0.16" +serde = "1.0.130" serde_json = "1.0.91" +thiserror = "1.0.30" [dev-dependencies] pretty_assertions = "1.3.0" diff --git a/src/custom_component_renderer/book_directory_renderer.rs b/src/custom_component_renderer/book_directory_renderer.rs new file mode 100644 index 00000000..c8fb8796 --- /dev/null +++ b/src/custom_component_renderer/book_directory_renderer.rs @@ -0,0 +1,141 @@ +use super::dom_manipulator::NodeManipulator; +use super::{error::RendererError, languages_configuration::LanguagesConfiguration, Component}; +use crate::custom_component_renderer::error::Result; +use scraper::{Html, Selector}; +use std::io::Write; +use std::{fs, io::Read, path::Path}; + +pub(crate) struct BoookDirectoryRenderer { + config: LanguagesConfiguration, + // Saves on reallocations + file_buffer_cache: String, + components: Vec>, +} + +impl BoookDirectoryRenderer { + pub(crate) fn new(config: LanguagesConfiguration) -> BoookDirectoryRenderer { + BoookDirectoryRenderer { + config, + file_buffer_cache: String::new(), + components: Vec::new(), + } + } + + pub(crate) fn render_book(&mut self, path: &Path) -> Result<()> { + if !path.is_dir() { + return Err(RendererError::InvalidPath(format!( + "{:?} is not a directory", + path + ))); + } + self.render_book_directory(path) + } + + pub(crate) fn add_component(&mut self, component: Box) { + self.components.push(component); + } + + fn render_components(&mut self) -> Result<()> { + let mut document = Html::parse_document(&self.file_buffer_cache); + for custom_component in &mut self.components { + let mut node_ids = Vec::new(); + + let selector = Selector::parse(&custom_component.identifier()) + .map_err(|err| RendererError::InvalidIdentifier(err.to_string()))?; + for node in document.select(&selector) { + node_ids.push(node.id()); + } + let tree = &mut document.tree; + for id in node_ids { + let dom_manipulator = NodeManipulator::new(tree, id); + custom_component.render(dom_manipulator, &self.config)?; + } + } + self.file_buffer_cache.clear(); + self.file_buffer_cache = document.html(); + Ok(()) + } + + fn process_file(&mut self, path: &Path) -> Result<()> { + if path.extension().unwrap_or_default() != "html" { + return Ok(()); + } + self.file_buffer_cache.clear(); + { + let mut file = fs::File::open(path)?; + file.read_to_string(&mut self.file_buffer_cache)?; + } + self.render_components()?; + let mut file = fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(path)?; + file.write_all(self.file_buffer_cache.as_bytes())?; + Ok(()) + } + + fn render_book_directory(&mut self, path: &Path) -> Result<()> { + for entry in path.read_dir()? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + self.render_book_directory(&path)?; + } else { + self.process_file(&path)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + #[test] + fn test_render_book() { + use super::*; + use crate::custom_components::test_component::TestComponent; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + let dir = tempdir().unwrap(); + { + let mut file = File::create(dir.path().join("test.html")).unwrap(); + file.write_all( + b" + + + +
TOREMOVE
+
+ + ", + ) + .unwrap(); + } + + let mut languages = BTreeMap::new(); + languages.insert(String::from("en"), String::from("English")); + languages.insert(String::from("fr"), String::from("French")); + let mock_config = LanguagesConfiguration { languages }; + + let mut renderer = BoookDirectoryRenderer::new(mock_config); + let test_component = Box::new(TestComponent::new()); + renderer.add_component(test_component); + renderer + .render_book(dir.path()) + .expect("Failed to render book"); + + let mut output = String::new(); + let mut file = File::open(dir.path().join("test.html")).unwrap(); + file.read_to_string(&mut output).unwrap(); + + const EXPECTED: &str = "\n
  • en: English
  • fr: French
\n \n "; + + let output_document = Html::parse_document(&output); + let expected_document = Html::parse_document(EXPECTED); + assert_eq!(output_document, expected_document); + } +} diff --git a/src/custom_component_renderer/component_trait.rs b/src/custom_component_renderer/component_trait.rs new file mode 100644 index 00000000..ae53ba6f --- /dev/null +++ b/src/custom_component_renderer/component_trait.rs @@ -0,0 +1,14 @@ +use super::dom_manipulator::NodeManipulator; +use crate::custom_component_renderer::error::Result; +use crate::custom_component_renderer::languages_configuration::LanguagesConfiguration; + +pub trait Component { + /// Returns the identifier of the component. ie `` -> `i18n-helpers` + fn identifier(&self) -> String; + + fn render<'a>( + &mut self, + node: NodeManipulator<'a>, + config: &LanguagesConfiguration, + ) -> Result<()>; +} diff --git a/src/custom_component_renderer/dom_manipulator.rs b/src/custom_component_renderer/dom_manipulator.rs new file mode 100644 index 00000000..4463a8d4 --- /dev/null +++ b/src/custom_component_renderer/dom_manipulator.rs @@ -0,0 +1,147 @@ +use super::error::RendererError; +use crate::custom_component_renderer::error::Result; +use ego_tree::{NodeId, NodeMut, NodeRef, Tree}; +use markup5ever::namespace_url; +use markup5ever::{ns, Attribute, LocalName, QualName}; +use scraper::node::{Element, Text}; +use scraper::Node; + +pub struct NodeManipulator<'a> { + tree: &'a mut Tree, + node_id: NodeId, + append_children_builder: Option, +} + +impl<'a> NodeManipulator<'a> { + pub fn new(tree: &'a mut Tree, node_id: NodeId) -> NodeManipulator<'a> { + NodeManipulator { + tree, + node_id, + append_children_builder: None, + } + } + + fn get_node(&'a self) -> Result> { + Ok(self.tree.get(self.node_id).ok_or_else(|| { + RendererError::InternalError(format!("Node with id {:?} does not exist", self.node_id)) + })?) + } + + fn get_node_mut(&mut self) -> Result> { + Ok(self.tree.get_mut(self.node_id).ok_or_else(|| { + RendererError::InternalError(format!("Node with id {:?} does not exist", self.node_id)) + })?) + } + + pub fn get_attribute(&self, attr: &str) -> Result> { + let node = self.get_node()?; + match node.value() { + Node::Element(element) => { + let attr = element.attr(attr); + Ok(attr) + } + _ => Err(RendererError::InternalError(format!( + "Node with id {:?} is not an element", + self.node_id + ))), + } + } + + /// Appends a child node and returns the id of the inserted node id. + pub fn append_child(&'a mut self, new_node: Node) -> Result { + let mut node = self.get_node_mut()?; + let inserted_id = node.append(new_node).id(); + Ok(Self::new(self.tree, inserted_id)) + } + + pub fn append_children(&mut self) -> &mut AppendChildrenBuilder { + let builder = AppendChildrenBuilder::new(None); + self.append_children_builder = Some(builder); + self.append_children_builder.as_mut().unwrap() + } + + fn build_children_impl(&mut self, builder: AppendChildrenBuilder) -> Result<()> { + let mut node = self.get_node_mut()?; + let mut builder_to_nodeid = Vec::new(); + for mut child in builder.children { + let inserted_id = node.append(child.value.take().unwrap()).id(); + builder_to_nodeid.push((child, inserted_id)); + } + let original_node_id = self.node_id; + for (child, inserted_id) in builder_to_nodeid { + self.node_id = inserted_id; + self.build_children_impl(child)?; + } + self.node_id = original_node_id; + Ok(()) + } + + pub fn build_children(&'a mut self) -> Result<()> { + let builder = self.append_children_builder.take().ok_or_else(|| { + RendererError::InternalError(format!("Missing children builder in build_children call")) + })?; + self.build_children_impl(builder) + } + + pub fn replace_with(mut self, new_node: Node) -> Result { + let mut node = self.get_node_mut()?; + let inserted_id = node.insert_after(new_node).id(); + node.detach(); + let Self { tree, .. } = self; + Ok(Self::new(tree, inserted_id)) + } +} + +pub struct AppendChildrenBuilder { + children: Vec, + value: Option, +} + +impl AppendChildrenBuilder { + fn new(value: Option) -> Self { + Self { + value, + children: Vec::new(), + } + } + + pub fn append_child(&mut self, new_node: Node) -> &mut AppendChildrenBuilder { + let new_builder = Self::new(Some(new_node)); + self.children.push(new_builder); + self.children.last_mut().unwrap() + } +} + +pub struct NodeAttribute { + pub name: String, + pub value: String, +} + +impl NodeAttribute { + pub fn new(name: &str, value: &str) -> Self { + Self { + name: String::from(name), + value: String::from(value), + } + } +} + +impl From for Attribute { + fn from(value: NodeAttribute) -> Self { + Attribute { + name: QualName::new(None, ns!(), LocalName::from(value.name)), + value: value.value.into(), + } + } +} + +pub fn create_node(name: &str, attributes: Vec) -> Node { + Node::Element(Element::new( + QualName::new(None, ns!(), LocalName::from(name)), + attributes.into_iter().map(Into::into).collect(), + )) +} + +pub fn create_text_node(text: &str) -> Node { + Node::Text(Text { text: text.into() }) +} diff --git a/src/custom_component_renderer/error.rs b/src/custom_component_renderer/error.rs new file mode 100644 index 00000000..53014888 --- /dev/null +++ b/src/custom_component_renderer/error.rs @@ -0,0 +1,17 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RendererError { + #[error("IO Error: {0}")] + IoError(#[from] std::io::Error), + #[error("Invalid Identifier: {0}")] + InvalidIdentifier(String), + #[error("Invalid path: {0}")] + InvalidPath(String), + #[error("Internal Error: {0}")] + InternalError(String), + #[error("Component Rendering Error: {0}")] + ComponentError(String), +} + +pub type Result = std::result::Result; diff --git a/src/custom_component_renderer/languages_configuration.rs b/src/custom_component_renderer/languages_configuration.rs new file mode 100644 index 00000000..50df040b --- /dev/null +++ b/src/custom_component_renderer/languages_configuration.rs @@ -0,0 +1,8 @@ +use std::collections::BTreeMap; + +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct LanguagesConfiguration { + pub languages: BTreeMap, +} diff --git a/src/custom_component_renderer/mod.rs b/src/custom_component_renderer/mod.rs new file mode 100644 index 00000000..b05c0d0e --- /dev/null +++ b/src/custom_component_renderer/mod.rs @@ -0,0 +1,7 @@ +pub(crate) mod book_directory_renderer; +mod component_trait; +pub(crate) mod dom_manipulator; +pub(crate) mod error; +pub(crate) mod languages_configuration; + +pub(crate) use component_trait::Component; diff --git a/src/custom_components/mod.rs b/src/custom_components/mod.rs new file mode 100644 index 00000000..d486e7cd --- /dev/null +++ b/src/custom_components/mod.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +pub(crate) mod test_component; diff --git a/src/custom_components/test_component.rs b/src/custom_components/test_component.rs new file mode 100644 index 00000000..25e00156 --- /dev/null +++ b/src/custom_components/test_component.rs @@ -0,0 +1,44 @@ +use ego_tree::NodeId; + +use crate::custom_component_renderer::dom_manipulator::{ + create_node, create_text_node, NodeAttribute, NodeManipulator, +}; +use crate::custom_component_renderer::error::RendererError; +use crate::custom_component_renderer::error::Result; +use crate::custom_component_renderer::languages_configuration::LanguagesConfiguration; +use crate::custom_component_renderer::Component; + +pub struct TestComponent {} + +impl TestComponent { + pub fn new() -> TestComponent { + TestComponent {} + } +} + +impl Component for TestComponent { + fn identifier(&self) -> String { + String::from("TestComponent") + } + + fn render<'a>( + &mut self, + node: NodeManipulator<'a>, + config: &LanguagesConfiguration, + ) -> Result<()> { + let name = node + .get_attribute("name")? + .ok_or_else(|| RendererError::ComponentError(String::from("Missing attribute name")))?; + let new_node = create_node("div", vec![NodeAttribute::new("name", name)]); + let mut new_node = node.replace_with(new_node)?; + let mut ul = new_node.append_child(create_node("ul", Vec::new()))?; + + let append_builder = ul.append_children(); + for (identifier, language) in &config.languages { + let li = append_builder.append_child(create_node("li", Vec::new())); + li.append_child(create_text_node(&format!("{}: {}", identifier, language))); + } + ul.build_children()?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..e12383b5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,30 @@ +mod custom_component_renderer; +mod custom_components; + +use custom_component_renderer::book_directory_renderer::BoookDirectoryRenderer; +use mdbook::renderer::RenderContext; +use std::io; + +use crate::custom_component_renderer::languages_configuration::LanguagesConfiguration; + +fn main() { + let mut stdin = io::stdin(); + + // Get the configs + let ctx = RenderContext::from_json(&mut stdin).unwrap(); + let cfg: LanguagesConfiguration = ctx + .config + .get_deserialized_opt("output.i18n-helpers") + .unwrap() + .unwrap(); + println!("CFG: {:?}", cfg); + let destination_folder = ctx + .destination + .parent() + .expect("Couldn't get destination folder"); + + let mut renderer = BoookDirectoryRenderer::new(cfg); + renderer + .render_book(destination_folder) + .expect("Failed to render book"); +}