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:
17624 files changed
+4564928
-38
No files matched your search
+1
@@ -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
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "92f114d1fd62525b2b4dc1adb77ae1e83c1214a9"
|
||||
},
|
||||
"path_in_vcs": "minijinja"
|
||||
}
|
||||
+422
@@ -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"
|
||||
Vendored
+151
@@ -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"
|
||||
Vendored
+201
@@ -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.
|
||||
Vendored
+167
@@ -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>
|
||||
|
||||
[](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE)
|
||||
[](https://crates.io/crates/minijinja)
|
||||
[](https://img.shields.io/badge/rust-1.63%2B-orange.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)
|
||||
Vendored
+12
@@ -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>
|
||||
Vendored
+631
@@ -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
@@ -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 ¯o_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
@@ -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);
|
||||
}
|
||||
+1005
File diff suppressed because it is too large.
Load diff
+307
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -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;
|
||||
+1365
File diff suppressed because it is too large.
Load diff
+157
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+90
@@ -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(())
|
||||
}
|
||||
Vendored
+317
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+859
@@ -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;
|
||||
Vendored
+419
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+119
@@ -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"))
|
||||
}
|
||||
}
|
||||
Vendored
+1738
File diff suppressed because it is too large.
Load diff
Vendored
+1571
File diff suppressed because it is too large.
Load diff
Vendored
+480
@@ -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::*;
|
||||
Vendored
+284
@@ -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)
|
||||
}
|
||||
}
|
||||
Vendored
+241
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+326
@@ -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)? ,)*)
|
||||
}
|
||||
}
|
||||
Vendored
+185
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+1228
File diff suppressed because it is too large.
Load diff
Vendored
+563
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+471
@@ -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::*;
|
||||
Vendored
+645
@@ -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!("<"),
|
||||
b'>' => escaping_body!(">"),
|
||||
b'&' => escaping_body!("&"),
|
||||
b'"' => escaping_body!("""),
|
||||
b'\'' => escaping_body!("'"),
|
||||
b'/' => escaping_body!("/"),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, "<>&"'/");
|
||||
}
|
||||
|
||||
#[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"]);
|
||||
}
|
||||
}
|
||||
+1187
File diff suppressed because it is too large.
Load diff
+435
@@ -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
@@ -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));
|
||||
}
|
||||
Vendored
+1892
File diff suppressed because it is too large.
Load diff
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+963
@@ -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);
|
||||
}
|
||||
Vendored
+744
@@ -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
@@ -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
@@ -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),*)
|
||||
}
|
||||
)*
|
||||
}
|
||||
)*
|
||||
};
|
||||
};
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
pub(crate) mod self_cell;
|
||||
+201
@@ -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
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
Vendored
+361
@@ -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(())
|
||||
}
|
||||
}
|
||||
Vendored
+70
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
Vendored
+1099
File diff suppressed because it is too large.
Load diff
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
Vendored
+489
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user