diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..94f2da0a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +.dockerignore +.git* +Dockerfile + +# From .gitignore +target +.idea/ +test_site/public +test_site_i18n/public +docs/public + +small-blog +medium-blog +big-blog +huge-blog +extra-huge-blog +small-kb +medium-kb +huge-kb + +current.bench +now.bench +*.zst + +# snapcraft artifacts +snap/.snapcraft +parts +prime +stage + +# nixos dependencies snippet +shell.nix +# vim temporary files +**/.*.sw* diff --git a/.gitignore b/.gitignore index b60977ca..4d71ae82 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ stage shell.nix # vim temporary files **/.*.sw* +.swp diff --git a/.gitmodules b/.gitmodules index 9856f3c5..ffd0dd08 100644 --- a/.gitmodules +++ b/.gitmodules @@ -61,3 +61,9 @@ [submodule "sublime/syntaxes/vue-syntax-highlight"] path = sublime/syntaxes/vue-syntax-highlight url = https://github.com/vuejs/vue-syntax-highlight.git +[submodule "sublime/syntaxes/sublime-glsl"] + path = sublime/syntaxes/sublime-glsl + url = https://github.com/euler0/sublime-glsl.git +[submodule "sublime/syntaxes/GDScript-sublime"] + path = sublime/syntaxes/GDScript-sublime + url = https://github.com/beefsack/GDScript-sublime.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 061466fc..e45bf0c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.12.0 (2020-09-04) + +### Breaking + +- 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 leads to a big increase in +binary size + +### Other + +- Add 2 syntax highlighting themes: `green` and `railsbase16-green-screen-dark` +- Enable task lists in Markdown +- Add support for SVG in `get_image_metadata` +- Fix parsing of dates in arrays in `extra` +- Add a `--force` argument to `zola init` to allow creating a Zola site in a non-empty directory +- Make themes more flexible: `include` can now be used +- Make search index generation configurable, see docs for examples +- Fix Sass trying to load folders starting with `_`, causing issues with frameworks +- Update livereload.js version +- Add Markdown-outputting shortcodes +- Taxonomies with the same name but different casing are now merged, eg Author and author + ## 0.11.0 (2020-05-25) ### Breaking @@ -8,6 +30,7 @@ - Config value `rss_limit` is renamed to `feed_limit` - Config value `languages.*.rss` is renamed to `languages.*.feed` - Config value `generate_rss` is renamed to `generate_feed` + - Taxonomy value `rss` is renamed to `feed` Users with existing feeds should either set `feed_filename = "rss.xml"` in config.toml to keep things the same, or set up a 3xx redirect from rss.xml to atom.xml so that existing feed consumers aren’t broken. @@ -23,7 +46,6 @@ - Pass missing `lang` template parameter to taxonomy list template - Fix default index section not having its path set to '/' - Change cachebust strategy to use SHA256 instead of timestamp -- Fix ## 0.10.1 (2020-03-12) @@ -48,7 +70,7 @@ accessible everywhere - Check for path collisions when building the site - Fix bug in template extension with themes - Use Rustls instead of openssl -- The continue reading HTML element is now a instead of a

