From 278cc82fc76e050608895f905cf7c3a1986a5be5 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 16 Aug 2020 18:39:04 +0200 Subject: [PATCH] Change zola serve to load HTML from memory instead of disk (#1114) * Change zola serve to load HTML from memory instead of disk * Be smart about assets copying * Be a tiny bit smarter on template changes * Add zola serve --fast --- CHANGELOG.md | 4 +- Cargo.lock | 293 ++++++++--- Cargo.toml | 5 +- components/config/src/config/mod.rs | 16 +- components/rebuild/Cargo.toml | 16 - components/rebuild/src/lib.rs | 493 ------------------ components/rebuild/tests/rebuild.rs | 284 ---------- components/rendering/src/shortcode.rs | 6 +- components/site/Cargo.toml | 1 + components/site/src/lib.rs | 332 +++++++----- components/utils/src/fs.rs | 30 +- .../documentation/getting-started/_index.md | 2 +- .../getting-started/cli-usage.md | 2 +- src/cli.rs | 5 + src/cmd/serve.rs | 246 +++++---- src/main.rs | 6 +- 16 files changed, 604 insertions(+), 1137 deletions(-) delete mode 100644 components/rebuild/Cargo.toml delete mode 100644 components/rebuild/src/lib.rs delete mode 100644 components/rebuild/tests/rebuild.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc69a77..52604c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ ### Breaking -- All paths (except colocated assets) now have a leading `/` +- All paths like `current_path`, `page.path`, `section.path` (except colocated assets) now have a leading `/` +- Search index generation for Chinese and Japanese has been disabled by default as it lead to a big increase in +binary size ### Other diff --git a/Cargo.lock b/Cargo.lock index 32970c72..643eb6c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,9 +8,15 @@ checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" [[package]] name = "adler32" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" [[package]] name = "aho-corasick" @@ -113,7 +119,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.3", + "generic-array 0.14.4", ] [[package]] @@ -183,6 +189,15 @@ version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" +[[package]] +name = "cedarwood" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963e82c7b94163808ca3a452608d260b64ba5bc7b5653b4af1af59887899f48d" +dependencies = [ + "smallvec", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -213,9 +228,9 @@ dependencies = [ [[package]] name = "clap" -version = "2.33.1" +version = "2.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" +checksum = "10040cdf04294b565d9e0319955430099ec3813a64c952b86a41200ad714ae48" dependencies = [ "ansi_term", "atty", @@ -390,7 +405,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.3", + "generic-array 0.14.4", ] [[package]] @@ -407,9 +422,9 @@ checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" [[package]] name = "either" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" [[package]] name = "elasticlunr-rs" @@ -417,7 +432,9 @@ version = "2.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35622eb004c8f0c5e7e2032815f3314a93df0db30a1ce5c94e62c1ecc81e22b9" dependencies = [ + "jieba-rs", "lazy_static", + "lindera", "regex", "rust-stemmers", "serde", @@ -427,6 +444,70 @@ dependencies = [ "strum_macros", ] +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.23" @@ -509,12 +590,6 @@ dependencies = [ "utils", ] -[[package]] -name = "fs_extra" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2a4a2034423744d2cc7ca2068453168dcdb82c438419e639a26bd87839c674" - [[package]] name = "fsevent" version = "0.4.0" @@ -637,9 +712,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60fb4bb6bba52f78a471264d9a3b7d026cc0af47b22cd2cffbc0b787ca003e63" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" dependencies = [ "typenum", "version_check", @@ -726,10 +801,11 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" dependencies = [ + "ahash", "autocfg", ] @@ -917,9 +993,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7" +checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9" dependencies = [ "autocfg", "hashbrown", @@ -966,6 +1042,20 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +[[package]] +name = "jieba-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca2de723e93727460917d9542f7ae35a74d03d93923f03380a0238d860d137c" +dependencies = [ + "cedarwood", + "hashbrown", + "lazy_static", + "phf", + "phf_codegen", + "regex", +] + [[package]] name = "jpeg-decoder" version = "0.1.20" @@ -1003,9 +1093,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "levenshtein_automata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a004f877f468548d8d0ac4977456a249d8fabbdb8416c36db163dfc8f2e8ca" [[package]] name = "libc" @@ -1035,6 +1131,70 @@ dependencies = [ "utils", ] +[[package]] +name = "lindera" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b867cd68f5fc19a6d8b8361a6aba55ed2485f243044b70da14b6ba5a128c00" +dependencies = [ + "bincode", + "byteorder", + "encoding", + "lindera-core", + "lindera-dictionary", + "lindera-fst", + "lindera-ipadic", + "serde", + "serde_json", +] + +[[package]] +name = "lindera-core" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b7f132a5d361c1236b28434c632097fb8867ebdf4e4c9ab4f793525bb681ff" +dependencies = [ + "bincode", + "byteorder", + "encoding", + "lindera-fst", + "serde", +] + +[[package]] +name = "lindera-dictionary" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a61a066057d24faab043586633274fa3468c5c54cb8191895659811218a8ec" +dependencies = [ + "bincode", + "byteorder", + "lindera-core", +] + +[[package]] +name = "lindera-fst" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6098a7ca6679296cd2d227efa232f990552c5278394c845bec8a70ab0284ae0" +dependencies = [ + "byteorder", + "levenshtein_automata", + "regex-syntax 0.4.2", + "utf8-ranges", +] + +[[package]] +name = "lindera-ipadic" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f12f44c385a6f4c1ff0863a2f0a91ce5f1ff6c2e0e44c69b37051b56fece112" +dependencies = [ + "bincode", + "byteorder", + "lindera-core", +] + [[package]] name = "line-wrap" version = "0.1.1" @@ -1561,9 +1721,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro-error" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc175e9777c3116627248584e8f8b3e2987405cabe1c0adf7d1dd28f09dc7880" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", @@ -1574,14 +1734,12 @@ dependencies = [ [[package]] name = "proc-macro-error-attr" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc9795ca17eb581285ec44936da7fc2335a3f34f2ddd13118b6f4d515435c50" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "syn", - "syn-mid", "version_check", ] @@ -1703,18 +1861,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "rebuild" -version = "0.1.0" -dependencies = [ - "errors", - "front_matter", - "fs_extra", - "library", - "site", - "tempfile", -] - [[package]] name = "redox_syscall" version = "0.1.57" @@ -1729,7 +1875,7 @@ checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.6.18", "thread_local", ] @@ -1742,6 +1888,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "regex-syntax" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e931c58b93d86f080c734bfd2bce7dd0079ae2331235818133c8be7f422e20e" + [[package]] name = "regex-syntax" version = "0.6.18" @@ -1940,15 +2092,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" dependencies = [ "proc-macro2", "quote", @@ -2019,6 +2174,7 @@ dependencies = [ "front_matter", "glob", "imageproc", + "lazy_static", "library", "link_checker", "rayon", @@ -2053,6 +2209,12 @@ dependencies = [ "deunicode", ] +[[package]] +name = "smallvec" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" + [[package]] name = "socket2" version = "0.3.12" @@ -2134,31 +2296,20 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.36" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdb98bcb1f9d81d07b536179c269ea15999b5d14ea958196413869445bb5250" +checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] -[[package]] -name = "syn-mid" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "syntect" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b43a6ca1829ccb0c933b615c9ea83ffc8793ae240cecbd15119b13d741161d" +checksum = "b57a45fdcf4891bc79f635be5c559210a4cfa464891f969724944c713282eedb" dependencies = [ "bincode", "bitflags", @@ -2168,7 +2319,7 @@ dependencies = [ "lazycell", "onig", "plist", - "regex-syntax", + "regex-syntax 0.6.18", "serde", "serde_derive", "serde_json", @@ -2227,9 +2378,9 @@ dependencies = [ [[package]] name = "tera" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9598067511caa7edb41886c4a29efe6d0564926837bde7dffa4a130ea6cc975" +checksum = "1381c83828bedd5ce4e59473110afa5381ffe523406d9ade4b77c9f7be70ff9a" dependencies = [ "chrono", "chrono-tz", @@ -2374,9 +2525,9 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbdf4ccd1652592b01286a5dbe1e2a77d78afaa34beadd9872a5f7396f92aaa9" +checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" dependencies = [ "cfg-if", "log", @@ -2385,9 +2536,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ae75f0d28ae10786f3b1895c55fe72e79928fd5ccdebb5438c75e93fec178f" +checksum = "db63662723c316b43ca36d833707cc93dff82a02ba3d7e354f342682cc8b3545" dependencies = [ "lazy_static", ] @@ -2528,6 +2679,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" + [[package]] name = "utils" version = "0.1.0" @@ -2816,8 +2973,6 @@ dependencies = [ "lazy_static", "notify", "open", - "rebuild", - "search", "site", "termcolor", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 73393077..28b62fc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ name = "zola" atty = "0.2.11" clap = { version = "2", default-features = false } chrono = "0.4" -lazy_static = "1.1.0" +lazy_static = "1.1" termcolor = "1.0.4" # Used in init to ensure the url given as base_url is a valid one url = "2" @@ -40,15 +40,12 @@ site = { path = "components/site" } errors = { path = "components/errors" } front_matter = { path = "components/front_matter" } utils = { path = "components/utils" } -rebuild = { path = "components/rebuild" } -search = { path = "components/search" } [workspace] members = [ "components/config", "components/errors", "components/front_matter", - "components/rebuild", "components/rendering", "components/site", "components/templates", diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index a429fa5a..0e715354 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -281,7 +281,7 @@ pub fn merge(into: &mut Toml, from: &Toml) -> Result<()> { (false, false) => { // These are not tables so we have nothing to merge Ok(()) - }, + } (true, true) => { // Recursively merge these tables let into_table = into.as_table_mut().unwrap(); @@ -295,7 +295,7 @@ pub fn merge(into: &mut Toml, from: &Toml) -> Result<()> { merge(into_table.get_mut(key).unwrap(), val)?; } Ok(()) - }, + } _ => { // Trying to merge a table with something else Err(Error::msg(&format!("Cannot merge config.toml with theme.toml because the following values have incompatibles types:\n- {}\n - {}", into, from))) @@ -464,7 +464,14 @@ truc = "default" assert_eq!(extra["sub"]["foo"].as_str().unwrap(), "bar".to_string()); assert_eq!(extra["sub"].get("truc").expect("The whole extra.sub table was overriden by theme data, discarding extra.sub.truc").as_str().unwrap(), "default".to_string()); assert_eq!(extra["sub"]["sub"]["foo"].as_str().unwrap(), "bar".to_string()); - assert_eq!(extra["sub"]["sub"].get("truc").expect("Failed to merge subsubtable extra.sub.sub").as_str().unwrap(), "default".to_string()); + assert_eq!( + extra["sub"]["sub"] + .get("truc") + .expect("Failed to merge subsubtable extra.sub.sub") + .as_str() + .unwrap(), + "default".to_string() + ); } const CONFIG_TRANSLATION: &str = r#" @@ -623,7 +630,6 @@ languages = [ assert_eq!("Default language `fr` should not appear both in `config.default_language` and `config.languages`", format!("{}", err)); } - #[test] fn cannot_overwrite_theme_mapping_with_invalid_type() { let config_str = r#" @@ -642,6 +648,4 @@ bar = "baz" // We expect an error here assert_eq!(false, config.add_theme_extra(&theme).is_ok()); } - - } diff --git a/components/rebuild/Cargo.toml b/components/rebuild/Cargo.toml deleted file mode 100644 index 20c76e79..00000000 --- a/components/rebuild/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "rebuild" -version = "0.1.0" -authors = ["Vincent Prouillet "] -edition = "2018" -include = ["src/lib.rs"] - -[dependencies] -errors = { path = "../errors" } -front_matter = { path = "../front_matter" } -library = { path = "../library" } -site = { path = "../site" } - -[dev-dependencies] -tempfile = "3" -fs_extra = "1.1" diff --git a/components/rebuild/src/lib.rs b/components/rebuild/src/lib.rs deleted file mode 100644 index 668c6ae8..00000000 --- a/components/rebuild/src/lib.rs +++ /dev/null @@ -1,493 +0,0 @@ -use std::path::{Component, Path}; - -use errors::{bail, Result}; -use front_matter::{PageFrontMatter, SectionFrontMatter}; -use library::{Page, Section}; -use site::Site; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PageChangesNeeded { - /// Editing `taxonomies` - Taxonomies, - /// Editing `date`, `order` or `weight` - Sort, - /// Editing anything causes a re-render of the page - Render, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum SectionChangesNeeded { - /// Editing `sort_by` - Sort, - /// Editing `title`, `description`, `extra`, `template` or setting `render` to true - Render, - /// Editing `paginate_by`, `paginate_path` or `insert_anchor_links` - RenderWithPages, - /// Setting `render` to false - Delete, - /// Changing `transparent` - Transparent, -} - -/// Evaluates all the params in the front matter that changed so we can do the smallest -/// delta in the serve command -/// Order matters as the actions will be done in insertion order -fn find_section_front_matter_changes( - current: &SectionFrontMatter, - new: &SectionFrontMatter, -) -> Vec { - let mut changes_needed = vec![]; - - if current.sort_by != new.sort_by { - changes_needed.push(SectionChangesNeeded::Sort); - } - - if current.transparent != new.transparent { - changes_needed.push(SectionChangesNeeded::Transparent); - } - - // We want to hide the section - // TODO: what to do on redirect_path change? - if current.render && !new.render { - changes_needed.push(SectionChangesNeeded::Delete); - // Nothing else we can do - return changes_needed; - } - - if current.paginate_by != new.paginate_by - || current.paginate_path != new.paginate_path - || current.insert_anchor_links != new.insert_anchor_links - { - changes_needed.push(SectionChangesNeeded::RenderWithPages); - // Nothing else we can do - return changes_needed; - } - - // Any new change will trigger a re-rendering of the section page only - changes_needed.push(SectionChangesNeeded::Render); - changes_needed -} - -/// Evaluates all the params in the front matter that changed so we can do the smallest -/// delta in the serve command -/// Order matters as the actions will be done in insertion order -fn find_page_front_matter_changes( - current: &PageFrontMatter, - other: &PageFrontMatter, -) -> Vec { - let mut changes_needed = vec![]; - - if current.taxonomies != other.taxonomies { - changes_needed.push(PageChangesNeeded::Taxonomies); - } - - if current.date != other.date || current.order != other.order || current.weight != other.weight - { - changes_needed.push(PageChangesNeeded::Sort); - } - - changes_needed.push(PageChangesNeeded::Render); - changes_needed -} - -/// Handles a path deletion: could be a page, a section, a folder -fn delete_element(site: &mut Site, path: &Path, is_section: bool) -> Result<()> { - { - let mut library = site.library.write().unwrap(); - // Ignore the event if this path was not known - if !library.contains_section(&path.to_path_buf()) - && !library.contains_page(&path.to_path_buf()) - { - return Ok(()); - } - - if is_section { - if let Some(s) = library.remove_section(&path.to_path_buf()) { - site.permalinks.remove(&s.file.relative); - } - } else if let Some(p) = library.remove_page(&path.to_path_buf()) { - site.permalinks.remove(&p.file.relative); - } - } - - // We might have delete the root _index.md so ensure we have at least the default one - // before populating - site.create_default_index_sections()?; - site.populate_sections(); - site.populate_taxonomies()?; - // Ensure we have our fn updated so it doesn't contain the permalink(s)/section/page deleted - site.register_early_global_fns(); - site.register_tera_global_fns(); - // Deletion is something that doesn't happen all the time so we - // don't need to optimise it too much - site.build() -} - -/// Handles a `_index.md` (a section) being edited in some ways -fn handle_section_editing(site: &mut Site, path: &Path) -> Result<()> { - let section = Section::from_file(path, &site.config, &site.base_path)?; - let pathbuf = path.to_path_buf(); - match site.add_section(section, true)? { - // Updating a section - Some(prev) => { - site.populate_sections(); - site.process_images()?; - { - let library = site.library.read().unwrap(); - - if library.get_section(&pathbuf).unwrap().meta == prev.meta { - // Front matter didn't change, only content did - // so we render only the section page, not its pages - return site.render_section(&library.get_section(&pathbuf).unwrap(), false); - } - } - - // Front matter changed - let changes = find_section_front_matter_changes( - &site.library.read().unwrap().get_section(&pathbuf).unwrap().meta, - &prev.meta, - ); - for change in changes { - // Sort always comes first if present so the rendering will be fine - match change { - SectionChangesNeeded::Sort => { - site.register_tera_global_fns(); - } - SectionChangesNeeded::Render => site.render_section( - &site.library.read().unwrap().get_section(&pathbuf).unwrap(), - false, - )?, - SectionChangesNeeded::RenderWithPages => site.render_section( - &site.library.read().unwrap().get_section(&pathbuf).unwrap(), - true, - )?, - // not a common enough operation to make it worth optimizing - SectionChangesNeeded::Delete | SectionChangesNeeded::Transparent => { - site.build()?; - } - }; - } - Ok(()) - } - // New section, only render that one - None => { - site.populate_sections(); - site.process_images()?; - site.register_tera_global_fns(); - site.render_section(&site.library.read().unwrap().get_section(&pathbuf).unwrap(), true) - } - } -} - -macro_rules! render_parent_sections { - ($site: expr, $path: expr) => { - for s in $site.library.read().unwrap().find_parent_sections($path) { - $site.render_section(s, false)?; - } - }; -} - -/// Handles a page being edited in some ways -fn handle_page_editing(site: &mut Site, path: &Path) -> Result<()> { - let page = Page::from_file(path, &site.config, &site.base_path)?; - let pathbuf = path.to_path_buf(); - match site.add_page(page, true)? { - // Updating a page - Some(prev) => { - site.populate_sections(); - site.populate_taxonomies()?; - site.register_tera_global_fns(); - site.process_images()?; - { - let library = site.library.read().unwrap(); - - // Front matter didn't change, only content did - if library.get_page(&pathbuf).unwrap().meta == prev.meta { - // Other than the page itself, the summary might be seen - // on a paginated list for a blog for example - if library.get_page(&pathbuf).unwrap().summary.is_some() { - render_parent_sections!(site, path); - } - return site.render_page(&library.get_page(&pathbuf).unwrap()); - } - } - - // Front matter changed - let changes = find_page_front_matter_changes( - &site.library.read().unwrap().get_page(&pathbuf).unwrap().meta, - &prev.meta, - ); - - for change in changes { - site.register_tera_global_fns(); - - // Sort always comes first if present so the rendering will be fine - match change { - PageChangesNeeded::Taxonomies => { - site.populate_taxonomies()?; - site.render_taxonomies()?; - } - PageChangesNeeded::Sort => { - site.render_index()?; - } - PageChangesNeeded::Render => { - render_parent_sections!(site, path); - site.render_page( - &site.library.read().unwrap().get_page(&path.to_path_buf()).unwrap(), - )?; - } - }; - } - Ok(()) - } - // It's a new page! - None => { - site.populate_sections(); - site.populate_taxonomies()?; - site.register_early_global_fns(); - site.register_tera_global_fns(); - site.process_images()?; - // No need to optimise that yet, we can revisit if it becomes an issue - site.build() - } - } -} - -/// What happens when we rename a file/folder in the content directory. -/// Note that this is only called for folders when it isn't empty -pub fn after_content_rename(site: &mut Site, old: &Path, new: &Path) -> Result<()> { - let new_path = if new.is_dir() { - if new.join("_index.md").exists() { - // This is a section keep the dir folder to differentiate from renaming _index.md - // which doesn't do the same thing - new.to_path_buf() - } else if new.join("index.md").exists() { - new.join("index.md") - } else { - bail!("Got unexpected folder {:?} while handling renaming that was not expected", new); - } - } else { - new.to_path_buf() - }; - - // A section folder has been renamed: just reload the whole site and rebuild it as we - // do not really know what needs to be rendered - if new_path.is_dir() { - site.load()?; - return site.build(); - } - - // We ignore renames on non-markdown files for now - if let Some(ext) = new_path.extension() { - if ext != "md" { - return Ok(()); - } - } - - // Renaming a file to _index.md, let the section editing do something and hope for the best - if new_path.file_name().unwrap() == "_index.md" { - // We aren't entirely sure where the original thing was so just try to delete whatever was - // at the old path - { - let mut library = site.library.write().unwrap(); - library.remove_page(&old.to_path_buf()); - library.remove_section(&old.to_path_buf()); - } - return handle_section_editing(site, &new_path); - } - - // If it is a page, just delete what was there before and - // fake it's a new page - let old_path = if new_path.file_name().unwrap() == "index.md" { - old.join("index.md") - } else { - old.to_path_buf() - }; - site.library.write().unwrap().remove_page(&old_path); - - let ignored_content_globset = site.config.ignored_content_globset.clone(); - let is_ignored_file = match ignored_content_globset { - Some(gs) => gs.is_match(new), - None => false, - }; - - if !is_ignored_file { - return handle_page_editing(site, &new_path); - } - Ok(()) -} - -fn is_section(path: &str, languages_codes: &[&str]) -> bool { - if path == "_index.md" { - return true; - } - - for language_code in languages_codes { - let lang_section_string = format!("_index.{}.md", language_code); - if path == lang_section_string { - return true; - } - } - - false -} - -/// What happens when a section or a page is created/edited -pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { - let is_section = { - let languages_codes = site.config.languages_codes(); - is_section(path.file_name().unwrap().to_str().unwrap(), &languages_codes) - }; - - let is_md = path.extension().unwrap() == "md"; - let index = path.parent().unwrap().join("index.md"); - - let mut potential_indices = vec![path.parent().unwrap().join("index.md")]; - for language in &site.config.languages { - potential_indices.push(path.parent().unwrap().join(format!("index.{}.md", language.code))); - } - let colocated_index = potential_indices.contains(&path.to_path_buf()); - - // A few situations can happen: - // 1. Change on .md files - // a. Is there already an `index.md`? Return an error if it's something other than delete - // b. Deleted? remove the element - // c. Edited? - // 1. filename is `_index.md`, this is a section - // 1. it's a page otherwise - // 2. Change on non .md files - // a. Try to find a corresponding `_index.md` - // 1. Nothing? Return Ok - // 2. Something? Update the page - if is_md { - // only delete if it was able to be added in the first place - if !index.exists() && !path.exists() { - return delete_element(site, path, is_section); - } - - // Added another .md in a assets directory - if index.exists() && path.exists() && !colocated_index { - bail!( - "Change on {:?} detected but only files named `index.md` with an optional language code are allowed", - path.display() - ); - } else if index.exists() && !path.exists() { - // deleted the wrong .md, do nothing - return Ok(()); - } - - if is_section { - handle_section_editing(site, path) - } else { - handle_page_editing(site, path) - } - } else if index.exists() { - handle_page_editing(site, &index) - } else { - Ok(()) - } -} - -/// What happens when a template is changed -pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> { - site.tera.full_reload()?; - let filename = path.file_name().unwrap().to_str().unwrap(); - - match filename { - "sitemap.xml" => site.render_sitemap(), - filename if filename == site.config.feed_filename => { - // FIXME: this is insufficient; for multilingual sites, it’s rendering the wrong - // content into the root feed, and it’s not regenerating any of the other feeds (other - // languages or taxonomies with feed enabled). - site.render_feed( - site.library.read().unwrap().pages_values(), - None, - &site.config.default_language, - |c| c, - ) - } - "split_sitemap_index.xml" => site.render_sitemap(), - "robots.txt" => site.render_robots(), - "single.html" | "list.html" => site.render_taxonomies(), - "page.html" => { - site.render_sections()?; - site.render_orphan_pages() - } - "section.html" => site.render_sections(), - "404.html" => site.render_404(), - // Either the index or some unknown template changed - // We can't really know what this change affects so rebuild all - // the things - _ => { - // If we are updating a shortcode, re-render the markdown of all pages/site - // because we have no clue which one needs rebuilding - // Same for the anchor-link template - // TODO: look if there the shortcode is used in the markdown instead of re-rendering - // everything - if filename == "anchor-link.html" - || path.components().any(|x| x == Component::Normal("shortcodes".as_ref())) - { - site.render_markdown()?; - } - site.populate_sections(); - site.populate_taxonomies()?; - site.render_sections()?; - site.process_images()?; - site.render_orphan_pages()?; - site.render_taxonomies() - } - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use super::{ - find_page_front_matter_changes, find_section_front_matter_changes, PageChangesNeeded, - SectionChangesNeeded, - }; - use front_matter::{PageFrontMatter, SectionFrontMatter, SortBy}; - - #[test] - fn can_find_taxonomy_changes_in_page_frontmatter() { - let mut taxonomies = HashMap::new(); - taxonomies.insert("tags".to_string(), vec!["a tag".to_string()]); - let new = PageFrontMatter { taxonomies, ..PageFrontMatter::default() }; - let changes = find_page_front_matter_changes(&PageFrontMatter::default(), &new); - assert_eq!(changes, vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Render]); - } - - #[test] - fn can_find_multiple_changes_in_page_frontmatter() { - let mut taxonomies = HashMap::new(); - taxonomies.insert("categories".to_string(), vec!["a category".to_string()]); - let current = PageFrontMatter { taxonomies, order: Some(1), ..PageFrontMatter::default() }; - let changes = find_page_front_matter_changes(¤t, &PageFrontMatter::default()); - assert_eq!( - changes, - vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render] - ); - } - - #[test] - fn can_find_sort_changes_in_section_frontmatter() { - let new = SectionFrontMatter { sort_by: SortBy::Date, ..SectionFrontMatter::default() }; - let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new); - assert_eq!(changes, vec![SectionChangesNeeded::Sort, SectionChangesNeeded::Render]); - } - - #[test] - fn can_find_render_changes_in_section_frontmatter() { - let new = SectionFrontMatter { render: false, ..SectionFrontMatter::default() }; - let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new); - assert_eq!(changes, vec![SectionChangesNeeded::Delete]); - } - - #[test] - fn can_find_paginate_by_changes_in_section_frontmatter() { - let new = SectionFrontMatter { paginate_by: Some(10), ..SectionFrontMatter::default() }; - let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new); - assert_eq!(changes, vec![SectionChangesNeeded::RenderWithPages]); - } -} diff --git a/components/rebuild/tests/rebuild.rs b/components/rebuild/tests/rebuild.rs deleted file mode 100644 index 06a7620e..00000000 --- a/components/rebuild/tests/rebuild.rs +++ /dev/null @@ -1,284 +0,0 @@ -use std::env; -use std::fs::{self, File}; -use std::io::prelude::*; - -use fs_extra::dir; -use site::Site; -use tempfile::tempdir; - -use rebuild::{after_content_change, after_content_rename}; - -// Loads the test_site in a tempdir and build it there -// Returns (site_path_in_tempdir, site) -macro_rules! load_and_build_site { - ($tmp_dir: expr, $site: expr) => {{ - let mut path = - env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); - path.push($site); - let mut options = dir::CopyOptions::new(); - options.copy_inside = true; - dir::copy(&path, &$tmp_dir, &options).unwrap(); - - let site_path = $tmp_dir.path().join($site); - let config_file = site_path.join("config.toml"); - let mut site = Site::new(&site_path, &config_file).unwrap(); - site.load().unwrap(); - let public = &site_path.join("public"); - site.set_output_path(&public); - site.build().unwrap(); - - (site_path, site) - }}; -} - -/// Replace the file at the path (starting from root) by the given content -/// and return the file path that was modified -macro_rules! edit_file { - ($site_path: expr, $path: expr, $content: expr) => {{ - let mut t = $site_path.clone(); - for c in $path.split('/') { - t.push(c); - } - let mut file = File::create(&t).expect("Could not open/create file"); - file.write_all($content).expect("Could not write to the file"); - t - }}; -} - -macro_rules! file_contains { - ($site_path: expr, $path: expr, $text: expr) => {{ - let mut path = $site_path.clone(); - for component in $path.split("/") { - path.push(component); - } - let mut file = File::open(&path).unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s).unwrap(); - println!("{:?} -> {}", path, s); - s.contains($text) - }}; -} - -/// Rename a file or a folder to the new given name -macro_rules! rename { - ($site_path: expr, $path: expr, $new_name: expr) => {{ - let mut t = $site_path.clone(); - for c in $path.split('/') { - t.push(c); - } - let mut new_path = t.parent().unwrap().to_path_buf(); - new_path.push($new_name); - fs::rename(&t, &new_path).unwrap(); - println!("Renamed {:?} to {:?}", t, new_path); - (t, new_path) - }}; -} - -#[test] -fn can_rebuild_after_simple_change_to_page_content() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let file_path = edit_file!( - site_path, - "content/rebuild/first.md", - br#" -+++ -title = "first" -weight = 1 -date = 2017-01-01 -+++ - -Some content"# - ); - - let res = after_content_change(&mut site, &file_path); - assert!(res.is_ok()); - assert!(file_contains!(site_path, "public/rebuild/first/index.html", "

Some content

")); -} - -#[test] -fn can_rebuild_after_title_change_page_global_func_usage() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let file_path = edit_file!( - site_path, - "content/rebuild/first.md", - br#" -+++ -title = "Premier" -weight = 10 -date = 2017-01-01 -+++ - -# A title"# - ); - - let res = after_content_change(&mut site, &file_path); - assert!(res.is_ok()); - assert!(file_contains!(site_path, "public/rebuild/index.html", "

Premier

")); -} - -#[test] -fn can_rebuild_after_sort_change_in_section() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let file_path = edit_file!( - site_path, - "content/rebuild/_index.md", - br#" -+++ -paginate_by = 1 -sort_by = "weight" -template = "rebuild.html" -+++ -"# - ); - - let res = after_content_change(&mut site, &file_path); - assert!(res.is_ok()); - assert!(file_contains!( - site_path, - "public/rebuild/index.html", - "

first

second

" - )); -} - -#[test] -fn can_rebuild_after_transparent_change() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let file_path = edit_file!( - site_path, - "content/posts/2018/_index.md", - br#" -+++ -transparent = false -render = false -+++ -"# - ); - // Also remove pagination from posts section so we check whether the transparent page title - // is there or not without dealing with pagination - edit_file!( - site_path, - "content/posts/_index.md", - br#" -+++ -template = "section.html" -insert_anchor_links = "left" -+++ -"# - ); - - let res = after_content_change(&mut site, &file_path); - assert!(res.is_ok()); - assert!(!file_contains!(site_path, "public/posts/index.html", "A transparent page")); -} - -#[test] -fn can_rebuild_after_renaming_page() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let (old_path, new_path) = rename!(site_path, "content/posts/simple.md", "hard.md"); - - let res = after_content_rename(&mut site, &old_path, &new_path); - println!("{:?}", res); - assert!(res.is_ok()); - assert!(file_contains!(site_path, "public/posts/hard/index.html", "A simple page")); -} - -// https://github.com/Keats/gutenberg/issues/385 -#[test] -fn can_rebuild_after_renaming_colocated_asset_folder() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let (old_path, new_path) = - rename!(site_path, "content/posts/with-assets", "with-assets-updated"); - assert!(file_contains!(site_path, "content/posts/with-assets-updated/index.md", "Hello")); - - let res = after_content_rename(&mut site, &old_path, &new_path); - println!("{:?}", res); - assert!(res.is_ok()); - assert!(file_contains!( - site_path, - "public/posts/with-assets-updated/index.html", - "Hello world" - )); -} - -// https://github.com/Keats/gutenberg/issues/385 -#[test] -fn can_rebuild_after_renaming_section_folder() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let (old_path, new_path) = rename!(site_path, "content/posts", "new-posts"); - assert!(file_contains!(site_path, "content/new-posts/simple.md", "simple")); - - let res = after_content_rename(&mut site, &old_path, &new_path); - assert!(res.is_ok()); - - assert!(file_contains!(site_path, "public/new-posts/simple/index.html", "simple")); -} - -#[test] -fn can_rebuild_after_renaming_non_md_asset_in_colocated_folder() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let (old_path, new_path) = - rename!(site_path, "content/posts/with-assets/zola.png", "gutenberg.png"); - - // Testing that we don't try to load some images as markdown or something - let res = after_content_rename(&mut site, &old_path, &new_path); - assert!(res.is_ok()); -} - -#[test] -fn can_rebuild_after_deleting_file() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let path = site_path.join("content").join("posts").join("fixed-slug.md"); - fs::remove_file(&path).unwrap(); - - let res = after_content_change(&mut site, &path); - println!("{:?}", res); - assert!(res.is_ok()); -} - -#[test] -fn can_rebuild_after_editing_in_colocated_asset_folder_with_language() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site_i18n"); - let file_path = edit_file!( - site_path, - "content/blog/with-assets/index.fr.md", - br#" -+++ -date = 2018-11-11 -+++ - -Edite -"# - ); - - let res = after_content_change(&mut site, &file_path); - println!("{:?}", res); - assert!(res.is_ok()); - assert!(file_contains!(site_path, "public/fr/blog/with-assets/index.html", "Edite")); -} - -// https://github.com/getzola/zola/issues/620 -#[test] -fn can_rebuild_after_renaming_section_and_deleting_file() { - let tmp_dir = tempdir().expect("create temp dir"); - let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site"); - let (old_path, new_path) = rename!(site_path, "content/posts/", "post/"); - let res = after_content_rename(&mut site, &old_path, &new_path); - assert!(res.is_ok()); - - let path = site_path.join("content").join("_index.md"); - fs::remove_file(&path).unwrap(); - - let res = after_content_change(&mut site, &path); - println!("{:?}", res); - assert!(res.is_ok()); -} diff --git a/components/rendering/src/shortcode.rs b/components/rendering/src/shortcode.rs index 308da13c..6e1520b9 100644 --- a/components/rendering/src/shortcode.rs +++ b/components/rendering/src/shortcode.rs @@ -443,7 +443,11 @@ Some body {{ hello() }}{%/* end */%}"#, #[test] fn shortcodes_that_emit_markdown() { let mut tera = Tera::default(); - tera.add_raw_template("shortcodes/youtube.md", "{% for i in [1,2,3] %}\n* {{ i }}\n{%- endfor %}").unwrap(); + tera.add_raw_template( + "shortcodes/youtube.md", + "{% for i in [1,2,3] %}\n* {{ i }}\n{%- endfor %}", + ) + .unwrap(); let res = render_shortcodes("{{ youtube() }}", &tera); assert_eq!(res, "* 1\n* 2\n* 3"); } diff --git a/components/site/Cargo.toml b/components/site/Cargo.toml index 300c6af5..12373c84 100644 --- a/components/site/Cargo.toml +++ b/components/site/Cargo.toml @@ -12,6 +12,7 @@ rayon = "1" serde = "1" serde_derive = "1" sass-rs = "0.2" +lazy_static = "1.1" errors = { path = "../errors" } config = { path = "../config" } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index f03ce70f..dc2befd3 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -5,11 +5,12 @@ pub mod sitemap; pub mod tpls; use std::collections::HashMap; -use std::fs::{copy, remove_dir_all}; +use std::fs::remove_dir_all; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, RwLock}; use glob::glob; +use lazy_static::lazy_static; use rayon::prelude::*; use tera::{Context, Tera}; @@ -18,10 +19,26 @@ use errors::{bail, Error, Result}; use front_matter::InsertAnchor; use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy}; use templates::render_redirect_template; -use utils::fs::{copy_directory, create_directory, create_file, ensure_directory_exists}; +use utils::fs::{ + copy_directory, copy_file_if_needed, create_directory, create_file, ensure_directory_exists, +}; use utils::net::get_available_port; use utils::templates::render_template; +lazy_static! { + /// The in-memory rendered map content + pub static ref SITE_CONTENT: Arc>> = Arc::new(RwLock::new(HashMap::new())); +} + +/// Where are we building the site +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum BuildMode { + /// On the filesystem -> `zola build`, The path is the `output_path` + Disk, + /// In memory for the content -> `zola serve` + Memory, +} + #[derive(Debug)] pub struct Site { /// The base path of the zola site @@ -43,6 +60,7 @@ pub struct Site { pub library: Arc>, /// Whether to load draft pages include_drafts: bool, + build_mode: BuildMode, } impl Site { @@ -65,6 +83,7 @@ impl Site { let static_path = path.join("static"); let imageproc = imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url); + let output_path = path.join("public"); let site = Site { base_path: path.to_path_buf(), @@ -72,7 +91,7 @@ impl Site { tera, imageproc: Arc::new(Mutex::new(imageproc)), live_reload: None, - output_path: path.join("public"), + output_path, content_path, static_path, taxonomies: Vec::new(), @@ -80,11 +99,19 @@ impl Site { include_drafts: false, // We will allocate it properly later on library: Arc::new(RwLock::new(Library::new(0, 0, false))), + build_mode: BuildMode::Disk, }; Ok(site) } + /// Enable some `zola serve` related options + pub fn enable_serve_mode(&mut self) { + SITE_CONTENT.write().unwrap().clear(); + self.config.enable_serve_mode(); + self.build_mode = BuildMode::Memory; + } + /// Set the site to load the drafts. /// Needs to be called before loading it pub fn include_drafts(&mut self) { @@ -111,6 +138,18 @@ impl Site { self.live_reload = get_available_port(port_to_avoid); } + /// Only used in `zola serve` to re-use the initial websocket port + pub fn enable_live_reload_with_port(&mut self, live_reload_port: u16) { + self.live_reload = Some(live_reload_port); + } + + /// Reloads the templates and rebuild the site without re-rendering the Markdown. + pub fn reload_templates(&mut self) -> Result<()> { + self.tera.full_reload()?; + // TODO: be smarter than that, no need to recompile sass for example + self.build() + } + pub fn set_base_url(&mut self, base_url: String) { let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)"); imageproc.set_base_url(&base_url); @@ -203,10 +242,10 @@ impl Site { // taxonomy Tera fns are loaded in `register_early_global_fns` // so we do need to populate it first. self.populate_taxonomies()?; - self.register_early_global_fns(); + tpls::register_early_global_fns(self); self.populate_sections(); self.render_markdown()?; - self.register_tera_global_fns(); + tpls::register_tera_global_fns(self); // Needs to be done after rendering markdown as we only get the anchors at that point link_checking::check_internal_links_with_anchors(&self)?; @@ -301,48 +340,58 @@ impl Site { Ok(()) } - // TODO: remove me in favour of the direct call to the fn once rebuild has changed - pub fn register_early_global_fns(&mut self) { - tpls::register_early_global_fns(self); - } - - // TODO: remove me in favour of the direct call to the fn once rebuild has changed - pub fn register_tera_global_fns(&mut self) { - tpls::register_tera_global_fns(self); - } - /// Add a page to the site - /// The `render` parameter is used in the serve command, when rebuilding a page. - /// If `true`, it will also render the markdown for that page - /// Returns the previous page struct if there was one at the same path - pub fn add_page(&mut self, mut page: Page, render: bool) -> Result> { + /// The `render` parameter is used in the serve command with --fast, when rebuilding a page. + pub fn add_page(&mut self, mut page: Page, render_md: bool) -> Result<()> { self.permalinks.insert(page.file.relative.clone(), page.permalink.clone()); - if render { + if render_md { let insert_anchor = self.find_parent_section_insert_anchor(&page.file.parent, &page.lang); page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?; } + let mut library = self.library.write().expect("Get lock for add_page"); - let prev = library.remove_page(&page.file.path); + library.remove_page(&page.file.path); library.insert_page(page); - Ok(prev) + Ok(()) + } + + /// Adds a page to the site and render it + /// Only used in `zola serve --fast` + pub fn add_and_render_page(&mut self, path: &Path) -> Result<()> { + let page = Page::from_file(path, &self.config, &self.base_path)?; + self.add_page(page, true)?; + self.populate_sections(); + self.populate_taxonomies()?; + let library = self.library.read().unwrap(); + let page = library.get_page(&path).unwrap(); + self.render_page(&page) } /// Add a section to the site - /// The `render` parameter is used in the serve command, when rebuilding a page. - /// If `true`, it will also render the markdown for that page - /// Returns the previous section struct if there was one at the same path - pub fn add_section(&mut self, mut section: Section, render: bool) -> Result> { + /// The `render` parameter is used in the serve command with --fast, when rebuilding a page. + pub fn add_section(&mut self, mut section: Section, render_md: bool) -> Result<()> { self.permalinks.insert(section.file.relative.clone(), section.permalink.clone()); - if render { + if render_md { section.render_markdown(&self.permalinks, &self.tera, &self.config)?; } let mut library = self.library.write().expect("Get lock for add_section"); - let prev = library.remove_section(§ion.file.path); + library.remove_section(§ion.file.path); library.insert_section(section); - Ok(prev) + Ok(()) + } + + /// Adds a section to the site and render it + /// Only used in `zola serve --fast` + pub fn add_and_render_section(&mut self, path: &Path) -> Result<()> { + let section = Section::from_file(path, &self.config, &self.base_path)?; + self.add_section(section, true)?; + self.populate_sections(); + let library = self.library.read().unwrap(); + let section = library.get_section(&path).unwrap(); + self.render_section(§ion, true) } /// Finds the insert_anchor for the parent section of the directory at `path`. @@ -437,32 +486,70 @@ impl Site { Ok(()) } - /// Renders a single content page - pub fn render_page(&self, page: &Page) -> Result<()> { + /// Handles whether to write to disk or to memory + pub fn write_content( + &self, + components: &[&str], + filename: &str, + content: String, + create_dirs: bool, + ) -> Result { + let write_dirs = self.build_mode == BuildMode::Disk || create_dirs; ensure_directory_exists(&self.output_path)?; - // Copy the nesting of the content directory if we have sections for that page let mut current_path = self.output_path.to_path_buf(); - for component in page.path.split('/') { + for component in components { current_path.push(component); - if !current_path.exists() { + if !current_path.exists() && write_dirs { create_directory(¤t_path)?; } } - // Make sure the folder exists - create_directory(¤t_path)?; + if write_dirs { + create_directory(¤t_path)?; + } - // Finally, create a index.html file there with the page rendered + match self.build_mode { + BuildMode::Disk => { + let end_path = current_path.join(filename); + create_file(&end_path, &content)?; + } + BuildMode::Memory => { + let path = if filename != "index.html" { + let p = current_path.join(filename); + p.as_os_str().to_string_lossy().replace("public/", "/") + } else { + // TODO" remove unwrap + let p = current_path.strip_prefix("public").unwrap(); + p.as_os_str().to_string_lossy().into_owned() + } + .trim_end_matches('/') + .to_owned(); + &SITE_CONTENT.write().unwrap().insert(path, content); + } + } + + Ok(current_path) + } + + fn copy_asset(&self, src: &Path, dest: &PathBuf) -> Result<()> { + copy_file_if_needed(src, dest, self.config.hard_link_static) + } + + /// Renders a single content page + pub fn render_page(&self, page: &Page) -> Result<()> { let output = page.render_html(&self.tera, &self.config, &self.library.read().unwrap())?; - create_file(¤t_path.join("index.html"), &self.inject_livereload(output))?; + let content = self.inject_livereload(output); + let components: Vec<&str> = page.path.split('/').collect(); + let current_path = + self.write_content(&components, "index.html", content, !page.assets.is_empty())?; // Copy any asset we found previously into the same directory as the index.html for asset in &page.assets { let asset_path = asset.as_path(); - copy( + self.copy_asset( &asset_path, ¤t_path .join(asset_path.file_name().expect("Couldn't get filename from page asset")), @@ -472,9 +559,12 @@ impl Site { Ok(()) } - /// Deletes the `public` directory and builds the site + /// Deletes the `public` directory (only for `zola build`) and builds the site pub fn build(&self) -> Result<()> { - self.clean()?; + // Do not clean on `zola serve` otherwise we end up copying assets all the time + if self.build_mode == BuildMode::Disk { + self.clean()?; + } // Generate/move all assets before rendering any content if let Some(ref theme) = self.config.theme { @@ -537,6 +627,8 @@ impl Site { pub fn build_search_index(&self) -> Result<()> { ensure_directory_exists(&self.output_path)?; + // TODO: add those to the SITE_CONTENT map + // index first create_file( &self.output_path.join(&format!("search_index.{}.js", self.config.default_language)), @@ -573,7 +665,6 @@ impl Site { } fn render_alias(&self, alias: &str, permalink: &str) -> Result<()> { - let mut output_path = self.output_path.to_path_buf(); let mut split = alias.split('/').collect::>(); // If the alias ends with an html file name, use that instead of mapping @@ -586,19 +677,9 @@ impl Site { } None => "index.html", }; - - for component in split { - output_path.push(&component); - - if !output_path.exists() { - create_directory(&output_path)?; - } - } - - create_file( - &output_path.join(page_name), - &render_redirect_template(&permalink, &self.tera)?, - ) + let content = render_redirect_template(&permalink, &self.tera)?; + self.write_content(&split, page_name, content, false)?; + Ok(()) } /// Renders all the aliases for each page/section: a magic HTML template that redirects to @@ -625,7 +706,9 @@ impl Site { let mut context = Context::new(); context.insert("config", &self.config); let output = render_template("404.html", &self.tera, context, &self.config.theme)?; - create_file(&self.output_path.join("404.html"), &self.inject_livereload(output)) + let content = self.inject_livereload(output); + self.write_content(&[], "404.html", content, false)?; + Ok(()) } /// Renders robots.txt @@ -633,10 +716,9 @@ impl Site { ensure_directory_exists(&self.output_path)?; let mut context = Context::new(); context.insert("config", &self.config); - create_file( - &self.output_path.join("robots.txt"), - &render_template("robots.txt", &self.tera, context, &self.config.theme)?, - ) + let content = render_template("robots.txt", &self.tera, context, &self.config.theme)?; + self.write_content(&[], "robots.txt", content, false)?; + Ok(()) } /// Renders all taxonomies @@ -654,33 +736,37 @@ impl Site { } ensure_directory_exists(&self.output_path)?; - let output_path = if taxonomy.kind.lang != self.config.default_language { - let mid_path = self.output_path.join(&taxonomy.kind.lang); - create_directory(&mid_path)?; - mid_path.join(&taxonomy.kind.name) - } else { - self.output_path.join(&taxonomy.kind.name) - }; + + let mut components = Vec::new(); + if taxonomy.kind.lang != self.config.default_language { + components.push(taxonomy.kind.lang.as_ref()); + } + + components.push(taxonomy.kind.name.as_ref()); + let list_output = taxonomy.render_all_terms(&self.tera, &self.config, &self.library.read().unwrap())?; - create_directory(&output_path)?; - create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?; + let content = self.inject_livereload(list_output); + self.write_content(&components, "index.html", content, false)?; + let library = self.library.read().unwrap(); taxonomy .items .par_iter() .map(|item| { - let path = output_path.join(&item.slug); + let mut comp = components.clone(); + comp.push(&item.slug); + if taxonomy.kind.is_paginated() { self.render_paginated( - &path, + comp.clone(), &Paginator::from_taxonomy(&taxonomy, item, &library), )?; } else { let single_output = taxonomy.render_term(item, &self.tera, &self.config, &library)?; - create_directory(&path)?; - create_file(&path.join("index.html"), &self.inject_livereload(single_output))?; + let content = self.inject_livereload(single_output); + self.write_content(&comp, "index.html", content, false)?; } if taxonomy.kind.feed { @@ -719,8 +805,8 @@ impl Site { // Create single sitemap let mut context = Context::new(); context.insert("entries", &all_sitemap_entries); - let sitemap = &render_template("sitemap.xml", &self.tera, context, &self.config.theme)?; - create_file(&self.output_path.join("sitemap.xml"), sitemap)?; + let sitemap = render_template("sitemap.xml", &self.tera, context, &self.config.theme)?; + self.write_content(&[], "sitemap.xml", sitemap, false)?; return Ok(()); } @@ -731,10 +817,10 @@ impl Site { { let mut context = Context::new(); context.insert("entries", &chunk); - let sitemap = &render_template("sitemap.xml", &self.tera, context, &self.config.theme)?; + let sitemap = render_template("sitemap.xml", &self.tera, context, &self.config.theme)?; let file_name = format!("sitemap{}.xml", i + 1); - create_file(&self.output_path.join(&file_name), sitemap)?; - let mut sitemap_url: String = self.config.make_permalink(&file_name); + self.write_content(&[], &file_name, sitemap, false)?; + let mut sitemap_url = self.config.make_permalink(&file_name); sitemap_url.pop(); // Remove trailing slash sitemap_index.push(sitemap_url); } @@ -742,13 +828,13 @@ impl Site { // Create main sitemap that reference numbered sitemaps let mut main_context = Context::new(); main_context.insert("sitemaps", &sitemap_index); - let sitemap = &render_template( + let sitemap = render_template( "split_sitemap_index.xml", &self.tera, main_context, &self.config.theme, )?; - create_file(&self.output_path.join("sitemap.xml"), sitemap)?; + self.write_content(&[], "sitemap.xml", sitemap, false)?; Ok(()) } @@ -773,16 +859,19 @@ impl Site { let feed_filename = &self.config.feed_filename; if let Some(ref base) = base_path { - let mut output_path = self.output_path.clone(); + let mut components = Vec::new(); for component in base.components() { - output_path.push(component); - if !output_path.exists() { - create_directory(&output_path)?; - } + // TODO: avoid cloning the paths + components.push(component.as_os_str().to_string_lossy().as_ref().to_string()); } - create_file(&output_path.join(feed_filename), &feed)?; + self.write_content( + &components.iter().map(|x| x.as_ref()).collect::>(), + &feed_filename, + feed, + false, + )?; } else { - create_file(&self.output_path.join(feed_filename), &feed)?; + self.write_content(&[], &feed_filename, feed, false)?; } Ok(()) } @@ -791,18 +880,23 @@ impl Site { pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> { ensure_directory_exists(&self.output_path)?; let mut output_path = self.output_path.clone(); + let mut components: Vec<&str> = Vec::new(); + let create_directories = self.build_mode == BuildMode::Disk || !section.assets.is_empty(); if section.lang != self.config.default_language { + components.push(§ion.lang); output_path.push(§ion.lang); - if !output_path.exists() { + + if !output_path.exists() && create_directories { create_directory(&output_path)?; } } for component in §ion.file.components { + components.push(component); output_path.push(component); - if !output_path.exists() { + if !output_path.exists() && create_directories { create_directory(&output_path)?; } } @@ -810,7 +904,7 @@ impl Site { // Copy any asset we found previously into the same directory as the index.html for asset in §ion.assets { let asset_path = asset.as_path(); - copy( + self.copy_asset( &asset_path, &output_path.join( asset_path.file_name().expect("Failed to get asset filename for section"), @@ -832,41 +926,31 @@ impl Site { if let Some(ref redirect_to) = section.meta.redirect_to { let permalink = self.config.make_permalink(redirect_to); - create_file( - &output_path.join("index.html"), - &render_redirect_template(&permalink, &self.tera)?, + self.write_content( + &components, + "index.html", + render_redirect_template(&permalink, &self.tera)?, + create_directories, )?; + return Ok(()); } if section.meta.is_paginated() { self.render_paginated( - &output_path, + components, &Paginator::from_section(§ion, &self.library.read().unwrap()), )?; } else { let output = section.render_html(&self.tera, &self.config, &self.library.read().unwrap())?; - create_file(&output_path.join("index.html"), &self.inject_livereload(output))?; + let content = self.inject_livereload(output); + self.write_content(&components, "index.html", content, false)?; } Ok(()) } - // TODO: remove me when reload has changed - /// Used only on reload - pub fn render_index(&self) -> Result<()> { - self.render_section( - &self - .library - .read() - .unwrap() - .get_section(&self.content_path.join("_index.md")) - .expect("Failed to get index section"), - false, - ) - } - /// Renders all sections pub fn render_sections(&self) -> Result<()> { self.library @@ -890,33 +974,43 @@ impl Site { } /// Renders a list of pages when the section/index is wanting pagination. - pub fn render_paginated(&self, output_path: &Path, paginator: &Paginator) -> Result<()> { + pub fn render_paginated<'a>( + &self, + components: Vec<&'a str>, + paginator: &'a Paginator, + ) -> Result<()> { ensure_directory_exists(&self.output_path)?; - let folder_path = output_path.join(&paginator.paginate_path); - create_directory(&folder_path)?; + let index_components = components.clone(); paginator .pagers .par_iter() .map(|pager| { - let page_path = folder_path.join(&format!("{}", pager.index)); - create_directory(&page_path)?; + let mut pager_components = index_components.clone(); + pager_components.push(&paginator.paginate_path); + let pager_path = format!("{}", pager.index); + pager_components.push(&pager_path); let output = paginator.render_pager( pager, &self.config, &self.tera, &self.library.read().unwrap(), )?; + let content = self.inject_livereload(output); + if pager.index > 1 { - create_file(&page_path.join("index.html"), &self.inject_livereload(output))?; + self.write_content(&pager_components, "index.html", content, false)?; } else { - create_file(&output_path.join("index.html"), &self.inject_livereload(output))?; - create_file( - &page_path.join("index.html"), - &render_redirect_template(&paginator.permalink, &self.tera)?, + self.write_content(&index_components, "index.html", content, false)?; + self.write_content( + &pager_components, + "index.html", + render_redirect_template(&paginator.permalink, &self.tera)?, + false, )?; } + Ok(()) }) .collect::>() diff --git a/components/utils/src/fs.rs b/components/utils/src/fs.rs index 5f54f27c..2b8e1c5f 100644 --- a/components/utils/src/fs.rs +++ b/components/utils/src/fs.rs @@ -96,10 +96,6 @@ pub fn find_related_assets(path: &Path) -> Vec { /// Copy a file but takes into account where to start the copy as /// there might be folders we need to create on the way. -/// No copy occurs if all of the following conditions are satisfied: -/// 1. A file with the same name already exists in the dest path. -/// 2. Its modification timestamp is identical to that of the src file. -/// 3. Its filesize is identical to that of the src file. pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf, hard_link: bool) -> Result<()> { let relative_path = src.strip_prefix(base_path).unwrap(); let target_path = dest.join(relative_path); @@ -108,21 +104,33 @@ pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf, hard_link: boo create_dir_all(parent_directory)?; } + copy_file_if_needed(src, &target_path, hard_link) +} + +/// No copy occurs if all of the following conditions are satisfied: +/// 1. A file with the same name already exists in the dest path. +/// 2. Its modification timestamp is identical to that of the src file. +/// 3. Its filesize is identical to that of the src file. +pub fn copy_file_if_needed(src: &Path, dest: &PathBuf, hard_link: bool) -> Result<()> { + if let Some(parent_directory) = dest.parent() { + create_dir_all(parent_directory)?; + } + if hard_link { - std::fs::hard_link(src, target_path)? + std::fs::hard_link(src, dest)? } else { let src_metadata = metadata(src)?; let src_mtime = FileTime::from_last_modification_time(&src_metadata); - if Path::new(&target_path).is_file() { - let target_metadata = metadata(&target_path)?; + if Path::new(&dest).is_file() { + let target_metadata = metadata(&dest)?; let target_mtime = FileTime::from_last_modification_time(&target_metadata); if !(src_mtime == target_mtime && src_metadata.len() == target_metadata.len()) { - copy(src, &target_path)?; - set_file_mtime(&target_path, src_mtime)?; + copy(src, &dest)?; + set_file_mtime(&dest, src_mtime)?; } } else { - copy(src, &target_path)?; - set_file_mtime(&target_path, src_mtime)?; + copy(src, &dest)?; + set_file_mtime(&dest, src_mtime)?; } } Ok(()) diff --git a/docs/content/documentation/getting-started/_index.md b/docs/content/documentation/getting-started/_index.md index ff983e02..f03b5d35 100644 --- a/docs/content/documentation/getting-started/_index.md +++ b/docs/content/documentation/getting-started/_index.md @@ -2,6 +2,6 @@ title = "Getting Started" weight = 1 sort_by = "weight" -redirect_to = "documentation/getting-started/installation" +redirect_to = "documentation/getting-started/overview" insert_anchor_links = "left" +++ diff --git a/docs/content/documentation/getting-started/cli-usage.md b/docs/content/documentation/getting-started/cli-usage.md index 3120f993..00e0d402 100644 --- a/docs/content/documentation/getting-started/cli-usage.md +++ b/docs/content/documentation/getting-started/cli-usage.md @@ -1,6 +1,6 @@ +++ title = "CLI usage" -weight = 2 +weight = 15 +++ Zola only has 4 commands: `init`, `build`, `serve` and `check`. diff --git a/src/cli.rs b/src/cli.rs index 147a32d5..bc14a5ab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -90,6 +90,11 @@ pub fn build_cli() -> App<'static, 'static> { .long("open") .takes_value(false) .help("Open site in the default browser"), + Arg::with_name("fast") + .short("f") + .long("fast") + .takes_value(false) + .help("Only rebuild the minimum on change - useful when working on a specific page/section"), ]), SubCommand::with_name("check") .about("Try building the project without rendering it. Checks links") diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 5637c08d..9c2aa29d 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -21,7 +21,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use std::collections::HashMap; use std::fs::{read_dir, remove_dir_all}; use std::path::{Path, PathBuf}; use std::sync::mpsc::channel; @@ -32,8 +31,6 @@ use hyper::header; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, Server, StatusCode}; use hyper_staticfile::ResolveResult; -use lazy_static::lazy_static; -use tokio::io::AsyncReadExt; use chrono::prelude::*; use notify::{watcher, RecursiveMode, Watcher}; @@ -42,10 +39,11 @@ use ws::{Message, Sender, WebSocket}; use errors::{Error as ZolaError, Result}; use globset::GlobSet; use site::sass::compile_sass; -use site::Site; +use site::{Site, SITE_CONTENT}; use utils::fs::copy_file; use crate::console; +use std::ffi::OsStr; #[derive(Debug, PartialEq)] enum ChangeKind { @@ -61,28 +59,19 @@ enum ChangeKind { enum WatchMode { Required, Optional, - Condition(bool) + Condition(bool), } -static INTERNAL_SERVER_ERROR_TEXT: &[u8] = b"Internal Server Error"; static METHOD_NOT_ALLOWED_TEXT: &[u8] = b"Method Not Allowed"; static NOT_FOUND_TEXT: &[u8] = b"Not Found"; // This is dist/livereload.min.js from the LiveReload.js v3.2.4 release const LIVE_RELOAD: &str = include_str!("livereload.js"); -lazy_static! { - pub static ref SITE_DATA: HashMap = { - let mut m = HashMap::new(); - m.insert("/hello".to_string(), "ho".to_string()); - m - }; -} - async fn handle_request(req: Request, root: PathBuf) -> Result> { - let path = req.uri().path(); + let path = req.uri().path().trim_end_matches('/').trim_start_matches('/'); // livereload.js is served using the LIVE_RELOAD str, not a file - if path == "/livereload.js" { + if path == "livereload.js" { if req.method() == Method::GET { return Ok(livereload_js()); } else { @@ -90,7 +79,7 @@ async fn handle_request(req: Request, root: PathBuf) -> Result, root: PathBuf) -> Result return Ok(method_not_allowed()), ResolveResult::NotFound | ResolveResult::UriNotMatched => { - return Ok(not_found(Path::new(&root.join("404.html"))).await) + let content_404 = SITE_CONTENT.read().unwrap().get("404.html").map(|x| x.clone()); + return Ok(not_found(content_404)); } _ => (), }; @@ -122,14 +112,6 @@ fn in_memory_html(content: &str) -> Response { .expect("Could not build HTML response") } -fn internal_server_error() -> Response { - Response::builder() - .header(header::CONTENT_TYPE, "text/plain") - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(INTERNAL_SERVER_ERROR_TEXT.into()) - .expect("Could not build Internal Server Error response") -} - fn method_not_allowed() -> Response { Response::builder() .header(header::CONTENT_TYPE, "text/plain") @@ -138,21 +120,16 @@ fn method_not_allowed() -> Response { .expect("Could not build Method Not Allowed response") } -async fn not_found(page_path: &Path) -> Response { - if let Ok(mut file) = tokio::fs::File::open(page_path).await { - let mut buf = Vec::new(); - if file.read_to_end(&mut buf).await.is_ok() { - return Response::builder() - .header(header::CONTENT_TYPE, "text/html") - .status(StatusCode::NOT_FOUND) - .body(buf.into()) - .expect("Could not build Not Found response"); - } - - return internal_server_error(); +fn not_found(content: Option) -> Response { + if let Some(body) = content { + return Response::builder() + .header(header::CONTENT_TYPE, "text/html") + .status(StatusCode::NOT_FOUND) + .body(body.into()) + .expect("Could not build Not Found response"); } - // Use a plain text response when page_path isn't available + // Use a plain text response when we can't find the body of the 404 Response::builder() .header(header::CONTENT_TYPE, "text/plain") .status(StatusCode::NOT_FOUND) @@ -187,30 +164,35 @@ fn rebuild_done_handling(broadcaster: &Option, res: Result<()>, reload_p fn create_new_site( root_dir: &Path, interface: &str, - port: u16, + interface_port: u16, output_dir: &Path, base_url: &str, config_file: &Path, include_drafts: bool, + ws_port: Option, ) -> Result<(Site, String)> { let mut site = Site::new(root_dir, config_file)?; - let base_address = format!("{}:{}", base_url, port); - let address = format!("{}:{}", interface, port); + let base_address = format!("{}:{}", base_url, interface_port); + let address = format!("{}:{}", interface, interface_port); let base_url = if site.config.base_url.ends_with('/') { format!("http://{}/", base_address) } else { format!("http://{}", base_address) }; - site.config.enable_serve_mode(); + site.enable_serve_mode(); site.set_base_url(base_url); site.set_output_path(output_dir); if include_drafts { site.include_drafts(); } site.load()?; - site.enable_live_reload(port); + if let Some(p) = ws_port { + site.enable_live_reload_with_port(p); + } else { + site.enable_live_reload(interface_port); + } console::notify_site_size(&site); console::warn_about_ignored_pages(&site); site.build()?; @@ -220,36 +202,38 @@ fn create_new_site( pub fn serve( root_dir: &Path, interface: &str, - port: u16, + interface_port: u16, output_dir: &Path, base_url: &str, config_file: &Path, watch_only: bool, open: bool, include_drafts: bool, + fast_rebuild: bool, ) -> Result<()> { let start = Instant::now(); let (mut site, address) = create_new_site( root_dir, interface, - port, + interface_port, output_dir, base_url, config_file, include_drafts, + None, )?; console::report_elapsed_time(start); // An array of (path, bool, bool) where the path should be watched for changes, and the boolean value // indicates whether this file/folder must exist for zola serve to operate - let watch_this = vec!( + let watch_this = vec![ ("config.toml", WatchMode::Required), ("content", WatchMode::Required), ("sass", WatchMode::Condition(site.config.compile_sass)), ("static", WatchMode::Optional), ("templates", WatchMode::Optional), - ("themes", WatchMode::Condition(site.config.theme.is_some())) - ); + ("themes", WatchMode::Condition(site.config.theme.is_some())), + ]; // Setup watchers let (tx, rx) = channel(); @@ -266,7 +250,7 @@ pub fn serve( let should_watch = match mode { WatchMode::Required => true, WatchMode::Optional => watch_path.exists(), - WatchMode::Condition(b) => b + WatchMode::Condition(b) => b, }; if should_watch { watcher @@ -276,7 +260,8 @@ pub fn serve( } } - let ws_address = format!("{}:{}", interface, site.live_reload.unwrap()); + let ws_port = site.live_reload; + let ws_address = format!("{}:{}", interface, ws_port.unwrap()); let output_path = Path::new(output_dir).to_path_buf(); // output path is going to need to be moved later on, so clone it for the @@ -344,11 +329,7 @@ pub fn serve( None }; - println!( - "Listening for changes in {}{{{}}}", - root_dir.display(), - watchers.join(", ") - ); + println!("Listening for changes in {}{{{}}}", root_dir.display(), watchers.join(", ")); println!("Press Ctrl+C to stop\n"); // Delete the output folder on ctrl+C @@ -363,17 +344,6 @@ pub fn serve( use notify::DebouncedEvent::*; - let reload_templates = |site: &mut Site, path: &Path| { - let msg = if path.is_dir() { - format!("-> Directory in `templates` folder changed {}", path.display()) - } else { - format!("-> Template changed {}", path.display()) - }; - console::info(&msg); - // Force refresh - rebuild_done_handling(&broadcaster, rebuild::after_template_change(site, &path), "/x.js"); - }; - let reload_sass = |site: &Site, path: &Path, partial_path: &Path| { let msg = if path.is_dir() { format!("-> Directory in `sass` folder changed {}", path.display()) @@ -388,6 +358,10 @@ pub fn serve( ); }; + let reload_templates = |site: &mut Site, path: &Path| { + rebuild_done_handling(&broadcaster, site.reload_templates(), &path.to_string_lossy()); + }; + let copy_static = |site: &Site, path: &Path, partial_path: &Path| { // Do nothing if the file/dir was deleted if !path.exists() { @@ -424,11 +398,12 @@ pub fn serve( let recreate_site = || match create_new_site( root_dir, interface, - port, + interface_port, output_dir, base_url, config_file, include_drafts, + ws_port, ) { Ok((s, _)) => { rebuild_done_handling(&broadcaster, Ok(()), "/x.js"); @@ -443,66 +418,25 @@ pub fn serve( loop { match rx.recv() { Ok(event) => { + let can_do_fast_reload = match event { + Remove(_) => false, + _ => true, + }; + match event { - Rename(old_path, path) => { - if path.is_file() && is_temp_file(&path) { - continue; - } - let (change_kind, partial_path) = detect_change_kind(&root_dir, &path); - - // We only care about changes in non-empty folders - if path.is_dir() && is_folder_empty(&path) { - continue; - } - - println!( - "Change detected @ {}", - Local::now().format("%Y-%m-%d %H:%M:%S").to_string() - ); - - let start = Instant::now(); - - match change_kind { - ChangeKind::Content => { - console::info(&format!("-> Content renamed {}", path.display())); - // Force refresh - rebuild_done_handling( - &broadcaster, - rebuild::after_content_rename(&mut site, &old_path, &path), - "/x.js", - ); - } - ChangeKind::Templates => reload_templates(&mut site, &path), - ChangeKind::StaticFiles => copy_static(&site, &path, &partial_path), - ChangeKind::Sass => reload_sass(&site, &path, &partial_path), - ChangeKind::Themes => { - console::info( - "-> Themes changed. The whole site will be reloaded.", - ); - - if let Some(s) = recreate_site() { - site = s; - } - } - ChangeKind::Config => { - console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible."); - - if let Some(s) = recreate_site() { - site = s; - } - } - } - console::report_elapsed_time(start); - } // Intellij does weird things on edit, chmod is there to count those changes // https://github.com/passcod/notify/issues/150#issuecomment-494912080 - Create(path) | Write(path) | Remove(path) | Chmod(path) => { + Rename(_, path) | Create(path) | Write(path) | Remove(path) | Chmod(path) => { if is_ignored_file(&site.config.ignored_content_globset, &path) { continue; } if is_temp_file(&path) || path.is_dir() { continue; } + // We only care about changes in non-empty folders + if path.is_dir() && is_folder_empty(&path) { + continue; + } println!( "Change detected @ {}", @@ -513,27 +447,79 @@ pub fn serve( match detect_change_kind(&root_dir, &path) { (ChangeKind::Content, _) => { console::info(&format!("-> Content changed {}", path.display())); - // Force refresh - rebuild_done_handling( - &broadcaster, - rebuild::after_content_change(&mut site, &path), - "/x.js", - ); + + if fast_rebuild { + if can_do_fast_reload { + let filename = path + .file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy(); + let res = if filename == "_index.md" { + site.add_and_render_section(&path) + } else if filename.ends_with(".md") { + site.add_and_render_page(&path) + } else { + // an asset changed? a folder renamed? + // should we make it smarter so it doesn't reload the whole site? + Err("dummy".into()) + }; + + if res.is_err() { + if let Some(s) = recreate_site() { + site = s; + } + } else { + rebuild_done_handling( + &broadcaster, + res, + &path.to_string_lossy(), + ); + } + } else { + // Should we be smarter than that? Is it worth it? + if let Some(s) = recreate_site() { + site = s; + } + } + } else { + if let Some(s) = recreate_site() { + site = s; + } + } + } + (ChangeKind::Templates, partial_path) => { + let msg = if path.is_dir() { + format!( + "-> Directory in `templates` folder changed {}", + path.display() + ) + } else { + format!("-> Template changed {}", path.display()) + }; + console::info(&msg); + + // A shortcode changed, we need to rebuild everything + if partial_path.starts_with("/templates/shortcodes") { + if let Some(s) = recreate_site() { + site = s; + } + } else { + println!("Reloading only template"); + // A normal template changed, no need to re-render Markdown. + reload_templates(&mut site, &path) + } } - (ChangeKind::Templates, _) => reload_templates(&mut site, &path), (ChangeKind::StaticFiles, p) => copy_static(&site, &path, &p), (ChangeKind::Sass, p) => reload_sass(&site, &path, &p), (ChangeKind::Themes, _) => { - console::info( - "-> Themes changed. The whole site will be reloaded.", - ); + console::info("-> Themes changed."); if let Some(s) = recreate_site() { site = s; } } (ChangeKind::Config, _) => { - console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible."); + console::info("-> Config changed. The browser needs to be refreshed to make the changes visible."); if let Some(s) = recreate_site() { site = s; diff --git a/src/main.rs b/src/main.rs index 7d60b7a5..8e17d989 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,9 @@ fn main() { let root_dir = match matches.value_of("root").unwrap() { "." => env::current_dir().unwrap(), - path => PathBuf::from(path).canonicalize().expect(&format!("Cannot find root directory: {}", path)), + path => PathBuf::from(path) + .canonicalize() + .expect(&format!("Cannot find root directory: {}", path)), }; let config_file = match matches.value_of("config") { Some(path) => PathBuf::from(path), @@ -62,6 +64,7 @@ fn main() { let watch_only = matches.is_present("watch_only"); let open = matches.is_present("open"); let include_drafts = matches.is_present("drafts"); + let fast = matches.is_present("fast"); // Default one if port != 1111 && !watch_only && !port_is_available(port) { @@ -90,6 +93,7 @@ fn main() { watch_only, open, include_drafts, + fast, ) { Ok(()) => (), Err(e) => {