From 2227d363f71ef79da5574b0345497330d1908c91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= <felix@passcod.name>
Date: Tue, 21 Mar 2023 14:36:02 +1300
Subject: [PATCH] Leon template library (#766)

* leon: first implementation

* Update crates/leon/src/values.rs

Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com>

* Workaround orphan rules to make API more intuitive

* Fmt

* Clippy

* Use CoW

* Use cow for items too

* Test that const construction works

* leon: Initial attempt at O(n) parser

* leon: finish parser (except escapes)

* leon: Improve ergonomics of compile-time templates

* Document helpers

* leon: Docs tweaks

* leon: Use macro to minimise parser tests

* leon: add escapes to parser

* leon: test escapes preceding keys

* leon: add multibyte tests

* leon: test escapes following keys

* Format

* Debug

* leon: Don't actually need to keep track of the key

* leon: Parse to vec first

* leon: there's actually no need for string cows

* leon: reorganise and redo macro now that there's no coww

* Well that was silly

* leon: Adjust text end when pushing

* leon: catch unbalanced keys

* Add error tests

* leon: Catch unfinished escape

* Comment out debugging

* leon: fuzz

* Clippy

* leon: Box parse error

* leon: &dyn instead of impl

* Can't impl FromStr, so rename to parse

* Add Vec<> to values

* leon: Add benches for ways to supply values

* leon: Add bench comparing to std and tt

* Fix fuzz

* Fmt

* Split ParseError and RenderError

* Make miette optional

* Remove RenderError lifetime

* Simplify ParseError type schema

* Write concrete Values types instead of generics

* Add license files

* Reduce criterion deps

* Make default a cow

* Add a CLI leon tool

* Fix tests

* Clippy

* Disable cli by default

* Avoid failing the build when cli is off

* Add to ci

* Update crates/leon/src/main.rs

Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com>

* Update crates/leon/Cargo.toml

Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com>

* Bump version

* Error not transparent

* Diagnostic can do forwarding

* Simplify error type

* Expand doc examples

* Generic Values for Hash and BTree maps

* One more borrowed

* Forward implementations

* More generics

* Add has_keys

* Lock stdout in leon tool

* No more debug comments in parser

* Even more generics

* Macros to reduce bench duplication

* Further simplify error

* Fix leon main

* Stable support

* Clippy

---------

Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com>
---
 .github/dependabot.yml                  |   4 +
 .github/workflows/release-pr.yml        |   1 +
 Cargo.lock                              | 489 +++++++++++++-------
 Cargo.toml                              |   1 +
 crates/leon/Cargo.toml                  |  33 ++
 crates/leon/LICENSE-APACHE              | 176 ++++++++
 crates/leon/LICENSE-MIT                 |  23 +
 crates/leon/benches/others.rs           |  89 ++++
 crates/leon/benches/values.rs           | 298 +++++++++++++
 crates/leon/fuzz/.gitignore             |   4 +
 crates/leon/fuzz/Cargo.lock             | 150 +++++++
 crates/leon/fuzz/Cargo.toml             |  27 ++
 crates/leon/fuzz/fuzz_targets/parser.rs |   9 +
 crates/leon/src/error.rs                |  85 ++++
 crates/leon/src/lib.rs                  | 148 +++++++
 crates/leon/src/macros.rs               |  50 +++
 crates/leon/src/main.rs                 |  61 +++
 crates/leon/src/parser.rs               | 565 ++++++++++++++++++++++++
 crates/leon/src/template.rs             | 198 +++++++++
 crates/leon/src/values.rs               | 133 ++++++
 20 files changed, 2382 insertions(+), 162 deletions(-)
 create mode 100644 crates/leon/Cargo.toml
 create mode 100644 crates/leon/LICENSE-APACHE
 create mode 100644 crates/leon/LICENSE-MIT
 create mode 100644 crates/leon/benches/others.rs
 create mode 100644 crates/leon/benches/values.rs
 create mode 100644 crates/leon/fuzz/.gitignore
 create mode 100644 crates/leon/fuzz/Cargo.lock
 create mode 100644 crates/leon/fuzz/Cargo.toml
 create mode 100644 crates/leon/fuzz/fuzz_targets/parser.rs
 create mode 100644 crates/leon/src/error.rs
 create mode 100644 crates/leon/src/lib.rs
 create mode 100644 crates/leon/src/macros.rs
 create mode 100644 crates/leon/src/main.rs
 create mode 100644 crates/leon/src/parser.rs
 create mode 100644 crates/leon/src/template.rs
 create mode 100644 crates/leon/src/values.rs

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index be614717..2d36099e 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -48,3 +48,7 @@ updates:
     directory: "/crates/detect-targets"
     schedule:
       interval: "daily"
+  - package-ecosystem: "cargo"
+    directory: "/crates/leon"
+    schedule:
+      interval: "daily"
diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
index 7e8ae121..899fb7ae 100644
--- a/.github/workflows/release-pr.yml
+++ b/.github/workflows/release-pr.yml
@@ -16,6 +16,7 @@ on:
           - detect-wasi
           - fs-lock
           - normalize-path
+          - leon
       version:
         description: Version to release
         required: true
diff --git a/Cargo.lock b/Cargo.lock
index 36ef7add..1e5b44b1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -52,6 +52,12 @@ dependencies = [
  "alloc-no-stdlib",
 ]
 
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
 [[package]]
 name = "async-compression"
 version = "0.3.15"
@@ -78,7 +84,7 @@ checksum = "86ea188f25f0255d8f92797797c97ebf5631fa88178beb1a46fdf5622c9a00e4"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.4",
+ "syn 2.0.3",
 ]
 
 [[package]]
@@ -95,6 +101,17 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -262,9 +279,9 @@ checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
 
 [[package]]
 name = "block-buffer"
-version = "0.10.4"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
 dependencies = [
  "generic-array",
 ]
@@ -292,9 +309,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.12.0"
+version = "3.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
 
 [[package]]
 name = "byteorder"
@@ -335,7 +352,7 @@ version = "0.21.3"
 dependencies = [
  "binstalk",
  "binstalk-manifests",
- "clap",
+ "clap 4.1.11",
  "compact_str",
  "dirs",
  "embed-resource",
@@ -365,9 +382,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f83bc2e401ed041b7057345ebc488c005efa0341d5541ce7004d30458d0090b"
 dependencies = [
  "serde",
- "toml 0.7.3",
+ "toml 0.7.1",
 ]
 
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
 [[package]]
 name = "castaway"
 version = "0.2.2"
@@ -379,9 +402,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.79"
+version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
 dependencies = [
  "jobserver",
 ]
@@ -392,6 +415,45 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "ciborium"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clap"
+version = "3.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+dependencies = [
+ "bitflags 1.3.2",
+ "clap_lex 0.2.4",
+ "indexmap",
+ "textwrap 0.16.0",
+]
+
 [[package]]
 name = "clap"
 version = "4.1.11"
