feat(templates): add Tera runtime template engine
Lay groundwork for user-editable templates by adding Tera as a runtime template engine alongside the existing maud templates. Changes: - Add tera dependency - Create TemplateEngine struct with render methods - Add TemplateLoad/TemplateRender error variants - Add section_type/template fields to Frontmatter - Create templates/ directory with base, page, section, and content templates Dead code warnings are expected; TemplateEngine will be wired in to replace maud in subsequent commits.
This commit is contained in:
283
Cargo.lock
generated
283
Cargo.lock
generated
@@ -41,6 +41,15 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.100"
|
version = "1.0.100"
|
||||||
@@ -53,6 +62,12 @@ version = "0.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64-simd"
|
name = "base64-simd"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -114,6 +129,16 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.1"
|
version = "3.19.1"
|
||||||
@@ -164,6 +189,39 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"num-traits",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono-tz"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz-build",
|
||||||
|
"phf 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono-tz-build"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
|
||||||
|
dependencies = [
|
||||||
|
"parse-zoneinfo",
|
||||||
|
"phf 0.11.3",
|
||||||
|
"phf_codegen 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-str"
|
name = "const-str"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -193,6 +251,12 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -339,6 +403,12 @@ dependencies = [
|
|||||||
"matches",
|
"matches",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deunicode"
|
||||||
|
version = "1.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -455,6 +525,30 @@ dependencies = [
|
|||||||
"wasip2",
|
"wasip2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globset"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"bstr",
|
||||||
|
"log",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globwalk"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"ignore",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "graphlib_rust"
|
name = "graphlib_rust"
|
||||||
version = "0.0.2"
|
version = "0.0.2"
|
||||||
@@ -516,12 +610,61 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humansize"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||||
|
dependencies = [
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ident_case"
|
name = "ident_case"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ignore"
|
||||||
|
version = "0.4.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"globset",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"same-file",
|
||||||
|
"walkdir",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -600,6 +743,12 @@ version = "0.2.180"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lightningcss"
|
name = "lightningcss"
|
||||||
version = "1.0.0-alpha.70"
|
version = "1.0.0-alpha.70"
|
||||||
@@ -724,6 +873,7 @@ dependencies = [
|
|||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"tera",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
@@ -743,6 +893,15 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -804,6 +963,15 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-zoneinfo"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
|
||||||
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pastey"
|
name = "pastey"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -816,6 +984,12 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.8.5"
|
version = "2.8.5"
|
||||||
@@ -964,6 +1138,15 @@ dependencies = [
|
|||||||
"siphasher",
|
"siphasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1078,6 +1261,18 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1086,6 +1281,9 @@ name = "rand_core"
|
|||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rapidhash"
|
name = "rapidhash"
|
||||||
@@ -1338,6 +1536,16 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slug"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
|
||||||
|
dependencies = [
|
||||||
|
"deunicode",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -1418,6 +1626,28 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tera"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
|
"globwalk",
|
||||||
|
"humansize",
|
||||||
|
"lazy_static",
|
||||||
|
"percent-encoding",
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"rand",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"slug",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -1821,12 +2051,65 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ lightningcss = "1.0.0-alpha.70"
|
|||||||
# Config parsing
|
# Config parsing
|
||||||
katex-rs = "0.2.3"
|
katex-rs = "0.2.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
tera = "1"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
|
||||||
# Diagram rendering
|
# Diagram rendering
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use gray_matter::{engine::YAML, Matter};
|
use gray_matter::{engine::YAML, Matter};
|
||||||
|
use serde::Serialize;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ pub enum ContentKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A navigation menu item discovered from the filesystem.
|
/// A navigation menu item discovered from the filesystem.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct NavItem {
|
pub struct NavItem {
|
||||||
/// Display label (from nav_label or title)
|
/// Display label (from nav_label or title)
|
||||||
pub label: String,
|
pub label: String,
|
||||||
@@ -42,6 +43,10 @@ pub struct Frontmatter {
|
|||||||
pub link_to: Option<String>,
|
pub link_to: Option<String>,
|
||||||
/// Custom navigation label (defaults to title)
|
/// Custom navigation label (defaults to title)
|
||||||
pub nav_label: Option<String>,
|
pub nav_label: Option<String>,
|
||||||
|
/// Section type for template dispatch (e.g., "blog", "projects")
|
||||||
|
pub section_type: Option<String>,
|
||||||
|
/// Override template for this content item
|
||||||
|
pub template: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A content item ready for rendering.
|
/// A content item ready for rendering.
|
||||||
@@ -134,6 +139,8 @@ fn parse_frontmatter(path: &Path, parsed: &gray_matter::ParsedEntity) -> Result<
|
|||||||
let weight = pod.get("weight").and_then(|v| v.as_i64().ok());
|
let weight = pod.get("weight").and_then(|v| v.as_i64().ok());
|
||||||
let link_to = pod.get("link_to").and_then(|v| v.as_string().ok());
|
let link_to = pod.get("link_to").and_then(|v| v.as_string().ok());
|
||||||
let nav_label = pod.get("nav_label").and_then(|v| v.as_string().ok());
|
let nav_label = pod.get("nav_label").and_then(|v| v.as_string().ok());
|
||||||
|
let section_type = pod.get("section_type").and_then(|v| v.as_string().ok());
|
||||||
|
let template = pod.get("template").and_then(|v| v.as_string().ok());
|
||||||
|
|
||||||
// Handle nested taxonomies.tags structure
|
// Handle nested taxonomies.tags structure
|
||||||
let tags = if let Some(taxonomies) = pod.get("taxonomies") {
|
let tags = if let Some(taxonomies) = pod.get("taxonomies") {
|
||||||
@@ -162,6 +169,8 @@ fn parse_frontmatter(path: &Path, parsed: &gray_matter::ParsedEntity) -> Result<
|
|||||||
weight,
|
weight,
|
||||||
link_to,
|
link_to,
|
||||||
nav_label,
|
nav_label,
|
||||||
|
section_type,
|
||||||
|
template,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ pub enum Error {
|
|||||||
/// Failed to parse configuration file.
|
/// Failed to parse configuration file.
|
||||||
#[error("invalid config in {path}: {message}")]
|
#[error("invalid config in {path}: {message}")]
|
||||||
Config { path: PathBuf, message: String },
|
Config { path: PathBuf, message: String },
|
||||||
|
|
||||||
|
/// Failed to load templates.
|
||||||
|
#[error("failed to load templates: {message}")]
|
||||||
|
TemplateLoad { message: String },
|
||||||
|
|
||||||
|
/// Failed to render template.
|
||||||
|
#[error("failed to render template '{template}': {message}")]
|
||||||
|
TemplateRender { template: String, message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result type alias for compiler operations.
|
/// Result type alias for compiler operations.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod highlight;
|
|||||||
mod math;
|
mod math;
|
||||||
mod mermaid;
|
mod mermaid;
|
||||||
mod render;
|
mod render;
|
||||||
|
mod template_engine;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
use crate::content::{discover_nav, Content, ContentKind, NavItem};
|
use crate::content::{discover_nav, Content, ContentKind, NavItem};
|
||||||
|
|||||||
199
src/template_engine.rs
Normal file
199
src/template_engine.rs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
//! Tera-based template engine for runtime HTML generation.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
|
use crate::config::SiteConfig;
|
||||||
|
use crate::content::{Content, NavItem};
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
/// Runtime template engine wrapping Tera.
|
||||||
|
pub struct TemplateEngine {
|
||||||
|
tera: Tera,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateEngine {
|
||||||
|
/// Load templates from a directory (glob pattern: `templates/**/*`).
|
||||||
|
pub fn new(template_dir: &Path) -> Result<Self> {
|
||||||
|
let pattern = template_dir.join("**/*").display().to_string();
|
||||||
|
let tera = Tera::new(&pattern).map_err(|e| Error::TemplateLoad {
|
||||||
|
message: e.to_string(),
|
||||||
|
})?;
|
||||||
|
Ok(Self { tera })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a template by name with the given context.
|
||||||
|
pub fn render(&self, template_name: &str, context: &Context) -> Result<String> {
|
||||||
|
self.tera
|
||||||
|
.render(template_name, context)
|
||||||
|
.map_err(|e| Error::TemplateRender {
|
||||||
|
template: template_name.to_string(),
|
||||||
|
message: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a standalone page (about, collab, etc.).
|
||||||
|
pub fn render_page(
|
||||||
|
&self,
|
||||||
|
content: &Content,
|
||||||
|
html_body: &str,
|
||||||
|
page_path: &str,
|
||||||
|
config: &SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
|
) -> Result<String> {
|
||||||
|
let mut ctx = self.base_context(page_path, config, nav);
|
||||||
|
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
|
||||||
|
ctx.insert("content", html_body);
|
||||||
|
self.render("page.html", &ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a content item (blog post, project, etc.).
|
||||||
|
pub fn render_content(
|
||||||
|
&self,
|
||||||
|
content: &Content,
|
||||||
|
html_body: &str,
|
||||||
|
page_path: &str,
|
||||||
|
config: &SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
|
) -> Result<String> {
|
||||||
|
let template = content
|
||||||
|
.frontmatter
|
||||||
|
.template
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("content/default.html");
|
||||||
|
let mut ctx = self.base_context(page_path, config, nav);
|
||||||
|
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
|
||||||
|
ctx.insert("content", html_body);
|
||||||
|
self.render(template, &ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a section index page (blog index, projects index).
|
||||||
|
pub fn render_section(
|
||||||
|
&self,
|
||||||
|
section: &Content,
|
||||||
|
items: &[ContentContext],
|
||||||
|
page_path: &str,
|
||||||
|
config: &SiteConfig,
|
||||||
|
nav: &[NavItem],
|
||||||
|
) -> Result<String> {
|
||||||
|
let section_type = section
|
||||||
|
.frontmatter
|
||||||
|
.section_type
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("default");
|
||||||
|
let template = format!("section/{}.html", section_type);
|
||||||
|
|
||||||
|
let mut ctx = self.base_context(page_path, config, nav);
|
||||||
|
ctx.insert("section", &FrontmatterContext::from(§ion.frontmatter));
|
||||||
|
ctx.insert("items", items);
|
||||||
|
self.render(&template, &ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build base context with common variables.
|
||||||
|
fn base_context(&self, page_path: &str, config: &SiteConfig, nav: &[NavItem]) -> Context {
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
ctx.insert("config", &ConfigContext::from(config));
|
||||||
|
ctx.insert("nav", nav);
|
||||||
|
ctx.insert("page_path", page_path);
|
||||||
|
ctx.insert("prefix", &relative_prefix(page_path));
|
||||||
|
ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute relative path prefix based on page depth.
|
||||||
|
fn relative_prefix(page_path: &str) -> String {
|
||||||
|
let depth = page_path.matches('/').count().saturating_sub(1);
|
||||||
|
if depth == 0 {
|
||||||
|
".".to_string()
|
||||||
|
} else {
|
||||||
|
(0..depth).map(|_| "..").collect::<Vec<_>>().join("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Context structs for Tera serialization
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Site config context for templates.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ConfigContext {
|
||||||
|
pub title: String,
|
||||||
|
pub author: String,
|
||||||
|
pub base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SiteConfig> for ConfigContext {
|
||||||
|
fn from(config: &SiteConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
title: config.title.clone(),
|
||||||
|
author: config.author.clone(),
|
||||||
|
base_url: config.base_url.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frontmatter context for templates.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct FrontmatterContext {
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub date: Option<String>,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
pub weight: Option<i64>,
|
||||||
|
pub link_to: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&crate::content::Frontmatter> for FrontmatterContext {
|
||||||
|
fn from(fm: &crate::content::Frontmatter) -> Self {
|
||||||
|
Self {
|
||||||
|
title: fm.title.clone(),
|
||||||
|
description: fm.description.clone(),
|
||||||
|
date: fm.date.clone(),
|
||||||
|
tags: fm.tags.clone(),
|
||||||
|
weight: fm.weight,
|
||||||
|
link_to: fm.link_to.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content item context for section listings.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ContentContext {
|
||||||
|
pub frontmatter: FrontmatterContext,
|
||||||
|
pub body: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentContext {
|
||||||
|
pub fn from_content(content: &Content, content_dir: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
frontmatter: FrontmatterContext::from(&content.frontmatter),
|
||||||
|
body: content.body.clone(),
|
||||||
|
slug: content.slug.clone(),
|
||||||
|
path: format!("/{}", content.output_path(content_dir).display()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relative_prefix_root() {
|
||||||
|
assert_eq!(relative_prefix("/index.html"), ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relative_prefix_depth_1() {
|
||||||
|
assert_eq!(relative_prefix("/blog/index.html"), "..");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relative_prefix_depth_2() {
|
||||||
|
assert_eq!(relative_prefix("/blog/posts/foo.html"), "../..");
|
||||||
|
}
|
||||||
|
}
|
||||||
25
templates/base.html
Normal file
25
templates/base.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ title }} | {{ config.title }}</title>
|
||||||
|
<link rel="canonical" href="{{ config.base_url | trim_end_matches(pat='/') }}{{ page_path }}">
|
||||||
|
<link rel="alternate" type="application/atom+xml" title="Atom Feed" href="{{ config.base_url | trim_end_matches(pat='/') }}/feed.xml">
|
||||||
|
<link rel="stylesheet" href="{{ prefix }}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="{{ prefix }}/index.html">{{ config.title }}</a>
|
||||||
|
{% for item in nav %}
|
||||||
|
<a href="{{ prefix }}{{ item.path }}">{{ item.label }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock content %}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>© {{ config.author }}</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
templates/content/default.html
Normal file
13
templates/content/default.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
{% if page.description %}
|
||||||
|
<p class="description">{{ page.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<section class="content">
|
||||||
|
{{ content | safe }}
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
25
templates/content/post.html
Normal file
25
templates/content/post.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="post">
|
||||||
|
<header>
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
{% if page.date %}
|
||||||
|
<time class="date">{{ page.date }}</time>
|
||||||
|
{% endif %}
|
||||||
|
{% if page.description %}
|
||||||
|
<p class="description">{{ page.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if page.tags %}
|
||||||
|
<ul class="tags">
|
||||||
|
{% for tag in page.tags %}
|
||||||
|
<li><a href="{{ prefix }}/tags/{{ tag }}.html">{{ tag }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
<section class="content">
|
||||||
|
{{ content | safe }}
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
13
templates/homepage.html
Normal file
13
templates/homepage.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="hero">
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
{% if page.description %}
|
||||||
|
<p class="tagline">{{ page.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section class="content">
|
||||||
|
{{ content | safe }}
|
||||||
|
</section>
|
||||||
|
{% endblock content %}
|
||||||
10
templates/page.html
Normal file
10
templates/page.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="page">
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
<section class="content">
|
||||||
|
{{ content | safe }}
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
20
templates/section/blog.html
Normal file
20
templates/section/blog.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ section.title }}</h1>
|
||||||
|
<ul class="post-list">
|
||||||
|
{% for item in items %}
|
||||||
|
<li>
|
||||||
|
<a href="./{{ item.slug }}.html">
|
||||||
|
<span class="title">{{ item.frontmatter.title }}</span>
|
||||||
|
{% if item.frontmatter.date %}
|
||||||
|
<time class="date">{{ item.frontmatter.date }}</time>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if item.frontmatter.description %}
|
||||||
|
<p class="description">{{ item.frontmatter.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
||||||
15
templates/section/default.html
Normal file
15
templates/section/default.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ section.title }}</h1>
|
||||||
|
<ul class="item-list">
|
||||||
|
{% for item in items %}
|
||||||
|
<li>
|
||||||
|
<a href="./{{ item.slug }}.html">{{ item.frontmatter.title }}</a>
|
||||||
|
{% if item.frontmatter.description %}
|
||||||
|
<p>{{ item.frontmatter.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
||||||
24
templates/section/projects.html
Normal file
24
templates/section/projects.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ section.title }}</h1>
|
||||||
|
<ul class="project-cards">
|
||||||
|
{% for item in items %}
|
||||||
|
<li class="card">
|
||||||
|
{% if item.frontmatter.link_to %}
|
||||||
|
<a href="{{ item.frontmatter.link_to }}" target="_blank" rel="noopener">
|
||||||
|
<h2>{{ item.frontmatter.title }}</h2>
|
||||||
|
{% if item.frontmatter.description %}
|
||||||
|
<p>{{ item.frontmatter.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<h2>{{ item.frontmatter.title }}</h2>
|
||||||
|
{% if item.frontmatter.description %}
|
||||||
|
<p>{{ item.frontmatter.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
||||||
Reference in New Issue
Block a user