From 68772b6151861d3e969c8e882fa87b42bfa81944 Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 25 Mar 2024 21:03:15 +0000 Subject: [PATCH 01/11] feat(whiskers)!: rewrite from scratch with tera and rich context --- Cargo.lock | 502 ++++++++++++++++-- whiskers/Cargo.toml | 37 +- whiskers/README.md | 4 +- .../examples/demo/{input.hbs => input.tera} | 4 +- whiskers/examples/demo/justfile | 2 +- whiskers/examples/{errors.hbs => errors.tera} | 0 .../frontmatter/{input.hbs => input.tera} | 0 whiskers/examples/frontmatter/justfile | 2 +- .../overrides/{input.hbs => input.tera} | 0 .../examples/single-file/overrides/justfile | 2 +- .../simple/{input.hbs => input.tera} | 0 whiskers/examples/single-file/simple/justfile | 2 +- whiskers/src/cli.rs | 125 +++++ whiskers/src/context.rs | 60 +++ whiskers/src/filters.rs | 100 ++++ whiskers/src/frontmatter.rs | 478 ++--------------- whiskers/src/functions.rs | 49 ++ whiskers/src/helper.rs | 123 ----- whiskers/src/lib.rs | 58 +- whiskers/src/main.rs | 463 ++++++++++------ whiskers/src/markdown.rs | 136 +++++ whiskers/src/matrix.rs | 73 +++ whiskers/src/models.rs | 415 +++++++++++++++ whiskers/src/parse.rs | 70 --- whiskers/src/postprocess.rs | 24 - whiskers/src/template.rs | 249 --------- whiskers/src/templating.rs | 196 +++++++ whiskers/tests/cli.rs | 80 +-- whiskers/tests/fixtures/multi/multi.md | 124 +++++ whiskers/tests/fixtures/multi/multi.tera | 22 + whiskers/tests/fixtures/multifile.tera | 12 + whiskers/tests/fixtures/single/single.md | 35 ++ whiskers/tests/fixtures/single/single.tera | 26 + .../fixtures/singlefile-multiflavor.tera | 10 + .../fixtures/singlefile-singleflavor.tera | 8 + 35 files changed, 2263 insertions(+), 1228 deletions(-) rename whiskers/examples/demo/{input.hbs => input.tera} (89%) rename whiskers/examples/{errors.hbs => errors.tera} (100%) rename whiskers/examples/frontmatter/{input.hbs => input.tera} (100%) rename whiskers/examples/single-file/overrides/{input.hbs => input.tera} (100%) rename whiskers/examples/single-file/simple/{input.hbs => input.tera} (100%) create mode 100644 whiskers/src/cli.rs create mode 100644 whiskers/src/context.rs create mode 100644 whiskers/src/filters.rs create mode 100644 whiskers/src/functions.rs delete mode 100644 whiskers/src/helper.rs create mode 100644 whiskers/src/markdown.rs create mode 100644 whiskers/src/matrix.rs create mode 100644 whiskers/src/models.rs delete mode 100644 whiskers/src/parse.rs delete mode 100644 whiskers/src/postprocess.rs delete mode 100644 whiskers/src/template.rs create mode 100644 whiskers/src/templating.rs create mode 100644 whiskers/tests/fixtures/multi/multi.md create mode 100644 whiskers/tests/fixtures/multi/multi.tera create mode 100644 whiskers/tests/fixtures/multifile.tera create mode 100644 whiskers/tests/fixtures/single/single.md create mode 100644 whiskers/tests/fixtures/single/single.tera create mode 100644 whiskers/tests/fixtures/singlefile-multiflavor.tera create mode 100644 whiskers/tests/fixtures/singlefile-singleflavor.tera diff --git a/Cargo.lock b/Cargo.lock index 4f763caa..710d7d05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.11" @@ -74,6 +89,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + [[package]] name = "assert_cmd" version = "2.0.12" @@ -165,6 +186,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "catppuccin" version = "2.1.0" @@ -193,26 +220,27 @@ dependencies = [ [[package]] name = "catppuccin-whiskers" -version = "1.1.4" +version = "2.0.0" dependencies = [ + "anyhow", "assert_cmd", "base64", "catppuccin", "clap", "clap-stdin", - "color-eyre", "css-colors", - "handlebars", "indexmap", - "json-patch", + "itertools", + "lzma-rust", "predicates", - "regex", + "rmp-serde", + "semver", "serde", "serde_json", "serde_yaml", "tempfile", + "tera", "thiserror", - "titlecase", ] [[package]] @@ -230,6 +258,40 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.0", +] + +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clap" version = "4.5.1" @@ -307,6 +369,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -325,6 +393,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -341,6 +434,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22c2bbfc5708f23437b074ba4e699b14fd6d7181a61695bccc8d944b78739236" +[[package]] +name = "deunicode" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" + [[package]] name = "difflib" version = "0.4.0" @@ -449,6 +548,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.0" @@ -456,17 +566,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] -name = "handlebars" -version = "5.1.0" +name = "globset" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab283476b99e66691dee3f1640fea91487a8d81f50fb5ecc75538f8f8879a1e4" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ + "aho-corasick", + "bstr", "log", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", ] [[package]] @@ -490,6 +610,54 @@ dependencies = [ "libc", ] +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indenter" version = "0.3.3" @@ -522,12 +690,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "joinery" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5" - [[package]] name = "js-sys" version = "0.3.64" @@ -537,18 +699,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" -dependencies = [ - "serde", - "serde_json", - "thiserror", - "treediff", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -561,6 +711,12 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libwebp-sys2" version = "0.1.9" @@ -586,6 +742,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lzma-rust" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f798132166cc040cb70dbab4ccbb89643a6966a4ac33f0b312e76a8238673a5" +dependencies = [ + "byteorder", +] + [[package]] name = "memchr" version = "2.6.4" @@ -638,6 +803,27 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pest" version = "2.7.5" @@ -683,6 +869,44 @@ dependencies = [ "sha2", ] +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -702,6 +926,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "predicates" version = "3.1.0" @@ -759,6 +989,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.10.2" @@ -800,6 +1060,28 @@ dependencies = [ "png", ] +[[package]] +name = "rmp" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -825,6 +1107,24 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] + [[package]] name = "serde" version = "1.0.196" @@ -887,6 +1187,22 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "strsim" version = "0.11.0" @@ -906,9 +1222,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", @@ -916,6 +1232,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "termtree" version = "0.4.1" @@ -943,36 +1281,66 @@ dependencies = [ ] [[package]] -name = "titlecase" -version = "2.2.1" +name = "typenum" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38397a8cdb017cfeb48bf6c154d6de975ac69ffeed35980fde199d2ee0842042" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" dependencies = [ - "joinery", - "lazy_static", - "regex", + "unic-char-range", ] [[package]] -name = "treediff" -version = "4.0.2" +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" dependencies = [ - "serde_json", + "unic-ucd-segment", ] [[package]] -name = "typenum" -version = "1.17.0" +name = "unic-ucd-segment" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] [[package]] -name = "ucd-trie" -version = "0.1.6" +name = "unic-ucd-version" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] [[package]] name = "unicode-ident" @@ -1013,6 +1381,22 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -1093,12 +1477,30 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/whiskers/Cargo.toml b/whiskers/Cargo.toml index a02d077f..8e9e605a 100644 --- a/whiskers/Cargo.toml +++ b/whiskers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "catppuccin-whiskers" -version = "1.1.4" +version = "2.0.0" authors = ["backwardspy "] edition = "2021" description = "Soothing port creation tool for the high-spirited!" @@ -9,27 +9,42 @@ homepage = "https://github.com/catppuccin/toolbox/tree/main/whiskers" repository = "https://github.com/catppuccin/toolbox" license = "MIT" +[lib] +name = "whiskers" +path = "src/lib.rs" + [[bin]] name = "whiskers" path = "src/main.rs" +[lints.clippy] +all = "warn" +pedantic = "warn" +nursery = "warn" +unwrap_used = "warn" +missing_errors_doc = "allow" +implicit_hasher = "allow" +cast_possible_truncation = "allow" +cast_sign_loss = "allow" + [dependencies] +anyhow = "1.0" base64 = "0.22" -catppuccin = { version = "2.1", features = ["css-colors"] } -indexmap = { version = "2.2", features = ["serde"] } +catppuccin = { version = "2.1", features = ["serde", "css-colors"] } clap = { version = "4.5", features = ["derive"] } -clap-stdin = "0.4" -color-eyre = { version = "0.6", default-features = false } +clap-stdin = "0.4.0" css-colors = "1.0" -handlebars = "5.1" -regex = "1.10" +indexmap = { version = "2.2", features = ["serde"] } +itertools = "0.12" +lzma-rust = "0.1" +rmp-serde = "1.1" +semver = { version = "1.0.22", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = "1.0" serde_yaml = "0.9" -tempfile = "3.10" +tempfile = "3.10.1" +tera = { version = "1.19", features = ["preserve_order"] } thiserror = "1.0" -titlecase = "2.2" -json-patch = "1.2" [dev-dependencies] assert_cmd = "2.0" diff --git a/whiskers/README.md b/whiskers/README.md index 3cb0e6f0..0d3020d4 100644 --- a/whiskers/README.md +++ b/whiskers/README.md @@ -380,10 +380,10 @@ the `DIFFTOOL` environment variable, falling back to `diff` if it's not set. The command will be invoked as `$DIFFTOOL `. ```console -$ whiskers theme.hbs latte --check themes/latte.cfg +$ whiskers theme.tera latte --check themes/latte.cfg (no output, exit code 0) -$ whiskers theme.hbs latte --check themes/latte.cfg +$ whiskers theme.tera latte --check themes/latte.cfg Templating would result in changes. 4c4 < accent is #ea76cb diff --git a/whiskers/examples/demo/input.hbs b/whiskers/examples/demo/input.tera similarity index 89% rename from whiskers/examples/demo/input.hbs rename to whiskers/examples/demo/input.tera index 6cf09e11..3206b305 100644 --- a/whiskers/examples/demo/input.hbs +++ b/whiskers/examples/demo/input.tera @@ -1,10 +1,10 @@ ## Demo -**flavor:** {{ flavorName }} +**flavor:** {{ flavor.name }} ### Colours -- **red:** #{{ red }} / {{ rgb red }} / {{ hsl red }} +- **red:** #{{ red.hex }} / {{ rgb red }} / {{ hsl red }} - **components:** r: {{ red_i red }} / {{ trunc (red_f red) 2 }}, g: {{ green_i red }} / {{ trunc (green_f red) 2 }}, b: {{ blue_i red }} / {{ trunc (blue_f red) 2 }} - **alpha:** {{ alpha_i (opacity red 0.6) }} / {{ trunc (alpha_f (opacity red 0.6)) 2 }} - **10% lighter:** #{{ lighten red 0.1 }} / {{ rgb (lighten red 0.1) }} / {{ hsl (lighten red 0.1) }} diff --git a/whiskers/examples/demo/justfile b/whiskers/examples/demo/justfile index 51ca550e..d3b99b3f 100644 --- a/whiskers/examples/demo/justfile +++ b/whiskers/examples/demo/justfile @@ -16,7 +16,7 @@ clean: # Generate a single flavor, e.g. "mocha" gen flavor: - @{{whiskers_cmd}} input.hbs {{flavor}} -o {{output}}/{{flavor}}.md + @{{whiskers_cmd}} input.tera -f {{flavor}} > {{output}}/{{flavor}}.md # Generate all four flavors all: setup (gen "latte") (gen "frappe") (gen "macchiato") (gen "mocha") \ No newline at end of file diff --git a/whiskers/examples/errors.hbs b/whiskers/examples/errors.tera similarity index 100% rename from whiskers/examples/errors.hbs rename to whiskers/examples/errors.tera diff --git a/whiskers/examples/frontmatter/input.hbs b/whiskers/examples/frontmatter/input.tera similarity index 100% rename from whiskers/examples/frontmatter/input.hbs rename to whiskers/examples/frontmatter/input.tera diff --git a/whiskers/examples/frontmatter/justfile b/whiskers/examples/frontmatter/justfile index 51ca550e..fd20d5a3 100644 --- a/whiskers/examples/frontmatter/justfile +++ b/whiskers/examples/frontmatter/justfile @@ -16,7 +16,7 @@ clean: # Generate a single flavor, e.g. "mocha" gen flavor: - @{{whiskers_cmd}} input.hbs {{flavor}} -o {{output}}/{{flavor}}.md + @{{whiskers_cmd}} input.tera --flavor {{flavor}} > {{output}}/{{flavor}}.md # Generate all four flavors all: setup (gen "latte") (gen "frappe") (gen "macchiato") (gen "mocha") \ No newline at end of file diff --git a/whiskers/examples/single-file/overrides/input.hbs b/whiskers/examples/single-file/overrides/input.tera similarity index 100% rename from whiskers/examples/single-file/overrides/input.hbs rename to whiskers/examples/single-file/overrides/input.tera diff --git a/whiskers/examples/single-file/overrides/justfile b/whiskers/examples/single-file/overrides/justfile index 57d04fb3..c9a610c2 100644 --- a/whiskers/examples/single-file/overrides/justfile +++ b/whiskers/examples/single-file/overrides/justfile @@ -7,4 +7,4 @@ whiskers_cmd := "cargo run --bin whiskers --" # Generate a single file containing all four flavors gen: - @{{whiskers_cmd}} input.hbs all -o output.md \ No newline at end of file + @{{whiskers_cmd}} input.tera > output.md \ No newline at end of file diff --git a/whiskers/examples/single-file/simple/input.hbs b/whiskers/examples/single-file/simple/input.tera similarity index 100% rename from whiskers/examples/single-file/simple/input.hbs rename to whiskers/examples/single-file/simple/input.tera diff --git a/whiskers/examples/single-file/simple/justfile b/whiskers/examples/single-file/simple/justfile index 57d04fb3..c9a610c2 100644 --- a/whiskers/examples/single-file/simple/justfile +++ b/whiskers/examples/single-file/simple/justfile @@ -7,4 +7,4 @@ whiskers_cmd := "cargo run --bin whiskers --" # Generate a single file containing all four flavors gen: - @{{whiskers_cmd}} input.hbs all -o output.md \ No newline at end of file + @{{whiskers_cmd}} input.tera > output.md \ No newline at end of file diff --git a/whiskers/src/cli.rs b/whiskers/src/cli.rs new file mode 100644 index 00000000..7bd8035a --- /dev/null +++ b/whiskers/src/cli.rs @@ -0,0 +1,125 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use clap::Parser; +use clap_stdin::FileOrStdin; + +type ValueMap = HashMap; + +#[derive(Parser, Debug)] +#[command(version, about)] +pub struct Args { + /// Path to the template file, or - for stdin + #[arg(required_unless_present = "list_functions")] + pub template: Option, + + /// Render a single flavor instead of all four + #[arg(long, short)] + pub flavor: Option, + + /// Set color overrides + #[arg(long, value_parser = json_map::)] + pub color_overrides: Option, + + /// Set frontmatter overrides + #[arg(long, value_parser = json_map::)] + pub overrides: Option, + + /// Instead of creating an output, check it against an example + /// + /// In single-output mode, a path to the example file must be provided. + /// In multi-output mode, no path is required and, if one is provided, it + /// will be ignored. + #[arg(long, value_name = "EXAMPLE_PATH")] + pub check: Option>, + + /// Dry run, don't write anything to disk + #[arg(long)] + pub dry_run: bool, + + /// List all Tera filters and functions + #[arg(short, long)] + pub list_functions: bool, + + /// Output format of --list-functions + #[arg(short, long, default_value = "json")] + pub output_format: OutputFormat, +} + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("Invalid JSON literal argument: {message}")] + InvalidJsonLiteralArg { message: String }, + + #[error("Invalid JSON file argument: {message}")] + InvalidJsonFileArg { message: String }, + + #[error("Failed to read file: {path}")] + ReadFile { + path: String, + #[source] + source: std::io::Error, + }, +} + +#[derive(Copy, Clone, Debug, clap::ValueEnum)] +pub enum Flavor { + Latte, + Frappe, + Macchiato, + Mocha, +} + +impl From for catppuccin::FlavorName { + fn from(val: Flavor) -> Self { + match val { + Flavor::Latte => Self::Latte, + Flavor::Frappe => Self::Frappe, + Flavor::Macchiato => Self::Macchiato, + Flavor::Mocha => Self::Mocha, + } + } +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct ColorOverrides { + #[serde(default)] + pub all: HashMap, + #[serde(default)] + pub latte: HashMap, + #[serde(default)] + pub frappe: HashMap, + #[serde(default)] + pub macchiato: HashMap, + #[serde(default)] + pub mocha: HashMap, +} + +#[derive(Clone, Copy, Debug, clap::ValueEnum)] +pub enum OutputFormat { + Json, + Yaml, + Markdown, + MarkdownTable, +} + +fn json_map(s: &str) -> Result +where + T: serde::de::DeserializeOwned, +{ + if Path::new(s).is_file() { + let s = std::fs::read_to_string(s).map_err(|e| Error::ReadFile { + path: s.to_string(), + source: e, + })?; + serde_json::from_str(&s).map_err(|e| Error::InvalidJsonFileArg { + message: e.to_string(), + }) + } else { + serde_json::from_str(s).map_err(|e| Error::InvalidJsonLiteralArg { + message: e.to_string(), + }) + } +} diff --git a/whiskers/src/context.rs b/whiskers/src/context.rs new file mode 100644 index 00000000..8ab9e358 --- /dev/null +++ b/whiskers/src/context.rs @@ -0,0 +1,60 @@ +/// Recursively merge two tera values into one. +#[must_use] +pub fn merge_values(a: &tera::Value, b: &tera::Value) -> tera::Value { + match (a, b) { + // if both are objects, merge them + (tera::Value::Object(a), tera::Value::Object(b)) => { + let mut result = a.clone(); + for (k, v) in b { + result.insert( + k.clone(), + merge_values(a.get(k).unwrap_or(&tera::Value::Null), v), + ); + } + tera::Value::Object(result) + } + // otherwise, use the second value + (_, b) => b.clone(), + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_merge_values() { + let a = tera::to_value(&json!({ + "a": 1, + "b": { + "c": 2, + "d": 3, + }, + })) + .expect("test value is always valid"); + let b = tera::to_value(&json!({ + "b": { + "c": 4, + "e": 5, + }, + "f": 6, + })) + .expect("test value is always valid"); + let result = merge_values(&a, &b); + assert_eq!( + result, + tera::to_value(&json!({ + "a": 1, + "b": { + "c": 4, + "d": 3, + "e": 5, + }, + "f": 6, + })) + .expect("test value is always valid") + ); + } +} diff --git a/whiskers/src/filters.rs b/whiskers/src/filters.rs new file mode 100644 index 00000000..3c28e209 --- /dev/null +++ b/whiskers/src/filters.rs @@ -0,0 +1,100 @@ +use std::{ + collections::{BTreeMap, HashMap}, + io::Write, +}; + +use base64::Engine as _; + +use crate::models::Color; + +pub fn modify( + value: &tera::Value, + args: &HashMap, +) -> Result { + let color: Color = tera::from_value(value.clone())?; + if let Some(hue) = args.get("hue") { + let hue = tera::from_value(hue.clone())?; + Ok(tera::to_value(color.mod_hue(hue))?) + } else if let Some(saturation) = args.get("saturation") { + let saturation = tera::from_value(saturation.clone())?; + Ok(tera::to_value(color.mod_saturation(saturation))?) + } else if let Some(lightness) = args.get("lightness") { + let lightness = tera::from_value(lightness.clone())?; + Ok(tera::to_value(color.mod_lightness(lightness))?) + } else if let Some(opacity) = args.get("opacity") { + let opacity = tera::from_value(opacity.clone())?; + Ok(tera::to_value(color.mod_opacity(opacity))?) + } else { + Ok(value.clone()) + } +} + +pub fn add( + value: &tera::Value, + args: &HashMap, +) -> Result { + let color: Color = tera::from_value(value.clone())?; + if let Some(hue) = args.get("hue") { + let hue = tera::from_value(hue.clone())?; + Ok(tera::to_value(color.add_hue(hue))?) + } else if let Some(saturation) = args.get("saturation") { + let saturation = tera::from_value(saturation.clone())?; + Ok(tera::to_value(color.add_saturation(saturation))?) + } else if let Some(lightness) = args.get("lightness") { + let lightness = tera::from_value(lightness.clone())?; + Ok(tera::to_value(color.add_lightness(lightness))?) + } else if let Some(opacity) = args.get("opacity") { + let opacity = tera::from_value(opacity.clone())?; + Ok(tera::to_value(color.add_opacity(opacity))?) + } else { + Ok(value.clone()) + } +} + +pub fn sub( + value: &tera::Value, + args: &HashMap, +) -> Result { + let color: Color = tera::from_value(value.clone())?; + if let Some(hue) = args.get("hue") { + let hue = tera::from_value(hue.clone())?; + Ok(tera::to_value(color.sub_hue(hue))?) + } else if let Some(saturation) = args.get("saturation") { + let saturation = tera::from_value(saturation.clone())?; + Ok(tera::to_value(color.sub_saturation(saturation))?) + } else if let Some(lightness) = args.get("lightness") { + let lightness = tera::from_value(lightness.clone())?; + Ok(tera::to_value(color.sub_lightness(lightness))?) + } else if let Some(opacity) = args.get("opacity") { + let opacity = tera::from_value(opacity.clone())?; + Ok(tera::to_value(color.sub_opacity(opacity))?) + } else { + Ok(value.clone()) + } +} + +pub fn urlencode_lzma( + value: &tera::Value, + _args: &HashMap, +) -> Result { + // encode the data with the following process: + // 1. messagepack the data + // 2. compress the messagepacked data with lzma (v1, preset 9) + // 3. urlsafe base64 encode the compressed data + let value: BTreeMap = tera::from_value(value.clone())?; + let packed = rmp_serde::to_vec(&value).map_err(|e| tera::Error::msg(e.to_string()))?; + let mut options = lzma_rust::LZMA2Options::with_preset(9); + options.dict_size = lzma_rust::LZMA2Options::DICT_SIZE_DEFAULT; + let mut compressed = Vec::new(); + let mut writer = lzma_rust::LZMAWriter::new( + lzma_rust::CountingWriter::new(&mut compressed), + &options, + true, + false, + Some(packed.len() as u64), + )?; + writer.write_all(&packed)?; + let _ = writer.write(&[])?; + let encoded = base64::engine::general_purpose::URL_SAFE.encode(compressed); + Ok(tera::to_value(encoded)?) +} diff --git a/whiskers/src/frontmatter.rs b/whiskers/src/frontmatter.rs index 3ef06075..15949fd1 100644 --- a/whiskers/src/frontmatter.rs +++ b/whiskers/src/frontmatter.rs @@ -1,454 +1,48 @@ -use handlebars::Handlebars; -use json_patch::merge; -use serde_json::Value; +use std::collections::HashMap; -use crate::{Map, COLOR_NAMES, FLAVOR_NAMES}; - -pub type FlavorContexts = Vec>; - -fn split(template: &str) -> Option<(&str, &str)> { - // we consider a template to possibly have frontmatter iff: - // * line 0 is "---" - // * there is another "---" on another line - let template = template.trim(); - let sep = "---"; - if !template.starts_with(sep) { - return None; - } - - template[sep.len()..] - .split_once(sep) - .map(|(a, b)| (a.trim(), b.trim())) +#[derive(Debug)] +pub struct Document { + pub frontmatter: HashMap, + pub body: String, } -/// Merges together overrides from the cli and frontmatter. -/// -/// The order of priority is as follows: -/// -/// 1. CLI overrides from the `--overrides` flag. -/// 2. The `"overrides": ("latte" | "frappe" | "macchiato" | "mocha")` frontmatter block(s) -/// 3. The root context (variables defined at the top level) -/// -fn merge_overrides(cli_overrides: Option, frontmatter: Value, flavor: &str) -> Value { - let mut merged = frontmatter; - - if let Some(yaml) = merged.get("overrides").cloned() { - // hosting current flavor overrides to root context - if let Some(flavor) = yaml.get(flavor) { - merge(&mut merged, flavor); - } - } - - // applying CLI overrides - if let Some(overrides) = cli_overrides { - merge(&mut merged, &overrides); - } - - let merged_map = merged - .as_object_mut() - .expect("merged can be converted to a mutable map"); - - // don't need the "overrides" block anymore since we've hoisted everything up - merged_map.remove("overrides"); - - // propagate overridden palette colors inside the ["colors"] handlebars iterator - let colours = merged_map - .clone() - .into_iter() - .filter(|(k, _)| COLOR_NAMES.iter().any(|s| s == k)) - .collect::(); - if !colours.is_empty() { - merged_map.insert("colors".to_string(), Value::from(colours)); - } - - serde_json::to_value(merged_map).expect("overridden frontmatter can be serialized") +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid YAML frontmatter (L{line}:{column}) : {message}")] + InvalidYaml { + line: usize, + column: usize, + message: String, + }, } -#[must_use] -#[allow(clippy::missing_panics_doc)] // panic here implies an internal issue -pub fn render_and_parse_all<'a>( - template: &'a str, - overrides: &Option, - reg: &Handlebars, - ctx: &Value, -) -> (&'a str, Map) { - let Some((_, content)) = split(template) else { - return ( - template, - FLAVOR_NAMES - .map(|v| (v.into(), Value::Null)) - .into_iter() - .collect::(), - ); +pub fn parse(input: &str) -> Result { + let Some((frontmatter, body)) = split(input) else { + // no frontmatter to parse + return Ok(Document { + frontmatter: HashMap::new(), + body: input.to_string(), + }); }; - let frontmatter = ctx - .as_object() - .expect("context is an object") - .into_iter() - .map(|(flavor, ctx)| { - let frontmatter = render_and_parse(template, overrides.clone(), flavor, reg, ctx).1; - (flavor.to_string(), frontmatter) - }) - .collect::(); - - (content, frontmatter) + Ok(Document { + frontmatter: serde_yaml::from_str(frontmatter).map_err(|e| Error::InvalidYaml { + line: e.location().map(|l| l.line()).unwrap_or_default(), + column: e.location().map(|l| l.column()).unwrap_or_default(), + message: e.to_string(), + })?, + body: body.to_string(), + }) } -#[must_use] -pub fn render_and_parse<'a>( - template: &'a str, - overrides: Option, - flavor: &'a str, - reg: &Handlebars, - ctx: &Value, -) -> (&'a str, Value) { - let Some((frontmatter, content)) = split(template) else { - return (template, Value::Null); - }; - - let parsed: Value = match serde_yaml::from_str(frontmatter) { - Ok(frontmatter) => frontmatter, - Err(e) => { - eprintln!("warning: Failed to parse YAML frontmatter ({e}). Proceeding without it."); - return (content, Value::Null); - } - }; - - let overridden = merge_overrides(overrides, parsed, flavor); - - let rendered = match reg.render_template(&overridden.to_string(), ctx) { - Ok(frontmatter) => frontmatter, - Err(e) => { - eprintln!( - "warning: Failed to render frontmatter templates ({e}). Proceeding without it" - ); - return (content, Value::Null); - } - }; - - match serde_yaml::from_str(&rendered) { - Ok(frontmatter) => (content, frontmatter), - Err(e) => { - eprintln!("warning: Failed to parse YAML frontmatter ({e}). Proceeding without it."); - (content, Value::Null) - } - } -} - -#[cfg(test)] -mod tests { - mod split { - use crate::frontmatter::split; - - #[test] - fn no_frontmatter() { - let content = "a\nb\nc"; - let result = split(content); - assert_eq!(result, None); - } - - #[test] - fn unclosed_frontmatter() { - let content = "---\na: b\nc: d"; - let result = split(content); - assert_eq!(result, None); - } - - #[test] - fn all_frontmatter_no_template() { - let content = "---\na: b\nc: d\n---"; - let result = split(content); - assert_eq!(result, Some(("a: b\nc: d", ""))); - } - - #[test] - fn some_frontmatter_some_template() { - let content = "---\na: b\nc: d\n---\na: b\nc: d\n"; - let result = split(content); - assert_eq!(result, Some(("a: b\nc: d", "a: b\nc: d"))); - } - } - - mod parse_and_render { - use handlebars::Handlebars; - use serde_json::{json, Value}; - - use crate::frontmatter::{render_and_parse, render_and_parse_all}; - use crate::Map; - - #[test] - fn parse_frontmatter() { - let content = "---\na: b\nc: d\n---\na: b\nc: d\n"; - let expected = json!({"a": "b", "c": "d"}); - let reg = Handlebars::new(); - let ctx = Value::Object(Map::new()); - let overrides = Some(Value::Object(Map::new())); - let result = render_and_parse(content, overrides, "mocha", ®, &ctx); - assert_eq!(result, ("a: b\nc: d", expected)); - } - - #[test] - fn fail_to_parse_frontmatter() { - let content = " - --- - a: b - c: - --- - "; - let reg = Handlebars::new(); - let ctx = Value::Object(Map::new()); - let overrides = Some(Value::Object(Map::new())); - let result = render_and_parse(content, overrides, "mocha", ®, &ctx); - assert_eq!(result, ("", Value::Null)); - } - - #[test] - fn parse_frontmatter_with_cli_overrides() { - let content = "---\na: b\nc: d\n---\na: b\nc: d\n"; - let expected = serde_json::from_str::(r#"{"a":"override","c":"d"}"#) - .expect("valid json fixture"); - let reg = Handlebars::new(); - let ctx = Value::Object(Map::new()); - let overrides = Some(json!({"a": "override"})); - let result = render_and_parse(content, overrides, "mocha", ®, &ctx); - assert_eq!(result, ("a: b\nc: d", expected)); - } - - #[test] - fn parse_frontmatter_with_override_block() { - let content = - "---\na: b\nc: d\noverrides:\n mocha:\n a: 'override'\n---\na: b\nc: d\n"; - let expected = serde_json::from_str::(r#"{"a":"override","c":"d"}"#) - .expect("valid json fixture"); - let reg = Handlebars::new(); - let ctx = Value::Object(Map::new()); - let overrides = Some(Value::Object(Map::new())); - let result = render_and_parse(content, overrides, "mocha", ®, &ctx); - assert_eq!(result, ("a: b\nc: d", expected)); - } - - #[test] - fn render_frontmatter() { - let content = "---\na: '{{var}}'\nc: d\n---\n{{a}}\nc: d\n"; - let expected = - serde_json::from_str::(r#"{"a":"b","c":"d"}"#).expect("valid json fixture"); - let reg = Handlebars::new(); - let ctx = serde_json::from_str::(r#"{"var":"b"}"#).expect("valid json fixture"); - let overrides = Some(Value::Object(Map::new())); - let result = render_and_parse(content, overrides, "mocha", ®, &ctx); - assert_eq!(result, ("{{a}}\nc: d", expected)); - } - - #[test] - fn single_file_with_frontmatter() { - let content = "---\na: '{{num}}'\nc: d\n---\n{{a}}\nc: d\n"; - let expected = json!({ - "latte": {"a": "1","c": "d"}, - "frappe": {"a": "2","c": "d"}, - "macchiato": {"a": "3","c": "d"}, - "mocha": {"a": "4","c": "d"} - }) - .as_object() - .expect("expected is valid json") - .clone(); - let reg = Handlebars::new(); - let ctx = serde_json::from_str::(r#"{"latte":{"num": 1}, "frappe": {"num": 2}, "macchiato": {"num": 3}, "mocha": {"num": 4}}"#).expect("valid json fixture"); - let overrides = Some(Value::Object(Map::new())); - let result = render_and_parse_all(content, &overrides, ®, &ctx); - assert_eq!(result, ("{{a}}\nc: d", expected)); - } - - #[test] - fn single_file_with_frontmatter_and_overrides() { - let content = "---\na: '{{num}}'\nc: d\n---\n{{a}}\nc: d\n"; - let expected = json!({ - "latte": {"a": "5","c": "d"}, - "frappe": {"a": "5","c": "d"}, - "macchiato": {"a": "5","c": "d"}, - "mocha": {"a": "5","c": "d"} - }) - .as_object() - .expect("expected is valid json") - .clone(); - let reg = Handlebars::new(); - let ctx = serde_json::from_str::(r#"{"latte":{"num": 1}, "frappe": {"num": 2}, "macchiato": {"num": 3}, "mocha": {"num": 4}}"#).expect("valid json fixture"); - let overrides = Some(json!({"a": "5"})); - let result = render_and_parse_all(content, &overrides, ®, &ctx); - assert_eq!(result, ("{{a}}\nc: d", expected)); - } - - #[test] - fn single_file_with_no_frontmatter() { - let content = "c: d"; - let expected = json!({ - "latte": null, - "frappe": null, - "macchiato": null, - "mocha": null - }) - .as_object() - .expect("expected is valid json") - .clone(); - let reg = Handlebars::new(); - let ctx = Value::Null; - let overrides = Some(Value::Object(Map::new())); - let result = render_and_parse_all(content, &overrides, ®, &ctx); - assert_eq!(result, ("c: d", expected)); - } +fn split(template: &str) -> Option<(&str, &str)> { + // we consider a template to possibly have frontmatter iff: + // * line 0 is "---" + // * there is another "---" on another line + let sep = "---\n"; + if !template.starts_with(sep) { + return None; } - mod overrides { - use serde_json::{json, Value}; - - use crate::frontmatter::merge_overrides; - use crate::yaml; - - #[test] - fn frontmatter_with_no_overrides() { - let frontmatter = yaml!( - r#" - accent: "{{ mauve }}" - primary: true - "# - ); - let actual = merge_overrides(None, frontmatter.clone(), "mocha"); - assert_eq!(actual, frontmatter); - } - - #[test] - fn frontmatter_with_single_flavor_override_and_is_current_flavor() { - let frontmatter = yaml!( - r#" - accent: "{{ mauve }}" - primary: true - overrides: - mocha: - accent: "{{ blue }}" - "# - ); - let expected = yaml!( - r#" - accent: "{{ blue }}" - primary: true - "# - ); - let actual = merge_overrides(None, frontmatter, "mocha"); - assert_eq!(actual, expected); - } - - #[test] - fn frontmatter_with_single_flavor_override_and_is_not_current_flavor() { - let frontmatter = yaml!( - r#" - accent: "{{ mauve }}" - primary: true - overrides: - mocha: - accent: "{{ blue }}" - "# - ); - let expected = yaml!( - r#" - accent: "{{ mauve }}" - primary: true - "# - ); - let actual = merge_overrides(None, frontmatter, "latte"); - assert_eq!(actual, expected); - } - - #[test] - fn frontmatter_with_palette_colours() { - let frontmatter = yaml!( - r#" - accent: "{{ mauve }}" - primary: true - overrides: - mocha: - accent: "{{ blue }}" - base: "020202" - mantle: "010101" - crust: "000000" - "# - ); - let expected = yaml!( - r#" - accent: "{{ blue }}" - base: "020202" - mantle: "010101" - crust: "000000" - primary: true - colors: - base: "020202" - mantle: "010101" - crust: "000000" - "# - ); - let actual = merge_overrides(None, frontmatter, "mocha"); - assert_eq!(actual, expected); - } - - #[test] - fn cli_overriding_frontmatter() { - let frontmatter = yaml!(r#"accent: "{{ mauve }}""#); - let overrides = Some(json!({ - "accent": "{{ pink }}" - })); - let actual = merge_overrides(overrides.clone(), frontmatter, "mocha"); - assert_eq!(Some(actual), overrides); - } - - #[test] - fn cli_merging_with_frontmatter() { - let frontmatter = yaml!( - r#" - accent: "{{ mauve }}" - user: "sgoudham" - overrides: - mocha: - accent: "{{ blue }}" - - "# - ); - let overrides = Some(json!({ - "accent": "{{ pink }}" - })); - let expected = yaml!( - r#" - accent: "{{ pink }}" - user: "sgoudham" - "# - ); - let actual = merge_overrides(overrides, frontmatter, "mocha"); - assert_eq!(actual, expected); - } - - #[test] - fn cli_merging_with_frontmatter_and_propagating_colors() { - let frontmatter = yaml!( - r#" - accent: "{{ mauve }}" - overrides: - mocha: - accent: "{{ blue }}" - base: "020202" - "# - ); - let overrides = Some(json!({ - "accent": "{{ pink }}", - "base": "no color" - })); - let expected = yaml!( - r#" - accent: "{{ pink }}" - base: "no color" - colors: - base: "no color" - "# - ); - let actual = merge_overrides(overrides, frontmatter, "mocha"); - assert_eq!(actual, expected); - } - } + template[sep.len()..].split_once(sep) } diff --git a/whiskers/src/functions.rs b/whiskers/src/functions.rs new file mode 100644 index 00000000..b486a5e8 --- /dev/null +++ b/whiskers/src/functions.rs @@ -0,0 +1,49 @@ +use std::collections::{BTreeMap, HashMap}; + +use crate::models::Color; + +pub fn mix(args: &HashMap) -> Result { + let base: Color = tera::from_value( + args.get("base") + .ok_or_else(|| tera::Error::msg("base color is required"))? + .clone(), + )?; + let blend: Color = tera::from_value( + args.get("blend") + .ok_or_else(|| tera::Error::msg("blend color is required"))? + .clone(), + )?; + let amount = args + .get("amount") + .ok_or_else(|| tera::Error::msg("amount is required"))? + .as_f64() + .ok_or_else(|| tera::Error::msg("amount must be a number"))?; + + let result = Color::mix(&base, &blend, amount); + + Ok(tera::to_value(result)?) +} + +pub fn if_fn(args: &HashMap) -> Result { + let cond = args + .get("cond") + .ok_or_else(|| tera::Error::msg("cond is required"))? + .as_bool() + .ok_or_else(|| tera::Error::msg("cond must be a boolean"))?; + let t = args + .get("t") + .ok_or_else(|| tera::Error::msg("t is required"))? + .clone(); + let f = args + .get("f") + .ok_or_else(|| tera::Error::msg("f is required"))? + .clone(); + + Ok(if cond { t } else { f }) +} + +pub fn object(args: &HashMap) -> Result { + // sorting the args gives us stable output + let args: BTreeMap<_, _> = args.iter().collect(); + Ok(tera::to_value(args)?) +} diff --git a/whiskers/src/helper.rs b/whiskers/src/helper.rs deleted file mode 100644 index 6da11859..00000000 --- a/whiskers/src/helper.rs +++ /dev/null @@ -1,123 +0,0 @@ -use base64::Engine; -use css_colors::{Color, Ratio, HSLA, RGBA}; -use handlebars::{ - handlebars_helper, Context, Handlebars, Helper, HelperResult, Output, RenderContext, - RenderError, RenderErrorReason, -}; - -use ::titlecase::titlecase as titlecase_ext; -use serde_json::Value; - -use crate::parse::ColorExt; - -fn hex_error( - helper: &'static str, - param: &'static str, -) -> impl FnOnce(crate::parse::Error) -> RenderError { - move |_| { - RenderErrorReason::ParamTypeMismatchForName(helper, param.to_string(), "hex".to_string()) - .into() - } -} - -handlebars_helper!(uppercase: |s: String| s.to_uppercase()); -handlebars_helper!(lowercase: |s: String| s.to_lowercase()); -handlebars_helper!(titlecase: |s: String| titlecase_ext(&s)); -handlebars_helper!(trunc: |number: f32, places: usize| format!("{number:.places$}")); -handlebars_helper!(lighten: |color: String, weight: f32| { - HSLA::from_hex(&color).map_err(hex_error("lighten", "0"))?.lighten(Ratio::from_f32(weight)).to_hex() -}); -handlebars_helper!(darken: |color: String, weight: f32| { - HSLA::from_hex(&color).map_err(hex_error("darken", "0"))?.darken(Ratio::from_f32(weight)).to_hex() -}); -handlebars_helper!(mix: |color_a: String, color_b: String, t: f32| { - let a = HSLA::from_hex(&color_a).map_err(hex_error("mix", "0"))?; - let b = HSLA::from_hex(&color_b).map_err(hex_error("mix", "1"))?; - a.mix(b, Ratio::from_f32(t)).to_hex() -}); -handlebars_helper!(opacity: |color: String, amount: f32| { - HSLA::from_hex(&color).map_err(hex_error("opacity", "0"))?.fade(Ratio::from_f32(amount)).to_hex() -}); -handlebars_helper!(rgb: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("rgb", "0"))?.to_rgb().to_string() -}); -handlebars_helper!(rgba: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("rgba", "0"))?.to_string() -}); -handlebars_helper!(hsl: |color: String| { - HSLA::from_hex(&color).map_err(hex_error("hsl", "0"))?.to_hsl().to_string() -}); -handlebars_helper!(hsla: |color: String| { - HSLA::from_hex(&color).map_err(hex_error("hsla", "0"))?.to_string() -}); -handlebars_helper!(red_i: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("red_i", "0"))?.r.as_u8() -}); -handlebars_helper!(green_i: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("green_i", "0"))?.g.as_u8() -}); -handlebars_helper!(blue_i: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("blue_i", "0"))?.b.as_u8() -}); -handlebars_helper!(alpha_i: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("alpha_i", "0"))?.a.as_u8() -}); -handlebars_helper!(red_f: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("red_f", "0"))?.r.as_f32() -}); -handlebars_helper!(green_f: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("green_f", "0"))?.g.as_f32() -}); -handlebars_helper!(blue_f: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("blue_f", "0"))?.b.as_f32() -}); -handlebars_helper!(alpha_f: |color: String| { - RGBA::from_hex(&color).map_err(hex_error("alpha_f", "0"))?.a.as_f32() -}); -handlebars_helper!(red_h: |color: String| { - format!("{:02x}", RGBA::from_hex(&color).map_err(hex_error("red_h", "0"))?.r.as_u8()) -}); -handlebars_helper!(green_h: |color: String| { - format!("{:02x}", RGBA::from_hex(&color).map_err(hex_error("green_h", "0"))?.g.as_u8()) -}); -handlebars_helper!(blue_h: |color: String| { - format!("{:02x}", RGBA::from_hex(&color).map_err(hex_error("blue_h", "0"))?.b.as_u8()) -}); -handlebars_helper!(alpha_h: |color: String| { - format!("{:02x}", RGBA::from_hex(&color).map_err(hex_error("alpha_h", "0"))?.a.as_u8()) -}); - -pub fn darklight( - h: &Helper, - _r: &Handlebars, - ctx: &Context, - rc: &mut RenderContext, - out: &mut dyn Output, -) -> HelperResult { - let dark = h - .param(0) - .ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("darklight", 0))?; - let light = h - .param(1) - .ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("darklight", 1))?; - - // if we're in an each block, we have to try and get the flavor from the iteration key - let flavor = rc - .block() - .and_then(|block| block.get_local_var("key")) - .unwrap_or_else(|| &ctx.data()["flavor"]); - - if flavor == "latte" { - out.write(&light.render())?; - } else { - out.write(&dark.render())?; - } - - Ok(()) -} - -handlebars_helper!(unquote: |value: Value| { - let content = serde_json::to_string(&value).map_err(RenderErrorReason::SerdeError)?; - let content = base64::engine::general_purpose::STANDARD_NO_PAD.encode(content); - format!("{{WHISKERS:UNQUOTE:{content}}}") -}); diff --git a/whiskers/src/lib.rs b/whiskers/src/lib.rs index 01f2b985..31dcfaf3 100644 --- a/whiskers/src/lib.rs +++ b/whiskers/src/lib.rs @@ -1,51 +1,9 @@ -#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::unwrap_used)] -// we like truncating u32s into u8s around here -#![allow(clippy::cast_possible_truncation)] - -use serde_json::Value; - +pub mod cli; +pub mod filters; pub mod frontmatter; -mod helper; -mod parse; -pub mod postprocess; -pub mod template; - -pub type Map = serde_json::Map; - -const COLOR_NAMES: [&str; 26] = [ - "rosewater", - "flamingo", - "pink", - "mauve", - "red", - "maroon", - "peach", - "yellow", - "green", - "teal", - "sky", - "sapphire", - "blue", - "lavender", - "text", - "subtext1", - "subtext0", - "overlay2", - "overlay1", - "overlay0", - "surface2", - "surface1", - "surface0", - "base", - "mantle", - "crust", -]; - -const FLAVOR_NAMES: [&str; 4] = ["latte", "frappe", "macchiato", "mocha"]; - -#[macro_export] -macro_rules! yaml { - ($yaml:expr) => {{ - serde_yaml::from_str::($yaml).expect("yaml can be parsed") - }}; -} +pub mod functions; +pub mod matrix; +pub mod models; +pub mod templating; +pub mod context; +pub mod markdown; \ No newline at end of file diff --git a/whiskers/src/main.rs b/whiskers/src/main.rs index c238699c..c004e8dc 100644 --- a/whiskers/src/main.rs +++ b/whiskers/src/main.rs @@ -1,194 +1,342 @@ -#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::unwrap_used)] -// we like truncating u32s into u8s around here -#![allow(clippy::cast_possible_truncation)] - use std::{ - env, fmt, fs, - io::Write, + collections::HashMap, + env, + io::Write as _, path::{Path, PathBuf}, process, }; -use clap::Parser; -use clap_stdin::FileOrStdin; -use color_eyre::{ - eyre::{eyre, Context}, - Result, -}; -use json_patch::merge; -use serde_json::{json, Value}; - -use catppuccin_whiskers::{ - frontmatter, - postprocess::postprocess, - template::{self, helpers}, - Map, +use anyhow::{anyhow, Context as _}; +use catppuccin::FlavorName; +use clap::Parser as _; +use itertools::Itertools; +use whiskers::{ + cli::{Args, OutputFormat}, + context::merge_values, + frontmatter, markdown, + matrix::{self, Matrix}, + models, templating, }; -#[derive(clap::ValueEnum, Clone, Debug)] -enum Flavor { - Latte, - Frappe, - Macchiato, - Mocha, - All, -} +const FRONTMATTER_OPTIONS_SECTION: &str = "whiskers"; -#[allow(clippy::fallible_impl_from)] -impl From for catppuccin::FlavorName { - fn from(value: Flavor) -> Self { - match value { - Flavor::Latte => Self::Latte, - Flavor::Frappe => Self::Frappe, - Flavor::Macchiato => Self::Macchiato, - Flavor::Mocha => Self::Mocha, - // This should never be called, but it's here to satisfy the compiler. - Flavor::All => panic!(), - } - } +#[derive(Default, Debug, serde::Deserialize)] +struct TemplateOptions { + version: Option, + matrix: Option, + filename: Option, + hex_prefix: Option, + #[serde(default)] + capitalize_hex: bool, } -impl fmt::Display for Flavor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::Latte => write!(f, "latte"), - Self::Frappe => write!(f, "frappe"), - Self::Macchiato => write!(f, "macchiato"), - Self::Mocha => write!(f, "mocha"), - Self::All => write!(f, "all"), +impl TemplateOptions { + fn from_frontmatter( + frontmatter: &HashMap, + only_flavor: Option, + ) -> anyhow::Result { + // a `TemplateOptions` object before matrix transformation + #[derive(serde::Deserialize)] + struct RawTemplateOptions { + version: Option, + matrix: Option>, + filename: Option, + hex_prefix: Option, + #[serde(default)] + capitalize_hex: bool, } - } -} -fn parse_overrides(s: &str) -> Result { - match serde_json::from_str(s) { - Ok(json) => Ok(json), - Err(err) => Err(eyre!("could not parse overrides as JSON: {}", err)), + if let Some(opts) = frontmatter.get(FRONTMATTER_OPTIONS_SECTION) { + let opts: RawTemplateOptions = tera::from_value(opts.clone()) + .context("Frontmatter `whiskers` section is invalid")?; + let matrix = opts + .matrix + .map(|m| matrix::from_values(m, only_flavor)) + .transpose() + .context("Frontmatter matrix is invalid")?; + Ok(Self { + version: opts.version, + matrix, + filename: opts.filename, + hex_prefix: opts.hex_prefix, + capitalize_hex: opts.capitalize_hex, + }) + } else { + Ok(Self::default()) + } } } -#[derive(clap::Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// Path to the template file to render, or `-` for stdin - #[arg(required_unless_present = "list_helpers")] - template: Option, +fn main() -> anyhow::Result<()> { + // parse command-line arguments & template frontmatter + let args = Args::parse(); - /// Flavor to get colors from - #[arg(value_enum, required_unless_present = "list_helpers")] - flavor: Option, + if args.list_functions { + list_functions(args.output_format); + return Ok(()); + } - /// The overrides to apply to the template in JSON format - #[arg(long, value_parser(parse_overrides))] - overrides: Option, + let template = args + .template + .expect("args.template is guaranteed by clap to be set"); + let template_from_stdin = matches!(template.source, clap_stdin::Source::Stdin); + let template_name = template_name(&template); + let doc = frontmatter::parse( + &template + .contents() + .context("Template contents could not be read")?, + ) + .context("Frontmatter is invalid")?; + let template_opts = + TemplateOptions::from_frontmatter(&doc.frontmatter, args.flavor.map(Into::into)) + .context("Could not get template options from frontmatter")?; + + if !template_from_stdin && !template_is_compatible(&template_opts) { + std::process::exit(1); + } - /// Path to write to instead of stdout - #[arg(short, long)] - output_path: Option, + // merge frontmatter with command-line overrides and add to Tera context + let mut frontmatter = doc.frontmatter; + if let Some(overrides) = args.overrides { + for (key, value) in &overrides { + frontmatter + .entry(key.clone()) + .and_modify(|v| { + *v = merge_values(v, value); + }) + .or_insert( + tera::to_value(value) + .with_context(|| format!("Value of {key} override is invalid"))?, + ); + } + } + let mut ctx = tera::Context::new(); + for (key, value) in &frontmatter { + ctx.insert(key, &value); + } - /// Instead of printing a result, check if anything would change - #[arg(long)] - check: Option, + // build the Tera engine and palette + let mut tera = templating::make_engine(); + tera.add_raw_template(&template_name, &doc.body) + .context("Template is invalid")?; + let palette = models::build_palette( + template_opts.capitalize_hex, + template_opts.hex_prefix.as_deref(), + args.color_overrides.as_ref(), + ) + .context("Palette context cannot be built")?; + + if let Some(matrix) = template_opts.matrix { + let Some(filename_template) = template_opts.filename else { + anyhow::bail!("Filename template is required for multi-output render"); + }; + render_multi_output( + matrix, + &filename_template, + &ctx, + &palette, + &tera, + &template_name, + args.dry_run, + args.check.is_some(), + ) + .context("Multi-output render failed")?; + } else { + let check = args + .check + .map(|c| { + c.ok_or_else(|| anyhow!("--check requires a file argument in single-output mode")) + }) + .transpose()?; + render_single_output( + args.flavor.map(Into::into), + &ctx, + &palette, + &tera, + &template_name, + check, + ) + .context("Single-output render failed")?; + } - /// List all template helpers in Markdown format - #[arg(short, long)] - list_helpers: bool, + Ok(()) } -fn merge_contexts_all(ctx: &Value, frontmatter: &Map) -> Value { - let ctx = ctx.as_object().expect("ctx is an object").clone(); - - // Slight inefficiency here as the root context variables are - // also duplicated into each flavor. - let flavors: Map = ctx - .into_iter() - .map(|(name, ctx)| { - let flavor = frontmatter - .get(&name) - .expect("flavor exists in frontmatter"); - let merged_ctx = merge_contexts(ctx, flavor); - (name, merged_ctx) - }) - .collect(); - - let merged = json!({ "flavors": flavors }); - - serde_json::to_value(merged).expect("merged context is serializable") +#[allow(clippy::too_many_lines)] +fn list_functions(format: OutputFormat) { + match format { + OutputFormat::Json | OutputFormat::Yaml => { + let output = serde_json::json!({ + "functions": templating::all_functions(), + "filters": templating::all_filters() + }); + println!( + "{}", + if matches!(format, OutputFormat::Json) { + serde_json::to_string_pretty(&output).expect("output is guaranteed to be valid") + } else { + serde_yaml::to_string(&output).expect("output is guaranteed to be valid") + } + ); + } + OutputFormat::Markdown => { + println!( + "{}", + markdown::format_filters_and_functions(markdown::Format::List) + ); + } + OutputFormat::MarkdownTable => { + println!( + "{}", + markdown::format_filters_and_functions(markdown::Format::Table) + ); + } + } } -fn merge_contexts(ctx: Value, frontmatter: &Value) -> Value { - let mut merged = ctx; - if !frontmatter.is_null() { - merge(&mut merged, frontmatter); +fn template_name(template: &clap_stdin::FileOrStdin) -> String { + match &template.source { + clap_stdin::Source::Stdin => "template".to_string(), + clap_stdin::Source::Arg(arg) => Path::new(&arg).file_name().map_or_else( + || "template".to_string(), + |name| name.to_string_lossy().to_string(), + ), } - merged } -fn main() -> Result<()> { - color_eyre::config::HookBuilder::default() - .panic_section("Consider reporting this issue: https://github.com/catppuccin/toolbox") - .display_env_section(false) - .install()?; +fn template_is_compatible(template_opts: &TemplateOptions) -> bool { + let whiskers_version = semver::Version::parse(env!("CARGO_PKG_VERSION")) + .expect("CARGO_PKG_VERSION is always valid"); + if let Some(template_version) = &template_opts.version { + if !template_version.matches(&whiskers_version) { + eprintln!("Template requires whiskers version {template_version}, but you are running whiskers {whiskers_version}"); + return false; + } + } else { + eprintln!("Warning: No Whiskers version requirement specified in template."); + eprintln!("This template may not be compatible with this version of Whiskers."); + eprintln!(); + eprintln!("To fix this, add the minimum supported Whiskers version to the template frontmatter as follows:"); + eprintln!(); + eprintln!("---"); + eprintln!("whiskers:"); + eprintln!(" version: \"{whiskers_version}\""); + eprintln!("---"); + eprintln!(); + }; - let args = Args::parse(); + true +} - if args.list_helpers { - list_helpers(); - return Ok(()); +fn render_single_output( + flavor: Option, + ctx: &tera::Context, + palette: &models::Palette, + tera: &tera::Tera, + template_name: &str, + check: Option, +) -> Result<(), anyhow::Error> { + let mut ctx = ctx.clone(); + ctx.insert("flavors", &palette.flavors); + if let Some(flavor) = flavor { + let flavor = &palette.flavors[flavor.identifier()]; + ctx.insert("flavor", flavor); + + // also throw in the flavor's colors for convenience + for (_, color) in flavor { + ctx.insert(&color.identifier, &color); + } } - let template = &args - .template - .expect("template_path is guaranteed to be set") - .contents() - .expect("template contents are readable"); - - let flavor = args.flavor.expect("flavor is guaranteed to be set"); - let flavor_string = flavor.to_string(); + let result = tera + .render(template_name, &ctx) + .context("Template render failed")?; - let reg = template::make_registry(); - - let (content, ctx) = if matches!(flavor, Flavor::All) { - let ctx = template::make_context_all(); - let (content, frontmatter) = - frontmatter::render_and_parse_all(template, &args.overrides, ®, &ctx); - let merged_ctx = merge_contexts_all(&ctx, &frontmatter); - (content, merged_ctx) + if let Some(path) = check { + check_result_with_file(&path, &result).context("Check mode failed")?; } else { - let ctx = template::make_context(&catppuccin::PALETTE[flavor.into()]); - let (content, frontmatter) = frontmatter::render_and_parse( - template, - args.overrides, - flavor_string.as_str(), - ®, - &ctx, - ); - let merged_ctx = merge_contexts(ctx, &frontmatter); - (content, merged_ctx) - }; + print!("{result}"); + } - let result = reg - .render_template(content, &ctx) - .wrap_err("Failed to render template")?; - let result = postprocess(&result); - - if let Some(expected_path) = args.check { - let expected = fs::read_to_string(&expected_path)?; - if result != expected { - eprintln!("Templating would result in changes."); - invoke_difftool(&result, &expected_path)?; - process::exit(1); + Ok(()) +} + +fn render_multi_output( + matrix: HashMap>, + filename_template: &str, + ctx: &tera::Context, + palette: &models::Palette, + tera: &tera::Tera, + template_name: &str, + dry_run: bool, + check: bool, +) -> Result<(), anyhow::Error> { + let iterables = matrix + .into_iter() + .map(|(key, iterable)| iterable.into_iter().map(move |v| (key.clone(), v))) + .multi_cartesian_product() + .collect::>(); + + for iterable in iterables { + let mut ctx = ctx.clone(); + for (key, value) in iterable { + // expand flavor automatically to prevent requiring: + // `{% set flavor = flavors[flavor] %}` + // at the top of every template. + if key == "flavor" { + let flavor: catppuccin::FlavorName = value.parse()?; + let flavor = &palette.flavors[flavor.identifier()]; + ctx.insert("flavor", flavor); + } else { + ctx.insert(key, &value); + } + } + let result = tera + .render(template_name, &ctx) + .context("Main template render failed")?; + let filename = tera::Tera::one_off(filename_template, &ctx, false) + .context("Filename template render failed")?; + + if dry_run || cfg!(test) { + println!( + "Would write {} bytes into {filename}", + result.as_bytes().len() + ); + } else if check { + check_result_with_file(&filename, &result).context("Check mode failed")?; + } else { + std::fs::write(&filename, result) + .with_context(|| format!("Couldn't write to {filename}"))?; } - } else if let Some(output_path) = args.output_path { - fs::write(output_path, result)?; - } else { - print!("{result}"); } Ok(()) } -fn invoke_difftool(actual: &str, expected_path: &Path) -> Result<(), color_eyre::eyre::Error> { +fn check_result_with_file

(path: &P, result: &str) -> Result<(), anyhow::Error> +where + P: AsRef, +{ + let path = path.as_ref(); + let expected = std::fs::read_to_string(path).with_context(|| { + format!( + "Couldn't read {} for comparison against result", + path.display() + ) + })?; + if *result != expected { + eprintln!("Output does not match {}", path.display()); + invoke_difftool(result, path)?; + std::process::exit(1); + } + Ok(()) +} + +fn invoke_difftool

(actual: &str, expected_path: P) -> Result<(), anyhow::Error> +where + P: AsRef, +{ + let expected_path = expected_path.as_ref(); let tool = env::var("DIFFTOOL").unwrap_or_else(|_| "diff".to_string()); let mut actual_file = tempfile::NamedTempFile::new()?; @@ -204,16 +352,3 @@ fn invoke_difftool(actual: &str, expected_path: &Path) -> Result<(), color_eyre: Ok(()) } - -fn list_helpers() { - for helper in helpers() { - print!("- `{}", helper.name); - for arg in helper.args { - print!(" {arg}"); - } - println!("` : {}", helper.description); - for (before, after) in helper.examples { - println!(" - `{{{{ {} {} }}}}` → {}", helper.name, before, after); - } - } -} diff --git a/whiskers/src/markdown.rs b/whiskers/src/markdown.rs new file mode 100644 index 00000000..74bca3e1 --- /dev/null +++ b/whiskers/src/markdown.rs @@ -0,0 +1,136 @@ +use itertools::Itertools as _; + +use crate::templating; + +#[derive(Clone, Copy)] +pub enum Format { + List, + Table, +} + +#[must_use] +pub fn format_filters_and_functions(format: Format) -> String { + match format { + Format::List => list_format(), + Format::Table => table_format(), + } +} + +fn list_format() -> String { + let mut result = String::new(); + result.push_str("## Functions\n\n"); + for function in templating::all_functions() { + result.push_str(&format!( + "### `{name}`\n\n{description}\n\n", + name = function.name, + description = function.description + )); + if !function.examples.is_empty() { + result.push_str("#### Examples\n\n"); + for example in &function.examples { + result.push_str(&format!( + "- `{name}({input})` => `{output}`\n", + name = function.name, + input = example + .inputs + .iter() + .map(|(k, v)| format!("{k}={v}")) + .join(", "), + output = example.output + )); + } + result.push('\n'); + } + } + + result.push_str("## Filters\n\n"); + for filter in templating::all_filters() { + result.push_str(&format!( + "### `{name}`\n\n{description}\n\n", + name = filter.name, + description = filter.description + )); + if !filter.examples.is_empty() { + result.push_str("#### Examples\n\n"); + for example in &filter.examples { + result.push_str(&format!( + "- `{value} | {name}({input})` => `{output}`\n", + value = example.value, + name = filter.name, + input = example + .inputs + .iter() + .map(|(k, v)| format!("{k}={v}")) + .join(", "), + output = example.output + )); + } + result.push('\n'); + } + } + + result +} + +fn table_format() -> String { + let mut result = String::new(); + result.push_str("## Functions\n\n"); + result.push_str("| Name | Description | Examples |\n"); + result.push_str("|------|-------------|----------|\n"); + for function in templating::all_functions() { + result.push_str(&format!( + "| `{name}` | {description} | {examples} |\n", + name = function.name, + description = function.description, + examples = if function.examples.is_empty() { + "None".to_string() + } else { + function + .examples + .first() + .map_or_else(String::new, |example| { + format!( + "`{name}({input})` => `{output}`", + name = function.name, + input = example + .inputs + .iter() + .map(|(k, v)| format!("{k}={v}")) + .join(", "), + output = example.output + ) + }) + } + )); + } + + result.push_str("## Filters\n\n"); + result.push_str("| Name | Description | Examples |\n"); + result.push_str("|------|-------------|----------|\n"); + for filter in templating::all_filters() { + result.push_str(&format!( + "| `{name}` | {description} | {examples} |\n", + name = filter.name, + description = filter.description, + examples = if filter.examples.is_empty() { + "None".to_string() + } else { + filter.examples.first().map_or_else(String::new, |example| { + format!( + "`{value} \\| {name}({input})` => `{output}`", + value = example.value, + name = filter.name, + input = example + .inputs + .iter() + .map(|(k, v)| format!("{k}={v}")) + .join(", "), + output = example.output + ) + }) + } + )); + } + + result +} diff --git a/whiskers/src/matrix.rs b/whiskers/src/matrix.rs new file mode 100644 index 00000000..01a0aa32 --- /dev/null +++ b/whiskers/src/matrix.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; + +use catppuccin::FlavorName; + +pub type Matrix = HashMap>; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Unknown magic iterable: {name}")] + UnknownIterable { name: String }, + + #[error("Invalid matrix array object element: must have a single key and an array of strings as value")] + InvalidObjectElement, + + #[error("Invalid matrix array element: must be a string or object")] + InvalidElement, +} + +// matrix in frontmatter is a list of strings or objects. +// objects must have a single key and an array of strings as the value. +// string array elements are substituted with the array from `iterables`. +pub fn from_values( + values: Vec, + only_flavor: Option, +) -> Result { + let iterables = magic_iterables(only_flavor); + values + .into_iter() + .map(|v| match v { + tera::Value::String(s) => { + let iterable = iterables + .get(s.as_str()) + .ok_or(Error::UnknownIterable { name: s.clone() })?; + Ok((s, iterable.clone())) + } + tera::Value::Object(o) => { + let (key, value) = o.into_iter().next().ok_or(Error::InvalidObjectElement)?; + let value: Vec = + tera::from_value(value).map_err(|_| Error::InvalidObjectElement)?; + Ok((key, value)) + } + _ => Err(Error::InvalidElement), + }) + .collect::>() +} + +fn magic_iterables(only_flavor: Option) -> HashMap<&'static str, Vec> { + HashMap::from([ + ( + "flavor", + only_flavor.map_or_else( + || { + catppuccin::PALETTE + .into_iter() + .map(|flavor| flavor.identifier().to_string()) + .collect::>() + }, + |flavor| vec![flavor.identifier().to_string()], + ), + ), + ("accent", ctp_accents()), + ]) +} + +fn ctp_accents() -> Vec { + catppuccin::PALETTE + .latte + .colors + .iter() + .filter(|c| c.accent) + .map(|c| c.name.identifier().to_string()) + .collect() +} diff --git a/whiskers/src/models.rs b/whiskers/src/models.rs new file mode 100644 index 00000000..61ca418f --- /dev/null +++ b/whiskers/src/models.rs @@ -0,0 +1,415 @@ +use css_colors::Color as _; +use indexmap::IndexMap; + +use crate::cli::ColorOverrides; + +// a frankenstein mix of Catppuccin & css_colors types to get all the +// functionality we want. +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Palette { + pub flavors: IndexMap, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Flavor { + pub name: String, + pub identifier: String, + pub dark: bool, + pub light: bool, + pub colors: IndexMap, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Color { + pub name: String, + pub identifier: String, + pub accent: bool, + pub hex: String, + pub rgb: RGB, + pub hsl: HSL, + pub opacity: u8, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct RGB { + pub r: u8, + pub g: u8, + pub b: u8, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct HSL { + pub h: u16, + pub s: f32, + pub l: f32, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to parse hex color: {0}")] + ParseHex(#[from] std::num::ParseIntError), +} + +/// attempt to canonicalize a hex string, optionally capitalizing it and adding a prefix. +fn format_hex(hex: &str, capitalize_hex_strings: bool, hex_prefix: Option<&str>) -> String { + let hex = hex.trim_start_matches('#'); + let hex = if capitalize_hex_strings { + hex.to_uppercase() + } else { + hex.to_string() + }; + if let Some(prefix) = hex_prefix { + format!("{prefix}{hex}") + } else { + hex + } +} + +fn color_from_hex( + hex: &str, + blueprint: &catppuccin::Color, + capitalize_hex_strings: bool, + hex_prefix: Option<&str>, +) -> Result { + let i = u32::from_str_radix(hex, 16)?; + let rgb = RGB { + r: ((i >> 16) & 0xff) as u8, + g: ((i >> 8) & 0xff) as u8, + b: (i & 0xff) as u8, + }; + let hsl = css_colors::rgb(rgb.r, rgb.g, rgb.b).to_hsl(); + let hex = format_hex(hex, capitalize_hex_strings, hex_prefix); + Ok(Color { + name: blueprint.name.to_string(), + identifier: blueprint.name.identifier().to_string(), + accent: blueprint.accent, + hex, + rgb, + hsl: HSL { + h: hsl.h.degrees(), + s: hsl.s.as_f32(), + l: hsl.l.as_f32(), + }, + opacity: 255, + }) +} + +fn color_from_catppuccin( + color: &catppuccin::Color, + capitalize_hex_strings: bool, + hex_prefix: Option<&str>, +) -> Color { + let hex = format_hex(&color.hex.to_string(), capitalize_hex_strings, hex_prefix); + Color { + name: color.name.to_string(), + identifier: color.name.identifier().to_string(), + accent: color.accent, + hex, + rgb: RGB { + r: color.rgb.r, + g: color.rgb.g, + b: color.rgb.b, + }, + hsl: HSL { + h: color.hsl.h.round() as u16, + s: color.hsl.s as f32, + l: color.hsl.l as f32, + }, + opacity: 255, + } +} + +/// Build a [`Palette`] from [`catppuccin::PALETTE`], optionally applying color overrides. +pub fn build_palette( + capitalize_hex_strings: bool, + hex_prefix: Option<&str>, + color_overrides: Option<&ColorOverrides>, +) -> Result { + // make a `Color` from a `catppuccin::Color`, taking into account `color_overrides`. + // overrides apply in this order: + // 1. base color + // 2. "all" override + // 3. flavor override + let make_color = + |color: &catppuccin::Color, flavor_name: catppuccin::FlavorName| -> Result { + let flavor_override = color_overrides + .map(|co| match flavor_name { + catppuccin::FlavorName::Latte => &co.latte, + catppuccin::FlavorName::Frappe => &co.frappe, + catppuccin::FlavorName::Macchiato => &co.macchiato, + catppuccin::FlavorName::Mocha => &co.mocha, + }) + .and_then(|o| o.get(color.name.identifier()).cloned()) + .map(|s| color_from_hex(&s, color, capitalize_hex_strings, hex_prefix)) + .transpose()?; + + let all_override = color_overrides + .and_then(|co| co.all.get(color.name.identifier()).cloned()) + .map(|s| color_from_hex(&s, color, capitalize_hex_strings, hex_prefix)) + .transpose()?; + + Ok(flavor_override.or(all_override).unwrap_or_else(|| { + color_from_catppuccin(color, capitalize_hex_strings, hex_prefix) + })) + }; + + let mut flavors = IndexMap::new(); + for flavor in &catppuccin::PALETTE { + let mut colors = IndexMap::new(); + for color in flavor { + colors.insert( + color.name.identifier().to_string(), + make_color(color, flavor.name)?, + ); + } + flavors.insert( + flavor.name.identifier().to_string(), + Flavor { + name: flavor.name.to_string(), + identifier: flavor.name.identifier().to_string(), + dark: flavor.dark, + light: !flavor.dark, + colors, + }, + ); + } + Ok(Palette { flavors }) +} + +impl Palette { + #[must_use] + pub fn iter(&self) -> indexmap::map::Iter { + self.flavors.iter() + } +} + +impl<'a> IntoIterator for &'a Palette { + type Item = (&'a String, &'a Flavor); + type IntoIter = indexmap::map::Iter<'a, String, Flavor>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl Flavor { + #[must_use] + pub fn iter(&self) -> indexmap::map::Iter { + self.colors.iter() + } +} + +impl<'a> IntoIterator for &'a Flavor { + type Item = (&'a String, &'a Color); + type IntoIter = indexmap::map::Iter<'a, String, Color>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +fn rgb_to_hex(rgb: &RGB, opacity: u8) -> String { + if opacity < 255 { + format!("{:02x}{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b, opacity) + } else { + format!("{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b) + } +} + +impl Color { + fn from_hsla(hsla: css_colors::HSLA, blueprint: &Self) -> Self { + let rgb = hsla.to_rgb(); + let rgb = RGB { + r: rgb.r.as_u8(), + g: rgb.g.as_u8(), + b: rgb.b.as_u8(), + }; + let hsl = HSL { + h: hsla.h.degrees(), + s: hsla.s.as_f32(), + l: hsla.l.as_f32(), + }; + let opacity = hsla.a.as_u8(); + Self { + name: blueprint.name.clone(), + identifier: blueprint.identifier.clone(), + accent: blueprint.accent, + hex: rgb_to_hex(&rgb, opacity), + rgb, + hsl, + opacity, + } + } + + fn from_rgba(rgba: css_colors::RGBA, blueprint: &Self) -> Self { + let hsl = rgba.to_hsl(); + let rgb = RGB { + r: rgba.r.as_u8(), + g: rgba.g.as_u8(), + b: rgba.b.as_u8(), + }; + let hsl = HSL { + h: hsl.h.degrees(), + s: hsl.s.as_f32(), + l: hsl.l.as_f32(), + }; + let opacity = rgba.a.as_u8(); + Self { + name: blueprint.name.clone(), + identifier: blueprint.identifier.clone(), + accent: blueprint.accent, + hex: rgb_to_hex(&rgb, opacity), + rgb, + hsl, + opacity, + } + } + + #[must_use] + pub fn mix(base: &Self, blend: &Self, amount: f64) -> Self { + let amount = (amount * 100.0).round() as u8; + let blueprint = base; + let base: css_colors::RGBA = base.into(); + let base = base.to_rgba(); + let blend: css_colors::RGBA = blend.into(); + let result = base.mix(blend, css_colors::percent(amount)); + Self::from_rgba(result, blueprint) + } + + #[must_use] + pub fn mod_hue(&self, hue: i32) -> Self { + let mut hsl: css_colors::HSL = self.into(); + hsl.h = css_colors::deg(hue); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn add_hue(&self, hue: i32) -> Self { + let hsl: css_colors::HSL = self.into(); + let hsl = hsl.spin(css_colors::deg(hue)); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn sub_hue(&self, hue: i32) -> Self { + let hsl: css_colors::HSL = self.into(); + let hsl = hsl.spin(-css_colors::deg(hue)); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn mod_saturation(&self, saturation: u8) -> Self { + let mut hsl: css_colors::HSL = self.into(); + hsl.s = css_colors::percent(saturation); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn add_saturation(&self, saturation: u8) -> Self { + let hsl: css_colors::HSL = self.into(); + let hsl = hsl.saturate(css_colors::percent(saturation)); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn sub_saturation(&self, saturation: u8) -> Self { + let hsl: css_colors::HSL = self.into(); + let hsl = hsl.desaturate(css_colors::percent(saturation)); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn mod_lightness(&self, lightness: u8) -> Self { + let mut hsl: css_colors::HSL = self.into(); + hsl.l = css_colors::percent(lightness); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn add_lightness(&self, lightness: u8) -> Self { + let hsl: css_colors::HSL = self.into(); + let hsl = hsl.lighten(css_colors::percent(lightness)); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn sub_lightness(&self, lightness: u8) -> Self { + let hsl: css_colors::HSL = self.into(); + let hsl = hsl.darken(css_colors::percent(lightness)); + Self::from_hsla(hsl.to_hsla(), self) + } + + #[must_use] + pub fn mod_opacity(&self, opacity: f32) -> Self { + let opacity = (opacity * 255.0).round() as u8; + Self { + opacity, + hex: rgb_to_hex(&self.rgb, opacity), + ..self.clone() + } + } + + #[must_use] + pub fn add_opacity(&self, opacity: f32) -> Self { + let opacity = (opacity * 255.0).round() as u8; + let opacity = self.opacity.saturating_add(opacity); + Self { + opacity, + hex: rgb_to_hex(&self.rgb, opacity), + ..self.clone() + } + } + + #[must_use] + pub fn sub_opacity(&self, opacity: f32) -> Self { + let opacity = (opacity * 255.0).round() as u8; + let opacity = self.opacity.saturating_sub(opacity); + Self { + opacity, + hex: rgb_to_hex(&self.rgb, opacity), + ..self.clone() + } + } +} + +impl From<&Color> for css_colors::RGB { + fn from(c: &Color) -> Self { + Self { + r: css_colors::Ratio::from_u8(c.rgb.r), + g: css_colors::Ratio::from_u8(c.rgb.g), + b: css_colors::Ratio::from_u8(c.rgb.b), + } + } +} + +impl From<&Color> for css_colors::RGBA { + fn from(c: &Color) -> Self { + Self { + r: css_colors::Ratio::from_u8(c.rgb.r), + g: css_colors::Ratio::from_u8(c.rgb.g), + b: css_colors::Ratio::from_u8(c.rgb.b), + a: css_colors::percent(c.opacity), + } + } +} + +impl From<&Color> for css_colors::HSL { + fn from(c: &Color) -> Self { + Self { + h: css_colors::Angle::new(c.hsl.h), + s: css_colors::Ratio::from_f32(c.hsl.s), + l: css_colors::Ratio::from_f32(c.hsl.l), + } + } +} + +impl From<&Color> for css_colors::HSLA { + fn from(c: &Color) -> Self { + Self { + h: css_colors::Angle::new(c.hsl.h), + s: css_colors::Ratio::from_f32(c.hsl.s), + l: css_colors::Ratio::from_f32(c.hsl.l), + a: css_colors::Ratio::from_u8(c.opacity), + } + } +} diff --git a/whiskers/src/parse.rs b/whiskers/src/parse.rs deleted file mode 100644 index 48f7e657..00000000 --- a/whiskers/src/parse.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::num::ParseIntError; - -use css_colors::{rgba, Color, Ratio, HSLA, RGB, RGBA}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("invalid length {0} for hex string, expected 6 or 8 characters")] - InvalidLength(usize), - - #[error("failed to parse as base 16 integer: {0}")] - ParseInt(ParseIntError), -} - -pub trait ColorExt { - fn from_hex(hex: &str) -> Result; - fn to_hex(&self) -> String; -} - -fn hex_to_u8s(hex: &str) -> Result, ParseIntError> { - (0..hex.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&hex[i..i + 2], 16)) - .collect() -} - -impl ColorExt for RGBA { - fn from_hex(hex: &str) -> Result { - if hex.len() != 6 && hex.len() != 8 { - return Err(Error::InvalidLength(hex.len())); - } - - let components = hex_to_u8s(hex).map_err(Error::ParseInt)?; - let [red, green, blue]: [u8; 3] = components[..3] - .try_into() - .expect("guaranteed to have at least 3 elements"); - let alpha = components.get(3).copied().unwrap_or(255); - - Ok(rgba(red, green, blue, Ratio::from_u8(alpha).as_f32())) - } - - fn to_hex(&self) -> String { - if self.a.as_u8() == 255 { - let RGB { r, g, b } = self.to_rgb(); - format!("{:02x}{:02x}{:02x}", r.as_u8(), g.as_u8(), b.as_u8()) - } else { - let Self { r, g, b, a } = self; - format!( - "{:02x}{:02x}{:02x}{:02x}", - r.as_u8(), - g.as_u8(), - b.as_u8(), - a.as_u8() - ) - } - } -} - -impl ColorExt for HSLA { - fn from_hex(hex: &str) -> Result { - Ok(RGBA::from_hex(hex)?.to_hsla()) - } - - fn to_hex(&self) -> String { - self.to_rgba().to_hex() - } -} - -#[cfg(test)] -mod tests {} diff --git a/whiskers/src/postprocess.rs b/whiskers/src/postprocess.rs deleted file mode 100644 index 943c057d..00000000 --- a/whiskers/src/postprocess.rs +++ /dev/null @@ -1,24 +0,0 @@ -use base64::Engine; - -struct UnquoteReplacer; -impl regex::Replacer for UnquoteReplacer { - fn replace_append(&mut self, caps: ®ex::Captures, dst: &mut String) { - let Ok(bytes) = &base64::engine::general_purpose::STANDARD_NO_PAD.decode(&caps["b64"]) - else { - eprintln!( - "warning: failed to decode whiskers unquote section. this is probably a bug." - ); - return; - }; - let json = String::from_utf8_lossy(bytes); - dst.push_str(&json); - } -} - -#[must_use] -#[allow(clippy::missing_panics_doc)] // a panic in here means we wrote the regex wrong -pub fn postprocess(input: &str) -> String { - let pattern = regex::Regex::new(r#""\{WHISKERS:UNQUOTE:(?.*)}""#).expect("regex is valid"); - let result = pattern.replace_all(input, UnquoteReplacer).to_string(); - result -} diff --git a/whiskers/src/template.rs b/whiskers/src/template.rs deleted file mode 100644 index 22393c10..00000000 --- a/whiskers/src/template.rs +++ /dev/null @@ -1,249 +0,0 @@ -use handlebars::Handlebars; -use handlebars::HelperDef; -use indexmap::IndexMap; -use serde_json::Value; - -use crate::helper; - -pub struct Helper { - pub name: &'static str, - pub description: &'static str, - pub args: &'static [&'static str], - pub examples: &'static [(&'static str, &'static str)], - handler: Box, -} - -#[allow(clippy::too_many_lines)] -pub fn helpers() -> Vec { - vec![ - Helper { - name: "uppercase", - description: "Convert a string to uppercase.", - args: &["string"], - examples: &[("\"hello\"", "`HELLO`")], - handler: Box::new(helper::uppercase), - }, - Helper { - name: "lowercase", - description: "Convert a string to lowercase.", - args: &["string"], - examples: &[("\"HELLO\"", "`hello`")], - handler: Box::new(helper::lowercase), - }, - Helper { - name: "titlecase", - description: "Convert a string to titlecase.", - args: &["string"], - examples: &[("\"hello there\"", "`Hello There`")], - handler: Box::new(helper::titlecase), - }, - Helper { - name: "trunc", - description: "Format a number to a string with a given number of places.", - args: &["number", "places"], - examples: &[("3.14159265 2", "`3.14`")], - handler: Box::new(helper::trunc), - }, - Helper { - name: "lighten", - description: "Lighten a color by a percentage.", - args: &["color", "amount"], - examples: &[("red 0.1", "`f8bacc` / `hsl(343, 81%, 85%)`")], - handler: Box::new(helper::lighten), - }, - Helper { - name: "darken", - description: "Darken a color by a percentage.", - args: &["color", "amount"], - examples: &[("red 0.1", "`ee5c85` / `hsl(343, 81%, 65%)`")], - handler: Box::new(helper::darken), - }, - Helper { - name: "mix", - description: "Mix two colors together in a given ratio.", - args: &["color_a", "color_b", "ratio"], - examples: &[("red base 0.3", "`5e4054` (30% red, 70% base)")], - handler: Box::new(helper::mix), - }, - Helper { - name: "opacity", - description: "Set the opacity of a color.", - args: &["color", "amount"], - examples: &[("red 0.5", "`hsla(343, 81%, 75%, 0.50)`")], - handler: Box::new(helper::opacity), - }, - Helper { - name: "unquote", - description: "Marks a value to be unquoted. Mostly useful for maintaining JSON syntax highlighting in template files when a non-string value is needed.", - args: &["value"], - examples: &[("isLight true", "`true` (the surrounding quotation marks have been removed)")], - handler: Box::new(helper::unquote), - }, - Helper { - name: "rgb", - description: "Convert a color to CSS RGB format.", - args: &["color"], - examples: &[("red", "`rgb(243, 139, 168)`")], - handler: Box::new(helper::rgb), - }, - Helper { - name: "rgba", - description: "Convert a color to CSS RGBA format.", - args: &["color"], - examples: &[("(opacity red 0.6)", "`rgba(243, 139, 168, 0.60)`")], - handler: Box::new(helper::rgba), - }, - Helper { - name: "hsl", - description: "Convert a color to CSS HSL format.", - args: &["color"], - examples: &[("red", "`hsl(343, 81%, 75%)`")], - handler: Box::new(helper::hsl), - }, - Helper { - name: "hsla", - description: "Convert a color to CSS HSLA format.", - args: &["color"], - examples: &[("(opacity red 0.6)", "`hsla(343, 81%, 75%, 0.60)`")], - handler: Box::new(helper::hsla), - }, - Helper { - name: "red_i", - description: "Get the red channel of a color as an integer from 0 to 255.", - args: &["color"], - examples: &[("red", "`243`")], - handler: Box::new(helper::red_i), - }, - Helper { - name: "green_i", - description: "Get the green channel of a color as an integer from 0 to 255.", - args: &["color"], - examples: &[("red", "`139`")], - handler: Box::new(helper::green_i), - }, - Helper { - name: "blue_i", - description: "Get the blue channel of a color as an integer from 0 to 255.", - args: &["color"], - examples: &[("red", "`168`")], - handler: Box::new(helper::blue_i), - }, - Helper { - name: "alpha_i", - description: "Get the alpha channel of a color as an integer from 0 to 255.", - args: &["color"], - examples: &[("(opacity red 0.6)", "`153`")], - handler: Box::new(helper::alpha_i), - }, - Helper { - name: "red_f", - description: "Get the red channel of a color as a float from 0 to 1.", - args: &["color"], - examples: &[("red", "`0.95` (truncated to 2 places)")], - handler: Box::new(helper::red_f), - }, - Helper { - name: "green_f", - description: "Get the green channel of a color as a float from 0 to 1.", - args: &["color"], - examples: &[("red", "`0.55` (truncated to 2 places)")], - handler: Box::new(helper::green_f), - }, - Helper { - name: "blue_f", - description: "Get the blue channel of a color as a float from 0 to 1.", - args: &["color"], - examples: &[("red", "`0.66` (truncated to 2 places)")], - handler: Box::new(helper::blue_f), - }, - Helper { - name: "alpha_f", - description: "Get the alpha channel of a color as a float from 0 to 1.", - args: &["color"], - examples: &[("(opacity red 0.6)", "`0.60` (truncated to 2 places)")], - handler: Box::new(helper::alpha_f), - }, - Helper { - name: "red_h", - description: "Get the red channel of a color as hex digits from 00 to FF.", - args: &["color"], - examples: &[("red", "`f3`")], - handler: Box::new(helper::red_h), - }, - Helper { - name: "green_h", - description: "Get the green channel of a color as hex digits from 00 to FF.", - args: &["color"], - examples: &[("red", "`8b`")], - handler: Box::new(helper::green_h), - }, - Helper { - name: "blue_h", - description: "Get the blue channel of a color as hex digits from 00 to FF.", - args: &["color"], - examples: &[("red", "`a8`")], - handler: Box::new(helper::blue_h), - }, - Helper { - name: "alpha_h", - description: "Get the alpha channel of a color as hex digits from 00 to FF.", - args: &["color"], - examples: &[("(opacity red 0.6)", "`99`")], - handler: Box::new(helper::alpha_h), - }, - Helper { - name: "darklight", - description: "Choose a value depending on the current flavor. Latte is light, while Frappé, Macchiato, and Mocha are all dark.", - args: &["if-dark", "if-light"], - examples: &[("\"Night\" \"Day\"", "`Day` on Latte, `Night` on other flavors")], - handler: Box::new(helper::darklight), - }, - ] -} - -#[must_use] -pub fn make_registry() -> Handlebars<'static> { - let mut reg = Handlebars::new(); - for helper in helpers() { - reg.register_helper(helper.name, helper.handler); - } - reg.set_strict_mode(true); - reg -} - -#[must_use] -#[allow(clippy::missing_panics_doc)] // panic here implies an internal issue -pub fn make_context_all() -> Value { - let ctx: IndexMap = catppuccin::PALETTE - .into_iter() - .map(|f| (f.name.identifier().to_string(), make_context(f))) - .collect(); - serde_json::to_value(ctx).expect("context is serializable into json") -} - -#[must_use] -#[allow(clippy::missing_panics_doc)] // panic here implies an internal issue -pub fn make_context(flavor: &catppuccin::Flavor) -> Value { - let color_map: IndexMap = flavor - .colors - .into_iter() - .map(|c| { - ( - c.name.identifier().to_string(), - format!("{:02x}{:02x}{:02x}", c.rgb.r, c.rgb.g, c.rgb.b), - ) - }) - .collect(); - - let mut context = - serde_json::to_value(color_map.clone()).expect("color names & hexcodes can be serialized"); - - context["flavor"] = flavor.name.identifier().to_string().into(); - context["flavorName"] = flavor.name.to_string().into(); - context["isLight"] = (!flavor.dark).into(); - context["isDark"] = flavor.dark.into(); - context["colors"] = - serde_json::to_value(color_map).expect("color names & hexcodes can be serialized"); - - context -} diff --git a/whiskers/src/templating.rs b/whiskers/src/templating.rs new file mode 100644 index 00000000..95754fef --- /dev/null +++ b/whiskers/src/templating.rs @@ -0,0 +1,196 @@ +use indexmap::IndexMap; + +use crate::{filters, functions}; + +/// Allows creation of a [`FilterExample`] with the following syntax: +/// +/// `function_example!(mix(base=base, blend=red, amount=0.5) => "#804040")` +macro_rules! function_example { + ($name:ident($($key:ident = $value:tt),*) => $output:expr) => { + $crate::templating::FunctionExample { + inputs: { + let mut map = indexmap::IndexMap::new(); + $(map.insert(stringify!($key).to_string(), stringify!($value).to_string());)* + map + }, + output: $output.to_string(), + } + }; +} + +/// Allows creation of a [`FilterExample`] with the following syntax: +/// +/// `filter_example!(red | add(hue=30)) => "#ff6666")` +macro_rules! filter_example { + ($value:ident | $name:ident => $output:expr) => { + $crate::templating::FilterExample { + value: stringify!($value).to_string(), + inputs: indexmap::IndexMap::new(), + output: $output.to_string(), + } + }; + ($value:ident | $name:ident($($key:ident = $arg_value:tt),*) => $output:expr) => { + $crate::templating::FilterExample { + value: stringify!($value).to_string(), + inputs: { + let mut map = indexmap::IndexMap::new(); + $(map.insert(stringify!($key).to_string(), stringify!($arg_value).to_string());)* + map + }, + output: $output.to_string(), + } + }; +} + +pub fn make_engine() -> tera::Tera { + let mut tera = tera::Tera::default(); + tera.register_filter("add", filters::add); + tera.register_filter("sub", filters::sub); + tera.register_filter("mod", filters::modify); + tera.register_filter("urlencode_lzma", filters::urlencode_lzma); + tera.register_function("mix", functions::mix); + tera.register_function("if", functions::if_fn); + tera.register_function("object", functions::object); + tera +} + +#[must_use] +pub fn all_functions() -> Vec { + vec![ + Function { + name: "mix".to_string(), + description: "Mix two colors together".to_string(), + examples: vec![ + function_example!(mix(base=base, blend=red, amount=0.5) => "#804040"), + function_example!(mix(base=base, blend=red, amount=0.5) => "#804040"), + ], + }, + Function { + name: "if".to_string(), + description: "Return one value if a condition is true, and another if it's false" + .to_string(), + examples: vec![ + function_example!(if(cond=true, t=1, f=0) => "1"), + function_example!(if(cond=false, t=1, f=0) => "0"), + ], + }, + Function { + name: "object".to_string(), + description: "Create an object from the input".to_string(), + examples: vec![ + function_example!(object(a=1, b=2) => "{a: 1, b: 2}"), + function_example!(object(a=1, b=2) => "{a: 1, b: 2}"), + ], + }, + ] +} + +#[must_use] +pub fn all_filters() -> Vec { + vec![ + Filter { + name: "add".to_string(), + description: "Add a value to a color".to_string(), + examples: vec![ + filter_example!(red | add(hue=30) => "#ff6666"), + filter_example!(red | add(saturation=0.5) => "#ff6666"), + ], + }, + Filter { + name: "sub".to_string(), + description: "Subtract a value from a color".to_string(), + examples: vec![ + filter_example!(red | sub(hue=30) => "#ff6666"), + filter_example!(red | sub(saturation=0.5) => "#ff6666"), + ], + }, + Filter { + name: "mod".to_string(), + description: "Modify a color".to_string(), + examples: vec![ + filter_example!(red | mod(lightness=0.5) => "#ff6666"), + filter_example!(red | mod(opacity=0.5) => "#ff6666"), + ], + }, + Filter { + name: "urlencode_lzma".to_string(), + description: "Serialize an object into a URL-safe string with LZMA compression" + .to_string(), + examples: vec![ + filter_example!(red | urlencode_lzma => "#ff6666"), + filter_example!(red | urlencode_lzma => "#ff6666"), + ], + }, + ] +} + +#[derive(serde::Serialize)] +pub struct Function { + pub name: String, + pub description: String, + pub examples: Vec, +} + +#[derive(serde::Serialize)] +pub struct Filter { + pub name: String, + pub description: String, + pub examples: Vec, +} + +#[derive(serde::Serialize)] +pub struct FunctionExample { + pub inputs: IndexMap, + pub output: String, +} + +#[derive(serde::Serialize)] +pub struct FilterExample { + pub value: String, + pub inputs: IndexMap, + pub output: String, +} + +#[cfg(test)] +mod tests { + #[test] + fn function_example_with_single_arg() { + let example = function_example!(mix(base=base) => "#804040"); + assert_eq!(example.inputs["base"], "base"); + assert_eq!(example.output, "#804040"); + } + + #[test] + fn function_example_with_multiple_args() { + let example = function_example!(mix(base=base, blend=red, amount=0.5) => "#804040"); + assert_eq!(example.inputs["base"], "base"); + assert_eq!(example.inputs["blend"], "red"); + assert_eq!(example.inputs["amount"], "0.5"); + assert_eq!(example.output, "#804040"); + } + + #[test] + fn filter_example_with_no_args() { + let example = filter_example!(red | add => "#ff6666"); + assert_eq!(example.value, "red"); + assert_eq!(example.inputs.len(), 0); + assert_eq!(example.output, "#ff6666"); + } + + #[test] + fn filter_example_with_single_arg() { + let example = filter_example!(red | add(hue=30) => "#ff6666"); + assert_eq!(example.value, "red"); + assert_eq!(example.inputs["hue"], "30"); + assert_eq!(example.output, "#ff6666"); + } + + #[test] + fn filter_example_with_multiple_args() { + let example = filter_example!(red | add(hue=30, saturation=0.5) => "#ff6666"); + assert_eq!(example.value, "red"); + assert_eq!(example.inputs["hue"], "30"); + assert_eq!(example.inputs["saturation"], "0.5"); + assert_eq!(example.output, "#ff6666"); + } +} diff --git a/whiskers/tests/cli.rs b/whiskers/tests/cli.rs index 045bd444..1eab3cc9 100644 --- a/whiskers/tests/cli.rs +++ b/whiskers/tests/cli.rs @@ -1,67 +1,73 @@ #[cfg(test)] mod happy_path { - use std::process::Command; - - use assert_cmd::assert::OutputAssertExt; - use assert_cmd::cargo::CommandCargoExt; + use assert_cmd::Command; use predicates::prelude::predicate; + /// Test that the CLI can render a single-flavor template file #[test] - fn example_file_has_flavor_mocha() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("whiskers")?; - let expected = include_str!("../examples/demo/output/mocha.md"); - cmd.arg("examples/demo/input.hbs").arg("mocha"); - cmd.assert() + fn test_single() { + let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); + let assert = cmd + .args(["tests/fixtures/single/single.tera", "-f", "latte"]) + .assert(); + assert .success() - .stdout(predicate::str::diff(expected)); - Ok(()) + .stdout(include_str!("fixtures/single/single.md")); } + /// Test that the CLI can render a multi-flavor template file #[test] - fn single_file_has_flavor_all() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("whiskers")?; - let expected = include_str!("../examples/single-file/simple/output.md"); - cmd.arg("examples/single-file/simple/input.hbs").arg("all"); - cmd.assert() + fn test_multi() { + let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); + let assert = cmd.args(["tests/fixtures/multi/multi.tera"]).assert(); + assert .success() - .stdout(predicate::str::diff(expected)); - Ok(()) + .stdout(include_str!("fixtures/multi/multi.md")); + } + + /// Test that the CLI can render a multi-flavor matrix template + #[test] + fn test_multifile_render() { + let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); + let assert = cmd + .args(["--dry-run", "tests/fixtures/multifile.tera"]) + .assert(); + assert.success().stdout(predicate::str::contains( + "catppuccin-macchiato-yellow-no-italics.ini", + )); } } #[cfg(test)] mod sad_path { - use std::process::Command; - - use assert_cmd::assert::OutputAssertExt; - use assert_cmd::cargo::CommandCargoExt; + use assert_cmd::Command; use predicates::prelude::predicate; #[test] - fn nonexistent_template_file() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("whiskers")?; - cmd.arg("test/file/doesnt/exist").arg("mocha"); - cmd.assert().failure(); - Ok(()) + fn nonexistent_template_file() { + let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); + cmd.arg("test/file/doesnt/exist"); + cmd.assert().failure().stderr(predicate::str::contains( + "Template contents could not be read", + )); } #[test] - fn invalid_flavor() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("whiskers")?; - cmd.arg("examples/demo/input.hbs").arg("invalid"); + fn invalid_flavor() { + let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); + cmd.arg("examples/demo/input.tera") + .args(["--flavor", "invalid"]); cmd.assert().failure().stderr(predicate::str::contains( - "error: invalid value 'invalid' for '[FLAVOR]'", + "error: invalid value 'invalid' for '--flavor '", )); - Ok(()) } #[test] - fn template_contains_invalid_syntax() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("whiskers")?; - cmd.arg("examples/errors.hbs").arg("mocha"); + fn template_contains_invalid_syntax() { + let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); + cmd.arg("examples/errors.tera").args(["-f", "mocha"]); cmd.assert() .failure() - .stderr(predicate::str::contains("Failed to render template")); - Ok(()) + .stderr(predicate::str::contains("Error: Template is invalid")); } } diff --git a/whiskers/tests/fixtures/multi/multi.md b/whiskers/tests/fixtures/multi/multi.md new file mode 100644 index 00000000..769c8211 --- /dev/null +++ b/whiskers/tests/fixtures/multi/multi.md @@ -0,0 +1,124 @@ + +# Latte + +| Color | Hex | RGB | HSL | +|-------|-----|-----|-----| +Rosewater | #dc8a78 | rgb(220, 138, 120) | hsl(11, 59%, 67%) | +Flamingo | #dd7878 | rgb(221, 120, 120) | hsl(0, 60%, 67%) | +Pink | #ea76cb | rgb(234, 118, 203) | hsl(316, 73%, 69%) | +Mauve | #8839ef | rgb(136, 57, 239) | hsl(266, 85%, 58%) | +Red | #d20f39 | rgb(210, 15, 57) | hsl(347, 87%, 44%) | +Maroon | #e64553 | rgb(230, 69, 83) | hsl(355, 76%, 59%) | +Peach | #fe640b | rgb(254, 100, 11) | hsl(22, 99%, 52%) | +Yellow | #df8e1d | rgb(223, 142, 29) | hsl(35, 77%, 49%) | +Green | #40a02b | rgb(64, 160, 43) | hsl(109, 58%, 40%) | +Teal | #179299 | rgb(23, 146, 153) | hsl(183, 74%, 35%) | +Sky | #04a5e5 | rgb(4, 165, 229) | hsl(197, 97%, 46%) | +Sapphire | #209fb5 | rgb(32, 159, 181) | hsl(189, 70%, 42%) | +Blue | #1e66f5 | rgb(30, 102, 245) | hsl(220, 91%, 54%) | +Lavender | #7287fd | rgb(114, 135, 253) | hsl(231, 97%, 72%) | +Text | #4c4f69 | rgb(76, 79, 105) | hsl(234, 16%, 35%) | +Subtext 1 | #5c5f77 | rgb(92, 95, 119) | hsl(233, 13%, 41%) | +Subtext 0 | #6c6f85 | rgb(108, 111, 133) | hsl(233, 10%, 47%) | +Overlay 2 | #7c7f93 | rgb(124, 127, 147) | hsl(232, 10%, 53%) | +Overlay 1 | #8c8fa1 | rgb(140, 143, 161) | hsl(231, 10%, 59%) | +Overlay 0 | #9ca0b0 | rgb(156, 160, 176) | hsl(228, 11%, 65%) | +Surface 2 | #acb0be | rgb(172, 176, 190) | hsl(227, 12%, 71%) | +Surface 1 | #bcc0cc | rgb(188, 192, 204) | hsl(225, 14%, 77%) | +Surface 0 | #ccd0da | rgb(204, 208, 218) | hsl(223, 16%, 83%) | +Base | #eff1f5 | rgb(239, 241, 245) | hsl(220, 23%, 95%) | +Mantle | #e6e9ef | rgb(230, 233, 239) | hsl(220, 22%, 92%) | +Crust | #dce0e8 | rgb(220, 224, 232) | hsl(220, 21%, 89%) | + +# Frappé + +| Color | Hex | RGB | HSL | +|-------|-----|-----|-----| +Rosewater | #f2d5cf | rgb(242, 213, 207) | hsl(10, 57%, 88%) | +Flamingo | #eebebe | rgb(238, 190, 190) | hsl(0, 59%, 84%) | +Pink | #f4b8e4 | rgb(244, 184, 228) | hsl(316, 73%, 84%) | +Mauve | #ca9ee6 | rgb(202, 158, 230) | hsl(277, 59%, 76%) | +Red | #e78284 | rgb(231, 130, 132) | hsl(359, 68%, 71%) | +Maroon | #ea999c | rgb(234, 153, 156) | hsl(358, 66%, 76%) | +Peach | #ef9f76 | rgb(239, 159, 118) | hsl(20, 79%, 70%) | +Yellow | #e5c890 | rgb(229, 200, 144) | hsl(40, 62%, 73%) | +Green | #a6d189 | rgb(166, 209, 137) | hsl(96, 44%, 68%) | +Teal | #81c8be | rgb(129, 200, 190) | hsl(172, 39%, 65%) | +Sky | #99d1db | rgb(153, 209, 219) | hsl(189, 48%, 73%) | +Sapphire | #85c1dc | rgb(133, 193, 220) | hsl(199, 55%, 69%) | +Blue | #8caaee | rgb(140, 170, 238) | hsl(222, 74%, 74%) | +Lavender | #babbf1 | rgb(186, 187, 241) | hsl(239, 66%, 84%) | +Text | #c6d0f5 | rgb(198, 208, 245) | hsl(227, 70%, 87%) | +Subtext 1 | #b5bfe2 | rgb(181, 191, 226) | hsl(227, 44%, 80%) | +Subtext 0 | #a5adce | rgb(165, 173, 206) | hsl(228, 29%, 73%) | +Overlay 2 | #949cbb | rgb(148, 156, 187) | hsl(228, 22%, 66%) | +Overlay 1 | #838ba7 | rgb(131, 139, 167) | hsl(227, 17%, 58%) | +Overlay 0 | #737994 | rgb(115, 121, 148) | hsl(229, 13%, 52%) | +Surface 2 | #626880 | rgb(98, 104, 128) | hsl(228, 13%, 44%) | +Surface 1 | #51576d | rgb(81, 87, 109) | hsl(227, 15%, 37%) | +Surface 0 | #414559 | rgb(65, 69, 89) | hsl(230, 16%, 30%) | +Base | #303446 | rgb(48, 52, 70) | hsl(229, 19%, 23%) | +Mantle | #292c3c | rgb(41, 44, 60) | hsl(231, 19%, 20%) | +Crust | #232634 | rgb(35, 38, 52) | hsl(229, 20%, 17%) | + +# Macchiato + +| Color | Hex | RGB | HSL | +|-------|-----|-----|-----| +Rosewater | #f4dbd6 | rgb(244, 219, 214) | hsl(10, 58%, 90%) | +Flamingo | #f0c6c6 | rgb(240, 198, 198) | hsl(0, 58%, 86%) | +Pink | #f5bde6 | rgb(245, 189, 230) | hsl(316, 74%, 85%) | +Mauve | #c6a0f6 | rgb(198, 160, 246) | hsl(267, 83%, 80%) | +Red | #ed8796 | rgb(237, 135, 150) | hsl(351, 74%, 73%) | +Maroon | #ee99a0 | rgb(238, 153, 160) | hsl(355, 71%, 77%) | +Peach | #f5a97f | rgb(245, 169, 127) | hsl(21, 86%, 73%) | +Yellow | #eed49f | rgb(238, 212, 159) | hsl(40, 70%, 78%) | +Green | #a6da95 | rgb(166, 218, 149) | hsl(105, 48%, 72%) | +Teal | #8bd5ca | rgb(139, 213, 202) | hsl(171, 47%, 69%) | +Sky | #91d7e3 | rgb(145, 215, 227) | hsl(189, 59%, 73%) | +Sapphire | #7dc4e4 | rgb(125, 196, 228) | hsl(199, 66%, 69%) | +Blue | #8aadf4 | rgb(138, 173, 244) | hsl(220, 83%, 75%) | +Lavender | #b7bdf8 | rgb(183, 189, 248) | hsl(234, 82%, 85%) | +Text | #cad3f5 | rgb(202, 211, 245) | hsl(227, 68%, 88%) | +Subtext 1 | #b8c0e0 | rgb(184, 192, 224) | hsl(228, 39%, 80%) | +Subtext 0 | #a5adcb | rgb(165, 173, 203) | hsl(227, 27%, 72%) | +Overlay 2 | #939ab7 | rgb(147, 154, 183) | hsl(228, 20%, 65%) | +Overlay 1 | #8087a2 | rgb(128, 135, 162) | hsl(228, 15%, 57%) | +Overlay 0 | #6e738d | rgb(110, 115, 141) | hsl(230, 12%, 49%) | +Surface 2 | #5b6078 | rgb(91, 96, 120) | hsl(230, 14%, 41%) | +Surface 1 | #494d64 | rgb(73, 77, 100) | hsl(231, 16%, 34%) | +Surface 0 | #363a4f | rgb(54, 58, 79) | hsl(230, 19%, 26%) | +Base | #24273a | rgb(36, 39, 58) | hsl(232, 23%, 18%) | +Mantle | #1e2030 | rgb(30, 32, 48) | hsl(233, 23%, 15%) | +Crust | #181926 | rgb(24, 25, 38) | hsl(236, 23%, 12%) | + +# Mocha + +| Color | Hex | RGB | HSL | +|-------|-----|-----|-----| +Rosewater | #f5e0dc | rgb(245, 224, 220) | hsl(10, 56%, 91%) | +Flamingo | #f2cdcd | rgb(242, 205, 205) | hsl(0, 59%, 88%) | +Pink | #f5c2e7 | rgb(245, 194, 231) | hsl(316, 72%, 86%) | +Mauve | #cba6f7 | rgb(203, 166, 247) | hsl(267, 84%, 81%) | +Red | #f38ba8 | rgb(243, 139, 168) | hsl(343, 81%, 75%) | +Maroon | #eba0ac | rgb(235, 160, 172) | hsl(350, 65%, 77%) | +Peach | #fab387 | rgb(250, 179, 135) | hsl(23, 92%, 75%) | +Yellow | #f9e2af | rgb(249, 226, 175) | hsl(41, 86%, 83%) | +Green | #a6e3a1 | rgb(166, 227, 161) | hsl(115, 54%, 76%) | +Teal | #94e2d5 | rgb(148, 226, 213) | hsl(170, 57%, 73%) | +Sky | #89dceb | rgb(137, 220, 235) | hsl(189, 71%, 73%) | +Sapphire | #74c7ec | rgb(116, 199, 236) | hsl(199, 76%, 69%) | +Blue | #89b4fa | rgb(137, 180, 250) | hsl(217, 92%, 76%) | +Lavender | #b4befe | rgb(180, 190, 254) | hsl(232, 97%, 85%) | +Text | #cdd6f4 | rgb(205, 214, 244) | hsl(226, 64%, 88%) | +Subtext 1 | #bac2de | rgb(186, 194, 222) | hsl(227, 35%, 80%) | +Subtext 0 | #a6adc8 | rgb(166, 173, 200) | hsl(228, 24%, 72%) | +Overlay 2 | #9399b2 | rgb(147, 153, 178) | hsl(228, 17%, 64%) | +Overlay 1 | #7f849c | rgb(127, 132, 156) | hsl(230, 13%, 55%) | +Overlay 0 | #6c7086 | rgb(108, 112, 134) | hsl(231, 11%, 47%) | +Surface 2 | #585b70 | rgb(88, 91, 112) | hsl(233, 12%, 39%) | +Surface 1 | #45475a | rgb(69, 71, 90) | hsl(234, 13%, 31%) | +Surface 0 | #313244 | rgb(49, 50, 68) | hsl(237, 16%, 23%) | +Base | #1e1e2e | rgb(30, 30, 46) | hsl(240, 21%, 15%) | +Mantle | #181825 | rgb(24, 24, 37) | hsl(240, 21%, 12%) | +Crust | #11111b | rgb(17, 17, 27) | hsl(240, 23%, 9%) | diff --git a/whiskers/tests/fixtures/multi/multi.tera b/whiskers/tests/fixtures/multi/multi.tera new file mode 100644 index 00000000..b1bda761 --- /dev/null +++ b/whiskers/tests/fixtures/multi/multi.tera @@ -0,0 +1,22 @@ +--- +whiskers: + version: 2.0.0 +--- +{%- macro css_rgb(v) -%} +rgb({{ v.r }}, {{ v.g }}, {{ v.b }}) +{%- endmacro -%} + +{%- macro css_hsl(v) -%} +hsl({{ v.h | round }}, {{ v.s * 100 | round }}%, {{ v.l * 100 | round }}%) +{%- endmacro -%} + +{% for flavor_key, flavor in flavors %} +# {{ flavor.name }} + +| Color | Hex | RGB | HSL | +|-------|-----|-----|-----| +{%- for color_key, color in flavor.colors %} +{{ color.name }} | #{{ color.hex }} | {{ self::css_rgb(v=color.rgb) }} | {{ self::css_hsl(v=color.hsl) }} | + +{%- endfor %} +{% endfor %} \ No newline at end of file diff --git a/whiskers/tests/fixtures/multifile.tera b/whiskers/tests/fixtures/multifile.tera new file mode 100644 index 00000000..ebb365ea --- /dev/null +++ b/whiskers/tests/fixtures/multifile.tera @@ -0,0 +1,12 @@ +--- +whiskers: + version: 2.0.0 + matrix: + - variant: ["normal", "no-italics"] + - flavor + - accent + filename: "catppuccin-{{flavor.identifier}}-{{accent}}-{{variant}}.ini" +--- +# Catppuccin {{flavor.name}}{% if variant == "no-italics" %} (no italics){% endif %} +[theme] +{{accent}}: #{{flavor.colors[accent].hex}} \ No newline at end of file diff --git a/whiskers/tests/fixtures/single/single.md b/whiskers/tests/fixtures/single/single.md new file mode 100644 index 00000000..01cb25f9 --- /dev/null +++ b/whiskers/tests/fixtures/single/single.md @@ -0,0 +1,35 @@ +# Latte + +| Color | Hex | RGB | HSL | +|-------|-----|-----|-----| +Rosewater | #dc8a78 | rgb(220, 138, 120) | hsl(11, 59%, 67%) | +Flamingo | #dd7878 | rgb(221, 120, 120) | hsl(0, 60%, 67%) | +Pink | #ea76cb | rgb(234, 118, 203) | hsl(316, 73%, 69%) | +Mauve | #8839ef | rgb(136, 57, 239) | hsl(266, 85%, 58%) | +Red | #d20f39 | rgb(210, 15, 57) | hsl(347, 87%, 44%) | +Maroon | #e64553 | rgb(230, 69, 83) | hsl(355, 76%, 59%) | +Peach | #fe640b | rgb(254, 100, 11) | hsl(22, 99%, 52%) | +Yellow | #df8e1d | rgb(223, 142, 29) | hsl(35, 77%, 49%) | +Green | #40a02b | rgb(64, 160, 43) | hsl(109, 58%, 40%) | +Teal | #179299 | rgb(23, 146, 153) | hsl(183, 74%, 35%) | +Sky | #04a5e5 | rgb(4, 165, 229) | hsl(197, 97%, 46%) | +Sapphire | #209fb5 | rgb(32, 159, 181) | hsl(189, 70%, 42%) | +Blue | #1e66f5 | rgb(30, 102, 245) | hsl(220, 91%, 54%) | +Lavender | #7287fd | rgb(114, 135, 253) | hsl(231, 97%, 72%) | +Text | #4c4f69 | rgb(76, 79, 105) | hsl(234, 16%, 35%) | +Subtext 1 | #5c5f77 | rgb(92, 95, 119) | hsl(233, 13%, 41%) | +Subtext 0 | #6c6f85 | rgb(108, 111, 133) | hsl(233, 10%, 47%) | +Overlay 2 | #7c7f93 | rgb(124, 127, 147) | hsl(232, 10%, 53%) | +Overlay 1 | #8c8fa1 | rgb(140, 143, 161) | hsl(231, 10%, 59%) | +Overlay 0 | #9ca0b0 | rgb(156, 160, 176) | hsl(228, 11%, 65%) | +Surface 2 | #acb0be | rgb(172, 176, 190) | hsl(227, 12%, 71%) | +Surface 1 | #bcc0cc | rgb(188, 192, 204) | hsl(225, 14%, 77%) | +Surface 0 | #ccd0da | rgb(204, 208, 218) | hsl(223, 16%, 83%) | +Base | #eff1f5 | rgb(239, 241, 245) | hsl(220, 23%, 95%) | +Mantle | #e6e9ef | rgb(230, 233, 239) | hsl(220, 22%, 92%) | +Crust | #dce0e8 | rgb(220, 224, 232) | hsl(220, 21%, 89%) | + +red: #d20f39 / hsl(347, 87%, 44%) +orangey: #d3470f / hsl(17, 87%, 44%) +green: #40a02b / hsl(109, 58%, 40%) +dark green: #205016 // hsl(109, 58%, 20%) \ No newline at end of file diff --git a/whiskers/tests/fixtures/single/single.tera b/whiskers/tests/fixtures/single/single.tera new file mode 100644 index 00000000..00028db5 --- /dev/null +++ b/whiskers/tests/fixtures/single/single.tera @@ -0,0 +1,26 @@ +--- +whiskers: + version: 2.0.0 +--- +{%- macro css_rgb(v) -%} +rgb({{ v.r }}, {{ v.g }}, {{ v.b }}) +{%- endmacro -%} + +{%- macro css_hsl(v) -%} +hsl({{ v.h | round }}, {{ v.s * 100 | round }}%, {{ v.l * 100 | round }}%) +{%- endmacro -%} + +# {{ flavor.name }} + +| Color | Hex | RGB | HSL | +|-------|-----|-----|-----| +{%- for _, color in flavor.colors %} +{{ color.name }} | #{{ color.hex }} | {{ self::css_rgb(v=color.rgb) }} | {{ self::css_hsl(v=color.hsl) }} | +{%- endfor %} + +{% set orange = red | add(hue=30) -%} +{% set darkgreen = green | sub(lightness=20) -%} +red: #{{ red.hex }} / {{ self::css_hsl(v=red.hsl) }} +orangey: #{{ orange.hex }} / {{ self::css_hsl(v=orange.hsl) }} +green: #{{ green.hex }} / {{ self::css_hsl(v=green.hsl) }} +dark green: #{{ darkgreen.hex }} // {{ self::css_hsl(v=darkgreen.hsl) }} \ No newline at end of file diff --git a/whiskers/tests/fixtures/singlefile-multiflavor.tera b/whiskers/tests/fixtures/singlefile-multiflavor.tera new file mode 100644 index 00000000..9ce6243b --- /dev/null +++ b/whiskers/tests/fixtures/singlefile-multiflavor.tera @@ -0,0 +1,10 @@ +--- +whiskers: + version: 2.0.0 +accent: mauve +--- +{% for _, flavor in flavors %} +# Catppuccin {{flavor.name}} +[theme] +{{accent}}: #{{flavor.colors[accent].hex}} +{% endfor %} \ No newline at end of file diff --git a/whiskers/tests/fixtures/singlefile-singleflavor.tera b/whiskers/tests/fixtures/singlefile-singleflavor.tera new file mode 100644 index 00000000..c0465900 --- /dev/null +++ b/whiskers/tests/fixtures/singlefile-singleflavor.tera @@ -0,0 +1,8 @@ +--- +whiskers: + version: 2.0.0 +accent: mauve +--- +# Catppuccin {{flavor.name}} +[theme] +{{accent}}: #{{flavor.colors[accent].hex}} \ No newline at end of file From 2ea377fc6d4eff387384b6df765b098227796746 Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 25 Mar 2024 21:42:53 +0000 Subject: [PATCH 02/11] feat(whiskers): convert demo example to tera --- Cargo.lock | 381 +++++++++------------ whiskers/examples/demo/input.tera | 32 +- whiskers/examples/demo/output/frappe.md | 14 +- whiskers/examples/demo/output/latte.md | 14 +- whiskers/examples/demo/output/macchiato.md | 12 +- whiskers/examples/demo/output/mocha.md | 12 +- whiskers/src/filters.rs | 34 ++ whiskers/src/functions.rs | 64 ++-- whiskers/src/models.rs | 4 +- whiskers/src/templating.rs | 64 +++- 10 files changed, 321 insertions(+), 310 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 710d7d05..1dd34ef9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.11" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -57,36 +57,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -97,9 +97,9 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "assert_cmd" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" +checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" dependencies = [ "anstyle", "bstr", @@ -129,9 +129,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -156,9 +156,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "regex-automata", @@ -182,9 +182,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "byteorder" @@ -194,9 +194,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "catppuccin" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3e1ab359c6e570aa60787010e3b70cf21ee4748726bceba8235f85068e1b64" +checksum = "3c5471d0652fadf9e2947e0e045769eed34acc57c36e8bd3679239bcd6d786e5" dependencies = [ "css-colors", "itertools", @@ -245,12 +245,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" @@ -267,7 +264,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.0", + "windows-targets", ] [[package]] @@ -294,9 +291,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -313,9 +310,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -325,18 +322,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.4" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffe91f06a11b4b9420f62103854e90867812cd5d01557f853c5ee8e791b12ae" +checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", @@ -352,9 +349,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "color-eyre" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" dependencies = [ "backtrace", "eyre", @@ -377,18 +374,18 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] @@ -481,14 +478,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -506,15 +503,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "fdeflate" -version = "0.3.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] @@ -561,9 +558,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "globset" @@ -591,15 +588,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -666,9 +663,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -686,15 +683,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -738,9 +735,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lzma-rust" @@ -753,15 +750,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", "simd-adler32", @@ -775,27 +772,27 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "owo-colors" @@ -826,9 +823,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.5" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" dependencies = [ "memchr", "thiserror", @@ -837,9 +834,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.5" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" dependencies = [ "pest", "pest_generator", @@ -847,9 +844,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.5" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" dependencies = [ "pest", "pest_meta", @@ -860,9 +857,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.5" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" dependencies = [ "once_cell", "pest", @@ -909,15 +906,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "png" -version = "0.17.10" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -964,9 +961,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1021,9 +1018,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1033,9 +1030,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1090,22 +1087,22 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" @@ -1127,18 +1124,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", @@ -1147,9 +1144,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "indexmap", "itoa", @@ -1159,9 +1156,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.27" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", @@ -1211,9 +1208,9 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "syn" -version = "2.0.49" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote", @@ -1229,7 +1226,7 @@ dependencies = [ "cfg-if", "fastrand", "rustix", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -1262,18 +1259,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -1350,9 +1347,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "utf8parse" @@ -1399,9 +1396,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1409,9 +1406,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -1424,9 +1421,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1434,9 +1431,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -1447,15 +1444,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -1498,16 +1495,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -1516,134 +1504,77 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "xflags" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4554b580522d0ca238369c16b8f6ce34524d61dafe7244993754bbd05f2c2ea" +checksum = "7d9e15fbb3de55454b0106e314b28e671279009b363e6f1d8e39fdc3bf048944" dependencies = [ "xflags-macros", ] [[package]] name = "xflags-macros" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e7b3ca8977093aae6b87b6a7730216fc4c53a6530bab5c43a783cd810c1a8" +checksum = "672423d4fea7ffa2f6c25ba60031ea13dc6258070556f125cc4d790007d4a155" diff --git a/whiskers/examples/demo/input.tera b/whiskers/examples/demo/input.tera index 3206b305..d4c7926a 100644 --- a/whiskers/examples/demo/input.tera +++ b/whiskers/examples/demo/input.tera @@ -1,27 +1,27 @@ +--- +whiskers: + version: "2.0.0" +--- ## Demo **flavor:** {{ flavor.name }} ### Colours -- **red:** #{{ red.hex }} / {{ rgb red }} / {{ hsl red }} -- **components:** r: {{ red_i red }} / {{ trunc (red_f red) 2 }}, g: {{ green_i red }} / {{ trunc (green_f red) 2 }}, b: {{ blue_i red }} / {{ trunc (blue_f red) 2 }} -- **alpha:** {{ alpha_i (opacity red 0.6) }} / {{ trunc (alpha_f (opacity red 0.6)) 2 }} -- **10% lighter:** #{{ lighten red 0.1 }} / {{ rgb (lighten red 0.1) }} / {{ hsl (lighten red 0.1) }} -- **10% darker:** #{{ darken red 0.1 }} / {{ rgb (darken red 0.1) }} / {{ hsl (darken red 0.1) }} +- **red:** #{{ red.hex }} / {{ css_rgb(color=red) }} / {{ css_hsl(color=red) }} +- **components:** r: {{ red.rgb.r }} / {{ red.rgb.r / 255 | trunc(places=2) }}, g: {{ red.rgb.g }} / {{ red.rgb.g / 255 | trunc(places=2) }}, b: {{ red.rgb.b }} / {{ red.rgb.b / 255 | trunc(places=2) }} +- **alpha:** {{ red.opacity }} / {{ red.opacity / 255 | trunc(places=2) }} +{% set lightred = red | add(lightness=10) -%} +- **10% lighter:** #{{ lightred.hex }} / {{ css_rgb(color=lightred) }} / {{ css_hsl(color=lightred) }} +{% set darkred = red | sub(lightness=10) -%} +- **10% darker:** #{{ darkred.hex }} / {{ css_rgb(color=darkred) }} / {{ css_hsl(color=darkred) }} -{{ #with (mix red base 0.3) as | red | }} -- **30% mix with base:** #{{ red }} / {{ rgb red }} / {{ hsl red }} -{{ /with }} +{% set palered = red | mix(color=base, amount=0.3) -%} +- **30% mix with base:** #{{ palered.hex }} / {{ css_rgb(color=palered) }} / {{ css_hsl(color=palered) }} -{{ #with (opacity red 0.5) as | red | }} -- **50% opacity:** #{{ red }} / {{ rgba red }} / {{ hsla red }} -{{ /with }} +{% set fadered = red | mod(opacity=0.5) -%} +- **50% opacity:** #{{ fadered.hex }} / {{ css_rgba(color=fadered) }} / {{ css_hsla(color=fadered) }} ### Conditionals -this is a {{ darklight "dark" "light" }} theme - -### Misc - -unquote this: "{{ unquote isLight }}" +this is a {{ if(cond=flavor.dark, t="dark", f="light") }} theme \ No newline at end of file diff --git a/whiskers/examples/demo/output/frappe.md b/whiskers/examples/demo/output/frappe.md index ef84de93..284ef6c2 100644 --- a/whiskers/examples/demo/output/frappe.md +++ b/whiskers/examples/demo/output/frappe.md @@ -6,18 +6,14 @@ - **red:** #e78284 / rgb(231, 130, 132) / hsl(359, 68%, 71%) - **components:** r: 231 / 0.91, g: 130 / 0.51, b: 132 / 0.52 -- **alpha:** 153 / 0.60 -- **10% lighter:** #f0aeb0 / rgb(240, 174, 176) / hsl(358, 69%, 81%) +- **alpha:** 255 / 1.00 +- **10% lighter:** #f0aeb0 / rgb(240, 174, 176) / hsl(359, 68%, 81%) - **10% darker:** #df5759 / rgb(223, 87, 89) / hsl(359, 68%, 61%) -- **30% mix with base:** #684c59 / rgb(104, 76, 89) / hsl(332, 16%, 35%) +- **30% mix with base:** #684b59 / rgb(104, 75, 89) / hsl(331, 16%, 35%) -- **50% opacity:** #e7838480 / rgba(231, 131, 132, 0.50) / hsla(359, 67%, 71%, 0.50) +- **50% opacity:** #e7828480 / rgba(231, 130, 132, 0.50) / hsla(359, 68%, 71%, 0.50) ### Conditionals -this is a dark theme - -### Misc - -unquote this: false +this is a dark theme \ No newline at end of file diff --git a/whiskers/examples/demo/output/latte.md b/whiskers/examples/demo/output/latte.md index 265be979..f9bfe94d 100644 --- a/whiskers/examples/demo/output/latte.md +++ b/whiskers/examples/demo/output/latte.md @@ -6,18 +6,14 @@ - **red:** #d20f39 / rgb(210, 15, 57) / hsl(347, 87%, 44%) - **components:** r: 210 / 0.82, g: 15 / 0.06, b: 57 / 0.22 -- **alpha:** 153 / 0.60 +- **alpha:** 255 / 1.00 - **10% lighter:** #f02652 / rgb(240, 38, 82) / hsl(347, 87%, 55%) -- **10% darker:** #a20c2c / rgb(162, 12, 44) / hsl(347, 86%, 34%) +- **10% darker:** #a20c2c / rgb(162, 12, 44) / hsl(347, 87%, 34%) -- **30% mix with base:** #e7adbd / rgb(231, 173, 189) / hsl(343, 55%, 79%) +- **30% mix with base:** #e6adbc / rgb(230, 173, 188) / hsl(344, 53%, 79%) -- **50% opacity:** #d30f3a80 / rgba(211, 15, 58, 0.50) / hsla(347, 87%, 44%, 0.50) +- **50% opacity:** #d20f3980 / rgba(210, 15, 57, 0.50) / hsla(347, 87%, 44%, 0.50) ### Conditionals -this is a light theme - -### Misc - -unquote this: true \ No newline at end of file +this is a light theme \ No newline at end of file diff --git a/whiskers/examples/demo/output/macchiato.md b/whiskers/examples/demo/output/macchiato.md index 5b9b2fa1..86725fa8 100644 --- a/whiskers/examples/demo/output/macchiato.md +++ b/whiskers/examples/demo/output/macchiato.md @@ -6,18 +6,14 @@ - **red:** #ed8796 / rgb(237, 135, 150) / hsl(351, 74%, 73%) - **components:** r: 237 / 0.93, g: 135 / 0.53, b: 150 / 0.59 -- **alpha:** 153 / 0.60 -- **10% lighter:** #f4b4be / rgb(244, 180, 190) / hsl(351, 75%, 83%) +- **alpha:** 255 / 1.00 +- **10% lighter:** #f4b4be / rgb(244, 180, 190) / hsl(351, 74%, 83%) - **10% darker:** #e65a6f / rgb(230, 90, 111) / hsl(351, 74%, 63%) -- **30% mix with base:** #624455 / rgb(98, 68, 85) / hsl(326, 18%, 33%) +- **30% mix with base:** #614455 / rgb(97, 68, 85) / hsl(325, 18%, 33%) - **50% opacity:** #ed879680 / rgba(237, 135, 150, 0.50) / hsla(351, 74%, 73%, 0.50) ### Conditionals -this is a dark theme - -### Misc - -unquote this: false \ No newline at end of file +this is a dark theme \ No newline at end of file diff --git a/whiskers/examples/demo/output/mocha.md b/whiskers/examples/demo/output/mocha.md index e9cb2b89..3d5aa62c 100644 --- a/whiskers/examples/demo/output/mocha.md +++ b/whiskers/examples/demo/output/mocha.md @@ -6,18 +6,14 @@ - **red:** #f38ba8 / rgb(243, 139, 168) / hsl(343, 81%, 75%) - **components:** r: 243 / 0.95, g: 139 / 0.55, b: 168 / 0.66 -- **alpha:** 153 / 0.60 -- **10% lighter:** #f8bacc / rgb(248, 186, 204) / hsl(343, 82%, 85%) +- **alpha:** 255 / 1.00 +- **10% lighter:** #f8bacc / rgb(248, 186, 204) / hsl(343, 81%, 85%) - **10% darker:** #ee5c85 / rgb(238, 92, 133) / hsl(343, 81%, 65%) -- **30% mix with base:** #5e4054 / rgb(94, 64, 84) / hsl(320, 19%, 31%) +- **30% mix with base:** #5e3f53 / rgb(94, 63, 83) / hsl(321, 20%, 31%) - **50% opacity:** #f38ba880 / rgba(243, 139, 168, 0.50) / hsla(343, 81%, 75%, 0.50) ### Conditionals -this is a dark theme - -### Misc - -unquote this: false \ No newline at end of file +this is a dark theme \ No newline at end of file diff --git a/whiskers/src/filters.rs b/whiskers/src/filters.rs index 3c28e209..4dc8ba2c 100644 --- a/whiskers/src/filters.rs +++ b/whiskers/src/filters.rs @@ -7,6 +7,27 @@ use base64::Engine as _; use crate::models::Color; +pub fn mix( + value: &tera::Value, + args: &HashMap, +) -> Result { + let base: Color = tera::from_value(value.clone())?; + let blend: Color = tera::from_value( + args.get("color") + .ok_or_else(|| tera::Error::msg("blend color is required"))? + .clone(), + )?; + let amount = args + .get("amount") + .ok_or_else(|| tera::Error::msg("blend amount is required"))? + .as_f64() + .ok_or_else(|| tera::Error::msg("blend amount must be a number"))?; + + let result = Color::mix(&base, &blend, amount); + + Ok(tera::to_value(result)?) +} + pub fn modify( value: &tera::Value, args: &HashMap, @@ -98,3 +119,16 @@ pub fn urlencode_lzma( let encoded = base64::engine::general_purpose::URL_SAFE.encode(compressed); Ok(tera::to_value(encoded)?) } + +pub fn trunc( + value: &tera::Value, + args: &HashMap, +) -> Result { + let value: f64 = tera::from_value(value.clone())?; + let places: usize = tera::from_value( + args.get("places") + .ok_or_else(|| tera::Error::msg("number of places is required"))? + .clone(), + )?; + Ok(tera::to_value(format!("{value:.places$}"))?) +} diff --git a/whiskers/src/functions.rs b/whiskers/src/functions.rs index b486a5e8..ef5334bf 100644 --- a/whiskers/src/functions.rs +++ b/whiskers/src/functions.rs @@ -2,28 +2,6 @@ use std::collections::{BTreeMap, HashMap}; use crate::models::Color; -pub fn mix(args: &HashMap) -> Result { - let base: Color = tera::from_value( - args.get("base") - .ok_or_else(|| tera::Error::msg("base color is required"))? - .clone(), - )?; - let blend: Color = tera::from_value( - args.get("blend") - .ok_or_else(|| tera::Error::msg("blend color is required"))? - .clone(), - )?; - let amount = args - .get("amount") - .ok_or_else(|| tera::Error::msg("amount is required"))? - .as_f64() - .ok_or_else(|| tera::Error::msg("amount must be a number"))?; - - let result = Color::mix(&base, &blend, amount); - - Ok(tera::to_value(result)?) -} - pub fn if_fn(args: &HashMap) -> Result { let cond = args .get("cond") @@ -47,3 +25,45 @@ pub fn object(args: &HashMap) -> Result = args.iter().collect(); Ok(tera::to_value(args)?) } + +pub fn css_rgb(args: &HashMap) -> Result { + let color: Color = tera::from_value( + args.get("color") + .ok_or_else(|| tera::Error::msg("color is required"))? + .clone(), + )?; + + let color: css_colors::RGB = (&color).into(); + Ok(tera::to_value(color.to_string())?) +} + +pub fn css_rgba(args: &HashMap) -> Result { + let color: Color = tera::from_value( + args.get("color") + .ok_or_else(|| tera::Error::msg("color is required"))? + .clone(), + )?; + let color: css_colors::RGBA = (&color).into(); + Ok(tera::to_value(color.to_string())?) +} + +pub fn css_hsl(args: &HashMap) -> Result { + let color: Color = tera::from_value( + args.get("color") + .ok_or_else(|| tera::Error::msg("color is required"))? + .clone(), + )?; + + let color: css_colors::HSL = (&color).into(); + Ok(tera::to_value(color.to_string())?) +} + +pub fn css_hsla(args: &HashMap) -> Result { + let color: Color = tera::from_value( + args.get("color") + .ok_or_else(|| tera::Error::msg("color is required"))? + .clone(), + )?; + let color: css_colors::HSLA = (&color).into(); + Ok(tera::to_value(color.to_string())?) +} diff --git a/whiskers/src/models.rs b/whiskers/src/models.rs index 61ca418f..6779ae0d 100644 --- a/whiskers/src/models.rs +++ b/whiskers/src/models.rs @@ -267,7 +267,7 @@ impl Color { #[must_use] pub fn mix(base: &Self, blend: &Self, amount: f64) -> Self { - let amount = (amount * 100.0).round() as u8; + let amount = (amount * 100.0).clamp(0.0, 100.0).round() as u8; let blueprint = base; let base: css_colors::RGBA = base.into(); let base = base.to_rgba(); @@ -388,7 +388,7 @@ impl From<&Color> for css_colors::RGBA { r: css_colors::Ratio::from_u8(c.rgb.r), g: css_colors::Ratio::from_u8(c.rgb.g), b: css_colors::Ratio::from_u8(c.rgb.b), - a: css_colors::percent(c.opacity), + a: css_colors::Ratio::from_u8(c.opacity), } } } diff --git a/whiskers/src/templating.rs b/whiskers/src/templating.rs index 95754fef..3e40d930 100644 --- a/whiskers/src/templating.rs +++ b/whiskers/src/templating.rs @@ -22,14 +22,14 @@ macro_rules! function_example { /// /// `filter_example!(red | add(hue=30)) => "#ff6666")` macro_rules! filter_example { - ($value:ident | $name:ident => $output:expr) => { + ($value:tt | $name:ident => $output:expr) => { $crate::templating::FilterExample { value: stringify!($value).to_string(), inputs: indexmap::IndexMap::new(), output: $output.to_string(), } }; - ($value:ident | $name:ident($($key:ident = $arg_value:tt),*) => $output:expr) => { + ($value:tt | $name:ident($($key:ident = $arg_value:tt),*) => $output:expr) => { $crate::templating::FilterExample { value: stringify!($value).to_string(), inputs: { @@ -48,23 +48,20 @@ pub fn make_engine() -> tera::Tera { tera.register_filter("sub", filters::sub); tera.register_filter("mod", filters::modify); tera.register_filter("urlencode_lzma", filters::urlencode_lzma); - tera.register_function("mix", functions::mix); + tera.register_filter("trunc", filters::trunc); + tera.register_filter("mix", filters::mix); tera.register_function("if", functions::if_fn); tera.register_function("object", functions::object); + tera.register_function("css_rgb", functions::css_rgb); + tera.register_function("css_rgba", functions::css_rgba); + tera.register_function("css_hsl", functions::css_hsl); + tera.register_function("css_hsla", functions::css_hsla); tera } #[must_use] pub fn all_functions() -> Vec { vec![ - Function { - name: "mix".to_string(), - description: "Mix two colors together".to_string(), - examples: vec![ - function_example!(mix(base=base, blend=red, amount=0.5) => "#804040"), - function_example!(mix(base=base, blend=red, amount=0.5) => "#804040"), - ], - }, Function { name: "if".to_string(), description: "Return one value if a condition is true, and another if it's false" @@ -82,6 +79,38 @@ pub fn all_functions() -> Vec { function_example!(object(a=1, b=2) => "{a: 1, b: 2}"), ], }, + Function { + name: "css_rgb".to_string(), + description: "Convert a color to an RGB CSS string".to_string(), + examples: vec![ + function_example!(css_rgb(color=red) => "rgb(255, 0, 0)"), + function_example!(css_rgb(color=red) => "rgb(255, 0, 0)"), + ], + }, + Function { + name: "css_rgba".to_string(), + description: "Convert a color to an RGBA CSS string".to_string(), + examples: vec![ + function_example!(css_rgba(color=red) => "rgba(255, 0, 0, 1)"), + function_example!(css_rgba(color=red) => "rgba(255, 0, 0, 1)"), + ], + }, + Function { + name: "css_hsl".to_string(), + description: "Convert a color to an HSL CSS string".to_string(), + examples: vec![ + function_example!(css_hsl(color=red) => "hsl(0, 100%, 50%)"), + function_example!(css_hsl(color=red) => "hsl(0, 100%, 50%)"), + ], + }, + Function { + name: "css_hsla".to_string(), + description: "Convert a color to an HSLA CSS string".to_string(), + examples: vec![ + function_example!(css_hsla(color=red) => "hsla(0, 100%, 50%, 1)"), + function_example!(css_hsla(color=red) => "hsla(0, 100%, 50%, 1)"), + ], + }, ] } @@ -112,6 +141,14 @@ pub fn all_filters() -> Vec { filter_example!(red | mod(opacity=0.5) => "#ff6666"), ], }, + Filter { + name: "mix".to_string(), + description: "Mix two colors together".to_string(), + examples: vec![ + filter_example!(red | mix(color=base, amount=0.5) => "#804040"), + filter_example!(red | mix(color=base, amount=0.5) => "#804040"), + ], + }, Filter { name: "urlencode_lzma".to_string(), description: "Serialize an object into a URL-safe string with LZMA compression" @@ -121,6 +158,11 @@ pub fn all_filters() -> Vec { filter_example!(red | urlencode_lzma => "#ff6666"), ], }, + Filter { + name: "trunc".to_string(), + description: "Truncate a number to a certain number of places".to_string(), + examples: vec![filter_example!(1.123456 | trunc(places=3) => "1.123")], + }, ] } From 8d5cd41a92994f4f61cba0722e28a4665a2e5cf1 Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 25 Mar 2024 21:51:43 +0000 Subject: [PATCH 03/11] feat(whiskers): convert frontmatter example to tera --- whiskers/examples/frontmatter/input.tera | 42 +++++++++---------- .../examples/frontmatter/output/frappe.md | 14 +++---- whiskers/examples/frontmatter/output/latte.md | 14 +++---- .../examples/frontmatter/output/macchiato.md | 12 ++---- whiskers/examples/frontmatter/output/mocha.md | 12 ++---- 5 files changed, 38 insertions(+), 56 deletions(-) diff --git a/whiskers/examples/frontmatter/input.tera b/whiskers/examples/frontmatter/input.tera index 43f43a1d..964b8a74 100644 --- a/whiskers/examples/frontmatter/input.tera +++ b/whiskers/examples/frontmatter/input.tera @@ -1,41 +1,39 @@ --- -# like jetbrains -parent: "{{ darklight 'darcula' 'default' }}" -# alternative built-in method -accent: "{{ #if isLight }}{{ pink }}{{ ^ }}{{ mauve }}{{ /if }}" +whiskers: + version: "2.0.0" +dark_accent: mauve +light_accent: pink --- +{% set parent = if(cond=flavor.dark, t='darcula', f='default') -%} +{% set accent = if(cond=flavor.dark, t=dark_accent, f=light_accent) -%} ## Demo With Frontmatter -**flavor:** {{ flavorName }} +**flavor:** {{ flavor.name }} This file also contains variables that have been defined in the frontmatter, as shown below: ### Frontmatter Variables - **parent** is {{ parent }} -- **accent** is #{{ accent }} +- **accent** is #{{ flavor.colors[accent].hex }} ### Colours -- **red:** #{{ red }} / {{ rgb red }} / {{ hsl red }} -- **components:** r: {{ red_i red }} / {{ trunc (red_f red) 2 }}, g: {{ green_i red }} / {{ trunc (green_f red) 2 }}, b: {{ blue_i red }} / {{ trunc (blue_f red) 2 }} -- **alpha:** {{ alpha_i (opacity red 0.6) }} / {{ trunc (alpha_f (opacity red 0.6)) 2 }} -- **10% lighter:** #{{ lighten red 0.1 }} / {{ rgb (lighten red 0.1) }} / {{ hsl (lighten red 0.1) }} -- **10% darker:** #{{ darken red 0.1 }} / {{ rgb (darken red 0.1) }} / {{ hsl (darken red 0.1) }} +- **red:** #{{ red.hex }} / {{ css_rgb(color=red) }} / {{ css_hsl(color=red) }} +- **components:** r: {{ red.rgb.r }} / {{ red.rgb.r / 255 | trunc(places=2) }}, g: {{ red.rgb.g }} / {{ red.rgb.g / 255 | trunc(places=2) }}, b: {{ red.rgb.b }} / {{ red.rgb.b / 255 | trunc(places=2) }} +- **alpha:** {{ red.opacity }} / {{ red.opacity / 255 | trunc(places=2) }} +{% set lightred = red | add(lightness=10) -%} +- **10% lighter:** #{{ lightred.hex }} / {{ css_rgb(color=lightred) }} / {{ css_hsl(color=lightred) }} +{% set darkred = red | sub(lightness=10) -%} +- **10% darker:** #{{ darkred.hex }} / {{ css_rgb(color=darkred) }} / {{ css_hsl(color=darkred) }} -{{ #with (mix red base 0.3) as | red | }} -- **30% mix with base:** #{{ red }} / {{ rgb red }} / {{ hsl red }} -{{ /with }} +{% set palered = red | mix(color=base, amount=0.3) -%} +- **30% mix with base:** #{{ palered.hex }} / {{ css_rgb(color=palered) }} / {{ css_hsl(color=palered) }} -{{ #with (opacity red 0.5) as | red | }} -- **50% opacity:** #{{ red }} / {{ rgba red }} / {{ hsla red }} -{{ /with }} +{% set fadered = red | mod(opacity=0.5) -%} +- **50% opacity:** #{{ fadered.hex }} / {{ css_rgba(color=fadered) }} / {{ css_hsla(color=fadered) }} ### Conditionals -this is a {{ darklight "dark" "light" }} theme - -### Misc - -unquote this: "{{ unquote isLight }}" +this is a {{ if(cond=flavor.dark, t="dark", f="light") }} theme \ No newline at end of file diff --git a/whiskers/examples/frontmatter/output/frappe.md b/whiskers/examples/frontmatter/output/frappe.md index 943d2fdc..50b8f1a9 100644 --- a/whiskers/examples/frontmatter/output/frappe.md +++ b/whiskers/examples/frontmatter/output/frappe.md @@ -13,18 +13,14 @@ This file also contains variables that have been defined in the frontmatter, as - **red:** #e78284 / rgb(231, 130, 132) / hsl(359, 68%, 71%) - **components:** r: 231 / 0.91, g: 130 / 0.51, b: 132 / 0.52 -- **alpha:** 153 / 0.60 -- **10% lighter:** #f0aeb0 / rgb(240, 174, 176) / hsl(358, 69%, 81%) +- **alpha:** 255 / 1.00 +- **10% lighter:** #f0aeb0 / rgb(240, 174, 176) / hsl(359, 68%, 81%) - **10% darker:** #df5759 / rgb(223, 87, 89) / hsl(359, 68%, 61%) -- **30% mix with base:** #684c59 / rgb(104, 76, 89) / hsl(332, 16%, 35%) +- **30% mix with base:** #684b59 / rgb(104, 75, 89) / hsl(331, 16%, 35%) -- **50% opacity:** #e7838480 / rgba(231, 131, 132, 0.50) / hsla(359, 67%, 71%, 0.50) +- **50% opacity:** #e7828480 / rgba(231, 130, 132, 0.50) / hsla(359, 68%, 71%, 0.50) ### Conditionals -this is a dark theme - -### Misc - -unquote this: false +this is a dark theme \ No newline at end of file diff --git a/whiskers/examples/frontmatter/output/latte.md b/whiskers/examples/frontmatter/output/latte.md index 89094144..b0780759 100644 --- a/whiskers/examples/frontmatter/output/latte.md +++ b/whiskers/examples/frontmatter/output/latte.md @@ -13,18 +13,14 @@ This file also contains variables that have been defined in the frontmatter, as - **red:** #d20f39 / rgb(210, 15, 57) / hsl(347, 87%, 44%) - **components:** r: 210 / 0.82, g: 15 / 0.06, b: 57 / 0.22 -- **alpha:** 153 / 0.60 +- **alpha:** 255 / 1.00 - **10% lighter:** #f02652 / rgb(240, 38, 82) / hsl(347, 87%, 55%) -- **10% darker:** #a20c2c / rgb(162, 12, 44) / hsl(347, 86%, 34%) +- **10% darker:** #a20c2c / rgb(162, 12, 44) / hsl(347, 87%, 34%) -- **30% mix with base:** #e7adbd / rgb(231, 173, 189) / hsl(343, 55%, 79%) +- **30% mix with base:** #e6adbc / rgb(230, 173, 188) / hsl(344, 53%, 79%) -- **50% opacity:** #d30f3a80 / rgba(211, 15, 58, 0.50) / hsla(347, 87%, 44%, 0.50) +- **50% opacity:** #d20f3980 / rgba(210, 15, 57, 0.50) / hsla(347, 87%, 44%, 0.50) ### Conditionals -this is a light theme - -### Misc - -unquote this: true \ No newline at end of file +this is a light theme \ No newline at end of file diff --git a/whiskers/examples/frontmatter/output/macchiato.md b/whiskers/examples/frontmatter/output/macchiato.md index f164a9e4..18f9345c 100644 --- a/whiskers/examples/frontmatter/output/macchiato.md +++ b/whiskers/examples/frontmatter/output/macchiato.md @@ -13,18 +13,14 @@ This file also contains variables that have been defined in the frontmatter, as - **red:** #ed8796 / rgb(237, 135, 150) / hsl(351, 74%, 73%) - **components:** r: 237 / 0.93, g: 135 / 0.53, b: 150 / 0.59 -- **alpha:** 153 / 0.60 -- **10% lighter:** #f4b4be / rgb(244, 180, 190) / hsl(351, 75%, 83%) +- **alpha:** 255 / 1.00 +- **10% lighter:** #f4b4be / rgb(244, 180, 190) / hsl(351, 74%, 83%) - **10% darker:** #e65a6f / rgb(230, 90, 111) / hsl(351, 74%, 63%) -- **30% mix with base:** #624455 / rgb(98, 68, 85) / hsl(326, 18%, 33%) +- **30% mix with base:** #614455 / rgb(97, 68, 85) / hsl(325, 18%, 33%) - **50% opacity:** #ed879680 / rgba(237, 135, 150, 0.50) / hsla(351, 74%, 73%, 0.50) ### Conditionals -this is a dark theme - -### Misc - -unquote this: false \ No newline at end of file +this is a dark theme \ No newline at end of file diff --git a/whiskers/examples/frontmatter/output/mocha.md b/whiskers/examples/frontmatter/output/mocha.md index c82a23fd..70301bc1 100644 --- a/whiskers/examples/frontmatter/output/mocha.md +++ b/whiskers/examples/frontmatter/output/mocha.md @@ -13,18 +13,14 @@ This file also contains variables that have been defined in the frontmatter, as - **red:** #f38ba8 / rgb(243, 139, 168) / hsl(343, 81%, 75%) - **components:** r: 243 / 0.95, g: 139 / 0.55, b: 168 / 0.66 -- **alpha:** 153 / 0.60 -- **10% lighter:** #f8bacc / rgb(248, 186, 204) / hsl(343, 82%, 85%) +- **alpha:** 255 / 1.00 +- **10% lighter:** #f8bacc / rgb(248, 186, 204) / hsl(343, 81%, 85%) - **10% darker:** #ee5c85 / rgb(238, 92, 133) / hsl(343, 81%, 65%) -- **30% mix with base:** #5e4054 / rgb(94, 64, 84) / hsl(320, 19%, 31%) +- **30% mix with base:** #5e3f53 / rgb(94, 63, 83) / hsl(321, 20%, 31%) - **50% opacity:** #f38ba880 / rgba(243, 139, 168, 0.50) / hsla(343, 81%, 75%, 0.50) ### Conditionals -this is a dark theme - -### Misc - -unquote this: false \ No newline at end of file +this is a dark theme \ No newline at end of file From 8276a800d8862aaa02dbf59d4ea93b7b8546770a Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 25 Mar 2024 21:57:49 +0000 Subject: [PATCH 04/11] feat(whiskers): convert simple single-file example to tera --- .../examples/single-file/simple/input.tera | 24 ++++--- .../examples/single-file/simple/output.md | 64 +++++++++---------- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/whiskers/examples/single-file/simple/input.tera b/whiskers/examples/single-file/simple/input.tera index 828f00bc..2d299e5b 100644 --- a/whiskers/examples/single-file/simple/input.tera +++ b/whiskers/examples/single-file/simple/input.tera @@ -1,8 +1,12 @@ +--- +whiskers: + version: "2.0.0" +--- # Catppuccin Palette v0.2.0 -{{#each flavors}} +{% for _, flavor in flavors -%}

-{{flavorName}} +{{flavor.name}} @@ -11,15 +15,15 @@ - {{#each colors as | color colorName |}} + {%- for _, color in flavor.colors %} - - - - - + + + + + - {{/each}} + {%- endfor %}
RGB HSL
{{titlecase colorName}}#{{color}}{{rgb color}}{{hsl color}}{{color.name}}#{{color.hex}}{{css_rgb(color=color)}}{{css_hsl(color=color)}}
-{{/each}} +{% endfor %} diff --git a/whiskers/examples/single-file/simple/output.md b/whiskers/examples/single-file/simple/output.md index 37a2b08f..99fd1363 100644 --- a/whiskers/examples/single-file/simple/output.md +++ b/whiskers/examples/single-file/simple/output.md @@ -117,56 +117,56 @@ - Subtext1 + Subtext 1 #5c5f77 rgb(92, 95, 119) hsl(233, 13%, 42%) - Subtext0 + Subtext 0 #6c6f85 rgb(108, 111, 133) hsl(233, 10%, 47%) - Overlay2 + Overlay 2 #7c7f93 rgb(124, 127, 147) hsl(232, 10%, 53%) - Overlay1 + Overlay 1 #8c8fa1 rgb(140, 143, 161) hsl(231, 10%, 59%) - Overlay0 + Overlay 0 #9ca0b0 rgb(156, 160, 176) hsl(228, 11%, 65%) - Surface2 + Surface 2 #acb0be rgb(172, 176, 190) hsl(227, 12%, 71%) - Surface1 + Surface 1 #bcc0cc rgb(188, 192, 204) hsl(225, 14%, 77%) - Surface0 + Surface 0 #ccd0da rgb(204, 208, 218) hsl(223, 16%, 83%) @@ -311,56 +311,56 @@ - Subtext1 + Subtext 1 #b5bfe2 rgb(181, 191, 226) hsl(227, 44%, 80%) - Subtext0 + Subtext 0 #a5adce rgb(165, 173, 206) hsl(228, 29%, 73%) - Overlay2 + Overlay 2 #949cbb rgb(148, 156, 187) hsl(228, 22%, 66%) - Overlay1 + Overlay 1 #838ba7 rgb(131, 139, 167) hsl(227, 17%, 58%) - Overlay0 + Overlay 0 #737994 rgb(115, 121, 148) hsl(229, 13%, 52%) - Surface2 + Surface 2 #626880 rgb(98, 104, 128) hsl(228, 13%, 44%) - Surface1 + Surface 1 #51576d rgb(81, 87, 109) hsl(227, 15%, 37%) - Surface0 + Surface 0 #414559 rgb(65, 69, 89) hsl(230, 16%, 30%) @@ -505,56 +505,56 @@ - Subtext1 + Subtext 1 #b8c0e0 rgb(184, 192, 224) hsl(228, 39%, 80%) - Subtext0 + Subtext 0 #a5adcb rgb(165, 173, 203) hsl(227, 27%, 72%) - Overlay2 + Overlay 2 #939ab7 rgb(147, 154, 183) hsl(228, 20%, 65%) - Overlay1 + Overlay 1 #8087a2 rgb(128, 135, 162) hsl(228, 15%, 57%) - Overlay0 + Overlay 0 #6e738d rgb(110, 115, 141) hsl(230, 12%, 49%) - Surface2 + Surface 2 #5b6078 rgb(91, 96, 120) hsl(230, 14%, 42%) - Surface1 + Surface 1 #494d64 rgb(73, 77, 100) hsl(231, 16%, 34%) - Surface0 + Surface 0 #363a4f rgb(54, 58, 79) hsl(230, 19%, 26%) @@ -699,56 +699,56 @@ - Subtext1 + Subtext 1 #bac2de rgb(186, 194, 222) hsl(227, 35%, 80%) - Subtext0 + Subtext 0 #a6adc8 rgb(166, 173, 200) hsl(228, 24%, 72%) - Overlay2 + Overlay 2 #9399b2 rgb(147, 153, 178) hsl(228, 17%, 64%) - Overlay1 + Overlay 1 #7f849c rgb(127, 132, 156) hsl(230, 13%, 56%) - Overlay0 + Overlay 0 #6c7086 rgb(108, 112, 134) hsl(231, 11%, 47%) - Surface2 + Surface 2 #585b70 rgb(88, 91, 112) hsl(233, 12%, 39%) - Surface1 + Surface 1 #45475a rgb(69, 71, 90) hsl(234, 13%, 31%) - Surface0 + Surface 0 #313244 rgb(49, 50, 68) hsl(237, 16%, 23%) From f32da865ce457dd862ea2168617a4bad42badaee Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 25 Mar 2024 22:11:55 +0000 Subject: [PATCH 05/11] feat(whiskers): convert overrides example to tera --- .../examples/single-file/overrides/input.tera | 37 ++++++++++--------- .../examples/single-file/overrides/output.md | 1 - 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/whiskers/examples/single-file/overrides/input.tera b/whiskers/examples/single-file/overrides/input.tera index 35e54873..7c8b6f1f 100644 --- a/whiskers/examples/single-file/overrides/input.tera +++ b/whiskers/examples/single-file/overrides/input.tera @@ -1,32 +1,35 @@ --- +whiskers: + version: "2.0.0" + # Set default accent color -accent: "{{mauve}}" +accent: "mauve" + # Set custom variables user: "@sgoudham" + overrides: - # And you can override variables for specific flavors latte: - accent: "{{pink}}" - emoji: "🌻" user: "@backwardspy" + accent: "pink" + emoji: "🌻" frappe: - accent: "{{blue}}" - emoji: "🪴" - flavor: "frappé" user: "@nullishamy" - macchiato: # defaults are used + accent: "blue" + emoji: "🪴" + macchiato: emoji: "🌺" mocha: - accent: "{{sky}}" - emoji: "🌿" user: "@nekowinston" + accent: "sky" + emoji: "🌿" --- - # Single File With Overrides +{% for id, flavor in flavors %} +{% set o = overrides[id] -%} +{% set user = o | get(key="user", default=user) -%} +{% set accent = o | get(key="accent", default=accent) -%} +## {{o.emoji}} {{flavor.name}} -{{#each flavors}} -## {{emoji}} {{flavorName}} - -{{user}}'s favourite hex code is #{{accent}} - -{{/each}} +{{user}}'s favourite hex code is #{{flavor.colors[accent].hex}} +{% endfor %} diff --git a/whiskers/examples/single-file/overrides/output.md b/whiskers/examples/single-file/overrides/output.md index b5137001..00a45217 100644 --- a/whiskers/examples/single-file/overrides/output.md +++ b/whiskers/examples/single-file/overrides/output.md @@ -15,4 +15,3 @@ ## 🌿 Mocha @nekowinston's favourite hex code is #89dceb - From b6de260184402b736837f2dedbf1b57ae5e28089 Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 25 Mar 2024 22:49:05 +0000 Subject: [PATCH 06/11] docs(whiskers): update for 2.0 --- whiskers/README.md | 423 +++++++++++++++++++++++++-------------------- 1 file changed, 233 insertions(+), 190 deletions(-) diff --git a/whiskers/README.md b/whiskers/README.md index 0d3020d4..a5182e36 100644 --- a/whiskers/README.md +++ b/whiskers/README.md @@ -37,77 +37,168 @@ into. $ whiskers --help Soothing port creation tool for the high-spirited! -Usage: whiskers [OPTIONS] [TEMPLATE] [FLAVOR] +Usage: whiskers [OPTIONS] [TEMPLATE] Arguments: - [TEMPLATE] Path to the template file to render, or `-` for stdin - [FLAVOR] Flavor to get colors from [possible values: latte, frappe, macchiato, mocha, all] + [TEMPLATE] + Path to the template file, or - for stdin Options: - --overrides The overrides to apply to the template in JSON format - -o, --output-path Path to write to instead of stdout - --check Instead of printing a result, check if anything would change - -l, --list-helpers List all template helpers in Markdown format - -h, --help Print help - -V, --version Print version + -f, --flavor + Render a single flavor instead of all four + + [possible values: latte, frappe, macchiato, mocha] + + --color-overrides + Set color overrides + + --overrides + Set frontmatter overrides + + --check [] + Instead of creating an output, check it against an example + + In single-output mode, a path to the example file must be provided. In multi-output mode, no path is required and, if one is provided, it will be ignored. + + --dry-run + Dry run, don't write anything to disk + + -l, --list-functions + List all Tera filters and functions + + -o, --output-format + Output format of --list-functions + + [default: json] + [possible values: json, yaml, markdown, markdown-table] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version + ``` ## Template -Please familiarize yourself with [Handlebars](https://handlebarsjs.com/guide/), -which is the templating engine used in whiskers. +Please familiarize yourself with [Tera](https://keats.github.io/tera/), +which is the templating engine used in Whiskers. ### Context Variables The following variables are available for use in your templates: -| Variable | Description | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `flavor` (string) | The name of the flavor being templated. Possible values: `latte`, `frappé`, `macchiato`, `mocha`. | -| `isLight` (bool) | True if `flavor` is `latte`, false otherwise. | -| `isDark` (bool) | True unless `flavor` is `latte`. | -| `rosewater`, `flamingo`, `pink`, [(etc.)](https://github.com/catppuccin/rust/blob/5124eb99eb98d7111dca24537d428a6078e5bbb6/src/flavour.rs#L41-L66) (string) | All named colors in each flavor, each color is formatted as hex by default. | -| `colors` (array) | An array containing all of the named colors. | -| `flavors` (array) | An array containing all of the named flavors, with every other context variable.
See [Single File Support](#Single-File-Support) for more information. | -| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. | - -### Helpers - -The following custom helpers are available: - -| Helper
(`<>` values are args) | Input | Output | Description | -| ------------------------------------- | --------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| uppercase \ | `{{ uppercase "hello" }}` | `HELLO` | Convert a string to uppercase. | -| lowercase \ | `{{ lowercase "HELLO" }}` | `hello` | Convert a string to lowercase. | -| titlecase \ | `{{ titlecase "hello there" }}` | `Hello There` | Convert a string to titlecase. | -| trunc \ \ | `{{ trunc 3.14159265 2 }}` | `3.14` | Format a number to a string with a given number of places. | -| lighten \ \ | `{{ lighten red 0.1 }}` | `f8bacc` / `hsl(343, 81%, 85%)` | Lighten a color by a percentage. | -| darken \ \ | `{{ darken red 0.1 }}` | `ee5c85` / `hsl(343, 81%, 65%)` | Darken a color by a percentage. | -| mix \ \ \ | `{{ mix red base 0.3 }}` | `5e4054` (30% red, 70% base) | Mix two colors together in a given ratio. | -| opacity \ \ | `{{ opacity red 0.5 }}` | `hsla(343, 81%, 75%, 0.50)` | Set the opacity of a color. | -| unquote \ | `"{{ unquote isLight true }}"` | `true` (the surrounding quotation marks have been removed) | Marks a value to be unquoted. Mostly useful for maintaining JSON syntax highlighting in template files when a non-string value is needed. | -| rgb \ | `{{ rgb red }}` | `rgb(243, 139, 168)` | Convert a color to CSS RGB format. | -| rgba \ | `{{ rgba (opacity red 0.6) }}` | `rgba(243, 139, 168, 0.60)` | Convert a color to CSS RGBA format. | -| hsl \ | `{{ hsl red }}` | `hsl(343, 81%, 75%)` | Convert a color to CSS HSL format. | -| hsla \ | `{{ hsla (opacity red 0.6) }}` | `hsla(343, 81%, 75%, 0.60)` | Convert a color to CSS HSLA format. | -| red_i \ | `{{ red_i red }}` | `243` | Get the red channel of a color as an integer from 0 to 255. | -| green_i \ | `{{ green_i red }}` | `139` | Get the green channel of a color as an integer from 0 to 255. | -| blue_i \ | `{{ blue_i red }}` | `168` | Get the blue channel of a color as an integer from 0 to 255. | -| alpha_i \ | `{{ alpha_i (opacity red 0.6) }}` | `153` | Get the alpha channel of a color as an integer from 0 to 255. | -| red_f \ | `{{ red_f red }}` | `0.95` (truncated to 2 places) | Get the red channel of a color as a float from 0 to 1. | -| green_f \ | `{{ green_f red }}` | `0.55` (truncated to 2 places) | Get the green channel of a color as a float from 0 to 1. | -| blue_f \ | `{{ blue_f red }}` | `0.66` (truncated to 2 places) | Get the blue channel of a color as a float from 0 to 1. | -| alpha_f \ | `{{ alpha_f (opacity red 0.6) }}` | `0.60` (truncated to 2 places) | Get the alpha channel of a color as a float from 0 to 1. | -| red_h \ | `{{ red_h red }}` | `f3` | Get the red channel of a color as a hexadecimal number from 00 to ff. | -| green_h \ | `{{ green_h red }}` | `8b` | Get the green channel of a color as a hexadecimal number from 00 to ff. | -| blue_h \ | `{{ blue_h red }}` | `a8` | Get the blue channel of a color as a hexadecimal number from 00 to ff. | -| alpha_h \ | `{{ alpha_h (opacity red 0.6) }}` | `99` | Get the alpha channel of a color as a hexadecimal number from 00 to ff. | -| darklight \ \ | `{{ darklight "Night" "Day" }}` | `Day` on Latte, `Night` on other flavors | Choose a value depending on the current flavor. Latte is light, while Frappé, Macchiato, and Mocha are all dark. | +#### Single-Flavor Mode + +| Variable | Description | +| - | - | +| `flavor` ([`Flavor`](#flavor)) | The flavor being templated. | +| `rosewater`, `flamingo`, `pink`, [etc.](https://github.com/catppuccin/catppuccin#-palette) ([`Color`](#color)) | All colors of the flavor being templated. | +| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. | + +#### Multi-Flavor Mode + +| Variable | Description | +| - | - | +| `flavors` (Map\) | An array containing all of the named flavors, with every other context variable. | +| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. | + +#### Types + +These types are designed to closely match the [palette.json](https://github.com/catppuccin/palette/blob/main/palette.json). + +##### Flavor + +| Field | Type | Description | Examples | +| - | - | - | - | +| `name` | `String` | The name of the flavor. | `"Latte"`, `"Frappé"`, `"Macchiato"`, `"Mocha"` | +| `identifier` | `String` | The identifier of the flavor. | `"latte"`, `"frappe"`, `"macchiato"`, `"mocha"` | +| `dark` | `bool` | Whether the flavor is dark. | `false` for Latte, `true` for others | +| `light` | `bool` | Whether the flavor is light. | `true` for Latte, `false` for others | +| `colors` | `Map` | A map of color identifiers to their respective values. | | + +##### Color + +| Field | Type | Description | Examples | +| - | - | - | - | +| `name` | `String` | The name of the color. | `"Rosewater"`, `"Surface 0"`, `"Base"` | +| `identifier` | `String` | The identifier of the color. | `"rosewater"`, `"surface0"`, `"base"` | +| `accent` | `bool` | Whether the color is an accent color. | | +| `hex` | `String` | The color in hexadecimal format. | `"1e1e2e"` | +| `rgb` | `RGB` | The color in RGB format. | | +| `hsl` | `HSL` | The color in HSL format. | | +| `opacity` | `u8` | The opacity of the color. | `0` to `255` | + +##### RGB + +| Field | Type | Description | +| - | - | - | +| `r` | `u8` | The red channel of the color. | +| `g` | `u8` | The green channel of the color. | +| `b` | `u8` | The blue channel of the color. | + +##### HSL + +| Field | Type | Description | +| - | - | - | +| `h` | `u16` | The hue of the color. | +| `s` | `u8` | The saturation of the color. | +| `l` | `u8` | The lightness of the color. | + +### Functions + +| Name | Description | Examples | +|------|-------------|----------| +| `if` | Return one value if a condition is true, and another if it's false | `if(cond=true, t=1, f=0)` => `1` | +| `object` | Create an object from the input | `object(a=1, b=2)` => `{a: 1, b: 2}` | +| `css_rgb` | Convert a color to an RGB CSS string | `css_rgb(color=red)` => `rgb(255, 0, 0)` | +| `css_rgba` | Convert a color to an RGBA CSS string | `css_rgba(color=red)` => `rgba(255, 0, 0, 1)` | +| `css_hsl` | Convert a color to an HSL CSS string | `css_hsl(color=red)` => `hsl(0, 100%, 50%)` | +| `css_hsla` | Convert a color to an HSLA CSS string | `css_hsla(color=red)` => `hsla(0, 100%, 50%, 1)` | + +### Filters + +| Name | Description | Examples | +|------|-------------|----------| +| `add` | Add a value to a color | `red \| add(hue=30)` => `#ff6666` | +| `sub` | Subtract a value from a color | `red \| sub(hue=30)` => `#ff6666` | +| `mod` | Modify a color | `red \| mod(lightness=0.5)` => `#ff6666` | +| `mix` | Mix two colors together | `red \| mix(color=base, amount=0.5)` => `#804040` | +| `urlencode_lzma` | Serialize an object into a URL-safe string with LZMA compression | `red \| urlencode_lzma()` => `#ff6666` | +| `trunc` | Truncate a number to a certain number of places | `1.123456 \| trunc(places=3)` => `1.123` | ## Frontmatter -You can include additional context variables in the templating process by adding -it to an optional YAML frontmatter section at the top of your template file. +Whiskers templates may include a frontmatter section at the top of the file. + +The frontmatter is a YAML block that contains metadata about the template. If +present, the frontmatter section must be the first thing in the file and must +take the form of valid YAML set between triple-dashed lines. + +### Template Version + +The most important frontmatter key is the Whiskers version. This key allows +Whiskers to ensure that it is rendering a template that it can understand. + +Example: + +```yaml +--- +whiskers: + version: "2.0.0" +--- +... standard template content goes here ... +``` + +If the version key is not present, Whiskers will display a warning and attempt +to render the template anyway. However, it is recommended to always include the +version key to ensure compatibility with future versions of Whiskers. + +### Frontmatter Variables + +You can also include additional context variables in the templating process by +adding them to your template's frontmatter. As a simple example, given the following template (`example.cfg`): @@ -118,11 +209,11 @@ author: 'winston' --- # Catppuccin for {{app}} # by {{author}} -bg = '{{base}}' -fg = '{{text}}' +bg = '{{base.hex}}' +fg = '{{text.hex}}' ``` -Running `whiskers example.cfg mocha` produces the following output: +Running `whiskers example.cfg -f mocha` produces the following output: ```yaml # Catppuccin for Pepperjack @@ -131,20 +222,18 @@ bg = '1e1e2e' fg = 'cdd6f4' ``` -Values in YAML frontmatter are rendered in the same way as the rest of the -template, which means you can also make use of context variables in your -frontmatter. This can be useful for things like setting an accent color: +A common use of frontmatter is setting an accent color for the theme: -```yaml +``` --- -accent: "{{mauve}}" -darkGreen: "{{darken green 0.3}}" +accent: "mauve" --- -bg = "#{{base}}" -fg = "#{{text}}" -border = "#{{accent}}" -diffAddFg = "#{{green}}" -diffAddBg = "#{{darkGreen}}" +{% set darkGreen = green | sub(lightness=30) %} +bg = "#{{base.hex}}" +fg = "#{{text.hex}}" +border = "#{{flavor.colors[accent].hex}}" +diffAddFg = "#{{green.hex}}" +diffAddBg = "#{{darkGreen.hex}}" ``` Rendering the above template produces the following output: @@ -161,6 +250,9 @@ diffaddbg = "#40b436" ### Frontmatter +> [!IMPORTANT] +> This feature is currently unavailable + Whiskers supports overriding template values in the frontmatter itself. For example, this can be useful for changing variables depending on the flavor: @@ -178,7 +270,7 @@ overrides: {{flavor}} has accent color {{accent}}. ``` -When running `whiskers example.yml {latte, frappe, macchiato, mocha}`, we see that: +When running `whiskers example.yml`, we see that: - Frappé & Macchiato will have the accent `mauve` hex code. - Latte will have the accent `pink` hex code. @@ -193,13 +285,13 @@ frontmatter. This is particularly useful with build scripts to automatically gen ```yaml --- -accent: "{{mauve}}" +accent: "mauve" --- theme: - accent: "{{accent}}" + accent: "{{flavor.colors[accent].hex}}" ``` -When running `whiskers example.yml latte --overrides '{"accent": "{{pink}}"}'`, +When running `whiskers example.yml -f latte --overrides '{"accent": "pink"}'`, the `accent` will be overridden to pink. ### Frontmatter & CLI @@ -215,19 +307,19 @@ To express this visually, given an `example.yml` file: ```yaml --- -accent: "{{mauve}}" # <-- Frontmatter Root Context -background: "{{base}}" -text: "{{text}}" +accent: "mauve" # <-- Frontmatter Root Context +background: "base" +text: "text" overrides: # <-- Frontmatter Overrides Block mocha: - accent: "{{blue}}" + accent: "blue" --- ``` and the command: ```shell -whiskers example.yml mocha --overrides '{"accent": "{{pink}}"}' # <-- CLI Overrides +whiskers example.yml -f mocha --overrides '{"accent": "{{pink}}"}' # <-- CLI Overrides ``` The resulting file will have the accent `pink` as the accent will go through the @@ -237,144 +329,95 @@ following transformations: 2. accent is overridden to `blue` in the overrides block. 3. accent is overridden again to `pink` in the CLI overrides. -## Single File Support +## Single-Flavor Mode -Sometimes, you may not want to generate a file per flavor, but rather use all -the flavors inside one single file. This is achieved specifying the `