@@ -400,7 +462,7 @@ checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098"
 dependencies = [
  "bitflags 2.0.2",
  "clap_derive",
- "clap_lex",
+ "clap_lex 0.3.1",
  "is-terminal",
  "once_cell",
  "strsim",
@@ -417,14 +479,23 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
 name = "clap_lex"
-version = "0.3.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade"
 dependencies = [
  "os_str_bytes",
 ]
@@ -489,6 +560,40 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "criterion"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
+dependencies = [
+ "anes",
+ "atty",
+ "cast",
+ "ciborium",
+ "clap 3.2.23",
+ "criterion-plot",
+ "itertools",
+ "lazy_static",
+ "num-traits",
+ "oorandom",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -572,9 +677,9 @@ dependencies = [
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.32"
+version = "0.8.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
 dependencies = [
  "cfg-if",
 ]
@@ -588,7 +693,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
@@ -614,9 +719,9 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "1.9.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
 dependencies = [
  "instant",
 ]
@@ -629,14 +734,14 @@ checksum = "bbe20e5f282c61432e5cf0e33185e5dde032b2c2e4281c4735c31ce5b455a869"
 
 [[package]]
 name = "filetime"
-version = "0.2.20"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412"
+checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
 dependencies = [
  "cfg-if",
  "libc",
  "redox_syscall",
- "windows-sys 0.45.0",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -700,24 +805,24 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.27"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac"
+checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
 dependencies = [
  "futures-core",
 ]
 
 [[package]]
 name = "futures-core"
-version = "0.3.27"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
+checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
 
 [[package]]
 name = "futures-io"
-version = "0.3.27"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91"
+checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
 
 [[package]]
 name = "futures-lite"
@@ -731,32 +836,32 @@ dependencies = [
 
 [[package]]
 name = "futures-macro"
-version = "0.3.27"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6"
+checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
 name = "futures-sink"
-version = "0.3.27"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2"
+checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
 
 [[package]]
 name = "futures-task"
-version = "0.3.27"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879"
+checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
 
 [[package]]
 name = "futures-util"
-version = "0.3.27"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
+checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
 dependencies = [
  "futures-core",
  "futures-io",
@@ -813,9 +918,9 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.27.2"
+version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
+checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793"
 
 [[package]]
 name = "guess_host_triple"
@@ -831,9 +936,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.3.16"
+version = "0.3.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
+checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
 dependencies = [
  "bytes",
  "fnv",
@@ -848,6 +953,12 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "half"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+
 [[package]]
 name = "hashbrown"
 version = "0.12.3"
@@ -859,9 +970,18 @@ dependencies = [
 
 [[package]]
 name = "heck"
-version = "0.4.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
 
 [[package]]
 name = "hermit-abi"
@@ -872,12 +992,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "hermit-abi"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
-
 [[package]]
 name = "home"
 version = "0.5.4"
@@ -900,9 +1014,9 @@ dependencies = [
 
 [[package]]
 name = "http"
-version = "0.2.9"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
 dependencies = [
  "bytes",
  "fnv",
@@ -934,9 +1048,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
 
 [[package]]
 name = "hyper"
-version = "0.14.25"
+version = "0.14.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899"
+checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -1030,13 +1144,12 @@ checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074"
 
 [[package]]
 name = "io-lifetimes"
-version = "1.0.9"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb"
+checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
 dependencies = [
- "hermit-abi 0.3.1",
  "libc",
- "windows-sys 0.45.0",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -1059,14 +1172,14 @@ checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
 
 [[package]]
 name = "is-terminal"
-version = "0.4.5"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e"
+checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
 dependencies = [
- "hermit-abi 0.3.1",
- "io-lifetimes 1.0.9",
- "rustix 0.36.11",
- "windows-sys 0.45.0",
+ "hermit-abi 0.2.6",
+ "io-lifetimes 1.0.4",
+ "rustix 0.36.6",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -1086,15 +1199,15 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.6"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
 
 [[package]]
 name = "jobserver"
-version = "0.1.26"
+version = "0.1.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
+checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
 dependencies = [
  "libc",
 ]
@@ -1115,9 +1228,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.61"
+version = "0.3.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -1128,11 +1241,23 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
+[[package]]
+name = "leon"
+version = "0.0.1"
+dependencies = [
+ "clap 4.1.11",
+ "criterion",
+ "miette",
+ "serde",
+ "thiserror",
+ "tinytemplate",
+]
+
 [[package]]
 name = "libc"
-version = "0.2.140"
+version = "0.2.139"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
 
 [[package]]
 name = "libmimalloc-sys"
@@ -1219,9 +1344,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
 
 [[package]]
 name = "matches"
-version = "0.1.10"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
 
 [[package]]
 name = "maybe-owned"
@@ -1253,7 +1378,7 @@ dependencies = [
  "supports-hyperlinks",
  "supports-unicode",
  "terminal_size",
- "textwrap",
+ "textwrap 0.15.2",
  "thiserror",
  "unicode-width",
 ]
@@ -1266,7 +1391,7 @@ checksum = "2a07ad93a80d1b92bb44cb42d7c49b49c9aab1778befefad49cceb5e4c5bf460"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
@@ -1280,9 +1405,9 @@ dependencies = [
 
 [[package]]
 name = "mime"
-version = "0.3.17"
+version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
 
 [[package]]
 name = "miniz_oxide"
@@ -1295,14 +1420,14 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "0.8.6"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
+checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
 dependencies = [
  "libc",
  "log",
  "wasi",
- "windows-sys 0.45.0",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -1325,9 +1450,9 @@ dependencies = [
 
 [[package]]
 name = "nix"
-version = "0.26.2"
+version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
+checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
 dependencies = [
  "bitflags 1.3.2",
  "cfg-if",
@@ -1349,6 +1474,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "num_cpus"
 version = "1.15.0"
@@ -1361,9 +1495,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.30.3"
+version = "0.30.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439"
+checksum = "2b8c786513eb403643f2a88c244c2aaa270ef2153f55094587d0c48a3cf22a83"
 dependencies = [
  "memchr",
 ]
@@ -1375,10 +1509,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
 
 [[package]]
-name = "openssl"
-version = "0.10.47"
+name = "oorandom"
+version = "11.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8b277f87dacc05a6b709965d1cbafac4649d6ce9f3ce9ceb88508b5666dfec9"
+checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
+
+[[package]]
+name = "openssl"
+version = "0.10.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
 dependencies = [
  "bitflags 1.3.2",
  "cfg-if",
@@ -1397,7 +1537,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
@@ -1408,9 +1548,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.82"
+version = "0.9.80"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a95792af3c4e0153c3914df2261bedd30a98476f94dc892b67dfe1d89d433a04"
+checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
 dependencies = [
  "autocfg",
  "cc",
@@ -1421,9 +1561,9 @@ dependencies = [
 
 [[package]]
 name = "os_str_bytes"
-version = "6.5.0"
+version = "6.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
+checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
 
 [[package]]
 name = "overload"
@@ -1449,15 +1589,15 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.7"
+version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
+checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
 dependencies = [
  "cfg-if",
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-sys 0.45.0",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -1483,7 +1623,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
@@ -1519,7 +1659,7 @@ dependencies = [
  "proc-macro-error-attr",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
  "version_check",
 ]
 
@@ -1780,16 +1920,16 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.36.11"
+version = "0.36.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e"
+checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549"
 dependencies = [
  "bitflags 1.3.2",
  "errno",
- "io-lifetimes 1.0.9",
+ "io-lifetimes 1.0.4",
  "libc",
  "linux-raw-sys 0.1.4",
- "windows-sys 0.45.0",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -1836,15 +1976,24 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.12"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
+checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
 
 [[package]]
 name = "ryu"
-version = "1.0.13"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
+
+[[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 = "schannel"
@@ -1873,9 +2022,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework"
-version = "2.8.2"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
+checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
 dependencies = [
  "bitflags 1.3.2",
  "core-foundation",
@@ -1886,9 +2035,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "2.8.0"
+version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -1905,9 +2054,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.157"
+version = "1.0.158"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707de5fcf5df2b5788fca98dd7eab490bc2fd9b7ef1404defc462833b83f25ca"
+checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9"
 dependencies = [
  "serde_derive",
 ]
@@ -1923,13 +2072,13 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.157"
+version = "1.0.158"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78997f4555c22a7971214540c4a661291970619afd56de19f77e0de86296e1e5"
+checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.4",
+ "syn 2.0.3",
 ]
 
 [[package]]
@@ -1988,18 +2137,18 @@ dependencies = [
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.4.1"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "slab"
-version = "0.4.8"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
 dependencies = [
  "autocfg",
 ]
@@ -2018,9 +2167,9 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
 
 [[package]]
 name = "socket2"
-version = "0.4.9"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
 dependencies = [
  "libc",
  "winapi",
@@ -2060,7 +2209,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
@@ -2093,9 +2242,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.109"
+version = "1.0.107"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2104,9 +2253,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.4"
+version = "2.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae"
+checksum = "e8234ae35e70582bfa0f1fedffa6daa248e41dd045310b19800c4a36382c8f60"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2122,7 +2271,7 @@ dependencies = [
  "cfg-if",
  "fastrand",
  "redox_syscall",
- "rustix 0.36.11",
+ "rustix 0.36.6",
  "windows-sys 0.42.0",
 ]
 
@@ -2156,6 +2305,12 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "textwrap"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+
 [[package]]
 name = "thiserror"
 version = "1.0.40"
@@ -2173,16 +2328,15 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.4",
+ "syn 2.0.3",
 ]
 
 [[package]]
 name = "thread_local"
-version = "1.1.7"
+version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
 dependencies = [
- "cfg-if",
  "once_cell",
 ]
 
@@ -2207,9 +2361,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec_macros"
-version = "0.1.1"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
@@ -2239,14 +2393,14 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
 name = "tokio-native-tls"
-version = "0.3.1"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
 dependencies = [
  "native-tls",
  "tokio",
@@ -2265,9 +2419,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-stream"
-version = "0.1.12"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313"
+checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
 dependencies = [
  "futures-core",
  "pin-project-lite",
@@ -2314,9 +2468,9 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.7.3"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
+checksum = "772c1426ab886e7362aedf4abc9c0d1348a979517efedfc25862944d10137af0"
 dependencies = [
  "serde",
  "serde_spanned",
@@ -2396,7 +2550,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
 ]
 
 [[package]]
@@ -2520,15 +2674,15 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
 
 [[package]]
 name = "unicode-bidi"
-version = "0.3.13"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.8"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
 
 [[package]]
 name = "unicode-linebreak"
@@ -2617,6 +2771,17 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
 [[package]]
 name = "want"
 version = "0.3.0"
@@ -2635,9 +2800,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.84"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -2645,24 +2810,24 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.84"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
 dependencies = [
  "bumpalo",
  "log",
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.34"
+version = "0.4.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -2672,9 +2837,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.84"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -2682,22 +2847,22 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.84"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 1.0.107",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.84"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
 
 [[package]]
 name = "wasm-streams"
@@ -2714,9 +2879,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.61"
+version = "0.3.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -2919,7 +3084,7 @@ version = "0.12.3+zstd.1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806"
 dependencies = [
- "zstd-safe 6.0.4+zstd.1.5.4",
+ "zstd-safe 6.0.3+zstd.1.5.2",
 ]
 
 [[package]]
@@ -2934,9 +3099,9 @@ dependencies = [
 
 [[package]]
 name = "zstd-safe"
-version = "6.0.4+zstd.1.5.4"
+version = "6.0.3+zstd.1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543"
+checksum = "68e4a3f57d13d0ab7e478665c60f35e2a613dcd527851c2c7287ce5c787e134a"
 dependencies = [
  "libc",
  "zstd-sys",
diff --git a/Cargo.toml b/Cargo.toml
index 4e3e1f52..c4fd9e9c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ members = [
     "crates/fs-lock",
     "crates/normalize-path",
     "crates/detect-targets",
+    "crates/leon",
 ]
 
 [profile.release]
diff --git a/crates/leon/Cargo.toml b/crates/leon/Cargo.toml
new file mode 100644
index 00000000..b12324d1
--- /dev/null
+++ b/crates/leon/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "leon"
+description = "Dead-simple string templating"
+repository = "https://github.com/cargo-bins/cargo-binstall"
+documentation = "https://docs.rs/leon"
+version = "0.0.1"
+rust-version = "1.61.0"
+authors = ["Félix Saparelli <felix@passcod.name>"]
+edition = "2021"
+license = "Apache-2.0 OR MIT"
+
+[dependencies]
+clap = { version = "4.1.4", features = ["derive"], optional = true }
+miette = { version = "5.5.0", default-features = false, optional = true }
+thiserror = "1.0.38"
+
+[features]
+default = ["miette"]
+cli = ["dep:clap", "miette?/fancy-no-backtrace"]
+miette = ["dep:miette"]
+
+[dev-dependencies]
+criterion = { version = "0.4.0", default-features = false, features = ["cargo_bench_support"] }
+serde = { version = "1.0.152", features = ["derive"] }
+tinytemplate = "1.2.1"
+
+[[bench]]
+name = "values"
+harness = false
+
+[[bench]]
+name = "others"
+harness = false
diff --git a/crates/leon/LICENSE-APACHE b/crates/leon/LICENSE-APACHE
new file mode 100644
index 00000000..1b5ec8b7
--- /dev/null
+++ b/crates/leon/LICENSE-APACHE
@@ -0,0 +1,176 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
diff --git a/crates/leon/LICENSE-MIT b/crates/leon/LICENSE-MIT
new file mode 100644
index 00000000..31aa7938
--- /dev/null
+++ b/crates/leon/LICENSE-MIT
@@ -0,0 +1,23 @@
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/crates/leon/benches/others.rs b/crates/leon/benches/others.rs
new file mode 100644
index 00000000..9b48f871
--- /dev/null
+++ b/crates/leon/benches/others.rs
@@ -0,0 +1,89 @@
+use std::borrow::Cow;
+
+use criterion::{black_box, criterion_group, criterion_main, Criterion};
+use leon::{vals, Template};
+use serde::Serialize;
+use tinytemplate::TinyTemplate;
+
+fn compare_impls(c: &mut Criterion) {
+    const TEMPLATE: &str = "hello {name}! i am {age} years old. my goal is to {goal}. i like: {flower}, {music}, {animal}, {color}, {food}. i'm drinking {drink}";
+    fn replace_fn<'s>(key: &'s str) -> Option<Cow<'s, str>> {
+        Some(Cow::Borrowed(match key {
+            "name" => "marcus",
+            "age" => "42",
+            "goal" => "primary",
+            "flower" => "lotus",
+            "music" => "jazz",
+            "animal" => "cat",
+            "color" => "blue",
+            "food" => "pizza",
+            "drink" => "coffee",
+            _ => return None,
+        }))
+    }
+
+    #[derive(Serialize)]
+    struct Context<'c> {
+        name: &'c str,
+        age: u8,
+        goal: &'c str,
+        flower: &'c str,
+        music: &'c str,
+        animal: &'c str,
+        color: &'c str,
+        food: &'c str,
+        drink: &'c str,
+    }
+
+    let tt_context = Context {
+        name: "marcus",
+        age: 42,
+        goal: "primary",
+        flower: "lotus",
+        music: "jazz",
+        animal: "cat",
+        color: "blue",
+        food: "pizza",
+        drink: "coffee",
+    };
+
+    c.bench_function("leon", |b| {
+        b.iter(|| {
+            let template = Template::parse(black_box(TEMPLATE)).unwrap();
+            let output = template.render(&vals(replace_fn)).unwrap();
+            black_box(output);
+        })
+    });
+
+    c.bench_function("std, string replaces", |b| {
+        b.iter(|| {
+            let mut output = black_box(TEMPLATE).to_string();
+            for (key, value) in [
+                ("name", "marcus"),
+                ("age", "42"),
+                ("goal", "primary"),
+                ("flower", "lotus"),
+                ("music", "jazz"),
+                ("animal", "cat"),
+                ("color", "blue"),
+                ("food", "pizza"),
+                ("drink", "coffee"),
+            ] {
+                output = output.replace(&format!("{{{}}}", key), value);
+            }
+            black_box(output);
+        })
+    });
+
+    c.bench_function("tiny template", |b| {
+        b.iter(|| {
+            let mut tt = TinyTemplate::new();
+            tt.add_template("tmp", black_box(TEMPLATE)).unwrap();
+            let output = tt.render("tmp", &tt_context).unwrap();
+            black_box(output);
+        })
+    });
+}
+
+criterion_group!(compare, compare_impls);
+criterion_main!(compare);
diff --git a/crates/leon/benches/values.rs b/crates/leon/benches/values.rs
new file mode 100644
index 00000000..b4fd7944
--- /dev/null
+++ b/crates/leon/benches/values.rs
@@ -0,0 +1,298 @@
+use std::{borrow::Cow, collections::HashMap};
+
+use criterion::{black_box, criterion_group, criterion_main, Criterion};
+use leon::{vals, Template, Values, ValuesFn};
+
+macro_rules! make_values {
+    ($($name:expr => $value:expr),*) => {
+        (
+            &[$(($name, $value)),*],
+            {
+                let mut map = HashMap::new();
+                $(
+                    map.insert($name, $value);
+                )*
+                map
+            },
+            vals(|key| match key {
+                $(
+                    $name => Some(Cow::Borrowed($value)),
+                )*
+                _ => None,
+            })
+        )
+    };
+}
+
+fn one_replace(c: &mut Criterion) {
+    const TEMPLATE: &str = "Hello, {name}!";
+
+    let (slice, hashmap, vals) = make_values!(
+        "name" => "marcus"
+    );
+
+    inner_bench("one replace", c, TEMPLATE, vals, hashmap, slice);
+}
+
+fn some_replaces(c: &mut Criterion) {
+    const TEMPLATE: &str = "hello {name}! i am {age} years old. my goal is to {goal}. i like: {flower}, {music}, {animal}, {color}, {food}. i'm drinking {drink}";
+
+    let (slice, hashmap, vals) = make_values!(
+        "name" => "marcus",
+        "age" => "42",
+        "goal" => "primary",
+        "flower" => "lotus",
+        "music" => "jazz",
+        "animal" => "cat",
+        "color" => "blue",
+        "food" => "pizza",
+        "drink" => "coffee"
+    );
+
+    inner_bench("some replaces", c, TEMPLATE, vals, hashmap, slice);
+}
+
+fn many_replaces(c: &mut Criterion) {
+    const TEMPLATE: &str = "
+        {artichoke}
+        {aubergine}
+        {asparagus}
+        {broccoflower}
+        {broccoli}
+        {brussels sprouts}
+        {cabbage}
+        {kohlrabi}
+        {Savoy cabbage}
+        {red cabbage}
+        {cauliflower}
+        {celery}
+        {endive}
+        {fiddleheads}
+        {frisee}
+        {fennel}
+        {greens}
+        {arugula}
+        {bok choy}
+        {chard}
+        {collard greens}
+        {kale}
+        {lettuce}
+        {mustard greens}
+        {spinach}
+        {herbs}
+        {anise}
+        {basil}
+        {caraway}
+        {coriander}
+        {chamomile}
+        {daikon}
+        {dill}
+        {squash}
+        {lavender}
+        {cymbopogon}
+        {marjoram}
+        {oregano}
+        {parsley}
+        {rosemary}
+        {thyme}
+        {legumes}
+        {alfalfa sprouts}
+        {azuki beans}
+        {bean sprouts}
+        {black beans}
+        {black-eyed peas}
+        {borlotti bean}
+        {broad beans}
+        {chickpeas, garbanzos, or ceci beans}
+        {green beans}
+        {kidney beans}
+        {lentils}
+        {lima beans or butter bean}
+        {mung beans}
+        {navy beans}
+        {peanuts}
+        {pinto beans}
+        {runner beans}
+        {split peas}
+        {soy beans}
+        {peas}
+        {mange tout or snap peas}
+        {mushrooms}
+        {nettles}
+        {New Zealand spinach}
+        {okra}
+        {onions}
+        {chives}
+        {garlic}
+        {leek}
+        {onion}
+        {shallot}
+        {scallion}
+        {peppers}
+        {bell pepper}
+        {chili pepper}
+        {jalapeño}
+        {habanero}
+        {paprika}
+        {tabasco pepper}
+        {cayenne pepper}
+        {radicchio}
+        {rhubarb}
+        {root vegetables}
+        {beetroot}
+        {beet}
+        {mangelwurzel}
+        {carrot}
+        {celeriac}
+        {corms}
+        {eddoe}
+        {konjac}
+        {taro}
+        {water chestnut}
+        {ginger}
+        {parsnip}
+        {rutabaga}
+        {radish}
+        {wasabi}
+    ";
+
+    let (slice, hashmap, vals) = make_values!(
+        "artichoke" => "Abiu",
+        "aubergine" => "Açaí",
+        "asparagus" => "Acerola",
+        "broccoflower" => "Akebi",
+        "broccoli" => "Ackee",
+        "brussels sprouts" => "African Cherry Orange",
+        "cabbage" => "American Mayapple",
+        "kohlrabi" => "Apple",
+        "Savoy cabbage" => "Apricot",
+        "red cabbage" => "Araza",
+        "cauliflower" => "Avocado",
+        "celery" => "Banana",
+        "endive" => "Bilberry",
+        "fiddleheads" => "Blackberry",
+        "frisee" => "Blackcurrant",
+        "fennel" => "Black sapote",
+        "greens" => "Blueberry",
+        "arugula" => "Boysenberry",
+        "bok choy" => "Breadfruit",
+        "chard" => "Buddha's hand",
+        "collard greens" => "Cactus pear",
+        "kale" => "Canistel",
+        "lettuce" => "Cashew",
+        "mustard greens" => "Cempedak",
+        "spinach" => "Cherimoya",
+        "herbs" => "Cherry",
+        "anise" => "Chico fruit",
+        "basil" => "Cloudberry",
+        "caraway" => "Coco de mer",
+        "coriander" => "Coconut",
+        "chamomile" => "Crab apple",
+        "daikon" => "Cranberry",
+        "dill" => "Currant",
+        "squash" => "Damson",
+        "lavender" => "Date",
+        "cymbopogon" => "Dragonfruit",
+        "marjoram" => "Pitaya",
+        "oregano" => "Durian",
+        "parsley" => "Elderberry",
+        "rosemary" => "Feijoa",
+        "thyme" => "Fig",
+        "legumes" => "Finger Lime",
+        "alfalfa sprouts" => "Caviar Lime",
+        "azuki beans" => "Goji berry",
+        "bean sprouts" => "Gooseberry",
+        "black beans" => "Grape",
+        "black-eyed peas" => "Raisin",
+        "borlotti bean" => "Grapefruit",
+        "broad beans" => "Grewia asiatica",
+        "chickpeas, garbanzos, or ceci beans" => "Guava",
+        "green beans" => "Hala Fruit",
+        "kidney beans" => "Honeyberry",
+        "lentils" => "Huckleberry",
+        "lima beans or butter bean" => "Jabuticaba",
+        "mung beans" => "Jackfruit",
+        "navy beans" => "Jambul",
+        "peanuts" => "Japanese plum",
+        "pinto beans" => "Jostaberry",
+        "runner beans" => "Jujube",
+        "split peas" => "Juniper berry",
+        "soy beans" => "Kaffir Lime",
+        "peas" => "Kiwano",
+        "mange tout or snap peas" => "Kiwifruit",
+        "mushrooms" => "Kumquat",
+        "nettles" => "Lemon",
+        "New Zealand spinach" => "Lime",
+        "okra" => "Loganberry",
+        "onions" => "Longan",
+        "chives" => "Loquat",
+        "garlic" => "Lulo",
+        "leek" => "Lychee",
+        "onion" => "Magellan Barberry",
+        "shallot" => "Mamey Apple",
+        "scallion" => "Mamey Sapote",
+        "peppers" => "Mango",
+        "bell pepper" => "Mangosteen",
+        "chili pepper" => "Marionberry",
+        "jalapeño" => "Melon",
+        "habanero" => "Cantaloupe",
+        "paprika" => "Galia melon",
+        "tabasco pepper" => "Honeydew",
+        "cayenne pepper" => "Mouse melon",
+        "radicchio" => "Musk melon",
+        "rhubarb" => "Watermelon",
+        "root vegetables" => "Miracle fruit",
+        "beetroot" => "Momordica fruit",
+        "beet" => "Monstera deliciosa",
+        "mangelwurzel" => "Mulberry",
+        "carrot" => "Nance",
+        "celeriac" => "Nectarine",
+        "corms" => "Orange",
+        "eddoe" => "Blood orange",
+        "konjac" => "Clementine",
+        "taro" => "Mandarine",
+        "water chestnut" => "Tangerine",
+        "ginger" => "Papaya",
+        "parsnip" => "Passionfruit",
+        "rutabaga" => "Pawpaw",
+        "radish" => "Peach",
+        "wasabi" => "Pear"
+    );
+
+    inner_bench("many replaces", c, TEMPLATE, vals, hashmap, slice);
+}
+
+fn inner_bench<F>(
+    name: &str,
+    c: &mut Criterion,
+    template_str: &str,
+    vals: ValuesFn<F>,
+    hashmap: HashMap<&str, &str>,
+    slice: &[(&str, &str)],
+) where
+    F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
+{
+    c.bench_function(&format!("{name}, fn"), |b| {
+        b.iter(|| {
+            let template = Template::parse(black_box(template_str)).unwrap();
+            black_box(template.render(&vals).unwrap());
+        })
+    });
+    c.bench_function(&format!("{name}, hashmap"), |b| {
+        b.iter(|| {
+            let template = Template::parse(black_box(template_str)).unwrap();
+            black_box(template.render(&hashmap).unwrap());
+        })
+    });
+    c.bench_function(&format!("{name}, slice"), |b| {
+        b.iter(|| {
+            let template = Template::parse(black_box(template_str)).unwrap();
+            black_box(template.render(&slice as &dyn Values).unwrap());
+        })
+    });
+}
+
+criterion_group!(one, one_replace);
+criterion_group!(some, some_replaces);
+criterion_group!(many, many_replaces);
+criterion_main!(one, some, many);
diff --git a/crates/leon/fuzz/.gitignore b/crates/leon/fuzz/.gitignore
new file mode 100644
index 00000000..1a45eee7
--- /dev/null
+++ b/crates/leon/fuzz/.gitignore
@@ -0,0 +1,4 @@
+target
+corpus
+artifacts
+coverage
diff --git a/crates/leon/fuzz/Cargo.lock b/crates/leon/fuzz/Cargo.lock
new file mode 100644
index 00000000..9409a0dd
--- /dev/null
+++ b/crates/leon/fuzz/Cargo.lock
@@ -0,0 +1,150 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "arbitrary"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e90af4de65aa7b293ef2d09daff88501eb254f58edde2e1ac02c82d873eadad"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+dependencies = [
+ "jobserver",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "leon"
+version = "0.0.0"
+dependencies = [
+ "miette",
+ "thiserror",
+]
+
+[[package]]
+name = "leon-fuzz"
+version = "0.0.0"
+dependencies = [
+ "leon",
+ "libfuzzer-sys",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "beb09950ae85a0a94b27676cccf37da5ff13f27076aa1adbc6545dd0d0e1bd4e"
+dependencies = [
+ "arbitrary",
+ "cc",
+ "once_cell",
+]
+
+[[package]]
+name = "miette"
+version = "5.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd9b301defa984bbdbe112b4763e093ed191750a0d914a78c1106b2d0fe703"
+dependencies = [
+ "miette-derive",
+ "once_cell",
+ "thiserror",
+ "unicode-width",
+]
+
+[[package]]
+name = "miette-derive"
+version = "5.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97c2401ab7ac5282ca5c8b518a87635b1a93762b0b90b9990c509888eeccba29"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
diff --git a/crates/leon/fuzz/Cargo.toml b/crates/leon/fuzz/Cargo.toml
new file mode 100644
index 00000000..1801da08
--- /dev/null
+++ b/crates/leon/fuzz/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "leon-fuzz"
+version = "0.0.0"
+publish = false
+edition = "2021"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+libfuzzer-sys = "0.4"
+
+[dependencies.leon]
+path = ".."
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[profile.release]
+debug = 1
+
+[[bin]]
+name = "parser"
+path = "fuzz_targets/parser.rs"
+test = false
+doc = false
diff --git a/crates/leon/fuzz/fuzz_targets/parser.rs b/crates/leon/fuzz/fuzz_targets/parser.rs
new file mode 100644
index 00000000..9cd51fe5
--- /dev/null
+++ b/crates/leon/fuzz/fuzz_targets/parser.rs
@@ -0,0 +1,9 @@
+#![no_main]
+
+use libfuzzer_sys::fuzz_target;
+
+fuzz_target!(|data: &[u8]| {
+    if let Ok(s) = std::str::from_utf8(data) {
+        let _ = leon::Template::parse(s);
+    }
+});
diff --git a/crates/leon/src/error.rs b/crates/leon/src/error.rs
new file mode 100644
index 00000000..6e064e2f
--- /dev/null
+++ b/crates/leon/src/error.rs
@@ -0,0 +1,85 @@
+#[derive(Debug, thiserror::Error)]
+#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
+pub enum RenderError {
+    /// A key was missing from the provided values.
+    #[error("missing key `{0}`")]
+    MissingKey(String),
+
+    /// An I/O error passed through from [`Template::render_into`].
+    #[error("write failed: {0}")]
+    Io(#[from] std::io::Error),
+}
+
+/// An error that can occur when parsing a template.
+///
+/// When the `miette` feature is enabled, this is a rich miette-powered error
+/// which will highlight the source of the error in the template when output
+/// (with miette's `fancy` feature). With `miette` disabled, this is opaque.
+#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
+#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
+#[cfg_attr(feature = "miette", diagnostic(transparent))]
+#[error(transparent)]
+pub struct ParseError(Box<InnerParseError>);
+
+/// The inner (unboxed) type of [`ParseError`].
+#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
+#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
+#[error("template parse failed")]
+struct InnerParseError {
+    #[cfg_attr(feature = "miette", source_code)]
+    src: String,
+
+    #[cfg_attr(feature = "miette", label("This bracket is not opening or closing anything. Try removing it, or escaping it with a backslash."))]
+    unbalanced: Option<(usize, usize)>,
+
+    #[cfg_attr(feature = "miette", label("This escape is malformed."))]
+    escape: Option<(usize, usize)>,
+
+    #[cfg_attr(feature = "miette", label("A key cannot be empty."))]
+    key_empty: Option<(usize, usize)>,
+
+    #[cfg_attr(feature = "miette", label("Escapes are not allowed in keys."))]
+    key_escape: Option<(usize, usize)>,
+}
+
+impl ParseError {
+    pub(crate) fn unbalanced(src: &str, start: usize, end: usize) -> Self {
+        Self(Box::new(InnerParseError {
+            src: String::from(src),
+            unbalanced: Some((start, end.saturating_sub(start) + 1)),
+            escape: None,
+            key_empty: None,
+            key_escape: None,
+        }))
+    }
+
+    pub(crate) fn escape(src: &str, start: usize, end: usize) -> Self {
+        Self(Box::new(InnerParseError {
+            src: String::from(src),
+            unbalanced: None,
+            escape: Some((start, end.saturating_sub(start) + 1)),
+            key_empty: None,
+            key_escape: None,
+        }))
+    }
+
+    pub(crate) fn key_empty(src: &str, start: usize, end: usize) -> Self {
+        Self(Box::new(InnerParseError {
+            src: String::from(src),
+            unbalanced: None,
+            escape: None,
+            key_empty: Some((start, end.saturating_sub(start) + 1)),
+            key_escape: None,
+        }))
+    }
+
+    pub(crate) fn key_escape(src: &str, start: usize, end: usize) -> Self {
+        Self(Box::new(InnerParseError {
+            src: String::from(src),
+            unbalanced: None,
+            escape: None,
+            key_empty: None,
+            key_escape: Some((start, end.saturating_sub(start) + 1)),
+        }))
+    }
+}
diff --git a/crates/leon/src/lib.rs b/crates/leon/src/lib.rs
new file mode 100644
index 00000000..17eeeeca
--- /dev/null
+++ b/crates/leon/src/lib.rs
@@ -0,0 +1,148 @@
+//! Dead-simple string templating.
+//!
+//! Leon parses a template string into a list of tokens, and then substitutes
+//! provided values in. Unlike other templating engines, it is extremely simple:
+//! it supports no logic, only replaces. It is even simpler than `format!()`,
+//! albeit with a similar syntax.
+//!
+//! # Syntax
+//!
+//! ```plain
+//! it is better to rule { group }
+//! one can live {adverb} without power
+//! ```
+//!
+//! A replacement is denoted by `{` and `}`. The contents of the braces, trimmed
+//! of any whitespace, are the key. Any text outside of braces is left as-is.
+//!
+//! To escape a brace, use `\{` or `\}`. To escape a backslash, use `\\`. Keys
+//! cannot contain escapes.
+//!
+//! ```plain
+//! \{ leon \}
+//! ```
+//!
+//! The above examples, given the values `group = "no one"` and
+//! `adverb = "honourably"`, would render to:
+//!
+//! ```plain
+//! it is better to rule no one
+//! one can live honourably without power
+//! { leon }
+//! ```
+//!
+//! # Usage
+//!
+//! A template is first parsed to a token list:
+//!
+//! ```
+//! use leon::Template;
+//!
+//! let template = Template::parse("hello {name}").unwrap();
+//! ```
+//!
+//! The template can be inspected, for example to check if a key is present:
+//!
+//! ```
+//! # use leon::Template;
+//! #
+//! # let template = Template::parse("hello {name}").unwrap();
+//! assert!(template.has_key("name"));
+//! ```
+//!
+//! The template can be rendered to a string:
+//!
+//! ```
+//! # use leon::Template;
+//! use leon::vals;
+//! #
+//! # let template = Template::parse("hello {name}").unwrap();
+//! assert_eq!(
+//!     template.render(
+//!         &vals(|_key| Some("marcus".into()))
+//!     ).unwrap().as_str(),
+//!     "hello marcus",
+//! );
+//! ```
+//!
+//! …or to a writer:
+//!
+//! ```
+//! use std::io::Write;
+//! # use leon::Template;
+//! use leon::vals;
+//! #
+//! # let template = Template::parse("hello {name}").unwrap();
+//! let mut buf: Vec<u8> = Vec::new();
+//! template.render_into(
+//!     &mut buf,
+//!     &vals(|key| if key == "name" {
+//!         Some("julius".into())
+//!     } else {
+//!         None
+//!     })
+//! ).unwrap();
+//! assert_eq!(buf.as_slice(), b"hello julius");
+//! ```
+//!
+//! …with a map:
+//!
+//! ```
+//! use std::collections::HashMap;
+//! # use leon::Template;
+//! # let template = Template::parse("hello {name}").unwrap();
+//! let mut values = HashMap::new();
+//! values.insert("name", "brutus");
+//! assert_eq!(template.render(&values).unwrap().as_str(), "hello brutus");
+//! ```
+//!
+//! …or with your own type, if you implement the [`Values`] trait:
+//!
+//! ```
+//! # use leon::Template;
+//! use std::borrow::Cow;
+//! use leon::Values;
+//!
+//! struct MyMap {
+//!   name: &'static str,
+//! }
+//! impl Values for MyMap {
+//!    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+//!       if key == "name" {
+//!         Some(self.name.into())
+//!      } else {
+//!        None
+//!     }
+//!    }
+//! }
+//! #
+//! # let template = Template::parse("hello {name}").unwrap();
+//! let values = MyMap { name: "pontifex" };
+//! assert_eq!(template.render(&values).unwrap().as_str(), "hello pontifex");
+//! ```
+//!
+//! # Errors
+//!
+//! Leon will return a [`LeonError::InvalidTemplate`] if the template fails to
+//! parse. This can happen if there are unbalanced braces, or if a key is empty.
+//!
+//! Leon will return a [`LeonError::MissingKey`] if a key is missing from keyed
+//! values passed to [`Template::render()`], unless a default value is provided
+//! with [`Template.default`].
+//!
+//! It will also pass through I/O errors when using [`Template::render_into()`].
+
+#[doc(inline)]
+pub use error::*;
+
+#[doc(inline)]
+pub use template::*;
+
+#[doc(inline)]
+pub use values::*;
+
+mod error;
+mod macros;
+mod parser;
+mod template;
+mod values;
diff --git a/crates/leon/src/macros.rs b/crates/leon/src/macros.rs
new file mode 100644
index 00000000..3833a3c3
--- /dev/null
+++ b/crates/leon/src/macros.rs
@@ -0,0 +1,50 @@
+/// Construct a template constant without needing to make an items constant.
+///
+/// This is essentially a shorthand for:
+///
+/// ```
+/// use leon::{Item, Template};
+/// Template::new({
+///     const ITEMS: &'static [Item<'static>] = &[Item::Text("Hello "), Item::Key("name")];
+///     ITEMS
+/// }, Some("world"));
+/// ```
+///
+/// # Examples
+///
+/// ```
+/// use leon::Item::*;
+/// assert_eq!(
+///     leon::template!(Text("Hello "), Key("name"))
+///         .render(&[("name", "Магда Нахман")])
+///         .unwrap(),
+///     "Hello Магда Нахман",
+/// );
+/// ```
+///
+/// With a default:
+///
+/// ```
+/// use leon::Item::*;
+/// assert_eq!(
+///     leon::template!(Text("Hello "), Key("name"); "M. P. T. Acharya")
+///         .render(&[("city", "Madras")])
+///         .unwrap(),
+///     "Hello M. P. T. Acharya",
+/// );
+/// ```
+#[macro_export]
+macro_rules! template {
+    ($($item:expr),* $(,)?) => {
+        $crate::Template::new({
+            const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*];
+            ITEMS
+        }, ::core::option::Option::None)
+    };
+    ($($item:expr),* $(,)? ; $default:expr) => {
+        $crate::Template::new({
+            const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*];
+            ITEMS
+        }, ::core::option::Option::Some($default))
+    };
+}
diff --git a/crates/leon/src/main.rs b/crates/leon/src/main.rs
new file mode 100644
index 00000000..acbdb1f5
--- /dev/null
+++ b/crates/leon/src/main.rs
@@ -0,0 +1,61 @@
+#[cfg(feature = "cli")]
+fn main() -> miette::Result<()> {
+    use std::{collections::HashMap, error::Error, io::stdout};
+
+    use clap::Parser;
+    use leon::Template;
+
+    /// Render a Leon template with the given values.
+    #[derive(Parser, Debug)]
+    #[command(author, version, about, long_about = None)]
+    struct Args {
+        /// Leon template
+        template: String,
+
+        /// Default to use for missing keys
+        #[arg(long)]
+        default: Option<String>,
+
+        /// Use values from the environment
+        #[arg(long)]
+        env: bool,
+
+        /// Key-value pairs to use
+        #[arg(short, long, value_parser = parse_key_val::<String, String>)]
+        values: Vec<(String, String)>,
+    }
+
+    /// Parse a single key-value pair
+    fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
+    where
+        T: std::str::FromStr,
+        T::Err: Error + Send + Sync + 'static,
+        U: std::str::FromStr,
+        U::Err: Error + Send + Sync + 'static,
+    {
+        let (k, v) = s
+            .split_once('=')
+            .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
+        Ok((k.parse()?, v.parse()?))
+    }
+
+    let args = Args::parse();
+    let mut values: HashMap<String, String> = HashMap::from_iter(args.values);
+    if args.env {
+        for (key, value) in std::env::vars() {
+            values.entry(key).or_insert(value);
+        }
+    }
+
+    let template = args.template;
+    let mut template = Template::parse(&template)?;
+    if let Some(default) = &args.default {
+        template.set_default(default);
+    }
+
+    template.render_into(&mut stdout().lock(), &values)?;
+    Ok(())
+}
+
+#[cfg(not(feature = "cli"))]
+fn main() {}
diff --git a/crates/leon/src/parser.rs b/crates/leon/src/parser.rs
new file mode 100644
index 00000000..5f9efb37
--- /dev/null
+++ b/crates/leon/src/parser.rs
@@ -0,0 +1,565 @@
+use std::mem::replace;
+
+use crate::{Item, ParseError, Template};
+
+#[derive(Debug, Clone, Copy)]
+enum Token {
+    Text {
+        start: usize,
+        end: usize,
+    },
+    BracePair {
+        start: usize,
+        key_seen: bool,
+        end: usize,
+    },
+    Escape {
+        start: usize,
+        end: usize,
+        ch: Option<char>,
+    },
+}
+
+impl Token {
+    fn start_text(pos: usize, ch: char) -> Self {
+        Self::Text {
+            start: pos,
+            end: pos + ch.len_utf8(),
+        }
+    }
+
+    fn start_text_single(pos: usize) -> Self {
+        Self::Text {
+            start: pos,
+            end: pos,
+        }
+    }
+
+    fn start_brace_pair(pos: usize, ch: char) -> Self {
+        Self::BracePair {
+            start: pos,
+            key_seen: false,
+            end: pos + ch.len_utf8(),
+        }
+    }
+
+    fn start_escape(pos: usize, ch: char) -> Self {
+        Self::Escape {
+            start: pos,
+            end: pos + ch.len_utf8(),
+            ch: None,
+        }
+    }
+
+    fn is_empty(&self, source_len: usize) -> bool {
+        match self {
+            Self::Text { start, end } => {
+                *start >= source_len || *end >= source_len || *start > *end
+            }
+            Self::BracePair {
+                start,
+                key_seen,
+                end,
+            } => !key_seen || *start >= source_len || *end >= source_len || *start > *end,
+            Self::Escape { start, end, .. } => {
+                *start >= source_len || *end >= source_len || *start > *end
+            }
+        }
+    }
+
+    fn start(&self) -> usize {
+        match self {
+            Self::Text { start, .. }
+            | Self::BracePair { start, .. }
+            | Self::Escape { start, .. } => *start,
+        }
+    }
+
+    fn end(&self) -> usize {
+        match self {
+            Self::Text { end, .. } | Self::BracePair { end, .. } | Self::Escape { end, .. } => *end,
+        }
+    }
+
+    fn set_end(&mut self, pos: usize) {
+        match self {
+            Self::Text { end, .. } | Self::BracePair { end, .. } | Self::Escape { end, .. } => {
+                *end = pos
+            }
+        }
+    }
+}
+
+impl<'s> Template<'s> {
+    pub(crate) fn parse_items(s: &'s str) -> Result<Vec<Item<'s>>, ParseError> {
+        let source_len = s.len();
+        let mut tokens = Vec::new();
+
+        let mut current = Token::start_text(0, '\0');
+
+        for (pos, chara) in s.char_indices() {
+            match (&mut current, chara) {
+                (tok @ (Token::Text { .. } | Token::Escape { ch: Some(_), .. }), ch @ '{') => {
+                    if matches!(tok, Token::Text { .. }) && tok.start() == pos {
+                        *tok = Token::start_brace_pair(pos, ch);
+                    } else {
+                        if let Token::Text { end, .. } = tok {
+                            *end = pos - 1;
+                        }
+                        tokens.push(replace(tok, Token::start_brace_pair(pos, ch)));
+                    }
+                }
+                (txt @ Token::Text { .. }, ch @ '\\') => {
+                    if txt.is_empty(source_len) || txt.start() == pos {
+                        *txt = Token::start_escape(pos, ch);
+                    } else {
+                        if let Token::Text { end, .. } = txt {
+                            *end = pos - 1;
+                        }
+                        tokens.push(replace(txt, Token::start_escape(pos, ch)));
+                    }
+                }
+                (bp @ Token::BracePair { .. }, '}') => {
+                    if let Token::BracePair { end, .. } = bp {
+                        *end = pos;
+                    } else {
+                        unreachable!("bracepair isn't bracepair");
+                    }
+
+                    tokens.push(replace(bp, Token::start_text_single(pos + 1)));
+                }
+                (Token::BracePair { start, .. }, '\\') => {
+                    return Err(ParseError::key_escape(s, *start, pos));
+                }
+                (Token::BracePair { key_seen, end, .. }, ws) if ws.is_whitespace() => {
+                    if *key_seen {
+                        *end = pos;
+                    } else {
+                        // We're in a brace pair, but we're not in the key yet.
+                    }
+                }
+                (Token::BracePair { key_seen, end, .. }, _) => {
+                    *key_seen = true;
+                    *end = pos + 1;
+                }
+                (Token::Text { .. }, '}') => {
+                    return Err(ParseError::unbalanced(s, pos, pos));
+                }
+                (Token::Text { end, .. }, _) => {
+                    *end = pos;
+                }
+                (esc @ Token::Escape { .. }, es @ ('\\' | '{' | '}')) => {
+                    if let Token::Escape { start, end, ch, .. } = esc {
+                        if ch.is_none() {
+                            *end = pos;
+                            *ch = Some(es);
+                        } else if es == '\\' {
+                            // A new escape right after a completed escape.
+                            tokens.push(replace(esc, Token::start_escape(pos, es)));
+                        } else if es == '{' {
+                            // A new brace pair right after a completed escape, should be handled prior to this.
+                            unreachable!("escape followed by brace pair, unhandled");
+                        } else {
+                            // } right after a completed escape, probably unreachable but just in case:
+                            return Err(ParseError::key_escape(s, *start, pos));
+                        }
+                    } else {
+                        unreachable!("escape is not an escape");
+                    }
+                }
+                (
+                    Token::Escape {
+                        start, ch: None, ..
+                    },
+                    _,
+                ) => {
+                    return Err(ParseError::escape(s, *start, pos));
+                }
+                (Token::Escape { ch: Some(_), .. }, _) => {
+                    tokens.push(replace(&mut current, Token::start_text_single(pos)));
+                }
+            }
+        }
+
+        if !current.is_empty(source_len) {
+            if current.end() < source_len - 1 {
+                current.set_end(source_len - 1);
+            }
+
+            tokens.push(current);
+        }
+
+        if let Token::BracePair { start, end, .. } = current {
+            return Err(ParseError::unbalanced(s, start, end));
+        }
+
+        if let Token::Escape {
+            start,
+            end,
+            ch: None,
+        } = current
+        {
+            return Err(ParseError::escape(s, start, end));
+        }
+
+        let mut items = Vec::with_capacity(tokens.len());
+        for token in tokens {
+            match token {
+                Token::Text { start, end } => {
+                    items.push(Item::Text(&s[start..=end]));
+                }
+                Token::BracePair {
+                    start,
+                    end,
+                    key_seen: false,
+                } => {
+                    return Err(ParseError::key_empty(s, start, end));
+                }
+                Token::BracePair {
+                    start,
+                    end,
+                    key_seen: true,
+                } => {
+                    let key = s[start..=end]
+                        .trim_matches(|c: char| c.is_whitespace() || c == '{' || c == '}');
+                    if key.is_empty() {
+                        return Err(ParseError::key_empty(s, start, end));
+                    } else {
+                        items.push(Item::Key(key));
+                    }
+                }
+                Token::Escape {
+                    ch: Some(_), end, ..
+                } => {
+                    items.push(Item::Text(&s[end..=end]));
+                }
+                Token::Escape {
+                    ch: None,
+                    start,
+                    end,
+                } => {
+                    return Err(ParseError::escape(s, start, end));
+                }
+            }
+        }
+
+        Ok(items)
+    }
+}
+
+#[cfg(test)]
+mod test_valid {
+    use crate::{template, Item::*, Template};
+
+    #[test]
+    fn empty() {
+        let template = Template::parse("").unwrap();
+        assert_eq!(template, Template::default());
+    }
+
+    #[test]
+    fn no_keys() {
+        let template = Template::parse("hello world").unwrap();
+        assert_eq!(template, template!(Text("hello world")));
+    }
+
+    #[test]
+    fn leading_key() {
+        let template = Template::parse("{salutation} world").unwrap();
+        assert_eq!(template, template!(Key("salutation"), Text(" world")));
+    }
+
+    #[test]
+    fn trailing_key() {
+        let template = Template::parse("hello {name}").unwrap();
+        assert_eq!(template, template!(Text("hello "), Key("name")));
+    }
+
+    #[test]
+    fn middle_key() {
+        let template = Template::parse("hello {name}!").unwrap();
+        assert_eq!(template, template!(Text("hello "), Key("name"), Text("!")));
+    }
+
+    #[test]
+    fn middle_text() {
+        let template = Template::parse("{salutation} good {title}").unwrap();
+        assert_eq!(
+            template,
+            template!(Key("salutation"), Text(" good "), Key("title"))
+        );
+    }
+
+    #[test]
+    fn multiline() {
+        let template = Template::parse(
+            "
+            And if thy native country was { ancient civilisation },
+            What need to slight thee? Came not {hero} thence,
+            Who gave to { country } her books and art of writing?
+        ",
+        )
+        .unwrap();
+        assert_eq!(
+            template,
+            template!(
+                Text("\n            And if thy native country was "),
+                Key("ancient civilisation"),
+                Text(",\n            What need to slight thee? Came not "),
+                Key("hero"),
+                Text(" thence,\n            Who gave to "),
+                Key("country"),
+                Text(" her books and art of writing?\n        "),
+            )
+        );
+    }
+
+    #[test]
+    fn key_no_whitespace() {
+        let template = Template::parse("{word}").unwrap();
+        assert_eq!(template, template!(Key("word")));
+    }
+
+    #[test]
+    fn key_leading_whitespace() {
+        let template = Template::parse("{ word}").unwrap();
+        assert_eq!(template, template!(Key("word")));
+    }
+
+    #[test]
+    fn key_trailing_whitespace() {
+        let template = Template::parse("{word\n}").unwrap();
+        assert_eq!(template, template!(Key("word")));
+    }
+
+    #[test]
+    fn key_both_whitespace() {
+        let template = Template::parse(
+            "{
+            \tword
+        }",
+        )
+        .unwrap();
+        assert_eq!(template, template!(Key("word")));
+    }
+
+    #[test]
+    fn key_inner_whitespace() {
+        let template = Template::parse("{ a word }").unwrap();
+        assert_eq!(template, template!(Key("a word")));
+    }
+
+    #[test]
+    fn escape_left() {
+        let template = Template::parse(r"this \{ single left brace").unwrap();
+        assert_eq!(
+            template,
+            template!(Text("this "), Text("{"), Text(" single left brace"))
+        );
+    }
+
+    #[test]
+    fn escape_right() {
+        let template = Template::parse(r"this \} single right brace").unwrap();
+        assert_eq!(
+            template,
+            template!(Text("this "), Text("}"), Text(" single right brace"))
+        );
+    }
+
+    #[test]
+    fn escape_both() {
+        let template = Template::parse(r"these \{ two \} braces").unwrap();
+        assert_eq!(
+            template,
+            template!(
+                Text("these "),
+                Text("{"),
+                Text(" two "),
+                Text("}"),
+                Text(" braces")
+            )
+        );
+    }
+
+    #[test]
+    fn escape_doubled() {
+        let template = Template::parse(r"these \{\{ four \}\} braces").unwrap();
+        assert_eq!(
+            template,
+            template!(
+                Text("these "),
+                Text("{"),
+                Text("{"),
+                Text(" four "),
+                Text("}"),
+                Text("}"),
+                Text(" braces")
+            )
+        );
+    }
+
+    #[test]
+    fn escape_escape() {
+        let template = Template::parse(r"these \\ backslashes \\\\").unwrap();
+        assert_eq!(
+            template,
+            template!(
+                Text("these "),
+                Text(r"\"),
+                Text(" backslashes "),
+                Text(r"\"),
+                Text(r"\"),
+            )
+        );
+    }
+
+    #[test]
+    fn escape_before_key() {
+        let template = Template::parse(r"\\{ a } \{{ b } \}{ c }").unwrap();
+        assert_eq!(
+            template,
+            template!(
+                Text(r"\"),
+                Key("a"),
+                Text(" "),
+                Text(r"{"),
+                Key("b"),
+                Text(" "),
+                Text(r"}"),
+                Key("c"),
+            )
+        );
+    }
+
+    #[test]
+    fn escape_after_key() {
+        let template = Template::parse(r"{ a }\\ { b }\{ { c }\}").unwrap();
+        assert_eq!(
+            template,
+            template!(
+                Key("a"),
+                Text(r"\"),
+                Text(" "),
+                Key("b"),
+                Text(r"{"),
+                Text(" "),
+                Key("c"),
+                Text(r"}"),
+            )
+        );
+    }
+
+    #[test]
+    fn multibyte_texts() {
+        let template = Template::parse("幸徳 {particle} 秋水").unwrap();
+        assert_eq!(
+            template,
+            template!(Text("幸徳 "), Key("particle"), Text(" 秋水"))
+        );
+    }
+
+    #[test]
+    fn multibyte_key() {
+        let template = Template::parse("The { 連盟 }").unwrap();
+        assert_eq!(template, template!(Text("The "), Key("連盟")));
+    }
+
+    #[test]
+    fn multibyte_both() {
+        let template = Template::parse("大杉 {栄}").unwrap();
+        assert_eq!(template, template!(Text("大杉 "), Key("栄")));
+    }
+
+    #[test]
+    fn multibyte_whitespace() {
+        let template = Template::parse("岩佐 作{ 太 }郎").unwrap();
+        assert_eq!(template, template!(Text("岩佐 作"), Key("太"), Text("郎")));
+    }
+
+    #[test]
+    fn multibyte_with_escapes() {
+        let template = Template::parse(r"日本\{アナキスト\}連盟").unwrap();
+        assert_eq!(
+            template,
+            template!(
+                Text("日本"),
+                Text(r"{"),
+                Text("アナキスト"),
+                Text(r"}"),
+                Text("連盟")
+            )
+        );
+    }
+
+    #[test]
+    fn multibyte_rtl_text() {
+        let template = Template::parse("محمد صايل").unwrap();
+        assert_eq!(template, template!(Text("محمد صايل")));
+    }
+
+    #[test]
+    fn multibyte_rtl_key() {
+        let template = Template::parse("محمد {ريشة}").unwrap();
+        assert_eq!(template, template!(Text("محمد "), Key("ريشة")));
+    }
+}
+
+#[cfg(test)]
+mod test_error {
+    use crate::{ParseError, Template};
+
+    #[test]
+    fn key_left_half() {
+        let template = Template::parse("{ open").unwrap_err();
+        assert_eq!(template, ParseError::unbalanced("{ open", 0, 6));
+    }
+
+    #[test]
+    fn key_right_half() {
+        let template = Template::parse("open }").unwrap_err();
+        assert_eq!(template, ParseError::unbalanced("open }", 5, 5));
+    }
+
+    #[test]
+    fn key_with_half_escape() {
+        let template = Template::parse(r"this is { not \ allowed }").unwrap_err();
+        assert_eq!(
+            template,
+            ParseError::key_escape(r"this is { not \ allowed }", 8, 14)
+        );
+    }
+
+    #[test]
+    fn key_with_full_escape() {
+        let template = Template::parse(r"{ not \} allowed }").unwrap_err();
+        assert_eq!(
+            template,
+            ParseError::key_escape(r"{ not \} allowed }", 0, 6)
+        );
+    }
+
+    #[test]
+    fn key_empty() {
+        let template = Template::parse(r"void: {}").unwrap_err();
+        assert_eq!(template, ParseError::key_empty(r"void: {}", 6, 7));
+    }
+
+    #[test]
+    fn key_only_whitespace() {
+        let template = Template::parse(r"nothing: { }").unwrap_err();
+        assert_eq!(template, ParseError::key_empty(r"nothing: { }", 9, 11));
+    }
+
+    #[test]
+    fn bad_escape() {
+        let template = Template::parse(r"not \a thing").unwrap_err();
+        assert_eq!(template, ParseError::escape(r"not \a thing", 4, 5));
+    }
+
+    #[test]
+    fn end_escape() {
+        let template = Template::parse(r"forget me not \").unwrap_err();
+        assert_eq!(template, ParseError::escape(r"forget me not \", 14, 15));
+    }
+}
diff --git a/crates/leon/src/template.rs b/crates/leon/src/template.rs
new file mode 100644
index 00000000..bf32e92d
--- /dev/null
+++ b/crates/leon/src/template.rs
@@ -0,0 +1,198 @@
+use std::{borrow::Cow, fmt::Display, io::Write, ops::Add};
+
+use crate::{ParseError, RenderError, Values};
+
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct Template<'s> {
+    pub items: Cow<'s, [Item<'s>]>,
+    pub default: Option<Cow<'s, str>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Item<'s> {
+    Text(&'s str),
+    Key(&'s str),
+}
+
+impl<'s> Template<'s> {
+    /// Construct a template with the given items and default.
+    ///
+    /// You can write a template literal without any help by constructing it directly:
+    ///
+    /// ```
+    /// use std::borrow::Cow;
+    /// use leon::{Item, Template};
+    /// const TEMPLATE: Template = Template {
+    ///     items: Cow::Borrowed({
+    ///         const ITEMS: &'static [Item<'static>] = &[
+    ///             Item::Text("Hello"),
+    ///             Item::Key("name"),
+    ///         ];
+    ///         ITEMS
+    ///     }),
+    ///     default: None,
+    /// };
+    /// assert_eq!(TEMPLATE.render(&[("name", "world")]).unwrap(), "Helloworld");
+    /// ```
+    ///
+    /// As that's a bit verbose, using this function and the enum shorthands can be helpful:
+    ///
+    /// ```
+    /// use leon::{Item, Item::*, Template};
+    /// const TEMPLATE: Template = Template::new({
+    ///     const ITEMS: &'static [Item<'static>] = &[Text("Hello "), Key("name")];
+    ///     ITEMS
+    /// }, Some("world"));
+    ///
+    /// assert_eq!(TEMPLATE.render(&[("unrelated", "value")]).unwrap(), "Hello world");
+    /// ```
+    ///
+    /// For an even more ergonomic syntax, see the [`leon::template!`] macro.
+    pub const fn new(items: &'s [Item<'s>], default: Option<&'s str>) -> Template<'s> {
+        Template {
+            items: Cow::Borrowed(items),
+            default: match default {
+                Some(default) => Some(Cow::Borrowed(default)),
+                None => None,
+            },
+        }
+    }
+
+    /// Parse a template from a string.
+    ///
+    /// # Syntax
+    ///
+    /// ```plain
+    /// it is better to rule { group }
+    /// one can live {adverb} without power
+    /// ```
+    ///
+    /// A replacement is denoted by `{` and `}`. The contents of the braces, trimmed
+    /// of any whitespace, are the key. Any text outside of braces is left as-is.
+    ///
+    /// To escape a brace, use `\{` or `\}`. To escape a backslash, use `\\`. Keys
+    /// cannot contain escapes.
+    ///
+    /// ```plain
+    /// \{ leon \}
+    /// ```
+    ///
+    /// The above examples, given the values `group = "no one"` and
+    /// `adverb = "honourably"`, would render to:
+    ///
+    /// ```plain
+    /// it is better to rule no one
+    /// one can live honourably without power
+    /// { leon }
+    /// ```
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use leon::Template;
+    /// let template = Template::parse("hello {name}").unwrap();
+    /// ```
+    ///
+    pub fn parse(s: &'s str) -> Result<Self, ParseError> {
+        Self::parse_items(s).map(|items| Template {
+            items: Cow::Owned(items),
+            default: None,
+        })
+    }
+
+    pub fn render_into(
+        &self,
+        writer: &mut dyn Write,
+        values: &dyn Values,
+    ) -> Result<(), RenderError> {
+        for token in self.items.as_ref() {
+            match token {
+                Item::Text(text) => writer.write_all(text.as_bytes())?,
+                Item::Key(key) => {
+                    if let Some(value) = values.get_value(key) {
+                        writer.write_all(value.as_bytes())?;
+                    } else if let Some(default) = &self.default {
+                        writer.write_all(default.as_bytes())?;
+                    } else {
+                        return Err(RenderError::MissingKey(key.to_string()));
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+
+    pub fn render(&self, values: &dyn Values) -> Result<String, RenderError> {
+        let mut buf = Vec::with_capacity(
+            self.items
+                .iter()
+                .map(|item| match item {
+                    Item::Key(_) => 0,
+                    Item::Text(t) => t.len(),
+                })
+                .sum(),
+        );
+        self.render_into(&mut buf, values)?;
+
+        // UNWRAP: We know that the buffer is valid UTF-8 because we only write strings.
+        Ok(String::from_utf8(buf).unwrap())
+    }
+
+    pub fn has_key(&self, key: &str) -> bool {
+        self.has_keys(&[key])
+    }
+
+    pub fn has_keys(&self, keys: &[&str]) -> bool {
+        self.items.iter().any(|token| match token {
+            Item::Key(k) => keys.contains(k),
+            _ => false,
+        })
+    }
+
+    pub fn keys(&self) -> impl Iterator<Item = &&str> {
+        self.items.iter().filter_map(|token| match token {
+            Item::Key(k) => Some(k),
+            _ => None,
+        })
+    }
+
+    /// Sets the default value for this template.
+    pub fn set_default(&mut self, default: &dyn Display) {
+        self.default = Some(Cow::Owned(default.to_string()));
+    }
+}
+
+impl<'s> Add for Template<'s> {
+    type Output = Self;
+
+    fn add(mut self, rhs: Self) -> Self::Output {
+        self.items
+            .to_mut()
+            .extend(rhs.items.as_ref().iter().cloned());
+        if let Some(default) = rhs.default {
+            self.default = Some(default);
+        }
+        self
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::Item::{Key, Text};
+
+    #[test]
+    fn concat_templates() {
+        let t1 = crate::template!(Text("Hello"), Key("name"));
+        let t2 = crate::template!(Text("have a"), Key("adjective"), Text("day"));
+        assert_eq!(
+            t1 + t2,
+            crate::template!(
+                Text("Hello"),
+                Key("name"),
+                Text("have a"),
+                Key("adjective"),
+                Text("day")
+            ),
+        );
+    }
+}
diff --git a/crates/leon/src/values.rs b/crates/leon/src/values.rs
new file mode 100644
index 00000000..3e7d8ca9
--- /dev/null
+++ b/crates/leon/src/values.rs
@@ -0,0 +1,133 @@
+use std::{
+    borrow::{Borrow, Cow},
+    collections::{BTreeMap, HashMap},
+    hash::{BuildHasher, Hash},
+};
+
+pub trait Values {
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>>;
+}
+
+impl<T> Values for &T
+where
+    T: Values,
+{
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+        T::get_value(self, key)
+    }
+}
+
+impl<K, V> Values for [(K, V)]
+where
+    K: AsRef<str>,
+    V: AsRef<str>,
+{
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+        self.iter().find_map(|(k, v)| {
+            if k.as_ref() == key {
+                Some(Cow::Borrowed(v.as_ref()))
+            } else {
+                None
+            }
+        })
+    }
+}
+
+impl<K, V> Values for &[(K, V)]
+where
+    K: AsRef<str>,
+    V: AsRef<str>,
+{
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+        (*self).get_value(key)
+    }
+}
+
+impl<K, V, const N: usize> Values for [(K, V); N]
+where
+    K: AsRef<str>,
+    V: AsRef<str>,
+{
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+        self.as_slice().get_value(key)
+    }
+}
+
+impl<K, V> Values for Vec<(K, V)>
+where
+    K: AsRef<str>,
+    V: AsRef<str>,
+{
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+        self.as_slice().get_value(key)
+    }
+}
+
+impl<K, V, S> Values for HashMap<K, V, S>
+where
+    K: Borrow<str> + Eq + Hash,
+    V: AsRef<str>,
+    S: BuildHasher,
+{
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+        self.get(key).map(|v| Cow::Borrowed(v.as_ref()))
+    }
+}
+
+impl<K, V> Values for BTreeMap<K, V>
+where
+    K: Borrow<str> + Ord,
+    V: AsRef<str>,
+{
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+        self.get(key).map(|v| Cow::Borrowed(v.as_ref()))
+    }
+}
+
+/// Workaround to allow using functions as [`Values`].
+///
+/// As this isn't constructible you'll want to use [`vals()`] instead.
+pub struct ValuesFn<F>
+where
+    F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
+{
+    inner: F,
+}
+
+impl<F> Values for ValuesFn<F>
+where
+    F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
+{
+    fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
+        (self.inner)(key)
+    }
+}
+
+impl<F> From<F> for ValuesFn<F>
+where
+    F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
+{
+    fn from(inner: F) -> Self {
+        Self { inner }
+    }
+}
+
+/// Workaround to allow using functions as [`Values`].
+///
+/// Wraps your function so it implements [`Values`].
+///
+/// # Example
+///
+/// ```
+/// use leon::{Values, vals};
+///
+/// fn use_values(_values: impl Values) {}
+///
+/// use_values(vals(|_| Some("hello".into())));
+/// ```
+pub const fn vals<F>(func: F) -> ValuesFn<F>
+where
+    F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
+{
+    ValuesFn { inner: func }
+}