+- The continue reading HTML element is now a `` instead of a `

` - Update livereload.js - Add --root global argument diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 988853f7..6a9fdbe1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,11 +56,23 @@ $ cargo run --example generate_sublime synpack ../../sublime/syntaxes ../../subl ### Adding a theme A gallery containing lots of themes is located at https://tmtheme-editor.herokuapp.com/#!/editor/theme/Agola%20Dark. -More themes can be easily added to Zola, just make a PR with the wanted theme added in the `sublime_themes` directory -and run the following command from the root of the components/config: +More themes can be easily added to Zola, just make a PR with the wanted theme added in the `sublime_themes` directory. + +If you want to test Zola with a new theme, it needs to be built into the syntect file `all.themedump`. + +First build the tool to generate the syntect file: ```bash -$ cargo run --example generate_sublime themepack ../../sublime/themes ../../sublime/themes/all.themedump +$ git clone https://github.com/getzola/zola.git && cd zola/components/config +$ cargo build --example generate_sublime +``` + +copy your theme in `sublime/themes/`, then regenerate the syntect file: + +``` bash +$ ./target/debug/examples/generate_sublime themepack sublime/themes/ sublime/themes/all.themedump ``` You should see the list of themes being added. + +To test your new theme, rebuild Zola with `cargo build`. diff --git a/Cargo.lock b/Cargo.lock index df09ae98..de7a7490 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,25 +1,28 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] -name = "adler32" -version = "1.0.4" +name = "adler" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ahash" -version = "0.2.18" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f33b5018f120946c1dcf279194f238a9f146725593ead1c08fa47ff22b0b5d3" -dependencies = [ - "const-random", -] +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" [[package]] name = "aho-corasick" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" dependencies = [ "memchr", ] @@ -45,14 +48,14 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] name = "assert-json-diff" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c356497fd3417158bcb318266ac83c391219ca3a5fa659049f42e0041ab57d6" +checksum = "4259cbe96513d2f1073027a259fc2ca917feb3026a5a8d984e3628e490255cc0" dependencies = [ "extend", "serde", @@ -67,38 +70,26 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] name = "autocfg" -version = "0.1.7" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" - -[[package]] -name = "autocfg" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "base64" -version = "0.11.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - -[[package]] -name = "base64" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "bincode" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf" +checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" dependencies = [ "byteorder", "serde", @@ -119,7 +110,16 @@ dependencies = [ "block-padding", "byte-tools", "byteorder", - "generic-array", + "generic-array 0.12.3", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.4", ] [[package]] @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5356f1d23ee24a1f785a56d1d1a5f0fd5b0f6a0c0fb2412ce11da71649ab78f6" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" [[package]] name = "byte-tools" @@ -157,9 +157,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431" +checksum = "92046dbb6f9332943252123f53623e0a6d513651af14967e2991c371ec20201c" [[package]] name = "byteorder" @@ -179,15 +179,15 @@ dependencies = [ [[package]] name = "bytes" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "cc" -version = "1.0.54" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" +checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" [[package]] name = "cedarwood" @@ -206,9 +206,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "chrono" -version = "0.4.11" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" +checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" dependencies = [ "num-integer", "num-traits", @@ -218,9 +218,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e430fad0384e4defc3dc6b1223d1b886087a8bf9b7080e5ae027f73851ea15" +checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120" dependencies = [ "chrono", "parse-zoneinfo", @@ -228,9 +228,9 @@ dependencies = [ [[package]] name = "clap" -version = "2.33.1" +version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ "ansi_term", "atty", @@ -241,15 +241,6 @@ dependencies = [ "vec_map", ] -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", -] - [[package]] name = "color_quant" version = "1.0.1" @@ -264,7 +255,7 @@ checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" dependencies = [ "atty", "lazy_static", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -283,40 +274,10 @@ dependencies = [ ] [[package]] -name = "const-random" -version = "0.1.8" +name = "cpuid-bool" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" -dependencies = [ - "const-random-macro", - "proc-macro-hack", -] - -[[package]] -name = "const-random-macro" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" -dependencies = [ - "getrandom", - "proc-macro-hack", -] - -[[package]] -name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" [[package]] name = "crc32fast" @@ -327,6 +288,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.7.3" @@ -344,7 +315,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ - "autocfg 1.0.0", + "autocfg", "cfg-if", "crossbeam-utils", "lazy_static", @@ -353,23 +324,13 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "crossbeam-queue" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "autocfg 1.0.0", + "autocfg", "cfg-if", "lazy_static", ] @@ -396,30 +357,21 @@ dependencies = [ "memchr", ] -[[package]] -name = "ct-logs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" -dependencies = [ - "sct", -] - [[package]] name = "ctrlc" -version = "3.1.4" +version = "3.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4ba686dff9fa4c1c9636ce1010b0cf98ceb421361b0bb3d6faeec43bd217a7" +checksum = "d0b676fa23f995faf587496dcd1c80fead847ed58d2da52ac1caca9a72790dd2" dependencies = [ "nix", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] name = "deflate" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" dependencies = [ "adler32", "byteorder", @@ -443,26 +395,41 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "generic-array", + "generic-array 0.12.3", ] [[package]] -name = "dtoa" -version = "0.4.5" +name = "digest" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dtoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" -version = "2.3.8" +version = "2.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35274f9f9208923544b450b98ef8b018357904261c07a9bc83c536412881263c" +checksum = "35622eb004c8f0c5e7e2032815f3314a93df0db30a1ce5c94e62c1ecc81e22b9" dependencies = [ "jieba-rs", "lazy_static", @@ -542,9 +509,9 @@ checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" [[package]] name = "encoding_rs" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" +checksum = "a51b8cf747471cb9499b6d59e59b0444f4c90eba8968c4e44874e92b5b64ace2" dependencies = [ "cfg-if", ] @@ -579,26 +546,26 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "filetime" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affc17579b132fc2461adf7c575cc6e8b134ebca52c51f5411388965227dc695" +checksum = "3ed85775dcc68644b5c950ac06a2b23768d3bc9390464151aaf27136998dcf9e" dependencies = [ "cfg-if", "libc", "redox_syscall", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] name = "flate2" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" +checksum = "766d0e77a2c1502169d4a93ff3b8c15a71fd946cd0126309752104e5f3c46d94" dependencies = [ "cfg-if", "crc32fast", "libc", - "miniz_oxide", + "miniz_oxide 0.4.1", ] [[package]] @@ -622,12 +589,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" @@ -647,12 +608,6 @@ dependencies = [ "libc", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -755,12 +710,13 @@ dependencies = [ ] [[package]] -name = "getopts" -version = "0.2.21" +name = "generic-array" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" dependencies = [ - "unicode-width", + "typenum", + "version_check", ] [[package]] @@ -771,7 +727,7 @@ checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -816,31 +772,31 @@ dependencies = [ [[package]] name = "h2" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" +checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" dependencies = [ - "bytes 0.5.4", + "bytes 0.5.6", "fnv", "futures-core", "futures-sink", "futures-util", "http", "indexmap", - "log", "slab", "tokio", "tokio-util", + "tracing", ] [[package]] name = "hashbrown" -version = "0.6.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" dependencies = [ "ahash", - "autocfg 0.1.7", + "autocfg", ] [[package]] @@ -854,9 +810,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" dependencies = [ "libc", ] @@ -881,7 +837,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" dependencies = [ - "bytes 0.5.4", + "bytes 0.5.6", "fnv", "itoa", ] @@ -892,7 +848,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" dependencies = [ - "bytes 0.5.4", + "bytes 0.5.6", "http", ] @@ -910,11 +866,11 @@ checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" [[package]] name = "hyper" -version = "0.13.5" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96816e1d921eca64d208a85aab4f7798455a8e34229ee5a88c935bdee1b78b14" +checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" dependencies = [ - "bytes 0.5.4", + "bytes 0.5.6", "futures-channel", "futures-core", "futures-util", @@ -923,28 +879,26 @@ dependencies = [ "http-body", "httparse", "itoa", - "log", - "net2", "pin-project", + "socket2", "time", "tokio", "tower-service", + "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac965ea399ec3a25ac7d13b8affd4b8f39325cca00858ddf5eb29b79e6b14b08" +checksum = "37743cc83e8ee85eacfce90f2f4102030d9ff0a95244098d781e9bee4a90abb6" dependencies = [ - "bytes 0.5.4", - "ct-logs", + "bytes 0.5.6", "futures-util", "hyper", "log", "rustls", - "rustls-native-certs", "tokio", "tokio-rustls", "webpki", @@ -952,9 +906,9 @@ dependencies = [ [[package]] name = "hyper-staticfile" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e576261b14050b48b3a52f652ee7e2b633decc3f54a48364d3726a394f14b8a" +checksum = "f059991575d4be26e3946276a4f3ee24bcf17a232449407354045eaf064d3475" dependencies = [ "chrono", "futures-util", @@ -964,7 +918,7 @@ dependencies = [ "percent-encoding", "tokio", "url", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -980,9 +934,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128b9e89d15a3faa642ee164c998fd4fae3d89d054463cddb2c25a7baad3a352" +checksum = "22dcbf2a4a289528dbef21686354904e1c694ac642610a9bff9e7df730d9ec72" dependencies = [ "crossbeam-utils", "globset", @@ -998,9 +952,9 @@ dependencies = [ [[package]] name = "image" -version = "0.23.4" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f4167a8f21fa2bb3f17a652a760acd7572645281c98e3b612a26242c96ee" +checksum = "974e194911d1f7efe3cd8a8f9db3b767e43536327e899e8bc9a12ef5711b74d2" dependencies = [ "bytemuck", "byteorder", @@ -1029,27 +983,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.3.2" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" +checksum = "4e47a3566dd4fd4eec714ae6ceabdee0caec795be835c223d92c2d40f1e8cf1c" dependencies = [ - "autocfg 1.0.0", -] - -[[package]] -name = "inflate" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" -dependencies = [ - "adler32", + "autocfg", + "hashbrown", ] [[package]] name = "inotify" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e40d6fd5d64e2082e0c796495c8ef5ad667a96d03e5aaa0becfd9d47bcbfb8" +checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" dependencies = [ "bitflags", "inotify-sys", @@ -1075,30 +1021,36 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "0.4.5" +name = "ipnet" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" [[package]] name = "jieba-rs" -version = "0.4.10" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b1f03a6d2ba3069edb9866fa53035096544077abb34dcaa921ca14276887b6" +checksum = "2ca2de723e93727460917d9542f7ae35a74d03d93923f03380a0238d860d137c" dependencies = [ "cedarwood", "hashbrown", "lazy_static", - "phf 0.7.24", - "phf_codegen 0.7.24", + "phf", + "phf_codegen", "regex", ] [[package]] name = "jpeg-decoder" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b47b4c4e017b01abdc5bcc126d2d1002e5a75bbe3ce73f9f4f311a916363704" +checksum = "cc797adac5f083b8ff0ca6f6294a999393d76e197c36488e2ef732c4715f6fa3" dependencies = [ "byteorder", "rayon", @@ -1106,9 +1058,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.39" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa5a448de267e7358beaf4a5d849518fe9a0c13fce7afd44b06e68550e5562a7" +checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73" dependencies = [ "wasm-bindgen", ] @@ -1131,9 +1083,9 @@ 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" @@ -1143,9 +1095,9 @@ checksum = "73a004f877f468548d8d0ac4977456a249d8fabbdb8416c36db163dfc8f2e8ca" [[package]] name = "libc" -version = "0.2.70" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f" +checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" [[package]] name = "library" @@ -1261,9 +1213,9 @@ checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "log" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ "cfg-if", ] @@ -1293,8 +1245,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab" dependencies = [ "log", - "phf 0.8.0", - "phf_codegen 0.8.0", + "phf", + "phf_codegen", "serde", "serde_derive", "serde_json", @@ -1335,11 +1287,11 @@ checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "memoffset" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" +checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" dependencies = [ - "autocfg 1.0.0", + "autocfg", ] [[package]] @@ -1359,14 +1311,34 @@ dependencies = [ ] [[package]] -name = "miniz_oxide" -version = "0.3.6" +name = "minify-html" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" +checksum = "30f114c59124a0424fff930aecd3c3b991fc9a775bf656ecd9b659975d1b9658" +dependencies = [ + "aho-corasick", + "lazy_static", + "memchr", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" dependencies = [ "adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d7559a8a40d0f97e1edea3220f698f78b1c5ab67532e49f68fde3910323b722" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.6.22" @@ -1412,9 +1384,9 @@ dependencies = [ [[package]] name = "mockito" -version = "0.25.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03dbb09048f444da040f95049763815e4352c9dcb49e4250f7ff2c6853e595dc" +checksum = "3a634720d366bcbce30fb05871a35da229cef101ad0b2ea4e46cf5abf031a273" dependencies = [ "assert-json-diff", "colored", @@ -1422,10 +1394,10 @@ dependencies = [ "httparse", "lazy_static", "log", - "percent-encoding", - "rand 0.7.3", + "rand", "regex", "serde_json", + "serde_urlencoded", ] [[package]] @@ -1436,7 +1408,7 @@ checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" dependencies = [ "cfg-if", "libc", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -1473,48 +1445,48 @@ dependencies = [ "mio", "mio-extras", "walkdir", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] name = "num-integer" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" dependencies = [ - "autocfg 1.0.0", + "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" +checksum = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f" dependencies = [ - "autocfg 1.0.0", + "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +checksum = "a5b4d7360f362cfb50dde8143501e6940b22f644be75a4cc90b2d81968908138" dependencies = [ - "autocfg 1.0.0", + "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" dependencies = [ - "autocfg 1.0.0", + "autocfg", ] [[package]] @@ -1529,9 +1501,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" +checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" [[package]] name = "onig" @@ -1561,26 +1533,26 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "open" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c283bf0114efea9e42f1a60edea9859e8c47528eae09d01df4b29c1e489cc48" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] -[[package]] -name = "openssl-probe" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" - [[package]] name = "parse-zoneinfo" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feece9d0113b400182a7d00adcff81ccf29158c49c5abd11e2eed8589bf6ff07" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" dependencies = [ "regex", ] @@ -1634,32 +1606,13 @@ dependencies = [ "sha-1", ] -[[package]] -name = "phf" -version = "0.7.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" -dependencies = [ - "phf_shared 0.7.24", -] - [[package]] name = "phf" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.7.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" -dependencies = [ - "phf_generator 0.7.24", - "phf_shared 0.7.24", + "phf_shared", ] [[package]] @@ -1668,18 +1621,8 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_generator" -version = "0.7.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" -dependencies = [ - "phf_shared 0.7.24", - "rand 0.6.5", + "phf_generator", + "phf_shared", ] [[package]] @@ -1688,17 +1631,8 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_shared" -version = "0.7.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" -dependencies = [ - "siphasher 0.2.3", + "phf_shared", + "rand", ] [[package]] @@ -1707,23 +1641,23 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" dependencies = [ - "siphasher 0.3.3", + "siphasher", ] [[package]] name = "pin-project" -version = "0.4.17" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc93aeee735e60ecb40cf740eb319ff23eab1c5748abfdb5c180e4ce49f7791" +checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.17" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58db2081ba5b4c93bd6be09c40fd36cb9193a8336c384f3b40012e531aa7e40" +checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" dependencies = [ "proc-macro2", "quote", @@ -1732,9 +1666,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f" +checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" [[package]] name = "pin-utils" @@ -1744,9 +1678,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" +checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" [[package]] name = "plist" @@ -1754,7 +1688,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b336d94e8e4ce29bf15bba393164629764744c567e8ad306cc1fdd0119967fd" dependencies = [ - "base64 0.12.1", + "base64", "chrono", "indexmap", "line-wrap", @@ -1764,21 +1698,21 @@ dependencies = [ [[package]] name = "png" -version = "0.16.3" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c68a431ed29933a4eb5709aca9800989758c97759345860fa5db3cfced0b65d" +checksum = "dfe7f9f1c730833200b134370e1d5098964231af8450bce9b78ee3ab5278b970" dependencies = [ "bitflags", "crc32fast", "deflate", - "inflate", + "miniz_oxide 0.3.7", ] [[package]] name = "ppv-lite86" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" +checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" [[package]] name = "precomputed-hash" @@ -1788,9 +1722,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro-error" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", @@ -1801,78 +1735,56 @@ dependencies = [ [[package]] name = "proc-macro-error-attr" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "syn", - "syn-mid", "version_check", ] [[package]] name = "proc-macro-hack" -version = "0.5.16" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" +checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" [[package]] name = "proc-macro-nested" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1502d12e458c49a4c9cbff560d0fe0060c252bc29799ed94ca2ed4bb665a0101" +checksum = "175c513d55719db99da20232b06cda8bab6b83ec2d04e3283edf0213c37c1a29" dependencies = [ "unicode-xid", ] [[package]] name = "pulldown-cmark" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e142c3b8f49d2200605ee6ba0b1d757310e9e7a72afe78c36ee2ef67300ee00" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" dependencies = [ "bitflags", - "getopts", "memchr", "unicase", ] [[package]] name = "quote" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -dependencies = [ - "autocfg 0.1.7", - "libc", - "rand_chacha 0.1.1", - "rand_core 0.4.2", - "rand_hc 0.1.0", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg 0.1.2", - "rand_xorshift", - "winapi 0.3.8", -] - [[package]] name = "rand" version = "0.7.3" @@ -1881,20 +1793,10 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom", "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", - "rand_pcg 0.2.1", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.7", - "rand_core 0.3.1", + "rand_chacha", + "rand_core", + "rand_hc", + "rand_pcg", ] [[package]] @@ -1904,24 +1806,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core 0.5.1", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" @@ -1931,66 +1818,13 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -dependencies = [ - "libc", - "rand_core 0.4.2", - "winapi 0.3.8", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi 0.3.8", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -dependencies = [ - "autocfg 0.1.7", - "rand_core 0.4.2", + "rand_core", ] [[package]] @@ -1999,24 +1833,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -dependencies = [ - "rand_core 0.3.1", + "rand_core", ] [[package]] name = "rayon" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" +checksum = "cfd016f0c045ad38b5251be2c9c0ab806917f82da4d36b2a327e5166adad9270" dependencies = [ + "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -2024,53 +1850,32 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" +checksum = "91739a34c4355b5434ce54c9086c5895604a9c278586d1f1aa95e04f66b525a0" dependencies = [ + "crossbeam-channel", "crossbeam-deque", - "crossbeam-queue", "crossbeam-utils", "lazy_static", "num_cpus", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rebuild" -version = "0.1.0" -dependencies = [ - "errors", - "front_matter", - "fs_extra", - "library", - "site", - "tempfile", -] - [[package]] name = "redox_syscall" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "regex" -version = "1.3.7" +version = "1.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" +checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.6.17", + "regex-syntax 0.6.18", "thread_local", ] @@ -2091,17 +1896,17 @@ checksum = "8e931c58b93d86f080c734bfd2bce7dd0079ae2331235818133c8be7f422e20e" [[package]] name = "regex-syntax" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" +checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" [[package]] name = "remove_dir_all" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2127,12 +1932,12 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.10.4" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b81e49ddec5109a9dcfc5f2a317ff53377c915e9ae9d4f2fb50914b85614e2" +checksum = "e9eaa17ac5d7b838b7503d118fa16ad88f440498bf9ffe5424e621f93190d61e" dependencies = [ - "base64 0.11.0", - "bytes 0.5.4", + "base64", + "bytes 0.5.6", "encoding_rs", "futures-core", "futures-util", @@ -2140,6 +1945,7 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", + "ipnet", "js-sys", "lazy_static", "log", @@ -2150,7 +1956,6 @@ dependencies = [ "rustls", "serde", "serde_urlencoded", - "time", "tokio", "tokio-rustls", "url", @@ -2163,9 +1968,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.13" +version = "0.16.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703516ae74571f24b465b4a1431e81e2ad51336cb0ded733a55a1aa3eccac196" +checksum = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" dependencies = [ "cc", "libc", @@ -2173,7 +1978,16 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi 0.3.8", + "winapi 0.3.9", +] + +[[package]] +name = "roxmltree" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5001f134077069d87f77c8b9452b690df2445f7a43f1c7ca4a1af8dd505789d" +dependencies = [ + "xmlparser", ] [[package]] @@ -2188,34 +2002,22 @@ dependencies = [ [[package]] name = "rustls" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" +checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" dependencies = [ - "base64 0.11.0", + "base64", "log", "ring", "sct", "webpki", ] -[[package]] -name = "rustls-native-certs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ffeb84a6bd9d014713119542ce415db3a3e4748f0bfce1e1416cd224a23a5" -dependencies = [ - "openssl-probe", - "rustls", - "schannel", - "security-framework", -] - [[package]] name = "ryu" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "safemem" @@ -2254,16 +2056,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "schannel" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" -dependencies = [ - "lazy_static", - "winapi 0.3.8", -] - [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -2291,49 +2083,27 @@ name = "search" version = "0.1.0" dependencies = [ "ammonia", + "config", "elasticlunr-rs", "errors", "lazy_static", "library", ] -[[package]] -name = "security-framework" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "serde" -version = "1.0.110" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" +checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.110" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" +checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" dependencies = [ "proc-macro2", "quote", @@ -2342,9 +2112,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.53" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" +checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" dependencies = [ "indexmap", "itoa", @@ -2370,30 +2140,25 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.7.3", + "digest 0.8.1", "fake-simd", - "opaque-debug", + "opaque-debug 0.2.3", ] [[package]] name = "sha2" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1" dependencies = [ - "block-buffer", - "digest", - "fake-simd", - "opaque-debug", + "block-buffer 0.9.0", + "cfg-if", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", ] -[[package]] -name = "siphasher" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" - [[package]] name = "siphasher" version = "0.3.3" @@ -2409,8 +2174,10 @@ dependencies = [ "front_matter", "glob", "imageproc", + "lazy_static", "library", "link_checker", + "minify-html", "rayon", "sass-rs", "search", @@ -2445,9 +2212,21 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" + +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.9", +] [[package]] name = "spin" @@ -2463,7 +2242,7 @@ checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a" dependencies = [ "lazy_static", "new_debug_unreachable", - "phf_shared 0.8.0", + "phf_shared", "precomputed-hash", "serde", ] @@ -2474,8 +2253,8 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -2488,15 +2267,15 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strum" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6138f8f88a16d90134763314e3fc76fa3ed6a7db4725d6acf9a3ef95a3188d22" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" [[package]] name = "strum_macros" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0054a7df764039a6cd8592b9de84be4bec368ff081d203a7d5371cbfa8e65c81" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck", "proc-macro2", @@ -2505,32 +2284,33 @@ dependencies = [ ] [[package]] -name = "syn" -version = "1.0.24" +name = "svg_metadata" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87bc5b2815ebb664de0392fdf1b95b6d10e160f86d9f64ff65e5679841ca06a" +checksum = "5fe5b1fbd62339f055704951dcaf2e757c460b9f6abe17f6de0d2563da821c57" +dependencies = [ + "doc-comment", + "lazy_static", + "regex", + "roxmltree", +] + +[[package]] +name = "syn" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" 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.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b43a6ca1829ccb0c933b615c9ea83ffc8793ae240cecbd15119b13d741161d" +checksum = "4e3978df05b5850c839a6b352d3c35ce0478944a4be689be826b53cf75363e88" dependencies = [ "bincode", "bitflags", @@ -2540,7 +2320,7 @@ dependencies = [ "lazycell", "onig", "plist", - "regex-syntax 0.6.17", + "regex-syntax 0.6.18", "serde", "serde_derive", "serde_json", @@ -2556,17 +2336,17 @@ checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ "cfg-if", "libc", - "rand 0.7.3", + "rand", "redox_syscall", "remove_dir_all", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] name = "templates" version = "0.1.0" dependencies = [ - "base64 0.12.1", + "base64", "config", "csv", "errors", @@ -2579,6 +2359,7 @@ dependencies = [ "reqwest", "serde_json", "sha2", + "svg_metadata", "tera", "toml", "url", @@ -2598,9 +2379,9 @@ dependencies = [ [[package]] name = "tera" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44567278e3f16c6f888f4a1426d1af33827e6bffbe3911fe24aec2c594f0dfcb" +checksum = "1381c83828bedd5ce4e59473110afa5381ffe523406d9ade4b77c9f7be70ff9a" dependencies = [ "chrono", "chrono-tz", @@ -2610,7 +2391,7 @@ dependencies = [ "percent-encoding", "pest", "pest_derive", - "rand 0.7.3", + "rand", "regex", "serde", "serde_json", @@ -2647,32 +2428,39 @@ dependencies = [ [[package]] name = "tiff" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002351e428db1eb1d8656d4ca61947c3519ac3191e1c804d4600cd32093b77ad" +checksum = "3f3b8a87c4da944c3f27e5943289171ac71a6150a79ff6bacfff06d159dfff2f" dependencies = [ "byteorder", "lzw", - "miniz_oxide", + "miniz_oxide 0.3.7", ] [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "winapi 0.3.8", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", ] [[package]] -name = "tokio" -version = "0.2.21" +name = "tinyvec" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58" +checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117" + +[[package]] +name = "tokio" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd" dependencies = [ - "bytes 0.5.4", + "bytes 0.5.6", "fnv", "futures-core", "iovec", @@ -2698,9 +2486,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.13.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15cb62a0d2770787abc96e99c1cd98fcf17f94959f3af63ca85bdfb203f051b4" +checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a" dependencies = [ "futures-core", "rustls", @@ -2714,7 +2502,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" dependencies = [ - "bytes 0.5.4", + "bytes 0.5.6", "futures-core", "futures-sink", "log", @@ -2738,10 +2526,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] -name = "try-lock" -version = "0.2.2" +name = "tracing" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +dependencies = [ + "cfg-if", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f0e00789804e99b20f12bc7003ca416309d28a6f495d6af58d1e2c2842461b5" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" @@ -2825,11 +2633,11 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" dependencies = [ - "smallvec", + "tinyvec", ] [[package]] @@ -2840,15 +2648,15 @@ checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" [[package]] name = "unicode-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "untrusted" @@ -2921,7 +2729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" dependencies = [ "same-file", - "winapi 0.3.8", + "winapi 0.3.9", "winapi-util", ] @@ -2942,10 +2750,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] -name = "wasm-bindgen" -version = "0.2.62" +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c7d40d09cdbf0f4895ae58cf57d92e1e57a9dd8ed2e8390514b54a47cc5551" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c" dependencies = [ "cfg-if", "serde", @@ -2955,9 +2769,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.62" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3972e137ebf830900db522d6c8fd74d1900dcfc733462e9a12e942b00b4ac94" +checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0" dependencies = [ "bumpalo", "lazy_static", @@ -2970,9 +2784,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.12" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a369c5e1dfb7569e14d62af4da642a3cbc2f9a3652fe586e26ac22222aa4b04" +checksum = "95f8d235a77f880bcef268d379810ea6c0af2eacfa90b1ad5af731776e0c4699" dependencies = [ "cfg-if", "js-sys", @@ -2982,9 +2796,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.62" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd85aa2c579e8892442954685f0d801f9129de24fa2136b2c6a539c76b65776" +checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2992,9 +2806,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.62" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb197bd3a47553334907ffd2f16507b4f4f01bbec3ac921a7719e0decdfe72a" +checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" dependencies = [ "proc-macro2", "quote", @@ -3005,15 +2819,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.62" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91c2916119c17a8e316507afaaa2dd94b47646048014bbdf6bef098c1bb58ad" +checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092" [[package]] name = "web-sys" -version = "0.3.39" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bc359e5dd3b46cb9687a051d50a2fdd228e4ba7cf6fcf861a5365c3d671a642" +checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47" dependencies = [ "js-sys", "wasm-bindgen", @@ -3021,9 +2835,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f50e1972865d6b1adb54167d1c8ed48606004c2c9d0ea5f1eeb34d95e863ef" +checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" dependencies = [ "ring", "untrusted", @@ -3031,9 +2845,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cd5736df7f12a964a5067a12c62fa38e1bd8080aff1f80bc29be7c80d19ab4" +checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" dependencies = [ "webpki", ] @@ -3046,9 +2860,9 @@ checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" [[package]] name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", @@ -3072,7 +2886,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -3083,11 +2897,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winreg" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -3102,7 +2916,7 @@ dependencies = [ "log", "mio", "mio-extras", - "rand 0.7.3", + "rand", "sha-1", "slab", "url", @@ -3137,17 +2951,23 @@ dependencies = [ ] [[package]] -name = "yaml-rust" -version = "0.4.3" +name = "xmlparser" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" +checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" + +[[package]] +name = "yaml-rust" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" dependencies = [ "linked-hash-map", ] [[package]] name = "zola" -version = "0.11.0" +version = "0.12.0" dependencies = [ "atty", "chrono", @@ -3161,7 +2981,6 @@ dependencies = [ "lazy_static", "notify", "open", - "rebuild", "site", "termcolor", "tokio", diff --git a/Cargo.toml b/Cargo.toml index ea8580d8..28b62fc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zola" -version = "0.11.0" +version = "0.12.0" authors = ["Vincent Prouillet "] edition = "2018" license = "MIT" @@ -9,7 +9,8 @@ description = "A fast static site generator with everything built-in" homepage = "https://www.getzola.org" repository = "https://github.com/getzola/zola" keywords = ["static", "site", "generator", "blog"] -# build = "build.rs" + +include = ["src/**/*", "LICENSE", "README.md"] [build-dependencies] clap = "2" @@ -19,9 +20,9 @@ name = "zola" [dependencies] atty = "0.2.11" -clap = "2" +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" @@ -39,14 +40,12 @@ site = { path = "components/site" } errors = { path = "components/errors" } front_matter = { path = "components/front_matter" } utils = { path = "components/utils" } -rebuild = { path = "components/rebuild" } [workspace] members = [ "components/config", "components/errors", "components/front_matter", - "components/rebuild", "components/rendering", "components/site", "components/templates", diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8bd2c620..05d6173c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,7 +21,7 @@ stages: rustup_toolchain: stable linux-pinned: imageName: 'ubuntu-16.04' - rustup_toolchain: 1.41.0 + rustup_toolchain: 1.43.0 pool: vmImage: $(imageName) steps: @@ -145,4 +145,4 @@ stages: assets: '$(Build.ArtifactStagingDirectory)/zola-$(Build.SourceBranchName)-$(TARGET).zip' title: '$(Build.SourceBranchName)' assetUploadMode: 'replace' - addChangeLog: true \ No newline at end of file + addChangeLog: true diff --git a/components/config/Cargo.toml b/components/config/Cargo.toml index 7fcf6ba7..43bbcddb 100644 --- a/components/config/Cargo.toml +++ b/components/config/Cargo.toml @@ -3,6 +3,7 @@ name = "config" version = "0.1.0" authors = ["Vincent Prouillet "] edition = "2018" +include = ["src/**/*"] [dependencies] toml = "0.5" diff --git a/components/config/examples/generate_sublime.rs b/components/config/examples/generate_sublime.rs index 1854d889..f47bff13 100644 --- a/components/config/examples/generate_sublime.rs +++ b/components/config/examples/generate_sublime.rs @@ -28,7 +28,7 @@ fn main() { builder.add_plain_text_syntax(); match builder.add_from_folder(package_dir, true) { Ok(_) => (), - Err(e) => println!("Loading error: {:?}", e) + Err(e) => println!("Loading error: {:?}", e), }; let ss = builder.build(); dump_to_file(&ss, packpath_newlines).unwrap(); diff --git a/components/config/src/config/languages.rs b/components/config/src/config/languages.rs new file mode 100644 index 00000000..dee61ffb --- /dev/null +++ b/components/config/src/config/languages.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; + +use serde_derive::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct Language { + /// The language code + pub code: String, + /// Whether to generate a feed for that language, defaults to `false` + pub feed: bool, + /// Whether to generate search index for that language, defaults to `false` + pub search: bool, +} + +pub type TranslateTerm = HashMap; diff --git a/components/config/src/config/link_checker.rs b/components/config/src/config/link_checker.rs new file mode 100644 index 00000000..9d597d49 --- /dev/null +++ b/components/config/src/config/link_checker.rs @@ -0,0 +1,16 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct LinkChecker { + /// Skip link checking for these URL prefixes + pub skip_prefixes: Vec, + /// Skip anchor checking for these URL prefixes + pub skip_anchor_prefixes: Vec, +} + +impl Default for LinkChecker { + fn default() -> LinkChecker { + LinkChecker { skip_prefixes: Vec::new(), skip_anchor_prefixes: Vec::new() } + } +} diff --git a/components/config/src/config.rs b/components/config/src/config/mod.rs similarity index 83% rename from components/config/src/config.rs rename to components/config/src/config/mod.rs index 393db8de..a23e3825 100644 --- a/components/config/src/config.rs +++ b/components/config/src/config/mod.rs @@ -1,18 +1,21 @@ +pub mod languages; +pub mod link_checker; +pub mod search; +pub mod slugify; +pub mod taxonomies; + use std::collections::HashMap; use std::path::{Path, PathBuf}; -use chrono::Utc; use globset::{Glob, GlobSet, GlobSetBuilder}; use serde_derive::{Deserialize, Serialize}; use syntect::parsing::{SyntaxSet, SyntaxSetBuilder}; -use toml; use toml::Value as Toml; use crate::highlighting::THEME_SET; use crate::theme::Theme; use errors::{bail, Error, Result}; use utils::fs::read_file_with_error; -use utils::slugs::SlugifyStrategy; // We want a default base url for tests static DEFAULT_BASE_URL: &str = "http://a-website.com"; @@ -24,104 +27,6 @@ pub enum Mode { Check, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct Slugify { - pub paths: SlugifyStrategy, - pub taxonomies: SlugifyStrategy, - pub anchors: SlugifyStrategy, -} - -impl Default for Slugify { - fn default() -> Self { - Slugify { - paths: SlugifyStrategy::On, - taxonomies: SlugifyStrategy::On, - anchors: SlugifyStrategy::On, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct Language { - /// The language code - pub code: String, - /// Whether to generate a feed for that language, defaults to `false` - pub feed: bool, - /// Whether to generate search index for that language, defaults to `false` - pub search: bool, -} - -impl Default for Language { - fn default() -> Self { - Language { code: String::new(), feed: false, search: false } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct Taxonomy { - /// The name used in the URL, usually the plural - pub name: String, - /// If this is set, the list of individual taxonomy term page will be paginated - /// by this much - pub paginate_by: Option, - pub paginate_path: Option, - /// Whether to generate a feed only for each taxonomy term, defaults to false - pub feed: bool, - /// The language for that taxonomy, only used in multilingual sites. - /// Defaults to the config `default_language` if not set - pub lang: String, -} - -impl Taxonomy { - pub fn is_paginated(&self) -> bool { - if let Some(paginate_by) = self.paginate_by { - paginate_by > 0 - } else { - false - } - } - - pub fn paginate_path(&self) -> &str { - if let Some(ref path) = self.paginate_path { - path - } else { - "page" - } - } -} - -impl Default for Taxonomy { - fn default() -> Self { - Taxonomy { - name: String::new(), - paginate_by: None, - paginate_path: None, - feed: false, - lang: String::new(), - } - } -} - -type TranslateTerm = HashMap; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct LinkChecker { - /// Skip link checking for these URL prefixes - pub skip_prefixes: Vec, - /// Skip anchor checking for these URL prefixes - pub skip_anchor_prefixes: Vec, -} - -impl Default for LinkChecker { - fn default() -> LinkChecker { - LinkChecker { skip_prefixes: Vec::new(), skip_anchor_prefixes: Vec::new() } - } -} - #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct Config { @@ -138,7 +43,7 @@ pub struct Config { /// The language used in the site. Defaults to "en" pub default_language: String, /// The list of supported languages outside of the default one - pub languages: Vec, + pub languages: Vec, /// Languages list and translated strings /// @@ -147,7 +52,7 @@ pub struct Config { /// /// The attribute is intentionally not public, use `get_translation()` method for translating /// key into different language. - translations: HashMap, + translations: HashMap, /// Whether to highlight all code blocks found in markdown files. Defaults to false pub highlight_code: bool, @@ -165,10 +70,12 @@ pub struct Config { /// If set, files from static/ will be hardlinked instead of copied to the output dir. pub hard_link_static: bool, - pub taxonomies: Vec, + pub taxonomies: Vec, /// Whether to compile the `sass` directory and output the css files into the static folder pub compile_sass: bool, + /// Whether to minify the html output + pub minify_html: bool, /// Whether to build the search index for the content pub build_search_index: bool, /// A list of file glob patterns to ignore when processing the content folder. Defaults to none. @@ -189,16 +96,16 @@ pub struct Config { #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need pub extra_syntax_set: Option, - pub link_checker: LinkChecker, + pub link_checker: link_checker::LinkChecker, /// The setup for which slugification strategies to use for paths, taxonomies and anchors - pub slugify: Slugify, + pub slugify: slugify::Slugify, + + /// The search config, telling what to include in the search index + pub search: search::Search, /// All user params set in [extra] in the config pub extra: HashMap, - - /// Set automatically when instantiating the config. Used for cachebusting - pub build_timestamp: Option, } impl Config { @@ -222,8 +129,6 @@ impl Config { bail!("Default language `{}` should not appear both in `config.default_language` and `config.languages`", config.default_language) } - config.build_timestamp = Some(Utc::now().timestamp()); - if !config.ignored_content.is_empty() { // Convert the file glob strings into a compiled glob set matcher. We want to do this once, // at program initialization, rather than for every page, for example. We arrange for the @@ -248,6 +153,9 @@ impl Config { } } + // TODO: re-enable once it's a bit more tested + config.minify_html = false; + Ok(config) } @@ -303,19 +211,14 @@ impl Config { /// Merges the extra data from the theme with the config extra data fn add_theme_extra(&mut self, theme: &Theme) -> Result<()> { - // 3 pass merging - // 1. save config to preserve user - let original = self.extra.clone(); - // 2. inject theme extra values for (key, val) in &theme.extra { - self.extra.entry(key.to_string()).or_insert_with(|| val.clone()); + if !self.extra.contains_key(key) { + // The key is not overriden in site config, insert it + self.extra.insert(key.to_string(), val.clone()); + continue; + } + merge(self.extra.get_mut(key).unwrap(), val)?; } - - // 3. overwrite with original config - for (key, val) in &original { - self.extra.entry(key.to_string()).or_insert_with(|| val.clone()); - } - Ok(()) } @@ -377,6 +280,34 @@ impl Config { } } +// merge TOML data that can be a table, or anything else +pub fn merge(into: &mut Toml, from: &Toml) -> Result<()> { + match (from.is_table(), into.is_table()) { + (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(); + for (key, val) in from.as_table().unwrap() { + if !into_table.contains_key(key) { + // An entry was missing in the first table, insert it + into_table.insert(key.to_string(), val.clone()); + continue; + } + // Two entries to compare, recurse + 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))) + } + } +} + impl Default for Config { fn default() -> Config { Config { @@ -394,6 +325,7 @@ impl Default for Config { hard_link_static: false, taxonomies: Vec::new(), compile_sass: false, + minify_html: false, mode: Mode::Build, build_search_index: false, ignored_content: Vec::new(), @@ -401,17 +333,18 @@ impl Default for Config { translations: HashMap::new(), extra_syntaxes: Vec::new(), extra_syntax_set: None, - link_checker: LinkChecker::default(), - slugify: Slugify::default(), + link_checker: link_checker::LinkChecker::default(), + slugify: slugify::Slugify::default(), + search: search::Search::default(), extra: HashMap::new(), - build_timestamp: Some(1), } } } #[cfg(test)] mod tests { - use super::{Config, SlugifyStrategy, Theme}; + use super::*; + use utils::slugs::SlugifyStrategy; #[test] fn can_import_valid_config() { @@ -512,18 +445,39 @@ base_url = "https://replace-this-with-your-url.com" [extra] hello = "world" +[extra.sub] +foo = "bar" +[extra.sub.sub] +foo = "bar" "#; let mut config = Config::parse(config_str).unwrap(); let theme_str = r#" [extra] hello = "foo" a_value = 10 +[extra.sub] +foo = "default" +truc = "default" +[extra.sub.sub] +foo = "default" +truc = "default" "#; let theme = Theme::parse(theme_str).unwrap(); assert!(config.add_theme_extra(&theme).is_ok()); let extra = config.extra; assert_eq!(extra["hello"].as_str().unwrap(), "world".to_string()); assert_eq!(extra["a_value"].as_integer().unwrap(), 10); + 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() + ); } const CONFIG_TRANSLATION: &str = r#" @@ -681,4 +635,23 @@ languages = [ let err = config.unwrap_err(); 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#" +base_url = "http://localhost:1312" +default_language = "fr" +[extra] +foo = "bar" + "#; + let mut config = Config::parse(config_str).unwrap(); + let theme_str = r#" +[extra] +[extra.foo] +bar = "baz" + "#; + let theme = Theme::parse(theme_str).unwrap(); + // We expect an error here + assert_eq!(false, config.add_theme_extra(&theme).is_ok()); + } } diff --git a/components/config/src/config/search.rs b/components/config/src/config/search.rs new file mode 100644 index 00000000..82d46c7a --- /dev/null +++ b/components/config/src/config/search.rs @@ -0,0 +1,27 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct Search { + /// Include the title of the page in the search index. `true` by default. + pub include_title: bool, + /// Includes the whole content in the search index. Ok for small sites but becomes + /// too big on large sites. `true` by default. + pub include_content: bool, + /// Optionally truncate the content down to `n` chars. This might cut content in a word + pub truncate_content_length: Option, + /// Includes the description in the search index. When the site becomes too large, you can switch + /// to that instead. `false` by default + pub include_description: bool, +} + +impl Default for Search { + fn default() -> Self { + Search { + include_title: true, + include_content: true, + include_description: false, + truncate_content_length: None, + } + } +} diff --git a/components/config/src/config/slugify.rs b/components/config/src/config/slugify.rs new file mode 100644 index 00000000..dae4be61 --- /dev/null +++ b/components/config/src/config/slugify.rs @@ -0,0 +1,11 @@ +use serde_derive::{Deserialize, Serialize}; + +use utils::slugs::SlugifyStrategy; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct Slugify { + pub paths: SlugifyStrategy, + pub taxonomies: SlugifyStrategy, + pub anchors: SlugifyStrategy, +} diff --git a/components/config/src/config/taxonomies.rs b/components/config/src/config/taxonomies.rs new file mode 100644 index 00000000..e18f54c6 --- /dev/null +++ b/components/config/src/config/taxonomies.rs @@ -0,0 +1,35 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct Taxonomy { + /// The name used in the URL, usually the plural + pub name: String, + /// If this is set, the list of individual taxonomy term page will be paginated + /// by this much + pub paginate_by: Option, + pub paginate_path: Option, + /// Whether to generate a feed only for each taxonomy term, defaults to false + pub feed: bool, + /// The language for that taxonomy, only used in multilingual sites. + /// Defaults to the config `default_language` if not set + pub lang: String, +} + +impl Taxonomy { + pub fn is_paginated(&self) -> bool { + if let Some(paginate_by) = self.paginate_by { + paginate_by > 0 + } else { + false + } + } + + pub fn paginate_path(&self) -> &str { + if let Some(ref path) = self.paginate_path { + path + } else { + "page" + } + } +} diff --git a/components/config/src/lib.rs b/components/config/src/lib.rs index 50eb9907..529a8b4a 100644 --- a/components/config/src/lib.rs +++ b/components/config/src/lib.rs @@ -1,7 +1,9 @@ mod config; pub mod highlighting; mod theme; -pub use crate::config::{Config, Language, LinkChecker, Taxonomy}; +pub use crate::config::{ + languages::Language, link_checker::LinkChecker, slugify::Slugify, taxonomies::Taxonomy, Config, +}; use std::path::Path; diff --git a/components/errors/Cargo.toml b/components/errors/Cargo.toml index 7932541b..2539432a 100644 --- a/components/errors/Cargo.toml +++ b/components/errors/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" tera = "1" toml = "0.5" image = "0.23" -syntect = "4.1" +syntect = "4.4" diff --git a/components/errors/src/lib.rs b/components/errors/src/lib.rs index 989fe565..037c48df 100644 --- a/components/errors/src/lib.rs +++ b/components/errors/src/lib.rs @@ -17,21 +17,18 @@ pub enum ErrorKind { pub struct Error { /// Kind of error pub kind: ErrorKind, - pub source: Option>, + pub source: Option>, } -unsafe impl Sync for Error {} -unsafe impl Send for Error {} impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { - let mut source = self.source.as_ref().map(|c| &**c); - if source.is_none() { - if let ErrorKind::Tera(ref err) = self.kind { - source = err.source(); - } + match self.source { + Some(ref err) => Some(&**err), + None => match self.kind { + ErrorKind::Tera(ref err) => err.source(), + _ => None, + }, } - - source } } @@ -55,7 +52,7 @@ impl Error { } /// Creates generic error with a cause - pub fn chain(value: impl ToString, source: impl Into>) -> Self { + pub fn chain(value: impl ToString, source: impl Into>) -> Self { Self { kind: ErrorKind::Msg(value.to_string()), source: Some(source.into()) } } diff --git a/components/front_matter/src/lib.rs b/components/front_matter/src/lib.rs index 83f24476..f0866797 100644 --- a/components/front_matter/src/lib.rs +++ b/components/front_matter/src/lib.rs @@ -13,7 +13,7 @@ pub use section::SectionFrontMatter; lazy_static! { static ref PAGE_RE: Regex = - Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); + Regex::new(r"^[[:space:]]*\+\+\+(\r?\n(?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); } #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] @@ -37,7 +37,7 @@ pub enum InsertAnchor { /// Split a file between the front matter and its content /// Will return an error if the front matter wasn't found -fn split_content(file_path: &Path, content: &str) -> Result<(String, String)> { +fn split_content<'c>(file_path: &Path, content: &'c str) -> Result<(&'c str, &'c str)> { if !PAGE_RE.is_match(content) { bail!( "Couldn't find front matter in `{}`. Did you forget to add `+++`?", @@ -50,15 +50,15 @@ fn split_content(file_path: &Path, content: &str) -> Result<(String, String)> { // caps[0] is the full match // caps[1] => front matter // caps[2] => content - Ok((caps[1].to_string(), caps[2].to_string())) + Ok((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())) } /// Split a file between the front matter and its content. /// Returns a parsed `SectionFrontMatter` and the rest of the content -pub fn split_section_content( +pub fn split_section_content<'c>( file_path: &Path, - content: &str, -) -> Result<(SectionFrontMatter, String)> { + content: &'c str, +) -> Result<(SectionFrontMatter, &'c str)> { let (front_matter, content) = split_content(file_path, content)?; let meta = SectionFrontMatter::parse(&front_matter).map_err(|e| { Error::chain( @@ -71,7 +71,10 @@ pub fn split_section_content( /// Split a file between the front matter and its content /// Returns a parsed `PageFrontMatter` and the rest of the content -pub fn split_page_content(file_path: &Path, content: &str) -> Result<(PageFrontMatter, String)> { +pub fn split_page_content<'c>( + file_path: &Path, + content: &'c str, +) -> Result<(PageFrontMatter, &'c str)> { let (front_matter, content) = split_content(file_path, content)?; let meta = PageFrontMatter::parse(&front_matter).map_err(|e| { Error::chain( diff --git a/components/front_matter/src/page.rs b/components/front_matter/src/page.rs index c26371af..f38f1a2e 100644 --- a/components/front_matter/src/page.rs +++ b/components/front_matter/src/page.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use chrono::prelude::*; use serde_derive::Deserialize; use tera::{Map, Value}; -use toml; use errors::{bail, Result}; use utils::de::{fix_toml_dates, from_toml_datetime}; @@ -38,8 +37,6 @@ pub struct PageFrontMatter { /// Can't be an empty string if present pub path: Option, pub taxonomies: HashMap>, - /// Integer to use to order content. Lowest is at the bottom, highest first - pub order: Option, /// Integer to use to order content. Highest is at the bottom, lowest first pub weight: Option, /// All aliases for that page. Zola will create HTML templates that will @@ -57,6 +54,20 @@ pub struct PageFrontMatter { pub extra: Map, } +/// Parse a string for a datetime coming from one of the supported TOML format +/// There are three alternatives: +/// 1. an offset datetime (plain RFC3339) +/// 2. a local datetime (RFC3339 with timezone omitted) +/// 3. a local date (YYYY-MM-DD). +/// This tries each in order. +fn parse_datetime(d: &str) -> Option { + DateTime::parse_from_rfc3339(d) + .or_else(|_| DateTime::parse_from_rfc3339(format!("{}Z", d).as_ref())) + .map(|s| s.naive_local()) + .or_else(|_| NaiveDate::parse_from_str(d, "%Y-%m-%d").map(|s| s.and_hms(0, 0, 0))) + .ok() +} + impl PageFrontMatter { pub fn parse(toml: &str) -> Result { let mut f: PageFrontMatter = match toml::from_str(toml) { @@ -83,31 +94,20 @@ impl PageFrontMatter { f.date_to_datetime(); + if let Some(ref date) = f.date { + if f.datetime.is_none() { + bail!("`date` could not be parsed: {}.", date); + } + } + Ok(f) } /// Converts the TOML datetime to a Chrono naive datetime /// Also grabs the year/month/day tuple that will be used in serialization pub fn date_to_datetime(&mut self) { - self.datetime = if let Some(ref d) = self.date { - if d.contains('T') { - DateTime::parse_from_rfc3339(&d).ok().map(|s| s.naive_local()) - } else { - NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok().map(|s| s.and_hms(0, 0, 0)) - } - } else { - None - }; - - self.datetime_tuple = if let Some(ref dt) = self.datetime { - Some((dt.year(), dt.month(), dt.day())) - } else { - None - }; - } - - pub fn order(&self) -> usize { - self.order.unwrap() + self.datetime = self.date.as_ref().map(|s| s.as_ref()).and_then(parse_datetime); + self.datetime_tuple = self.datetime.map(|dt| (dt.year(), dt.month(), dt.day())); } pub fn weight(&self) -> usize { @@ -128,7 +128,6 @@ impl Default for PageFrontMatter { slug: None, path: None, taxonomies: HashMap::new(), - order: None, weight: None, aliases: Vec::new(), in_search_index: true, @@ -198,7 +197,7 @@ mod tests { date = 2016-10-10 "#; let res = PageFrontMatter::parse(content).unwrap(); - assert!(res.date.is_some()); + assert!(res.datetime.is_some()); } #[test] @@ -209,7 +208,51 @@ mod tests { date = 2002-10-02T15:00:00Z "#; let res = PageFrontMatter::parse(content).unwrap(); - assert!(res.date.is_some()); + assert!(res.datetime.is_some()); + } + + #[test] + fn can_parse_date_rfc3339_without_timezone() { + let content = r#" + title = "Hello" + description = "hey there" + date = 2002-10-02T15:00:00 + "#; + let res = PageFrontMatter::parse(content).unwrap(); + assert!(res.datetime.is_some()); + } + + #[test] + fn can_parse_date_rfc3339_with_space() { + let content = r#" + title = "Hello" + description = "hey there" + date = 2002-10-02 15:00:00+02:00 + "#; + let res = PageFrontMatter::parse(content).unwrap(); + assert!(res.datetime.is_some()); + } + + #[test] + fn can_parse_date_rfc3339_with_space_without_timezone() { + let content = r#" + title = "Hello" + description = "hey there" + date = 2002-10-02 15:00:00 + "#; + let res = PageFrontMatter::parse(content).unwrap(); + assert!(res.datetime.is_some()); + } + + #[test] + fn can_parse_date_rfc3339_with_microseconds() { + let content = r#" + title = "Hello" + description = "hey there" + date = 2002-10-02T15:00:00.123456Z + "#; + let res = PageFrontMatter::parse(content).unwrap(); + assert!(res.datetime.is_some()); } #[test] @@ -270,6 +313,23 @@ mod tests { assert_eq!(res.unwrap().extra["something"]["some-date"], to_value("2002-14-01").unwrap()); } + #[test] + fn can_parse_fully_nested_dates_in_extra() { + let content = r#" + title = "Hello" + description = "hey there" + + [extra] + date_example = 2020-05-04 + [[extra.questions]] + date = 2020-05-03 + name = "Who is the prime minister of Uganda?""#; + let res = PageFrontMatter::parse(content); + println!("{:?}", res); + assert!(res.is_ok()); + assert_eq!(res.unwrap().extra["questions"][0]["date"], to_value("2020-05-03").unwrap()); + } + #[test] fn can_parse_taxonomies() { let content = r#" diff --git a/components/front_matter/src/section.rs b/components/front_matter/src/section.rs index 5bcfef50..365dd524 100644 --- a/components/front_matter/src/section.rs +++ b/components/front_matter/src/section.rs @@ -1,6 +1,5 @@ use serde_derive::{Deserialize, Serialize}; use tera::{Map, Value}; -use toml; use super::{InsertAnchor, SortBy}; use errors::{bail, Result}; @@ -29,6 +28,9 @@ pub struct SectionFrontMatter { /// How many pages to be displayed per paginated page. No pagination will happen if this isn't set #[serde(skip_serializing)] pub paginate_by: Option, + /// Whether to reverse the order of the pages before segmenting into pagers + #[serde(skip_serializing)] + pub paginate_reversed: bool, /// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. #[serde(skip_serializing)] pub paginate_path: String, @@ -61,6 +63,9 @@ pub struct SectionFrontMatter { /// redirect to this #[serde(skip_serializing)] pub aliases: Vec, + /// Whether to generate a feed for the current section + #[serde(skip_serializing)] + pub generate_feed: bool, /// Any extra parameter present in the front matter pub extra: Map, } @@ -98,6 +103,7 @@ impl Default for SectionFrontMatter { weight: 0, template: None, paginate_by: None, + paginate_reversed: false, paginate_path: DEFAULT_PAGINATE_PATH.to_string(), render: true, redirect_to: None, @@ -106,6 +112,7 @@ impl Default for SectionFrontMatter { transparent: false, page_template: None, aliases: Vec::new(), + generate_feed: false, extra: Map::new(), } } diff --git a/components/library/src/content/file_info.rs b/components/library/src/content/file_info.rs index 3dc0a537..7cae3c11 100644 --- a/components/library/src/content/file_info.rs +++ b/components/library/src/content/file_info.rs @@ -27,7 +27,7 @@ pub fn find_content_components>(path: P) -> Vec { } /// Struct that contains all the information about the actual file -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct FileInfo { /// The full path to the .md file pub path: PathBuf, @@ -143,22 +143,6 @@ impl FileInfo { } } -#[doc(hidden)] -impl Default for FileInfo { - fn default() -> FileInfo { - FileInfo { - path: PathBuf::new(), - parent: PathBuf::new(), - grand_parent: None, - filename: String::new(), - name: String::new(), - components: vec![], - relative: String::new(), - canonical: PathBuf::new(), - } - } -} - #[cfg(test)] mod tests { use std::path::{Path, PathBuf}; diff --git a/components/library/src/content/page.rs b/components/library/src/content/page.rs index dbea1bd9..5e7578b1 100644 --- a/components/library/src/content/page.rs +++ b/components/library/src/content/page.rs @@ -29,7 +29,7 @@ lazy_static! { ).unwrap(); } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Page { /// All info about the actual file pub file: FileInfo, @@ -48,7 +48,7 @@ pub struct Page { /// The slug of that page. /// First tries to find the slug in the meta and defaults to filename otherwise pub slug: String, - /// The URL path of the page + /// The URL path of the page, always starting with a slash pub path: String, /// The components of the path of the page pub components: Vec, @@ -91,31 +91,7 @@ impl Page { pub fn new>(file_path: P, meta: PageFrontMatter, base_path: &PathBuf) -> Page { let file_path = file_path.as_ref(); - Page { - file: FileInfo::new_page(file_path, base_path), - meta, - ancestors: vec![], - raw_content: "".to_string(), - assets: vec![], - serialized_assets: vec![], - content: "".to_string(), - slug: "".to_string(), - path: "".to_string(), - components: vec![], - permalink: "".to_string(), - summary: None, - earlier: None, - later: None, - lighter: None, - heavier: None, - toc: vec![], - word_count: None, - reading_time: None, - lang: String::new(), - translations: Vec::new(), - internal_links_with_anchors: Vec::new(), - external_links: Vec::new(), - } + Page { file: FileInfo::new_page(file_path, base_path), meta, ..Self::default() } } pub fn is_draft(&self) -> bool { @@ -136,7 +112,7 @@ impl Page { page.lang = page.file.find_language(config)?; - page.raw_content = content; + page.raw_content = content.to_string(); let (word_count, reading_time) = get_reading_analytics(&page.raw_content); page.word_count = Some(word_count); page.reading_time = Some(reading_time); @@ -182,8 +158,14 @@ impl Page { } }; - if let Some(ref p) = page.meta.path { - page.path = p.trim().trim_start_matches('/').to_string(); + page.path = if let Some(ref p) = page.meta.path { + let path = p.trim(); + + if path.starts_with('/') { + path.into() + } else { + format!("/{}", path) + } } else { let mut path = if page.file.components.is_empty() { page.slug.clone() @@ -195,8 +177,8 @@ impl Page { path = format!("{}/{}", page.lang, path); } - page.path = path; - } + format!("/{}", path) + }; if !page.path.ends_with('/') { page.path = format!("{}/", page.path); @@ -238,7 +220,7 @@ impl Page { page.assets = assets .into_iter() .filter(|path| match path.file_name() { - None => true, + None => false, Some(file) => !globset.is_match(file), }) .collect(); @@ -335,36 +317,6 @@ impl Page { } } -impl Default for Page { - fn default() -> Page { - Page { - file: FileInfo::default(), - meta: PageFrontMatter::default(), - ancestors: vec![], - raw_content: "".to_string(), - assets: vec![], - serialized_assets: vec![], - content: "".to_string(), - slug: "".to_string(), - path: "".to_string(), - components: vec![], - permalink: "".to_string(), - summary: None, - earlier: None, - later: None, - lighter: None, - heavier: None, - toc: vec![], - word_count: None, - reading_time: None, - lang: String::new(), - translations: Vec::new(), - internal_links_with_anchors: Vec::new(), - external_links: Vec::new(), - } - } -} - #[cfg(test)] mod tests { use std::collections::HashMap; @@ -420,7 +372,7 @@ Hello world"#; Page::parse(Path::new("content/posts/intro/start.md"), content, &conf, &PathBuf::new()); assert!(res.is_ok()); let page = res.unwrap(); - assert_eq!(page.path, "posts/intro/hello-world/"); + assert_eq!(page.path, "/posts/intro/hello-world/"); assert_eq!(page.components, vec!["posts", "intro", "hello-world"]); assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world/"); } @@ -436,7 +388,7 @@ Hello world"#; let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); assert!(res.is_ok()); let page = res.unwrap(); - assert_eq!(page.path, "hello-world/"); + assert_eq!(page.path, "/hello-world/"); assert_eq!(page.components, vec!["hello-world"]); assert_eq!(page.permalink, config.make_permalink("hello-world")); } @@ -453,7 +405,7 @@ Hello world"#; let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); assert!(res.is_ok()); let page = res.unwrap(); - assert_eq!(page.path, "hello-world/"); + assert_eq!(page.path, "/hello-world/"); assert_eq!(page.components, vec!["hello-world"]); assert_eq!(page.permalink, config.make_permalink("hello-world")); } @@ -470,7 +422,7 @@ Hello world"#; let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); assert!(res.is_ok()); let page = res.unwrap(); - assert_eq!(page.path, "日本/"); + assert_eq!(page.path, "/日本/"); assert_eq!(page.components, vec!["日本"]); assert_eq!(page.permalink, config.make_permalink("日本")); } @@ -491,7 +443,7 @@ Hello world"#; ); assert!(res.is_ok()); let page = res.unwrap(); - assert_eq!(page.path, "hello-world/"); + assert_eq!(page.path, "/hello-world/"); assert_eq!(page.components, vec!["hello-world"]); assert_eq!(page.permalink, config.make_permalink("hello-world")); } @@ -512,7 +464,7 @@ Hello world"#; ); assert!(res.is_ok()); let page = res.unwrap(); - assert_eq!(page.path, "hello-world/"); + assert_eq!(page.path, "/hello-world/"); assert_eq!(page.permalink, config.make_permalink("hello-world")); } diff --git a/components/library/src/content/section.rs b/components/library/src/content/section.rs index 9630a725..c3c5270f 100644 --- a/components/library/src/content/section.rs +++ b/components/library/src/content/section.rs @@ -17,13 +17,14 @@ use crate::content::has_anchor; use crate::content::ser::SerializingSection; use crate::library::Library; -#[derive(Clone, Debug, PartialEq)] +// Default is used to create a default index section if there is no _index.md in the root content directory +#[derive(Clone, Debug, Default, PartialEq)] pub struct Section { /// All info about the actual file pub file: FileInfo, /// The front matter meta-data pub meta: SectionFrontMatter, - /// The URL path of the page + /// The URL path of the page, always starting with a slash pub path: String, /// The components for the path of that page pub components: Vec, @@ -74,28 +75,7 @@ impl Section { ) -> Section { let file_path = file_path.as_ref(); - Section { - file: FileInfo::new_section(file_path, base_path), - meta, - ancestors: vec![], - path: "".to_string(), - components: vec![], - permalink: "".to_string(), - raw_content: "".to_string(), - assets: vec![], - serialized_assets: vec![], - content: "".to_string(), - pages: vec![], - ignored_pages: vec![], - subsections: vec![], - toc: vec![], - word_count: None, - reading_time: None, - lang: String::new(), - translations: Vec::new(), - internal_links_with_anchors: Vec::new(), - external_links: Vec::new(), - } + Section { file: FileInfo::new_section(file_path, base_path), meta, ..Self::default() } } pub fn parse( @@ -107,20 +87,22 @@ impl Section { let (meta, content) = split_section_content(file_path, content)?; let mut section = Section::new(file_path, meta, base_path); section.lang = section.file.find_language(config)?; - section.raw_content = content; + section.raw_content = content.to_string(); let (word_count, reading_time) = get_reading_analytics(§ion.raw_content); section.word_count = Some(word_count); section.reading_time = Some(reading_time); + let path = section.file.components.join("/"); - if section.lang != config.default_language { - if path.is_empty() { - section.path = format!("{}/", section.lang); - } else { - section.path = format!("{}/{}/", section.lang, path); - } + let lang_path = if section.lang != config.default_language { + format!("/{}", section.lang) } else { - section.path = format!("{}/", path); - } + "".into() + }; + section.path = if path.is_empty() { + format!("{}/", lang_path) + } else { + format!("{}/{}/", lang_path, path) + }; section.components = section .path @@ -156,7 +138,7 @@ impl Section { section.assets = assets .into_iter() .filter(|path| match path.file_name() { - None => true, + None => false, Some(file) => !globset.is_match(file), }) .collect(); @@ -252,32 +234,14 @@ impl Section { pub fn to_serialized_basic<'a>(&'a self, library: &'a Library) -> SerializingSection<'a> { SerializingSection::from_section_basic(self, Some(library)) } -} -/// Used to create a default index section if there is no _index.md in the root content directory -impl Default for Section { - fn default() -> Section { - Section { - file: FileInfo::default(), - meta: SectionFrontMatter::default(), - ancestors: vec![], - path: "".to_string(), - components: vec![], - permalink: "".to_string(), - raw_content: "".to_string(), - assets: vec![], - serialized_assets: vec![], - content: "".to_string(), - pages: vec![], - ignored_pages: vec![], - subsections: vec![], - toc: vec![], - reading_time: None, - word_count: None, - lang: String::new(), - translations: Vec::new(), - internal_links_with_anchors: Vec::new(), - external_links: Vec::new(), + pub fn paginate_by(&self) -> Option { + match self.meta.paginate_by { + None => None, + Some(x) => match x { + 0 => None, + _ => Some(x), + }, } } } diff --git a/components/library/src/content/ser.rs b/components/library/src/content/ser.rs index 39e6439c..23f8aca1 100644 --- a/components/library/src/content/ser.rs +++ b/components/library/src/content/ser.rs @@ -150,6 +150,11 @@ impl<'a> SerializingPage<'a> { } } + /// currently only used in testing + pub fn get_title(&'a self) -> &'a Option { + &self.title + } + /// Same as from_page but does not fill sibling pages pub fn from_page_basic(page: &'a Page, library: Option<&'a Library>) -> Self { let mut year = None; diff --git a/components/library/src/pagination/mod.rs b/components/library/src/pagination/mod.rs index c186406f..0a837d4b 100644 --- a/components/library/src/pagination/mod.rs +++ b/components/library/src/pagination/mod.rs @@ -12,6 +12,8 @@ use crate::content::{Section, SerializingPage, SerializingSection}; use crate::library::Library; use crate::taxonomies::{Taxonomy, TaxonomyItem}; +use std::borrow::Cow; + #[derive(Clone, Debug, PartialEq)] enum PaginationRoot<'a> { Section(&'a Section), @@ -45,11 +47,13 @@ impl<'a> Pager<'a> { #[derive(Clone, Debug, PartialEq)] pub struct Paginator<'a> { /// All pages in the section/taxonomy - all_pages: &'a [DefaultKey], + all_pages: Cow<'a, [DefaultKey]>, /// Pages split in chunks of `paginate_by` pub pagers: Vec>, /// How many content pages on a paginated page at max paginate_by: usize, + /// whether to reverse before grouping + paginate_reversed: bool, /// The thing we are creating the paginator for: section or taxonomy root: PaginationRoot<'a>, // Those below can be obtained from the root but it would make the code more complex than needed @@ -66,10 +70,12 @@ impl<'a> Paginator<'a> { /// It will always at least create one pager (the first) even if there are not enough pages to paginate pub fn from_section(section: &'a Section, library: &'a Library) -> Paginator<'a> { let paginate_by = section.meta.paginate_by.unwrap(); + let paginate_reversed = section.meta.paginate_reversed; let mut paginator = Paginator { - all_pages: §ion.pages, + all_pages: Cow::from(§ion.pages[..]), pagers: Vec::with_capacity(section.pages.len() / paginate_by), paginate_by, + paginate_reversed, root: PaginationRoot::Section(section), permalink: section.permalink.clone(), path: section.path.clone(), @@ -91,12 +97,13 @@ impl<'a> Paginator<'a> { ) -> Paginator<'a> { let paginate_by = taxonomy.kind.paginate_by.unwrap(); let mut paginator = Paginator { - all_pages: &item.pages, + all_pages: Cow::Borrowed(&item.pages), pagers: Vec::with_capacity(item.pages.len() / paginate_by), paginate_by, + paginate_reversed: false, root: PaginationRoot::Taxonomy(taxonomy, item), permalink: item.permalink.clone(), - path: format!("{}/{}", taxonomy.kind.name, item.slug), + path: format!("/{}/{}/", taxonomy.kind.name, item.slug), paginate_path: taxonomy .kind .paginate_path @@ -106,6 +113,7 @@ impl<'a> Paginator<'a> { template: format!("{}/single.html", taxonomy.kind.name), }; + // taxonomy paginators have no sorting so we won't have to reverse paginator.fill_pagers(library); paginator } @@ -116,8 +124,12 @@ impl<'a> Paginator<'a> { // the pages in the current pagers let mut current_page = vec![]; - for key in self.all_pages { - let page = library.get_page_by_key(*key); + if self.paginate_reversed { + self.all_pages.to_mut().reverse(); + } + + for key in self.all_pages.to_mut().iter_mut() { + let page = library.get_page_by_key(key.clone()); current_page.push(page.to_serialized_basic(library)); if current_page.len() == self.paginate_by { @@ -146,7 +158,7 @@ impl<'a> Paginator<'a> { let permalink = format!("{}{}", self.permalink, page_path); let pager_path = if self.is_index { - page_path + format!("/{}", page_path) } else if self.path.ends_with('/') { format!("{}{}", self.path, page_path) } else { @@ -246,30 +258,39 @@ mod tests { use super::Paginator; - fn create_section(is_index: bool) -> Section { + fn create_section(is_index: bool, paginate_reversed: bool) -> Section { let mut f = SectionFrontMatter::default(); f.paginate_by = Some(2); f.paginate_path = "page".to_string(); + f.paginate_reversed = paginate_reversed; let mut s = Section::new("content/_index.md", f, &PathBuf::new()); if !is_index { - s.path = "posts/".to_string(); + s.path = "/posts/".to_string(); s.permalink = "https://vincent.is/posts/".to_string(); s.file.components = vec!["posts".to_string()]; } else { + s.path = "/".into(); s.permalink = "https://vincent.is/".to_string(); } s } - fn create_library(is_index: bool) -> (Section, Library) { - let mut library = Library::new(3, 0, false); - library.insert_page(Page::default()); - library.insert_page(Page::default()); - library.insert_page(Page::default()); + fn create_library( + is_index: bool, + num_pages: usize, + paginate_reversed: bool, + ) -> (Section, Library) { + let mut library = Library::new(num_pages, 0, false); + for i in 1..=num_pages { + let mut page = Page::default(); + page.meta.title = Some(i.to_string()); + library.insert_page(page); + } + let mut draft = Page::default(); draft.meta.draft = true; library.insert_page(draft); - let mut section = create_section(is_index); + let mut section = create_section(is_index, paginate_reversed); section.pages = library.pages().keys().collect(); library.insert_section(section.clone()); @@ -278,41 +299,88 @@ mod tests { #[test] fn test_can_create_paginator() { - let (section, library) = create_library(false); + let (section, library) = create_library(false, 3, false); let paginator = Paginator::from_section(§ion, &library); assert_eq!(paginator.pagers.len(), 2); assert_eq!(paginator.pagers[0].index, 1); assert_eq!(paginator.pagers[0].pages.len(), 2); assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/"); - assert_eq!(paginator.pagers[0].path, "posts/"); + assert_eq!(paginator.pagers[0].path, "/posts/"); assert_eq!(paginator.pagers[1].index, 2); assert_eq!(paginator.pagers[1].pages.len(), 2); assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/page/2/"); - assert_eq!(paginator.pagers[1].path, "posts/page/2/"); + assert_eq!(paginator.pagers[1].path, "/posts/page/2/"); + } + + #[test] + fn test_can_create_reversed_paginator() { + // 6 pages, 5 normal and 1 draft + let (section, library) = create_library(false, 5, true); + let paginator = Paginator::from_section(§ion, &library); + assert_eq!(paginator.pagers.len(), 3); + + assert_eq!(paginator.pagers[0].index, 1); + assert_eq!(paginator.pagers[0].pages.len(), 2); + assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/"); + assert_eq!(paginator.pagers[0].path, "/posts/"); + assert_eq!( + vec!["".to_string(), "5".to_string()], + paginator.pagers[0] + .pages + .iter() + .map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string()) + .collect::>() + ); + + assert_eq!(paginator.pagers[1].index, 2); + assert_eq!(paginator.pagers[1].pages.len(), 2); + assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/page/2/"); + assert_eq!(paginator.pagers[1].path, "/posts/page/2/"); + assert_eq!( + vec!["4".to_string(), "3".to_string()], + paginator.pagers[1] + .pages + .iter() + .map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string()) + .collect::>() + ); + + assert_eq!(paginator.pagers[2].index, 3); + assert_eq!(paginator.pagers[2].pages.len(), 2); + assert_eq!(paginator.pagers[2].permalink, "https://vincent.is/posts/page/3/"); + assert_eq!(paginator.pagers[2].path, "/posts/page/3/"); + assert_eq!( + vec!["2".to_string(), "1".to_string()], + paginator.pagers[2] + .pages + .iter() + .map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string()) + .collect::>() + ); } #[test] fn test_can_create_paginator_for_index() { - let (section, library) = create_library(true); + let (section, library) = create_library(true, 3, false); let paginator = Paginator::from_section(§ion, &library); assert_eq!(paginator.pagers.len(), 2); assert_eq!(paginator.pagers[0].index, 1); assert_eq!(paginator.pagers[0].pages.len(), 2); assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/"); - assert_eq!(paginator.pagers[0].path, ""); + assert_eq!(paginator.pagers[0].path, "/"); assert_eq!(paginator.pagers[1].index, 2); assert_eq!(paginator.pagers[1].pages.len(), 2); assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/page/2/"); - assert_eq!(paginator.pagers[1].path, "page/2/"); + assert_eq!(paginator.pagers[1].path, "/page/2/"); } #[test] fn test_can_build_paginator_context() { - let (section, library) = create_library(false); + let (section, library) = create_library(false, 3, false); let paginator = Paginator::from_section(§ion, &library); assert_eq!(paginator.pagers.len(), 2); @@ -336,7 +404,7 @@ mod tests { #[test] fn test_can_create_paginator_for_taxonomy() { - let (_, library) = create_library(false); + let (_, library) = create_library(false, 3, false); let taxonomy_def = TaxonomyConfig { name: "tags".to_string(), paginate_by: Some(2), @@ -355,18 +423,18 @@ mod tests { assert_eq!(paginator.pagers[0].index, 1); assert_eq!(paginator.pagers[0].pages.len(), 2); assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/tags/something/"); - assert_eq!(paginator.pagers[0].path, "tags/something"); + assert_eq!(paginator.pagers[0].path, "/tags/something/"); assert_eq!(paginator.pagers[1].index, 2); assert_eq!(paginator.pagers[1].pages.len(), 2); assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/tags/something/page/2/"); - assert_eq!(paginator.pagers[1].path, "tags/something/page/2/"); + assert_eq!(paginator.pagers[1].path, "/tags/something/page/2/"); } // https://github.com/getzola/zola/issues/866 #[test] fn works_with_empty_paginate_path() { - let (mut section, library) = create_library(false); + let (mut section, library) = create_library(false, 3, false); section.meta.paginate_path = String::new(); let paginator = Paginator::from_section(§ion, &library); assert_eq!(paginator.pagers.len(), 2); @@ -374,12 +442,12 @@ mod tests { assert_eq!(paginator.pagers[0].index, 1); assert_eq!(paginator.pagers[0].pages.len(), 2); assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/"); - assert_eq!(paginator.pagers[0].path, "posts/"); + assert_eq!(paginator.pagers[0].path, "/posts/"); assert_eq!(paginator.pagers[1].index, 2); assert_eq!(paginator.pagers[1].pages.len(), 2); assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/2/"); - assert_eq!(paginator.pagers[1].path, "posts/2/"); + assert_eq!(paginator.pagers[1].path, "/posts/2/"); let context = paginator.build_paginator_context(&paginator.pagers[0]); assert_eq!(context["base_url"], to_value("https://vincent.is/posts/").unwrap()); diff --git a/components/library/src/taxonomies/mod.rs b/components/library/src/taxonomies/mod.rs index c2f700f1..62062c30 100644 --- a/components/library/src/taxonomies/mod.rs +++ b/components/library/src/taxonomies/mod.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::collections::HashMap; use serde_derive::Serialize; @@ -40,7 +41,7 @@ impl<'a> SerializedTaxonomyItem<'a> { } /// A taxonomy with all its pages -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct TaxonomyItem { pub name: String, pub slug: String, @@ -70,22 +71,33 @@ impl TaxonomyItem { }) .collect(); let (mut pages, ignored_pages) = sort_pages_by_date(data); - let slug = slugify_paths(name, config.slugify.taxonomies); + let item_slug = slugify_paths(name, config.slugify.taxonomies); + let taxo_slug = slugify_paths(&taxonomy.name, config.slugify.taxonomies); let permalink = if taxonomy.lang != config.default_language { - config.make_permalink(&format!("/{}/{}/{}", taxonomy.lang, taxonomy.name, slug)) + config.make_permalink(&format!("/{}/{}/{}", taxonomy.lang, taxo_slug, item_slug)) } else { - config.make_permalink(&format!("/{}/{}", taxonomy.name, slug)) + config.make_permalink(&format!("/{}/{}", taxo_slug, item_slug)) }; // We still append pages without dates at the end pages.extend(ignored_pages); - TaxonomyItem { name: name.to_string(), permalink, slug, pages } + TaxonomyItem { name: name.to_string(), permalink, slug: item_slug, pages } } pub fn serialize<'a>(&'a self, library: &'a Library) -> SerializedTaxonomyItem<'a> { SerializedTaxonomyItem::from_item(self, library) } + + pub fn merge(&mut self, other: Self) { + self.pages.extend(other.pages); + } +} + +impl PartialEq for TaxonomyItem { + fn eq(&self, other: &Self) -> bool { + self.permalink == other.permalink + } } #[derive(Debug, Clone, PartialEq, Serialize)] @@ -121,8 +133,23 @@ impl Taxonomy { for (name, pages) in items { sorted_items.push(TaxonomyItem::new(&name, &kind, config, pages, library)); } - sorted_items.sort_by(|a, b| a.name.cmp(&b.name)); - + //sorted_items.sort_by(|a, b| a.name.cmp(&b.name)); + sorted_items.sort_by(|a, b| match a.slug.cmp(&b.slug) { + Ordering::Less => Ordering::Less, + Ordering::Greater => Ordering::Greater, + Ordering::Equal => a.name.cmp(&b.name), + }); + sorted_items.dedup_by(|a, b| { + // custom Eq impl checks for equal permalinks + // here we make sure all pages from a get coppied to b + // before dedup gets rid of it + if a == b { + b.merge(a.to_owned()); + true + } else { + false + } + }); Taxonomy { kind, items: sorted_items } } @@ -150,7 +177,7 @@ impl Taxonomy { "current_url", &config.make_permalink(&format!("{}/{}", self.kind.name, item.slug)), ); - context.insert("current_path", &format!("/{}/{}", self.kind.name, item.slug)); + context.insert("current_path", &format!("/{}/{}/", self.kind.name, item.slug)); render_template(&format!("{}/single.html", self.kind.name), tera, context, &config.theme) .map_err(|e| { @@ -172,7 +199,7 @@ impl Taxonomy { context.insert("lang", &self.kind.lang); context.insert("taxonomy", &self.kind); context.insert("current_url", &config.make_permalink(&self.kind.name)); - context.insert("current_path", &self.kind.name); + context.insert("current_path", &format!("/{}/", self.kind.name)); render_template(&format!("{}/list.html", self.kind.name), tera, context, &config.theme) .map_err(|e| { @@ -189,23 +216,25 @@ pub fn find_taxonomies(config: &Config, library: &Library) -> Result Result { let body = { let mut buf: Vec = vec![]; response.copy_to(&mut buf).unwrap(); - String::from_utf8(buf).unwrap() + match String::from_utf8(buf) { + Ok(s) => s, + Err(_) => return Err("The page didn't return valid UTF-8".to_string()), + } }; match check_page_for_anchor(url, body) { @@ -101,11 +104,15 @@ fn has_anchor(url: &str) -> bool { fn check_page_for_anchor(url: &str, body: String) -> errors::Result<()> { let index = url.find('#').unwrap(); let anchor = url.get(index + 1..).unwrap(); - let checks: [String; 8] = [ + let checks = [ + format!(" id={}", anchor), + format!(" ID={}", anchor), format!(" id='{}'", anchor), format!(" ID='{}'", anchor), format!(r#" id="{}""#, anchor), format!(r#" ID="{}""#, anchor), + format!(" name={}", anchor), + format!(" NAME={}", anchor), format!(" name='{}'", anchor), format!(" NAME='{}'", anchor), format!(r#" name="{}""#, anchor), @@ -256,7 +263,7 @@ mod tests { } #[test] - fn can_validate_anchors() { + fn can_validate_anchors_with_double_quotes() { let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect"; let body = r#"

