@@ -1,3 +1,8 @@ | |||
[[package]] | |||
name = "adler32" | |||
version = "1.0.3" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "aho-corasick" | |||
version = "0.6.4" | |||
@@ -183,6 +188,11 @@ dependencies = [ | |||
"cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "color_quant" | |||
version = "1.0.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "config" | |||
version = "0.1.0" | |||
@@ -204,6 +214,7 @@ dependencies = [ | |||
"errors 0.1.0", | |||
"front_matter 0.1.0", | |||
"globset 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"imageproc 0.1.0", | |||
"rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"rendering 0.1.0", | |||
"serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -254,6 +265,15 @@ dependencies = [ | |||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "deflate" | |||
version = "0.7.18" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "dtoa" | |||
version = "0.4.2" | |||
@@ -290,6 +310,14 @@ dependencies = [ | |||
"strum_macros 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "enum_primitive" | |||
version = "0.1.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "error-chain" | |||
version = "0.11.0" | |||
@@ -303,6 +331,7 @@ name = "errors" | |||
version = "0.1.0" | |||
dependencies = [ | |||
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"tera 0.11.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -401,6 +430,15 @@ name = "getopts" | |||
version = "0.2.17" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "gif" | |||
version = "0.9.2" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "glob" | |||
version = "0.2.11" | |||
@@ -500,6 +538,44 @@ dependencies = [ | |||
"unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "image" | |||
version = "0.18.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"gif 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"jpeg-decoder 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"png 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "imageproc" | |||
version = "0.1.0" | |||
dependencies = [ | |||
"errors 0.1.0", | |||
"image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"rayon 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"tera 0.11.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"twox-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"utils 0.1.0", | |||
] | |||
[[package]] | |||
name = "inflate" | |||
version = "0.3.4" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "inotify" | |||
version = "0.3.0" | |||
@@ -537,6 +613,15 @@ name = "itoa" | |||
version = "0.4.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "jpeg-decoder" | |||
version = "0.1.15" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "kernel32-sys" | |||
version = "0.2.2" | |||
@@ -587,6 +672,11 @@ dependencies = [ | |||
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "lzw" | |||
version = "0.10.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "mac" | |||
version = "0.1.1" | |||
@@ -810,6 +900,32 @@ dependencies = [ | |||
"num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "num-iter" | |||
version = "0.1.37" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "num-rational" | |||
version = "0.1.42" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "num-traits" | |||
version = "0.1.43" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "num-traits" | |||
version = "0.2.5" | |||
@@ -948,6 +1064,17 @@ dependencies = [ | |||
"typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "png" | |||
version = "0.11.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"deflate 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"inflate 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "precomputed-hash" | |||
version = "0.1.1" | |||
@@ -999,6 +1126,16 @@ dependencies = [ | |||
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "rand" | |||
version = "0.3.22" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "rand" | |||
version = "0.4.2" | |||
@@ -1009,6 +1146,15 @@ dependencies = [ | |||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "rayon" | |||
version = "0.9.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "rayon" | |||
version = "1.0.1" | |||
@@ -1175,6 +1321,11 @@ dependencies = [ | |||
"pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "scoped_threadpool" | |||
version = "0.1.9" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "scopeguard" | |||
version = "0.3.3" | |||
@@ -1250,6 +1401,7 @@ dependencies = [ | |||
"errors 0.1.0", | |||
"front_matter 0.1.0", | |||
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"imageproc 0.1.0", | |||
"pagination 0.1.0", | |||
"rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"sass-rs 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -1441,6 +1593,7 @@ dependencies = [ | |||
"config 0.1.0", | |||
"content 0.1.0", | |||
"errors 0.1.0", | |||
"imageproc 0.1.0", | |||
"lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"pulldown-cmark 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"taxonomies 0.1.0", | |||
@@ -1544,6 +1697,14 @@ name = "traitobject" | |||
version = "0.1.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "twox-hash" | |||
version = "1.1.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "typeable" | |||
version = "0.1.2" | |||
@@ -1750,6 +1911,7 @@ dependencies = [ | |||
] | |||
[metadata] | |||
"checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" | |||
"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" | |||
"checksum ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fd4c682378117e4186a492b2252b9537990e1617f44aed9788b9a1149de45477" | |||
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" | |||
@@ -1773,14 +1935,17 @@ dependencies = [ | |||
"checksum chrono 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6962c635d530328acc53ac6a955e83093fedc91c5809dfac1fa60fa470830a37" | |||
"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536" | |||
"checksum cmake 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "95470235c31c726d72bf2e1f421adc1e65b9d561bf5529612cbe1a72da1467b3" | |||
"checksum color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" | |||
"checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3" | |||
"checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150" | |||
"checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9" | |||
"checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e" | |||
"checksum deflate 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)" = "32c8120d981901a9970a3a1c97cf8b630e0fa8c3ca31e75b6fd6fd5f9f427b31" | |||
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" | |||
"checksum duct 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "166298c17c5b4fe5997b962c2f22e887c7c5adc44308eb9103ce5b66af45a423" | |||
"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" | |||
"checksum elasticlunr-rs 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4837d77a1e157489a3933b743fd774ae75074e0e390b2b7f071530048a0d87ee" | |||
"checksum enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" | |||
"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" | |||
"checksum filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "714653f3e34871534de23771ac7b26e999651a0a228f47beb324dfdf1dd4b10f" | |||
"checksum flate2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fac2277e84e5e858483756647a9d0aa8d9a2b7cba517fd84325a0aaa69a0909" | |||
@@ -1793,6 +1958,7 @@ dependencies = [ | |||
"checksum futf 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" | |||
"checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb" | |||
"checksum getopts 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "b900c08c1939860ce8b54dc6a89e26e00c04c380fd0e09796799bd7f12861e05" | |||
"checksum gif 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e2e41945ba23db3bf51b24756d73d81acb4f28d85c3dccc32c6fae904438c25f" | |||
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" | |||
"checksum globset 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "142754da2c9b3722affd909f9e27f2a6700a7a303f362971e0a74c652005a43d" | |||
"checksum html5ever 0.22.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b04478cf718862650a0bf66acaf8f2f8c906fbc703f35c916c1f4211b069a364" | |||
@@ -1800,10 +1966,13 @@ dependencies = [ | |||
"checksum humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" | |||
"checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2" | |||
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" | |||
"checksum image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "545f000e8aa4e569e93f49c446987133452e0091c2494ac3efd3606aa3d309f2" | |||
"checksum inflate 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4" | |||
"checksum inotify 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887fcc180136e77a85e6a6128579a719027b1bab9b1c38ea4444244fe262c20c" | |||
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" | |||
"checksum iron 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d8e17268922834707e1c29e8badbf9c712c9c43378e1b6a3388946baff10be2" | |||
"checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" | |||
"checksum jpeg-decoder 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "c8b7d43206b34b3f94ea9445174bda196e772049b9bddbc620c9d29b2d20110d" | |||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" | |||
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" | |||
"checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739" | |||
@@ -1812,6 +1981,7 @@ dependencies = [ | |||
"checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" | |||
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" | |||
"checksum log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6fddaa003a65722a7fb9e26b0ce95921fe4ba590542ced664d8ce2fa26f9f3ac" | |||
"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" | |||
"checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" | |||
"checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43" | |||
"checksum markup5ever 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfedc97d5a503e96816d10fedcd5b42f760b2e525ce2f7ec71f6a41780548475" | |||
@@ -1835,6 +2005,9 @@ dependencies = [ | |||
"checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" | |||
"checksum notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5c3812da3098f210a0bb440f9c008471a031aa4c1de07a264fdd75456c95a4eb" | |||
"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" | |||
"checksum num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "af3fdbbc3291a5464dc57b03860ec37ca6bf915ed6ee385e7c6c052c422b2124" | |||
"checksum num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" | |||
"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" | |||
"checksum num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "630de1ef5cc79d0cdd78b7e33b81f083cbfe90de0f4b2b2f07f905867c70e9fe" | |||
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" | |||
"checksum onig 3.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f5eeb268a4620c74ea5768c6d2ccd492d60a47a8754666b91a46bfc35cd4d1ba" | |||
@@ -1850,6 +2023,7 @@ dependencies = [ | |||
"checksum pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "110d5ee3593dbb73f56294327fe5668bcc997897097cbc76b51e7aed3f52452f" | |||
"checksum plist 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c61ac2afed2856590ae79d6f358a24b85ece246d2aa134741a66d589519b7503" | |||
"checksum plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" | |||
"checksum png 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925" | |||
"checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" | |||
"checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7" | |||
"checksum proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "effdb53b25cdad54f8f48843d67398f7ef2e14f12c1b4cb4effc549a6462a4d6" | |||
@@ -1857,7 +2031,9 @@ dependencies = [ | |||
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" | |||
"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" | |||
"checksum quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e44651a0dc4cdd99f71c83b561e221f714912d11af1a4dff0631f923d53af035" | |||
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" | |||
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" | |||
"checksum rayon 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed02d09394c94ffbdfdc755ad62a132e94c3224a8354e78a1200ced34df12edf" | |||
"checksum rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80e811e76f1dbf68abf87a759083d34600017fc4e10b6bd5ad84a700f9dba4b1" | |||
"checksum rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d24ad214285a7729b174ed6d3bcfcb80177807f959d95fafd5bfc5c4f201ac8" | |||
"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" | |||
@@ -1874,6 +2050,7 @@ dependencies = [ | |||
"checksum same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637" | |||
"checksum sass-rs 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90f8cf6e645aa843ffffcbdc1e8752b1f221dfa314c81895aeb229a77aea7e05" | |||
"checksum sass-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "bae88baa915f59c39820e544cfd9296d815a6b8efc3f276a78a0505866d2b4e9" | |||
"checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" | |||
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" | |||
"checksum sequence_trie 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "32157204e5c9d3c04007bd7e56e96e987635ce0e8e23c085b1e403861b76c351" | |||
"checksum serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)" = "e9a2d9a9ac5120e0f768801ca2b58ad6eec929dc9d1d616c162f208869c2ce95" | |||
@@ -1909,6 +2086,7 @@ dependencies = [ | |||
"checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" | |||
"checksum toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a0263c6c02c4db6c8f7681f9fd35e90de799ebd4cfdeab77a38f4ff6b3d8c0d9" | |||
"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" | |||
"checksum twox-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "475352206e7a290c5fccc27624a163e8d0d115f7bb60ca18a64fc9ce056d7435" | |||
"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" | |||
"checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" | |||
"checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" | |||
@@ -53,4 +53,5 @@ members = [ | |||
"components/templates", | |||
"components/utils", | |||
"components/search", | |||
"components/imageproc", | |||
] |
@@ -14,6 +14,7 @@ config = { path = "../config" } | |||
utils = { path = "../utils" } | |||
rendering = { path = "../rendering" } | |||
front_matter = { path = "../front_matter" } | |||
imageproc = { path = "../imageproc" } | |||
[dev-dependencies] | |||
tempfile = "3" | |||
@@ -7,6 +7,7 @@ extern crate errors; | |||
extern crate config; | |||
extern crate front_matter; | |||
extern crate rendering; | |||
extern crate imageproc; | |||
extern crate utils; | |||
#[cfg(test)] | |||
@@ -15,6 +15,7 @@ use utils::site::get_reading_analytics; | |||
use utils::templates::render_template; | |||
use front_matter::{PageFrontMatter, InsertAnchor, split_page_content}; | |||
use rendering::{RenderContext, Header, render_content}; | |||
use imageproc; | |||
use file_info::FileInfo; | |||
@@ -162,13 +163,16 @@ impl Page { | |||
/// We need access to all pages url to render links relative to content | |||
/// so that can't happen at the same time as parsing | |||
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> { | |||
let context = RenderContext::new( | |||
let mut context = RenderContext::new( | |||
tera, | |||
config, | |||
&self.permalink, | |||
permalinks, | |||
anchor_insert | |||
); | |||
context.teracontext.add("page", self); | |||
let res = render_content( | |||
&self.raw_content.replacen("<!-- more -->", "<a name=\"continue-reading\"></a>", 1), | |||
&context | |||
@@ -202,6 +206,23 @@ impl Page { | |||
render_template(&tpl_name, tera, &context, &config.theme) | |||
.chain_err(|| format!("Failed to render page '{}'", self.file.path.display())) | |||
} | |||
/// Creates two vectors of asset URLs. The first one contains all asset files, | |||
/// the second one that which appear to be an image file. | |||
fn serialize_assets(&self) -> (Vec<String>, Vec<String>) { | |||
let mut assets = vec![]; | |||
let mut images = vec![]; | |||
for asset in self.assets.iter() { | |||
if let Some(filename) = asset.file_name().and_then(|f| f.to_str()) { | |||
let url = self.path.clone() + filename; | |||
if imageproc::file_is_img(&asset) { | |||
images.push(url.clone()); | |||
} | |||
assets.push(url); | |||
} | |||
} | |||
(assets, images) | |||
} | |||
} | |||
impl Default for Page { | |||
@@ -246,6 +267,9 @@ impl ser::Serialize for Page { | |||
state.serialize_field("next", &self.next)?; | |||
state.serialize_field("toc", &self.toc)?; | |||
state.serialize_field("draft", &self.is_draft())?; | |||
let (assets, assets_imgs) = self.serialize_assets(); | |||
state.serialize_field("assets", &assets)?; | |||
state.serialize_field("assets_imgs", &assets_imgs)?; | |||
state.end() | |||
} | |||
} | |||
@@ -98,13 +98,16 @@ impl Section { | |||
/// We need access to all pages url to render links relative to content | |||
/// so that can't happen at the same time as parsing | |||
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> { | |||
let context = RenderContext::new( | |||
let mut context = RenderContext::new( | |||
tera, | |||
config, | |||
&self.permalink, | |||
permalinks, | |||
self.meta.insert_anchor_links, | |||
); | |||
context.teracontext.add("section", self); | |||
let res = render_content(&self.raw_content, &context) | |||
.chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?; | |||
self.content = res.0; | |||
@@ -7,3 +7,4 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
error-chain = "0.11" | |||
tera = "0.11" | |||
toml = "0.4" | |||
image = "0.18.0" |
@@ -4,6 +4,7 @@ | |||
extern crate error_chain; | |||
extern crate tera; | |||
extern crate toml; | |||
extern crate image; | |||
error_chain! { | |||
errors {} | |||
@@ -15,6 +16,7 @@ error_chain! { | |||
foreign_links { | |||
Io(::std::io::Error); | |||
Toml(toml::de::Error); | |||
Image(image::ImageError); | |||
} | |||
} | |||
@@ -0,0 +1,15 @@ | |||
[package] | |||
name = "imageproc" | |||
version = "0.1.0" | |||
authors = ["Vojtěch Král <vojtech@kral.hk>"] | |||
[dependencies] | |||
lazy_static = "1" | |||
regex = "0.2" | |||
tera = "0.11.0" | |||
image = "0.18.0" | |||
rayon = "0.9" | |||
twox-hash = "1.1" | |||
errors = { path = "../errors" } | |||
utils = { path = "../utils" } |
@@ -0,0 +1,327 @@ | |||
#[macro_use] | |||
extern crate lazy_static; | |||
extern crate regex; | |||
extern crate image; | |||
extern crate rayon; | |||
extern crate twox_hash; | |||
extern crate utils; | |||
extern crate errors; | |||
use std::path::{Path, PathBuf}; | |||
use std::hash::{Hash, Hasher}; | |||
use std::collections::HashMap; | |||
use std::collections::hash_map::Entry as HEntry; | |||
use std::fs::{self, File}; | |||
use regex::Regex; | |||
use image::{GenericImage, FilterType}; | |||
use image::jpeg::JPEGEncoder; | |||
use rayon::prelude::*; | |||
use twox_hash::XxHash; | |||
use utils::fs as ufs; | |||
use errors::{Result, ResultExt}; | |||
static RESIZED_SUBDIR: &'static str = "_resized_images"; | |||
lazy_static!{ | |||
pub static ref RESIZED_FILENAME: Regex = Regex::new(r#"([0-9a-f]{16})([0-9a-f]{2})[.]jpg"#).unwrap(); | |||
} | |||
/// Describes the precise kind of a resize operation | |||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | |||
pub enum ResizeOp { | |||
/// A simple scale operation that doesn't take aspect ratio into account | |||
Scale(u32, u32), | |||
/// Scales the image to a specified width with height computed such that aspect ratio is preserved | |||
FitWidth(u32), | |||
/// Scales the image to a specified height with width computed such that aspect ratio is preserved | |||
FitHeight(u32), | |||
/// Scales the image such that it fits within the specified width and height preserving aspect ratio. | |||
/// Either dimension may end up being smaller, but never larger than specified. | |||
Fit(u32, u32), | |||
/// Scales the image such that it fills the specified width and height. Output will always have the exact dimensions specified. | |||
/// The part of the image that doesn't fit in the thumbnail due to differing aspect ratio will be cropped away, if any. | |||
Fill(u32, u32), | |||
} | |||
impl ResizeOp { | |||
pub fn from_args(op: &str, width: Option<u32>, height: Option<u32>) -> Result<ResizeOp> { | |||
use ResizeOp::*; | |||
// Validate args: | |||
match op { | |||
"fitwidth" => if width.is_none() { return Err(format!("op=fitwidth requires a `width` argument").into()) }, | |||
"fitheight" => if height.is_none() { return Err(format!("op=fitwidth requires a `height` argument").into()) }, | |||
"scale" | "fit" | "fill" => if width.is_none() || height.is_none() { | |||
return Err(format!("op={} requires a `width` and `height` argument", op).into()) | |||
}, | |||
_ => return Err(format!("Invalid image resize operation: {}", op).into()) | |||
}; | |||
Ok(match op { | |||
"scale" => Scale(width.unwrap(), height.unwrap()), | |||
"fitwidth" => FitWidth(width.unwrap()), | |||
"fitheight" => FitHeight(height.unwrap()), | |||
"fit" => Fit(width.unwrap(), height.unwrap()), | |||
"fill" => Fill(width.unwrap(), height.unwrap()), | |||
_ => unreachable!(), | |||
}) | |||
} | |||
pub fn width(self) -> Option<u32> { | |||
use ResizeOp::*; | |||
match self { | |||
Scale(w, _) => Some(w), | |||
FitWidth(w) => Some(w), | |||
FitHeight(_) => None, | |||
Fit(w, _) => Some(w), | |||
Fill(w, _) => Some(w), | |||
} | |||
} | |||
pub fn height(self) -> Option<u32> { | |||
use ResizeOp::*; | |||
match self { | |||
Scale(_, h) => Some(h), | |||
FitWidth(_) => None, | |||
FitHeight(h) => Some(h), | |||
Fit(_, h) => Some(h), | |||
Fill(_, h) => Some(h), | |||
} | |||
} | |||
} | |||
impl From<ResizeOp> for u8 { | |||
fn from(op: ResizeOp) -> u8 { | |||
use ResizeOp::*; | |||
match op { | |||
Scale(_, _) => 1, | |||
FitWidth(_) => 2, | |||
FitHeight(_) => 3, | |||
Fit(_, _) => 4, | |||
Fill(_, _) => 5, | |||
} | |||
} | |||
} | |||
impl Hash for ResizeOp { | |||
fn hash<H: Hasher>(&self, hasher: &mut H) { | |||
hasher.write_u8(u8::from(*self)); | |||
if let Some(w) = self.width() { hasher.write_u32(w); } | |||
if let Some(h) = self.height() { hasher.write_u32(h); } | |||
} | |||
} | |||
/// Holds all data needed to perform a resize operation | |||
#[derive(Debug, PartialEq, Eq)] | |||
pub struct ImageOp { | |||
source: String, | |||
op: ResizeOp, | |||
quality: u8, | |||
hash: u64, | |||
collision: Option<u32>, | |||
} | |||
impl ImageOp { | |||
pub fn new(source: String, op: ResizeOp, quality: u8) -> ImageOp { | |||
let mut hasher = XxHash::with_seed(0); | |||
hasher.write(source.as_ref()); | |||
op.hash(&mut hasher); | |||
hasher.write_u8(quality); | |||
let hash = hasher.finish(); | |||
ImageOp { source, op, quality, hash, collision: None } | |||
} | |||
pub fn from_args(source: String, op: &str, width: Option<u32>, height: Option<u32>, quality: u8) -> Result<ImageOp> { | |||
let op = ResizeOp::from_args(op, width, height)?; | |||
Ok(Self::new(source, op, quality)) | |||
} | |||
fn num_colli(&self) -> u32 { self.collision.unwrap_or(0) } | |||
fn perform(&self, content_path: &Path, target_path: &Path) -> Result<()> { | |||
use ResizeOp::*; | |||
let src_path = content_path.join(&self.source); | |||
if !ufs::file_stale(&src_path, target_path) { | |||
return Ok(()) | |||
} | |||
let mut img = image::open(&src_path)?; | |||
let (img_w, img_h) = img.dimensions(); | |||
const RESIZE_FILTER: FilterType = FilterType::Gaussian; | |||
const RATIO_EPSILLION: f32 = 0.1; | |||
let img = match self.op { | |||
Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER), | |||
FitWidth(w) => img.resize(w, u32::max_value(), RESIZE_FILTER), | |||
FitHeight(h) => img.resize(u32::max_value(), h, RESIZE_FILTER), | |||
Fit(w, h) => img.resize(w, h, RESIZE_FILTER), | |||
Fill(w, h) => { | |||
let fw = img_w as f32 / w as f32; | |||
let fh = img_h as f32 / h as f32; | |||
if (fw - fh).abs() <= RATIO_EPSILLION { | |||
// The aspect is similar enough that there's not much point in cropping | |||
img.resize_exact(w, h, RESIZE_FILTER) | |||
} else { | |||
// We perform the fill such that a crop is performed first and then resize_exact can be used, | |||
// which should be cheaper than resizing and then cropping (smaller number of pixels to resize). | |||
let (crop_w, crop_h) = match fw < fh { | |||
true => (img_w, (fw * h as f32).round() as u32), | |||
false => ((fh * w as f32).round() as u32, img_h), | |||
}; | |||
let (off_w, off_h) = match fw < fh { | |||
true => (0, (img_h - crop_h) / 2), | |||
false => ((img_w - crop_w) / 2, 0), | |||
}; | |||
img.crop(off_w, off_h, crop_w, crop_h).resize_exact(w, h, RESIZE_FILTER) | |||
} | |||
}, | |||
}; | |||
let mut f = File::create(target_path)?; | |||
let mut enc = JPEGEncoder::new_with_quality(&mut f, self.quality); | |||
let (img_w, img_h) = img.dimensions(); | |||
enc.encode(&img.raw_pixels(), img_w, img_h, img.color())?; | |||
Ok(()) | |||
} | |||
} | |||
/// A strcture into which image operations can be enqueued and then performed. | |||
/// All output is written in a subdirectory in `static_path`, | |||
/// taking care of file stale status based on timestamps and possible hash collisions. | |||
#[derive(Debug)] | |||
pub struct Processor { | |||
content_path: PathBuf, | |||
resized_path: PathBuf, | |||
resized_url: String, | |||
img_ops: HashMap<u64, ImageOp>, | |||
// Hash collisions go here: | |||
img_ops_colls: Vec<ImageOp>, | |||
} | |||
impl Processor { | |||
pub fn new(content_path: PathBuf, static_path: &Path, base_url: &str) -> Processor { | |||
Processor { | |||
content_path, | |||
resized_path: static_path.join(RESIZED_SUBDIR), | |||
resized_url: Self::resized_url(base_url), | |||
img_ops: HashMap::new(), | |||
img_ops_colls: Vec::new(), | |||
} | |||
} | |||
fn resized_url(base_url: &str) -> String { | |||
match base_url.ends_with('/') { | |||
true => format!("{}{}", base_url, RESIZED_SUBDIR), | |||
false => format!("{}/{}", base_url, RESIZED_SUBDIR), | |||
} | |||
} | |||
pub fn set_base_url(&mut self, base_url: &str) { | |||
self.resized_url = Self::resized_url(base_url); | |||
} | |||
pub fn source_exists(&self, source: &str) -> bool { | |||
self.content_path.join(source).exists() | |||
} | |||
pub fn num_img_ops(&self) -> usize { | |||
self.img_ops.len() + self.img_ops_colls.len() | |||
} | |||
fn insert_with_colls(&mut self, mut img_op: ImageOp) -> u32 { | |||
match self.img_ops.entry(img_op.hash) { | |||
HEntry::Occupied(entry) => if *entry.get() == img_op { return 0; }, | |||
HEntry::Vacant(entry) => { | |||
entry.insert(img_op); | |||
return 0; | |||
}, | |||
} | |||
// If we get here, that means a hash collision. | |||
let mut num = 1; | |||
for op in self.img_ops_colls.iter().filter(|op| op.hash == img_op.hash) { | |||
if *op == img_op { | |||
return num; | |||
} else { | |||
num += 1; | |||
} | |||
} | |||
if num == 1 { | |||
self.img_ops.get_mut(&img_op.hash).unwrap().collision = Some(0); | |||
} | |||
img_op.collision = Some(num); | |||
self.img_ops_colls.push(img_op); | |||
num | |||
} | |||
fn op_filename(hash: u64, colli_num: u32) -> String { | |||
// Please keep this in sync with RESIZED_FILENAME | |||
assert!(colli_num < 256, "Unexpectedly large number of collisions: {}", colli_num); | |||
format!("{:016x}{:02x}.jpg", hash, colli_num) | |||
} | |||
fn op_url(&self, hash: u64, colli_num: u32) -> String { | |||
format!("{}/{}", &self.resized_url, Self::op_filename(hash, colli_num)) | |||
} | |||
pub fn insert(&mut self, img_op: ImageOp) -> String { | |||
let hash = img_op.hash; | |||
let num_colli = self.insert_with_colls(img_op); | |||
self.op_url(hash, num_colli) | |||
} | |||
pub fn prune(&self) -> Result<()> { | |||
ufs::ensure_directory_exists(&self.resized_path)?; | |||
let entries = fs::read_dir(&self.resized_path)?; | |||
for entry in entries { | |||
let entry_path = entry?.path(); | |||
if entry_path.is_file() { | |||
let filename = entry_path.file_name().unwrap().to_string_lossy(); | |||
if let Some(capts) = RESIZED_FILENAME.captures(filename.as_ref()) { | |||
let hash = u64::from_str_radix(capts.get(1).unwrap().as_str(), 16).unwrap(); | |||
let num_colli = u32::from_str_radix(capts.get(2).unwrap().as_str(), 16).unwrap(); | |||
if num_colli > 0 || !self.img_ops.contains_key(&hash) { | |||
fs::remove_file(&entry_path)?; | |||
} | |||
} | |||
} | |||
} | |||
Ok(()) | |||
} | |||
pub fn do_process(&mut self) -> Result<()> { | |||
self.img_ops.par_iter().map(|(hash, op)| { | |||
let target = self.resized_path.join(Self::op_filename(*hash, op.num_colli())); | |||
op.perform(&self.content_path, &target) | |||
.chain_err(|| format!("Failed to process image: {}", op.source)) | |||
}) | |||
.fold(|| Ok(()), Result::and) | |||
.reduce(|| Ok(()), Result::and) | |||
} | |||
} | |||
/// Looks at file's extension and returns whether it's a supported image format | |||
pub fn file_is_img<P: AsRef<Path>>(p: P) -> bool { | |||
p.as_ref().extension().and_then(|s| s.to_str()).map(|ext| { | |||
match ext.to_lowercase().as_str() { | |||
"jpg" | "jpeg" => true, | |||
"png" => true, | |||
"gif" => true, | |||
"bmp" => true, | |||
_ => false, | |||
} | |||
}).unwrap_or(false) | |||
} |
@@ -1,6 +1,6 @@ | |||
use std::collections::HashMap; | |||
use tera::Tera; | |||
use tera::{Tera, Context}; | |||
use front_matter::InsertAnchor; | |||
use config::Config; | |||
@@ -10,6 +10,7 @@ use config::Config; | |||
pub struct RenderContext<'a> { | |||
pub tera: &'a Tera, | |||
pub config: &'a Config, | |||
pub teracontext: Context, | |||
pub current_page_permalink: &'a str, | |||
pub permalinks: &'a HashMap<String, String>, | |||
pub insert_anchor: InsertAnchor, | |||
@@ -23,8 +24,11 @@ impl<'a> RenderContext<'a> { | |||
permalinks: &'a HashMap<String, String>, | |||
insert_anchor: InsertAnchor, | |||
) -> RenderContext<'a> { | |||
let mut teracontext = Context::new(); | |||
teracontext.insert("config", config); | |||
RenderContext { | |||
tera, | |||
teracontext, | |||
current_page_permalink, | |||
permalinks, | |||
insert_anchor, | |||
@@ -34,7 +34,7 @@ pub use context::RenderContext; | |||
pub fn render_content(content: &str, context: &RenderContext) -> Result<(String, Vec<Header>)> { | |||
// Don't do anything if there is nothing like a shortcode in the content | |||
if content.contains("{{") || content.contains("{%") { | |||
let rendered = render_shortcodes(content, context.tera, context.config)?; | |||
let rendered = render_shortcodes(content, context)?; | |||
return markdown_to_html(&rendered, context); | |||
} | |||
@@ -1,9 +1,9 @@ | |||
use pest::Parser; | |||
use pest::iterators::Pair; | |||
use tera::{Tera, Map, Context, Value, to_value}; | |||
use tera::{Map, Context, Value, to_value}; | |||
use errors::{Result, ResultExt}; | |||
use config::Config; | |||
use ::context::RenderContext; | |||
// This include forces recompiling this source file if the grammar file changes. | |||
// Uncomment it when doing changes to the .pest file | |||
@@ -84,20 +84,20 @@ fn parse_shortcode_call(pair: Pair<Rule>) -> (String, Map<String, Value>) { | |||
} | |||
fn render_shortcode(name: String, args: Map<String, Value>, tera: &Tera, config: &Config, body: Option<&str>) -> Result<String> { | |||
let mut context = Context::new(); | |||
fn render_shortcode(name: String, args: Map<String, Value>, context: &RenderContext, body: Option<&str>) -> Result<String> { | |||
let mut teracontext = Context::new(); | |||
for (key, value) in args.iter() { | |||
context.insert(key, value); | |||
teracontext.insert(key, value); | |||
} | |||
if let Some(ref b) = body { | |||
// Trimming right to avoid most shortcodes with bodies ending up with a HTML new line | |||
context.insert("body", b.trim_right()); | |||
teracontext.insert("body", b.trim_right()); | |||
} | |||
context.insert("config", config); | |||
teracontext.extend(context.teracontext.clone()); | |||
let tpl_name = format!("shortcodes/{}.html", name); | |||
let res = tera | |||
.render(&tpl_name, &context) | |||
let res = context.tera | |||
.render(&tpl_name, &teracontext) | |||
.chain_err(|| format!("Failed to render {} shortcode", name))?; | |||
// We trim left every single line of a shortcode to avoid the accidental | |||
@@ -105,7 +105,7 @@ fn render_shortcode(name: String, args: Map<String, Value>, tera: &Tera, config: | |||
Ok(res.lines().map(|s| s.trim_left()).collect()) | |||
} | |||
pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result<String> { | |||
pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<String> { | |||
let mut res = String::with_capacity(content.len()); | |||
let mut pairs = match ContentParser::parse(Rule::page, content) { | |||
@@ -138,7 +138,7 @@ pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result< | |||
Rule::text | Rule::text_in_ignored_body_sc | Rule::text_in_body_sc => res.push_str(p.into_span().as_str()), | |||
Rule::inline_shortcode => { | |||
let (name, args) = parse_shortcode_call(p); | |||
res.push_str(&render_shortcode(name, args, tera, config, None)?); | |||
res.push_str(&render_shortcode(name, args, context, None)?); | |||
}, | |||
Rule::shortcode_with_body => { | |||
let mut inner = p.into_inner(); | |||
@@ -146,7 +146,7 @@ pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result< | |||
// we don't care about the closing tag | |||
let (name, args) = parse_shortcode_call(inner.next().unwrap()); | |||
let body = inner.next().unwrap().into_span().as_str(); | |||
res.push_str(&render_shortcode(name, args, tera, config, Some(body))?); | |||
res.push_str(&render_shortcode(name, args, context, Some(body))?); | |||
}, | |||
Rule::ignored_inline_shortcode => { | |||
res.push_str( | |||
@@ -179,6 +179,10 @@ pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result< | |||
#[cfg(test)] | |||
mod tests { | |||
use std::collections::HashMap; | |||
use tera::Tera; | |||
use config::Config; | |||
use front_matter::InsertAnchor; | |||
use super::*; | |||
macro_rules! assert_lex_rule { | |||
@@ -195,6 +199,13 @@ mod tests { | |||
}; | |||
} | |||
fn render_shortcodes(code: &str, tera: &Tera) -> String { | |||
let config = Config::default(); | |||
let permalinks = HashMap::new(); | |||
let context = RenderContext::new(&tera, &config, "", &permalinks, InsertAnchor::None); | |||
super::render_shortcodes(code, &context).unwrap() | |||
} | |||
#[test] | |||
fn lex_text() { | |||
let inputs = vec!["Hello world", "HEllo \n world", "Hello 1 2 true false 'hey'"]; | |||
@@ -281,26 +292,22 @@ mod tests { | |||
#[test] | |||
fn does_nothing_with_no_shortcodes() { | |||
let res = render_shortcodes("Hello World", &Tera::default(), &Config::default()); | |||
assert_eq!(res.unwrap(), "Hello World"); | |||
let res = render_shortcodes("Hello World", &Tera::default()); | |||
assert_eq!(res, "Hello World"); | |||
} | |||
#[test] | |||
fn can_unignore_inline_shortcode() { | |||
let res = render_shortcodes( | |||
"Hello World {{/* youtube() */}}", | |||
&Tera::default(), | |||
&Config::default(), | |||
); | |||
assert_eq!(res.unwrap(), "Hello World {{ youtube() }}"); | |||
let res = render_shortcodes("Hello World {{/* youtube() */}}", &Tera::default()); | |||
assert_eq!(res, "Hello World {{ youtube() }}"); | |||
} | |||
#[test] | |||
fn can_unignore_shortcode_with_body() { | |||
let res = render_shortcodes(r#" | |||
Hello World | |||
{%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#, &Tera::default(), &Config::default()); | |||
assert_eq!(res.unwrap(), "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}"); | |||
{%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#, &Tera::default()); | |||
assert_eq!(res, "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}"); | |||
} | |||
#[test] | |||
@@ -343,7 +350,7 @@ Hello World | |||
fn can_render_inline_shortcodes() { | |||
let mut tera = Tera::default(); | |||
tera.add_raw_template("shortcodes/youtube.html", "Hello {{id}}").unwrap(); | |||
let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera, &Config::default()).unwrap(); | |||
let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera); | |||
assert_eq!(res, "Inline Hello 1."); | |||
} | |||
@@ -351,7 +358,7 @@ Hello World | |||
fn can_render_shortcodes_with_body() { | |||
let mut tera = Tera::default(); | |||
tera.add_raw_template("shortcodes/youtube.html", "{{body}}").unwrap(); | |||
let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera, &Config::default()).unwrap(); | |||
let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera); | |||
assert_eq!(res, "Body\n Hey!"); | |||
} | |||
} |
@@ -20,6 +20,7 @@ pagination = { path = "../pagination" } | |||
taxonomies = { path = "../taxonomies" } | |||
content = { path = "../content" } | |||
search = { path = "../search" } | |||
imageproc = { path = "../imageproc" } | |||
[dev-dependencies] | |||
tempfile = "3" |
@@ -16,6 +16,7 @@ extern crate pagination; | |||
extern crate taxonomies; | |||
extern crate content; | |||
extern crate search; | |||
extern crate imageproc; | |||
#[cfg(test)] | |||
extern crate tempfile; | |||
@@ -24,6 +25,7 @@ use std::collections::HashMap; | |||
use std::fs::{create_dir_all, remove_dir_all, copy}; | |||
use std::mem; | |||
use std::path::{Path, PathBuf}; | |||
use std::sync::{Arc, Mutex}; | |||
use glob::glob; | |||
use tera::{Tera, Context}; | |||
@@ -66,9 +68,11 @@ pub struct Site { | |||
pub pages: HashMap<PathBuf, Page>, | |||
pub sections: HashMap<PathBuf, Section>, | |||
pub tera: Tera, | |||
imageproc: Arc<Mutex<imageproc::Processor>>, | |||
// the live reload port to be used if there is one | |||
pub live_reload: Option<u16>, | |||
pub output_path: PathBuf, | |||
content_path: PathBuf, | |||
pub static_path: PathBuf, | |||
pub tags: Option<Taxonomy>, | |||
pub categories: Option<Taxonomy>, | |||
@@ -109,15 +113,21 @@ impl Site { | |||
// the `extend` above already does it but hey | |||
tera.build_inheritance_chains()?; | |||
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 site = Site { | |||
base_path: path.to_path_buf(), | |||
config, | |||
tera, | |||
pages: HashMap::new(), | |||
sections: HashMap::new(), | |||
imageproc: Arc::new(Mutex::new(imageproc)), | |||
live_reload: None, | |||
output_path: path.join("public"), | |||
static_path: path.join("static"), | |||
content_path, | |||
static_path, | |||
tags: None, | |||
categories: None, | |||
permalinks: HashMap::new(), | |||
@@ -128,7 +138,7 @@ impl Site { | |||
/// The index section is ALWAYS at that path | |||
pub fn index_section_path(&self) -> PathBuf { | |||
self.base_path.join("content").join("_index.md") | |||
self.content_path.join("_index.md") | |||
} | |||
pub fn enable_live_reload(&mut self) { | |||
@@ -153,6 +163,12 @@ impl Site { | |||
orphans | |||
} | |||
pub fn set_base_url(&mut self, base_url: String) { | |||
let mut imageproc = self.imageproc.lock().unwrap(); | |||
imageproc.set_base_url(&base_url); | |||
self.config.base_url = base_url; | |||
} | |||
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) { | |||
self.output_path = path.as_ref().to_path_buf(); | |||
} | |||
@@ -217,7 +233,7 @@ impl Site { | |||
if !self.sections.contains_key(&index_path) { | |||
let mut index_section = Section::default(); | |||
index_section.permalink = self.config.make_permalink(""); | |||
index_section.file.parent = self.base_path.join("content"); | |||
index_section.file.parent = self.content_path.clone(); | |||
index_section.file.relative = "_index.md".to_string(); | |||
self.sections.insert(index_path, index_section); | |||
} | |||
@@ -229,10 +245,10 @@ impl Site { | |||
self.add_page(p, false)?; | |||
} | |||
self.register_early_global_fns(); | |||
self.render_markdown()?; | |||
self.populate_sections(); | |||
self.populate_tags_and_categories(); | |||
self.register_tera_global_fns(); | |||
Ok(()) | |||
@@ -270,6 +286,16 @@ impl Site { | |||
Ok(()) | |||
} | |||
/// Adds global fns that are to be available to shortcodes while rendering markdown | |||
pub fn register_early_global_fns(&mut self) { | |||
self.tera.register_global_function( | |||
"get_url", global_fns::make_get_url(self.permalinks.clone(), self.config.clone()) | |||
); | |||
self.tera.register_global_function( | |||
"resize_image", global_fns::make_resize_image(self.imageproc.clone()) | |||
); | |||
} | |||
pub fn register_tera_global_fns(&mut self) { | |||
self.tera.register_global_function("trans", global_fns::make_trans(self.config.clone())); | |||
self.tera.register_global_function("get_page", global_fns::make_get_page(&self.pages)); | |||
@@ -278,10 +304,6 @@ impl Site { | |||
"get_taxonomy_url", | |||
global_fns::make_get_taxonomy_url(self.tags.clone(), self.categories.clone()) | |||
); | |||
self.tera.register_global_function( | |||
"get_url", | |||
global_fns::make_get_url(self.permalinks.clone(), self.config.clone()) | |||
); | |||
} | |||
/// Add a page to the site | |||
@@ -441,6 +463,17 @@ impl Site { | |||
Ok(()) | |||
} | |||
pub fn num_img_ops(&self) -> usize { | |||
let imageproc = self.imageproc.lock().unwrap(); | |||
imageproc.num_img_ops() | |||
} | |||
pub fn process_images(&self) -> Result<()> { | |||
let mut imageproc = self.imageproc.lock().unwrap(); | |||
imageproc.prune()?; | |||
imageproc.do_process() | |||
} | |||
/// Deletes the `public` directory if it exists | |||
pub fn clean(&self) -> Result<()> { | |||
if self.output_path.exists() { | |||
@@ -510,6 +543,7 @@ impl Site { | |||
self.compile_sass(&self.base_path)?; | |||
} | |||
self.process_images()?; | |||
self.copy_static_directories()?; | |||
if self.config.build_search_index { | |||
@@ -820,7 +854,7 @@ impl Site { | |||
/// Used only on reload | |||
pub fn render_index(&self) -> Result<()> { | |||
self.render_section( | |||
&self.sections[&self.base_path.join("content").join("_index.md")], | |||
&self.sections[&self.content_path.join("_index.md")], | |||
false | |||
) | |||
} | |||
@@ -14,3 +14,4 @@ utils = { path = "../utils" } | |||
content = { path = "../content" } | |||
config = { path = "../config" } | |||
taxonomies = { path = "../taxonomies" } | |||
imageproc = { path = "../imageproc" } |
@@ -1,5 +1,6 @@ | |||
use std::collections::HashMap; | |||
use std::path::{PathBuf}; | |||
use std::path::PathBuf; | |||
use std::sync::{Arc, Mutex}; | |||
use tera::{GlobalFn, Value, from_value, to_value, Result}; | |||
@@ -7,29 +8,41 @@ use content::{Page, Section}; | |||
use config::Config; | |||
use utils::site::resolve_internal_link; | |||
use taxonomies::Taxonomy; | |||
use imageproc; | |||
macro_rules! required_string_arg { | |||
($e: expr, $err: expr) => { | |||
macro_rules! required_arg { | |||
($ty: ty, $e: expr, $err: expr) => { | |||
match $e { | |||
Some(v) => match from_value::<String>(v.clone()) { | |||
Some(v) => match from_value::<$ty>(v.clone()) { | |||
Ok(u) => u, | |||
Err(_) => return Err($err.into()) | |||
}, | |||
None => return Err($err.into()) | |||
}; | |||
} | |||
}; | |||
} | |||
macro_rules! optional_arg { | |||
($ty: ty, $e: expr, $err: expr) => { | |||
match $e { | |||
Some(v) => match from_value::<$ty>(v.clone()) { | |||
Ok(u) => Some(u), | |||
Err(_) => return Err($err.into()) | |||
}, | |||
None => None | |||
} | |||
}; | |||
} | |||
pub fn make_trans(config: Config) -> GlobalFn { | |||
let translations_config = config.translations; | |||
let default_lang = to_value(config.default_language).unwrap(); | |||
let default_lang = config.default_language.clone(); | |||
Box::new(move |args| -> Result<Value> { | |||
let key = required_string_arg!(args.get("key"), "`trans` requires a `key` argument."); | |||
let lang_arg = args.get("lang").unwrap_or(&default_lang).clone(); | |||
let lang = from_value::<String>(lang_arg).unwrap(); | |||
let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument."); | |||
let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.").unwrap_or(default_lang.clone()); | |||
let translations = &translations_config[lang.as_str()]; | |||
Ok(to_value(&translations[key.as_str()]).unwrap()) | |||
}) | |||
@@ -43,7 +56,7 @@ pub fn make_get_page(all_pages: &HashMap<PathBuf, Page>) -> GlobalFn { | |||
} | |||
Box::new(move |args| -> Result<Value> { | |||
let path = required_string_arg!(args.get("path"), "`get_page` requires a `path` argument with a string value"); | |||
let path = required_arg!(String, args.get("path"), "`get_page` requires a `path` argument with a string value"); | |||
match pages.get(&path) { | |||
Some(p) => Ok(to_value(p).unwrap()), | |||
None => Err(format!("Page `{}` not found.", path).into()) | |||
@@ -61,7 +74,7 @@ pub fn make_get_section(all_sections: &HashMap<PathBuf, Section>) -> GlobalFn { | |||
} | |||
Box::new(move |args| -> Result<Value> { | |||
let path = required_string_arg!(args.get("path"), "`get_section` requires a `path` argument with a string value"); | |||
let path = required_arg!(String, args.get("path"), "`get_section` requires a `path` argument with a string value"); | |||
//println!("Found {:#?}", sections.get(&path).unwrap().pages[0]); | |||
match sections.get(&path) { | |||
Some(p) => Ok(to_value(p).unwrap()), | |||
@@ -84,7 +97,7 @@ pub fn make_get_url(permalinks: HashMap<String, String>, config: Config) -> Glob | |||
from_value::<bool>(c.clone()).unwrap_or(true) | |||
}); | |||
let path = required_string_arg!(args.get("path"), "`get_url` requires a `path` argument with a string value"); | |||
let path = required_arg!(String, args.get("path"), "`get_url` requires a `path` argument with a string value"); | |||
if path.starts_with("./") { | |||
match resolve_internal_link(&path, &permalinks) { | |||
Ok(url) => Ok(to_value(url).unwrap()), | |||
@@ -107,8 +120,8 @@ pub fn make_get_url(permalinks: HashMap<String, String>, config: Config) -> Glob | |||
pub fn make_get_taxonomy_url(tags: Option<Taxonomy>, categories: Option<Taxonomy>) -> GlobalFn { | |||
Box::new(move |args| -> Result<Value> { | |||
let kind = required_string_arg!(args.get("kind"), "`get_taxonomy_url` requires a `kind` argument with a string value"); | |||
let name = required_string_arg!(args.get("name"), "`get_taxonomy_url` requires a `name` argument with a string value"); | |||
let kind = required_arg!(String, args.get("kind"), "`get_taxonomy_url` requires a `kind` argument with a string value"); | |||
let name = required_arg!(String, args.get("name"), "`get_taxonomy_url` requires a `name` argument with a string value"); | |||
let container = match kind.as_ref() { | |||
"tag" => &tags, | |||
"category" => &categories, | |||
@@ -128,6 +141,33 @@ pub fn make_get_taxonomy_url(tags: Option<Taxonomy>, categories: Option<Taxonomy | |||
}) | |||
} | |||
pub fn make_resize_image(imageproc: Arc<Mutex<imageproc::Processor>>) -> GlobalFn { | |||
static DEFAULT_OP: &'static str = "fill"; | |||
const DEFAULT_Q: u8 = 75; | |||
Box::new(move |args| -> Result<Value> { | |||
let path = required_arg!(String, args.get("path"), "`resize_image` requires a `path` argument with a string value"); | |||
let width = optional_arg!(u32, args.get("width"), "`resize_image`: `width` must be a non-negative integer"); | |||
let height = optional_arg!(u32, args.get("height"), "`resize_image`: `height` must be a non-negative integer"); | |||
let op = optional_arg!(String, args.get("op"), "`resize_image`: `op` must be a string").unwrap_or(DEFAULT_OP.to_owned()); | |||
let quality = optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number").unwrap_or(DEFAULT_Q); | |||
if quality == 0 || quality > 100 { | |||
return Err("`resize_image`: `quality` must be in range 1-100".to_string().into()); | |||
} | |||
let mut imageproc = imageproc.lock().unwrap(); | |||
if !imageproc.source_exists(&path) { | |||
return Err(format!("`resize_image`: Cannot find path: {}", path).into()); | |||
} | |||
let imageop = imageproc::ImageOp::from_args(path.clone(), &op, width, height, quality).map_err(|e| format!("`resize_image`: {}", e))?; | |||
let url = imageproc.insert(imageop); | |||
to_value(url).map_err(|err| err.into()) | |||
}) | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use super::{make_get_url, make_get_taxonomy_url, make_trans}; | |||
@@ -11,6 +11,7 @@ extern crate utils; | |||
extern crate content; | |||
extern crate config; | |||
extern crate taxonomies; | |||
extern crate imageproc; | |||
pub mod filters; | |||
pub mod global_fns; | |||
@@ -93,6 +93,32 @@ pub fn copy_directory(src: &PathBuf, dest: &PathBuf) -> Result<()> { | |||
Ok(()) | |||
} | |||
/// Compares source and target files' timestamps and returns true if the source file | |||
/// has been created _or_ updated after the target file has | |||
pub fn file_stale<PS, PT>(p_source: PS, p_target: PT) -> bool where PS: AsRef<Path>, PT: AsRef<Path> { | |||
let p_source = p_source.as_ref(); | |||
let p_target = p_target.as_ref(); | |||
if ! p_target.exists() { | |||
return true; | |||
} | |||
let get_time = |path: &Path| path.metadata().ok().and_then(|meta| { | |||
Some(match (meta.created().ok(), meta.modified().ok()) { | |||
(Some(tc), Some(tm)) => tc.max(tm), | |||
(Some(tc), None) => tc, | |||
(None, Some(tm)) => tm, | |||
(None, None) => return None, | |||
}) | |||
}); | |||
let time_source = get_time(p_source); | |||
let time_target = get_time(p_target); | |||
time_source.and_then(|ts| time_target.map(|tt| ts > tt)).unwrap_or(true) | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use std::fs::File; | |||
@@ -0,0 +1,8 @@ | |||
+++ | |||
title = "Image Resizing" | |||
weight = 120 | |||
+++ | |||
TODO: talk about resize_image | |||
{{ gallery() }} |
@@ -37,6 +37,7 @@ previous: Page?; | |||
next: Page?; | |||
// See the Table of contents section below for more details | |||
toc: Array<Header>; | |||
// TODO: add assets & assets_imgs (also draft is missing?) | |||
``` | |||
## Section variables | |||
@@ -0,0 +1,6 @@ | |||
{% for img in page.assets_imgs %} | |||
<a href="{{ config.base_url }}/{{ img }}"> | |||
<img src="{{ resize_image(path=img, width=240, height=180, op="fill") }}" /> | |||
</a> | |||
  | |||
{% endfor %} |
@@ -0,0 +1 @@ | |||
<img src="{{ resize_image(path=path, width=width, height=height, op=op) }}" /> |
@@ -86,13 +86,13 @@ fn create_new_site(interface: &str, port: &str, output_dir: &str, base_url: &str | |||
let base_address = format!("{}:{}", base_url, port); | |||
let address = format!("{}:{}", interface, port); | |||
site.config.base_url = if site.config.base_url.ends_with('/') { | |||
let base_url = if site.config.base_url.ends_with('/') { | |||
format!("http://{}/", base_address) | |||
} else { | |||
format!("http://{}", base_address) | |||
}; | |||
site.set_base_url(base_url); | |||
site.set_output_path(output_dir); | |||
site.load()?; | |||
site.enable_live_reload(); | |||
@@ -27,10 +27,11 @@ pub fn error(message: &str) { | |||
/// Display in the console the number of pages/sections in the site | |||
pub fn notify_site_size(site: &Site) { | |||
println!( | |||
"-> Creating {} pages ({} orphan) and {} sections", | |||
"-> Creating {} pages ({} orphan), {} sections, and processing {} images", | |||
site.pages.len(), | |||
site.get_all_orphan_pages().len(), | |||
site.sections.len() - 1, // -1 since we do not the index as a section | |||
site.num_img_ops(), | |||
); | |||
} | |||