diff --git a/CHANGELOG.md b/CHANGELOG.md index e45bf0c5..281f7e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.12.1 (2020-09-27) + +- Add line highlighting in code blocks +- Fix the new `zola serve` being broken on Windows +- Fix slugified taxonomies not being rendered at the right path +- Fix issues with shortcodes with newlines and read more + ## 0.12.0 (2020-09-04) ### Breaking diff --git a/Cargo.lock b/Cargo.lock index de7a7490..34326147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,9 +157,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92046dbb6f9332943252123f53623e0a6d513651af14967e2991c371ec20201c" +checksum = "41aa2ec95ca3b5c54cf73c91acf06d24f4495d5f1b1c12506ae3483d646177ac" [[package]] name = "byteorder" @@ -185,9 +185,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "cc" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" +checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c" [[package]] name = "cedarwood" @@ -206,14 +206,16 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "chrono" -version = "0.4.15" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" +checksum = "d021fddb7bd3e734370acfa4a83f34095571d8570c039f1420d77540f68d5772" dependencies = [ + "libc", "num-integer", "num-traits", "serde", "time", + "winapi 0.3.9", ] [[package]] @@ -290,12 +292,12 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" dependencies = [ - "cfg-if", "crossbeam-utils", + "maybe-uninit", ] [[package]] @@ -421,9 +423,9 @@ checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" [[package]] name = "either" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "elasticlunr-rs" @@ -565,7 +567,7 @@ dependencies = [ "cfg-if", "crc32fast", "libc", - "miniz_oxide 0.4.1", + "miniz_oxide 0.4.2", ] [[package]] @@ -721,9 +723,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" dependencies = [ "cfg-if", "libc", @@ -732,12 +734,12 @@ dependencies = [ [[package]] name = "gif" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" +checksum = "02efba560f227847cb41463a7395c514d127d4f74fff12ef0137fff1b84b96c4" dependencies = [ "color_quant", - "lzw", + "weezl", ] [[package]] @@ -799,6 +801,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "hashbrown" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7" + [[package]] name = "heck" version = "0.3.1" @@ -810,9 +818,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" dependencies = [ "libc", ] @@ -858,6 +866,12 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + [[package]] name = "humansize" version = "1.1.0" @@ -866,9 +880,9 @@ checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" [[package]] name = "hyper" -version = "0.13.7" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" +checksum = "2f3afcfae8af5ad0576a31e768415edb627824129e8e5a29b8bfccb2f234e835" dependencies = [ "bytes 0.5.6", "futures-channel", @@ -878,10 +892,10 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project", "socket2", - "time", "tokio", "tower-service", "tracing", @@ -952,9 +966,9 @@ dependencies = [ [[package]] name = "image" -version = "0.23.9" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "974e194911d1f7efe3cd8a8f9db3b767e43536327e899e8bc9a12ef5711b74d2" +checksum = "985fc06b1304d19c28d5c562ed78ef5316183f2b0053b46763a0b94862373c34" dependencies = [ "bytemuck", "byteorder", @@ -983,12 +997,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e47a3566dd4fd4eec714ae6ceabdee0caec795be835c223d92c2d40f1e8cf1c" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.9.0", ] [[package]] @@ -1039,7 +1053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ca2de723e93727460917d9542f7ae35a74d03d93923f03380a0238d860d137c" dependencies = [ "cedarwood", - "hashbrown", + "hashbrown 0.8.2", "lazy_static", "phf", "phf_codegen", @@ -1058,9 +1072,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73" +checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" dependencies = [ "wasm-bindgen", ] @@ -1095,9 +1109,9 @@ checksum = "73a004f877f468548d8d0ac4977456a249d8fabbdb8416c36db163dfc8f2e8ca" [[package]] name = "libc" -version = "0.2.76" +version = "0.2.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" +checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" [[package]] name = "library" @@ -1287,9 +1301,9 @@ checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "memoffset" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" dependencies = [ "autocfg", ] @@ -1332,11 +1346,12 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d7559a8a40d0f97e1edea3220f698f78b1c5ab67532e49f68fde3910323b722" +checksum = "c60c0dfe32c10b43a144bad8fc83538c52f58302c92300ea7ec7bf7b38d5a7b9" dependencies = [ "adler", + "autocfg", ] [[package]] @@ -1402,9 +1417,9 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" +checksum = "3ebc3ec692ed7c9a255596c67808dee269f64655d8baf7b4f0638e51ba1d6853" dependencies = [ "cfg-if", "libc", @@ -1507,9 +1522,9 @@ checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" [[package]] name = "onig" -version = "6.0.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd91ccd8a02fce2f7e8a86655aec67bc6c171e6f8e704118a0e8c4b866a05a8a" +checksum = "8a155d13862da85473665694f4c05d77fb96598bdceeaf696433c84ea9567e20" dependencies = [ "bitflags", "lazy_static", @@ -1519,9 +1534,9 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.5.0" +version = "69.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3814583fad89f3c60ae0701d80e87e1fd3028741723deda72d0d4a0ecf0cb0db" +checksum = "9bff06597a6b17855040955cae613af000fc0bfc8ad49ea68b9479a74e59292d" dependencies = [ "cc", "pkg-config", @@ -1646,18 +1661,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +checksum = "f48fad7cfbff853437be7cf54d7b993af21f53be7f0988cbfe4a51535aa77205" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" +checksum = "24c6d293bdd3ca5a1697997854c6cf7855e43fb6a0ba1c47af57a5bcafd158ae" dependencies = [ "proc-macro2", "quote", @@ -1666,9 +1681,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" +checksum = "71f349a4f0e70676ffb2dbafe16d0c992382d02f0a952e3ddf584fc289dac6b3" [[package]] name = "pin-utils" @@ -1758,9 +1773,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175c513d55719db99da20232b06cda8bab6b83ec2d04e3283edf0213c37c1a29" +checksum = "51ef7cd2518ead700af67bf9d1a658d90b6037d77110fd9c0445429d0ba1c6c9" dependencies = [ "unicode-xid", ] @@ -1850,9 +1865,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91739a34c4355b5434ce54c9086c5895604a9c278586d1f1aa95e04f66b525a0" +checksum = "e8c4fec834fb6e6d2dd5eece3c7b432a52f0ba887cf40e595190c4107edc08bf" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -1900,6 +1915,12 @@ version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +[[package]] +name = "relative-path" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65aff7c83039e88c1c0b4bedf8dfa93d6ec84d5fc2945b37c1fa4186f46c5f94" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -2092,18 +2113,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" +checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" +checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" dependencies = [ "proc-macro2", "quote", @@ -2179,6 +2200,7 @@ dependencies = [ "link_checker", "minify-html", "rayon", + "relative-path", "sass-rs", "search", "serde", @@ -2218,9 +2240,9 @@ checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" [[package]] name = "socket2" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44" dependencies = [ "cfg-if", "libc", @@ -2297,9 +2319,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.39" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" +checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" dependencies = [ "proc-macro2", "quote", @@ -2538,9 +2560,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f0e00789804e99b20f12bc7003ca416309d28a6f495d6af58d1e2c2842461b5" +checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" dependencies = [ "lazy_static", ] @@ -2757,9 +2779,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c" +checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" dependencies = [ "cfg-if", "serde", @@ -2769,9 +2791,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0" +checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" dependencies = [ "bumpalo", "lazy_static", @@ -2784,9 +2806,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95f8d235a77f880bcef268d379810ea6c0af2eacfa90b1ad5af731776e0c4699" +checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da" dependencies = [ "cfg-if", "js-sys", @@ -2796,9 +2818,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2" +checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2806,9 +2828,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" +checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" dependencies = [ "proc-macro2", "quote", @@ -2819,15 +2841,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092" +checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" [[package]] name = "web-sys" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47" +checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" dependencies = [ "js-sys", "wasm-bindgen", @@ -2852,6 +2874,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "weezl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d2f24b6c3aa92fb33279566dbebf1cbe66b03a73f09aa69cf8cf14d2f9feb9" + [[package]] name = "winapi" version = "0.2.8" @@ -2967,7 +2995,7 @@ dependencies = [ [[package]] name = "zola" -version = "0.12.0" +version = "0.12.1" dependencies = [ "atty", "chrono", @@ -2981,6 +3009,7 @@ dependencies = [ "lazy_static", "notify", "open", + "relative-path", "site", "termcolor", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 28b62fc7..81193759 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zola" -version = "0.12.0" +version = "0.12.1" authors = ["Vincent Prouillet "] edition = "2018" license = "MIT" @@ -35,6 +35,7 @@ ws = "0.9" ctrlc = "3" open = "1.2" globset = "0.4" +relative-path = "1" site = { path = "components/site" } errors = { path = "components/errors" } diff --git a/components/config/src/highlighting.rs b/components/config/src/highlighting.rs index 263b5f98..8b3d8d50 100644 --- a/components/config/src/highlighting.rs +++ b/components/config/src/highlighting.rs @@ -17,11 +17,11 @@ lazy_static! { } /// Returns the highlighter and whether it was found in the extra or not -pub fn get_highlighter<'a>(info: &str, config: &Config) -> (HighlightLines<'a>, bool) { +pub fn get_highlighter(language: Option<&str>, config: &Config) -> (HighlightLines<'static>, bool) { let theme = &THEME_SET.themes[&config.highlight_theme]; let mut in_extra = false; - if let Some(ref lang) = info.split(' ').next() { + if let Some(ref lang) = language { let syntax = SYNTAX_SET .find_syntax_by_token(lang) .or_else(|| { diff --git a/components/library/src/pagination/mod.rs b/components/library/src/pagination/mod.rs index 0a837d4b..37037f44 100644 --- a/components/library/src/pagination/mod.rs +++ b/components/library/src/pagination/mod.rs @@ -103,7 +103,7 @@ impl<'a> Paginator<'a> { paginate_reversed: false, root: PaginationRoot::Taxonomy(taxonomy, item), permalink: item.permalink.clone(), - path: format!("/{}/{}/", taxonomy.kind.name, item.slug), + path: format!("/{}/{}/", taxonomy.slug, item.slug), paginate_path: taxonomy .kind .paginate_path @@ -129,7 +129,7 @@ impl<'a> Paginator<'a> { } for key in self.all_pages.to_mut().iter_mut() { - let page = library.get_page_by_key(key.clone()); + let page = library.get_page_by_key(*key); current_page.push(page.to_serialized_basic(library)); if current_page.len() == self.paginate_by { @@ -416,7 +416,11 @@ mod tests { permalink: "https://vincent.is/tags/something/".to_string(), pages: library.pages().keys().collect(), }; - let taxonomy = Taxonomy { kind: taxonomy_def, items: vec![taxonomy_item.clone()] }; + let taxonomy = Taxonomy { + kind: taxonomy_def, + slug: "tags".to_string(), + items: vec![taxonomy_item.clone()], + }; let paginator = Paginator::from_taxonomy(&taxonomy, &taxonomy_item, &library); assert_eq!(paginator.pagers.len(), 2); @@ -431,6 +435,39 @@ mod tests { assert_eq!(paginator.pagers[1].path, "/tags/something/page/2/"); } + #[test] + fn test_can_create_paginator_for_slugified_taxonomy() { + let (_, library) = create_library(false, 3, false); + let taxonomy_def = TaxonomyConfig { + name: "some tags".to_string(), + paginate_by: Some(2), + ..TaxonomyConfig::default() + }; + let taxonomy_item = TaxonomyItem { + name: "Something".to_string(), + slug: "something".to_string(), + permalink: "https://vincent.is/some-tags/something/".to_string(), + pages: library.pages().keys().collect(), + }; + let taxonomy = Taxonomy { + kind: taxonomy_def, + slug: "some-tags".to_string(), + items: vec![taxonomy_item.clone()], + }; + let paginator = Paginator::from_taxonomy(&taxonomy, &taxonomy_item, &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/some-tags/something/"); + assert_eq!(paginator.pagers[0].path, "/some-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/some-tags/something/page/2/"); + assert_eq!(paginator.pagers[1].path, "/some-tags/something/page/2/"); + } + // https://github.com/getzola/zola/issues/866 #[test] fn works_with_empty_paginate_path() { diff --git a/components/library/src/taxonomies/mod.rs b/components/library/src/taxonomies/mod.rs index 62062c30..c048bfef 100644 --- a/components/library/src/taxonomies/mod.rs +++ b/components/library/src/taxonomies/mod.rs @@ -53,6 +53,7 @@ impl TaxonomyItem { pub fn new( name: &str, taxonomy: &TaxonomyConfig, + taxo_slug: &str, config: &Config, keys: Vec, library: &Library, @@ -72,7 +73,6 @@ impl TaxonomyItem { .collect(); let (mut pages, ignored_pages) = sort_pages_by_date(data); 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, taxo_slug, item_slug)) } else { @@ -118,6 +118,7 @@ impl<'a> SerializedTaxonomy<'a> { #[derive(Debug, Clone, PartialEq)] pub struct Taxonomy { pub kind: TaxonomyConfig, + pub slug: String, // this vec is sorted by the count of item pub items: Vec, } @@ -130,8 +131,9 @@ impl Taxonomy { library: &Library, ) -> Taxonomy { let mut sorted_items = vec![]; + let slug = slugify_paths(&kind.name, config.slugify.taxonomies); for (name, pages) in items { - sorted_items.push(TaxonomyItem::new(&name, &kind, config, pages, library)); + sorted_items.push(TaxonomyItem::new(&name, &kind, &slug, config, pages, library)); } //sorted_items.sort_by(|a, b| a.name.cmp(&b.name)); sorted_items.sort_by(|a, b| match a.slug.cmp(&b.slug) { @@ -150,7 +152,7 @@ impl Taxonomy { false } }); - Taxonomy { kind, items: sorted_items } + Taxonomy { kind, slug, items: sorted_items } } pub fn len(&self) -> usize { diff --git a/components/rendering/src/lib.rs b/components/rendering/src/lib.rs index dd896f93..36c1f61d 100644 --- a/components/rendering/src/lib.rs +++ b/components/rendering/src/lib.rs @@ -14,8 +14,7 @@ pub fn render_content(content: &str, context: &RenderContext) -> Result", "\n"); + let html = markdown_to_html(&rendered, context)?; return Ok(html); } diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index 75b644e0..ff419fbd 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -1,14 +1,11 @@ use lazy_static::lazy_static; use pulldown_cmark as cmark; use regex::Regex; -use syntect::easy::HighlightLines; -use syntect::html::{ - start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground, -}; +use syntect::html::{start_highlighted_html_snippet, IncludeBackground}; use crate::context::RenderContext; use crate::table_of_contents::{make_table_of_contents, Heading}; -use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET}; +use config::highlighting::THEME_SET; use errors::{Error, Result}; use front_matter::InsertAnchor; use utils::site::resolve_internal_link; @@ -18,6 +15,10 @@ use utils::vec::InsertMany; use self::cmark::{Event, LinkType, Options, Parser, Tag}; use pulldown_cmark::CodeBlockKind; +mod codeblock; +mod fence; +use self::codeblock::CodeBlock; + const CONTINUE_READING: &str = ""; const ANCHOR_LINK_TEMPLATE: &str = "anchor-link.html"; @@ -172,8 +173,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result = None; + let mut highlighter: Option = None; let mut inserted_anchors: Vec = vec![]; let mut headings: Vec = vec![]; @@ -182,6 +182,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result Result { - // if we are in the middle of a code block - if let Some((ref mut highlighter, in_extra)) = highlighter { - let highlighted = if in_extra { - if let Some(ref extra) = context.config.extra_syntax_set { - highlighter.highlight(&text, &extra) - } else { - unreachable!( - "Got a highlighter from extra syntaxes but no extra?" - ); - } - } else { - highlighter.highlight(&text, &SYNTAX_SET) - }; - //let highlighted = &highlighter.highlight(&text, ss); - let html = styled_line_to_highlighted_html(&highlighted, background); - return Event::Html(html.into()); + // if we are in the middle of a highlighted code block + if let Some(ref mut code_block) = highlighter { + let html = code_block.highlight(&text); + Event::Html(html.into()) + } else { + // Business as usual + Event::Text(text) } - - // Business as usual - Event::Text(text) } Event::Start(Tag::CodeBlock(ref kind)) => { if !context.config.highlight_code { @@ -221,16 +210,21 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result (), - CodeBlockKind::Fenced(info) => { - highlighter = Some(get_highlighter(info, &context.config)); + CodeBlockKind::Fenced(fence_info) => { + // This selects the background color the same way that + // start_coloured_html_snippet does + let color = theme + .settings + .background + .unwrap_or(::syntect::highlighting::Color::WHITE); + + highlighter = Some(CodeBlock::new( + fence_info, + &context.config, + IncludeBackground::IfDifferent(color), + )); } }; - // This selects the background color the same way that start_coloured_html_snippet does - let color = theme - .settings - .background - .unwrap_or(::syntect::highlighting::Color::WHITE); - background = IncludeBackground::IfDifferent(color); let snippet = start_highlighted_html_snippet(theme); let mut html = snippet.0; html.push_str(""); @@ -273,9 +267,27 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result") => { - has_summary = true; - Event::Html(CONTINUE_READING.into()) + Event::Html(ref markup) => { + if markup.contains("") { + has_summary = true; + Event::Html(CONTINUE_READING.into()) + } else { + if in_html_block && markup.contains("") { + in_html_block = false; + Event::Html(markup.replacen("", "", 1).into()) + } else if markup.contains("pre data-shortcode") { + in_html_block = true; + let m = markup.replacen("
", "", 1);
+                                if m.contains("
") { + in_html_block = false; + Event::Html(m.replacen("", "", 1).into()) + } else { + Event::Html(m.into()) + } + } else { + event + } + } } _ => event, } diff --git a/components/rendering/src/markdown/codeblock.rs b/components/rendering/src/markdown/codeblock.rs new file mode 100644 index 00000000..f27e592d --- /dev/null +++ b/components/rendering/src/markdown/codeblock.rs @@ -0,0 +1,182 @@ +use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET}; +use config::Config; +use std::cmp::min; +use std::collections::HashSet; +use syntect::easy::HighlightLines; +use syntect::highlighting::{Color, Style, Theme}; +use syntect::html::{styled_line_to_highlighted_html, IncludeBackground}; +use syntect::parsing::SyntaxSet; + +use super::fence::{FenceSettings, Range}; + +pub struct CodeBlock<'config> { + highlighter: HighlightLines<'static>, + extra_syntax_set: Option<&'config SyntaxSet>, + background: IncludeBackground, + theme: &'static Theme, + + /// List of ranges of lines to highlight. + highlight_lines: Vec, + /// The number of lines in the code block being processed. + num_lines: usize, +} + +impl<'config> CodeBlock<'config> { + pub fn new(fence_info: &str, config: &'config Config, background: IncludeBackground) -> Self { + let fence_info = FenceSettings::new(fence_info); + let theme = &THEME_SET.themes[&config.highlight_theme]; + let (highlighter, in_extra) = get_highlighter(fence_info.language, config); + Self { + highlighter, + extra_syntax_set: match in_extra { + true => config.extra_syntax_set.as_ref(), + false => None, + }, + background, + theme, + + highlight_lines: fence_info.highlight_lines, + num_lines: 0, + } + } + + pub fn highlight(&mut self, text: &str) -> String { + let highlighted = + self.highlighter.highlight(text, self.extra_syntax_set.unwrap_or(&SYNTAX_SET)); + let line_boundaries = self.find_line_boundaries(&highlighted); + + // First we make sure that `highlighted` is split at every line + // boundary. The `styled_line_to_highlighted_html` function will + // merge split items with identical styles, so this is not a + // problem. + // + // Note that this invalidates the values in `line_boundaries`. + // The `perform_split` function takes it by value to ensure that + // we don't use it later. + let mut highlighted = perform_split(&highlighted, line_boundaries); + + let hl_background = + self.theme.settings.line_highlight.unwrap_or(Color { r: 255, g: 255, b: 0, a: 0 }); + + let hl_lines = self.get_highlighted_lines(); + color_highlighted_lines(&mut highlighted, &hl_lines, hl_background); + + styled_line_to_highlighted_html(&highlighted, self.background) + } + + fn find_line_boundaries(&mut self, styled: &[(Style, &str)]) -> Vec { + let mut boundaries = Vec::new(); + for (vec_idx, (_style, s)) in styled.iter().enumerate() { + for (str_idx, character) in s.char_indices() { + if character == '\n' { + boundaries.push(StyledIdx { vec_idx, str_idx }); + } + } + } + self.num_lines = boundaries.len() + 1; + boundaries + } + + fn get_highlighted_lines(&self) -> HashSet { + let mut lines = HashSet::new(); + for range in &self.highlight_lines { + for line in range.from..=min(range.to, self.num_lines) { + // Ranges are one-indexed + lines.insert(line.saturating_sub(1)); + } + } + lines + } +} + +/// This is an index of a character in a `&[(Style, &'b str)]`. The `vec_idx` is the +/// index in the slice, and `str_idx` is the byte index of the character in the +/// corresponding string slice. +/// +/// The `Ord` impl on this type sorts lexiographically on `vec_idx`, and then `str_idx`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct StyledIdx { + vec_idx: usize, + str_idx: usize, +} + +/// This is a utility used by `perform_split`. If the `vec_idx` in the `StyledIdx` is +/// equal to the provided value, return the `str_idx`, otherwise return `None`. +fn get_str_idx_if_vec_idx_is(idx: Option<&StyledIdx>, vec_idx: usize) -> Option { + match idx { + Some(idx) if idx.vec_idx == vec_idx => Some(idx.str_idx), + _ => None, + } +} + +/// This function assumes that `line_boundaries` is sorted according to the `Ord` impl on +/// the `StyledIdx` type. +fn perform_split<'b>( + split: &[(Style, &'b str)], + line_boundaries: Vec, +) -> Vec<(Style, &'b str)> { + let mut result = Vec::new(); + + let mut idxs_iter = line_boundaries.into_iter().peekable(); + + for (split_idx, item) in split.iter().enumerate() { + let mut last_split = 0; + + // Since `line_boundaries` is sorted, we know that any remaining indexes in + // `idxs_iter` have `vec_idx >= split_idx`, and that if there are any with + // `vec_idx == split_idx`, they will be first. + // + // Using the `get_str_idx_if_vec_idx_is` utility, this loop will keep consuming + // indexes from `idxs_iter` as long as `vec_idx == split_idx` holds. Once + // `vec_idx` becomes larger than `split_idx`, the loop will finish without + // consuming that index. + // + // If `idxs_iter` is empty, or there are no indexes with `vec_idx == split_idx`, + // the loop does nothing. + while let Some(str_idx) = get_str_idx_if_vec_idx_is(idxs_iter.peek(), split_idx) { + // Consume the value we just peeked. + idxs_iter.next(); + + // This consumes the index to split at. We add one to include the newline + // together with its own line, rather than as the first character in the next + // line. + let split_at = min(str_idx + 1, item.1.len()); + + // This will fail if `line_boundaries` is not sorted. + debug_assert!(split_at >= last_split); + + // Skip splitting if the string slice would be empty. + if last_split != split_at { + result.push((item.0, &item.1[last_split..split_at])); + last_split = split_at; + } + } + + // Now append the remainder. If the current item was not split, this will + // append the entire item. + if last_split != item.1.len() { + result.push((item.0, &item.1[last_split..])); + } + } + + result +} + +fn color_highlighted_lines(data: &mut [(Style, &str)], lines: &HashSet, background: Color) { + if lines.is_empty() { + return; + } + + let mut current_line = 0; + + for item in data { + if lines.contains(¤t_line) { + item.0.background = background; + } + + // We split the lines such that every newline is at the end of an item. + if item.1.ends_with('\n') { + current_line += 1; + } + } +} diff --git a/components/rendering/src/markdown/fence.rs b/components/rendering/src/markdown/fence.rs new file mode 100644 index 00000000..e41b73a0 --- /dev/null +++ b/components/rendering/src/markdown/fence.rs @@ -0,0 +1,90 @@ +#[derive(Copy, Clone, Debug)] +pub struct Range { + pub from: usize, + pub to: usize, +} + +impl Range { + fn parse(s: &str) -> Option { + match s.find('-') { + Some(dash) => { + let mut from = s[..dash].parse().ok()?; + let mut to = s[dash + 1..].parse().ok()?; + if to < from { + std::mem::swap(&mut from, &mut to); + } + Some(Range { from, to }) + } + None => { + let val = s.parse().ok()?; + Some(Range { from: val, to: val }) + } + } + } +} + +#[derive(Debug)] +pub struct FenceSettings<'a> { + pub language: Option<&'a str>, + pub line_numbers: bool, + pub highlight_lines: Vec, +} +impl<'a> FenceSettings<'a> { + pub fn new(fence_info: &'a str) -> Self { + let mut me = Self { language: None, line_numbers: false, highlight_lines: Vec::new() }; + + for token in FenceIter::new(fence_info) { + match token { + FenceToken::Language(lang) => me.language = Some(lang), + FenceToken::EnableLineNumbers => me.line_numbers = true, + FenceToken::HighlightLines(lines) => me.highlight_lines.extend(lines), + } + } + + me + } +} + +#[derive(Debug)] +enum FenceToken<'a> { + Language(&'a str), + EnableLineNumbers, + HighlightLines(Vec), +} + +struct FenceIter<'a> { + split: std::str::Split<'a, char>, +} +impl<'a> FenceIter<'a> { + fn new(fence_info: &'a str) -> Self { + Self { split: fence_info.split(',') } + } +} + +impl<'a> Iterator for FenceIter<'a> { + type Item = FenceToken<'a>; + + fn next(&mut self) -> Option> { + loop { + let tok = self.split.next()?.trim(); + + let mut tok_split = tok.split('='); + match tok_split.next().unwrap_or("").trim() { + "" => continue, + "linenos" => return Some(FenceToken::EnableLineNumbers), + "hl_lines" => { + let mut ranges = Vec::new(); + for range in tok_split.next().unwrap_or("").split(' ') { + if let Some(range) = Range::parse(range) { + ranges.push(range); + } + } + return Some(FenceToken::HighlightLines(ranges)); + } + lang => { + return Some(FenceToken::Language(lang)); + } + } + } + } +} diff --git a/components/rendering/src/shortcode.rs b/components/rendering/src/shortcode.rs index 6e1520b9..497a28f4 100644 --- a/components/rendering/src/shortcode.rs +++ b/components/rendering/src/shortcode.rs @@ -131,7 +131,7 @@ fn render_shortcode( // 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()) + Ok(format!("
{}
", res)) } else { Ok(res.to_string()) } @@ -404,7 +404,7 @@ Some body {{ hello() }}{%/* end */%}"#, let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", "Hello {{id}}").unwrap(); let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera); - assert_eq!(res, "Inline Hello 1."); + assert_eq!(res, "Inline
Hello 1
."); } #[test] @@ -412,7 +412,7 @@ Some body {{ hello() }}{%/* end */%}"#, let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", "{{body}}").unwrap(); let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera); - assert_eq!(res, "Body\n Hey!"); + assert_eq!(res, "Body\n
Hey!
"); } // https://github.com/Keats/gutenberg/issues/462 @@ -421,7 +421,7 @@ Some body {{ hello() }}{%/* end */%}"#, let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", "{{body | safe}}").unwrap(); let res = render_shortcodes("Body\n {% youtube() %}\nHello \n \n\n World{% end %}", &tera); - assert_eq!(res, "Body\n Hello World"); + assert_eq!(res, "Body\n
Hello \n \n\n World
"); } #[test] @@ -429,7 +429,7 @@ Some body {{ hello() }}{%/* end */%}"#, let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", " \n {{body}} \n ").unwrap(); let res = render_shortcodes("\n{% youtube() %} \n content \n {% end %}\n", &tera); - assert_eq!(res, "\n content \n"); + assert_eq!(res, "\n
  content  
\n"); } #[test] @@ -437,7 +437,7 @@ Some body {{ hello() }}{%/* end */%}"#, let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", " \n Hello, Zola. \n ").unwrap(); let res = render_shortcodes("\n{{ youtube() }}\n", &tera); - assert_eq!(res, "\n Hello, Zola. \n"); + assert_eq!(res, "\n
  Hello, Zola.  
\n"); } #[test] diff --git a/components/rendering/tests/codeblock_hl_lines.rs b/components/rendering/tests/codeblock_hl_lines.rs new file mode 100644 index 00000000..bbbc3655 --- /dev/null +++ b/components/rendering/tests/codeblock_hl_lines.rs @@ -0,0 +1,400 @@ +use std::collections::HashMap; + +use tera::Tera; + +use config::Config; +use front_matter::InsertAnchor; +use rendering::{render_content, RenderContext}; + +macro_rules! colored_html_line { + ( @no $s:expr ) => {{ + let mut result = "".to_string(); + result.push_str($s); + result.push_str("\n"); + result + }}; + ( @hl $s:expr ) => {{ + let mut result = "".to_string(); + result.push_str($s); + result.push_str("\n"); + result + }}; +} + +macro_rules! colored_html { + ( $(@$kind:tt $s:expr),* $(,)* ) => {{ + let mut result = "
\n".to_string();
+        $(
+            result.push_str(colored_html_line!(@$kind $s).as_str());
+        )*
+        result.push_str("
"); + result + }}; +} + +#[test] +fn hl_lines_simple() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=2 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @no "foo", + @hl "bar", + @no "bar\nbaz", + ) + ); +} + +#[test] +fn hl_lines_in_middle() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=2-3 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @no "foo", + @hl "bar\nbar", + @no "baz", + ) + ); +} + +#[test] +fn hl_lines_all() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=1-4 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @hl "foo\nbar\nbar\nbaz", + ) + ); +} + +#[test] +fn hl_lines_start_from_one() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=1-3 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @hl "foo\nbar\nbar", + @no "baz", + ) + ); +} + +#[test] +fn hl_lines_start_from_zero() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=0-3 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @hl "foo\nbar\nbar", + @no "baz", + ) + ); +} + +#[test] +fn hl_lines_end() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=3-4 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @no "foo\nbar", + @hl "bar\nbaz", + ) + ); +} + +#[test] +fn hl_lines_end_out_of_bounds() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=3-4294967295 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @no "foo\nbar", + @hl "bar\nbaz", + ) + ); +} + +#[test] +fn hl_lines_overlap() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=2-3 1-2 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @hl "foo\nbar\nbar", + @no "baz", + ) + ); +} +#[test] +fn hl_lines_multiple() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=2-3,hl_lines=1-2 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @hl "foo\nbar\nbar", + @no "baz", + ) + ); +} + +#[test] +fn hl_lines_extra_spaces() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +``` hl_lines = 2 - 3 1 - 2 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @hl "foo\nbar\nbar", + @no "baz", + ) + ); +} + +#[test] +fn hl_lines_int_and_range() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=1 3-4 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @hl "foo", + @no "bar", + @hl "bar\nbaz", + ) + ); +} + +#[test] +fn hl_lines_single_line_range() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=2-2 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @no "foo", + @hl "bar", + @no "bar\nbaz", + ) + ); +} + +#[test] +fn hl_lines_reverse_range() { + let tera_ctx = Tera::default(); + let permalinks_ctx = HashMap::new(); + let mut config = Config::default(); + config.highlight_code = true; + let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); + let res = render_content( + r#" +```hl_lines=3-2 +foo +bar +bar +baz +``` + "#, + &context, + ) + .unwrap(); + assert_eq!( + res.body, + colored_html!( + @no "foo", + @hl "bar\nbar", + @no "baz", + ) + ); +} diff --git a/components/rendering/tests/markdown.rs b/components/rendering/tests/markdown.rs index 4b11b397..e37f9212 100644 --- a/components/rendering/tests/markdown.rs +++ b/components/rendering/tests/markdown.rs @@ -142,7 +142,7 @@ fn can_render_body_shortcode_with_markdown_char_in_name() { let res = render_content(&format!("{{% {}(author=\"Bob\") %}}\nhey\n{{% end %}}", i), &context) .unwrap(); - println!("{:?}", res); + assert!(res.body.contains("
hey - Bob
")); } } @@ -166,12 +166,12 @@ Here is another paragraph.

Here is another paragraph.

"; - tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); + tera.add_raw_template("shortcodes/figure.html", 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); } @@ -199,12 +199,12 @@ Here is another paragraph.

Here is another paragraph.

"; - tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); + tera.add_raw_template("shortcodes/figure.html", 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); } @@ -790,7 +790,7 @@ fn doesnt_try_to_highlight_content_from_shortcode() { let expected = "
\n \n \"Some\n \n\n
Some spheres.
\n
"; - tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap(); + tera.add_raw_template("shortcodes/figure.html", shortcode).unwrap(); let config = Config::default(); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); @@ -937,11 +937,102 @@ Bla bla"#;

Bla bla

"#; - tera.add_raw_template(&format!("shortcodes/{}.md", "quote"), shortcode).unwrap(); + tera.add_raw_template("shortcodes/quote.md", 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); +} + +// https://github.com/getzola/zola/issues/1172 +#[test] +fn can_render_shortcode_body_with_no_invalid_escaping() { + let permalinks_ctx = HashMap::new(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + + let shortcode = r#" + + {{ alt }} + +

(click for full size)

+
+"#; + + let markdown_string = r#"{{ resize_image(path="tlera-corp-gnat/gnat-with-picoblade-cable.jpg", width=600, alt="Some alt") }}"#; + + let expected = "\n \n \n

(click for full size)

\n
"; + + tera.add_raw_template("shortcodes/resize_image.html", 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); +} + +// https://github.com/getzola/zola/issues/1172 +#[test] +fn can_render_commented_out_shortcodes_fine() { + let permalinks_ctx = HashMap::new(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + + let shortcode = r#" + + {{ alt }} + +

(click for full size)

+
+"#; + + let markdown_string = r#""#; + + let expected = ""; + + tera.add_raw_template("shortcodes/resize_image.html", 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); +} + + +// https://zola.discourse.group/t/zola-12-issue-with-continue-reading/590/7 +#[test] +fn can_render_read_more_after_shortcode() { + let permalinks_ctx = HashMap::new(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + + let shortcode = r#"

Quote: {{body}}

"#; + let markdown_string = r#" +# Title + +Some text +{{ quote(body="Nothing is impossible. The word itself says - I'm Possible" author="Audrey Hepburn")}} + + +Again more text"#; + + let expected = r#"

Title

+

Some text

+

Quote: Nothing is impossible. The word itself says - I'm Possible

+ +

Again more text

+"#; + + tera.add_raw_template("shortcodes/quote.md", 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/site/Cargo.toml b/components/site/Cargo.toml index 60008852..ceda391d 100644 --- a/components/site/Cargo.toml +++ b/components/site/Cargo.toml @@ -8,12 +8,13 @@ include = ["src/**/*"] [dependencies] tera = "1" glob = "0.3" -minify-html = "0.3.6" +minify-html = "0.3.8" rayon = "1" serde = "1" serde_derive = "1" sass-rs = "0.2" lazy_static = "1.1" +relative-path = "1" errors = { path = "../errors" } config = { path = "../config" } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 61961b79..7d06b27a 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -11,7 +11,7 @@ use std::sync::{Arc, Mutex, RwLock}; use glob::glob; use lazy_static::lazy_static; -use minify_html::{truncate, Cfg}; +use minify_html::{with_friendly_error, Cfg}; use rayon::prelude::*; use tera::{Context, Tera}; @@ -19,6 +19,7 @@ use config::{get_config, Config}; use errors::{bail, Error, Result}; use front_matter::InsertAnchor; use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy}; +use relative_path::RelativePathBuf; use templates::render_redirect_template; use utils::fs::{ copy_directory, copy_file_if_needed, create_directory, create_file, ensure_directory_exists, @@ -28,7 +29,7 @@ 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())); + pub static ref SITE_CONTENT: Arc>> = Arc::new(RwLock::new(HashMap::new())); } /// Where are we building the site @@ -450,13 +451,18 @@ impl Site { 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) { + match with_friendly_error(&mut input_bytes, cfg) { + Ok(_len) => match std::str::from_utf8(&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); + bail!( + "Failed to truncate html at character {}: {} \n {}", + minify_error.position, + minify_error.message, + minify_error.code_context + ); } } } @@ -513,10 +519,12 @@ impl Site { let write_dirs = self.build_mode == BuildMode::Disk || create_dirs; ensure_directory_exists(&self.output_path)?; + let mut site_path = RelativePathBuf::new(); let mut current_path = self.output_path.to_path_buf(); for component in components { current_path.push(component); + site_path.push(component); if !current_path.exists() && write_dirs { create_directory(¤t_path)?; @@ -542,17 +550,10 @@ impl Site { 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); + let site_path = + if filename != "index.html" { site_path.join(filename) } else { site_path }; + + SITE_CONTENT.write().unwrap().insert(site_path, final_content); } } @@ -568,12 +569,8 @@ impl Site { let output = page.render_html(&self.tera, &self.config, &self.library.read().unwrap())?; 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(), - )?; + 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 { @@ -771,7 +768,7 @@ impl Site { components.push(taxonomy.kind.lang.as_ref()); } - components.push(taxonomy.kind.name.as_ref()); + components.push(taxonomy.slug.as_ref()); let list_output = taxonomy.render_all_terms(&self.tera, &self.config, &self.library.read().unwrap())?; @@ -801,7 +798,7 @@ impl Site { if taxonomy.kind.feed { self.render_feed( item.pages.iter().map(|p| library.get_page_by_key(*p)).collect(), - Some(&PathBuf::from(format!("{}/{}", taxonomy.kind.name, item.slug))), + Some(&PathBuf::from(format!("{}/{}", taxonomy.slug, item.slug))), if self.config.is_multilingual() && !taxonomy.kind.lang.is_empty() { &taxonomy.kind.lang } else { @@ -932,11 +929,7 @@ impl Site { 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(); + let pages = section.pages.iter().map(|k| library.get_page_by_key(*k)).collect(); self.render_feed( pages, Some(&PathBuf::from(§ion.path[1..])), diff --git a/components/site/src/sass.rs b/components/site/src/sass.rs index 2b1b1ef3..a9dc5b96 100644 --- a/components/site/src/sass.rs +++ b/components/site/src/sass.rs @@ -44,14 +44,7 @@ fn compile_sass_glob( 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 files = get_non_partial_scss(sass_path, extension); let mut compiled_paths = Vec::new(); for file in files { @@ -71,3 +64,48 @@ fn compile_sass_glob( Ok(compiled_paths) } + +fn get_non_partial_scss(sass_path: &Path, extension: &str) -> Vec { + let glob_string = format!("{}/**/*.{}", sass_path.display(), extension); + glob(&glob_string) + .expect("Invalid glob for sass") + .filter_map(|e| e.ok()) + .filter(|entry| { + !entry + .as_path() + .iter() + .last() + .map(|c| c.to_string_lossy().starts_with('_')) + .unwrap_or(true) + }) + .collect::>() +} + +#[test] +fn test_get_non_partial_scss() { + use std::env; + + let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); + path.push("test_site"); + path.push("sass"); + + let result = get_non_partial_scss(&path, "scss"); + + assert!(result.len() != 0); + assert!(result.iter().filter_map(|path| path.file_name()).any(|file| file == "scss.scss")) +} +#[test] +fn test_get_non_partial_scss_underscores() { + use std::env; + + let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); + path.push("test_site"); + path.push("_dir_with_underscores"); + path.push(".."); + path.push("sass"); + + let result = get_non_partial_scss(&path, "scss"); + + assert!(result.len() != 0); + assert!(result.iter().filter_map(|path| path.file_name()).any(|file| file == "scss.scss")) +} diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index 1778f408..af15ed54 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -154,6 +154,10 @@ fn can_build_site_without_live_reload() { assert_eq!(file_exists!(public, "categories/index.html"), true); assert_eq!(file_exists!(public, "categories/a-category/index.html"), true); assert_eq!(file_exists!(public, "categories/a-category/atom.xml"), true); + // and podcast_authors (https://github.com/getzola/zola/issues/1177) + assert_eq!(file_exists!(public, "podcast-authors/index.html"), true); + assert_eq!(file_exists!(public, "podcast-authors/some-person/index.html"), true); + assert_eq!(file_exists!(public, "podcast-authors/some-person/atom.xml"), true); // But no tags assert_eq!(file_exists!(public, "tags/index.html"), false); diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index 0469612a..2b3693d4 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -572,6 +572,7 @@ mod tests { let tag = TaxonomyItem::new( "Programming", &taxo_config, + "tags", &config, vec![], &library.read().unwrap(), @@ -579,12 +580,14 @@ mod tests { let tag_fr = TaxonomyItem::new( "Programmation", &taxo_config_fr, + "tags", &config, vec![], &library.read().unwrap(), ); - let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; - let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] }; + let tags = Taxonomy { kind: taxo_config, slug: "tags".to_string(), items: vec![tag] }; + let tags_fr = + Taxonomy { kind: taxo_config_fr, slug: "tags".to_string(), items: vec![tag_fr] }; let taxonomies = vec![tags.clone(), tags_fr.clone()]; let static_fn = @@ -647,10 +650,12 @@ mod tests { ..TaxonomyConfig::default() }; let library = Library::new(0, 0, false); - let tag = TaxonomyItem::new("Programming", &taxo_config, &config, vec![], &library); - let tag_fr = TaxonomyItem::new("Programmation", &taxo_config_fr, &config, vec![], &library); - let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; - let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] }; + let tag = TaxonomyItem::new("Programming", &taxo_config, "tags", &config, vec![], &library); + let tag_fr = + TaxonomyItem::new("Programmation", &taxo_config_fr, "tags", &config, vec![], &library); + let tags = Taxonomy { kind: taxo_config, slug: "tags".to_string(), items: vec![tag] }; + let tags_fr = + Taxonomy { kind: taxo_config_fr, slug: "tags".to_string(), items: vec![tag_fr] }; let taxonomies = vec![tags.clone(), tags_fr.clone()]; let static_fn = diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index c76696af..96ad924f 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -82,7 +82,8 @@ taxonomies = [] # languages = [] -# When set to "true", the Sass files in the `sass` directory are compiled. +# When set to "true", the Sass files in the `sass` directory in the site root are compiled. +# Sass files in theme directories are always compiled. compile_sass = false # A list of glob patterns specifying asset files to ignore when the content diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 9c2aa29d..83d2dd7b 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -26,6 +26,7 @@ use std::path::{Path, PathBuf}; use std::sync::mpsc::channel; use std::thread; use std::time::{Duration, Instant}; +use std::net::{SocketAddrV4, TcpListener}; use hyper::header; use hyper::service::{make_service_fn, service_fn}; @@ -38,6 +39,7 @@ use ws::{Message, Sender, WebSocket}; use errors::{Error as ZolaError, Result}; use globset::GlobSet; +use relative_path::{RelativePath, RelativePathBuf}; use site::sass::compile_sass; use site::{Site, SITE_CONTENT}; use utils::fs::copy_file; @@ -69,7 +71,12 @@ static NOT_FOUND_TEXT: &[u8] = b"Not Found"; const LIVE_RELOAD: &str = include_str!("livereload.js"); async fn handle_request(req: Request, root: PathBuf) -> Result> { - let path = req.uri().path().trim_end_matches('/').trim_start_matches('/'); + let mut path = RelativePathBuf::new(); + + for c in req.uri().path().split('/') { + path.push(c); + } + // livereload.js is served using the LIVE_RELOAD str, not a file if path == "livereload.js" { if req.method() == Method::GET { @@ -79,7 +86,7 @@ async fn handle_request(req: Request, root: PathBuf) -> Result, root: PathBuf) -> Result return Ok(method_not_allowed()), ResolveResult::NotFound | ResolveResult::UriNotMatched => { - let content_404 = SITE_CONTENT.read().unwrap().get("404.html").map(|x| x.clone()); + let not_found_path = RelativePath::new("404.html"); + let content_404 = SITE_CONTENT.read().unwrap().get(not_found_path).cloned(); return Ok(not_found(content_404)); } _ => (), @@ -175,6 +183,13 @@ fn create_new_site( let base_address = format!("{}:{}", base_url, interface_port); let address = format!("{}:{}", interface, interface_port); + + // Stop right there if we can't bind to the address + let bind_address: SocketAddrV4 = address.parse().unwrap(); + if (TcpListener::bind(&bind_address)).is_err() { + return Err(format!("Cannot start server on address {}.", address))?; + } + let base_url = if site.config.base_url.ends_with('/') { format!("http://{}/", base_address) } else { @@ -255,7 +270,7 @@ pub fn serve( if should_watch { watcher .watch(root_dir.join(entry), RecursiveMode::Recursive) - .map_err(|e| ZolaError::chain(format!("Can't watch `{}` for changes in folder `{}`. Do you have correct permissions?", entry, root_dir.display()), e))?; + .map_err(|e| ZolaError::chain(format!("Can't watch `{}` for changes in folder `{}`. Does it exist, and do you have correct permissions?", entry, root_dir.display()), e))?; watchers.push(entry.to_string()); } } @@ -319,10 +334,17 @@ pub fn serve( } }) .unwrap(); + let broadcaster = ws_server.broadcaster(); + + let ws_server = ws_server + .bind(&*ws_address) + .map_err(|_| format!("Cannot bind to address {} for the websocket server. Maybe the port is already in use?", &ws_address))?; + thread::spawn(move || { - ws_server.listen(&*ws_address).unwrap(); + ws_server.run().unwrap(); }); + Some(broadcaster) } else { println!("Watching in watch only mode, no web server will be started"); diff --git a/src/main.rs b/src/main.rs index 8e17d989..3eec3ba3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ fn main() { "." => env::current_dir().unwrap(), path => PathBuf::from(path) .canonicalize() - .expect(&format!("Cannot find root directory: {}", path)), + .unwrap_or_else(|_| panic!("Cannot find root directory: {}", path)), }; let config_file = match matches.value_of("config") { Some(path) => PathBuf::from(path), diff --git a/test_site/_dir_with_underscores/.gitkeep b/test_site/_dir_with_underscores/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test_site/config.toml b/test_site/config.toml index 9c09c598..3efcd691 100644 --- a/test_site/config.toml +++ b/test_site/config.toml @@ -4,16 +4,21 @@ highlight_code = true compile_sass = true generate_feed = true theme = "sample" -slugify_paths = true taxonomies = [ {name = "categories", feed = true}, + {name = "podcast_authors", feed = true}, ] extra_syntaxes = ["syntaxes"] ignored_content = ["*/ignored.md"] +[slugify] +paths = "on" +taxonomies = "on" +anchors = "on" + [link_checker] skip_prefixes = [ "http://[2001:db8::]/", diff --git a/test_site/content/rebuild/first.md b/test_site/content/rebuild/first.md index 708e286e..4e9c2268 100644 --- a/test_site/content/rebuild/first.md +++ b/test_site/content/rebuild/first.md @@ -5,6 +5,7 @@ date = 2017-01-01 [taxonomies] categories = ["a-category"] +podcast_authors = ["Some Person"] +++ # A title diff --git a/test_site/templates/index.html b/test_site/templates/index.html index 5a098951..9ed72df3 100644 --- a/test_site/templates/index.html +++ b/test_site/templates/index.html @@ -8,6 +8,8 @@ {% endfor %} + +

<<<

{% endblock content %} {% block script %} diff --git a/test_site/templates/podcast_authors/list.html b/test_site/templates/podcast_authors/list.html new file mode 100644 index 00000000..03e6b37f --- /dev/null +++ b/test_site/templates/podcast_authors/list.html @@ -0,0 +1,4 @@ +{% for term in terms %} + {{ term.name }} {{ term.slug }} {{ term.pages | length }} +{% endfor %} +Current path: {{ current_path }} \ No newline at end of file diff --git a/test_site/templates/podcast_authors/single.html b/test_site/templates/podcast_authors/single.html new file mode 100644 index 00000000..432ddac1 --- /dev/null +++ b/test_site/templates/podcast_authors/single.html @@ -0,0 +1,10 @@ +Category: {{ term.name }} + + +{% for page in term.pages %} + +{% endfor %} + +Current path: {{ current_path }} \ No newline at end of file