collect

"#.to_string(); let res = check_page_for_anchor(url, body); @@ -273,9 +280,17 @@ mod tests { } #[test] - fn can_validate_anchors_with_other_quotes() { + fn can_validate_anchors_with_single_quotes() { let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect"; - let body = r#"

collect

"#.to_string(); + let body = "

collect

".to_string(); + let res = check_page_for_anchor(url, body); + assert!(res.is_ok()); + } + + #[test] + fn can_validate_anchors_without_quotes() { + let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect"; + let body = "

collect

".to_string(); let res = check_page_for_anchor(url, body); assert!(res.is_ok()); } diff --git a/components/rebuild/Cargo.toml b/components/rebuild/Cargo.toml deleted file mode 100644 index 5dca9b19..00000000 --- a/components/rebuild/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "rebuild" -version = "0.1.0" -authors = ["Vincent Prouillet "] -edition = "2018" - -[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 c0918954..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, - None, - ) - } - "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/Cargo.toml b/components/rendering/Cargo.toml index 5e30e0de..9cca92fb 100644 --- a/components/rendering/Cargo.toml +++ b/components/rendering/Cargo.toml @@ -3,11 +3,12 @@ name = "rendering" version = "0.1.0" authors = ["Vincent Prouillet "] edition = "2018" +include = ["src/**/*"] [dependencies] tera = { version = "1", features = ["preserve_order"] } syntect = "4.1" -pulldown-cmark = "0.7" +pulldown-cmark = { version = "0.8", default-features = false } serde = "1" serde_derive = "1" pest = "2" diff --git a/components/rendering/src/lib.rs b/components/rendering/src/lib.rs index d6a6cd3e..dd896f93 100644 --- a/components/rendering/src/lib.rs +++ b/components/rendering/src/lib.rs @@ -14,7 +14,9 @@ pub fn render_content(content: &str, context: &RenderContext) -> Result", "\n"); + return Ok(html); } markdown_to_html(&content, context) diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index 9111e2ce..75b644e0 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -185,6 +185,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result" at this stage, which will be undone after markdown rendering in lib.rs. Since + // that is an HTML comment, it shouldn't be rendered anyway. and not cause problems unless + // someone wants to include that comment in their content. This behaviour is unwanted in when + // rendering markdown shortcodes. + if template_name.ends_with(".html") { + Ok(res.replace('\n', "").to_string()) + } else { + Ok(res.to_string()) + } } pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result { @@ -413,8 +420,8 @@ Some body {{ hello() }}{%/* end */%}"#, fn shortcodes_with_body_do_not_eat_newlines() { let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", "{{body | safe}}").unwrap(); - let res = render_shortcodes("Body\n {% youtube() %}\nHello \n World{% end %}", &tera); - assert_eq!(res, "Body\n Hello \n World"); + let res = render_shortcodes("Body\n {% youtube() %}\nHello \n \n\n World{% end %}", &tera); + assert_eq!(res, "Body\n Hello World"); } #[test] @@ -432,4 +439,16 @@ Some body {{ hello() }}{%/* end */%}"#, let res = render_shortcodes("\n{{ youtube() }}\n", &tera); assert_eq!(res, "\n Hello, Zola. \n"); } + + #[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(); + let res = render_shortcodes("{{ youtube() }}", &tera); + assert_eq!(res, "* 1\n* 2\n* 3"); + } } diff --git a/components/rendering/src/table_of_contents.rs b/components/rendering/src/table_of_contents.rs index 5f1d54ef..8e7773d0 100644 --- a/components/rendering/src/table_of_contents.rs +++ b/components/rendering/src/table_of_contents.rs @@ -1,7 +1,7 @@ use serde_derive::Serialize; /// Populated while receiving events from the markdown parser -#[derive(Debug, PartialEq, Clone, Serialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct Heading { pub level: u32, pub id: String, @@ -12,19 +12,7 @@ pub struct Heading { impl Heading { pub fn new(level: u32) -> Heading { - Heading { - level, - id: String::new(), - permalink: String::new(), - title: String::new(), - children: Vec::new(), - } - } -} - -impl Default for Heading { - fn default() -> Self { - Heading::new(0) + Heading { level, ..Self::default() } } } diff --git a/components/rendering/tests/markdown.rs b/components/rendering/tests/markdown.rs index e3d48c92..4b11b397 100644 --- a/components/rendering/tests/markdown.rs +++ b/components/rendering/tests/markdown.rs @@ -788,10 +788,7 @@ fn doesnt_try_to_highlight_content_from_shortcode() { let markdown_string = r#"{{ figure(src="spherecluster.png", caption="Some spheres.") }}"#; - let expected = r#"
- Some spheres. -
Some spheres.
-
"#; + let expected = "
\n \n \"Some\n \n\n
Some spheres.
\n
"; tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); let config = Config::default(); @@ -801,6 +798,28 @@ fn doesnt_try_to_highlight_content_from_shortcode() { assert_eq!(res.body, expected); } +#[test] +fn can_emit_newlines_and_whitespace_with_shortcode() { + let permalinks_ctx = HashMap::new(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + + let shortcode = r#"
+{{ body }}
+
"#; + + let markdown_string = "{% preformatted() %}\nHello\n \n Zola\n \n !\n{% end %}"; + + let expected = "
\nHello\n    \n    Zola\n   \n  !\n
"; + + tera.add_raw_template(&format!("shortcodes/{}.html", "preformatted"), shortcode).unwrap(); + let config = Config::default(); + let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); + + let res = render_content(markdown_string, &context).unwrap(); + assert_eq!(res.body, expected); +} + // TODO: re-enable once it's fixed in Tera // https://github.com/Keats/tera/issues/373 //#[test] @@ -885,3 +904,44 @@ fn stops_with_an_error_on_an_empty_link() { assert!(res.is_err()); assert_eq!(res.unwrap_err().to_string(), expected); } + +#[test] +fn can_passthrough_markdown_from_shortcode() { + let permalinks_ctx = HashMap::new(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + + let shortcode = r#"{% for line in body | split(pat="\n") %} +> {{ line }} +{%- endfor %} + +-- {{ author }} +"#; + let markdown_string = r#" +Hello + +{% quote(author="Vincent") %} +# Passing through + +*to* **the** document +{% end %} + +Bla bla"#; + + let expected = r#"

