Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"000a2a4430bf0c0b66fa9d4e6307b2c335d19f94dd28c49ea0390c26975db3a6","CHANGELOG.md":"1ad63b204035771dadb8e7ecf56e1eab7af6fdb8dafc1f342af47bc996386e1c","Cargo.lock":"332d2fe4bcc592e6486d8138bfcd5635f83c980d0b88b16d240655d187e77ac2","Cargo.toml":"76d8f08e81505aa985e47252609310923d7164fb83663d92b5f2cd003ef0efe5","Cargo.toml.orig":"8f6357ef29e94f8c065695de3388aabc5bb57993d2e05049e7d841f9c71441f5","LICENSE":"8a51762984f85322bcd303a36eb0108a4bc9314e8f060a6794e2f2777b05eed5","README.md":"15da45ec0074781519eeb12b4ddf046f433c0af4b2b37e39c88034118100af7a","src/builtins/filters/array.rs":"41583e61feaf29515251bc65a11f939b4fc8d627c20373e2dcaaff39291881c7","src/builtins/filters/common.rs":"909744d20378370a5e7f381a87d9ade263c4cd5b25d38b7aeca55e4f56363dfd","src/builtins/filters/mod.rs":"c43a98c02fb6fd6a004a83a4ec354776f607e06ea49455cbf2244d8e2eae0813","src/builtins/filters/number.rs":"9544cbd222d6a610eedb416dbee1de360c6b587f73901617068309230538cb5d","src/builtins/filters/object.rs":"b3b3132872d15599010e3a6b7f7f4df1f15450b5086cf86bdb888ddba97ad0f7","src/builtins/filters/string.rs":"e8755c66343f63c94aab32c0910faded76a980541ebd201a0dee674d500dcae4","src/builtins/functions.rs":"10c00cee65515c2a95daa0b0bfe4dfbc4fa3a9ae678a42eb01c2dafa198f0ff1","src/builtins/mod.rs":"c4ceef5b71551ba03d13fbd13c38fac8bbd400a876c2231bf0c147be5aff7961","src/builtins/testers.rs":"bfaf0f0dcbb63360449852eb29d915e1640e6c8304cf6dbb55b9e108fe24ac08","src/context.rs":"932152e0e100b73e713fdb22a5c5c22e0f73e9aa44ff816e8b9b7cf92051f874","src/errors.rs":"93eedd022d9b3f548985a1ba0348c2c46609d71ae482a728cadbd24f247e4f9f","src/filter_utils.rs":"3c18e3f1fd30240342168611a58932c11115286d15cc46e5d787f8e8ead892f3","src/lib.rs":"7e6bf17a40784a96e3e729bc9bf225c0cb2b1ef64fcb153835150ac8a50c4a4a","src/macros.rs":"e46ee0d8f0974e9945dbf594eb71e81520fc4ac1968bd10c285e3c9357ea6558","src/parser/ast.rs":"c0b3ae506131e74e5ecbea4f713bcf810e8a37890a9479dc6b63e5526b0abb1b","src/parser/mod.rs":"35f2c2a308bd900defada423b6e3541bd59679fb3dd2c63b47e4a553b8efad3e","src/parser/tera.pest":"4bca7c93833f6e931ab26a89ca395f94407a2d3582c2c3b65f59f94b5a6868c1","src/parser/tests/errors.rs":"49a9711d4ad20e334f700247277e538945d9fe26a5c9618826d7c3294a183400","src/parser/tests/lexer.rs":"809da333071873a2a731bfdf67bdd24fb57432ccb99b1bfe9ee3afee890db101","src/parser/tests/mod.rs":"a1f1580db6a0d5b45d1ae15dfed8fad1a50fd9d57c763a87566b6695fccab889","src/parser/tests/parser.rs":"f58dd20d55cb56f36cd5613e3fead367ab3c142059cbc25787bd552025b09447","src/parser/tests/whitespace.rs":"1b453f8854c25f56594e3733ae5cbbdc74b47884dba8a161016f15328620cc57","src/parser/whitespace.rs":"696bc5eb68526431643cce25740854fa1dd28e92e5f78c5381635ff776e59351","src/renderer/call_stack.rs":"3ea5a9cc26f76adb058eefc84145ac00840e5e1e2bd7a2d1ded34075ba9368b7","src/renderer/for_loop.rs":"fb2616399eb12a67244a37677794437879553965487f651e8e5d505dfb609502","src/renderer/macros.rs":"d03301bdab883eafd9ea09b23de62743bc8adee5d991f9f778ee8122347133d2","src/renderer/mod.rs":"69c92d17dcc0dafd654e51c665dafd0a386b658072bcf90caa5aeeec6054a69b","src/renderer/processor.rs":"a6cdcff9bdb7c8d575809cb86f4e19856bbb64500101f375f1d5e4c7f3d7d754","src/renderer/square_brackets.rs":"69b21dd48e2520b3360111f6c6f125b8ab702cc52affe5b047fccb3eda38e07d","src/renderer/stack_frame.rs":"073ef623beb55b5ca8e80f29467cc9ee7676cef79231544e645f11ebfdc2b81d","src/renderer/tests/basic.rs":"bb14f63638684a07745a53e0af4d2af8a51ad768474401707251dffe94fe5d62","src/renderer/tests/errors.rs":"f80953b1be2fbb5fc50ce2e74121ea88a2b7bc8b4a34a6626b02974cd298f871","src/renderer/tests/inheritance.rs":"14475f5bcbae86a0acc1d1547a8f20177b839ec09341758a82525ef71fc7d366","src/renderer/tests/macros.rs":"ef41f0e8ef3b4b1c9a851cb8b76d2031e0b9fe83a13ab61ebfa142f341edd535","src/renderer/tests/mod.rs":"1ca5a8f0a0921517519844cf79cd86f302e91d284d3866b733397982589a25cd","src/renderer/tests/square_brackets.rs":"23a8c4f52e7c21f57ebc30086d2a714efa525bb701f1f146d1096a3bce33a005","src/renderer/tests/whitespace.rs":"849b1098e868522af69c5c97614a695182a54a505241d8c5ccf3b9f240f40742","src/template.rs":"9448d276fbe950d1d6af68df7ca8b83efb8f7b68589e100d6c7a57d0c4ab1081","src/tera.rs":"e3270ab8ba5965f80433717965ba5fb983d9af8495e2fdd0cb06b76933d693f7","src/utils.rs":"d1ab843cd3ab5893207970b3913609817cda7fb57b3d22bd719574a71f7a788b"},"package":"e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "4e5145e2231c7b7b5939b1aa4bc85789a8a75fcd"
},
"path_in_vcs": ""
}
+547
View File
@@ -0,0 +1,547 @@
# Changelog
## 1.21.1 (2025-10-30)
- Replace unmaintained unic-segment dependency
## 1.20.0 (2024-05-27)
- Support parenthesis in if statements
- 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<Value>, Vec<Value>) -> Result<bool>` to `fn(Option<Value>, Vec<Value>) -> Result<bool>`
- 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
+967
View File
@@ -0,0 +1,967 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[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.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[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.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cc"
version = "1.2.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"num-traits",
"pure-rust-locales",
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[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.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
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.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[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.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
[[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.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "globwalk"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags",
"ignore",
"walkdir",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[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.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "ignore"
version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]]
name = "indexmap"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[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.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[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.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4"
dependencies = [
"memchr",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a"
dependencies = [
"pest",
"sha2",
]
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pure-rust-locales"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "869675ad2d7541aea90c6d88c81f46a7f4ea9af8cd0395d38f11a95126998a0d"
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[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 0.2.16",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rustix"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"indexmap",
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slug"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
dependencies = [
"deunicode",
"wasm-bindgen",
]
[[package]]
name = "syn"
version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "tera"
version = "1.20.1"
dependencies = [
"chrono",
"chrono-tz",
"globwalk",
"humansize",
"lazy_static",
"percent-encoding",
"pest",
"pest_derive",
"pretty_assertions",
"rand",
"regex",
"serde",
"serde_derive",
"serde_json",
"slug",
"tempfile",
"unicode-segmentation",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[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.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+125
View File
@@ -0,0 +1,125 @@
# 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.85"
name = "tera"
version = "1.20.1"
authors = ["Vincent Prouillet <hello@prouilletvincent.com>"]
build = false
include = [
"/src/**/*",
"/LICENSE",
"/README.md",
"/CHANGELOG.md",
]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
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"
[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"]
[lib]
name = "tera"
path = "src/lib.rs"
[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.unicode-segmentation]
version = "1.12"
[dev-dependencies.pretty_assertions]
version = "1"
[dev-dependencies.serde_derive]
version = "1.0"
[dev-dependencies.tempfile]
version = "3"
+51
View File
@@ -0,0 +1,51 @@
[package]
name = "tera"
version = "1.20.1"
authors = ["Vincent Prouillet <hello@prouilletvincent.com>"]
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.85"
[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
unicode-segmentation = "1.12"
# 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"]
+22
View File
@@ -0,0 +1,22 @@
The 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.
+28
View File
@@ -0,0 +1,28 @@
# 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
<title>{% block title %}{% endblock title %}</title>
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
```
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.
+840
View File
@@ -0,0 +1,840 @@
/// 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<String, Value>) -> Result<Value> {
let arr = try_get_value!("nth", "value", Vec<Value>, 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<String, Value>) -> Result<Value> {
let mut arr = try_get_value!("first", "value", Vec<Value>, 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<String, Value>) -> Result<Value> {
let mut arr = try_get_value!("last", "value", Vec<Value>, 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<String, Value>) -> Result<Value> {
let arr = try_get_value!("join", "value", Vec<Value>, 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::<Result<Vec<_>>>()?;
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<String, Value>) -> Result<Value> {
let arr = try_get_value!("sort", "value", Vec<Value>, 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<String, Value>) -> Result<Value> {
let arr = try_get_value!("unique", "value", Vec<Value>, 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::<Result<Vec<_>>>();
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<String, Value>) -> Result<Value> {
let arr = try_get_value!("group_by", "value", Vec<Value>, 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<String, Value>) -> Result<Value> {
let mut arr = try_get_value!("filter", "value", Vec<Value>, 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::<Vec<_>>();
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<String, Value>) -> Result<Value> {
let arr = try_get_value!("map", "value", Vec<Value>, 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::<Vec<_>>();
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<String, Value>) -> Result<Value> {
let arr = try_get_value!("slice", "value", Vec<Value>, 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::<Value>::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<String, Value>) -> Result<Value> {
let mut arr = try_get_value!("concat", "value", Vec<Value>, 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<Value> = 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<Value> = 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<Value> = 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<Value> = 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::<f64>::new()).unwrap();
let args = HashMap::new();
let result = sort(&v, &args);
assert!(result.is_ok());
assert_eq!(result.unwrap(), to_value(Vec::<f64>::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::<f64>::new()).unwrap();
let args = HashMap::new();
let result = sort(&v, &args);
assert!(result.is_ok());
assert_eq!(result.unwrap(), to_value(Vec::<f64>::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<usize>, end: Option<f64>) -> HashMap<String, Value> {
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());
}
}
+488
View File
@@ -0,0 +1,488 @@
/// 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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
let format = match args.get("format") {
Some(val) => try_get_value!("date", "format", String, val),
None => "%Y-%m-%d".to_string(),
};
let items: Vec<Item> =
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::<Tz>() {
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::<DateTime<FixedOffset>>() {
Ok(val) => match timezone {
Some(timezone) => {
val.with_timezone(&timezone).format_localized(&format, locale)
}
None => val.format_localized(&format, locale),
},
Err(_) => match s.parse::<NaiveDateTime>() {
Ok(val) => DateTime::<Utc>::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::<Utc>::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::<DateTime<FixedOffset>>() {
Ok(val) => match timezone {
Some(timezone) => val.with_timezone(&timezone).format(&format),
None => val.format(&format),
},
Err(_) => match s.parse::<NaiveDateTime>() {
Ok(val) => {
DateTime::<Utc>::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::<Utc>::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<String, Value>) -> Result<Value> {
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<String, String> = 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<String, String> = 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> = 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()
);
}
}
+30
View File
@@ -0,0 +1,30 @@
use 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<String, Value>) -> Result<Value>;
/// Whether the current filter's output should be treated as safe, defaults to `false`
fn is_safe(&self) -> bool {
false
}
}
impl<F> Filter for F
where
F: Fn(&Value, &HashMap<String, Value>) -> Result<Value> + Sync + Send,
{
fn filter(&self, value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
self(value, args)
}
}
+236
View File
@@ -0,0 +1,236 @@
/// 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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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());
}
}
@@ -0,0 +1,90 @@
/// 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<String, Value>) -> Result<Value> {
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());
}
}
+937
View File
@@ -0,0 +1,937 @@
/// 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 unicode_segmentation::UnicodeSegmentation;
#[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<first>[\w'])(?P<rest>[\w']*)\b").unwrap();
static ref SPACELESS_RE: Regex = Regex::new(r">\s+<").unwrap();
}
/// Convert a value to uppercase.
pub fn upper(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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 = s.grapheme_indices(true).collect::<Vec<(usize, &str)>>();
// 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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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::<String>() + &chars.as_str().to_lowercase();
Ok(to_value(res).unwrap())
}
}
}
/// Percent-encodes reserved URI characters
#[cfg(feature = "urlencode")]
pub fn urlencode(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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 (`<br>`).
///
/// Example: The input "Hello\nWorld" turns into "Hello<br>World".
pub fn linebreaksbr(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
let s = try_get_value!("linebreaksbr", "value", String, value);
Ok(to_value(s.replace("\r\n", "<br>").replace('\n', "<br>")).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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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<String, Value>) -> Result<Value> {
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("&amp;"),
'<' => output.push_str("&lt;"),
'>' => output.push_str("&gt;"),
'"' => output.push_str("&quot;"),
'\'' => output.push_str("&apos;"),
_ => output.push(c),
}
}
Ok(Value::String(output))
}
/// Split the given string by the given pattern.
pub fn split(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
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::<Vec<_>>()).unwrap())
}
/// Convert the value to a signed integer number
pub fn int(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
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::<f64>() {
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<String, Value>) -> Result<Value> {
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::<f64>().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("<br>").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<br>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#"<a>'"#, r#"<a>\'"#),
(
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 <bar>", "Foo <Bar>"),
(" 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"<b>Joel</b> <button>is</button> a <span>slug</span>", "Joel is a slug"),
(
r#"<p>just a small \n <a href="x"> example</a> link</p>\n<p>to a webpage</p><!-- <p>and some commented stuff</p> -->"#,
r#"just a small \n example link\nto a webpage"#,
),
(
r"<p>See: &#39;&eacute; is an apostrophe followed by e acute</p>",
r"See: &#39;&eacute; is an apostrophe followed by e acute",
),
(r"<adf>a", "a"),
(r"</adf>a", "a"),
(r"<asdf><asdf>e", "e"),
(r"hi, <f x", "hi, <f x"),
("234<235, right?", "234<235, right?"),
("a4<a5 right?", "a4<a5 right?"),
("b7>b2!", "b7>b2!"),
("</fe", "</fe"),
("<x>b<y>", "b"),
(r#"a<p a >b</p>c"#, "abc"),
(r#"d<a:b c:d>e</p>f"#, "def"),
(r#"<strong>foo</strong><a href="http://example.com">bar</a>"#, "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![
("<p>\n<a>test</a>\r\n </p>", "<p><a>test</a></p>"),
("<p>\n<a> </a>\r\n </p>", "<p><a></a></p>"),
("<p> </p>", "<p></p>"),
("<p> <a>", "<p><a>"),
("<p> test</p>", "<p> test</p>"),
("<p>\r\n</p>", "<p></p>"),
];
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-&amp;-ho"),
(r"hey-'-ho", "hey-&apos;-ho"),
(r"hey-&'-ho", "hey-&amp;&apos;-ho"),
(r#"hey-&'"-ho"#, "hey-&amp;&apos;&quot;-ho"),
(r#"hey-&'"<-ho"#, "hey-&amp;&apos;&quot;&lt;-ho"),
(r#"hey-&'"<>-ho"#, "hey-&amp;&apos;&quot;&lt;&gt;-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<br>world"),
("hello\r\nworld", "hello<br>world"),
("hello\n\rworld", "hello<br>\rworld"),
("hello\r\n\nworld", "hello<br><br>world"),
("hello<br>world\n", "hello<br>world<br>"),
];
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());
}
}
}
+341
View File
@@ -0,0 +1,341 @@
use 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<String, Value>) -> Result<Value>;
/// Whether the current function's output should be treated as safe, defaults to `false`
fn is_safe(&self) -> bool {
false
}
}
impl<F> Function for F
where
F: Fn(&HashMap<String, Value>) -> Result<Value> + Sync + Send,
{
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
self(args)
}
}
pub fn range(args: &HashMap<String, Value>) -> Result<Value> {
let start = match args.get("start") {
Some(val) => match from_value::<usize>(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::<usize>(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::<usize>(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<String, Value>) -> Result<Value> {
let utc = match args.get("utc") {
Some(val) => match from_value::<bool>(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::<bool>(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<String, Value>) -> Result<Value> {
match args.get("message") {
Some(val) => match from_value::<String>(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<String, Value>) -> Result<Value> {
let start = match args.get("start") {
Some(val) => match from_value::<isize>(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::<isize>(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<String, Value>) -> Result<Value> {
let name = match args.get("name") {
Some(val) => match from_value::<String>(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");
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod filters;
pub mod functions;
pub mod testers;
+348
View File
@@ -0,0 +1,348 @@
use 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<bool>;
}
impl<F> Test for F
where
F: Fn(Option<&Value>, &[Value]) -> Result<bool> + Sync + Send,
{
fn test(&self, value: Option<&Value>, args: &[Value]) -> Result<bool> {
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<Value>`] 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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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<bool> {
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()
);
}
}
+512
View File
@@ -0,0 +1,512 @@
use 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<String, Value>,
}
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<T: Serialize + ?Sized, S: Into<String>>(&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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
/// # 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<T: Serialize + ?Sized, S: Into<String>>(
&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<Self> {
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<Self> {
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<Value> {
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<f64, ()>;
}
// Needed for all the maths
// Convert everything to f64, seems like a terrible idea
impl ValueNumber for Value {
fn to_number(&self) -> Result<f64, ()> {
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<Self::Item> {
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<usize> {
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);
}
}
+235
View File
@@ -0,0 +1,235 @@
use 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<String>,
},
/// 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<Box<dyn StdError + Sync + Send>>,
}
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<String>) -> 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<Box<dyn StdError + Send + Sync>>) -> 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<Box<dyn StdError + Send + Sync>>,
) -> 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<Box<dyn StdError + Send + Sync>>,
) -> 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<Box<dyn StdError + Send + Sync>>,
) -> 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<std::io::Error> 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<String> for Error {
fn from(e: String) -> Self {
Self::msg(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Self::json(e)
}
}
/// Convenient wrapper around std::Result.
pub type Result<T> = ::std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
#[test]
fn test_error_is_send_and_sync() {
fn test_send_sync<T: Send + Sync>() {}
test_send_sync::<super::Error>();
}
}
+222
View File
@@ -0,0 +1,222 @@
use 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<Ordering> {
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<Self>;
}
impl GetValue for OrderedF64 {
fn get_value(val: &Value) -> Result<Self> {
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<Self> {
val.as_i64().ok_or_else(|| Error::msg(format!("expected number got {}", val)))
}
}
impl GetValue for bool {
fn get_value(val: &Value) -> Result<Self> {
val.as_bool().ok_or_else(|| Error::msg(format!("expected bool got {}", val)))
}
}
impl GetValue for String {
fn get_value(val: &Value) -> Result<Self> {
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<Self> {
let arr =
val.as_array().ok_or_else(|| Error::msg(format!("expected array got {}", val)))?;
Ok(ArrayLen(arr.len()))
}
}
#[derive(Default)]
pub struct SortPairs<K: Ord> {
pairs: Vec<(Value, K)>,
}
type SortNumbers = SortPairs<OrderedF64>;
type SortBools = SortPairs<bool>;
type SortStrings = SortPairs<String>;
type SortArrays = SortPairs<ArrayLen>;
impl<K: GetValue> SortPairs<K> {
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<Value> {
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<Value>;
}
impl<K: GetValue> SortStrategy for SortPairs<K> {
fn try_add_pair(&mut self, val: &Value, key: &Value) -> Result<()> {
SortPairs::try_add_pair(self, val, key)
}
fn sort(&mut self) -> Vec<Value> {
SortPairs::sort(self)
}
}
pub fn get_sort_strategy_for_type(ty: &Value) -> Result<Box<dyn SortStrategy>> {
use crate::Value::*;
match *ty {
Null => Err(Error::msg("Null is not a sortable value")),
Bool(_) => Ok(Box::<SortBools>::default()),
Number(_) => Ok(Box::<SortNumbers>::default()),
String(_) => Ok(Box::<SortStrings>::default()),
Array(_) => Ok(Box::<SortArrays>::default()),
Object(_) => Err(Error::msg("Object is not a sortable value")),
}
}
#[derive(Default)]
pub struct Unique<K: Eq + std::hash::Hash> {
unique: std::collections::HashSet<K>,
}
type UniqueNumbers = Unique<i64>;
type UniqueBools = Unique<bool>;
struct UniqueStrings {
u: Unique<String>,
case_sensitive: bool,
}
pub trait UniqueStrategy {
fn insert(&mut self, val: &Value) -> Result<bool>;
}
impl<K: GetValue + Eq + std::hash::Hash> UniqueStrategy for Unique<K> {
fn insert(&mut self, val: &Value) -> Result<bool> {
Ok(self.unique.insert(K::get_value(val)?))
}
}
impl UniqueStrings {
fn new(case_sensitive: bool) -> UniqueStrings {
UniqueStrings { u: Unique::<String>::default(), case_sensitive }
}
}
impl UniqueStrategy for UniqueStrings {
fn insert(&mut self, val: &Value) -> Result<bool> {
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<Box<dyn UniqueStrategy>> {
use crate::Value::*;
match *ty {
Null => Err(Error::msg("Null is not a unique value")),
Bool(_) => Ok(Box::<UniqueBools>::default()),
Number(ref val) => {
if val.is_f64() {
Err(Error::msg("Unique floats are not implemented"))
} else {
Ok(Box::<UniqueNumbers>::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")),
}
}
+105
View File
@@ -0,0 +1,105 @@
#![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};
}
}
+35
View File
@@ -0,0 +1,35 @@
/// 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>, 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)
)));
}
}
}
}};
}
+347
View File
@@ -0,0 +1,347 @@
use 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<String, Expr>,
}
/// A mathematical expression
#[derive(Clone, Debug, PartialEq)]
pub struct MathExpr {
/// The left hand side of the expression
pub lhs: Box<Expr>,
/// The right hand side of the expression
pub rhs: Box<Expr>,
/// 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<Expr>,
/// The right hand side of the expression
pub rhs: Box<Expr>,
/// 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<ExprVal>,
}
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<Expr>,
/// The haystack, can be a string, an array or an ident only currently
pub rhs: Box<Expr>,
/// 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<Expr>),
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<FunctionCall>,
}
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<FunctionCall>) -> 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<Expr>,
}
/// 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<Node>,
}
/// 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<String, Expr>,
}
/// 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<String, Option<Expr>>,
/// The macro content
pub body: Vec<Node>,
}
/// A block definition
#[derive(Clone, Debug, PartialEq)]
pub struct Block {
/// The block name
pub name: String,
/// The block content
pub body: Vec<Node>,
}
/// 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<String>,
/// 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<Node>,
/// The body to execute in case of an empty object
pub empty_body: Option<Vec<Node>>,
}
/// 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<Node>)>,
/// The optional `else` block
pub otherwise: Option<(WS, Vec<Node>)>,
}
/// 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<String>, 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),
}
File diff suppressed because it is too large Load Diff
+320
View File
@@ -0,0 +1,320 @@
// 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
}
+303
View File
@@ -0,0 +1,303 @@
use 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"]);
}
+688
View File
@@ -0,0 +1,688 @@
use 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());
}
}
+4
View File
@@ -0,0 +1,4 @@
mod errors;
mod lexer;
mod parser;
mod whitespace;
File diff suppressed because it is too large Load Diff
+264
View File
@@ -0,0 +1,264 @@
use 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()),
]
);
}
+188
View File
@@ -0,0 +1,188 @@
use 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<Node>, body_ws: Option<WS>) -> Vec<Node> {
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
}
+230
View File
@@ -0,0 +1,230 @@
use 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<StackFrame<'a>>,
/// 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<Val<'a>> {
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()
}
}
+225
View File
@@ -0,0 +1,225 @@
use std::borrow::Cow;
use serde_json::Value;
use unicode_segmentation::UnicodeSegmentation;
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 = values.as_str().expect("Is string").graphemes(true);
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<String>,
/// 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..]);
}
}
+119
View File
@@ -0,0 +1,119 @@
use 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<String, MacroDefinition>;
/// 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, &macro_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[&macro_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
)))
}
}
}
+62
View File
@@ -0,0 +1,62 @@
mod 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<String> {
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)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
/// Return a Vec of all substrings contained in '[ ]'s
/// Ignore quoted strings and integers.
pub fn pull_out_square_bracket(s: &str) -> Vec<String> {
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::<usize>().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::<String>::new());
assert_eq!(pull_out_square_bracket("['hi']"), Vec::<String>::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"]);
}
}
+200
View File
@@ -0,0 +1,200 @@
use 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<Val<'a>> {
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<ForLoop<'a>>,
/// 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<Val<'a>> {
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<Val<'a>> {
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<Val<'a>> {
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<String, Value> {
let mut context = HashMap::new();
for (key, val) in &self.context {
context.insert((*key).to_string(), val.clone().into_owned());
}
context
}
}
+992
View File
@@ -0,0 +1,992 @@
use 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<String> {
let mut tera = Tera::default();
tera.add_raw_template("hello.html", content).unwrap();
tera.register_function("get_number", |_: &HashMap<String, Value>| Ok(Value::Number(10.into())));
tera.register_function("get_true", |_: &HashMap<String, Value>| Ok(Value::Bool(true)));
tera.register_function("get_string", |_: &HashMap<String, Value>| {
Ok(Value::String("Hello".to_string()))
});
tera.render("hello.html", context)
}
#[test]
fn render_simple_string() {
let result = render_template("<h1>Hello world</h1>", &Context::new());
assert_eq!(result.unwrap(), "<h1>Hello world</h1>".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", &"<html>");
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 }}", "&lt;html&gt;"),
("{{ \"<html>\" }}", "&lt;html&gt;"),
("{{ \" html \" | upper | trim }}", "HTML"),
("{{ 'html' }}", "html"),
("{{ `html` }}", "html"),
// https://github.com/Keats/tera/issues/273
(
r#"{{ 'hangar new "Will Smoth <will_s@example.com>"' | safe }}"#,
r#"hangar new "Will Smoth <will_s@example.com>""#,
),
("{{ malicious | safe }}", "<html>"),
("{{ malicious | upper }}", "&lt;HTML&gt;"),
("{{ malicious | upper | safe }}", "<HTML>"),
("{{ malicious | safe | upper }}", "&lt;HTML&gt;"),
("{{ 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='<br>') | safe }}",
"Animal Alphabets<br>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", &"<html>");
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", &"<html>");
let inputs = vec![
("{{ name }}", "john"),
("{{ malicious }}", "<html>"),
("{{ malicious | safe }}", "<html>"),
("{{ malicious | upper }}", "<HTML>"),
("{{ malicious | upper | safe }}", "<HTML>"),
("{{ malicious | safe | upper }}", "<HTML>"),
];
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>' }}", "&lt;html&gt;"),
("{{ '<html>' | safe }}", "<html>"),
("{{ 'hello' | safe | replace(from='h', to='&') }}", "&amp;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(), "&amp;ey");
}
#[test]
fn render_include_tag() {
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("world", "world"),
("hello", "<h1>Hello {% include \"world\" %}</h1>"),
])
.unwrap();
let result = tera.render("hello", &Context::new()).unwrap();
assert_eq!(result, "<h1>Hello world</h1>".to_owned());
}
#[test]
fn render_include_array_tag() {
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("world", "world"),
("hello", "<h1>Hello {% include [\"custom/world\", \"world\"] %}</h1>"),
])
.unwrap();
let result = tera.render("hello", &Context::new()).unwrap();
assert_eq!(result, "<h1>Hello world</h1>".to_owned());
tera.add_raw_template("custom/world", "custom world").unwrap();
let result = tera.render("hello", &Context::new()).unwrap();
assert_eq!(result, "<h1>Hello custom world</h1>".to_owned());
}
#[test]
fn render_include_tag_missing() {
let mut tera = Tera::default();
tera.add_raw_template("hello", "<h1>Hello {% include \"world\" %}</h1>").unwrap();
let result = tera.render("hello", &Context::new());
assert!(result.is_err());
let mut tera = Tera::default();
tera.add_raw_template("hello", "<h1>Hello {% include \"world\" ignore missing %}</h1>")
.unwrap();
let result = tera.render("hello", &Context::new()).unwrap();
assert_eq!(result, "<h1>Hello </h1>".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", "<h1>Hello {% include \"world\" %}</h1>"),
])
.unwrap();
let result = tera.render("hello", &Context::new()).unwrap();
assert_eq!(result, "<h1>Hello world</h1>".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", &"<html>");
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 = "<html>" %}{{ i | safe }}"#, "<html>"),
(r#"{% set i = "<html>" %}{{ i }}"#, "&lt;html&gt;"),
("{% set i = my_var %}{{ i }}", "hey"),
("{% set i = malicious %}{{ i | safe }}", "<html>"),
("{% set i = malicious %}{{ i }}", "&lt;html&gt;"),
("{% 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 %}<Hello>{% endfilter %}{% endfilter%}", "<HELLO>")
];
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::<Option<usize>, _>("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", &"<html>");
let result = render_template("{{ __tera_context }}", &context);
assert_eq!(
result.unwrap(),
r#"{
"html": "<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<usize> = 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<usize> = 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", &"<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": "<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", &"<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<String, Value>) -> Result<Value> {
Ok(Value::Number(self.0.fetch_add(1, Ordering::Relaxed).into()))
}
}
#[derive(Clone)]
struct SharedNext(Arc<Next>);
impl Function for SharedNext {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
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",
"<h1>{{ get_next() }}, {{ get_next_shared() }}, {{ get_next() }}...</h1>",
)
.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(),
"<h1>1, 1, 2...</h1>".to_owned()
);
assert_eq!(
make_tera().render("fn.html", &Context::new()).unwrap(),
"<h1>1, 2, 2...</h1>".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<String, Value>) -> Result<Value> {
Ok(Value::String(format!("<div>{}</div>", 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(), "<div>Hello</div>");
}
#[test]
fn safe_function_works() {
struct Safe;
impl crate::Function for Safe {
fn call(&self, _args: &HashMap<String, Value>) -> Result<Value> {
Ok(Value::String("<div>Hello</div>".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(), "<div>Hello</div>");
}
+258
View File
@@ -0,0 +1,258 @@
use 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 -%}
<p>{{ comment.content }}</p>
<p>{{ whocares.content }}</p>
<p>{{ doesntmatter.content }}</p>
{% 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"
);
}
@@ -0,0 +1,176 @@
use 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());
}
+369
View File
@@ -0,0 +1,369 @@
use 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());
}
+32
View File
@@ -0,0 +1,32 @@
use 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<Box<NestedObject>>,
pub numbers: Vec<usize>,
}
#[derive(Debug, Serialize)]
pub struct Review {
title: String,
paragraphs: Vec<String>,
}
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()],
}
}
}
@@ -0,0 +1,110 @@
use 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<String>,
}
#[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");
}
@@ -0,0 +1,118 @@
use 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: ");
}
+170
View File
@@ -0,0 +1,170 @@
use 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<String>,
/// Parsed AST, after whitespace removal
pub ast: Vec<Node>,
/// 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<String, MacroDefinition>,
/// (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<String>,
/// Only used during initial parsing. Rendering will use `self.blocks_definitions`
pub blocks: HashMap<String, Block>,
// 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<String>,
/// 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<String, Vec<(String, Block)>>,
}
impl Template {
/// Parse the template string given
pub fn new(tpl_name: &str, tpl_path: Option<String>, input: &str) -> Result<Template> {
let ast = remove_whitespace(parse(input)?, None);
// First we want all the blocks used in that template
// This is recursive as we can have blocks inside blocks
let mut blocks = HashMap::new();
fn find_blocks(ast: &[Node], blocks: &mut HashMap<String, Block>) -> Result<()> {
for node in ast {
match *node {
Node::Block(_, ref block, _) => {
if blocks.contains_key(&block.name) {
return Err(Error::msg(format!(
"Block `{}` is duplicated",
block.name
)));
}
blocks.insert(block.name.to_string(), block.clone());
find_blocks(&block.body, blocks)?;
}
_ => continue,
};
}
Ok(())
}
find_blocks(&ast, &mut blocks)?;
// And now we find the potential parent and everything macro related (definition, import)
let mut macros = HashMap::new();
let mut imported_macro_files = vec![];
let mut parent = None;
for node in &ast {
match *node {
Node::Extends(_, ref name) => parent = Some(name.to_string()),
Node::MacroDefinition(_, ref macro_def, _) => {
if macros.contains_key(&macro_def.name) {
return Err(Error::msg(format!(
"Macro `{}` is duplicated",
macro_def.name
)));
}
macros.insert(macro_def.name.clone(), macro_def.clone());
}
Node::ImportMacro(_, ref tpl_name, ref namespace) => {
imported_macro_files.push((tpl_name.to_string(), namespace.to_string()));
}
_ => continue,
}
}
Ok(Template {
name: tpl_name.to_string(),
path: tpl_path,
ast,
parent,
blocks,
macros,
imported_macro_files,
parents: vec![],
blocks_definitions: HashMap::new(),
from_extend: false,
})
}
}
#[cfg(test)]
mod tests {
use super::Template;
#[test]
fn can_parse_ok_template() {
Template::new("hello", None, "Hello {{ world }}.").unwrap();
}
#[test]
fn can_find_parent_template() {
let tpl = Template::new("hello", None, "{% extends \"base.html\" %}").unwrap();
assert_eq!(tpl.parent.unwrap(), "base.html".to_string());
}
#[test]
fn can_find_blocks() {
let tpl = Template::new(
"hello",
None,
"{% extends \"base.html\" %}{% block hey %}{% endblock hey %}",
)
.unwrap();
assert_eq!(tpl.parent.unwrap(), "base.html".to_string());
assert!(tpl.blocks.contains_key("hey"));
}
#[test]
fn can_find_nested_blocks() {
let tpl = Template::new(
"hello",
None,
"{% extends \"base.html\" %}{% block hey %}{% block extrahey %}{% endblock extrahey %}{% endblock hey %}",
).unwrap();
assert_eq!(tpl.parent.unwrap(), "base.html".to_string());
assert!(tpl.blocks.contains_key("hey"));
assert!(tpl.blocks.contains_key("extrahey"));
}
#[test]
fn can_find_macros() {
let tpl = Template::new("hello", None, "{% macro hey() %}{% endmacro hey %}").unwrap();
assert!(tpl.macros.contains_key("hey"));
}
#[test]
fn can_find_imported_macros() {
let tpl = Template::new("hello", None, "{% import \"macros.html\" as macros %}").unwrap();
assert_eq!(
tpl.imported_macro_files,
vec![("macros.html".to_string(), "macros".to_string())]
);
}
}
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
use crate::errors::Error;
/// Escape HTML following [OWASP](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)
///
/// Escape the following characters with HTML entity encoding to prevent switching
/// into any execution context, such as script, style, or event handlers. Using
/// hex entities is recommended in the spec. In addition to the 5 characters
/// significant in XML (&, <, >, ", '), the forward slash is included as it helps
/// to end an HTML entity.
///
/// ```text
/// & --> &amp;
/// < --> &lt;
/// > --> &gt;
/// " --> &quot;
/// ' --> &#x27; &apos; is not recommended
/// / --> &#x2F; forward slash is included as it helps end an HTML entity
/// ```
#[inline]
pub fn escape_html(input: &str) -> String {
let mut output = String::with_capacity(input.len() * 2);
for c in input.chars() {
match c {
'&' => output.push_str("&amp;"),
'<' => output.push_str("&lt;"),
'>' => output.push_str("&gt;"),
'"' => output.push_str("&quot;"),
'\'' => output.push_str("&#x27;"),
'/' => output.push_str("&#x2F;"),
_ => output.push(c),
}
}
// Not using shrink_to_fit() on purpose
output
}
pub(crate) fn render_to_string<C, F, E>(context: C, render: F) -> Result<String, Error>
where
C: FnOnce() -> String,
F: FnOnce(&mut Vec<u8>) -> Result<(), E>,
Error: From<E>,
{
let mut buffer = Vec::new();
render(&mut buffer).map_err(Error::from)?;
buffer_to_string(context, buffer)
}
pub(crate) fn buffer_to_string<F>(context: F, buffer: Vec<u8>) -> Result<String, Error>
where
F: FnOnce() -> String,
{
String::from_utf8(buffer).map_err(|error| Error::utf8_conversion_error(error, context()))
}
#[cfg(test)]
mod tests {
use super::escape_html;
use super::render_to_string;
#[test]
fn test_escape_html() {
let tests = vec![
(r"", ""),
(r"a&b", "a&amp;b"),
(r"<a", "&lt;a"),
(r">a", "&gt;a"),
(r#"""#, "&quot;"),
(r#"'"#, "&#x27;"),
(r#"大阪"#, "大阪"),
];
for (input, expected) in tests {
assert_eq!(escape_html(input), expected);
}
let empty = String::new();
assert_eq!(escape_html(&empty), empty);
}
#[test]
fn test_render_to_string() {
use std::io::Write;
let string = render_to_string(|| panic!(), |w| write!(w, "test")).unwrap();
assert_eq!(string, "test".to_owned());
}
}