feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"files":{".cargo_vcs_info.json":"da56363c60115bb459fa65db8130be672b0bfd430fa1069f12b5fd1c22d9d3c3","Cargo.lock":"b56ef933a1822e7eabdbde7234b316822d1d399b86770ac60725b609c9f42cf3","Cargo.toml":"5918f7badda2071f9a1a5b6bad9ea4e121b86c46867c60939d9ca80a880ca0e1","Cargo.toml.orig":"7061408e3951a5cb5cedf6e8b6ba51eaf268b92829eedde4c7be1a98d8ef9487","LICENSE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","README.md":"a9313ae108f3c24f1026848e5af8d482f8749e992733bfef1cb17a181cfa3e41","doc-header.html":"3c1fea930b377f7fba200a10cd01e3648b39cba7b3e9818ad0f553b1bd4546df","src/compiler/ast.rs":"ef889051ac949c5e5000d281c74cd0309afc8180d4da452f132ba404a0f63e7e","src/compiler/codegen.rs":"9e38d3baf3d179cd6b0eb8febf51c359852bed04c4a64832797b5d1ce310caf0","src/compiler/instructions.rs":"5b4cfe54ed8e6aadaef94094c5c853bed3dd1e53d40af1dd31172640cd7ca93e","src/compiler/lexer.rs":"796cba2bf999bd4bf62cf78aa228f5bc8ce38364adb939f1555eab1559c592e1","src/compiler/meta.rs":"84242e539fac6383d033ce66bc1e2a39a910c5f945b6696ee7fc9a87608085ec","src/compiler/mod.rs":"c2d2a38c1851b771f42b83f8bda94d22c675e9875364ad27fc9493cafc4c027a","src/compiler/parser.rs":"2d00c5f18cdc77d5b10fcf6ad9d54af58a30194215ea549733358b0f636d1609","src/compiler/tokens.rs":"afcb90b5f368b625a2fdfafdceb370adb08a0bca6216d9a9e7aaed7cd3ee772e","src/debug.rs":"fe1e6c59a1a7c1016142c108b6a67a99d3ed62f848070404650fdf7a3e557878","src/defaults.rs":"5211d305f4759fe63d2b4a99a24657e4f122cd8282bda56356fdacd192f0cf93","src/environment.rs":"10aea33d305545ad98af17f078906e6164852b203cd775666b3de861f2ac8a1f","src/error.rs":"fb70a3ab21d9562ad56419bf0f11791d5ec676442671d9c493ae1b6a466165fa","src/expression.rs":"41762d22e2d0a4f95ba83858472b49f531699ccf1a50ffdd0c6508fef78d36e0","src/filters.rs":"46c56bbc19717751eafa7e2e0a77562073d5aa2fdf69045cc95a7e776da461fd","src/format_utils.rs":"b750ff23971db0c9003da54a607b0a9814316203ece77c62ae93363cc86112b6","src/functions.rs":"a3957a0cf4126817f40db93fea91471e6c66baf453d49ae86aa7179ec0752699","src/lib.rs":"ad725a52c03b18ff1cf7b7b5a194b78f15b224bb8d5c54f670e0647b85b0e3c8","src/loader.rs":"cecfe6a7cd3866bcc716eb2d0420658a8a31d4cbdbc8ecd90592554035982fc3","src/macros.rs":"33f11566bfbb0fec478faf5d33c26c6fe7ca4339aa5aea68fb8b88338241ebdc","src/output.rs":"a56f2ebfeccb15b6edbd7268a1d7ce8ff4fe3736ba7eeefb68a7ef53e64ad2db","src/syntax.rs":"8c2153235c324e4c6b11cd13a54dea940996157ac386428bc78270c5820f4316","src/template.rs":"122212282bdc2cb80f8ba862ff9b9be4e101cb135afb91c9dbe1efa7b82d10b3","src/tests.rs":"5477a7d28d89dd6a5e9d3d508bdeedc9a0a9187d5deb126701848aff60bccf21","src/utils.rs":"1f4bffe028025cf6623bcc8d38a13ccfa0a21a5fe772119fb672571362167824","src/value/argtypes.rs":"5dcbe97ed090bf00a57ce1809b4569a6679683047035f80be69384f83935c8a2","src/value/deserialize.rs":"1556f550a0d8fd0286b40dd3cf83cb35fe03d826b88203ddb94ca3eb416fe4fb","src/value/merge_object.rs":"9e4a03ca2d52fa52c16a5005cd6b42bc5d2ed9c65107485878464c6d1c40d2da","src/value/mod.rs":"bf8504ab6a4c6d017f2e0468c67d8a38f5c6369df898540d59d9563f09659709","src/value/namespace_object.rs":"f728339ec8491df2e46c8a6284abc3ad6b59809fcec8c24f62c738dcde081e33","src/value/object.rs":"afb4e563ea1d698bb913a48444e26e316bd697d8a9f314b872a9403d472edc60","src/value/ops.rs":"0f860ca0953aee0c43b29e6e3757c2555d095eaaf6099119e21562ab336df701","src/value/serialize.rs":"a1feeb1fd48726f0e2a138386323b42f456a08c9a4aab1c6828946ae63995065","src/value/type_erase.rs":"39fefb4dd981ae3298e47596d4ab3cf9bf2ab6667514543090478f3a86579c0a","src/vendor/mod.rs":"0388670ca72a710f612f08602388623f191458b03cb49ddeff060a1d95dd3602","src/vendor/self_cell/LICENSE":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4","src/vendor/self_cell/mod.rs":"2688027a615c6b9f38c064406a110206aea161a8c552dfee23fa5c933627454a","src/vendor/self_cell/unsafe_self_cell.rs":"d34d8ce575cffeff01a40c76cc86250de8b155f7da674e1d934256ec2eeb5099","src/vm/closure_object.rs":"04109ef02507ec003f7ff8c4813483a1b4464c5c6918e35c71ef52df201ee6a2","src/vm/context.rs":"6a9d0d1ee2720fcda707bf2129e0d9cb7b27276f1a537027defcfa0015279441","src/vm/fuel.rs":"69e290dc96ca957c93699098f7fa2a4fec5fac5ed29611d71d0465639f87d02b","src/vm/loop_object.rs":"433cc10a2a1af7cd5aa0e1b967bed0a630c83566dea3c3a9fabdab9d027792ea","src/vm/macro_object.rs":"18f50ba7b0c943a2971f9b1e6089f2107aee04d1f131499e298fa804fd0712a8","src/vm/mod.rs":"c00bbeb7a7505d5787a14eb8b6b0834e0f44e48e319c38e868b7221303f3de1a","src/vm/module_object.rs":"b0683a9fdc47d9968c376a50fdf327b233ecce7ee4f8976547f9b34297c94a7b","src/vm/state.rs":"f9d772580548fe8fadd2ea159479aa47f40e652ef8d89c22dac5d1b81957cb17"},"package":"328251e58ad8e415be6198888fc207502727dc77945806421ab34f35bf012e7d"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "92f114d1fd62525b2b4dc1adb77ae1e83c1214a9"
},
"path_in_vcs": "minijinja"
}
+422
View File
@@ -0,0 +1,422 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "cc"
version = "1.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"windows-sys",
]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "globset"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "insta"
version = "1.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
dependencies = [
"console",
"globset",
"once_cell",
"serde",
"similar",
"walkdir",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memo-map"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
[[package]]
name = "minijinja"
version = "2.18.0"
dependencies = [
"aho-corasick",
"indexmap",
"insta",
"memo-map",
"percent-encoding",
"serde",
"serde_json",
"similar-asserts",
"stacker",
"unicase",
"unicode-ident",
"v_htmlescape",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
dependencies = [
"cc",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[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 = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
dependencies = [
"bstr",
"unicode-segmentation",
]
[[package]]
name = "similar-asserts"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b441962c817e33508847a22bd82f03a30cff43642dc2fae8b050566121eb9a"
dependencies = [
"console",
"similar",
]
[[package]]
name = "stacker"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "v_htmlescape"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+151
View File
@@ -0,0 +1,151 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.70"
name = "minijinja"
version = "2.18.0"
authors = ["Armin Ronacher <armin.ronacher@active-4.com>"]
build = false
exclude = ["tests"]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "a powerful template engine for Rust with minimal dependencies"
homepage = "https://github.com/mitsuhiko/minijinja"
readme = "README.md"
keywords = [
"jinja",
"jinja2",
"templates",
"template",
]
categories = ["template-engine"]
license = "Apache-2.0"
repository = "https://github.com/mitsuhiko/minijinja"
[package.metadata.docs.rs]
features = [
"loader",
"json",
"urlencode",
"custom_syntax",
"fuel",
]
rustdoc-args = [
"--cfg",
"docsrs",
"--html-in-header",
"doc-header.html",
]
[features]
adjacent_loop_items = []
builtins = []
custom_syntax = ["dep:aho-corasick"]
debug = []
default = [
"builtins",
"debug",
"deserialization",
"macros",
"multi_template",
"adjacent_loop_items",
"std_collections",
"serde",
]
deserialization = []
fuel = []
internal_debug = []
internal_safe_search = []
json = ["serde_json"]
key_interning = []
loader = []
loop_controls = []
macros = []
multi_template = []
preserve_order = ["indexmap"]
serde = []
speedups = ["v_htmlescape"]
std_collections = []
unicode = [
"unicode-ident",
"unicase",
]
unstable_machinery = ["internal_debug"]
unstable_machinery_serde = [
"unstable_machinery",
"serde/derive",
]
urlencode = ["percent-encoding"]
[lib]
name = "minijinja"
path = "src/lib.rs"
[dependencies.aho-corasick]
version = "1.0"
optional = true
default-features = false
[dependencies.indexmap]
version = "2.2.0"
optional = true
[dependencies.memo-map]
version = "0.3.1"
[dependencies.percent-encoding]
version = "2.2.0"
optional = true
[dependencies.serde]
version = "1.0.130"
[dependencies.serde_json]
version = "1.0.68"
optional = true
[dependencies.stacker]
version = "0.1.15"
optional = true
[dependencies.unicase]
version = "2.6.0"
optional = true
[dependencies.unicode-ident]
version = "1.0.5"
optional = true
[dependencies.v_htmlescape]
version = "0.15.8"
optional = true
[dev-dependencies.insta]
version = "1.38.0"
features = [
"glob",
"serde",
]
[dev-dependencies.serde]
version = "1.0.130"
features = ["derive"]
[dev-dependencies.serde_json]
version = "1.0.68"
[dev-dependencies.similar-asserts]
version = "1.4.2"
+201
View File
@@ -0,0 +1,201 @@
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
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+167
View File
@@ -0,0 +1,167 @@
<div align="center">
<img src="https://github.com/mitsuhiko/minijinja/raw/main/artwork/logo.png" alt="" width=320>
<p><strong>MiniJinja: a powerful template engine with minimal dependencies</strong></p>
[![License](https://img.shields.io/github/license/mitsuhiko/minijinja)](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE)
[![Crates.io](https://img.shields.io/crates/d/minijinja.svg)](https://crates.io/crates/minijinja)
[![rustc 1.63.0](https://img.shields.io/badge/rust-1.63%2B-orange.svg)](https://img.shields.io/badge/rust-1.63%2B-orange.svg)
[![Documentation](https://docs.rs/minijinja/badge.svg)](https://docs.rs/minijinja)
</div>
MiniJinja is a powerful but minimal dependency template engine
which is based on the syntax and behavior of the
[Jinja2](https://jinja.palletsprojects.com/) template engine for Python.
It's implemented in [Rust](https://github.com/mitsuhiko/minijinja/tree/main/minijinja) and [Go](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-go) and is also available via WASM for [JavaScript](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-js)
and as a [Python extension module](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-py) and as a [command line utility](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-cli).
It's supports all `serde` types and only has it as a single required
dependency. It supports [a range of features from Jinja2](https://github.com/mitsuhiko/minijinja/blob/main/COMPATIBILITY.md)
including inheritance, filters and more. The goal is that it should be possible
to use some templates in Rust programs without the fear of pulling in complex
dependencies for a small problem. Additionally it tries not to re-invent
something but stay in line with prior art to leverage an already existing
ecosystem of editor integrations.
```
$ cargo tree
minimal v0.1.0 (examples/minimal)
└── minijinja v2.18.0 (minijinja)
└── serde v1.0.144
```
Additionally minijinja is also available as an (optionally pre-compiled) command line executable
called [`minijinja-cli`](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-cli):
```
$ curl -sSfL https://github.com/mitsuhiko/minijinja/releases/latest/download/minijinja-cli-installer.sh | sh
$ echo "Hello {{ name }}" | minijinja-cli - -Dname=World
Hello World
```
You can play with MiniJinja online [in the browser playground](https://mitsuhiko.github.io/minijinja-playground/)
powered by a WASM build of MiniJinja.
**Goals:**
* [Well documented](https://docs.rs/minijinja), compact API
* Minimal dependencies, reasonable compile times and [decent runtime performance](https://github.com/mitsuhiko/minijinja/tree/main/benchmarks#comparison-results)
* [Stay as close as possible](https://github.com/mitsuhiko/minijinja/blob/main/COMPATIBILITY.md) to Jinja2
* Support for [expression evaluation](https://docs.rs/minijinja/latest/minijinja/struct.Expression.html) which
allows the use [as a DSL](https://github.com/mitsuhiko/minijinja/tree/main/examples/dsl)
* Support for all [`serde`](https://serde.rs) compatible types
* [Well tested](https://github.com/mitsuhiko/minijinja/tree/main/minijinja/tests)
* Support for [dynamic runtime objects](https://docs.rs/minijinja/latest/minijinja/value/trait.Object.html) with methods and dynamic attributes
* [Descriptive errors](https://github.com/mitsuhiko/minijinja/tree/main/examples/error)
* Bindings for [JavaScript](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-js),
[Python](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-py), and [C](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-cabi)
* Also available for [Go](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-go)
* Comes with a handy [CLI](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-cli)
* [Compiles to WebAssembly](https://github.com/mitsuhiko/minijinja-playground/blob/main/src/lib.rs)
## Example
**Example Template:**
```jinja
{% extends "layout.html" %}
{% block body %}
<p>Hello {{ name }}!</p>
{% endblock %}
```
**Invoking from Rust:**
```rust
use minijinja::{Environment, context};
fn main() {
let mut env = Environment::new();
env.add_template("hello.txt", "Hello {{ name }}!").unwrap();
let template = env.get_template("hello.txt").unwrap();
println!("{}", template.render(context! { name => "World" }).unwrap());
}
```
## Getting Help
If you are stuck with `MiniJinja`, have suggestions or need help, you can use the
[GitHub Discussions](https://github.com/mitsuhiko/minijinja/discussions).
## Related Crates
* [minijinja-autoreload](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-autoreload): provides
auto reloading functionality of environments
* [minijinja-embed](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-embed): provides
utilities for embedding templates in a binary
* [minijinja-contrib](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-contrib): provides
additional utilities too specific for the core
* [minijinja-py](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-py): makes MiniJinja
available to Python
* [minijinja-js](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-js): makes MiniJinja
available to JavaScript via WASM (for Node and Browser)
* [minijinja-go](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-go): a native Go
implementation of MiniJinja
* [minijinja-cli](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-cli): a command line utility.
* [minijinja-cabi](https://github.com/mitsuhiko/minijinja/tree/main/minijinja-cabi): a C binding to MiniJinja.
## Use Cases and Users
Here are some interesting Open Source users and use cases of MiniJinja. The examples link directly to where
the engine is used so you can see how it's utilized:
* AI Chat Templating:
* **[HuggingFace](https://huggingface.co/docs/text-generation-inference/index)** uses it to [render LLM chat templates](https://github.com/huggingface/text-generation-inference/blob/0759ec495e15a865d2a59befc2b796b5acc09b50/router/src/infer/mod.rs)
* **[mistral.rs](https://github.com/EricLBuehler/mistral.rs)** uses it to [render LLM chat templates](https://github.com/EricLBuehler/mistral.rs/blob/c834f59fe0b3b020a56cb6a0279a051370554539/mistralrs-core/src/pipeline/chat_template.rs)
* **[BoundaryML's BAML](https://docs.boundaryml.com/)** uses it to [render LLM chat templates](https://github.com/BoundaryML/baml/blob/17123de7ea653f51547576169bb0589d39053edc/engine/baml-lib/jinja/src/lib.rs)
* **[LSP-AI](https://github.com/SilasMarvin/lsp-ai)** uses it to [render LLM chat templates](https://github.com/SilasMarvin/lsp-ai/blob/1f70756c5b48e9098d64a7c5ce63ac803bc5d0ab/crates/lsp-ai/src/template.rs)
* **[LoRAX](https://loraexchange.ai/)** uses it to [render LLM chat templates](https://github.com/predibase/lorax/blob/6a83954b8c6ffd51eb69e7096ee2730d53b903dd/router/src/infer.rs)
* **[tensorzero](https://www.tensorzero.com/)** uses it to [render LLM and system templates](https://github.com/tensorzero/tensorzero/blob/26ae697f219c1f0385fe7936b9f04b97ff318f61/tensorzero-internal/src/minijinja_util.rs#L2)
* Data and Processing:
* **[Cube](https://cube.dev/docs/product/data-modeling/dynamic/jinja)** uses it [for data modelling](https://github.com/cube-js/cube/tree/db11c121c77c663845242366d3d972b9bc30ae54/packages/cubejs-backend-native/src/template/mj_value)
* **[PRQL](https://prql-lang.org/)** uses it [to handle DBT style pipelines](https://github.com/PRQL/prql/blob/59fb3cc4b9b6c9e195c928b1ba1134e2c5706ea3/prqlc/prqlc/src/cli/jinja.rs#L21)
* **[qsv](https://qsv.dathere.com)** uses it [to render templates from CSV files](https://github.com/jqnatividad/qsv/blob/master/src/cmd/template.rs#L2), to [construct payloads to post to web services](https://github.com/jqnatividad/qsv/blob/master/src/cmd/fetchpost.rs#L3) and to [infer Data Dictionaries, Descriptions & Tags or Chat with your data](https://github.com/dathere/qsv/blob/master/src/cmd/describegpt.rs#L2).
* HTML Generation:
* **[Zine](https://github.com/zineland/zine)** uses it to [generate static HTML](https://github.com/zineland/zine/blob/17285efe9f9a63b79a42a738b54d4d730b8cd551/src/engine.rs#L8)
* **[Oranda](https://github.com/axodotdev/oranda)** uses it to [generate HTML landing pages](https://github.com/axodotdev/oranda/blob/fb97859c99ab81f644ab5b1449f725fc5c3e9721/src/site/templates.rs)
* Code Generation:
* **[OpenTelemetry's Weaver](https://github.com/open-telemetry/weaver)** uses it to [generate documentation, code and other outputs](https://github.com/open-telemetry/weaver/blob/d49881445e09beb42e1a394bfa5f3068c660daf3/crates/weaver_forge/src/lib.rs#L482-L567) from the OTel specification.
* **[Maturin](https://github.com/PyO3/maturin)** uses it to [generate project structures](https://github.com/PyO3/maturin/blob/e35097e6cf3b9115736e8ae208972178029a20d0/src/new_project.rs)
* **[cargo-dist](https://github.com/axodotdev/cargo-dist)** uses it to [generate CI and project configuration](https://github.com/axodotdev/cargo-dist/blob/4cd61134863f54ca5a037400ebec71d039d42742/cargo-dist/src/backend/templates.rs)
## Similar Projects
These are related template engines for Rust:
* [Askama](https://crates.io/crates/askama): Jinja inspired, type-safe, requires template
precompilation. Has significant divergence from Jinja syntax in parts.
* [Tera](https://crates.io/crates/tera): Jinja inspired, dynamic, has divergences from Jinja.
* [Liquid](https://crates.io/crates/liquid): an implementation of Liquid templates for Rust.
Liquid was inspired by Django from which Jinja took its inspiration.
* [TinyTemplate](https://crates.io/crates/tinytemplate): minimal footprint template engine
with syntax that takes lose inspiration from Jinja and handlebars.
## Sponsor
If you like the project and find it useful you can [become a
sponsor](https://github.com/sponsors/mitsuhiko).
## AI Use Disclaimer
This codebase mostly predates LLM based code generation but some recent features
have been built with support of AI. For the AI contribution rules see
[AI Disclosure Rules](CONTRIBUTING.md#ai-disclosure).
## License and Links
- [Documentation](https://docs.rs/minijinja/)
- [Discussions](https://github.com/mitsuhiko/minijinja/discussions)
- [Examples](https://github.com/mitsuhiko/minijinja/tree/main/examples)
- [Issue Tracker](https://github.com/mitsuhiko/minijinja/issues)
- [MiniJinja Playground](https://mitsuhiko.github.io/minijinja-playground/)
- [Updating Guide](UPDATING.md)
- License: [Apache-2.0](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE)
+12
View File
@@ -0,0 +1,12 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/night-owl.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/languages/django.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("pre code").forEach(function(node) {
if (node.parentNode.className.match(/jinja|json/)) {
hljs.highlightElement(node.parentNode);
}
});
});
</script>
+631
View File
@@ -0,0 +1,631 @@
use std::ops::Deref;
#[cfg(feature = "internal_debug")]
use std::fmt;
use crate::compiler::tokens::Span;
use crate::value::{ops, value_map_with_capacity, Value};
/// Container for nodes with location info.
///
/// This container fulfills two purposes: it adds location information
/// to nodes, but it also ensures the nodes is heap allocated. The
/// latter is useful to ensure that enum variants do not cause the enum
/// to become too large.
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Spanned<T> {
inner: Box<(T, Span)>,
}
impl<T> Spanned<T> {
/// Creates a new spanned node.
pub fn new(node: T, span: Span) -> Spanned<T> {
Spanned {
inner: Box::new((node, span)),
}
}
/// Accesses the span.
pub fn span(&self) -> Span {
self.inner.1
}
}
impl<T> Deref for Spanned<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner.0
}
}
#[cfg(feature = "internal_debug")]
impl<T: fmt::Debug> fmt::Debug for Spanned<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
ok!(fmt::Debug::fmt(&self.inner.0, f));
write!(f, "{:?}", self.inner.1)
}
}
/// A statement node.
#[cfg_attr(
feature = "unstable_machinery_serde",
derive(serde::Serialize),
serde(tag = "stmt")
)]
pub enum Stmt<'a> {
Template(Spanned<Template<'a>>),
EmitExpr(Spanned<EmitExpr<'a>>),
EmitRaw(Spanned<EmitRaw<'a>>),
ForLoop(Spanned<ForLoop<'a>>),
IfCond(Spanned<IfCond<'a>>),
WithBlock(Spanned<WithBlock<'a>>),
Set(Spanned<Set<'a>>),
SetBlock(Spanned<SetBlock<'a>>),
AutoEscape(Spanned<AutoEscape<'a>>),
FilterBlock(Spanned<FilterBlock<'a>>),
#[cfg(feature = "multi_template")]
Block(Spanned<Block<'a>>),
#[cfg(feature = "multi_template")]
Import(Spanned<Import<'a>>),
#[cfg(feature = "multi_template")]
FromImport(Spanned<FromImport<'a>>),
#[cfg(feature = "multi_template")]
Extends(Spanned<Extends<'a>>),
#[cfg(feature = "multi_template")]
Include(Spanned<Include<'a>>),
#[cfg(feature = "macros")]
Macro(Spanned<Macro<'a>>),
#[cfg(feature = "macros")]
CallBlock(Spanned<CallBlock<'a>>),
#[cfg(feature = "loop_controls")]
Continue(Spanned<Continue>),
#[cfg(feature = "loop_controls")]
Break(Spanned<Break>),
Do(Spanned<Do<'a>>),
}
#[cfg(feature = "internal_debug")]
impl fmt::Debug for Stmt<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Stmt::Template(s) => fmt::Debug::fmt(s, f),
Stmt::EmitExpr(s) => fmt::Debug::fmt(s, f),
Stmt::EmitRaw(s) => fmt::Debug::fmt(s, f),
Stmt::ForLoop(s) => fmt::Debug::fmt(s, f),
Stmt::IfCond(s) => fmt::Debug::fmt(s, f),
Stmt::WithBlock(s) => fmt::Debug::fmt(s, f),
Stmt::Set(s) => fmt::Debug::fmt(s, f),
Stmt::SetBlock(s) => fmt::Debug::fmt(s, f),
Stmt::AutoEscape(s) => fmt::Debug::fmt(s, f),
Stmt::FilterBlock(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::Block(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::Extends(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::Include(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::Import(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::FromImport(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "macros")]
Stmt::Macro(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "macros")]
Stmt::CallBlock(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "loop_controls")]
Stmt::Continue(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "loop_controls")]
Stmt::Break(s) => fmt::Debug::fmt(s, f),
Stmt::Do(s) => fmt::Debug::fmt(s, f),
}
}
}
/// An expression node.
#[allow(clippy::enum_variant_names)]
#[cfg_attr(
feature = "unstable_machinery_serde",
derive(serde::Serialize),
serde(tag = "expr")
)]
pub enum Expr<'a> {
Var(Spanned<Var<'a>>),
Const(Spanned<Const>),
Slice(Spanned<Slice<'a>>),
UnaryOp(Spanned<UnaryOp<'a>>),
BinOp(Spanned<BinOp<'a>>),
IfExpr(Spanned<IfExpr<'a>>),
Filter(Spanned<Filter<'a>>),
Test(Spanned<Test<'a>>),
GetAttr(Spanned<GetAttr<'a>>),
GetItem(Spanned<GetItem<'a>>),
Call(Spanned<Call<'a>>),
List(Spanned<List<'a>>),
Map(Spanned<Map<'a>>),
}
#[cfg(feature = "internal_debug")]
impl fmt::Debug for Expr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expr::Var(s) => fmt::Debug::fmt(s, f),
Expr::Const(s) => fmt::Debug::fmt(s, f),
Expr::Slice(s) => fmt::Debug::fmt(s, f),
Expr::UnaryOp(s) => fmt::Debug::fmt(s, f),
Expr::BinOp(s) => fmt::Debug::fmt(s, f),
Expr::IfExpr(s) => fmt::Debug::fmt(s, f),
Expr::Filter(s) => fmt::Debug::fmt(s, f),
Expr::Test(s) => fmt::Debug::fmt(s, f),
Expr::GetAttr(s) => fmt::Debug::fmt(s, f),
Expr::GetItem(s) => fmt::Debug::fmt(s, f),
Expr::Call(s) => fmt::Debug::fmt(s, f),
Expr::List(s) => fmt::Debug::fmt(s, f),
Expr::Map(s) => fmt::Debug::fmt(s, f),
}
}
}
impl Expr<'_> {
pub fn description(&self) -> &'static str {
match self {
Expr::Var(_) => "variable",
Expr::Const(_) => "constant",
Expr::Slice(_)
| Expr::UnaryOp(_)
| Expr::BinOp(_)
| Expr::IfExpr(_)
| Expr::GetAttr(_)
| Expr::GetItem(_) => "expression",
Expr::Call(_) => "call",
Expr::List(_) => "list literal",
Expr::Map(_) => "map literal",
Expr::Test(_) => "test expression",
Expr::Filter(_) => "filter expression",
}
}
pub fn span(&self) -> Span {
match self {
Expr::Var(s) => s.span(),
Expr::Const(s) => s.span(),
Expr::Slice(s) => s.span(),
Expr::UnaryOp(s) => s.span(),
Expr::BinOp(s) => s.span(),
Expr::IfExpr(s) => s.span(),
Expr::Filter(s) => s.span(),
Expr::Test(s) => s.span(),
Expr::GetAttr(s) => s.span(),
Expr::GetItem(s) => s.span(),
Expr::Call(s) => s.span(),
Expr::List(s) => s.span(),
Expr::Map(s) => s.span(),
}
}
pub fn as_const(&self) -> Option<Value> {
match self {
Expr::Const(c) => Some(c.value.clone()),
Expr::List(l) => l.as_const(),
Expr::Map(m) => m.as_const(),
Expr::UnaryOp(c) => match c.op {
UnaryOpKind::Not => c.expr.as_const().map(|value| Value::from(!value.is_true())),
UnaryOpKind::Neg => c.expr.as_const().and_then(|v| ops::neg(&v).ok()),
},
Expr::BinOp(c) => {
let (Some(left), Some(right)) = (c.left.as_const(), c.right.as_const()) else {
return None;
};
match c.op {
BinOpKind::Add => ops::add(&left, &right).ok(),
BinOpKind::Sub => ops::sub(&left, &right).ok(),
BinOpKind::Mul => ops::mul(&left, &right).ok(),
BinOpKind::Div => ops::div(&left, &right).ok(),
BinOpKind::FloorDiv => ops::int_div(&left, &right).ok(),
BinOpKind::Rem => ops::rem(&left, &right).ok(),
BinOpKind::Pow => ops::pow(&left, &right).ok(),
BinOpKind::Concat => Some(ops::string_concat(left, &right)),
BinOpKind::Eq => Some(Value::from(left == right)),
BinOpKind::Ne => Some(Value::from(left != right)),
BinOpKind::Lt => Some(Value::from(left < right)),
BinOpKind::Lte => Some(Value::from(left <= right)),
BinOpKind::Gt => Some(Value::from(left > right)),
BinOpKind::Gte => Some(Value::from(left >= right)),
BinOpKind::In => ops::contains(&right, &left).ok(),
BinOpKind::ScAnd => Some(if left.is_true() && right.is_true() {
right
} else {
Value::from(false)
}),
BinOpKind::ScOr => Some(if left.is_true() { left } else { right }),
}
}
_ => None,
}
}
}
/// Root template node.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Template<'a> {
pub children: Vec<Stmt<'a>>,
}
/// A for loop.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct ForLoop<'a> {
pub target: Expr<'a>,
pub iter: Expr<'a>,
pub filter_expr: Option<Expr<'a>>,
pub recursive: bool,
pub body: Vec<Stmt<'a>>,
pub else_body: Vec<Stmt<'a>>,
}
/// An if/else condition.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct IfCond<'a> {
pub expr: Expr<'a>,
pub true_body: Vec<Stmt<'a>>,
pub false_body: Vec<Stmt<'a>>,
}
/// A with block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct WithBlock<'a> {
pub assignments: Vec<(Expr<'a>, Expr<'a>)>,
pub body: Vec<Stmt<'a>>,
}
/// A set statement.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Set<'a> {
pub target: Expr<'a>,
pub expr: Expr<'a>,
}
/// A set capture statement.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct SetBlock<'a> {
pub target: Expr<'a>,
pub filter: Option<Expr<'a>>,
pub body: Vec<Stmt<'a>>,
}
/// A block for inheritance elements.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Block<'a> {
pub name: &'a str,
pub body: Vec<Stmt<'a>>,
}
/// An extends block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Extends<'a> {
pub name: Expr<'a>,
}
/// An include block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Include<'a> {
pub name: Expr<'a>,
pub ignore_missing: bool,
}
/// An auto escape control block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct AutoEscape<'a> {
pub enabled: Expr<'a>,
pub body: Vec<Stmt<'a>>,
}
/// Applies filters to a block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct FilterBlock<'a> {
pub filter: Expr<'a>,
pub body: Vec<Stmt<'a>>,
}
/// Declares a macro.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "macros")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Macro<'a> {
pub name: &'a str,
pub args: Vec<Expr<'a>>,
pub defaults: Vec<Expr<'a>>,
pub body: Vec<Stmt<'a>>,
}
/// A call block
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "macros")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct CallBlock<'a> {
pub call: Spanned<Call<'a>>,
pub macro_decl: Spanned<Macro<'a>>,
}
/// Continue
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "loop_controls")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Continue;
/// Break
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "loop_controls")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Break;
/// A call block
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Do<'a> {
pub call: Spanned<Call<'a>>,
}
/// A "from" import
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct FromImport<'a> {
pub expr: Expr<'a>,
pub names: Vec<(Expr<'a>, Option<Expr<'a>>)>,
}
/// A full module import
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Import<'a> {
pub expr: Expr<'a>,
pub name: Expr<'a>,
}
/// Outputs the expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct EmitExpr<'a> {
pub expr: Expr<'a>,
}
/// Outputs raw template code.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct EmitRaw<'a> {
pub raw: &'a str,
}
/// Looks up a variable.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Var<'a> {
pub id: &'a str,
}
/// Loads a constant
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Const {
pub value: Value,
}
/// Represents a slice.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Slice<'a> {
pub expr: Expr<'a>,
pub start: Option<Expr<'a>>,
pub stop: Option<Expr<'a>>,
pub step: Option<Expr<'a>>,
}
/// A kind of unary operator.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum UnaryOpKind {
Not,
Neg,
}
/// An unary operator expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct UnaryOp<'a> {
pub op: UnaryOpKind,
pub expr: Expr<'a>,
}
/// A kind of binary operator.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum BinOpKind {
Eq,
Ne,
Lt,
Lte,
Gt,
Gte,
ScAnd,
ScOr,
Add,
Sub,
Mul,
Div,
FloorDiv,
Rem,
Pow,
Concat,
In,
}
/// A binary operator expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct BinOp<'a> {
pub op: BinOpKind,
pub left: Expr<'a>,
pub right: Expr<'a>,
}
/// An if expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct IfExpr<'a> {
pub test_expr: Expr<'a>,
pub true_expr: Expr<'a>,
pub false_expr: Option<Expr<'a>>,
}
/// A filter expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Filter<'a> {
pub name: &'a str,
pub expr: Option<Expr<'a>>,
pub args: Vec<CallArg<'a>>,
}
/// A test expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Test<'a> {
pub name: &'a str,
pub expr: Expr<'a>,
pub args: Vec<CallArg<'a>>,
}
/// An attribute lookup expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct GetAttr<'a> {
pub expr: Expr<'a>,
pub name: &'a str,
}
/// An item lookup expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct GetItem<'a> {
pub expr: Expr<'a>,
pub subscript_expr: Expr<'a>,
}
/// Calls something.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Call<'a> {
pub expr: Expr<'a>,
pub args: Vec<CallArg<'a>>,
}
/// A call argument helper
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum CallArg<'a> {
Pos(Expr<'a>),
Kwarg(&'a str, Expr<'a>),
PosSplat(Expr<'a>),
KwargSplat(Expr<'a>),
}
/// Creates a list of values.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct List<'a> {
pub items: Vec<Expr<'a>>,
}
impl List<'_> {
pub fn as_const(&self) -> Option<Value> {
if !self.items.iter().all(|x| matches!(x, Expr::Const(_))) {
return None;
}
let items = self.items.iter();
let sequence = items.filter_map(|expr| match expr {
Expr::Const(v) => Some(v.value.clone()),
_ => None,
});
Some(Value::from(sequence.collect::<Vec<_>>()))
}
}
/// Creates a map of values.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Map<'a> {
pub keys: Vec<Expr<'a>>,
pub values: Vec<Expr<'a>>,
}
impl Map<'_> {
pub fn as_const(&self) -> Option<Value> {
if !self.keys.iter().all(|x| matches!(x, Expr::Const(_)))
|| !self.values.iter().all(|x| matches!(x, Expr::Const(_)))
{
return None;
}
let mut rv = value_map_with_capacity(self.keys.len());
for (key, value) in self.keys.iter().zip(self.values.iter()) {
if let (Expr::Const(maybe_key), Expr::Const(value)) = (key, value) {
rv.insert(maybe_key.value.clone(), value.value.clone());
}
}
Some(Value::from_object(rv))
}
}
/// Defines the specific type of call.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum CallType<'ast, 'source> {
Function(&'source str),
Method(&'ast Expr<'source>, &'source str),
#[cfg(feature = "multi_template")]
Block(&'source str),
Object(&'ast Expr<'source>),
}
impl<'a> Call<'a> {
/// Try to isolate a method call.
///
/// name + call and attribute lookup + call are really method
/// calls which are easier to handle for the compiler as a separate
/// thing.
pub fn identify_call(&self) -> CallType<'_, 'a> {
match self.expr {
Expr::Var(ref var) => CallType::Function(var.id),
Expr::GetAttr(ref attr) => {
#[cfg(feature = "multi_template")]
{
if let Expr::Var(ref var) = attr.expr {
if var.id == "self" {
return CallType::Block(attr.name);
}
}
}
CallType::Method(&attr.expr, attr.name)
}
_ => CallType::Object(&self.expr),
}
}
}
+982
View File
@@ -0,0 +1,982 @@
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::mem;
use crate::compiler::ast;
use crate::compiler::instructions::{
Instruction, Instructions, LocalId, LOOP_FLAG_RECURSIVE, LOOP_FLAG_WITH_LOOP_VAR, MAX_LOCALS,
};
use crate::compiler::tokens::Span;
use crate::output::CaptureMode;
use crate::value::ops::neg;
use crate::value::{Kwargs, UndefinedType, Value, ValueMap, ValueRepr};
#[cfg(test)]
use similar_asserts::assert_eq;
#[cfg(feature = "macros")]
type Caller<'source> = ast::Spanned<ast::Macro<'source>>;
#[cfg(not(feature = "macros"))]
type Caller<'source> = std::marker::PhantomData<&'source ()>;
/// For the first `MAX_LOCALS` filters/tests, an ID is returned for faster lookups from the stack.
fn get_local_id<'source>(ids: &mut BTreeMap<&'source str, LocalId>, name: &'source str) -> LocalId {
if let Some(id) = ids.get(name) {
*id
} else if ids.len() >= MAX_LOCALS {
!0
} else {
let next_id = ids.len() as LocalId;
ids.insert(name, next_id);
next_id
}
}
/// Represents an open block of code that does not yet have updated
/// jump targets.
enum PendingBlock {
Branch {
jump_instr: u32,
},
Loop {
iter_instr: u32,
jump_instrs: Vec<u32>,
},
ScBool {
jump_instrs: Vec<u32>,
},
}
const CODEGEN_POOL_MAX_ITEMS: usize = 64;
const CODEGEN_POOLED_MAX_CAPACITY: usize = 64;
thread_local! {
static PENDING_BLOCK_POOL: RefCell<Vec<Vec<PendingBlock>>> = const { RefCell::new(Vec::new()) };
static SPAN_STACK_POOL: RefCell<Vec<Vec<Span>>> = const { RefCell::new(Vec::new()) };
}
#[inline(always)]
fn take_pending_block_buffer() -> Vec<PendingBlock> {
let mut buf = PENDING_BLOCK_POOL
.with(|pool| pool.borrow_mut().pop())
.unwrap_or_else(|| Vec::with_capacity(8));
buf.clear();
buf
}
#[inline(always)]
fn take_span_stack_buffer() -> Vec<Span> {
let mut buf = SPAN_STACK_POOL
.with(|pool| pool.borrow_mut().pop())
.unwrap_or_else(|| Vec::with_capacity(8));
buf.clear();
buf
}
#[inline(always)]
fn recycle_pending_block_buffer(mut buf: Vec<PendingBlock>) {
if buf.capacity() <= CODEGEN_POOLED_MAX_CAPACITY {
buf.clear();
PENDING_BLOCK_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < CODEGEN_POOL_MAX_ITEMS {
pool.push(buf);
}
});
}
}
#[inline(always)]
fn recycle_span_stack_buffer(mut buf: Vec<Span>) {
if buf.capacity() <= CODEGEN_POOLED_MAX_CAPACITY {
buf.clear();
SPAN_STACK_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < CODEGEN_POOL_MAX_ITEMS {
pool.push(buf);
}
});
}
}
/// Provides a convenient interface to creating instructions for the VM.
pub struct CodeGenerator<'source> {
instructions: Instructions<'source>,
blocks: BTreeMap<&'source str, Instructions<'source>>,
pending_block: Vec<PendingBlock>,
current_line: u16,
span_stack: Vec<Span>,
filter_local_ids: BTreeMap<&'source str, LocalId>,
test_local_ids: BTreeMap<&'source str, LocalId>,
raw_template_bytes: usize,
}
impl<'source> CodeGenerator<'source> {
/// Creates a new code generator.
pub fn new(file: &'source str, source: &'source str) -> CodeGenerator<'source> {
CodeGenerator {
instructions: Instructions::new(file, source),
blocks: BTreeMap::new(),
pending_block: take_pending_block_buffer(),
current_line: 0,
span_stack: take_span_stack_buffer(),
filter_local_ids: BTreeMap::new(),
test_local_ids: BTreeMap::new(),
raw_template_bytes: 0,
}
}
/// Sets the current location's line.
pub fn set_line(&mut self, lineno: u16) {
self.current_line = lineno;
}
/// Sets line from span.
pub fn set_line_from_span(&mut self, span: Span) {
self.set_line(span.start_line);
}
/// Pushes a span to the stack
pub fn push_span(&mut self, span: Span) {
self.span_stack.push(span);
self.set_line_from_span(span);
}
/// Pops a span from the stack.
pub fn pop_span(&mut self) {
self.span_stack.pop();
}
/// Add a simple instruction with the current location.
pub fn add(&mut self, instr: Instruction<'source>) -> u32 {
if let Some(span) = self.span_stack.last() {
if span.start_line == self.current_line {
return self.instructions.add_with_span(instr, *span);
}
}
self.instructions.add_with_line(instr, self.current_line)
}
/// Add a simple instruction with other location.
pub fn add_with_span(&mut self, instr: Instruction<'source>, span: Span) -> u32 {
self.instructions.add_with_span(instr, span)
}
/// Returns the next instruction index.
pub fn next_instruction(&self) -> u32 {
self.instructions.len() as u32
}
/// Creates a sub generator.
#[cfg(feature = "multi_template")]
fn new_subgenerator(&self) -> CodeGenerator<'source> {
let mut sub = CodeGenerator::new(self.instructions.name(), self.instructions.source());
sub.current_line = self.current_line;
sub.span_stack = self.span_stack.last().copied().into_iter().collect();
sub
}
/// Finishes a sub generator and syncs it back.
#[cfg(feature = "multi_template")]
fn finish_subgenerator(&mut self, sub: CodeGenerator<'source>) -> Instructions<'source> {
self.current_line = sub.current_line;
let (instructions, blocks) = sub.finish();
self.blocks.extend(blocks);
instructions
}
/// Starts a for loop
pub fn start_for_loop(&mut self, with_loop_var: bool, recursive: bool) {
let mut flags = 0;
if with_loop_var {
flags |= LOOP_FLAG_WITH_LOOP_VAR;
}
if recursive {
flags |= LOOP_FLAG_RECURSIVE;
}
self.add(Instruction::PushLoop(flags));
let instr = self.add(Instruction::Iterate(!0));
self.pending_block.push(PendingBlock::Loop {
iter_instr: instr,
jump_instrs: Vec::new(),
});
}
/// Ends the open for loop
pub fn end_for_loop(&mut self, push_did_not_iterate: bool) {
if let Some(PendingBlock::Loop {
iter_instr,
jump_instrs,
}) = self.pending_block.pop()
{
self.add(Instruction::Jump(iter_instr));
let loop_end = self.next_instruction();
if push_did_not_iterate {
self.add(Instruction::PushDidNotIterate);
};
self.add(Instruction::PopLoopFrame);
for instr in jump_instrs.into_iter().chain(Some(iter_instr)) {
match self.instructions.get_mut(instr) {
Some(&mut Instruction::Iterate(ref mut jump_target))
| Some(&mut Instruction::Jump(ref mut jump_target)) => {
*jump_target = loop_end;
}
_ => unreachable!(),
}
}
} else {
unreachable!()
}
}
/// Begins an if conditional
pub fn start_if(&mut self) {
let jump_instr = self.add(Instruction::JumpIfFalse(!0));
self.pending_block.push(PendingBlock::Branch { jump_instr });
}
/// Begins an else conditional
pub fn start_else(&mut self) {
let jump_instr = self.add(Instruction::Jump(!0));
self.end_condition(jump_instr + 1);
self.pending_block.push(PendingBlock::Branch { jump_instr });
}
/// Closes the current if block.
pub fn end_if(&mut self) {
self.end_condition(self.next_instruction());
}
/// Starts a short-circuited bool block.
pub fn start_sc_bool(&mut self) {
self.pending_block.push(PendingBlock::ScBool {
jump_instrs: Vec::new(),
});
}
/// Emits a short-circuited bool operator.
pub fn sc_bool(&mut self, and: bool) {
if let Some(&mut PendingBlock::ScBool {
ref mut jump_instrs,
}) = self.pending_block.last_mut()
{
jump_instrs.push(self.instructions.add(if and {
Instruction::JumpIfFalseOrPop(!0)
} else {
Instruction::JumpIfTrueOrPop(!0)
}));
} else {
unreachable!();
}
}
/// Ends a short-circuited bool block.
pub fn end_sc_bool(&mut self) {
let end = self.next_instruction();
if let Some(PendingBlock::ScBool { jump_instrs }) = self.pending_block.pop() {
for instr in jump_instrs {
match self.instructions.get_mut(instr) {
Some(&mut Instruction::JumpIfFalseOrPop(ref mut target))
| Some(&mut Instruction::JumpIfTrueOrPop(ref mut target)) => {
*target = end;
}
_ => unreachable!(),
}
}
}
}
fn end_condition(&mut self, new_jump_instr: u32) {
match self.pending_block.pop() {
Some(PendingBlock::Branch { jump_instr }) => {
match self.instructions.get_mut(jump_instr) {
Some(&mut Instruction::JumpIfFalse(ref mut target))
| Some(&mut Instruction::Jump(ref mut target)) => {
*target = new_jump_instr;
}
_ => {}
}
}
_ => unreachable!(),
}
}
/// Compiles a statement.
pub fn compile_stmt(&mut self, stmt: &ast::Stmt<'source>) {
match stmt {
ast::Stmt::Template(t) => {
self.set_line_from_span(t.span());
for node in &t.children {
self.compile_stmt(node);
}
}
ast::Stmt::EmitExpr(expr) => {
self.compile_emit_expr(expr);
}
ast::Stmt::EmitRaw(raw) => {
self.set_line_from_span(raw.span());
self.add(Instruction::EmitRaw(raw.raw));
self.raw_template_bytes += raw.raw.len();
}
ast::Stmt::ForLoop(for_loop) => {
self.compile_for_loop(for_loop);
}
ast::Stmt::IfCond(if_cond) => {
self.compile_if_stmt(if_cond);
}
ast::Stmt::WithBlock(with_block) => {
self.set_line_from_span(with_block.span());
self.add(Instruction::PushWith);
for (target, expr) in &with_block.assignments {
self.compile_expr(expr);
self.compile_assignment(target);
}
for node in &with_block.body {
self.compile_stmt(node);
}
self.add(Instruction::PopFrame);
}
ast::Stmt::Set(set) => {
self.set_line_from_span(set.span());
self.compile_expr(&set.expr);
self.compile_assignment(&set.target);
}
ast::Stmt::SetBlock(set_block) => {
self.set_line_from_span(set_block.span());
self.add(Instruction::BeginCapture(CaptureMode::Capture));
for node in &set_block.body {
self.compile_stmt(node);
}
self.add(Instruction::EndCapture);
if let Some(ref filter) = set_block.filter {
self.compile_expr(filter);
}
self.compile_assignment(&set_block.target);
}
ast::Stmt::AutoEscape(auto_escape) => {
self.set_line_from_span(auto_escape.span());
self.compile_expr(&auto_escape.enabled);
self.add(Instruction::PushAutoEscape);
for node in &auto_escape.body {
self.compile_stmt(node);
}
self.add(Instruction::PopAutoEscape);
}
ast::Stmt::FilterBlock(filter_block) => {
self.set_line_from_span(filter_block.span());
self.add(Instruction::BeginCapture(CaptureMode::Capture));
for node in &filter_block.body {
self.compile_stmt(node);
}
self.add(Instruction::EndCapture);
self.compile_expr(&filter_block.filter);
self.add(Instruction::Emit);
}
#[cfg(feature = "multi_template")]
ast::Stmt::Block(block) => {
self.compile_block(block);
}
#[cfg(feature = "multi_template")]
ast::Stmt::Import(import) => {
self.add(Instruction::BeginCapture(CaptureMode::Capture));
self.add(Instruction::PushWith);
self.compile_expr(&import.expr);
self.add_with_span(Instruction::Include(false), import.span());
self.add(Instruction::EndCapture);
self.add(Instruction::ExportLocals);
self.add(Instruction::PopFrame);
self.compile_assignment(&import.name);
}
#[cfg(feature = "multi_template")]
ast::Stmt::FromImport(from_import) => {
self.add(Instruction::BeginCapture(CaptureMode::Discard));
self.add(Instruction::PushWith);
self.compile_expr(&from_import.expr);
self.add_with_span(Instruction::Include(false), from_import.span());
for (name, _) in &from_import.names {
self.compile_expr(name);
}
self.add(Instruction::PopFrame);
for (name, alias) in from_import.names.iter().rev() {
self.compile_assignment(alias.as_ref().unwrap_or(name));
}
self.add(Instruction::EndCapture);
}
#[cfg(feature = "multi_template")]
ast::Stmt::Extends(extends) => {
self.set_line_from_span(extends.span());
self.compile_expr(&extends.name);
self.add_with_span(Instruction::LoadBlocks, extends.span());
}
#[cfg(feature = "multi_template")]
ast::Stmt::Include(include) => {
self.set_line_from_span(include.span());
self.compile_expr(&include.name);
self.add_with_span(Instruction::Include(include.ignore_missing), include.span());
}
#[cfg(feature = "macros")]
ast::Stmt::Macro(macro_decl) => {
self.compile_macro(macro_decl);
}
#[cfg(feature = "macros")]
ast::Stmt::CallBlock(call_block) => {
self.compile_call_block(call_block);
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Continue(cont) => {
self.set_line_from_span(cont.span());
for pending_block in self.pending_block.iter().rev() {
if let PendingBlock::Loop { iter_instr, .. } = pending_block {
self.add(Instruction::Jump(*iter_instr));
break;
}
}
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Break(brk) => {
self.set_line_from_span(brk.span());
let instr = self.add(Instruction::Jump(0));
for pending_block in self.pending_block.iter_mut().rev() {
if let &mut PendingBlock::Loop {
ref mut jump_instrs,
..
} = pending_block
{
jump_instrs.push(instr);
break;
}
}
}
ast::Stmt::Do(do_tag) => {
self.compile_do(do_tag);
}
}
}
#[cfg(feature = "multi_template")]
fn compile_block(&mut self, block: &ast::Spanned<ast::Block<'source>>) {
self.set_line_from_span(block.span());
let mut sub = self.new_subgenerator();
for node in &block.body {
sub.compile_stmt(node);
}
let instructions = self.finish_subgenerator(sub);
self.blocks.insert(block.name, instructions);
self.add(Instruction::CallBlock(block.name));
}
#[cfg(feature = "macros")]
fn compile_macro_expression(&mut self, macro_decl: &ast::Spanned<ast::Macro<'source>>) {
use crate::compiler::instructions::MACRO_CALLER;
self.set_line_from_span(macro_decl.span());
let instr = self.add(Instruction::Jump(!0));
let mut defaults_iter = macro_decl.defaults.iter().rev();
for arg in macro_decl.args.iter().rev() {
if let Some(default) = defaults_iter.next() {
self.add(Instruction::DupTop);
self.add(Instruction::IsUndefined);
self.start_if();
self.add(Instruction::DiscardTop);
self.compile_expr(default);
self.end_if();
}
self.compile_assignment(arg);
}
for node in &macro_decl.body {
self.compile_stmt(node);
}
self.add(Instruction::Return);
let mut undeclared = crate::compiler::meta::find_macro_closure(macro_decl);
let caller_reference = undeclared.remove("caller");
let macro_instr = self.next_instruction();
for name in &undeclared {
self.add(Instruction::Enclose(name));
}
self.add(Instruction::GetClosure);
self.add(Instruction::LoadConst(Value::from_object(
macro_decl
.args
.iter()
.map(|x| match x {
ast::Expr::Var(var) => Value::from(var.id),
_ => unreachable!(),
})
.collect::<Vec<Value>>(),
)));
let mut flags = 0;
if caller_reference {
flags |= MACRO_CALLER;
}
self.add(Instruction::BuildMacro(macro_decl.name, instr + 1, flags));
if let Some(&mut Instruction::Jump(ref mut target)) = self.instructions.get_mut(instr) {
*target = macro_instr;
} else {
unreachable!();
}
}
#[cfg(feature = "macros")]
fn compile_macro(&mut self, macro_decl: &ast::Spanned<ast::Macro<'source>>) {
self.compile_macro_expression(macro_decl);
self.add(Instruction::StoreLocal(macro_decl.name));
}
#[cfg(feature = "macros")]
fn compile_call_block(&mut self, call_block: &ast::Spanned<ast::CallBlock<'source>>) {
self.compile_call(&call_block.call, Some(&call_block.macro_decl));
self.add(Instruction::Emit);
}
fn compile_do(&mut self, do_tag: &ast::Spanned<ast::Do<'source>>) {
self.compile_call(&do_tag.call, None);
}
fn compile_if_stmt(&mut self, if_cond: &ast::Spanned<ast::IfCond<'source>>) {
self.set_line_from_span(if_cond.span());
self.push_span(if_cond.expr.span());
self.compile_expr(&if_cond.expr);
self.start_if();
self.pop_span();
for node in &if_cond.true_body {
self.compile_stmt(node);
}
if !if_cond.false_body.is_empty() {
self.start_else();
for node in &if_cond.false_body {
self.compile_stmt(node);
}
}
self.end_if();
}
fn compile_emit_expr(&mut self, expr: &ast::Spanned<ast::EmitExpr<'source>>) {
if let ast::Expr::Call(call) = &expr.expr {
self.set_line_from_span(expr.expr.span());
match call.identify_call() {
ast::CallType::Function(name) => {
if name == "super" && call.args.is_empty() {
self.add_with_span(Instruction::FastSuper, call.span());
return;
} else if name == "loop" && call.args.len() == 1 {
self.compile_call_args(std::slice::from_ref(&call.args[0]), 0, None);
self.add_with_span(Instruction::FastRecurse, call.span());
return;
}
}
#[cfg(feature = "multi_template")]
ast::CallType::Block(name) => {
self.add(Instruction::CallBlock(name));
return;
}
_ => {}
}
}
self.push_span(expr.expr.span());
self.compile_expr(&expr.expr);
self.add(Instruction::Emit);
self.pop_span();
}
fn compile_for_loop(&mut self, for_loop: &ast::Spanned<ast::ForLoop<'source>>) {
self.set_line_from_span(for_loop.span());
// filter expressions work like a nested for loop without
// the special loop variable. in one loop, the condition is checked and
// passing items accumulated into a list. in the second, that list is
// iterated over normally
if let Some(ref filter_expr) = for_loop.filter_expr {
self.add(Instruction::LoadConst(Value::from(0usize)));
self.push_span(filter_expr.span());
self.compile_expr(&for_loop.iter);
self.start_for_loop(false, false);
self.add(Instruction::DupTop);
self.compile_assignment(&for_loop.target);
self.compile_expr(filter_expr);
self.start_if();
self.add(Instruction::Swap);
self.add(Instruction::LoadConst(Value::from(1usize)));
self.add(Instruction::Add);
self.start_else();
self.add(Instruction::DiscardTop);
self.end_if();
self.pop_span();
self.end_for_loop(false);
self.add(Instruction::BuildList(None));
self.start_for_loop(true, for_loop.recursive);
} else {
self.push_span(for_loop.iter.span());
self.compile_expr(&for_loop.iter);
self.start_for_loop(true, for_loop.recursive);
self.pop_span();
}
self.compile_assignment(&for_loop.target);
for node in &for_loop.body {
self.compile_stmt(node);
}
self.end_for_loop(!for_loop.else_body.is_empty());
if !for_loop.else_body.is_empty() {
self.start_if();
for node in &for_loop.else_body {
self.compile_stmt(node);
}
self.end_if();
};
}
/// Compiles an assignment expression.
pub fn compile_assignment(&mut self, expr: &ast::Expr<'source>) {
match expr {
ast::Expr::Var(var) => {
self.add(Instruction::StoreLocal(var.id));
}
ast::Expr::List(list) => {
self.push_span(list.span());
self.add(Instruction::UnpackList(list.items.len()));
for expr in &list.items {
self.compile_assignment(expr);
}
self.pop_span();
}
ast::Expr::GetAttr(attr) => {
self.push_span(attr.span());
self.compile_expr(&attr.expr);
self.add(Instruction::SetAttr(attr.name));
}
_ => unreachable!(),
}
}
/// Compiles an expression.
pub fn compile_expr(&mut self, expr: &ast::Expr<'source>) {
// try to do constant folding
if let Some(v) = expr.as_const() {
self.set_line_from_span(expr.span());
self.add(Instruction::LoadConst(v.clone()));
return;
}
match expr {
ast::Expr::Var(v) => {
self.set_line_from_span(v.span());
self.add(Instruction::Lookup(v.id));
}
ast::Expr::Const(_) => unreachable!(), // handled by constant folding
ast::Expr::Slice(s) => {
self.push_span(s.span());
self.compile_expr(&s.expr);
if let Some(ref start) = s.start {
self.compile_expr(start);
} else {
self.add(Instruction::LoadConst(Value::from(())));
}
if let Some(ref stop) = s.stop {
self.compile_expr(stop);
} else {
self.add(Instruction::LoadConst(Value::from(())));
}
if let Some(ref step) = s.step {
self.compile_expr(step);
} else {
self.add(Instruction::LoadConst(Value::from(())));
}
self.add(Instruction::Slice);
self.pop_span();
}
ast::Expr::UnaryOp(c) => {
self.set_line_from_span(c.span());
match c.op {
ast::UnaryOpKind::Not => {
self.compile_expr(&c.expr);
self.add(Instruction::Not);
}
ast::UnaryOpKind::Neg => {
// common case: negative numbers. In that case we
// directly negate them if this is possible without
// an error.
if let ast::Expr::Const(ref c) = c.expr {
if let Ok(negated) = neg(&c.value) {
self.add(Instruction::LoadConst(negated));
return;
}
}
self.compile_expr(&c.expr);
self.add_with_span(Instruction::Neg, c.span());
}
}
}
ast::Expr::BinOp(c) => {
self.compile_bin_op(c);
}
ast::Expr::IfExpr(i) => {
self.set_line_from_span(i.span());
self.compile_expr(&i.test_expr);
self.start_if();
self.compile_expr(&i.true_expr);
self.start_else();
if let Some(ref false_expr) = i.false_expr {
self.compile_expr(false_expr);
} else {
// special behavior: missing false block have a silent undefined
// to permit special casing. This is for compatibility also with
// what Jinja2 does.
self.add(Instruction::LoadConst(
ValueRepr::Undefined(UndefinedType::Silent).into(),
));
}
self.end_if();
}
ast::Expr::Filter(f) => {
self.push_span(f.span());
if let Some(ref expr) = f.expr {
self.compile_expr(expr);
}
let arg_count = self.compile_call_args(&f.args, 1, None);
let local_id = get_local_id(&mut self.filter_local_ids, f.name);
self.add(Instruction::ApplyFilter(f.name, arg_count, local_id));
self.pop_span();
}
ast::Expr::Test(f) => {
self.push_span(f.span());
self.compile_expr(&f.expr);
let arg_count = self.compile_call_args(&f.args, 1, None);
let local_id = get_local_id(&mut self.test_local_ids, f.name);
self.add(Instruction::PerformTest(f.name, arg_count, local_id));
self.pop_span();
}
ast::Expr::GetAttr(g) => {
self.push_span(g.span());
self.compile_expr(&g.expr);
self.add(Instruction::GetAttr(g.name));
self.pop_span();
}
ast::Expr::GetItem(g) => {
self.push_span(g.span());
self.compile_expr(&g.expr);
self.compile_expr(&g.subscript_expr);
self.add(Instruction::GetItem);
self.pop_span();
}
ast::Expr::Call(c) => {
self.compile_call(c, None);
}
ast::Expr::List(l) => {
self.set_line_from_span(l.span());
for item in &l.items {
self.compile_expr(item);
}
self.add(Instruction::BuildList(Some(l.items.len())));
}
ast::Expr::Map(m) => {
self.set_line_from_span(m.span());
assert_eq!(m.keys.len(), m.values.len());
for (key, value) in m.keys.iter().zip(m.values.iter()) {
self.compile_expr(key);
self.compile_expr(value);
}
self.add(Instruction::BuildMap(m.keys.len()));
}
}
}
fn compile_call(
&mut self,
c: &ast::Spanned<ast::Call<'source>>,
caller: Option<&Caller<'source>>,
) {
self.push_span(c.span());
match c.identify_call() {
ast::CallType::Function(name) => {
let arg_count = self.compile_call_args(&c.args, 0, caller);
self.add(Instruction::CallFunction(name, arg_count));
}
#[cfg(feature = "multi_template")]
ast::CallType::Block(name) => {
self.add(Instruction::BeginCapture(CaptureMode::Capture));
self.add(Instruction::CallBlock(name));
self.add(Instruction::EndCapture);
}
ast::CallType::Method(expr, name) => {
self.compile_expr(expr);
let arg_count = self.compile_call_args(&c.args, 1, caller);
self.add(Instruction::CallMethod(name, arg_count));
}
ast::CallType::Object(expr) => {
self.compile_expr(expr);
let arg_count = self.compile_call_args(&c.args, 1, caller);
self.add(Instruction::CallObject(arg_count));
}
};
self.pop_span();
}
fn compile_call_args(
&mut self,
args: &[ast::CallArg<'source>],
extra_args: usize,
caller: Option<&Caller<'source>>,
) -> Option<u16> {
let mut pending_args = extra_args;
let mut num_args_batches = 0;
let mut has_kwargs = caller.is_some();
let mut static_kwargs = caller.is_none();
for arg in args {
match arg {
ast::CallArg::Pos(expr) => {
self.compile_expr(expr);
pending_args += 1;
}
ast::CallArg::PosSplat(expr) => {
if pending_args > 0 {
self.add(Instruction::BuildList(Some(pending_args)));
pending_args = 0;
num_args_batches += 1;
}
self.compile_expr(expr);
num_args_batches += 1;
}
ast::CallArg::Kwarg(_, expr) => {
if !matches!(expr, ast::Expr::Const(_)) {
static_kwargs = false;
}
has_kwargs = true;
}
ast::CallArg::KwargSplat(_) => {
static_kwargs = false;
has_kwargs = true;
}
}
}
if has_kwargs {
let mut pending_kwargs = 0;
let mut num_kwargs_batches = 0;
let mut collected_kwargs = ValueMap::new();
for arg in args {
match arg {
ast::CallArg::Kwarg(key, value) => {
if static_kwargs {
if let ast::Expr::Const(c) = value {
collected_kwargs.insert(Value::from(*key), c.value.clone());
} else {
unreachable!();
}
} else {
self.add(Instruction::LoadConst(Value::from(*key)));
self.compile_expr(value);
pending_kwargs += 1;
}
}
ast::CallArg::KwargSplat(expr) => {
if pending_kwargs > 0 {
self.add(Instruction::BuildKwargs(pending_kwargs));
num_kwargs_batches += 1;
pending_kwargs = 0;
}
self.compile_expr(expr);
num_kwargs_batches += 1;
}
ast::CallArg::Pos(_) | ast::CallArg::PosSplat(_) => {}
}
}
if !collected_kwargs.is_empty() {
self.add(Instruction::LoadConst(Kwargs::wrap(collected_kwargs)));
} else {
// The conditions above guarantee that if we collect static kwargs
// we cannot enter this block (single kwargs batch, no caller).
#[cfg(feature = "macros")]
{
if let Some(caller) = caller {
self.add(Instruction::LoadConst(Value::from("caller")));
self.compile_macro_expression(caller);
pending_kwargs += 1
}
}
if num_kwargs_batches > 0 {
if pending_kwargs > 0 {
self.add(Instruction::BuildKwargs(pending_kwargs));
num_kwargs_batches += 1;
}
self.add(Instruction::MergeKwargs(num_kwargs_batches));
} else {
self.add(Instruction::BuildKwargs(pending_kwargs));
}
}
pending_args += 1;
}
if num_args_batches > 0 {
if pending_args > 0 {
self.add(Instruction::BuildList(Some(pending_args)));
num_args_batches += 1;
}
self.add(Instruction::UnpackLists(num_args_batches));
None
} else {
assert!(pending_args as u16 as usize == pending_args);
Some(pending_args as u16)
}
}
fn compile_bin_op(&mut self, c: &ast::Spanned<ast::BinOp<'source>>) {
self.push_span(c.span());
let instr = match c.op {
ast::BinOpKind::Eq => Instruction::Eq,
ast::BinOpKind::Ne => Instruction::Ne,
ast::BinOpKind::Lt => Instruction::Lt,
ast::BinOpKind::Lte => Instruction::Lte,
ast::BinOpKind::Gt => Instruction::Gt,
ast::BinOpKind::Gte => Instruction::Gte,
ast::BinOpKind::ScAnd | ast::BinOpKind::ScOr => {
self.start_sc_bool();
self.compile_expr(&c.left);
self.sc_bool(matches!(c.op, ast::BinOpKind::ScAnd));
self.compile_expr(&c.right);
self.end_sc_bool();
self.pop_span();
return;
}
ast::BinOpKind::Add => Instruction::Add,
ast::BinOpKind::Sub => Instruction::Sub,
ast::BinOpKind::Mul => Instruction::Mul,
ast::BinOpKind::Div => Instruction::Div,
ast::BinOpKind::FloorDiv => Instruction::IntDiv,
ast::BinOpKind::Rem => Instruction::Rem,
ast::BinOpKind::Pow => Instruction::Pow,
ast::BinOpKind::Concat => Instruction::StringConcat,
ast::BinOpKind::In => Instruction::In,
};
self.compile_expr(&c.left);
self.compile_expr(&c.right);
self.add(instr);
self.pop_span();
}
/// Returns the size hint for buffers.
///
/// This is a proposal for the initial buffer size when rendering directly to a string.
pub fn buffer_size_hint(&self) -> usize {
// for now the assumption is made that twice the bytes of template code without
// control structures, rounded up to the next power of two is a good default. The
// round to the next power of two is chosen because the underlying vector backing
// strings prefers powers of two.
(self.raw_template_bytes * 2).next_power_of_two()
}
/// Converts the compiler into the instructions.
pub fn finish(
mut self,
) -> (
Instructions<'source>,
BTreeMap<&'source str, Instructions<'source>>,
) {
assert!(self.pending_block.is_empty());
recycle_pending_block_buffer(mem::take(&mut self.pending_block));
recycle_span_stack_buffer(mem::take(&mut self.span_stack));
(self.instructions, self.blocks)
}
}
+481
View File
@@ -0,0 +1,481 @@
#[cfg(feature = "internal_debug")]
use std::fmt;
use crate::compiler::tokens::Span;
use crate::output::CaptureMode;
use crate::value::Value;
/// This loop has the loop var.
pub const LOOP_FLAG_WITH_LOOP_VAR: u8 = 1;
/// This loop is recursive.
pub const LOOP_FLAG_RECURSIVE: u8 = 2;
/// This macro uses the caller var.
#[cfg(feature = "macros")]
pub const MACRO_CALLER: u8 = 2;
/// Rust type to represent locals.
pub type LocalId = u8;
/// The maximum number of filters/tests that can be cached.
pub const MAX_LOCALS: usize = 50;
/// Represents an instruction for the VM.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(
feature = "unstable_machinery_serde",
derive(serde::Serialize),
serde(tag = "op", content = "arg")
)]
#[derive(Clone)]
pub enum Instruction<'source> {
/// Emits raw source
EmitRaw(&'source str),
/// Stores a variable (only possible in for loops)
StoreLocal(&'source str),
/// Load a variable,
Lookup(&'source str),
/// Looks up an attribute.
GetAttr(&'source str),
/// Sets an attribute.
SetAttr(&'source str),
/// Looks up an item.
GetItem,
/// Performs a slice operation.
Slice,
/// Loads a constant value.
LoadConst(Value),
/// Builds a map of the last n pairs on the stack.
BuildMap(usize),
/// Builds a kwargs map of the last n pairs on the stack.
BuildKwargs(usize),
/// Merges N kwargs maps on the list into one.
MergeKwargs(usize),
/// Builds a list of the last n pairs on the stack.
BuildList(Option<usize>),
/// Unpacks a list into N stack items.
UnpackList(usize),
/// Unpacks N lists onto the stack and pushes the number of items there were unpacked.
UnpackLists(usize),
/// Add the top two values
Add,
/// Subtract the top two values
Sub,
/// Multiply the top two values
Mul,
/// Divide the top two values
Div,
/// Integer divide the top two values as "integer".
///
/// Note that in MiniJinja this currently uses an euclidean
/// division to match the rem implementation. In Python this
/// instead uses a flooring division and a flooring remainder.
IntDiv,
/// Calculate the remainder the top two values
Rem,
/// x to the power of y.
Pow,
/// Negates the value.
Neg,
/// `=` operator
Eq,
/// `!=` operator
Ne,
/// `>` operator
Gt,
/// `>=` operator
Gte,
/// `<` operator
Lt,
/// `<=` operator
Lte,
/// Unary not
Not,
/// String concatenation operator
StringConcat,
/// Performs a containment check
In,
/// Apply a filter.
ApplyFilter(&'source str, Option<u16>, LocalId),
/// Perform a filter.
PerformTest(&'source str, Option<u16>, LocalId),
/// Emit the stack top as output
Emit,
/// Starts a loop
///
/// The argument are loop flags.
PushLoop(u8),
/// Starts a with block.
PushWith,
/// Does a single loop iteration
///
/// The argument is the jump target for when the loop
/// ends and must point to a `PopFrame` instruction.
Iterate(u32),
/// Push a bool that indicates that the loop iterated.
PushDidNotIterate,
/// Pops the topmost frame
PopFrame,
/// Pops the topmost frame and runs loop logic
PopLoopFrame,
/// Jump to a specific instruction
Jump(u32),
/// Jump if the stack top evaluates to false
JumpIfFalse(u32),
/// Jump if the stack top evaluates to false or pops the value
JumpIfFalseOrPop(u32),
/// Jump if the stack top evaluates to true or pops the value
JumpIfTrueOrPop(u32),
/// Sets the auto escape flag to the current value.
PushAutoEscape,
/// Resets the auto escape flag to the previous value.
PopAutoEscape,
/// Begins capturing of output (false) or discard (true).
BeginCapture(CaptureMode),
/// Ends capturing of output.
EndCapture,
/// Calls a global function
CallFunction(&'source str, Option<u16>),
/// Calls a method
CallMethod(&'source str, Option<u16>),
/// Calls an object
CallObject(Option<u16>),
/// Duplicates the top item
DupTop,
/// Discards the top item
DiscardTop,
/// A fast super instruction without intermediate capturing.
FastSuper,
/// A fast loop recurse instruction without intermediate capturing.
FastRecurse,
/// Swaps the top two items in the stack.
Swap,
/// Call into a block.
#[cfg(feature = "multi_template")]
CallBlock(&'source str),
/// Loads block from a template with name on stack ("extends")
#[cfg(feature = "multi_template")]
LoadBlocks,
/// Includes another template.
#[cfg(feature = "multi_template")]
Include(bool),
/// Builds a module
#[cfg(feature = "multi_template")]
ExportLocals,
/// Builds a macro on the stack.
#[cfg(feature = "macros")]
BuildMacro(&'source str, u32, u8),
/// Breaks from the interpreter loop (exists a function)
#[cfg(feature = "macros")]
Return,
/// True if the value is undefined
#[cfg(feature = "macros")]
IsUndefined,
/// Encloses a variable.
#[cfg(feature = "macros")]
Enclose(&'source str),
/// Returns the closure of this context level.
#[cfg(feature = "macros")]
GetClosure,
}
#[derive(Copy, Clone)]
struct LineInfo {
first_instruction: u32,
line: u16,
}
#[cfg(feature = "debug")]
#[derive(Copy, Clone)]
struct SpanInfo {
first_instruction: u32,
span: Span,
}
/// Wrapper around instructions to help with location management.
pub struct Instructions<'source> {
pub(crate) instructions: Vec<Instruction<'source>>,
line_infos: Vec<LineInfo>,
#[cfg(feature = "debug")]
span_infos: Vec<SpanInfo>,
name: &'source str,
source: &'source str,
}
pub(crate) static EMPTY_INSTRUCTIONS: Instructions<'static> = Instructions {
instructions: Vec::new(),
line_infos: Vec::new(),
#[cfg(feature = "debug")]
span_infos: Vec::new(),
name: "<unknown>",
source: "",
};
impl<'source> Instructions<'source> {
/// Creates a new instructions object.
pub fn new(name: &'source str, source: &'source str) -> Instructions<'source> {
Instructions {
instructions: Vec::with_capacity(256),
line_infos: Vec::with_capacity(128),
#[cfg(feature = "debug")]
span_infos: Vec::with_capacity(128),
name,
source,
}
}
/// Returns the name of the template.
pub fn name(&self) -> &'source str {
self.name
}
/// Returns the source reference.
pub fn source(&self) -> &'source str {
self.source
}
/// Returns an instruction by index
#[inline(always)]
pub fn get(&self, idx: u32) -> Option<&Instruction<'source>> {
self.instructions.get(idx as usize)
}
/// Returns an instruction by index mutably
pub fn get_mut(&mut self, idx: u32) -> Option<&mut Instruction<'source>> {
self.instructions.get_mut(idx as usize)
}
/// Adds a new instruction
pub fn add(&mut self, instr: Instruction<'source>) -> u32 {
let rv = self.instructions.len();
self.instructions.push(instr);
rv as u32
}
fn add_line_record(&mut self, instr: u32, line: u16) {
let same_loc = self
.line_infos
.last()
.is_some_and(|last_loc| last_loc.line == line);
if !same_loc {
self.line_infos.push(LineInfo {
first_instruction: instr,
line,
});
}
}
/// Adds a new instruction with line number.
pub fn add_with_line(&mut self, instr: Instruction<'source>, line: u16) -> u32 {
let rv = self.add(instr);
self.add_line_record(rv, line);
// if we follow up to a valid span with no more span, clear it out
#[cfg(feature = "debug")]
{
if self
.span_infos
.last()
.is_some_and(|x| x.span != Span::default())
{
self.span_infos.push(SpanInfo {
first_instruction: rv,
span: Span::default(),
});
}
}
rv
}
/// Adds a new instruction with span.
pub fn add_with_span(&mut self, instr: Instruction<'source>, span: Span) -> u32 {
let rv = self.add(instr);
#[cfg(feature = "debug")]
{
let same_loc = self
.span_infos
.last()
.is_some_and(|last_loc| last_loc.span == span);
if !same_loc {
self.span_infos.push(SpanInfo {
first_instruction: rv,
span,
});
}
}
self.add_line_record(rv, span.start_line);
rv
}
/// Looks up the line for an instruction
pub fn get_line(&self, idx: u32) -> Option<usize> {
let loc = match self
.line_infos
.binary_search_by_key(&idx, |x| x.first_instruction)
{
Ok(idx) => &self.line_infos[idx],
Err(0) => return None,
Err(idx) => &self.line_infos[idx - 1],
};
Some(loc.line as usize)
}
/// Looks up a span for an instruction.
pub fn get_span(&self, idx: u32) -> Option<Span> {
#[cfg(feature = "debug")]
{
let loc = match self
.span_infos
.binary_search_by_key(&idx, |x| x.first_instruction)
{
Ok(idx) => &self.span_infos[idx],
Err(0) => return None,
Err(idx) => &self.span_infos[idx - 1],
};
(loc.span != Span::default()).then_some(loc.span)
}
#[cfg(not(feature = "debug"))]
{
let _ = idx;
None
}
}
/// Returns a list of all names referenced in the current block backwards
/// from the given pc.
#[cfg(feature = "debug")]
pub fn get_referenced_names(&self, idx: u32) -> Vec<&'source str> {
let mut rv = Vec::new();
// make sure we don't crash on empty instructions
if self.instructions.is_empty() {
return rv;
}
let idx = (idx as usize).min(self.instructions.len() - 1);
for instr in self.instructions[..=idx].iter().rev() {
let name = match instr {
Instruction::Lookup(name)
| Instruction::StoreLocal(name)
| Instruction::CallFunction(name, _) => *name,
Instruction::PushLoop(flags) if flags & LOOP_FLAG_WITH_LOOP_VAR != 0 => "loop",
Instruction::PushLoop(_) | Instruction::PushWith => break,
_ => continue,
};
if !rv.contains(&name) {
rv.push(name);
}
}
rv
}
/// Returns the number of instructions
pub fn len(&self) -> usize {
self.instructions.len()
}
/// Do we have any instructions?
#[allow(unused)]
pub fn is_empty(&self) -> bool {
self.instructions.is_empty()
}
}
#[cfg(feature = "internal_debug")]
impl fmt::Debug for Instructions<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct InstructionWrapper<'a>(usize, &'a Instruction<'a>, Option<usize>);
impl fmt::Debug for InstructionWrapper<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
ok!(write!(f, "{:>05} | {:?}", self.0, self.1,));
if let Some(line) = self.2 {
ok!(write!(f, " [line {line}]"));
}
Ok(())
}
}
let mut list = f.debug_list();
let mut last_line = None;
for (idx, instr) in self.instructions.iter().enumerate() {
let line = self.get_line(idx as u32);
list.entry(&InstructionWrapper(
idx,
instr,
if line != last_line { line } else { None },
));
last_line = line;
}
list.finish()
}
}
#[test]
#[cfg(target_pointer_width = "64")]
fn test_sizes() {
assert_eq!(std::mem::size_of::<Instruction>(), 32);
}
File diff suppressed because it is too large. Load diff
+307
View File
@@ -0,0 +1,307 @@
use std::collections::HashSet;
use std::fmt::Write;
use crate::compiler::ast;
struct AssignmentTracker<'a> {
out: HashSet<&'a str>,
nested_out: Option<HashSet<String>>,
assigned: Vec<HashSet<&'a str>>,
}
impl<'a> AssignmentTracker<'a> {
fn is_assigned(&self, name: &str) -> bool {
self.assigned.iter().any(|x| x.contains(name))
}
fn assign(&mut self, name: &'a str) {
self.assigned.last_mut().unwrap().insert(name);
}
fn assign_nested(&mut self, name: String) {
if let Some(ref mut nested_out) = self.nested_out {
if !nested_out.contains(&name) {
nested_out.insert(name);
}
}
}
fn push(&mut self) {
self.assigned.push(Default::default());
}
fn pop(&mut self) {
self.assigned.pop();
}
}
/// Finds all variables that need to be captured as closure for a macro.
#[cfg(feature = "macros")]
pub fn find_macro_closure<'a>(m: &ast::Macro<'a>) -> HashSet<&'a str> {
let mut state = AssignmentTracker {
out: HashSet::new(),
nested_out: None,
assigned: vec![Default::default()],
};
tracker_visit_macro(m, &mut state, false);
state.out
}
/// Finds all variables that are undeclared in a template.
pub fn find_undeclared(t: &ast::Stmt<'_>, track_nested: bool) -> HashSet<String> {
let mut state = AssignmentTracker {
out: HashSet::new(),
nested_out: if track_nested {
Some(HashSet::new())
} else {
None
},
assigned: vec![Default::default()],
};
track_walk(t, &mut state);
if let Some(nested) = state.nested_out {
nested
} else {
state.out.into_iter().map(|x| x.to_string()).collect()
}
}
fn tracker_visit_expr_opt<'a>(expr: &Option<ast::Expr<'a>>, state: &mut AssignmentTracker<'a>) {
if let Some(expr) = expr {
tracker_visit_expr(expr, state);
}
}
#[cfg(feature = "macros")]
fn tracker_visit_macro<'a>(
m: &ast::Macro<'a>,
state: &mut AssignmentTracker<'a>,
declare_caller: bool,
) {
if declare_caller {
// this is not completely correct as caller is actually only defined
// if the macro was used in the context of a call block. However it
// is impossible to determine this at compile time so we err on the
// side of assuming caller is there.
state.assign("caller");
}
m.args.iter().for_each(|arg| track_assign(arg, state));
m.defaults
.iter()
.for_each(|expr| tracker_visit_expr(expr, state));
m.body.iter().for_each(|node| track_walk(node, state));
}
fn tracker_visit_callarg<'a>(callarg: &ast::CallArg<'a>, state: &mut AssignmentTracker<'a>) {
match callarg {
ast::CallArg::Pos(expr)
| ast::CallArg::Kwarg(_, expr)
| ast::CallArg::PosSplat(expr)
| ast::CallArg::KwargSplat(expr) => tracker_visit_expr(expr, state),
}
}
fn tracker_visit_expr<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
match expr {
ast::Expr::Var(var) => {
if !state.is_assigned(var.id) {
state.out.insert(var.id);
// if we are not tracking nested assignments, we can consider a variable
// to be assigned the first time we perform a lookup.
if state.nested_out.is_none() {
state.assign(var.id);
} else {
state.assign_nested(var.id.to_string());
}
}
}
ast::Expr::Const(_) => {}
ast::Expr::UnaryOp(expr) => tracker_visit_expr(&expr.expr, state),
ast::Expr::BinOp(expr) => {
tracker_visit_expr(&expr.left, state);
tracker_visit_expr(&expr.right, state);
}
ast::Expr::IfExpr(expr) => {
tracker_visit_expr(&expr.test_expr, state);
tracker_visit_expr(&expr.true_expr, state);
tracker_visit_expr_opt(&expr.false_expr, state);
}
ast::Expr::Filter(expr) => {
tracker_visit_expr_opt(&expr.expr, state);
expr.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
}
ast::Expr::Test(expr) => {
tracker_visit_expr(&expr.expr, state);
expr.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
}
ast::Expr::GetAttr(expr) => {
// if we are tracking nested, we check if we have a chain of attribute
// lookups that terminate in a variable lookup. In that case we can
// assign the nested lookup.
if state.nested_out.is_some() {
let mut attrs = vec![expr.name];
let mut ptr = &expr.expr;
loop {
match ptr {
ast::Expr::Var(var) => {
if !state.is_assigned(var.id) {
let mut rv = var.id.to_string();
for attr in attrs.iter().rev() {
write!(rv, ".{attr}").ok();
}
state.assign_nested(rv);
return;
} else {
break;
}
}
ast::Expr::GetAttr(expr) => {
attrs.push(expr.name);
ptr = &expr.expr;
continue;
}
_ => break,
}
}
}
tracker_visit_expr(&expr.expr, state)
}
ast::Expr::GetItem(expr) => {
tracker_visit_expr(&expr.expr, state);
tracker_visit_expr(&expr.subscript_expr, state);
}
ast::Expr::Slice(slice) => {
tracker_visit_expr_opt(&slice.start, state);
tracker_visit_expr_opt(&slice.stop, state);
tracker_visit_expr_opt(&slice.step, state);
}
ast::Expr::Call(expr) => {
tracker_visit_expr(&expr.expr, state);
expr.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
}
ast::Expr::List(expr) => expr.items.iter().for_each(|x| tracker_visit_expr(x, state)),
ast::Expr::Map(expr) => expr.keys.iter().zip(expr.values.iter()).for_each(|(k, v)| {
tracker_visit_expr(k, state);
tracker_visit_expr(v, state);
}),
}
}
fn track_assign<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
match expr {
ast::Expr::Var(var) => state.assign(var.id),
ast::Expr::List(list) => list.items.iter().for_each(|x| track_assign(x, state)),
_ => {}
}
}
fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
match node {
ast::Stmt::Template(stmt) => {
state.assign("self");
stmt.children.iter().for_each(|x| track_walk(x, state));
}
ast::Stmt::EmitExpr(expr) => tracker_visit_expr(&expr.expr, state),
ast::Stmt::EmitRaw(_) => {}
ast::Stmt::ForLoop(stmt) => {
state.push();
state.assign("loop");
tracker_visit_expr(&stmt.iter, state);
track_assign(&stmt.target, state);
tracker_visit_expr_opt(&stmt.filter_expr, state);
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
state.push();
stmt.else_body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::IfCond(stmt) => {
tracker_visit_expr(&stmt.expr, state);
state.push();
stmt.true_body.iter().for_each(|x| track_walk(x, state));
state.pop();
state.push();
stmt.false_body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::WithBlock(stmt) => {
state.push();
for (target, expr) in &stmt.assignments {
track_assign(target, state);
tracker_visit_expr(expr, state);
}
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::Set(stmt) => {
track_assign(&stmt.target, state);
tracker_visit_expr(&stmt.expr, state);
}
ast::Stmt::AutoEscape(stmt) => {
state.push();
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::FilterBlock(stmt) => {
state.push();
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::SetBlock(stmt) => {
track_assign(&stmt.target, state);
state.push();
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
#[cfg(feature = "multi_template")]
ast::Stmt::Block(stmt) => {
state.push();
state.assign("super");
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
#[cfg(feature = "multi_template")]
ast::Stmt::Extends(_) | ast::Stmt::Include(_) => {}
#[cfg(feature = "multi_template")]
ast::Stmt::Import(stmt) => {
track_assign(&stmt.name, state);
}
#[cfg(feature = "multi_template")]
ast::Stmt::FromImport(stmt) => stmt.names.iter().for_each(|(arg, alias)| {
track_assign(alias.as_ref().unwrap_or(arg), state);
}),
#[cfg(feature = "macros")]
ast::Stmt::Macro(stmt) => {
state.assign(stmt.name);
state.push();
tracker_visit_macro(stmt, state, true);
state.pop();
}
#[cfg(feature = "macros")]
ast::Stmt::CallBlock(stmt) => {
tracker_visit_expr(&stmt.call.expr, state);
stmt.call
.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
state.push();
tracker_visit_macro(&stmt.macro_decl, state, true);
state.pop();
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Continue(_) | ast::Stmt::Break(_) => {}
ast::Stmt::Do(stmt) => {
tracker_visit_expr(&stmt.call.expr, state);
stmt.call
.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
}
}
}
+9
View File
@@ -0,0 +1,9 @@
#![allow(missing_docs)]
/// This module contains the internals of the compiler.
pub mod ast;
pub mod codegen;
pub mod instructions;
pub mod lexer;
pub mod meta;
pub mod parser;
pub mod tokens;
File diff suppressed because it is too large. Load diff
+157
View File
@@ -0,0 +1,157 @@
use std::fmt;
/// Represents a token in the stream.
#[derive(Debug)]
#[cfg_attr(
feature = "unstable_machinery_serde",
derive(serde::Serialize),
serde(tag = "name", content = "payload")
)]
pub enum Token<'a> {
/// Raw template data.
TemplateData(&'a str),
/// Variable block start.
VariableStart,
/// Variable block end
VariableEnd,
/// Statement block start
BlockStart,
/// Statement block end
BlockEnd,
/// An identifier.
Ident(&'a str),
/// A borrowed string.
Str(&'a str),
/// An allocated string.
String(Box<str>),
/// An integer (limited to i64)
Int(u64),
/// A large integer
Int128(Box<u128>),
/// A float
Float(f64),
/// A plus (`+`) operator.
Plus,
/// A plus (`-`) operator.
Minus,
/// A mul (`*`) operator.
Mul,
/// A div (`/`) operator.
Div,
/// A floor division (`//`) operator.
FloorDiv,
/// Power operator (`**`).
Pow,
/// A mod (`%`) operator.
Mod,
/// A dot operator (`.`)
Dot,
/// The comma operator (`,`)
Comma,
/// The colon operator (`:`)
Colon,
/// The tilde operator (`~`)
Tilde,
/// The assignment operator (`=`)
Assign,
/// The pipe symbol.
Pipe,
/// `==` operator
Eq,
/// `!=` operator
Ne,
/// `>` operator
Gt,
/// `>=` operator
Gte,
/// `<` operator
Lt,
/// `<=` operator
Lte,
/// Open Bracket
BracketOpen,
/// Close Bracket
BracketClose,
/// Open Parenthesis
ParenOpen,
/// Close Parenthesis
ParenClose,
/// Open Brace
BraceOpen,
/// Close Brace
BraceClose,
}
impl fmt::Display for Token<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::TemplateData(_) => f.write_str("template-data"),
Token::VariableStart => f.write_str("start of variable block"),
Token::VariableEnd => f.write_str("end of variable block"),
Token::BlockStart => f.write_str("start of block"),
Token::BlockEnd => f.write_str("end of block"),
Token::Ident(_) => f.write_str("identifier"),
Token::Str(_) | Token::String(_) => f.write_str("string"),
Token::Int(_) | Token::Int128(_) => f.write_str("integer"),
Token::Float(_) => f.write_str("float"),
Token::Plus => f.write_str("`+`"),
Token::Minus => f.write_str("`-`"),
Token::Mul => f.write_str("`*`"),
Token::Div => f.write_str("`/`"),
Token::FloorDiv => f.write_str("`//`"),
Token::Pow => f.write_str("`**`"),
Token::Mod => f.write_str("`%`"),
Token::Dot => f.write_str("`.`"),
Token::Comma => f.write_str("`,`"),
Token::Colon => f.write_str("`:`"),
Token::Tilde => f.write_str("`~`"),
Token::Assign => f.write_str("`=`"),
Token::Pipe => f.write_str("`|`"),
Token::Eq => f.write_str("`==`"),
Token::Ne => f.write_str("`!=`"),
Token::Gt => f.write_str("`>`"),
Token::Gte => f.write_str("`>=`"),
Token::Lt => f.write_str("`<`"),
Token::Lte => f.write_str("`<=`"),
Token::BracketOpen => f.write_str("`[`"),
Token::BracketClose => f.write_str("`]`"),
Token::ParenOpen => f.write_str("`(`"),
Token::ParenClose => f.write_str("`)`"),
Token::BraceOpen => f.write_str("`{`"),
Token::BraceClose => f.write_str("`}`"),
}
}
}
/// Token span information
#[derive(Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Span {
pub start_line: u16,
pub start_col: u16,
pub start_offset: u32,
pub end_line: u16,
pub end_col: u16,
pub end_offset: u32,
}
impl fmt::Debug for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_struct("Span")
.field("start_line", &self.start_line)
.field("start_col", &self.start_col)
.field("start_offset", &self.start_offset)
.field("end_line", &self.end_line)
.field("end_col", &self.end_col)
.field("end_offset", &self.end_offset)
.finish()
} else {
write!(
f,
" @ {}:{}-{}:{}",
self.start_line, self.start_col, self.end_line, self.end_col
)
}
}
}
+90
View File
@@ -0,0 +1,90 @@
use std::collections::BTreeMap;
use std::fmt;
use crate::compiler::tokens::Span;
use crate::error::ErrorKind;
use crate::value::Value;
/// This is a snapshot of the debug information.
#[cfg_attr(docsrs, doc(cfg(feature = "debug")))]
#[derive(Default)]
pub(crate) struct DebugInfo {
pub(crate) template_source: Option<String>,
pub(crate) referenced_locals: BTreeMap<String, Value>,
}
struct VarPrinter<'x>(&'x BTreeMap<String, Value>);
impl fmt::Debug for VarPrinter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
return f.write_str("No referenced variables");
}
let mut m = f.debug_struct("Referenced variables:");
let mut vars = self.0.iter().collect::<Vec<_>>();
vars.sort_by_key(|x| x.0);
for (key, value) in vars {
m.field(key, value);
}
m.finish()
}
}
impl DebugInfo {
pub fn source(&self) -> Option<&str> {
self.template_source.as_deref()
}
}
pub(super) fn render_debug_info(
f: &mut fmt::Formatter,
name: Option<&str>,
kind: ErrorKind,
line: Option<usize>,
span: Option<Span>,
info: &DebugInfo,
) -> fmt::Result {
if let Some(source) = info.source() {
let title = format!(
" {} ",
name.unwrap_or_default()
.rsplit(&['/', '\\'])
.next()
.unwrap_or("Template Source")
);
ok!(writeln!(f));
writeln!(f, "{:-^1$}", title, 79).unwrap();
let lines: Vec<_> = source.lines().enumerate().collect();
let idx = line.unwrap_or(1).saturating_sub(1);
let skip = idx.saturating_sub(3);
let pre = lines.iter().skip(skip).take(3.min(idx)).collect::<Vec<_>>();
let post = lines.iter().skip(idx + 1).take(3).collect::<Vec<_>>();
for (idx, line) in pre {
writeln!(f, "{:>4} | {}", idx + 1, line).unwrap();
}
if let Some(line) = lines.get(idx) {
writeln!(f, "{:>4} > {}", idx + 1, line.1).unwrap();
}
if let Some(span) = span {
if span.start_line == span.end_line {
ok!(writeln!(
f,
" i {}{} {}",
" ".repeat(span.start_col as usize),
"^".repeat(span.end_col as usize - span.start_col as usize),
kind,
));
}
}
for (idx, line) in post {
writeln!(f, "{:>4} | {}", idx + 1, line).unwrap();
}
write!(f, "{:~^1$}", "", 79).unwrap();
}
ok!(writeln!(f));
ok!(writeln!(f, "{:#?}", VarPrinter(&info.referenced_locals)));
write!(f, "{:-^1$}", "", 79).unwrap();
Ok(())
}
+317
View File
@@ -0,0 +1,317 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock};
use crate::error::Error;
use crate::filters;
use crate::output::Output;
use crate::tests;
use crate::utils::{write_escaped, AutoEscape};
use crate::value::Value;
use crate::vm::State;
pub(crate) fn no_auto_escape(_: &str) -> AutoEscape {
AutoEscape::None
}
/// These are extensions on template names that are ignored for the auto escape logic.
const IGNORED_EXTENSIONS: [&str; 3] = [".j2", ".jinja2", ".jinja"];
/// The default logic for auto escaping based on file extension.
///
/// * [`Html`](AutoEscape::Html): `.html`, `.htm`, `.xml`
#[cfg_attr(
feature = "json",
doc = r" * [`Json`](AutoEscape::Json): `.json`, `.json5`, `.js`, `.yaml`, `.yml`"
)]
/// * [`None`](AutoEscape::None): _all others_
///
/// Additionally `.j2`, `.jinja` and `.jinja2` as final extension is ignored. So
/// `.html.j2` is the considered the same as `.html`.
pub fn default_auto_escape_callback(mut name: &str) -> AutoEscape {
for ext in IGNORED_EXTENSIONS {
if let Some(stripped) = name.strip_suffix(ext) {
name = stripped;
break;
}
}
match name.rsplit('.').next() {
Some("html" | "htm" | "xml") => AutoEscape::Html,
#[cfg(feature = "json")]
Some("json" | "json5" | "js" | "yaml" | "yml") => AutoEscape::Json,
_ => AutoEscape::None,
}
}
/// The default formatter.
///
/// This formatter takes a value and directly writes it into the output format
/// while honoring the requested auto escape format of the state. If the
/// value is already marked as safe, it's handled as if no auto escaping
/// was requested.
///
/// * [`Html`](AutoEscape::Html): performs HTML escaping
#[cfg_attr(
feature = "json",
doc = r" * [`Json`](AutoEscape::Json): serializes values to JSON"
)]
/// * [`None`](AutoEscape::None): no escaping
/// * [`Custom(..)`](AutoEscape::Custom): results in an error
pub fn escape_formatter(out: &mut Output, state: &State, value: &Value) -> Result<(), Error> {
write_escaped(out, state.auto_escape(), value)
}
fn build_builtin_filters() -> BTreeMap<Cow<'static, str>, Value> {
let mut rv = BTreeMap::new();
rv.insert("safe".into(), Value::from_function(filters::safe));
let escape = Value::from_function(filters::escape);
rv.insert("escape".into(), escape.clone());
rv.insert("e".into(), escape);
#[cfg(feature = "builtins")]
{
rv.insert("lower".into(), Value::from_function(filters::lower));
rv.insert("upper".into(), Value::from_function(filters::upper));
rv.insert("title".into(), Value::from_function(filters::title));
rv.insert(
"capitalize".into(),
Value::from_function(filters::capitalize),
);
rv.insert("replace".into(), Value::from_function(filters::replace));
let length = Value::from_function(filters::length);
rv.insert("length".into(), length.clone());
rv.insert("count".into(), length);
rv.insert("dictsort".into(), Value::from_function(filters::dictsort));
rv.insert("items".into(), Value::from_function(filters::items));
rv.insert("reverse".into(), Value::from_function(filters::reverse));
rv.insert("trim".into(), Value::from_function(filters::trim));
rv.insert("join".into(), Value::from_function(filters::join));
rv.insert("split".into(), Value::from_function(filters::split));
rv.insert("lines".into(), Value::from_function(filters::lines));
rv.insert("default".into(), Value::from_function(filters::default));
rv.insert("d".into(), Value::from_function(filters::default));
rv.insert("round".into(), Value::from_function(filters::round));
rv.insert("abs".into(), Value::from_function(filters::abs));
rv.insert("int".into(), Value::from_function(filters::int));
rv.insert("float".into(), Value::from_function(filters::float));
rv.insert("attr".into(), Value::from_function(filters::attr));
rv.insert("first".into(), Value::from_function(filters::first));
rv.insert("last".into(), Value::from_function(filters::last));
rv.insert("min".into(), Value::from_function(filters::min));
rv.insert("max".into(), Value::from_function(filters::max));
rv.insert("sort".into(), Value::from_function(filters::sort));
rv.insert("list".into(), Value::from_function(filters::list));
rv.insert("string".into(), Value::from_function(filters::string));
rv.insert("bool".into(), Value::from_function(filters::bool));
rv.insert("batch".into(), Value::from_function(filters::batch));
rv.insert("slice".into(), Value::from_function(filters::slice));
rv.insert("sum".into(), Value::from_function(filters::sum));
rv.insert("indent".into(), Value::from_function(filters::indent));
rv.insert("select".into(), Value::from_function(filters::select));
rv.insert("reject".into(), Value::from_function(filters::reject));
rv.insert(
"selectattr".into(),
Value::from_function(filters::selectattr),
);
rv.insert(
"rejectattr".into(),
Value::from_function(filters::rejectattr),
);
rv.insert("map".into(), Value::from_function(filters::map));
rv.insert("groupby".into(), Value::from_function(filters::groupby));
rv.insert("unique".into(), Value::from_function(filters::unique));
rv.insert("chain".into(), Value::from_function(filters::chain));
rv.insert("zip".into(), Value::from_function(filters::zip));
rv.insert("pprint".into(), Value::from_function(filters::pprint));
rv.insert("format".into(), Value::from_function(filters::format));
#[cfg(feature = "json")]
{
rv.insert("tojson".into(), Value::from_function(filters::tojson));
}
#[cfg(feature = "urlencode")]
{
rv.insert("urlencode".into(), Value::from_function(filters::urlencode));
}
}
rv
}
pub(crate) fn get_builtin_filters() -> Arc<BTreeMap<Cow<'static, str>, Value>> {
static FILTERS: OnceLock<Arc<BTreeMap<Cow<'static, str>, Value>>> = OnceLock::new();
FILTERS
.get_or_init(|| Arc::new(build_builtin_filters()))
.clone()
}
fn build_builtin_tests() -> BTreeMap<Cow<'static, str>, Value> {
let mut rv = BTreeMap::new();
rv.insert(
"undefined".into(),
Value::from_function(tests::is_undefined),
);
rv.insert("defined".into(), Value::from_function(tests::is_defined));
rv.insert("none".into(), Value::from_function(tests::is_none));
let is_safe = Value::from_function(tests::is_safe);
rv.insert("safe".into(), is_safe.clone());
rv.insert("escaped".into(), is_safe);
#[cfg(feature = "builtins")]
{
rv.insert("boolean".into(), Value::from_function(tests::is_boolean));
rv.insert("odd".into(), Value::from_function(tests::is_odd));
rv.insert("even".into(), Value::from_function(tests::is_even));
rv.insert(
"divisibleby".into(),
Value::from_function(tests::is_divisibleby),
);
rv.insert("number".into(), Value::from_function(tests::is_number));
rv.insert("integer".into(), Value::from_function(tests::is_integer));
rv.insert("int".into(), Value::from_function(tests::is_integer));
rv.insert("float".into(), Value::from_function(tests::is_float));
rv.insert("string".into(), Value::from_function(tests::is_string));
rv.insert("sequence".into(), Value::from_function(tests::is_sequence));
rv.insert("iterable".into(), Value::from_function(tests::is_iterable));
rv.insert("mapping".into(), Value::from_function(tests::is_mapping));
rv.insert(
"startingwith".into(),
Value::from_function(tests::is_startingwith),
);
rv.insert(
"endingwith".into(),
Value::from_function(tests::is_endingwith),
);
rv.insert("lower".into(), Value::from_function(tests::is_lower));
rv.insert("upper".into(), Value::from_function(tests::is_upper));
rv.insert("sameas".into(), Value::from_function(tests::is_sameas));
// operators
let is_eq = Value::from_function(tests::is_eq);
rv.insert("eq".into(), is_eq.clone());
rv.insert("equalto".into(), is_eq.clone());
rv.insert("==".into(), is_eq);
let is_ne = Value::from_function(tests::is_ne);
rv.insert("ne".into(), is_ne.clone());
rv.insert("!=".into(), is_ne);
let is_lt = Value::from_function(tests::is_lt);
rv.insert("lt".into(), is_lt.clone());
rv.insert("lessthan".into(), is_lt.clone());
rv.insert("<".into(), is_lt);
let is_le = Value::from_function(tests::is_le);
rv.insert("le".into(), is_le.clone());
rv.insert("<=".into(), is_le);
let is_gt = Value::from_function(tests::is_gt);
rv.insert("gt".into(), is_gt.clone());
rv.insert("greaterthan".into(), is_gt.clone());
rv.insert(">".into(), is_gt);
let is_ge = Value::from_function(tests::is_ge);
rv.insert("ge".into(), is_ge.clone());
rv.insert(">=".into(), is_ge);
rv.insert("in".into(), Value::from_function(tests::is_in));
rv.insert("true".into(), Value::from_function(tests::is_true));
rv.insert("false".into(), Value::from_function(tests::is_false));
rv.insert("filter".into(), Value::from_function(tests::is_filter));
rv.insert("test".into(), Value::from_function(tests::is_test));
}
rv
}
pub(crate) fn get_builtin_tests() -> Arc<BTreeMap<Cow<'static, str>, Value>> {
static TESTS: OnceLock<Arc<BTreeMap<Cow<'static, str>, Value>>> = OnceLock::new();
TESTS
.get_or_init(|| Arc::new(build_builtin_tests()))
.clone()
}
fn build_globals() -> BTreeMap<Cow<'static, str>, Value> {
#[allow(unused_mut)]
let mut rv = BTreeMap::new();
#[cfg(feature = "builtins")]
{
use crate::functions::{self, BoxedFunction};
rv.insert(
"range".into(),
BoxedFunction::new(functions::range).to_value(),
);
rv.insert(
"dict".into(),
BoxedFunction::new(functions::dict).to_value(),
);
rv.insert(
"debug".into(),
BoxedFunction::new(functions::debug).to_value(),
);
rv.insert(
"namespace".into(),
BoxedFunction::new(functions::namespace).to_value(),
);
}
rv
}
pub(crate) fn get_globals() -> Arc<BTreeMap<Cow<'static, str>, Value>> {
static GLOBALS: OnceLock<Arc<BTreeMap<Cow<'static, str>, Value>>> = OnceLock::new();
GLOBALS.get_or_init(|| Arc::new(build_globals())).clone()
}
#[cfg(test)]
mod unit_tests {
use super::*;
use similar_asserts::assert_eq;
#[test]
fn test_default_uto_escape() {
assert_eq!(default_auto_escape_callback("foo.html"), AutoEscape::Html);
assert_eq!(
default_auto_escape_callback("foo.html.j2"),
AutoEscape::Html
);
assert_eq!(default_auto_escape_callback("foo.htm"), AutoEscape::Html);
assert_eq!(default_auto_escape_callback("foo.htm.j2"), AutoEscape::Html);
assert_eq!(default_auto_escape_callback("foo.txt"), AutoEscape::None);
assert_eq!(default_auto_escape_callback("foo.txt.j2"), AutoEscape::None);
assert_eq!(
default_auto_escape_callback("foo.html.jinja"),
AutoEscape::Html
);
assert_eq!(
default_auto_escape_callback("foo.htm.jinja"),
AutoEscape::Html
);
assert_eq!(
default_auto_escape_callback("foo.txt.jinja"),
AutoEscape::None
);
assert_eq!(
default_auto_escape_callback("foo.html.jinja2"),
AutoEscape::Html
);
assert_eq!(
default_auto_escape_callback("foo.htm.jinja2"),
AutoEscape::Html
);
assert_eq!(
default_auto_escape_callback("foo.txt.jinja2"),
AutoEscape::None
);
// only one is removed
assert_eq!(
default_auto_escape_callback("foo.html.j2.jinja"),
AutoEscape::None
);
#[cfg(feature = "json")]
{
assert_eq!(default_auto_escape_callback("foo.yaml"), AutoEscape::Json);
assert_eq!(
default_auto_escape_callback("foo.json.j2"),
AutoEscape::Json
);
}
}
}
+859
View File
@@ -0,0 +1,859 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
use std::sync::{Arc, OnceLock};
use serde::Serialize;
use crate::compiler::codegen::CodeGenerator;
use crate::compiler::instructions::Instructions;
use crate::compiler::parser::parse_expr;
use crate::error::{attach_basic_debug_info, Error, ErrorKind};
use crate::expression::Expression;
use crate::output::Output;
use crate::template::{
AutoEscapeFunc, CompiledTemplate, CompiledTemplateRef, Template, TemplateConfig,
};
use crate::utils::{write_escaped, AutoEscape, BTreeMapKeysDebug, UndefinedBehavior};
use crate::value::{FunctionArgs, FunctionResult, UndefinedType, Value, ValueRepr};
use crate::vm::State;
use crate::{defaults, functions};
type FormatterFunc = dyn Fn(&mut Output, &State, &Value) -> Result<(), Error> + Sync + Send;
type PathJoinFunc = dyn for<'s> Fn(&'s str, &'s str) -> Cow<'s, str> + Sync + Send;
type UnknownMethodFunc =
dyn Fn(&State, &Value, &str, &[Value]) -> Result<Value, Error> + Sync + Send;
fn default_auto_escape_callback() -> Arc<AutoEscapeFunc> {
static DEFAULT_AUTO_ESCAPE: OnceLock<Arc<AutoEscapeFunc>> = OnceLock::new();
DEFAULT_AUTO_ESCAPE
.get_or_init(|| Arc::new(defaults::default_auto_escape_callback))
.clone()
}
fn no_auto_escape_callback() -> Arc<AutoEscapeFunc> {
static NO_AUTO_ESCAPE: OnceLock<Arc<AutoEscapeFunc>> = OnceLock::new();
NO_AUTO_ESCAPE
.get_or_init(|| Arc::new(defaults::no_auto_escape))
.clone()
}
fn default_formatter() -> Arc<FormatterFunc> {
static FORMATTER: OnceLock<Arc<FormatterFunc>> = OnceLock::new();
FORMATTER
.get_or_init(|| Arc::new(defaults::escape_formatter))
.clone()
}
/// The maximum recursion in the VM. Normally each stack frame
/// adds one to this counter (e.g., every time a frame is added).
/// However in some situations more depth is pushed if the cost
/// of the stack frame is higher. Raising this limit
/// requires enabling the `stacker` feature.
const MAX_RECURSION: usize = 500;
/// An abstraction that holds the engine configuration.
///
/// This object holds the central configuration state for templates. It is also
/// the container for all loaded templates.
///
/// The environment holds references to the source the templates were created from.
/// This makes it very inconvenient to pass around unless the templates are static
/// strings.
///
/// There are generally two ways to construct an environment:
///
/// * [`Environment::new`] creates an environment preconfigured with sensible
/// defaults. It will contain all built-in filters, tests and globals as well
/// as a callback for auto escaping based on file extension.
/// * [`Environment::empty`] creates a completely blank environment.
#[derive(Clone)]
pub struct Environment<'source> {
templates: TemplateStore<'source>,
filters: Arc<BTreeMap<Cow<'source, str>, Value>>,
tests: Arc<BTreeMap<Cow<'source, str>, Value>>,
globals: Arc<BTreeMap<Cow<'source, str>, Value>>,
path_join_callback: Option<Arc<PathJoinFunc>>,
pub(crate) unknown_method_callback: Option<Arc<UnknownMethodFunc>>,
undefined_behavior: UndefinedBehavior,
formatter: Arc<FormatterFunc>,
formatter_is_default: bool,
#[cfg(feature = "debug")]
debug: bool,
#[cfg(feature = "fuel")]
fuel: Option<u64>,
recursion_limit: usize,
}
impl Default for Environment<'_> {
fn default() -> Self {
Environment::empty()
}
}
impl fmt::Debug for Environment<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Environment")
.field("globals", &self.globals)
.field("tests", &BTreeMapKeysDebug(&self.tests))
.field("filters", &BTreeMapKeysDebug(&self.filters))
.field("templates", &self.templates)
.finish()
}
}
impl<'source> Environment<'source> {
/// Creates a new environment with sensible defaults.
///
/// This environment does not yet contain any templates but it will have all
/// the default filters, tests and globals loaded. If you do not want any
/// default configuration you can use the alternative
/// [`empty`](Environment::empty) method.
#[cfg_attr(
not(feature = "serde"),
deprecated(
since = "2.0.4",
note = "Attempted to instantiate an environment with serde. Future \
versions of MiniJinja will require enabling the 'serde' feature to use \
serde types. To silence this warning add 'serde' to the list of features of minijinja."
)
)]
pub fn new() -> Environment<'source> {
Environment {
templates: TemplateStore::new(TemplateConfig::new(default_auto_escape_callback())),
filters: defaults::get_builtin_filters(),
tests: defaults::get_builtin_tests(),
globals: defaults::get_globals(),
path_join_callback: None,
unknown_method_callback: None,
undefined_behavior: UndefinedBehavior::default(),
formatter: default_formatter(),
formatter_is_default: true,
#[cfg(feature = "debug")]
debug: cfg!(debug_assertions),
#[cfg(feature = "fuel")]
fuel: None,
recursion_limit: MAX_RECURSION,
}
}
/// Creates a completely empty environment.
///
/// This environment has no filters, no templates, no globals and no default
/// logic for auto escaping configured.
pub fn empty() -> Environment<'source> {
Environment {
templates: TemplateStore::new(TemplateConfig::new(no_auto_escape_callback())),
filters: Default::default(),
tests: Default::default(),
globals: Default::default(),
path_join_callback: None,
unknown_method_callback: None,
undefined_behavior: UndefinedBehavior::default(),
formatter: default_formatter(),
formatter_is_default: true,
#[cfg(feature = "debug")]
debug: cfg!(debug_assertions),
#[cfg(feature = "fuel")]
fuel: None,
recursion_limit: MAX_RECURSION,
}
}
/// Loads a template from a string into the environment.
///
/// The `name` parameter defines the name of the template which identifies
/// it. To look up a loaded template use the [`get_template`](Self::get_template)
/// method.
///
/// ```
/// # use minijinja::Environment;
/// let mut env = Environment::new();
/// env.add_template("index.html", "Hello {{ name }}!").unwrap();
/// ```
/// This method fails if the template has a syntax error.
///
/// Note that there are situations where the interface of this method is
/// too restrictive as you need to hold on to the strings for the lifetime
/// of the environment. To avoid this restriction use
/// [`add_template_owned`](Self::add_template_owned).
pub fn add_template(&mut self, name: &'source str, source: &'source str) -> Result<(), Error> {
self.templates.insert(name, source)
}
/// Adds a template without borrowing.
///
/// This lets you place an owned [`String`] in the environment rather than the
/// borrowed `&str` without having to worry about lifetimes.
///
/// ```
/// # use minijinja::Environment;
/// let mut env = Environment::new();
/// env.add_template_owned("index.html".to_string(), "Hello {{ name }}!".to_string()).unwrap();
/// ```
///
/// **Note**: the name is a bit of a misnomer as this API also allows borrowing,
/// as the parameters are actually [`Cow`]. This method fails if the template
/// has a syntax error.
pub fn add_template_owned<N, S>(&mut self, name: N, source: S) -> Result<(), Error>
where
N: Into<Cow<'source, str>>,
S: Into<Cow<'source, str>>,
{
self.templates.insert_cow(name.into(), source.into())
}
/// Register a template loader as source of templates.
///
/// When a template loader is registered, the environment gains the ability
/// to dynamically load templates. The loader is invoked with the name of
/// the template. If this template exists `Ok(Some(template_source))` has
/// to be returned, otherwise `Ok(None)`. Once a template has been loaded
/// it's stored on the environment. This means the loader is only invoked
/// once per template name.
///
/// For loading templates from the file system, you can use the
/// [`path_loader`](crate::path_loader) function.
///
/// # Example
///
/// ```rust
/// # use minijinja::Environment;
/// fn create_env() -> Environment<'static> {
/// let mut env = Environment::new();
/// env.set_loader(|name| {
/// if name == "layout.html" {
/// Ok(Some("...".into()))
/// } else {
/// Ok(None)
/// }
/// });
/// env
/// }
/// ```
pub fn set_loader<F>(&mut self, f: F)
where
F: Fn(&str) -> Result<Option<String>, Error> + Send + Sync + 'static,
{
self.templates.set_loader(f);
}
/// Preserve the trailing newline when rendering templates.
///
/// The default is `false`, which causes a single newline, if present, to be
/// stripped from the end of the template.
///
/// This setting is used whenever a template is loaded into the environment.
/// Changing it at a later point only affects future templates loaded.
pub fn set_keep_trailing_newline(&mut self, yes: bool) {
self.templates
.template_config
.ws_config
.keep_trailing_newline = yes;
}
/// Returns the value of the trailing newline preservation flag.
pub fn keep_trailing_newline(&self) -> bool {
self.templates
.template_config
.ws_config
.keep_trailing_newline
}
/// Remove the first newline after a block.
///
/// If this is set to `true` then the first newline after a block is removed
/// (block, not variable tag!). Defaults to `false`.
pub fn set_trim_blocks(&mut self, yes: bool) {
self.templates.template_config.ws_config.trim_blocks = yes;
}
/// Returns the value of the trim blocks flag.
pub fn trim_blocks(&self) -> bool {
self.templates.template_config.ws_config.trim_blocks
}
/// Remove leading spaces and tabs from the start of a line to a block.
///
/// If this is set to `true` then leading spaces and tabs from the start of a line
/// to the block tag are removed.
pub fn set_lstrip_blocks(&mut self, yes: bool) {
self.templates.template_config.ws_config.lstrip_blocks = yes;
}
/// Returns the value of the lstrip blocks flag.
pub fn lstrip_blocks(&self) -> bool {
self.templates.template_config.ws_config.lstrip_blocks
}
/// Removes a template by name.
pub fn remove_template(&mut self, name: &str) {
self.templates.remove(name);
}
/// Sets a callback to join template paths.
///
/// By default this returns the template path unchanged, but it can be used
/// to implement relative path resolution between templates. The first
/// argument to the callback is the name of the template to be loaded, the
/// second argument is the parent path.
///
/// The following example demonstrates how a basic path joining algorithm
/// can be implemented.
///
/// ```
/// # let mut env = minijinja::Environment::new();
/// env.set_path_join_callback(|name, parent| {
/// let mut rv = parent.split('/').collect::<Vec<_>>();
/// rv.pop();
/// name.split('/').for_each(|segment| match segment {
/// "." => {}
/// ".." => { rv.pop(); }
/// _ => { rv.push(segment); }
/// });
/// rv.join("/").into()
/// });
/// ```
pub fn set_path_join_callback<F>(&mut self, f: F)
where
F: for<'s> Fn(&'s str, &'s str) -> Cow<'s, str> + Send + Sync + 'static,
{
self.path_join_callback = Some(Arc::new(f));
}
/// Sets a callback invoked for unknown methods on objects.
///
/// This registers a function with the environment that is invoked when invoking a method
/// on a value results in a [`UnknownMethod`](crate::ErrorKind::UnknownMethod) error.
/// In that case the callback is invoked with [`State`], the [`Value`], the name of
/// the method as `&str` as well as all arguments in a slice.
///
/// This for instance implements a `.items()` method that invokes the `|items` filter:
///
/// ```rust
/// use minijinja::value::{ValueKind, from_args};
/// use minijinja::{Error, ErrorKind};
/// # let mut env = minijinja::Environment::new();
///
/// env.set_unknown_method_callback(|state, value, method, args| {
/// if value.kind() == ValueKind::Map && method == "items" {
/// let _: () = from_args(args)?;
/// state.apply_filter("items", &[value.clone()])
/// } else {
/// Err(Error::from(ErrorKind::UnknownMethod))
/// }
/// });
/// ```
///
/// This can be used to increase the compatibility with Jinja2 templates that might
/// call Python methods on objects which are not available in minijinja. A range of
/// common Python methods is implemented in `minijinja-contrib`. For more information
/// see [minijinja_contrib::pycompat](https://docs.rs/minijinja-contrib/latest/minijinja_contrib/pycompat/).
pub fn set_unknown_method_callback<F>(&mut self, f: F)
where
F: Fn(&State, &Value, &str, &[Value]) -> Result<Value, Error> + Sync + Send + 'static,
{
self.unknown_method_callback = Some(Arc::new(f));
}
/// Removes all stored templates.
///
/// This method is mainly useful when combined with a loader as it causes
/// the loader to "reload" templates. By calling this method one can trigger
/// a reload.
pub fn clear_templates(&mut self) {
self.templates.clear();
}
/// Returns an iterator over the already loaded templates and their names.
///
/// Only templates that are already loaded will be returned.
///
/// ```
/// # use minijinja::{Environment, context};
/// let mut env = Environment::new();
/// env.add_template("hello.txt", "Hello {{ name }}!").unwrap();
/// env.add_template("goodbye.txt", "Goodbye {{ name }}!").unwrap();
///
/// # assert_eq!(env.templates().count(), 2);
/// for (name, tmpl) in env.templates() {
/// println!("{}", tmpl.render(context!{ name => "World" }).unwrap());
/// }
/// ```
pub fn templates(&self) -> impl Iterator<Item = (&str, Template<'_, '_>)> {
self.templates.iter().map(|(name, template)| {
let template = Template::new(self, CompiledTemplateRef::Borrowed(template));
(name, template)
})
}
/// Fetches a template by name.
///
/// This requires that the template has been loaded with
/// [`add_template`](Environment::add_template) beforehand. If the template was
/// not loaded an error of kind `TemplateNotFound` is returned. If a loader was
/// added to the engine this can also dynamically load templates.
///
/// ```
/// # use minijinja::{Environment, context};
/// let mut env = Environment::new();
/// env.add_template("hello.txt", "Hello {{ name }}!").unwrap();
/// let tmpl = env.get_template("hello.txt").unwrap();
/// println!("{}", tmpl.render(context!{ name => "World" }).unwrap());
/// ```
pub fn get_template(&self, name: &str) -> Result<Template<'_, '_>, Error> {
let compiled = ok!(self.templates.get(name));
Ok(Template::new(self, CompiledTemplateRef::Borrowed(compiled)))
}
/// Loads a template from a string.
///
/// In some cases you only need to work with (e.g., render) a template once.
///
/// ```
/// # use minijinja::{Environment, context};
/// let env = Environment::new();
/// let tmpl = env.template_from_named_str("template_name", "Hello {{ name }}").unwrap();
/// let rv = tmpl.render(context! { name => "World" });
/// println!("{}", rv.unwrap());
/// ```
pub fn template_from_named_str(
&self,
name: &'source str,
source: &'source str,
) -> Result<Template<'_, 'source>, Error> {
Ok(Template::new(
self,
CompiledTemplateRef::Owned(Arc::new(ok!(CompiledTemplate::new(
name,
source,
&self.templates.template_config,
)))),
))
}
/// Loads a template from a string, with name `<string>`.
///
/// This is a shortcut to [`template_from_named_str`](Self::template_from_named_str)
/// with name set to `<string>`.
pub fn template_from_str(&self, source: &'source str) -> Result<Template<'_, 'source>, Error> {
self.template_from_named_str("<string>", source)
}
/// Parses and renders a template from a string in one go with name.
///
/// Like [`render_str`](Self::render_str), but provide a name for the
/// template to be used instead of the default `<string>`. This is an
/// alias for [`template_from_named_str`](Self::template_from_named_str) paired with
/// [`render`](Template::render).
///
/// ```
/// # use minijinja::{Environment, context};
/// let env = Environment::new();
/// let rv = env.render_named_str(
/// "template_name",
/// "Hello {{ name }}",
/// context!{ name => "World" }
/// );
/// println!("{}", rv.unwrap());
/// ```
///
/// **Note on values:** The [`Value`] type implements `Serialize` and can be
/// efficiently passed to render. It does not undergo actual serialization.
pub fn render_named_str<S: Serialize>(
&self,
name: &str,
source: &str,
ctx: S,
) -> Result<String, Error> {
ok!(self.template_from_named_str(name, source)).render(ctx)
}
/// Parses and renders a template from a string in one go.
///
/// In some cases you really only need a template to be rendered once from
/// a string and returned. The internal name of the template is `<string>`.
///
/// This is an alias for [`template_from_str`](Self::template_from_str) paired with
/// [`render`](Template::render).
///
/// **Note on values:** The [`Value`] type implements `Serialize` and can be
/// efficiently passed to render. It does not undergo actual serialization.
pub fn render_str<S: Serialize>(&self, source: &str, ctx: S) -> Result<String, Error> {
// reduce total amount of code falling under mono morphization into
// this function, and share the rest in _eval.
ok!(self.template_from_str(source)).render(ctx)
}
/// Sets a new function to select the default auto escaping.
///
/// This function is invoked when templates are loaded into the environment
/// to determine the default auto escaping behavior. The function is
/// invoked with the name of the template and can make an initial auto
/// escaping decision based on that. The default implementation
/// ([`default_auto_escape_callback`](defaults::default_auto_escape_callback))
/// turns on escaping depending on the file extension.
///
/// ```
/// # use minijinja::{Environment, AutoEscape};
/// # let mut env = Environment::new();
/// env.set_auto_escape_callback(|name| {
/// if matches!(name.rsplit('.').next().unwrap_or(""), "html" | "htm" | "aspx") {
/// AutoEscape::Html
/// } else {
/// AutoEscape::None
/// }
/// });
/// ```
pub fn set_auto_escape_callback<F>(&mut self, f: F)
where
F: Fn(&str) -> AutoEscape + 'static + Sync + Send,
{
self.templates.template_config.default_auto_escape = Arc::new(f);
}
/// Changes the undefined behavior.
///
/// This changes the runtime behavior of [`undefined`](Value::UNDEFINED) values in
/// the template engine. For more information see [`UndefinedBehavior`]. The
/// default is [`UndefinedBehavior::Lenient`].
pub fn set_undefined_behavior(&mut self, behavior: UndefinedBehavior) {
self.undefined_behavior = behavior;
}
/// Returns the current undefined behavior.
///
/// This is particularly useful if a filter function or similar wants to change its
/// behavior with regards to undefined values.
#[inline(always)]
pub fn undefined_behavior(&self) -> UndefinedBehavior {
self.undefined_behavior
}
/// Sets a different formatter function.
///
/// The formatter is invoked to format the given value into the provided
/// [`Output`]. The default implementation is
/// [`escape_formatter`](defaults::escape_formatter).
///
/// When implementing a custom formatter it depends on if auto escaping
/// should be supported or not. If auto escaping should be supported then
/// it's easiest to just wrap the default formatter. The
/// following example swaps out `None` values before rendering for
/// `Undefined` which renders as an empty string instead.
///
/// The current value of the auto escape flag can be retrieved directly
/// from the [`State`].
///
/// ```
/// # use minijinja::Environment;
/// # let mut env = Environment::new();
/// use minijinja::escape_formatter;
/// use minijinja::value::Value;
///
/// env.set_formatter(|out, state, value| {
/// escape_formatter(
/// out,
/// state,
/// if value.is_none() {
/// &Value::UNDEFINED
/// } else {
/// value
/// },
/// )
///});
/// # assert_eq!(env.render_str("{{ none }}", ()).unwrap(), "");
/// ```
pub fn set_formatter<F>(&mut self, f: F)
where
F: Fn(&mut Output, &State, &Value) -> Result<(), Error> + 'static + Sync + Send,
{
self.formatter = Arc::new(f);
self.formatter_is_default = false;
}
/// Enable or disable the debug mode.
///
/// When the debug mode is enabled the engine will dump out some of the
/// execution state together with the source information of the executing
/// template when an error is created. The cost of this is relatively
/// high as the data including the template source is cloned.
///
/// When this is enabled templates will print debug information with source
/// context when the error is printed.
///
/// This requires the `debug` feature. This is enabled by default if
/// debug assertions are enabled and false otherwise.
#[cfg(feature = "debug")]
#[cfg_attr(docsrs, doc(cfg(feature = "debug")))]
pub fn set_debug(&mut self, enabled: bool) {
self.debug = enabled;
}
/// Returns the current value of the debug flag.
#[cfg(feature = "debug")]
pub fn debug(&self) -> bool {
self.debug
}
/// Sets the optional fuel of the engine.
///
/// When MiniJinja is compiled with the `fuel` feature then every
/// instruction consumes a certain amount of fuel. Usually `1`, some will
/// consume no fuel. By default the engine has the fuel feature disabled
/// (`None`). To turn on fuel set something like `Some(50000)` which will
/// allow 50.000 instructions to execute before running out of fuel.
///
/// To find out how much fuel is consumed, you can access the fuel levels
/// from the [`State`](crate::State).
///
/// Fuel consumed per-render.
#[cfg(feature = "fuel")]
#[cfg_attr(docsrs, doc(cfg(feature = "fuel")))]
pub fn set_fuel(&mut self, fuel: Option<u64>) {
self.fuel = fuel;
}
/// Returns the configured fuel.
#[cfg(feature = "fuel")]
#[cfg_attr(docsrs, doc(cfg(feature = "fuel")))]
pub fn fuel(&self) -> Option<u64> {
self.fuel
}
/// Sets the syntax for the environment.
///
/// This setting is used whenever a template is loaded into the environment.
/// Changing it at a later point only affects future templates loaded.
///
/// See [`SyntaxConfig`](crate::syntax::SyntaxConfig) for more information.
#[cfg(feature = "custom_syntax")]
#[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
pub fn set_syntax(&mut self, syntax: crate::syntax::SyntaxConfig) {
self.templates.template_config.syntax_config = syntax;
}
/// Returns the current syntax config.
#[cfg(feature = "custom_syntax")]
#[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
pub fn syntax(&self) -> &crate::syntax::SyntaxConfig {
&self.templates.template_config.syntax_config
}
/// Reconfigures the runtime recursion limit.
///
/// This defaults to `500`. Raising it above that level requires the `stacker`
/// feature to be enabled. Otherwise the limit is silently capped at that safe
/// maximum. Note that the maximum is not necessarily safe if the thread uses
/// a lot of stack space already, it's just a value that was validated once to
/// provide basic protection.
///
/// Every operation that requires recursion in MiniJinja increments an internal
/// recursion counter. The actual cost attributed to that recursion depends on
/// the cost of the operation. If statements and for loops for instance only
/// increase the counter by 1, whereas template includes and macros might increase
/// it to 10 or more.
///
/// **Note on stack growth:** even if the stacker feature is enabled it does not
/// mean that in all cases stack can grow to the limits desired. For instance in
/// WASM the maximum limits are additionally enforced by the runtime.
pub fn set_recursion_limit(&mut self, level: usize) {
#[cfg(not(feature = "stacker"))]
{
self.recursion_limit = level.min(MAX_RECURSION);
}
#[cfg(feature = "stacker")]
{
self.recursion_limit = level;
}
}
/// Returns the current max recursion limit.
pub fn recursion_limit(&self) -> usize {
self.recursion_limit
}
/// Compiles an expression.
///
/// This lets you compile an expression in the template language and evaluate it.
/// This makes it possible to use the language's expressions as a minimal
/// scripting language. For more information and an
/// example see [`Expression`].
pub fn compile_expression(&self, expr: &'source str) -> Result<Expression<'_, 'source>, Error> {
self._compile_expression(expr)
.map(|instr| Expression::new(self, instr))
}
/// Compiles an expression without capturing the lifetime.
///
/// This works exactly like [`compile_expression`](Self::compile_expression) but
/// lets you pass an owned string without capturing the lifetime.
pub fn compile_expression_owned<E>(&self, expr: E) -> Result<Expression<'_, 'source>, Error>
where
E: Into<Cow<'source, str>>,
{
crate::loader::OwnedInstructions::try_new(Box::from(expr.into()), |expr| {
self._compile_expression(expr)
})
.map(|instr| Expression::new_owned(self, instr))
}
fn _compile_expression<'expr>(&self, expr: &'expr str) -> Result<Instructions<'expr>, Error> {
attach_basic_debug_info(
parse_expr(expr).map(|ast| {
let mut g = CodeGenerator::new("<expression>", expr);
g.compile_expr(&ast);
g.finish().0
}),
expr,
)
}
/// Adds a new filter function.
///
/// Filter functions are functions that can be applied to values in
/// templates. For details about filters have a look at
/// [`filters`](crate::filters).
pub fn add_filter<N, F, Rv, Args>(&mut self, name: N, f: F)
where
N: Into<Cow<'source, str>>,
F: functions::Function<Rv, Args>,
Rv: FunctionResult,
Args: for<'a> FunctionArgs<'a>,
{
Arc::make_mut(&mut self.filters).insert(name.into(), Value::from_function(f));
}
/// Removes a filter by name.
pub fn remove_filter(&mut self, name: &str) {
Arc::make_mut(&mut self.filters).remove(name);
}
/// Adds a new test function.
///
/// Test functions are similar to filters but perform a check on a value
/// where the return value is always true or false. For details about tests
/// have a look at [`tests`](crate::tests).
pub fn add_test<N, F, Rv, Args>(&mut self, name: N, f: F)
where
N: Into<Cow<'source, str>>,
F: functions::Function<Rv, Args>,
Rv: FunctionResult,
Args: for<'a> FunctionArgs<'a>,
{
Arc::make_mut(&mut self.tests).insert(name.into(), Value::from_function(f));
}
/// Removes a test by name.
pub fn remove_test(&mut self, name: &str) {
Arc::make_mut(&mut self.tests).remove(name);
}
/// Adds a new global function.
///
/// For details about functions have a look at [`functions`]. Note that
/// functions and other global variables share the same namespace.
/// For more details about functions have a look at
/// [`Function`](crate::functions::Function).
///
/// This is a shortcut for calling [`add_global`](Self::add_global) with the
/// function wrapped with [`Value::from_function`].
pub fn add_function<N, F, Rv, Args>(&mut self, name: N, f: F)
where
N: Into<Cow<'source, str>>,
F: functions::Function<Rv, Args>,
Rv: FunctionResult,
Args: for<'a> FunctionArgs<'a>,
{
self.add_global(name.into(), Value::from_function(f))
}
/// Adds a global variable.
pub fn add_global<N, V>(&mut self, name: N, value: V)
where
N: Into<Cow<'source, str>>,
V: Into<Value>,
{
Arc::make_mut(&mut self.globals).insert(name.into(), value.into());
}
/// Removes a global function or variable by name.
pub fn remove_global(&mut self, name: &str) {
Arc::make_mut(&mut self.globals).remove(name);
}
/// Returns an iterator of all global variables.
pub fn globals(&self) -> impl Iterator<Item = (&str, Value)> {
self.globals
.iter()
.map(|(key, value)| (key as &str, value.clone()))
}
/// Returns an empty [`State`] for testing purposes and similar.
pub fn empty_state(&self) -> State<'_, '_> {
State::new_for_env(self)
}
/// Looks up a global.
pub(crate) fn get_global(&self, name: &str) -> Option<Value> {
self.globals.get(name).cloned()
}
/// Looks up a filter.
pub(crate) fn get_filter(&self, name: &str) -> Option<&Value> {
self.filters.get(name)
}
/// Looks up a test function.
pub(crate) fn get_test(&self, name: &str) -> Option<&Value> {
self.tests.get(name)
}
pub(crate) fn initial_auto_escape(&self, name: &str) -> AutoEscape {
(self.templates.template_config.default_auto_escape)(name)
}
#[inline(always)]
pub(crate) fn is_default_formatter(&self) -> bool {
self.formatter_is_default
}
/// Formats a value into the final format.
///
/// This step is called finalization in Jinja2 but since we are writing into
/// the output stream rather than converting values, it's renamed to format
/// here.
pub(crate) fn format(
&self,
value: &Value,
state: &State,
out: &mut Output,
) -> Result<(), Error> {
match (self.undefined_behavior, &value.0) {
// this intentionally does not check for SilentUndefined. SilentUndefined is
// exclusively used in the missing else condition of an if expression to match
// Jinja2 behavior. Those go straight to the formatter.
(
UndefinedBehavior::Strict | UndefinedBehavior::SemiStrict,
&ValueRepr::Undefined(UndefinedType::Default),
) => Err(Error::from(ErrorKind::UndefinedError)),
_ => {
if self.formatter_is_default {
write_escaped(out, state.auto_escape(), value)
} else {
(self.formatter)(out, state, value)
}
}
}
}
/// Performs a template path join.
pub(crate) fn join_template_path<'s>(&self, name: &'s str, parent: &'s str) -> Cow<'s, str> {
match self.path_join_callback {
Some(ref cb) => cb(name, parent),
None => Cow::Borrowed(name),
}
}
}
use crate::loader::LoaderStore as TemplateStore;
+419
View File
@@ -0,0 +1,419 @@
use std::borrow::Cow;
use std::fmt;
use std::sync::Arc;
use crate::compiler::tokens::Span;
/// Represents template errors.
///
/// If debug mode is enabled a template error contains additional debug
/// information that can be displayed by formatting an error with the
/// alternative formatting (``format!("{:#}", err)``). That information
/// is also shown for the [`Debug`] display where the extended information
/// is hidden when the alternative formatting is used.
///
/// Since MiniJinja takes advantage of chained errors it's recommended
/// to render the entire chain to better understand the causes.
///
/// # Example
///
/// Here is an example of how you might want to render errors:
///
/// ```rust
/// # let mut env = minijinja::Environment::new();
/// # env.add_template("", "");
/// # let template = env.get_template("").unwrap(); let ctx = ();
/// match template.render(ctx) {
/// Ok(result) => println!("{}", result),
/// Err(err) => {
/// eprintln!("Could not render template: {:#}", err);
/// // render causes as well
/// let mut err = &err as &dyn std::error::Error;
/// while let Some(next_err) = err.source() {
/// eprintln!();
/// eprintln!("caused by: {:#}", next_err);
/// err = next_err;
/// }
/// }
/// }
/// ```
pub struct Error {
repr: Box<ErrorRepr>,
}
/// The internal error data
#[derive(Clone)]
struct ErrorRepr {
kind: ErrorKind,
detail: Option<Cow<'static, str>>,
name: Option<String>,
lineno: usize,
span: Option<Span>,
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
#[cfg(feature = "debug")]
debug_info: Option<Arc<crate::debug::DebugInfo>>,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut err = f.debug_struct("Error");
err.field("kind", &self.kind());
if let Some(ref detail) = self.repr.detail {
err.field("detail", detail);
}
if let Some(ref name) = self.name() {
err.field("name", name);
}
if let Some(line) = self.line() {
err.field("line", &line);
}
if let Some(ref source) = std::error::Error::source(self) {
err.field("source", source);
}
ok!(err.finish());
// so this is a bit questionable, but because of how commonly errors are just
// unwrapped i think it's sensible to spit out the debug info following the
// error struct dump.
#[cfg(feature = "debug")]
{
if !f.alternate() && self.debug_info().is_some() {
ok!(writeln!(f));
ok!(writeln!(f, "{}", self.display_debug_info()));
}
}
Ok(())
}
}
/// An enum describing the error kind.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
/// A non primitive value was encountered where one was expected.
NonPrimitive,
/// A value is not valid for a key in a map.
NonKey,
/// An invalid operation was attempted.
InvalidOperation,
/// The template has a syntax error
SyntaxError,
/// A template was not found.
TemplateNotFound,
/// Too many arguments were passed to a function.
TooManyArguments,
/// A expected argument was missing
MissingArgument,
/// A filter is unknown
UnknownFilter,
/// A test is unknown
UnknownTest,
/// A function is unknown
UnknownFunction,
/// Un unknown method was called
UnknownMethod,
/// A bad escape sequence in a string was encountered.
BadEscape,
/// An operation on an undefined value was attempted.
UndefinedError,
/// Not able to serialize this value.
BadSerialization,
/// Not able to deserialize this value.
#[cfg(feature = "deserialization")]
CannotDeserialize,
/// An error happened in an include.
BadInclude,
/// An error happened in a super block.
EvalBlock,
/// Unable to unpack a value.
CannotUnpack,
/// Failed writing output.
WriteFailure,
/// Engine ran out of fuel
#[cfg(feature = "fuel")]
OutOfFuel,
#[cfg(feature = "custom_syntax")]
/// Error creating aho-corasick delimiters
InvalidDelimiter,
/// An unknown block was called
#[cfg(feature = "multi_template")]
UnknownBlock,
}
impl ErrorKind {
fn description(self) -> &'static str {
match self {
ErrorKind::NonPrimitive => "not a primitive",
ErrorKind::NonKey => "not a key type",
ErrorKind::InvalidOperation => "invalid operation",
ErrorKind::SyntaxError => "syntax error",
ErrorKind::TemplateNotFound => "template not found",
ErrorKind::TooManyArguments => "too many arguments",
ErrorKind::MissingArgument => "missing argument",
ErrorKind::UnknownFilter => "unknown filter",
ErrorKind::UnknownFunction => "unknown function",
ErrorKind::UnknownTest => "unknown test",
ErrorKind::UnknownMethod => "unknown method",
ErrorKind::BadEscape => "bad string escape",
ErrorKind::UndefinedError => "undefined value",
ErrorKind::BadSerialization => "could not serialize to value",
ErrorKind::BadInclude => "could not render include",
ErrorKind::EvalBlock => "could not render block",
ErrorKind::CannotUnpack => "cannot unpack",
ErrorKind::WriteFailure => "failed to write output",
#[cfg(feature = "deserialization")]
ErrorKind::CannotDeserialize => "cannot deserialize",
#[cfg(feature = "fuel")]
ErrorKind::OutOfFuel => "engine ran out of fuel",
#[cfg(feature = "custom_syntax")]
ErrorKind::InvalidDelimiter => "invalid custom delimiters",
#[cfg(feature = "multi_template")]
ErrorKind::UnknownBlock => "unknown block",
}
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description())
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ref detail) = self.repr.detail {
ok!(write!(f, "{}: {}", self.kind(), detail));
} else {
ok!(write!(f, "{}", self.kind()));
}
if let Some(ref filename) = self.name() {
ok!(write!(f, " (in {}:{})", filename, self.line().unwrap_or(0)))
}
#[cfg(feature = "debug")]
{
if f.alternate() && self.debug_info().is_some() {
ok!(write!(f, "{}", self.display_debug_info()));
}
}
Ok(())
}
}
impl Error {
/// Creates a new error with kind and detail.
pub fn new<D: Into<Cow<'static, str>>>(kind: ErrorKind, detail: D) -> Error {
Error {
repr: Box::new(ErrorRepr {
kind,
detail: Some(detail.into()),
name: None,
lineno: 0,
span: None,
source: None,
#[cfg(feature = "debug")]
debug_info: None,
}),
}
}
pub(crate) fn internal_clone(&self) -> Error {
Error {
repr: self.repr.clone(),
}
}
pub(crate) fn set_filename_and_line(&mut self, filename: &str, lineno: usize) {
self.repr.name = Some(filename.into());
self.repr.lineno = lineno;
}
pub(crate) fn set_filename_and_span(&mut self, filename: &str, span: Span) {
self.repr.name = Some(filename.into());
self.repr.span = Some(span);
self.repr.lineno = span.start_line as usize;
}
pub(crate) fn new_not_found(name: &str) -> Error {
Error::new(
ErrorKind::TemplateNotFound,
format!("template {name:?} does not exist"),
)
}
/// Attaches another error as source to this error.
pub fn with_source<E: std::error::Error + Send + Sync + 'static>(mut self, source: E) -> Self {
self.repr.source = Some(Arc::new(source));
self
}
/// Returns the error kind
pub fn kind(&self) -> ErrorKind {
self.repr.kind
}
/// Returns the error detail
///
/// The detail is an error message that provides further details about
/// the error kind.
pub fn detail(&self) -> Option<&str> {
self.repr.detail.as_deref()
}
/// Overrides the detail.
pub(crate) fn set_detail<D: Into<Cow<'static, str>>>(&mut self, d: D) {
self.repr.detail = Some(d.into());
}
/// Returns the filename of the template that caused the error.
pub fn name(&self) -> Option<&str> {
self.repr.name.as_deref()
}
/// Returns the line number where the error occurred.
pub fn line(&self) -> Option<usize> {
if self.repr.lineno > 0 {
Some(self.repr.lineno)
} else {
None
}
}
/// Returns the byte range of where the error occurred if available.
///
/// In combination with [`template_source`](Self::template_source) this can be
/// used to better visualize where the error is coming from. By indexing into
/// the template source one ends up with the source of the failing expression.
///
/// Note that debug mode ([`Environment::set_debug`](crate::Environment::set_debug))
/// needs to be enabled, and the `debug` feature must be turned on. The engine
/// usually keeps track of spans in all cases, but there is no absolute guarantee
/// that it is able to provide a range in all error cases.
///
/// ```
/// # use minijinja::{Error, Environment, context};
/// # let mut env = Environment::new();
/// # env.set_debug(true);
/// let tmpl = env.template_from_str("Hello {{ foo + bar }}!").unwrap();
/// let err = tmpl.render(context!(foo => "a string", bar => 0)).unwrap_err();
/// let src = err.template_source().unwrap();
/// assert_eq!(&src[err.range().unwrap()], "foo + bar");
/// ```
#[cfg(feature = "debug")]
#[cfg_attr(docsrs, doc(cfg(feature = "debug")))]
pub fn range(&self) -> Option<std::ops::Range<usize>> {
self.repr
.span
.map(|x| x.start_offset as usize..x.end_offset as usize)
}
/// Helper function that renders all known debug info on format.
///
/// This method returns an object that when formatted prints out the debug information
/// that is contained on that error. Normally this is automatically rendered when the
/// error is displayed but in some cases you might want to decide for yourself when and
/// how to display that information.
#[cfg(feature = "debug")]
#[cfg_attr(docsrs, doc(cfg(feature = "debug")))]
pub fn display_debug_info(&self) -> impl fmt::Display + '_ {
struct Proxy<'a>(&'a Error);
impl fmt::Display for Proxy<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(info) = self.0.debug_info() {
crate::debug::render_debug_info(
f,
self.0.name(),
self.0.kind(),
self.0.line(),
self.0.span(),
info,
)
} else {
Ok(())
}
}
}
Proxy(self)
}
/// Returns the template source if available.
#[cfg(feature = "debug")]
#[cfg_attr(docsrs, doc(cfg(feature = "debug")))]
pub fn template_source(&self) -> Option<&str> {
self.debug_info().and_then(|x| x.source())
}
/// Returns the line number where the error occurred.
#[cfg(feature = "debug")]
pub(crate) fn span(&self) -> Option<Span> {
self.repr.span
}
/// Returns the template debug information is available.
///
/// The debug info snapshot is only embedded into the error if the debug
/// mode is enabled on the environment
/// ([`Environment::set_debug`](crate::Environment::set_debug)).
#[cfg(feature = "debug")]
pub(crate) fn debug_info(&self) -> Option<&crate::debug::DebugInfo> {
self.repr.debug_info.as_deref()
}
#[cfg(feature = "debug")]
#[cfg_attr(docsrs, doc(cfg(feature = "debug")))]
pub(crate) fn attach_debug_info(&mut self, value: crate::debug::DebugInfo) {
self.repr.debug_info = Some(Arc::new(value));
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.repr.source.as_ref().map(|err| err.as_ref() as _)
}
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Self {
Error {
repr: Box::new(ErrorRepr {
kind,
detail: None,
name: None,
lineno: 0,
span: None,
source: None,
#[cfg(feature = "debug")]
debug_info: None,
}),
}
}
}
impl From<fmt::Error> for Error {
fn from(_: fmt::Error) -> Self {
Error::new(ErrorKind::WriteFailure, "formatting failed")
}
}
pub fn attach_basic_debug_info<T>(rv: Result<T, Error>, source: &str) -> Result<T, Error> {
#[cfg(feature = "debug")]
{
match rv {
Ok(rv) => Ok(rv),
Err(mut err) => {
err.repr.debug_info = Some(Arc::new(crate::debug::DebugInfo {
template_source: Some(source.to_string()),
..Default::default()
}));
Err(err)
}
}
}
#[cfg(not(feature = "debug"))]
{
let _source = source;
rv
}
}
+119
View File
@@ -0,0 +1,119 @@
use std::collections::{BTreeMap, HashSet};
use std::fmt;
use serde::Serialize;
use crate::compiler::ast;
use crate::compiler::instructions::Instructions;
use crate::compiler::meta::find_undeclared;
use crate::compiler::parser::parse_expr;
use crate::environment::Environment;
use crate::error::Error;
use crate::output::Output;
use crate::value::Value;
use crate::vm::Vm;
/// A handle to a compiled expression.
///
/// An expression is created via the
/// [`compile_expression`](Environment::compile_expression) method. It provides
/// a method to evaluate the expression and return the result as value object.
/// This for instance can be used to evaluate simple expressions from user
/// provided input to implement features such as dynamic filtering.
///
/// This is usually best paired with [`context`](crate::context!) to pass
/// a single value to it.
///
/// # Example
///
/// ```rust
/// # use minijinja::{Environment, context};
/// let env = Environment::new();
/// let expr = env.compile_expression("number > 10 and number < 20").unwrap();
/// let rv = expr.eval(context!(number => 15)).unwrap();
/// assert!(rv.is_true());
/// ```
pub struct Expression<'env, 'source> {
env: &'env Environment<'source>,
instr: ExpressionBacking<'source>,
}
enum ExpressionBacking<'source> {
Borrowed(Instructions<'source>),
Owned(crate::loader::OwnedInstructions),
}
impl fmt::Debug for Expression<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Expression")
.field("env", &self.env)
.finish()
}
}
impl<'env, 'source> Expression<'env, 'source> {
pub(crate) fn new(
env: &'env Environment<'source>,
instructions: Instructions<'source>,
) -> Expression<'env, 'source> {
Expression {
env,
instr: ExpressionBacking::Borrowed(instructions),
}
}
pub(crate) fn new_owned(
env: &'env Environment<'source>,
instructions: crate::loader::OwnedInstructions,
) -> Expression<'env, 'source> {
Expression {
env,
instr: ExpressionBacking::Owned(instructions),
}
}
fn instructions(&self) -> &Instructions<'_> {
match self.instr {
ExpressionBacking::Borrowed(ref x) => x,
ExpressionBacking::Owned(ref x) => x.borrow_dependent(),
}
}
/// Evaluates the expression with some context.
///
/// The result of the expression is returned as [`Value`].
pub fn eval<S: Serialize>(&self, ctx: S) -> Result<Value, Error> {
// reduce total amount of code falling under mono morphization into
// this function, and share the rest in _eval.
self._eval(Value::from_serialize(&ctx))
}
/// Returns a set of all undeclared variables in the expression.
///
/// This works the same as
/// [`Template::undeclared_variables`](crate::Template::undeclared_variables).
pub fn undeclared_variables(&self, nested: bool) -> HashSet<String> {
match parse_expr(self.instructions().source()) {
Ok(expr) => find_undeclared(
&ast::Stmt::EmitExpr(ast::Spanned::new(
ast::EmitExpr { expr },
Default::default(),
)),
nested,
),
Err(_) => HashSet::new(),
}
}
fn _eval(&self, root: Value) -> Result<Value, Error> {
Ok(ok!(Vm::new(self.env).eval(
self.instructions(),
root,
&BTreeMap::new(),
&mut Output::null(),
crate::AutoEscape::None,
))
.0
.expect("expression evaluation did not leave value on stack"))
}
}
+1738
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+480
View File
@@ -0,0 +1,480 @@
//! Global functions and abstractions.
//!
//! This module provides the abstractions for functions that can registered as
//! global functions to the environment via
//! [`add_function`](crate::Environment::add_function).
//!
//! # Using Functions
//!
//! Functions can be called in any place where an expression is valid. They
//! are useful to retrieve data. Some functions are special and provided
//! by the engine (like `super`) within certain context, others are global.
//!
//! The following is a motivating example:
//!
//! ```jinja
//! <pre>{{ debug() }}</pre>
//! ```
//!
//! # Custom Functions
//!
//! A custom global function is just a simple rust function which accepts optional
//! arguments and then returns a result. Global functions are typically used to
//! perform a data loading operation. For instance these functions can be used
//! to expose data to the template that hasn't been provided by the individual
//! render invocation.
//!
//! ```rust
//! # use minijinja::Environment;
//! # let mut env = Environment::new();
//! use minijinja::{Error, ErrorKind};
//!
//! fn include_file(name: String) -> Result<String, Error> {
//! std::fs::read_to_string(&name)
//! .map_err(|e| Error::new(
//! ErrorKind::InvalidOperation,
//! "cannot load file"
//! ).with_source(e))
//! }
//!
//! env.add_function("include_file", include_file);
//! ```
//!
#![cfg_attr(
feature = "deserialization",
doc = r#"
# Arguments in Custom Functions
All arguments in custom functions must implement the [`ArgType`] trait.
Standard types, such as `String`, `i32`, `bool`, `f64`, etc, already implement this trait.
There are also helper types that will make it easier to extract an arguments with custom types.
The [`ViaDeserialize<T>`](crate::value::ViaDeserialize) type, for instance, can accept any
type `T` that implements the `Deserialize` trait from `serde`.
```rust
# use minijinja::Environment;
# use serde::Deserialize;
# let mut env = Environment::new();
use minijinja::value::ViaDeserialize;
#[derive(Deserialize)]
struct Person {
name: String,
age: i32,
}
fn is_adult(person: ViaDeserialize<Person>) -> bool {
person.age >= 18
}
env.add_function("is_adult", is_adult);
```
"#
)]
//!
//! # Note on Keyword Arguments
//!
//! MiniJinja inherits a lot of the runtime model from Jinja2. That includes support for
//! keyword arguments. These however are a concept not native to Rust which makes them
//! somewhat uncomfortable to work with. In MiniJinja keyword arguments are implemented by
//! converting them into an extra parameter represented by a map. That means if you call
//! a function as `foo(1, 2, three=3, four=4)` the function gets three arguments:
//!
//! ```json
//! [1, 2, {"three": 3, "four": 4}]
//! ```
//!
//! If a function wants to disambiguate between a value passed as keyword argument or not,
//! the [`Value::is_kwargs`] can be used which returns `true` if a value represents
//! keyword arguments as opposed to just a map. A more convenient way to work with keyword
//! arguments is the [`Kwargs`](crate::value::Kwargs) type.
//!
//! # Built-in Functions
//!
//! When the `builtins` feature is enabled a range of built-in functions are
//! automatically added to the environment. These are also all provided in
//! this module. Note though that these functions are not to be
//! called from Rust code as their exact interface (arguments and return types)
//! might change from one MiniJinja version to another.
use std::fmt;
use std::sync::Arc;
use crate::error::Error;
use crate::utils::SealedMarker;
use crate::value::{ArgType, FunctionArgs, FunctionResult, Object, ObjectRepr, Value};
use crate::vm::State;
type FuncFunc = dyn Fn(&State, &[Value]) -> Result<Value, Error> + Sync + Send + 'static;
/// A boxed function.
#[derive(Clone)]
pub(crate) struct BoxedFunction(Arc<FuncFunc>, #[cfg(feature = "debug")] &'static str);
/// A utility trait that represents global functions.
///
/// This trait is used by the [`add_function`](crate::Environment::add_function)
/// method to abstract over different types of functions.
///
/// Functions which at the very least accept the [`State`] by reference as first
/// parameter and additionally up to 4 further parameters. They share much of
/// their interface with [`filters`](crate::filters).
///
/// A function can return any of the following types:
///
/// * `Rv` where `Rv` implements `Into<Value>`
/// * `Result<Rv, Error>` where `Rv` implements `Into<Value>`
///
/// The parameters can be marked optional by using `Option<T>`. The last
/// argument can also use [`Rest<T>`](crate::value::Rest) to capture the
/// remaining arguments. All types are supported for which
/// [`ArgType`] is implemented.
///
/// For a list of built-in functions see [`functions`](crate::functions).
///
/// **Note:** this trait cannot be implemented and only exists drive the
/// functionality of [`add_function`](crate::Environment::add_function)
/// and [`from_function`](crate::value::Value::from_function). If you want
/// to implement a custom callable, you can directly implement
/// [`Object::call`] which is what the engine actually uses internally.
///
/// This trait is also used for [`filters`](crate::filters) and
/// [`tests`](crate::tests).
///
/// # Basic Example
///
/// ```rust
/// # use minijinja::Environment;
/// # let mut env = Environment::new();
/// use minijinja::{Error, ErrorKind};
///
/// fn include_file(name: String) -> Result<String, Error> {
/// std::fs::read_to_string(&name)
/// .map_err(|e| Error::new(
/// ErrorKind::InvalidOperation,
/// "cannot load file"
/// ).with_source(e))
/// }
///
/// env.add_function("include_file", include_file);
/// ```
///
/// ```jinja
/// {{ include_file("filename.txt") }}
/// ```
///
/// # Variadic
///
/// ```
/// # use minijinja::Environment;
/// # let mut env = Environment::new();
/// use minijinja::value::Rest;
///
/// fn sum(values: Rest<i64>) -> i64 {
/// values.iter().sum()
/// }
///
/// env.add_function("sum", sum);
/// ```
///
/// ```jinja
/// {{ sum(1, 2, 3) }} -> 6
/// ```
///
/// # Optional Arguments
///
/// ```
/// # use minijinja::Environment;
/// # let mut env = Environment::new();
/// fn substr(value: String, start: u32, end: Option<u32>) -> String {
/// let end = end.unwrap_or(value.len() as _);
/// value.get(start as usize..end as usize).unwrap_or_default().into()
/// }
///
/// env.add_filter("substr", substr);
/// ```
///
/// ```jinja
/// {{ "Foo Bar Baz"|substr(4) }} -> Bar Baz
/// {{ "Foo Bar Baz"|substr(4, 7) }} -> Bar
/// ```
pub trait Function<Rv, Args: for<'a> FunctionArgs<'a>>: Send + Sync + 'static {
/// Calls a function with the given arguments.
#[doc(hidden)]
fn invoke(&self, args: <Args as FunctionArgs<'_>>::Output, _: SealedMarker) -> Rv;
}
// This is necessary to avoid a bug in the trait solver. See
// https://github.com/mitsuhiko/minijinja/pull/787 for more details.
trait FunctionHelper<Rv, Args> {
fn invoke_nested(&self, args: Args) -> Rv;
}
macro_rules! tuple_impls {
( $( $name:ident )* ) => {
impl<Func, Rv, $($name),*> FunctionHelper<Rv, ($($name,)*)> for Func
where
Func: Fn($($name),*) -> Rv
{
fn invoke_nested(&self, args: ($($name,)*)) -> Rv {
#[allow(non_snake_case)]
let ($($name,)*) = args;
(self)($($name,)*)
}
}
impl<Func, Rv, $($name),*> Function<Rv, ($($name,)*)> for Func
where
Func: Send + Sync + 'static,
// the crazy bounds here exist to enable borrowing in closures
Func: Fn($($name),*) -> Rv + for<'a> FunctionHelper<Rv, ($(<$name as ArgType<'a>>::Output,)*)>,
Rv: FunctionResult,
$($name: for<'a> ArgType<'a>,)*
{
// Need to allow this lint for the one-element tuple case.
#[allow(clippy::needless_lifetimes)]
fn invoke<'a>(&self, args: ($(<$name as ArgType<'a>>::Output,)*), _: SealedMarker) -> Rv {
self.invoke_nested(args)
}
}
};
}
tuple_impls! {}
tuple_impls! { A }
tuple_impls! { A B }
tuple_impls! { A B C }
tuple_impls! { A B C D }
tuple_impls! { A B C D E }
impl BoxedFunction {
/// Creates a new boxed filter.
pub fn new<F, Rv, Args>(f: F) -> BoxedFunction
where
F: Function<Rv, Args>,
Rv: FunctionResult,
Args: for<'a> FunctionArgs<'a>,
{
BoxedFunction(
Arc::new(move |state, args| -> Result<Value, Error> {
f.invoke(ok!(Args::from_values(Some(state), args)), SealedMarker)
.into_result()
}),
#[cfg(feature = "debug")]
std::any::type_name::<F>(),
)
}
/// Invokes the function.
pub fn invoke(&self, state: &State, args: &[Value]) -> Result<Value, Error> {
(self.0)(state, args)
}
/// Creates a value from a boxed function.
pub fn to_value(&self) -> Value {
Value::from_object(self.clone())
}
}
impl fmt::Debug for BoxedFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(feature = "debug")]
{
if !self.1.is_empty() {
return f.write_str(self.1);
}
}
f.write_str("function")
}
}
impl Object for BoxedFunction {
fn repr(self: &Arc<Self>) -> ObjectRepr {
ObjectRepr::Plain
}
fn call(self: &Arc<Self>, state: &State, args: &[Value]) -> Result<Value, Error> {
self.invoke(state, args)
}
}
#[cfg(feature = "builtins")]
mod builtins {
use std::cmp::Ordering;
use super::*;
use crate::error::ErrorKind;
use crate::value::{Rest, ValueMap, ValueRepr};
/// Returns a range.
///
/// Return a list containing an arithmetic progression of integers. `range(i,
/// j)` returns `[i, i+1, i+2, ..., j-1]`. `lower` defaults to 0. When `step` is
/// given, it specifies the increment (or decrement). For example, `range(4)`
/// and `range(0, 4, 1)` return `[0, 1, 2, 3]`. The end point is omitted.
///
/// ```jinja
/// <ul>
/// {% for num in range(1, 11) %}
/// <li>{{ num }}
/// {% endfor %}
/// </ul>
/// ```
///
/// This function will refuse to create ranges over 10.000 items.
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn range(lower: isize, upper: Option<isize>, step: Option<isize>) -> Result<Value, Error> {
fn to_result<I: ExactSizeIterator<Item = isize> + Send + Sync + Clone + 'static>(
i: I,
) -> Result<Value, Error> {
if i.len() > 100000 {
Err(Error::new(
ErrorKind::InvalidOperation,
"range has too many elements",
))
} else {
Ok(Value::make_iterable(move || i.clone()))
}
}
let rng = match upper {
Some(upper) => lower..upper,
None => 0..lower,
};
let Some(step) = step else {
return to_result(rng);
};
match step.cmp(&0) {
Ordering::Equal => Err(Error::new(
ErrorKind::InvalidOperation,
"cannot create range with step of 0",
)),
Ordering::Greater => to_result(rng.step_by(step as usize)),
Ordering::Less => {
// handle negative steps
debug_assert!(step < 0);
let (start, end) = match upper {
Some(upper) => (lower, upper),
None => (0, lower),
};
let len = if start <= end {
0
} else {
((start - end + (-step) - 1) / (-step)) as usize
};
let iter = (0..len).map(move |i| start + (i as isize) * step);
to_result(iter)
}
}
}
/// Creates a dictionary.
///
/// This is a convenient alternative for a dictionary literal.
/// `{"foo": "bar"}` is the same as `dict(foo="bar")`.
///
/// ```jinja
/// <script>const CONFIG = {{ dict(
/// DEBUG=true,
/// API_URL_PREFIX="/api"
/// )|tojson }};</script>
/// ```
///
/// Additionally this can be used to merge objects by passing extra keyword
/// arguments:
///
/// ```jinja
/// {% set new_dict = dict(old_dict, extra_value=2) %}
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn dict(value: Option<Value>, update_with: crate::value::Kwargs) -> Result<Value, Error> {
let mut rv = match value {
None => ValueMap::default(),
Some(value) => match value.0 {
ValueRepr::Undefined(_) => ValueMap::default(),
ValueRepr::Object(obj) if obj.repr() == ObjectRepr::Map => {
obj.try_iter_pairs().into_iter().flatten().collect()
}
_ => return Err(Error::from(ErrorKind::InvalidOperation)),
},
};
if update_with.values.is_true() {
rv.extend(
update_with
.values
.iter()
.map(|(k, v)| (k.clone(), v.clone())),
);
}
Ok(Value::from_object(rv))
}
/// Outputs the current context or the arguments stringified.
///
/// This is a useful function to quickly figure out the state of affairs
/// in a template. It emits a stringified debug dump of the current
/// engine state including the layers of the context, the current block
/// and auto escaping setting. The exact output is not defined and might
/// change from one version of Jinja2 to the next.
///
/// ```jinja
/// <pre>{{ debug() }}</pre>
/// <pre>{{ debug(variable1, variable2) }}</pre>
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn debug(state: &State, args: Rest<Value>) -> String {
if args.is_empty() {
format!("{state:#?}")
} else if args.len() == 1 {
format!("{:#?}", args.0[0])
} else {
format!("{:#?}", &args.0[..])
}
}
/// Creates a new container that allows attribute assignment using the `{% set %}` tag.
///
/// ```jinja
/// {% set ns = namespace() %}
/// {% set ns.foo = 'bar' %}
/// ```
///
/// The main purpose of this is to allow carrying a value from within a loop body
/// to an outer scope. Initial values can be provided as a dict, as keyword arguments,
/// or both (same behavior as [`dict`]).
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn namespace(defaults: Option<Value>) -> Result<Value, Error> {
let ns = crate::value::namespace_object::Namespace::default();
if let Some(defaults) = defaults {
if let Some(pairs) = defaults
.as_object()
.filter(|x| matches!(x.repr(), ObjectRepr::Map))
.and_then(|x| x.try_iter_pairs())
{
for (key, value) in pairs {
if let Some(key) = key.as_str() {
ns.set_value(key, value);
}
}
} else {
return Err(Error::new(
ErrorKind::InvalidOperation,
format!(
"expected object or keyword arguments, got {}",
defaults.kind()
),
));
}
}
Ok(Value::from_object(ns))
}
}
#[cfg(feature = "builtins")]
pub use self::builtins::*;
+284
View File
@@ -0,0 +1,284 @@
//! <div align=center>
//! <img src="https://github.com/mitsuhiko/minijinja/raw/main/artwork/logo.png" alt="" width=320>
//! <p><strong>MiniJinja: a powerful template engine for Rust with minimal dependencies</strong></p>
//! </div>
//!
//! MiniJinja is a powerful but minimal dependency template engine for Rust which
//! is based on the syntax and behavior of the
//! [Jinja2](https://jinja.palletsprojects.com/) template engine for Python. It's
//! implemented on top of [`serde`]. The goal is to be able to render a large
//! chunk of the Jinja2 template ecosystem from Rust with a minimal engine and to
//! leverage an already existing ecosystem of editor integrations.
//!
//! ```jinja
//! {% for user in users %}
//! <li>{{ user.name }}</li>
//! {% endfor %}
//! ```
//!
//! You can play with MiniJinja online [in the browser
//! playground](https://mitsuhiko.github.io/minijinja-playground/) powered by a
//! WASM build of MiniJinja.
//!
//! # Why MiniJinja
//!
//! MiniJinja by its name wants to be a good default choice if you need a little
//! bit of templating with minimal dependencies. It has the following goals:
//!
//! * Well documented, compact API
//! * Minimal dependencies, reasonable compile times and decent runtime performance
//! * Stay close as possible to Jinja2
//! * Support for expression evaluation
//! * Support for all `serde` compatible types
//! * Excellent test coverage
//! * Support for dynamic runtime objects with methods and dynamic attributes
//!
//! # Template Usage
//!
//! To use MiniJinja one needs to create an [`Environment`] and populate it with
//! templates. Afterwards templates can be loaded and rendered. To pass data
//! one can pass any serde serializable value. The [`context!`] macro can be
//! used to quickly construct a template context:
//!
//! ```
//! use minijinja::{Environment, context};
//!
//! let mut env = Environment::new();
//! env.add_template("hello", "Hello {{ name }}!").unwrap();
//! let tmpl = env.get_template("hello").unwrap();
//! println!("{}", tmpl.render(context!(name => "John")).unwrap());
//! ```
//!
//! ```plain
//! Hello John!
//! ```
//!
//! For super trivial cases where you need to render a string once, you can
//! also use the [`render!`] macro which acts a bit like a replacement
//! for the [`format!`] macro.
//!
//! # Expression Usage
//!
//! MiniJinja — like Jinja2 — allows to be used as expression language. This can be
//! useful to express logic in configuration files or similar things. For this
//! purpose the [`Environment::compile_expression`] method can be used. It returns
//! an expression object that can then be evaluated, returning the result:
//!
//! ```
//! use minijinja::{Environment, context};
//!
//! let env = Environment::new();
//! let expr = env.compile_expression("number < 42").unwrap();
//! let result = expr.eval(context!(number => 23)).unwrap();
//! assert_eq!(result.is_true(), true);
//! ```
//!
//! This becomes particularly powerful when [dynamic objects](crate::value::Object) are
//! exposed to templates.
//!
//! # Custom Filters
//!
//! MiniJinja lets you register functions as filter functions (see
//! [`Function`](crate::functions::Function)) with the engine. These can then be
//! invoked directly from the template:
//!
//! ```
//! use minijinja::{Environment, context};
//!
//! let mut env = Environment::new();
//! env.add_filter("repeat", str::repeat);
//! env.add_template("hello", "{{ 'Na '|repeat(3) }} {{ name }}!").unwrap();
//! let tmpl = env.get_template("hello").unwrap();
//! println!("{}", tmpl.render(context!(name => "Batman")).unwrap());
//! ```
//!
//! ```plain
//! Na Na Na Batman!
//! ```
//!
//! # Learn more
//!
//! - [`Environment`]: the main API entry point. Teaches you how to configure the environment.
//! - [`Template`]: the template object API. Shows you how templates can be rendered.
//! - [`syntax`]: provides documentation of the template engine syntax.
//! - [`filters`]: teaches you how to write custom filters and to see the list of built-in filters.
//! - [`tests`]: teaches you how to write custom test functions and to see the list of built-in tests.
//! - [`functions`]: teaches how to write custom functions and to see the list of built-in functions.
//! - For auto reloading use the [`minijinja-autoreload`](https://docs.rs/minijinja-autoreload) crate.
//! - For simpler embedding of templates use the [`minijinja-embed`](https://docs.rs/minijinja-embed) crate.
//!
//! Additionally there is an [list of examples](https://github.com/mitsuhiko/minijinja/tree/main/examples)
//! with many different small example programs on GitHub to explore.
//!
//! # Error Handling
//!
//! MiniJinja tries to give you good errors out of the box. However if you use includes or
//! template inheritance your experience will improve greatly if you ensure to render chained
//! errors. For more information see [`Error`] with an example.
//!
//! # Size and Compile Times
//!
//! MiniJinja attempts to compile fast so it can be used as a sensible template engine choice
//! when compile times matter. Because of this it's internally modular so unnecessary bits and
//! pieces can be removed. The default set of features tries to strike a balance but in
//! situations where only a subset is needed (eg: `build.rs`) all default features can be
//! be disabled.
//!
//! # Optional Features
//!
//! MiniJinja comes with a lot of optional features, some of which are turned on by
//! default. If you plan on using MiniJinja in a library, please consider turning
//! off all default features and to opt-in explicitly into the ones you actually
//! need.
//!
//! <details><summary><strong style="cursor: pointer">Configurable Features</strong></summary>
//!
//! To cut down on size of the engine some default functionality can be removed:
//!
//! - **Engine Features:**
//!
//! - `builtins`: if this feature is removed the default filters, tests and
//! functions are not implemented.
//! - `macros`: when removed the `{% macro %}` tag is not included.
//! - `multi_template`: when removed the templates related to imports and extends
//! are removed (`{% from %}`, `{% import %}`, `{% include %}`, and `{% extends %}`
//! as well as `{% block %}`).
//! - `adjacent_loop_items`: when removed the `previtem` and `nextitem` attributes of
//! the `loop` object no longer exist. Removing this feature can provide faster
//! template execution when a lot of loops are involved.
//! - `unicode`: when added unicode identifiers are supported and the `sort`
//! filter's case insensitive comparison changes to using unicode and not
//! ASCII rules. Without this features only ASCII identifiers can be used
//! for variable names and attributes.
//! - `serde`: enables or disables serde support. In current versions of MiniJinja
//! it's not possible to disable serde but it will become possible. To prevent
//! breakage, MiniJinja warns if this feature is disabled.
//!
//! - **Rust Functionality:**
//!
//! - `debug`: if this feature is removed some debug functionality of the engine is
//! removed as well. This mainly affects the quality of error reporting.
//! - `deserialization`: when removed this disables deserialization support for
//! the [`Value`] type, removes the `ViaDeserialize` type and the error type
//! no longer implements `serde::de::Error`.
//! - `std_collections`: if this feature is removed some [`Object`](crate::value::Object)
//! implementations for standard library collections are removed. Only the
//! ones needed for the engine to function itself are retained.
//!
//! There are some additional features that provide extra functionality:
//!
//! - `fuel`: enables the `fuel` feature which makes the engine track fuel consumption which
//! can be used to better protect against expensive templates.
//! - `loader`: retained for backwards compatibility and now a no-op.
//! - `custom_syntax`: when this feature is enabled, custom delimiters are supported by
//! the parser.
//! - `preserve_order`: When enable the internal value implementation uses an indexmap
//! which preserves the original order of maps and structs.
//! - `json`: When enabled the `tojson` filter is added as builtin filter as well as
//! the ability to auto escape via `AutoEscape::Json`.
//! - `urlencode`: When enabled the `urlencode` filter is added as builtin filter.
//! - `loop_controls`: enables the `{% break %}` and `{% continue %}` loop control flow
//! tags.
//!
//! Performance and memory related features:
//!
//! - `stacker`: enables automatic stack growth which permits much larger levels of recursion
//! at runtime. This does not affect the maximum recursion at parsing time which is always
//! limited.
//! - `speedups`: enables all speedups, in particular it turns on the `v_htmlescape` dependency
//! for faster HTML escaping.
//!
//! Internals:
//!
//! - `unstable_machinery`: exposes an unstable internal API (no semver guarantees) to parse
//! templates and interact with the engine internals. If you need this functionality, please
//! leave some feedback on GitHub.
//!
//! </details>
#![allow(clippy::cognitive_complexity)]
#![allow(clippy::get_first)]
#![allow(clippy::default_constructed_unit_structs)]
#![allow(clippy::needless_borrowed_reference)]
#![allow(clippy::vec_init_then_push)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
#![doc(html_logo_url = "https://github.com/mitsuhiko/minijinja/raw/main/artwork/logo-square.png")]
#[macro_use]
mod macros;
mod compiler;
mod defaults;
mod environment;
mod error;
mod expression;
#[cfg(feature = "builtins")]
mod format_utils;
mod output;
mod template;
mod utils;
mod vendor;
mod vm;
pub mod filters;
pub mod functions;
pub mod syntax;
pub mod tests;
pub mod value;
mod loader;
pub use loader::path_loader;
#[cfg(feature = "debug")]
mod debug;
pub use self::defaults::{default_auto_escape_callback, escape_formatter};
pub use self::environment::Environment;
pub use self::error::{Error, ErrorKind};
pub use self::expression::Expression;
pub use self::output::Output;
pub use self::template::{Captured, Template};
pub use self::utils::{AutoEscape, HtmlEscape, UndefinedBehavior};
#[cfg(feature = "builtins")]
pub use self::format_utils::{format_filter, FormatStyle};
/// Re-export for convenience.
pub use self::value::Value;
pub use self::macros::__context;
pub use self::vm::State;
/// This module gives access to the low level machinery.
///
/// This module is only provided by the `unstable_machinery` feature and does not
/// have a stable interface. It mostly exists for internal testing purposes and
/// for debugging.
#[cfg(feature = "unstable_machinery")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable_machinery")))]
pub mod machinery {
#![allow(missing_docs)]
pub use crate::compiler::ast;
pub use crate::compiler::codegen::CodeGenerator;
pub use crate::compiler::instructions::{Instruction, Instructions};
pub use crate::compiler::lexer::{tokenize, Tokenizer, WhitespaceConfig};
pub use crate::compiler::parser::{parse, parse_expr};
pub use crate::compiler::tokens::{Span, Token};
pub use crate::template::{CompiledTemplate, TemplateConfig};
pub use crate::vm::Vm;
use crate::Output;
/// Returns a reference to a [`CompiledTemplate`] from a [`Template`](crate::Template).
pub fn get_compiled_template<'x, 'env>(
tmpl: &'x crate::Template<'env, 'env>,
) -> &'x CompiledTemplate<'env> {
&tmpl.compiled
}
/// Creates an [`Output`] that writes into a string.
pub fn make_string_output(s: &mut String) -> Output<'_> {
Output::new(s)
}
}
+241
View File
@@ -0,0 +1,241 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use crate::vendor::self_cell::self_cell;
use memo_map::MemoMap;
use crate::compiler::instructions::Instructions;
use crate::error::{Error, ErrorKind};
use crate::template::CompiledTemplate;
use crate::template::TemplateConfig;
type LoadFunc = dyn for<'a> Fn(&'a str) -> Result<Option<String>, Error> + Send + Sync;
/// Internal utility for dynamic template loading.
///
/// Because an [`Environment`](crate::Environment) holds a reference to the
/// source lifetime it borrows templates from, it becomes very inconvenient when
/// it is shared. This object provides a solution for such cases. First templates
/// are loaded into the source to decouple the lifetimes from the environment.
#[derive(Clone)]
pub(crate) struct LoaderStore<'source> {
pub template_config: TemplateConfig,
loader: Option<Arc<LoadFunc>>,
owned_templates: MemoMap<Arc<str>, Arc<LoadedTemplate>>,
borrowed_templates: BTreeMap<&'source str, Arc<CompiledTemplate<'source>>>,
}
impl fmt::Debug for LoaderStore<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut l = f.debug_list();
for key in self.owned_templates.keys() {
l.entry(key);
}
for key in self.borrowed_templates.keys() {
if !self.owned_templates.contains_key(*key) {
l.entry(key);
}
}
l.finish()
}
}
self_cell! {
struct LoadedTemplate {
owner: (Arc<str>, Box<str>),
#[covariant]
dependent: CompiledTemplate,
}
}
self_cell! {
pub(crate) struct OwnedInstructions {
owner: Box<str>,
#[covariant]
dependent: Instructions,
}
}
impl fmt::Debug for LoadedTemplate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.borrow_dependent(), f)
}
}
impl<'source> LoaderStore<'source> {
pub fn new(template_config: TemplateConfig) -> LoaderStore<'source> {
LoaderStore {
template_config,
loader: None,
owned_templates: MemoMap::default(),
borrowed_templates: BTreeMap::default(),
}
}
pub fn insert(&mut self, name: &'source str, source: &'source str) -> Result<(), Error> {
self.insert_cow(Cow::Borrowed(name), Cow::Borrowed(source))
}
pub fn insert_cow(
&mut self,
name: Cow<'source, str>,
source: Cow<'source, str>,
) -> Result<(), Error> {
match (source, name) {
(Cow::Borrowed(source), Cow::Borrowed(name)) => {
self.owned_templates.remove(name);
self.borrowed_templates.insert(
name,
Arc::new(ok!(CompiledTemplate::new(
name,
source,
&self.template_config
))),
);
}
(source, name) => {
self.borrowed_templates.remove(&name as &str);
let name: Arc<str> = name.into();
self.owned_templates.replace(
name.clone(),
ok!(self.make_owned_template(name, source.to_string())),
);
}
}
Ok(())
}
pub fn remove(&mut self, name: &str) {
self.borrowed_templates.remove(name);
self.owned_templates.remove(name);
}
pub fn clear(&mut self) {
self.borrowed_templates.clear();
self.owned_templates.clear();
}
pub fn get(&self, name: &str) -> Result<&CompiledTemplate<'_>, Error> {
if let Some(rv) = self.borrowed_templates.get(name) {
Ok(&**rv)
} else {
let name: Arc<str> = name.into();
self.owned_templates
.get_or_try_insert(&name.clone(), || -> Result<_, Error> {
let loader_result = match self.loader {
Some(ref loader) => ok!(loader(&name)),
None => None,
}
.ok_or_else(|| Error::new_not_found(&name));
self.make_owned_template(name, ok!(loader_result))
})
.map(|x| x.borrow_dependent())
}
}
pub fn set_loader<F>(&mut self, f: F)
where
F: Fn(&str) -> Result<Option<String>, Error> + Send + Sync + 'static,
{
self.loader = Some(Arc::new(f));
}
fn make_owned_template(
&self,
name: Arc<str>,
source: String,
) -> Result<Arc<LoadedTemplate>, Error> {
LoadedTemplate::try_new(
(name, source.into_boxed_str()),
|(name, source)| -> Result<_, Error> {
CompiledTemplate::new(name, source, &self.template_config)
},
)
.map(Arc::new)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &CompiledTemplate<'_>)> {
let borrowed = self
.borrowed_templates
.iter()
.map(|(name, template)| (*name, &**template));
let owned = self
.owned_templates
.iter()
.map(|(name, template)| (&**name, template.borrow_dependent()));
borrowed.chain(owned)
}
}
/// Safely joins two paths.
pub fn safe_join(base: &Path, template: &str) -> Option<PathBuf> {
let mut rv = base.to_path_buf();
for segment in template.split('/') {
if segment.starts_with('.') || segment.contains('\\') {
return None;
}
rv.push(segment);
}
Some(rv)
}
/// Helper to load templates from a given directory.
///
/// This creates a dynamic loader which looks up templates in the
/// given directory. Templates that start with a dot (`.`) or are contained in
/// a folder starting with a dot cannot be loaded.
///
/// # Example
///
/// ```rust
/// # use minijinja::{path_loader, Environment};
/// fn create_env() -> Environment<'static> {
/// let mut env = Environment::new();
/// env.set_loader(path_loader("path/to/templates"));
/// env
/// }
/// ```
pub fn path_loader<'x, P: AsRef<Path> + 'x>(
dir: P,
) -> impl for<'a> Fn(&'a str) -> Result<Option<String>, Error> + Send + Sync + 'static {
let dir = dir.as_ref().to_path_buf();
move |name| {
let Some(path) = safe_join(&dir, name) else {
return Ok(None);
};
match fs::read_to_string(path) {
Ok(result) => Ok(Some(result)),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(
Error::new(ErrorKind::InvalidOperation, "could not read template").with_source(err),
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use similar_asserts::assert_eq;
#[test]
fn test_safe_join() {
assert_eq!(
safe_join(Path::new("foo"), "bar/baz"),
Some(PathBuf::from("foo").join("bar").join("baz"))
);
assert_eq!(safe_join(Path::new("foo"), ".bar/baz"), None);
assert_eq!(safe_join(Path::new("foo"), "bar/.baz"), None);
assert_eq!(safe_join(Path::new("foo"), "bar/../baz"), None);
}
}
+326
View File
@@ -0,0 +1,326 @@
// `ok!` and `some!` are less bloaty alternatives to the standard library's try operator (`?`).
// Since we do not need type conversions in this crate we can fall back to much easier match
// patterns that compile faster and produce less bloaty code.
macro_rules! ok {
($expr:expr) => {
match $expr {
Ok(val) => val,
Err(err) => return Err(err),
}
};
}
macro_rules! some {
($expr:expr) => {
match $expr {
Some(val) => val,
None => return None,
}
};
}
/// Hidden utility module for the [`context!`](crate::context!) macro.
#[doc(hidden)]
pub mod __context {
use crate::value::Value;
use crate::Environment;
use std::collections::BTreeMap;
use std::rc::Rc;
#[inline(always)]
pub fn make() -> BTreeMap<&'static str, Value> {
BTreeMap::default()
}
#[inline(always)]
pub fn add(ctx: &mut BTreeMap<&'static str, Value>, key: &'static str, value: Value) {
ctx.insert(key, value);
}
#[inline(always)]
pub fn build(ctx: BTreeMap<&'static str, Value>) -> Value {
Value::from_object(ctx)
}
pub fn thread_local_env() -> Rc<Environment<'static>> {
thread_local! {
static ENV: Rc<Environment<'static>> = Rc::new(Environment::new());
}
ENV.with(|x| x.clone())
}
}
/// Creates a template context from keys and values or merging in another value.
///
/// ```rust
/// # use minijinja::context;
/// let ctx = context!{
/// name => "Peter",
/// location => "World",
/// };
/// ```
///
/// Alternatively if the variable name matches the key name it can
/// be omitted:
///
/// ```rust
/// # use minijinja::context;
/// let name = "Peter";
/// let ctx = context!{ name };
/// ```
///
/// The return value is a [`Value`](crate::value::Value).
///
/// Note that [`context!`](crate::context!) can also be used recursively if you need to
/// create nested objects:
///
/// ```rust
/// # use minijinja::context;
/// let ctx = context! {
/// nav => vec![
/// context!(path => "/", title => "Index"),
/// context!(path => "/downloads", title => "Downloads"),
/// context!(path => "/faq", title => "FAQ"),
/// ]
/// };
/// ```
///
/// Additionally the macro supports a second syntax that can merge other
/// contexts or values. In that case one or more values need to be
/// passed with a leading `..` operator. This is useful to supply extra
/// values into render in a common place. The order of precedence is
/// left to right:
///
/// ```rust
/// # use minijinja::context;
/// let ctx = context! { a => "A" };
/// let ctx = context! { ..ctx, ..context! {
/// b => "B"
/// }};
///
/// // or
///
/// let ctx = context! {
/// a => "A",
/// ..context! {
/// b => "B"
/// }
/// };
/// ```
///
/// The merge works with an value, not just values created by the `context!`
/// macro and is performed lazy. This means it also works with dynamic
/// [`Object`](crate::value::Object)s. The merge uses the underlying
/// [`merge_maps`](crate::value::merge_maps) function.
///
/// # Note on Conversions
///
/// This macro uses [`Value::from_serialize`](crate::Value::from_serialize)
/// for conversions.
///
/// This macro currently does not move passed values. Future versions of
/// MiniJinja are going to change the move behavior and it's recommended to not
/// depend on this implicit reference behavior. You should thus pass values
/// with `&value` if you intend on still being able to reference them
/// after the macro invocation.
#[macro_export]
macro_rules! context {
() => {
$crate::__context::build($crate::__context::make())
};
(
$($key:ident $(=> $value:expr)?),*
$(, .. $ctx:expr),* $(,)?
) => {{
let mut ctx = $crate::__context::make();
$(
$crate::__context_pair!(ctx, $key $(=> $value)?);
)*
let ctx = $crate::__context::build(ctx);
let merge_ctx = [
$(
$crate::value::Value::from($ctx),
)*
];
if merge_ctx.is_empty() {
ctx
} else {
$crate::value::merge_maps(
merge_ctx.into_iter().rev().chain(::std::iter::once(ctx)))
}
}};
(
$(.. $ctx:expr),* $(,)?
) => {{
$crate::value::merge_maps([
$(
$crate::value::Value::from($ctx),
)*
].into_iter().rev())
}};
}
#[macro_export]
#[doc(hidden)]
macro_rules! __context_pair {
($ctx:ident, $key:ident) => {{
$crate::__context_pair!($ctx, $key => $key);
}};
($ctx:ident, $key:ident => $value:literal) => {
$crate::__context::add(&mut $ctx, stringify!($key), $crate::value::Value::from($value));
};
($ctx:ident, $key:ident => context! { $($inner:tt)* }) => {
$crate::__context::add(&mut $ctx, stringify!($key), $crate::context! { $($inner)* });
};
($ctx:ident, $key:ident => context!($($inner:tt)*)) => {
$crate::__context::add(&mut $ctx, stringify!($key), $crate::context!($($inner)*));
};
($ctx:ident, $key:ident => $value:expr) => {
$crate::__context::add(
&mut $ctx,
stringify!($key),
$crate::value::Value::from_serialize(&$value),
);
};
}
/// An utility macro to create arguments for function calls.
///
/// This creates a slice of values on the stack which can be
/// passed to [`call`](crate::value::Value::call),
/// [`call_method`](crate::value::Value::call_method),
/// [`apply_filter`](crate::State::apply_filter),
/// [`perform_test`](crate::State::perform_test) or similar
/// APIs that take slices of values.
///
/// It supports both positional and keyword arguments.
/// To mark a parameter as keyword argument define it as
/// `name => value`, otherwise just use `value`.
///
/// ```
/// # use minijinja::{value::Value, args, Environment};
/// # let env = Environment::default();
/// # let state = &env.empty_state();
/// # let value = Value::from(());
/// value.call(state, args!(1, 2, foo => "bar"));
/// ```
///
/// Note that this like [`context!`](crate::context) goes through
/// [`Value::from_serialize`](crate::value::Value::from_serialize).
#[macro_export]
macro_rules! args {
() => { &[][..] as &[$crate::value::Value] };
($($arg:tt)*) => { $crate::__args_helper!(branch [[$($arg)*]], [$($arg)*]) };
}
/// Utility macro for `args!`
#[macro_export]
#[doc(hidden)]
macro_rules! __args_helper {
// branch helper between `args` and `kwargs`.
//
// It analyzes the first bracket enclosed tt bundle to see if kwargs are
// used. If yes, it uses `kwargs` to handle the second tt
// bundle, otherwise it uses `args`.
(branch [[]], $args:tt) => { $crate::__args_helper!(args $args) };
(branch [[$n:ident => $e:expr]], $args:tt) => { $crate::__args_helper!(kwargs $args) };
(branch [[$n:ident => $e:expr, $($r:tt)*]], $args:tt) => { $crate::__args_helper!(kwargs $args) };
(branch [[$e:expr]], $args:tt) => { $crate::__args_helper!(args $args) };
(branch [[$e:expr, $($rest:tt)*]], $args:tt) => { $crate::__args_helper!(branch [[$($rest)*]], $args) };
// creates args on the stack
(args [$($arg:tt)*]) => {&{
let mut args = Vec::<$crate::value::Value>::new();
$crate::__args_helper!(peel args, args, false, [$($arg)*]);
args
}[..]};
// creates args with kwargs on the stack
(kwargs [$($arg:tt)*]) => {&{
let mut args = Vec::<$crate::value::Value>::new();
let mut kwargs = Vec::<(&str, $crate::value::Value)>::new();
$crate::__args_helper!(peel args, kwargs, false, [$($arg)*]);
args.push($crate::value::Kwargs::from_iter(kwargs.into_iter()).into());
args
}[..]};
// Peels a single argument from the arguments and stuffs them into
// `$args` or `$kwargs` depending on type.
(peel $args:ident, $kwargs:ident, $has_kwargs:ident, []) => {};
(peel $args:ident, $kwargs:ident, $has_kwargs:ident, [$name:ident => $expr:expr]) => {
$kwargs.push((stringify!($name), $crate::value::Value::from_serialize(&$expr)));
};
(peel $args:ident, $kwargs:ident, $has_kwargs:ident, [$name:ident => $expr:expr, $($rest:tt)*]) => {
$kwargs.push((stringify!($name), $crate::value::Value::from_serialize(&$expr)));
$crate::__args_helper!(peel $args, $kwargs, true, [$($rest)*]);
};
(peel $args:ident, $kwargs:ident, false, [$expr:expr]) => {
$args.push($crate::value::Value::from_serialize(&$expr));
};
(peel $args:ident, $kwargs:ident, false, [$expr:expr, $($rest:tt)*]) => {
$args.push($crate::value::Value::from_serialize(&$expr));
$crate::__args_helper!(peel $args, $kwargs, false, [$($rest)*]);
};
}
/// A macro similar to [`format!`] but that uses MiniJinja for rendering.
///
/// This can be used to quickly render a MiniJinja template into a string
/// without having to create an environment first which can be useful in
/// some situations. Note however that the template is re-parsed every
/// time the [`render!`](crate::render) macro is called which is potentially
/// slow.
///
/// There are two forms for this macro. The default form takes template
/// source and context variables, the extended form also lets you provide
/// a custom environment that should be used rather than a default one.
/// The context variables are passed the same way as with the
/// [`context!`](crate::context) macro.
///
/// # Example
///
/// Passing context explicitly:
///
/// ```
/// # use minijinja::render;
/// println!("{}", render!("Hello {{ name }}!", name => "World"));
/// ```
///
/// Passing variables with the default name:
///
/// ```
/// # use minijinja::render;
/// let name = "World";
/// println!("{}", render!("Hello {{ name }}!", name));
/// ```
///
/// Passing an explicit environment:
///
/// ```
/// # use minijinja::{Environment, render};
/// let env = Environment::new();
/// println!("{}", render!(in env, "Hello {{ name }}!", name => "World"));
/// ```
///
/// # Panics
///
/// This macro panics if the format string is an invalid template or the
/// template evaluation failed.
#[macro_export]
macro_rules! render {
(
in $env:expr,
$tmpl:expr
$(, $key:ident $(=> $value:expr)?)* $(,)?
) => {
($env).render_str($tmpl, $crate::context! { $($key $(=> $value)? ,)* })
.expect("failed to render expression")
};
(
$tmpl:expr
$(, $key:ident $(=> $value:expr)?)* $(,)?
) => {
$crate::render!(in $crate::__context::thread_local_env(), $tmpl, $($key $(=> $value)? ,)*)
}
}
+185
View File
@@ -0,0 +1,185 @@
use std::ptr::addr_of_mut;
use std::{fmt, io};
use crate::error::{Error, ErrorKind};
use crate::utils::AutoEscape;
use crate::value::Value;
/// How should output be captured?
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum CaptureMode {
Capture,
#[allow(unused)]
Discard,
}
/// An abstraction over [`fmt::Write`](std::fmt::Write) for the rendering.
///
/// This is a utility type used in the engine which can be written into like one
/// can write into an [`std::fmt::Write`] value. It's primarily used internally
/// in the engine but it's also passed to the custom formatter function.
pub struct Output<'a> {
w: *mut (dyn fmt::Write + 'a),
target: *mut (dyn fmt::Write + 'a),
capture_stack: Vec<Option<String>>,
}
impl<'a> Output<'a> {
/// Creates a new output.
pub(crate) fn new(w: &'a mut (dyn fmt::Write + 'a)) -> Self {
Self {
w,
target: w,
capture_stack: Vec::new(),
}
}
/// Creates a null output that writes nowhere.
pub(crate) fn null() -> Self {
// The null writer also has a single entry on the discarding capture
// stack. In fact, `w` is more or less useless here as we always
// shadow it. This is done so that `is_discarding` returns true.
Self {
w: NullWriter::get_mut(),
target: NullWriter::get_mut(),
capture_stack: vec![None],
}
}
/// Begins capturing into a string or discard.
pub(crate) fn begin_capture(&mut self, mode: CaptureMode) {
self.capture_stack.push(match mode {
CaptureMode::Capture => Some(String::new()),
CaptureMode::Discard => None,
});
self.retarget();
}
/// Ends capturing and returns the captured string as value.
pub(crate) fn end_capture(&mut self, auto_escape: AutoEscape) -> Value {
let rv = if let Some(captured) = self.capture_stack.pop().unwrap() {
if !matches!(auto_escape, AutoEscape::None) {
Value::from_safe_string(captured)
} else {
Value::from(captured)
}
} else {
Value::UNDEFINED
};
self.retarget();
rv
}
fn retarget(&mut self) {
self.target = match self.capture_stack.last_mut() {
Some(Some(stream)) => stream,
Some(None) => NullWriter::get_mut(),
None => self.w,
};
}
#[inline(always)]
fn target(&mut self) -> &mut dyn fmt::Write {
// SAFETY: this is safe because we carefully maintain the capture stack
// to update self.target whenever it's modified
unsafe { &mut *self.target }
}
/// Returns `true` if the output is discarding.
#[inline(always)]
#[allow(unused)]
pub(crate) fn is_discarding(&self) -> bool {
matches!(self.capture_stack.last(), Some(None))
}
/// Writes some data to the underlying buffer contained within this output.
#[inline]
pub fn write_str(&mut self, s: &str) -> fmt::Result {
self.target().write_str(s)
}
/// Writes some formatted information into this instance.
#[inline]
pub fn write_fmt(&mut self, a: fmt::Arguments<'_>) -> fmt::Result {
self.target().write_fmt(a)
}
}
impl fmt::Write for Output<'_> {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
fmt::Write::write_str(self.target(), s)
}
#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
fmt::Write::write_char(self.target(), c)
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
fmt::Write::write_fmt(self.target(), args)
}
}
pub struct NullWriter;
impl NullWriter {
/// Returns a reference to the null writer.
pub fn get_mut() -> &'static mut NullWriter {
static mut NULL_WRITER: NullWriter = NullWriter;
// SAFETY: this is safe as the null writer is a ZST
unsafe { &mut *addr_of_mut!(NULL_WRITER) }
}
}
impl fmt::Write for NullWriter {
#[inline]
fn write_str(&mut self, _s: &str) -> fmt::Result {
Ok(())
}
#[inline]
fn write_char(&mut self, _c: char) -> fmt::Result {
Ok(())
}
}
pub struct WriteWrapper<W> {
pub w: W,
pub err: Option<io::Error>,
}
impl<W> WriteWrapper<W> {
/// Replaces the given error with the held error if available.
pub fn take_err(&mut self, original: Error) -> Error {
self.err
.take()
.map(|io_err| {
Error::new(ErrorKind::WriteFailure, "I/O error during rendering")
.with_source(io_err)
})
.unwrap_or(original)
}
}
impl<W: io::Write> fmt::Write for WriteWrapper<W> {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.w.write_all(s.as_bytes()).map_err(|e| {
self.err = Some(e);
fmt::Error
})
}
#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
self.w
.write_all(c.encode_utf8(&mut [0; 4]).as_bytes())
.map_err(|e| {
self.err = Some(e);
fmt::Error
})
}
}
+1228
View File
File diff suppressed because it is too large. Load diff
+563
View File
@@ -0,0 +1,563 @@
use std::cell::RefCell;
use std::collections::{BTreeMap, HashSet};
use std::ops::Deref;
use std::sync::Arc;
use std::{fmt, io};
use crate::vendor::self_cell::self_cell;
use serde::Serialize;
use crate::compiler::codegen::CodeGenerator;
use crate::compiler::instructions::Instructions;
use crate::compiler::lexer::WhitespaceConfig;
use crate::compiler::meta::find_undeclared;
use crate::compiler::parser::parse;
use crate::environment::Environment;
use crate::error::{attach_basic_debug_info, Error};
use crate::output::{Output, WriteWrapper};
use crate::syntax::SyntaxConfig;
use crate::utils::AutoEscape;
use crate::value::Value;
use crate::vm::{prepare_blocks, Context, State, Vm};
/// Callback for auto escape determination
pub type AutoEscapeFunc = dyn Fn(&str) -> AutoEscape + Sync + Send;
/// Internal struct that holds template loading level config values.
#[derive(Clone)]
pub struct TemplateConfig {
/// The syntax used for the template.
pub syntax_config: SyntaxConfig,
/// Controls whitespace behavior.
pub ws_config: WhitespaceConfig,
/// The callback that determines the initial auto escaping for templates.
pub default_auto_escape: Arc<AutoEscapeFunc>,
}
impl TemplateConfig {
pub(crate) fn new(default_auto_escape: Arc<AutoEscapeFunc>) -> TemplateConfig {
TemplateConfig {
syntax_config: SyntaxConfig::default(),
ws_config: WhitespaceConfig::default(),
default_auto_escape,
}
}
}
/// Represents a handle to a template.
///
/// Templates are stored in the [`Environment`] as bytecode instructions. With the
/// [`Environment::get_template`] method that is looked up and returned in form of
/// this handle. Such a template can be cheaply copied as it only holds references.
///
/// To render the [`render`](Template::render) method can be used.
#[derive(Clone)]
pub struct Template<'env: 'source, 'source> {
env: &'env Environment<'env>,
pub(crate) compiled: CompiledTemplateRef<'env, 'source>,
}
struct CapturedData<'state> {
output: String,
state: State<'state, 'state>,
}
self_cell! {
struct CapturedCell<'source> {
owner: Template<'source, 'source>,
#[covariant]
dependent: CapturedData,
}
}
/// Represents a rendered template output together with a captured [`State`].
///
/// This type keeps the originating [`Template`] alive together with its
/// [`State`]. This is useful in situations where a temporary template handle
/// is used and a state needs to be inspected later (for instance to call
/// exported macros).
///
/// When created from [`render_captured`](Template::render_captured)
/// the [`output`](Self::output) contains the rendered string. When created from
/// [`render_captured_to`](Template::render_captured_to) the output is
/// an empty string as the output was written to the provided writer.
pub struct Captured<'source> {
cell: CapturedCell<'source>,
}
impl fmt::Debug for Captured<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Captured")
.field("output", &self.output())
.field("state", &self.state())
.finish()
}
}
impl<'source> Captured<'source> {
/// Returns the rendered output.
///
/// When created from [`render_captured_to`](Template::render_captured_to)
/// this returns an empty string.
pub fn output(&self) -> &str {
self.cell.borrow_dependent().output.as_str()
}
/// Returns a reference to the captured state.
pub fn state(&self) -> &State<'_, '_> {
&self.cell.borrow_dependent().state
}
/// Invokes a closure with mutable access to the captured state.
pub fn with_state_mut<R>(
&mut self,
f: impl for<'state> FnOnce(&mut State<'state, 'state>) -> R,
) -> R {
self.cell
.with_dependent_mut(|_, dependent| f(&mut dependent.state))
}
/// Consumes the capture and returns the rendered output string.
///
/// When created from [`render_captured_to`](Template::render_captured_to)
/// this returns an empty string.
pub fn into_output(mut self) -> String {
self.cell
.with_dependent_mut(|_, dependent| std::mem::take(&mut dependent.output))
}
}
impl fmt::Debug for Template<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ds = f.debug_struct("Template");
ds.field("name", &self.name());
#[cfg(feature = "internal_debug")]
{
ds.field("instructions", &self.compiled.instructions);
ds.field("blocks", &self.compiled.blocks);
}
ds.field("initial_auto_escape", &self.compiled.initial_auto_escape);
ds.finish()
}
}
impl<'env, 'source> Template<'env, 'source> {
pub(crate) fn new(
env: &'env Environment<'env>,
compiled: CompiledTemplateRef<'env, 'source>,
) -> Template<'env, 'source> {
Template { env, compiled }
}
/// Returns the name of the template.
pub fn name(&self) -> &str {
self.compiled.instructions.name()
}
/// Returns the source code of the template.
pub fn source(&self) -> &str {
self.compiled.instructions.source()
}
/// Renders the template into a string.
///
/// The provided value is used as the initial context for the template. It
/// can be any object that implements [`Serialize`](serde::Serialize). You
/// can either create your own struct and derive `Serialize` for it or the
/// [`context!`](crate::context) macro can be used to create an ad-hoc context.
///
/// For very large contexts and to avoid the overhead of serialization of
/// potentially unused values, you might consider using a dynamic
/// [`Object`](crate::value::Object) as value. For more
/// information see [Map as Context](crate::value::Object#map-as-context).
///
/// ```
/// # use minijinja::{Environment, context};
/// # let mut env = Environment::new();
/// # env.add_template("hello", "Hello {{ name }}!").unwrap();
/// let tmpl = env.get_template("hello").unwrap();
/// println!("{}", tmpl.render(context!(name => "John")).unwrap());
/// ```
///
/// To render a single block use [`render_captured`](Self::render_captured)
/// in combination with [`State::render_block`].
///
/// **Note on values:** The [`Value`] type implements `Serialize` and can be
/// efficiently passed to render. It does not undergo actual serialization.
pub fn render<S: Serialize>(&self, ctx: S) -> Result<String, Error> {
// reduce total amount of code falling under mono morphization into
// this function, and share the rest in _render.
self._render(Value::from_serialize(&ctx)).map(|x| x.0)
}
/// Like [`render`](Self::render) but also return the evaluated [`State`].
///
/// This can be used to inspect the [`State`] of the template post evaluation
/// for instance to get fuel consumption numbers or to access globally set
/// variables.
///
/// ```
/// # use minijinja::{Environment, context, value::Value};
/// # let mut env = Environment::new();
/// let tmpl = env.template_from_str("{% set x = 42 %}Hello {{ what }}!").unwrap();
/// let (rv, state) = tmpl.render_and_return_state(context!{ what => "World" }).unwrap();
/// assert_eq!(rv, "Hello World!");
/// assert_eq!(state.lookup("x"), Some(Value::from(42)));
/// ```
///
/// **Note on values:** The [`Value`] type implements `Serialize` and can be
/// efficiently passed to render. It does not undergo actual serialization.
#[deprecated(since = "2.18.0", note = "use render_captured instead")]
pub fn render_and_return_state<S: Serialize>(
&self,
ctx: S,
) -> Result<(String, State<'_, 'env>), Error> {
// reduce total amount of code falling under mono morphization into
// this function, and share the rest in _render.
self._render(Value::from_serialize(&ctx))
}
/// Like [`render`](Self::render) but also returns the evaluated [`State`]
/// while keeping the template alive with the returned state.
///
/// This is primarily useful when working with temporary template handles,
/// as the resulting [`State`] can continue to be used through the returned
/// wrapper.
///
/// ```
/// # use minijinja::{Environment, value::Value};
/// let env = Environment::new();
/// let rendered = env
/// .template_from_str("{% set x = 42 %}")
/// .unwrap()
/// .render_captured(())
/// .unwrap();
/// assert_eq!(rendered.output(), "");
/// assert_eq!(rendered.state().lookup("x"), Some(Value::from(42)));
/// ```
pub fn render_captured<S: Serialize>(&self, ctx: S) -> Result<Captured<'source>, Error> {
self.clone()
._capture_state(Value::from_serialize(&ctx), true)
}
/// Like [`render`](Self::render) but writes to an [`io::Write`] and keeps
/// the template alive with the returned state.
///
/// This is useful when working with temporary template handles and
/// the state needs to be inspected afterwards. The [`output`](Captured::output)
/// of the returned [`Captured`] will be an empty string since the
/// output was written to the provided writer.
///
/// ```
/// # use minijinja::{Environment, context};
/// let env = Environment::new();
/// let mut buf = Vec::new();
/// let captured = env
/// .template_from_str("{% set x = 42 %}Hello!")
/// .unwrap()
/// .render_captured_to((), &mut buf)
/// .unwrap();
/// assert_eq!(std::str::from_utf8(&buf).unwrap(), "Hello!");
/// assert_eq!(captured.output(), "");
/// ```
pub fn render_captured_to<S: Serialize, W: io::Write>(
&self,
ctx: S,
w: W,
) -> Result<Captured<'source>, Error> {
let root = Value::from_serialize(&ctx);
let w = std::cell::RefCell::new(WriteWrapper { w, err: None });
self.clone()
._capture_state_with_output(root, &w)
.map_err(|err| w.into_inner().take_err(err))
}
fn _render(&self, root: Value) -> Result<(String, State<'_, 'env>), Error> {
let mut rv = String::with_capacity(self.compiled.buffer_size_hint);
self._eval(root, &mut Output::new(&mut rv))
.map(|(_, state)| (rv, state))
}
fn _capture_state(self, root: Value, capture_output: bool) -> Result<Captured<'source>, Error> {
let this: Template<'source, 'source> = self;
let cell = ok!(CapturedCell::try_new(
this,
move |template| -> Result<CapturedData<'_>, Error> {
let mut output = if capture_output {
String::with_capacity(template.compiled.buffer_size_hint)
} else {
String::new()
};
let (_, state) = if capture_output {
ok!(template._eval(root, &mut Output::new(&mut output)))
} else {
ok!(template._eval(root, &mut Output::null()))
};
Ok(CapturedData { output, state })
}
));
Ok(Captured { cell })
}
fn _capture_state_with_output<W: io::Write>(
self,
root: Value,
w: &RefCell<WriteWrapper<W>>,
) -> Result<Captured<'source>, Error> {
let this: Template<'source, 'source> = self;
let cell = ok!(CapturedCell::try_new(
this,
move |template| -> Result<CapturedData<'_>, Error> {
let (_, state) = ok!(template._eval(root, &mut Output::new(&mut *w.borrow_mut())));
Ok(CapturedData {
output: String::new(),
state,
})
}
));
Ok(Captured { cell })
}
/// Renders the template into an [`io::Write`].
///
/// This works exactly like [`render`](Self::render), but writes the template
/// into an [`io::Write`] as it is evaluated.
///
/// ```
/// # use minijinja::{Environment, context};
/// # let mut env = Environment::new();
/// # env.add_template("hello", "Hello {{ name }}!").unwrap();
/// use std::io::stdout;
///
/// let tmpl = env.get_template("hello").unwrap();
/// tmpl.render_to_write(context!(name => "John"), &mut stdout()).unwrap();
/// ```
///
/// **Note on values:** The [`Value`] type implements `Serialize` and can be
/// efficiently passed to render. It does not undergo actual serialization.
#[deprecated(since = "2.18.0", note = "use render_captured_to instead")]
pub fn render_to_write<S: Serialize, W: io::Write>(
&self,
ctx: S,
w: W,
) -> Result<State<'_, 'env>, Error> {
let mut wrapper = WriteWrapper { w, err: None };
self._eval(Value::from_serialize(&ctx), &mut Output::new(&mut wrapper))
.map(|(_, state)| state)
.map_err(|err| wrapper.take_err(err))
}
/// Evaluates the template into a [`State`].
///
/// This evaluates the template, discards the output and returns the final
/// `State` for introspection. From there global variables or blocks
/// can be accessed. What this does is quite similar to how the engine
/// internally works with templates that are extended or imported from.
///
/// ```
/// # use minijinja::{Environment, context};
/// # fn test() -> Result<(), minijinja::Error> {
/// # let mut env = Environment::new();
/// # env.add_template("hello", "")?;
/// let tmpl = env.get_template("hello")?;
/// let state = tmpl.eval_to_state(context!(name => "John"))?;
/// println!("{:?}", state.exports());
/// # Ok(()) }
/// ```
///
/// If you also want to render, use [`render_captured`](Self::render_captured).
///
/// For more information see [`State`].
#[deprecated(since = "2.18.0", note = "use render_captured instead")]
pub fn eval_to_state<S: Serialize>(&self, ctx: S) -> Result<State<'_, 'env>, Error> {
let root = Value::from_serialize(&ctx);
let mut out = Output::null();
let vm = Vm::new(self.env);
let state = ok!(vm.eval(
&self.compiled.instructions,
root,
&self.compiled.blocks,
&mut out,
self.compiled.initial_auto_escape,
))
.1;
Ok(state)
}
fn _eval(
&self,
root: Value,
out: &mut Output,
) -> Result<(Option<Value>, State<'_, 'env>), Error> {
Vm::new(self.env).eval(
&self.compiled.instructions,
root,
&self.compiled.blocks,
out,
self.compiled.initial_auto_escape,
)
}
/// Returns a set of all undeclared variables in the template.
///
/// This returns a set of all variables that might be looked up
/// at runtime by the template. Since this runs a static
/// analysis, the actual control flow is not considered. This
/// also cannot take into account what happens due to includes,
/// imports or extending. If `nested` is set to `true`, then also
/// nested trivial attribute lookups are considered and returned.
///
/// ```rust
/// # use minijinja::Environment;
/// let mut env = Environment::new();
/// env.add_template("x", "{% set x = foo %}{{ x }}{{ bar.baz }}").unwrap();
/// let tmpl = env.get_template("x").unwrap();
/// let undeclared = tmpl.undeclared_variables(false);
/// // returns ["foo", "bar"]
/// let undeclared = tmpl.undeclared_variables(true);
/// // returns ["foo", "bar.baz"]
/// ```
///
/// Note that this does not special case global variables. This means
/// that for instance a template that uses `namespace()` will return
/// `namespace` in the return value.
pub fn undeclared_variables(&self, nested: bool) -> HashSet<String> {
match parse(
self.compiled.instructions.source(),
self.name(),
self.compiled.syntax_config.clone(),
// TODO: this is not entirely great, but good enough for this use case.
Default::default(),
) {
Ok(ast) => find_undeclared(&ast, nested),
Err(_) => HashSet::new(),
}
}
/// Creates an empty [`State`] for this template.
///
/// It's very rare that you need to actually do this but it can be useful when
/// testing values or working with macros or other callable objects from outside
/// the template environment.
pub fn new_state(&self) -> State<'_, 'env> {
State::new(
Context::new(self.env),
self.compiled.initial_auto_escape,
&self.compiled.instructions,
prepare_blocks(&self.compiled.blocks),
)
}
/// Returns the instructions and blocks if the template is loaded from the
/// environment.
///
/// For templates loaded as string on the environment this API contract
/// cannot be upheld because the template might not live long enough. Under
/// normal circumstances however such a template object would never make it
/// to the callers of this API as this API is used for including or extending,
/// both of which should only ever get access to a template from the environment
/// which holds a borrowed ref.
#[cfg(feature = "multi_template")]
pub(crate) fn instructions_and_blocks(
&self,
) -> Result<
(
&'env Instructions<'env>,
&'env BTreeMap<&'env str, Instructions<'env>>,
),
Error,
> {
match self.compiled {
CompiledTemplateRef::Borrowed(x) => Ok((&x.instructions, &x.blocks)),
CompiledTemplateRef::Owned(_) => Err(Error::new(
crate::ErrorKind::InvalidOperation,
"cannot extend or include template not borrowed from environment",
)),
}
}
/// Returns the initial auto escape setting.
#[cfg(feature = "multi_template")]
pub(crate) fn initial_auto_escape(&self) -> AutoEscape {
self.compiled.initial_auto_escape
}
}
#[derive(Clone)]
pub(crate) enum CompiledTemplateRef<'env: 'source, 'source> {
Owned(Arc<CompiledTemplate<'source>>),
Borrowed(&'env CompiledTemplate<'source>),
}
impl<'source> Deref for CompiledTemplateRef<'_, 'source> {
type Target = CompiledTemplate<'source>;
fn deref(&self) -> &Self::Target {
match *self {
CompiledTemplateRef::Owned(ref x) => x,
CompiledTemplateRef::Borrowed(x) => x,
}
}
}
/// Represents a compiled template in memory.
pub struct CompiledTemplate<'source> {
/// The root instructions.
pub instructions: Instructions<'source>,
/// Block local instructions.
pub blocks: BTreeMap<&'source str, Instructions<'source>>,
/// Optional size hint for string rendering.
pub buffer_size_hint: usize,
/// The syntax config that created it.
pub syntax_config: SyntaxConfig,
/// The initial setting of auto escaping.
pub initial_auto_escape: AutoEscape,
}
impl fmt::Debug for CompiledTemplate<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ds = f.debug_struct("CompiledTemplate");
#[cfg(feature = "internal_debug")]
{
ds.field("instructions", &self.instructions);
ds.field("blocks", &self.blocks);
}
ds.finish()
}
}
impl<'source> CompiledTemplate<'source> {
/// Creates a compiled template from name and source using the given settings.
pub fn new(
name: &'source str,
source: &'source str,
config: &TemplateConfig,
) -> Result<CompiledTemplate<'source>, Error> {
attach_basic_debug_info(Self::_new_impl(name, source, config), source)
}
fn _new_impl(
name: &'source str,
source: &'source str,
config: &TemplateConfig,
) -> Result<CompiledTemplate<'source>, Error> {
let ast = ok!(parse(
source,
name,
config.syntax_config.clone(),
config.ws_config
));
let mut g = CodeGenerator::new(name, source);
g.compile_stmt(&ast);
let buffer_size_hint = g.buffer_size_hint();
let (instructions, blocks) = g.finish();
Ok(CompiledTemplate {
instructions,
blocks,
buffer_size_hint,
syntax_config: config.syntax_config.clone(),
initial_auto_escape: (config.default_auto_escape)(name),
})
}
}
+471
View File
@@ -0,0 +1,471 @@
//! Test functions and abstractions.
//!
//! Test functions in MiniJinja are like [`filters`](crate::filters) but a
//! different syntax is used to invoke them and they have to return boolean
//! values. For instance the expression `{% if foo is odd %}` invokes the
//! [`is_odd`] test to check if the value is indeed an odd number.
//!
//! MiniJinja comes with some built-in test functions that are listed below. To
//! create a custom test write a function that takes at least a value argument
//! that returns a boolean result, then register it with
//! [`add_filter`](crate::Environment::add_test).
//!
//! # Using Tests
//!
//! Tests are useful to "test" a value in a specific way. For instance if
//! you want to assign different classes to alternating rows one way is
//! using the `odd` test:
//!
//! ```jinja
//! {% if seq is defined %}
//! <ul>
//! {% for item in seq %}
//! <li class="{{ 'even' if loop.index is even else 'odd' }}">{{ item }}</li>
//! {% endfor %}
//! </ul>
//! {% endif %}
//! ```
//!
//! # Custom Tests
//!
//! A custom test function is just a simple function which accepts its
//! inputs as parameters and then returns a bool. For instance the following
//! shows a test function which takes an input value and checks if it's
//! lowercase:
//!
//! ```
//! # use minijinja::Environment;
//! # let mut env = Environment::new();
//! fn is_lowercase(value: String) -> bool {
//! value.chars().all(|x| x.is_lowercase())
//! }
//!
//! env.add_test("lowercase", is_lowercase);
//! ```
//!
//! MiniJinja will perform the necessary conversions automatically. For more
//! information see the [`Function`](crate::functions::Function) trait. If a
//! test returns a value that is not a bool, it will be evaluated for truthiness
//! with [`Value::is_true`].
//!
//! # Built-in Tests
//!
//! When the `builtins` feature is enabled a range of built-in tests are
//! automatically added to the environment. These are also all provided in
//! this module. Note though that these functions are not to be
//! called from Rust code as their exact interface (arguments and return types)
//! might change from one MiniJinja version to another.
use crate::error::Error;
use crate::value::Value;
use crate::vm::State;
/// Deprecated alias
#[deprecated = "Use the minijinja::functions::Function instead"]
#[doc(hidden)]
pub use crate::functions::Function as Test;
#[deprecated = "Use the minijinja::value::FunctionResult instead"]
#[doc(hidden)]
pub use crate::value::FunctionResult as TestResult;
/// Checks if a value is undefined.
///
/// ```jinja
/// {{ 42 is undefined }} -> false
/// ```
pub fn is_undefined(v: &Value) -> bool {
v.is_undefined()
}
/// Checks if a value is defined.
///
/// ```jinja
/// {{ 42 is defined }} -> true
/// ```
pub fn is_defined(v: &Value) -> bool {
!v.is_undefined()
}
/// Checks if a value is none.
///
/// ```jinja
/// {{ none is none }} -> true
/// ```
pub fn is_none(v: &Value) -> bool {
v.is_none()
}
/// Checks if a value is safe.
///
/// ```jinja
/// {{ "<hello>"|escape is safe }} -> true
/// ```
///
/// This filter is also registered with the `escaped` alias.
pub fn is_safe(v: &Value) -> bool {
v.is_safe()
}
#[cfg(feature = "builtins")]
mod builtins {
use super::*;
use std::borrow::Cow;
use crate::value::ops::{coerce, CoerceResult};
use crate::value::ValueKind;
/// Return true if the object is a boolean value.
///
/// ```jinja
/// {{ true is boolean }} -> true
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_boolean(v: &Value) -> bool {
v.kind() == ValueKind::Bool
}
/// Checks if a value is odd.
///
/// ```jinja
/// {{ 41 is odd }} -> true
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_odd(v: Value) -> bool {
i128::try_from(v).ok().is_some_and(|x| x % 2 != 0)
}
/// Checks if a value is even.
///
/// ```jinja
/// {{ 42 is even }} -> true
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_even(v: Value) -> bool {
i128::try_from(v).ok().is_some_and(|x| x % 2 == 0)
}
/// Return true if the value is divisible by another one.
///
/// ```jinja
/// {{ 42 is divisibleby(2) }} -> true
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_divisibleby(v: &Value, other: &Value) -> bool {
match coerce(v, other, false) {
Some(CoerceResult::I128(a, b)) => (a % b) == 0,
Some(CoerceResult::F64(a, b)) => (a % b) == 0.0,
_ => false,
}
}
/// Checks if this value is a number.
///
/// ```jinja
/// {{ 42 is number }} -> true
/// {{ "42" is number }} -> false
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_number(v: &Value) -> bool {
matches!(v.kind(), ValueKind::Number)
}
/// Checks if this value is an integer.
///
/// ```jinja
/// {{ 42 is integer }} -> true
/// {{ 42.0 is integer }} -> false
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_integer(v: &Value) -> bool {
v.is_integer()
}
/// Checks if this value is a float
///
/// ```jinja
/// {{ 42 is float }} -> false
/// {{ 42.0 is float }} -> true
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_float(v: &Value) -> bool {
matches!(v.0, crate::value::ValueRepr::F64(_))
}
/// Checks if this value is a string.
///
/// ```jinja
/// {{ "42" is string }} -> true
/// {{ 42 is string }} -> false
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_string(v: &Value) -> bool {
matches!(v.kind(), ValueKind::String)
}
/// Checks if this value is a sequence
///
/// ```jinja
/// {{ [1, 2, 3] is sequence }} -> true
/// {{ 42 is sequence }} -> false
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_sequence(v: &Value) -> bool {
matches!(v.kind(), ValueKind::Seq)
}
/// Checks if this value can be iterated over.
///
/// ```jinja
/// {{ [1, 2, 3] is iterable }} -> true
/// {{ 42 is iterable }} -> false
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_iterable(v: &Value) -> bool {
v.try_iter().is_ok()
}
/// Checks if this value is a mapping
///
/// ```jinja
/// {{ {"foo": "bar"} is mapping }} -> true
/// {{ [1, 2, 3] is mapping }} -> false
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_mapping(v: &Value) -> bool {
matches!(v.kind(), ValueKind::Map)
}
/// Checks if the value is starting with a string.
///
/// ```jinja
/// {{ "foobar" is startingwith "foo" }} -> true
/// {{ "foobar" is startingwith "bar" }} -> false
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_startingwith(v: Cow<'_, str>, other: Cow<'_, str>) -> bool {
v.starts_with(&other as &str)
}
/// Checks if the value is ending with a string.
///
/// ```jinja
/// {{ "foobar" is endingwith "bar" }} -> true
/// {{ "foobar" is endingwith "foo" }} -> false
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn is_endingwith(v: Cow<'_, str>, other: Cow<'_, str>) -> bool {
v.ends_with(&other as &str)
}
/// Test version of `==`.
///
/// This is useful when combined with [`select`](crate::filters::select).
///
/// ```jinja
/// {{ 1 is eq 1 }} -> true
/// {{ [1, 2, 3]|select("==", 1) }} => [1]
/// ```
///
/// By default aliased to `equalto` and `==`.
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_eq(value: &Value, other: &Value) -> bool {
*value == *other
}
/// Test version of `!=`.
///
/// This is useful when combined with [`select`](crate::filters::select).
///
/// ```jinja
/// {{ 2 is ne 1 }} -> true
/// {{ [1, 2, 3]|select("!=", 1) }} => [2, 3]
/// ```
///
/// By default aliased to `!=`.
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_ne(value: &Value, other: &Value) -> bool {
*value != *other
}
/// Test version of `<`.
///
/// This is useful when combined with [`select`](crate::filters::select).
///
/// ```jinja
/// {{ 1 is lt 2 }} -> true
/// {{ [1, 2, 3]|select("<", 2) }} => [1]
/// ```
///
/// By default aliased to `lessthan` and `<`.
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_lt(value: &Value, other: &Value) -> bool {
*value < *other
}
/// Test version of `<=`.
///
/// This is useful when combined with [`select`](crate::filters::select).
///
/// ```jinja
/// {{ 1 is le 2 }} -> true
/// {{ [1, 2, 3]|select("<=", 2) }} => [1, 2]
/// ```
///
/// By default aliased to `<=`.
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_le(value: &Value, other: &Value) -> bool {
*value <= *other
}
/// Test version of `>`.
///
/// This is useful when combined with [`select`](crate::filters::select).
///
/// ```jinja
/// {{ 2 is gt 1 }} -> true
/// {{ [1, 2, 3]|select(">", 2) }} => [3]
/// ```
///
/// By default aliased to `greaterthan` and `>`.
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_gt(value: &Value, other: &Value) -> bool {
*value > *other
}
/// Test version of `>=`.
///
/// This is useful when combined with [`select`](crate::filters::select).
///
/// ```jinja
/// {{ 2 is ge 1 }} -> true
/// {{ [1, 2, 3]|select(">=", 2) }} => [2, 3]
/// ```
///
/// By default aliased to `>=`.
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_ge(value: &Value, other: &Value) -> bool {
*value >= *other
}
/// Test version of `in`.
///
/// ```jinja
/// {{ 1 is in [1, 2, 3] }} -> true
/// {{ [1, 2, 3]|select("in", [1, 2]) }} => [1, 2]
/// ```
///
/// This is useful when combined with [`select`](crate::filters::select).
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_in(state: &State, value: &Value, other: &Value) -> Result<bool, Error> {
ok!(state.undefined_behavior().assert_iterable(other));
Ok(crate::value::ops::contains(other, value)
.map(|value| value.is_true())
.unwrap_or(false))
}
/// Checks if a value is `true`.
///
/// ```jinja
/// {% if value is true %}...{% endif %}
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_true(value: &Value) -> bool {
matches!(value.0, crate::value::ValueRepr::Bool(true))
}
/// Checks if a value is `false`.
///
/// ```jinja
/// {% if value is false %}...{% endif %}
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_false(value: &Value) -> bool {
matches!(value.0, crate::value::ValueRepr::Bool(false))
}
/// Checks if a filter with a given name is available.
///
/// ```jinja
/// {% if 'tojson' is filter %}...{% endif %}
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_filter(state: &State, name: &str) -> bool {
state.env().get_filter(name).is_some()
}
/// Checks if a test with a given name is available.
///
/// ```jinja
/// {% if 'greaterthan' is test %}...{% endif %}
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_test(state: &State, name: &str) -> bool {
state.env().get_test(name).is_some()
}
/// Checks if a string is all lowercase.
///
/// ```jinja
/// {{ 'foo' is lower }} -> true
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_lower(name: &str) -> bool {
name.chars().all(|x| x.is_lowercase())
}
/// Checks if a string is all uppercase.
///
/// ```jinja
/// {{ 'FOO' is upper }} -> true
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_upper(name: &str) -> bool {
name.chars().all(|x| x.is_uppercase())
}
/// Checks if two values are identical.
///
/// This primarily exists for compatibilith with Jinja2. It can be seen as a much
/// stricter comparison than a regular comparison. The main difference is that
/// values that have the same structure but a different internal object will not
/// compare equal.
///
/// ```jinja
/// {{ [1, 2, 3] is sameas [1, 2, 3] }}
/// -> false
///
/// {{ false is sameas false }}
/// -> true
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
#[cfg(feature = "builtins")]
pub fn is_sameas(value: &Value, other: &Value) -> bool {
match (value.as_object(), other.as_object()) {
(Some(a), Some(b)) => a.is_same_object(b),
(None, Some(_)) | (Some(_), None) => false,
(None, None) => {
if value.kind() != other.kind() || value.is_integer() != other.is_integer() {
false
} else {
value == other
}
}
}
}
}
#[cfg(feature = "builtins")]
pub use self::builtins::*;
+645
View File
@@ -0,0 +1,645 @@
use std::char::decode_utf16;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fmt;
use std::iter::{once, repeat};
use std::str::Chars;
use std::sync::OnceLock;
use crate::error::{Error, ErrorKind};
use crate::value::{StringType, UndefinedType, Value, ValueIter, ValueKind, ValueRepr};
use crate::Output;
/// internal marker to seal up some trait methods
pub struct SealedMarker;
pub fn memchr(haystack: &[u8], needle: u8) -> Option<usize> {
haystack.iter().position(|&x| x == needle)
}
pub fn memstr(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
/// Helper for dealing with untrusted size hints.
#[inline(always)]
pub(crate) fn untrusted_size_hint(value: usize) -> usize {
value.min(1024)
}
const SMALL_INT_FORMAT_CACHE_LIMIT: usize = 256;
#[inline(always)]
fn small_u64_format(value: u64) -> Option<&'static str> {
static CACHE: OnceLock<Vec<Box<str>>> = OnceLock::new();
if value >= SMALL_INT_FORMAT_CACHE_LIMIT as u64 {
return None;
}
let cache = CACHE.get_or_init(|| {
(0..SMALL_INT_FORMAT_CACHE_LIMIT)
.map(|n| n.to_string().into_boxed_str())
.collect::<Vec<_>>()
});
Some(cache[value as usize].as_ref())
}
#[inline(always)]
fn is_ascii_integer_str(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
let (first, rest) = bytes.split_first().unwrap();
if *first == b'-' {
!rest.is_empty() && rest.iter().all(|b| b.is_ascii_digit())
} else {
first.is_ascii_digit() && rest.iter().all(|b| b.is_ascii_digit())
}
}
#[inline(always)]
fn needs_html_escaping(s: &str) -> bool {
for &b in s.as_bytes() {
if b.wrapping_sub(b'"') <= b'>' - b'"'
&& matches!(b, b'<' | b'>' | b'&' | b'"' | b'\'' | b'/')
{
return true;
}
}
false
}
fn write_with_html_escaping(out: &mut Output, value: &Value) -> fmt::Result {
match value.0 {
ValueRepr::U64(v) => {
return match small_u64_format(v) {
Some(s) => out.write_str(s),
None => write!(out, "{v}"),
}
}
ValueRepr::I64(v) if v >= 0 => {
return match small_u64_format(v as u64) {
Some(s) => out.write_str(s),
None => write!(out, "{v}"),
}
}
ValueRepr::I64(v) => return write!(out, "{v}"),
ValueRepr::Bool(v) => {
return out.write_str(if v { "true" } else { "false" });
}
_ => {}
}
if let ValueRepr::SmallStr(ref s) = value.0 {
let s = s.as_str();
if is_ascii_integer_str(s) {
return out.write_str(s);
}
}
if let Some(s) = value.as_str() {
if !needs_html_escaping(s) {
out.write_str(s)
} else {
write!(out, "{}", HtmlEscape(s))
}
} else if matches!(
value.kind(),
ValueKind::Undefined | ValueKind::None | ValueKind::Bool | ValueKind::Number
) {
write!(out, "{value}")
} else {
write!(out, "{}", HtmlEscape(&value.to_string()))
}
}
#[cold]
fn invalid_autoescape(name: &str) -> Result<(), Error> {
Err(Error::new(
ErrorKind::InvalidOperation,
format!("Default formatter does not know how to format to custom format '{name}'"),
))
}
#[cfg(feature = "json")]
fn json_escape_write(out: &mut Output, value: &Value) -> Result<(), Error> {
let value = ok!(serde_json::to_string(&value).map_err(|err| {
Error::new(ErrorKind::BadSerialization, "unable to format to JSON").with_source(err)
}));
write!(out, "{value}").map_err(Error::from)
}
#[inline(always)]
pub fn write_escaped(
out: &mut Output,
auto_escape: AutoEscape,
value: &Value,
) -> Result<(), Error> {
// string strings bypass all of this
if let ValueRepr::String(ref s, StringType::Safe) = value.0 {
return out.write_str(s).map_err(Error::from);
}
match auto_escape {
AutoEscape::None => write!(out, "{value}").map_err(Error::from),
AutoEscape::Html => write_with_html_escaping(out, value).map_err(Error::from),
#[cfg(feature = "json")]
AutoEscape::Json => json_escape_write(out, value),
AutoEscape::Custom(name) => invalid_autoescape(name),
}
}
/// Controls the autoescaping behavior.
///
/// For more information see
/// [`set_auto_escape_callback`](crate::Environment::set_auto_escape_callback).
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AutoEscape {
/// Do not apply auto escaping.
None,
/// Use HTML auto escaping rules.
///
/// Any value will be converted into a string and the following characters
/// will be escaped in ways compatible to XML and HTML: `<`, `>`, `&`, `"`,
/// `'`, and `/`.
Html,
/// Use escaping rules suitable for JSON/JavaScript or YAML.
///
/// Any value effectively ends up being serialized to JSON upon printing. The
/// serialized values will be compatible with JavaScript and YAML as well.
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
Json,
/// A custom auto escape format.
///
/// The default formatter does not know how to deal with a custom escaping
/// format and would error. The use of these requires a custom formatter.
/// See [`set_formatter`](crate::Environment::set_formatter).
Custom(&'static str),
}
/// Defines the behavior of undefined values in the engine.
///
/// At present there are three types of behaviors available which mirror the
/// behaviors that Jinja2 provides out of the box and an extra option called
/// `SemiStrict` which is a slightly less strict undefined.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum UndefinedBehavior {
/// The default, somewhat lenient undefined behavior.
///
/// * **printing:** allowed (returns empty string)
/// * **iteration:** allowed (returns empty array)
/// * **attribute access of undefined values:** fails
/// * **if true:** allowed (is considered false)
#[default]
Lenient,
/// Like `Lenient`, but also allows chaining of undefined lookups.
///
/// * **printing:** allowed (returns empty string)
/// * **iteration:** allowed (returns empty array)
/// * **attribute access of undefined values:** allowed (returns [`undefined`](Value::UNDEFINED))
/// * **if true:** allowed (is considered false)
Chainable,
/// Like strict, but does not error when the undefined is checked for truthyness.
///
/// * **printing:** fails
/// * **iteration:** fails
/// * **attribute access of undefined values:** fails
/// * **string coercion in filters/functions:** fails
/// * **if true:** allowed (is considered false)
SemiStrict,
/// Complains very quickly about undefined values.
///
/// * **printing:** fails
/// * **iteration:** fails
/// * **attribute access of undefined values:** fails
/// * **string coercion in filters/functions:** fails
/// * **if true:** fails
Strict,
}
impl UndefinedBehavior {
/// Utility method used in the engine to determine what to do when an undefined is
/// encountered.
///
/// The flag indicates if this is the first or second level of undefined value. The
/// parent value is passed too.
pub(crate) fn handle_undefined(self, parent_was_undefined: bool) -> Result<Value, Error> {
match (self, parent_was_undefined) {
(UndefinedBehavior::Lenient, false)
| (UndefinedBehavior::Strict, false)
| (UndefinedBehavior::SemiStrict, false)
| (UndefinedBehavior::Chainable, _) => Ok(Value::UNDEFINED),
(UndefinedBehavior::Lenient, true)
| (UndefinedBehavior::Strict, true)
| (UndefinedBehavior::SemiStrict, true) => Err(Error::from(ErrorKind::UndefinedError)),
}
}
/// Utility method to check if something is true.
///
/// This fails only for strict undefined values.
#[inline]
pub(crate) fn is_true(self, value: &Value) -> Result<bool, Error> {
match (self, &value.0) {
// silent undefined doesn't error, even in strict mode
(UndefinedBehavior::Strict, &ValueRepr::Undefined(UndefinedType::Default)) => {
Err(Error::from(ErrorKind::UndefinedError))
}
_ => Ok(value.is_true()),
}
}
/// Tries to iterate over a value while handling the undefined value.
///
/// If the value is undefined, then iteration fails if the behavior is set to strict,
/// otherwise it succeeds with an empty iteration. This is also internally used in the
/// engine to convert values to lists.
#[inline]
pub(crate) fn try_iter(self, value: Value) -> Result<ValueIter, Error> {
self.assert_iterable(&value).and_then(|_| value.try_iter())
}
/// Are we strict on iteration?
#[inline]
pub(crate) fn assert_iterable(self, value: &Value) -> Result<(), Error> {
match (self, &value.0) {
// silent undefined doesn't error, even in strict mode
(
UndefinedBehavior::Strict | UndefinedBehavior::SemiStrict,
&ValueRepr::Undefined(UndefinedType::Default),
) => Err(Error::from(ErrorKind::UndefinedError)),
_ => Ok(()),
}
}
/// Checks that undefined cannot be coerced into a concrete type (e.g. string).
///
/// This is called when an undefined value is passed to a filter or function
/// argument that expects a concrete type. Filters that explicitly handle
/// undefined values (such as `default`) avoid this by using `&Value` as
/// their first argument instead of a concrete type like `String`.
#[inline]
pub(crate) fn assert_value_not_undefined(self, value: &Value) -> Result<(), Error> {
match (self, &value.0) {
// silent undefined never errors
(
UndefinedBehavior::Strict | UndefinedBehavior::SemiStrict,
&ValueRepr::Undefined(UndefinedType::Default),
) => Err(Error::from(ErrorKind::UndefinedError)),
_ => Ok(()),
}
}
}
/// Helper to HTML escape a string.
pub struct HtmlEscape<'a>(pub &'a str);
impl fmt::Display for HtmlEscape<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(feature = "v_htmlescape")]
{
fmt::Display::fmt(&v_htmlescape::escape(self.0), f)
}
// this is taken from askama-escape
#[cfg(not(feature = "v_htmlescape"))]
{
let bytes = self.0.as_bytes();
let mut start = 0;
for (i, b) in bytes.iter().enumerate() {
macro_rules! escaping_body {
($quote:expr) => {{
if start < i {
// SAFETY: this is safe because we only push valid utf-8 bytes over
ok!(f.write_str(unsafe {
std::str::from_utf8_unchecked(&bytes[start..i])
}));
}
ok!(f.write_str($quote));
start = i + 1;
}};
}
if b.wrapping_sub(b'"') <= b'>' - b'"' {
match *b {
b'<' => escaping_body!("&lt;"),
b'>' => escaping_body!("&gt;"),
b'&' => escaping_body!("&amp;"),
b'"' => escaping_body!("&quot;"),
b'\'' => escaping_body!("&#x27;"),
b'/' => escaping_body!("&#x2f;"),
_ => (),
}
}
}
if start < bytes.len() {
// SAFETY: this is safe because we only push valid utf-8 bytes over
f.write_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..]) })
} else {
Ok(())
}
}
}
}
struct Unescaper {
out: String,
pending_surrogate: u16,
}
impl Unescaper {
fn unescape(mut self, s: &str) -> Result<String, Error> {
let mut char_iter = s.chars();
while let Some(c) = char_iter.next() {
if c == '\\' {
match char_iter.next() {
None => return Err(ErrorKind::BadEscape.into()),
Some(d) => match d {
'"' | '\\' | '/' | '\'' => ok!(self.push_char(d)),
'b' => ok!(self.push_char('\x08')),
'f' => ok!(self.push_char('\x0C')),
'n' => ok!(self.push_char('\n')),
'r' => ok!(self.push_char('\r')),
't' => ok!(self.push_char('\t')),
'u' => {
let val = ok!(self.parse_u16(&mut char_iter));
ok!(self.push_u16(val));
}
'x' => {
let val = ok!(self.parse_hex_byte(&mut char_iter));
ok!(self.push_char(val as char));
}
'0'..='7' => {
let val = ok!(self.parse_octal_byte(d, &mut char_iter));
ok!(self.push_char(val as char));
}
_ => {
ok!(self.push_char('\\'));
ok!(self.push_char(d));
}
},
}
} else {
ok!(self.push_char(c));
}
}
if self.pending_surrogate != 0 {
Err(ErrorKind::BadEscape.into())
} else {
Ok(self.out)
}
}
fn parse_u16(&self, chars: &mut Chars) -> Result<u16, Error> {
let hexnum = chars.chain(repeat('\0')).take(4).collect::<String>();
u16::from_str_radix(&hexnum, 16).map_err(|_| ErrorKind::BadEscape.into())
}
fn parse_hex_byte(&self, chars: &mut Chars) -> Result<u8, Error> {
let hexnum = chars.take(2).collect::<String>();
if hexnum.len() != 2 {
return Err(ErrorKind::BadEscape.into());
}
u8::from_str_radix(&hexnum, 16).map_err(|_| ErrorKind::BadEscape.into())
}
fn parse_octal_byte(&self, first_digit: char, chars: &mut Chars) -> Result<u8, Error> {
let mut octal_str = String::new();
octal_str.push(first_digit);
// Collect up to 2 more octal digits (0-7)
for _ in 0..2 {
let next_char = chars.as_str().chars().next();
if let Some(c) = next_char {
if ('0'..='7').contains(&c) {
octal_str.push(c);
chars.next(); // consume the character
} else {
break;
}
} else {
break;
}
}
u8::from_str_radix(&octal_str, 8).map_err(|_| ErrorKind::BadEscape.into())
}
fn push_u16(&mut self, c: u16) -> Result<(), Error> {
match (self.pending_surrogate, (0xD800..=0xDFFF).contains(&c)) {
(0, false) => match decode_utf16(once(c)).next() {
Some(Ok(c)) => self.out.push(c),
_ => return Err(ErrorKind::BadEscape.into()),
},
(_, false) => return Err(ErrorKind::BadEscape.into()),
(0, true) => self.pending_surrogate = c,
(prev, true) => match decode_utf16(once(prev).chain(once(c))).next() {
Some(Ok(c)) => {
self.out.push(c);
self.pending_surrogate = 0;
}
_ => return Err(ErrorKind::BadEscape.into()),
},
}
Ok(())
}
fn push_char(&mut self, c: char) -> Result<(), Error> {
if self.pending_surrogate != 0 {
Err(ErrorKind::BadEscape.into())
} else {
self.out.push(c);
Ok(())
}
}
}
/// Un-escape a string, following Jinja-compatible string escape rules.
pub fn unescape(s: &str) -> Result<String, Error> {
Unescaper {
out: String::new(),
pending_surrogate: 0,
}
.unescape(s)
}
pub struct BTreeMapKeysDebug<'a, K: fmt::Debug, V>(pub &'a BTreeMap<K, V>);
impl<K: fmt::Debug, V> fmt::Debug for BTreeMapKeysDebug<'_, K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(self.0.iter().map(|x| x.0)).finish()
}
}
#[cfg(feature = "builtins")]
pub fn splitn_whitespace(s: &str, maxsplits: usize) -> impl Iterator<Item = &str> + '_ {
let mut splits = 1;
let mut skip_ws = true;
let mut split_start = None;
let mut last_split_end = 0;
let mut chars = s.char_indices();
std::iter::from_fn(move || {
for (idx, c) in chars.by_ref() {
if splits >= maxsplits && !skip_ws {
continue;
} else if c.is_whitespace() {
if let Some(old) = split_start {
let rv = &s[old..idx];
split_start = None;
last_split_end = idx;
splits += 1;
skip_ws = true;
return Some(rv);
}
} else {
skip_ws = false;
if split_start.is_none() {
split_start = Some(idx);
last_split_end = idx;
}
}
}
let rest = &s[last_split_end..];
if !rest.is_empty() {
last_split_end = s.len();
Some(rest)
} else {
None
}
})
}
/// Because the Python crate violates our ordering guarantees by design
/// we want to catch failed sorts in a landing pad. This is not ideal but
/// it at least gives us error context for when invalid search operations
/// are taking place.
#[cfg_attr(not(feature = "internal_safe_search"), inline)]
pub fn safe_sort<T, F>(seq: &mut [T], f: F) -> Result<(), Error>
where
F: FnMut(&T, &T) -> Ordering,
{
#[cfg(feature = "internal_safe_search")]
{
if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
seq.sort_by(f);
})) {
let msg = panic
.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(|x| x.as_str()));
return Err(Error::new(
ErrorKind::InvalidOperation,
format!(
"failed to sort: {}",
msg.unwrap_or("comparator does not implement total order")
),
));
}
}
#[cfg(not(feature = "internal_safe_search"))]
{
seq.sort_by(f);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use similar_asserts::assert_eq;
#[test]
fn test_small_u64_format_cache_bounds() {
assert_eq!(small_u64_format(0), Some("0"));
let last_cached = (SMALL_INT_FORMAT_CACHE_LIMIT - 1) as u64;
let expected = (SMALL_INT_FORMAT_CACHE_LIMIT - 1).to_string();
assert_eq!(small_u64_format(last_cached), Some(expected.as_str()));
assert_eq!(small_u64_format(SMALL_INT_FORMAT_CACHE_LIMIT as u64), None);
}
#[test]
fn test_small_u64_format_large_values_are_not_cached() {
// This value wraps to 0 when cast to usize on 32-bit targets.
assert_eq!(small_u64_format(u64::from(u32::MAX) + 1), None);
assert_eq!(small_u64_format(u64::MAX), None);
}
#[test]
fn test_html_escape() {
let input = "<>&\"'/";
let output = HtmlEscape(input).to_string();
assert_eq!(output, "&lt;&gt;&amp;&quot;&#x27;&#x2f;");
}
#[test]
fn test_unescape() {
assert_eq!(unescape(r"foo\u2603bar").unwrap(), "foo\u{2603}bar");
assert_eq!(unescape(r"\t\b\f\r\n\\\/").unwrap(), "\t\x08\x0c\r\n\\/");
assert_eq!(unescape("foobarbaz").unwrap(), "foobarbaz");
assert_eq!(unescape(r"\ud83d\udca9").unwrap(), "💩");
// Test new escape sequences
assert_eq!(unescape(r"\0").unwrap(), "\0");
assert_eq!(unescape(r"foo\0bar").unwrap(), "foo\0bar");
assert_eq!(unescape(r"\x00").unwrap(), "\0");
assert_eq!(unescape(r"\x42").unwrap(), "B");
assert_eq!(unescape(r"\xab").unwrap(), "\u{ab}");
assert_eq!(unescape(r"foo\x42bar").unwrap(), "fooBbar");
assert_eq!(unescape(r"\x0a").unwrap(), "\n");
assert_eq!(unescape(r"\x0d").unwrap(), "\r");
// Unknown escapes are preserved as-is for Jinja compatibility.
assert_eq!(unescape(r"\s").unwrap(), r"\s");
assert_eq!(unescape(r"\q").unwrap(), r"\q");
assert_eq!(unescape(r"foo\sbar").unwrap(), r"foo\sbar");
assert_eq!(unescape(r"\8").unwrap(), r"\8");
assert_eq!(unescape(r"\9").unwrap(), r"\9");
// Test truncation
assert!(unescape(r"\x").is_err()); // truncated \x
assert!(unescape(r"\x1").is_err()); // truncated \x1
assert!(unescape(r"\x1g").is_err()); // invalid hex digit
assert!(unescape(r"\x1G").is_err()); // invalid hex digit
// Test octal escape sequences
assert_eq!(unescape(r"\0").unwrap(), "\0"); // octal 0 = null
assert_eq!(unescape(r"\1").unwrap(), "\x01"); // octal 1 = SOH
assert_eq!(unescape(r"\12").unwrap(), "\n"); // octal 12 = 10 decimal = LF
assert_eq!(unescape(r"\123").unwrap(), "S"); // octal 123 = 83 decimal = 'S'
assert_eq!(unescape(r"\141").unwrap(), "a"); // octal 141 = 97 decimal = 'a'
assert_eq!(unescape(r"\177").unwrap(), "\x7f"); // octal 177 = 127 decimal = DEL
assert_eq!(unescape(r"foo\123bar").unwrap(), "fooSbar"); // 'S' in the middle
assert_eq!(unescape(r"\101\102\103").unwrap(), "ABC"); // octal for A, B, C
}
#[test]
#[cfg(feature = "builtins")]
fn test_splitn_whitespace() {
fn s(s: &str, n: usize) -> Vec<&str> {
splitn_whitespace(s, n).collect::<Vec<_>>()
}
assert_eq!(s("a b c", 1), vec!["a b c"]);
assert_eq!(s("a b c", 2), vec!["a", "b c"]);
assert_eq!(s("a b c", 2), vec!["a", "b c"]);
assert_eq!(s("a b c ", 2), vec!["a", "b c "]);
assert_eq!(s("a b c", 3), vec!["a", "b", "c"]);
assert_eq!(s("a b c", 4), vec!["a", "b", "c"]);
assert_eq!(s(" a b c", 3), vec!["a", "b", "c"]);
assert_eq!(s(" a b c", 4), vec!["a", "b", "c"]);
}
}
File diff suppressed because it is too large. Load diff
+435
View File
@@ -0,0 +1,435 @@
use std::ops::{Deref, DerefMut};
use serde::de::value::{MapDeserializer, SeqDeserializer};
use serde::de::{
self, Deserialize, DeserializeOwned, DeserializeSeed, Deserializer, EnumAccess,
IntoDeserializer, MapAccess, SeqAccess, Unexpected, VariantAccess, Visitor,
};
use serde::forward_to_deserialize_any;
use crate::value::{ArgType, ObjectRepr, Value, ValueKind, ValueMap, ValueRepr};
use crate::{Error, ErrorKind};
#[cfg_attr(docsrs, doc(cfg(feature = "deserialization")))]
impl<'de> Deserialize<'de> for Value {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(ValueVisitor)
}
}
struct ValueVisitor;
macro_rules! visit_value_primitive {
($name:ident, $ty:ty) => {
fn $name<E: de::Error>(self, v: $ty) -> Result<Value, E> {
Ok(Value::from(v))
}
};
}
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Value;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.write_str("any MiniJinja compatible value")
}
visit_value_primitive!(visit_bool, bool);
visit_value_primitive!(visit_i8, i8);
visit_value_primitive!(visit_i16, i16);
visit_value_primitive!(visit_i32, i32);
visit_value_primitive!(visit_i64, i64);
visit_value_primitive!(visit_i128, i128);
visit_value_primitive!(visit_u16, u16);
visit_value_primitive!(visit_u32, u32);
visit_value_primitive!(visit_u64, u64);
visit_value_primitive!(visit_u128, u128);
visit_value_primitive!(visit_f32, f32);
visit_value_primitive!(visit_f64, f64);
visit_value_primitive!(visit_char, char);
visit_value_primitive!(visit_str, &str);
visit_value_primitive!(visit_string, String);
visit_value_primitive!(visit_bytes, &[u8]);
visit_value_primitive!(visit_byte_buf, Vec<u8>);
fn visit_none<E: de::Error>(self) -> Result<Value, E> {
Ok(Value::from(()))
}
fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Value, D::Error> {
Deserialize::deserialize(deserializer)
}
fn visit_unit<E: de::Error>(self) -> Result<Value, E> {
Ok(Value::from(()))
}
fn visit_newtype_struct<D: Deserializer<'de>>(
self,
deserializer: D,
) -> Result<Value, D::Error> {
Deserialize::deserialize(deserializer)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut visitor: A) -> Result<Value, A::Error> {
let mut rv = Vec::<Value>::new();
while let Some(e) = ok!(visitor.next_element()) {
rv.push(e);
}
Ok(Value::from(rv))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
let mut rv = ValueMap::default();
while let Some((k, v)) = ok!(map.next_entry()) {
rv.insert(k, v);
}
Ok(Value::from_object(rv))
}
}
/// Utility type to deserialize an argument.
///
/// This allows you to directly accept a type that implements [`Deserialize`] as an
/// argument to a filter or test. The type dereferences into the inner type and
/// it also lets you move out the inner type.
///
/// ```rust
/// # use minijinja::Environment;
/// use std::path::PathBuf;
/// use minijinja::value::ViaDeserialize;
///
/// fn dirname(path: ViaDeserialize<PathBuf>) -> String {
/// match path.parent() {
/// Some(parent) => parent.display().to_string(),
/// None => "".to_string()
/// }
/// }
///
/// # let mut env = Environment::new();
/// env.add_filter("dirname", dirname);
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "deserialization")))]
pub struct ViaDeserialize<T: DeserializeOwned>(pub T);
impl<'a, T: DeserializeOwned> ArgType<'a> for ViaDeserialize<T> {
type Output = Self;
fn from_value(value: Option<&'a Value>) -> Result<Self, Error> {
match value {
Some(value) => {
if value.is_kwargs() {
return Err(Error::new(
ErrorKind::InvalidOperation,
"cannot deserialize from kwargs",
));
}
T::deserialize(value).map(ViaDeserialize)
}
None => Err(Error::from(ErrorKind::MissingArgument)),
}
}
}
impl<T: DeserializeOwned> Deref for ViaDeserialize<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T: DeserializeOwned> DerefMut for ViaDeserialize<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
// this is a macro so that we don't accidentally diverge between
// the Value and &Value deserializer
macro_rules! common_forward {
() => {
forward_to_deserialize_any! {
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit
seq bytes byte_buf map
tuple_struct struct tuple ignored_any identifier
}
};
}
#[cfg_attr(docsrs, doc(cfg(feature = "deserialization")))]
impl IntoDeserializer<'_, Error> for Value {
type Deserializer = Value;
fn into_deserializer(self) -> Value {
self
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "deserialization")))]
impl<'de> Deserializer<'de> for Value {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
match self.0 {
ValueRepr::Invalid(ref error) => Err(de::Error::custom(error)),
ValueRepr::Bool(v) => visitor.visit_bool(v),
ValueRepr::U64(v) => visitor.visit_u64(v),
ValueRepr::I64(v) => visitor.visit_i64(v),
ValueRepr::I128(v) => visitor.visit_i128(v.0),
ValueRepr::U128(v) => visitor.visit_u128(v.0),
ValueRepr::F64(v) => visitor.visit_f64(v),
ValueRepr::String(ref v, _) => visitor.visit_str(v),
ValueRepr::SmallStr(v) => visitor.visit_str(v.as_str()),
ValueRepr::Undefined(_) | ValueRepr::None => visitor.visit_unit(),
ValueRepr::Bytes(ref v) => visitor.visit_bytes(v),
ValueRepr::Object(o) => match o.repr() {
ObjectRepr::Plain => Err(de::Error::custom("cannot deserialize plain objects")),
ObjectRepr::Seq | ObjectRepr::Iterable => {
visitor.visit_seq(SeqDeserializer::new(o.try_iter().into_iter().flatten()))
}
ObjectRepr::Map => visitor.visit_map(MapDeserializer::new(
o.try_iter_pairs().into_iter().flatten(),
)),
},
}
}
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
match self.0 {
ValueRepr::None | ValueRepr::Undefined(_) => visitor.visit_unit(),
_ => visitor.visit_some(self),
}
}
fn deserialize_enum<V: Visitor<'de>>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Error> {
let (variant, value) = match self.kind() {
ValueKind::Map => {
let mut iter = ok!(self.try_iter());
let variant = match iter.next() {
Some(v) => v,
None => {
return Err(de::Error::invalid_value(
Unexpected::Map,
&"map with a single key",
))
}
};
if iter.next().is_some() {
return Err(de::Error::invalid_value(
Unexpected::Map,
&"map with a single key",
));
}
let val = self.get_item_opt(&variant);
(variant, val)
}
ValueKind::String => (self, None),
_ => {
return Err(de::Error::invalid_type(
value_to_unexpected(&self),
&"string or map",
))
}
};
visitor.visit_enum(EnumDeserializer { variant, value })
}
#[inline]
fn deserialize_unit_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Error> {
self.deserialize_unit(visitor)
}
#[inline]
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Error> {
visitor.visit_newtype_struct(self)
}
common_forward!();
}
struct EnumDeserializer {
variant: Value,
value: Option<Value>,
}
impl<'de> EnumAccess<'de> for EnumDeserializer {
type Error = Error;
type Variant = VariantDeserializer;
fn variant_seed<V: DeserializeSeed<'de>>(
self,
seed: V,
) -> Result<(V::Value, VariantDeserializer), Error> {
seed.deserialize(self.variant)
.map(|v| (v, VariantDeserializer { value: self.value }))
}
}
struct VariantDeserializer {
value: Option<Value>,
}
impl<'de> VariantAccess<'de> for VariantDeserializer {
type Error = Error;
fn unit_variant(self) -> Result<(), Error> {
match self.value {
Some(value) => Deserialize::deserialize(value),
None => Ok(()),
}
}
fn newtype_variant_seed<T: DeserializeSeed<'de>>(self, seed: T) -> Result<T::Value, Error> {
match self.value {
Some(value) => seed.deserialize(value),
None => Err(de::Error::invalid_type(
Unexpected::UnitVariant,
&"newtype variant",
)),
}
}
fn tuple_variant<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value, Error> {
match self.value.as_ref().and_then(|x| x.as_object()) {
Some(obj) if matches!(obj.repr(), ObjectRepr::Seq) => Deserializer::deserialize_any(
SeqDeserializer::new(obj.try_iter().into_iter().flatten()),
visitor,
),
_ => Err(de::Error::invalid_type(
self.value
.as_ref()
.map_or(Unexpected::Unit, value_to_unexpected),
&"tuple variant",
)),
}
}
fn struct_variant<V: Visitor<'de>>(
self,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Error> {
match self.value.as_ref().map(|x| (x.kind(), x)) {
Some((ValueKind::Map, val)) => Deserializer::deserialize_any(
MapDeserializer::new(
ok!(val.try_iter())
.map(|ref k| (k.clone(), val.get_item(k).unwrap_or_default())),
),
visitor,
),
_ => Err(de::Error::invalid_type(
self.value
.as_ref()
.map_or(Unexpected::Unit, value_to_unexpected),
&"struct variant",
)),
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "deserialization")))]
impl<'de> Deserializer<'de> for &Value {
type Error = Error;
#[inline]
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
self.clone().deserialize_any(visitor)
}
#[inline]
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
self.clone().deserialize_option(visitor)
}
#[inline]
fn deserialize_enum<V: Visitor<'de>>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Error> {
self.clone().deserialize_enum(name, variants, visitor)
}
#[inline]
fn deserialize_unit_struct<V: Visitor<'de>>(
self,
name: &'static str,
visitor: V,
) -> Result<V::Value, Error> {
self.clone().deserialize_unit_struct(name, visitor)
}
#[inline]
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
name: &'static str,
visitor: V,
) -> Result<V::Value, Error> {
self.clone().deserialize_newtype_struct(name, visitor)
}
common_forward!();
}
#[cfg_attr(docsrs, doc(cfg(feature = "deserialization")))]
impl<'de> IntoDeserializer<'de, Error> for &'de Value {
type Deserializer = &'de Value;
fn into_deserializer(self) -> &'de Value {
self
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "deserialization")))]
impl de::Error for Error {
fn custom<T: std::fmt::Display>(msg: T) -> Self {
Error::new(ErrorKind::CannotDeserialize, msg.to_string())
}
}
fn value_to_unexpected(value: &Value) -> Unexpected<'_> {
match value.0 {
ValueRepr::Undefined(_) | ValueRepr::None => Unexpected::Unit,
ValueRepr::Bool(val) => Unexpected::Bool(val),
ValueRepr::U64(val) => Unexpected::Unsigned(val),
ValueRepr::I64(val) => Unexpected::Signed(val),
ValueRepr::F64(val) => Unexpected::Float(val),
ValueRepr::Invalid(_) => Unexpected::Other("<invalid value>"),
ValueRepr::U128(val) => {
let unsigned = val.0 as u64;
if unsigned as u128 == val.0 {
Unexpected::Unsigned(unsigned)
} else {
Unexpected::Other("u128")
}
}
ValueRepr::I128(val) => {
let signed = val.0 as i64;
if signed as i128 == val.0 {
Unexpected::Signed(signed)
} else {
Unexpected::Other("u128")
}
}
ValueRepr::String(ref s, _) => Unexpected::Str(s),
ValueRepr::SmallStr(ref s) => Unexpected::Str(s.as_str()),
ValueRepr::Bytes(ref b) => Unexpected::Bytes(b),
ValueRepr::Object(..) => Unexpected::Other("<dynamic value>"),
}
}
+162
View File
@@ -0,0 +1,162 @@
use std::collections::BTreeSet;
use std::sync::Arc;
use crate::value::ops::LenIterWrap;
use crate::value::{Enumerator, Object, ObjectExt, ObjectRepr, Value, ValueKind};
/// Dictionary merging behavior - create custom object with lookup capability
#[derive(Debug)]
pub struct MergeDict {
values: Box<[Value]>,
}
impl MergeDict {
pub fn new(values: Vec<Value>) -> Self {
Self {
values: values.into_boxed_slice(),
}
}
}
impl Object for MergeDict {
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
// Look up key in reverse order (last matching dict wins)
for value in self.values.iter().rev() {
if let Ok(v) = value.get_item(key) {
if !v.is_undefined() {
return Some(v);
}
}
}
None
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
// Collect all keys from all dictionaries (only include maps)
let keys: BTreeSet<Value> = self
.values
.iter()
.filter(|x| x.kind() == ValueKind::Map)
.filter_map(|v| v.try_iter().ok())
.flatten()
.collect();
Enumerator::Iter(Box::new(keys.into_iter()))
}
}
/// List merging behavior - calculate total length for size hint
#[derive(Debug)]
pub struct MergeSeq {
values: Box<[Value]>,
total_len: Option<usize>,
}
impl MergeSeq {
pub fn new(values: Vec<Value>) -> Self {
Self {
total_len: values.iter().map(|v| v.len()).sum(),
values: values.into_boxed_slice(),
}
}
}
impl Object for MergeSeq {
fn repr(self: &Arc<Self>) -> ObjectRepr {
ObjectRepr::Seq
}
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
if let Some(idx) = key.as_usize() {
let mut current_idx = 0;
for value in self.values.iter() {
let len = value.len().unwrap_or(0);
if idx < current_idx + len {
return value.get_item(&Value::from(idx - current_idx)).ok();
}
current_idx += len;
}
}
None
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
self.mapped_enumerator(|this| {
let iter = this.values.iter().flat_map(|v| match v.try_iter() {
Ok(iter) => Box::new(iter) as Box<dyn Iterator<Item = Value> + Send + Sync>,
Err(err) => Box::new(Some(Value::from(err)).into_iter())
as Box<dyn Iterator<Item = Value> + Send + Sync>,
});
if let Some(total_len) = this.total_len {
Box::new(LenIterWrap(total_len, iter))
} else {
Box::new(iter)
}
})
}
}
/// Utility function to merge multiple maps into a single one.
///
/// If values are passed that are not maps, they are for the most part ignored.
/// They cannot be enumerated, but attribute lookups can still work. That's
/// because [`get_value`](crate::value::Object::get_value) is forwarded through
/// to all objects.
///
/// This is the operation the [`context!`](crate::context) macro uses behind
/// the scenes. The merge is done lazily which means that any dynamic object
/// that behaves like a map can be used here. Note though that the order of
/// this function is inverse to what the macro does.
///
/// ```
/// use minijinja::{context, value::merge_maps};
///
/// let ctx1 = context!{
/// name => "John",
/// age => 30
/// };
///
/// let ctx2 = context!{
/// location => "New York",
/// age => 25 // This will be overridden by ctx1's value
/// };
///
/// let merged = merge_maps([ctx1, ctx2]);
/// ```
pub fn merge_maps<I, V>(iter: I) -> Value
where
I: IntoIterator<Item = V>,
V: Into<Value>,
{
let sources: Vec<Value> = iter.into_iter().map(Into::into).collect();
// if we only have a single source, we can use it directly to avoid making
// an unnecessary indirection.
if sources.len() == 1 {
sources[0].clone()
} else {
Value::from_object(MergeDict::new(sources))
}
}
#[test]
fn test_merge_object() {
use std::collections::BTreeMap;
let o = merge_maps([Value::from("abc"), Value::from(vec![1, 2, 3])]);
assert_eq!(o, Value::from(BTreeMap::<String, String>::new()));
let mut map1 = BTreeMap::new();
map1.insert("a", 1);
map1.insert("b", 2);
let mut map2 = BTreeMap::new();
map2.insert("b", 3);
map2.insert("c", 4);
let merged = merge_maps([Value::from(map1), Value::from(map2)]);
// Check that the merged object contains all keys with expected values
// The value from the latter map should be used when keys overlap
assert_eq!(merged.get_attr("a").unwrap(), Value::from(1));
assert_eq!(merged.get_attr("b").unwrap(), Value::from(3)); // Takes value from map2
assert_eq!(merged.get_attr("c").unwrap(), Value::from(4));
}
+1892
View File
File diff suppressed because it is too large. Load diff
+32
View File
@@ -0,0 +1,32 @@
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use crate::value::{Enumerator, Object, Value};
/// This object exists for the `namespace` function.
///
/// It's special in that it behaves like a dictionary in many ways but it's the only
/// object that can be used with `{% set %}` assignments. This is used internally
/// in the vm via downcasting.
#[derive(Debug, Default)]
pub(crate) struct Namespace {
data: Mutex<BTreeMap<Arc<str>, Value>>,
}
impl Object for Namespace {
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.data.lock().unwrap().get(some!(key.as_str())).cloned()
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
let data = self.data.lock().unwrap();
let keys = data.keys().cloned().map(Value::from);
Enumerator::Values(keys.collect())
}
}
impl Namespace {
pub(crate) fn set_value(&self, key: &str, value: Value) {
self.data.lock().unwrap().insert(key.into(), value);
}
}
+963
View File
@@ -0,0 +1,963 @@
use std::any::Any;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fmt;
use std::hash::Hash;
use std::sync::Arc;
use crate::error::{Error, ErrorKind};
use crate::value::{mapped_enumerator, Value, ValueRepr};
use crate::vm::State;
/// A trait that represents a dynamic object.
///
/// There is a type erased wrapper of this trait available called
/// [`DynObject`] which is what the engine actually holds internally.
///
/// # Basic Struct
///
/// The following example shows how to implement a dynamic object which
/// represents a struct. All that's needed is to implement
/// [`get_value`](Self::get_value) to look up a field by name as well as
/// [`enumerate`](Self::enumerate) to return an enumerator over the known keys.
/// The [`repr`](Self::repr) defaults to `Map` so nothing needs to be done here.
///
/// ```
/// use std::sync::Arc;
/// use minijinja::value::{Value, Object, Enumerator};
///
/// #[derive(Debug)]
/// struct Point(f32, f32, f32);
///
/// impl Object for Point {
/// fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
/// match key.as_str()? {
/// "x" => Some(Value::from(self.0)),
/// "y" => Some(Value::from(self.1)),
/// "z" => Some(Value::from(self.2)),
/// _ => None,
/// }
/// }
///
/// fn enumerate(self: &Arc<Self>) -> Enumerator {
/// Enumerator::Str(&["x", "y", "z"])
/// }
/// }
///
/// let value = Value::from_object(Point(1.0, 2.5, 3.0));
/// ```
///
/// # Basic Sequence
///
/// The following example shows how to implement a dynamic object which
/// represents a sequence. All that's needed is to implement
/// [`repr`](Self::repr) to indicate that this is a sequence,
/// [`get_value`](Self::get_value) to look up a field by index, and
/// [`enumerate`](Self::enumerate) to return a sequential enumerator.
/// This enumerator will automatically call `get_value` from `0..length`.
///
/// ```
/// use std::sync::Arc;
/// use minijinja::value::{Value, Object, ObjectRepr, Enumerator};
///
/// #[derive(Debug)]
/// struct Point(f32, f32, f32);
///
/// impl Object for Point {
/// fn repr(self: &Arc<Self>) -> ObjectRepr {
/// ObjectRepr::Seq
/// }
///
/// fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
/// match key.as_usize()? {
/// 0 => Some(Value::from(self.0)),
/// 1 => Some(Value::from(self.1)),
/// 2 => Some(Value::from(self.2)),
/// _ => None,
/// }
/// }
///
/// fn enumerate(self: &Arc<Self>) -> Enumerator {
/// Enumerator::Seq(3)
/// }
/// }
///
/// let value = Value::from_object(Point(1.0, 2.5, 3.0));
/// ```
///
/// # Iterables
///
/// If you have something that is not quite a sequence but is capable of yielding
/// values over time, you can directly implement an iterable. This is somewhat
/// uncommon as you can normally directly use [`Value::make_iterable`]. Here
/// is how this can be done though:
///
/// ```
/// use std::sync::Arc;
/// use minijinja::value::{Value, Object, ObjectRepr, Enumerator};
///
/// #[derive(Debug)]
/// struct Range10;
///
/// impl Object for Range10 {
/// fn repr(self: &Arc<Self>) -> ObjectRepr {
/// ObjectRepr::Iterable
/// }
///
/// fn enumerate(self: &Arc<Self>) -> Enumerator {
/// Enumerator::Iter(Box::new((1..10).map(Value::from)))
/// }
/// }
///
/// let value = Value::from_object(Range10);
/// ```
///
/// Iteration is encouraged to fail immediately (object is not iterable) or not at
/// all. However this is not always possible, but the iteration interface itself
/// does not support fallible iteration. It is however possible to accomplish the
/// same thing by creating an [invalid value](index.html#invalid-values).
///
/// # Map As Context
///
/// Map can also be used as template rendering context. This has a lot of
/// benefits as it means that the serialization overhead can be largely to
/// completely avoided. This means that even if templates take hundreds of
/// values, MiniJinja does not spend time eagerly converting them into values.
///
/// Here is a very basic example of how a template can be rendered with a dynamic
/// context. Note that the implementation of [`enumerate`](Self::enumerate)
/// is optional for this to work. It's in fact not used by the engine during
/// rendering but it is necessary for the [`debug()`](crate::functions::debug)
/// function to be able to show which values exist in the context.
///
/// ```
/// # fn main() -> Result<(), minijinja::Error> {
/// # use minijinja::Environment;
/// use std::sync::Arc;
/// use minijinja::value::{Value, Object};
///
/// #[derive(Debug)]
/// pub struct DynamicContext {
/// magic: i32,
/// }
///
/// impl Object for DynamicContext {
/// fn get_value(self: &Arc<Self>, field: &Value) -> Option<Value> {
/// match field.as_str()? {
/// #[cfg(not(target_os = "wasi"))]
/// "pid" => Some(Value::from(std::process::id())),
/// #[cfg(target_os = "wasi")]
/// "pid" => Some(Value::from(1234_u32)), // Mock PID for WASI
/// #[cfg(not(target_os = "wasi"))]
/// "env" => Some(Value::from_iter(std::env::vars())),
/// #[cfg(target_os = "wasi")]
/// "env" => Some(Value::from_iter([("HOME".to_string(), "/home/user".to_string())])), // Mock env for WASI
/// "magic" => Some(Value::from(self.magic)),
/// _ => None,
/// }
/// }
/// }
///
/// # let env = Environment::new();
/// let tmpl = env.template_from_str("HOME={{ env.HOME }}; PID={{ pid }}; MAGIC={{ magic }}")?;
/// let ctx = Value::from_object(DynamicContext { magic: 42 });
/// let rv = tmpl.render(ctx)?;
/// # Ok(()) }
/// ```
///
/// One thing of note here is that in the above example `env` would be re-created every
/// time the template needs it. A better implementation would cache the value after it
/// was created first.
pub trait Object: fmt::Debug + Send + Sync {
/// Indicates the natural representation of an object.
///
/// The default implementation returns [`ObjectRepr::Map`].
fn repr(self: &Arc<Self>) -> ObjectRepr {
ObjectRepr::Map
}
/// Given a key, looks up the associated value.
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
let _ = key;
None
}
/// Given a string key, looks up the associated value.
///
/// By default this creates a temporary value and calls [`get_value`](Self::get_value).
/// Implementors can override this to avoid temporary allocations for common
/// string-key lookups.
fn get_value_by_str(self: &Arc<Self>, key: &str) -> Option<Value> {
self.get_value(&Value::from(key))
}
/// Enumerates the object.
///
/// The engine uses the returned enumerator to implement iteration and
/// the size information of an object. For more information see
/// [`Enumerator`]. The default implementation returns `Empty` for
/// all object representations other than [`ObjectRepr::Plain`] which
/// default to `NonEnumerable`.
///
/// When wrapping other objects you might want to consider using
/// [`ObjectExt::mapped_enumerator`] and [`ObjectExt::mapped_rev_enumerator`].
fn enumerate(self: &Arc<Self>) -> Enumerator {
match self.repr() {
ObjectRepr::Plain => Enumerator::NonEnumerable,
ObjectRepr::Iterable | ObjectRepr::Map | ObjectRepr::Seq => Enumerator::Empty,
}
}
/// Returns the length of the enumerator.
///
/// By default the length is taken by calling [`enumerate`](Self::enumerate) and
/// inspecting the [`Enumerator`]. This means that in order to determine
/// the length, an iteration is started. If you think this is a problem for your
/// uses, you can manually implement this. This might for instance be
/// needed if your type can only be iterated over once.
fn enumerator_len(self: &Arc<Self>) -> Option<usize> {
self.enumerate().query_len()
}
/// Returns `true` if this object is considered true for if conditions.
///
/// The default implementation checks if the [`enumerator_len`](Self::enumerator_len)
/// is not `Some(0)` which is the recommended behavior for objects.
fn is_true(self: &Arc<Self>) -> bool {
self.enumerator_len() != Some(0)
}
/// The engine calls this to invoke the object itself.
///
/// The default implementation returns an
/// [`InvalidOperation`](crate::ErrorKind::InvalidOperation) error.
fn call(self: &Arc<Self>, state: &State<'_, '_>, args: &[Value]) -> Result<Value, Error> {
let (_, _) = (state, args);
Err(Error::new(
ErrorKind::InvalidOperation,
"object is not callable",
))
}
/// The engine calls this to invoke a method on the object.
///
/// The default implementation returns an
/// [`UnknownMethod`](crate::ErrorKind::UnknownMethod) error. When this error
/// is returned the engine will invoke the
/// [`unknown_method_callback`](crate::Environment::set_unknown_method_callback) of
/// the environment.
fn call_method(
self: &Arc<Self>,
state: &State<'_, '_>,
method: &str,
args: &[Value],
) -> Result<Value, Error> {
if let Some(value) = self.get_value(&Value::from(method)) {
return value.call(state, args);
}
Err(Error::from(ErrorKind::UnknownMethod))
}
/// Custom comparison of this object against another object of the same type.
///
/// This must return either `None` or `Some(Ordering)`. When implemented this
/// must guarantee a total ordering as otherwise sort functions will crash.
/// This will only compare against other objects of the same type, not
/// anything else. Objects of different types are given an absolute
/// ordering outside the scope of this method.
///
/// The requirement is that an implementer downcasts the other [`DynObject`]
/// to itself, and it that cannot be accomplished `None` must be returned.
///
/// ```rust
/// # use std::sync::Arc;
/// # use std::cmp::Ordering;
/// # use minijinja::value::{DynObject, Object};
/// # #[derive(Debug)]
/// # struct Thing { num: u32 };
/// impl Object for Thing {
/// fn custom_cmp(self: &Arc<Self>, other: &DynObject) -> Option<Ordering> {
/// let other = other.downcast_ref::<Self>()?;
/// Some(self.num.cmp(&other.num))
/// }
/// }
/// ```
fn custom_cmp(self: &Arc<Self>, other: &DynObject) -> Option<Ordering> {
let _ = other;
None
}
/// Formats the object for stringification.
///
/// The default implementation is specific to the behavior of
/// [`repr`](Self::repr) and usually does not need modification.
fn render(self: &Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result
where
Self: Sized + 'static,
{
match self.repr() {
ObjectRepr::Map => {
let mut dbg = f.debug_map();
for (key, value) in self.try_iter_pairs().into_iter().flatten() {
dbg.entry(&key, &value);
}
dbg.finish()
}
// for either sequences or iterables, a length is needed, otherwise we
// don't want to risk iteration during printing and fall back to the
// debug print.
ObjectRepr::Seq | ObjectRepr::Iterable if self.enumerator_len().is_some() => {
let mut dbg = f.debug_list();
for value in self.try_iter().into_iter().flatten() {
dbg.entry(&value);
}
dbg.finish()
}
_ => {
write!(f, "{self:?}")
}
}
}
}
macro_rules! impl_object_helpers {
($vis:vis $self_ty: ty) => {
/// Iterates over this object.
///
/// If this returns `None` then the default object iteration as defined by
/// the object's `enumeration` is used.
$vis fn try_iter(self: $self_ty) -> Option<Box<dyn Iterator<Item = Value> + Send + Sync>>
where
Self: 'static,
{
match self.enumerate() {
Enumerator::NonEnumerable => None,
Enumerator::Empty => Some(Box::new(None::<Value>.into_iter())),
Enumerator::Seq(l) => {
let self_clone = self.clone();
Some(Box::new((0..l).map(move |idx| {
self_clone.get_value(&Value::from(idx)).unwrap_or_default()
})))
}
Enumerator::Iter(iter) => Some(iter),
Enumerator::RevIter(iter) => Some(Box::new(iter)),
Enumerator::Str(s) => Some(Box::new(s.iter().copied().map(Value::from))),
Enumerator::Values(v) => Some(Box::new(v.into_iter())),
}
}
/// Iterate over key and value at once.
$vis fn try_iter_pairs(
self: $self_ty,
) -> Option<Box<dyn Iterator<Item = (Value, Value)> + Send + Sync>> {
let iter = some!(self.try_iter());
let repr = self.repr();
let self_clone = self.clone();
Some(Box::new(iter.enumerate().map(move |(idx, item)| {
match repr {
ObjectRepr::Map => {
let value = self_clone.get_value(&item);
(item, value.unwrap_or_default())
}
_ => (Value::from(idx), item)
}
})))
}
};
}
/// Provides utility methods for working with objects.
pub trait ObjectExt: Object + Send + Sync + 'static {
/// Creates a new iterator enumeration that projects into the given object.
///
/// It takes a method that is passed a reference to `self` and is expected
/// to return an [`Iterator`]. This iterator is then wrapped in an
/// [`Enumerator::Iter`]. This allows one to create an iterator that borrows
/// out of the object.
///
/// # Example
///
/// ```
/// # use std::collections::HashMap;
/// use std::sync::Arc;
/// use minijinja::value::{Value, Object, ObjectExt, Enumerator};
///
/// #[derive(Debug)]
/// struct CustomMap(HashMap<usize, i64>);
///
/// impl Object for CustomMap {
/// fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
/// self.0.get(&key.as_usize()?).copied().map(Value::from)
/// }
///
/// fn enumerate(self: &Arc<Self>) -> Enumerator {
/// self.mapped_enumerator(|this| {
/// Box::new(this.0.keys().copied().map(Value::from))
/// })
/// }
/// }
/// ```
fn mapped_enumerator<F>(self: &Arc<Self>, maker: F) -> Enumerator
where
F: for<'a> FnOnce(&'a Self) -> Box<dyn Iterator<Item = Value> + Send + Sync + 'a>
+ Send
+ Sync
+ 'static,
Self: Sized,
{
mapped_enumerator(self, maker)
}
/// Creates a new reversible iterator enumeration that projects into the given object.
///
/// It takes a method that is passed a reference to `self` and is expected
/// to return a [`DoubleEndedIterator`]. This iterator is then wrapped in an
/// [`Enumerator::RevIter`]. This allows one to create an iterator that borrows
/// out of the object and is reversible.
///
/// # Example
///
/// ```
/// # use std::collections::HashMap;
/// use std::sync::Arc;
/// use std::ops::Range;
/// use minijinja::value::{Value, Object, ObjectExt, ObjectRepr, Enumerator};
///
/// #[derive(Debug)]
/// struct VecView(Vec<usize>);
///
/// impl Object for VecView {
/// fn repr(self: &Arc<Self>) -> ObjectRepr {
/// ObjectRepr::Iterable
/// }
///
/// fn enumerate(self: &Arc<Self>) -> Enumerator {
/// self.mapped_enumerator(|this| {
/// Box::new(this.0.iter().cloned().map(Value::from))
/// })
/// }
/// }
/// ```
fn mapped_rev_enumerator<F>(self: &Arc<Self>, maker: F) -> Enumerator
where
F: for<'a> FnOnce(
&'a Self,
)
-> Box<dyn DoubleEndedIterator<Item = Value> + Send + Sync + 'a>
+ Send
+ Sync
+ 'static,
Self: Sized,
{
// Taken from `mapped_enumerator`.
struct Iter {
iter: Box<dyn DoubleEndedIterator<Item = Value> + Send + Sync + 'static>,
_object: DynObject,
}
impl Iterator for Iter {
type Item = Value;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.iter.size_hint()
}
}
impl DoubleEndedIterator for Iter {
fn next_back(&mut self) -> Option<Self::Item> {
self.iter.next_back()
}
}
// SAFETY: this is safe because the `Iter` will keep our object alive.
let iter = unsafe {
std::mem::transmute::<
Box<dyn DoubleEndedIterator<Item = _>>,
Box<dyn DoubleEndedIterator<Item = _> + Send + Sync>,
>(maker(self))
};
let _object = DynObject::new(self.clone());
Enumerator::RevIter(Box::new(Iter { iter, _object }))
}
impl_object_helpers!(&Arc<Self>);
}
impl<T: Object + Send + Sync + 'static> ObjectExt for T {}
/// Enumerators help define iteration behavior for [`Object`]s.
///
/// When Jinja wants to know the length of an object, if it's empty or
/// not or if it wants to iterate over it, it will ask the [`Object`] to
/// enumerate itself with the [`enumerate`](Object::enumerate) method. The
/// returned enumerator has enough information so that the object can be
/// iterated over, but it does not necessarily mean that iteration actually
/// starts or that it has the data to yield the right values.
///
/// In fact, you should never inspect an enumerator. You can create it or
/// forward it. For actual iteration use [`ObjectExt::try_iter`] etc.
#[non_exhaustive]
pub enum Enumerator {
/// Marks non enumerable objects.
///
/// Such objects cannot be iterated over, the length is unknown which
/// means they are not considered empty by the engine. This is a good
/// choice for plain objects.
///
/// | Iterable | Length |
/// |----------|---------|
/// | no | unknown |
NonEnumerable,
/// The empty enumerator. It yields no elements.
///
/// | Iterable | Length |
/// |----------|-------------|
/// | yes | known (`0`) |
Empty,
/// A slice of static strings.
///
/// This is a useful enumerator to enumerate the attributes of an
/// object or the keys in a string hash map.
///
/// | Iterable | Length |
/// |----------|--------------|
/// | yes | known |
Str(&'static [&'static str]),
/// A dynamic iterator over values.
///
/// The length is known if the [`Iterator::size_hint`] has matching lower
/// and upper bounds. The logic used by the engine is the following:
///
/// ```
/// # let iter = Some(1).into_iter();
/// let len = match iter.size_hint() {
/// (lower, Some(upper)) if lower == upper => Some(lower),
/// _ => None
/// };
/// ```
///
/// Because the engine prefers repeatable iteration, it will keep creating
/// new enumerators every time the iteration should restart. Sometimes
/// that might not always be possible (eg: you stream data in) in which
/// case
///
/// | Iterable | Length |
/// |----------|-----------------|
/// | yes | sometimes known |
Iter(Box<dyn Iterator<Item = Value> + Send + Sync>),
/// Like `Iter` but supports efficient reversing.
///
/// This means that the iterator has to be of type [`DoubleEndedIterator`].
///
/// | Iterable | Length |
/// |----------|-----------------|
/// | yes | sometimes known |
RevIter(Box<dyn DoubleEndedIterator<Item = Value> + Send + Sync>),
/// Indicates sequential iteration.
///
/// This instructs the engine to iterate over an object by enumerating it
/// from `0` to `n` by calling [`Object::get_value`]. This is essentially the
/// way sequences are supposed to be enumerated.
///
/// | Iterable | Length |
/// |----------|-----------------|
/// | yes | known |
Seq(usize),
/// A vector of known values to iterate over.
///
/// The iterator will yield each value in the vector one after another.
///
/// | Iterable | Length |
/// |----------|-----------------|
/// | yes | known |
Values(Vec<Value>),
}
/// Defines the natural representation of this object.
///
/// An [`ObjectRepr`] is a reduced form of
/// [`ValueKind`](crate::value::ValueKind) which only contains value which can
/// be represented by objects. For instance an object can never be a primitive
/// and as such those kinds are unavailable.
///
/// The representation influences how values are serialized, stringified or
/// what kind they report.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum ObjectRepr {
/// An object that has no reasonable representation.
///
/// - **Default Render:** [`Debug`]
/// - **Collection Behavior:** none
/// - **Iteration Behavior:** none
/// - **Serialize:** [`Debug`] / [`render`](Object::render) output as string
Plain,
/// Represents a map or object.
///
/// - **Default Render:** `{key: value,...}` pairs
/// - **Collection Behavior:** looks like a map, can be indexed by key, has a length
/// - **Iteration Behavior:** iterates over keys
/// - **Serialize:** Serializes as map
Map,
/// Represents a sequence (eg: array/list).
///
/// - **Default Render:** `[value,...]`
/// - **Collection Behavior:** looks like a list, can be indexed by index, has a length
/// - **Iteration Behavior:** iterates over values
/// - **Serialize:** Serializes as list
Seq,
/// Represents a non indexable, iterable object.
///
/// - **Default Render:** `[value,...]` (if length is known), `"<iterator>"` otherwise.
/// - **Collection Behavior:** looks like a list if length is known, cannot be indexed
/// - **Iteration Behavior:** iterates over values
/// - **Serialize:** Serializes as list
Iterable,
}
type_erase! {
pub trait Object => DynObject {
fn repr(&self) -> ObjectRepr;
fn get_value(&self, key: &Value) -> Option<Value>;
fn get_value_by_str(&self, key: &str) -> Option<Value>;
fn enumerate(&self) -> Enumerator;
fn is_true(&self) -> bool;
fn enumerator_len(&self) -> Option<usize>;
fn call(
&self,
state: &State<'_, '_>,
args: &[Value]
) -> Result<Value, Error>;
fn call_method(
&self,
state: &State<'_, '_>,
method: &str,
args: &[Value]
) -> Result<Value, Error>;
fn custom_cmp(&self, other: &DynObject) -> Option<Ordering>;
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
impl fmt::Debug {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
}
}
}
unsafe impl Send for DynObject {}
unsafe impl Sync for DynObject {}
impl DynObject {
impl_object_helpers!(pub &Self);
/// Checks if this dyn object is the same as another.
pub(crate) fn is_same_object(&self, other: &DynObject) -> bool {
self.ptr == other.ptr && self.vtable == other.vtable
}
/// Checks if the two dyn objects are of the same type.
pub(crate) fn is_same_object_type(&self, other: &DynObject) -> bool {
self.type_id() == other.type_id()
}
}
impl Hash for DynObject {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if let Some(iter) = self.try_iter_pairs() {
for (key, value) in iter {
key.hash(state);
value.hash(state);
}
}
}
}
impl fmt::Display for DynObject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.render(f)
}
}
impl Enumerator {
fn query_len(&self) -> Option<usize> {
Some(match self {
Enumerator::Empty => 0,
Enumerator::Values(v) => v.len(),
Enumerator::Str(v) => v.len(),
Enumerator::Iter(i) => match i.size_hint() {
(a, Some(b)) if a == b => a,
_ => return None,
},
Enumerator::RevIter(i) => match i.size_hint() {
(a, Some(b)) if a == b => a,
_ => return None,
},
Enumerator::Seq(v) => *v,
Enumerator::NonEnumerable => return None,
})
}
}
macro_rules! impl_value_vec {
($vec_type:ident) => {
impl<T> Object for $vec_type<T>
where
T: Into<Value> + Clone + Send + Sync + fmt::Debug + 'static,
{
fn repr(self: &Arc<Self>) -> ObjectRepr {
ObjectRepr::Seq
}
#[inline(always)]
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.get(some!(key.as_usize())).cloned().map(|v| v.into())
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
Enumerator::Seq(self.len())
}
}
impl<T> From<$vec_type<T>> for Value
where
T: Into<Value> + Clone + Send + Sync + fmt::Debug + 'static,
{
fn from(val: $vec_type<T>) -> Self {
Value::from_object(val)
}
}
};
}
#[allow(unused)]
macro_rules! impl_value_iterable {
($iterable_type:ident, $enumerator:ident) => {
impl<T> Object for $iterable_type<T>
where
T: Into<Value> + Clone + Send + Sync + fmt::Debug + 'static,
{
fn repr(self: &Arc<Self>) -> ObjectRepr {
ObjectRepr::Iterable
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
self.clone()
.$enumerator(|this| Box::new(this.iter().map(|x| x.clone().into())))
}
}
impl<T> From<$iterable_type<T>> for Value
where
T: Into<Value> + Clone + Send + Sync + fmt::Debug + 'static,
{
fn from(val: $iterable_type<T>) -> Self {
Value::from_object(val)
}
}
};
}
macro_rules! impl_str_map_helper {
($map_type:ident, $key_type:ty, $enumerator:ident) => {
impl<V> Object for $map_type<$key_type, V>
where
V: Into<Value> + Clone + Send + Sync + fmt::Debug + 'static,
{
#[inline(always)]
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.get(some!(key.as_str())).cloned().map(|v| v.into())
}
#[inline(always)]
fn get_value_by_str(self: &Arc<Self>, key: &str) -> Option<Value> {
self.get(key).cloned().map(|v| v.into())
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
self.$enumerator(|this| Box::new(this.keys().map(|x| Value::from(x as &str))))
}
fn enumerator_len(self: &Arc<Self>) -> Option<usize> {
Some(self.len())
}
}
};
}
macro_rules! impl_static_str_map_helper {
($map_type:ident, $enumerator:ident) => {
impl<V> Object for $map_type<&'static str, V>
where
V: Into<Value> + Clone + Send + Sync + fmt::Debug + 'static,
{
#[inline(always)]
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.get(some!(key.as_str())).cloned().map(|v| v.into())
}
#[inline(always)]
fn get_value_by_str(self: &Arc<Self>, key: &str) -> Option<Value> {
if self.len() <= 8 {
self.iter().find_map(|(map_key, value)| {
(*map_key == key).then(|| value.clone().into())
})
} else {
self.get(key).cloned().map(|v| v.into())
}
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
self.$enumerator(|this| Box::new(this.keys().map(|x| Value::from(*x))))
}
fn enumerator_len(self: &Arc<Self>) -> Option<usize> {
Some(self.len())
}
}
};
}
macro_rules! impl_str_map {
($map_type:ident, $enumerator:ident) => {
impl_str_map_helper!($map_type, String, $enumerator);
impl_str_map_helper!($map_type, Arc<str>, $enumerator);
impl_static_str_map_helper!($map_type, $enumerator);
impl<V> From<$map_type<String, V>> for Value
where
V: Into<Value> + Send + Sync + Clone + fmt::Debug + 'static,
{
fn from(val: $map_type<String, V>) -> Self {
Value::from_object(val)
}
}
impl<V> From<$map_type<Arc<str>, V>> for Value
where
V: Into<Value> + Send + Sync + Clone + fmt::Debug + 'static,
{
fn from(val: $map_type<Arc<str>, V>) -> Self {
Value::from_object(val)
}
}
impl<'a, V> From<$map_type<&'a str, V>> for Value
where
V: Into<Value> + Send + Sync + Clone + fmt::Debug + 'static,
{
fn from(val: $map_type<&'a str, V>) -> Self {
Value::from(
val.into_iter()
.map(|(k, v)| (Arc::from(k), v))
.collect::<$map_type<Arc<str>, V>>(),
)
}
}
impl<'a, V> From<$map_type<Cow<'a, str>, V>> for Value
where
V: Into<Value> + Send + Sync + Clone + fmt::Debug + 'static,
{
fn from(val: $map_type<Cow<'a, str>, V>) -> Self {
Value::from(
val.into_iter()
.map(|(k, v)| (Arc::from(k), v))
.collect::<$map_type<Arc<str>, V>>(),
)
}
}
};
}
macro_rules! impl_value_map {
($map_type:ident, $enumerator:ident) => {
impl<V> Object for $map_type<Value, V>
where
V: Into<Value> + Clone + Send + Sync + fmt::Debug + 'static,
{
#[inline(always)]
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.get(key).cloned().map(|v| v.into())
}
#[inline(always)]
fn get_value_by_str(self: &Arc<Self>, key: &str) -> Option<Value> {
if self.len() <= 12 {
self.iter().find_map(|(k, v)| match &k.0 {
ValueRepr::String(s, _) if &**s == key => Some(v.clone().into()),
ValueRepr::SmallStr(s) if s.as_str() == key => Some(v.clone().into()),
_ => None,
})
} else {
self.get(&Value::from(key)).cloned().map(|v| v.into())
}
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
self.$enumerator(|this| Box::new(this.keys().cloned()))
}
fn enumerator_len(self: &Arc<Self>) -> Option<usize> {
Some(self.len())
}
}
impl<V> From<$map_type<Value, V>> for Value
where
V: Into<Value> + Send + Sync + Clone + fmt::Debug + 'static,
{
fn from(val: $map_type<Value, V>) -> Self {
Value::from_object(val)
}
}
};
}
impl_value_vec!(Vec);
impl_value_map!(BTreeMap, mapped_rev_enumerator);
impl_str_map!(BTreeMap, mapped_rev_enumerator);
#[cfg(feature = "std_collections")]
mod std_collections_impls {
use super::*;
use std::collections::{BTreeSet, HashMap, HashSet, LinkedList, VecDeque};
impl_value_iterable!(LinkedList, mapped_rev_enumerator);
impl_value_iterable!(HashSet, mapped_enumerator);
impl_value_iterable!(BTreeSet, mapped_rev_enumerator);
impl_str_map!(HashMap, mapped_enumerator);
impl_value_map!(HashMap, mapped_enumerator);
impl_value_vec!(VecDeque);
}
#[cfg(feature = "preserve_order")]
mod preserve_order_impls {
use super::*;
use indexmap::IndexMap;
impl_value_map!(IndexMap, mapped_rev_enumerator);
}
+744
View File
@@ -0,0 +1,744 @@
use crate::error::{Error, ErrorKind};
use crate::value::{DynObject, ObjectRepr, Value, ValueKind, ValueRepr};
const MIN_I128_AS_POS_U128: u128 = 170141183460469231731687303715884105728;
/// Iterator wrapper that provides exact size hints for iterators with known length.
pub(crate) struct LenIterWrap<I: Send + Sync>(pub(crate) usize, pub(crate) I);
impl<I: Iterator<Item = Value> + Send + Sync> Iterator for LenIterWrap<I> {
type Item = Value;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
self.1.next()
}
#[inline(always)]
fn size_hint(&self) -> (usize, Option<usize>) {
(self.0, Some(self.0))
}
}
pub enum CoerceResult<'a> {
I128(i128, i128),
F64(f64, f64),
Str(&'a str, &'a str),
}
pub(crate) fn as_f64(value: &Value, lossy: bool) -> Option<f64> {
macro_rules! checked {
($expr:expr, $ty:ty) => {{
let rv = $expr as f64;
return if lossy || rv as $ty == $expr {
Some(rv)
} else {
None
};
}};
}
Some(match value.0 {
ValueRepr::Bool(x) => x as i64 as f64,
ValueRepr::U64(x) => checked!(x, u64),
ValueRepr::U128(x) => checked!(x.0, u128),
ValueRepr::I64(x) => checked!(x, i64),
ValueRepr::I128(x) => checked!(x.0, i128),
ValueRepr::F64(x) => x,
_ => return None,
})
}
pub fn coerce<'x>(a: &'x Value, b: &'x Value, lossy: bool) -> Option<CoerceResult<'x>> {
match (&a.0, &b.0) {
// equal mappings are trivial
(ValueRepr::U64(a), ValueRepr::U64(b)) => Some(CoerceResult::I128(*a as i128, *b as i128)),
(ValueRepr::U128(a), ValueRepr::U128(b)) => {
Some(CoerceResult::I128(a.0 as i128, b.0 as i128))
}
(ValueRepr::String(a, _), ValueRepr::String(b, _)) => Some(CoerceResult::Str(a, b)),
(ValueRepr::SmallStr(a), ValueRepr::SmallStr(b)) => {
Some(CoerceResult::Str(a.as_str(), b.as_str()))
}
(ValueRepr::SmallStr(a), ValueRepr::String(b, _)) => Some(CoerceResult::Str(a.as_str(), b)),
(ValueRepr::String(a, _), ValueRepr::SmallStr(b)) => Some(CoerceResult::Str(a, b.as_str())),
(ValueRepr::I64(a), ValueRepr::I64(b)) => Some(CoerceResult::I128(*a as i128, *b as i128)),
(ValueRepr::I128(a), ValueRepr::I128(b)) => Some(CoerceResult::I128(a.0, b.0)),
(ValueRepr::F64(a), ValueRepr::F64(b)) => Some(CoerceResult::F64(*a, *b)),
// are floats involved?
(ValueRepr::F64(a), _) => Some(CoerceResult::F64(*a, some!(as_f64(b, lossy)))),
(_, ValueRepr::F64(b)) => Some(CoerceResult::F64(some!(as_f64(a, lossy)), *b)),
// everything else goes up to i128
_ => Some(CoerceResult::I128(
some!(i128::try_from(a.clone()).ok()),
some!(i128::try_from(b.clone()).ok()),
)),
}
}
fn get_offset_and_len<F: FnOnce() -> usize>(
start: Option<i64>,
stop: Option<i64>,
end: F,
) -> (usize, usize) {
let start = start.unwrap_or(0);
if start < 0 || stop.map_or(true, |x| x < 0) {
let end = end();
let start = if start < 0 {
std::cmp::max(0, end as i64 + start) as usize
} else {
start as usize
};
let stop = match stop {
None => end,
Some(x) if x < 0 => std::cmp::max(0, end as i64 + x) as usize,
Some(x) => x as usize,
};
(start, stop.saturating_sub(start))
} else {
(
start as usize,
(stop.unwrap() as usize).saturating_sub(start as usize),
)
}
}
fn range_step_backwards(
start: Option<i64>,
stop: Option<i64>,
step: usize,
end: usize,
) -> impl Iterator<Item = usize> {
let start = match start {
None => end.saturating_sub(1),
Some(start) if start >= end as i64 => end.saturating_sub(1),
Some(start) if start >= 0 => start as usize,
Some(start) => (end as i64 + start).max(0) as usize,
};
let stop = match stop {
None => 0,
Some(stop) if stop < 0 => (end as i64 + stop).max(0) as usize,
Some(stop) => stop as usize,
};
let length = if stop == 0 {
(start + step) / step
} else {
(start - stop + step - 1) / step
};
(stop..=start).rev().step_by(step).take(length)
}
pub fn slice(value: Value, start: Value, stop: Value, step: Value) -> Result<Value, Error> {
let start = if start.is_none() {
None
} else {
Some(ok!(start.try_into()))
};
let stop = if stop.is_none() {
None
} else {
Some(ok!(i64::try_from(stop)))
};
let step = if step.is_none() {
1i64
} else {
ok!(i64::try_from(step))
};
if step == 0 {
return Err(Error::new(
ErrorKind::InvalidOperation,
"cannot slice by step size of 0",
));
}
let kind = value.kind();
let error = Err(Error::new(
ErrorKind::InvalidOperation,
format!("value of type {kind} cannot be sliced"),
));
match value.0 {
ValueRepr::String(..) | ValueRepr::SmallStr(_) => {
let s = value.as_str().unwrap();
if step > 0 {
let (start, len) = get_offset_and_len(start, stop, || s.chars().count());
Ok(Value::from(
s.chars()
.skip(start)
.take(len)
.step_by(step as usize)
.collect::<String>(),
))
} else {
let chars: Vec<char> = s.chars().collect();
Ok(Value::from(
range_step_backwards(start, stop, -step as usize, chars.len())
.map(move |i| chars[i])
.collect::<String>(),
))
}
}
ValueRepr::Bytes(ref b) => {
if step > 0 {
let (start, len) = get_offset_and_len(start, stop, || b.len());
Ok(Value::from_bytes(
b.iter()
.skip(start)
.take(len)
.step_by(step as usize)
.copied()
.collect(),
))
} else {
Ok(Value::from_bytes(
range_step_backwards(start, stop, -step as usize, b.len())
.map(|i| b[i])
.collect::<Vec<u8>>(),
))
}
}
ValueRepr::Undefined(_) | ValueRepr::None => Ok(Value::from(Vec::<Value>::new())),
ValueRepr::Object(obj) if matches!(obj.repr(), ObjectRepr::Seq | ObjectRepr::Iterable) => {
if step > 0 {
let len = obj.enumerator_len().unwrap_or_default();
let (start, len) = get_offset_and_len(start, stop, || len);
Ok(Value::make_object_iterable(obj, move |obj| {
if let Some(iter) = obj.try_iter() {
Box::new(iter.skip(start).take(len).step_by(step as usize))
} else {
Box::new(None.into_iter())
}
}))
} else {
Ok(Value::make_object_iterable(obj.clone(), move |obj| {
if let Some(iter) = obj.try_iter() {
let vec: Vec<Value> = iter.collect();
Box::new(
range_step_backwards(start, stop, -step as usize, vec.len())
.map(move |i| vec[i].clone()),
)
} else {
Box::new(None.into_iter())
}
}))
}
}
_ => error,
}
}
fn int_as_value(val: i128) -> Value {
if val as i64 as i128 == val {
(val as i64).into()
} else {
val.into()
}
}
fn impossible_op(op: &str, lhs: &Value, rhs: &Value) -> Error {
Error::new(
ErrorKind::InvalidOperation,
format!(
"tried to use {} operator on unsupported types {} and {}",
op,
lhs.kind(),
rhs.kind()
),
)
}
fn failed_op(op: &str, lhs: &Value, rhs: &Value) -> Error {
Error::new(
ErrorKind::InvalidOperation,
format!("unable to calculate {lhs} {op} {rhs}"),
)
}
macro_rules! math_binop {
($name:ident, $int:ident, $float:tt) => {
pub fn $name(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
match coerce(lhs, rhs, true) {
Some(CoerceResult::I128(a, b)) => match a.$int(b) {
Some(val) => Ok(int_as_value(val)),
None => Err(failed_op(stringify!($float), lhs, rhs))
},
Some(CoerceResult::F64(a, b)) => Ok((a $float b).into()),
_ => Err(impossible_op(stringify!($float), lhs, rhs))
}
}
}
}
pub fn add(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
if matches!(lhs.kind(), ValueKind::Seq | ValueKind::Iterable)
&& matches!(rhs.kind(), ValueKind::Seq | ValueKind::Iterable)
{
let lhs = lhs.clone();
let rhs = rhs.clone();
return Ok(Value::make_iterable(move || {
if let Ok(lhs) = lhs.try_iter() {
if let Ok(rhs) = rhs.try_iter() {
return Box::new(lhs.chain(rhs))
as Box<dyn Iterator<Item = Value> + Send + Sync>;
}
}
Box::new(None.into_iter()) as Box<dyn Iterator<Item = Value> + Send + Sync>
}));
}
match coerce(lhs, rhs, true) {
Some(CoerceResult::I128(a, b)) => a
.checked_add(b)
.ok_or_else(|| failed_op("+", lhs, rhs))
.map(int_as_value),
Some(CoerceResult::F64(a, b)) => Ok((a + b).into()),
Some(CoerceResult::Str(a, b)) => Ok(Value::from([a, b].concat())),
_ => Err(impossible_op("+", lhs, rhs)),
}
}
math_binop!(sub, checked_sub, -);
math_binop!(rem, checked_rem_euclid, %);
pub fn mul(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
if let Some((s, n)) = lhs
.as_str()
.map(|s| (s, rhs))
.or_else(|| rhs.as_str().map(|s| (s, lhs)))
{
return Ok(Value::from(s.repeat(ok!(n.as_usize().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
"strings can only be multiplied with integers",
)
})))));
} else if let Some((seq, n)) = lhs
.as_object()
.map(|s| (s, rhs))
.or_else(|| rhs.as_object().map(|s| (s, lhs)))
.filter(|x| matches!(x.0.repr(), ObjectRepr::Iterable | ObjectRepr::Seq))
{
return repeat_iterable(n, seq);
}
match coerce(lhs, rhs, true) {
Some(CoerceResult::I128(a, b)) => match a.checked_mul(b) {
Some(val) => Ok(int_as_value(val)),
None => Err(failed_op(stringify!(*), lhs, rhs)),
},
Some(CoerceResult::F64(a, b)) => Ok((a * b).into()),
_ => Err(impossible_op(stringify!(*), lhs, rhs)),
}
}
fn repeat_iterable(n: &Value, seq: &DynObject) -> Result<Value, Error> {
let n = ok!(n.as_usize().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
"sequences and iterables can only be multiplied with integers",
)
}));
let len = ok!(seq.enumerator_len().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
"cannot repeat unsized iterables",
)
}));
// This is not optimal. We only query the enumerator for the length once
// but we support repeated iteration. We could both lie about our length
// here and we could actually deal with an object that changes how much
// data it returns. This is not really permissible so we won't try to
// improve on this here.
Ok(Value::make_object_iterable(seq.clone(), move |seq| {
Box::new(LenIterWrap(
len * n,
(0..n).flat_map(move |_| {
seq.try_iter().unwrap_or_else(|| {
Box::new(
std::iter::repeat(Value::from(Error::new(
ErrorKind::InvalidOperation,
"iterable did not iterate against expectations",
)))
.take(len),
)
})
}),
))
}))
}
pub fn div(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
fn do_it(lhs: &Value, rhs: &Value) -> Option<Value> {
let a = some!(as_f64(lhs, true));
let b = some!(as_f64(rhs, true));
Some((a / b).into())
}
do_it(lhs, rhs).ok_or_else(|| impossible_op("/", lhs, rhs))
}
pub fn int_div(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
match coerce(lhs, rhs, true) {
Some(CoerceResult::I128(a, b)) => {
if b != 0 {
a.checked_div_euclid(b)
.ok_or_else(|| failed_op("//", lhs, rhs))
.map(int_as_value)
} else {
Err(failed_op("//", lhs, rhs))
}
}
Some(CoerceResult::F64(a, b)) => Ok(a.div_euclid(b).into()),
_ => Err(impossible_op("//", lhs, rhs)),
}
}
/// Implements a binary `pow` operation on values.
pub fn pow(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
match coerce(lhs, rhs, true) {
Some(CoerceResult::I128(a, b)) => {
match TryFrom::try_from(b).ok().and_then(|b| a.checked_pow(b)) {
Some(val) => Ok(int_as_value(val)),
None => Err(failed_op("**", lhs, rhs)),
}
}
Some(CoerceResult::F64(a, b)) => Ok((a.powf(b)).into()),
_ => Err(impossible_op("**", lhs, rhs)),
}
}
/// Implements an unary `neg` operation on value.
pub fn neg(val: &Value) -> Result<Value, Error> {
if val.kind() == ValueKind::Number {
match val.0 {
ValueRepr::F64(x) => Ok((-x).into()),
// special case for the largest i128 that can still be
// represented.
ValueRepr::U128(x) if x.0 == MIN_I128_AS_POS_U128 => {
Ok(Value::from(MIN_I128_AS_POS_U128))
}
_ => {
if let Ok(x) = i128::try_from(val.clone()) {
x.checked_mul(-1)
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "overflow"))
.map(int_as_value)
} else {
Err(Error::from(ErrorKind::InvalidOperation))
}
}
}
} else {
Err(Error::from(ErrorKind::InvalidOperation))
}
}
/// Attempts a string concatenation.
pub fn string_concat(left: Value, right: &Value) -> Value {
Value::from(format!("{left}{right}"))
}
/// Implements a containment operation on values.
pub fn contains(container: &Value, value: &Value) -> Result<Value, Error> {
// Special case where if the container is undefined, it cannot hold
// values. For strict containment checks the vm has a special case.
if container.is_undefined() {
return Ok(Value::from(false));
}
let rv = if let Some(s) = container.as_str() {
if let Some(s2) = value.as_str() {
s.contains(s2)
} else {
s.contains(&value.to_string())
}
} else if let ValueRepr::Object(ref obj) = container.0 {
match obj.repr() {
ObjectRepr::Plain => false,
ObjectRepr::Map => obj.get_value(value).is_some(),
ObjectRepr::Seq | ObjectRepr::Iterable => {
obj.try_iter().into_iter().flatten().any(|v| &v == value)
}
}
} else {
return Err(Error::new(
ErrorKind::InvalidOperation,
"cannot perform a containment check on this value",
));
};
Ok(Value::from(rv))
}
#[cfg(test)]
mod tests {
use super::*;
use similar_asserts::assert_eq;
#[test]
fn test_neg() {
let err = neg(&Value::from(i128::MIN)).unwrap_err();
assert_eq!(err.to_string(), "invalid operation: overflow");
}
#[test]
fn test_adding() {
let err = add(&Value::from("a"), &Value::from(42)).unwrap_err();
assert_eq!(
err.to_string(),
"invalid operation: tried to use + operator on unsupported types string and number"
);
assert_eq!(
add(&Value::from(1), &Value::from(2)).unwrap(),
Value::from(3)
);
assert_eq!(
add(&Value::from("foo"), &Value::from("bar")).unwrap(),
Value::from("foobar")
);
let err = add(&Value::from(i128::MAX), &Value::from(1)).unwrap_err();
assert_eq!(
err.to_string(),
"invalid operation: unable to calculate 170141183460469231731687303715884105727 + 1"
);
}
#[test]
fn test_subtracting() {
let err = sub(&Value::from("a"), &Value::from(42)).unwrap_err();
assert_eq!(
err.to_string(),
"invalid operation: tried to use - operator on unsupported types string and number"
);
let err = sub(&Value::from("foo"), &Value::from("bar")).unwrap_err();
assert_eq!(
err.to_string(),
"invalid operation: tried to use - operator on unsupported types string and string"
);
assert_eq!(
sub(&Value::from(2), &Value::from(1)).unwrap(),
Value::from(1)
);
}
#[test]
fn test_dividing() {
let err = div(&Value::from("a"), &Value::from(42)).unwrap_err();
assert_eq!(
err.to_string(),
"invalid operation: tried to use / operator on unsupported types string and number"
);
let err = div(&Value::from("foo"), &Value::from("bar")).unwrap_err();
assert_eq!(
err.to_string(),
"invalid operation: tried to use / operator on unsupported types string and string"
);
assert_eq!(
div(&Value::from(100), &Value::from(2)).unwrap(),
Value::from(50.0)
);
let err = int_div(&Value::from(i128::MIN), &Value::from(-1i128)).unwrap_err();
assert_eq!(
err.to_string(),
"invalid operation: unable to calculate -170141183460469231731687303715884105728 // -1"
);
}
#[test]
fn test_concat() {
assert_eq!(
string_concat(Value::from("foo"), &Value::from(42)),
Value::from("foo42")
);
assert_eq!(
string_concat(Value::from(23), &Value::from(42)),
Value::from("2342")
);
}
#[test]
fn test_slicing() {
let v = Value::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
// [::] - full slice
assert_eq!(
slice(v.clone(), Value::from(()), Value::from(()), Value::from(())).unwrap(),
Value::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
);
// [::2] - every 2nd element
assert_eq!(
slice(v.clone(), Value::from(()), Value::from(()), Value::from(2)).unwrap(),
Value::from(vec![0, 2, 4, 6, 8])
);
// [1:2:2] - slice with start, stop, step
assert_eq!(
slice(v.clone(), Value::from(1), Value::from(2), Value::from(2)).unwrap(),
Value::from(vec![1])
);
// [::-2] - reverse with step of 2
assert_eq!(
slice(v.clone(), Value::from(()), Value::from(()), Value::from(-2)).unwrap(),
Value::from(vec![9, 7, 5, 3, 1])
);
// [:-8:] - from index 0 to -8
assert_eq!(
slice(v.clone(), Value::from(()), Value::from(-8), Value::from(())).unwrap(),
Value::from(vec![0, 1])
);
// [-8::] - from index -8 to the end
assert_eq!(
slice(v.clone(), Value::from(-8), Value::from(()), Value::from(())).unwrap(),
Value::from(vec![2, 3, 4, 5, 6, 7, 8, 9])
);
// [-11::] - from index -11 to the end, which is the same as [::]
// because the start index is before the start of the vector
assert_eq!(
slice(
v.clone(),
Value::from(-11),
Value::from(()),
Value::from(())
)
.unwrap(),
Value::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
);
// [:-11:] - from index -11 to the end, which is the same as [:0:]
// because the end index is before the start of the vector
assert_eq!(
slice(
v.clone(),
Value::from(()),
Value::from(-11),
Value::from(())
)
.unwrap(),
Value::from(Vec::<usize>::new())
);
// [2::-2] - from index 2 to start, reverse with step of 2
assert_eq!(
slice(v.clone(), Value::from(2), Value::from(()), Value::from(-2)).unwrap(),
Value::from(vec![2, 0])
);
// [4:2:-2] - from index 4 to 2, reverse with step of 2
assert_eq!(
slice(v.clone(), Value::from(4), Value::from(2), Value::from(-2)).unwrap(),
Value::from(vec![4])
);
// [8:3:-2] - from index 8 to 3, reverse with step of 2
assert_eq!(
slice(v.clone(), Value::from(8), Value::from(3), Value::from(-2)).unwrap(),
Value::from(vec![8, 6, 4])
);
}
#[test]
fn test_string_slicing() {
let s = Value::from("abcdefghij");
// [::] - full slice
assert_eq!(
slice(s.clone(), Value::from(()), Value::from(()), Value::from(())).unwrap(),
Value::from("abcdefghij")
);
// [::2] - every 2nd character
assert_eq!(
slice(s.clone(), Value::from(()), Value::from(()), Value::from(2)).unwrap(),
Value::from("acegi")
);
// [1:2:2] - slice with start, stop, step
assert_eq!(
slice(s.clone(), Value::from(1), Value::from(2), Value::from(2)).unwrap(),
Value::from("b")
);
// [::-2] - reverse with step of 2
assert_eq!(
slice(s.clone(), Value::from(()), Value::from(()), Value::from(-2)).unwrap(),
Value::from("jhfdb")
);
// [2::-2] - from index 2 to start, reverse with step of 2
assert_eq!(
slice(s.clone(), Value::from(2), Value::from(()), Value::from(-2)).unwrap(),
Value::from("ca")
);
// [4:2:-2] - from index 4 to 2, reverse with step of 2
assert_eq!(
slice(s.clone(), Value::from(4), Value::from(2), Value::from(-2)).unwrap(),
Value::from("e")
);
// [8:3:-2] - from index 8 to 3, reverse with step of 2
assert_eq!(
slice(s.clone(), Value::from(8), Value::from(3), Value::from(-2)).unwrap(),
Value::from("ige")
);
}
#[test]
fn test_bytes_slicing() {
let s = Value::from_bytes(b"abcdefghij".to_vec());
// [::] - full slice
assert_eq!(
slice(s.clone(), Value::from(()), Value::from(()), Value::from(())).unwrap(),
Value::from_bytes(b"abcdefghij".to_vec())
);
// [::2] - every 2nd character
assert_eq!(
slice(s.clone(), Value::from(()), Value::from(()), Value::from(2)).unwrap(),
Value::from_bytes(b"acegi".to_vec())
);
// [1:2:2] - slice with start, stop, step
assert_eq!(
slice(s.clone(), Value::from(1), Value::from(2), Value::from(2)).unwrap(),
Value::from_bytes(b"b".to_vec())
);
// [::-2] - reverse with step of 2
assert_eq!(
slice(s.clone(), Value::from(()), Value::from(()), Value::from(-2)).unwrap(),
Value::from_bytes(b"jhfdb".to_vec())
);
// [2::-2] - from index 2 to start, reverse with step of 2
assert_eq!(
slice(s.clone(), Value::from(2), Value::from(()), Value::from(-2)).unwrap(),
Value::from_bytes(b"ca".to_vec())
);
// [4:2:-2] - from index 4 to 2, reverse with step of 2
assert_eq!(
slice(s.clone(), Value::from(4), Value::from(2), Value::from(-2)).unwrap(),
Value::from_bytes(b"e".to_vec())
);
// [8:3:-2] - from index 8 to 3, reverse with step of 2
assert_eq!(
slice(s.clone(), Value::from(8), Value::from(3), Value::from(-2)).unwrap(),
Value::from_bytes(b"ige".to_vec())
);
}
}
+465
View File
@@ -0,0 +1,465 @@
use std::collections::BTreeMap;
use std::fmt;
use serde::{ser, Serialize, Serializer};
use crate::error::{Error, ErrorKind};
use crate::utils::untrusted_size_hint;
use crate::value::{
value_map_with_capacity, Arc, Enumerator, Object, Packed, Value, ValueMap, ValueRepr,
VALUE_HANDLES, VALUE_HANDLE_MARKER,
};
#[derive(Debug)]
struct StaticKeyMap(Vec<(&'static str, Value)>);
impl Object for StaticKeyMap {
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.get_value_by_str(key.as_str()?)
}
fn get_value_by_str(self: &Arc<Self>, key: &str) -> Option<Value> {
self.0
.iter()
.find_map(|(map_key, value)| (*map_key == key).then(|| value.clone()))
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
Enumerator::Values(self.0.iter().map(|(key, _)| Value::from(*key)).collect())
}
fn enumerator_len(self: &Arc<Self>) -> Option<usize> {
Some(self.0.len())
}
}
#[derive(Debug)]
pub struct InvalidValue(String);
impl std::error::Error for InvalidValue {}
impl fmt::Display for InvalidValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl serde::ser::Error for InvalidValue {
fn custom<T>(msg: T) -> Self
where
T: fmt::Display,
{
InvalidValue(msg.to_string())
}
}
/// Transforms a serializable value to a value object.
///
/// This neither fails nor panics. For objects that cannot be represented
/// the value might be represented as a half broken error object.
#[cold]
fn to_invalid_value(invalid: InvalidValue) -> Value {
Value::from(Error::new(ErrorKind::BadSerialization, invalid.0))
}
pub fn transform<T: Serialize>(value: T) -> Value {
match value.serialize(ValueSerializer) {
Ok(rv) => rv,
Err(invalid) => to_invalid_value(invalid),
}
}
pub struct ValueSerializer;
impl Serializer for ValueSerializer {
type Ok = Value;
type Error = InvalidValue;
type SerializeSeq = SerializeSeq;
type SerializeTuple = SerializeTuple;
type SerializeTupleStruct = SerializeTupleStruct;
type SerializeTupleVariant = SerializeTupleVariant;
type SerializeMap = SerializeMap;
type SerializeStruct = SerializeStruct;
type SerializeStructVariant = SerializeStructVariant;
fn serialize_bool(self, v: bool) -> Result<Value, InvalidValue> {
Ok(ValueRepr::Bool(v).into())
}
fn serialize_i8(self, v: i8) -> Result<Value, InvalidValue> {
Ok(ValueRepr::I64(v as i64).into())
}
fn serialize_i16(self, v: i16) -> Result<Value, InvalidValue> {
Ok(ValueRepr::I64(v as i64).into())
}
fn serialize_i32(self, v: i32) -> Result<Value, InvalidValue> {
Ok(ValueRepr::I64(v as i64).into())
}
fn serialize_i64(self, v: i64) -> Result<Value, InvalidValue> {
Ok(ValueRepr::I64(v).into())
}
fn serialize_i128(self, v: i128) -> Result<Value, InvalidValue> {
Ok(ValueRepr::I128(Packed(v)).into())
}
fn serialize_u8(self, v: u8) -> Result<Value, InvalidValue> {
Ok(ValueRepr::U64(v as u64).into())
}
fn serialize_u16(self, v: u16) -> Result<Value, InvalidValue> {
Ok(ValueRepr::U64(v as u64).into())
}
fn serialize_u32(self, v: u32) -> Result<Value, InvalidValue> {
Ok(ValueRepr::U64(v as u64).into())
}
fn serialize_u64(self, v: u64) -> Result<Value, InvalidValue> {
Ok(ValueRepr::U64(v).into())
}
fn serialize_u128(self, v: u128) -> Result<Value, InvalidValue> {
Ok(ValueRepr::U128(Packed(v)).into())
}
fn serialize_f32(self, v: f32) -> Result<Value, InvalidValue> {
Ok(ValueRepr::F64(v as f64).into())
}
fn serialize_f64(self, v: f64) -> Result<Value, InvalidValue> {
Ok(ValueRepr::F64(v).into())
}
fn serialize_char(self, v: char) -> Result<Value, InvalidValue> {
Ok(Value::from(v))
}
fn serialize_str(self, value: &str) -> Result<Value, InvalidValue> {
Ok(Value::from(value))
}
fn serialize_bytes(self, value: &[u8]) -> Result<Value, InvalidValue> {
Ok(ValueRepr::Bytes(Arc::new(value.to_owned())).into())
}
fn serialize_none(self) -> Result<Value, InvalidValue> {
Ok(ValueRepr::None.into())
}
fn serialize_some<T>(self, value: &T) -> Result<Value, InvalidValue>
where
T: Serialize + ?Sized,
{
Ok(transform(value))
}
fn serialize_unit(self) -> Result<Value, InvalidValue> {
Ok(ValueRepr::None.into())
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<Value, InvalidValue> {
Ok(ValueRepr::None.into())
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<Value, InvalidValue> {
Ok(Value::from(variant))
}
fn serialize_newtype_struct<T>(
self,
_name: &'static str,
value: &T,
) -> Result<Value, InvalidValue>
where
T: Serialize + ?Sized,
{
Ok(transform(value))
}
fn serialize_newtype_variant<T>(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Value, InvalidValue>
where
T: Serialize + ?Sized,
{
let mut map = value_map_with_capacity(1);
map.insert(variant.into(), transform(value));
Ok(Value::from_object(map))
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, InvalidValue> {
Ok(SerializeSeq {
elements: Vec::with_capacity(untrusted_size_hint(len.unwrap_or(0))),
})
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, InvalidValue> {
Ok(SerializeTuple {
elements: Vec::with_capacity(untrusted_size_hint(len)),
})
}
fn serialize_tuple_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, InvalidValue> {
Ok(if name == VALUE_HANDLE_MARKER {
SerializeTupleStruct::Handle(None)
} else {
SerializeTupleStruct::Fields(Vec::with_capacity(untrusted_size_hint(len)))
})
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeTupleVariant, InvalidValue> {
Ok(SerializeTupleVariant {
name: variant,
fields: Vec::with_capacity(untrusted_size_hint(len)),
})
}
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, InvalidValue> {
Ok(SerializeMap {
entries: value_map_with_capacity(len.unwrap_or(0)),
key: None,
})
}
fn serialize_struct(
self,
_name: &'static str,
len: usize,
) -> Result<Self::SerializeStruct, InvalidValue> {
Ok(SerializeStruct {
fields: Vec::with_capacity(untrusted_size_hint(len)),
})
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeStructVariant, InvalidValue> {
Ok(SerializeStructVariant {
variant,
map: value_map_with_capacity(len),
})
}
}
pub struct SerializeSeq {
elements: Vec<Value>,
}
impl ser::SerializeSeq for SerializeSeq {
type Ok = Value;
type Error = InvalidValue;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), InvalidValue>
where
T: Serialize + ?Sized,
{
self.elements.push(transform(value));
Ok(())
}
fn end(self) -> Result<Value, InvalidValue> {
Ok(Value::from_object(self.elements))
}
}
pub struct SerializeTuple {
elements: Vec<Value>,
}
impl ser::SerializeTuple for SerializeTuple {
type Ok = Value;
type Error = InvalidValue;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), InvalidValue>
where
T: Serialize + ?Sized,
{
self.elements.push(transform(value));
Ok(())
}
fn end(self) -> Result<Value, InvalidValue> {
Ok(Value::from_object(self.elements))
}
}
pub enum SerializeTupleStruct {
Handle(Option<u32>),
Fields(Vec<Value>),
}
impl ser::SerializeTupleStruct for SerializeTupleStruct {
type Ok = Value;
type Error = InvalidValue;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), InvalidValue>
where
T: Serialize + ?Sized,
{
match self {
SerializeTupleStruct::Handle(ref mut handle) => {
*handle = transform(value).as_usize().map(|x| x as u32);
}
SerializeTupleStruct::Fields(ref mut fields) => {
fields.push(transform(value));
}
}
Ok(())
}
fn end(self) -> Result<Value, InvalidValue> {
match self {
SerializeTupleStruct::Handle(handle) => VALUE_HANDLES.with(|handles| {
handle
.and_then(|h| handles.borrow_mut().remove(h))
.ok_or_else(|| InvalidValue("value handle not in registry".into()))
}),
SerializeTupleStruct::Fields(fields) => Ok(Value::from_object(fields)),
}
}
}
pub struct SerializeTupleVariant {
name: &'static str,
fields: Vec<Value>,
}
impl ser::SerializeTupleVariant for SerializeTupleVariant {
type Ok = Value;
type Error = InvalidValue;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), InvalidValue>
where
T: Serialize + ?Sized,
{
self.fields.push(transform(value));
Ok(())
}
fn end(self) -> Result<Value, InvalidValue> {
let mut map = value_map_with_capacity(1);
map.insert(self.name.into(), Value::from_object(self.fields));
Ok(Value::from_object(map))
}
}
pub struct SerializeMap {
entries: ValueMap,
key: Option<Value>,
}
impl ser::SerializeMap for SerializeMap {
type Ok = Value;
type Error = InvalidValue;
fn serialize_key<T>(&mut self, key: &T) -> Result<(), InvalidValue>
where
T: Serialize + ?Sized,
{
match key.serialize(ValueSerializer) {
Ok(key) => self.key = Some(key),
Err(_) => self.key = None,
}
Ok(())
}
fn serialize_value<T>(&mut self, value: &T) -> Result<(), InvalidValue>
where
T: Serialize + ?Sized,
{
if let Some(key) = self.key.take() {
self.entries.insert(key, transform(value));
}
Ok(())
}
fn end(self) -> Result<Value, InvalidValue> {
Ok(Value::from_object(self.entries))
}
fn serialize_entry<K, V>(&mut self, key: &K, value: &V) -> Result<(), InvalidValue>
where
K: Serialize + ?Sized,
V: Serialize + ?Sized,
{
if let Ok(key) = key.serialize(ValueSerializer) {
self.entries.insert(key, transform(value));
}
Ok(())
}
}
pub struct SerializeStruct {
fields: Vec<(&'static str, Value)>,
}
impl ser::SerializeStruct for SerializeStruct {
type Ok = Value;
type Error = InvalidValue;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), InvalidValue>
where
T: Serialize + ?Sized,
{
self.fields.push((key, transform(value)));
Ok(())
}
fn end(self) -> Result<Value, InvalidValue> {
let mut fields = self.fields;
fields.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
Ok(Value::from_object(StaticKeyMap(fields)))
}
}
pub struct SerializeStructVariant {
variant: &'static str,
map: ValueMap,
}
impl ser::SerializeStructVariant for SerializeStructVariant {
type Ok = Value;
type Error = InvalidValue;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), InvalidValue>
where
T: Serialize + ?Sized,
{
self.map.insert(key.into(), transform(value));
Ok(())
}
fn end(self) -> Result<Value, InvalidValue> {
let mut rv = BTreeMap::new();
rv.insert(self.variant.to_string(), Value::from_object(self.map));
Ok(rv.into())
}
}
+152
View File
@@ -0,0 +1,152 @@
/// Utility macro that creates a type erased version of a trait.
///
/// This is used in the engine to create a `DynObject` for an `Object`.
/// For the exact use of this look at where the macro is invoked.
macro_rules! type_erase {
($v:vis trait $t_name:ident => $erased_t_name:ident {
$(fn $f:ident(&self $(, $p:ident: $t:ty $(,)?)*) $(-> $r:ty)?;)*
$(
impl $impl_name:path {
$(
fn $f_impl:ident(
&self $(, $p_impl:ident: $t_impl:ty $(,)?)*
) $(-> $r_impl:ty)?;
)*
}
)*
}) => {
#[doc = concat!("Type-erased version of [`", stringify!($t_name), "`]")]
$v struct $erased_t_name {
ptr: *const (),
vtable: *const (),
}
const _: () = {
struct VTable {
$($f: fn(*const (), $($p: $t),*) $(-> $r)?,)*
$($($f_impl: fn(*const (), $($p_impl: $t_impl),*) $(-> $r_impl)?,)*)*
__type_id: fn() -> std::any::TypeId,
__type_name: fn() -> &'static str,
__incref: fn(*const ()),
__decref: fn(*const ()),
}
#[inline(always)]
fn vt(e: &$erased_t_name) -> &VTable {
unsafe { &*(e.vtable as *const VTable) }
}
impl $erased_t_name {
#[doc = concat!("Returns a new boxed, type-erased [`", stringify!($t_name), "`].")]
$v fn new<T: $t_name + 'static>(v: std::sync::Arc<T>) -> Self {
let ptr = std::sync::Arc::into_raw(v) as *const T as *const ();
let vtable = &VTable {
$(
$f: |ptr, $($p),*| unsafe {
let arc = std::mem::ManuallyDrop::new(std::sync::Arc::<T>::from_raw(ptr as *const T));
<T as $t_name>::$f(&arc, $($p),*)
},
)*
$($(
$f_impl: |ptr, $($p_impl),*| unsafe {
let arc = std::mem::ManuallyDrop::new(std::sync::Arc::<T>::from_raw(ptr as *const T));
<T as $impl_name>::$f_impl(&*arc, $($p_impl),*)
},
)*)*
__type_id: || std::any::TypeId::of::<T>(),
__type_name: || std::any::type_name::<T>(),
__incref: |ptr| unsafe {
std::sync::Arc::<T>::increment_strong_count(ptr as *const T);
},
__decref: |ptr| unsafe {
std::sync::Arc::from_raw(ptr as *const T);
},
};
Self { ptr, vtable: vtable as *const VTable as *const () }
}
$(
#[doc = concat!(
"Calls [`", stringify!($t_name), "::", stringify!($f),
"`] of the underlying boxed value."
)]
$v fn $f(&self, $($p: $t),*) $(-> $r)? {
(vt(self).$f)(self.ptr, $($p),*)
}
)*
/// Returns the type name of the concrete underlying type.
$v fn type_name(&self) -> &'static str {
(vt(self).__type_name)()
}
/// Downcast to `T` if the boxed value holds a `T`.
///
/// This works like [`Any::downcast_ref`](std::any::Any#method.downcast_ref).
$v fn downcast_ref<T: 'static>(&self) -> Option<&T> {
if (vt(self).__type_id)() == std::any::TypeId::of::<T>() {
unsafe {
return Some(&*(self.ptr as *const T));
}
}
None
}
/// Downcast to `T` if the boxed value holds a `T`.
///
/// This is similar to [`downcast_ref`](Self::downcast_ref) but returns the [`Arc`].
$v fn downcast<T: 'static>(&self) -> Option<Arc<T>> {
if (vt(self).__type_id)() == std::any::TypeId::of::<T>() {
unsafe {
std::sync::Arc::<T>::increment_strong_count(self.ptr as *const T);
return Some(std::sync::Arc::<T>::from_raw(self.ptr as *const T));
}
}
None
}
/// Checks if the boxed value is a `T`.
///
/// This works like [`Any::is`](std::any::Any#method.is).
$v fn is<T: 'static>(&self) -> bool {
self.downcast::<T>().is_some()
}
}
impl Clone for $erased_t_name {
fn clone(&self) -> Self {
(vt(self).__incref)(self.ptr);
Self {
ptr: self.ptr,
vtable: self.vtable,
}
}
}
impl Drop for $erased_t_name {
fn drop(&mut self) {
(vt(self).__decref)(self.ptr);
}
}
impl<T: $t_name + 'static> From<Arc<T>> for $erased_t_name {
fn from(value: Arc<T>) -> Self {
$erased_t_name::new(value)
}
}
$(
impl $impl_name for $erased_t_name {
$(
fn $f_impl(&self, $($p_impl: $t_impl),*) $(-> $r_impl)? {
(vt(self).$f_impl)(self.ptr, $($p_impl),*)
}
)*
}
)*
};
};
}
+1
View File
@@ -0,0 +1 @@
pub(crate) mod self_cell;
+201
View File
@@ -0,0 +1,201 @@
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
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+156
View File
@@ -0,0 +1,156 @@
#![allow(unused_imports, unused_mut)]
// NOTE: this is a vendored and intentionally pruned subset of upstream
// self_cell, kept to the API surface MiniJinja currently needs.
//! Minimal, vendored subset of `self_cell` used by MiniJinja.
//!
//! We only keep the functionality that is currently exercised in this crate:
//! `try_new`, `borrow_dependent` (covariant cells), and `with_dependent_mut`.
#[doc(hidden)]
pub(crate) extern crate alloc;
#[doc(hidden)]
pub(crate) mod unsafe_self_cell;
/// Declares a self-referential struct with the subset MiniJinja uses.
///
/// Generated API:
///
/// ```ignore
/// fn try_new<Err>(
/// owner: $Owner,
/// dependent_builder: impl for<'a> FnOnce(&'a $Owner) -> Result<$Dependent<'a>, Err>
/// ) -> Result<Self, Err>
///
/// fn borrow_dependent<'a>(&'a self) -> &'a $Dependent<'a> // covariant only
///
/// fn with_dependent_mut<'outer_fn, Ret>(
/// &'outer_fn mut self,
/// func: impl for<'a> FnOnce(&'a $Owner, &'outer_fn mut $Dependent<'a>) -> Ret
/// ) -> Ret
/// ```
///
macro_rules! self_cell {
(
$(#[$StructMeta:meta])*
$Vis:vis struct $StructName:ident $(<$OwnerLifetime:lifetime>)? {
owner: $Owner:ty,
#[covariant]
dependent: $Dependent:ident,
}
) => {
#[repr(transparent)]
$(#[$StructMeta])*
$Vis struct $StructName $(<$OwnerLifetime>)? {
unsafe_self_cell: $crate::vendor::self_cell::unsafe_self_cell::UnsafeSelfCell<
$StructName$(<$OwnerLifetime>)?,
$Owner,
$Dependent<'static>
>,
$(owner_marker: ::core::marker::PhantomData<&$OwnerLifetime ()> ,)?
}
#[allow(dead_code)]
impl <$($OwnerLifetime)?> $StructName <$($OwnerLifetime)?> {
$crate::vendor::self_cell::_self_cell_try_new!($Vis, $Owner $(=> $OwnerLifetime)?, $Dependent);
/// Calls given closure `func` with an unique reference to dependent.
$Vis fn with_dependent_mut<'outer_fn, Ret>(
&'outer_fn mut self,
func: impl for<'_q> ::core::ops::FnOnce(&'_q $Owner, &'outer_fn mut $Dependent<'_q>) -> Ret
) -> Ret {
let (owner, dependent) = unsafe {
self.unsafe_self_cell.borrow_mut()
};
func(owner, dependent)
}
/// Borrows dependent.
$Vis fn borrow_dependent<'_q>(&'_q self) -> &'_q $Dependent<'_q> {
fn _assert_covariance<'x: 'y, 'y>(x: &'y $Dependent<'x>) -> &'y $Dependent<'y> {
x
}
unsafe { self.unsafe_self_cell.borrow_dependent() }
}
}
impl $(<$OwnerLifetime>)? Drop for $StructName $(<$OwnerLifetime>)? {
fn drop(&mut self) {
unsafe {
self.unsafe_self_cell.drop_joined::<$Dependent>();
}
}
}
};
}
#[doc(hidden)]
macro_rules! _self_cell_try_new {
($Vis:vis, $Owner:ty $(=> $OwnerLifetime:lifetime)?, $Dependent:ident) => {
/// Constructs a new self-referential struct or returns an error.
///
/// Consumes owner on error.
$Vis fn try_new<Err>(
owner: $Owner,
dependent_builder:
impl for<'_q> ::core::ops::FnOnce(&'_q $Owner) -> ::core::result::Result<$Dependent<'_q>, Err>
) -> ::core::result::Result<Self, Err> {
type JoinedCell<'_q $(, $OwnerLifetime)?> =
$crate::vendor::self_cell::unsafe_self_cell::JoinedCell<$Owner, $Dependent<'_q>>;
// unsafe placed here to make sure the body macro can't be abused.
unsafe {
$crate::vendor::self_cell::_self_cell_try_new_body!(JoinedCell, owner $(=> $OwnerLifetime)?, dependent_builder)
}
}
};
}
#[doc(hidden)]
macro_rules! _self_cell_try_new_body {
($JoinedCell:ty, $owner:expr $(=> $OwnerLifetime:lifetime)?, $dependent_builder:expr) => {{
// Allocate and initialize the owner/dependent pair in one cell.
let layout = $crate::vendor::self_cell::alloc::alloc::Layout::new::<$JoinedCell>();
assert!(layout.size() != 0);
let joined_void_ptr = ::core::ptr::NonNull::new($crate::vendor::self_cell::alloc::alloc::alloc(layout)).unwrap();
let joined_ptr = joined_void_ptr.cast::<$JoinedCell>();
let (owner_ptr, dependent_ptr) = <$JoinedCell>::_field_pointers(joined_ptr.as_ptr());
// Move owner into newly allocated space.
owner_ptr.write($owner);
// Drop guard that cleans up should building the dependent panic.
let drop_guard =
$crate::vendor::self_cell::unsafe_self_cell::OwnerAndCellDropGuard::new(joined_ptr);
match $dependent_builder(&*owner_ptr) {
::core::result::Result::Ok(dependent) => {
dependent_ptr.write(dependent);
::core::mem::forget(drop_guard);
::core::result::Result::Ok(Self {
unsafe_self_cell: $crate::vendor::self_cell::unsafe_self_cell::UnsafeSelfCell::new(
joined_void_ptr,
),
$(owner_marker: ::core::marker::PhantomData::<&$OwnerLifetime ()> ,)?
})
}
::core::result::Result::Err(err) => ::core::result::Result::Err(err)
}
}}
}
pub(crate) use _self_cell_try_new;
pub(crate) use _self_cell_try_new_body;
pub(crate) use self_cell;
@@ -0,0 +1,164 @@
#![allow(clippy::missing_safety_doc, clippy::needless_lifetimes)]
use core::marker::PhantomData;
use core::ptr::{drop_in_place, NonNull};
extern crate alloc;
use alloc::alloc::{dealloc, Layout};
// Self referential structs are currently not supported with safe vanilla Rust.
// The only reasonable safe alternative is to expect the user to juggle 2 separate
// data structures which is a mess. The library solution rental is both no longer
// maintained and really heavy to compile. So begrudgingly I rolled my own version.
// These are some of the core invariants we require for this to be safe to use.
//
// 1. owner is initialized when UnsafeSelfCell is constructed.
// 2. owner is NEVER changed again.
// 3. The pointer to owner and dependent never changes, even when moved.
// 4. The only access to owner and dependent is as immutable reference.
// 5. owner lives longer than dependent.
#[doc(hidden)]
pub struct JoinedCell<Owner, Dependent> {
pub owner: Owner,
pub dependent: Dependent,
}
// Library controlled struct that marks all accesses as unsafe.
// Because the macro generated struct impl can be extended, could be unsafe.
#[doc(hidden)]
pub struct UnsafeSelfCell<ContainedIn, Owner, DependentStatic: 'static> {
joined_void_ptr: NonNull<u8>,
// ContainedIn is necessary for type safety since we don't fully
// prohibit access to the UnsafeSelfCell; swapping between different
// structs can be unsafe otherwise, see Issue #17.
contained_in_marker: PhantomData<ContainedIn>,
owner_marker: PhantomData<Owner>,
// DependentStatic is only used to correctly derive Send and Sync.
dependent_marker: PhantomData<DependentStatic>,
}
impl<ContainedIn, Owner, DependentStatic> UnsafeSelfCell<ContainedIn, Owner, DependentStatic> {
pub unsafe fn new(joined_void_ptr: NonNull<u8>) -> Self {
Self {
joined_void_ptr,
contained_in_marker: PhantomData,
owner_marker: PhantomData,
dependent_marker: PhantomData,
}
}
// Calling any of these *unsafe* functions with the wrong Dependent type is UB.
pub unsafe fn borrow_dependent<'a, Dependent>(&'a self) -> &'a Dependent {
let joined_ptr = self.joined_void_ptr.cast::<JoinedCell<Owner, Dependent>>();
&(*joined_ptr.as_ptr()).dependent
}
pub unsafe fn borrow_mut<'a, Dependent>(&'a mut self) -> (&'a Owner, &'a mut Dependent) {
let joined_ptr = self.joined_void_ptr.cast::<JoinedCell<Owner, Dependent>>();
// This function used to return `&'a mut JoinedCell<Owner, Dependent>`.
// It now creates two references to the fields instead to avoid claiming mutable access
// to the whole `JoinedCell` (including the owner!) here.
(
&(*joined_ptr.as_ptr()).owner,
&mut (*joined_ptr.as_ptr()).dependent,
)
}
// Any subsequent use of this struct other than dropping it is UB.
pub unsafe fn drop_joined<Dependent>(&mut self) {
let joined_ptr = self.joined_void_ptr.cast::<JoinedCell<Owner, Dependent>>();
// Also used in case drop_in_place(...dependent) fails
let _guard = OwnerAndCellDropGuard { joined_ptr };
// IMPORTANT dependent must be dropped before owner.
// We don't want to rely on an implicit order of struct fields.
// So we drop the struct, field by field manually.
drop_in_place(&mut (*joined_ptr.as_ptr()).dependent);
// Dropping owner
// and deallocating
// due to _guard at end of scope.
}
}
unsafe impl<ContainedIn, Owner, DependentStatic> Send
for UnsafeSelfCell<ContainedIn, Owner, DependentStatic>
where
// Only derive Send if Owner and DependentStatic is also Send
Owner: Send,
DependentStatic: Send,
{
}
unsafe impl<ContainedIn, Owner, DependentStatic> Sync
for UnsafeSelfCell<ContainedIn, Owner, DependentStatic>
where
// Only derive Sync if Owner and DependentStatic is also Sync
Owner: Sync,
DependentStatic: Sync,
{
}
// This struct is used to safely deallocate only the owner if dependent
// construction fails.
//
// mem::forget it once it's no longer needed or dtor will be UB.
#[doc(hidden)]
pub struct OwnerAndCellDropGuard<Owner, Dependent> {
joined_ptr: NonNull<JoinedCell<Owner, Dependent>>,
}
impl<Owner, Dependent> OwnerAndCellDropGuard<Owner, Dependent> {
pub unsafe fn new(joined_ptr: NonNull<JoinedCell<Owner, Dependent>>) -> Self {
Self { joined_ptr }
}
}
impl<Owner, Dependent> Drop for OwnerAndCellDropGuard<Owner, Dependent> {
fn drop(&mut self) {
struct DeallocGuard {
ptr: *mut u8,
layout: Layout,
}
impl Drop for DeallocGuard {
fn drop(&mut self) {
unsafe { dealloc(self.ptr, self.layout) }
}
}
// Deallocate even when the drop_in_place(...owner) panics
let _guard = DeallocGuard {
ptr: self.joined_ptr.as_ptr() as *mut u8,
layout: Layout::new::<JoinedCell<Owner, Dependent>>(),
};
unsafe {
// We must only drop owner and the struct itself,
// The whole point of this drop guard is to clean up the partially
// initialized struct should building the dependent fail.
drop_in_place(&mut (*self.joined_ptr.as_ptr()).owner);
}
// Deallocation happens at end of scope
}
}
// Because of 'procedural macros cannot expand to macro definitions' we wrap
// field pointer acquisition in a helper function.
impl<Owner, Dependent> JoinedCell<Owner, Dependent> {
#[doc(hidden)]
pub unsafe fn _field_pointers(this: *mut Self) -> (*mut Owner, *mut Dependent) {
let owner_ptr = core::ptr::addr_of_mut!((*this).owner);
let dependent_ptr = core::ptr::addr_of_mut!((*this).dependent);
(owner_ptr, dependent_ptr)
}
}
+73
View File
@@ -0,0 +1,73 @@
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use crate::value::{Enumerator, Object, Value};
/// Closure cycle breaker utility.
///
/// The closure tracker is a crude way to forcefully break the cycles
/// caused by closures on teardown of the state. Whenever a closure is
/// exposed to the engine it's also passed to the [`track_closure`] function.
#[derive(Default)]
pub(crate) struct ClosureTracker {
closures: Mutex<Vec<Arc<Closure>>>,
}
impl ClosureTracker {
/// This accepts a closure as value and registers it in the
/// tracker for cycle breaking.
pub(crate) fn track_closure(&self, closure: Arc<Closure>) {
self.closures.lock().unwrap().push(closure);
}
}
impl Drop for ClosureTracker {
fn drop(&mut self) {
for closure in self.closures.lock().unwrap().iter() {
closure.clear();
}
}
}
/// Utility to enclose values for macros.
///
/// See `closure` on the [`Frame`] for how it's used.
#[derive(Debug, Default)]
pub(crate) struct Closure {
values: Mutex<BTreeMap<Arc<str>, Value>>,
}
impl Closure {
/// Stores a value by key in the closure.
pub fn store(&self, key: &str, value: Value) {
self.values.lock().unwrap().insert(Arc::from(key), value);
}
/// Upset a value into the closure.
#[cfg(feature = "macros")]
pub fn store_if_missing<F: FnOnce() -> Value>(&self, key: &str, f: F) {
let mut values = self.values.lock().unwrap();
if !values.contains_key(key) {
values.insert(Arc::from(key), f());
}
}
/// Clears the closure.
///
/// This is required to break cycles.
pub fn clear(&self) {
self.values.lock().unwrap().clear();
}
}
impl Object for Closure {
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.values.lock().unwrap().get(key.as_str()?).cloned()
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
let values = self.values.lock().unwrap();
let keys = values.keys().cloned().map(Value::from);
Enumerator::Values(keys.collect())
}
}
+361
View File
@@ -0,0 +1,361 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, HashSet};
use std::fmt;
#[cfg(feature = "macros")]
use std::sync::Arc;
use crate::environment::Environment;
use crate::error::{Error, ErrorKind};
use crate::value::Value;
use crate::vm::loop_object::LoopState;
#[cfg(feature = "macros")]
use crate::vm::closure_object::Closure;
type Locals<'env> = BTreeMap<&'env str, Value>;
pub(crate) struct Frame<'env> {
pub(crate) locals: Locals<'env>,
pub(crate) ctx: Value,
pub(crate) current_loop: Option<LoopState>,
// normally a frame does not carry a closure, but it can when a macro is
// declared. Once that happens, all writes to the frames locals are also
// duplicated into the closure. Macros declared on that level, then share
// the closure object to enclose the parent values. This emulates the
// behavior of closures in Jinja2.
#[cfg(feature = "macros")]
pub(crate) closure: Option<Arc<Closure>>,
}
impl<'env> Default for Frame<'env> {
fn default() -> Frame<'env> {
Frame::new(Value::UNDEFINED)
}
}
impl<'env> Frame<'env> {
/// Creates a new frame with the given context and no validation
pub fn new(ctx: Value) -> Frame<'env> {
Frame {
locals: Locals::new(),
ctx,
current_loop: None,
#[cfg(feature = "macros")]
closure: None,
}
}
/// Creates a new frame with the given context and validates the value is not invalid
pub fn new_checked(root: Value) -> Result<Frame<'env>, Error> {
Ok(Frame::new(ok!(root.validate())))
}
}
#[cfg(feature = "internal_debug")]
impl fmt::Debug for Frame<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut m = f.debug_map();
m.entry(&"locals", &self.locals);
if let Some(LoopState {
object: ref controller,
..
}) = self.current_loop
{
m.entry(&"loop", controller);
}
if !self.ctx.is_undefined() {
m.entry(&"ctx", &self.ctx);
}
m.finish()
}
}
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub(crate) struct Stack {
values: Vec<Value>,
}
impl Default for Stack {
fn default() -> Stack {
Stack {
values: Vec::with_capacity(24),
}
}
}
impl Stack {
pub fn push(&mut self, arg: Value) {
self.values.push(arg);
}
#[track_caller]
pub fn pop(&mut self) -> Value {
self.values.pop().unwrap()
}
pub fn reverse_top(&mut self, n: usize) {
let start = self.values.len() - n;
self.values[start..].reverse();
}
pub fn get_call_args(&mut self, n: Option<u16>) -> &[Value] {
let n = match n {
Some(n) => n as usize,
None => self.pop().as_usize().unwrap(),
};
&self.values[self.values.len() - n..]
}
pub fn drop_top(&mut self, n: usize) {
self.values.truncate(self.values.len() - n);
}
pub fn try_pop(&mut self) -> Option<Value> {
self.values.pop()
}
#[track_caller]
pub fn peek(&self) -> &Value {
self.values.last().unwrap()
}
}
impl From<Vec<Value>> for Stack {
fn from(values: Vec<Value>) -> Stack {
Stack { values }
}
}
pub(crate) struct Context<'env> {
env: &'env Environment<'env>,
stack: Vec<Frame<'env>>,
outer_stack_depth: usize,
recursion_limit: usize,
}
impl fmt::Debug for Context<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut vars = Vec::from_iter(self.known_variables(false));
vars.sort();
f.debug_map()
.entries(vars.into_iter().map(|key| {
let value = self.load(&key).unwrap_or_default();
(key, value)
}))
.finish()
}
}
impl<'env> Context<'env> {
/// Creates an empty context.
pub fn new(env: &'env Environment<'env>) -> Context<'env> {
Context {
env,
stack: Vec::with_capacity(40),
outer_stack_depth: 0,
recursion_limit: env.recursion_limit(),
}
}
/// Creates a context
pub fn new_with_frame(env: &'env Environment<'env>, frame: Frame<'env>) -> Context<'env> {
let mut rv = Context::new(env);
rv.stack.push(frame);
rv
}
/// The env
#[inline(always)]
pub fn env(&self) -> &'env Environment<'env> {
self.env
}
/// Stores a variable in the context.
pub fn store(&mut self, key: &'env str, value: Value) {
let top = self.stack.last_mut().unwrap();
#[cfg(feature = "macros")]
{
if let Some(ref closure) = top.closure {
closure.store(key, value.clone());
}
}
top.locals.insert(key, value);
}
/// Adds a value to a closure if missing.
///
/// All macros declare on a certain level reuse the same closure. This is done
/// to emulate the behavior of how scopes work in Jinja2 in Python. The
/// unfortunate downside is that this has to be done with a `Mutex`.
#[cfg(feature = "macros")]
pub fn enclose(&mut self, key: &str) {
self.stack
.last_mut()
.unwrap()
.closure
.as_mut()
.unwrap()
.clone()
.store_if_missing(key, || self.load(key).unwrap_or(Value::UNDEFINED));
}
/// Loads the closure and returns it.
#[cfg(feature = "macros")]
pub fn closure(&mut self) -> Option<&Arc<Closure>> {
self.stack.last_mut().unwrap().closure.as_ref()
}
/// Temporarily takes the closure.
///
/// This is done because includes are in the same scope as the module that
/// triggers the import, but we do not want to allow closures to be modified
/// from another file as this would be very confusing.
///
/// This means that if you override a variable referenced by a macro after
/// including in the parent template, it will not override the value seen by
/// the macro.
#[cfg(all(feature = "multi_template", feature = "macros"))]
pub fn take_closure(&mut self) -> Option<Arc<Closure>> {
self.stack.last_mut().unwrap().closure.take()
}
/// Puts the closure back.
#[cfg(feature = "macros")]
pub fn reset_closure(&mut self, closure: Option<Arc<Closure>>) {
self.stack.last_mut().unwrap().closure = closure;
}
/// Return the base context value
#[cfg(feature = "macros")]
pub fn clone_base(&self) -> Value {
self.stack
.first()
.map(|x| x.ctx.clone())
.unwrap_or_default()
}
/// Looks up a variable in the context.
pub fn load(&self, key: &str) -> Option<Value> {
for frame in self.stack.iter().rev() {
// look at locals first
if let Some(value) = frame.locals.get(key) {
return Some(value.clone());
}
// if we are a loop, check if we are looking up the special loop var.
if let Some(ref l) = frame.current_loop {
if l.with_loop_var && key == "loop" {
return Some(Value::from_dyn_object(l.object.clone()));
}
}
// perform a fast lookup. This one will not produce errors if the
// context is undefined or of the wrong type.
if let Some(rv) = frame.ctx.get_attr_fast(key) {
return Some(rv);
}
}
self.env.get_global(key)
}
/// Returns an iterable of all declared variables.
pub fn known_variables(&self, with_globals: bool) -> HashSet<Cow<'_, str>> {
let mut seen = HashSet::<Cow<'_, str>>::new();
for frame in self.stack.iter().rev() {
for key in frame.locals.keys() {
if !seen.contains(&Cow::Borrowed(*key)) {
seen.insert(Cow::Borrowed(key));
}
}
if let Some(ref l) = frame.current_loop {
if l.with_loop_var && !seen.contains("loop") {
seen.insert(Cow::Borrowed("loop"));
}
}
if let Ok(iter) = frame.ctx.try_iter() {
for key in iter {
if let Some(str_key) = key.as_str() {
if !seen.contains(&Cow::Borrowed(str_key))
&& frame.ctx.get_item(&key).is_ok()
{
seen.insert(Cow::Owned(str_key.to_owned()));
}
}
}
}
}
if with_globals {
seen.extend(self.env.globals().map(|x| Cow::Borrowed(x.0)));
}
seen
}
/// Pushes a new layer.
pub fn push_frame(&mut self, layer: Frame<'env>) -> Result<(), Error> {
ok!(self.check_depth());
self.stack.push(layer);
Ok(())
}
/// Pops the topmost layer.
#[track_caller]
pub fn pop_frame(&mut self) -> Frame<'env> {
self.stack.pop().unwrap()
}
/// Returns the root locals (exports)
#[track_caller]
pub fn exports(&self) -> &Locals<'env> {
&self.stack.first().unwrap().locals
}
/// Returns the current locals mutably.
#[track_caller]
#[cfg(feature = "multi_template")]
pub fn current_locals_mut(&mut self) -> &mut Locals<'env> {
&mut self.stack.last_mut().unwrap().locals
}
/// Returns the current innermost loop state.
pub fn current_loop(&mut self) -> Option<&mut LoopState> {
self.stack
.iter_mut()
.rev()
.filter_map(|x| x.current_loop.as_mut())
.next()
}
/// The real depth of the context.
pub fn depth(&self) -> usize {
self.outer_stack_depth + self.stack.len()
}
/// Increase the stack depth.
#[allow(unused)]
pub fn incr_depth(&mut self, delta: usize) -> Result<(), Error> {
self.outer_stack_depth += delta;
ok!(self.check_depth());
Ok(())
}
/// Decrease the stack depth.
#[allow(unused)]
pub fn decr_depth(&mut self, delta: usize) {
self.outer_stack_depth -= delta;
}
fn check_depth(&self) -> Result<(), Error> {
if self.depth() > self.recursion_limit {
return Err(Error::new(
ErrorKind::InvalidOperation,
"recursion limit exceeded",
));
}
Ok(())
}
}
+70
View File
@@ -0,0 +1,70 @@
use crate::compiler::instructions::Instruction;
use crate::error::{Error, ErrorKind};
use std::sync::atomic::{AtomicIsize, Ordering};
use std::sync::Arc;
/// Helper for tracking fuel consumption
pub struct FuelTracker {
// The initial fuel level.
initial: u64,
// This should be an AtomicI64 but sadly 32bit targets do not necessarily have
// AtomicI64 available.
remaining: AtomicIsize,
}
impl FuelTracker {
/// Creates a new fuel tracker.
///
/// The fuel tracker is always wrapped in an `Arc` so that it can be
/// shared across nested invocations of the template evaluation.
pub fn new(fuel: u64) -> Arc<FuelTracker> {
Arc::new(FuelTracker {
initial: fuel,
remaining: AtomicIsize::new(fuel as isize),
})
}
/// Tracks an instruction. If it runs out of fuel an error is returned.
pub fn track(&self, instr: &Instruction) -> Result<(), Error> {
let fuel_to_consume = fuel_for_instruction(instr);
if fuel_to_consume != 0 {
let old_fuel = self.remaining.fetch_sub(fuel_to_consume, Ordering::Relaxed);
if old_fuel - fuel_to_consume <= 0 {
return Err(Error::from(ErrorKind::OutOfFuel));
}
}
Ok(())
}
/// Returns the remaining fuel.
pub fn remaining(&self) -> u64 {
self.remaining.load(Ordering::Relaxed) as _
}
/// Returns the consumed fuel.
pub fn consumed(&self) -> u64 {
self.initial.saturating_sub(self.remaining())
}
}
/// How much fuel does an instruction consume?
fn fuel_for_instruction(instruction: &Instruction) -> isize {
match instruction {
Instruction::BeginCapture(_)
| Instruction::PushLoop(_)
| Instruction::PushDidNotIterate
| Instruction::PushWith
| Instruction::PopFrame
| Instruction::PopLoopFrame
| Instruction::DupTop
| Instruction::DiscardTop
| Instruction::PushAutoEscape
| Instruction::PopAutoEscape => 0,
#[cfg(feature = "multi_template")]
Instruction::ExportLocals => 0,
#[cfg(feature = "macros")]
Instruction::LoadBlocks | Instruction::BuildMacro(..) | Instruction::Return => 0,
_ => 1,
}
}
+243
View File
@@ -0,0 +1,243 @@
use std::fmt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use crate::error::{Error, ErrorKind};
use crate::value::{Enumerator, Object, Value, ValueIter};
use crate::vm::state::State;
pub(crate) struct LoopState {
pub(crate) with_loop_var: bool,
// if we're popping the frame, do we want to jump somewhere? The
// first item is the target jump instruction, the second argument
// tells us if we need to end capturing.
pub(crate) current_recursion_jump: Option<(u32, bool)>,
pub(crate) object: Arc<Loop>,
// Depending on if adjacent_loop_items is enabled or not, the iterator
// is stored either on the loop state or in the loop object. This is
// done because when the feature is disabled, we can avoid using a mutex.
#[cfg(not(feature = "adjacent_loop_items"))]
iter: ValueIter,
}
impl LoopState {
pub fn new(
iter: ValueIter,
depth: usize,
with_loop_var: bool,
recurse_jump_target: Option<u32>,
current_recursion_jump: Option<(u32, bool)>,
) -> LoopState {
// for an iterator where the lower and upper bound are matching we can
// consider them to have ExactSizeIterator semantics. We do however not
// expect ExactSizeIterator bounds themselves to support iteration by
// other means.
let len = match iter.size_hint() {
(lower, Some(upper)) if lower == upper => Some(lower),
_ => None,
};
LoopState {
with_loop_var,
current_recursion_jump,
object: Arc::new(Loop {
idx: AtomicUsize::new(!0usize),
len,
depth,
recurse_jump_target,
last_changed_value: Mutex::default(),
#[cfg(feature = "adjacent_loop_items")]
iter: Mutex::new(AdjacentLoopItemIterWrapper::new(iter)),
}),
#[cfg(not(feature = "adjacent_loop_items"))]
iter,
}
}
pub fn did_not_iterate(&self) -> bool {
self.object.idx.load(Ordering::Relaxed) == 0
}
pub fn next(&mut self) -> Option<Value> {
self.object.idx.fetch_add(1, Ordering::Relaxed);
#[cfg(feature = "adjacent_loop_items")]
{
self.object.iter.lock().unwrap().next()
}
#[cfg(not(feature = "adjacent_loop_items"))]
{
self.iter.next()
}
}
}
#[cfg(feature = "adjacent_loop_items")]
pub(crate) struct AdjacentLoopItemIterWrapper {
prev_item: Option<Value>,
current_item: Option<Value>,
next_item: Option<Value>,
iter: ValueIter,
}
#[cfg(feature = "adjacent_loop_items")]
impl AdjacentLoopItemIterWrapper {
pub fn new(iter: ValueIter) -> AdjacentLoopItemIterWrapper {
AdjacentLoopItemIterWrapper {
prev_item: None,
current_item: None,
next_item: None,
iter,
}
}
fn next(&mut self) -> Option<Value> {
self.prev_item = self.current_item.take();
self.current_item = self.next_item.take().or_else(|| self.iter.next());
self.current_item.clone()
}
fn prev_item(&self) -> Value {
self.prev_item.clone().unwrap_or_default()
}
fn next_item(&mut self) -> Value {
self.next_item.clone().unwrap_or_else(|| {
self.next_item = self.iter.next();
self.next_item.clone().unwrap_or_default()
})
}
}
pub(crate) struct Loop {
pub len: Option<usize>,
pub idx: AtomicUsize,
pub depth: usize,
pub last_changed_value: Mutex<Option<Vec<Value>>>,
pub recurse_jump_target: Option<u32>,
#[cfg(feature = "adjacent_loop_items")]
iter: Mutex<AdjacentLoopItemIterWrapper>,
}
impl fmt::Debug for Loop {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Loop")
.field("len", &self.len)
.field("idx", &self.idx)
.field("depth", &self.depth)
.finish()
}
}
impl Object for Loop {
fn call(self: &Arc<Self>, _state: &State, _args: &[Value]) -> Result<Value, Error> {
// this could happen if a filter or some other code where to get hold
// on the loop and try to call it. The template execution itself will
// not end up here as the CallFunction opcode has a special code path
// for loop recursion.
Err(Error::new(
ErrorKind::InvalidOperation,
"loop recursion cannot be called this way",
))
}
fn call_method(
self: &Arc<Self>,
_state: &State,
name: &str,
args: &[Value],
) -> Result<Value, Error> {
if name == "changed" {
let mut last_changed_value = self.last_changed_value.lock().unwrap();
let value = args.to_owned();
let changed = last_changed_value.as_ref() != Some(&value);
if changed {
*last_changed_value = Some(value);
Ok(Value::from(true))
} else {
Ok(Value::from(false))
}
} else if name == "cycle" {
let idx = self.idx.load(Ordering::Relaxed);
match args.get(idx % args.len()) {
Some(arg) => Ok(arg.clone()),
None => Ok(Value::UNDEFINED),
}
} else {
Err(Error::from(ErrorKind::UnknownMethod))
}
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
Enumerator::Str(&[
"index0",
"index",
"length",
"revindex",
"revindex0",
"first",
"last",
"depth",
"depth0",
#[cfg(feature = "adjacent_loop_items")]
"previtem",
#[cfg(feature = "adjacent_loop_items")]
"nextitem",
])
}
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.get_value_by_str(some!(key.as_str()))
}
fn get_value_by_str(self: &Arc<Self>, key: &str) -> Option<Value> {
let idx = self.idx.load(Ordering::Relaxed) as u64;
// if we never iterated, then all attributes are undefined.
// this can happen in some rare circumstances where the engine
// did not manage to iterate
if idx == !0 {
return Some(Value::UNDEFINED);
}
if key == "index" {
return Some(Value::from(idx + 1));
}
let len = self.len.map(|x| x as u64);
match key {
"index0" => Some(Value::from(idx)),
"length" => Some(len.map(Value::from).unwrap_or(Value::UNDEFINED)),
"revindex" => Some(
len.map(|len| Value::from(len.saturating_sub(idx)))
.unwrap_or(Value::UNDEFINED),
),
"revindex0" => Some(
len.map(|len| Value::from(len.saturating_sub(idx).saturating_sub(1)))
.unwrap_or(Value::UNDEFINED),
),
"first" => Some(Value::from(idx == 0)),
"last" => Some(len.map_or(Value::from(false), |len| {
Value::from(len == 0 || idx == len - 1)
})),
"depth" => Some(Value::from(self.depth + 1)),
"depth0" => Some(Value::from(self.depth)),
#[cfg(feature = "adjacent_loop_items")]
"previtem" => Some(self.iter.lock().unwrap().prev_item()),
#[cfg(feature = "adjacent_loop_items")]
"nextitem" => Some(self.iter.lock().unwrap().next_item()),
_ => None,
}
}
fn render(self: &Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"<loop {}/{}>",
self.idx.load(Ordering::Relaxed),
match self.len {
Some(ref len) => len as &dyn fmt::Display,
None => &"?" as &dyn fmt::Display,
},
)
}
}
+148
View File
@@ -0,0 +1,148 @@
use std::collections::BTreeSet;
use std::fmt;
use std::sync::Arc;
use crate::error::{Error, ErrorKind};
use crate::output::Output;
use crate::utils::AutoEscape;
use crate::value::{Enumerator, Kwargs, Object, Value};
use crate::vm::state::State;
use crate::vm::Vm;
pub(crate) struct Macro {
pub name: Value,
pub arg_spec: Vec<Value>,
// because values need to be 'static, we can't hold a reference to the
// instructions that declared the macro. Instead of that we place the
// reference to the macro instruction (and the jump offset) in the
// state under `state.macros`.
pub macro_ref_id: usize,
pub state_id: isize,
pub closure: Value,
pub caller_reference: bool,
}
impl fmt::Debug for Macro {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<macro {}>", self.name)
}
}
impl Object for Macro {
fn enumerate(self: &Arc<Self>) -> Enumerator {
Enumerator::Str(&["name", "arguments", "caller"])
}
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
Some(match some!(key.as_str()) {
"name" => self.name.clone(),
"arguments" => Value::from_iter(self.arg_spec.iter().cloned()),
"caller" => Value::from(self.caller_reference),
_ => return None,
})
}
fn call(self: &Arc<Self>, state: &State<'_, '_>, args: &[Value]) -> Result<Value, Error> {
// we can only call macros that point to loaded template state.
if state.id != self.state_id {
return Err(Error::new(
ErrorKind::InvalidOperation,
"cannot call this macro. template state went away.",
));
}
let (args, kwargs) = match args.last() {
Some(last) => match Kwargs::extract(last) {
Some(kwargs) => (&args[..args.len() - 1], Some(kwargs)),
None => (args, None),
},
_ => (args, None),
};
if args.len() > self.arg_spec.len() {
return Err(Error::from(ErrorKind::TooManyArguments));
}
let mut kwargs_used = BTreeSet::new();
let mut arg_values = Vec::with_capacity(self.arg_spec.len());
for (idx, name) in self.arg_spec.iter().enumerate() {
let name = match name.as_str() {
Some(name) => name,
None => {
arg_values.push(Value::UNDEFINED);
continue;
}
};
let kwarg: Option<&Value> = match kwargs {
Some(ref kwargs) => kwargs.get(name).ok(),
_ => None,
};
arg_values.push(match (args.get(idx), kwarg) {
(Some(_), Some(_)) => {
return Err(Error::new(
ErrorKind::TooManyArguments,
format!("duplicate argument `{name}`"),
))
}
(Some(arg), None) => arg.clone(),
(None, Some(kwarg)) => {
kwargs_used.insert(name as &str);
kwarg.clone()
}
(None, None) => Value::UNDEFINED,
});
}
let caller = if self.caller_reference {
kwargs_used.insert("caller");
Some(
kwargs
.as_ref()
.and_then(|x| x.get("caller").ok())
.unwrap_or(Value::UNDEFINED),
)
} else {
None
};
if let Some(kwargs) = kwargs {
for key in kwargs.values.keys().filter_map(|x| x.as_str()) {
if !kwargs_used.contains(key) {
return Err(Error::new(
ErrorKind::TooManyArguments,
format!("unknown keyword argument `{key}`"),
));
}
}
}
let vm = Vm::new(state.env());
let mut rv = String::new();
// This requires some explanation here. Because we get the state as
// &State and not &mut State we are required to create a new state in
// eval_macro. This is unfortunate but makes the calling interface more
// convenient for the rest of the system. Because macros cannot return
// anything other than strings (most importantly they) can't return
// other macros this is however not an issue, as modifications in the
// macro cannot leak out.
ok!(vm.eval_macro(
state,
self.macro_ref_id,
&mut Output::new(&mut rv),
self.closure.clone(),
caller,
arg_values
));
Ok(if !matches!(state.auto_escape(), AutoEscape::None) {
Value::from_safe_string(rv)
} else {
Value::from(rv)
})
}
fn render(self: &Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<macro {}>", self.name)
}
}
+1099
View File
File diff suppressed because it is too large. Load diff
+32
View File
@@ -0,0 +1,32 @@
use std::fmt;
use std::sync::Arc;
use crate::value::{Enumerator, Object, Value, ValueMap};
/// Holds the locals of a module.
#[derive(Debug, Default)]
pub(crate) struct Module {
values: ValueMap,
captured: Value,
}
impl Module {
pub fn new(values: ValueMap, captured: Value) -> Self {
Self { values, captured }
}
}
impl Object for Module {
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
self.values.get(key).cloned()
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
let keys = self.values.keys().cloned();
Enumerator::Values(keys.collect())
}
fn render(self: &Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.captured)
}
}
+489
View File
@@ -0,0 +1,489 @@
use std::borrow::Cow;
use std::cell::Cell;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::sync::{Arc, Mutex};
use crate::compiler::instructions::Instructions;
use crate::environment::Environment;
use crate::error::{Error, ErrorKind};
use crate::output::Output;
use crate::template::Template;
use crate::utils::{AutoEscape, UndefinedBehavior};
use crate::value::{ArgType, Object, Value};
use crate::vm::context::Context;
#[cfg(feature = "fuel")]
use crate::vm::fuel::FuelTracker;
/// When macros are used, the state carries an `id` counter. Whenever a state is
/// created, the counter is incremented. This exists because macros can keep a reference
/// to instructions from another state by index. Without this counter it would
/// be possible for a macro to be called with a different state (different id)
/// which mean we likely panic.
#[cfg(feature = "macros")]
static STATE_ID: std::sync::atomic::AtomicIsize = std::sync::atomic::AtomicIsize::new(0);
/// Provides access to the current execution state of the engine.
///
/// A read only reference is passed to filter functions and similar objects to
/// allow limited interfacing with the engine. The state is useful to look up
/// information about the engine in filter, test or global functions. It not
/// only provides access to the template environment but also the context
/// variables of the engine, the current auto escaping behavior as well as the
/// auto escape flag.
///
/// In some testing scenarios or more advanced use cases you might need to get
/// a [`State`]. The state is managed as part of the template execution but the
/// initial state can be retrieved via [`Template::new_state`](crate::Template::new_state).
/// The most common way to get hold of the state however is via functions of filters.
///
/// **Notes on lifetimes:** the state object exposes some of the internal
/// lifetimes through the type. You should always elide these lifetimes
/// as there might be lifetimes added or removed between releases.
pub struct State<'template, 'env> {
pub(crate) ctx: Context<'env>,
pub(crate) current_block: Option<&'env str>,
pub(crate) auto_escape: Cell<AutoEscape>,
pub(crate) instructions: &'template Instructions<'env>,
pub(crate) temps: Arc<Mutex<BTreeMap<Box<str>, Value>>>,
pub(crate) blocks: BTreeMap<&'env str, BlockStack<'template, 'env>>,
#[allow(unused)]
pub(crate) loaded_templates: BTreeSet<&'env str>,
#[cfg(feature = "macros")]
pub(crate) id: isize,
#[cfg(feature = "macros")]
pub(crate) macros: std::sync::Arc<Vec<(&'template Instructions<'env>, u32)>>,
#[cfg(feature = "macros")]
pub(crate) closure_tracker: std::sync::Arc<crate::vm::closure_object::ClosureTracker>,
#[cfg(feature = "fuel")]
pub(crate) fuel_tracker: Option<std::sync::Arc<FuelTracker>>,
}
impl fmt::Debug for State<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ds = f.debug_struct("State");
ds.field("name", &self.instructions.name());
ds.field("current_block", &self.current_block);
ds.field("auto_escape", &self.auto_escape.get());
ds.field("ctx", &self.ctx);
ds.field("env", &self.env());
ds.finish()
}
}
impl<'template, 'env> State<'template, 'env> {
/// Creates a new state.
pub(crate) fn new(
ctx: Context<'env>,
auto_escape: AutoEscape,
instructions: &'template Instructions<'env>,
blocks: BTreeMap<&'env str, BlockStack<'template, 'env>>,
) -> State<'template, 'env> {
State {
#[cfg(feature = "macros")]
id: STATE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
current_block: None,
auto_escape: Cell::new(auto_escape),
instructions,
blocks,
temps: Default::default(),
loaded_templates: BTreeSet::new(),
#[cfg(feature = "macros")]
macros: Default::default(),
#[cfg(feature = "macros")]
closure_tracker: Default::default(),
#[cfg(feature = "fuel")]
fuel_tracker: ctx.env().fuel().map(FuelTracker::new),
ctx,
}
}
/// Creates an empty state for an environment.
pub(crate) fn new_for_env(env: &'env Environment) -> State<'env, 'env> {
State::new(
Context::new(env),
AutoEscape::None,
&crate::compiler::instructions::EMPTY_INSTRUCTIONS,
BTreeMap::new(),
)
}
/// Returns a reference to the current environment.
#[inline(always)]
pub fn env(&self) -> &'env Environment<'env> {
self.ctx.env()
}
/// Returns the name of the current template.
pub fn name(&self) -> &str {
self.instructions.name()
}
/// Returns the current value of the auto escape flag.
#[inline(always)]
pub fn auto_escape(&self) -> AutoEscape {
self.auto_escape.get()
}
pub(crate) fn with_auto_escape<R>(
&self,
auto_escape: AutoEscape,
f: impl FnOnce(&State<'template, 'env>) -> R,
) -> R {
if self.auto_escape.get() == auto_escape {
return f(self);
}
let old = self.auto_escape.replace(auto_escape);
let rv = f(self);
self.auto_escape.set(old);
rv
}
/// Returns the current undefined behavior.
#[inline(always)]
pub fn undefined_behavior(&self) -> UndefinedBehavior {
self.env().undefined_behavior()
}
/// Returns the name of the innermost block.
#[inline(always)]
pub fn current_block(&self) -> Option<&str> {
self.current_block
}
/// Looks up a variable by name in the context.
///
/// # Note on Closures
///
/// Macros and call blocks analyze which variables are referenced and
/// create closures for them. This means that unless a variable is defined
/// as a [global](Environment::add_global) in the environment, was passed in the
/// initial render context, or was referenced by a macro, this method won't be
/// able to find it.
#[inline(always)]
pub fn lookup(&self, name: &str) -> Option<Value> {
self.ctx.load(name)
}
/// Looks up a global macro and calls it.
///
/// This looks up a value as [`lookup`](Self::lookup) does and calls it
/// with the passed args.
#[cfg(feature = "macros")]
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
pub fn call_macro(&self, name: &str, args: &[Value]) -> Result<String, Error> {
let f = ok!(self.lookup(name).ok_or_else(|| Error::new(
crate::error::ErrorKind::UnknownFunction,
"macro not found"
)));
f.call(self, args).map(Into::into)
}
/// Renders a block with the given name into a string.
///
/// This method works like [`Template::render`](crate::Template::render) but
/// it only renders a specific block in the template. The first argument is
/// the name of the block.
///
/// This renders only the block `hi` in the template:
///
/// ```
/// # use minijinja::{Environment, context};
/// # fn test() -> Result<(), minijinja::Error> {
/// # let mut env = Environment::new();
/// # env.add_template("hello", "{% block hi %}Hello {{ name }}!{% endblock %}")?;
/// let tmpl = env.get_template("hello")?;
/// let mut rendered = tmpl
/// .render_captured(context!(name => "John"))?;
/// let rv = rendered.with_state_mut(|state| state.render_block("hi"))?;
/// println!("{}", rv);
/// # Ok(()) }
/// ```
///
/// Note that rendering a block is a stateful operation. If an error
/// is returned the module has to be re-created as the internal state
/// can end up corrupted. This also means you can only render blocks
/// if you have a mutable reference to the state which is not possible
/// from within filters or similar.
#[cfg(feature = "multi_template")]
#[cfg_attr(docsrs, doc(cfg(feature = "multi_template")))]
pub fn render_block(&mut self, block: &str) -> Result<String, Error> {
let mut buf = String::new();
crate::vm::Vm::new(self.env())
.call_block(block, self, &mut Output::new(&mut buf))
.map(|_| buf)
}
/// Renders a block with the given name into an [`io::Write`](std::io::Write).
///
/// For details see [`render_block`](Self::render_block).
#[cfg(feature = "multi_template")]
#[cfg_attr(docsrs, doc(cfg(feature = "multi_template")))]
pub fn render_block_to_write<W>(&mut self, block: &str, w: W) -> Result<(), Error>
where
W: std::io::Write,
{
let mut wrapper = crate::output::WriteWrapper { w, err: None };
crate::vm::Vm::new(self.env())
.call_block(block, self, &mut Output::new(&mut wrapper))
.map(|_| ())
.map_err(|err| wrapper.take_err(err))
}
/// Returns a list of the names of all exports (top-level variables).
pub fn exports(&self) -> Vec<&str> {
self.ctx.exports().keys().copied().collect()
}
/// Returns a list of all known variables.
///
/// This list contains all variables that are currently known to the state.
/// To retrieve the values you can use [`lookup`](Self::lookup). This will
/// include all the globals of the environment. Note that if the context
/// has been initialized with an object that lies about variables (eg: it
/// does not correctly implement enumeration), the returned list might not
/// be complete.
pub fn known_variables(&self) -> Vec<Cow<'_, str>> {
Vec::from_iter(self.ctx.known_variables(true))
}
/// Fetches a template by name with path joining.
///
/// This works like [`Environment::get_template`] with the difference that the lookup
/// undergoes path joining. If the environment has a configured path joining callback,
/// it will be invoked with the name of the current template as parent template.
///
/// For more information see [`Environment::set_path_join_callback`].
pub fn get_template(&self, name: &str) -> Result<Template<'env, 'env>, Error> {
self.env()
.get_template(&self.env().join_template_path(name, self.name()))
}
/// Invokes a filter with some arguments.
///
/// ```
/// # use minijinja::Environment;
/// # let mut env = Environment::new();
/// # env.add_filter("upper", |x: &str| x.to_uppercase());
/// # let tmpl = env.template_from_str("").unwrap();
/// # let state = tmpl.new_state();
/// let rv = state.apply_filter("upper", &["hello world".into()]).unwrap();
/// assert_eq!(rv.as_str(), Some("HELLO WORLD"));
/// ```
pub fn apply_filter(&self, filter: &str, args: &[Value]) -> Result<Value, Error> {
match self.env().get_filter(filter) {
Some(filter) => filter.call(self, args),
None => Err(Error::from(ErrorKind::UnknownFilter)),
}
}
/// Invokes a test function on a value.
///
/// ```
/// # use minijinja::Environment;
/// # let mut env = Environment::new();
/// # env.add_test("even", |x: i32| x % 2 == 0);
/// # let tmpl = env.template_from_str("").unwrap();
/// # let state = tmpl.new_state();
/// let rv = state.perform_test("even", &[42i32.into()]).unwrap();
/// assert!(rv);
/// ```
pub fn perform_test(&self, test: &str, args: &[Value]) -> Result<bool, Error> {
match self.env().get_test(test) {
Some(test) => test.call(self, args).map(|x| x.is_true()),
None => Err(Error::from(ErrorKind::UnknownTest)),
}
}
/// Formats a value to a string using the formatter on the environment.
///
/// ```
/// # use minijinja::{value::Value, Environment};
/// # let mut env = Environment::new();
/// # let tmpl = env.template_from_str("").unwrap();
/// # let state = tmpl.new_state();
/// let rv = state.format(Value::from(42)).unwrap();
/// assert_eq!(rv, "42");
/// ```
pub fn format(&self, value: Value) -> Result<String, Error> {
let mut rv = String::new();
let mut out = Output::new(&mut rv);
self.env().format(&value, self, &mut out).map(|_| rv)
}
/// Returns the fuel levels.
///
/// When the fuel feature is enabled, during evaluation the template will keep
/// track of how much fuel it has consumed. If the fuel tracker is turned on
/// the returned value will be `Some((consumed, remaining))`. If fuel tracking
/// is not enabled, `None` is returned instead.
#[cfg(feature = "fuel")]
#[cfg_attr(docsrs, doc(cfg(feature = "fuel")))]
pub fn fuel_levels(&self) -> Option<(u64, u64)> {
self.fuel_tracker
.as_ref()
.map(|x| (x.consumed(), x.remaining()))
}
/// Looks up a temp and returns it.
///
/// Temps are similar to context values but the engine never looks them up
/// on their own and they are not scoped. The lifetime of temps is limited
/// to the rendering process of a template. Temps are useful so that
/// filters and other things can temporarily stash away state without having
/// to resort to thread locals which are hard to manage. Unlike context
/// variables, temps can also be modified during evaluation by filters and
/// functions.
///
/// Temps are values but if you want to hold complex state you can store a
/// custom object there.
///
/// # Example
///
/// ```
/// use minijinja::{Value, State};
///
/// fn inc(state: &State) -> Value {
/// let old = state
/// .get_temp("my_counter")
/// .unwrap_or_else(|| Value::from(0i64));
/// let new = Value::from(i64::try_from(old).unwrap() + 1);
/// state.set_temp("my_counter", new.clone());
/// new
/// }
/// ```
pub fn get_temp(&self, name: &str) -> Option<Value> {
self.temps.lock().unwrap().get(name).cloned()
}
/// Inserts a temp and returns the old temp.
///
/// For more information see [`get_temp`](Self::get_temp).
pub fn set_temp(&self, name: &str, value: Value) -> Option<Value> {
self.temps
.lock()
.unwrap()
.insert(name.to_owned().into(), value)
}
/// Shortcut for registering an object as a temp.
///
/// If the value is already there, it's returned as an object. If it's
/// not there yet, the function is invoked to create it.
///
/// # Example
///
/// ```
/// use std::sync::atomic::{AtomicUsize, Ordering};
/// use minijinja::{Value, State};
/// use minijinja::value::Object;
///
/// #[derive(Debug, Default)]
/// struct MyObject(AtomicUsize);
///
/// impl Object for MyObject {}
///
/// fn inc(state: &State) -> Value {
/// let obj = state.get_or_set_temp_object("my_counter", MyObject::default);
/// let old = obj.0.fetch_add(1, Ordering::AcqRel);
/// Value::from(old + 1)
/// }
/// ```
///
/// # Panics
///
/// This will panic if the value registered under that name is not
/// the object expected.
pub fn get_or_set_temp_object<O, F>(&self, name: &str, f: F) -> Arc<O>
where
O: Object + 'static,
F: FnOnce() -> O,
{
self.get_temp(name)
.unwrap_or_else(|| {
let rv = Value::from_object(f());
self.set_temp(name, rv.clone());
rv
})
.downcast_object()
.expect("downcast unexpectedly failed. Name conflict?")
}
#[cfg(feature = "debug")]
pub(crate) fn make_debug_info(
&self,
pc: u32,
instructions: &Instructions<'_>,
) -> crate::debug::DebugInfo {
crate::debug::DebugInfo {
template_source: Some(instructions.source().to_string()),
referenced_locals: instructions
.get_referenced_names(pc)
.into_iter()
.filter_map(|n| Some((n.to_string(), some!(self.lookup(n)))))
.collect(),
}
}
}
impl<'a> ArgType<'a> for &State<'_, '_> {
type Output = &'a State<'a, 'a>;
fn from_value(_value: Option<&'a Value>) -> Result<Self::Output, Error> {
Err(Error::new(
ErrorKind::InvalidOperation,
"cannot use state type in this position",
))
}
fn from_state_and_value(
state: Option<&'a State>,
_value: Option<&'a Value>,
) -> Result<(Self::Output, usize), Error> {
match state {
None => Err(Error::new(ErrorKind::InvalidOperation, "state unavailable")),
Some(state) => Ok((state, 0)),
}
}
}
/// Tracks a block and its parents for super.
#[derive(Default)]
pub(crate) struct BlockStack<'template, 'env> {
instructions: Vec<&'template Instructions<'env>>,
depth: usize,
}
impl<'template, 'env> BlockStack<'template, 'env> {
pub fn new(instructions: &'template Instructions<'env>) -> BlockStack<'template, 'env> {
BlockStack {
instructions: vec![instructions],
depth: 0,
}
}
pub fn instructions(&self) -> &'template Instructions<'env> {
self.instructions.get(self.depth).copied().unwrap()
}
pub fn push(&mut self) -> bool {
if self.depth + 1 < self.instructions.len() {
self.depth += 1;
true
} else {
false
}
}
#[track_caller]
pub fn pop(&mut self) {
self.depth = self.depth.checked_sub(1).unwrap()
}
#[cfg(feature = "multi_template")]
pub fn append_instructions(&mut self, instructions: &'template Instructions<'env>) {
self.instructions.push(instructions);
}
}