Hello

+
+

Passing through

+

to the document

+
+

-- Vincent

+

Bla bla

+"#; + + tera.add_raw_template(&format!("shortcodes/{}.md", "quote"), shortcode).unwrap(); + let config = Config::default(); + let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); + + let res = render_content(markdown_string, &context).unwrap(); + println!("{:?}", res); + assert_eq!(res.body, expected); +} diff --git a/components/search/Cargo.toml b/components/search/Cargo.toml index 1b08648b..04326df0 100644 --- a/components/search/Cargo.toml +++ b/components/search/Cargo.toml @@ -5,9 +5,15 @@ authors = ["Vincent Prouillet "] edition = "2018" [dependencies] -elasticlunr-rs = "2" +elasticlunr-rs = {version = "2", default-features = false, features = ["da", "de", "du", "es", "fi", "fr", "it", "pt", "ro", "ru", "sv", "tr"] } ammonia = "3" lazy_static = "1" errors = { path = "../errors" } library = { path = "../library" } +config = { path = "../config" } + +[features] +default = [] +indexing-zh = ["elasticlunr-rs/zh"] +indexing-ja = ["elasticlunr-rs/ja"] diff --git a/components/search/src/lib.rs b/components/search/src/lib.rs index 76eee5d9..c8314e5a 100644 --- a/components/search/src/lib.rs +++ b/components/search/src/lib.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet}; use elasticlunr::{Index, Language}; use lazy_static::lazy_static; +use config::Config; use errors::{bail, Result}; use library::{Library, Section}; @@ -25,11 +26,61 @@ lazy_static! { }; } +fn build_fields(config: &Config) -> Vec { + let mut fields = vec![]; + if config.search.include_title { + fields.push("title".to_owned()); + } + + if config.search.include_description { + fields.push("description".to_owned()); + } + + if config.search.include_content { + fields.push("body".to_owned()); + } + + fields +} + +fn fill_index( + config: &Config, + title: &Option, + description: &Option, + content: &str, +) -> Vec { + let mut row = vec![]; + + if config.search.include_title { + row.push(title.clone().unwrap_or_default()); + } + + if config.search.include_description { + row.push(description.clone().unwrap_or_default()); + } + + if config.search.include_content { + let body = AMMONIA.clean(&content).to_string(); + if let Some(truncate_len) = config.search.truncate_content_length { + // Not great for unicode + // TODO: fix it like the truncate in Tera + match body.char_indices().nth(truncate_len) { + None => row.push(body), + Some((idx, _)) => row.push((&body[..idx]).to_string()), + }; + } else { + row.push(body); + }; + } + + row +} + /// Returns the generated JSON index with all the documents of the site added using /// the language given /// Errors if the language given is not available in Elasticlunr /// TODO: is making `in_search_index` apply to subsections of a `false` section useful? -pub fn build_index(lang: &str, library: &Library) -> Result { +pub fn build_index(lang: &str, library: &Library, config: &Config) -> Result { let language = match Language::from_code(lang) { Some(l) => l, None => { @@ -37,18 +88,18 @@ pub fn build_index(lang: &str, library: &Library) -> Result { } }; - let mut index = Index::with_language(language, &["title", "body"]); + let mut index = Index::with_language(language, &build_fields(&config)); for section in library.sections_values() { if section.lang == lang { - add_section_to_index(&mut index, section, library); + add_section_to_index(&mut index, section, library, config); } } Ok(index.to_json()) } -fn add_section_to_index(index: &mut Index, section: &Section, library: &Library) { +fn add_section_to_index(index: &mut Index, section: &Section, library: &Library, config: &Config) { if !section.meta.in_search_index { return; } @@ -57,10 +108,7 @@ fn add_section_to_index(index: &mut Index, section: &Section, library: &Library) if section.meta.redirect_to.is_none() { index.add_doc( §ion.permalink, - &[ - §ion.meta.title.clone().unwrap_or_default(), - &AMMONIA.clean(§ion.content).to_string(), - ], + &fill_index(config, §ion.meta.title, §ion.meta.description, §ion.content), ); } @@ -72,10 +120,76 @@ fn add_section_to_index(index: &mut Index, section: &Section, library: &Library) index.add_doc( &page.permalink, - &[ - &page.meta.title.clone().unwrap_or_default(), - &AMMONIA.clean(&page.content).to_string(), - ], + &fill_index(config, &page.meta.title, &page.meta.description, &page.content), ); } } + +#[cfg(test)] +mod tests { + use super::*; + + use config::Config; + + #[test] + fn can_build_fields() { + let mut config = Config::default(); + let fields = build_fields(&config); + assert_eq!(fields, vec!["title", "body"]); + + config.search.include_content = false; + config.search.include_description = true; + let fields = build_fields(&config); + assert_eq!(fields, vec!["title", "description"]); + + config.search.include_content = true; + let fields = build_fields(&config); + assert_eq!(fields, vec!["title", "description", "body"]); + + config.search.include_title = false; + let fields = build_fields(&config); + assert_eq!(fields, vec!["description", "body"]); + } + + #[test] + fn can_fill_index_default() { + let config = Config::default(); + let title = Some("A title".to_string()); + let description = Some("A description".to_string()); + let content = "Some content".to_string(); + + let res = fill_index(&config, &title, &description, &content); + assert_eq!(res.len(), 2); + assert_eq!(res[0], title.unwrap()); + assert_eq!(res[1], content); + } + + #[test] + fn can_fill_index_description() { + let mut config = Config::default(); + config.search.include_description = true; + let title = Some("A title".to_string()); + let description = Some("A description".to_string()); + let content = "Some content".to_string(); + + let res = fill_index(&config, &title, &description, &content); + assert_eq!(res.len(), 3); + assert_eq!(res[0], title.unwrap()); + assert_eq!(res[1], description.unwrap()); + assert_eq!(res[2], content); + } + + #[test] + fn can_fill_index_truncated_content() { + let mut config = Config::default(); + config.search.truncate_content_length = Some(5); + let title = Some("A title".to_string()); + let description = Some("A description".to_string()); + let content = "Some content".to_string(); + + let res = fill_index(&config, &title, &description, &content); + assert_eq!(res.len(), 2); + assert_eq!(res[0], title.unwrap()); + assert_eq!(res[1], content[..5]); + } +} diff --git a/components/site/Cargo.toml b/components/site/Cargo.toml index 3174e873..60008852 100644 --- a/components/site/Cargo.toml +++ b/components/site/Cargo.toml @@ -3,14 +3,17 @@ name = "site" version = "0.1.0" authors = ["Vincent Prouillet "] edition = "2018" +include = ["src/**/*"] [dependencies] tera = "1" glob = "0.3" +minify-html = "0.3.6" 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/benches/gen.py b/components/site/benches/gen.py index 0fe649a9..469d1cea 100644 --- a/components/site/benches/gen.py +++ b/components/site/benches/gen.py @@ -98,7 +98,6 @@ def gen_skeleton(name, is_blog): shutil.rmtree(name) os.makedirs(os.path.join(name, "content")) - os.makedirs(os.path.join(name, "static")) with open(os.path.join(name, "config.toml"), "w") as f: if is_blog: @@ -128,6 +127,7 @@ name = "Vincent Prouillet" # Re-use the test templates shutil.copytree("../../../test_site/templates", os.path.join(name, "templates")) shutil.copytree("../../../test_site/themes", os.path.join(name, "themes")) + shutil.copytree("../../../test_site/static", os.path.join(name, "static")) def gen_section(path, num_pages, is_blog): diff --git a/components/site/benches/site.rs b/components/site/benches/site.rs index 754eb413..c5805a49 100644 --- a/components/site/benches/site.rs +++ b/components/site/benches/site.rs @@ -46,7 +46,7 @@ fn bench_render_feed(b: &mut test::Bencher) { site.library.read().unwrap().pages_values(), None, &site.config.default_language, - None, + |c| c, ) .unwrap(); }); diff --git a/components/site/src/feed.rs b/components/site/src/feed.rs new file mode 100644 index 00000000..13c45cdb --- /dev/null +++ b/components/site/src/feed.rs @@ -0,0 +1,79 @@ +use std::path::PathBuf; + +use rayon::prelude::*; +use serde_derive::Serialize; +use tera::Context; + +use crate::Site; +use errors::Result; +use library::{sort_actual_pages_by_date, Page, TaxonomyItem}; +use utils::templates::render_template; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct SerializedFeedTaxonomyItem<'a> { + name: &'a str, + slug: &'a str, + permalink: &'a str, +} + +impl<'a> SerializedFeedTaxonomyItem<'a> { + pub fn from_item(item: &'a TaxonomyItem) -> Self { + SerializedFeedTaxonomyItem { + name: &item.name, + slug: &item.slug, + permalink: &item.permalink, + } + } +} + +pub fn render_feed( + site: &Site, + all_pages: Vec<&Page>, + lang: &str, + base_path: Option<&PathBuf>, + additional_context_fn: impl Fn(Context) -> Context, +) -> Result> { + let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).collect::>(); + + // Don't generate a feed if none of the pages has a date + if pages.is_empty() { + return Ok(None); + } + + pages.par_sort_unstable_by(sort_actual_pages_by_date); + + let mut context = Context::new(); + context.insert( + "last_updated", + pages + .iter() + .filter_map(|page| page.meta.updated.as_ref()) + .chain(pages[0].meta.date.as_ref()) + .max() // I love lexicographically sorted date strings + .unwrap(), // Guaranteed because of pages[0].meta.date + ); + let library = site.library.read().unwrap(); + // limit to the last n elements if the limit is set; otherwise use all. + let num_entries = site.config.feed_limit.unwrap_or_else(|| pages.len()); + let p = + pages.iter().take(num_entries).map(|x| x.to_serialized_basic(&library)).collect::>(); + + context.insert("pages", &p); + context.insert("config", &site.config); + context.insert("lang", lang); + + let feed_filename = &site.config.feed_filename; + let feed_url = if let Some(ref base) = base_path { + site.config.make_permalink(&base.join(feed_filename).to_string_lossy().replace('\\', "/")) + } else { + site.config.make_permalink(feed_filename) + }; + + context.insert("feed_url", &feed_url); + + context = additional_context_fn(context); + + let feed = render_template(feed_filename, &site.tera, context, &site.config.theme)?; + + Ok(Some(feed)) +} diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index dc6e4331..61961b79 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -1,27 +1,44 @@ +pub mod feed; +pub mod link_checking; +pub mod sass; pub mod sitemap; +pub mod tpls; use std::collections::HashMap; -use std::fs::{copy, create_dir_all, 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 minify_html::{truncate, Cfg}; use rayon::prelude::*; -use sass_rs::{compile_file, Options as SassOptions, OutputStyle}; -use serde_derive::Serialize; use tera::{Context, Tera}; -use config::{get_config, Config, Taxonomy as TaxonomyConfig}; -use errors::{bail, Error, ErrorKind, Result}; +use config::{get_config, Config}; +use errors::{bail, Error, Result}; use front_matter::InsertAnchor; -use library::{ - find_taxonomies, sort_actual_pages_by_date, Library, Page, Paginator, Section, Taxonomy, - TaxonomyItem, +use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy}; +use templates::render_redirect_template; +use utils::fs::{ + copy_directory, copy_file_if_needed, create_directory, create_file, ensure_directory_exists, }; -use templates::{global_fns, render_redirect_template, ZOLA_TERA}; -use utils::fs::{copy_directory, create_directory, create_file, ensure_directory_exists}; use utils::net::get_available_port; -use utils::templates::{render_template, rewrite_theme_paths}; +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 { @@ -44,19 +61,7 @@ pub struct Site { pub library: Arc>, /// Whether to load draft pages include_drafts: bool, -} - -#[derive(Debug, Clone, PartialEq, Serialize)] -struct SerializedTaxonomyItem<'a> { - name: &'a str, - slug: &'a str, - permalink: &'a str, -} - -impl<'a> SerializedTaxonomyItem<'a> { - pub fn from_item(item: &'a TaxonomyItem) -> Self { - SerializedTaxonomyItem { name: &item.name, slug: &item.slug, permalink: &item.permalink } - } + build_mode: BuildMode, } impl Site { @@ -68,54 +73,18 @@ impl Site { let mut config = get_config(config_file); config.load_extra_syntaxes(path)?; - let tpl_glob = - format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.*ml"); - // Only parsing as we might be extending templates from themes and that would error - // as we haven't loaded them yet - let mut tera = - Tera::parse(&tpl_glob).map_err(|e| Error::chain("Error parsing templates", e))?; if let Some(theme) = config.theme.clone() { // Grab data from the extra section of the theme config.merge_with_theme(&path.join("themes").join(&theme).join("theme.toml"))?; - - // Test that the templates folder exist for that theme - let theme_path = path.join("themes").join(&theme); - if !theme_path.join("templates").exists() { - bail!("Theme `{}` is missing a templates folder", theme); - } - - let theme_tpl_glob = format!( - "{}/{}", - path.to_string_lossy().replace("\\", "/"), - format!("themes/{}/templates/**/*.*ml", theme) - ); - let mut tera_theme = Tera::parse(&theme_tpl_glob) - .map_err(|e| Error::chain("Error parsing templates from themes", e))?; - rewrite_theme_paths( - &mut tera_theme, - tera.templates.values().map(|v| v.name.as_ref()).collect(), - &theme, - ); - // TODO: we do that twice, make it dry? - if theme_path.join("templates").join("robots.txt").exists() { - tera_theme - .add_template_file(theme_path.join("templates").join("robots.txt"), None)?; - } - tera.extend(&tera_theme)?; } - tera.extend(&ZOLA_TERA)?; - tera.build_inheritance_chains()?; - // TODO: Tera doesn't use globset right now so we can load the robots.txt as part - // of the glob above, therefore we load it manually if it exists. - if path.join("templates").join("robots.txt").exists() { - tera.add_template_file(path.join("templates").join("robots.txt"), Some("robots.txt"))?; - } + let tera = tpls::load_tera(path, &config)?; let content_path = path.join("content"); 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(), @@ -123,7 +92,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(), @@ -131,11 +100,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) { @@ -143,7 +120,7 @@ impl Site { } /// The index sections are ALWAYS at those paths - /// There are one index section for the basic language + 1 per language + /// There are one index section for the default language + 1 per language fn index_section_paths(&self) -> Vec<(PathBuf, Option)> { let mut res = vec![(self.content_path.join("_index.md"), None)]; for language in &self.config.languages { @@ -162,9 +139,16 @@ impl Site { self.live_reload = get_available_port(port_to_avoid); } - /// Get the number of orphan (== without section) pages in the site - pub fn get_number_orphan_pages(&self) -> usize { - self.library.read().unwrap().get_all_orphan_pages().len() + /// 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) { @@ -259,193 +243,21 @@ 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 - self.check_internal_links_with_anchors()?; + link_checking::check_internal_links_with_anchors(&self)?; if self.config.is_in_check_mode() { - self.check_external_links()?; + link_checking::check_external_links(&self)?; } Ok(()) } - /// Very similar to check_external_links but can't be merged as far as I can see since we always - /// want to check the internal links but only the external in zola check :/ - pub fn check_internal_links_with_anchors(&self) -> Result<()> { - let library = self.library.write().expect("Get lock for check_internal_links_with_anchors"); - let page_links = library - .pages() - .values() - .map(|p| { - let path = &p.file.path; - p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l)) - }) - .flatten(); - let section_links = library - .sections() - .values() - .map(|p| { - let path = &p.file.path; - p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l)) - }) - .flatten(); - let all_links = page_links.chain(section_links).collect::>(); - - if self.config.is_in_check_mode() { - println!("Checking {} internal link(s) with an anchor.", all_links.len()); - } - - if all_links.is_empty() { - return Ok(()); - } - - let mut full_path = self.base_path.clone(); - full_path.push("content"); - - let errors: Vec<_> = all_links - .iter() - .filter_map(|(page_path, (md_path, anchor))| { - // There are a few `expect` here since the presence of the .md file will - // already have been checked in the markdown rendering - let mut p = full_path.clone(); - for part in md_path.split('/') { - p.push(part); - } - if md_path.contains("_index.md") { - let section = library - .get_section(&p) - .expect("Couldn't find section in check_internal_links_with_anchors"); - if section.has_anchor(&anchor) { - None - } else { - Some((page_path, md_path, anchor)) - } - } else { - let page = library - .get_page(&p) - .expect("Couldn't find section in check_internal_links_with_anchors"); - if page.has_anchor(&anchor) { - None - } else { - Some((page_path, md_path, anchor)) - } - } - }) - .collect(); - - if self.config.is_in_check_mode() { - println!( - "> Checked {} internal link(s) with an anchor: {} error(s) found.", - all_links.len(), - errors.len() - ); - } - - if errors.is_empty() { - return Ok(()); - } - - let msg = errors - .into_iter() - .map(|(page_path, md_path, anchor)| { - format!( - "The anchor in the link `@/{}#{}` in {} does not exist.", - md_path, - anchor, - page_path.to_string_lossy(), - ) - }) - .collect::>() - .join("\n"); - Err(Error { kind: ErrorKind::Msg(msg), source: None }) - } - - pub fn check_external_links(&self) -> Result<()> { - let library = self.library.write().expect("Get lock for check_external_links"); - let page_links = library - .pages() - .values() - .map(|p| { - let path = &p.file.path; - p.external_links.iter().map(move |l| (path.clone(), l)) - }) - .flatten(); - let section_links = library - .sections() - .values() - .map(|p| { - let path = &p.file.path; - p.external_links.iter().map(move |l| (path.clone(), l)) - }) - .flatten(); - let all_links = page_links.chain(section_links).collect::>(); - println!("Checking {} external link(s).", all_links.len()); - - if all_links.is_empty() { - return Ok(()); - } - - // create thread pool with lots of threads so we can fetch - // (almost) all pages simultaneously - let threads = std::cmp::min(all_links.len(), 32); - let pool = rayon::ThreadPoolBuilder::new() - .num_threads(threads) - .build() - .map_err(|e| Error { kind: ErrorKind::Msg(e.to_string()), source: None })?; - - let errors: Vec<_> = pool.install(|| { - all_links - .par_iter() - .filter_map(|(page_path, link)| { - if self - .config - .link_checker - .skip_prefixes - .iter() - .any(|prefix| link.starts_with(prefix)) - { - return None; - } - let res = link_checker::check_url(&link, &self.config.link_checker); - if link_checker::is_valid(&res) { - None - } else { - Some((page_path, link, res)) - } - }) - .collect() - }); - - println!( - "> Checked {} external link(s): {} error(s) found.", - all_links.len(), - errors.len() - ); - - if errors.is_empty() { - return Ok(()); - } - - let msg = errors - .into_iter() - .map(|(page_path, link, check_res)| { - format!( - "Dead link in {} to {}: {}", - page_path.to_string_lossy(), - link, - link_checker::message(&check_res) - ) - }) - .collect::>() - .join("\n"); - Err(Error { kind: ErrorKind::Msg(msg), source: None }) - } - /// Insert a default index section for each language if necessary so we don't need to create /// a _index.md to render the index page at the root of the site pub fn create_default_index_sections(&mut self) -> Result<()> { @@ -529,80 +341,58 @@ impl Site { Ok(()) } - /// Adds global fns that are to be available to shortcodes while - /// markdown - pub fn register_early_global_fns(&mut self) { - self.tera.register_function( - "get_url", - global_fns::GetUrl::new(self.config.clone(), self.permalinks.clone(), self.content_path.clone()), - ); - self.tera.register_function( - "resize_image", - global_fns::ResizeImage::new(self.imageproc.clone()), - ); - self.tera.register_function( - "get_image_metadata", - global_fns::GetImageMeta::new(self.content_path.clone()), - ); - self.tera.register_function("load_data", global_fns::LoadData::new(self.base_path.clone())); - self.tera.register_function("trans", global_fns::Trans::new(self.config.clone())); - self.tera.register_function( - "get_taxonomy_url", - global_fns::GetTaxonomyUrl::new(&self.config.default_language, &self.taxonomies), - ); - } - - pub fn register_tera_global_fns(&mut self) { - self.tera.register_function( - "get_page", - global_fns::GetPage::new(self.base_path.clone(), self.library.clone()), - ); - self.tera.register_function( - "get_section", - global_fns::GetSection::new(self.base_path.clone(), self.library.clone()), - ); - self.tera.register_function( - "get_taxonomy", - global_fns::GetTaxonomy::new( - &self.config.default_language, - self.taxonomies.clone(), - self.library.clone(), - ), - ); - } - /// 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`. @@ -656,6 +446,21 @@ impl Site { html } + /// Minifies html content + fn minify(&self, html: String) -> Result { + let cfg = &Cfg { minify_js: false }; + let mut input_bytes = html.as_bytes().to_vec(); + match truncate(&mut input_bytes, cfg) { + Ok(_len) => match std::str::from_utf8(&mut input_bytes) { + Ok(result) => Ok(result.to_string()), + Err(err) => bail!("Failed to convert bytes to string : {}", err), + }, + Err(minify_error) => { + bail!("Failed to truncate html at character {}:", minify_error.position); + } + } + } + /// Copy the main `static` folder and the theme `static` folder if a theme is used pub fn copy_static_directories(&self) -> Result<()> { // The user files will overwrite the theme files @@ -697,32 +502,83 @@ 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 + let final_content = if !filename.ends_with("html") || !self.config.minify_html { + content + } else { + match self.minify(content) { + Ok(minified_content) => minified_content, + Err(error) => bail!(error), + } + }; + + match self.build_mode { + BuildMode::Disk => { + let end_path = current_path.join(filename); + create_file(&end_path, &final_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, final_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")), @@ -732,20 +588,23 @@ 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 { let theme_path = self.base_path.join("themes").join(theme); if theme_path.join("sass").exists() { - self.compile_sass(&theme_path)?; + sass::compile_sass(&theme_path, &self.output_path)?; } } if self.config.compile_sass { - self.compile_sass(&self.base_path)?; + sass::compile_sass(&self.base_path, &self.output_path)?; } if self.config.build_search_index { @@ -771,7 +630,7 @@ impl Site { } else { library.pages_values() }; - self.render_feed(pages, None, &self.config.default_language, None)?; + self.render_feed(pages, None, &self.config.default_language, |c| c)?; } for lang in &self.config.languages { @@ -780,7 +639,7 @@ impl Site { } let pages = library.pages_values().iter().filter(|p| p.lang == lang.code).cloned().collect(); - self.render_feed(pages, Some(&PathBuf::from(lang.code.clone())), &lang.code, None)?; + self.render_feed(pages, Some(&PathBuf::from(lang.code.clone())), &lang.code, |c| c)?; } self.render_404()?; @@ -797,12 +656,18 @@ 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)), &format!( "window.searchIndex = {};", - search::build_index(&self.config.default_language, &self.library.read().unwrap())? + search::build_index( + &self.config.default_language, + &self.library.read().unwrap(), + &self.config + )? ), )?; @@ -812,7 +677,11 @@ impl Site { &self.output_path.join(&format!("search_index.{}.js", &language.code)), &format!( "window.searchIndex = {};", - search::build_index(&language.code, &self.library.read().unwrap())? + search::build_index( + &language.code, + &self.library.read().unwrap(), + &self.config + )? ), )?; } @@ -824,73 +693,7 @@ impl Site { Ok(()) } - pub fn compile_sass(&self, base_path: &Path) -> Result<()> { - ensure_directory_exists(&self.output_path)?; - - let sass_path = { - let mut sass_path = PathBuf::from(base_path); - sass_path.push("sass"); - sass_path - }; - - let mut options = SassOptions::default(); - options.output_style = OutputStyle::Compressed; - let mut compiled_paths = self.compile_sass_glob(&sass_path, "scss", &options.clone())?; - - options.indented_syntax = true; - compiled_paths.extend(self.compile_sass_glob(&sass_path, "sass", &options)?); - - compiled_paths.sort(); - for window in compiled_paths.windows(2) { - if window[0].1 == window[1].1 { - bail!( - "SASS path conflict: \"{}\" and \"{}\" both compile to \"{}\"", - window[0].0.display(), - window[1].0.display(), - window[0].1.display(), - ); - } - } - - Ok(()) - } - - fn compile_sass_glob( - &self, - sass_path: &Path, - extension: &str, - options: &SassOptions, - ) -> Result> { - let glob_string = format!("{}/**/*.{}", sass_path.display(), extension); - let files = glob(&glob_string) - .expect("Invalid glob for sass") - .filter_map(|e| e.ok()) - .filter(|entry| { - !entry.as_path().file_name().unwrap().to_string_lossy().starts_with('_') - }) - .collect::>(); - - let mut compiled_paths = Vec::new(); - for file in files { - let css = compile_file(&file, options.clone())?; - - let path_inside_sass = file.strip_prefix(&sass_path).unwrap(); - let parent_inside_sass = path_inside_sass.parent(); - let css_output_path = self.output_path.join(path_inside_sass).with_extension("css"); - - if parent_inside_sass.is_some() { - create_dir_all(&css_output_path.parent().unwrap())?; - } - - create_file(&css_output_path, &css)?; - compiled_paths.push((path_inside_sass.to_owned(), css_output_path)); - } - - Ok(compiled_paths) - } - 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 @@ -903,21 +706,13 @@ 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 + /// the canonical one pub fn render_aliases(&self) -> Result<()> { ensure_directory_exists(&self.output_path)?; let library = self.library.read().unwrap(); @@ -940,7 +735,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 @@ -948,10 +745,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 @@ -969,33 +765,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 { @@ -1007,7 +807,12 @@ impl Site { } else { &self.config.default_language }, - Some((&taxonomy.kind, &item)), + |mut context: Context| { + context.insert("taxonomy", &taxonomy.kind); + context + .insert("term", &feed::SerializedFeedTaxonomyItem::from_item(item)); + context + }, ) } else { Ok(()) @@ -1021,20 +826,16 @@ impl Site { ensure_directory_exists(&self.output_path)?; let library = self.library.read().unwrap(); - let all_sitemap_entries = { - let mut all_sitemap_entries = - sitemap::find_entries(&library, &self.taxonomies[..], &self.config); - all_sitemap_entries.sort(); - all_sitemap_entries - }; + let all_sitemap_entries = + { sitemap::find_entries(&library, &self.taxonomies[..], &self.config) }; let sitemap_limit = 30000; if all_sitemap_entries.len() < sitemap_limit { // 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(()); } @@ -1045,23 +846,24 @@ 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); } + // 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(()) } @@ -1074,70 +876,31 @@ impl Site { all_pages: Vec<&Page>, base_path: Option<&PathBuf>, lang: &str, - taxonomy_and_item: Option<(&TaxonomyConfig, &TaxonomyItem)>, + additional_context_fn: impl Fn(Context) -> Context, ) -> Result<()> { ensure_directory_exists(&self.output_path)?; - let mut context = Context::new(); - let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).collect::>(); - - // Don't generate a feed if none of the pages has a date - if pages.is_empty() { - return Ok(()); - } - - pages.par_sort_unstable_by(sort_actual_pages_by_date); - - context.insert( - "last_updated", - pages - .iter() - .filter_map(|page| page.meta.updated.as_ref()) - .chain(pages[0].meta.date.as_ref()) - .max() // I love lexicographically sorted date strings - .unwrap(), // Guaranteed because of pages[0].meta.date - ); - let library = self.library.read().unwrap(); - // limit to the last n elements if the limit is set; otherwise use all. - let num_entries = self.config.feed_limit.unwrap_or_else(|| pages.len()); - let p = pages - .iter() - .take(num_entries) - .map(|x| x.to_serialized_basic(&library)) - .collect::>(); - - context.insert("pages", &p); - context.insert("config", &self.config); - context.insert("lang", lang); - - let feed_filename = &self.config.feed_filename; - let feed_url = if let Some(ref base) = base_path { - self.config - .make_permalink(&base.join(feed_filename).to_string_lossy().replace('\\', "/")) - } else { - self.config.make_permalink(feed_filename) + let feed = match feed::render_feed(self, all_pages, lang, base_path, additional_context_fn)? + { + Some(v) => v, + None => return Ok(()), }; - - context.insert("feed_url", &feed_url); - - if let Some((taxonomy, item)) = taxonomy_and_item { - context.insert("taxonomy", taxonomy); - context.insert("term", &SerializedTaxonomyItem::from_item(item)); - } - - let feed = &render_template(feed_filename, &self.tera, context, &self.config.theme)?; + 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(()) } @@ -1146,26 +909,49 @@ 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)?; } } + if section.meta.generate_feed { + let library = &self.library.read().unwrap(); + let pages = section + .pages + .iter() + .map(|k| library.get_page_by_key(*k)) + .collect(); + self.render_feed( + pages, + Some(&PathBuf::from(§ion.path[1..])), + §ion.lang, + |mut context: Context| { + context.insert("section", §ion.to_serialized(library)); + context + }, + )?; + } + // 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"), @@ -1187,40 +973,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(()) } - /// 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 @@ -1244,33 +1021,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/site/src/link_checking.rs b/components/site/src/link_checking.rs new file mode 100644 index 00000000..b7facdff --- /dev/null +++ b/components/site/src/link_checking.rs @@ -0,0 +1,172 @@ +use rayon::prelude::*; + +use crate::Site; +use errors::{Error, ErrorKind, Result}; + +/// Very similar to check_external_links but can't be merged as far as I can see since we always +/// want to check the internal links but only the external in zola check :/ +pub fn check_internal_links_with_anchors(site: &Site) -> Result<()> { + let library = site.library.write().expect("Get lock for check_internal_links_with_anchors"); + let page_links = library + .pages() + .values() + .map(|p| { + let path = &p.file.path; + p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l)) + }) + .flatten(); + let section_links = library + .sections() + .values() + .map(|p| { + let path = &p.file.path; + p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l)) + }) + .flatten(); + let all_links = page_links.chain(section_links).collect::>(); + + if site.config.is_in_check_mode() { + println!("Checking {} internal link(s) with an anchor.", all_links.len()); + } + + if all_links.is_empty() { + return Ok(()); + } + + let mut full_path = site.base_path.clone(); + full_path.push("content"); + + let errors: Vec<_> = all_links + .iter() + .filter_map(|(page_path, (md_path, anchor))| { + // There are a few `expect` here since the presence of the .md file will + // already have been checked in the markdown rendering + let mut p = full_path.clone(); + for part in md_path.split('/') { + p.push(part); + } + if md_path.contains("_index.md") { + let section = library + .get_section(&p) + .expect("Couldn't find section in check_internal_links_with_anchors"); + if section.has_anchor(&anchor) { + None + } else { + Some((page_path, md_path, anchor)) + } + } else { + let page = library + .get_page(&p) + .expect("Couldn't find section in check_internal_links_with_anchors"); + if page.has_anchor(&anchor) { + None + } else { + Some((page_path, md_path, anchor)) + } + } + }) + .collect(); + + if site.config.is_in_check_mode() { + println!( + "> Checked {} internal link(s) with an anchor: {} error(s) found.", + all_links.len(), + errors.len() + ); + } + + if errors.is_empty() { + return Ok(()); + } + + let msg = errors + .into_iter() + .map(|(page_path, md_path, anchor)| { + format!( + "The anchor in the link `@/{}#{}` in {} does not exist.", + md_path, + anchor, + page_path.to_string_lossy(), + ) + }) + .collect::>() + .join("\n"); + Err(Error { kind: ErrorKind::Msg(msg), source: None }) +} + +pub fn check_external_links(site: &Site) -> Result<()> { + let library = site.library.write().expect("Get lock for check_external_links"); + let page_links = library + .pages() + .values() + .map(|p| { + let path = &p.file.path; + p.external_links.iter().map(move |l| (path.clone(), l)) + }) + .flatten(); + let section_links = library + .sections() + .values() + .map(|p| { + let path = &p.file.path; + p.external_links.iter().map(move |l| (path.clone(), l)) + }) + .flatten(); + let all_links = page_links.chain(section_links).collect::>(); + println!("Checking {} external link(s).", all_links.len()); + + if all_links.is_empty() { + return Ok(()); + } + + // create thread pool with lots of threads so we can fetch + // (almost) all pages simultaneously + let threads = std::cmp::min(all_links.len(), 32); + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build() + .map_err(|e| Error { kind: ErrorKind::Msg(e.to_string()), source: None })?; + + let errors: Vec<_> = pool.install(|| { + all_links + .par_iter() + .filter_map(|(page_path, link)| { + if site + .config + .link_checker + .skip_prefixes + .iter() + .any(|prefix| link.starts_with(prefix)) + { + return None; + } + let res = link_checker::check_url(&link, &site.config.link_checker); + if link_checker::is_valid(&res) { + None + } else { + Some((page_path, link, res)) + } + }) + .collect() + }); + + println!("> Checked {} external link(s): {} error(s) found.", all_links.len(), errors.len()); + + if errors.is_empty() { + return Ok(()); + } + + let msg = errors + .into_iter() + .map(|(page_path, link, check_res)| { + format!( + "Dead link in {} to {}: {}", + page_path.to_string_lossy(), + link, + link_checker::message(&check_res) + ) + }) + .collect::>() + .join("\n"); + Err(Error { kind: ErrorKind::Msg(msg), source: None }) +} diff --git a/components/site/src/sass.rs b/components/site/src/sass.rs new file mode 100644 index 00000000..2b1b1ef3 --- /dev/null +++ b/components/site/src/sass.rs @@ -0,0 +1,73 @@ +use std::fs::create_dir_all; +use std::path::{Path, PathBuf}; + +use glob::glob; +use sass_rs::{compile_file, Options, OutputStyle}; + +use errors::{bail, Result}; +use utils::fs::{create_file, ensure_directory_exists}; + +pub fn compile_sass(base_path: &Path, output_path: &Path) -> Result<()> { + ensure_directory_exists(&output_path)?; + + let sass_path = { + let mut sass_path = PathBuf::from(base_path); + sass_path.push("sass"); + sass_path + }; + + let mut options = Options::default(); + options.output_style = OutputStyle::Compressed; + let mut compiled_paths = compile_sass_glob(&sass_path, output_path, "scss", &options)?; + + options.indented_syntax = true; + compiled_paths.extend(compile_sass_glob(&sass_path, output_path, "sass", &options)?); + + compiled_paths.sort(); + for window in compiled_paths.windows(2) { + if window[0].1 == window[1].1 { + bail!( + "SASS path conflict: \"{}\" and \"{}\" both compile to \"{}\"", + window[0].0.display(), + window[1].0.display(), + window[0].1.display(), + ); + } + } + + Ok(()) +} + +fn compile_sass_glob( + sass_path: &Path, + output_path: &Path, + extension: &str, + options: &Options, +) -> Result> { + let glob_string = format!("{}/**/*.{}", sass_path.display(), extension); + let files = glob(&glob_string) + .expect("Invalid glob for sass") + .filter_map(|e| e.ok()) + .filter(|entry| { + !entry.as_path().components().any(|c| c.as_os_str().to_string_lossy().starts_with('_')) + }) + .collect::>(); + + let mut compiled_paths = Vec::new(); + for file in files { + let css = compile_file(&file, options.clone())?; + + let path_inside_sass = file.strip_prefix(&sass_path).unwrap(); + let parent_inside_sass = path_inside_sass.parent(); + let css_output_path = output_path.join(path_inside_sass).with_extension("css"); + + if parent_inside_sass.is_some() { + create_dir_all(&css_output_path.parent().unwrap())?; + } + + create_file(&css_output_path, &css)?; + compiled_paths.push((path_inside_sass.to_owned(), css_output_path)); + } + + Ok(compiled_paths) +} diff --git a/components/site/src/sitemap.rs b/components/site/src/sitemap.rs index afb20928..14c4bba5 100644 --- a/components/site/src/sitemap.rs +++ b/components/site/src/sitemap.rs @@ -85,12 +85,14 @@ pub fn find_entries<'a>( }) .collect::>(); - for section in library.sections_values().iter().filter(|s| s.meta.paginate_by.is_some()) { - let number_pagers = - (section.pages.len() as f64 / section.meta.paginate_by.unwrap() as f64).ceil() as isize; - for i in 1..=number_pagers { - let permalink = format!("{}{}/{}/", section.permalink, section.meta.paginate_path, i); - sections.push(SitemapEntry::new(Cow::Owned(permalink), None)) + for section in library.sections_values().iter() { + if let Some(paginate_by) = section.paginate_by() { + let number_pagers = (section.pages.len() as f64 / paginate_by as f64).ceil() as isize; + for i in 1..=number_pagers { + let permalink = + format!("{}{}/{}/", section.permalink, section.meta.paginate_path, i); + sections.push(SitemapEntry::new(Cow::Owned(permalink), None)) + } } } @@ -138,5 +140,7 @@ pub fn find_entries<'a>( } } - all_sitemap_entries.into_iter().collect::>() + let mut entries = all_sitemap_entries.into_iter().collect::>(); + entries.sort(); + entries } diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs new file mode 100644 index 00000000..6a2c9a11 --- /dev/null +++ b/components/site/src/tpls.rs @@ -0,0 +1,105 @@ +use std::path::Path; + +use tera::Tera; + +use crate::Site; +use config::Config; +use errors::{bail, Error, Result}; +use templates::{global_fns, ZOLA_TERA}; +use utils::templates::rewrite_theme_paths; + +pub fn load_tera(path: &Path, config: &Config) -> Result { + let tpl_glob = + format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.{*ml,md}"); + + // Only parsing as we might be extending templates from themes and that would error + // as we haven't loaded them yet + let mut tera = + Tera::parse(&tpl_glob).map_err(|e| Error::chain("Error parsing templates", e))?; + + if let Some(ref theme) = config.theme { + // Test that the templates folder exist for that theme + let theme_path = path.join("themes").join(&theme); + if !theme_path.join("templates").exists() { + bail!("Theme `{}` is missing a templates folder", theme); + } + + let theme_tpl_glob = format!( + "{}/{}", + path.to_string_lossy().replace("\\", "/"), + format!("themes/{}/templates/**/*.{{*ml,md}}", theme) + ); + let mut tera_theme = Tera::parse(&theme_tpl_glob) + .map_err(|e| Error::chain("Error parsing templates from themes", e))?; + rewrite_theme_paths(&mut tera_theme, &theme); + + if theme_path.join("templates").join("robots.txt").exists() { + tera_theme.add_template_file(theme_path.join("templates").join("robots.txt"), None)?; + } + tera.extend(&tera_theme)?; + } + tera.extend(&ZOLA_TERA)?; + tera.build_inheritance_chains()?; + + if path.join("templates").join("robots.txt").exists() { + tera.add_template_file(path.join("templates").join("robots.txt"), Some("robots.txt"))?; + } + + Ok(tera) +} + +/// Adds global fns that are to be available to shortcodes while rendering markdown +pub fn register_early_global_fns(site: &mut Site) { + site.tera.register_function( + "get_url", + global_fns::GetUrl::new( + site.config.clone(), + site.permalinks.clone(), + vec![site.static_path.clone(), site.output_path.clone(), site.content_path.clone()], + ), + ); + site.tera + .register_function("resize_image", global_fns::ResizeImage::new(site.imageproc.clone())); + site.tera.register_function( + "get_image_metadata", + global_fns::GetImageMeta::new(site.content_path.clone()), + ); + site.tera.register_function("load_data", global_fns::LoadData::new(site.base_path.clone())); + site.tera.register_function("trans", global_fns::Trans::new(site.config.clone())); + site.tera.register_function( + "get_taxonomy_url", + global_fns::GetTaxonomyUrl::new( + &site.config.default_language, + &site.taxonomies, + site.config.slugify.taxonomies, + ), + ); + site.tera.register_function( + "get_file_hash", + global_fns::GetFileHash::new(vec![ + site.static_path.clone(), + site.output_path.clone(), + site.content_path.clone(), + ]), + ); +} + +/// Functions filled once we have parsed all the pages/sections only, so not available in shortcodes +pub fn register_tera_global_fns(site: &mut Site) { + site.tera.register_function( + "get_page", + global_fns::GetPage::new(site.base_path.clone(), site.library.clone()), + ); + site.tera.register_function( + "get_section", + global_fns::GetSection::new(site.base_path.clone(), site.library.clone()), + ); + site.tera.register_function( + "get_taxonomy", + global_fns::GetTaxonomy::new( + &site.config.default_language, + site.taxonomies.clone(), + site.library.clone(), + ), + ); +} diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index 5dbc7115..1778f408 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -19,12 +19,12 @@ fn can_parse_site() { let library = site.library.read().unwrap(); // Correct number of pages (sections do not count as pages, draft are ignored) - assert_eq!(library.pages().len(), 21); + assert_eq!(library.pages().len(), 32); let posts_path = path.join("content").join("posts"); // Make sure the page with a url doesn't have any sections let url_post = library.get_page(&posts_path.join("fixed-url.md")).unwrap(); - assert_eq!(url_post.path, "a-fixed-url/"); + assert_eq!(url_post.path, "/a-fixed-url/"); // Make sure the article in a folder with only asset doesn't get counted as a section let asset_folder_post = @@ -32,12 +32,12 @@ fn can_parse_site() { assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]); // That we have the right number of sections - assert_eq!(library.sections().len(), 11); + assert_eq!(library.sections().len(), 12); // And that the sections are correct let index_section = library.get_section(&path.join("content").join("_index.md")).unwrap(); - assert_eq!(index_section.subsections.len(), 4); - assert_eq!(index_section.pages.len(), 1); + assert_eq!(index_section.subsections.len(), 5); + assert_eq!(index_section.pages.len(), 3); assert!(index_section.ancestors.is_empty()); let posts_section = library.get_section(&posts_path.join("_index.md")).unwrap(); @@ -370,7 +370,7 @@ fn can_build_site_with_pagination_for_section() { assert!(file_contains!( public, "posts/page/1/index.html", - "http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/posts/\"" + "http-equiv=\"refresh\" content=\"0; url=https://replace-this-with-your-url.com/posts/\"" )); assert!(file_contains!(public, "posts/index.html", "Num pagers: 5")); assert!(file_contains!(public, "posts/index.html", "Page size: 2")); @@ -446,6 +446,16 @@ fn can_build_site_with_pagination_for_section() { "sitemap.xml", "https://replace-this-with-your-url.com/posts/page/4/" )); + + // current_path + assert!(file_contains!(public, "posts/index.html", ¤t_path("/posts/"))); + assert!(file_contains!(public, "posts/page/2/index.html", ¤t_path("/posts/page/2/"))); + assert!(file_contains!(public, "posts/python/index.html", ¤t_path("/posts/python/"))); + assert!(file_contains!( + public, + "posts/tutorials/index.html", + ¤t_path("/posts/tutorials/") + )); } #[test] @@ -484,7 +494,7 @@ fn can_build_site_with_pagination_for_index() { assert!(file_contains!( public, "page/1/index.html", - "http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/\"" + "http-equiv=\"refresh\" content=\"0; url=https://replace-this-with-your-url.com/\"" )); assert!(file_contains!(public, "page/1/index.html", "Redirect")); assert!(file_contains!( @@ -492,19 +502,28 @@ fn can_build_site_with_pagination_for_index() { "page/1/index.html", "Click here" )); - assert!(file_contains!(public, "index.html", "Num pages: 1")); + assert!(file_contains!(public, "index.html", "Num pages: 2")); assert!(file_contains!(public, "index.html", "Current index: 1")); assert!(file_contains!(public, "index.html", "First: https://replace-this-with-your-url.com/")); - assert!(file_contains!(public, "index.html", "Last: https://replace-this-with-your-url.com/")); + assert!(file_contains!( + public, + "index.html", + "Last: https://replace-this-with-your-url.com/page/2/" + )); assert_eq!(file_contains!(public, "index.html", "has_prev"), false); - assert_eq!(file_contains!(public, "index.html", "has_next"), false); + assert_eq!(file_contains!(public, "index.html", "has_next"), true); // sitemap contains the pager pages assert!(file_contains!( public, "sitemap.xml", "https://replace-this-with-your-url.com/page/1/" - )) + )); + + // current_path + assert!(file_contains!(public, "index.html", ¤t_path("/"))); + assert!(file_contains!(public, "page/2/index.html", ¤t_path("/page/2/"))); + assert!(file_contains!(public, "paginated/index.html", ¤t_path("/paginated/"))); } #[test] @@ -561,9 +580,9 @@ fn can_build_site_with_pagination_for_taxonomy() { assert!(file_contains!( public, "tags/a/page/1/index.html", - "http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/tags/a/\"" + "http-equiv=\"refresh\" content=\"0; url=https://replace-this-with-your-url.com/tags/a/\"" )); - assert!(file_contains!(public, "tags/a/index.html", "Num pagers: 6")); + assert!(file_contains!(public, "tags/a/index.html", "Num pagers: 8")); assert!(file_contains!(public, "tags/a/index.html", "Page size: 2")); assert!(file_contains!(public, "tags/a/index.html", "Current index: 1")); assert!(!file_contains!(public, "tags/a/index.html", "has_prev")); @@ -576,7 +595,7 @@ fn can_build_site_with_pagination_for_taxonomy() { assert!(file_contains!( public, "tags/a/index.html", - "Last: https://replace-this-with-your-url.com/tags/a/page/6/" + "Last: https://replace-this-with-your-url.com/tags/a/page/8/" )); assert_eq!(file_contains!(public, "tags/a/index.html", "has_prev"), false); @@ -584,12 +603,17 @@ fn can_build_site_with_pagination_for_taxonomy() { assert!(file_contains!( public, "sitemap.xml", - "https://replace-this-with-your-url.com/tags/a/page/6/" - )) + "https://replace-this-with-your-url.com/tags/a/page/8/" + )); + + // current_path + assert!(file_contains!(public, "tags/index.html", ¤t_path("/tags/"))); + assert!(file_contains!(public, "tags/a/index.html", ¤t_path("/tags/a/"))); + assert!(file_contains!(public, "tags/a/page/2/index.html", ¤t_path("/tags/a/page/2/"))); } #[test] -fn can_build_feed() { +fn can_build_feeds() { let (_, _tmp_dir, public) = build_site("test_site"); assert!(&public.exists()); @@ -598,6 +622,14 @@ fn can_build_feed() { assert!(file_contains!(public, "atom.xml", "Extra Syntax")); // Next is posts/simple.md assert!(file_contains!(public, "atom.xml", "Simple article with shortcodes")); + + // Test section feeds + assert!(file_exists!(public, "posts/tutorials/programming/atom.xml")); + // It contains both sections articles + assert!(file_contains!(public, "posts/tutorials/programming/atom.xml", "Python tutorial")); + assert!(file_contains!(public, "posts/tutorials/programming/atom.xml", "Rust")); + // It doesn't contain articles from other sections + assert!(!file_contains!(public, "posts/tutorials/programming/atom.xml", "Extra Syntax")); } #[test] @@ -680,12 +712,47 @@ fn can_build_site_custom_builtins_from_theme() { assert!(file_contains!(public, "404.html", "Oops")); } +#[test] +fn can_build_site_with_html_minified() { + let (_, _tmp_dir, public) = build_site_with_setup("test_site", |mut site| { + site.config.minify_html = true; + (site, true) + }); + + assert!(&public.exists()); + assert!(file_exists!(public, "index.html")); + assert!(file_contains!( + public, + "index.html", + "" + )); +} + #[test] fn can_ignore_markdown_content() { let (_, _tmp_dir, public) = build_site("test_site"); assert!(!file_exists!(public, "posts/ignored/index.html")); } +#[test] +fn can_cachebust_static_files() { + let (_, _tmp_dir, public) = build_site("test_site"); + assert!(file_contains!(public, "index.html", + "")); +} + +#[test] +fn can_get_hash_for_static_files() { + let (_, _tmp_dir, public) = build_site("test_site"); + assert!(file_contains!( + public, + "index.html", + "src=\"https://replace-this-with-your-url.com/scripts/hello.js\"" + )); + assert!(file_contains!(public, "index.html", + "integrity=\"sha384-01422f31eaa721a6c4ac8c6fa09a27dd9259e0dfcf3c7593d7810d912a9de5ca2f582df978537bcd10f76896db61fbb9\"")); +} + #[test] fn check_site() { let (mut site, _tmp_dir, _public) = build_site("test_site"); @@ -699,3 +766,8 @@ fn check_site() { site.config.enable_check_mode(); site.load().expect("link check test_site"); } + +// Follows test_site/themes/sample/templates/current_path.html +fn current_path(path: &str) -> String { + format!("[current_path]({})", path) +} diff --git a/components/templates/Cargo.toml b/components/templates/Cargo.toml index 24189765..a5e25f2e 100644 --- a/components/templates/Cargo.toml +++ b/components/templates/Cargo.toml @@ -8,12 +8,12 @@ edition = "2018" tera = "1" base64 = "0.12" lazy_static = "1" -pulldown-cmark = "0.7" +pulldown-cmark = { version = "0.8", default-features = false } toml = "0.5" csv = "1" image = "0.23" serde_json = "1.0" -sha2 = "0.8" +sha2 = "0.9" url = "2" errors = { path = "../errors" } @@ -21,6 +21,7 @@ utils = { path = "../utils" } library = { path = "../library" } config = { path = "../config" } imageproc = { path = "../imageproc" } +svg_metadata = "0.4.1" [dependencies.reqwest] version = "0.10" @@ -28,4 +29,4 @@ default-features = false features = ["blocking", "rustls-tls"] [dev-dependencies] -mockito = "0.25" +mockito = "0.27" diff --git a/components/templates/src/builtins/atom.xml b/components/templates/src/builtins/atom.xml index b2c89443..3ebacb9b 100644 --- a/components/templates/src/builtins/atom.xml +++ b/components/templates/src/builtins/atom.xml @@ -2,13 +2,20 @@ {{ config.title }} {%- if term %} - {{ term.name }} + {%- elif section.title %} - {{ section.title }} {%- endif -%} {%- if config.description %} {{ config.description }} {%- endif %} - + Zola {{ last_updated | date(format="%+") }} {{ feed_url | safe }} diff --git a/components/templates/src/builtins/internal/alias.html b/components/templates/src/builtins/internal/alias.html index 54f782e2..b88bae24 100644 --- a/components/templates/src/builtins/internal/alias.html +++ b/components/templates/src/builtins/internal/alias.html @@ -1,6 +1,6 @@ - + Redirect

