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
This commit is contained in:
Vincent Prouillet 2020-08-16 18:39:04 +02:00 committed by GitHub
parent 623817120c
commit 278cc82fc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 604 additions and 1137 deletions

View file

@ -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

293
Cargo.lock generated
View file

@ -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",

View file

@ -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",

View file

@ -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());
}
}

View file

@ -1,16 +0,0 @@
[package]
name = "rebuild"
version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
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"

View file

@ -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<SectionChangesNeeded> {
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<PageChangesNeeded> {
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, its rendering the wrong
// content into the root feed, and its 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(&current, &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]);
}
}

View file

@ -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", "<p>Some content</p>"));
}
#[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", "<h1>Premier</h1>"));
}
#[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",
"<h1>first</h1><h1>second</h1>"
));
}
#[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());
}

View file

@ -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");
}

View file

@ -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" }

View file

@ -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<RwLock<HashMap<String, String>>> = 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<RwLock<Library>>,
/// 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<Option<Page>> {
/// 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<Option<Section>> {
/// 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(&section.file.path);
library.remove_section(&section.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(&section, 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<PathBuf> {
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(&current_path)?;
}
}
// Make sure the folder exists
create_directory(&current_path)?;
if write_dirs {
create_directory(&current_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(&current_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,
&current_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::<Vec<_>>();
// 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::<Vec<_>>(),
&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(&section.lang);
output_path.push(&section.lang);
if !output_path.exists() {
if !output_path.exists() && create_directories {
create_directory(&output_path)?;
}
}
for component in &section.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 &section.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(&section, &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::<Result<()>>()

View file

@ -96,10 +96,6 @@ pub fn find_related_assets(path: &Path) -> Vec<PathBuf> {
/// 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(())

View file

@ -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"
+++

View file

@ -1,6 +1,6 @@
+++
title = "CLI usage"
weight = 2
weight = 15
+++
Zola only has 4 commands: `init`, `build`, `serve` and `check`.

View file

@ -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")

View file

@ -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<String, String> = {
let mut m = HashMap::new();
m.insert("/hello".to_string(), "ho".to_string());
m
};
}
async fn handle_request(req: Request<Body>, root: PathBuf) -> Result<Response<Body>> {
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<Body>, root: PathBuf) -> Result<Response<Bo
}
}
if let Some(content) = SITE_DATA.get(path) {
if let Some(content) = SITE_CONTENT.read().unwrap().get(path) {
return Ok(in_memory_html(content));
}
@ -98,7 +87,8 @@ async fn handle_request(req: Request<Body>, root: PathBuf) -> Result<Response<Bo
match result {
ResolveResult::MethodNotMatched => 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<Body> {
.expect("Could not build HTML response")
}
fn internal_server_error() -> Response<Body> {
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<Body> {
Response::builder()
.header(header::CONTENT_TYPE, "text/plain")
@ -138,21 +120,16 @@ fn method_not_allowed() -> Response<Body> {
.expect("Could not build Method Not Allowed response")
}
async fn not_found(page_path: &Path) -> Response<Body> {
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<String>) -> Response<Body> {
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<Sender>, 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<u16>,
) -> 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;

View file

@ -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) => {