tera-1.20.0/.cargo_vcs_info.json0000644000000001360000000000100120470ustar { "git": { "sha1": "c05bff14ae74b3cd3970f64b87347949fe4150d4" }, "path_in_vcs": "" }tera-1.20.0/CHANGELOG.md000064400000000000000000000423511046102023000124550ustar 00000000000000# Changelog ## 1.12.0 (unreleased) - Support parenthesis in if statemetns - Allow escaped newline and tab join separator - ## 1.19.1 (2023-09-03) - Minimum supported Rust version (MSRV) is now 1.63 due to transitive dependencies. - Update crates.io metadata for the website ## 1.19.0 (2023-05-31) - Revert change to glob path to not error if the path doesn't exist - Allow macro definition in renderable template ## 1.18.1 (2023-03-15) - Fix panic on invalid globs to Tera::new ## 1.18.0 (2023-03-08) - Add `abs` filter - Add `indent` filter - Deprecate `get_json_pointer` in favour of `dotted_pointer`, a faster alternative - Always canonicalize glob paths passed to Tera to workaround a globwalk bug - Handle apostrophes in title case filter - Some performance improvement ## 1.17.1 (2022-09-19) - Make `get_random` use isize instead of i32 and bad error message - Fix variables lookup when the evaluated key has a `.` or quotes - Fix changed output of f64 from serde_json 1.0.85 ## 1.17.0 (2022-08-14) - Fix bug where operands in `in` operation were escaped before comparison - Force chrono dep to be 0.4.20 minimum - Better support for parenthesis in expression ## 1.16.0 (2022-06-10) - Add a feature just for the urlencode builtin - Fix bug in slice filter if start >= end - Allow supplying a timezone to a timestamp for the date filter ## 1.15.0 (2021-11-03) - Add `default` parameter to `get` filter - `Tera::extend` now also copies over function - Remove the new Context-local Tera function support, it was an accidental breaking change and will be added in v2 in some ways instead ## 1.14.0 (2021-11-01) - YANKED as it added a generic to Context, a breaking change - Ensure `Context` stays valid in Sync+Send, fixing an issue introduced in 1.13. 1.113 will be yanked. ## 1.13.0 (2021-10-17) - YANKED as it made Context not Send+Sync - Add `default` parameter to `get` filter - `Tera::extend` now also copies over function - Add Context-local Tera functions ## 1.12.1 (2021-07-13) - Remove unused feature of chrono - Remove unwanted bloat in crate from accidental WASM files ## 1.12.0 (2021-06-29) - Add `spaceless` filter from Django ## 1.11.0 (2021-06-14) - Allow iterating on strings with `for` ## 1.10.0 (2021-05-21) - Add `Tera::get_template_names` ## 1.9.0 (2021-05-16) - Add `Context::remove` ## 1.8.0 (2021-04-21) - Add `linebreaksbr` filter from Django - Allow dots in context object key names ## 1.7.1 (2021-04-12) - Fix parsing of filter arguments separated by whitespaces ## 1.7.0 (2021-03-07) - Allow rendering to `std::io::Write` - Follow symlinks in glob - Allow including lists of templates - Comment tags can now use whitespace control ## 1.6.1 (2020-12-29) - Fix date filter sometimes panicking with some format input ## 1.6.0 (2020-12-19) - Allow multiline function kwargs with trailing comma - Add `Context::try_insert` ## 1.5.0 (2020-08-10) - Add the concept of safe functions and filters - Allow negative index on `slice` filter ## 1.4.0 (2020-07-24) - Add `Context::get` and `Context::contains_key` ## 1.3.1 (2020-06-09) - Fix `raw` tag swallowing all whitespace at beginning and end - Make batch template sources generic - Automatically add function/test/filter function name to their error message ## 1.3.0 (2020-05-16) - Add a `urlencode_strict` filter - Add more array literals feature in templates - Make `filter` filter value argument optional ## 1.2.0 (2020-03-29) - Add `trim_start`, `trim_end`, `trim_start_matches` and `trim_end_matches` filters - Allow blocks in filter sections ## 1.1.0 (2020-03-08) - Add Tera::render_str, like Tera::one_off but can use an existing Tera instance ## 1.0.2 (2020-01-13) - Length filter now errors for things other than array, objects and strings. The fact that it was returning 0 before for other types was something that should have been fixed before 1.0 but got forgotten and was considered a bug. ## 1.0.1 (2019-12-18) - Fix filter sections not keeping whitespaces - The filesizeformat filter now takes a usize instead of a i64: no changes to behaviour ## 1.0.0 (2019-12-07) ### Breaking changes - Now requires Rust 1.34 - Removed error-chain errors and added rich Error enum instead - Filter, Tester and Function are now traits and now take borrowed values instead of owned - Updated for 2018 edition - Require macros import to be at the top of the files along with `extends` as it is fairly cheap and the code already only really look there. - Enforce spacing in tags at the parser, before `ifsomething` was considered ok - Pluralize filter now uses `singular` and `plural` arguments instead of `suffix` - Add a test for checking whether a variable is an object - Escaping now happens before inserting the final result of an expression: no need anymore to add `| safe` everywhere, only at the last position - Remove `safe` argument of the urlencode filter, `/` is still escaped by default - The `compile_templates!` macro has been removed ### Others - Tests can now use `value is not defined` order for negation (https://github.com/Keats/tera/issues/308) - Add `nth` filter to get the nth value in an array - You can now use glob patterns in `Tera::new` - `default` filter now works on Null values - Literal numbers in template overflowing i64/f64 will now be an error instead of panicking - Allow arrays as test arguments - Add the `in` operator to check if a left operand is contained in a right one. Also supports negation as `not in` - Add `Context::from_value` to instantiate a `Context` from a serde_json `Value` - Add `Context::from_serialize` to instantiate a `Context` from something that impl `Serialize` - Make tests helper fns `number_args_allowed`, `value_defined` and `extract_string` public - Add `else` clause to for loops - Filters are now evaluated when checking if/elif conditions - Allow `{{-` and `-}}` for whitespace management - Add `xml_escape` filter - Grave accent is no longer escaped in HTML, it is not really needed anymore - Add a `builtins` default feature that gate all filters/functions requiring additional dependencies - Add `unique` and `map` filter - Add a `timezone` attribute to the `date` filter - Add a `get_random` function to get a random number in a range - Add a `get_env` function to get the value of an environment variable ## 0.11.20 (2018-11-14) - Fix bugs in `filter` and `get` filters ## 0.11.19 (2018-10-31) - Allow function calls in math expressions - Allow string concatenation to start with a number - Allow function calls in string concatenations - Add a `concat` filter to concat arrays or push an element to an array ## 0.11.18 (2018-10-16) - Allow concatenation of strings and numbers ## 0.11.17 (2018-10-09) - Clear local context on each forloop iteration - Fix variable lookup with `.` that was completely wrong - Now requires Rust 1.26 because of some dependencies update ## 0.11.16 (2018-09-12) - Fix `set`/`set_global` not working correctly in macros - Deprecate `register_global_function` for `register_function` ## 0.11.15 (2018-09-09) - Remove invalid `unreachable!` call causing panic in some combination or for loop and specific filters - Fix macros loading in parent templates and using them in child ones - Fix macros loading other macros not working when called in inheritance - Mark `Context::add` as deprecated and do not display it in the docs anymore (aka TIL the `deprecated` attribute) - Fix `__tera_context` not getting all the available context (`set`, `forloop` etc) - Better error message when variable indexing fails ## 0.11.14 (2018-09-02) - Remove stray println ## 0.11.13 (2018-09-02) - Add `as_str` filter - Way fewer allocations and significant speedup (2-5x) for templates with large objects/loops - Checks that all macro files are accounted for at compile time and errors if it's not the case ## 0.11.12 (2018-08-04) - `filter` filter was not properly registered (╯°□°)╯︵ ┻━┻ ## 0.11.11 (2018-08-01) - `truncate` filter now works correctly on multichar graphemes ## 0.11.10 (2018-08-01) - Add a `throw` global function to fail rendering from inside a template ## 0.11.9 (2018-07-16) - Add a `matching` tester - Register `now` global function so it is available - Update `error-chain` ## 0.11.8 (2018-06-20) - Add `True` and `False` as boolean values to match Python - Allow user to define their own escape function, if you want to generate JSON for example - Add `end` argument to the `truncate` filter to override the default ellipsis - Add a `group_by` filter - Add a `filter` filter - Add the `~` operator to concatenate strings - Add a `now` global function to get local and UTC datetimes - Add feature to enable the `preserve_order` feature of serde_json - Less confusing behaviour with math arithmetics ## 0.11.7 (2018-04-24) - Add array literal instantiation from inside Tera for set, set_global, kwargs and for loop container - Fix panic on truncate filter ## 0.11.6 (2018-03-25) - Add `break` and `continue` to forloops - Fix strings delimited by single quote and backtick not removing the delimiters ## 0.11.5 (2018-03-01) - Re-export `serde_json::Number` as well ## 0.11.4 (2018-02-28) - Re-export `serde_json::Map` as well - You can now access inside a variable using index notation: `{{ arr[0] }}`, `{{ arr[idx] }}` etc thanks to @bootandy - Add `Context::insert` identical to `Context::add` to mirror Rust HashMap/BTreeMap syntax ## 0.11.3 (2018-02-15) - Add a `slice` filter for arrays - Fix macro files importing other macro files not loading properly - Fix forloop container being allowed logic expressions - Much improved parsing error messages ## 0.11.2 (2018-02-01) - Fix regression when including templates that import macros - Fix `pluralize` filter for real this time! ## 0.11.1 (2018-01-25) - Fix regression with expressions in comparisons ## 0.11.0 (2018-01-22) ### Breaking changes - Tests parentheses are now mandatory if there are arguments (`divisibleby 2` -> `divisibleby(2)`) - Tests can be only used on variables now, not on expressions - Escaping happens immediately now instead of waiting for the filters to be called, unless `safe` is first. If you want the old behaviour you will need to start the a chain of filters with `| safe` as the first one ### Others - Tests, global functions calls and macro calls are now expressions and can be combined like so: `if x is divisibleby(2) and x > 10` - Add default arguments for macro arguments - Add whitespace management similar to Liquid and Jinja2 - Add parentheses to expressions to remove ambiguities - Block & macro end tag name are no longer mandatory and it doesn't error on mismatched names between the start and end tag anymore - Filters can now be applied to expressions - Add modulo operator `%` for math expressions - Allow comment tags before the extend tag - Make `NaiveDateTime` work with the `date` filter - `pluralize` filter now returns the plural suffix for 0 thing as it's apparently what English does - Add a `set_global` tag that allows you to set something in the global context: meant to be used in forloops where the normal `set` would put the value into the loop context - Add `starting_with`, `ending_with` and `containing` tests - Add `json_encode`, `default` and `sort` filters - Strings can now also be contained in backticks and single quotes in templates ## 0.10.10 (2017-08-24) - Add `Tera::parse` for some niche use-cases ## 0.10.9 (2017-08-02) - Handle path to templates starting with "./" - Fix loop and macro context overlaps - Fix variables being escaped when given to `set` or as arguments to filters/macros/global fns ## 0.10.8 (2017-06-24) - Update chrono ## 0.10.7 (2017-06-16) - Fix not being able to use variables starting with `or`, `and` and `not` - Fix `<=` and `>=` not being recognised properly - Fix if/elif conditions falling through: only the first valid one will be rendered - Handle NaN results in `{% set %}` instead of panicking - Allow math node on if/elif conditions & fix f64 truthiness ## 0.10.6 (2017-05-23) - Fix not being able to call global functions without arguments - Fix multiple inheritance not rendering blocks as expected for nested blocks - Allow filters on key/value for loop containers ## 0.10.5 (2017-05-13) - Fix bug with `{% set %}` in forloops ## 0.10.4 (2017-05-09) - Add `Send` to `GlobalFn` return type ## 0.10.3 (2017-05-09) - Add global functions, see README - Add set tag, see README - Add get filter ## 0.10.2 (2017-05-03) - Fix bug with section filter swallowing all content after the end tag - Allow whitespace in function args ## 0.10.1 (2017-04-25) - Fix bug with variable in loop using starting with the container name (#165) - Allow whitespace in macros/filters params ## 0.10.0 (2017-04-21) ### Breaking changes - Update Serde to 1.0.0 ### Others - Fix date filter converting everything to UTC - Fix panic when using filters on forloop container ## 0.9.0 (2017-04-05) ### Breaking changes - Fix bug in Windows where the glob path was not removed correctly ### Others - `Tera::extend` now also copy filters and testers ## 0.8.1 (2017-03-15) - Macro rendering perf improved and general code cleanup thanks to @Peternator7 - Fix bug in parser with floats - Make `date` filter work with string input in `YYYY-MM-DD` format - Big parsing improvement (~20-40%) for projects with macros and inheritance - Add `Tera::extend` to extend another instance of Tera - Add `Tera::full_reload` that will re-run the glob and parse all templates found. - Make `Tera::add_raw_template{s}` and `Tera::add_template_file{s}` part of the public API - Fix location in error message when erroring in a child template ## 0.8.0 (2017-03-03) ### Breaking changes - Remove `value_render` and `value_one_off`, you can now use `render` and `one_off` for both values and context ### Others - Speed improvements on both parsing and rendering (~20-40% faster) - Better error message on variable lookup failure in loops - Can now iterate on maps/struct using the `{% for key, val in my_object %}` construct ## 0.7.2 (2017-02-18) - Update chrono version - Make variable block like `{{ "hey" }}` render correctly ## 0.7.1 (2017-02-05) - Support filter sections - Fix path prefix trimming on Windows ## 0.7.0 (2017-02-01) ### Breaking changes - `Tera::add_template` -> `Tera::add_raw_template` - `Tera::add_templates` -> `Tera::add_raw_templates` ### Others - Performance improvement thanks to @clarcharr - Better error message for `value_render`. Thanks to @SilverWingedSeraph for the report - Hide `add_raw_template` and `add_raw_templates` from docs, they were meant for internal use - Exported macros now use the `$crate` variable, which means you don't need to import anything from Tera to have them working - Expose AST (not covered by semver) - Add a `Context::extend` method to merge a context object into another one ## 0.6.2 (2017-01-08) - Performance improvements thanks to @wdv4758h - Correctly register `date` filter and make it work on a RFC3339 string as well thanks to @philwhineray ## 0.6.1 (2016-12-28) - Added `Tera::value_one_off` to parse and render a single template using a Json value as context ## 0.6.0 (2016-12-26) ### BREAKING CHANGES - `not` is now a Tera keyword ### Others - Added `#![deny(missing_docs)]` to the crate - Added `Tera::one_off` to parse and render a single template - Added `not` operator in conditions to mean falsiness (equivalent to `!` in Rust) - Remove specific error message when using `||` or `&&` - Improved performances for parsing and rendering (~5-20%) - Added `precision` arg to `round` filter - Added `date` filter to format a timestamp to a date(time) string ## 0.5.0 (2016-12-19) A few breaking changes in this one ### BREAKING CHANGES - Tera no longer panics when parsing templates, it returns an error instead - Tester fn signature changes from `fn(&str, Option, Vec) -> Result` to `fn(Option, Vec) -> Result` - Rename `TeraResult` export to `Result` ### Others - Stabilized `Tera::add_template` and `Tera::add_templates` - Added `compile_templates!` macro to try to compile all templates and, in case of errors, print them and exit the process - Much improved error messages - Add a magical variable `__tera_context` that will pretty print the current context - More documentation inside the crate itself - Actually register the `filesizeformat`, `slugify`, `addslashes`, good thing no one noticed - Add `divisibleby` and `iterable` test - Made `try_get_value!` macro work outside of Tera ## 0.4.1 (2016/12/07) - Remove println! left behind - Fix macros not being found in child templates - Export `Value` and `to_value` (currently from serde-json) ## 0.4.0 (2016/12/02) - Add macros - Add `filesizeformat` filter - Add autoescape - Add multiple level inheritance - Add nested blocks - Add `{{ super() }}` Thanks to @SergioBenitez and @yonran for the help! ## 0.3.1 (2016/10/11) - Fix regression when using variables in forloops + add test for it ## 0.3.0 (2016/10/11) - Change signature of tests functions (BREAKING CHANGE) - Add more tests: `undefined`, `odd`, `even`, `number` and `string` - Add `include` directive to include another file - Indexed array/tuple access using the `.x` where `x` is an integer Thanks to @SergioBenitez and @andrelmartins for the contributions! ## 0.2.0 (2016/09/27) - Added filters, see README for current list - Added tests, only `defined` for now Thanks to @SergioBenitez, @orhanbalci, @foophoof and @Peternator7 for the contribution! ## 0.1.3 (2016/08/14) - Completely new parser - Expose TeraError tera-1.20.0/Cargo.lock0000644000000614500000000000100100300ustar # 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 = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bstr" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "serde", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "pure-rust-locales", "windows-targets", ] [[package]] name = "chrono-tz" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" dependencies = [ "chrono", "chrono-tz-build", "phf", ] [[package]] name = "chrono-tz-build" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" dependencies = [ "parse-zoneinfo", "phf", "phf_codegen", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "deunicode" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "globset" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", "log", "regex-automata", "regex-syntax", ] [[package]] name = "globwalk" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ "bitflags", "ignore", "walkdir", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "humansize" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" dependencies = [ "libm", ] [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "ignore" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", "regex-automata", "same-file", "walkdir", "winapi-util", ] [[package]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parse-zoneinfo" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" dependencies = [ "regex", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn", ] [[package]] name = "pest_meta" version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", "sha2", ] [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_shared" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty_assertions" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro2" version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] [[package]] name = "pure-rust-locales" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1190fd18ae6ce9e137184f207593877e70f39b015040156b1e05081cdfe3733a" [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "regex" version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[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.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "indexmap", "itoa", "ryu", "serde", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slug" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" dependencies = [ "deunicode", "wasm-bindgen", ] [[package]] name = "syn" version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys", ] [[package]] name = "tera" version = "1.20.0" dependencies = [ "chrono", "chrono-tz", "globwalk", "humansize", "lazy_static", "percent-encoding", "pest", "pest_derive", "pretty_assertions", "rand", "regex", "serde", "serde_derive", "serde_json", "slug", "tempfile", "unic-segment", ] [[package]] name = "thiserror" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unic-char-property" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" dependencies = [ "unic-char-range", ] [[package]] name = "unic-char-range" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" [[package]] name = "unic-common" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" [[package]] name = "unic-segment" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" dependencies = [ "unic-ucd-segment", ] [[package]] name = "unic-ucd-segment" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" dependencies = [ "unic-char-property", "unic-char-range", "unic-ucd-version", ] [[package]] name = "unic-ucd-version" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" dependencies = [ "unic-common", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "winapi-util" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ "windows-sys", ] [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] name = "windows_i686_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" tera-1.20.0/Cargo.toml0000644000000042010000000000100100420ustar # 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 = "2018" rust-version = "1.70" name = "tera" version = "1.20.0" authors = ["Vincent Prouillet "] include = [ "/src/**/*", "/LICENSE", "/README.md", "/CHANGELOG.md", ] description = "Template engine based on Jinja2/Django templates" homepage = "https://keats.github.io/tera/" readme = "README.md" keywords = [ "template", "html", "django", "markup", "jinja2", ] categories = ["template-engine"] license = "MIT" repository = "https://github.com/Keats/tera" [dependencies.chrono] version = "0.4.27" features = [ "std", "clock", ] optional = true default-features = false [dependencies.chrono-tz] version = "0.9" optional = true [dependencies.globwalk] version = "0.9.1" [dependencies.humansize] version = "2.1" optional = true [dependencies.lazy_static] version = "1.4" [dependencies.percent-encoding] version = "2.2" optional = true [dependencies.pest] version = "2.5.5" [dependencies.pest_derive] version = "2.5.5" [dependencies.rand] version = "0.8" optional = true [dependencies.regex] version = "1.7" [dependencies.serde] version = "1.0" [dependencies.serde_json] version = "1.0" [dependencies.slug] version = "0.1" optional = true [dependencies.unic-segment] version = "0.9" [dev-dependencies.pretty_assertions] version = "1" [dev-dependencies.serde_derive] version = "1.0" [dev-dependencies.tempfile] version = "3" [features] builtins = [ "urlencode", "slug", "humansize", "chrono", "chrono-tz", "rand", ] date-locale = [ "builtins", "chrono/unstable-locales", ] default = ["builtins"] preserve_order = ["serde_json/preserve_order"] urlencode = ["percent-encoding"] tera-1.20.0/Cargo.toml.orig000064400000000000000000000031131046102023000135240ustar 00000000000000[package] name = "tera" version = "1.20.0" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" description = "Template engine based on Jinja2/Django templates" homepage = "https://keats.github.io/tera/" repository = "https://github.com/Keats/tera" keywords = ["template", "html", "django", "markup", "jinja2"] categories = ["template-engine"] edition = "2018" include = ["/src/**/*", "/LICENSE", "/README.md", "/CHANGELOG.md"] rust-version = "1.70" [dependencies] globwalk = "0.9.1" serde = "1.0" serde_json = "1.0" pest = "2.5.5" pest_derive = "2.5.5" lazy_static = "1.4" # used in striptags, spaceless & titles filters. Already pulled by globwalk regex = "1.7" # used in truncate filter and string iteration unic-segment = "0.9" # used in slugify filter slug = {version = "0.1", optional = true} # used in urlencode filter percent-encoding = {version = "2.2", optional = true} # used in filesizeformat filter humansize = {version = "2.1", optional = true} # used in date format filter chrono = {version = "0.4.27", optional = true, default-features = false, features = ["std", "clock"]} # used in date format filter chrono-tz = {version = "0.9", optional = true} # used in get_random function rand = {version = "0.8", optional = true} [dev-dependencies] serde_derive = "1.0" pretty_assertions = "1" tempfile = "3" [features] default = ["builtins"] builtins = ["urlencode", "slug", "humansize", "chrono", "chrono-tz", "rand"] urlencode = ["percent-encoding"] preserve_order = ["serde_json/preserve_order"] date-locale = ["builtins", "chrono/unstable-locales"] tera-1.20.0/LICENSE000064400000000000000000000020751046102023000116500ustar 00000000000000The MIT License (MIT) Copyright (c) 2015 Vincent Prouillet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tera-1.20.0/README.md000064400000000000000000000024271046102023000121230ustar 00000000000000# Tera [![Actions Status](https://github.com/Keats/tera/workflows/ci/badge.svg)](https://github.com/Keats/tera/actions) [![Crates.io](https://img.shields.io/crates/v/tera.svg)](https://crates.io/crates/tera) [![Docs](https://docs.rs/tera/badge.svg)](https://docs.rs/crate/tera/) [![Gitter](https://badges.gitter.im/Tera-templates/community.svg)](https://gitter.im/Tera-templates/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) Tera is a template engine inspired by [Jinja2](http://jinja.pocoo.org/) and the [Django template language](https://docs.djangoproject.com/en/3.1/topics/templates/). ```jinja2 {% block title %}{% endblock title %} ``` It does not aim to be 100% compatible with them but has many of the Jinja2/Django filters and testers. ## Documentation API documentation is available on [docs.rs](https://docs.rs/crate/tera/). Tera documentation is available on its [site](http://keats.github.io/tera/docs). ## SemVer This project follows SemVer only for the public API, public API here meaning functions appearing in the docs. Some features, like accessing the AST, are also available but breaking changes in them can happen in any versions. tera-1.20.0/src/builtins/filters/array.rs000064400000000000000000000651031046102023000164200ustar 00000000000000/// Filters operating on array use std::collections::HashMap; use crate::context::{dotted_pointer, ValueRender}; use crate::errors::{Error, Result}; use crate::filter_utils::{get_sort_strategy_for_type, get_unique_strategy_for_type}; use crate::utils::render_to_string; use serde_json::value::{to_value, Map, Value}; /// Returns the nth value of an array /// If the array is empty, returns empty string pub fn nth(value: &Value, args: &HashMap) -> Result { let arr = try_get_value!("nth", "value", Vec, value); if arr.is_empty() { return Ok(to_value("").unwrap()); } let index = match args.get("n") { Some(val) => try_get_value!("nth", "n", usize, val), None => return Err(Error::msg("The `nth` filter has to have an `n` argument")), }; Ok(arr.get(index).unwrap_or(&to_value("").unwrap()).to_owned()) } /// Returns the first value of an array /// If the array is empty, returns empty string pub fn first(value: &Value, _: &HashMap) -> Result { let mut arr = try_get_value!("first", "value", Vec, value); if arr.is_empty() { Ok(to_value("").unwrap()) } else { Ok(arr.swap_remove(0)) } } /// Returns the last value of an array /// If the array is empty, returns empty string pub fn last(value: &Value, _: &HashMap) -> Result { let mut arr = try_get_value!("last", "value", Vec, value); Ok(arr.pop().unwrap_or_else(|| to_value("").unwrap())) } /// Joins all values in the array by the `sep` argument given /// If no separator is given, it will use `""` (empty string) as separator /// If the array is empty, returns empty string pub fn join(value: &Value, args: &HashMap) -> Result { let arr = try_get_value!("join", "value", Vec, value); let sep = match args.get("sep") { Some(val) => { let s = try_get_value!("truncate", "sep", String, val); // When reading from a file, it will escape `\n` to `\\n` for example so we need // to replace double escape. In practice it might cause issues if someone wants to join // with `\\n` for real but that seems pretty unlikely s.replace("\\n", "\n").replace("\\t", "\t") } None => String::new(), }; // Convert all the values to strings before we join them together. let rendered = arr .iter() .map(|v| render_to_string(|| "joining array".to_string(), |w| v.render(w))) .collect::>>()?; to_value(rendered.join(&sep)).map_err(Error::json) } /// Sorts the array in ascending order. /// Use the 'attribute' argument to define a field to sort by. pub fn sort(value: &Value, args: &HashMap) -> Result { let arr = try_get_value!("sort", "value", Vec, value); if arr.is_empty() { return Ok(arr.into()); } let attribute = match args.get("attribute") { Some(val) => try_get_value!("sort", "attribute", String, val), None => String::new(), }; let first = dotted_pointer(&arr[0], &attribute).ok_or_else(|| { Error::msg(format!("attribute '{}' does not reference a field", attribute)) })?; let mut strategy = get_sort_strategy_for_type(first)?; for v in &arr { let key = dotted_pointer(v, &attribute).ok_or_else(|| { Error::msg(format!("attribute '{}' does not reference a field", attribute)) })?; strategy.try_add_pair(v, key)?; } let sorted = strategy.sort(); Ok(sorted.into()) } /// Remove duplicates from an array. /// Use the 'attribute' argument to define a field to filter on. /// For strings, use the 'case_sensitive' argument (defaults to false) to control the comparison. pub fn unique(value: &Value, args: &HashMap) -> Result { let arr = try_get_value!("unique", "value", Vec, value); if arr.is_empty() { return Ok(arr.into()); } let case_sensitive = match args.get("case_sensitive") { Some(val) => try_get_value!("unique", "case_sensitive", bool, val), None => false, }; let attribute = match args.get("attribute") { Some(val) => try_get_value!("unique", "attribute", String, val), None => String::new(), }; let first = dotted_pointer(&arr[0], &attribute).ok_or_else(|| { Error::msg(format!("attribute '{}' does not reference a field", attribute)) })?; let disc = std::mem::discriminant(first); let mut strategy = get_unique_strategy_for_type(first, case_sensitive)?; let arr = arr .into_iter() .filter_map(|v| match dotted_pointer(&v, &attribute) { Some(key) => { if disc == std::mem::discriminant(key) { match strategy.insert(key) { Ok(false) => None, Ok(true) => Some(Ok(v)), Err(e) => Some(Err(e)), } } else { Some(Err(Error::msg("unique filter can't compare multiple types"))) } } None => None, }) .collect::>>(); Ok(to_value(arr?).unwrap()) } /// Group the array values by the `attribute` given /// Returns a hashmap of key => values, items without the `attribute` or where `attribute` is `null` are discarded. /// The returned keys are stringified pub fn group_by(value: &Value, args: &HashMap) -> Result { let arr = try_get_value!("group_by", "value", Vec, value); if arr.is_empty() { return Ok(Map::new().into()); } let key = match args.get("attribute") { Some(val) => try_get_value!("group_by", "attribute", String, val), None => { return Err(Error::msg("The `group_by` filter has to have an `attribute` argument")) } }; let mut grouped = Map::new(); for val in arr { if let Some(key_val) = dotted_pointer(&val, &key).cloned() { if key_val.is_null() { continue; } let str_key = match key_val.as_str() { Some(key) => key.to_owned(), None => format!("{}", key_val), }; if let Some(vals) = grouped.get_mut(&str_key) { vals.as_array_mut().unwrap().push(val); continue; } grouped.insert(str_key, Value::Array(vec![val])); } } Ok(to_value(grouped).unwrap()) } /// Filter the array values, returning only the values where the `attribute` is equal to the `value` /// Values without the `attribute` or with a null `attribute` are discarded /// If the `value` is not passed, discard all elements where the attribute is null. pub fn filter(value: &Value, args: &HashMap) -> Result { let mut arr = try_get_value!("filter", "value", Vec, value); if arr.is_empty() { return Ok(arr.into()); } let key = match args.get("attribute") { Some(val) => try_get_value!("filter", "attribute", String, val), None => return Err(Error::msg("The `filter` filter has to have an `attribute` argument")), }; let value = args.get("value").unwrap_or(&Value::Null); arr = arr .into_iter() .filter(|v| { let val = dotted_pointer(v, &key).unwrap_or(&Value::Null); if value.is_null() { !val.is_null() } else { val == value } }) .collect::>(); Ok(to_value(arr).unwrap()) } /// Map retrieves an attribute from a list of objects. /// The 'attribute' argument specifies what to retrieve. pub fn map(value: &Value, args: &HashMap) -> Result { let arr = try_get_value!("map", "value", Vec, value); if arr.is_empty() { return Ok(arr.into()); } let attribute = match args.get("attribute") { Some(val) => try_get_value!("map", "attribute", String, val), None => return Err(Error::msg("The `map` filter has to have an `attribute` argument")), }; let arr = arr .into_iter() .filter_map(|v| match dotted_pointer(&v, &attribute) { Some(val) if !val.is_null() => Some(val.clone()), _ => None, }) .collect::>(); Ok(to_value(arr).unwrap()) } #[inline] fn get_index(i: f64, array: &[Value]) -> usize { if i >= 0.0 { i as usize } else { (array.len() as f64 + i) as usize } } /// Slice the array /// Use the `start` argument to define where to start (inclusive, default to `0`) /// and `end` argument to define where to stop (exclusive, default to the length of the array) /// `start` and `end` are 0-indexed pub fn slice(value: &Value, args: &HashMap) -> Result { let arr = try_get_value!("slice", "value", Vec, value); if arr.is_empty() { return Ok(arr.into()); } let start = match args.get("start") { Some(val) => get_index(try_get_value!("slice", "start", f64, val), &arr), None => 0, }; let mut end = match args.get("end") { Some(val) => get_index(try_get_value!("slice", "end", f64, val), &arr), None => arr.len(), }; if end > arr.len() { end = arr.len(); } // Not an error, but returns an empty Vec if start >= end { return Ok(Vec::::new().into()); } Ok(arr[start..end].into()) } /// Concat the array with another one if the `with` parameter is an array or /// just append it otherwise pub fn concat(value: &Value, args: &HashMap) -> Result { let mut arr = try_get_value!("concat", "value", Vec, value); let value = match args.get("with") { Some(val) => val, None => return Err(Error::msg("The `concat` filter has to have a `with` argument")), }; if value.is_array() { match value { Value::Array(vals) => { for val in vals { arr.push(val.clone()); } } _ => unreachable!("Got something other than an array??"), } } else { arr.push(value.clone()); } Ok(to_value(arr).unwrap()) } #[cfg(test)] mod tests { use super::*; use serde_derive::{Deserialize, Serialize}; use serde_json::json; use serde_json::value::{to_value, Value}; use std::collections::HashMap; #[test] fn test_nth() { let mut args = HashMap::new(); args.insert("n".to_string(), to_value(1).unwrap()); let result = nth(&to_value(vec![1, 2, 3, 4]).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(2).unwrap()); } #[test] fn test_nth_empty() { let v: Vec = Vec::new(); let mut args = HashMap::new(); args.insert("n".to_string(), to_value(1).unwrap()); let result = nth(&to_value(v).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("").unwrap()); } #[test] fn test_first() { let result = first(&to_value(vec![1, 2, 3, 4]).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(1).unwrap()); } #[test] fn test_first_empty() { let v: Vec = Vec::new(); let result = first(&to_value(v).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.ok().unwrap(), to_value("").unwrap()); } #[test] fn test_last() { let result = last(&to_value(vec!["Hello", "World"]).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("World").unwrap()); } #[test] fn test_last_empty() { let v: Vec = Vec::new(); let result = last(&to_value(v).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.ok().unwrap(), to_value("").unwrap()); } #[test] fn test_join_sep() { let mut args = HashMap::new(); args.insert("sep".to_owned(), to_value("==").unwrap()); let result = join(&to_value(vec!["Cats", "Dogs"]).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("Cats==Dogs").unwrap()); } #[test] fn test_join_sep_omitted() { let result = join(&to_value(vec![1.2, 3.4]).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("1.23.4").unwrap()); } #[test] fn test_join_empty() { let v: Vec = Vec::new(); let mut args = HashMap::new(); args.insert("sep".to_owned(), to_value("==").unwrap()); let result = join(&to_value(v).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("").unwrap()); } #[test] fn test_join_newlines_and_tabs() { let mut args = HashMap::new(); args.insert("sep".to_owned(), to_value(",\\n\\t").unwrap()); let result = join(&to_value(vec!["Cats", "Dogs"]).unwrap(), &args); assert_eq!(result.unwrap(), to_value("Cats,\n\tDogs").unwrap()); } #[test] fn test_sort() { let v = to_value(vec![3, -1, 2, 5, 4]).unwrap(); let args = HashMap::new(); let result = sort(&v, &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(vec![-1, 2, 3, 4, 5]).unwrap()); } #[test] fn test_sort_empty() { let v = to_value(Vec::::new()).unwrap(); let args = HashMap::new(); let result = sort(&v, &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(Vec::::new()).unwrap()); } #[derive(Deserialize, Eq, Hash, PartialEq, Serialize)] struct Foo { a: i32, b: i32, } #[test] fn test_sort_attribute() { let v = to_value(vec![ Foo { a: 3, b: 5 }, Foo { a: 2, b: 8 }, Foo { a: 4, b: 7 }, Foo { a: 1, b: 6 }, ]) .unwrap(); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("a").unwrap()); let result = sort(&v, &args); assert!(result.is_ok()); assert_eq!( result.unwrap(), to_value(vec![ Foo { a: 1, b: 6 }, Foo { a: 2, b: 8 }, Foo { a: 3, b: 5 }, Foo { a: 4, b: 7 }, ]) .unwrap() ); } #[test] fn test_sort_invalid_attribute() { let v = to_value(vec![Foo { a: 3, b: 5 }]).unwrap(); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("invalid_field").unwrap()); let result = sort(&v, &args); assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), "attribute 'invalid_field' does not reference a field" ); } #[test] fn test_sort_multiple_types() { let v = to_value(vec![Value::Number(12.into()), Value::Array(vec![])]).unwrap(); let args = HashMap::new(); let result = sort(&v, &args); assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), "expected number got []"); } #[test] fn test_sort_non_finite_numbers() { let v = to_value(vec![ ::std::f64::NEG_INFINITY, // NaN and friends get deserialized as Null by serde. ::std::f64::NAN, ]) .unwrap(); let args = HashMap::new(); let result = sort(&v, &args); assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), "Null is not a sortable value"); } #[derive(Deserialize, Eq, Hash, PartialEq, Serialize)] struct TupleStruct(i32, i32); #[test] fn test_sort_tuple() { let v = to_value(vec![ TupleStruct(0, 1), TupleStruct(7, 0), TupleStruct(-1, 12), TupleStruct(18, 18), ]) .unwrap(); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("0").unwrap()); let result = sort(&v, &args); assert!(result.is_ok()); assert_eq!( result.unwrap(), to_value(vec![ TupleStruct(-1, 12), TupleStruct(0, 1), TupleStruct(7, 0), TupleStruct(18, 18), ]) .unwrap() ); } #[test] fn test_unique_numbers() { let v = to_value(vec![3, -1, 3, 3, 5, 2, 5, 4]).unwrap(); let args = HashMap::new(); let result = unique(&v, &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(vec![3, -1, 5, 2, 4]).unwrap()); } #[test] fn test_unique_strings() { let v = to_value(vec!["One", "Two", "Three", "one", "Two"]).unwrap(); let mut args = HashMap::new(); let result = unique(&v, &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(vec!["One", "Two", "Three"]).unwrap()); args.insert("case_sensitive".to_string(), to_value(true).unwrap()); let result = unique(&v, &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(vec!["One", "Two", "Three", "one"]).unwrap()); } #[test] fn test_unique_empty() { let v = to_value(Vec::::new()).unwrap(); let args = HashMap::new(); let result = sort(&v, &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(Vec::::new()).unwrap()); } #[test] fn test_unique_attribute() { let v = to_value(vec![ Foo { a: 1, b: 2 }, Foo { a: 3, b: 3 }, Foo { a: 1, b: 3 }, Foo { a: 0, b: 4 }, ]) .unwrap(); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("a").unwrap()); let result = unique(&v, &args); assert!(result.is_ok()); assert_eq!( result.unwrap(), to_value(vec![Foo { a: 1, b: 2 }, Foo { a: 3, b: 3 }, Foo { a: 0, b: 4 },]).unwrap() ); } #[test] fn test_unique_invalid_attribute() { let v = to_value(vec![Foo { a: 3, b: 5 }]).unwrap(); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("invalid_field").unwrap()); let result = unique(&v, &args); assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), "attribute 'invalid_field' does not reference a field" ); } #[test] fn test_unique_multiple_types() { let v = to_value(vec![Value::Number(12.into()), Value::Array(vec![])]).unwrap(); let args = HashMap::new(); let result = unique(&v, &args); assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), "unique filter can't compare multiple types"); } #[test] fn test_unique_non_finite_numbers() { let v = to_value(vec![ ::std::f64::NEG_INFINITY, // NaN and friends get deserialized as Null by serde. ::std::f64::NAN, ]) .unwrap(); let args = HashMap::new(); let result = unique(&v, &args); assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), "Null is not a unique value"); } #[test] fn test_unique_tuple() { let v = to_value(vec![ TupleStruct(0, 1), TupleStruct(-7, -1), TupleStruct(-1, 1), TupleStruct(18, 18), ]) .unwrap(); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("1").unwrap()); let result = unique(&v, &args); assert!(result.is_ok()); assert_eq!( result.unwrap(), to_value(vec![TupleStruct(0, 1), TupleStruct(-7, -1), TupleStruct(18, 18),]).unwrap() ); } #[test] fn test_slice() { fn make_args(start: Option, end: Option) -> HashMap { let mut args = HashMap::new(); if let Some(s) = start { args.insert("start".to_string(), to_value(s).unwrap()); } if let Some(e) = end { args.insert("end".to_string(), to_value(e).unwrap()); } args } let v = to_value(vec![1, 2, 3, 4, 5]).unwrap(); let inputs = vec![ (make_args(Some(1), None), vec![2, 3, 4, 5]), (make_args(None, Some(2.0)), vec![1, 2]), (make_args(Some(1), Some(2.0)), vec![2]), (make_args(None, Some(-2.0)), vec![1, 2, 3]), (make_args(None, None), vec![1, 2, 3, 4, 5]), (make_args(Some(3), Some(1.0)), vec![]), (make_args(Some(9), None), vec![]), ]; for (args, expected) in inputs { let res = slice(&v, &args); assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_group_by() { let input = json!([ {"id": 1, "year": 2015}, {"id": 2, "year": 2015}, {"id": 3, "year": 2016}, {"id": 4, "year": 2017}, {"id": 5, "year": 2017}, {"id": 6, "year": 2017}, {"id": 7, "year": 2018}, {"id": 8}, {"id": 9, "year": null}, ]); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("year").unwrap()); let expected = json!({ "2015": [{"id": 1, "year": 2015}, {"id": 2, "year": 2015}], "2016": [{"id": 3, "year": 2016}], "2017": [{"id": 4, "year": 2017}, {"id": 5, "year": 2017}, {"id": 6, "year": 2017}], "2018": [{"id": 7, "year": 2018}], }); let res = group_by(&input, &args); assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } #[test] fn test_group_by_nested_key() { let input = json!([ {"id": 1, "company": {"id": 1}}, {"id": 2, "company": {"id": 2}}, {"id": 3, "company": {"id": 3}}, {"id": 4, "company": {"id": 4}}, {"id": 5, "company": {"id": 4}}, {"id": 6, "company": {"id": 5}}, {"id": 7, "company": {"id": 5}}, {"id": 8}, {"id": 9, "company": null}, ]); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("company.id").unwrap()); let expected = json!({ "1": [{"id": 1, "company": {"id": 1}}], "2": [{"id": 2, "company": {"id": 2}}], "3": [{"id": 3, "company": {"id": 3}}], "4": [{"id": 4, "company": {"id": 4}}, {"id": 5, "company": {"id": 4}}], "5": [{"id": 6, "company": {"id": 5}}, {"id": 7, "company": {"id": 5}}], }); let res = group_by(&input, &args); assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } #[test] fn test_filter_empty() { let res = filter(&json!([]), &HashMap::new()); assert!(res.is_ok()); assert_eq!(res.unwrap(), json!([])); } #[test] fn test_filter() { let input = json!([ {"id": 1, "year": 2015}, {"id": 2, "year": 2015}, {"id": 3, "year": 2016}, {"id": 4, "year": 2017}, {"id": 5, "year": 2017}, {"id": 6, "year": 2017}, {"id": 7, "year": 2018}, {"id": 8}, {"id": 9, "year": null}, ]); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("year").unwrap()); args.insert("value".to_string(), to_value(2015).unwrap()); let expected = json!([ {"id": 1, "year": 2015}, {"id": 2, "year": 2015}, ]); let res = filter(&input, &args); assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } #[test] fn test_filter_no_value() { let input = json!([ {"id": 1, "year": 2015}, {"id": 2, "year": 2015}, {"id": 3, "year": 2016}, {"id": 4, "year": 2017}, {"id": 5, "year": 2017}, {"id": 6, "year": 2017}, {"id": 7, "year": 2018}, {"id": 8}, {"id": 9, "year": null}, ]); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("year").unwrap()); let expected = json!([ {"id": 1, "year": 2015}, {"id": 2, "year": 2015}, {"id": 3, "year": 2016}, {"id": 4, "year": 2017}, {"id": 5, "year": 2017}, {"id": 6, "year": 2017}, {"id": 7, "year": 2018}, ]); let res = filter(&input, &args); assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } #[test] fn test_map_empty() { let res = map(&json!([]), &HashMap::new()); assert!(res.is_ok()); assert_eq!(res.unwrap(), json!([])); } #[test] fn test_map() { let input = json!([ {"id": 1, "year": 2015}, {"id": 2, "year": true}, {"id": 3, "year": 2016.5}, {"id": 4, "year": "2017"}, {"id": 5, "year": 2017}, {"id": 6, "year": 2017}, {"id": 7, "year": [1900, 1901]}, {"id": 8, "year": {"a": 2018, "b": 2019}}, {"id": 9}, {"id": 10, "year": null}, ]); let mut args = HashMap::new(); args.insert("attribute".to_string(), to_value("year").unwrap()); let expected = json!([2015, true, 2016.5, "2017", 2017, 2017, [1900, 1901], {"a": 2018, "b": 2019}]); let res = map(&input, &args); assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } #[test] fn test_concat_array() { let input = json!([1, 2, 3,]); let mut args = HashMap::new(); args.insert("with".to_string(), json!([3, 4])); let expected = json!([1, 2, 3, 3, 4,]); let res = concat(&input, &args); assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } #[test] fn test_concat_single_value() { let input = json!([1, 2, 3,]); let mut args = HashMap::new(); args.insert("with".to_string(), json!(4)); let expected = json!([1, 2, 3, 4,]); let res = concat(&input, &args); assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } } tera-1.20.0/src/builtins/filters/common.rs000064400000000000000000000443461046102023000166000ustar 00000000000000/// Filters operating on multiple types use std::collections::HashMap; #[cfg(feature = "date-locale")] use std::convert::TryFrom; use std::iter::FromIterator; use crate::errors::{Error, Result}; use crate::utils::render_to_string; #[cfg(feature = "builtins")] use chrono::{ format::{Item, StrftimeItems}, DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone, Utc, }; #[cfg(feature = "builtins")] use chrono_tz::Tz; use serde_json::value::{to_value, Value}; use serde_json::{to_string, to_string_pretty}; use crate::context::ValueRender; // Returns the number of items in an array or an object, or the number of characters in a string. pub fn length(value: &Value, _: &HashMap) -> Result { match value { Value::Array(arr) => Ok(to_value(arr.len()).unwrap()), Value::Object(m) => Ok(to_value(m.len()).unwrap()), Value::String(s) => Ok(to_value(s.chars().count()).unwrap()), _ => Err(Error::msg( "Filter `length` was used on a value that isn't an array, an object, or a string.", )), } } // Reverses the elements of an array or the characters in a string. pub fn reverse(value: &Value, _: &HashMap) -> Result { match value { Value::Array(arr) => { let mut rev = arr.clone(); rev.reverse(); to_value(&rev).map_err(Error::json) } Value::String(s) => to_value(String::from_iter(s.chars().rev())).map_err(Error::json), _ => Err(Error::msg(format!( "Filter `reverse` received an incorrect type for arg `value`: \ got `{}` but expected Array|String", value ))), } } // Encodes a value of any type into json, optionally `pretty`-printing it // `pretty` can be true to enable pretty-print, or omitted for compact printing pub fn json_encode(value: &Value, args: &HashMap) -> Result { let pretty = args.get("pretty").and_then(Value::as_bool).unwrap_or(false); if pretty { to_string_pretty(&value).map(Value::String).map_err(Error::json) } else { to_string(&value).map(Value::String).map_err(Error::json) } } /// Returns a formatted time according to the given `format` argument. /// `format` defaults to the ISO 8601 `YYYY-MM-DD` format. /// /// Input can be an i64 timestamp (seconds since epoch) or an RFC3339 string /// (default serialization format for `chrono::DateTime`). /// /// a full reference for the time formatting syntax is available /// on [chrono docs](https://lifthrasiir.github.io/rust-chrono/chrono/format/strftime/index.html) #[cfg(feature = "builtins")] pub fn date(value: &Value, args: &HashMap) -> Result { let format = match args.get("format") { Some(val) => try_get_value!("date", "format", String, val), None => "%Y-%m-%d".to_string(), }; let items: Vec = StrftimeItems::new(&format).filter(|item| matches!(item, Item::Error)).collect(); if !items.is_empty() { return Err(Error::msg(format!("Invalid date format `{}`", format))); } let timezone = match args.get("timezone") { Some(val) => { let timezone = try_get_value!("date", "timezone", String, val); match timezone.parse::() { Ok(timezone) => Some(timezone), Err(_) => { return Err(Error::msg(format!("Error parsing `{}` as a timezone", timezone))) } } } None => None, }; #[cfg(feature = "date-locale")] let formatted = { let locale = match args.get("locale") { Some(val) => { let locale = try_get_value!("date", "locale", String, val); chrono::Locale::try_from(locale.as_str()) .map_err(|_| Error::msg(format!("Error parsing `{}` as a locale", locale)))? } None => chrono::Locale::POSIX, }; match value { Value::Number(n) => match n.as_i64() { Some(i) => { let date = NaiveDateTime::from_timestamp_opt(i, 0).expect( "out of bound seconds should not appear, as we set nanoseconds to zero", ); match timezone { Some(timezone) => { timezone.from_utc_datetime(&date).format_localized(&format, locale) } None => date.format(&format), } } None => { return Err(Error::msg(format!("Filter `date` was invoked on a float: {}", n))) } }, Value::String(s) => { if s.contains('T') { match s.parse::>() { Ok(val) => match timezone { Some(timezone) => { val.with_timezone(&timezone).format_localized(&format, locale) } None => val.format_localized(&format, locale), }, Err(_) => match s.parse::() { Ok(val) => DateTime::::from_naive_utc_and_offset(val, Utc) .format_localized(&format, locale), Err(_) => { return Err(Error::msg(format!( "Error parsing `{:?}` as rfc3339 date or naive datetime", s ))); } }, } } else { match NaiveDate::parse_from_str(s, "%Y-%m-%d") { Ok(val) => DateTime::::from_naive_utc_and_offset( val.and_hms_opt(0, 0, 0).expect( "out of bound should not appear, as we set the time to zero", ), Utc, ) .format_localized(&format, locale), Err(_) => { return Err(Error::msg(format!( "Error parsing `{:?}` as YYYY-MM-DD date", s ))); } } } } _ => { return Err(Error::msg(format!( "Filter `date` received an incorrect type for arg `value`: \ got `{:?}` but expected i64|u64|String", value ))); } } }; #[cfg(not(feature = "date-locale"))] let formatted = match value { Value::Number(n) => match n.as_i64() { Some(i) => { let date = NaiveDateTime::from_timestamp_opt(i, 0).expect( "out of bound seconds should not appear, as we set nanoseconds to zero", ); match timezone { Some(timezone) => timezone.from_utc_datetime(&date).format(&format), None => date.format(&format), } } None => return Err(Error::msg(format!("Filter `date` was invoked on a float: {}", n))), }, Value::String(s) => { if s.contains('T') { match s.parse::>() { Ok(val) => match timezone { Some(timezone) => val.with_timezone(&timezone).format(&format), None => val.format(&format), }, Err(_) => match s.parse::() { Ok(val) => { DateTime::::from_naive_utc_and_offset(val, Utc).format(&format) } Err(_) => { return Err(Error::msg(format!( "Error parsing `{:?}` as rfc3339 date or naive datetime", s ))); } }, } } else { match NaiveDate::parse_from_str(s, "%Y-%m-%d") { Ok(val) => DateTime::::from_naive_utc_and_offset( val.and_hms_opt(0, 0, 0) .expect("out of bound should not appear, as we set the time to zero"), Utc, ) .format(&format), Err(_) => { return Err(Error::msg(format!( "Error parsing `{:?}` as YYYY-MM-DD date", s ))); } } } } _ => { return Err(Error::msg(format!( "Filter `date` received an incorrect type for arg `value`: \ got `{:?}` but expected i64|u64|String", value ))); } }; to_value(formatted.to_string()).map_err(Error::json) } // Returns the given value as a string. pub fn as_str(value: &Value, _: &HashMap) -> Result { let value = render_to_string(|| format!("as_str for value of kind {}", value), |w| value.render(w))?; to_value(value).map_err(Error::json) } #[cfg(test)] mod tests { use super::*; #[cfg(feature = "builtins")] use chrono::{DateTime, Local}; use serde_json; use serde_json::value::to_value; use std::collections::HashMap; #[test] fn as_str_object() { let map: HashMap = HashMap::new(); let result = as_str(&to_value(map).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("[object]").unwrap()); } #[test] fn as_str_vec() { let result = as_str(&to_value(vec![1, 2, 3, 4]).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("[1, 2, 3, 4]").unwrap()); } #[test] fn length_vec() { let result = length(&to_value(vec![1, 2, 3, 4]).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(4).unwrap()); } #[test] fn length_object() { let mut map: HashMap = HashMap::new(); map.insert("foo".to_string(), "bar".to_string()); let result = length(&to_value(&map).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(1).unwrap()); } #[test] fn length_str() { let result = length(&to_value("Hello World").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(11).unwrap()); } #[test] fn length_str_nonascii() { let result = length(&to_value("日本語").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(3).unwrap()); } #[test] fn length_num() { let result = length(&to_value(15).unwrap(), &HashMap::new()); assert!(result.is_err()); } #[test] fn reverse_vec() { let result = reverse(&to_value(vec![1, 2, 3, 4]).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(vec![4, 3, 2, 1]).unwrap()); } #[test] fn reverse_str() { let result = reverse(&to_value("Hello World").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("dlroW olleH").unwrap()); } #[test] fn reverse_num() { let result = reverse(&to_value(1.23).unwrap(), &HashMap::new()); assert!(result.is_err()); assert_eq!( result.err().unwrap().to_string(), "Filter `reverse` received an incorrect type for arg `value`: got `1.23` but expected Array|String" ); } #[cfg(feature = "builtins")] #[test] fn date_default() { let args = HashMap::new(); let result = date(&to_value(1482720453).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("2016-12-26").unwrap()); } #[cfg(feature = "builtins")] #[test] fn date_custom_format() { let mut args = HashMap::new(); args.insert("format".to_string(), to_value("%Y-%m-%d %H:%M").unwrap()); let result = date(&to_value(1482720453).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("2016-12-26 02:47").unwrap()); } // https://zola.discourse.group/t/can-i-generate-a-random-number-within-a-range/238?u=keats // https://github.com/chronotope/chrono/issues/47 #[cfg(feature = "builtins")] #[test] fn date_errors_on_incorrect_format() { let mut args = HashMap::new(); args.insert("format".to_string(), to_value("%2f").unwrap()); let result = date(&to_value(1482720453).unwrap(), &args); assert!(result.is_err()); } #[cfg(feature = "builtins")] #[test] fn date_rfc3339() { let args = HashMap::new(); let dt: DateTime = Local::now(); let result = date(&to_value(dt.to_rfc3339()).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(dt.format("%Y-%m-%d").to_string()).unwrap()); } #[cfg(feature = "builtins")] #[test] fn date_rfc3339_preserves_timezone() { let mut args = HashMap::new(); args.insert("format".to_string(), to_value("%Y-%m-%d %z").unwrap()); let result = date(&to_value("1996-12-19T16:39:57-08:00").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("1996-12-19 -0800").unwrap()); } #[cfg(feature = "builtins")] #[test] fn date_yyyy_mm_dd() { let mut args = HashMap::new(); args.insert("format".to_string(), to_value("%a, %d %b %Y %H:%M:%S %z").unwrap()); let result = date(&to_value("2017-03-05").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("Sun, 05 Mar 2017 00:00:00 +0000").unwrap()); } #[cfg(feature = "builtins")] #[test] fn date_from_naive_datetime() { let mut args = HashMap::new(); args.insert("format".to_string(), to_value("%a, %d %b %Y %H:%M:%S").unwrap()); let result = date(&to_value("2017-03-05T00:00:00.602").unwrap(), &args); println!("{:?}", result); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("Sun, 05 Mar 2017 00:00:00").unwrap()); } // https://github.com/getzola/zola/issues/1279 #[cfg(feature = "builtins")] #[test] fn date_format_doesnt_panic() { let mut args = HashMap::new(); args.insert("format".to_string(), to_value("%+S").unwrap()); let result = date(&to_value("2017-01-01T00:00:00").unwrap(), &args); assert!(result.is_ok()); } #[cfg(feature = "builtins")] #[test] fn date_with_timezone() { let mut args = HashMap::new(); args.insert("timezone".to_string(), to_value("America/New_York").unwrap()); let result = date(&to_value("2019-09-19T01:48:44.581Z").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("2019-09-18").unwrap()); } #[cfg(feature = "builtins")] #[test] fn date_with_invalid_timezone() { let mut args = HashMap::new(); args.insert("timezone".to_string(), to_value("Narnia").unwrap()); let result = date(&to_value("2019-09-19T01:48:44.581Z").unwrap(), &args); assert!(result.is_err()); assert_eq!(result.err().unwrap().to_string(), "Error parsing `Narnia` as a timezone"); } #[cfg(feature = "builtins")] #[test] fn date_timestamp() { let mut args = HashMap::new(); args.insert("format".to_string(), to_value("%Y-%m-%d").unwrap()); let result = date(&to_value(1648302603).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("2022-03-26").unwrap()); } #[cfg(feature = "builtins")] #[test] fn date_timestamp_with_timezone() { let mut args = HashMap::new(); args.insert("timezone".to_string(), to_value("Europe/Berlin").unwrap()); let result = date(&to_value(1648252203).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("2022-03-26").unwrap()); } #[cfg(feature = "date-locale")] #[test] fn date_timestamp_with_timezone_and_locale() { let mut args = HashMap::new(); args.insert("format".to_string(), to_value("%A %-d %B").unwrap()); args.insert("timezone".to_string(), to_value("Europe/Paris").unwrap()); args.insert("locale".to_string(), to_value("fr_FR").unwrap()); let result = date(&to_value(1659817310).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("samedi 6 août").unwrap()); } #[cfg(feature = "date-locale")] #[test] fn date_with_invalid_locale() { let mut args = HashMap::new(); args.insert("locale".to_string(), to_value("xx_XX").unwrap()); let result = date(&to_value("2019-09-19T01:48:44.581Z").unwrap(), &args); assert!(result.is_err()); assert_eq!(result.err().unwrap().to_string(), "Error parsing `xx_XX` as a locale"); } #[test] fn test_json_encode() { let args = HashMap::new(); let result = json_encode(&serde_json::from_str("{\"key\": [\"value1\", 2, true]}").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("{\"key\":[\"value1\",2,true]}").unwrap()); } #[test] fn test_json_encode_pretty() { let mut args = HashMap::new(); args.insert("pretty".to_string(), to_value(true).unwrap()); let result = json_encode(&serde_json::from_str("{\"key\": [\"value1\", 2, true]}").unwrap(), &args); assert!(result.is_ok()); assert_eq!( result.unwrap(), to_value("{\n \"key\": [\n \"value1\",\n 2,\n true\n ]\n}").unwrap() ); } } tera-1.20.0/src/builtins/filters/mod.rs000064400000000000000000000013461046102023000160600ustar 00000000000000use std::collections::HashMap; use crate::errors::Result; use serde_json::value::Value; pub mod array; pub mod common; pub mod number; pub mod object; pub mod string; /// The filter function type definition pub trait Filter: Sync + Send { /// The filter function type definition fn filter(&self, value: &Value, args: &HashMap) -> Result; /// Whether the current filter's output should be treated as safe, defaults to `false` fn is_safe(&self) -> bool { false } } impl Filter for F where F: Fn(&Value, &HashMap) -> Result + Sync + Send, { fn filter(&self, value: &Value, args: &HashMap) -> Result { self(value, args) } } tera-1.20.0/src/builtins/filters/number.rs000064400000000000000000000203461046102023000165720ustar 00000000000000/// Filters operating on numbers use std::collections::HashMap; #[cfg(feature = "builtins")] use humansize::format_size; use serde_json::value::{to_value, Value}; use crate::errors::{Error, Result}; /// Returns the absolute value of the argument. pub fn abs(value: &Value, _: &HashMap) -> Result { if value.as_u64().is_some() { Ok(value.clone()) } else if let Some(num) = value.as_i64() { Ok(to_value(num.abs()).unwrap()) } else if let Some(num) = value.as_f64() { Ok(to_value(num.abs()).unwrap()) } else { Err(Error::msg("Filter `abs` was used on a value that isn't a number.")) } } /// Returns a plural suffix if the value is not equal to ±1, or a singular /// suffix otherwise. The plural suffix defaults to `s` and the singular suffix /// defaults to the empty string (i.e nothing). pub fn pluralize(value: &Value, args: &HashMap) -> Result { let num = try_get_value!("pluralize", "value", f64, value); let plural = match args.get("plural") { Some(val) => try_get_value!("pluralize", "plural", String, val), None => "s".to_string(), }; let singular = match args.get("singular") { Some(val) => try_get_value!("pluralize", "singular", String, val), None => "".to_string(), }; // English uses plural when it isn't one if (num.abs() - 1.).abs() > ::std::f64::EPSILON { Ok(to_value(plural).unwrap()) } else { Ok(to_value(singular).unwrap()) } } /// Returns a rounded number using the `method` arg and `precision` given. /// `method` defaults to `common` which will round to the nearest number. /// `ceil` and `floor` are also available as method. /// `precision` defaults to `0`, meaning it will round to an integer pub fn round(value: &Value, args: &HashMap) -> Result { let num = try_get_value!("round", "value", f64, value); let method = match args.get("method") { Some(val) => try_get_value!("round", "method", String, val), None => "common".to_string(), }; let precision = match args.get("precision") { Some(val) => try_get_value!("round", "precision", i32, val), None => 0, }; let multiplier = if precision == 0 { 1.0 } else { 10.0_f64.powi(precision) }; match method.as_ref() { "common" => Ok(to_value((multiplier * num).round() / multiplier).unwrap()), "ceil" => Ok(to_value((multiplier * num).ceil() / multiplier).unwrap()), "floor" => Ok(to_value((multiplier * num).floor() / multiplier).unwrap()), _ => Err(Error::msg(format!( "Filter `round` received an incorrect value for arg `method`: got `{:?}`, \ only common, ceil and floor are allowed", method ))), } } /// Returns a human-readable file size (i.e. '110 MB') from an integer #[cfg(feature = "builtins")] pub fn filesizeformat(value: &Value, args: &HashMap) -> Result { let num = try_get_value!("filesizeformat", "value", usize, value); let binary = match args.get("binary") { Some(binary) => try_get_value!("filesizeformat", "binary", bool, binary), None => false, }; let format = if binary { humansize::BINARY } else { humansize::WINDOWS }; Ok(to_value(format_size(num, format)) .expect("json serializing should always be possible for a string")) } #[cfg(test)] mod tests { use super::*; use serde_json::value::to_value; use std::collections::HashMap; #[test] fn test_abs_unsigend() { let result = abs(&to_value(1).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(1).unwrap()); } #[test] fn test_abs_negative_integer() { let result = abs(&to_value(-1).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(1).unwrap()); } #[test] fn test_abs_negative_float() { let result = abs(&to_value(-1.0).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(1.0).unwrap()); } #[test] fn test_abs_non_number() { let result = abs(&to_value("nan").unwrap(), &HashMap::new()); assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), "Filter `abs` was used on a value that isn't a number." ); } #[test] fn test_pluralize_single() { let result = pluralize(&to_value(1).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("").unwrap()); } #[test] fn test_pluralize_multiple() { let result = pluralize(&to_value(2).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("s").unwrap()); } #[test] fn test_pluralize_zero() { let result = pluralize(&to_value(0).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("s").unwrap()); } #[test] fn test_pluralize_multiple_custom_plural() { let mut args = HashMap::new(); args.insert("plural".to_string(), to_value("es").unwrap()); let result = pluralize(&to_value(2).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("es").unwrap()); } #[test] fn test_pluralize_multiple_custom_singular() { let mut args = HashMap::new(); args.insert("singular".to_string(), to_value("y").unwrap()); let result = pluralize(&to_value(1).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("y").unwrap()); } #[test] fn test_round_default() { let result = round(&to_value(2.1).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(2.0).unwrap()); } #[test] fn test_round_default_precision() { let mut args = HashMap::new(); args.insert("precision".to_string(), to_value(2).unwrap()); let result = round(&to_value(3.15159265359).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(3.15).unwrap()); } #[test] fn test_round_ceil() { let mut args = HashMap::new(); args.insert("method".to_string(), to_value("ceil").unwrap()); let result = round(&to_value(2.1).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(3.0).unwrap()); } #[test] fn test_round_ceil_precision() { let mut args = HashMap::new(); args.insert("method".to_string(), to_value("ceil").unwrap()); args.insert("precision".to_string(), to_value(1).unwrap()); let result = round(&to_value(2.11).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(2.2).unwrap()); } #[test] fn test_round_floor() { let mut args = HashMap::new(); args.insert("method".to_string(), to_value("floor").unwrap()); let result = round(&to_value(2.1).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(2.0).unwrap()); } #[test] fn test_round_floor_precision() { let mut args = HashMap::new(); args.insert("method".to_string(), to_value("floor").unwrap()); args.insert("precision".to_string(), to_value(1).unwrap()); let result = round(&to_value(2.91).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(2.9).unwrap()); } #[cfg(feature = "builtins")] #[test] fn test_filesizeformat() { let args = HashMap::new(); let result = filesizeformat(&to_value(123456789).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("117.74 MB").unwrap()); } #[cfg(feature = "builtins")] #[test] fn test_filesizeformat_binary() { let mut args = HashMap::new(); args.insert("binary".to_string(), to_value(true).unwrap()); let result = filesizeformat(&to_value(123456789).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("117.74 MiB").unwrap()); } } tera-1.20.0/src/builtins/filters/object.rs000064400000000000000000000061731046102023000165520ustar 00000000000000/// Filters operating on numbers use std::collections::HashMap; use serde_json::value::Value; use crate::errors::{Error, Result}; /// Returns a value by a `key` argument from a given object pub fn get(value: &Value, args: &HashMap) -> Result { let default = args.get("default"); let key = match args.get("key") { Some(val) => try_get_value!("get", "key", String, val), None => return Err(Error::msg("The `get` filter has to have an `key` argument")), }; match value.as_object() { Some(o) => match o.get(&key) { Some(val) => Ok(val.clone()), // If the value is not present, allow for an optional default value None => match default { Some(def) => Ok(def.clone()), None => Err(Error::msg(format!( "Filter `get` tried to get key `{}` but it wasn't found", &key ))), }, }, None => Err(Error::msg("Filter `get` was used on a value that isn't an object")), } } #[cfg(test)] mod tests { use super::*; use serde_json::value::to_value; use std::collections::HashMap; #[test] fn test_get_filter_exists() { let mut obj = HashMap::new(); obj.insert("1".to_string(), "first".to_string()); obj.insert("2".to_string(), "second".to_string()); let mut args = HashMap::new(); args.insert("key".to_string(), to_value("1").unwrap()); let result = get(&to_value(&obj).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("first").unwrap()); } #[test] fn test_get_filter_doesnt_exist() { let mut obj = HashMap::new(); obj.insert("1".to_string(), "first".to_string()); obj.insert("2".to_string(), "second".to_string()); let mut args = HashMap::new(); args.insert("key".to_string(), to_value("3").unwrap()); let result = get(&to_value(&obj).unwrap(), &args); assert!(result.is_err()); } #[test] fn test_get_filter_with_default_exists() { let mut obj = HashMap::new(); obj.insert("1".to_string(), "first".to_string()); obj.insert("2".to_string(), "second".to_string()); let mut args = HashMap::new(); args.insert("key".to_string(), to_value("1").unwrap()); args.insert("default".to_string(), to_value("default").unwrap()); let result = get(&to_value(&obj).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("first").unwrap()); } #[test] fn test_get_filter_with_default_doesnt_exist() { let mut obj = HashMap::new(); obj.insert("1".to_string(), "first".to_string()); obj.insert("2".to_string(), "second".to_string()); let mut args = HashMap::new(); args.insert("key".to_string(), to_value("3").unwrap()); args.insert("default".to_string(), to_value("default").unwrap()); let result = get(&to_value(&obj).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("default").unwrap()); } } tera-1.20.0/src/builtins/filters/string.rs000064400000000000000000001022331046102023000166040ustar 00000000000000/// Filters operating on string use std::collections::HashMap; use lazy_static::lazy_static; use regex::{Captures, Regex}; use serde_json::value::{to_value, Value}; use unic_segment::GraphemeIndices; #[cfg(feature = "urlencode")] use percent_encoding::{percent_encode, AsciiSet, NON_ALPHANUMERIC}; use crate::errors::{Error, Result}; use crate::utils; /// https://url.spec.whatwg.org/#fragment-percent-encode-set #[cfg(feature = "urlencode")] const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); /// https://url.spec.whatwg.org/#path-percent-encode-set #[cfg(feature = "urlencode")] const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}'); /// https://url.spec.whatwg.org/#userinfo-percent-encode-set #[cfg(feature = "urlencode")] const USERINFO_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET .add(b'/') .add(b':') .add(b';') .add(b'=') .add(b'@') .add(b'[') .add(b'\\') .add(b']') .add(b'^') .add(b'|'); /// Same as Python quote /// https://github.com/python/cpython/blob/da27d9b9dc44913ffee8f28d9638985eaaa03755/Lib/urllib/parse.py#L787 /// with `/` not escaped #[cfg(feature = "urlencode")] const PYTHON_ENCODE_SET: &AsciiSet = &USERINFO_ENCODE_SET .remove(b'/') .add(b':') .add(b'?') .add(b'#') .add(b'[') .add(b']') .add(b'@') .add(b'!') .add(b'$') .add(b'&') .add(b'\'') .add(b'(') .add(b')') .add(b'*') .add(b'+') .add(b',') .add(b';') .add(b'='); lazy_static! { static ref STRIPTAGS_RE: Regex = Regex::new(r"(|<[^>]*>)").unwrap(); static ref WORDS_RE: Regex = Regex::new(r"\b(?P[\w'])(?P[\w']*)\b").unwrap(); static ref SPACELESS_RE: Regex = Regex::new(r">\s+<").unwrap(); } /// Convert a value to uppercase. pub fn upper(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("upper", "value", String, value); Ok(to_value(s.to_uppercase()).unwrap()) } /// Convert a value to lowercase. pub fn lower(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("lower", "value", String, value); Ok(to_value(s.to_lowercase()).unwrap()) } /// Strip leading and trailing whitespace. pub fn trim(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("trim", "value", String, value); Ok(to_value(s.trim()).unwrap()) } /// Strip leading whitespace. pub fn trim_start(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("trim_start", "value", String, value); Ok(to_value(s.trim_start()).unwrap()) } /// Strip trailing whitespace. pub fn trim_end(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("trim_end", "value", String, value); Ok(to_value(s.trim_end()).unwrap()) } /// Strip leading characters that match the given pattern. pub fn trim_start_matches(value: &Value, args: &HashMap) -> Result { let s = try_get_value!("trim_start_matches", "value", String, value); let pat = match args.get("pat") { Some(pat) => { let p = try_get_value!("trim_start_matches", "pat", String, pat); // When reading from a file, it will escape `\n` to `\\n` for example so we need // to replace double escape. In practice it might cause issues if someone wants to split // by `\\n` for real but that seems pretty unlikely p.replace("\\n", "\n").replace("\\t", "\t") } None => return Err(Error::msg("Filter `trim_start_matches` expected an arg called `pat`")), }; Ok(to_value(s.trim_start_matches(&pat)).unwrap()) } /// Strip trailing characters that match the given pattern. pub fn trim_end_matches(value: &Value, args: &HashMap) -> Result { let s = try_get_value!("trim_end_matches", "value", String, value); let pat = match args.get("pat") { Some(pat) => { let p = try_get_value!("trim_end_matches", "pat", String, pat); // When reading from a file, it will escape `\n` to `\\n` for example so we need // to replace double escape. In practice it might cause issues if someone wants to split // by `\\n` for real but that seems pretty unlikely p.replace("\\n", "\n").replace("\\t", "\t") } None => return Err(Error::msg("Filter `trim_end_matches` expected an arg called `pat`")), }; Ok(to_value(s.trim_end_matches(&pat)).unwrap()) } /// Truncates a string to the indicated length. /// /// # Arguments /// /// * `value` - The string that needs to be truncated. /// * `args` - A set of key/value arguments that can take the following /// keys. /// * `length` - The length at which the string needs to be truncated. If /// the length is larger than the length of the string, the string is /// returned untouched. The default value is 255. /// * `end` - The ellipsis string to be used if the given string is /// truncated. The default value is "…". /// /// # Remarks /// /// The return value of this function might be longer than `length`: the `end` /// string is *added* after the truncation occurs. /// pub fn truncate(value: &Value, args: &HashMap) -> Result { let s = try_get_value!("truncate", "value", String, value); let length = match args.get("length") { Some(l) => try_get_value!("truncate", "length", usize, l), None => 255, }; let end = match args.get("end") { Some(l) => try_get_value!("truncate", "end", String, l), None => "…".to_string(), }; let graphemes = GraphemeIndices::new(&s).collect::>(); // Nothing to truncate? if length >= graphemes.len() { return Ok(to_value(&s).unwrap()); } let result = s[..graphemes[length].0].to_string() + &end; Ok(to_value(result).unwrap()) } /// Gets the number of words in a string. pub fn wordcount(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("wordcount", "value", String, value); Ok(to_value(s.split_whitespace().count()).unwrap()) } /// Replaces given `from` substring with `to` string. pub fn replace(value: &Value, args: &HashMap) -> Result { let s = try_get_value!("replace", "value", String, value); let from = match args.get("from") { Some(val) => try_get_value!("replace", "from", String, val), None => return Err(Error::msg("Filter `replace` expected an arg called `from`")), }; let to = match args.get("to") { Some(val) => try_get_value!("replace", "to", String, val), None => return Err(Error::msg("Filter `replace` expected an arg called `to`")), }; Ok(to_value(s.replace(&from, &to)).unwrap()) } /// First letter of the string is uppercase rest is lowercase pub fn capitalize(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("capitalize", "value", String, value); let mut chars = s.chars(); match chars.next() { None => Ok(to_value("").unwrap()), Some(f) => { let res = f.to_uppercase().collect::() + &chars.as_str().to_lowercase(); Ok(to_value(res).unwrap()) } } } /// Percent-encodes reserved URI characters #[cfg(feature = "urlencode")] pub fn urlencode(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("urlencode", "value", String, value); let encoded = percent_encode(s.as_bytes(), PYTHON_ENCODE_SET).to_string(); Ok(Value::String(encoded)) } /// Percent-encodes all non-alphanumeric characters #[cfg(feature = "urlencode")] pub fn urlencode_strict(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("urlencode_strict", "value", String, value); let encoded = percent_encode(s.as_bytes(), NON_ALPHANUMERIC).to_string(); Ok(Value::String(encoded)) } /// Escapes quote characters pub fn addslashes(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("addslashes", "value", String, value); Ok(to_value(s.replace('\\', "\\\\").replace('\"', "\\\"").replace('\'', "\\\'")).unwrap()) } /// Transform a string into a slug #[cfg(feature = "builtins")] pub fn slugify(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("slugify", "value", String, value); Ok(to_value(slug::slugify(s)).unwrap()) } /// Capitalizes each word in the string pub fn title(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("title", "value", String, value); Ok(to_value(WORDS_RE.replace_all(&s, |caps: &Captures| { let first = caps["first"].to_uppercase(); let rest = caps["rest"].to_lowercase(); format!("{}{}", first, rest) })) .unwrap()) } /// Convert line breaks (`\n` or `\r\n`) to HTML linebreaks (`
`). /// /// Example: The input "Hello\nWorld" turns into "Hello
World". pub fn linebreaksbr(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("linebreaksbr", "value", String, value); Ok(to_value(s.replace("\r\n", "
").replace('\n', "
")).unwrap()) } /// Indents a string by the specified width. /// /// # Arguments /// /// * `value` - The string to indent. /// * `args` - A set of key/value arguments that can take the following /// keys. /// * `prefix` - The prefix used for indentation. The default value is 4 spaces. /// * `first` - True indents the first line. The default is false. /// * `blank` - True indents blank lines. The default is false. /// pub fn indent(value: &Value, args: &HashMap) -> Result { let s = try_get_value!("indent", "value", String, value); let prefix = match args.get("prefix") { Some(p) => try_get_value!("indent", "prefix", String, p), None => " ".to_string(), }; let first = match args.get("first") { Some(f) => try_get_value!("indent", "first", bool, f), None => false, }; let blank = match args.get("blank") { Some(b) => try_get_value!("indent", "blank", bool, b), None => false, }; // Attempt to pre-allocate enough space to prevent additional allocations/copies let mut out = String::with_capacity( s.len() + (prefix.len() * (s.chars().filter(|&c| c == '\n').count() + 1)), ); let mut first_pass = true; for line in s.lines() { if first_pass { if first { out.push_str(&prefix); } first_pass = false; } else { out.push('\n'); if blank || !line.trim_start().is_empty() { out.push_str(&prefix); } } out.push_str(line); } Ok(to_value(&out).unwrap()) } /// Removes html tags from string pub fn striptags(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("striptags", "value", String, value); Ok(to_value(STRIPTAGS_RE.replace_all(&s, "")).unwrap()) } /// Removes spaces between html tags from string pub fn spaceless(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("spaceless", "value", String, value); Ok(to_value(SPACELESS_RE.replace_all(&s, "><")).unwrap()) } /// Returns the given text with all special HTML characters encoded pub fn escape_html(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("escape_html", "value", String, value); Ok(Value::String(utils::escape_html(&s))) } /// Returns the given text with all special XML characters encoded /// Very similar to `escape_html`, just a few characters less are encoded pub fn escape_xml(value: &Value, _: &HashMap) -> Result { let s = try_get_value!("escape_html", "value", String, value); let mut output = String::with_capacity(s.len() * 2); for c in s.chars() { match c { '&' => output.push_str("&"), '<' => output.push_str("<"), '>' => output.push_str(">"), '"' => output.push_str("""), '\'' => output.push_str("'"), _ => output.push(c), } } Ok(Value::String(output)) } /// Split the given string by the given pattern. pub fn split(value: &Value, args: &HashMap) -> Result { let s = try_get_value!("split", "value", String, value); let pat = match args.get("pat") { Some(pat) => { let p = try_get_value!("split", "pat", String, pat); // When reading from a file, it will escape `\n` to `\\n` for example so we need // to replace double escape. In practice it might cause issues if someone wants to split // by `\\n` for real but that seems pretty unlikely p.replace("\\n", "\n").replace("\\t", "\t") } None => return Err(Error::msg("Filter `split` expected an arg called `pat`")), }; Ok(to_value(s.split(&pat).collect::>()).unwrap()) } /// Convert the value to a signed integer number pub fn int(value: &Value, args: &HashMap) -> Result { let default = match args.get("default") { Some(d) => try_get_value!("int", "default", i64, d), None => 0, }; let base = match args.get("base") { Some(b) => try_get_value!("int", "base", u32, b), None => 10, }; let v = match value { Value::String(s) => { let s = s.trim(); let s = match base { 2 => s.trim_start_matches("0b"), 8 => s.trim_start_matches("0o"), 16 => s.trim_start_matches("0x"), _ => s, }; match i64::from_str_radix(s, base) { Ok(v) => v, Err(_) => { if s.contains('.') { match s.parse::() { Ok(f) => f as i64, Err(_) => default, } } else { default } } } } Value::Number(n) => match n.as_f64() { Some(f) => f as i64, None => match n.as_i64() { Some(i) => i, None => default, }, }, _ => return Err(Error::msg("Filter `int` received an unexpected type")), }; Ok(to_value(v).unwrap()) } /// Convert the value to a floating point number pub fn float(value: &Value, args: &HashMap) -> Result { let default = match args.get("default") { Some(d) => try_get_value!("float", "default", f64, d), None => 0.0, }; let v = match value { Value::String(s) => { let s = s.trim(); s.parse::().unwrap_or(default) } Value::Number(n) => match n.as_f64() { Some(f) => f, None => match n.as_i64() { Some(i) => i as f64, None => default, }, }, _ => return Err(Error::msg("Filter `float` received an unexpected type")), }; Ok(to_value(v).unwrap()) } #[cfg(test)] mod tests { use std::collections::HashMap; use serde_json::value::to_value; use super::*; #[test] fn test_upper() { let result = upper(&to_value("hello").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("HELLO").unwrap()); } #[test] fn test_upper_error() { let result = upper(&to_value(50).unwrap(), &HashMap::new()); assert!(result.is_err()); assert_eq!( result.err().unwrap().to_string(), "Filter `upper` was called on an incorrect value: got `50` but expected a String" ); } #[test] fn test_trim() { let result = trim(&to_value(" hello ").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("hello").unwrap()); } #[test] fn test_trim_start() { let result = trim_start(&to_value(" hello ").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("hello ").unwrap()); } #[test] fn test_trim_end() { let result = trim_end(&to_value(" hello ").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(" hello").unwrap()); } #[test] fn test_trim_start_matches() { let tests: Vec<(_, _, _)> = vec![ ("/a/b/cde/", "/", "a/b/cde/"), ("\nhello\nworld\n", "\n", "hello\nworld\n"), (", hello, world, ", ", ", "hello, world, "), ]; for (input, pat, expected) in tests { let mut args = HashMap::new(); args.insert("pat".to_string(), to_value(pat).unwrap()); let result = trim_start_matches(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_trim_end_matches() { let tests: Vec<(_, _, _)> = vec![ ("/a/b/cde/", "/", "/a/b/cde"), ("\nhello\nworld\n", "\n", "\nhello\nworld"), (", hello, world, ", ", ", ", hello, world"), ]; for (input, pat, expected) in tests { let mut args = HashMap::new(); args.insert("pat".to_string(), to_value(pat).unwrap()); let result = trim_end_matches(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_truncate_smaller_than_length() { let mut args = HashMap::new(); args.insert("length".to_string(), to_value(255).unwrap()); let result = truncate(&to_value("hello").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("hello").unwrap()); } #[test] fn test_truncate_when_required() { let mut args = HashMap::new(); args.insert("length".to_string(), to_value(2).unwrap()); let result = truncate(&to_value("日本語").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("日本…").unwrap()); } #[test] fn test_truncate_custom_end() { let mut args = HashMap::new(); args.insert("length".to_string(), to_value(2).unwrap()); args.insert("end".to_string(), to_value("").unwrap()); let result = truncate(&to_value("日本語").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("日本").unwrap()); } #[test] fn test_truncate_multichar_grapheme() { let mut args = HashMap::new(); args.insert("length".to_string(), to_value(5).unwrap()); args.insert("end".to_string(), to_value("…").unwrap()); let result = truncate(&to_value("👨‍👩‍👧‍👦 family").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("👨‍👩‍👧‍👦 fam…").unwrap()); } #[test] fn test_lower() { let result = lower(&to_value("HELLO").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("hello").unwrap()); } #[test] fn test_wordcount() { let result = wordcount(&to_value("Joel is a slug").unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(4).unwrap()); } #[test] fn test_replace() { let mut args = HashMap::new(); args.insert("from".to_string(), to_value("Hello").unwrap()); args.insert("to".to_string(), to_value("Goodbye").unwrap()); let result = replace(&to_value("Hello world!").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("Goodbye world!").unwrap()); } // https://github.com/Keats/tera/issues/435 #[test] fn test_replace_newline() { let mut args = HashMap::new(); args.insert("from".to_string(), to_value("\n").unwrap()); args.insert("to".to_string(), to_value("
").unwrap()); let result = replace(&to_value("Animal Alphabets\nB is for Bee-Eater").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("Animal Alphabets
B is for Bee-Eater").unwrap()); } #[test] fn test_replace_missing_arg() { let mut args = HashMap::new(); args.insert("from".to_string(), to_value("Hello").unwrap()); let result = replace(&to_value("Hello world!").unwrap(), &args); assert!(result.is_err()); assert_eq!( result.err().unwrap().to_string(), "Filter `replace` expected an arg called `to`" ); } #[test] fn test_capitalize() { let tests = vec![("CAPITAL IZE", "Capital ize"), ("capital ize", "Capital ize")]; for (input, expected) in tests { let result = capitalize(&to_value(input).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_addslashes() { let tests = vec![ (r#"I'm so happy"#, r#"I\'m so happy"#), (r#"Let "me" help you"#, r#"Let \"me\" help you"#), (r#"'"#, r#"\'"#), ( r#""double quotes" and \'single quotes\'"#, r#"\"double quotes\" and \\\'single quotes\\\'"#, ), (r#"\ : backslashes too"#, r#"\\ : backslashes too"#), ]; for (input, expected) in tests { let result = addslashes(&to_value(input).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[cfg(feature = "builtins")] #[test] fn test_slugify() { // slug crate already has tests for general slugification so we just // check our function works let tests = vec![(r#"Hello world"#, r#"hello-world"#), (r#"Hello 世界"#, r#"hello-shi-jie"#)]; for (input, expected) in tests { let result = slugify(&to_value(input).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[cfg(feature = "urlencode")] #[test] fn test_urlencode() { let tests = vec![ ( r#"https://www.example.org/foo?a=b&c=d"#, r#"https%3A//www.example.org/foo%3Fa%3Db%26c%3Dd"#, ), ( r#"https://www.example.org/apples-&-oranges/"#, r#"https%3A//www.example.org/apples-%26-oranges/"#, ), (r#"https://www.example.org/"#, r#"https%3A//www.example.org/"#), (r#"/test&"/me?/"#, r#"/test%26%22/me%3F/"#), (r#"escape/slash"#, r#"escape/slash"#), ]; for (input, expected) in tests { let args = HashMap::new(); let result = urlencode(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[cfg(feature = "urlencode")] #[test] fn test_urlencode_strict() { let tests = vec![ ( r#"https://www.example.org/foo?a=b&c=d"#, r#"https%3A%2F%2Fwww%2Eexample%2Eorg%2Ffoo%3Fa%3Db%26c%3Dd"#, ), ( r#"https://www.example.org/apples-&-oranges/"#, r#"https%3A%2F%2Fwww%2Eexample%2Eorg%2Fapples%2D%26%2Doranges%2F"#, ), (r#"https://www.example.org/"#, r#"https%3A%2F%2Fwww%2Eexample%2Eorg%2F"#), (r#"/test&"/me?/"#, r#"%2Ftest%26%22%2Fme%3F%2F"#), (r#"escape/slash"#, r#"escape%2Fslash"#), ]; for (input, expected) in tests { let args = HashMap::new(); let result = urlencode_strict(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_title() { let tests = vec![ ("foo bar", "Foo Bar"), ("foo\tbar", "Foo\tBar"), ("foo bar", "Foo Bar"), ("f bar f", "F Bar F"), ("foo-bar", "Foo-Bar"), ("FOO\tBAR", "Foo\tBar"), ("foo (bar)", "Foo (Bar)"), ("foo (bar) ", "Foo (Bar) "), ("foo {bar}", "Foo {Bar}"), ("foo [bar]", "Foo [Bar]"), ("foo ", "Foo "), (" foo bar", " Foo Bar"), ("\tfoo\tbar\t", "\tFoo\tBar\t"), ("foo bar ", "Foo Bar "), ("foo bar\t", "Foo Bar\t"), ("foo's bar", "Foo's Bar"), ]; for (input, expected) in tests { let result = title(&to_value(input).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_indent_defaults() { let args = HashMap::new(); let result = indent(&to_value("one\n\ntwo\nthree").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("one\n\n two\n three").unwrap()); } #[test] fn test_indent_args() { let mut args = HashMap::new(); args.insert("first".to_string(), to_value(true).unwrap()); args.insert("prefix".to_string(), to_value(" ").unwrap()); args.insert("blank".to_string(), to_value(true).unwrap()); let result = indent(&to_value("one\n\ntwo\nthree").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(" one\n \n two\n three").unwrap()); } #[test] fn test_striptags() { let tests = vec![ (r"Joel a slug", "Joel is a slug"), ( r#"

just a small \n example link

\n

to a webpage

"#, r#"just a small \n example link\nto a webpage"#, ), ( r"

See: 'é is an apostrophe followed by e acute

", r"See: 'é is an apostrophe followed by e acute", ), (r"a", "a"), (r"a", "a"), (r"e", "e"), (r"hi, b2!", "b7>b2!"), ("b", "b"), (r#"a

b

c"#, "abc"), (r#"de

f"#, "def"), (r#"foobar"#, "foobar"), ]; for (input, expected) in tests { let result = striptags(&to_value(input).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_spaceless() { let tests = vec![ ("

\ntest\r\n

", "

test

"), ("

\n \r\n

", "

"), ("

", "

"), ("

", "

"), ("

test

", "

test

"), ("

\r\n

", "

"), ]; for (input, expected) in tests { let result = spaceless(&to_value(input).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_split() { let tests: Vec<(_, _, &[&str])> = vec![ ("a/b/cde", "/", &["a", "b", "cde"]), ("hello\nworld", "\n", &["hello", "world"]), ("hello, world", ", ", &["hello", "world"]), ]; for (input, pat, expected) in tests { let mut args = HashMap::new(); args.insert("pat".to_string(), to_value(pat).unwrap()); let result = split(&to_value(input).unwrap(), &args).unwrap(); let result = result.as_array().unwrap(); assert_eq!(result.len(), expected.len()); for (result, expected) in result.iter().zip(expected.iter()) { assert_eq!(result, expected); } } } #[test] fn test_xml_escape() { let tests = vec![ (r"hey-&-ho", "hey-&-ho"), (r"hey-'-ho", "hey-'-ho"), (r"hey-&'-ho", "hey-&'-ho"), (r#"hey-&'"-ho"#, "hey-&'"-ho"), (r#"hey-&'"<-ho"#, "hey-&'"<-ho"), (r#"hey-&'"<>-ho"#, "hey-&'"<>-ho"), ]; for (input, expected) in tests { let result = escape_xml(&to_value(input).unwrap(), &HashMap::new()); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_int_decimal_strings() { let tests: Vec<(&str, i64)> = vec![ ("0", 0), ("-5", -5), ("9223372036854775807", i64::max_value()), ("0b1010", 0), ("1.23", 1), ]; for (input, expected) in tests { let args = HashMap::new(); let result = int(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_int_others() { let mut args = HashMap::new(); let result = int(&to_value(1.23).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(1).unwrap()); let result = int(&to_value(-5).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(-5).unwrap()); args.insert("default".to_string(), to_value(5).unwrap()); args.insert("base".to_string(), to_value(2).unwrap()); let tests: Vec<(&str, i64)> = vec![("0", 0), ("-3", 5), ("1010", 10), ("0b1010", 10), ("0xF00", 5)]; for (input, expected) in tests { let result = int(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } args.insert("default".to_string(), to_value(-4).unwrap()); args.insert("base".to_string(), to_value(8).unwrap()); let tests: Vec<(&str, i64)> = vec![("21", 17), ("-3", -3), ("9OO", -4), ("0o567", 375), ("0b101", -4)]; for (input, expected) in tests { let result = int(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } args.insert("default".to_string(), to_value(0).unwrap()); args.insert("base".to_string(), to_value(16).unwrap()); let tests: Vec<(&str, i64)> = vec![("1011", 4113), ("0xC3", 195)]; for (input, expected) in tests { let result = int(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } args.insert("default".to_string(), to_value(0).unwrap()); args.insert("base".to_string(), to_value(5).unwrap()); let tests: Vec<(&str, i64)> = vec![("4321", 586), ("-100", -25), ("0b100", 0)]; for (input, expected) in tests { let result = int(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } #[test] fn test_float() { let mut args = HashMap::new(); let tests: Vec<(&str, f64)> = vec![("0", 0.0), ("-5.3", -5.3)]; for (input, expected) in tests { let result = float(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } args.insert("default".to_string(), to_value(3.18).unwrap()); let result = float(&to_value("bad_val").unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(3.18).unwrap()); let result = float(&to_value(1.23).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(1.23).unwrap()); } #[test] fn test_linebreaksbr() { let args = HashMap::new(); let tests: Vec<(&str, &str)> = vec![ ("hello world", "hello world"), ("hello\nworld", "hello
world"), ("hello\r\nworld", "hello
world"), ("hello\n\rworld", "hello
\rworld"), ("hello\r\n\nworld", "hello

world"), ("hello
world\n", "hello
world
"), ]; for (input, expected) in tests { let result = linebreaksbr(&to_value(input).unwrap(), &args); assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value(expected).unwrap()); } } } tera-1.20.0/src/builtins/functions.rs000064400000000000000000000241631046102023000156430ustar 00000000000000use std::collections::HashMap; #[cfg(feature = "builtins")] use chrono::prelude::*; #[cfg(feature = "builtins")] use rand::Rng; use serde_json::value::{from_value, to_value, Value}; use crate::errors::{Error, Result}; /// The global function type definition pub trait Function: Sync + Send { /// The global function type definition fn call(&self, args: &HashMap) -> Result; /// Whether the current function's output should be treated as safe, defaults to `false` fn is_safe(&self) -> bool { false } } impl Function for F where F: Fn(&HashMap) -> Result + Sync + Send, { fn call(&self, args: &HashMap) -> Result { self(args) } } pub fn range(args: &HashMap) -> Result { let start = match args.get("start") { Some(val) => match from_value::(val.clone()) { Ok(v) => v, Err(_) => { return Err(Error::msg(format!( "Function `range` received start={} but `start` can only be a number", val ))); } }, None => 0, }; let step_by = match args.get("step_by") { Some(val) => match from_value::(val.clone()) { Ok(v) => v, Err(_) => { return Err(Error::msg(format!( "Function `range` received step_by={} but `step` can only be a number", val ))); } }, None => 1, }; let end = match args.get("end") { Some(val) => match from_value::(val.clone()) { Ok(v) => v, Err(_) => { return Err(Error::msg(format!( "Function `range` received end={} but `end` can only be a number", val ))); } }, None => { return Err(Error::msg("Function `range` was called without a `end` argument")); } }; if start > end { return Err(Error::msg( "Function `range` was called with a `start` argument greater than the `end` one", )); } let mut i = start; let mut res = vec![]; while i < end { res.push(i); i += step_by; } Ok(to_value(res).unwrap()) } #[cfg(feature = "builtins")] pub fn now(args: &HashMap) -> Result { let utc = match args.get("utc") { Some(val) => match from_value::(val.clone()) { Ok(v) => v, Err(_) => { return Err(Error::msg(format!( "Function `now` received utc={} but `utc` can only be a boolean", val ))); } }, None => false, }; let timestamp = match args.get("timestamp") { Some(val) => match from_value::(val.clone()) { Ok(v) => v, Err(_) => { return Err(Error::msg(format!( "Function `now` received timestamp={} but `timestamp` can only be a boolean", val ))); } }, None => false, }; if utc { let datetime = Utc::now(); if timestamp { return Ok(to_value(datetime.timestamp()).unwrap()); } Ok(to_value(datetime.to_rfc3339()).unwrap()) } else { let datetime = Local::now(); if timestamp { return Ok(to_value(datetime.timestamp()).unwrap()); } Ok(to_value(datetime.to_rfc3339()).unwrap()) } } pub fn throw(args: &HashMap) -> Result { match args.get("message") { Some(val) => match from_value::(val.clone()) { Ok(v) => Err(Error::msg(v)), Err(_) => Err(Error::msg(format!( "Function `throw` received message={} but `message` can only be a string", val ))), }, None => Err(Error::msg("Function `throw` was called without a `message` argument")), } } #[cfg(feature = "builtins")] pub fn get_random(args: &HashMap) -> Result { let start = match args.get("start") { Some(val) => match from_value::(val.clone()) { Ok(v) => v, Err(_) => { return Err(Error::msg(format!( "Function `get_random` received start={} but `start` can only be a number", val ))); } }, None => 0, }; let end = match args.get("end") { Some(val) => match from_value::(val.clone()) { Ok(v) => v, Err(_) => { return Err(Error::msg(format!( "Function `get_random` received end={} but `end` can only be a number", val ))); } }, None => return Err(Error::msg("Function `get_random` didn't receive an `end` argument")), }; let mut rng = rand::thread_rng(); let res = rng.gen_range(start..end); Ok(Value::Number(res.into())) } pub fn get_env(args: &HashMap) -> Result { let name = match args.get("name") { Some(val) => match from_value::(val.clone()) { Ok(v) => v, Err(_) => { return Err(Error::msg(format!( "Function `get_env` received name={} but `name` can only be a string", val ))); } }, None => return Err(Error::msg("Function `get_env` didn't receive a `name` argument")), }; match std::env::var(&name).ok() { Some(res) => Ok(Value::String(res)), None => match args.get("default") { Some(default) => Ok(default.clone()), None => Err(Error::msg(format!("Environment variable `{}` not found", &name))), }, } } #[cfg(test)] mod tests { use std::collections::HashMap; use serde_json::value::to_value; use super::*; #[test] fn range_default() { let mut args = HashMap::new(); args.insert("end".to_string(), to_value(5).unwrap()); let res = range(&args).unwrap(); assert_eq!(res, to_value(vec![0, 1, 2, 3, 4]).unwrap()); } #[test] fn range_start() { let mut args = HashMap::new(); args.insert("end".to_string(), to_value(5).unwrap()); args.insert("start".to_string(), to_value(1).unwrap()); let res = range(&args).unwrap(); assert_eq!(res, to_value(vec![1, 2, 3, 4]).unwrap()); } #[test] fn range_start_greater_than_end() { let mut args = HashMap::new(); args.insert("end".to_string(), to_value(5).unwrap()); args.insert("start".to_string(), to_value(6).unwrap()); assert!(range(&args).is_err()); } #[test] fn range_step_by() { let mut args = HashMap::new(); args.insert("end".to_string(), to_value(10).unwrap()); args.insert("step_by".to_string(), to_value(2).unwrap()); let res = range(&args).unwrap(); assert_eq!(res, to_value(vec![0, 2, 4, 6, 8]).unwrap()); } #[cfg(feature = "builtins")] #[test] fn now_default() { let args = HashMap::new(); let res = now(&args).unwrap(); assert!(res.is_string()); assert!(res.as_str().unwrap().contains('T')); } #[cfg(feature = "builtins")] #[test] fn now_datetime_utc() { let mut args = HashMap::new(); args.insert("utc".to_string(), to_value(true).unwrap()); let res = now(&args).unwrap(); assert!(res.is_string()); let val = res.as_str().unwrap(); println!("{}", val); assert!(val.contains('T')); assert!(val.contains("+00:00")); } #[cfg(feature = "builtins")] #[test] fn now_timestamp() { let mut args = HashMap::new(); args.insert("timestamp".to_string(), to_value(true).unwrap()); let res = now(&args).unwrap(); assert!(res.is_number()); } #[test] fn throw_errors_with_message() { let mut args = HashMap::new(); args.insert("message".to_string(), to_value("Hello").unwrap()); let res = throw(&args); assert!(res.is_err()); let err = res.unwrap_err(); assert_eq!(err.to_string(), "Hello"); } #[cfg(feature = "builtins")] #[test] fn get_random_no_start() { let mut args = HashMap::new(); args.insert("end".to_string(), to_value(10).unwrap()); let res = get_random(&args).unwrap(); println!("{}", res); assert!(res.is_number()); assert!(res.as_i64().unwrap() >= 0); assert!(res.as_i64().unwrap() < 10); } #[cfg(feature = "builtins")] #[test] fn get_random_with_start() { let mut args = HashMap::new(); args.insert("start".to_string(), to_value(5).unwrap()); args.insert("end".to_string(), to_value(10).unwrap()); let res = get_random(&args).unwrap(); println!("{}", res); assert!(res.is_number()); assert!(res.as_i64().unwrap() >= 5); assert!(res.as_i64().unwrap() < 10); } #[test] fn get_env_existing() { std::env::set_var("TERA_TEST", "true"); let mut args = HashMap::new(); args.insert("name".to_string(), to_value("TERA_TEST").unwrap()); let res = get_env(&args).unwrap(); assert!(res.is_string()); assert_eq!(res.as_str().unwrap(), "true"); std::env::remove_var("TERA_TEST"); } #[test] fn get_env_non_existing_no_default() { let mut args = HashMap::new(); args.insert("name".to_string(), to_value("UNKNOWN_VAR").unwrap()); let res = get_env(&args); assert!(res.is_err()); } #[test] fn get_env_non_existing_with_default() { let mut args = HashMap::new(); args.insert("name".to_string(), to_value("UNKNOWN_VAR").unwrap()); args.insert("default".to_string(), to_value("false").unwrap()); let res = get_env(&args).unwrap(); assert!(res.is_string()); assert_eq!(res.as_str().unwrap(), "false"); } } tera-1.20.0/src/builtins/mod.rs000064400000000000000000000000651046102023000144050ustar 00000000000000pub mod filters; pub mod functions; pub mod testers; tera-1.20.0/src/builtins/testers.rs000064400000000000000000000261731046102023000153270ustar 00000000000000use crate::context::ValueNumber; use crate::errors::{Error, Result}; use regex::Regex; use serde_json::value::Value; /// The tester function type definition pub trait Test: Sync + Send { /// The tester function type definition fn test(&self, value: Option<&Value>, args: &[Value]) -> Result; } impl Test for F where F: Fn(Option<&Value>, &[Value]) -> Result + Sync + Send, { fn test(&self, value: Option<&Value>, args: &[Value]) -> Result { self(value, args) } } /// Check that the number of args match what was expected pub fn number_args_allowed(tester_name: &str, max: usize, args_len: usize) -> Result<()> { if max == 0 && args_len > max { return Err(Error::msg(format!( "Tester `{}` was called with some args but this test doesn't take args", tester_name ))); } if args_len > max { return Err(Error::msg(format!( "Tester `{}` was called with {} args, the max number is {}", tester_name, args_len, max ))); } Ok(()) } /// Called to check if the Value is defined and return an Err if not pub fn value_defined(tester_name: &str, value: Option<&Value>) -> Result<()> { if value.is_none() { return Err(Error::msg(format!( "Tester `{}` was called on an undefined variable", tester_name ))); } Ok(()) } /// Helper function to extract string from an [`Option`] to remove boilerplate /// with tester error handling pub fn extract_string<'a>( tester_name: &str, part: &str, value: Option<&'a Value>, ) -> Result<&'a str> { match value.and_then(Value::as_str) { Some(s) => Ok(s), None => Err(Error::msg(format!( "Tester `{}` was called {} that isn't a string", tester_name, part ))), } } /// Returns true if `value` is defined. Otherwise, returns false. pub fn defined(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("defined", 0, params.len())?; Ok(value.is_some()) } /// Returns true if `value` is undefined. Otherwise, returns false. pub fn undefined(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("undefined", 0, params.len())?; Ok(value.is_none()) } /// Returns true if `value` is a string. Otherwise, returns false. pub fn string(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("string", 0, params.len())?; value_defined("string", value)?; match value { Some(Value::String(_)) => Ok(true), _ => Ok(false), } } /// Returns true if `value` is a number. Otherwise, returns false. pub fn number(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("number", 0, params.len())?; value_defined("number", value)?; match value { Some(Value::Number(_)) => Ok(true), _ => Ok(false), } } /// Returns true if `value` is an odd number. Otherwise, returns false. pub fn odd(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("odd", 0, params.len())?; value_defined("odd", value)?; match value.and_then(|v| v.to_number().ok()) { Some(f) => Ok(f % 2.0 != 0.0), _ => Err(Error::msg("Tester `odd` was called on a variable that isn't a number")), } } /// Returns true if `value` is an even number. Otherwise, returns false. pub fn even(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("even", 0, params.len())?; value_defined("even", value)?; let is_odd = odd(value, params)?; Ok(!is_odd) } /// Returns true if `value` is divisible by the first param. Otherwise, returns false. pub fn divisible_by(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("divisibleby", 1, params.len())?; value_defined("divisibleby", value)?; match value.and_then(|v| v.to_number().ok()) { Some(val) => match params.first().and_then(|v| v.to_number().ok()) { Some(p) => Ok(val % p == 0.0), None => Err(Error::msg( "Tester `divisibleby` was called with a parameter that isn't a number", )), }, None => { Err(Error::msg("Tester `divisibleby` was called on a variable that isn't a number")) } } } /// Returns true if `value` can be iterated over in Tera (ie is an array/tuple or an object). /// Otherwise, returns false. pub fn iterable(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("iterable", 0, params.len())?; value_defined("iterable", value)?; Ok(value.unwrap().is_array() || value.unwrap().is_object()) } /// Returns true if the given variable is an object (ie can be iterated over key, value). /// Otherwise, returns false. pub fn object(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("object", 0, params.len())?; value_defined("object", value)?; Ok(value.unwrap().is_object()) } /// Returns true if `value` starts with the given string. Otherwise, returns false. pub fn starting_with(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("starting_with", 1, params.len())?; value_defined("starting_with", value)?; let value = extract_string("starting_with", "on a variable", value)?; let needle = extract_string("starting_with", "with a parameter", params.first())?; Ok(value.starts_with(needle)) } /// Returns true if `value` ends with the given string. Otherwise, returns false. pub fn ending_with(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("ending_with", 1, params.len())?; value_defined("ending_with", value)?; let value = extract_string("ending_with", "on a variable", value)?; let needle = extract_string("ending_with", "with a parameter", params.first())?; Ok(value.ends_with(needle)) } /// Returns true if `value` contains the given argument. Otherwise, returns false. pub fn containing(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("containing", 1, params.len())?; value_defined("containing", value)?; match value.unwrap() { Value::String(v) => { let needle = extract_string("containing", "with a parameter", params.first())?; Ok(v.contains(needle)) } Value::Array(v) => Ok(v.contains(params.first().unwrap())), Value::Object(v) => { let needle = extract_string("containing", "with a parameter", params.first())?; Ok(v.contains_key(needle)) } _ => Err(Error::msg("Tester `containing` can only be used on string, array or map")), } } /// Returns true if `value` is a string and matches the regex in the argument. Otherwise, returns false. pub fn matching(value: Option<&Value>, params: &[Value]) -> Result { number_args_allowed("matching", 1, params.len())?; value_defined("matching", value)?; let value = extract_string("matching", "on a variable", value)?; let regex = extract_string("matching", "with a parameter", params.first())?; let regex = match Regex::new(regex) { Ok(regex) => regex, Err(err) => { return Err(Error::msg(format!( "Tester `matching`: Invalid regular expression: {}", err ))); } }; Ok(regex.is_match(value)) } #[cfg(test)] mod tests { use std::collections::HashMap; use super::{ containing, defined, divisible_by, ending_with, iterable, matching, object, starting_with, string, }; use serde_json::value::to_value; #[test] fn test_number_args_ok() { assert!(defined(None, &[]).is_ok()) } #[test] fn test_too_many_args() { assert!(defined(None, &[to_value(1).unwrap()]).is_err()) } #[test] fn test_value_defined() { assert!(string(None, &[]).is_err()) } #[test] fn test_divisible_by() { let tests = vec![ (1.0, 2.0, false), (4.0, 2.0, true), (4.0, 2.1, false), (10.0, 2.0, true), (10.0, 0.0, false), ]; for (val, divisor, expected) in tests { assert_eq!( divisible_by(Some(&to_value(val).unwrap()), &[to_value(divisor).unwrap()],) .unwrap(), expected ); } } #[test] fn test_iterable() { assert!(iterable(Some(&to_value(vec!["1"]).unwrap()), &[]).unwrap()); assert!(!iterable(Some(&to_value(1).unwrap()), &[]).unwrap()); assert!(!iterable(Some(&to_value("hello").unwrap()), &[]).unwrap()); } #[test] fn test_object() { let mut h = HashMap::new(); h.insert("a", 1); assert!(object(Some(&to_value(h).unwrap()), &[]).unwrap()); assert!(!object(Some(&to_value(1).unwrap()), &[]).unwrap()); assert!(!object(Some(&to_value("hello").unwrap()), &[]).unwrap()); } #[test] fn test_starting_with() { assert!(starting_with( Some(&to_value("helloworld").unwrap()), &[to_value("hello").unwrap()], ) .unwrap()); assert!( !starting_with(Some(&to_value("hello").unwrap()), &[to_value("hi").unwrap()],).unwrap() ); } #[test] fn test_ending_with() { assert!( ending_with(Some(&to_value("helloworld").unwrap()), &[to_value("world").unwrap()],) .unwrap() ); assert!( !ending_with(Some(&to_value("hello").unwrap()), &[to_value("hi").unwrap()],).unwrap() ); } #[test] fn test_containing() { let mut map = HashMap::new(); map.insert("hey", 1); let tests = vec![ (to_value("hello world").unwrap(), to_value("hel").unwrap(), true), (to_value("hello world").unwrap(), to_value("hol").unwrap(), false), (to_value(vec![1, 2, 3]).unwrap(), to_value(3).unwrap(), true), (to_value(vec![1, 2, 3]).unwrap(), to_value(4).unwrap(), false), (to_value(map.clone()).unwrap(), to_value("hey").unwrap(), true), (to_value(map.clone()).unwrap(), to_value("ho").unwrap(), false), ]; for (container, needle, expected) in tests { assert_eq!(containing(Some(&container), &[needle]).unwrap(), expected); } } #[test] fn test_matching() { let tests = vec![ (to_value("abc").unwrap(), to_value("b").unwrap(), true), (to_value("abc").unwrap(), to_value("^b$").unwrap(), false), ( to_value("Hello, World!").unwrap(), to_value(r"(?i)(hello\W\sworld\W)").unwrap(), true, ), ( to_value("The date was 2018-06-28").unwrap(), to_value(r"\d{4}-\d{2}-\d{2}$").unwrap(), true, ), ]; for (container, needle, expected) in tests { assert_eq!(matching(Some(&container), &[needle]).unwrap(), expected); } assert!( matching(Some(&to_value("").unwrap()), &[to_value("(Invalid regex").unwrap()]).is_err() ); } } tera-1.20.0/src/context.rs000064400000000000000000000411411046102023000134610ustar 00000000000000use std::collections::BTreeMap; use std::io::Write; use serde::ser::Serialize; use serde_json::value::{to_value, Map, Value}; use crate::errors::{Error, Result as TeraResult}; /// The struct that holds the context of a template rendering. /// /// Light wrapper around a `BTreeMap` for easier insertions of Serializable /// values #[derive(Debug, Clone, PartialEq)] pub struct Context { data: BTreeMap, } impl Context { /// Initializes an empty context pub fn new() -> Self { Context { data: BTreeMap::new() } } /// Converts the `val` parameter to `Value` and insert it into the context. /// /// Panics if the serialization fails. /// /// ```rust /// # use tera::Context; /// let mut context = tera::Context::new(); /// context.insert("number_users", &42); /// ``` pub fn insert>(&mut self, key: S, val: &T) { self.data.insert(key.into(), to_value(val).unwrap()); } /// Converts the `val` parameter to `Value` and insert it into the context. /// /// Returns an error if the serialization fails. /// /// ```rust /// # use tera::Context; /// # struct CannotBeSerialized; /// # impl serde::Serialize for CannotBeSerialized { /// # fn serialize(&self, serializer: S) -> Result { /// # Err(serde::ser::Error::custom("Error")) /// # } /// # } /// # let user = CannotBeSerialized; /// let mut context = Context::new(); /// // user is an instance of a struct implementing `Serialize` /// if let Err(_) = context.try_insert("number_users", &user) { /// // Serialization failed /// } /// ``` pub fn try_insert>( &mut self, key: S, val: &T, ) -> TeraResult<()> { self.data.insert(key.into(), to_value(val)?); Ok(()) } /// Appends the data of the `source` parameter to `self`, overwriting existing keys. /// The source context will be dropped. /// /// ```rust /// # use tera::Context; /// let mut target = Context::new(); /// target.insert("a", &1); /// target.insert("b", &2); /// let mut source = Context::new(); /// source.insert("b", &3); /// source.insert("d", &4); /// target.extend(source); /// ``` pub fn extend(&mut self, mut source: Context) { self.data.append(&mut source.data); } /// Converts the context to a `serde_json::Value` consuming the context. pub fn into_json(self) -> Value { let mut m = Map::new(); for (key, value) in self.data { m.insert(key, value); } Value::Object(m) } /// Takes a serde-json `Value` and convert it into a `Context` with no overhead/cloning. pub fn from_value(obj: Value) -> TeraResult { match obj { Value::Object(m) => { let mut data = BTreeMap::new(); for (key, value) in m { data.insert(key, value); } Ok(Context { data }) } _ => Err(Error::msg( "Creating a Context from a Value/Serialize requires it being a JSON object", )), } } /// Takes something that impl Serialize and create a context with it. /// Meant to be used if you have a hashmap or a struct and don't want to insert values /// one by one in the context. pub fn from_serialize(value: impl Serialize) -> TeraResult { let obj = to_value(value).map_err(Error::json)?; Context::from_value(obj) } /// Returns the value at a given key index. pub fn get(&self, index: &str) -> Option<&Value> { self.data.get(index) } /// Remove a key from the context, returning the value at the key if the key was previously inserted into the context. pub fn remove(&mut self, index: &str) -> Option { self.data.remove(index) } /// Checks if a value exists at a specific index. pub fn contains_key(&self, index: &str) -> bool { self.data.contains_key(index) } } impl Default for Context { fn default() -> Context { Context::new() } } pub trait ValueRender { fn render(&self, write: &mut impl Write) -> std::io::Result<()>; } // Convert serde Value to String. impl ValueRender for Value { fn render(&self, write: &mut impl Write) -> std::io::Result<()> { match *self { Value::String(ref s) => write!(write, "{}", s), Value::Number(ref i) => { if let Some(v) = i.as_i64() { write!(write, "{}", v) } else if let Some(v) = i.as_u64() { write!(write, "{}", v) } else if let Some(v) = i.as_f64() { write!(write, "{}", v) } else { unreachable!() } } Value::Bool(i) => write!(write, "{}", i), Value::Null => Ok(()), Value::Array(ref a) => { let mut first = true; write!(write, "[")?; for i in a.iter() { if !first { write!(write, ", ")?; } first = false; i.render(write)?; } write!(write, "]")?; Ok(()) } Value::Object(_) => write!(write, "[object]"), } } } pub trait ValueNumber { fn to_number(&self) -> Result; } // Needed for all the maths // Convert everything to f64, seems like a terrible idea impl ValueNumber for Value { fn to_number(&self) -> Result { match *self { Value::Number(ref i) => Ok(i.as_f64().unwrap()), _ => Err(()), } } } // From handlebars-rust pub trait ValueTruthy { fn is_truthy(&self) -> bool; } impl ValueTruthy for Value { fn is_truthy(&self) -> bool { match *self { Value::Number(ref i) => { if i.is_i64() { return i.as_i64().unwrap() != 0; } if i.is_u64() { return i.as_u64().unwrap() != 0; } let f = i.as_f64().unwrap(); f != 0.0 && !f.is_nan() } Value::Bool(ref i) => *i, Value::Null => false, Value::String(ref i) => !i.is_empty(), Value::Array(ref i) => !i.is_empty(), Value::Object(ref i) => !i.is_empty(), } } } /// Converts a dotted path to a json pointer one #[inline] #[deprecated( since = "1.8.0", note = "`get_json_pointer` converted a dotted pointer to a json pointer, use dotted_pointer for direct lookups of values" )] pub fn get_json_pointer(key: &str) -> String { lazy_static::lazy_static! { // Split the key into dot-separated segments, respecting quoted strings as single units // to fix https://github.com/Keats/tera/issues/590 static ref JSON_POINTER_REGEX: regex::Regex = regex::Regex::new(r#""[^"]*"|[^.]+"#).unwrap(); } let mut res = String::with_capacity(key.len() + 1); if key.find('"').is_some() { for mat in JSON_POINTER_REGEX.find_iter(key) { res.push('/'); res.push_str(mat.as_str().trim_matches('"')); } } else { res.push('/'); res.push_str(&key.replace('.', "/")); } res } /// following iterator immitates regex::Regex::new(r#""[^"]*"|[^.\[\]]+"#) but also strips `"` and `'` struct PointerMachina<'a> { pointer: &'a str, single_quoted: bool, dual_quoted: bool, escaped: bool, last_position: usize, } impl PointerMachina<'_> { fn new(pointer: &str) -> PointerMachina { PointerMachina { pointer, single_quoted: false, dual_quoted: false, escaped: false, last_position: 0, } } } impl<'a> Iterator for PointerMachina<'a> { type Item = &'a str; // next() is the only required method fn next(&mut self) -> Option { let forwarded = &self.pointer[self.last_position..]; let mut offset: usize = 0; for (i, character) in forwarded.chars().enumerate() { match character { '"' => { if !self.escaped { self.dual_quoted = !self.dual_quoted; if i == offset { offset += 1; } else { let result = &self.pointer[self.last_position + offset..self.last_position + i]; self.last_position += i + 1; // +1 for skipping this quote if !result.is_empty() { return Some(result); } } } } '\'' => { if !self.escaped { self.single_quoted = !self.single_quoted; if i == offset { offset += 1; } else { let result = &self.pointer[self.last_position + offset..self.last_position + i]; self.last_position += i + 1; // +1 for skipping this quote if !result.is_empty() { return Some(result); } } } } '\\' => { self.escaped = true; continue; } '[' => { if !self.single_quoted && !self.dual_quoted && !self.escaped { let result = &self.pointer[self.last_position + offset..self.last_position + i]; self.last_position += i + 1; if !result.is_empty() { return Some(result); } } } ']' => { if !self.single_quoted && !self.dual_quoted && !self.escaped { offset += 1; } } '.' => { if !self.single_quoted && !self.dual_quoted && !self.escaped { if i == offset { offset += 1; } else { let result = &self.pointer[self.last_position + offset..self.last_position + i]; self.last_position += i + 1; if !result.is_empty() { return Some(result); } } } } _ => (), } self.escaped = false; } if self.last_position + offset < self.pointer.len() { let result = &self.pointer[self.last_position + offset..]; self.last_position = self.pointer.len(); return Some(result); } None } } /// Lookups a dotted path in a json value /// contrary to the json slash pointer it's not allowed to begin with a dot #[inline] #[must_use] pub fn dotted_pointer<'a>(value: &'a Value, pointer: &str) -> Option<&'a Value> { if pointer.is_empty() { return Some(value); } PointerMachina::new(pointer).map(|mat| mat.replace("~1", "/").replace("~0", "~")).try_fold( value, |target, token| match target { Value::Object(map) => map.get(&token), Value::Array(list) => parse_index(&token).and_then(|x| list.get(x)), _ => None, }, ) } /// serde jsons parse_index #[inline] fn parse_index(s: &str) -> Option { if s.starts_with('+') || (s.starts_with('0') && s.len() != 1) { return None; } s.parse().ok() } #[cfg(test)] mod tests { use super::*; use serde_json::json; use std::collections::HashMap; #[test] fn test_dotted_pointer() { let data = r#"{ "foo": { "bar": { "goo": { "moo": { "cows": [ { "name": "betsy", "age" : 2, "temperament": "calm" }, { "name": "elsie", "age": 3, "temperament": "calm" }, { "name": "veal", "age": 1, "temperament": "ornery" } ] } } }, "http://example.com/": { "goo": { "moo": { "cows": [ { "name": "betsy", "age" : 2, "temperament": "calm" }, { "name": "elsie", "age": 3, "temperament": "calm" }, { "name": "veal", "age": 1, "temperament": "ornery" } ] } } } } }"#; let value = serde_json::from_str(data).unwrap(); assert_eq!(dotted_pointer(&value, ""), Some(&value)); assert_eq!(dotted_pointer(&value, "foo"), value.pointer("/foo")); assert_eq!(dotted_pointer(&value, "foo.bar.goo"), value.pointer("/foo/bar/goo")); assert_eq!(dotted_pointer(&value, "skrr"), value.pointer("/skrr")); assert_eq!( dotted_pointer(&value, r#"foo["bar"].baz"#), value.pointer(r#"/foo["bar"]/baz"#) ); assert_eq!( dotted_pointer(&value, r#"foo["bar"].baz["qux"].blub"#), value.pointer(r#"/foo["bar"]/baz["qux"]/blub"#) ); } #[test] fn can_extend_context() { let mut target = Context::new(); target.insert("a", &1); target.insert("b", &2); let mut source = Context::new(); source.insert("b", &3); source.insert("c", &4); target.extend(source); assert_eq!(*target.data.get("a").unwrap(), to_value(1).unwrap()); assert_eq!(*target.data.get("b").unwrap(), to_value(3).unwrap()); assert_eq!(*target.data.get("c").unwrap(), to_value(4).unwrap()); } #[test] fn can_create_context_from_value() { let obj = json!({ "name": "bob", "age": 25 }); let context_from_value = Context::from_value(obj).unwrap(); let mut context = Context::new(); context.insert("name", "bob"); context.insert("age", &25); assert_eq!(context_from_value, context); } #[test] fn can_create_context_from_impl_serialize() { let mut map = HashMap::new(); map.insert("name", "bob"); map.insert("last_name", "something"); let context_from_serialize = Context::from_serialize(&map).unwrap(); let mut context = Context::new(); context.insert("name", "bob"); context.insert("last_name", "something"); assert_eq!(context_from_serialize, context); } #[test] fn can_remove_a_key() { let mut context = Context::new(); context.insert("name", "foo"); context.insert("bio", "Hi, I'm foo."); let mut expected = Context::new(); expected.insert("name", "foo"); assert_eq!(context.remove("bio"), Some(to_value("Hi, I'm foo.").unwrap())); assert_eq!(context.get("bio"), None); assert_eq!(context, expected); } #[test] fn remove_return_none_with_unknown_index() { let mut context = Context::new(); assert_eq!(context.remove("unknown"), None); } } tera-1.20.0/src/errors.rs000064400000000000000000000202611046102023000133110ustar 00000000000000use std::convert::Into; use std::error::Error as StdError; use std::fmt; /// The kind of an error (non-exhaustive) #[derive(Debug)] #[allow(clippy::manual_non_exhaustive)] // reason = "we want to stay backwards compatible, therefore we keep the manual implementation of non_exhaustive" pub enum ErrorKind { /// Generic error Msg(String), /// A loop was found while looking up the inheritance chain CircularExtend { /// Name of the template with the loop tpl: String, /// All the parents templates we found so far inheritance_chain: Vec, }, /// A template is extending a template that wasn't found in the Tera instance MissingParent { /// The template we are currently looking at current: String, /// The missing template parent: String, }, /// A template was missing (more generic version of MissingParent) TemplateNotFound(String), /// A filter wasn't found FilterNotFound(String), /// A test wasn't found TestNotFound(String), /// A macro was defined in the middle of a template InvalidMacroDefinition(String), /// A function wasn't found FunctionNotFound(String), /// An error happened while serializing JSON Json(serde_json::Error), /// An error occured while executing a function. CallFunction(String), /// An error occured while executing a filter. CallFilter(String), /// An error occured while executing a test. CallTest(String), /// An IO error occured Io(std::io::ErrorKind), /// UTF-8 conversion error /// /// This should not occur unless invalid UTF8 chars are rendered Utf8Conversion { /// The context that indicates where the error occurs in the rendering process context: String, }, /// This enum may grow additional variants, so this makes sure clients /// don't count on exhaustive matching. (Otherwise, adding a new variant /// could break existing code.) #[doc(hidden)] __Nonexhaustive, } /// The Error type #[derive(Debug)] pub struct Error { /// Kind of error pub kind: ErrorKind, source: Option>, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.kind { ErrorKind::Msg(ref message) => write!(f, "{}", message), ErrorKind::CircularExtend { ref tpl, ref inheritance_chain } => write!( f, "Circular extend detected for template '{}'. Inheritance chain: `{:?}`", tpl, inheritance_chain ), ErrorKind::MissingParent { ref current, ref parent } => write!( f, "Template '{}' is inheriting from '{}', which doesn't exist or isn't loaded.", current, parent ), ErrorKind::TemplateNotFound(ref name) => write!(f, "Template '{}' not found", name), ErrorKind::FilterNotFound(ref name) => write!(f, "Filter '{}' not found", name), ErrorKind::TestNotFound(ref name) => write!(f, "Test '{}' not found", name), ErrorKind::FunctionNotFound(ref name) => write!(f, "Function '{}' not found", name), ErrorKind::InvalidMacroDefinition(ref info) => { write!(f, "Invalid macro definition: `{}`", info) } ErrorKind::Json(ref e) => write!(f, "{}", e), ErrorKind::CallFunction(ref name) => write!(f, "Function call '{}' failed", name), ErrorKind::CallFilter(ref name) => write!(f, "Filter call '{}' failed", name), ErrorKind::CallTest(ref name) => write!(f, "Test call '{}' failed", name), ErrorKind::Io(ref io_error) => { write!(f, "Io error while writing rendered value to output: {:?}", io_error) } ErrorKind::Utf8Conversion { ref context } => { write!(f, "UTF-8 conversion error occured while rendering template: {}", context) } ErrorKind::__Nonexhaustive => write!(f, "Nonexhaustive"), } } } impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { self.source.as_ref().map(|c| &**c as &(dyn StdError + 'static)) } } impl Error { /// Creates generic error pub fn msg(value: impl ToString) -> Self { Self { kind: ErrorKind::Msg(value.to_string()), source: None } } /// Creates a circular extend error pub fn circular_extend(tpl: impl ToString, inheritance_chain: Vec) -> Self { Self { kind: ErrorKind::CircularExtend { tpl: tpl.to_string(), inheritance_chain }, source: None, } } /// Creates a missing parent error pub fn missing_parent(current: impl ToString, parent: impl ToString) -> Self { Self { kind: ErrorKind::MissingParent { current: current.to_string(), parent: parent.to_string(), }, source: None, } } /// Creates a template not found error pub fn template_not_found(tpl: impl ToString) -> Self { Self { kind: ErrorKind::TemplateNotFound(tpl.to_string()), source: None } } /// Creates a filter not found error pub fn filter_not_found(name: impl ToString) -> Self { Self { kind: ErrorKind::FilterNotFound(name.to_string()), source: None } } /// Creates a test not found error pub fn test_not_found(name: impl ToString) -> Self { Self { kind: ErrorKind::TestNotFound(name.to_string()), source: None } } /// Creates a function not found error pub fn function_not_found(name: impl ToString) -> Self { Self { kind: ErrorKind::FunctionNotFound(name.to_string()), source: None } } /// Creates generic error with a source pub fn chain(value: impl ToString, source: impl Into>) -> Self { Self { kind: ErrorKind::Msg(value.to_string()), source: Some(source.into()) } } /// Creates an error wrapping a failed function call. pub fn call_function( name: impl ToString, source: impl Into>, ) -> Self { Self { kind: ErrorKind::CallFunction(name.to_string()), source: Some(source.into()) } } /// Creates an error wrapping a failed filter call. pub fn call_filter( name: impl ToString, source: impl Into>, ) -> Self { Self { kind: ErrorKind::CallFilter(name.to_string()), source: Some(source.into()) } } /// Creates an error wrapping a failed test call. pub fn call_test( name: impl ToString, source: impl Into>, ) -> Self { Self { kind: ErrorKind::CallTest(name.to_string()), source: Some(source.into()) } } /// Creates JSON error pub fn json(value: serde_json::Error) -> Self { Self { kind: ErrorKind::Json(value), source: None } } /// Creates an invalid macro definition error pub fn invalid_macro_def(name: impl ToString) -> Self { Self { kind: ErrorKind::InvalidMacroDefinition(name.to_string()), source: None } } /// Creates an IO error pub fn io_error(error: std::io::Error) -> Self { Self { kind: ErrorKind::Io(error.kind()), source: Some(Box::new(error)) } } /// Creates an utf8 conversion error pub fn utf8_conversion_error(error: std::string::FromUtf8Error, context: String) -> Self { Self { kind: ErrorKind::Utf8Conversion { context }, source: Some(Box::new(error)) } } } impl From for Error { fn from(error: std::io::Error) -> Self { Self::io_error(error) } } impl From<&str> for Error { fn from(e: &str) -> Self { Self::msg(e) } } impl From for Error { fn from(e: String) -> Self { Self::msg(e) } } impl From for Error { fn from(e: serde_json::Error) -> Self { Self::json(e) } } /// Convenient wrapper around std::Result. pub type Result = ::std::result::Result; #[cfg(test)] mod tests { #[test] fn test_error_is_send_and_sync() { fn test_send_sync() {} test_send_sync::(); } } tera-1.20.0/src/filter_utils.rs000064400000000000000000000145321046102023000145060ustar 00000000000000use crate::errors::{Error, Result}; use serde_json::Value; use std::cmp::Ordering; #[derive(PartialEq, Default, Copy, Clone)] pub struct OrderedF64(f64); impl OrderedF64 { fn new(n: f64) -> Self { OrderedF64(n) } } impl Eq for OrderedF64 {} impl Ord for OrderedF64 { fn cmp(&self, other: &OrderedF64) -> Ordering { // unwrap is safe because self.0 is finite. self.partial_cmp(other).unwrap() } } impl PartialOrd for OrderedF64 { fn partial_cmp(&self, other: &OrderedF64) -> Option { Some(total_cmp(&self.0, &other.0)) } } /// Return the ordering between `self` and `other` f64. /// /// https://doc.rust-lang.org/std/primitive.f64.html#method.total_cmp /// /// Backported from Rust 1.62 to keep MSRV at 1.56 /// /// Unlike the standard partial comparison between floating point numbers, /// this comparison always produces an ordering in accordance to /// the `totalOrder` predicate as defined in the IEEE 754 (2008 revision) /// floating point standard. The values are ordered in the following sequence: /// /// - negative quiet NaN /// - negative signaling NaN /// - negative infinity /// - negative numbers /// - negative subnormal numbers /// - negative zero /// - positive zero /// - positive subnormal numbers /// - positive numbers /// - positive infinity /// - positive signaling NaN /// - positive quiet NaN. /// /// The ordering established by this function does not always agree with the /// [`PartialOrd`] and [`PartialEq`] implementations of `f64`. For example, /// they consider negative and positive zero equal, while `total_cmp` /// doesn't. /// /// The interpretation of the signaling NaN bit follows the definition in /// the IEEE 754 standard, which may not match the interpretation by some of /// the older, non-conformant (e.g. MIPS) hardware implementations. /// #[must_use] #[inline] fn total_cmp(a: &f64, b: &f64) -> Ordering { let mut left = a.to_bits() as i64; let mut right = b.to_bits() as i64; left ^= (((left >> 63) as u64) >> 1) as i64; right ^= (((right >> 63) as u64) >> 1) as i64; left.cmp(&right) } #[derive(Default, Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] pub struct ArrayLen(usize); pub trait GetValue: Ord + Sized + Clone { fn get_value(val: &Value) -> Result; } impl GetValue for OrderedF64 { fn get_value(val: &Value) -> Result { let n = val.as_f64().ok_or_else(|| Error::msg(format!("expected number got {}", val)))?; Ok(OrderedF64::new(n)) } } impl GetValue for i64 { fn get_value(val: &Value) -> Result { val.as_i64().ok_or_else(|| Error::msg(format!("expected number got {}", val))) } } impl GetValue for bool { fn get_value(val: &Value) -> Result { val.as_bool().ok_or_else(|| Error::msg(format!("expected bool got {}", val))) } } impl GetValue for String { fn get_value(val: &Value) -> Result { let str: Result<&str> = val.as_str().ok_or_else(|| Error::msg(format!("expected string got {}", val))); Ok(str?.to_owned()) } } impl GetValue for ArrayLen { fn get_value(val: &Value) -> Result { let arr = val.as_array().ok_or_else(|| Error::msg(format!("expected array got {}", val)))?; Ok(ArrayLen(arr.len())) } } #[derive(Default)] pub struct SortPairs { pairs: Vec<(Value, K)>, } type SortNumbers = SortPairs; type SortBools = SortPairs; type SortStrings = SortPairs; type SortArrays = SortPairs; impl SortPairs { fn try_add_pair(&mut self, val: &Value, key: &Value) -> Result<()> { let key = K::get_value(key)?; self.pairs.push((val.clone(), key)); Ok(()) } fn sort(&mut self) -> Vec { self.pairs.sort_by_key(|a| a.1.clone()); self.pairs.iter().map(|a| a.0.clone()).collect() } } pub trait SortStrategy { fn try_add_pair(&mut self, val: &Value, key: &Value) -> Result<()>; fn sort(&mut self) -> Vec; } impl SortStrategy for SortPairs { fn try_add_pair(&mut self, val: &Value, key: &Value) -> Result<()> { SortPairs::try_add_pair(self, val, key) } fn sort(&mut self) -> Vec { SortPairs::sort(self) } } pub fn get_sort_strategy_for_type(ty: &Value) -> Result> { use crate::Value::*; match *ty { Null => Err(Error::msg("Null is not a sortable value")), Bool(_) => Ok(Box::::default()), Number(_) => Ok(Box::::default()), String(_) => Ok(Box::::default()), Array(_) => Ok(Box::::default()), Object(_) => Err(Error::msg("Object is not a sortable value")), } } #[derive(Default)] pub struct Unique { unique: std::collections::HashSet, } type UniqueNumbers = Unique; type UniqueBools = Unique; struct UniqueStrings { u: Unique, case_sensitive: bool, } pub trait UniqueStrategy { fn insert(&mut self, val: &Value) -> Result; } impl UniqueStrategy for Unique { fn insert(&mut self, val: &Value) -> Result { Ok(self.unique.insert(K::get_value(val)?)) } } impl UniqueStrings { fn new(case_sensitive: bool) -> UniqueStrings { UniqueStrings { u: Unique::::default(), case_sensitive } } } impl UniqueStrategy for UniqueStrings { fn insert(&mut self, val: &Value) -> Result { let mut key = String::get_value(val)?; if !self.case_sensitive { key = key.to_lowercase() } Ok(self.u.unique.insert(key)) } } pub fn get_unique_strategy_for_type( ty: &Value, case_sensitive: bool, ) -> Result> { use crate::Value::*; match *ty { Null => Err(Error::msg("Null is not a unique value")), Bool(_) => Ok(Box::::default()), Number(ref val) => { if val.is_f64() { Err(Error::msg("Unique floats are not implemented")) } else { Ok(Box::::default()) } } String(_) => Ok(Box::new(UniqueStrings::new(case_sensitive))), Array(_) => Err(Error::msg("Unique arrays are not implemented")), Object(_) => Err(Error::msg("Unique objects are not implemented")), } } tera-1.20.0/src/lib.rs000064400000000000000000000066031046102023000125470ustar 00000000000000#![doc(html_root_url = "https://docs.rs/tera")] //! # Tera //! //! A powerful, fast and easy-to-use template engine for Rust //! //! This crate provides an implementation of the Tera template engine, which is designed for use in //! Rust applications. Inspired by [Jinja2] and [Django] templates, Tera provides a familiar and //! expressive syntax for creating dynamic HTML, XML, and other text-based documents. It supports //! template inheritance, variable interpolation, conditionals, loops, filters, and custom //! functions, enabling developers to build complex applications with ease. //! //! See the [site](http://keats.github.io/tera/) for more information and to get started. //! //! ## Features //! //! - High-performance template rendering //! - Safe and sandboxed execution environment //! - Template inheritance and includes //! - Expressive and familiar syntax //! - Extensible with custom filters and functions //! - Automatic escaping of HTML/XML by default //! - Strict mode for enforcing variable existence //! - Template caching and auto-reloading for efficient development //! - Built-in support for JSON and other data formats //! - Comprehensive error messages and debugging information //! //! ## Example //! //! ```rust //! use tera::Tera; //! //! // Create a new Tera instance and add a template from a string //! let mut tera = Tera::new("templates/**/*").unwrap(); //! tera.add_raw_template("hello", "Hello, {{ name }}!").unwrap(); //! // Prepare the context with some data //! let mut context = tera::Context::new(); //! context.insert("name", "World"); //! //! // Render the template with the given context //! let rendered = tera.render("hello", &context).unwrap(); //! assert_eq!(rendered, "Hello, World!"); //! ``` //! //! ## Getting Started //! //! Add the following to your Cargo.toml file: //! //! ```toml //! [dependencies] //! tera = "1.0" //! ``` //! //! Then, consult the official documentation and examples to learn more about using Tera in your //! Rust projects. //! //! [Jinja2]: http://jinja.pocoo.org/ //! [Django]: https://docs.djangoproject.com/en/3.1/topics/templates/ #![deny(missing_docs)] #[macro_use] mod macros; mod builtins; mod context; mod errors; mod filter_utils; mod parser; mod renderer; mod template; mod tera; mod utils; // Library exports. pub use crate::builtins::filters::Filter; pub use crate::builtins::functions::Function; pub use crate::builtins::testers::Test; pub use crate::context::Context; pub use crate::errors::{Error, ErrorKind, Result}; // Template, dotted_pointer and get_json_pointer are meant to be used internally only but is exported for test/bench. #[doc(hidden)] pub use crate::context::dotted_pointer; #[doc(hidden)] #[allow(deprecated)] pub use crate::context::get_json_pointer; #[doc(hidden)] pub use crate::template::Template; pub use crate::tera::Tera; pub use crate::utils::escape_html; // Re-export Value and other useful things from serde // so apps/tools can encode data in Tera types pub use serde_json::value::{from_value, to_value, Map, Number, Value}; // Exposes the AST if one needs it but changing the AST is not considered // a breaking change so it isn't public #[doc(hidden)] pub use crate::parser::ast; /// Re-export some helper fns useful to write filters/fns/tests pub mod helpers { /// Functions helping writing tests pub mod tests { pub use crate::builtins::testers::{extract_string, number_args_allowed, value_defined}; } } tera-1.20.0/src/macros.rs000064400000000000000000000024211046102023000132570ustar 00000000000000/// Helper macro to get real values out of Value while retaining /// proper errors in filters /// /// Takes 4 args: /// /// - the filter name, /// - the variable name: use "value" if you are using it on the variable the filter is ran on /// - the expected type /// - the actual variable /// /// ```no_compile /// let arr = try_get_value!("first", "value", Vec, value); /// let val = try_get_value!("pluralize", "suffix", String, val.clone()); /// ``` #[macro_export] macro_rules! try_get_value { ($filter_name:expr, $var_name:expr, $ty:ty, $val:expr) => {{ match $crate::from_value::<$ty>($val.clone()) { Ok(s) => s, Err(_) => { if $var_name == "value" { return Err($crate::Error::msg(format!( "Filter `{}` was called on an incorrect value: got `{}` but expected a {}", $filter_name, $val, stringify!($ty) ))); } else { return Err($crate::Error::msg(format!( "Filter `{}` received an incorrect type for arg `{}`: got `{}` but expected a {}", $filter_name, $var_name, $val, stringify!($ty) ))); } } } }}; } tera-1.20.0/src/parser/ast.rs000064400000000000000000000217601046102023000140650ustar 00000000000000use std::collections::HashMap; use std::fmt; /// Whether to remove the whitespace of a `{% %}` tag #[derive(Clone, Copy, Debug, PartialEq, Default)] pub struct WS { /// `true` if the tag is `{%-` pub left: bool, /// `true` if the tag is `-%}` pub right: bool, } /// All math operators #[derive(Copy, Clone, Debug, PartialEq)] pub enum MathOperator { /// + Add, /// - Sub, /// * Mul, /// / Div, /// % Modulo, } impl fmt::Display for MathOperator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", match *self { MathOperator::Add => "+", MathOperator::Sub => "-", MathOperator::Mul => "*", MathOperator::Div => "/", MathOperator::Modulo => "%", } ) } } /// All logic operators #[derive(Copy, Clone, Debug, PartialEq)] pub enum LogicOperator { /// > Gt, /// >= Gte, /// < Lt, /// <= Lte, /// == Eq, /// != NotEq, /// and And, /// or Or, } impl fmt::Display for LogicOperator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", match *self { LogicOperator::Gt => ">", LogicOperator::Gte => ">=", LogicOperator::Lt => "<", LogicOperator::Lte => "<=", LogicOperator::Eq => "==", LogicOperator::NotEq => "!=", LogicOperator::And => "and", LogicOperator::Or => "or", } ) } } /// A function call, can be a filter or a global function #[derive(Clone, Debug, PartialEq)] pub struct FunctionCall { /// The name of the function pub name: String, /// The args of the function: key -> value pub args: HashMap, } /// A mathematical expression #[derive(Clone, Debug, PartialEq)] pub struct MathExpr { /// The left hand side of the expression pub lhs: Box, /// The right hand side of the expression pub rhs: Box, /// The operator used pub operator: MathOperator, } /// A logical expression #[derive(Clone, Debug, PartialEq)] pub struct LogicExpr { /// The left hand side of the expression pub lhs: Box, /// The right hand side of the expression pub rhs: Box, /// The operator used pub operator: LogicOperator, } /// Can only be a combination of string + ident or ident + ident #[derive(Clone, Debug, PartialEq)] pub struct StringConcat { /// All the values we're concatening into a string pub values: Vec, } impl StringConcat { pub(crate) fn to_template_string(&self) -> String { let mut res = Vec::new(); for value in &self.values { match value { ExprVal::String(ref s) => res.push(format!("'{}'", s)), ExprVal::Ident(ref s) => res.push(s.to_string()), _ => res.push("unknown".to_string()), } } res.join(" ~ ") } } /// Something that checks whether the left side is contained in the right side #[derive(Clone, Debug, PartialEq)] pub struct In { /// The needle, a string or a basic expression/literal pub lhs: Box, /// The haystack, can be a string, an array or an ident only currently pub rhs: Box, /// Is it using `not` as in `b` not in `...`? pub negated: bool, } /// An expression is the node found in variable block, kwargs and conditions. #[derive(Clone, Debug, PartialEq)] #[allow(missing_docs)] pub enum ExprVal { String(String), Int(i64), Float(f64), Bool(bool), Ident(String), Math(MathExpr), Logic(LogicExpr), Test(Test), MacroCall(MacroCall), FunctionCall(FunctionCall), // A vec of Expr, not ExprVal since filters are allowed // on values inside arrays Array(Vec), StringConcat(StringConcat), In(In), } /// An expression is a value that can be negated and followed by /// optional filters #[derive(Clone, Debug, PartialEq)] pub struct Expr { /// The expression we are evaluating pub val: ExprVal, /// Is it using `not`? pub negated: bool, /// List of filters used on that value pub filters: Vec, } impl Expr { /// Create a new basic Expr pub fn new(val: ExprVal) -> Expr { Expr { val, negated: false, filters: vec![] } } /// Create a new negated Expr pub fn new_negated(val: ExprVal) -> Expr { Expr { val, negated: true, filters: vec![] } } /// Create a new basic Expr with some filters pub fn with_filters(val: ExprVal, filters: Vec) -> Expr { Expr { val, filters, negated: false } } /// Check if the expr has a default filter as first filter pub fn has_default_filter(&self) -> bool { if self.filters.is_empty() { return false; } self.filters[0].name == "default" } /// Check if the last filter is `safe` pub fn is_marked_safe(&self) -> bool { if self.filters.is_empty() { return false; } self.filters[self.filters.len() - 1].name == "safe" } } /// A test node `if my_var is odd` #[derive(Clone, Debug, PartialEq)] pub struct Test { /// Which variable is evaluated pub ident: String, /// Is it using `not`? pub negated: bool, /// Name of the test pub name: String, /// Any optional arg given to the test pub args: Vec, } /// A filter section node `{{ filter name(param="value") }} content {{ endfilter }}` #[derive(Clone, Debug, PartialEq)] pub struct FilterSection { /// The filter call itsel pub filter: FunctionCall, /// The filter body pub body: Vec, } /// Set a variable in the context `{% set val = "hey" %}` #[derive(Clone, Debug, PartialEq)] pub struct Set { /// The name for that value in the context pub key: String, /// The value to assign pub value: Expr, /// Whether we want to set the variable globally or locally /// global_set is only useful in loops pub global: bool, } /// A call to a namespaced macro `macros::my_macro()` #[derive(Clone, Debug, PartialEq)] pub struct MacroCall { /// The namespace we're looking for that macro in pub namespace: String, /// The macro name pub name: String, /// The args for that macro: name -> value pub args: HashMap, } /// A Macro definition #[derive(Clone, Debug, PartialEq)] pub struct MacroDefinition { /// The macro name pub name: String, /// The args for that macro: name -> optional default value pub args: HashMap>, /// The macro content pub body: Vec, } /// A block definition #[derive(Clone, Debug, PartialEq)] pub struct Block { /// The block name pub name: String, /// The block content pub body: Vec, } /// A forloop: can be over values or key/values #[derive(Clone, Debug, PartialEq)] pub struct Forloop { /// Name of the key in the loop (only when iterating on map-like objects) pub key: Option, /// Name of the local variable for the value in the loop pub value: String, /// Expression being iterated on pub container: Expr, /// What's in the forloop itself pub body: Vec, /// The body to execute in case of an empty object pub empty_body: Option>, } /// An if/elif/else condition with their respective body #[derive(Clone, Debug, PartialEq)] pub struct If { /// First item if the if, all the ones after are elif pub conditions: Vec<(WS, Expr, Vec)>, /// The optional `else` block pub otherwise: Option<(WS, Vec)>, } /// All Tera nodes that can be encountered #[derive(Clone, Debug, PartialEq)] pub enum Node { /// A call to `{{ super() }}` in a block Super, /// Some actual text Text(String), /// A `{{ }}` block VariableBlock(WS, Expr), /// A `{% macro hello() %}...{% endmacro %}` MacroDefinition(WS, MacroDefinition, WS), /// The `{% extends "blabla.html" %}` node, contains the template name Extends(WS, String), /// The `{% include "blabla.html" %}` node, contains the template name Include(WS, Vec, bool), /// The `{% import "macros.html" as macros %}` ImportMacro(WS, String, String), /// The `{% set val = something %}` tag Set(WS, Set), /// The text between `{% raw %}` and `{% endraw %}` Raw(WS, String, WS), /// A filter section node `{{ filter name(param="value") }} content {{ endfilter }}` FilterSection(WS, FilterSection, WS), /// A `{% block name %}...{% endblock %}` Block(WS, Block, WS), /// A `{% for i in items %}...{% endfor %}` Forloop(WS, Forloop, WS), /// A if/elif/else block, WS for the if/elif/else is directly in the struct If(If, WS), /// The `{% break %}` tag Break(WS), /// The `{% continue %}` tag Continue(WS), /// The `{# #} `comment tag and its content Comment(WS, String), } tera-1.20.0/src/parser/mod.rs000064400000000000000000001343161046102023000140570ustar 00000000000000use std::collections::HashMap; use lazy_static::lazy_static; use pest::iterators::Pair; use pest::pratt_parser::{Assoc, Op, PrattParser}; use pest::Parser; use pest_derive::Parser; use crate::errors::{Error, Result as TeraResult}; // This include forces recompiling this source file if the grammar file changes. // Uncomment it when doing changes to the .pest file const _GRAMMAR: &str = include_str!("tera.pest"); #[derive(Parser)] #[grammar = "parser/tera.pest"] pub struct TeraParser; /// The AST of Tera pub mod ast; mod whitespace; #[cfg(test)] mod tests; use self::ast::*; pub use self::whitespace::remove_whitespace; lazy_static! { static ref MATH_PARSER: PrattParser = PrattParser::new() .op(Op::infix(Rule::op_plus, Assoc::Left) | Op::infix(Rule::op_minus, Assoc::Left)) // +, - .op(Op::infix(Rule::op_times, Assoc::Left) | Op::infix(Rule::op_slash, Assoc::Left) | Op::infix(Rule::op_modulo, Assoc::Left)); // *, /, % static ref COMPARISON_EXPR_PARSER: PrattParser = PrattParser::new() .op(Op::infix(Rule::op_lt, Assoc::Left) | Op::infix(Rule::op_lte, Assoc::Left) | Op::infix(Rule::op_gt, Assoc::Left) | Op::infix(Rule::op_gte, Assoc::Left) | Op::infix(Rule::op_eq, Assoc::Left)| Op::infix(Rule::op_ineq, Assoc::Left)); // <, <=, >, >=, ==, != static ref LOGIC_EXPR_PARSER: PrattParser = PrattParser::new() .op(Op::infix(Rule::op_or, Assoc::Left)).op(Op::infix(Rule::op_and, Assoc::Left)); } /// Strings are delimited by double quotes, single quotes and backticks /// We need to remove those before putting them in the AST fn replace_string_markers(input: &str) -> String { match input.chars().next().unwrap() { '"' => input.replace('"', ""), '\'' => input.replace('\'', ""), '`' => input.replace('`', ""), _ => unreachable!("How did you even get there"), } } fn parse_kwarg(pair: Pair) -> TeraResult<(String, Expr)> { let mut name = None; let mut val = None; for p in pair.into_inner() { match p.as_rule() { Rule::ident => name = Some(p.as_span().as_str().to_string()), Rule::logic_expr => val = Some(parse_logic_expr(p)?), Rule::array_filter => val = Some(parse_array_with_filters(p)?), _ => unreachable!("{:?} not supposed to get there (parse_kwarg)!", p.as_rule()), }; } Ok((name.unwrap(), val.unwrap())) } fn parse_fn_call(pair: Pair) -> TeraResult { let mut name = None; let mut args = HashMap::new(); for p in pair.into_inner() { match p.as_rule() { Rule::ident => name = Some(p.as_span().as_str().to_string()), Rule::kwarg => { let (name, val) = parse_kwarg(p)?; args.insert(name, val); } _ => unreachable!("{:?} not supposed to get there (parse_fn_call)!", p.as_rule()), }; } Ok(FunctionCall { name: name.unwrap(), args }) } fn parse_filter(pair: Pair) -> TeraResult { let mut name = None; let mut args = HashMap::new(); for p in pair.into_inner() { match p.as_rule() { Rule::ident => name = Some(p.as_span().as_str().to_string()), Rule::kwarg => { let (name, val) = parse_kwarg(p)?; args.insert(name, val); } Rule::fn_call => { return parse_fn_call(p); } _ => unreachable!("{:?} not supposed to get there (parse_filter)!", p.as_rule()), }; } Ok(FunctionCall { name: name.unwrap(), args }) } fn parse_test_call(pair: Pair) -> TeraResult<(String, Vec)> { let mut name = None; let mut args = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::ident => name = Some(p.as_span().as_str().to_string()), Rule::test_arg => // iterate on the test_arg rule { for p2 in p.into_inner() { match p2.as_rule() { Rule::logic_expr => { args.push(parse_logic_expr(p2)?); } Rule::array => { args.push(Expr::new(parse_array(p2)?)); } _ => unreachable!("Invalid arg type for test {:?}", p2.as_rule()), } } } _ => unreachable!("{:?} not supposed to get there (parse_test_call)!", p.as_rule()), }; } Ok((name.unwrap(), args)) } fn parse_test(pair: Pair) -> TeraResult { let mut ident = None; let mut name = None; let mut args = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::dotted_square_bracket_ident => ident = Some(p.as_str().to_string()), Rule::test_call => { let (_name, _args) = parse_test_call(p)?; name = Some(_name); args = _args; } _ => unreachable!("{:?} not supposed to get there (parse_ident)!", p.as_rule()), }; } Ok(Test { ident: ident.unwrap(), negated: false, name: name.unwrap(), args }) } fn parse_string_concat(pair: Pair) -> TeraResult { let mut values = vec![]; let mut current_str = String::new(); // Can we fold it into a simple string? for p in pair.into_inner() { match p.as_rule() { Rule::string => { current_str.push_str(&replace_string_markers(p.as_str())); } Rule::int => { if !current_str.is_empty() { values.push(ExprVal::String(current_str)); current_str = String::new(); } values.push(ExprVal::Int(p.as_str().parse().map_err(|_| { Error::msg(format!("Integer out of bounds: `{}`", p.as_str())) })?)); } Rule::float => { if !current_str.is_empty() { values.push(ExprVal::String(current_str)); current_str = String::new(); } values.push(ExprVal::Float( p.as_str().parse().map_err(|_| { Error::msg(format!("Float out of bounds: `{}`", p.as_str())) })?, )); } Rule::dotted_square_bracket_ident => { if !current_str.is_empty() { values.push(ExprVal::String(current_str)); current_str = String::new(); } values.push(ExprVal::Ident(p.as_str().to_string())) } Rule::fn_call => { if !current_str.is_empty() { values.push(ExprVal::String(current_str)); current_str = String::new(); } values.push(ExprVal::FunctionCall(parse_fn_call(p)?)) } _ => unreachable!("Got {:?} in parse_string_concat", p), }; } if values.is_empty() { // we only got a string return Ok(ExprVal::String(current_str)); } if !current_str.is_empty() { values.push(ExprVal::String(current_str)); } Ok(ExprVal::StringConcat(StringConcat { values })) } fn parse_basic_expression(pair: Pair) -> TeraResult { let primary = parse_basic_expression; let infix = |lhs: TeraResult, op: Pair, rhs: TeraResult| { Ok(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(lhs?)), operator: match op.as_rule() { Rule::op_plus => MathOperator::Add, Rule::op_minus => MathOperator::Sub, Rule::op_times => MathOperator::Mul, Rule::op_slash => MathOperator::Div, Rule::op_modulo => MathOperator::Modulo, _ => unreachable!(), }, rhs: Box::new(Expr::new(rhs?)), })) }; let expr = match pair.as_rule() { Rule::int => ExprVal::Int( pair.as_str() .parse() .map_err(|_| Error::msg(format!("Integer out of bounds: `{}`", pair.as_str())))?, ), Rule::float => ExprVal::Float( pair.as_str() .parse() .map_err(|_| Error::msg(format!("Float out of bounds: `{}`", pair.as_str())))?, ), Rule::boolean => match pair.as_str() { "true" => ExprVal::Bool(true), "True" => ExprVal::Bool(true), "false" => ExprVal::Bool(false), "False" => ExprVal::Bool(false), _ => unreachable!(), }, Rule::test => ExprVal::Test(parse_test(pair)?), Rule::test_not => { let mut test = parse_test(pair)?; test.negated = true; ExprVal::Test(test) } Rule::fn_call => ExprVal::FunctionCall(parse_fn_call(pair)?), Rule::macro_call => ExprVal::MacroCall(parse_macro_call(pair)?), Rule::dotted_square_bracket_ident => ExprVal::Ident(pair.as_str().to_string()), Rule::basic_expr => { MATH_PARSER.map_primary(primary).map_infix(infix).parse(pair.into_inner())? } _ => unreachable!("Got {:?} in parse_basic_expression: {}", pair.as_rule(), pair.as_str()), }; Ok(expr) } /// A basic expression with optional filters fn parse_basic_expr_with_filters(pair: Pair) -> TeraResult { let mut expr_val = None; let mut filters = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::basic_expr => expr_val = Some(parse_basic_expression(p)?), Rule::filter => filters.push(parse_filter(p)?), _ => unreachable!("Got {:?}", p), }; } Ok(Expr { val: expr_val.unwrap(), negated: false, filters }) } /// A string expression with optional filters fn parse_string_expr_with_filters(pair: Pair) -> TeraResult { let mut expr_val = None; let mut filters = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::string => expr_val = Some(ExprVal::String(replace_string_markers(p.as_str()))), Rule::string_concat => expr_val = Some(parse_string_concat(p)?), Rule::filter => filters.push(parse_filter(p)?), _ => unreachable!("Got {:?}", p), }; } Ok(Expr { val: expr_val.unwrap(), negated: false, filters }) } /// An array with optional filters fn parse_array_with_filters(pair: Pair) -> TeraResult { let mut array = None; let mut filters = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::array => array = Some(parse_array(p)?), Rule::filter => filters.push(parse_filter(p)?), _ => unreachable!("Got {:?}", p), }; } Ok(Expr { val: array.unwrap(), negated: false, filters }) } fn parse_in_condition_container(pair: Pair) -> TeraResult { let mut expr = None; for p in pair.into_inner() { match p.as_rule() { Rule::array_filter => expr = Some(parse_array_with_filters(p)?), Rule::dotted_square_bracket_ident => { expr = Some(Expr::new(ExprVal::Ident(p.as_str().to_string()))) } Rule::string_expr_filter => expr = Some(parse_string_expr_with_filters(p)?), _ => unreachable!("Got {:?} in parse_in_condition_container", p), }; } Ok(expr.unwrap()) } fn parse_in_condition(pair: Pair) -> TeraResult { let mut lhs = None; let mut rhs = None; let mut negated = false; for p in pair.into_inner() { match p.as_rule() { // lhs Rule::string_expr_filter => lhs = Some(parse_string_expr_with_filters(p)?), Rule::basic_expr_filter => lhs = Some(parse_basic_expr_with_filters(p)?), // rhs Rule::in_cond_container => rhs = Some(parse_in_condition_container(p)?), Rule::op_not => negated = true, _ => unreachable!("Got {:?} in parse_in_condition", p), }; } Ok(Expr::new(ExprVal::In(In { lhs: Box::new(lhs.unwrap()), rhs: Box::new(rhs.unwrap()), negated, }))) } /// A basic expression with optional filters with prece fn parse_comparison_val(pair: Pair) -> TeraResult { let primary = parse_comparison_val; let infix = |lhs: TeraResult, op: Pair, rhs: TeraResult| { Ok(Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(lhs?), operator: match op.as_rule() { Rule::op_plus => MathOperator::Add, Rule::op_minus => MathOperator::Sub, Rule::op_times => MathOperator::Mul, Rule::op_slash => MathOperator::Div, Rule::op_modulo => MathOperator::Modulo, _ => unreachable!(), }, rhs: Box::new(rhs?), }))) }; let expr = match pair.as_rule() { Rule::basic_expr_filter => parse_basic_expr_with_filters(pair)?, Rule::comparison_val => { MATH_PARSER.map_primary(primary).map_infix(infix).parse(pair.into_inner())? } _ => unreachable!("Got {:?} in parse_comparison_val", pair.as_rule()), }; Ok(expr) } fn parse_comparison_expression(pair: Pair) -> TeraResult { let primary = parse_comparison_expression; let infix = |lhs: TeraResult, op: Pair, rhs: TeraResult| { Ok(Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(lhs?), operator: match op.as_rule() { Rule::op_lt => LogicOperator::Lt, Rule::op_lte => LogicOperator::Lte, Rule::op_gt => LogicOperator::Gt, Rule::op_gte => LogicOperator::Gte, Rule::op_ineq => LogicOperator::NotEq, Rule::op_eq => LogicOperator::Eq, _ => unreachable!(), }, rhs: Box::new(rhs?), }))) }; let expr = match pair.as_rule() { Rule::comparison_val => parse_comparison_val(pair)?, Rule::string_expr_filter => parse_string_expr_with_filters(pair)?, Rule::comparison_expr => { COMPARISON_EXPR_PARSER.map_primary(primary).map_infix(infix).parse(pair.into_inner())? } _ => unreachable!("Got {:?} in parse_comparison_expression", pair.as_rule()), }; Ok(expr) } /// An expression that can be negated fn parse_logic_val(pair: Pair) -> TeraResult { let mut negated = false; let mut expr = None; for p in pair.into_inner() { match p.as_rule() { Rule::op_not => negated = true, Rule::in_cond => expr = Some(parse_in_condition(p)?), Rule::comparison_expr => expr = Some(parse_comparison_expression(p)?), Rule::string_expr_filter => expr = Some(parse_string_expr_with_filters(p)?), Rule::logic_expr => expr = Some(parse_logic_expr(p)?), _ => unreachable!(), }; } let mut e = expr.unwrap(); e.negated = negated; Ok(e) } fn parse_logic_expr(pair: Pair) -> TeraResult { let primary = parse_logic_expr; let infix = |lhs: TeraResult, op: Pair, rhs: TeraResult| match op.as_rule() { Rule::op_or => Ok(Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(lhs?), operator: LogicOperator::Or, rhs: Box::new(rhs?), }))), Rule::op_and => Ok(Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(lhs?), operator: LogicOperator::And, rhs: Box::new(rhs?), }))), _ => unreachable!( "{:?} not supposed to get there (infix of logic_expression)!", op.as_rule() ), }; let expr = match pair.as_rule() { Rule::logic_val => parse_logic_val(pair)?, Rule::logic_expr => { LOGIC_EXPR_PARSER.map_primary(primary).map_infix(infix).parse(pair.into_inner())? } _ => unreachable!("Got {:?} in parse_logic_expr", pair.as_rule()), }; Ok(expr) } fn parse_array(pair: Pair) -> TeraResult { let mut vals = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::logic_val => { vals.push(parse_logic_val(p)?); } _ => unreachable!("Got {:?} in parse_array", p.as_rule()), } } Ok(ExprVal::Array(vals)) } fn parse_string_array(pair: Pair) -> Vec { let mut vals = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::string => { vals.push(replace_string_markers(p.as_span().as_str())); } _ => unreachable!("Got {:?} in parse_string_array", p.as_rule()), } } vals } fn parse_macro_call(pair: Pair) -> TeraResult { let mut namespace = None; let mut name = None; let mut args = HashMap::new(); for p in pair.into_inner() { match p.as_rule() { Rule::ident => { // namespace comes first if namespace.is_none() { namespace = Some(p.as_span().as_str().to_string()); } else { name = Some(p.as_span().as_str().to_string()); } } Rule::kwarg => { let (key, val) = parse_kwarg(p)?; args.insert(key, val); } _ => unreachable!("Got {:?} in parse_macro_call", p.as_rule()), } } Ok(MacroCall { namespace: namespace.unwrap(), name: name.unwrap(), args }) } fn parse_variable_tag(pair: Pair) -> TeraResult { let mut ws = WS::default(); let mut expr = None; for p in pair.into_inner() { match p.as_rule() { Rule::variable_start => { ws.left = p.as_span().as_str() == "{{-"; } Rule::variable_end => { ws.right = p.as_span().as_str() == "-}}"; } Rule::logic_expr => expr = Some(parse_logic_expr(p)?), Rule::array_filter => expr = Some(parse_array_with_filters(p)?), _ => unreachable!("unexpected {:?} rule in parse_variable_tag", p.as_rule()), } } Ok(Node::VariableBlock(ws, expr.unwrap())) } fn parse_import_macro(pair: Pair) -> Node { let mut ws = WS::default(); let mut file = None; let mut ident = None; for p in pair.into_inner() { match p.as_rule() { Rule::tag_start => { ws.left = p.as_span().as_str() == "{%-"; } Rule::string => file = Some(replace_string_markers(p.as_span().as_str())), Rule::ident => ident = Some(p.as_span().as_str().to_string()), Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } _ => unreachable!(), }; } Node::ImportMacro(ws, file.unwrap(), ident.unwrap()) } fn parse_extends(pair: Pair) -> Node { let mut ws = WS::default(); let mut file = None; for p in pair.into_inner() { match p.as_rule() { Rule::tag_start => { ws.left = p.as_span().as_str() == "{%-"; } Rule::string => file = Some(replace_string_markers(p.as_span().as_str())), Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } _ => unreachable!(), }; } Node::Extends(ws, file.unwrap()) } fn parse_include(pair: Pair) -> Node { let mut ws = WS::default(); let mut files = vec![]; let mut ignore_missing = false; for p in pair.into_inner() { match p.as_rule() { Rule::tag_start => { ws.left = p.as_span().as_str() == "{%-"; } Rule::string => { files.push(replace_string_markers(p.as_span().as_str())); } Rule::string_array => files.extend(parse_string_array(p)), Rule::ignore_missing => ignore_missing = true, Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } _ => unreachable!(), }; } Node::Include(ws, files, ignore_missing) } fn parse_set_tag(pair: Pair, global: bool) -> TeraResult { let mut ws = WS::default(); let mut key = None; let mut expr = None; for p in pair.into_inner() { match p.as_rule() { Rule::tag_start => { ws.left = p.as_span().as_str() == "{%-"; } Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } Rule::ident => key = Some(p.as_str().to_string()), Rule::logic_expr => expr = Some(parse_logic_expr(p)?), Rule::array_filter => expr = Some(parse_array_with_filters(p)?), _ => unreachable!("unexpected {:?} rule in parse_set_tag", p.as_rule()), } } Ok(Node::Set(ws, Set { key: key.unwrap(), value: expr.unwrap(), global })) } fn parse_raw_tag(pair: Pair) -> Node { let mut start_ws = WS::default(); let mut end_ws = WS::default(); let mut text = None; for p in pair.into_inner() { match p.as_rule() { Rule::raw_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => start_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => start_ws.right = p2.as_span().as_str() == "-%}", _ => unreachable!(), } } } Rule::raw_text => text = Some(p.as_str().to_string()), Rule::endraw_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", _ => unreachable!(), } } } _ => unreachable!("unexpected {:?} rule in parse_raw_tag", p.as_rule()), }; } Node::Raw(start_ws, text.unwrap(), end_ws) } fn parse_filter_section(pair: Pair) -> TeraResult { let mut start_ws = WS::default(); let mut end_ws = WS::default(); let mut filter = None; let mut body = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::filter_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => start_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => start_ws.right = p2.as_span().as_str() == "-%}", Rule::fn_call => filter = Some(parse_fn_call(p2)?), Rule::ident => { filter = Some(FunctionCall { name: p2.as_str().to_string(), args: HashMap::new(), }); } _ => unreachable!("Got {:?} while parsing filter_tag", p2), } } } Rule::content | Rule::macro_content | Rule::block_content | Rule::filter_section_content | Rule::for_content => { body.extend(parse_content(p)?); } Rule::endfilter_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", _ => unreachable!(), } } } _ => unreachable!("unexpected {:?} rule in parse_filter_section", p.as_rule()), }; } Ok(Node::FilterSection(start_ws, FilterSection { filter: filter.unwrap(), body }, end_ws)) } fn parse_block(pair: Pair) -> TeraResult { let mut start_ws = WS::default(); let mut end_ws = WS::default(); let mut name = None; let mut body = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::block_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => start_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => start_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => name = Some(p2.as_span().as_str().to_string()), _ => unreachable!(), }; } } Rule::block_content => body.extend(parse_content(p)?), Rule::endblock_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => (), _ => unreachable!(), }; } } _ => unreachable!("unexpected {:?} rule in parse_filter_section", p.as_rule()), }; } Ok(Node::Block(start_ws, Block { name: name.unwrap(), body }, end_ws)) } fn parse_macro_arg(p: Pair) -> TeraResult { let val = match p.as_rule() { Rule::int => Some(ExprVal::Int( p.as_str() .parse() .map_err(|_| Error::msg(format!("Integer out of bounds: `{}`", p.as_str())))?, )), Rule::float => Some(ExprVal::Float( p.as_str() .parse() .map_err(|_| Error::msg(format!("Float out of bounds: `{}`", p.as_str())))?, )), Rule::boolean => match p.as_str() { "true" => Some(ExprVal::Bool(true)), "True" => Some(ExprVal::Bool(true)), "false" => Some(ExprVal::Bool(false)), "False" => Some(ExprVal::Bool(false)), _ => unreachable!(), }, Rule::string => Some(ExprVal::String(replace_string_markers(p.as_str()))), _ => unreachable!("Got {:?} in parse_macro_arg: {}", p.as_rule(), p.as_str()), }; Ok(val.unwrap()) } fn parse_macro_fn(pair: Pair) -> TeraResult<(String, HashMap>)> { let mut name = String::new(); let mut args = HashMap::new(); for p2 in pair.into_inner() { match p2.as_rule() { Rule::ident => name = p2.as_str().to_string(), Rule::macro_def_arg => { let mut arg_name = None; let mut default_val = None; for p3 in p2.into_inner() { match p3.as_rule() { Rule::ident => arg_name = Some(p3.as_str().to_string()), _ => default_val = Some(Expr::new(parse_macro_arg(p3)?)), }; } args.insert(arg_name.unwrap(), default_val); } _ => continue, } } Ok((name, args)) } fn parse_macro_definition(pair: Pair) -> TeraResult { let mut start_ws = WS::default(); let mut end_ws = WS::default(); let mut name = String::new(); let mut args = HashMap::new(); let mut body = vec![]; for p in pair.into_inner() { match p.as_rule() { Rule::macro_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => start_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => start_ws.right = p2.as_span().as_str() == "-%}", Rule::macro_fn_wrapper => { let macro_fn = parse_macro_fn(p2)?; name = macro_fn.0; args = macro_fn.1; } _ => continue, }; } } Rule::macro_content => body.extend(parse_content(p)?), Rule::endmacro_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => (), _ => unreachable!(), }; } } _ => unreachable!("unexpected {:?} rule in parse_macro_definition", p.as_rule()), } } Ok(Node::MacroDefinition(start_ws, MacroDefinition { name, args, body }, end_ws)) } fn parse_forloop(pair: Pair) -> TeraResult { let mut start_ws = WS::default(); let mut end_ws = WS::default(); let mut key = None; let mut value = None; let mut container = None; let mut body = vec![]; let mut empty_body: Option> = None; for p in pair.into_inner() { match p.as_rule() { Rule::for_tag => { let mut idents = vec![]; for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => start_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => start_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => idents.push(p2.as_str().to_string()), Rule::basic_expr_filter => { container = Some(parse_basic_expr_with_filters(p2)?); } Rule::array_filter => container = Some(parse_array_with_filters(p2)?), _ => unreachable!(), }; } if idents.len() == 1 { value = Some(idents[0].clone()); } else { key = Some(idents[0].clone()); value = Some(idents[1].clone()); } } Rule::content | Rule::macro_content | Rule::block_content | Rule::filter_section_content | Rule::for_content => { match empty_body { Some(ref mut empty_body) => empty_body.extend(parse_content(p)?), None => body.extend(parse_content(p)?), }; } Rule::else_tag => { empty_body = Some(vec![]); } Rule::endfor_tag => { for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => (), _ => unreachable!(), }; } } _ => unreachable!("unexpected {:?} rule in parse_forloop", p.as_rule()), }; } Ok(Node::Forloop( start_ws, Forloop { key, value: value.unwrap(), container: container.unwrap(), body, empty_body }, end_ws, )) } fn parse_break_tag(pair: Pair) -> Node { let mut ws = WS::default(); for p in pair.into_inner() { match p.as_rule() { Rule::tag_start => { ws.left = p.as_span().as_str() == "{%-"; } Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } _ => unreachable!(), }; } Node::Break(ws) } fn parse_continue_tag(pair: Pair) -> Node { let mut ws = WS::default(); for p in pair.into_inner() { match p.as_rule() { Rule::tag_start => { ws.left = p.as_span().as_str() == "{%-"; } Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } _ => unreachable!(), }; } Node::Continue(ws) } fn parse_comment_tag(pair: Pair) -> Node { let mut ws = WS::default(); let mut content = String::new(); for p in pair.into_inner() { match p.as_rule() { Rule::comment_start => { ws.left = p.as_span().as_str() == "{#-"; } Rule::comment_end => { ws.right = p.as_span().as_str() == "-#}"; } Rule::comment_text => { content = p.as_str().to_owned(); } _ => unreachable!(), }; } Node::Comment(ws, content) } fn parse_if(pair: Pair) -> TeraResult { // the `endif` tag ws handling let mut end_ws = WS::default(); let mut conditions = vec![]; let mut otherwise = None; // the current node we're exploring let mut current_ws = WS::default(); let mut expr = None; let mut current_body = vec![]; let mut in_else = false; for p in pair.into_inner() { match p.as_rule() { Rule::if_tag | Rule::elif_tag => { // Reset everything for elifs if p.as_rule() == Rule::elif_tag { conditions.push((current_ws, expr.unwrap(), current_body)); expr = None; current_ws = WS::default(); current_body = vec![]; } for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => current_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => current_ws.right = p2.as_span().as_str() == "-%}", Rule::logic_expr => expr = Some(parse_logic_expr(p2)?), _ => unreachable!(), }; } } Rule::content | Rule::macro_content | Rule::block_content | Rule::for_content | Rule::filter_section_content => current_body.extend(parse_content(p)?), Rule::else_tag => { // had an elif before the else if expr.is_some() { conditions.push((current_ws, expr.unwrap(), current_body)); expr = None; current_ws = WS::default(); current_body = vec![]; } in_else = true; for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => current_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => current_ws.right = p2.as_span().as_str() == "-%}", _ => unreachable!(), }; } } Rule::endif_tag => { if in_else { otherwise = Some((current_ws, current_body)); } else { // the last elif conditions.push((current_ws, expr.unwrap(), current_body)); } for p2 in p.into_inner() { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", _ => unreachable!(), }; } break; } _ => unreachable!("unreachable rule in parse_if: {:?}", p.as_rule()), } } Ok(Node::If(If { conditions, otherwise }, end_ws)) } fn parse_content(pair: Pair) -> TeraResult> { let pairs = pair.into_inner(); let mut nodes = Vec::with_capacity(pairs.len()); for p in pairs { match p.as_rule() { Rule::include_tag => nodes.push(parse_include(p)), Rule::comment_tag => nodes.push(parse_comment_tag(p)), Rule::super_tag => nodes.push(Node::Super), Rule::set_tag => nodes.push(parse_set_tag(p, false)?), Rule::set_global_tag => nodes.push(parse_set_tag(p, true)?), Rule::raw => nodes.push(parse_raw_tag(p)), Rule::variable_tag => nodes.push(parse_variable_tag(p)?), Rule::forloop => nodes.push(parse_forloop(p)?), Rule::break_tag => nodes.push(parse_break_tag(p)), Rule::continue_tag => nodes.push(parse_continue_tag(p)), Rule::content_if | Rule::macro_if | Rule::block_if | Rule::for_if | Rule::filter_section_if => nodes.push(parse_if(p)?), Rule::filter_section => nodes.push(parse_filter_section(p)?), Rule::text => nodes.push(Node::Text(p.as_span().as_str().to_string())), Rule::block => nodes.push(parse_block(p)?), _ => unreachable!("unreachable content rule: {:?}", p.as_rule()), }; } Ok(nodes) } pub fn parse(input: &str) -> TeraResult> { let mut pairs = match TeraParser::parse(Rule::template, input) { Ok(p) => p, Err(e) => { let fancy_e = e.renamed_rules(|rule| { match *rule { Rule::EOI => "end of input".to_string(), Rule::int => "an integer".to_string(), Rule::float => "a float".to_string(), Rule::string | Rule::double_quoted_string | Rule::single_quoted_string | Rule::backquoted_quoted_string => { "a string".to_string() } Rule::string_concat => "a concatenation of strings".to_string(), Rule::string_expr_filter => "a string or a concatenation of strings".to_string(), Rule::all_chars => "a character".to_string(), Rule::array => "an array of values".to_string(), Rule::array_filter => "an array of values with an optional filter".to_string(), Rule::string_array => "an array of strings".to_string(), Rule::basic_val => "a value".to_string(), Rule::basic_op => "a mathematical operator".to_string(), Rule::comparison_op => "a comparison operator".to_string(), Rule::boolean => "`true` or `false`".to_string(), Rule::ident => "an identifier (must start with a-z)".to_string(), Rule::dotted_ident => "a dotted identifier (identifiers separated by `.`)".to_string(), Rule::dotted_square_bracket_ident => "a square bracketed identifier (identifiers separated by `.` or `[]`s)".to_string(), Rule::square_brackets => "an identifier, string or integer inside `[]`s".to_string(), Rule::basic_expr_filter => "an expression with an optional filter".to_string(), Rule::comparison_val => "a comparison value".to_string(), Rule::basic_expr | Rule::comparison_expr => "an expression".to_string(), Rule::logic_val => "a value that can be negated".to_string(), Rule::logic_expr => "any expressions".to_string(), Rule::fn_call => "a function call".to_string(), Rule::kwarg => "a keyword argument: `key=value` where `value` can be any expressions".to_string(), Rule::kwargs => "a list of keyword arguments: `key=value` where `value` can be any expressions and separated by `,`".to_string(), Rule::op_or => "`or`".to_string(), Rule::op_and => "`and`".to_string(), Rule::op_not => "`not`".to_string(), Rule::op_lte => "`<=`".to_string(), Rule::op_gte => "`>=`".to_string(), Rule::op_lt => "`<`".to_string(), Rule::op_gt => "`>`".to_string(), Rule::op_ineq => "`!=`".to_string(), Rule::op_eq => "`==`".to_string(), Rule::op_plus => "`+`".to_string(), Rule::op_minus => "`-`".to_string(), Rule::op_times => "`*`".to_string(), Rule::op_slash => "`/`".to_string(), Rule::op_modulo => "`%`".to_string(), Rule::filter => "a filter".to_string(), Rule::test => "a test".to_string(), Rule::test_not => "a negated test".to_string(), Rule::test_call => "a test call".to_string(), Rule::test_arg => "a test argument (any expressions including arrays)".to_string(), Rule::test_args => "a list of test arguments (any expression including arrays)".to_string(), Rule::macro_fn | Rule::macro_fn_wrapper => "a macro function".to_string(), Rule::macro_call => "a macro function call".to_string(), Rule::macro_def_arg => { "an argument name with an optional default literal value: `id`, `key=1`".to_string() } Rule::macro_def_args => { "a list of argument names with an optional default literal value: `id`, `key=1`".to_string() } Rule::endmacro_tag => "`{% endmacro %}`".to_string(), Rule::macro_content => "the macro content".to_string(), Rule::filter_section_content => "the filter section content".to_string(), Rule::set_tag => "a `set` tag`".to_string(), Rule::set_global_tag => "a `set_global` tag`".to_string(), Rule::block_content | Rule::content | Rule::for_content => { "some content".to_string() }, Rule::text => "some text".to_string(), // Pest will error an unexpected tag as Rule::tag_start // and just showing `{%` is not clear as some other valid // tags will also start with `{%` Rule::tag_start => "tag".to_string(), Rule::tag_end => "`%}` or `-%}`".to_string(), Rule::super_tag => "`{{ super() }}`".to_string(), Rule::raw_tag => "`{% raw %}`".to_string(), Rule::raw_text => "some raw text".to_string(), Rule::raw => "a raw block (`{% raw %}...{% endraw %}`".to_string(), Rule::endraw_tag => "`{% endraw %}`".to_string(), Rule::ignore_missing => "ignore missing mark for include tag".to_string(), Rule::include_tag => r#"an include tag (`{% include "..." %}`)"#.to_string(), Rule::comment_tag => "a comment tag (`{#...#}`)".to_string(), Rule::comment_text => "the context of a comment (`{# ... #}`)".to_string(), Rule::variable_tag => "a variable tag (`{{ ... }}`)".to_string(), Rule::filter_tag | Rule::filter_section => { "a filter section (`{% filter something %}...{% endfilter %}`)".to_string() } Rule::for_tag | Rule::forloop => { "a forloop (`{% for i in something %}...{% endfor %}".to_string() }, Rule::endfilter_tag => "an endfilter tag (`{% endfilter %}`)".to_string(), Rule::endfor_tag => "an endfor tag (`{% endfor %}`)".to_string(), Rule::if_tag | Rule::content_if | Rule::block_if | Rule::macro_if | Rule::for_if | Rule::filter_section_if => { "an `if` tag".to_string() } Rule::elif_tag => "an `elif` tag".to_string(), Rule::else_tag => "an `else` tag".to_string(), Rule::endif_tag => "an endif tag (`{% endif %}`)".to_string(), Rule::WHITESPACE => "whitespace".to_string(), Rule::variable_start => "a variable start (`{{`)".to_string(), Rule::variable_end => "a variable end (`}}`)".to_string(), Rule::comment_start => "a comment start (`{#`)".to_string(), Rule::comment_end => "a comment end (`#}`)".to_string(), Rule::block_start => "`{{`, `{%` or `{#`".to_string(), Rule::import_macro_tag => r#"an import macro tag (`{% import "filename" as namespace %}`"#.to_string(), Rule::block | Rule::block_tag => r#"a block tag (`{% block block_name %}`"#.to_string(), Rule::endblock_tag => r#"an endblock tag (`{% endblock block_name %}`"#.to_string(), Rule::macro_definition | Rule::macro_tag => r#"a macro definition tag (`{% macro my_macro() %}`"#.to_string(), Rule::extends_tag => r#"an extends tag (`{% extends "myfile" %}`"#.to_string(), Rule::template => "a template".to_string(), Rule::break_tag => "a break tag".to_string(), Rule::continue_tag => "a continue tag".to_string(), Rule::top_imports => "top imports".to_string(), Rule::in_cond => "a `in` condition".to_string(), Rule::in_cond_container => "a `in` condition container: a string, an array or an ident".to_string(), } }); return Err(Error::msg(fancy_e)); } }; let mut nodes = vec![]; // We must have at least a `template` pair if we got there for p in pairs.next().unwrap().into_inner() { match p.as_rule() { Rule::extends_tag => nodes.push(parse_extends(p)), Rule::import_macro_tag => nodes.push(parse_import_macro(p)), Rule::content => nodes.extend(parse_content(p)?), Rule::macro_definition => nodes.push(parse_macro_definition(p)?), Rule::comment_tag => (), Rule::EOI => (), _ => unreachable!("unknown tpl rule: {:?}", p.as_rule()), } } Ok(nodes) } tera-1.20.0/src/parser/tera.pest000064400000000000000000000237361046102023000145650ustar 00000000000000// More about pest syntax https://pest.rs/book/grammars/syntax.html // Built-in rules (WHITESPACE, ANY, SOI, DOI and others) https://pest.rs/book/grammars/built-ins.html // ----------------------------------------------- WHITESPACE = _{ " " | "\t" | "\r" | "\n" } /// LITERALS int = @{ "-" ? ~ ("0" | '1'..'9' ~ '0'..'9' * ) } float = @{ "-" ? ~ ( "0" ~ "." ~ '0'..'9' + | '1'..'9' ~ '0'..'9' * ~ "." ~ '0'..'9' + ) } // matches anything between 2 double quotes double_quoted_string = @{ "\"" ~ (!("\"") ~ ANY)* ~ "\""} // matches anything between 2 single quotes single_quoted_string = @{ "\'" ~ (!("\'") ~ ANY)* ~ "\'"} // matches anything between 2 backquotes\backticks backquoted_quoted_string = @{ "`" ~ (!("`") ~ ANY)* ~ "`"} string = @{ double_quoted_string | single_quoted_string | backquoted_quoted_string } boolean = { "true" | "false" | "True" | "False" } // ----------------------------------------------- /// OPERATORS op_or = @{ "or" ~ WHITESPACE } op_and = @{ "and" ~ WHITESPACE } op_not = @{ "not" ~ WHITESPACE } op_lte = { "<=" } op_gte = { ">=" } op_lt = { "<" } op_gt = { ">" } op_eq = { "==" } op_ineq = { "!=" } op_plus = { "+" } op_minus = { "-" } op_times = { "*" } op_slash = { "/" } op_modulo = { "%" } // ------------------------------------------------- /// Idents all_chars = _{'a'..'z' | 'A'..'Z' | "_" | '0'..'9'} // Used everywhere where an ident is used, except when accessing // data from the context. // Eg block name, argument name, macro name etc ident = @{ ('a'..'z' | 'A'..'Z' | "_") ~ all_chars* } // The context_ident used to get data from the context. // Same as ident but allows `.` in it dotted_ident = @{ ('a'..'z' | 'A'..'Z' | "_") ~ all_chars* ~ ("." ~ all_chars+)* } square_brackets = @{ "[" ~ (int | string | dotted_square_bracket_ident) ~ "]" } dotted_square_bracket_ident = @{ dotted_ident ~ ( ("." ~ all_chars+) | square_brackets )* } string_concat = { (fn_call | float | int | string | dotted_square_bracket_ident) ~ ("~" ~ (fn_call | float | int | string | dotted_square_bracket_ident))+ } // ---------------------------------------------------- /// EXPRESSIONS /// We'll use precedence climbing on those in the parser phase // boolean first so they are not caught as identifiers basic_val = _{ boolean | test_not | test | macro_call | fn_call | dotted_square_bracket_ident | float | int } basic_op = _{ op_plus | op_minus | op_times | op_slash | op_modulo } basic_expr = { ("(" ~ basic_expr ~ ")" | basic_val) ~ (basic_op ~ ("(" ~ basic_expr ~ ")" | basic_val))* } basic_expr_filter = !{ basic_expr ~ filter* } string_expr_filter = !{ (string_concat | string) ~ filter* } comparison_val = { basic_expr_filter ~ (basic_op ~ basic_expr_filter)* } comparison_op = _{ op_lte | op_gte | op_gt | op_lt | op_eq | op_ineq } comparison_expr = { (string_expr_filter | comparison_val) ~ (comparison_op ~ (string_expr_filter | comparison_val))? } // The `in` operator in_cond_container = {string_expr_filter | array_filter | dotted_square_bracket_ident} in_cond = !{ (string_expr_filter | basic_expr_filter) ~ op_not? ~ "in" ~ in_cond_container } logic_val = !{ op_not? ~ (in_cond | comparison_expr) | "(" ~ logic_expr ~ ")" } logic_expr = !{ logic_val ~ ((op_or | op_and) ~ logic_val)* } array = !{ "[" ~ (logic_val ~ ",")* ~ logic_val? ~ "]"} array_filter = !{ array ~ filter* } string_array = !{ "[" ~ (string ~ ",")* ~ string? ~ "]"} // ---------------------------------------------------- /// FUNCTIONS & FILTERS // A keyword argument: something=10, something="a value", something=1+10 etc kwarg = { ident ~ "=" ~ (logic_expr | array_filter) } kwargs = _{ kwarg ~ ("," ~ kwarg )* ~ ","? } fn_call = !{ ident ~ "(" ~ kwargs? ~ ")" } filter = { "|" ~ (fn_call | ident) } // ------------------------------------------------------ /// MACROS // A macro argument can have default value, only a literal though macro_def_arg = ${ (ident ~ "=" ~ (boolean | string | float | int)) | ident } macro_def_args = _{ macro_def_arg ~ ("," ~ macro_def_arg)* } macro_fn = _{ ident ~ "(" ~ macro_def_args? ~ ")" } macro_fn_wrapper = !{ macro_fn } macro_call = { ident ~ "::" ~ ident ~ "(" ~ kwargs? ~ ")" } // ------------------------------------------------------- /// TESTS // It's a bit weird that tests are the only thing in Tera not using kwargs // but at the same time it's one arg most of the time so... test_arg = { logic_expr | array_filter } test_args = _{ test_arg ~ ("," ~ test_arg)* } test_call = !{ ident ~ ("(" ~ test_args ~ ")")? } test_not = { dotted_square_bracket_ident ~ "is" ~ "not" ~ test_call } test = { dotted_square_bracket_ident ~ "is" ~ test_call } // ------------------------------------------------------- /// TERA // All the blocks that Tera recognises variable_start = { "{{-" | "{{" } variable_end = { "-}}" | "}}" } // whitespace control tag_start = { "{%-" | "{%" } tag_end = { "-%}" | "%}" } comment_start = { "{#-" | "{#" } comment_end = { "-#}" | "#}" } block_start = _{ variable_start | tag_start | comment_start } comment_text = ${ (!(comment_end) ~ ANY)+ } // Tag marks ignore_missing = { "ignore" ~ WHITESPACE* ~ "missing" } // Actual tags include_tag = ${ tag_start ~ WHITESPACE* ~ "include" ~ WHITESPACE+ ~ (string | string_array) ~ WHITESPACE* ~ ignore_missing? ~ WHITESPACE* ~ tag_end } comment_tag = ${ comment_start ~ comment_text ~ comment_end } block_tag = ${ tag_start ~ WHITESPACE* ~ "block" ~ WHITESPACE+ ~ ident ~ WHITESPACE* ~ tag_end } macro_tag = ${ tag_start ~ WHITESPACE* ~ "macro" ~ WHITESPACE+ ~ macro_fn_wrapper ~ WHITESPACE* ~ tag_end } if_tag = ${ tag_start ~ WHITESPACE* ~ "if" ~ WHITESPACE+ ~ logic_expr ~ WHITESPACE* ~ tag_end } elif_tag = ${ tag_start ~ WHITESPACE* ~ "elif" ~ WHITESPACE+ ~ logic_expr ~ WHITESPACE* ~ tag_end } else_tag = !{ tag_start ~ "else" ~ tag_end } for_tag = ${ tag_start ~ WHITESPACE* ~ "for"~ WHITESPACE+ ~ ident ~ ("," ~ WHITESPACE* ~ ident)? ~ WHITESPACE+ ~ "in" ~ WHITESPACE+ ~ (basic_expr_filter | array_filter) ~ WHITESPACE* ~ tag_end } filter_tag = ${ tag_start ~ WHITESPACE* ~ "filter" ~ WHITESPACE+ ~ (fn_call | ident) ~ WHITESPACE* ~ tag_end } set_tag = ${ tag_start ~ WHITESPACE* ~ "set" ~ WHITESPACE+ ~ ident ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ (logic_expr | array_filter) ~ WHITESPACE* ~ tag_end } set_global_tag = ${ tag_start ~ WHITESPACE* ~ "set_global" ~ WHITESPACE+ ~ ident ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ (logic_expr | array_filter) ~ WHITESPACE* ~ tag_end } endblock_tag = !{ tag_start ~ "endblock" ~ ident? ~ tag_end } endmacro_tag = !{ tag_start ~ "endmacro" ~ ident? ~ tag_end } endif_tag = !{ tag_start ~ "endif" ~ tag_end } endfor_tag = !{ tag_start ~ "endfor" ~ tag_end } endfilter_tag = !{ tag_start ~ "endfilter" ~ tag_end } break_tag = !{ tag_start ~ "break" ~ tag_end } continue_tag = !{ tag_start ~ "continue" ~ tag_end } variable_tag = !{ variable_start ~ (logic_expr | array_filter) ~ variable_end } super_tag = !{ variable_start ~ "super()" ~ variable_end } text = ${ (!(block_start) ~ ANY)+ } raw_tag = !{ tag_start ~ "raw" ~ tag_end } endraw_tag = !{ tag_start ~ "endraw" ~ tag_end } raw_text = ${ (!endraw_tag ~ ANY)* } raw = ${ raw_tag ~ raw_text ~ endraw_tag } filter_section = ${ filter_tag ~ filter_section_content* ~ endfilter_tag } forloop = ${ for_tag ~ for_content* ~ (else_tag ~ for_content*)* ~ endfor_tag } macro_if = ${ if_tag ~ macro_content* ~ (elif_tag ~ macro_content*)* ~ (else_tag ~ macro_content*)? ~ endif_tag } block_if = ${ if_tag ~ block_content* ~ (elif_tag ~ block_content*)* ~ (else_tag ~ block_content*)? ~ endif_tag } for_if = ${ if_tag ~ for_content* ~ (elif_tag ~ for_content*)* ~ (else_tag ~ for_content*)? ~ endif_tag } filter_section_if = ${ if_tag ~ filter_section_content* ~ (elif_tag ~ filter_section_content*)* ~ (else_tag ~ filter_section_content*)? ~ endif_tag } content_if = ${ if_tag ~ content* ~ (elif_tag ~ content*)* ~ (else_tag ~ content*)? ~ endif_tag } block = ${ block_tag ~ block_content* ~ endblock_tag } macro_definition = ${ macro_tag ~ macro_content* ~ endmacro_tag } filter_section_content = @{ include_tag | variable_tag | comment_tag | set_tag | set_global_tag | block | forloop | filter_section_if | raw | filter_section | text } // smaller sets of allowed content in macros macro_content = @{ include_tag | variable_tag | comment_tag | set_tag | set_global_tag | macro_if | forloop | filter_section | raw | text } // smaller set of allowed content in block block_content = @{ include_tag | super_tag | variable_tag | comment_tag | set_tag | set_global_tag | block | block_if | forloop | filter_section | raw | text } // set of allowed content inside for loops for_content = @{ include_tag | variable_tag | comment_tag | set_tag | set_global_tag | for_if | forloop | break_tag | continue_tag | filter_section | raw | text } content = @{ include_tag | variable_tag | comment_tag | set_tag | set_global_tag | block | content_if | forloop | filter_section | raw | text } extends_tag = ${ WHITESPACE* ~ tag_start ~ WHITESPACE* ~ "extends" ~ WHITESPACE+ ~ string ~ WHITESPACE* ~ tag_end ~ WHITESPACE* } import_macro_tag = ${ WHITESPACE* ~ tag_start ~ WHITESPACE* ~ "import" ~ WHITESPACE+ ~ string ~ WHITESPACE+ ~ "as" ~ WHITESPACE+ ~ ident ~ WHITESPACE* ~ tag_end ~ WHITESPACE* } top_imports = _{ (extends_tag ~ import_macro_tag*) | (import_macro_tag+ ~ extends_tag?) } // top level rule template = ${ SOI ~ comment_tag* ~ top_imports? ~ (content | macro_definition)* // macro_definition must be top-level ~ EOI } tera-1.20.0/src/parser/tests/errors.rs000064400000000000000000000157321046102023000157560ustar 00000000000000use crate::parser::parse; fn assert_err_msg(input: &str, needles: &[&str]) { let res = parse(input); assert!(res.is_err()); let err = res.unwrap_err(); let err_msg = err.to_string(); println!("{}", err_msg); println!("Looking for:"); for needle in needles { println!("{}", needle); assert!(err_msg.contains(needle)); } } #[test] fn invalid_number() { assert_err_msg( "{{ 1.2.2 }}", &[ "1:7", "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" ], ); } #[test] fn invalid_op() { assert_err_msg("{{ 1.2 >+ 3 }}", &["1:9", "expected an expression"]); } #[test] fn wrong_start_block() { assert_err_msg( "{{ if true %}", &[ "1:7", "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" ], ); } #[test] fn wrong_end_block() { assert_err_msg( "{{ hey %}", &[ "1:9", "expected an integer, a float, `true` or `false`, an identifier (must start with a-z), a square bracketed identifier (identifiers separated by `.` or `[]`s), or an expression" ], ); } #[test] fn unterminated_variable_block() { assert_err_msg( "{{ hey", &[ "1:7", "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" ], ); } #[test] fn unterminated_string() { assert_err_msg(r#"{{ "hey }}"#, &["1:4", "expected a value that can be negated"]); } #[test] fn unterminated_if_tag() { assert_err_msg(r#"{% if true %}sd"#, &["1:16", r#"expected tag or some content"#]); } #[test] fn unterminated_filter_section() { assert_err_msg( r#"{% filter uppercase %}sd"#, &["1:25", r#"expected tag or the filter section content"#], ); } #[test] fn invalid_filter_section_missing_name() { assert_err_msg( r#"{% filter %}sd{% endfilter %}"#, &["1:11", "expected an identifier (must start with a-z)"], ); } #[test] fn invalid_macro_content() { assert_err_msg( r#" {% macro input(label, type) %} {% macro nested() %} {% endmacro nested %} {% endmacro input %} "#, &["3:5", "unexpected tag; expected `{% endmacro %}` or the macro content"], ); } #[test] fn invalid_macro_not_toplevel() { assert_err_msg( r#" {% if val %} {% macro input(label, type) %} {% endmacro input %} {% endif %} "#, &["3:5", "unexpected tag; expected an `elif` tag, an `else` tag, an endif tag (`{% endif %}`), or some content"], ); } #[test] fn invalid_macro_default_arg_value() { assert_err_msg( r#" {% macro input(label=something) %} {% endmacro input %} "#, &["2:22", "expected an integer, a float, a string, or `true` or `false`"], ); } #[test] fn invalid_elif() { assert_err_msg( r#" {% if true %} {% else %} {% elif false %} {% endif %} "#, &["4:1", "unexpected tag; expected an endif tag (`{% endif %}`) or some content"], ); } #[test] fn invalid_else() { assert_err_msg( r#" {% if true %} {% else %} {% else %} {% endif %} "#, &["4:1", "unexpected tag; expected an endif tag (`{% endif %}`) or some content"], ); } #[test] fn invalid_extends_position() { assert_err_msg( r#" hello {% extends "hey.html" %} "#, &["3:1", "unexpected tag; expected end of input, a macro definition tag (`{% macro my_macro() %}`, or some content"], ); } #[test] fn invalid_operator() { assert_err_msg( "{{ hey =! }}", &[ "1:8", "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" ], ); } #[test] fn missing_expression_with_not() { assert_err_msg("{% if not %}", &["1:11", "expected an expression"]); } #[test] fn missing_expression_in_if() { assert_err_msg("{% if %}", &["1:7", "expected a value that can be negated"]); } #[test] fn missing_container_name_in_forloop() { assert_err_msg("{% for i in %}", &["1:13", "expected an expression or an array of values"]); } #[test] fn missing_variable_name_in_set() { assert_err_msg("{% set = 1 %}", &["1:8", "expected an identifier (must start with a-z)"]); } #[test] fn missing_value_in_set() { assert_err_msg( "{% set a = %}", &["1:13", "expected a value that can be negated or an array of values"], ); } #[test] fn unterminated_fn_call() { assert_err_msg("{{ a | slice( }}", &["1:15", "expected an identifier (must start with a-z)"]); } #[test] fn invalid_fn_call_missing_value() { assert_err_msg( "{{ a | slice(start=) }}", &["1:20", "expected a value that can be negated or an array of values"], ); } #[test] fn unterminated_macro_call() { assert_err_msg("{{ my::macro( }}", &["1:15", "expected an identifier (must start with a-z)"]); } #[test] fn invalid_macro_call() { assert_err_msg( "{{ my:macro() }}", &[ "1:6", "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" ], ); } #[test] fn unterminated_include() { assert_err_msg("{% include %}", &["1:12", "expected a string"]); } #[test] fn invalid_include_no_string() { assert_err_msg("{% include 1 %}", &["1:12", "expected a string"]); } #[test] fn unterminated_extends() { assert_err_msg("{% extends %}", &["1:12", "expected a string"]); } #[test] fn invalid_extends_no_string() { assert_err_msg("{% extends 1 %}", &["1:12", "expected a string"]); } #[test] fn invalid_import_macros_missing_filename() { assert_err_msg("{% import as macros %}", &["1:11", "expected a string"]); } #[test] fn invalid_import_macros_missing_namespace() { assert_err_msg( r#"{% import "hello" as %}"#, &["1:22", "expected an identifier (must start with a-z)"], ); } #[test] fn invalid_block_missing_name() { assert_err_msg(r#"{% block %}"#, &["1:10", "expected an identifier (must start with a-z)"]); } #[test] fn unterminated_test() { assert_err_msg( r#"{% if a is odd( %}"#, &["1:17", "a test argument (any expressions including arrays)"], ); } #[test] fn invalid_test_argument() { assert_err_msg( r#"{% if a is odd(key=1) %}"#, &[ "1:19", "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, or a filter" ], ); } #[test] fn unterminated_raw_tag() { assert_err_msg(r#"{% raw %}sd"#, &["1:12", "expected tag"]); } #[test] fn invalid_break_outside_loop() { assert_err_msg(r#"{% break %}"#, &["1:1", "{% break %}", "expected a template"]); } #[test] fn invalid_continue_outside_loop() { assert_err_msg(r#"{% continue %}"#, &["1:1", "{% continue %}", "expected a template"]); } tera-1.20.0/src/parser/tests/lexer.rs000064400000000000000000000377641046102023000155720ustar 00000000000000use pest::Parser; use crate::parser::{Rule, TeraParser}; macro_rules! assert_lex_rule { ($rule: expr, $input: expr) => { let res = TeraParser::parse($rule, $input); println!("{:?}", $input); println!("{:#?}", res); if res.is_err() { println!("{}", res.unwrap_err()); panic!(); } assert!(res.is_ok()); assert_eq!(res.unwrap().last().unwrap().as_span().end(), $input.len()); }; } #[test] fn lex_boolean() { let inputs = vec!["true", "false", "True", "False"]; for i in inputs { assert_lex_rule!(Rule::boolean, i); } } #[test] fn lex_int() { let inputs = vec!["-10", "0", "100", "250000"]; for i in inputs { assert_lex_rule!(Rule::int, i); } } #[test] fn lex_float() { let inputs = vec!["123.5", "123.5", "0.1", "-1.1"]; for i in inputs { assert_lex_rule!(Rule::float, i); } } #[test] fn lex_string() { let inputs = vec![ "\"Blabla\"", "\"123\"", "\'123\'", "\'This is still a string\'", "`this is backquted`", "`and this too`", ]; for i in inputs { assert_lex_rule!(Rule::string, i); } } #[test] fn lex_ident() { let inputs = vec!["hello", "hello_", "hello_1", "HELLO", "_1"]; for i in inputs { assert_lex_rule!(Rule::ident, i); } assert!(TeraParser::parse(Rule::ident, "909").is_err()); } #[test] fn lex_dotted_ident() { let inputs = vec![ "hello", "hello_", "hello_1", "HELLO", "_1", "hey.ho", "h", "ho", "hey.ho.hu", "hey.0", "h.u", ]; for i in inputs { assert_lex_rule!(Rule::dotted_ident, i); } let invalid_inputs = vec![".", "9.w"]; for i in invalid_inputs { assert!(TeraParser::parse(Rule::dotted_ident, i).is_err()); } } #[test] fn lex_dotted_square_bracket_ident() { let inputs = vec![ "hey.ho.hu", "hey.0", "h.u.x.0", "hey['ho'][\"hu\"]", "hey[0]", "h['u'].x[0]", "hey[a[0]]", ]; for i in inputs { assert_lex_rule!(Rule::dotted_square_bracket_ident, i); } let invalid_inputs = vec![".", "9.w"]; for i in invalid_inputs { assert!(TeraParser::parse(Rule::dotted_square_bracket_ident, i).is_err()); } } #[test] fn lex_string_concat() { let inputs = vec![ "'hello' ~ `hey`", "'hello' ~ 1", "'hello' ~ 3.18", "1 ~ 'hello'", "3.18 ~ 'hello'", "'hello' ~ ident", "ident ~ 'hello'", "'hello' ~ ident[0]", "'hello' ~ a_function()", "a_function() ~ 'hello'", r#"'hello' ~ "hey""#, r#"a_string ~ " world""#, "'hello' ~ ident ~ `ho`", ]; for i in inputs { assert_lex_rule!(Rule::string_concat, i); } } #[test] fn lex_array() { let inputs = vec![ "[]", "[1,2,3]", "[1, 2,3,]", "[1 + 1, 2,3 * 2,]", "[\"foo\", \"bar\"]", "[1,true,'string', 0.5, hello(), macros::hey(arg=1)]", ]; for i in inputs { assert_lex_rule!(Rule::array, i); } } #[test] fn lex_basic_expr() { let inputs = vec![ "admin", "true", "macros::something()", "something()", "a is defined", "a is defined(2)", "1 + 1", "1 + counts", "1 + counts.first", "1 + 2 + 3 * 9/2 + 2.1", "(1 + 2 + 3) * 9/2 + 2.1", "10 * 2 % 5", ]; for i in inputs { assert_lex_rule!(Rule::basic_expr, i); } } #[test] fn lex_basic_expr_with_filter() { let inputs = vec![ "admin | hello", "true | ho", "macros::something() | hey", "something() | hey", "a is defined | ho", "a is defined(2) | ho", "1 + 1 | round", "1 + counts | round", "1 + counts.first | round", "1 + 2 + 3 * 9/2 + 2.1 | round", "(1 + 2 + 3) * 9/2 + 2.1 | round", "10 * 2 % 5 | round", ]; for i in inputs { assert_lex_rule!(Rule::basic_expr_filter, i); } } #[test] fn lex_string_expr_with_filter() { let inputs = vec![ r#""hey" | capitalize"#, r#""hey""#, r#""hey" ~ 'ho' | capitalize"#, r#""hey" ~ ho | capitalize"#, r#"ho ~ ho ~ ho | capitalize"#, r#"ho ~ 'ho' ~ ho | capitalize"#, r#"ho ~ 'ho' ~ ho"#, ]; for i in inputs { assert_lex_rule!(Rule::string_expr_filter, i); } } #[test] fn lex_comparison_val() { let inputs = vec![ // all the basic expr still work "admin", "true", "macros::something()", "something()", "a is defined", "a is defined(2)", "1 + 1", "1 + counts", "1 + counts.first", "1 + 2 + 3 * 9/2 + 2.1", "(1 + 2 + 3) * 9/2 + 2.1", "10 * 2 % 5", // but now ones with filters also work "admin | upper", "admin | upper | round", "admin | upper | round(var=2)", "1.5 + a | round(var=2)", // and maths after filters is ok "a | length - 1", "1.5 + a | round - 1", "1.5 + a | round - (1 + 1.5) | round", "1.5 + a | round - (1 + 1.5) | round", ]; for i in inputs { assert_lex_rule!(Rule::comparison_val, i); } } #[test] fn lex_in_cond() { let inputs = vec![ "a in b", "1 in b", "'b' in b", "'b' in b", "a in request.path", "'index.html' in request.build_absolute_uri", "a in [1, 2, 3]", "a | capitalize in [1, 2, 3]", "a | capitalize in [1, 'hey']", "a | capitalize in [ho, 1, 'hey']", "'e' in 'hello'", "'e' in 'hello' | capitalize", "e in 'hello'", ]; for i in inputs { assert_lex_rule!(Rule::in_cond, i); } } #[test] fn lex_comparison_expr() { let inputs = vec![ "1.5 + a | round(var=2) > 10", "1.5 + a | round(var=2) > a | round", "a == b", "a + 1 == b", "a != b", "a % 2 == 0", "a == 'admin'", "a != 'admin'", "a == 'admin' | capitalize", "a != 'admin' | capitalize", "a > b", "a >= b", "a < b", "a <= b", "true > false", ]; for i in inputs { assert_lex_rule!(Rule::comparison_expr, i); } } #[test] fn lex_logic_val() { let inputs = vec![ // all the basic expr still work "admin", "true", "macros::something()", "something()", r#""hey""#, "a is defined", "a is defined(2)", "a is not defined", "1 + 1", "1 + counts", "1 + counts.first", "1 + 2 + 3 * 9/2 + 2.1", "(1 + 2 + 3) * 9/2 + 2.1", "10 * 2 % 5", // filters still work "admin | upper", "admin | upper | round", "admin | upper | round(var=2)", "1.5 + a | round(var=2)", // but now we can negate things "not true", "not admin", "not num + 1 == 0", ]; for i in inputs { assert_lex_rule!(Rule::logic_val, i); } } #[test] fn lex_logic_expr() { let inputs = vec![ "1.5 + a | round(var=2) > 10 and admin", "1.5 + a | round(var=2) > a | round or true", "1 > 0 and 2 < 3", ]; for i in inputs { assert_lex_rule!(Rule::logic_expr, i); } } #[test] fn lex_kwarg() { let inputs = vec![ "hello=1", "hello=1+1", "hello=[]", "hello=[true, false]", "hello1=true", "hello=name", "hello=name|filter", "hello=name|filter(with_arg=true)", ]; for i in inputs { assert_lex_rule!(Rule::kwarg, i); } } #[test] fn lex_kwargs() { let inputs = vec![ "hello=1", "hello=1+1,hey=1", "hello1=true,name=name,admin=true", "hello=name", "hello=name|filter,id=1", "hello=name|filter(with_arg=true),id=1", ]; for i in inputs { assert_lex_rule!(Rule::kwargs, i); } } #[test] fn lex_fn_call() { let inputs = vec![ "fn(hello=1)", "fn(hello=1+1,hey=1)", "fn(hello1=true,name=name,admin=true)", "fn(hello=name)", "fn(hello=name,)", "fn(\n hello=name,\n)", "fn(hello=name|filter,id=1)", ]; for i in inputs { assert_lex_rule!(Rule::fn_call, i); } } #[test] fn lex_filter() { let inputs = vec![ "|attr", "|attr()", "|attr(key=1)", "|attr(key=1, more=true)", "|attr(key=1,more=true)", ]; for i in inputs { assert_lex_rule!(Rule::filter, i); } } #[test] fn lex_macro_definition() { let inputs = vec![ "hello()", "hello(name, admin)", "hello(name, admin=1)", "hello(name=\"bob\", admin)", "hello(name=\"bob\",admin=true)", ]; for i in inputs { // The () are not counted as tokens for some reasons so can't use the macro assert!(TeraParser::parse(Rule::macro_fn, i).is_ok()); } } #[test] fn lex_test() { let inputs = vec!["a is defined", "a is defined()", "a is divisibleby(2)", "a is in([1, 2, something])"]; for i in inputs { // The () are not counted as tokens for some reasons so can't use the macro assert!(TeraParser::parse(Rule::test, i).is_ok()); } } #[test] fn lex_include_tag() { assert!(TeraParser::parse(Rule::include_tag, "{% include \"index.html\" %}").is_ok()); assert!(TeraParser::parse(Rule::include_tag, "{% include [\"index.html\"] %}").is_ok()); assert!(TeraParser::parse(Rule::include_tag, "{% include [\"index.html\"] ignore missing %}") .is_ok()); } #[test] fn lex_import_macro_tag() { assert!(TeraParser::parse(Rule::import_macro_tag, "{% import \"macros.html\" as macros %}",) .is_ok()); } #[test] fn lex_extends_tag() { assert!(TeraParser::parse(Rule::extends_tag, "{% extends \"index.html\" %}").is_ok()); } #[test] fn lex_comment_tag() { let inputs = vec![ "{# #comment# {{}} {%%} #}", "{# #comment# {{}} {%%} #}", "{#- #comment# {{}} {%%} #}", "{# #comment# {{}} {%%} -#}", "{#- #comment# {{}} {%%} -#}", ]; for i in inputs { assert_lex_rule!(Rule::comment_tag, i); } } #[test] fn lex_block_tag() { let inputs = vec!["{% block tag %}", "{% block my_block %}"]; for i in inputs { assert_lex_rule!(Rule::block_tag, i); } } #[test] fn lex_filter_tag() { let inputs = vec![ "{%- filter tag() %}", "{% filter foo(bar=baz) -%}", "{% filter foo(bar=42) %}", "{% filter foo(bar=baz,qux=quz) %}", "{% filter foo(bar=baz, qux=quz) %}", "{% filter foo ( bar=\"baz\", qux=42 ) %}", ]; for i in inputs { assert_lex_rule!(Rule::filter_tag, i); } } #[test] fn lex_macro_tag() { let inputs = vec![ "{%- macro tag() %}", "{% macro my_block(name) -%}", "{% macro my_block(name=42) %}", "{% macro foo ( bar=\"baz\", qux=42 ) %}", ]; for i in inputs { assert_lex_rule!(Rule::macro_tag, i); } } #[test] fn lex_if_tag() { let inputs = vec![ "{%- if name %}", "{% if true -%}", "{% if admin or show %}", "{% if 1 + 2 == 2 and true %}", "{% if 1 + 2 == 2 and admin is defined %}", ]; for i in inputs { assert_lex_rule!(Rule::if_tag, i); } } #[test] fn lex_elif_tag() { let inputs = vec![ "{%- elif name %}", "{% elif true -%}", "{% elif admin or show %}", "{% elif 1 + 2 == 2 and true %}", "{% elif 1 + 2 == 2 and admin is defined %}", ]; for i in inputs { assert_lex_rule!(Rule::elif_tag, i); } } #[test] fn lex_else_tag() { assert!(TeraParser::parse(Rule::else_tag, "{% else %}").is_ok()); } #[test] fn lex_for_tag() { let inputs = vec![ "{%- for a in array %}", "{% for a, b in object -%}", "{% for a, b in fn_call() %}", "{% for a in fn_call() %}", "{% for a in [] %}", "{% for a in [1,2,3,] %}", "{% for a,b in fn_call(with_args=true, name=name) %}", "{% for client in clients | slice(start=1, end=9) %}", ]; for i in inputs { assert_lex_rule!(Rule::for_tag, i); } } #[test] fn lex_break_tag() { assert!(TeraParser::parse(Rule::break_tag, "{% break %}").is_ok()); } #[test] fn lex_continue_tag() { assert!(TeraParser::parse(Rule::continue_tag, "{% continue %}").is_ok()); } #[test] fn lex_set_tag() { let inputs = vec![ "{%- set a = true %}", "{% set a = object -%}", "{% set a = [1,2,3, 'hey'] -%}", "{% set a = fn_call() %}", "{% set a = fn_call(with_args=true, name=name) %}", "{% set a = macros::fn_call(with_args=true, name=name) %}", "{% set a = var | caps %}", "{% set a = var +1 >= 2%}", ]; for i in inputs { assert_lex_rule!(Rule::set_tag, i); } } #[test] fn lex_set_global_tag() { let inputs = vec![ "{% set_global a = 1 %}", "{% set_global a = [1,2,3, 'hey'] -%}", "{% set_global a = another_var %}", "{% set_global a = another_var | filter %}", "{% set_global a = var +1 >= 2%}", "{%- set_global a = var +1 >= 2 -%}", ]; for i in inputs { assert_lex_rule!(Rule::set_global_tag, i); } } #[test] fn lex_variable_tag() { let inputs = vec![ "{{ a }}", "{{ a | caps }}", r#"{{ "hey" }}"#, r#"{{ 'hey' }}"#, r#"{{ `hey` }}"#, "{{ fn_call() }}", "{{ macros::fn() }}", "{{ name + 42 }}", "{{ loop.index + 1 }}", "{{ name is defined and name >= 42 }}", "{{ my_macros::macro1(hello=\"world\", foo=bar, hey=1+2) }}", "{{ 'hello' ~ `ho` }}", r#"{{ hello ~ `ho` }}"#, ]; for i in inputs { assert_lex_rule!(Rule::variable_tag, i); } } #[test] fn lex_content() { let inputs = vec![ "some text", "{{ name }}", "{# comment #}", "{% filter upper %}hey{% endfilter %}", "{% filter upper() %}hey{% endfilter %}", "{% raw %}{{ hey }}{% endraw %}", "{% for a in b %}{{a}}{% endfor %}", "{% if i18n %}世界{% else %}world{% endif %}", ]; for i in inputs { assert_lex_rule!(Rule::content, i); } } #[test] fn lex_template() { assert!(TeraParser::parse( Rule::template, "{# Greeter template #} Hello {% if i18n %}世界{% else %}world{% endif %} {% for country in countries %} {{ loop.index }}.{{ country }} {% endfor %}", ) .is_ok()); } #[test] fn lex_extends_with_imports() { let sample = r#" {% extends "base.html" %} {% import "macros/image.html" as image %} {% import "macros/masonry.html" as masonry %} {% import "macros/breadcrumb.html" as breadcrumb %} {% import "macros/ul_links.html" as ul_links %} {% import "macros/location.html" as location %} "#; assert_lex_rule!(Rule::template, sample); } // https://github.com/Keats/tera/issues/379 #[test] fn lex_requires_whitespace_between_things() { // All the ones below should fail parsing let inputs = vec![ "{% filterupper %}hey{% endfilter %}", "{% blockhey %}{%endblock%}", "{% macrohey() %}{%endmacro%}", "{% setident = 1 %}", "{% set_globalident = 1 %}", "{% extends'base.html' %}", "{% import 'macros/image.html' asimage %}", "{% import'macros/image.html' as image %}", "{% fora in b %}{{a}}{% endfor %}", "{% for a inb %}{{a}}{% endfor %}", "{% for a,bin c %}{{a}}{% endfor %}", "{% for a,b inc %}{{a}}{% endfor %}", "{% ifi18n %}世界{% else %}world{% endif %}", "{% if i18n %}世界{% eliftrue %}world{% endif %}", "{% include'base.html' %}", ]; for i in inputs { let res = TeraParser::parse(Rule::template, i); println!("{:?}", i); assert!(res.is_err()); } } tera-1.20.0/src/parser/tests/mod.rs000064400000000000000000000000631046102023000152100ustar 00000000000000mod errors; mod lexer; mod parser; mod whitespace; tera-1.20.0/src/parser/tests/parser.rs000064400000000000000000001022061046102023000157270ustar 00000000000000use std::collections::HashMap; use crate::parser::ast::*; use crate::parser::parse; #[test] fn parse_empty_template() { let ast = parse("").unwrap(); assert_eq!(ast.len(), 0); } #[test] fn parse_text() { let ast = parse("hello world").unwrap(); assert_eq!(ast[0], Node::Text("hello world".to_string())); } #[test] fn parse_text_with_whitespace() { let ast = parse(" hello world ").unwrap(); assert_eq!(ast[0], Node::Text(" hello world ".to_string())); } #[test] fn parse_include_tag() { let ast = parse("{% include \"index.html\" -%}").unwrap(); assert_eq!( ast[0], Node::Include(WS { left: false, right: true }, vec!["index.html".to_string()], false,), ); let ast = parse("{% include [\"custom/index.html\", \"index.html\"] ignore missing %}").unwrap(); assert_eq!( ast[0], Node::Include( WS { left: false, right: false }, vec!["custom/index.html".to_string(), "index.html".to_string()], true, ), ); } #[test] fn parse_extends() { let ast = parse("{% extends \"index.html\" -%}").unwrap(); assert_eq!(ast[0], Node::Extends(WS { left: false, right: true }, "index.html".to_string(),),); } #[test] fn parse_comments_before_extends() { let ast = parse("{# A comment #}{% extends \"index.html\" -%}").unwrap(); assert_eq!(ast[0], Node::Extends(WS { left: false, right: true }, "index.html".to_string(),),); } #[test] fn parse_import_macro() { let ast = parse("\n{% import \"macros.html\" as macros -%}").unwrap(); assert_eq!( ast[0], Node::ImportMacro( WS { left: false, right: true }, "macros.html".to_string(), "macros".to_string(), ), ); } #[test] fn parse_variable_with_whitespace_trimming() { let ast = parse("{{- id }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS { left: true, right: false }, Expr::new(ExprVal::Ident("id".to_string())) ), ); } #[test] fn parse_variable_tag_ident() { let ast = parse("{{ id }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock(WS::default(), Expr::new(ExprVal::Ident("id".to_string()))), ); } #[test] fn parse_variable_tag_ident_with_simple_filters() { let ast = parse("{{ arr | first | join(n=2) }}").unwrap(); let mut join_args = HashMap::new(); join_args.insert("n".to_string(), Expr::new(ExprVal::Int(2))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::with_filters( ExprVal::Ident("arr".to_string()), vec![ FunctionCall { name: "first".to_string(), args: HashMap::new() }, FunctionCall { name: "join".to_string(), args: join_args }, ], ) ) ); } #[test] fn parse_variable_tag_lit() { let ast = parse("{{ 2 }}{{ 3.18 }}{{ \"hey\" }}{{ true }}").unwrap(); assert_eq!(ast[0], Node::VariableBlock(WS::default(), Expr::new(ExprVal::Int(2)))); assert_eq!(ast[1], Node::VariableBlock(WS::default(), Expr::new(ExprVal::Float(3.18)))); assert_eq!( ast[2], Node::VariableBlock(WS::default(), Expr::new(ExprVal::String("hey".to_string()))), ); assert_eq!(ast[3], Node::VariableBlock(WS::default(), Expr::new(ExprVal::Bool(true)))); } #[test] fn parse_variable_tag_array_lit() { let ast = parse("{{ [1, 2, 3] }}").unwrap(); let mut join_args = HashMap::new(); join_args.insert("n".to_string(), Expr::new(ExprVal::Int(2))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Array(vec![ Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Int(2)), Expr::new(ExprVal::Int(3)) ]),) ) ); } #[test] fn parse_variable_tag_array_lit_with_filter() { let ast = parse("{{ [1, 2, 3] | length }}").unwrap(); let mut join_args = HashMap::new(); join_args.insert("n".to_string(), Expr::new(ExprVal::Int(2))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::with_filters( ExprVal::Array(vec![ Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Int(2)), Expr::new(ExprVal::Int(3)) ]), vec![FunctionCall { name: "length".to_string(), args: HashMap::new() },], ) ) ); } #[test] fn parse_variable_tag_lit_math_expression() { let ast = parse("{{ count + 1 * 2.5 }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Ident("count".to_string()))), operator: MathOperator::Add, rhs: Box::new(Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Int(1))), operator: MathOperator::Mul, rhs: Box::new(Expr::new(ExprVal::Float(2.5))), },))), },)) ), ); } #[test] fn parse_variable_tag_lit_math_expression_with_parentheses() { let ast = parse("{{ (count + 1) * 2.5 }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Ident("count".to_string()))), operator: MathOperator::Add, rhs: Box::new(Expr::new(ExprVal::Int(1))), },))), operator: MathOperator::Mul, rhs: Box::new(Expr::new(ExprVal::Float(2.5))), },)) ) ); } #[test] fn parse_variable_tag_lit_math_expression_with_parentheses_and_filter() { let ast = parse("{{ (count + 1) * 2.5 | round }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::with_filters( ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Ident("count".to_string()))), operator: MathOperator::Add, rhs: Box::new(Expr::new(ExprVal::Int(1))), },))), operator: MathOperator::Mul, rhs: Box::new(Expr::new(ExprVal::Float(2.5))), },), vec![FunctionCall { name: "round".to_string(), args: HashMap::new() },], ) ) ); } #[test] fn parse_variable_math_on_filter() { let ast = parse("{{ a | length - 1 }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::with_filters( ExprVal::Ident("a".to_string()), vec![FunctionCall { name: "length".to_string(), args: HashMap::new() },], )), operator: MathOperator::Sub, rhs: Box::new(Expr::new(ExprVal::Int(1))), },)) ) ); } #[test] fn parse_variable_tag_simple_logic_expression() { let ast = parse("{{ 1 > 2 }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new(ExprVal::Int(1))), operator: LogicOperator::Gt, rhs: Box::new(Expr::new(ExprVal::Int(2))), },)) ) ); } #[test] fn parse_variable_tag_math_and_logic_expression() { let ast = parse("{{ count + 1 * 2.5 and admin }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Ident("count".to_string()))), operator: MathOperator::Add, rhs: Box::new(Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Int(1))), operator: MathOperator::Mul, rhs: Box::new(Expr::new(ExprVal::Float(2.5))), },))), },))), operator: LogicOperator::And, rhs: Box::new(Expr::new(ExprVal::Ident("admin".to_string()))), },)) ) ); } #[test] fn parse_variable_tag_math_with_filters_and_logic_expression() { let ast = parse("{{ count + 1 * 2.5 | round and admin }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::with_filters( ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Ident("count".to_string()))), operator: MathOperator::Add, rhs: Box::new(Expr::new(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Int(1))), operator: MathOperator::Mul, rhs: Box::new(Expr::new(ExprVal::Float(2.5))), },))), },), vec![FunctionCall { name: "round".to_string(), args: HashMap::new() },], )), operator: LogicOperator::And, rhs: Box::new(Expr::new(ExprVal::Ident("admin".to_string()))), },)) ) ); } #[test] fn parse_variable_tag_simple_negated_expr() { let ast = parse("{{ not id }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock(WS::default(), Expr::new_negated(ExprVal::Ident("id".to_string()))) ); } #[test] fn parse_test() { let ast = parse("{{ a is divisibleby(2) }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Test(Test { ident: "a".to_string(), negated: false, name: "divisibleby".to_string(), args: vec![Expr::new(ExprVal::Int(2))] })) ) ); } #[test] fn parse_variable_tag_negated_expr() { let ast = parse("{{ not id and not true and not 1 + 1 }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new_negated(ExprVal::Ident("id".to_string()))), operator: LogicOperator::And, rhs: Box::new(Expr::new_negated(ExprVal::Bool(true))), },))), operator: LogicOperator::And, rhs: Box::new(Expr::new_negated(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Int(1))), operator: MathOperator::Add, rhs: Box::new(Expr::new(ExprVal::Int(1))), },))), },)) ) ); } #[test] fn parse_variable_tag_negated_expr_with_parentheses() { let ast = parse("{{ (not id or not true) and not 1 + 1 }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new_negated(ExprVal::Ident("id".to_string()))), operator: LogicOperator::Or, rhs: Box::new(Expr::new_negated(ExprVal::Bool(true))), },))), operator: LogicOperator::And, rhs: Box::new(Expr::new_negated(ExprVal::Math(MathExpr { lhs: Box::new(Expr::new(ExprVal::Int(1))), operator: MathOperator::Add, rhs: Box::new(Expr::new(ExprVal::Int(1))), },))), },)) ) ); } #[test] fn parse_variable_tag_simple_test() { let ast = parse("{{ id is defined }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Test(Test { ident: "id".to_string(), negated: false, name: "defined".to_string(), args: vec![], },)) ) ); } #[test] fn parse_variable_tag_simple_negated_test() { let ast = parse("{{ id is not defined }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Test(Test { ident: "id".to_string(), negated: true, name: "defined".to_string(), args: vec![], },)) ) ); } #[test] fn parse_variable_tag_test_as_expression() { let ast = parse("{{ user is defined and user.admin }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new(ExprVal::Test(Test { ident: "user".to_string(), negated: false, name: "defined".to_string(), args: vec![], },))), operator: LogicOperator::And, rhs: Box::new(Expr::new(ExprVal::Ident("user.admin".to_string()))), },)) ) ); } #[test] fn parse_variable_tag_macro_call() { let ast = parse("{{ macros::get_time(some=1) }}").unwrap(); let mut args = HashMap::new(); args.insert("some".to_string(), Expr::new(ExprVal::Int(1))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::MacroCall(MacroCall { namespace: "macros".to_string(), name: "get_time".to_string(), args, },)), ) ); } #[test] fn parse_allow_block_in_filter_section() { let ast = parse("{% filter upper %}{% block content %}Hello{% endblock %}{% endfilter %}").unwrap(); assert_eq!( ast[0], Node::FilterSection( WS::default(), FilterSection { filter: FunctionCall { name: "upper".to_owned(), args: HashMap::default() }, body: vec![Node::Block( WS::default(), Block { name: "content".to_owned(), body: vec![Node::Text("Hello".to_owned())] }, WS::default(), )], }, WS::default(), ) ); } // smoke test for array in kwargs #[test] fn parse_variable_tag_macro_call_with_array() { let ast = parse("{{ macros::get_time(some=[1, 2]) }}").unwrap(); let mut args = HashMap::new(); args.insert( "some".to_string(), Expr::new(ExprVal::Array(vec![Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Int(2))])), ); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::MacroCall(MacroCall { namespace: "macros".to_string(), name: "get_time".to_string(), args, },)) ) ); } // smoke test for array in kwargs #[test] fn parse_variable_tag_macro_call_with_array_with_filters() { let ast = parse("{{ macros::get_time(some=[1, 2] | reverse) }}").unwrap(); let mut args = HashMap::new(); args.insert( "some".to_string(), Expr::with_filters( ExprVal::Array(vec![Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Int(2))]), vec![FunctionCall { name: "reverse".to_string(), args: HashMap::new() }], ), ); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::MacroCall(MacroCall { namespace: "macros".to_string(), name: "get_time".to_string(), args, },)) ) ); } #[test] fn parse_variable_tag_macro_call_with_filter() { let ast = parse("{{ macros::get_time(some=1) | round }}").unwrap(); let mut args = HashMap::new(); args.insert("some".to_string(), Expr::new(ExprVal::Int(1))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::with_filters( ExprVal::MacroCall(MacroCall { namespace: "macros".to_string(), name: "get_time".to_string(), args, },), vec![FunctionCall { name: "round".to_string(), args: HashMap::new() },], ) ) ); } #[test] fn parse_variable_tag_global_function() { let ast = parse("{{ get_time(some=1) }}").unwrap(); let mut args = HashMap::new(); args.insert("some".to_string(), Expr::new(ExprVal::Int(1))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::FunctionCall(FunctionCall { name: "get_time".to_string(), args },)) ) ); } #[test] fn parse_in_condition() { let ast = parse("{{ b in c }}").unwrap(); let mut args = HashMap::new(); args.insert("some".to_string(), Expr::new(ExprVal::Int(1))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::In(In { lhs: Box::new(Expr::new(ExprVal::Ident("b".to_string()))), rhs: Box::new(Expr::new(ExprVal::Ident("c".to_string()))), negated: false, })) ) ); } #[test] fn parse_negated_in_condition() { let ast = parse("{{ b not in c }}").unwrap(); let mut args = HashMap::new(); args.insert("some".to_string(), Expr::new(ExprVal::Int(1))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::In(In { lhs: Box::new(Expr::new(ExprVal::Ident("b".to_string()))), rhs: Box::new(Expr::new(ExprVal::Ident("c".to_string()))), negated: true, })) ) ); } #[test] fn parse_variable_tag_global_function_with_filter() { let ast = parse("{{ get_time(some=1) | round | upper }}").unwrap(); let mut args = HashMap::new(); args.insert("some".to_string(), Expr::new(ExprVal::Int(1))); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::with_filters( ExprVal::FunctionCall(FunctionCall { name: "get_time".to_string(), args },), vec![ FunctionCall { name: "round".to_string(), args: HashMap::new() }, FunctionCall { name: "upper".to_string(), args: HashMap::new() }, ], ) ) ); } #[test] fn parse_comment_tag() { let ast = parse("{# hey #}").unwrap(); assert!(ast.is_empty()); } #[test] fn parse_set_tag_lit() { let ast = parse("{% set hello = \"hi\" %}").unwrap(); assert_eq!( ast[0], Node::Set( WS::default(), Set { key: "hello".to_string(), value: Expr::new(ExprVal::String("hi".to_string())), global: false, }, ) ); } #[test] fn parse_set_tag_macro_call() { let ast = parse("{% set hello = macros::something() %}").unwrap(); assert_eq!( ast[0], Node::Set( WS::default(), Set { key: "hello".to_string(), value: Expr::new(ExprVal::MacroCall(MacroCall { namespace: "macros".to_string(), name: "something".to_string(), args: HashMap::new(), },)), global: false, }, ) ); } #[test] fn parse_set_tag_fn_call() { let ast = parse("{% set hello = utcnow() %}").unwrap(); assert_eq!( ast[0], Node::Set( WS::default(), Set { key: "hello".to_string(), value: Expr::new(ExprVal::FunctionCall(FunctionCall { name: "utcnow".to_string(), args: HashMap::new(), },)), global: false, }, ) ); } #[test] fn parse_set_array() { let ast = parse("{% set hello = [1, true, 'hello'] %}").unwrap(); assert_eq!( ast[0], Node::Set( WS::default(), Set { key: "hello".to_string(), value: Expr::new(ExprVal::Array(vec![ Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Bool(true)), Expr::new(ExprVal::String("hello".to_string())), ])), global: false, }, ) ); } #[test] fn parse_set_array_with_filter() { let ast = parse("{% set hello = [1, true, 'hello'] | length %}").unwrap(); assert_eq!( ast[0], Node::Set( WS::default(), Set { key: "hello".to_string(), value: Expr::with_filters( ExprVal::Array(vec![ Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Bool(true)), Expr::new(ExprVal::String("hello".to_string())), ]), vec![FunctionCall { name: "length".to_string(), args: HashMap::new() },], ), global: false, }, ) ); } #[test] fn parse_set_global_tag() { let ast = parse("{% set_global hello = utcnow() %}").unwrap(); assert_eq!( ast[0], Node::Set( WS::default(), Set { key: "hello".to_string(), value: Expr::new(ExprVal::FunctionCall(FunctionCall { name: "utcnow".to_string(), args: HashMap::new(), },)), global: true, }, ) ); } #[test] fn parse_raw_tag() { let ast = parse("{% raw -%}{{hey}}{%- endraw %}").unwrap(); let start_ws = WS { right: true, ..Default::default() }; let end_ws = WS { left: true, ..Default::default() }; assert_eq!(ast[0], Node::Raw(start_ws, "{{hey}}".to_string(), end_ws)); } // https://github.com/Keats/tera/issues/513 #[test] fn parse_raw_tag_with_ws() { // println!("{}", parse("{% raw %} yaml_test: {% endraw %}").unwrap_err()); let ast = parse("{% raw %} yaml_test: {% endraw %}").unwrap(); let start_ws = WS::default(); let end_ws = WS::default(); assert_eq!(ast[0], Node::Raw(start_ws, " yaml_test: ".to_string(), end_ws)); } #[test] fn parse_filter_section_without_args() { let ast = parse("{% filter upper -%}A{%- endfilter %}").unwrap(); let start_ws = WS { right: true, ..Default::default() }; let end_ws = WS { left: true, ..Default::default() }; assert_eq!( ast[0], Node::FilterSection( start_ws, FilterSection { filter: FunctionCall { name: "upper".to_string(), args: HashMap::new() }, body: vec![Node::Text("A".to_string())], }, end_ws, ) ); } #[test] fn parse_filter_section_with_args() { let ast = parse("{% filter upper(attr=1) -%}A{%- endfilter %}").unwrap(); let start_ws = WS { right: true, ..Default::default() }; let end_ws = WS { left: true, ..Default::default() }; let mut args = HashMap::new(); args.insert("attr".to_string(), Expr::new(ExprVal::Int(1))); assert_eq!( ast[0], Node::FilterSection( start_ws, FilterSection { filter: FunctionCall { name: "upper".to_string(), args }, body: vec![Node::Text("A".to_string())], }, end_ws, ) ); } #[test] fn parse_filter_section_preserves_ws() { let ast = parse("{% filter upper %} {{a}} B {% endfilter %}").unwrap(); assert_eq!( ast[0], Node::FilterSection( WS::default(), FilterSection { filter: FunctionCall { name: "upper".to_string(), args: HashMap::new() }, body: vec![ Node::Text(" ".to_string()), Node::VariableBlock(WS::default(), Expr::new(ExprVal::Ident("a".to_string()))), Node::Text(" B ".to_string()) ] }, WS::default(), ) ); } #[test] fn parse_block() { let ast = parse("{% block hello %}{{super()}} hey{%- endblock hello %}").unwrap(); let start_ws = WS::default(); let end_ws = WS { left: true, ..Default::default() }; assert_eq!( ast[0], Node::Block( start_ws, Block { name: "hello".to_string(), body: vec![Node::Super, Node::Text(" hey".to_string())], }, end_ws, ) ); } #[test] fn parse_simple_macro_definition() { let ast = parse("{% macro hello(a=1, b='hello', c) %}A: {{a}}{% endmacro %}").unwrap(); let mut args = HashMap::new(); args.insert("a".to_string(), Some(Expr::new(ExprVal::Int(1)))); args.insert("b".to_string(), Some(Expr::new(ExprVal::String("hello".to_string())))); args.insert("c".to_string(), None); assert_eq!( ast[0], Node::MacroDefinition( WS::default(), MacroDefinition { name: "hello".to_string(), args, body: vec![ Node::Text("A: ".to_string()), Node::VariableBlock(WS::default(), Expr::new(ExprVal::Ident("a".to_string()))), ], }, WS::default(), ) ); } #[test] fn parse_value_forloop() { let ast = parse("{% for item in items | reverse %}A{%- endfor %}").unwrap(); let start_ws = WS::default(); let end_ws = WS { left: true, ..Default::default() }; assert_eq!( ast[0], Node::Forloop( start_ws, Forloop { key: None, value: "item".to_string(), container: Expr::with_filters( ExprVal::Ident("items".to_string()), vec![FunctionCall { name: "reverse".to_string(), args: HashMap::new() },], ), body: vec![Node::Text("A".to_string())], empty_body: None, }, end_ws, ) ); } #[test] fn parse_key_value_forloop() { let ast = parse("{% for key, item in get_map() %}A{%- endfor %}").unwrap(); let start_ws = WS::default(); let end_ws = WS { left: true, ..Default::default() }; assert_eq!( ast[0], Node::Forloop( start_ws, Forloop { key: Some("key".to_string()), value: "item".to_string(), container: Expr::new(ExprVal::FunctionCall(FunctionCall { name: "get_map".to_string(), args: HashMap::new(), },)), body: vec![Node::Text("A".to_string())], empty_body: None, }, end_ws, ) ); } #[test] fn parse_value_forloop_array() { let ast = parse("{% for item in [1,2,] %}A{%- endfor %}").unwrap(); let start_ws = WS::default(); let end_ws = WS { left: true, ..Default::default() }; assert_eq!( ast[0], Node::Forloop( start_ws, Forloop { key: None, value: "item".to_string(), container: Expr::new(ExprVal::Array(vec![ Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Int(2)), ])), body: vec![Node::Text("A".to_string())], empty_body: None, }, end_ws, ) ); } #[test] fn parse_value_forloop_array_with_filter() { let ast = parse("{% for item in [1,2,] | reverse %}A{%- endfor %}").unwrap(); let start_ws = WS::default(); let end_ws = WS { left: true, ..Default::default() }; assert_eq!( ast[0], Node::Forloop( start_ws, Forloop { key: None, value: "item".to_string(), container: Expr::with_filters( ExprVal::Array(vec![Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Int(2)),]), vec![FunctionCall { name: "reverse".to_string(), args: HashMap::new() },], ), body: vec![Node::Text("A".to_string())], empty_body: None, }, end_ws, ) ); } #[test] fn parse_value_forloop_empty() { let ast = parse("{% for item in [1,2,] %}A{% else %}B{%- endfor %}").unwrap(); let start_ws = WS::default(); let end_ws = WS { left: true, ..Default::default() }; assert_eq!( ast[0], Node::Forloop( start_ws, Forloop { key: None, value: "item".to_string(), container: Expr::new(ExprVal::Array(vec![ Expr::new(ExprVal::Int(1)), Expr::new(ExprVal::Int(2)), ])), body: vec![Node::Text("A".to_string())], empty_body: Some(vec![Node::Text("B".to_string())]), }, end_ws, ) ); } #[test] fn parse_if() { let ast = parse("{% if item or admin %}A {%- elif 1 > 2 %}B{% else -%} C{%- endif %}").unwrap(); let end_ws = WS { left: true, ..Default::default() }; let else_ws = WS { right: true, ..Default::default() }; assert_eq!( ast[0], Node::If( If { conditions: vec![ ( WS::default(), Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new(ExprVal::Ident("item".to_string()))), operator: LogicOperator::Or, rhs: Box::new(Expr::new(ExprVal::Ident("admin".to_string()))), })), vec![Node::Text("A ".to_string())], ), ( end_ws, Expr::new(ExprVal::Logic(LogicExpr { lhs: Box::new(Expr::new(ExprVal::Int(1))), operator: LogicOperator::Gt, rhs: Box::new(Expr::new(ExprVal::Int(2))), })), vec![Node::Text("B".to_string())], ), ], otherwise: Some((else_ws, vec![Node::Text(" C".to_string())])), }, end_ws, ) ); } #[test] fn parse_break() { let ast = parse("{% for item in items %}{% break -%}{% endfor %}").unwrap(); let for_ws = WS::default(); assert_eq!( ast[0], Node::Forloop( for_ws, Forloop { key: None, value: "item".to_string(), container: Expr::new(ExprVal::Ident("items".to_string())), body: vec![Node::Break(WS { left: false, right: true }),], empty_body: None, }, for_ws, ) ); } #[test] fn parse_continue() { let ast = parse("{% for item in items %}{% continue -%}{% endfor %}").unwrap(); let for_ws = WS::default(); assert_eq!( ast[0], Node::Forloop( for_ws, Forloop { key: None, value: "item".to_string(), container: Expr::new(ExprVal::Ident("items".to_string())), body: vec![Node::Continue(WS { left: false, right: true }),], empty_body: None, }, for_ws, ) ); } #[test] fn parse_string_concat_can_merge() { let ast = parse("{{ `hello` ~ 'hey' }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock(WS::default(), Expr::new(ExprVal::String("hellohey".to_string()))), ); } #[test] fn parse_string_concat() { let ast = parse("{{ `hello` ~ ident }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::StringConcat(StringConcat { values: vec![ ExprVal::String("hello".to_string()), ExprVal::Ident("ident".to_string()), ] })) ), ); } #[test] fn parse_string_concat_multiple() { let ast = parse("{{ `hello` ~ ident ~ 'ho' }}").unwrap(); assert_eq!( ast[0], Node::VariableBlock( WS::default(), Expr::new(ExprVal::StringConcat(StringConcat { values: vec![ ExprVal::String("hello".to_string()), ExprVal::Ident("ident".to_string()), ExprVal::String("ho".to_string()), ] })) ), ); } tera-1.20.0/src/parser/tests/whitespace.rs000064400000000000000000000201221046102023000165630ustar 00000000000000use crate::parser::ast::*; use crate::parser::remove_whitespace; use std::collections::HashMap; #[test] fn do_nothing_if_unneeded() { let ast = vec![Node::Text("hey ".to_string())]; assert_eq!(remove_whitespace(ast.clone(), None), ast); } #[test] fn remove_previous_ws_if_single_opening_tag_requires_it() { let ws = WS { left: true, right: false }; let ast = vec![ Node::Text("hey ".to_string()), Node::ImportMacro(ws, "hey ".to_string(), "ho".to_string()), ]; assert_eq!( remove_whitespace(ast, None), vec![ Node::Text("hey".to_string()), // it removed the trailing space Node::ImportMacro(ws, "hey ".to_string(), "ho".to_string()), ] ); } #[test] fn remove_next_ws_if_single_opening_tag_requires_it() { let ws = WS { left: true, right: true }; let ast = vec![ Node::ImportMacro(ws, "hey ".to_string(), "ho".to_string()), Node::Text(" hey".to_string()), ]; assert_eq!( remove_whitespace(ast, None), vec![ Node::ImportMacro(ws, "hey ".to_string(), "ho".to_string()), Node::Text("hey".to_string()), // it removed the leading space ] ); } #[test] fn handle_ws_both_sides_for_raw_tag() { let start_ws = WS { left: true, right: false }; let end_ws = WS { left: true, right: true }; let ast = vec![Node::Raw(start_ws, " hey ".to_string(), end_ws), Node::Text(" hey".to_string())]; assert_eq!( remove_whitespace(ast, None), vec![ // it removed only the space at the end Node::Raw(start_ws, " hey".to_string(), end_ws), Node::Text("hey".to_string()), ] ); } #[test] fn handle_ws_both_sides_for_macro_definitions() { let start_ws = WS { left: true, right: true }; let end_ws = WS { left: true, right: true }; let ast = vec![Node::MacroDefinition( start_ws, MacroDefinition { name: "something".to_string(), args: HashMap::new(), body: vec![ Node::Text("\n ".to_string()), Node::Text("hey".to_string()), Node::Text(" ".to_string()), ], }, end_ws, )]; assert_eq!( remove_whitespace(ast, None), vec![Node::MacroDefinition( start_ws, MacroDefinition { name: "something".to_string(), args: HashMap::new(), body: vec![Node::Text("hey".to_string())], }, end_ws, ),] ); } #[test] fn handle_ws_both_sides_for_forloop_tag_and_remove_empty_node() { let start_ws = WS { left: true, right: true }; let end_ws = WS { left: true, right: true }; let ast = vec![ Node::Forloop( start_ws, Forloop { key: None, value: "item".to_string(), container: Expr::new(ExprVal::Int(1)), // not valid but we don't care about it here body: vec![Node::Text(" ".to_string()), Node::Text("hey ".to_string())], empty_body: None, }, end_ws, ), Node::Text(" hey".to_string()), ]; assert_eq!( remove_whitespace(ast, None), vec![ Node::Forloop( start_ws, Forloop { key: None, value: "item".to_string(), container: Expr::new(ExprVal::Int(1)), // not valid but we don't care about it here body: vec![Node::Text("hey".to_string())], empty_body: None, }, end_ws, ), Node::Text("hey".to_string()), ] ); } #[test] fn handle_ws_for_if_nodes() { let end_ws = WS { left: false, right: true }; let ast = vec![ Node::Text("C ".to_string()), Node::If( If { conditions: vec![ ( WS { left: true, right: true }, Expr::new(ExprVal::Int(1)), vec![Node::Text(" a ".to_string())], ), ( WS { left: true, right: false }, Expr::new(ExprVal::Int(1)), vec![Node::Text(" a ".to_string())], ), ( WS { left: true, right: true }, Expr::new(ExprVal::Int(1)), vec![Node::Text(" a ".to_string())], ), ], otherwise: None, }, end_ws, ), Node::Text(" hey".to_string()), ]; assert_eq!( remove_whitespace(ast, None), vec![ Node::Text("C".to_string()), Node::If( If { conditions: vec![ ( WS { left: true, right: true }, Expr::new(ExprVal::Int(1)), vec![Node::Text("a".to_string())], ), ( WS { left: true, right: false }, Expr::new(ExprVal::Int(1)), vec![Node::Text(" a".to_string())], ), ( WS { left: true, right: true }, Expr::new(ExprVal::Int(1)), vec![Node::Text("a ".to_string())], ), ], otherwise: None, }, end_ws, ), Node::Text("hey".to_string()), ] ); } #[test] fn handle_ws_for_if_nodes_with_else() { let end_ws = WS { left: true, right: true }; let ast = vec![ Node::Text("C ".to_string()), Node::If( If { conditions: vec![ ( WS { left: true, right: true }, Expr::new(ExprVal::Int(1)), vec![Node::Text(" a ".to_string())], ), ( WS { left: true, right: false }, Expr::new(ExprVal::Int(1)), vec![Node::Text(" a ".to_string())], ), ( WS { left: true, right: true }, Expr::new(ExprVal::Int(1)), vec![Node::Text(" a ".to_string())], ), ], otherwise: Some(( WS { left: true, right: true }, vec![Node::Text(" a ".to_string())], )), }, end_ws, ), Node::Text(" hey".to_string()), ]; assert_eq!( remove_whitespace(ast, None), vec![ Node::Text("C".to_string()), Node::If( If { conditions: vec![ ( WS { left: true, right: true }, Expr::new(ExprVal::Int(1)), vec![Node::Text("a".to_string())], ), ( WS { left: true, right: false }, Expr::new(ExprVal::Int(1)), vec![Node::Text(" a".to_string())], ), ( WS { left: true, right: true }, Expr::new(ExprVal::Int(1)), vec![Node::Text("a".to_string())], ), ], otherwise: Some(( WS { left: true, right: true }, vec![Node::Text("a".to_string())], )), }, end_ws, ), Node::Text("hey".to_string()), ] ); } tera-1.20.0/src/parser/whitespace.rs000064400000000000000000000167611046102023000154370ustar 00000000000000use crate::parser::ast::*; macro_rules! trim_right_previous { ($vec: expr) => { if let Some(last) = $vec.pop() { if let Node::Text(mut s) = last { s = s.trim_end().to_string(); if !s.is_empty() { $vec.push(Node::Text(s)); } } else { $vec.push(last); } } }; ($cond: expr, $vec: expr) => { if $cond { trim_right_previous!($vec); } }; } /// Removes whitespace from the AST nodes according to the `{%-` and `-%}` defined in the template. /// Empty string nodes will be discarded. /// /// The `ws` param is used when recursing through nested bodies to know whether to know /// how to handle the whitespace for that whole body: /// - set the initial `trim_left_next` to `ws.left` /// - trim last node if it is a text node if `ws.right == true` pub fn remove_whitespace(nodes: Vec, body_ws: Option) -> Vec { let mut res = Vec::with_capacity(nodes.len()); // Whether the node we just added to res is a Text node let mut previous_was_text = false; // Whether the previous block ended wth `-%}` and we need to trim left the next text node let mut trim_left_next = body_ws.map_or(false, |ws| ws.left); for n in nodes { match n { Node::Text(s) => { previous_was_text = true; if !trim_left_next { res.push(Node::Text(s)); continue; } trim_left_next = false; let new_val = s.trim_start(); if !new_val.is_empty() { res.push(Node::Text(new_val.to_string())); } // empty text nodes will be skipped continue; } Node::VariableBlock(ws, _) | Node::ImportMacro(ws, _, _) | Node::Extends(ws, _) | Node::Include(ws, _, _) | Node::Set(ws, _) | Node::Break(ws) | Node::Comment(ws, _) | Node::Continue(ws) => { trim_right_previous!(previous_was_text && ws.left, res); trim_left_next = ws.right; } Node::Raw(start_ws, ref s, end_ws) => { trim_right_previous!(previous_was_text && start_ws.left, res); previous_was_text = false; trim_left_next = end_ws.right; if start_ws.right || end_ws.left { let val = if start_ws.right && end_ws.left { s.trim() } else if start_ws.right { s.trim_start() } else { s.trim_end() }; res.push(Node::Raw(start_ws, val.to_string(), end_ws)); continue; } } // Those nodes have a body surrounded by 2 tags Node::Forloop(start_ws, _, end_ws) | Node::MacroDefinition(start_ws, _, end_ws) | Node::FilterSection(start_ws, _, end_ws) | Node::Block(start_ws, _, end_ws) => { trim_right_previous!(previous_was_text && start_ws.left, res); previous_was_text = false; trim_left_next = end_ws.right; // let's remove ws from the bodies now and append the cleaned up node let body_ws = WS { left: start_ws.right, right: end_ws.left }; match n { Node::Forloop(_, mut forloop, _) => { forloop.body = remove_whitespace(forloop.body, Some(body_ws)); res.push(Node::Forloop(start_ws, forloop, end_ws)); } Node::MacroDefinition(_, mut macro_def, _) => { macro_def.body = remove_whitespace(macro_def.body, Some(body_ws)); res.push(Node::MacroDefinition(start_ws, macro_def, end_ws)); } Node::FilterSection(_, mut filter_section, _) => { filter_section.body = remove_whitespace(filter_section.body, Some(body_ws)); res.push(Node::FilterSection(start_ws, filter_section, end_ws)); } Node::Block(_, mut block, _) => { block.body = remove_whitespace(block.body, Some(body_ws)); res.push(Node::Block(start_ws, block, end_ws)); } _ => unreachable!(), }; continue; } // The ugly one Node::If(If { conditions, otherwise }, end_ws) => { trim_left_next = end_ws.right; let mut new_conditions: Vec<(_, _, Vec<_>)> = Vec::with_capacity(conditions.len()); for mut condition in conditions { if condition.0.left { // We need to trim the text node before the if tag if new_conditions.is_empty() && previous_was_text { trim_right_previous!(res); } else if let Some(&mut (_, _, ref mut body)) = new_conditions.last_mut() { trim_right_previous!(body); } } // we can't peek at the next one to know whether we need to trim right since // are consuming conditions. We'll find out at the next iteration. condition.2 = remove_whitespace( condition.2, Some(WS { left: condition.0.right, right: false }), ); new_conditions.push(condition); } previous_was_text = false; // We now need to look for the last potential `{%-` bit for if/elif // That can be a `{%- else` if let Some((else_ws, body)) = otherwise { if else_ws.left { if let Some(&mut (_, _, ref mut body)) = new_conditions.last_mut() { trim_right_previous!(body); } } let mut else_body = remove_whitespace(body, Some(WS { left: else_ws.right, right: false })); // if we have an `else`, the `endif` will affect the else node so we need to check if end_ws.left { trim_right_previous!(else_body); } res.push(Node::If( If { conditions: new_conditions, otherwise: Some((else_ws, else_body)) }, end_ws, )); continue; } // Or `{%- endif` if end_ws.left { if let Some(&mut (_, _, ref mut body)) = new_conditions.last_mut() { trim_right_previous!(true, body); } } res.push(Node::If(If { conditions: new_conditions, otherwise }, end_ws)); continue; } Node::Super => (), }; // If we are there, that means it's not a text node and we didn't have to modify the node previous_was_text = false; res.push(n); } if let Some(whitespace) = body_ws { trim_right_previous!(whitespace.right, res); } res } tera-1.20.0/src/renderer/call_stack.rs000064400000000000000000000173161046102023000157120ustar 00000000000000use std::borrow::Cow; use std::collections::HashMap; use serde_json::{to_value, Value}; use crate::context::dotted_pointer; use crate::errors::{Error, Result}; use crate::renderer::for_loop::{ForLoop, ForLoopState}; use crate::renderer::stack_frame::{FrameContext, FrameType, StackFrame, Val}; use crate::template::Template; use crate::Context; /// Contains the user data and allows no mutation #[derive(Debug)] pub struct UserContext<'a> { /// Read-only context inner: &'a Context, } impl<'a> UserContext<'a> { /// Create an immutable user context to be used in the call stack pub fn new(context: &'a Context) -> Self { UserContext { inner: context } } pub fn find_value(&self, key: &str) -> Option<&'a Value> { self.inner.get(key) } pub fn find_value_by_dotted_pointer(&self, pointer: &str) -> Option<&'a Value> { let root = pointer.split('.').next().unwrap().replace("~1", "/").replace("~0", "~"); let rest = &pointer[root.len() + 1..]; self.inner.get(&root).and_then(|val| dotted_pointer(val, rest)) } } /// Contains the stack of frames #[derive(Debug)] pub struct CallStack<'a> { /// The stack of frames stack: Vec>, /// User supplied context for the render context: UserContext<'a>, } impl<'a> CallStack<'a> { /// Create the initial call stack pub fn new(context: &'a Context, template: &'a Template) -> CallStack<'a> { CallStack { stack: vec![StackFrame::new(FrameType::Origin, "ORIGIN", template)], context: UserContext::new(context), } } pub fn push_for_loop_frame(&mut self, name: &'a str, for_loop: ForLoop<'a>) { let tpl = self.stack.last().expect("Stack frame").active_template; self.stack.push(StackFrame::new_for_loop(name, tpl, for_loop)); } pub fn push_macro_frame( &mut self, namespace: &'a str, name: &'a str, context: FrameContext<'a>, tpl: &'a Template, ) { self.stack.push(StackFrame::new_macro(name, tpl, namespace, context)); } pub fn push_include_frame(&mut self, name: &'a str, tpl: &'a Template) { self.stack.push(StackFrame::new_include(name, tpl)); } /// Returns mutable reference to global `StackFrame` /// i.e gets first stack outside current for loops pub fn global_frame_mut(&mut self) -> &mut StackFrame<'a> { if self.current_frame().kind == FrameType::ForLoop { for stack_frame in self.stack.iter_mut().rev() { // walk up the parent stacks until we meet the current template if stack_frame.kind != FrameType::ForLoop { return stack_frame; } } unreachable!("Global frame not found when trying to break out of for loop"); } else { // Macro, Origin, or Include self.current_frame_mut() } } /// Returns mutable reference to current `StackFrame` pub fn current_frame_mut(&mut self) -> &mut StackFrame<'a> { self.stack.last_mut().expect("No current frame exists") } /// Returns immutable reference to current `StackFrame` pub fn current_frame(&self) -> &StackFrame<'a> { self.stack.last().expect("No current frame exists") } /// Pop the last frame pub fn pop(&mut self) { self.stack.pop().expect("Mistakenly popped Origin frame"); } pub fn lookup(&self, key: &str) -> Option> { for stack_frame in self.stack.iter().rev() { let found = stack_frame.find_value(key); if found.is_some() { return found; } // If we looked in a macro or origin frame, no point continuing // Origin is the last one and macro frame don't have access to parent frames if stack_frame.kind == FrameType::Macro || stack_frame.kind == FrameType::Origin { break; } } // Not in stack frame, look in user supplied context if key.contains('.') { return self.context.find_value_by_dotted_pointer(key).map(Cow::Borrowed); } else if let Some(value) = self.context.find_value(key) { return Some(Cow::Borrowed(value)); } None } /// Add an assignment value (via {% set ... %} and {% set_global ... %} ) pub fn add_assignment(&mut self, key: &'a str, global: bool, value: Val<'a>) { if global { self.global_frame_mut().insert(key, value); } else { self.current_frame_mut().insert(key, value); } } /// Breaks current for loop pub fn break_for_loop(&mut self) -> Result<()> { match self.current_frame_mut().for_loop { Some(ref mut for_loop) => { for_loop.break_loop(); Ok(()) } None => Err(Error::msg("Attempted `break` while not in `for loop`")), } } /// Continues current for loop pub fn increment_for_loop(&mut self) -> Result<()> { let frame = self.current_frame_mut(); frame.clear_context(); match frame.for_loop { Some(ref mut for_loop) => { for_loop.increment(); Ok(()) } None => Err(Error::msg("Attempted `increment` while not in `for loop`")), } } /// Continues current for loop pub fn continue_for_loop(&mut self) -> Result<()> { match self.current_frame_mut().for_loop { Some(ref mut for_loop) => { for_loop.continue_loop(); Ok(()) } None => Err(Error::msg("Attempted `continue` while not in `for loop`")), } } /// True if should break body, applicable to `break` and `continue` pub fn should_break_body(&self) -> bool { match self.current_frame().for_loop { Some(ref for_loop) => { for_loop.state == ForLoopState::Break || for_loop.state == ForLoopState::Continue } None => false, } } /// True if should break loop, applicable to `break` only pub fn should_break_for_loop(&self) -> bool { match self.current_frame().for_loop { Some(ref for_loop) => for_loop.state == ForLoopState::Break, None => false, } } /// Grab the current frame template pub fn active_template(&self) -> &'a Template { self.current_frame().active_template } pub fn current_context_cloned(&self) -> Value { let mut context = HashMap::new(); // Go back the stack in reverse to see what we have access to for frame in self.stack.iter().rev() { context.extend(frame.context_owned()); if let Some(ref for_loop) = frame.for_loop { context.insert( for_loop.value_name.to_string(), for_loop.get_current_value().into_owned(), ); if for_loop.is_key_value() { context.insert( for_loop.key_name.clone().unwrap(), Value::String(for_loop.get_current_key()), ); } } // Macros don't have access to the user context, we're done if frame.kind == FrameType::Macro { return to_value(&context).unwrap(); } } // If we are here we take the user context // and add the values found in the stack to it. // We do it this way as we can override global variable temporarily in forloops let mut new_ctx = self.context.inner.clone(); for (key, val) in context { new_ctx.insert(key, &val) } new_ctx.into_json() } } tera-1.20.0/src/renderer/for_loop.rs000064400000000000000000000157531046102023000154340ustar 00000000000000use std::borrow::Cow; use serde_json::Value; use unic_segment::Graphemes; use crate::renderer::stack_frame::Val; /// Enumerates the two types of for loops #[derive(Debug, PartialEq)] pub enum ForLoopKind { /// Loop over values, eg an `Array` Value, /// Loop over key value pairs, eg a `HashMap` or `Object` style iteration KeyValue, } /// Enumerates the states of a for loop #[derive(Clone, Copy, Debug, PartialEq)] pub enum ForLoopState { /// State during iteration Normal, /// State on encountering *break* statement Break, /// State on encountering *continue* statement Continue, } /// Enumerates on the types of values to be iterated, scalars and pairs #[derive(Debug)] pub enum ForLoopValues<'a> { /// Values for an array style iteration Array(Val<'a>), /// Values for a per-character iteration on a string String(Val<'a>), /// Values for an object style iteration Object(Vec<(String, Val<'a>)>), } impl<'a> ForLoopValues<'a> { pub fn current_key(&self, i: usize) -> String { match *self { ForLoopValues::Array(_) | ForLoopValues::String(_) => { unreachable!("No key in array list or string") } ForLoopValues::Object(ref values) => { values.get(i).expect("Failed getting current key").0.clone() } } } pub fn current_value(&self, i: usize) -> Val<'a> { match *self { ForLoopValues::Array(ref values) => match *values { Cow::Borrowed(v) => { Cow::Borrowed(v.as_array().expect("Is array").get(i).expect("Value")) } Cow::Owned(_) => { Cow::Owned(values.as_array().expect("Is array").get(i).expect("Value").clone()) } }, ForLoopValues::String(ref values) => { let mut graphemes = Graphemes::new(values.as_str().expect("Is string")); Cow::Owned(Value::String(graphemes.nth(i).expect("Value").to_string())) } ForLoopValues::Object(ref values) => values.get(i).expect("Value").1.clone(), } } } // We need to have some data in the renderer for when we are in a ForLoop // For example, accessing the local variable would fail when // looking it up in the global context #[derive(Debug)] pub struct ForLoop<'a> { /// The key name when iterate as a Key-Value, ie in `{% for i, person in people %}` it would be `i` pub key_name: Option, /// The value name, ie in `{% for person in people %}` it would be `person` pub value_name: String, /// What's the current loop index (0-indexed) pub current: usize, /// A list of (key, value) for the forloop. The key is `None` for `ForLoopKind::Value` pub values: ForLoopValues<'a>, /// Value or KeyValue? pub kind: ForLoopKind, /// Has the for loop encountered break or continue? pub state: ForLoopState, } impl<'a> ForLoop<'a> { pub fn from_array(value_name: &str, values: Val<'a>) -> Self { ForLoop { key_name: None, value_name: value_name.to_string(), current: 0, values: ForLoopValues::Array(values), kind: ForLoopKind::Value, state: ForLoopState::Normal, } } pub fn from_string(value_name: &str, values: Val<'a>) -> Self { ForLoop { key_name: None, value_name: value_name.to_string(), current: 0, values: ForLoopValues::String(values), kind: ForLoopKind::Value, state: ForLoopState::Normal, } } pub fn from_object(key_name: &str, value_name: &str, object: &'a Value) -> Self { let object_values = object.as_object().unwrap(); let mut values = Vec::with_capacity(object_values.len()); for (k, v) in object_values { values.push((k.to_string(), Cow::Borrowed(v))); } ForLoop { key_name: Some(key_name.to_string()), value_name: value_name.to_string(), current: 0, values: ForLoopValues::Object(values), kind: ForLoopKind::KeyValue, state: ForLoopState::Normal, } } pub fn from_object_owned(key_name: &str, value_name: &str, object: Value) -> Self { let object_values = match object { Value::Object(c) => c, _ => unreachable!( "Tried to create a Forloop from an object owned but it wasn't an object" ), }; let mut values = Vec::with_capacity(object_values.len()); for (k, v) in object_values { values.push((k.to_string(), Cow::Owned(v))); } ForLoop { key_name: Some(key_name.to_string()), value_name: value_name.to_string(), current: 0, values: ForLoopValues::Object(values), kind: ForLoopKind::KeyValue, state: ForLoopState::Normal, } } #[inline] pub fn increment(&mut self) { self.current += 1; self.state = ForLoopState::Normal; } pub fn is_key_value(&self) -> bool { self.kind == ForLoopKind::KeyValue } #[inline] pub fn break_loop(&mut self) { self.state = ForLoopState::Break; } #[inline] pub fn continue_loop(&mut self) { self.state = ForLoopState::Continue; } #[inline] pub fn get_current_value(&self) -> Val<'a> { self.values.current_value(self.current) } /// Only called in `ForLoopKind::KeyValue` #[inline] pub fn get_current_key(&self) -> String { self.values.current_key(self.current) } /// Checks whether the key string given is the variable used as key for /// the current forloop pub fn is_key(&self, name: &str) -> bool { if self.kind == ForLoopKind::Value { return false; } if let Some(ref key_name) = self.key_name { return key_name == name; } false } pub fn len(&self) -> usize { match self.values { ForLoopValues::Array(ref values) => values.as_array().expect("Value is array").len(), ForLoopValues::String(ref values) => { values.as_str().expect("Value is string").chars().count() } ForLoopValues::Object(ref values) => values.len(), } } } #[cfg(test)] mod tests { use std::borrow::Cow; use serde_json::Value; use super::ForLoop; #[test] fn test_that_iterating_on_string_yields_grapheme_clusters() { let text = "a\u{310}e\u{301}o\u{308}\u{332}".to_string(); let string = Value::String(text.clone()); let mut string_loop = ForLoop::from_string("whatever", Cow::Borrowed(&string)); assert_eq!(*string_loop.get_current_value(), text[0..3]); string_loop.increment(); assert_eq!(*string_loop.get_current_value(), text[3..6]); string_loop.increment(); assert_eq!(*string_loop.get_current_value(), text[6..]); } } tera-1.20.0/src/renderer/macros.rs000064400000000000000000000106611046102023000150720ustar 00000000000000use crate::errors::{Error, Result}; use crate::parser::ast::MacroDefinition; use crate::template::Template; use crate::tera::Tera; use std::collections::HashMap; // Types around Macros get complicated, simplify it a bit by using aliases /// Maps { macro => macro_definition } pub type MacroDefinitionMap = HashMap; /// Maps { namespace => ( macro_template, { macro => macro_definition }) } pub type MacroNamespaceMap<'a> = HashMap<&'a str, (&'a str, &'a MacroDefinitionMap)>; /// Maps { template => { namespace => ( macro_template, { macro => macro_definition }) } pub type MacroTemplateMap<'a> = HashMap<&'a str, MacroNamespaceMap<'a>>; /// Collection of all macro templates by file #[derive(Clone, Debug, Default)] pub struct MacroCollection<'a> { macros: MacroTemplateMap<'a>, } impl<'a> MacroCollection<'a> { pub fn from_original_template(tpl: &'a Template, tera: &'a Tera) -> MacroCollection<'a> { let mut macro_collection = MacroCollection { macros: MacroTemplateMap::new() }; macro_collection .add_macros_from_template(tera, tpl) .expect("Couldn't load macros from base template"); macro_collection } /// Add macros from parsed template to `MacroCollection` /// /// Macro templates can import other macro templates so the macro loading needs to /// happen recursively. We need all of the macros loaded in one go to be in the same /// HashMap for easy popping as well, otherwise there could be stray macro /// definitions remaining pub fn add_macros_from_template( &mut self, tera: &'a Tera, template: &'a Template, ) -> Result<()> { let template_name = &template.name[..]; if self.macros.contains_key(template_name) { return Ok(()); } let mut macro_namespace_map = MacroNamespaceMap::new(); if !template.macros.is_empty() { macro_namespace_map.insert("self", (template_name, &template.macros)); } for (filename, namespace) in &template.imported_macro_files { let macro_tpl = tera.get_template(filename)?; macro_namespace_map.insert(namespace, (filename, ¯o_tpl.macros)); self.add_macros_from_template(tera, macro_tpl)?; // We need to load the macros loaded in our macros in our namespace as well, unless we override it for (namespace, m) in &self.macros[¯o_tpl.name.as_ref()].clone() { if macro_namespace_map.contains_key(namespace) { continue; } // We inserted before so we're safe macro_namespace_map.insert(namespace, *m); } } self.macros.insert(template_name, macro_namespace_map); for parent in &template.parents { let parent = &parent[..]; let parent_template = tera.get_template(parent)?; self.add_macros_from_template(tera, parent_template)?; // We need to load the parent macros in our namespace as well, unless we override it for (namespace, m) in &self.macros[parent].clone() { if self.macros[template_name].contains_key(namespace) { continue; } // We inserted before so we're safe self.macros.get_mut(template_name).unwrap().insert(namespace, *m); } } Ok(()) } pub fn lookup_macro( &self, template_name: &'a str, macro_namespace: &'a str, macro_name: &'a str, ) -> Result<(&'a str, &'a MacroDefinition)> { let namespace = self .macros .get(template_name) .and_then(|namespace_map| namespace_map.get(macro_namespace)); if let Some(n) = namespace { let &(macro_template, macro_definition_map) = n; if let Some(m) = macro_definition_map.get(macro_name).map(|md| (macro_template, md)) { Ok(m) } else { Err(Error::msg(format!( "Macro `{}::{}` not found in template `{}`", macro_namespace, macro_name, template_name ))) } } else { Err(Error::msg(format!( "Macro namespace `{}` was not found in template `{}`. Have you maybe forgotten to import it, or misspelled it?", macro_namespace, template_name ))) } } } tera-1.20.0/src/renderer/mod.rs000064400000000000000000000035071046102023000143660ustar 00000000000000mod square_brackets; #[cfg(test)] mod tests; mod call_stack; mod for_loop; mod macros; mod processor; mod stack_frame; use std::io::Write; use self::processor::Processor; use crate::errors::Result; use crate::template::Template; use crate::tera::Tera; use crate::utils::buffer_to_string; use crate::Context; /// Given a `Tera` and reference to `Template` and a `Context`, renders text #[derive(Debug)] pub struct Renderer<'a> { /// Template to render template: &'a Template, /// Houses other templates, filters, global functions, etc tera: &'a Tera, /// Read-only context to be bound to template˝ context: &'a Context, /// If set rendering should be escaped should_escape: bool, } impl<'a> Renderer<'a> { /// Create a new `Renderer` #[inline] pub fn new(template: &'a Template, tera: &'a Tera, context: &'a Context) -> Renderer<'a> { let should_escape = tera.autoescape_suffixes.iter().any(|ext| { // We prefer a `path` if set, otherwise use the `name` if let Some(ref p) = template.path { return p.ends_with(ext); } template.name.ends_with(ext) }); Renderer { template, tera, context, should_escape } } /// Combines the context with the Template to generate the end result pub fn render(&self) -> Result { let mut output = Vec::with_capacity(2000); self.render_to(&mut output)?; buffer_to_string(|| "converting rendered buffer to string".to_string(), output) } /// Combines the context with the Template to write the end result to output pub fn render_to(&self, mut output: impl Write) -> Result<()> { let mut processor = Processor::new(self.template, self.tera, self.context, self.should_escape); processor.render(&mut output) } } tera-1.20.0/src/renderer/processor.rs000064400000000000000000001252701046102023000156300ustar 00000000000000use std::borrow::Cow; use std::collections::HashMap; use std::io::Write; use serde_json::{to_string_pretty, to_value, Number, Value}; use crate::context::{ValueRender, ValueTruthy}; use crate::errors::{Error, Result}; use crate::parser::ast::*; use crate::renderer::call_stack::CallStack; use crate::renderer::for_loop::ForLoop; use crate::renderer::macros::MacroCollection; use crate::renderer::square_brackets::pull_out_square_bracket; use crate::renderer::stack_frame::{FrameContext, FrameType, Val}; use crate::template::Template; use crate::tera::Tera; use crate::utils::render_to_string; use crate::Context; /// Special string indicating request to dump context static MAGICAL_DUMP_VAR: &str = "__tera_context"; /// This will convert a Tera variable to a json pointer if it is possible by replacing /// the index with their evaluated stringified value fn evaluate_sub_variables(key: &str, call_stack: &CallStack) -> Result { let sub_vars_to_calc = pull_out_square_bracket(key); let mut new_key = key.to_string(); for sub_var in &sub_vars_to_calc { // Translate from variable name to variable value match process_path(sub_var.as_ref(), call_stack) { Err(e) => { return Err(Error::msg(format!( "Variable {} can not be evaluated because: {}", key, e ))); } Ok(post_var) => { let post_var_as_str = match *post_var { Value::String(ref s) => format!(r#""{}""#, s), Value::Number(ref n) => n.to_string(), _ => { return Err(Error::msg(format!( "Only variables evaluating to String or Number can be used as \ index (`{}` of `{}`)", sub_var, key, ))); } }; // Rebuild the original key String replacing variable name with value let nk = new_key.clone(); let divider = "[".to_string() + sub_var + "]"; let mut the_parts = nk.splitn(2, divider.as_str()); new_key = the_parts.next().unwrap().to_string() + "." + post_var_as_str.as_ref() + the_parts.next().unwrap_or(""); } } } Ok(new_key .replace('/', "~1") // https://tools.ietf.org/html/rfc6901#section-3 .replace("['", ".\"") .replace("[\"", ".\"") .replace('[', ".") .replace("']", "\"") .replace("\"]", "\"") .replace(']', "")) } fn process_path<'a>(path: &str, call_stack: &CallStack<'a>) -> Result> { if !path.contains('[') { match call_stack.lookup(path) { Some(v) => Ok(v), None => Err(Error::msg(format!( "Variable `{}` not found in context while rendering '{}'", path, call_stack.active_template().name ))), } } else { let full_path = evaluate_sub_variables(path, call_stack)?; match call_stack.lookup(&full_path) { Some(v) => Ok(v), None => Err(Error::msg(format!( "Variable `{}` not found in context while rendering '{}': \ the evaluated version was `{}`. Maybe the index is out of bounds?", path, call_stack.active_template().name, full_path, ))), } } } /// Processes the ast and renders the output pub struct Processor<'a> { /// The template we're trying to render template: &'a Template, /// Root template of template to render - contains ast to use for rendering /// Can be the same as `template` if a template has no inheritance template_root: &'a Template, /// The Tera object with template details tera: &'a Tera, /// The call stack for processing call_stack: CallStack<'a>, /// The macros organised by template and namespaces macros: MacroCollection<'a>, /// If set, rendering should be escaped should_escape: bool, /// Used when super() is used in a block, to know where we are in our stack of /// definitions and for which block /// Vec<(block name, tpl_name, level)> blocks: Vec<(&'a str, &'a str, usize)>, } impl<'a> Processor<'a> { /// Create a new `Processor` that will do the rendering pub fn new( template: &'a Template, tera: &'a Tera, context: &'a Context, should_escape: bool, ) -> Self { // Gets the root template if we are rendering something with inheritance or just return // the template we're dealing with otherwise let template_root = template .parents .last() .map(|parent| tera.get_template(parent).unwrap()) .unwrap_or(template); let call_stack = CallStack::new(context, template); Processor { template, template_root, tera, call_stack, macros: MacroCollection::from_original_template(template, tera), should_escape, blocks: Vec::new(), } } fn render_body(&mut self, body: &'a [Node], write: &mut impl Write) -> Result<()> { for n in body { self.render_node(n, write)?; if self.call_stack.should_break_body() { break; } } Ok(()) } fn render_for_loop(&mut self, for_loop: &'a Forloop, write: &mut impl Write) -> Result<()> { let container_name = match for_loop.container.val { ExprVal::Ident(ref ident) => ident, ExprVal::FunctionCall(FunctionCall { ref name, .. }) => name, ExprVal::Array(_) => "an array literal", _ => return Err(Error::msg(format!( "Forloop containers have to be an ident or a function call (tried to iterate on '{:?}')", for_loop.container.val, ))), }; let for_loop_name = &for_loop.value; let for_loop_body = &for_loop.body; let for_loop_empty_body = &for_loop.empty_body; let container_val = self.safe_eval_expression(&for_loop.container)?; let for_loop = match *container_val { Value::Array(_) => { if for_loop.key.is_some() { return Err(Error::msg(format!( "Tried to iterate using key value on variable `{}`, but it isn't an object/map", container_name, ))); } ForLoop::from_array(&for_loop.value, container_val) } Value::String(_) => { if for_loop.key.is_some() { return Err(Error::msg(format!( "Tried to iterate using key value on variable `{}`, but it isn't an object/map", container_name, ))); } ForLoop::from_string(&for_loop.value, container_val) } Value::Object(_) => { if for_loop.key.is_none() { return Err(Error::msg(format!( "Tried to iterate using key value on variable `{}`, but it is missing a key", container_name, ))); } match container_val { Cow::Borrowed(c) => { ForLoop::from_object(for_loop.key.as_ref().unwrap(), &for_loop.value, c) } Cow::Owned(c) => ForLoop::from_object_owned( for_loop.key.as_ref().unwrap(), &for_loop.value, c, ), } } _ => { return Err(Error::msg(format!( "Tried to iterate on a container (`{}`) that has a unsupported type", container_name, ))); } }; let len = for_loop.len(); match (len, for_loop_empty_body) { (0, Some(empty_body)) => self.render_body(empty_body, write), (0, _) => Ok(()), (_, _) => { self.call_stack.push_for_loop_frame(for_loop_name, for_loop); for _ in 0..len { self.render_body(for_loop_body, write)?; if self.call_stack.should_break_for_loop() { break; } self.call_stack.increment_for_loop()?; } self.call_stack.pop(); Ok(()) } } } fn render_if_node(&mut self, if_node: &'a If, write: &mut impl Write) -> Result<()> { for (_, expr, body) in &if_node.conditions { if self.eval_as_bool(expr)? { return self.render_body(body, write); } } if let Some((_, ref body)) = if_node.otherwise { return self.render_body(body, write); } Ok(()) } /// The way inheritance work is that the top parent will be rendered by the renderer so for blocks /// we want to look from the bottom (`level = 0`, the template the user is actually rendering) /// to the top (the base template). fn render_block( &mut self, block: &'a Block, level: usize, write: &mut impl Write, ) -> Result<()> { let level_template = match level { 0 => self.call_stack.active_template(), _ => self .tera .get_template(&self.call_stack.active_template().parents[level - 1]) .unwrap(), }; let blocks_definitions = &level_template.blocks_definitions; // Can we find this one block in these definitions? If so render it if let Some(block_def) = blocks_definitions.get(&block.name) { let (_, Block { ref body, .. }) = block_def[0]; self.blocks.push((&block.name[..], &level_template.name[..], level)); return self.render_body(body, write); } // Do we have more parents to look through? if level < self.call_stack.active_template().parents.len() { return self.render_block(block, level + 1, write); } // Nope, just render the body we got self.render_body(&block.body, write) } fn get_default_value(&mut self, expr: &'a Expr) -> Result> { if let Some(default_expr) = expr.filters[0].args.get("value") { self.eval_expression(default_expr) } else { Err(Error::msg("The `default` filter requires a `value` argument.")) } } fn eval_in_condition(&mut self, in_cond: &'a In) -> Result { let lhs = self.safe_eval_expression(&in_cond.lhs)?; let rhs = self.safe_eval_expression(&in_cond.rhs)?; let present = match *rhs { Value::Array(ref v) => v.contains(&lhs), Value::String(ref s) => match *lhs { Value::String(ref s2) => s.contains(s2), _ => { return Err(Error::msg(format!( "Tried to check if {:?} is in a string, but it isn't a string", lhs ))) } }, Value::Object(ref map) => match *lhs { Value::String(ref s2) => map.contains_key(s2), _ => { return Err(Error::msg(format!( "Tried to check if {:?} is in a object, but it isn't a string", lhs ))) } }, _ => { return Err(Error::msg( "The `in` operator only supports strings, arrays and objects.", )) } }; Ok(if in_cond.negated { !present } else { present }) } fn eval_expression(&mut self, expr: &'a Expr) -> Result> { let mut needs_escape = false; let mut res = match expr.val { ExprVal::Array(ref arr) => { let mut values = vec![]; for v in arr { values.push(self.eval_expression(v)?.into_owned()); } Cow::Owned(Value::Array(values)) } ExprVal::In(ref in_cond) => Cow::Owned(Value::Bool(self.eval_in_condition(in_cond)?)), ExprVal::String(ref val) => { needs_escape = true; Cow::Owned(Value::String(val.to_string())) } ExprVal::StringConcat(ref str_concat) => { let mut res = String::new(); for s in &str_concat.values { match *s { ExprVal::String(ref v) => res.push_str(v), ExprVal::Int(ref v) => res.push_str(&format!("{}", v)), ExprVal::Float(ref v) => res.push_str(&format!("{}", v)), ExprVal::Ident(ref i) => match *self.lookup_ident(i)? { Value::String(ref v) => res.push_str(v), Value::Number(ref v) => res.push_str(&v.to_string()), _ => return Err(Error::msg(format!( "Tried to concat a value that is not a string or a number from ident {}", i ))), }, ExprVal::FunctionCall(ref fn_call) => match *self.eval_tera_fn_call(fn_call, &mut needs_escape)? { Value::String(ref v) => res.push_str(v), Value::Number(ref v) => res.push_str(&v.to_string()), _ => return Err(Error::msg(format!( "Tried to concat a value that is not a string or a number from function call {}", fn_call.name ))), }, _ => unreachable!(), }; } Cow::Owned(Value::String(res)) } ExprVal::Int(val) => Cow::Owned(Value::Number(val.into())), ExprVal::Float(val) => Cow::Owned(Value::Number(Number::from_f64(val).unwrap())), ExprVal::Bool(val) => Cow::Owned(Value::Bool(val)), ExprVal::Ident(ref ident) => { needs_escape = ident != MAGICAL_DUMP_VAR; // Negated idents are special cased as `not undefined_ident` should not // error but instead be falsy values match self.lookup_ident(ident) { Ok(val) => { if val.is_null() && expr.has_default_filter() { self.get_default_value(expr)? } else { val } } Err(e) => { if expr.has_default_filter() { self.get_default_value(expr)? } else { if !expr.negated { return Err(e); } // A negative undefined ident is !false so truthy return Ok(Cow::Owned(Value::Bool(true))); } } } } ExprVal::FunctionCall(ref fn_call) => { self.eval_tera_fn_call(fn_call, &mut needs_escape)? } ExprVal::MacroCall(ref macro_call) => { let val = render_to_string( || format!("macro {}", macro_call.name), |w| self.eval_macro_call(macro_call, w), )?; Cow::Owned(Value::String(val)) } ExprVal::Test(ref test) => Cow::Owned(Value::Bool(self.eval_test(test)?)), ExprVal::Logic(_) => Cow::Owned(Value::Bool(self.eval_as_bool(expr)?)), ExprVal::Math(_) => match self.eval_as_number(&expr.val) { Ok(Some(n)) => Cow::Owned(Value::Number(n)), Ok(None) => Cow::Owned(Value::String("NaN".to_owned())), Err(e) => return Err(Error::msg(e)), }, }; for filter in &expr.filters { if filter.name == "safe" || filter.name == "default" { continue; } res = self.eval_filter(&res, filter, &mut needs_escape)?; } // Lastly, we need to check if the expression is negated, thus turning it into a bool if expr.negated { return Ok(Cow::Owned(Value::Bool(!res.is_truthy()))); } // Checks if it's a string and we need to escape it (if the last filter is `safe` we don't) if self.should_escape && needs_escape && res.is_string() && !expr.is_marked_safe() { res = Cow::Owned( to_value(self.tera.get_escape_fn()(res.as_str().unwrap())).map_err(Error::json)?, ); } Ok(res) } /// Render an expression and never escape its result fn safe_eval_expression(&mut self, expr: &'a Expr) -> Result> { let should_escape = self.should_escape; self.should_escape = false; let res = self.eval_expression(expr); self.should_escape = should_escape; res } /// Evaluate a set tag and add the value to the right context fn eval_set(&mut self, set: &'a Set) -> Result<()> { let assigned_value = self.safe_eval_expression(&set.value)?; self.call_stack.add_assignment(&set.key[..], set.global, assigned_value); Ok(()) } fn eval_test(&mut self, test: &'a Test) -> Result { let tester_fn = self.tera.get_tester(&test.name)?; let err_wrap = |e| Error::call_test(&test.name, e); let mut tester_args = vec![]; for arg in &test.args { tester_args .push(self.safe_eval_expression(arg).map_err(err_wrap)?.clone().into_owned()); } let found = self.lookup_ident(&test.ident).map(|found| found.clone().into_owned()).ok(); let result = tester_fn.test(found.as_ref(), &tester_args).map_err(err_wrap)?; if test.negated { Ok(!result) } else { Ok(result) } } fn eval_tera_fn_call( &mut self, function_call: &'a FunctionCall, needs_escape: &mut bool, ) -> Result> { let tera_fn = self.tera.get_function(&function_call.name)?; *needs_escape = !tera_fn.is_safe(); let err_wrap = |e| Error::call_function(&function_call.name, e); let mut args = HashMap::with_capacity(function_call.args.len()); for (arg_name, expr) in &function_call.args { args.insert( arg_name.to_string(), self.safe_eval_expression(expr).map_err(err_wrap)?.clone().into_owned(), ); } Ok(Cow::Owned(tera_fn.call(&args).map_err(err_wrap)?)) } fn eval_macro_call(&mut self, macro_call: &'a MacroCall, write: &mut impl Write) -> Result<()> { let active_template_name = if let Some(block) = self.blocks.last() { block.1 } else if self.template.name != self.template_root.name { &self.template_root.name } else { &self.call_stack.active_template().name }; let (macro_template_name, macro_definition) = self.macros.lookup_macro( active_template_name, ¯o_call.namespace[..], ¯o_call.name[..], )?; let mut frame_context = FrameContext::with_capacity(macro_definition.args.len()); // First the default arguments for (arg_name, default_value) in ¯o_definition.args { let value = match macro_call.args.get(arg_name) { Some(val) => self.safe_eval_expression(val)?, None => match *default_value { Some(ref val) => self.safe_eval_expression(val)?, None => { return Err(Error::msg(format!( "Macro `{}` is missing the argument `{}`", macro_call.name, arg_name ))); } }, }; frame_context.insert(arg_name, value); } self.call_stack.push_macro_frame( ¯o_call.namespace, ¯o_call.name, frame_context, self.tera.get_template(macro_template_name)?, ); self.render_body(¯o_definition.body, write)?; self.call_stack.pop(); Ok(()) } fn eval_filter( &mut self, value: &Val<'a>, fn_call: &'a FunctionCall, needs_escape: &mut bool, ) -> Result> { let filter_fn = self.tera.get_filter(&fn_call.name)?; *needs_escape = !filter_fn.is_safe(); let err_wrap = |e| Error::call_filter(&fn_call.name, e); let mut args = HashMap::with_capacity(fn_call.args.len()); for (arg_name, expr) in &fn_call.args { args.insert( arg_name.to_string(), self.safe_eval_expression(expr).map_err(err_wrap)?.clone().into_owned(), ); } Ok(Cow::Owned(filter_fn.filter(value, &args).map_err(err_wrap)?)) } fn eval_as_bool(&mut self, bool_expr: &'a Expr) -> Result { let res = match bool_expr.val { ExprVal::Logic(LogicExpr { ref lhs, ref rhs, ref operator }) => { match *operator { LogicOperator::Or => self.eval_as_bool(lhs)? || self.eval_as_bool(rhs)?, LogicOperator::And => self.eval_as_bool(lhs)? && self.eval_as_bool(rhs)?, LogicOperator::Gt | LogicOperator::Gte | LogicOperator::Lt | LogicOperator::Lte => { let l = self.eval_expr_as_number(lhs)?; let r = self.eval_expr_as_number(rhs)?; let (ll, rr) = match (l, r) { (Some(nl), Some(nr)) => (nl, nr), _ => return Err(Error::msg("Comparison to NaN")), }; match *operator { LogicOperator::Gte => ll.as_f64().unwrap() >= rr.as_f64().unwrap(), LogicOperator::Gt => ll.as_f64().unwrap() > rr.as_f64().unwrap(), LogicOperator::Lte => ll.as_f64().unwrap() <= rr.as_f64().unwrap(), LogicOperator::Lt => ll.as_f64().unwrap() < rr.as_f64().unwrap(), _ => unreachable!(), } } LogicOperator::Eq | LogicOperator::NotEq => { let mut lhs_val = self.eval_expression(lhs)?; let mut rhs_val = self.eval_expression(rhs)?; // Monomorphize number vals. if lhs_val.is_number() || rhs_val.is_number() { // We're not implementing JS so can't compare things of different types if !lhs_val.is_number() || !rhs_val.is_number() { return Ok(false); } lhs_val = Cow::Owned(Value::Number( Number::from_f64(lhs_val.as_f64().unwrap()).unwrap(), )); rhs_val = Cow::Owned(Value::Number( Number::from_f64(rhs_val.as_f64().unwrap()).unwrap(), )); } match *operator { LogicOperator::Eq => *lhs_val == *rhs_val, LogicOperator::NotEq => *lhs_val != *rhs_val, _ => unreachable!(), } } } } ExprVal::Ident(_) => { let mut res = self .eval_expression(bool_expr) .unwrap_or(Cow::Owned(Value::Bool(false))) .is_truthy(); if bool_expr.negated { res = !res; } res } ExprVal::Math(_) | ExprVal::Int(_) | ExprVal::Float(_) => { match self.eval_as_number(&bool_expr.val)? { Some(n) => n.as_f64().unwrap() != 0.0, None => false, } } ExprVal::In(ref in_cond) => self.eval_in_condition(in_cond)?, ExprVal::Test(ref test) => self.eval_test(test)?, ExprVal::Bool(val) => val, ExprVal::String(ref string) => !string.is_empty(), ExprVal::FunctionCall(ref fn_call) => { let v = self.eval_tera_fn_call(fn_call, &mut false)?; match v.as_bool() { Some(val) => val, None => { return Err(Error::msg(format!( "Function `{}` was used in a logic operation but is not returning a bool", fn_call.name, ))); } } } ExprVal::StringConcat(_) => { let res = self.eval_expression(bool_expr)?; !res.as_str().unwrap().is_empty() } ExprVal::MacroCall(ref macro_call) => { let mut buf = Vec::new(); self.eval_macro_call(macro_call, &mut buf)?; !buf.is_empty() } _ => unreachable!("unimplemented logic operation for {:?}", bool_expr), }; if bool_expr.negated { return Ok(!res); } Ok(res) } /// In some cases, we will have filters in lhs/rhs of a math expression /// `eval_as_number` only works on ExprVal rather than Expr fn eval_expr_as_number(&mut self, expr: &'a Expr) -> Result> { if !expr.filters.is_empty() { match *self.eval_expression(expr)? { Value::Number(ref s) => Ok(Some(s.clone())), _ => { Err(Error::msg("Tried to do math with an expression not resulting in a number")) } } } else { self.eval_as_number(&expr.val) } } /// Return the value of an expression as a number fn eval_as_number(&mut self, expr: &'a ExprVal) -> Result> { let result = match *expr { ExprVal::Ident(ref ident) => { let v = &*self.lookup_ident(ident)?; if v.is_i64() { Some(Number::from(v.as_i64().unwrap())) } else if v.is_u64() { Some(Number::from(v.as_u64().unwrap())) } else if v.is_f64() { Some(Number::from_f64(v.as_f64().unwrap()).unwrap()) } else { return Err(Error::msg(format!( "Variable `{}` was used in a math operation but is not a number", ident ))); } } ExprVal::Int(val) => Some(Number::from(val)), ExprVal::Float(val) => Some(Number::from_f64(val).unwrap()), ExprVal::Math(MathExpr { ref lhs, ref rhs, ref operator }) => { let (l, r) = match (self.eval_expr_as_number(lhs)?, self.eval_expr_as_number(rhs)?) { (Some(l), Some(r)) => (l, r), _ => return Ok(None), }; match *operator { MathOperator::Mul => { if l.is_i64() && r.is_i64() { let ll = l.as_i64().unwrap(); let rr = r.as_i64().unwrap(); let res = match ll.checked_mul(rr) { Some(s) => s, None => { return Err(Error::msg(format!( "{} x {} results in an out of bounds i64", ll, rr ))); } }; Some(Number::from(res)) } else if l.is_u64() && r.is_u64() { let ll = l.as_u64().unwrap(); let rr = r.as_u64().unwrap(); let res = match ll.checked_mul(rr) { Some(s) => s, None => { return Err(Error::msg(format!( "{} x {} results in an out of bounds u64", ll, rr ))); } }; Some(Number::from(res)) } else { let ll = l.as_f64().unwrap(); let rr = r.as_f64().unwrap(); Number::from_f64(ll * rr) } } MathOperator::Div => { let ll = l.as_f64().unwrap(); let rr = r.as_f64().unwrap(); let res = ll / rr; if res.is_nan() { None } else if res.round() == res && res.is_finite() { Some(Number::from(res as i64)) } else { Number::from_f64(res) } } MathOperator::Add => { if l.is_i64() && r.is_i64() { let ll = l.as_i64().unwrap(); let rr = r.as_i64().unwrap(); let res = match ll.checked_add(rr) { Some(s) => s, None => { return Err(Error::msg(format!( "{} + {} results in an out of bounds i64", ll, rr ))); } }; Some(Number::from(res)) } else if l.is_u64() && r.is_u64() { let ll = l.as_u64().unwrap(); let rr = r.as_u64().unwrap(); let res = match ll.checked_add(rr) { Some(s) => s, None => { return Err(Error::msg(format!( "{} + {} results in an out of bounds u64", ll, rr ))); } }; Some(Number::from(res)) } else { let ll = l.as_f64().unwrap(); let rr = r.as_f64().unwrap(); Some(Number::from_f64(ll + rr).unwrap()) } } MathOperator::Sub => { if l.is_i64() && r.is_i64() { let ll = l.as_i64().unwrap(); let rr = r.as_i64().unwrap(); let res = match ll.checked_sub(rr) { Some(s) => s, None => { return Err(Error::msg(format!( "{} - {} results in an out of bounds i64", ll, rr ))); } }; Some(Number::from(res)) } else if l.is_u64() && r.is_u64() { let ll = l.as_u64().unwrap(); let rr = r.as_u64().unwrap(); let res = match ll.checked_sub(rr) { Some(s) => s, None => { return Err(Error::msg(format!( "{} - {} results in an out of bounds u64", ll, rr ))); } }; Some(Number::from(res)) } else { let ll = l.as_f64().unwrap(); let rr = r.as_f64().unwrap(); Some(Number::from_f64(ll - rr).unwrap()) } } MathOperator::Modulo => { if l.is_i64() && r.is_i64() { let ll = l.as_i64().unwrap(); let rr = r.as_i64().unwrap(); if rr == 0 { return Err(Error::msg(format!( "Tried to do a modulo by zero: {:?}/{:?}", lhs, rhs ))); } Some(Number::from(ll % rr)) } else if l.is_u64() && r.is_u64() { let ll = l.as_u64().unwrap(); let rr = r.as_u64().unwrap(); if rr == 0 { return Err(Error::msg(format!( "Tried to do a modulo by zero: {:?}/{:?}", lhs, rhs ))); } Some(Number::from(ll % rr)) } else { let ll = l.as_f64().unwrap(); let rr = r.as_f64().unwrap(); Number::from_f64(ll % rr) } } } } ExprVal::FunctionCall(ref fn_call) => { let v = self.eval_tera_fn_call(fn_call, &mut false)?; if v.is_i64() { Some(Number::from(v.as_i64().unwrap())) } else if v.is_u64() { Some(Number::from(v.as_u64().unwrap())) } else if v.is_f64() { Some(Number::from_f64(v.as_f64().unwrap()).unwrap()) } else { return Err(Error::msg(format!( "Function `{}` was used in a math operation but is not returning a number", fn_call.name ))); } } ExprVal::String(ref val) => { return Err(Error::msg(format!("Tried to do math with a string: `{}`", val))); } ExprVal::Bool(val) => { return Err(Error::msg(format!("Tried to do math with a boolean: `{}`", val))); } ExprVal::StringConcat(ref val) => { return Err(Error::msg(format!( "Tried to do math with a string concatenation: {}", val.to_template_string() ))); } ExprVal::Test(ref test) => { return Err(Error::msg(format!("Tried to do math with a test: {}", test.name))); } _ => unreachable!("unimplemented math expression for {:?}", expr), }; Ok(result) } /// Only called while rendering a block. /// This will look up the block we are currently rendering and its level and try to render /// the block at level + n, where would be the next template in the hierarchy the block is present fn do_super(&mut self, write: &mut impl Write) -> Result<()> { let &(block_name, _, level) = self.blocks.last().unwrap(); let mut next_level = level + 1; while next_level <= self.template.parents.len() { let blocks_definitions = &self .tera .get_template(&self.template.parents[next_level - 1]) .unwrap() .blocks_definitions; if let Some(block_def) = blocks_definitions.get(block_name) { let (ref tpl_name, Block { ref body, .. }) = block_def[0]; self.blocks.push((block_name, tpl_name, next_level)); self.render_body(body, write)?; self.blocks.pop(); // Can't go any higher for that block anymore? if next_level >= self.template.parents.len() { // then remove it from the stack, we're done with it self.blocks.pop(); } return Ok(()); } else { next_level += 1; } } Err(Error::msg("Tried to use super() in the top level block")) } /// Looks up identifier and returns its value fn lookup_ident(&self, key: &str) -> Result> { // Magical variable that just dumps the context if key == MAGICAL_DUMP_VAR { // Unwraps are safe since we are dealing with things that are already Value return Ok(Cow::Owned( to_value( to_string_pretty(&self.call_stack.current_context_cloned().take()).unwrap(), ) .unwrap(), )); } process_path(key, &self.call_stack) } /// Process the given node, appending the string result to the buffer /// if it is possible fn render_node(&mut self, node: &'a Node, write: &mut impl Write) -> Result<()> { match *node { // Comments are ignored when rendering Node::Comment(_, _) => (), Node::Text(ref s) | Node::Raw(_, ref s, _) => write!(write, "{}", s)?, Node::VariableBlock(_, ref expr) => self.eval_expression(expr)?.render(write)?, Node::Set(_, ref set) => self.eval_set(set)?, Node::FilterSection(_, FilterSection { ref filter, ref body }, _) => { let body = render_to_string( || format!("filter {}", filter.name), |w| self.render_body(body, w), )?; // the safe filter doesn't actually exist if filter.name == "safe" { write!(write, "{}", body)?; } else { self.eval_filter(&Cow::Owned(Value::String(body)), filter, &mut false)? .render(write)?; } } // Macros have been imported at the beginning Node::ImportMacro(_, _, _) => (), Node::If(ref if_node, _) => self.render_if_node(if_node, write)?, Node::Forloop(_, ref forloop, _) => self.render_for_loop(forloop, write)?, Node::Break(_) => { self.call_stack.break_for_loop()?; } Node::Continue(_) => { self.call_stack.continue_for_loop()?; } Node::Block(_, ref block, _) => self.render_block(block, 0, write)?, Node::Super => self.do_super(write)?, Node::Include(_, ref tpl_names, ignore_missing) => { let mut found = false; for tpl_name in tpl_names { let template = self.tera.get_template(tpl_name); if template.is_err() { continue; } let template = template.unwrap(); self.macros.add_macros_from_template(self.tera, template)?; self.call_stack.push_include_frame(tpl_name, template); self.render_body(&template.ast, write)?; self.call_stack.pop(); found = true; break; } if !found && !ignore_missing { return Err(Error::template_not_found( ["[", &tpl_names.join(", "), "]"].join(""), )); } } Node::Extends(_, ref name) => { return Err(Error::msg(format!( "Inheritance in included templates is currently not supported: extended `{}`", name ))); } // Macro definitions are ignored when rendering Node::MacroDefinition(_, _, _) => (), }; Ok(()) } /// Helper fn that tries to find the current context: are we in a macro? in a parent template? /// in order to give the best possible error when getting an error when rendering a tpl fn get_error_location(&self) -> String { let mut error_location = format!("Failed to render '{}'", self.template.name); // in a macro? if self.call_stack.current_frame().kind == FrameType::Macro { let frame = self.call_stack.current_frame(); error_location += &format!( ": error while rendering macro `{}::{}`", frame.macro_namespace.expect("Macro namespace"), frame.name, ); } // which template are we in? if let Some(&(name, _template, ref level)) = self.blocks.last() { let block_def = self.template.blocks_definitions.get(&name.to_string()).and_then(|b| b.get(*level)); if let Some((tpl_name, _)) = block_def { if tpl_name != &self.template.name { error_location += &format!(" (error happened in '{}').", tpl_name); } } else { error_location += " (error happened in a parent template)"; } } else if let Some(parent) = self.template.parents.last() { // Error happened in the base template, outside of blocks error_location += &format!(" (error happened in '{}').", parent); } error_location } /// Entry point for the rendering pub fn render(&mut self, write: &mut impl Write) -> Result<()> { for node in &self.template_root.ast { self.render_node(node, write) .map_err(|e| Error::chain(self.get_error_location(), e))?; } Ok(()) } } tera-1.20.0/src/renderer/square_brackets.rs000064400000000000000000000034531046102023000167650ustar 00000000000000/// Return a Vec of all substrings contained in '[ ]'s /// Ignore quoted strings and integers. pub fn pull_out_square_bracket(s: &str) -> Vec { let mut chars = s.chars(); let mut results = vec![]; loop { match chars.next() { Some('[') => { let c = chars.next().unwrap(); if c != '"' && c != '\'' { let mut inside_bracket = vec![c]; let mut bracket_count = 1; loop { let c = chars.next(); match c { Some(']') => bracket_count -= 1, Some('[') => bracket_count += 1, Some(_) => (), None => break, }; if bracket_count == 0 { // Only store results which aren't numbers let sub: String = inside_bracket.into_iter().collect(); if sub.parse::().is_err() { results.push(sub); } break; } inside_bracket.push(c.unwrap()); } } } None => break, _ => (), } } results } #[cfg(test)] mod tests { use super::*; #[test] fn can_pull_out_square_bracket() { assert_eq!(pull_out_square_bracket("hi"), Vec::::new()); assert_eq!(pull_out_square_bracket("['hi']"), Vec::::new()); assert_eq!(pull_out_square_bracket("[hi] a[0]"), vec!["hi"]); assert_eq!(pull_out_square_bracket("hi [th[e]['r']e] [fish]"), vec!["th[e]['r']e", "fish"]); } } tera-1.20.0/src/renderer/stack_frame.rs000064400000000000000000000143011046102023000160600ustar 00000000000000use std::borrow::Cow; use std::collections::HashMap; use serde_json::Value; use crate::context::dotted_pointer; use crate::renderer::for_loop::ForLoop; use crate::template::Template; pub type Val<'a> = Cow<'a, Value>; pub type FrameContext<'a> = HashMap<&'a str, Val<'a>>; /// Gets a value within a value by pointer, keeping lifetime #[inline] pub fn value_by_pointer<'a>(pointer: &str, val: &Val<'a>) -> Option> { match *val { Cow::Borrowed(r) => dotted_pointer(r, pointer).map(Cow::Borrowed), Cow::Owned(ref r) => dotted_pointer(r, pointer).map(|found| Cow::Owned(found.clone())), } } /// Enumerates the types of stack frames #[derive(Clone, Copy, Debug, PartialEq)] pub enum FrameType { /// Original frame Origin, /// New frame for macro call Macro, /// New frame for for loop ForLoop, /// Include template Include, } /// Entry in the stack frame #[derive(Debug)] pub struct StackFrame<'a> { /// Type of stack frame pub kind: FrameType, /// Frame name for context/debugging pub name: &'a str, /// Assigned value (via {% set ... %}, {% for ... %}, {% namespace::macro(a=a, b=b) %}) /// /// - {% set ... %} adds to current frame_context /// - {% for ... %} builds frame_context before iteration /// - {% namespace::macro(a=a, b=b)} builds frame_context before invocation context: FrameContext<'a>, /// Active template for frame pub active_template: &'a Template, /// `ForLoop` if frame is for a for loop pub for_loop: Option>, /// Macro namespace if MacroFrame pub macro_namespace: Option<&'a str>, } impl<'a> StackFrame<'a> { pub fn new(kind: FrameType, name: &'a str, tpl: &'a Template) -> Self { StackFrame { kind, name, context: FrameContext::new(), active_template: tpl, for_loop: None, macro_namespace: None, } } pub fn new_for_loop(name: &'a str, tpl: &'a Template, for_loop: ForLoop<'a>) -> Self { StackFrame { kind: FrameType::ForLoop, name, context: FrameContext::new(), active_template: tpl, for_loop: Some(for_loop), macro_namespace: None, } } pub fn new_macro( name: &'a str, tpl: &'a Template, macro_namespace: &'a str, context: FrameContext<'a>, ) -> Self { StackFrame { kind: FrameType::Macro, name, context, active_template: tpl, for_loop: None, macro_namespace: Some(macro_namespace), } } pub fn new_include(name: &'a str, tpl: &'a Template) -> Self { StackFrame { kind: FrameType::Include, name, context: FrameContext::new(), active_template: tpl, for_loop: None, macro_namespace: None, } } /// Finds a value in the stack frame. /// Looks first in `frame_context`, then compares to for_loop key_name and value_name. pub fn find_value(&self, key: &str) -> Option> { self.find_value_in_frame(key).or_else(|| self.find_value_in_for_loop(key)) } /// Finds a value in `frame_context`. pub fn find_value_in_frame(&self, key: &str) -> Option> { if let Some(dot) = key.find('.') { if dot < key.len() + 1 { if let Some(found_value) = self.context.get(&key[0..dot]).map(|v| value_by_pointer(&key[dot + 1..], v)) { return found_value; } } } else if let Some(found) = self.context.get(key) { return Some(found.clone()); } None } /// Finds a value in the `for_loop` if there is one pub fn find_value_in_for_loop(&self, key: &str) -> Option> { if let Some(ref for_loop) = self.for_loop { // 1st case: the variable is the key of a KeyValue for loop if for_loop.is_key(key) { return Some(Cow::Owned(Value::String(for_loop.get_current_key()))); } let (real_key, tail) = if let Some(tail_pos) = key.find('.') { (&key[..tail_pos], &key[tail_pos + 1..]) } else { (key, "") }; // 2nd case: one of Tera loop built-in variable if real_key == "loop" { match tail { "index" => { return Some(Cow::Owned(Value::Number((for_loop.current + 1).into()))); } "index0" => { return Some(Cow::Owned(Value::Number(for_loop.current.into()))); } "first" => { return Some(Cow::Owned(Value::Bool(for_loop.current == 0))); } "last" => { return Some(Cow::Owned(Value::Bool( for_loop.current == for_loop.len() - 1, ))); } _ => return None, }; } // Last case: the variable is/starts with the value name of the for loop // The `set` case will have been taken into account before // Exact match to the loop value and no tail if key == for_loop.value_name { return Some(for_loop.get_current_value()); } if real_key == for_loop.value_name && !tail.is_empty() { return value_by_pointer(tail, &for_loop.get_current_value()); } } None } /// Insert a value in the context pub fn insert(&mut self, key: &'a str, value: Val<'a>) { self.context.insert(key, value); } /// Context is cleared on each loop pub fn clear_context(&mut self) { if self.for_loop.is_some() { self.context.clear(); } } pub fn context_owned(&self) -> HashMap { let mut context = HashMap::new(); for (key, val) in &self.context { context.insert((*key).to_string(), val.clone().into_owned()); } context } } tera-1.20.0/src/renderer/tests/basic.rs000064400000000000000000001030351046102023000160270ustar 00000000000000use std::collections::{BTreeMap, HashMap}; use std::error::Error; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use lazy_static::lazy_static; use serde_derive::Serialize; use serde_json::{json, Value}; use crate::builtins::functions::Function; use crate::context::Context; use crate::errors::Result; use crate::tera::Tera; use super::Review; fn render_template(content: &str, context: &Context) -> Result { let mut tera = Tera::default(); tera.add_raw_template("hello.html", content).unwrap(); tera.register_function("get_number", |_: &HashMap| Ok(Value::Number(10.into()))); tera.register_function("get_true", |_: &HashMap| Ok(Value::Bool(true))); tera.register_function("get_string", |_: &HashMap| { Ok(Value::String("Hello".to_string())) }); tera.render("hello.html", context) } #[test] fn render_simple_string() { let result = render_template("

Hello world

", &Context::new()); assert_eq!(result.unwrap(), "

Hello world

".to_owned()); } #[test] fn render_variable_block_lit_expr() { let inputs = vec![ ("{{ 1 }}", "1"), ("{{ 3.18 }}", "3.18"), ("{{ \"hey\" }}", "hey"), (r#"{{ "{{ hey }}" }}"#, "{{ hey }}"), ("{{ true }}", "true"), ("{{ false }}", "false"), ("{{ false and true or true }}", "true"), ("{{ 1 + 1 }}", "2"), ("{{ 1 + 1.1 }}", "2.1"), ("{{ 3 - 1 }}", "2"), ("{{ 3 - 1.1 }}", "1.9"), ("{{ 2 * 5 }}", "10"), ("{{ 10 / 5 }}", "2"), ("{{ 2.1 * 5 }}", "10.5"), ("{{ 2.1 * 5.05 }}", "10.605"), ("{{ 2 / 0.5 }}", "4"), ("{{ 2.1 / 0.5 }}", "4.2"), ("{{ 2 + 1 * 2 }}", "4"), ("{{ (2 + 1) * 2 }}", "6"), ("{{ 2 * 4 % 8 }}", "0"), ("{{ 2.8 * 2 | round }}", "6"), ("{{ 1 / 0 }}", "NaN"), ("{{ true and 10 }}", "true"), ("{{ true and not 10 }}", "false"), ("{{ not true }}", "false"), ("{{ [1, 2, 3] }}", "[1, 2, 3]"), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &Context::new()).unwrap(), expected); } } #[test] fn render_variable_block_ident() { let mut context = Context::new(); context.insert("name", &"john"); context.insert("malicious", &""); context.insert("a", &2); context.insert("b", &3); context.insert("numbers", &vec![1, 2, 3]); context.insert("tuple_list", &vec![(1, 2, 3), (1, 2, 3)]); context.insert("review", &Review::new()); context.insert("with_newline", &"Animal Alphabets\nB is for Bee-Eater"); let inputs = vec![ ("{{ name }}", "john"), ("{{ malicious }}", "<html>"), ("{{ \"\" }}", "<html>"), ("{{ \" html \" | upper | trim }}", "HTML"), ("{{ 'html' }}", "html"), ("{{ `html` }}", "html"), // https://github.com/Keats/tera/issues/273 ( r#"{{ 'hangar new "Will Smoth "' | safe }}"#, r#"hangar new "Will Smoth ""#, ), ("{{ malicious | safe }}", ""), ("{{ malicious | upper }}", "<HTML>"), ("{{ malicious | upper | safe }}", ""), ("{{ malicious | safe | upper }}", "<HTML>"), ("{{ review | length }}", "2"), ("{{ review.paragraphs.1 }}", "B"), ("{{ numbers }}", "[1, 2, 3]"), ("{{ numbers.0 }}", "1"), ("{{ tuple_list.1.1 }}", "2"), ("{{ name and true }}", "true"), ("{{ name | length }}", "4"), ("{{ name is defined }}", "true"), ("{{ not name is defined }}", "false"), ("{{ name is not defined }}", "false"), ("{{ not name is not defined }}", "true"), ("{{ a is odd }}", "false"), ("{{ a is odd or b is odd }}", "true"), ("{{ range(start=1, end=4) }}", "[1, 2, 3]"), ("{{ a + b }}", "5"), ("{{ a + 1.5 }}", "3.5"), ("{{ 1 + 1 + 1 }}", "3"), ("{{ 2 - 2 - 1 }}", "-1"), ("{{ 1 - 1 + 1 }}", "1"), ("{{ 1 + get_number() }}", "11"), ("{{ get_number() + 1 }}", "11"), ("{{ (1.9 + a) | round }}", "4"), ("{{ 1.9 + a | round }}", "4"), ("{{ numbers | length - 1 }}", "2"), ("{{ 1.9 + a | round - 1 }}", "3"), ("{{ 1.9 + a | round - 1.8 + a | round }}", "0"), ("{{ 1.9 + a | round - 1.8 + a | round - 1 }}", "-1"), ("{{ 4 + 40 / (2 + 8) / 4 }}", "5"), ("{{ ( ( 2 ) + ( 2 ) ) }}", "4"), ("{{ ( ( 4 / 1 ) + ( 2 / 1 ) ) }}", "6"), ("{{ ( ( 4 + 2 ) / ( 2 + 1 ) ) }}", "2"), // https://github.com/Keats/tera/issues/435 ( "{{ with_newline | replace(from='\n', to='
') | safe }}", "Animal Alphabets
B is for Bee-Eater", ), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn render_variable_block_logic_expr() { let mut context = Context::new(); context.insert("name", &"john"); context.insert("malicious", &""); context.insert("a", &2); context.insert("b", &3); context.insert("numbers", &vec![1, 2, 3]); context.insert("tuple_list", &vec![(1, 2, 3), (1, 2, 3)]); let mut hashmap = HashMap::new(); hashmap.insert("a", 1); hashmap.insert("b", 10); hashmap.insert("john", 100); context.insert("object", &hashmap); context.insert("urls", &vec!["https://test"]); let inputs = vec![ ("{{ (1.9 + a) | round > 10 }}", "false"), ("{{ (1.9 + a) | round > 10 or b > a }}", "true"), ("{{ 1.9 + a | round == 4 and numbers | length == 3}}", "true"), ("{{ numbers | length > 1 }}", "true"), ("{{ numbers | length == 1 }}", "false"), ("{{ numbers | length - 2 == 1 }}", "true"), ("{{ not name }}", "false"), ("{{ not true }}", "false"), ("{{ not undefined }}", "true"), ("{{ name == 'john' }}", "true"), ("{{ name != 'john' }}", "false"), ("{{ name == 'john' | capitalize }}", "false"), ("{{ name != 'john' | capitalize }}", "true"), ("{{ 1 in numbers }}", "true"), ("{{ 1 not in numbers }}", "false"), ("{{ 40 not in numbers }}", "true"), ("{{ 'e' in 'hello' }}", "true"), ("{{ 'e' not in 'hello' }}", "false"), ("{{ 'x' not in 'hello' }}", "true"), ("{{ name in 'hello john' }}", "true"), ("{{ name not in 'hello john' }}", "false"), ("{{ name not in 'hello' }}", "true"), ("{{ name in ['bob', 2, 'john'] }}", "true"), ("{{ a in ['bob', 2, 'john'] }}", "true"), ("{{ \"https://test\" in [\"https://test\"] }}", "true"), ("{{ \"https://test\" in urls }}", "true"), ("{{ 'n' in name }}", "true"), ("{{ '<' in malicious }}", "true"), ("{{ 'a' in object }}", "true"), ("{{ name in object }}", "true"), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn render_variable_block_autoescaping_disabled() { let mut context = Context::new(); context.insert("name", &"john"); context.insert("malicious", &""); let inputs = vec![ ("{{ name }}", "john"), ("{{ malicious }}", ""), ("{{ malicious | safe }}", ""), ("{{ malicious | upper }}", ""), ("{{ malicious | upper | safe }}", ""), ("{{ malicious | safe | upper }}", ""), ]; for (input, expected) in inputs { let mut tera = Tera::default(); tera.add_raw_template("hello.sql", input).unwrap(); assert_eq!(tera.render("hello.sql", &context).unwrap(), expected); } } #[test] fn comments_are_ignored() { let inputs = vec![ ("Hello {# comment #}world", "Hello world"), ("Hello {# comment {# nested #}world", "Hello world"), ("My name {# was {{ name }} #}is No One.", "My name is No One."), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &Context::new()).unwrap(), expected); } } #[test] fn escaping_happens_at_the_end() { let inputs = vec![ #[cfg(feature = "builtins")] ("{{ url | urlencode | safe }}", "https%3A//www.example.org/apples-%26-oranges/"), ("{{ '' }}", "<html>"), ("{{ '' | safe }}", ""), ("{{ 'hello' | safe | replace(from='h', to='&') }}", "&ello"), ("{{ 'hello' | replace(from='h', to='&') | safe }}", "&ello"), ]; for (input, expected) in inputs { let mut context = Context::new(); context.insert("url", "https://www.example.org/apples-&-oranges/"); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn filter_args_are_not_escaped() { let mut context = Context::new(); context.insert("my_var", &"hey"); context.insert("to", &"&"); let input = r#"{{ my_var | replace(from="h", to=to) }}"#; assert_eq!(render_template(input, &context).unwrap(), "&ey"); } #[test] fn render_include_tag() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("world", "world"), ("hello", "

Hello {% include \"world\" %}

"), ]) .unwrap(); let result = tera.render("hello", &Context::new()).unwrap(); assert_eq!(result, "

Hello world

".to_owned()); } #[test] fn render_include_array_tag() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("world", "world"), ("hello", "

Hello {% include [\"custom/world\", \"world\"] %}

"), ]) .unwrap(); let result = tera.render("hello", &Context::new()).unwrap(); assert_eq!(result, "

Hello world

".to_owned()); tera.add_raw_template("custom/world", "custom world").unwrap(); let result = tera.render("hello", &Context::new()).unwrap(); assert_eq!(result, "

Hello custom world

".to_owned()); } #[test] fn render_include_tag_missing() { let mut tera = Tera::default(); tera.add_raw_template("hello", "

Hello {% include \"world\" %}

").unwrap(); let result = tera.render("hello", &Context::new()); assert!(result.is_err()); let mut tera = Tera::default(); tera.add_raw_template("hello", "

Hello {% include \"world\" ignore missing %}

") .unwrap(); let result = tera.render("hello", &Context::new()).unwrap(); assert_eq!(result, "

Hello

".to_owned()); } #[test] fn can_set_variables_in_included_templates() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("world", r#"{% set a = "world" %}{{a}}"#), ("hello", "

Hello {% include \"world\" %}

"), ]) .unwrap(); let result = tera.render("hello", &Context::new()).unwrap(); assert_eq!(result, "

Hello world

".to_owned()); } #[test] fn render_raw_tag() { let inputs = vec![ ("{% raw %}hey{% endraw %}", "hey"), ("{% raw %}{{hey}}{% endraw %}", "{{hey}}"), ("{% raw %}{% if true %}{% endraw %}", "{% if true %}"), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &Context::new()).unwrap(), expected); } } #[test] fn add_set_values_in_context() { let mut context = Context::new(); context.insert("my_var", &"hey"); context.insert("malicious", &""); context.insert("admin", &true); context.insert("num", &1); let inputs = vec![ ("{% set i = 1 %}{{ i }}", "1"), ("{% set i = 1 + 2 %}{{ i }}", "3"), (r#"{% set i = "hey" %}{{ i }}"#, "hey"), (r#"{% set i = "" %}{{ i | safe }}"#, ""), (r#"{% set i = "" %}{{ i }}"#, "<html>"), ("{% set i = my_var %}{{ i }}", "hey"), ("{% set i = malicious %}{{ i | safe }}", ""), ("{% set i = malicious %}{{ i }}", "<html>"), ("{% set i = my_var | upper %}{{ i }}", "HEY"), ("{% set i = range(end=3) %}{{ i }}", "[0, 1, 2]"), ("{% set i = admin or true %}{{ i }}", "true"), ("{% set i = admin and num > 0 %}{{ i }}", "true"), ("{% set i = 0 / 0 %}{{ i }}", "NaN"), ("{% set i = [1,2] %}{{ i }}", "[1, 2]"), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn render_filter_section() { let inputs = vec![ ("{% filter upper %}Hello{% endfilter %}", "HELLO"), ("{% filter upper %}Hello{% if true %} world{% endif %}{% endfilter %}", "HELLO WORLD"), ("{% filter upper %}Hello {% for i in range(end=3) %}i{% endfor %}{% endfilter %}", "HELLO III"), ( "{% filter upper %}Hello {% for i in range(end=3) %}{% if i == 1 %}{% break %} {% endif %}i{% endfor %}{% endfilter %}", "HELLO I", ), ("{% filter title %}Hello {% if true %}{{ 'world' | upper | safe }}{% endif %}{% endfilter %}", "Hello World"), ("{% filter safe %}{% filter upper %}{% endfilter %}{% endfilter%}", "") ]; let context = Context::new(); for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn render_tests() { let mut context = Context::new(); context.insert("is_true", &true); context.insert("is_false", &false); context.insert("age", &18); context.insert("name", &"john"); let mut map = HashMap::new(); map.insert(0, 1); context.insert("map", &map); context.insert("numbers", &vec![1, 2, 3]); context.insert::, _>("maybe", &None); let inputs = vec![ ("{% if is_true is defined %}Admin{% endif %}", "Admin"), ("{% if hello is undefined %}Admin{% endif %}", "Admin"), ("{% if name is string %}Admin{% endif %}", "Admin"), ("{% if age is number %}Admin{% endif %}", "Admin"), ("{% if age is even %}Admin{% endif %}", "Admin"), ("{% if age is odd %}Admin{%else%}even{% endif %}", "even"), ("{% if age is divisibleby(2) %}Admin{% endif %}", "Admin"), ("{% if numbers is iterable %}Admin{% endif %}", "Admin"), ("{% if map is iterable %}Admin{% endif %}", "Admin"), ("{% if map is object %}Admin{% endif %}", "Admin"), ("{% if name is starting_with('j') %}Admin{% endif %}", "Admin"), ("{% if name is ending_with('n') %}Admin{% endif %}", "Admin"), ("{% if numbers is containing(2) %}Admin{% endif %}", "Admin"), ("{% if name is matching('^j.*') %}Admin{% endif %}", "Admin"), ("{% if maybe is defined %}Admin{% endif %}", "Admin"), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn render_if_elif_else() { let mut context = Context::new(); context.insert("is_true", &true); context.insert("is_false", &false); context.insert("age", &18); context.insert("name", &"john"); context.insert("empty_string", &""); context.insert("numbers", &vec![1, 2, 3]); let inputs = vec![ ("{% if is_true %}Admin{% endif %}", "Admin"), ("{% if is_true or age + 1 > 18 %}Adult{% endif %}", "Adult"), ("{% if is_true and age == 18 %}Adult{% endif %}", "Adult"), // https://github.com/Keats/tera/issues/187 ("{% if 1 <= 2 %}a{% endif %}", "a"), ("{% if 2 >= 1 %}a{% endif %}", "a"), ("{% if 1 < 2 %}a{% endif %}", "a"), ("{% if 2 > 1 %}a{% endif %}", "a"), ("{% if 1 == 1 %}a{% endif %}", "a"), ("{% if 1 != 2 %}a{% endif %}", "a"), // testing string conditions ("{% if 'true' %}a{% endif %}", "a"), ("{% if name %}a{% endif %}", "a"), ("{% if '' %}a{% endif %}", ""), ("{% if empty_string %}a{% endif %}", ""), ("{% if '' ~ name %}a{% endif %}", "a"), ("{% if '' ~ empty_string %}a{% endif %}", ""), // some not conditions ("{% if not is_false %}a{% endif %}", "a"), ("{% if not is_true %}a{% endif %}", ""), ("{% if undefined %}a{% endif %}", ""), ("{% if not undefined %}a{% endif %}", "a"), ("{% if not is_false and is_true %}a{% endif %}", "a"), ("{% if not is_false or numbers | length > 0 %}a{% endif %}", "a"), // doesn't panic with NaN results ("{% if 0 / 0 %}a{% endif %}", ""), // if and else ("{% if is_true %}Admin{% else %}User{% endif %}", "Admin"), ("{% if is_false %}Admin{% else %}User{% endif %}", "User"), // if and elifs ("{% if is_true %}Admin{% elif is_false %}User{% endif %}", "Admin"), ("{% if is_true %}Admin{% elif is_true %}User{% endif %}", "Admin"), ("{% if is_true %}Admin{% elif numbers | length > 0 %}User{% endif %}", "Admin"), // if, elifs and else ("{% if is_true %}Admin{% elif is_false %}User{% else %}Hmm{% endif %}", "Admin"), ("{% if false %}Admin{% elif is_false %}User{% else %}Hmm{% endif %}", "Hmm"), // doesn't fallthrough elifs // https://github.com/Keats/tera/issues/188 ("{% if 1 < 4 %}a{% elif 2 < 4 %}b{% elif 3 < 4 %}c{% else %}d{% endif %}", "a"), // with in operator ( "{% if 1 in numbers %}Admin{% elif 100 in numbers %}User{% else %}Hmm{% endif %}", "Admin", ), ("{% if 100 in numbers %}Admin{% elif 1 in numbers %}User{% else %}Hmm{% endif %}", "User"), ("{% if 'n' in name %}Admin{% else %}Hmm{% endif %}", "Admin"), // function in if ("{% if get_true() %}Truth{% endif %}", "Truth"), // Parentheses around logic expressions ("{% if age >= 18 and name == 'john' %}Truth{% endif %}", "Truth"), ("{% if (age >= 18) and (name == 'john') %}Truth{% endif %}", "Truth"), ("{% if (age >= 18) or (name == 'john') %}Truth{% endif %}", "Truth"), ("{% if (age < 18) or (name == 'john') %}Truth{% endif %}", "Truth"), ("{% if (age >= 18) or (name != 'john') %}Truth{% endif %}", "Truth"), ("{% if (age < 18) and (name != 'john') %}Truth{% endif %}", ""), ("{% if (age >= 18) and (name != 'john') %}Truth{% endif %}", ""), ("{% if (age >= 18 and name == 'john') %}Truth{% endif %}", "Truth"), ("{% if (age < 18 and name == 'john') %}Truth{% endif %}", ""), ("{% if (age >= 18 and name != 'john') %}Truth{% endif %}", ""), ("{% if age >= 18 or name == 'john' and is_false %}Truth{% endif %}", "Truth"), ("{% if (age >= 18 or name == 'john') and is_false %}Truth{% endif %}", ""), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn render_for() { let mut context = Context::new(); let mut map = BTreeMap::new(); map.insert("name", "bob"); map.insert("age", "18"); context.insert("data", &vec![1, 2, 3]); context.insert("notes", &vec![1, 2, 3]); context.insert("vectors", &vec![vec![0, 3, 6], vec![1, 4, 7]]); context.insert("vectors_some_empty", &vec![vec![0, 3, 6], vec![], vec![1, 4, 7]]); context.insert("map", &map); context.insert("truthy", &2); let inputs = vec![ ("{% for i in data %}{{i}}{% endfor %}", "123"), ("{% for key, val in map %}{{key}}:{{val}} {% endfor %}", "age:18 name:bob "), ( "{% for i in data %}{{loop.index}}{{loop.index0}}{{loop.first}}{{loop.last}}{% endfor %}", "10truefalse21falsefalse32falsetrue" ), ( "{% for vector in vectors %}{% for j in vector %}{{ j }}{% endfor %}{% endfor %}", "036147" ), ( "{% for vector in vectors_some_empty %}{% for j in vector %}{{ j }}{% endfor %}{% endfor %}", "036147" ), ( "{% for val in data %}{% if val == truthy %}on{% else %}off{% endif %}{% endfor %}", "offonoff" ), ("{% for i in range(end=5) %}{{i}}{% endfor %}", "01234"), ("{% for i in range(end=5) | reverse %}{{i}}{% endfor %}", "43210"), ( "{% set looped = 0 %}{% for i in range(end=5) %}{% set looped = i %}{{looped}}{% endfor%}{{looped}}", "012340" ), // https://github.com/Keats/tera/issues/184 ("{% for note in notes %}{{ note }}{% endfor %}", "123"), ("{% for note in notes | reverse %}{{ note }}{% endfor %}", "321"), ("{% for v in vectors %}{{ v.0 }}{% endfor %}", "01"), // Loop control (`break` and `continue`) // https://github.com/Keats/tera/issues/267 ( "{% for i in data %}{{ i }}{% if i == 2 %}{% break %}{% endif %}{% endfor %}", "12" ), ( "{% for i in data %}{% if i == 2 %}{% continue %}{% endif %}{{ i }}{% endfor %}", "13" ), ( "{% for v in vectors %}{% for i in v %}{% if i == 3 %}{% break %}{% endif %}{{ i }}{% endfor %}{% endfor %}", "0147" ), ( "{% for v in vectors %}{% for i in v %}{% if i == 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}{% endfor %}", "06147" ), ( "{% for a in [1, true, 1.1, 'hello'] %}{{a}}{% endfor %}", "1true1.1hello" ), // https://github.com/Keats/tera/issues/301 ( "{% set start = 0 %}{% set end = start + 3 %}{% for i in range(start=start, end=end) %}{{ i }}{% endfor%}", "012" ), // https://github.com/Keats/tera/issues/395 ( "{% for a in [] %}{{a}}{% else %}hello{% endfor %}", "hello" ), ( "{% for a in undefined_variable | default(value=[]) %}{{a}}{% else %}hello{% endfor %}", "hello" ), ( "{% for a in [] %}{{a}}{% else %}{% if 1 == 2 %}A{% else %}B{% endif %}{% endfor %}", "B" ), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn render_magic_variable_isnt_escaped() { let mut context = Context::new(); context.insert("html", &""); let result = render_template("{{ __tera_context }}", &context); assert_eq!( result.unwrap(), r#"{ "html": "" }"# .to_owned() ); } // https://github.com/Keats/tera/issues/185 #[test] fn ok_many_variable_blocks() { let mut context = Context::new(); context.insert("username", &"bob"); let mut tpl = String::new(); for _ in 0..200 { tpl.push_str("{{ username }}") } let mut expected = String::new(); for _ in 0..200 { expected.push_str("bob") } assert_eq!(render_template(&tpl, &context).unwrap(), expected); } #[test] fn can_set_variable_in_global_context_in_forloop() { let mut context = Context::new(); context.insert("tags", &vec![1, 2, 3]); context.insert("default", &"default"); let result = render_template( r#" {%- for i in tags -%} {%- set default = 1 -%} {%- set_global global_val = i -%} {%- endfor -%} {{ default }}{{ global_val }}"#, &context, ); assert_eq!(result.unwrap(), "default3"); } #[test] fn default_filter_works() { let mut context = Context::new(); let i: Option = None; context.insert("existing", "hello"); context.insert("null", &i); let inputs = vec![ (r#"{{ existing | default(value="hey") }}"#, "hello"), (r#"{{ val | default(value=1) }}"#, "1"), (r#"{{ val | default(value="hey") | capitalize }}"#, "Hey"), (r#"{{ obj.val | default(value="hey") | capitalize }}"#, "Hey"), (r#"{{ obj.val | default(value="hey") | capitalize }}"#, "Hey"), (r#"{{ not admin | default(value=false) }}"#, "true"), (r#"{{ not admin | default(value=true) }}"#, "false"), (r#"{{ null | default(value=true) }}"#, "true"), (r#"{{ null | default(value="hey") | capitalize }}"#, "Hey"), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn filter_filter_works() { #[derive(Debug, Serialize)] struct Author { id: u8, } let mut context = Context::new(); context.insert("authors", &vec![Author { id: 1 }, Author { id: 2 }, Author { id: 3 }]); let inputs = vec![(r#"{{ authors | filter(attribute="id", value=1) | first | get(key="id") }}"#, "1")]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn filter_on_array_literal_works() { let mut context = Context::new(); let i: Option = None; context.insert("existing", "hello"); context.insert("null", &i); let inputs = vec![ (r#"{{ [1, 2, 3] | length }}"#, "3"), (r#"{% set a = [1, 2, 3] | length %}{{ a }}"#, "3"), (r#"{% for a in [1, 2, 3] | slice(start=1) %}{{ a }}{% endfor %}"#, "23"), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn can_do_string_concat() { let mut context = Context::new(); context.insert("a_string", "hello"); context.insert("another_string", "xXx"); context.insert("an_int", &1); context.insert("a_float", &3.18); let inputs = vec![ (r#"{{ "hello" ~ " world" }}"#, "hello world"), (r#"{{ "hello" ~ 1 }}"#, "hello1"), (r#"{{ "hello" ~ 3.18 }}"#, "hello3.18"), (r#"{{ 3.18 ~ "hello"}}"#, "3.18hello"), (r#"{{ "hello" ~ get_string() }}"#, "helloHello"), (r#"{{ get_string() ~ "hello" }}"#, "Hellohello"), (r#"{{ get_string() ~ 3.18 }}"#, "Hello3.18"), (r#"{{ a_string ~ " world" }}"#, "hello world"), (r#"{{ a_string ~ ' world ' ~ another_string }}"#, "hello world xXx"), (r#"{{ a_string ~ another_string }}"#, "helloxXx"), (r#"{{ a_string ~ an_int }}"#, "hello1"), (r#"{{ a_string ~ a_float }}"#, "hello3.18"), ]; for (input, expected) in inputs { println!("{:?} -> {:?}", input, expected); assert_eq!(render_template(input, &context).unwrap(), expected); } } #[test] fn can_fail_rendering_from_template() { let mut context = Context::new(); context.insert("title", "hello"); let res = render_template( r#"{{ throw(message="Error: " ~ title ~ " did not include a summary") }}"#, &context, ); let err = res.expect_err("This should always fail to render"); let source = err.source().expect("Must have a source"); assert_eq!(source.to_string(), "Function call 'throw' failed"); let source = source.source().expect("Should have a nested error"); assert_eq!(source.to_string(), "Error: hello did not include a summary"); } #[test] fn does_render_owned_for_loop_with_objects() { let mut context = Context::new(); let data = json!([ {"id": 1, "year": 2015}, {"id": 2, "year": 2015}, {"id": 3, "year": 2016}, {"id": 4, "year": 2017}, {"id": 5, "year": 2017}, {"id": 6, "year": 2017}, {"id": 7, "year": 2018}, {"id": 8}, {"id": 9, "year": null}, ]); context.insert("something", &data); let tpl = r#"{% for year, things in something | group_by(attribute="year") %}{{year}},{% endfor %}"#; let expected = "2015,2016,2017,2018,"; assert_eq!(render_template(tpl, &context).unwrap(), expected); } #[test] fn does_render_owned_for_loop_with_objects_string_keys() { let mut context = Context::new(); let data = json!([ {"id": 1, "group": "a"}, {"id": 2, "group": "b"}, {"id": 3, "group": "c"}, {"id": 4, "group": "a"}, {"id": 5, "group": "b"}, {"id": 6, "group": "c"}, {"id": 7, "group": "a"}, {"id": 8}, {"id": 9, "year": null}, ]); context.insert("something", &data); let tpl = r#"{% for group, things in something | group_by(attribute="group") %}{{group}},{% endfor %}"#; let expected = "a,b,c,"; assert_eq!(render_template(tpl, &context).unwrap(), expected); } #[test] fn render_magic_variable_gets_all_contexts() { let mut context = Context::new(); context.insert("html", &""); context.insert("num", &1); context.insert("i", &10); let result = render_template( "{% set some_val = 1 %}{% for i in range(start=0, end=1) %}{% set for_val = i %}{{ __tera_context }}{% endfor %}", &context ); assert_eq!( result.unwrap(), r#"{ "for_val": 0, "html": "", "i": 0, "num": 1, "some_val": 1 }"# .to_owned() ); } #[test] fn render_magic_variable_macro_doesnt_leak() { let mut context = Context::new(); context.insert("html", &""); context.insert("num", &1); context.insert("i", &10); let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello(arg=1) %}{{ __tera_context }}{% endmacro hello %}"), ("tpl", "{% import \"macros\" as macros %}{{macros::hello()}}"), ]) .unwrap(); let result = tera.render("tpl", &context); assert_eq!( result.unwrap(), r#"{ "arg": 1 }"# .to_owned() ); } // https://github.com/Keats/tera/issues/342 #[test] fn redefining_loop_value_doesnt_break_loop() { let mut tera = Tera::default(); tera.add_raw_template( "tpl", r#" {%- set string = "abcdefghdijklm" | split(pat="d") -%} {% for i in string -%} {%- set j = i ~ "lol" ~ " " -%} {{ j }} {%- endfor -%} "#, ) .unwrap(); let context = Context::new(); let result = tera.render("tpl", &context); assert_eq!(result.unwrap(), "abclol efghlol ijklmlol "); } #[test] fn can_use_concat_to_push_to_array() { let mut tera = Tera::default(); tera.add_raw_template( "tpl", r#" {%- set ids = [] -%} {% for i in range(end=5) -%} {%- set_global ids = ids | concat(with=i) -%} {%- endfor -%} {{ids}}"#, ) .unwrap(); let context = Context::new(); let result = tera.render("tpl", &context); assert_eq!(result.unwrap(), "[0, 1, 2, 3, 4]"); } struct Next(AtomicUsize); impl Function for Next { fn call(&self, _args: &HashMap) -> Result { Ok(Value::Number(self.0.fetch_add(1, Ordering::Relaxed).into())) } } #[derive(Clone)] struct SharedNext(Arc); impl Function for SharedNext { fn call(&self, args: &HashMap) -> Result { self.0.call(args) } } lazy_static! { static ref NEXT_GLOBAL: SharedNext = SharedNext(Arc::new(Next(AtomicUsize::new(1)))); } #[test] fn stateful_global_fn() { fn make_tera() -> Tera { let mut tera = Tera::default(); tera.add_raw_template( "fn.html", "

{{ get_next() }}, {{ get_next_shared() }}, {{ get_next() }}...

", ) .unwrap(); tera.register_function("get_next", Next(AtomicUsize::new(1))); tera.register_function("get_next_shared", NEXT_GLOBAL.clone()); tera } assert_eq!( make_tera().render("fn.html", &Context::new()).unwrap(), "

1, 1, 2...

".to_owned() ); assert_eq!( make_tera().render("fn.html", &Context::new()).unwrap(), "

1, 2, 2...

".to_owned() ); } // https://github.com/Keats/tera/issues/373 #[test] fn split_on_context_value() { let mut tera = Tera::default(); tera.add_raw_template("split.html", r#"{{ body | split(pat="\n") }}"#).unwrap(); let mut context = Context::new(); context.insert("body", "multi\nple\nlines"); let res = tera.render("split.html", &context); assert_eq!(res.unwrap(), "[multi, ple, lines]"); } // https://github.com/Keats/tera/issues/422 #[test] fn default_filter_works_in_condition() { let mut tera = Tera::default(); tera.add_raw_template("test.html", r#"{% if frobnicate|default(value=True) %}here{% endif %}"#) .unwrap(); let res = tera.render("test.html", &Context::new()); assert_eq!(res.unwrap(), "here"); } #[test] fn safe_filter_works() { struct Safe; impl crate::Filter for Safe { fn filter(&self, value: &Value, _args: &HashMap) -> Result { Ok(Value::String(format!("
{}
", value.as_str().unwrap()))) } fn is_safe(&self) -> bool { true } } let mut tera = Tera::default(); tera.register_filter("safe_filter", Safe); tera.add_raw_template("test.html", r#"{{ "Hello" | safe_filter }}"#).unwrap(); let res = tera.render("test.html", &Context::new()); assert_eq!(res.unwrap(), "
Hello
"); } #[test] fn safe_function_works() { struct Safe; impl crate::Function for Safe { fn call(&self, _args: &HashMap) -> Result { Ok(Value::String("
Hello
".to_owned())) } fn is_safe(&self) -> bool { true } } let mut tera = Tera::default(); tera.register_function("safe_function", Safe); tera.add_raw_template("test.html", "{{ safe_function() }}").unwrap(); let res = tera.render("test.html", &Context::new()); assert_eq!(res.unwrap(), "
Hello
"); } tera-1.20.0/src/renderer/tests/errors.rs000064400000000000000000000175511046102023000162710ustar 00000000000000use std::collections::HashMap; use std::error::Error; use crate::context::Context; use crate::tera::Tera; #[test] fn error_location_basic() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{{ 1 + true }}")]).unwrap(); let result = tera.render("tpl", &Context::new()); assert_eq!(result.unwrap_err().to_string(), "Failed to render \'tpl\'"); } #[test] fn error_location_inside_macro() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}{{ 1 + true }}{% endmacro hello %}"), ("tpl", "{% import \"macros\" as macros %}{{ macros::hello() }}"), ]) .unwrap(); let result = tera.render("tpl", &Context::new()); assert_eq!( result.unwrap_err().to_string(), "Failed to render \'tpl\': error while rendering macro `macros::hello`" ); } #[test] fn error_loading_macro_from_unloaded_namespace() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}{{ 1 + true }}{% endmacro hello %}"), ("tpl", "{% import \"macros\" as macros %}{{ macro::hello() }}"), ]) .unwrap(); let result = tera.render("tpl", &Context::new()); println!("{:#?}", result); assert_eq!( result.unwrap_err().source().unwrap().to_string(), "Macro namespace `macro` was not found in template `tpl`. Have you maybe forgotten to import it, or misspelled it?" ); } #[test] fn error_location_base_template() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("parent", "Hello {{ greeting + 1}} {% block bob %}{% endblock bob %}"), ("child", "{% extends \"parent\" %}{% block bob %}Hey{% endblock bob %}"), ]) .unwrap(); let result = tera.render("child", &Context::new()); assert_eq!( result.unwrap_err().to_string(), "Failed to render \'child\' (error happened in 'parent')." ); } #[test] fn error_location_in_parent_block() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("parent", "Hello {{ greeting }} {% block bob %}{{ 1 + true }}{% endblock bob %}"), ("child", "{% extends \"parent\" %}{% block bob %}{{ super() }}Hey{% endblock bob %}"), ]) .unwrap(); let result = tera.render("child", &Context::new()); assert_eq!( result.unwrap_err().to_string(), "Failed to render \'child\' (error happened in 'parent')." ); } #[test] fn error_location_in_parent_in_macro() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}{{ 1 + true }}{% endmacro hello %}"), ("parent", "{% import \"macros\" as macros %}{{ macros::hello() }}{% block bob %}{% endblock bob %}"), ("child", "{% extends \"parent\" %}{% block bob %}{{ super() }}Hey{% endblock bob %}"), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!( result.unwrap_err().to_string(), "Failed to render \'child\': error while rendering macro `macros::hello` (error happened in \'parent\')." ); } #[test] fn error_out_of_range_index() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{{ arr[10] }}")]).unwrap(); let mut context = Context::new(); context.insert("arr", &[1, 2, 3]); let result = tera.render("tpl", &Context::new()); assert_eq!( result.unwrap_err().source().unwrap().to_string(), "Variable `arr[10]` not found in context while rendering \'tpl\': the evaluated version was `arr.10`. Maybe the index is out of bounds?" ); } #[test] fn error_unknown_index_variable() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{{ arr[a] }}")]).unwrap(); let mut context = Context::new(); context.insert("arr", &[1, 2, 3]); let result = tera.render("tpl", &context); assert_eq!( result.unwrap_err().source().unwrap().to_string(), "Variable arr[a] can not be evaluated because: Variable `a` not found in context while rendering \'tpl\'" ); } #[test] fn error_invalid_type_index_variable() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{{ arr[a] }}")]).unwrap(); let mut context = Context::new(); context.insert("arr", &[1, 2, 3]); context.insert("a", &true); let result = tera.render("tpl", &context); assert_eq!( result.unwrap_err().source().unwrap().to_string(), "Only variables evaluating to String or Number can be used as index (`a` of `arr[a]`)" ); } #[test] fn error_when_missing_macro_templates() { let mut tera = Tera::default(); let result = tera.add_raw_templates(vec![( "parent", "{% import \"macros\" as macros %}{{ macros::hello() }}{% block bob %}{% endblock bob %}", )]); assert_eq!( result.unwrap_err().to_string(), "Template `parent` loads macros from `macros` which isn\'t present in Tera" ); } #[test] fn error_when_using_variable_set_in_included_templates_outside() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("included", r#"{{a}}{% set b = "hi" %}-{{b}}"#), ("base", r#"{{a}}{% include "included" %}{{b}}"#), ]) .unwrap(); let mut context = Context::new(); context.insert("a", &10); let result = tera.render("base", &context); assert_eq!( result.unwrap_err().source().unwrap().to_string(), "Variable `b` not found in context while rendering \'base\'" ); } // https://github.com/Keats/tera/issues/344 // Yes it is as silly as it sounds #[test] fn right_variable_name_is_needed_in_for_loop() { let mut data = HashMap::new(); data.insert("content", "hello"); let mut context = Context::new(); context.insert("comments", &vec![data]); let mut tera = Tera::default(); tera.add_raw_template( "tpl", r#" {%- for comment in comments -%}

{{ comment.content }}

{{ whocares.content }}

{{ doesntmatter.content }}

{% endfor -%}"#, ) .unwrap(); let result = tera.render("tpl", &context); assert_eq!( result.unwrap_err().source().unwrap().to_string(), "Variable `whocares.content` not found in context while rendering \'tpl\'" ); } // https://github.com/Keats/tera/issues/385 // https://github.com/Keats/tera/issues/370 #[test] fn errors_with_inheritance_in_included_template() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("base", "Base - {% include \"child\" %}"), ("parent", "{% block title %}Parent{% endblock %}"), ("child", "{% extends \"parent\" %}{% block title %}{{ super() }} - Child{% endblock %}"), ]) .unwrap(); let result = tera.render("base", &Context::new()); assert_eq!( result.unwrap_err().source().unwrap().to_string(), "Inheritance in included templates is currently not supported: extended `parent`" ); } #[test] fn error_string_concat_math_logic() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{{ 'ho' ~ name < 10 }}")]).unwrap(); let mut context = Context::new(); context.insert("name", &"john"); let result = tera.render("tpl", &context); assert_eq!( result.unwrap_err().source().unwrap().to_string(), "Tried to do math with a string concatenation: 'ho' ~ name" ); } #[test] fn error_gives_source_on_tests() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{% if a is undefined(1) %}-{% endif %}")]).unwrap(); let result = tera.render("tpl", &Context::new()); println!("{:?}", result); let err = result.unwrap_err(); let source = err.source().unwrap(); assert_eq!(source.to_string(), "Test call \'undefined\' failed"); let source2 = source.source().unwrap(); assert_eq!( source2.to_string(), "Tester `undefined` was called with some args but this test doesn\'t take args" ); } tera-1.20.0/src/renderer/tests/inheritance.rs000064400000000000000000000133101046102023000172330ustar 00000000000000use crate::context::Context; use crate::tera::Tera; #[test] fn render_simple_inheritance() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("top", "{% block pre %}{% endblock pre %}{% block main %}{% endblock main %}"), ("bottom", "{% extends \"top\" %}{% block main %}MAIN{% endblock %}"), ]) .unwrap(); let result = tera.render("bottom", &Context::new()); assert_eq!(result.unwrap(), "MAIN".to_string()); } #[test] fn render_simple_inheritance_super() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("top", "{% block main %}TOP{% endblock main %}"), ("bottom", "{% extends \"top\" %}{% block main %}{{ super() }}MAIN{% endblock %}"), ]) .unwrap(); let result = tera.render("bottom", &Context::new()); assert_eq!(result.unwrap(), "TOPMAIN".to_string()); } #[test] fn render_multiple_inheritance() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("top", "{% block pre %}{% endblock pre %}{% block main %}{% endblock main %}"), ("mid", "{% extends \"top\" %}{% block pre %}PRE{% endblock pre %}"), ("bottom", "{% extends \"mid\" %}{% block main %}MAIN{% endblock main %}"), ]) .unwrap(); let result = tera.render("bottom", &Context::new()); assert_eq!(result.unwrap(), "PREMAIN".to_string()); } #[test] fn render_multiple_inheritance_with_super() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ( "grandparent", "{% block hey %}hello{% endblock hey %} {% block ending %}sincerely{% endblock ending %}", ), ( "parent", "{% extends \"grandparent\" %}{% block hey %}hi and grandma says {{ super() }}{% endblock hey %}", ), ( "child", "{% extends \"parent\" %}{% block hey %}dad says {{ super() }}{% endblock hey %}{% block ending %}{{ super() }} with love{% endblock ending %}", ), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!( result.unwrap(), "dad says hi and grandma says hello sincerely with love".to_string() ); } #[test] fn render_filter_section_inheritance_no_override() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("top", "{% filter upper %}hello {% block main %}top{% endblock main %}{% endfilter %}"), ("bottom", "{% extends 'top' %}"), ]) .unwrap(); let result = tera.render("bottom", &Context::new()); assert_eq!(result.unwrap(), "HELLO TOP".to_string()); } #[test] fn render_filter_section_inheritance() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("top", "{% filter upper %}hello {% block main %}top{% endblock main %}{% endfilter %}"), ("bottom", "{% extends 'top' %}{% block main %}bottom{% endblock %}"), ]) .unwrap(); let result = tera.render("bottom", &Context::new()); assert_eq!(result.unwrap(), "HELLO BOTTOM".to_string()); } #[test] fn render_super_multiple_inheritance_nested_block() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ( "grandparent", "{% block hey %}hello{% endblock hey %}", ), ( "parent", "{% extends \"grandparent\" %}{% block hey %}hi and grandma says {{ super() }} {% block ending %}sincerely{% endblock ending %}{% endblock hey %}", ), ( "child", "{% extends \"parent\" %}{% block hey %}dad says {{ super() }}{% endblock hey %}{% block ending %}{{ super() }} with love{% endblock ending %}", ), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!( result.unwrap(), "dad says hi and grandma says hello sincerely with love".to_string() ); } #[test] fn render_nested_block_multiple_inheritance_no_super() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("index", "{% block content%}INDEX{% endblock content %}"), ( "docs", "{% extends \"index\" %}{% block content%}DOCS{% block more %}MORE{% endblock more %}{% endblock content %}", ), ("page", "{% extends \"docs\" %}{% block more %}PAGE{% endblock more %}"), ]).unwrap(); let result = tera.render("page", &Context::new()); assert_eq!(result.unwrap(), "DOCSPAGE".to_string()); } #[test] fn render_super_in_top_block_errors() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("index", "{% block content%}{{super()}}{% endblock content %}")]) .unwrap(); let result = tera.render("index", &Context::new()); assert!(result.is_err()); } // https://github.com/Keats/tera/issues/215 #[test] fn render_super_in_grandchild_without_redefining_works() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("grandparent", "{% block title %}Title{% endblock %}"), ( "parent", "{% extends \"grandparent\" %}{% block title %}{{ super() }} - More{% endblock %}", ), ("child", "{% extends \"parent\" %}"), ]) .unwrap(); let result = tera.render("child", &Context::new()); assert_eq!(result.unwrap(), "Title - More".to_string()); } #[test] fn render_super_in_grandchild_without_redefining_in_parent_works() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("grandparent", "{% block title %}Title{% endblock %}"), ("parent", "{% extends \"grandparent\" %}"), ("child", "{% extends \"parent\" %}{% block title %}{{ super() }} - More{% endblock %}"), ]) .unwrap(); let result = tera.render("child", &Context::new()); assert_eq!(result.unwrap(), "Title - More".to_string()); } tera-1.20.0/src/renderer/tests/macros.rs000064400000000000000000000324041046102023000162330ustar 00000000000000use crate::context::Context; use crate::tera::Tera; use super::NestedObject; #[test] fn render_macros() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}Hello{% endmacro hello %}"), ( "tpl", "{% import \"macros\" as macros %}{% block hey %}{{macros::hello()}}{% endblock hey %}", ), ]) .unwrap(); let result = tera.render("tpl", &Context::new()); assert_eq!(result.unwrap(), "Hello".to_string()); } #[test] fn render_macros_defined_in_template() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("tpl", "{% macro hello()%}Hello{% endmacro hello %}{% block hey %}{{self::hello()}}{% endblock hey %}"), ]) .unwrap(); let result = tera.render("tpl", &Context::new()); assert_eq!(result.unwrap(), "Hello".to_string()); } #[test] fn render_macros_expression_arg() { let mut context = Context::new(); context.insert("pages", &vec![1, 2, 3, 4, 5]); let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello(val)%}{{val}}{% endmacro hello %}"), ("tpl", "{% import \"macros\" as macros %}{{macros::hello(val=pages|last)}}"), ]) .unwrap(); let result = tera.render("tpl", &context); assert_eq!(result.unwrap(), "5".to_string()); } #[test] fn render_macros_in_child_templates_same_namespace() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("grandparent", "{% block hey %}hello{% endblock hey %}"), ("macros", "{% macro hello()%}Hello{% endmacro hello %}"), ("macros2", "{% macro hi()%}Hi{% endmacro hi %}"), ("parent", "{% extends \"grandparent\" %}{% import \"macros\" as macros %}{% block hey %}{{macros::hello()}}{% endblock hey %}"), ("child", "{% extends \"parent\" %}{% import \"macros2\" as macros %}{% block hey %}{{super()}}/{{macros::hi()}}{% endblock hey %}"), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!(result.unwrap(), "Hello/Hi".to_string()); } #[test] fn render_macros_in_child_templates_different_namespace() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("grandparent", "{% block hey %}hello{% endblock hey %}"), ("macros", "{% macro hello()%}Hello{% endmacro hello %}"), ("macros2", "{% macro hi()%}Hi{% endmacro hi %}"), ("parent", "{% extends \"grandparent\" %}{% import \"macros\" as macros %}{% block hey %}{{macros::hello()}}{% endblock hey %}"), ("child", "{% extends \"parent\" %}{% import \"macros2\" as macros2 %}{% block hey %}{{super()}}/{{macros2::hi()}}{% endblock hey %}"), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!(result.unwrap(), "Hello/Hi".to_string()); } #[test] fn render_macros_in_parent_template_with_inheritance() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}Hello{% endmacro hello %}"), ("grandparent", "{% import \"macros\" as macros %}{% block hey %}{{macros::hello()}}{% endblock hey %}"), ("child", "{% extends \"grandparent\" %}{% import \"macros\" as macros %}{% block hey %}{{super()}}/{{macros::hello()}}{% endblock hey %}"), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!(result.unwrap(), "Hello/Hello".to_string()); } #[test] fn macro_param_arent_escaped() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros.html", r#"{% macro print(val) %}{{val|safe}}{% endmacro print %}"#), ("hello.html", r#"{% import "macros.html" as macros %}{{ macros::print(val=my_var)}}"#), ]) .unwrap(); let mut context = Context::new(); context.insert("my_var", &"&"); let result = tera.render("hello.html", &context); assert_eq!(result.unwrap(), "&".to_string()); } #[test] fn render_set_tag_macro() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}Hello{% endmacro hello %}"), ( "hello.html", "{% import \"macros\" as macros %}{% set my_var = macros::hello() %}{{my_var}}", ), ]) .unwrap(); let result = tera.render("hello.html", &Context::new()); assert_eq!(result.unwrap(), "Hello".to_string()); } #[test] fn render_macros_with_default_args() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello(val=1) %}{{val}}{% endmacro hello %}"), ("hello.html", "{% import \"macros\" as macros %}{{macros::hello()}}"), ]) .unwrap(); let result = tera.render("hello.html", &Context::new()); assert_eq!(result.unwrap(), "1".to_string()); } #[test] fn render_macros_override_default_args() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello(val=1) %}{{val}}{% endmacro hello %}"), ("hello.html", "{% import \"macros\" as macros %}{{macros::hello(val=2)}}"), ]) .unwrap(); let result = tera.render("hello.html", &Context::new()); assert_eq!(result.unwrap(), "2".to_string()); } #[test] fn render_recursive_macro() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ( "macros", "{% macro factorial(n) %}{% if n > 1 %}{{ n }} - {{ self::factorial(n=n-1) }}{% else %}1{% endif %}{{ n }}{% endmacro factorial %}", ), ("hello.html", "{% import \"macros\" as macros %}{{macros::factorial(n=7)}}"), ]).unwrap(); let result = tera.render("hello.html", &Context::new()); assert_eq!(result.unwrap(), "7 - 6 - 5 - 4 - 3 - 2 - 11234567".to_string()); } // https://github.com/Keats/tera/issues/202 #[test] fn recursive_macro_with_loops() { let parent = NestedObject { label: "Parent".to_string(), parent: None, numbers: vec![1, 2, 3] }; let child = NestedObject { label: "Child".to_string(), parent: Some(Box::new(parent)), numbers: vec![1, 2, 3], }; let mut context = Context::new(); context.insert("objects", &vec![child]); let mut tera = Tera::default(); tera.add_raw_templates(vec![ ( "macros.html", r#" {% macro label_for(obj, sep) -%} {%- if obj.parent -%} {{ self::label_for(obj=obj.parent, sep=sep) }}{{sep}} {%- endif -%} {{obj.label}} {%- for i in obj.numbers -%}{{ i }}{%- endfor -%} {%- endmacro label_for %} "#, ), ( "recursive", r#" {%- import "macros.html" as macros -%} {%- for obj in objects -%} {{ macros::label_for(obj=obj, sep="|") }} {%- endfor -%} "#, ), ]) .unwrap(); let result = tera.render("recursive", &context); assert_eq!(result.unwrap(), "Parent123|Child123".to_string()); } // https://github.com/Keats/tera/issues/250 #[test] fn render_macros_in_included() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro my_macro() %}my macro{% endmacro %}"), ("includeme", r#"{% import "macros" as macros %}{{ macros::my_macro() }}"#), ("example", r#"{% include "includeme" %}"#), ]) .unwrap(); let result = tera.render("example", &Context::new()); assert_eq!(result.unwrap(), "my macro".to_string()); } // https://github.com/Keats/tera/issues/255 #[test] fn import_macros_into_other_macro_files() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("submacros", "{% macro test() %}Success!{% endmacro %}"), ( "macros", r#"{% import "submacros" as sub %}{% macro test() %}{{ sub::test() }}{% endmacro %}"#, ), ("index", r#"{% import "macros" as macros %}{{ macros::test() }}"#), ]) .unwrap(); let result = tera.render("index", &Context::new()); assert_eq!(result.unwrap(), "Success!".to_string()); } #[test] fn can_load_parent_macro_in_child() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}{{ 1 }}{% endmacro hello %}"), ("parent", "{% import \"macros\" as macros %}{{ macros::hello() }}{% block bob %}{% endblock bob %}"), ("child", "{% extends \"parent\" %}{% block bob %}{{ super() }}Hey{% endblock bob %}"), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!(result.unwrap(), "1Hey".to_string()); } #[test] fn can_load_macro_in_child() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}{{ 1 }}{% endmacro hello %}"), ("parent", "{% block bob %}{% endblock bob %}"), ("child", "{% extends \"parent\" %}{% import \"macros\" as macros %}{% block bob %}{{ macros::hello() }}{% endblock bob %}"), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!(result.unwrap(), "1".to_string()); } // https://github.com/Keats/tera/issues/333 // this test fails in 0.11.14, worked in 0.11.10 #[test] fn can_inherit_macro_import_from_parent() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}HELLO{% endmacro hello %}"), ("parent", "{% import \"macros\" as macros %}{% block bob %}parent{% endblock bob %}"), ("child", "{% extends \"parent\" %}{% block bob %}{{macros::hello()}}{% endblock bob %}"), ]) .unwrap(); let result = tera.render("child", &Context::default()); assert_eq!(result.unwrap(), "HELLO".to_string()); } #[test] fn can_inherit_macro_import_from_grandparent() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}HELLO{% endmacro hello %}"), ("grandparent", "{% import \"macros\" as macros %}{% block bob %}grandparent{% endblock bob %}"), ("parent", "{% extends \"grandparent\" %}{% import \"macros\" as macros2 %}{% block bob %}parent{% endblock bob %}"), ("child", "{% extends \"parent\" %}{% block bob %}{{macros::hello()}}-{{macros2::hello()}}{% endblock bob %}"), ]).unwrap(); let result = tera.render("child", &Context::default()); assert_eq!(result.unwrap(), "HELLO-HELLO".to_string()); } #[test] fn can_load_macro_in_parent_with_grandparent() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello()%}{{ 1 }}{% endmacro hello %}"), ("grandparent", "{% block bob %}{% endblock bob %}"), ("parent", "{% extends \"grandparent\" %}{% import \"macros\" as macros %}{% block bob %}{{ macros::hello() }} - Hey{% endblock bob %}"), ("child", "{% extends \"parent\" %}{% block bob %}{{ super() }}{% endblock bob %}"), ]).unwrap(); let result = tera.render("child", &Context::new()); assert_eq!(result.unwrap(), "1 - Hey".to_string()); } #[test] fn macro_can_load_macro_from_macro_files() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("submacros", "{% macro emma() %}Emma{% endmacro emma %}"), ("macros", "{% import \"submacros\" as submacros %}{% macro hommage() %}{{ submacros::emma() }} was an amazing person!{% endmacro hommage %}"), ("parent", "{% block main %}Someone was a terrible person!{% endblock main %} Don't you think?"), ("child", "{% extends \"parent\" %}{% import \"macros\" as macros %}{% block main %}{{ macros::hommage() }}{% endblock main %}") ]).unwrap(); let result = tera.render("child", &Context::new()); //println!("{:#?}", result); assert_eq!(result.unwrap(), "Emma was an amazing person! Don't you think?".to_string()); } #[test] fn macro_can_access_global_context() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("parent", r#"{% import "macros" as macros %}{{ macros::test_global() }}"#), ("macros", r#"{% macro test_global() %}{% set_global value1 = "42" %}{% for i in range(end=1) %}{% set_global value2 = " is the truth." %}{% endfor %}{{ value1 }}{% endmacro test_global %}"#) ]).unwrap(); let result = tera.render("parent", &Context::new()); assert_eq!(result.unwrap(), "42".to_string()); } #[test] fn template_cant_access_macros_context() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("parent", r#"{% import "macros" as macros %}{{ macros::empty() }}{{ quote | default(value="I'd rather have roses on my table than diamonds on my neck.") }}"#), ("macros", r#"{% macro empty() %}{% set_global quote = "This should not reachable from the calling template!" %}{% endmacro empty %}"#) ]).unwrap(); let result = tera.render("parent", &Context::new()); assert_eq!(result.unwrap(), "I'd rather have roses on my table than diamonds on my neck."); } #[test] fn parent_macro_cant_access_child_macro_context() { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("parent", "{% import \"macros\" as macros %}{{ macros::test_global() }}"), ("macros", r#"{% import "moremacros" as moremacros %}{% macro test_global() %}{% set_global value1 = "ACAB" %}{{ moremacros::another_one() }}{{ value1 }}-{{ value2 | default(value="ACAB") }}{% endmacro test_global %}"#), ("moremacros", r#"{% macro another_one() %}{% set_global value2 = "1312" %}{% endmacro another_one %}"#) ]).unwrap(); let result = tera.render("parent", &Context::new()); assert_eq!(result.unwrap(), "ACAB-ACAB".to_string()); } tera-1.20.0/src/renderer/tests/mod.rs000064400000000000000000000011461046102023000155250ustar 00000000000000use serde_derive::Serialize; mod basic; mod errors; mod inheritance; mod macros; mod square_brackets; mod whitespace; #[allow(dead_code)] #[derive(Debug, Serialize)] pub struct NestedObject { pub label: String, pub parent: Option>, pub numbers: Vec, } #[derive(Debug, Serialize)] pub struct Review { title: String, paragraphs: Vec, } impl Review { #[allow(dead_code)] pub fn new() -> Review { Review { title: "My review".to_owned(), paragraphs: vec!["A".to_owned(), "B".to_owned(), "C".to_owned()], } } } tera-1.20.0/src/renderer/tests/square_brackets.rs000064400000000000000000000057361046102023000201350ustar 00000000000000use std::collections::HashMap; use crate::context::Context; use crate::tera::Tera; use serde_derive::Serialize; #[derive(Serialize)] struct Test { a: String, b: String, c: Vec, } #[test] fn var_access_by_square_brackets() { let mut context = Context::new(); context.insert( "var", &Test { a: "hi".into(), b: "i_am_actually_b".into(), c: vec!["fred".into()] }, ); context.insert("zero", &0); context.insert("a", "b"); let mut map = HashMap::new(); map.insert("true", "yes"); map.insert("false", "no"); map.insert("with space", "works"); map.insert("with/slash", "works"); let mut deep_map = HashMap::new(); deep_map.insert("inner_map", &map); context.insert("map", &map); context.insert("deep_map", &deep_map); context.insert("bool_vec", &vec!["true", "false"]); let inputs = vec![ ("{{var.a}}", "hi"), ("{{var['a']}}", "hi"), ("{{var[\"a\"]}}", "hi"), ("{{var['c'][0]}}", "fred"), ("{{var['c'][zero]}}", "fred"), ("{{var[a]}}", "i_am_actually_b"), ("{{map['with space']}}", "works"), ("{{map['with/slash']}}", "works"), ("{{deep_map['inner_map'][bool_vec[zero]]}}", "yes"), ]; for (input, expected) in inputs { let result = Tera::one_off(input, &context, true).unwrap(); println!("{:?} -> {:?} = {:?}", input, expected, result); assert_eq!(result, expected); } } #[test] fn var_access_by_square_brackets_errors() { let mut context = Context::new(); context.insert("var", &Test { a: "hi".into(), b: "there".into(), c: vec![] }); let t = Tera::one_off("{{var[csd]}}", &context, true); assert!(t.is_err(), "Access of csd should be impossible"); } // https://github.com/Keats/tera/issues/334 #[test] fn var_access_by_loop_index() { let context = Context::new(); let res = Tera::one_off( r#" {% set ics = ["fa-rocket","fa-paper-plane","fa-diamond","fa-signal"] %} {% for a in ics %} {{ ics[loop.index0] }} {% endfor %} "#, &context, true, ); assert!(res.is_ok()); } // https://github.com/Keats/tera/issues/334 #[test] fn var_access_by_loop_index_with_set() { let context = Context::new(); let res = Tera::one_off( r#" {% set ics = ["fa-rocket","fa-paper-plane","fa-diamond","fa-signal"] %} {% for a in ics %} {% set i = loop.index - 1 %} {{ ics[i] }} {% endfor %} "#, &context, true, ); assert!(res.is_ok()); } // https://github.com/Keats/tera/issues/754 #[test] fn can_get_value_if_key_contains_period() { let mut context = Context::new(); context.insert("name", "Mt. Robson Provincial Park"); let mut map = HashMap::new(); map.insert("Mt. Robson Provincial Park".to_string(), "hello".to_string()); context.insert("tag_info", &map); let res = Tera::one_off(r#"{{ tag_info[name] }}"#, &context, true); assert!(res.is_ok()); let res = res.unwrap(); assert_eq!(res, "hello"); } tera-1.20.0/src/renderer/tests/whitespace.rs000064400000000000000000000104171046102023000171030ustar 00000000000000use crate::context::Context; use crate::tera::Tera; #[test] fn can_remove_whitespace_basic() { let mut context = Context::new(); context.insert("numbers", &vec![1, 2, 3]); let inputs = vec![ (" {%- for n in numbers %}{{n}}{% endfor -%} ", "123"), ("{%- for n in numbers %} {{n}}{%- endfor -%} ", " 1 2 3"), ("{%- for n in numbers -%}\n {{n}}\n {%- endfor -%} ", "123"), ("{%- if true -%}\n {{numbers}}\n {%- endif -%} ", "[1, 2, 3]"), ("{%- if false -%}\n {{numbers}}\n {% else %} Nope{%- endif -%} ", " Nope"), (" {%- if false -%}\n {{numbers}}\n {% else -%} Nope {%- endif -%} ", "Nope"), (" {%- if false -%}\n {{numbers}}\n {% elif true -%} Nope {%- endif -%} ", "Nope"), (" {%- if false -%}\n {{numbers}}\n {% elif false -%} Nope {% else %} else {%- endif -%} ", " else"), (" {%- set var = 2 -%} {{var}}", "2"), (" {% set var = 2 -%} {{var}}", " 2"), (" {% raw -%} {{2}} {% endraw -%} ", " {{2}} "), (" {% filter upper -%} hey {%- endfilter -%} ", " HEY"), (" {{ \"hello\" -}} ", " hello"), (" {{- \"hello\" }} ", "hello "), (" {{- \"hello\" -}} ", "hello"), // Comments are not rendered so it should be just whitespace if anything (" {#- \"hello\" -#} ", ""), (" {# \"hello\" -#} ", " "), (" {#- \"hello\" #} ", " "), ]; for (input, expected) in inputs { let mut tera = Tera::default(); tera.add_raw_template("tpl", input).unwrap(); println!("{} -> {:?}", input, expected); assert_eq!(tera.render("tpl", &context).unwrap(), expected); } } #[test] fn can_remove_whitespace_include() { let mut context = Context::new(); context.insert("numbers", &vec![1, 2, 3]); let inputs = vec![ (r#"Hi {%- include "include" -%} "#, "HiIncluded"), (r#"Hi {% include "include" -%} "#, "Hi Included"), (r#"Hi {% include "include" %} "#, "Hi Included "), ]; for (input, expected) in inputs { let mut tera = Tera::default(); tera.add_raw_templates(vec![("include", "Included"), ("tpl", input)]).unwrap(); assert_eq!(tera.render("tpl", &context).unwrap(), expected); } } #[test] fn can_remove_whitespace_macros() { let mut context = Context::new(); context.insert("numbers", &vec![1, 2, 3]); let inputs = vec![ (r#" {%- import "macros" as macros -%} {{macros::hey()}}"#, "Hey!"), (r#" {% import "macros" as macros %} {{macros::hey()}}"#, "Hey!"), (r#" {%- import "macros" as macros %} {%- set hey = macros::hey() -%} {{hey}}"#, "Hey!"), ]; for (input, expected) in inputs { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hey() -%} Hey! {%- endmacro %}"), ("tpl", input), ]) .unwrap(); assert_eq!(tera.render("tpl", &context).unwrap(), expected); } } #[test] fn can_remove_whitespace_inheritance() { let mut context = Context::new(); context.insert("numbers", &vec![1, 2, 3]); let inputs = vec![ (r#"{%- extends "base" -%} {% block content %}{{super()}}{% endblock %}"#, " Hey! "), (r#"{%- extends "base" -%} {% block content -%}{{super()}}{%- endblock %}"#, " Hey! "), (r#"{%- extends "base" %} {%- block content -%}{{super()}}{%- endblock -%} "#, " Hey! "), ]; for (input, expected) in inputs { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("base", "{% block content %} Hey! {% endblock %}"), ("tpl", input), ]) .unwrap(); assert_eq!(tera.render("tpl", &context).unwrap(), expected); } } // https://github.com/Keats/tera/issues/475 #[test] fn works_with_filter_section() { let mut context = Context::new(); context.insert("d", "d"); let input = r#"{% filter upper %} {{ "c" }} d{% endfilter %}"#; let res = Tera::one_off(input, &context, true).unwrap(); assert_eq!(res, " C D"); } #[test] fn make_sure_not_to_delete_whitespaces() { let mut context = Context::new(); context.insert("d", "d"); let input = r#"{% raw %} yaml_test: {% endraw %}"#; let res = Tera::one_off(input, &context, true).unwrap(); assert_eq!(res, " yaml_test: "); } tera-1.20.0/src/template.rs000064400000000000000000000141001046102023000136030ustar 00000000000000use std::collections::HashMap; use crate::errors::{Error, Result}; use crate::parser::ast::{Block, MacroDefinition, Node}; use crate::parser::{parse, remove_whitespace}; /// This is the parsed equivalent of a template file. /// It also does some pre-processing to ensure it does as little as possible at runtime /// Not meant to be used directly. #[derive(Debug, Clone)] pub struct Template { /// Name of the template, usually very similar to the path pub name: String, /// Original path of the file. A template doesn't necessarily have /// a file associated with it though so it's optional. pub path: Option, /// Parsed AST, after whitespace removal pub ast: Vec, /// Whether this template came from a call to `Tera::extend`, so we do /// not remove it when we are doing a template reload pub from_extend: bool, /// Macros defined in that file: name -> definition ast pub macros: HashMap, /// (filename, namespace) for the macros imported in that file pub imported_macro_files: Vec<(String, String)>, /// Only used during initial parsing. Rendering will use `self.parents` pub parent: Option, /// Only used during initial parsing. Rendering will use `self.blocks_definitions` pub blocks: HashMap, // Below are filled when all templates have been parsed so we know the full hierarchy of templates /// The full list of parent templates pub parents: Vec, /// The definition of all the blocks for the current template and the definition of those blocks /// in parent templates if there are some. /// Needed for super() to work without having to find them each time. /// The type corresponds to the following `block_name -> [(template name, definition)]` /// The order of the Vec is from the first in hierarchy to the current template and the template /// name is needed in order to load its macros if necessary. pub blocks_definitions: HashMap>, } impl Template { /// Parse the template string given pub fn new(tpl_name: &str, tpl_path: Option, input: &str) -> Result