Click here to be redirected.

diff --git a/components/templates/src/builtins/rss.xml b/components/templates/src/builtins/rss.xml index a76f6475..4eeffdde 100644 --- a/components/templates/src/builtins/rss.xml +++ b/components/templates/src/builtins/rss.xml @@ -1,8 +1,17 @@ - {{ config.title }} - {{ config.base_url | escape_xml | safe }} + {{ config.title }} + {%- if term %} - {{ term.name }} + {%- elif section.title %} - {{ section.title }} + {%- endif -%} + + {%- if section -%} + {{ section.permalink | escape_xml | safe }} + {%- else -%} + {{ config.base_url | escape_xml | safe }} + {%- endif -%} + {{ config.description }} Zola {{ config.default_language }} diff --git a/components/templates/src/filters.rs b/components/templates/src/filters.rs index 555bbfed..5f4e521a 100644 --- a/components/templates/src/filters.rs +++ b/components/templates/src/filters.rs @@ -19,6 +19,7 @@ pub fn markdown( opts.insert(cmark::Options::ENABLE_TABLES); opts.insert(cmark::Options::ENABLE_FOOTNOTES); opts.insert(cmark::Options::ENABLE_STRIKETHROUGH); + opts.insert(cmark::Options::ENABLE_TASKLISTS); let mut html = String::new(); let parser = cmark::Parser::new_ext(&s, opts); diff --git a/components/templates/src/global_fns/load_data.rs b/components/templates/src/global_fns/load_data.rs index 7432a6c0..811f1321 100644 --- a/components/templates/src/global_fns/load_data.rs +++ b/components/templates/src/global_fns/load_data.rs @@ -209,11 +209,9 @@ impl TeraFn for LoadData { .header(header::ACCEPT, file_format.as_accept_header()) .send() .and_then(|res| res.error_for_status()) - .map_err(|e| { - match e.status() { - Some(status) => format!("Failed to request {}: {}", url, status), - None => format!("Could not get response status for url: {}", url), - } + .map_err(|e| match e.status() { + Some(status) => format!("Failed to request {}: {}", url, status), + None => format!("Could not get response status for url: {}", url), })?; response .text() diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index 4535d8b7..0469612a 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -1,18 +1,18 @@ use std::collections::HashMap; +use std::ffi::OsStr; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; use std::{fs, io, result}; -use sha2::{Digest, Sha256}; +use sha2::{Digest, Sha256, Sha384, Sha512}; +use svg_metadata as svg; use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value}; use config::Config; -use image; use image::GenericImageView; use library::{Library, Taxonomy}; use utils::site::resolve_internal_link; - -use imageproc; +use utils::slugs::{slugify_paths, SlugifyStrategy}; #[macro_use] mod macros; @@ -49,16 +49,20 @@ impl TeraFn for Trans { pub struct GetUrl { config: Config, permalinks: HashMap, - content_path: PathBuf, + search_paths: Vec, } impl GetUrl { - pub fn new(config: Config, permalinks: HashMap, content_path: PathBuf) -> Self { - Self { config, permalinks, content_path } + pub fn new( + config: Config, + permalinks: HashMap, + search_paths: Vec, + ) -> Self { + Self { config, permalinks, search_paths } } } fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result { - if lang == &config.default_language { + if lang == config.default_language { return Ok(path); } @@ -68,17 +72,48 @@ fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result = path.split(".").map(String::from).collect(); + let mut splitted_path: Vec = path.split('.').map(String::from).collect(); let ilast = splitted_path.len() - 1; splitted_path[ilast] = format!("{}.{}", lang, splitted_path[ilast]); Ok(splitted_path.join(".")) } -fn compute_file_sha256(path: &PathBuf) -> result::Result { - let mut file = fs::File::open(path)?; +fn open_file(search_paths: &[PathBuf], url: &str) -> result::Result { + let cleaned_url = url.trim_start_matches("@/").trim_start_matches('/'); + for base_path in search_paths { + match fs::File::open(base_path.join(cleaned_url)) { + Ok(f) => return Ok(f), + Err(_) => continue, + }; + } + Err(io::Error::from(io::ErrorKind::NotFound)) +} + +fn compute_file_sha256(mut file: fs::File) -> result::Result { let mut hasher = Sha256::new(); io::copy(&mut file, &mut hasher)?; - Ok(format!("{:x}", hasher.result())) + Ok(format!("{:x}", hasher.finalize())) +} + +fn compute_file_sha384(mut file: fs::File) -> result::Result { + let mut hasher = Sha384::new(); + io::copy(&mut file, &mut hasher)?; + Ok(format!("{:x}", hasher.finalize())) +} + +fn compute_file_sha512(mut file: fs::File) -> result::Result { + let mut hasher = Sha512::new(); + io::copy(&mut file, &mut hasher)?; + Ok(format!("{:x}", hasher.finalize())) +} + +fn file_not_found_err(search_paths: &[PathBuf], url: &str) -> Result { + Err(format!( + "file `{}` not found; searched in{}", + url, + search_paths.iter().fold(String::new(), |acc, arg| acc + " " + arg.to_str().unwrap()) + ) + .into()) } impl TeraFn for GetUrl { @@ -120,10 +155,11 @@ impl TeraFn for GetUrl { } if cachebust { - let full_path = self.content_path.join(&path); - permalink = match compute_file_sha256(&full_path) { - Ok(digest) => format!("{}?h={}", permalink, digest), - Err(_) => return Err(format!("Could not read file `{}`. Expected location: {}", path, full_path.to_str().unwrap()).into()), + match open_file(&self.search_paths, &path).and_then(compute_file_sha256) { + Ok(hash) => { + permalink = format!("{}?h={}", permalink, hash); + } + Err(_) => return file_not_found_err(&self.search_paths, &path), }; } Ok(to_value(permalink).unwrap()) @@ -131,6 +167,48 @@ impl TeraFn for GetUrl { } } +#[derive(Debug)] +pub struct GetFileHash { + search_paths: Vec, +} +impl GetFileHash { + pub fn new(search_paths: Vec) -> Self { + Self { search_paths } + } +} + +const DEFAULT_SHA_TYPE: u16 = 384; + +impl TeraFn for GetFileHash { + fn call(&self, args: &HashMap) -> Result { + let path = required_arg!( + String, + args.get("path"), + "`get_file_hash` requires a `path` argument with a string value" + ); + let sha_type = optional_arg!( + u16, + args.get("sha_type"), + "`get_file_hash`: `sha_type` must be 256, 384 or 512" + ) + .unwrap_or(DEFAULT_SHA_TYPE); + + let compute_hash_fn = match sha_type { + 256 => compute_file_sha256, + 384 => compute_file_sha384, + 512 => compute_file_sha512, + _ => return Err("`get_file_hash`: `sha_type` must be 256, 384 or 512".into()), + }; + + let hash = open_file(&self.search_paths, &path).and_then(compute_hash_fn); + + match hash { + Ok(digest) => Ok(to_value(digest).unwrap()), + Err(_) => file_not_found_err(&self.search_paths, &path), + } + } +} + #[derive(Debug)] pub struct ResizeImage { imageproc: Arc>, @@ -211,31 +289,49 @@ impl TeraFn for GetImageMeta { if !src_path.exists() { return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into()); } - let img = image::open(&src_path) - .map_err(|e| Error::chain(format!("Failed to process image: {}", path), e))?; + let (height, width) = image_dimensions(&src_path)?; let mut map = tera::Map::new(); - map.insert(String::from("height"), Value::Number(tera::Number::from(img.height()))); - map.insert(String::from("width"), Value::Number(tera::Number::from(img.width()))); + map.insert(String::from("height"), Value::Number(tera::Number::from(height))); + map.insert(String::from("width"), Value::Number(tera::Number::from(width))); Ok(Value::Object(map)) } } +// Try to read the image dimensions for a given image +fn image_dimensions(path: &PathBuf) -> Result<(u32, u32)> { + if let Some("svg") = path.extension().and_then(OsStr::to_str) { + let img = svg::Metadata::parse_file(&path) + .map_err(|e| Error::chain(format!("Failed to process SVG: {}", path.display()), e))?; + match (img.height(), img.width(), img.view_box()) { + (Some(h), Some(w), _) => Ok((h as u32, w as u32)), + (_, _, Some(view_box)) => Ok((view_box.height as u32, view_box.width as u32)), + _ => Err("Invalid dimensions: SVG width/height and viewbox not set.".into()), + } + } else { + let img = image::open(&path) + .map_err(|e| Error::chain(format!("Failed to process image: {}", path.display()), e))?; + Ok((img.height(), img.width())) + } +} + #[derive(Debug)] pub struct GetTaxonomyUrl { taxonomies: HashMap>, default_lang: String, + slugify: SlugifyStrategy, } + impl GetTaxonomyUrl { - pub fn new(default_lang: &str, all_taxonomies: &[Taxonomy]) -> Self { + pub fn new(default_lang: &str, all_taxonomies: &[Taxonomy], slugify: SlugifyStrategy) -> Self { let mut taxonomies = HashMap::new(); for taxo in all_taxonomies { let mut items = HashMap::new(); for item in &taxo.items { - items.insert(item.name.clone(), item.permalink.clone()); + items.insert(slugify_paths(&item.name.clone(), slugify), item.permalink.clone()); } taxonomies.insert(format!("{}-{}", taxo.kind.name, taxo.kind.lang), items); } - Self { taxonomies, default_lang: default_lang.to_string() } + Self { taxonomies, default_lang: default_lang.to_string(), slugify: slugify } } } impl TeraFn for GetTaxonomyUrl { @@ -265,7 +361,7 @@ impl TeraFn for GetTaxonomyUrl { } }; - if let Some(permalink) = container.get(&name) { + if let Some(permalink) = container.get(&slugify_paths(&name, self.slugify)) { return Ok(to_value(permalink).unwrap()); } @@ -379,7 +475,7 @@ impl TeraFn for GetTaxonomy { #[cfg(test)] mod tests { - use super::{GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans}; + use super::{GetFileHash, GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans}; use std::collections::HashMap; use std::env::temp_dir; @@ -397,20 +493,20 @@ mod tests { use utils::slugs::SlugifyStrategy; struct TestContext { - content_path: PathBuf, + static_path: PathBuf, } impl TestContext { fn setup() -> Self { - let dir = temp_dir().join("test_global_fns"); + let dir = temp_dir().join("static"); create_directory(&dir).expect("Could not create test directory"); create_file(&dir.join("app.css"), "// Hello world!") .expect("Could not create test content (app.css)"); - Self { content_path: dir } + Self { static_path: dir } } } impl Drop for TestContext { fn drop(&mut self) { - remove_dir_all(&self.content_path).expect("Could not free test directory"); + remove_dir_all(&self.static_path).expect("Could not free test directory"); } } @@ -421,7 +517,7 @@ mod tests { #[test] fn can_add_cachebust_to_url() { let config = Config::default(); - let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone()); + let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("cachebust".to_string(), to_value(true).unwrap()); @@ -431,7 +527,7 @@ mod tests { #[test] fn can_add_trailing_slashes() { let config = Config::default(); - let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone()); + let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("trailing_slash".to_string(), to_value(true).unwrap()); @@ -441,7 +537,7 @@ mod tests { #[test] fn can_add_slashes_and_cachebust() { let config = Config::default(); - let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone()); + let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("trailing_slash".to_string(), to_value(true).unwrap()); @@ -452,7 +548,7 @@ mod tests { #[test] fn can_link_to_some_static_file() { let config = Config::default(); - let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone()); + let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css"); @@ -557,7 +653,9 @@ mod tests { let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] }; let taxonomies = vec![tags.clone(), tags_fr.clone()]; - let static_fn = GetTaxonomyUrl::new(&config.default_language, &taxonomies); + let static_fn = + GetTaxonomyUrl::new(&config.default_language, &taxonomies, config.slugify.taxonomies); + // can find it correctly let mut args = HashMap::new(); args.insert("kind".to_string(), to_value("tags").unwrap()); @@ -566,6 +664,16 @@ mod tests { static_fn.call(&args).unwrap(), to_value("http://a-website.com/tags/programming/").unwrap() ); + + // can find it correctly with inconsistent capitalisation + let mut args = HashMap::new(); + args.insert("kind".to_string(), to_value("tags").unwrap()); + args.insert("name".to_string(), to_value("programming").unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + to_value("http://a-website.com/tags/programming/").unwrap() + ); + // works with other languages let mut args = HashMap::new(); args.insert("kind".to_string(), to_value("tags").unwrap()); @@ -639,7 +747,7 @@ title = "A title" #[test] fn error_when_language_not_available() { let config = Config::parse(TRANS_CONFIG).unwrap(); - let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone()); + let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); args.insert("lang".to_string(), to_value("it").unwrap()); @@ -662,7 +770,7 @@ title = "A title" "a_section/a_page.en.md".to_string(), "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(), ); - let static_fn = GetUrl::new(config, permalinks, TEST_CONTEXT.content_path.clone()); + let static_fn = GetUrl::new(config, permalinks, vec![TEST_CONTEXT.static_path.clone()]); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); args.insert("lang".to_string(), to_value("fr").unwrap()); @@ -684,7 +792,7 @@ title = "A title" "a_section/a_page.en.md".to_string(), "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(), ); - let static_fn = GetUrl::new(config, permalinks, TEST_CONTEXT.content_path.clone()); + let static_fn = GetUrl::new(config, permalinks, vec![TEST_CONTEXT.static_path.clone()]); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); args.insert("lang".to_string(), to_value("en").unwrap()); @@ -693,4 +801,47 @@ title = "A title" "https://remplace-par-ton-url.fr/en/a_section/a_page/" ); } + + #[test] + fn can_get_file_hash_sha256() { + let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("sha_type".to_string(), to_value(256).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840" + ); + } + + #[test] + fn can_get_file_hash_sha384() { + let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + assert_eq!(static_fn.call(&args).unwrap(), "141c09bd28899773b772bbe064d8b718fa1d6f2852b7eafd5ed6689d26b74883b79e2e814cd69d5b52ab476aa284c414"); + } + + #[test] + fn can_get_file_hash_sha512() { + let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("sha_type".to_string(), to_value(512).unwrap()); + assert_eq!(static_fn.call(&args).unwrap(), "379dfab35123b9159d9e4e92dc90e2be44cf3c2f7f09b2e2df80a1b219b461de3556c93e1a9ceb3008e999e2d6a54b4f1d65ee9be9be63fa45ec88931623372f"); + } + + #[test] + fn error_when_file_not_found_for_hash() { + let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("doesnt-exist").unwrap()); + assert_eq!( + format!( + "file `doesnt-exist` not found; searched in {}", + TEST_CONTEXT.static_path.to_str().unwrap() + ), + format!("{}", static_fn.call(&args).unwrap_err()) + ); + } } diff --git a/components/utils/Cargo.toml b/components/utils/Cargo.toml index 4c79f6ff..9923f80f 100644 --- a/components/utils/Cargo.toml +++ b/components/utils/Cargo.toml @@ -3,6 +3,7 @@ name = "utils" version = "0.1.0" authors = ["Vincent Prouillet "] edition = "2018" +include = ["src/**/*"] [dependencies] tera = "1" @@ -13,7 +14,7 @@ serde = "1" serde_derive = "1" slug = "0.1" percent-encoding = "2" -filetime = "0.2.8" +filetime = "0.2.12" errors = { path = "../errors" } diff --git a/components/utils/src/de.rs b/components/utils/src/de.rs index e6ff63db..654eaee1 100644 --- a/components/utils/src/de.rs +++ b/components/utils/src/de.rs @@ -1,6 +1,5 @@ use serde::{Deserialize, Deserializer}; use tera::{Map, Value}; -use toml; /// Used as an attribute when we want to convert from TOML to a string date pub fn from_toml_datetime<'de, D>(deserializer: D) -> Result, D::Error> @@ -43,6 +42,16 @@ pub fn fix_toml_dates(table: Map) -> Value { Value::Object(o) => { new.insert(key, convert_toml_date(o)); } + Value::Array(arr) => { + let mut new_arr = Vec::with_capacity(arr.len()); + for v in arr { + match v { + Value::Object(o) => new_arr.push(fix_toml_dates(o)), + _ => new_arr.push(v), + }; + } + new.insert(key, Value::Array(new_arr)); + } _ => { new.insert(key, value); } 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/components/utils/src/slugs.rs b/components/utils/src/slugs.rs index 476eec54..f4ae57e7 100644 --- a/components/utils/src/slugs.rs +++ b/components/utils/src/slugs.rs @@ -11,6 +11,12 @@ pub enum SlugifyStrategy { Off, } +impl Default for SlugifyStrategy { + fn default() -> Self { + SlugifyStrategy::On + } +} + fn strip_chars(s: &str, chars: &str) -> String { let mut sanitized_string = s.to_string(); sanitized_string.retain(|c| !chars.contains(c)); diff --git a/components/utils/src/templates.rs b/components/utils/src/templates.rs index 5c25d255..aa2ed463 100644 --- a/components/utils/src/templates.rs +++ b/components/utils/src/templates.rs @@ -61,52 +61,21 @@ pub fn render_template( } } -/// Rewrites the path from extend/macros of the theme used to ensure -/// that they will point to the right place (theme/templates/...) -/// Include is NOT supported as it would be a pain to add and using blocks -/// or macros is always better anyway for themes -/// This will also rename the shortcodes to NOT have the themes in the path -/// so themes shortcodes can be used. -pub fn rewrite_theme_paths(tera_theme: &mut Tera, site_templates: Vec<&str>, theme: &str) { - let mut shortcodes_to_move = vec![]; - let mut templates = HashMap::new(); - let old_templates = ::std::mem::replace(&mut tera_theme.templates, HashMap::new()); - - // We want to match the paths in the templates to the new names - for (key, mut tpl) in old_templates { - tpl.name = format!("{}/templates/{}", theme, tpl.name); - // First the parent if there is one - // If a template with the same name is also in site, assumes it overrides the theme one - // and do not change anything - if let Some(ref p) = tpl.parent.clone() { - if !site_templates.contains(&p.as_ref()) { - tpl.parent = Some(format!("{}/templates/{}", theme, p)); - } - } - - // Next the macros import - let mut updated = vec![]; - for &(ref filename, ref namespace) in &tpl.imported_macro_files { - updated.push((format!("{}/templates/{}", theme, filename), namespace.to_string())); - } - tpl.imported_macro_files = updated; - - if tpl.name.starts_with(&format!("{}/templates/shortcodes", theme)) { - let new_name = tpl.name.replace(&format!("{}/templates/", theme), ""); - shortcodes_to_move.push((key, new_name.clone())); - tpl.name = new_name; - } - - templates.insert(tpl.name.clone(), tpl); - } - - tera_theme.templates = templates; - - // and then replace shortcodes in the Tera instance using the new names - for (old_name, new_name) in shortcodes_to_move { - let tpl = tera_theme.templates.remove(&old_name).unwrap(); - tera_theme.templates.insert(new_name, tpl); +/// Rewrites the path of duplicate templates to include the complete theme path +/// Theme templates will be injected into site templates, with higher priority for site +/// templates. To keep a copy of the template in case it's being extended from a site template +/// of the same name, we reinsert it with the theme path prepended +pub fn rewrite_theme_paths(tera_theme: &mut Tera, theme: &str) { + let theme_basepath = format!("{}/templates/", theme); + let mut new_templates = HashMap::new(); + for (key, template) in &tera_theme.templates { + let mut tpl = template.clone(); + tpl.name = format!("{}{}", theme_basepath, key); + new_templates.insert(tpl.name.clone(), tpl); } + // Contrary to tera.extend, hashmap.extend does replace existing keys + // We can safely extend because there's no conflicting paths anymore + tera_theme.templates.extend(new_templates); } #[cfg(test)] @@ -117,7 +86,7 @@ mod tests { #[test] fn can_rewrite_all_paths_of_theme() { let mut tera = Tera::parse("test-templates/*.html").unwrap(); - rewrite_theme_paths(&mut tera, vec!["base.html"], "hyde"); + rewrite_theme_paths(&mut tera, "hyde"); // special case to make the test work: we also rename the files to // match the imports for (key, val) in &tera.templates.clone() { @@ -133,7 +102,7 @@ mod tests { ); assert_eq!( tera.templates["hyde/templates/child.html"].parent, - Some("hyde/templates/index.html".to_string()) + Some("index.html".to_string()) ); } } diff --git a/docs/content/documentation/content/multilingual.md b/docs/content/documentation/content/multilingual.md index b9541fa7..443e7e49 100644 --- a/docs/content/documentation/content/multilingual.md +++ b/docs/content/documentation/content/multilingual.md @@ -20,6 +20,12 @@ languages = [ If you want to use per-language taxonomies, ensure you set the `lang` field in their configuration. +Note: By default, Chinese and Japanese search indexing is not included. You can include +the support by building `zola` using `cargo build --features search/indexing-ja search/indexing-zh`. +Please also note that, enabling Chinese indexing will increase the binary size by approximately +5 MB while enabling Japanese indexing will increase the binary size by approximately 70 MB +due to the incredibly large dictionaries. + ## Content Once the languages have been added, you can start to translate your content. Zola uses the filename to detect the language: diff --git a/docs/content/documentation/content/search.md b/docs/content/documentation/content/search.md index e46711b2..df29f773 100644 --- a/docs/content/documentation/content/search.md +++ b/docs/content/documentation/content/search.md @@ -20,3 +20,7 @@ After `zola build` or `zola serve`, you should see two files in your static dire As each site will be different, Zola makes no assumptions about your search function and doesn't provide the JavaScript/CSS code to do an actual search and display results. You can look at how this site implements it to get an idea: [search.js](https://github.com/getzola/zola/tree/master/docs/static/search.js). + +## Configuring the search index +In some cases, the default indexing strategy is not suitable. You can customise which fields to include and whether +to truncate the content in the [search configuration](@/documentation/getting-started/configuration.md). diff --git a/docs/content/documentation/content/section.md b/docs/content/documentation/content/section.md index 93df4663..80003aea 100644 --- a/docs/content/documentation/content/section.md +++ b/docs/content/documentation/content/section.md @@ -93,6 +93,12 @@ transparent = false # current one. This takes an array of paths, not URLs. aliases = [] +# If set to "true", a feed file will be generated for this section at the +# section's root path. This is independent of the site-wide variable of the same +# name. The section feed will only include posts from that respective feed, and +# not from any other sections, including sub-sections under that section. +generate_feed = false + # Your own data. [extra] ``` @@ -150,6 +156,7 @@ This will be sort all pages by their `weight` field, from lightest weight page gets `page.lighter` and `page.heavier` variables that contain the pages with lighter and heavier weights, respectively. +### Reversed sorting When iterating through pages, you may wish to use the Tera `reverse` filter, which reverses the order of the pages. For example, after using the `reverse` filter, pages sorted by weight will be sorted from lightest (at the top) to heaviest @@ -158,8 +165,10 @@ to newest (at the bottom). `reverse` has no effect on `page.later`/`page.earlier` or `page.heavier`/`page.lighter`. +If the section is paginated the `paginate_reversed=true` in the front matter of the relevant section should be set instead of using the filter. + ## Sorting subsections -Sorting sections is a bit less flexible: sections are always sorted by `weight`, +Sorting sections is a bit less flexible: sections can only be sorted by `weight`, and do not have variables that point to the heavier/lighter sections. By default, the lightest (lowest `weight`) subsections will be at diff --git a/docs/content/documentation/content/shortcodes.md b/docs/content/documentation/content/shortcodes.md index 22bf6aba..b0fba7f9 100644 --- a/docs/content/documentation/content/shortcodes.md +++ b/docs/content/documentation/content/shortcodes.md @@ -3,14 +3,20 @@ title = "Shortcodes" weight = 40 +++ -Although Markdown is good for writing, it isn't great when you need write inline -HTML to add some styling for example. - -To solve this, Zola borrows the concept of [shortcodes](https://codex.wordpress.org/Shortcode_API) -from WordPress. +Zola borrows the concept of [shortcodes](https://codex.wordpress.org/Shortcode_API) from WordPress. In our case, a shortcode corresponds to a template defined in the `templates/shortcodes` directory or -a built-in one that can be used in a Markdown file. If you want to use something similar to shortcodes in your templates, try [Tera macros](https://tera.netlify.com/docs#macros). +a built-in one that can be used in a Markdown file. If you want to use something similar to shortcodes in your templates, +try [Tera macros](https://tera.netlify.com/docs#macros). +Broadly speaking, Zola's shortcodes cover two distinct use cases: + +* Inject more complex HTML: Markdown is good for writing, but it isn't great when you need add inline HTML or styling. +* Ease repetitive data based tasks: when you have [external data](@/documentation/templates/overview.md#load-data) that you + want to display in your page's body. + +The latter may also be solved by writing HTML, however Zola allows the use of Markdown based shortcodes which end in `.md` +rather than `.html`. This may be particularly useful if you want to include headings generated by the shortcode in the +[table of contents](@/documentation/content/table-of-contents.md). ## Writing a shortcode Let's write a shortcode to embed YouTube videos as an example. @@ -34,12 +40,27 @@ are in an `if` statement, they are optional. That's it. Zola will now recognise this template as a shortcode named `youtube` (the filename minus the `.html` extension). -The Markdown renderer will wrap an inline HTML node such as `` or `` into a paragraph. +The Markdown renderer will wrap an inline HTML node such as `` or `` into a paragraph. If you want to disable this behaviour, wrap your shortcode in a `
`. -Shortcodes are rendered before the Markdown is parsed so they don't have access to the table of contents. Because of that, -you also cannot use the `get_page`/`get_section`/`get_taxonomy` global functions. It might work while running -`zola serve` because it has been loaded but it will fail during `zola build`. +A Markdown based shortcode in turn will be treated as if what it returned was part of the page's body. If we create +`books.md` in `templates/shortcodes` for example: + +```jinja2 +{% set data = load_data(path=path) -%} +{% for book in data.books %} +### {{ book.title }} + +{{ book.description | safe }} +{% endfor %} +``` + +This will create a shortcode `books` with the argument `path` pointing to a `.toml` file where it loads lists of books with +titles and descriptions. They will flow with the rest of the document in which `books` is called. + +Shortcodes are rendered before the page's Markdown is parsed so they don't have access to the page's table of contents. +Because of that, you also cannot use the `get_page`/`get_section`/`get_taxonomy` global functions. It might work while +running `zola serve` because it has been loaded but it will fail during `zola build`. ## Using shortcodes diff --git a/docs/content/documentation/content/syntax-highlighting.md b/docs/content/documentation/content/syntax-highlighting.md index 499f283e..4143567e 100644 --- a/docs/content/documentation/content/syntax-highlighting.md +++ b/docs/content/documentation/content/syntax-highlighting.md @@ -54,6 +54,7 @@ Here is a full list of supported languages and their short names: - Fortran (Modern) -> ["F03", "F08", "F90", "F95", "f03", "f08", "f90", "f95"] - Fortran Namelist -> ["namelist"] - Friendly Interactive Shell (fish) -> ["fish"] +- GDScript (Godot Engine) -> ["gd"] - Generic Config -> [".dircolors", ".gitattributes", ".gitignore", ".gitmodules", ".inputrc", "Doxyfile", "cfg", "conf", "config", "dircolors", "gitattributes", "gitignore", "gitmodules", "ini", "inputrc", "mak", "mk", "pro"] - Git Attributes -> [".gitattributes", "attributes", "gitattributes"] - Git Commit -> ["COMMIT_EDITMSG", "MERGE_MSG", "TAG_EDITMSG"] @@ -63,6 +64,7 @@ Here is a full list of supported languages and their short names: - Git Log -> ["gitlog"] - Git Mailmap -> [".mailmap", "mailmap"] - Git Rebase Todo -> ["git-rebase-todo"] +- GLSL -> ["comp", "frag", "fs", "fsh", "fshader", "geom", "glsl", "gs", "gsh", "gshader", "tesc", "tese", "vert", "vs", "vsh", "vshader"] - Go -> ["go"] - GraphQL -> ["gql", "graphql"] - Graphviz (DOT) -> ["DOT", "dot", "gv"] diff --git a/docs/content/documentation/content/table-of-contents.md b/docs/content/documentation/content/table-of-contents.md index d1924011..b42a4dfd 100644 --- a/docs/content/documentation/content/table-of-contents.md +++ b/docs/content/documentation/content/table-of-contents.md @@ -3,7 +3,7 @@ title = "Table of Contents" weight = 60 +++ -Each page/section will automatically generate a table of contents for itself based on the headers present. +Each page/section will automatically generate a table of contents for itself based on the headers generated with markdown. It is available in the template through the `page.toc` or `section.toc` variable. You can view the [template variables](@/documentation/templates/pages-sections.md#table-of-contents) diff --git a/docs/content/documentation/content/taxonomies.md b/docs/content/documentation/content/taxonomies.md index d6433575..8cc2a84e 100644 --- a/docs/content/documentation/content/taxonomies.md +++ b/docs/content/documentation/content/taxonomies.md @@ -64,4 +64,4 @@ The taxonomy pages are then available at the following paths: $BASE_URL/$NAME/ (taxonomy) $BASE_URL/$NAME/$SLUG (taxonomy entry) ``` - +Note that taxonomies are case insensitive so terms that have the same slug will get merged, e.g. sections and pages containing the tag "example" will be shown in the same taxonomy page as ones containing "Example" diff --git a/docs/content/documentation/deployment/vercel.md b/docs/content/documentation/deployment/vercel.md index b2bb2416..66f5d432 100644 --- a/docs/content/documentation/deployment/vercel.md +++ b/docs/content/documentation/deployment/vercel.md @@ -27,7 +27,7 @@ specifying the `ZOLA_VERSION` we want to use to deploy the site. { "build": { "env": { - "ZOLA_VERSION": "0.11.0" + "ZOLA_VERSION": "0.12.0" } } } 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 95485a53..1cc25ddc 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`. @@ -20,6 +20,8 @@ $ zola init If the `my_site` directory already exists, Zola will only populate it if it contains only hidden files (dotfiles are ignored). If no `my_site` argument is passed, Zola will try to populate the current directory. +In case you want to attempt to populate a non-empty directory and are brave, you can use `zola init --force`. Note that this will _not_ overwrite existing folders or files; in those cases you will get a `File exists (os error 17)` error or similar. + You can initialize a git repository and a Zola site directly from within a new folder: ```bash diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index be8220b8..c76696af 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -15,8 +15,9 @@ Here are the current `config.toml` sections: 1. main (unnamed) 2. link_checker 3. slugify -4. translations -5. extra +4. search +5. translations +6. extra **Only the `base_url` variable is mandatory**. Everything else is optional. All configuration variables used by Zola as well as their default values are listed below: @@ -84,10 +85,6 @@ languages = [] # When set to "true", the Sass files in the `sass` directory are compiled. compile_sass = false -# When set to "true", a search index is built from the pages and section -# content for `default_language`. -build_search_index = false - # A list of glob patterns specifying asset files to ignore when the content # directory is processed. Defaults to none, which means that all asset files are # copied over to the `public` directory. @@ -117,6 +114,22 @@ paths = "on" taxonomies = "on" anchors = "on" +# When set to "true", a search index is built from the pages and section +# content for `default_language`. +build_search_index = false + +[search] +# Whether to include the title of the page/section in the index +include_title = true +# Whether to include the description of the page/section in the index +include_description = false +# Whether to include the rendered content of the page/section in the index +include_content = true +# At which character to truncate the content to. Useful if you have a lot of pages and the index would +# become too big to load on the site. Defaults to not being set. +# truncate_content_length = 100 + +# Optional translation object. Keys should be language codes. # Optional translation object. The key if present should be a language code. # Example: # default_language = "fr" @@ -175,6 +188,7 @@ Zola currently has the following highlight themes available: - [nord](https://github.com/crabique/Nord-plist/tree/0d655b23d6b300e691676d9b90a68d92b267f7ec) - [nyx-bold](https://github.com/GalAster/vscode-theme-nyx) - [one-dark](https://github.com/andresmichel/one-dark-theme) +- [OneHalf](https://github.com/sonph/onehalf) - [solarized-dark](https://tmtheme-editor.herokuapp.com/#!/editor/theme/Solarized%20(dark)) - [solarized-light](https://tmtheme-editor.herokuapp.com/#!/editor/theme/Solarized%20(light)) - [subway-madrid](https://github.com/idleberg/Subway.tmTheme) diff --git a/docs/content/documentation/getting-started/installation.md b/docs/content/documentation/getting-started/installation.md index fb7a2186..8e785ce4 100644 --- a/docs/content/documentation/getting-started/installation.md +++ b/docs/content/documentation/getting-started/installation.md @@ -70,7 +70,7 @@ $ choco install zola Zola does not work in PowerShell ISE. ## From source -To build Zola from source, you will need to have Git, [Rust (at least 1.36) and Cargo](https://www.rust-lang.org/) +To build Zola from source, you will need to have Git, [Rust (at least 1.43) and Cargo](https://www.rust-lang.org/) installed. You will also need to meet additional dependencies to compile [libsass](https://github.com/sass/libsass): - OSX, Linux and other Unix-like operating systems: `make` (`gmake` on BSDs), `g++`, `libssl-dev` diff --git a/docs/content/documentation/templates/feeds/index.md b/docs/content/documentation/templates/feeds/index.md index 77630f6b..fa95f20b 100644 --- a/docs/content/documentation/templates/feeds/index.md +++ b/docs/content/documentation/templates/feeds/index.md @@ -33,6 +33,13 @@ Feeds for taxonomy terms get two more variables, using types from the - `taxonomy`: of type `TaxonomyConfig` - `term`: of type `TaxonomyTerm`, but without `term.pages` (use `pages` instead) +You can also enable separate feeds for each section by setting the +`generate_feed` variable to true in the respective section's front matter. +Section feeds will use the same template as indicated in the `config.toml` file. +Section feeds, in addition to the five feed template variables, get the +`section` variable from the [section +template](@/documentation/templates/pages-sections.md). + Enable feed autodiscovery allows feed readers and browsers to notify user about a RSS or Atom feed available on your web site. So it is easier for user to subscribe. As an example this is how it looks like using [Firefox](https://en.wikipedia.org/wiki/Mozilla_Firefox) [Livemarks](https://addons.mozilla.org/en-US/firefox/addon/livemarks/?src=search) addon. diff --git a/docs/content/documentation/templates/overview.md b/docs/content/documentation/templates/overview.md index 79245161..2150225e 100644 --- a/docs/content/documentation/templates/overview.md +++ b/docs/content/documentation/templates/overview.md @@ -16,7 +16,7 @@ you can place `{{ __tera_context }}` in the template to print the whole context. A few variables are available on all templates except feeds and the sitemap: - `config`: the [configuration](@/documentation/getting-started/configuration.md) without any modifications -- `current_path`: the path (full URL without `base_url`) of the current page, never starting with a `/` +- `current_path`: the path (full URL without `base_url`) of the current page, always starting with a `/` - `current_url`: the full URL for the current page - `lang`: the language for the current page @@ -146,8 +146,27 @@ In the case of non-internal links, you can also add a cachebust of the format `? by passing `cachebust=true` to the `get_url` function. +### 'get_file_hash` + +Gets the hash digest for a static file. Supported hashes are SHA-256, SHA-384 (default) and SHA-512. Requires `path`. The `sha_type` key is optional and must be one of 256, 384 or 512. + +```jinja2 +{{/* get_file_hash(path="js/app.js", sha_type=256) */}} +``` + +This can be used to implement subresource integrity. Do note that subresource integrity is typically used when using external scripts, which `get_file_hash` does not support. + +```jinja2 + +``` + +Whenever hashing files, whether using `get_file_hash` or `get_url(..., cachebust=true)`, the file is searched for in three places: `static/`, `content/` and the output path (so e.g. compiled SASS can be hashed, too.) + + ### `get_image_metadata` -Gets metadata for an image. Currently, the only supported keys are `width` and `height`. +Gets metadata for an image. This supports common formats like JPEG, PNG, as well as SVG. +Currently, the only supported keys are `width` and `height`. ```jinja2 {% set meta = get_image_metadata(path="...") %} @@ -173,6 +192,15 @@ Gets the whole taxonomy of a specific kind. {% set categories = get_taxonomy(kind="categories") %} ``` +The type of the output is: + +```ts +kind: TaxonomyConfig; +items: Array; +``` + +See the [Taxonomies documentation](@/documentation/templates/taxonomies.md) for a full documentation of those types. + ### `load_data` Loads data from a file or URL. Supported file types include *toml*, *json* and *csv*. Any other file type will be loaded as plain text. diff --git a/docs/content/documentation/templates/pages-sections.md b/docs/content/documentation/templates/pages-sections.md index 0916b83a..ec550ed5 100644 --- a/docs/content/documentation/templates/pages-sections.md +++ b/docs/content/documentation/templates/pages-sections.md @@ -80,7 +80,9 @@ path: String; components: Array; permalink: String; extra: HashMap; -// Pages directly in this section, sorted if asked +// Pages directly in this section. By default, the pages are not sorted. Please set the "sorted_by" +// variable in the _index.md file of the corresponding section to "date" or "weight" for sorting by +// date and weight, respectively. pages: Array; // Direct subsections to this section, sorted by subsections weight // This only contains the path to use in the `get_section` Tera function to get diff --git a/docs/content/documentation/themes/creating-a-theme.md b/docs/content/documentation/themes/creating-a-theme.md index 6eb078b5..b01577c7 100644 --- a/docs/content/documentation/themes/creating-a-theme.md +++ b/docs/content/documentation/themes/creating-a-theme.md @@ -51,11 +51,6 @@ theme, with live reload working as expected. Make sure to commit every directory (including `content`) in order for other people to be able to build the theme from your repository. -### Caveat - -Please note that [include paths](https://tera.netlify.com/docs#include) can only be used in normal templates. -Theme templates should use [macros](https://tera.netlify.com/docs#macros) instead. - ## Submitting a theme to the gallery If you want your theme to be featured in the [themes](@/themes/_index.md) section diff --git a/docs/content/documentation/themes/extending-a-theme.md b/docs/content/documentation/themes/extending-a-theme.md new file mode 100644 index 00000000..d403427c --- /dev/null +++ b/docs/content/documentation/themes/extending-a-theme.md @@ -0,0 +1,21 @@ ++++ +title = "Extending a theme" +weight = 30 ++++ + +When your site uses a theme, you can replace parts of it in your site's templates folder. For any given theme template, you can either override a single block in it, or replace the whole template. If a site template and a theme template collide, the site template will be given priority. Whether a theme template collides or not, theme templates remain accessible from any template within `theme_name/templates/`. + +## Replacing a template + +When a site template and a theme template have the same path, for example `templates/page.html` and `themes/theme_name/templates/page.html`, the site template is the one that will be used. This is how you can replace a whole template for a theme. + +## Overriding a block + +If you don't want to replace a whole template, but override parts of it, you can [extend the template](https://tera.netlify.app/docs/#inheritance) and redefine some specific blocks. For example, if you want to override the `title` block in your theme's page.html, you can create a page.html file in your site templates with the following content: + +``` +{% extends "theme_name/templates/page.html" %} +{% block title %}{{ page.title }}{% endblock %} +``` + +If you extend `page.html` and not `theme_name/templates/page.html` specifically, it will extend the site's page template if it exists, and the theme's page template otherwise. This makes it possible to override your theme's base template(s) from your site templates, as long as the theme templates do not hardcode the theme name in template paths. For instance, children templates in the theme should use `{% extends 'index.html' %}`, not `{% extends 'theme_name/templates/index.html' %}`. diff --git a/docs/sass/_docs.scss b/docs/sass/_docs.scss index e73956d5..22d1bb97 100644 --- a/docs/sass/_docs.scss +++ b/docs/sass/_docs.scss @@ -47,19 +47,12 @@ h1, h2, h3, h4, h5, h6 { .zola-anchor { font-size: 1.25rem; - visibility: hidden; margin-left: -2rem; margin-right: 0.75rem; text-decoration: none; border-bottom-color: transparent; cursor: pointer; } - - &:hover { - .zola-anchor { - visibility: visible; - } - } } blockquote { diff --git a/netlify.toml b/netlify.toml index bbb60f3e..d9fb82b4 100644 --- a/netlify.toml +++ b/netlify.toml @@ -4,7 +4,7 @@ command = "zola build" [build.environment] - ZOLA_VERSION = "0.8.0" + ZOLA_VERSION = "0.11.0" [context.deploy-preview] command = "zola build --base-url $DEPLOY_PRIME_URL" diff --git a/snapcraft.yaml b/snapcraft.yaml index 52c627cb..5a904400 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: zola -version: 0.11.0 +version: 0.12.0 summary: A fast static site generator in a single binary with everything built-in. description: | A fast static site generator in a single binary with everything built-in. @@ -21,7 +21,7 @@ parts: zola: source-type: git source: https://github.com/getzola/zola.git - source-tag: v0.11.0 + source-tag: v0.12.0 plugin: rust rust-channel: stable build-packages: diff --git a/src/cli.rs b/src/cli.rs index 79d758ce..bc14a5ab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,11 +24,15 @@ pub fn build_cli() -> App<'static, 'static> { .subcommands(vec![ SubCommand::with_name("init") .about("Create a new Zola project") - .arg( + .args(&[ Arg::with_name("name") .default_value(".") - .help("Name of the project. Will create a new directory with that name in the current directory") - ), + .help("Name of the project. Will create a new directory with that name in the current directory"), + Arg::with_name("force") + .short("f") + .takes_value(false) + .help("Force creation of project even if directory is non-empty") + ]), SubCommand::with_name("build") .about("Deletes the output directory if there is one and builds the site") .args(&[ @@ -86,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/init.rs b/src/cmd/init.rs index ad22bb46..07f230e5 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,5 +1,6 @@ use std::fs::{canonicalize, create_dir}; use std::path::Path; +use std::path::PathBuf; use errors::{bail, Result}; use utils::fs::create_file; @@ -25,6 +26,15 @@ build_search_index = %SEARCH% # Put all your custom variables here "#; +// canonicalize(path) function on windows system returns a path with UNC. +// Example: \\?\C:\Users\VssAdministrator\AppData\Local\Temp\new_project +// More details on Universal Naming Convention (UNC): +// https://en.wikipedia.org/wiki/Path_(computing)#Uniform_Naming_Convention +// So the following const will be used to remove the network part of the UNC to display users a more common +// path on windows systems. +// This is a workaround until this issue https://github.com/rust-lang/rust/issues/42869 was fixed. +const LOCAL_UNC: &str = "\\\\?\\"; + // Given a path, return true if it is a directory and it doesn't have any // non-hidden files, otherwise return false (path is assumed to exist) pub fn is_directory_quasi_empty(path: &Path) -> Result { @@ -56,10 +66,17 @@ pub fn is_directory_quasi_empty(path: &Path) -> Result { Ok(false) } -pub fn create_new_project(name: &str) -> Result<()> { +// Remove the unc part of a windows path +fn strip_unc(path: &PathBuf) -> String { + let path_to_refine = path.to_str().unwrap(); + path_to_refine.trim_start_matches(LOCAL_UNC).to_string() +} + +pub fn create_new_project(name: &str, force: bool) -> Result<()> { let path = Path::new(name); + // Better error message than the rust default - if path.exists() && !is_directory_quasi_empty(&path)? { + if path.exists() && !is_directory_quasi_empty(&path)? && !force { if name == "." { bail!("The current directory is not an empty folder (hidden files are ignored)."); } else { @@ -89,7 +106,10 @@ pub fn create_new_project(name: &str) -> Result<()> { populate(&path, compile_sass, &config)?; println!(); - console::success(&format!("Done! Your site was created in {:?}", canonicalize(path).unwrap())); + console::success(&format!( + "Done! Your site was created in {}", + strip_unc(&canonicalize(path).unwrap()) + )); println!(); console::info( "Get started by moving into the directory and using the built-in server: `zola serve`", @@ -119,6 +139,7 @@ mod tests { use super::*; use std::env::temp_dir; use std::fs::{create_dir, remove_dir, remove_dir_all}; + use std::path::Path; #[test] fn init_empty_directory() { @@ -224,4 +245,47 @@ mod tests { remove_dir_all(&dir).unwrap(); } + + #[test] + fn strip_unc_test() { + let mut dir = temp_dir(); + dir.push("new_project"); + if dir.exists() { + remove_dir_all(&dir).expect("Could not free test directory"); + } + create_dir(&dir).expect("Could not create test directory"); + if cfg!(target_os = "windows") { + assert_eq!( + strip_unc(&canonicalize(Path::new(&dir)).unwrap()), + "C:\\Users\\VssAdministrator\\AppData\\Local\\Temp\\new_project" + ) + } else { + assert_eq!( + strip_unc(&canonicalize(Path::new(&dir)).unwrap()), + canonicalize(Path::new(&dir)).unwrap().to_str().unwrap().to_string() + ); + } + + remove_dir_all(&dir).unwrap(); + } + + // If the following test fails it means that the canonicalize function is fixed and strip_unc + // function/workaround is not anymore required. + // See issue https://github.com/rust-lang/rust/issues/42869 as a reference. + #[test] + #[cfg(target_os = "windows")] + fn strip_unc_required_test() { + let mut dir = temp_dir(); + dir.push("new_project"); + if dir.exists() { + remove_dir_all(&dir).expect("Could not free test directory"); + } + create_dir(&dir).expect("Could not create test directory"); + assert_eq!( + canonicalize(Path::new(&dir)).unwrap().to_str().unwrap(), + "\\\\?\\C:\\Users\\VssAdministrator\\AppData\\Local\\Temp\\new_project" + ); + + remove_dir_all(&dir).unwrap(); + } } diff --git a/src/cmd/livereload.js b/src/cmd/livereload.js index 01066a6b..eda2d087 100644 --- a/src/cmd/livereload.js +++ b/src/cmd/livereload.js @@ -1 +1 @@ -!function(){return function e(t,r,n){function o(s,c){if(!r[s]){if(!t[s]){var a="function"==typeof require&&require;if(!c&&a)return a(s,!0);if(i)return i(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var u=r[s]={exports:{}};t[s][0].call(u.exports,function(e){return o(t[s][1][e]||e)},u,u.exports,e,t,r,n)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;su;)if((c=a[u++])!=c)return!0}else for(;l>u;u++)if((e||u in a)&&a[u]===r)return e||u||0;return!e&&-1}}},{"./_to-absolute-index":66,"./_to-iobject":68,"./_to-length":69}],6:[function(e,t,r){var n=e("./_cof"),o=e("./_wks")("toStringTag"),i="Arguments"==n(function(){return arguments}());t.exports=function(e){var t,r,s;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),o))?r:i?n(t):"Object"==(s=n(t))&&"function"==typeof t.callee?"Arguments":s}},{"./_cof":7,"./_wks":75}],7:[function(e,t,r){var n={}.toString;t.exports=function(e){return n.call(e).slice(8,-1)}},{}],8:[function(e,t,r){var n=t.exports={version:"2.6.11"};"number"==typeof __e&&(__e=n)},{}],9:[function(e,t,r){"use strict";var n=e("./_object-dp"),o=e("./_property-desc");t.exports=function(e,t,r){t in e?n.f(e,t,o(0,r)):e[t]=r}},{"./_object-dp":42,"./_property-desc":53}],10:[function(e,t,r){var n=e("./_a-function");t.exports=function(e,t,r){if(n(e),void 0===t)return e;switch(r){case 1:return function(r){return e.call(t,r)};case 2:return function(r,n){return e.call(t,r,n)};case 3:return function(r,n,o){return e.call(t,r,n,o)}}return function(){return e.apply(t,arguments)}}},{"./_a-function":1}],11:[function(e,t,r){t.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},{}],12:[function(e,t,r){t.exports=!e("./_fails")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},{"./_fails":18}],13:[function(e,t,r){var n=e("./_is-object"),o=e("./_global").document,i=n(o)&&n(o.createElement);t.exports=function(e){return i?o.createElement(e):{}}},{"./_global":22,"./_is-object":31}],14:[function(e,t,r){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},{}],15:[function(e,t,r){var n=e("./_object-keys"),o=e("./_object-gops"),i=e("./_object-pie");t.exports=function(e){var t=n(e),r=o.f;if(r)for(var s,c=r(e),a=i.f,l=0;c.length>l;)a.call(e,s=c[l++])&&t.push(s);return t}},{"./_object-gops":47,"./_object-keys":50,"./_object-pie":51}],16:[function(e,t,r){var n=e("./_global"),o=e("./_core"),i=e("./_hide"),s=e("./_redefine"),c=e("./_ctx"),a=function(e,t,r){var l,u,f,d,h=e&a.F,p=e&a.G,_=e&a.S,g=e&a.P,v=e&a.B,y=p?n:_?n[t]||(n[t]={}):(n[t]||{}).prototype,m=p?o:o[t]||(o[t]={}),b=m.prototype||(m.prototype={});for(l in p&&(r=t),r)f=((u=!h&&y&&void 0!==y[l])?y:r)[l],d=v&&u?c(f,n):g&&"function"==typeof f?c(Function.call,f):f,y&&s(y,l,f,e&a.U),m[l]!=f&&i(m,l,d),g&&b[l]!=f&&(b[l]=f)};n.core=o,a.F=1,a.G=2,a.S=4,a.P=8,a.B=16,a.W=32,a.U=64,a.R=128,t.exports=a},{"./_core":8,"./_ctx":10,"./_global":22,"./_hide":24,"./_redefine":54}],17:[function(e,t,r){var n=e("./_wks")("match");t.exports=function(e){var t=/./;try{"/./"[e](t)}catch(r){try{return t[n]=!1,!"/./"[e](t)}catch(e){}}return!0}},{"./_wks":75}],18:[function(e,t,r){t.exports=function(e){try{return!!e()}catch(e){return!0}}},{}],19:[function(e,t,r){"use strict";e("./es6.regexp.exec");var n=e("./_redefine"),o=e("./_hide"),i=e("./_fails"),s=e("./_defined"),c=e("./_wks"),a=e("./_regexp-exec"),l=c("species"),u=!i(function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$")}),f=function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var r="ab".split(e);return 2===r.length&&"a"===r[0]&&"b"===r[1]}();t.exports=function(e,t,r){var d=c(e),h=!i(function(){var t={};return t[d]=function(){return 7},7!=""[e](t)}),p=h?!i(function(){var t=!1,r=/a/;return r.exec=function(){return t=!0,null},"split"===e&&(r.constructor={},r.constructor[l]=function(){return r}),r[d](""),!t}):void 0;if(!h||!p||"replace"===e&&!u||"split"===e&&!f){var _=/./[d],g=r(s,d,""[e],function(e,t,r,n,o){return t.exec===a?h&&!o?{done:!0,value:_.call(t,r,n)}:{done:!0,value:e.call(r,t,n)}:{done:!1}}),v=g[0],y=g[1];n(String.prototype,e,v),o(RegExp.prototype,d,2==t?function(e,t){return y.call(e,this,t)}:function(e){return y.call(e,this)})}}},{"./_defined":11,"./_fails":18,"./_hide":24,"./_redefine":54,"./_regexp-exec":56,"./_wks":75,"./es6.regexp.exec":82}],20:[function(e,t,r){"use strict";var n=e("./_an-object");t.exports=function(){var e=n(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t}},{"./_an-object":4}],21:[function(e,t,r){t.exports=e("./_shared")("native-function-to-string",Function.toString)},{"./_shared":61}],22:[function(e,t,r){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},{}],23:[function(e,t,r){var n={}.hasOwnProperty;t.exports=function(e,t){return n.call(e,t)}},{}],24:[function(e,t,r){var n=e("./_object-dp"),o=e("./_property-desc");t.exports=e("./_descriptors")?function(e,t,r){return n.f(e,t,o(1,r))}:function(e,t,r){return e[t]=r,e}},{"./_descriptors":12,"./_object-dp":42,"./_property-desc":53}],25:[function(e,t,r){var n=e("./_global").document;t.exports=n&&n.documentElement},{"./_global":22}],26:[function(e,t,r){t.exports=!e("./_descriptors")&&!e("./_fails")(function(){return 7!=Object.defineProperty(e("./_dom-create")("div"),"a",{get:function(){return 7}}).a})},{"./_descriptors":12,"./_dom-create":13,"./_fails":18}],27:[function(e,t,r){var n=e("./_is-object"),o=e("./_set-proto").set;t.exports=function(e,t,r){var i,s=t.constructor;return s!==r&&"function"==typeof s&&(i=s.prototype)!==r.prototype&&n(i)&&o&&o(e,i),e}},{"./_is-object":31,"./_set-proto":57}],28:[function(e,t,r){var n=e("./_cof");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==n(e)?e.split(""):Object(e)}},{"./_cof":7}],29:[function(e,t,r){var n=e("./_iterators"),o=e("./_wks")("iterator"),i=Array.prototype;t.exports=function(e){return void 0!==e&&(n.Array===e||i[o]===e)}},{"./_iterators":38,"./_wks":75}],30:[function(e,t,r){var n=e("./_cof");t.exports=Array.isArray||function(e){return"Array"==n(e)}},{"./_cof":7}],31:[function(e,t,r){t.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},{}],32:[function(e,t,r){var n=e("./_is-object"),o=e("./_cof"),i=e("./_wks")("match");t.exports=function(e){var t;return n(e)&&(void 0!==(t=e[i])?!!t:"RegExp"==o(e))}},{"./_cof":7,"./_is-object":31,"./_wks":75}],33:[function(e,t,r){var n=e("./_an-object");t.exports=function(e,t,r,o){try{return o?t(n(r)[0],r[1]):t(r)}catch(t){var i=e.return;throw void 0!==i&&n(i.call(e)),t}}},{"./_an-object":4}],34:[function(e,t,r){"use strict";var n=e("./_object-create"),o=e("./_property-desc"),i=e("./_set-to-string-tag"),s={};e("./_hide")(s,e("./_wks")("iterator"),function(){return this}),t.exports=function(e,t,r){e.prototype=n(s,{next:o(1,r)}),i(e,t+" Iterator")}},{"./_hide":24,"./_object-create":41,"./_property-desc":53,"./_set-to-string-tag":59,"./_wks":75}],35:[function(e,t,r){"use strict";var n=e("./_library"),o=e("./_export"),i=e("./_redefine"),s=e("./_hide"),c=e("./_iterators"),a=e("./_iter-create"),l=e("./_set-to-string-tag"),u=e("./_object-gpo"),f=e("./_wks")("iterator"),d=!([].keys&&"next"in[].keys()),h=function(){return this};t.exports=function(e,t,r,p,_,g,v){a(r,t,p);var y,m,b,x=function(e){if(!d&&e in S)return S[e];switch(e){case"keys":case"values":return function(){return new r(this,e)}}return function(){return new r(this,e)}},j=t+" Iterator",w="values"==_,k=!1,S=e.prototype,O=S[f]||S["@@iterator"]||_&&S[_],R=O||x(_),L=_?w?x("entries"):R:void 0,E="Array"==t&&S.entries||O;if(E&&(b=u(E.call(new e)))!==Object.prototype&&b.next&&(l(b,j,!0),n||"function"==typeof b[f]||s(b,f,h)),w&&O&&"values"!==O.name&&(k=!0,R=function(){return O.call(this)}),n&&!v||!d&&!k&&S[f]||s(S,f,R),c[t]=R,c[j]=h,_)if(y={values:w?R:x("values"),keys:g?R:x("keys"),entries:L},v)for(m in y)m in S||i(S,m,y[m]);else o(o.P+o.F*(d||k),t,y);return y}},{"./_export":16,"./_hide":24,"./_iter-create":34,"./_iterators":38,"./_library":39,"./_object-gpo":48,"./_redefine":54,"./_set-to-string-tag":59,"./_wks":75}],36:[function(e,t,r){var n=e("./_wks")("iterator"),o=!1;try{var i=[7][n]();i.return=function(){o=!0},Array.from(i,function(){throw 2})}catch(e){}t.exports=function(e,t){if(!t&&!o)return!1;var r=!1;try{var i=[7],s=i[n]();s.next=function(){return{done:r=!0}},i[n]=function(){return s},e(i)}catch(e){}return r}},{"./_wks":75}],37:[function(e,t,r){t.exports=function(e,t){return{value:t,done:!!e}}},{}],38:[function(e,t,r){t.exports={}},{}],39:[function(e,t,r){t.exports=!1},{}],40:[function(e,t,r){var n=e("./_uid")("meta"),o=e("./_is-object"),i=e("./_has"),s=e("./_object-dp").f,c=0,a=Object.isExtensible||function(){return!0},l=!e("./_fails")(function(){return a(Object.preventExtensions({}))}),u=function(e){s(e,n,{value:{i:"O"+ ++c,w:{}}})},f=t.exports={KEY:n,NEED:!1,fastKey:function(e,t){if(!o(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!i(e,n)){if(!a(e))return"F";if(!t)return"E";u(e)}return e[n].i},getWeak:function(e,t){if(!i(e,n)){if(!a(e))return!0;if(!t)return!1;u(e)}return e[n].w},onFreeze:function(e){return l&&f.NEED&&a(e)&&!i(e,n)&&u(e),e}}},{"./_fails":18,"./_has":23,"./_is-object":31,"./_object-dp":42,"./_uid":72}],41:[function(e,t,r){var n=e("./_an-object"),o=e("./_object-dps"),i=e("./_enum-bug-keys"),s=e("./_shared-key")("IE_PROTO"),c=function(){},a=function(){var t,r=e("./_dom-create")("iframe"),n=i.length;for(r.style.display="none",e("./_html").appendChild(r),r.src="javascript:",(t=r.contentWindow.document).open(),t.write(" +{% endblock script %} diff --git a/test_site/templates/index_paginated.html b/test_site/templates/index_paginated.html index 851ddb38..fe66d79a 100644 --- a/test_site/templates/index_paginated.html +++ b/test_site/templates/index_paginated.html @@ -29,5 +29,8 @@
{% endblock content %} + + {% include "current_path.html" %} + diff --git a/test_site/templates/section.html b/test_site/templates/section.html index 724795a6..3a262d3b 100644 --- a/test_site/templates/section.html +++ b/test_site/templates/section.html @@ -1,13 +1,7 @@ -{% extends "index.html" %} - +{% extends "sample/templates/section-specific-extends.html" %} {% block content %} - {% for page in section.pages %} + {% for page in section.pages %} {{page.title}} {% endfor %} - {{ section.relative_path | safe }} - {% for sub in section.subsections %} - {% set subsection = get_section(path=sub) %} - {{subsection.title}} - Sub-pages: {{subsection.pages | length}} - {% endfor %} + {{ super() }} {% endblock content %} diff --git a/test_site/templates/shortcodes/pirate.html b/test_site/templates/shortcodes/pirate_included.html similarity index 100% rename from test_site/templates/shortcodes/pirate.html rename to test_site/templates/shortcodes/pirate_included.html diff --git a/test_site/templates/tags/list.html b/test_site/templates/tags/list.html index 946e6f10..c13f40fd 100644 --- a/test_site/templates/tags/list.html +++ b/test_site/templates/tags/list.html @@ -1,3 +1,5 @@ {% for tag in terms %} {{ tag.name }} {{ tag.slug }} {{ tag.pages | length }} {% endfor %} + +{% include "current_path.html" %} \ No newline at end of file diff --git a/test_site/templates/tags/single.html b/test_site/templates/tags/single.html index 0c3f8fb8..6e2ffd1b 100644 --- a/test_site/templates/tags/single.html +++ b/test_site/templates/tags/single.html @@ -19,3 +19,5 @@ {% if paginator.previous %}has_prev{% endif%} {% if paginator.next %}has_next{% endif%} {% endif %} + +{% include "current_path.html" %} \ No newline at end of file diff --git a/test_site/themes/sample/templates/current_path.html b/test_site/themes/sample/templates/current_path.html new file mode 100644 index 00000000..fc6c7653 --- /dev/null +++ b/test_site/themes/sample/templates/current_path.html @@ -0,0 +1 @@ +
[current_path]({{ current_path | safe }})
\ No newline at end of file diff --git a/test_site/themes/sample/templates/index.html b/test_site/themes/sample/templates/index.html index e965047a..e0a8d28d 100644 --- a/test_site/themes/sample/templates/index.html +++ b/test_site/themes/sample/templates/index.html @@ -1 +1,24 @@ -Hello + + + + + + + + + + + {{ config.title }} + + + +
+ {% block content %}Hello{% endblock content %} +
+ + {% include "current_path.html" %} + + {% block script %} + {% endblock script %} + + diff --git a/test_site/themes/sample/templates/section-specific-extends.html b/test_site/themes/sample/templates/section-specific-extends.html new file mode 100644 index 00000000..7124bf1a --- /dev/null +++ b/test_site/themes/sample/templates/section-specific-extends.html @@ -0,0 +1,9 @@ +{% extends "index.html" %} +{% block content %} + {{ section.relative_path | safe }} + {% for sub in section.subsections %} + {% set subsection = get_section(path=sub) %} + {{subsection.title}} + Sub-pages: {{subsection.pages | length}} + {% endfor %} +{% endblock content %} diff --git a/test_site/themes/sample/templates/section.html b/test_site/themes/sample/templates/section.html new file mode 100644 index 00000000..c206205c --- /dev/null +++ b/test_site/themes/sample/templates/section.html @@ -0,0 +1,4 @@ +{% extends 'index.html' %} +{% block content %} +I'm overriden in all cases so seeing me is a bug +{% endblock %} diff --git a/test_site/themes/sample/templates/shortcodes/pirate.html b/test_site/themes/sample/templates/shortcodes/pirate.html new file mode 100644 index 00000000..7c5326b2 --- /dev/null +++ b/test_site/themes/sample/templates/shortcodes/pirate.html @@ -0,0 +1 @@ +{% include 'shortcodes/pirate_included.html' %} diff --git a/test_site/themes/sample/templates/shortcodes/pirate_included.html b/test_site/themes/sample/templates/shortcodes/pirate_included.html new file mode 100644 index 00000000..2e5b9387 --- /dev/null +++ b/test_site/themes/sample/templates/shortcodes/pirate_included.html @@ -0,0 +1 @@ +SHOULDNOTAPPEAR: overriden by site shortcode diff --git a/test_site_i18n/templates/auteurs/list.html b/test_site_i18n/templates/auteurs/list.html index 0acbafe4..3ef76ee6 100644 --- a/test_site_i18n/templates/auteurs/list.html +++ b/test_site_i18n/templates/auteurs/list.html @@ -1,4 +1,4 @@ {% for author in terms %} {{ author.name }} {{ author.slug }} {{ author.pages | length }} {% endfor %} -{{lang}} \ No newline at end of file +{{lang}}