diff --git a/.gitignore b/.gitignore index ce698d0..801d9ba 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ Cargo.lock # Backup files generated by rustfmt **/*.rs.bk + +# Javascript junk if you run the compliance tests sans docker +js/node_modules +js/package-lock.json diff --git a/Cargo.toml b/Cargo.toml index a7e16fe..189129c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,5 @@ path = "src/bin.rs" [dependencies] nom = { git = "https://github.com/tomalexander/nom.git", branch = "take_until_parser_matches" } +serde = "1.0.106" +serde_json = "1.0.51" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0bb15a1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM rustlang/rust:nightly-alpine3.10 + +# findutils needed to replace busybox xargs because it is failing to +# pass stdin through when using the `-a` flag +RUN apk --no-cache add bash git npm nodejs findutils + +# Create unprivileged user +RUN addgroup -S duster && adduser -S duster -G duster + +# Install LinkedIn DustJS. Installing it globally before copying in +# the repo to avoid spamming the npm servers with every codebase +# change. +RUN npm install -g dustjs-linkedin dustjs-helpers + + +# Copy repo into duster user's directory +RUN mkdir /home/duster/duster +WORKDIR /home/duster/duster +COPY . . +RUN chown -R duster:duster /home/duster/duster + +USER duster +RUN git clean -dfxq +RUN cargo build +RUN npm link dustjs-linkedin dustjs-helpers +ENTRYPOINT ["/home/duster/duster/js/run_compliance_suite.bash"] diff --git a/README.md b/README.md index 029cb00..fb4a17a 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ An implementation of the [LinkedIn fork of DustJS](https://www.dustjs.com/) writ **NOT RECOMMENDED FOR PUBLIC USE** -This code is available free and open source under the [0BSD](https://choosealicense.com/licenses/0bsd/), but it is a very early-stage project. You're welcome to use it, fork it, print it out and fold it into a hat, etc... but you will find that this project is not yet polished nor feature complete. While this repository uses the 0BSD license which does not require the inclusion of a copyright notice/text in any distribution, it depends on [nom](https://github.com/Geal/nom) which is under the MIT license and the Rust standard library which is [dual licensed](https://github.com/rust-lang/rust/issues/67014). +This code is available free and open source under the [0BSD](https://choosealicense.com/licenses/0bsd/), but it is a very early-stage project. You're welcome to use it, fork it, print it out and fold it into a hat, etc... but you will find that this project is not yet polished nor feature complete. While this repository uses the 0BSD license which does not require the inclusion of a copyright notice/text in any distribution, it depends on [nom](https://github.com/Geal/nom) which is under the MIT license, the Rust standard library which is [dual licensed](https://github.com/rust-lang/rust/issues/67014), serde_json which is [dual licensed](https://github.com/serde-rs/json), and serde which is [dual licensed](https://github.com/serde-rs/serde). diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..993cb49 --- /dev/null +++ b/js/README.md @@ -0,0 +1,59 @@ +Compliance Tests +================ + +These tests run my implementation of LinkedIn's Dust and the official implementation of [LinkedIn's DustJS](https://www.dustjs.com) as compares the output to ensure they match. + +Running in Docker +================= + +Go to the root directory of this repository where there is a `Dockerfile` and run: + +``` sh +docker build -t duster . && docker run --rm -i -t duster +``` + +This command will run through the test cases in `js/test_cases`, comparing the output of the official LinkedIn DustJS implementation against the output of the `duster` implementation. If there are any differences, it will flag the test as a failure. + +The tests have a structure of: + +``` text +test_cases +└── hello_world + ├── input1.json + ├── input2.json + ├── main.dust + └── partial.dust +``` + +The folder name `hello_world` is the name of the test group. For each test group there must be a file named `main.dust`. This file will be the top-level template that gets rendered. All other `*.dust` files will get registered into the dust rendering context as partials, with the same name as their file excluding the file extension. For example, `main.dust` could invoke `partial.dust` with `{>partial/}`. Each `*.json` file is a separate context that will be passed into the dust renderer, making each `*.json` file form a test inside the test group. + +Running Manually +================ + +Individually +------------ + +If you want to invoke the LinkedIn DustJS manually, the `dustjs_shim` expects the json context to be passed in over stdin, and a list of templates to be passed in as arguments, with the first argument serving as the main template. An example invocation from this folder would therefore be: + +``` sh +npm install dustjs-linkedin +cat test_cases/hello_world/input1.json | node dustjs_shim.js test_cases/hello_world/main.dust +``` + +Unlike the docker variant, there is no requirement for the template to be named `main.dust` since it will render the first argument as the main template. Running the same test case through the `duster` implementation would be: + +``` sh +npm install dustjs-linkedin +cat test_cases/hello_world/input1.json | ../target/debug/duster-cli test_cases/hello_world/main.dust +``` + +Batch +----- + +If you instead would like to run the entire compliance test suite manually outside of docker, you can run: + +``` sh +npm install dustjs-linkedin +(cd .. && cargo build) +./run_compliance_suite.bash +``` diff --git a/js/dustjs_shim.js b/js/dustjs_shim.js new file mode 100644 index 0000000..8e90167 --- /dev/null +++ b/js/dustjs_shim.js @@ -0,0 +1,50 @@ +// var dust = require("dustjs-linkedin"); +var dust = require("dustjs-helpers"); +var fs = require("fs"); +const path = require("path"); + +dust.helpers.dumpParameters = function(chunk, context, bodies, params) { + // Dump the parameters to a dust helper to figure out what is passed + // to a helper. I figure I need to make sure I'm passing + // approximately the same information to my render functions if I + // end up supporting custom helpers in the future to make sure + // helpers can be ported easily. + // console.log(JSON.stringify(Array.prototype.slice.call(arguments))); + console.log("chunk: ", chunk); + console.log("context: ", JSON.stringify(context)); + console.log("bodies: ", bodies); + console.log("params: ", params); +} + +var argv = process.argv.slice(2); +if (argv.length < 1) { + console.error("Expecting only 1 argument (a path to a template)"); + process.exit(1); +} +var context = JSON.parse(fs.readFileSync(0, "utf-8")); +var main_template = path.parse(argv[0])["name"]; + +for (var i = 0, len = argv.length; i < len; ++i) { + var filename = path.parse(argv[i])["name"]; + + try { + var template_source = fs.readFileSync(argv[i], "utf-8"); + } catch (err) { + console.error(err); + process.exit(1); + } + + var compiled_template = dust.compile(template_source, filename); + dust.loadSource(compiled_template); + +} + +dust.render(main_template, context, function(err, out) { + if(err) { + console.error(err); + process.exit(1); + } else { + console.log(out); + process.exit(0); + } +}); diff --git a/js/permanently_disabled_test_cases/README.md b/js/permanently_disabled_test_cases/README.md new file mode 100644 index 0000000..1435ea6 --- /dev/null +++ b/js/permanently_disabled_test_cases/README.md @@ -0,0 +1,11 @@ +This folder is for test cases that are NEVER expected to work on duster but I've written to investigate the behavior of original dust. Examples of this are: + +Custom javascript dust helpers +------------------------------ + +Inside the shim I wrote a very basic custom dust helper to investigate the chunk, context, bodies, and params parameters so that I can theoretically expose a similar interface for custom rust dust helpers. Since I won't be perfectly replicating those objects, duster would never output a byte-for-byte identical output for that helper. + +Javascript context helpers +-------------------------- + +I do not intend to integrate a javascript engine into duster, so context helpers written as javascript inside the json context will not work on duster. I believe, based on my interface-based approach, I'll be able to support runtime functions written in rust and stored in rust objects (not from JSON) to serve the same functionality as context helpers but that still remains to be seen. diff --git a/js/permanently_disabled_test_cases/helpers_dump_parameters/README.md b/js/permanently_disabled_test_cases/helpers_dump_parameters/README.md new file mode 100644 index 0000000..2c1d01e --- /dev/null +++ b/js/permanently_disabled_test_cases/helpers_dump_parameters/README.md @@ -0,0 +1,30 @@ +Chunk +----- + +Some sort of object which contains an array of rendered elements. For example, using: + +``` +Testing dump parameters +{#names} + {.} +{/names} +``` + +The first interation would have `["Testing dump parameters", "Alice"]`. + +Question: Do any helpers read from chunk or just write to it? If its just writing, then my current architecture of just returning a `String` would work fine, though having a shared buffer thats written to would probably reduce the allocations and therefore improve performance. + +Context +------- + +Some sort of object that contains essentially the "breadcrumbs" variable I am currently using to track the context tree. Also tracks template name, globals and options. I guess options would be that whole preserve whitespace or not option. I was just going to store this option on the dust renderer type, but I guess I should bake it into the breadcrumbs to pass into helpers if I implement support for custom helpers. + +Bodies +------ + +No idea, but based on the name I assume it contains the contents of the helper, or at least some way to render the internal body of the helper. + +Params +------ + +A mapping of the parameters directly on the helper. diff --git a/js/permanently_disabled_test_cases/helpers_dump_parameters/input1.json b/js/permanently_disabled_test_cases/helpers_dump_parameters/input1.json new file mode 100644 index 0000000..74f3606 --- /dev/null +++ b/js/permanently_disabled_test_cases/helpers_dump_parameters/input1.json @@ -0,0 +1,6 @@ +{ + "names": [ + "Alice", + "Bob" + ] +} diff --git a/js/permanently_disabled_test_cases/helpers_dump_parameters/main.dust b/js/permanently_disabled_test_cases/helpers_dump_parameters/main.dust new file mode 100644 index 0000000..ebc883e --- /dev/null +++ b/js/permanently_disabled_test_cases/helpers_dump_parameters/main.dust @@ -0,0 +1,7 @@ +Testing dump parameters +{#names} + {.} + {@dumpParameters foo="bar"} + Internal + {/dumpParameters} +{/names} diff --git a/js/run_compliance_suite.bash b/js/run_compliance_suite.bash new file mode 100755 index 0000000..48324d7 --- /dev/null +++ b/js/run_compliance_suite.bash @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# +# Runs the full suite of tests against LinkedIn DustJS and duster to compare the result +set -euo pipefail +IFS=$'\n\t' +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +failed_count=0 +show_diff=0 + +while (( "$#" )); do + if [ "$1" = "--help" ]; then + cat<&2 echo "Unrecognized option: $1") + exit 1 + fi + shift +done + +while read -r test_group; do + set +e + if [ $show_diff -eq 1 ]; then + "$DIR/run_single_test.bash" --show-diff "$test_group" + else + "$DIR/run_single_test.bash" --show-diff "$test_group" + fi + result=$? + if [ $result -ne 0 ]; then + failed_count=$((failed_count + result)) + fi + set -e +done <<<"$(find "$DIR/test_cases" -maxdepth 1 -mindepth 1 -type d ! -name '_*' | sort)" + +ignored_count=$(find "$DIR/test_cases" -maxdepth 1 -mindepth 1 -type d -name '_*' | wc -l) + +echo "" +echo "$ignored_count ignored tests" + +if [ $failed_count -ne 0 ]; then + echo "$failed_count failed tests" + exit 1 +else + echo "All tests passed" +fi diff --git a/js/run_dockerized_compliance_suite.bash b/js/run_dockerized_compliance_suite.bash new file mode 100755 index 0000000..10de8d7 --- /dev/null +++ b/js/run_dockerized_compliance_suite.bash @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# +# Runs the full suite of tests against LinkedIn DustJS and duster to compare the result +set -euo pipefail +IFS=$'\n\t' +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +cd "$DIR/../" +docker build -t duster . && docker run --rm -i -t duster "${@}" diff --git a/js/run_single_test.bash b/js/run_single_test.bash new file mode 100755 index 0000000..d3394e9 --- /dev/null +++ b/js/run_single_test.bash @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# Runs a single test against LinkedIn DustJS and duster to compare the result +set -euo pipefail +IFS=$'\n\t' +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +test_group="" +test_mode="" + +function show_help { + cat< + +Options: + --show-diff Shows the difference between the two dust implementations + --dustjs Print the output of dustjs instead of comparing + --duster Print the output of duster instead of comparing +EOF +} + +while (( "$#" )); do + if [ "$1" = "--help" ]; then + show_help + exit 0 + elif [ "$1" = "--show-diff" ]; then + show_diff=1 + elif [ "$1" = "--dustjs" ]; then + test_mode="dustjs" + elif [ "$1" = "--duster" ]; then + test_mode="duster" + elif [ ! "$1" = -* ]; then + test_group="$1" + else + (>&2 echo "Unrecognized option: $1") + exit 1 + fi + shift +done + +# Assert a test group was specified +if [ "$test_group" = "" ]; then + show_help + exit 1 +fi + +failed_count=0 +test_group_name=$(basename "$test_group") +while read -r test_case; do + test_case_file_name=$(basename "$test_case") + test_case_name=${test_case_file_name%.*} + set +e + if [ "$test_mode" = "dustjs" ] || [ "$test_mode" = "" ]; then + dustjs_output=$(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) node "$DIR/dustjs_shim.js" < "$test_case") + fi + if [ "$test_mode" = "duster" ] || [ "$test_mode" = "" ]; then + duster_output=$(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) "$DIR/../target/debug/duster-cli" < "$test_case") + fi + + if [ "$test_mode" = "dustjs" ]; then + cat <<<"$dustjs_output" + elif [ "$test_mode" = "duster" ]; then + cat <<<"$duster_output" + else + ( + if cmp -s <(cat <<<"$dustjs_output") <(cat <<<"$duster_output"); then + echo "$test_group_name::$test_case_name PASSED" + else + echo "$test_group_name::$test_case_name FAILED" + if [ $show_diff -eq 1 ]; then + diff --label "dustjs-linkedin" --label "duster" <(cat <<<"$dustjs_output") <(cat <<<"$duster_output") + fi + exit 1 + fi + ) + if [ $? -ne 0 ]; then + failed_count=$((failed_count + 1)) + fi + fi + set -e +done <<<"$(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.json' | sort)" + +exit "$failed_count" diff --git a/js/test_cases/_type_casting/README.md b/js/test_cases/_type_casting/README.md new file mode 100644 index 0000000..397e086 --- /dev/null +++ b/js/test_cases/_type_casting/README.md @@ -0,0 +1,3 @@ +Type casting appears to work on referenced values as well as literals. Do I add a separate type cast function or push the entire implementation of the helpers off into the traits? If I'm supporting custom helpers in the future I'll need to push off the helper implementation eventually anyway. + +What other types are available? All I see referenced in the docs is "number". diff --git a/js/test_cases/_type_casting/input1.json b/js/test_cases/_type_casting/input1.json new file mode 100644 index 0000000..113c7b5 --- /dev/null +++ b/js/test_cases/_type_casting/input1.json @@ -0,0 +1,4 @@ +{ + "str": "7", + "int": 7 +} diff --git a/js/test_cases/_type_casting/main.dust b/js/test_cases/_type_casting/main.dust new file mode 100644 index 0000000..69207b9 --- /dev/null +++ b/js/test_cases/_type_casting/main.dust @@ -0,0 +1,5 @@ +{@eq key=str value="7"}str is equal to "7"{:else}str does not equal "7"{/eq}{~n} +{@eq key=int value="7"}int is equal to "7"{:else}int does not equal "7"{/eq}{~n} +{@eq key=int value="7" type="number"}int is equal to "7"::number{:else}int does not equal "7"::number{/eq}{~n} +{@eq key=int value=str}int is equal to str{:else}int does not equal str{/eq}{~n} +{@eq key=int value=str type="number"}int is equal to str::number{:else}int does not equal str::number{/eq}{~n} diff --git a/js/test_cases/block_before/base.dust b/js/test_cases/block_before/base.dust new file mode 100644 index 0000000..bf568c4 --- /dev/null +++ b/js/test_cases/block_before/base.dust @@ -0,0 +1 @@ +The block content is from {+inject/} diff --git a/js/test_cases/block_before/input1.json b/js/test_cases/block_before/input1.json new file mode 100644 index 0000000..272dfa5 --- /dev/null +++ b/js/test_cases/block_before/input1.json @@ -0,0 +1,4 @@ +{"people": [ + {"name": "Alice", "item": "cat"}, + {"name": "Bob"} +]} diff --git a/js/test_cases/block_before/main.dust b/js/test_cases/block_before/main.dust new file mode 100644 index 0000000..2d9efd5 --- /dev/null +++ b/js/test_cases/block_before/main.dust @@ -0,0 +1,2 @@ +{"base"/} diff --git a/js/test_cases/block_context/base.dust b/js/test_cases/block_context/base.dust new file mode 100644 index 0000000..bf568c4 --- /dev/null +++ b/js/test_cases/block_context/base.dust @@ -0,0 +1 @@ +The block content is from {+inject/} diff --git a/js/test_cases/block_context/input1.json b/js/test_cases/block_context/input1.json new file mode 100644 index 0000000..09c9919 --- /dev/null +++ b/js/test_cases/block_context/input1.json @@ -0,0 +1,14 @@ +{ + "people": [ + { + "name": "Alice", + "item": "cat" + }, + { + "name": "Bob" + } + ], + "pet": { + "name": "fluffy" + } +} diff --git a/js/test_cases/block_context/main.dust b/js/test_cases/block_context/main.dust new file mode 100644 index 0000000..4a63112 --- /dev/null +++ b/js/test_cases/block_context/main.dust @@ -0,0 +1,6 @@ +{#pet} + {>"base"/} +{/pet} +{#people} + {"alpha"/} diff --git a/js/test_cases/block_inverse_register_order/input1.json b/js/test_cases/block_inverse_register_order/input1.json new file mode 100644 index 0000000..272dfa5 --- /dev/null +++ b/js/test_cases/block_inverse_register_order/input1.json @@ -0,0 +1,4 @@ +{"people": [ + {"name": "Alice", "item": "cat"}, + {"name": "Bob"} +]} diff --git a/js/test_cases/block_inverse_register_order/main.dust b/js/test_cases/block_inverse_register_order/main.dust new file mode 100644 index 0000000..e224313 --- /dev/null +++ b/js/test_cases/block_inverse_register_order/main.dust @@ -0,0 +1,3 @@ +{>"beta"/} +{>"alpha"/} +{"alpha"/} diff --git a/js/test_cases/block_register_order/input1.json b/js/test_cases/block_register_order/input1.json new file mode 100644 index 0000000..272dfa5 --- /dev/null +++ b/js/test_cases/block_register_order/input1.json @@ -0,0 +1,4 @@ +{"people": [ + {"name": "Alice", "item": "cat"}, + {"name": "Bob"} +]} diff --git a/js/test_cases/block_register_order/main.dust b/js/test_cases/block_register_order/main.dust new file mode 100644 index 0000000..03b48c7 --- /dev/null +++ b/js/test_cases/block_register_order/main.dust @@ -0,0 +1,3 @@ +The block content is from {+inject/}{~n} +{>"beta"/} +The block content is from {+inject/}{~n} diff --git a/js/test_cases/block_simple/README.md b/js/test_cases/block_simple/README.md new file mode 100644 index 0000000..4b48992 --- /dev/null +++ b/js/test_cases/block_simple/README.md @@ -0,0 +1,9 @@ +Blocks seem to be rendered with the last inline partial of the same name. + +Blocks appear to be able to be registered in a loop, however, their context is defined by the context during the call to the block `{+}` as opposed to when their override is defined by an inline partial `{<}`. + +Even if the surrounding dust would prevent a section from being rendered, the inline context is still registered, which makes me think inline contexts are parsed out in a single pass regardless of the context. + +Inline partials in sub-templates do not seem to bubble up to blocks in the higher templates. + +After the inverse register order test it seems that it takes the last inline partial with that name at that level, walking up the tree of partials. So each file will have exactly one value for each block name, consisting of the final inline partial with that name. Then when rendering the block, it will walk up the tree just like the context tree. diff --git a/js/test_cases/block_simple/base.dust b/js/test_cases/block_simple/base.dust new file mode 100644 index 0000000..bf568c4 --- /dev/null +++ b/js/test_cases/block_simple/base.dust @@ -0,0 +1 @@ +The block content is from {+inject/} diff --git a/js/test_cases/block_simple/input1.json b/js/test_cases/block_simple/input1.json new file mode 100644 index 0000000..272dfa5 --- /dev/null +++ b/js/test_cases/block_simple/input1.json @@ -0,0 +1,4 @@ +{"people": [ + {"name": "Alice", "item": "cat"}, + {"name": "Bob"} +]} diff --git a/js/test_cases/block_simple/main.dust b/js/test_cases/block_simple/main.dust new file mode 100644 index 0000000..d269fb1 --- /dev/null +++ b/js/test_cases/block_simple/main.dust @@ -0,0 +1,3 @@ +{"base"/} +{"base"/} +{/pet} +{#people} + {"tmpl{name}"/} diff --git a/js/test_cases/dynamic_partial/tmplalpha.dust b/js/test_cases/dynamic_partial/tmplalpha.dust new file mode 100644 index 0000000..4cfa36d --- /dev/null +++ b/js/test_cases/dynamic_partial/tmplalpha.dust @@ -0,0 +1 @@ +beta template{~n} diff --git a/js/test_cases/dynamic_partial/tmplbeta.dust b/js/test_cases/dynamic_partial/tmplbeta.dust new file mode 100644 index 0000000..4cfa36d --- /dev/null +++ b/js/test_cases/dynamic_partial/tmplbeta.dust @@ -0,0 +1 @@ +beta template{~n} diff --git a/js/test_cases/exists/README.md b/js/test_cases/exists/README.md new file mode 100644 index 0000000..3da699b --- /dev/null +++ b/js/test_cases/exists/README.md @@ -0,0 +1 @@ +Exists appears to follow the same truthiness rules that sections follow, rather than merely checking if a value exists. diff --git a/js/test_cases/exists/input1.json b/js/test_cases/exists/input1.json new file mode 100644 index 0000000..69197e0 --- /dev/null +++ b/js/test_cases/exists/input1.json @@ -0,0 +1 @@ +{"things": ["Alice", "Bob", "Chris"]} diff --git a/js/test_cases/exists/input10.json b/js/test_cases/exists/input10.json new file mode 100644 index 0000000..08f519d --- /dev/null +++ b/js/test_cases/exists/input10.json @@ -0,0 +1 @@ +{"things": {}} diff --git a/js/test_cases/exists/input11.json b/js/test_cases/exists/input11.json new file mode 100644 index 0000000..b9ee8db --- /dev/null +++ b/js/test_cases/exists/input11.json @@ -0,0 +1 @@ +["cat", "dog"] diff --git a/js/test_cases/exists/input2.json b/js/test_cases/exists/input2.json new file mode 100644 index 0000000..4743329 --- /dev/null +++ b/js/test_cases/exists/input2.json @@ -0,0 +1 @@ +{"things": {"name": "Alice", "keyboard": "K-Type"}} diff --git a/js/test_cases/exists/input3.json b/js/test_cases/exists/input3.json new file mode 100644 index 0000000..8040d63 --- /dev/null +++ b/js/test_cases/exists/input3.json @@ -0,0 +1 @@ +{"there_are_no_things": 4} diff --git a/js/test_cases/exists/input4.json b/js/test_cases/exists/input4.json new file mode 100644 index 0000000..03d1e8a --- /dev/null +++ b/js/test_cases/exists/input4.json @@ -0,0 +1 @@ +{"things": "just a string"} diff --git a/js/test_cases/exists/input5.json b/js/test_cases/exists/input5.json new file mode 100644 index 0000000..4ef7571 --- /dev/null +++ b/js/test_cases/exists/input5.json @@ -0,0 +1 @@ +{"things": false} diff --git a/js/test_cases/exists/input6.json b/js/test_cases/exists/input6.json new file mode 100644 index 0000000..feb65c6 --- /dev/null +++ b/js/test_cases/exists/input6.json @@ -0,0 +1 @@ +{"things": null} diff --git a/js/test_cases/exists/input7.json b/js/test_cases/exists/input7.json new file mode 100644 index 0000000..d9fc4f8 --- /dev/null +++ b/js/test_cases/exists/input7.json @@ -0,0 +1 @@ +{"things": 0} diff --git a/js/test_cases/exists/input8.json b/js/test_cases/exists/input8.json new file mode 100644 index 0000000..1486a8a --- /dev/null +++ b/js/test_cases/exists/input8.json @@ -0,0 +1 @@ +{"things": ""} diff --git a/js/test_cases/exists/input9.json b/js/test_cases/exists/input9.json new file mode 100644 index 0000000..93babc6 --- /dev/null +++ b/js/test_cases/exists/input9.json @@ -0,0 +1 @@ +{"things": []} diff --git a/js/test_cases/exists/main.dust b/js/test_cases/exists/main.dust new file mode 100644 index 0000000..22d0d2c --- /dev/null +++ b/js/test_cases/exists/main.dust @@ -0,0 +1,5 @@ +{?things} +Thing: {things} +{:else} +No things {.} +{/things} diff --git a/js/test_cases/explicit_context_setting/README.md b/js/test_cases/explicit_context_setting/README.md new file mode 100644 index 0000000..8c7c820 --- /dev/null +++ b/js/test_cases/explicit_context_setting/README.md @@ -0,0 +1,74 @@ +Priority +-------- +Partials: Explicit context takes priority over parameters + +Sections: New context takes priority, then parameters, then explicit + +Sections with loops: Loop variables ($idx and $len) take priority over parameters and explicit context + +$idx and $len +------------- + +$idx and $len do not survive through an explicit context setting, which will work perfectly with my injected-context architecture. + +You can use $idx and $len as your explicit context, but as scalar values I do not think there is a way to access them anyway. + +Exists and Not-Exists +--------------------- + +Looks like you can exlicitly set a context with exists and not-exists tags too. This works out well in the parser because I am using the same code for those blocks. + +Partials +-------- + +Explicitly setting a context in a partial also works. The explicit context takes priority over the parameters in the partial tag. + +This works for both regular named partials and quoted partials. + +While normally parameters are injected 1 level above the current context, if both parameters and explicit are set, they are injected below the current context. So if the context tree was `1->2->3`, with just parameters you'd have `1->2->parameters->3` but with an explicit context you have `parameters->explicit`. + +Helpers +------- + +Explicitly setting a context in a helper works too. + +Blocks and Inline Partials +-------------------------- + +Explicitly setting a context on an inline partial does not work, but it does not error out either, so I need to add support for this in the parser. + +Explicitly setting a context on a block does work. + +The explicit context for blocks is evaluated in the context for that template file containing the block, not the inline partial (so, whatever the context is when we invoke the partial containing the block). + +Parameters on blocks and inline partials appear to not be read but they do not error out. + +References +---------- + +Explicitly setting a context does not work on a reference, but it has revealed that invalid dust is rendered verbatim. I'm going to leave that commented out until I can address that in a later branch. + +Paths +----- + +Explicit contexts do support deep paths. + +Else Blocks +----------- + +Else blocks also use an explicit context. + +Complete Failure +---------------- + +If the walk destination does not exist, and the explicit context does not exist, then you end up with absolutely no context. + +Regular Path Failure +-------------------- + +If the regular path fails but the explicit path succeeds then the context is set to the explicit path. + +Falsey Explicit Path +-------------------- + +Since dust would not walk to a falsey path, theres no difference between a falsey path and a non-existent path. diff --git a/js/test_cases/explicit_context_setting/default.dust b/js/test_cases/explicit_context_setting/default.dust new file mode 100644 index 0000000..9c4a660 --- /dev/null +++ b/js/test_cases/explicit_context_setting/default.dust @@ -0,0 +1 @@ +{+pet_line}BLOCK {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}{/pet_line} diff --git a/js/test_cases/explicit_context_setting/default_explicit.dust b/js/test_cases/explicit_context_setting/default_explicit.dust new file mode 100644 index 0000000..aaa5436 --- /dev/null +++ b/js/test_cases/explicit_context_setting/default_explicit.dust @@ -0,0 +1 @@ +{+pet_line:explicit}BLOCK {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}{/pet_line} diff --git a/js/test_cases/explicit_context_setting/explicit_evaluation_time.dust b/js/test_cases/explicit_context_setting/explicit_evaluation_time.dust new file mode 100644 index 0000000..704b614 --- /dev/null +++ b/js/test_cases/explicit_context_setting/explicit_evaluation_time.dust @@ -0,0 +1,6 @@ +{#block} + {+pet_line}BLOCK: {contents}{~n}{/pet_line} +{/block} +{#inline_partial} + {explicit_evaluation_time_split_default/} +{/block.message} +{other/} +{/loop} + +Partial Explicit{~n} +================{~n} +{#loop} + {>other:explicit/} +{/loop} + +Quoted Partial Explicit{~n} +======================={~n} +{#loop} + {>"other":explicit/} +{/loop} + +Partial Regular with parameters{~n} +==============================={~n} +{#loop} + {>other pet="rabbit"/} +{/loop} + +Partial Explicit with parameters{~n} +================================{~n} +{#loop} + {>other:explicit pet="rabbit"/} +{/loop} + +{! Can you explicitly set the context for a helper? !} +Helper Regular{~n} +=============={~n} +{#loop} + {@eq key="foo" value="foo"} + {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n} + {/eq} +{/loop} + +Helper Explicit{~n} +==============={~n} +{#loop} + {@eq:explicit key="foo" value="foo"} + {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n} + {/eq} +{/loop} + +{! Can you explicitly set the context for inline partials or blocks? !} +Block Regular{~n} +============={~n} +{#loop} + {>default/} +{/loop} + +Inline Partial Regular{~n} +======================{~n} +{#loop} + {>override/} +{/loop} + +Block Explicit{~n} +=============={~n} +{#loop} + {>default_explicit/} +{/loop} + +Inline Partial Explicit{~n} +======================={~n} +{#loop} + {>override_explicit/} +{/loop} + +Inline Partial and Block Explicit{~n} +================================={~n} +{#loop} + {>override_double_explicit/} +{/loop} + +{! Can you explicitly set the context for references? !} +{! Commented out until I add support for rendering invalid dust verbatim +Reference Regular{~n} +================={~n} +{.}{~n} + +Reference Explicit{~n} +=================={~n} +{.:some_global}{~n} +!} + +{! Can you explicitly set the context with a path? !} +Path Regular{~n} +============{~n} +{#loop} + {#person} + {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/person} +{/loop} + +Path Explicit{~n} +============={~n} +{#loop} + {#person:deep_explicit.explicit} + {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/person} +{/loop} + +{! Can you explicitly set the context to a variable? !} +Variable Regular{~n} +================{~n} +{#loop} + {#person} + {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/person} +{/loop} + +Variable Explicit{~n} +================={~n} +{#loop} + {#person:$idx} + {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/person} +{/loop} + +{! What context is set on else blocks? !} +Else Block Regular{~n} +=================={~n} +{#loop} + {#empty_array} + MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {:else} + ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/empty_array} +{/loop} + +Else Block Explicit{~n} +==================={~n} +{#loop} + {#empty_array:explicit} + MAIN {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n} + {:else} + ELSE {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n} + {/empty_array} +{/loop} + +{! What context is set when the explicit context path does not exist? !} +Failed Explicit Regular{~n} +======================={~n} +{#loop} + {#person} + {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/person} +{/loop} + +Failed Explicit Explicit{~n} +========================{~n} +{#loop} + {#person:foobar} + {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/person} +{/loop} + +{! What context is set when the regular path and explicit context path does not exist? !} +Failed Everything Regular{~n} +========================={~n} +BEFORE {.|js}{~n} +{#foo} + MAIN {.|js}{~n} +{:else} + ELSE {.|js}{~n} +{/foo} + +Failed Everything Explicit{~n} +=========================={~n} +{#foo:bar} + MAIN {.|js}{~n} +{:else} + ELSE {.|js}{~n} +{/foo} + +{! What context is set when the regular context path does not exist? !} +Failed Regular Path Regular{~n} +==========================={~n} +{#loop} + {#foo} + MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {:else} + ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/foo} +{/loop} + +Failed Regular Path Explicit{~n} +============================{~n} +{#loop} + {#foo:explicit} + MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {:else} + ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/foo} +{/loop} + +{! What context is set when the explicit path is falsey? !} +Falsey Explicit Path Regular{~n} +============================{~n} +{#loop} + {#foo} + MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {:else} + ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {/foo} +{/loop} + +Falsey Explicit Path Explicit{~n} +============================={~n} +{#loop} + {#foo:empty_array} + MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {:else} + ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n} + {.|js}{~n} + {/foo} +{/loop} + +{! Since partial parameters are normally injected 1 level above the current context, and explicit contexts are below the partial parameters, is the order parameters->current_context->explicit or current_context->parameters->explicit when both are being used? !} +Partial Overloaded Regular with parameters{~n} +=========================================={~n} +{#loop} + {>other_friend friend="Dave" pet="rabbit"/} +{/loop} + +Partial Overloaded Explicit with parameters{~n} +==========================================={~n} +{#loop} + {>other_friend:explicit friend="Dave" pet="rabbit"/} +{/loop} + +{! Is an explicit context on an inline partial evaluated in the context of the block or the context of the inline partial? !} +Explicit Evaluation Time Global{~n} +==============================={~n} +{>explicit_evaluation_time/} + +Explicit Evaluation Time Partial Context{~n} +========================================{~n} +{#partial_context} + {>explicit_evaluation_time/} +{/partial_context} + +{! The previous test discovered that the inline partial's explicit context is evaluated based on the context when invoking the full-blown partial, HOWEVER, it shared the same partial context for both the block and the inline partial. To conclusively prove if the explicit context is evaluated on the inline partial and not the block, we need a different partial context for each one. !} +Explicit Evaluation Time Split Partial Context Default{~n} +======================================================{~n} +{#partial_context} + {#block.message} + {>explicit_evaluation_time_split_default/} + {/block.message} +{/partial_context} + +Explicit Evaluation Time Split Partial Context OVerride{~n} +======================================================={~n} +{#partial_context} + {#inline_partial} + {>explicit_evaluation_time_split_override/} + {/inline_partial} +{/partial_context} + +{! What happens with sections with explicit context and parameters !} +Section set $idx as a parameter{~n} +==============================={~n} +{#explicit $idx="7"} + $idx is {$idx}{~n} + $len is {$len}{~n} +{/explicit} + +Section set $idx as a parameter and new context{~n} +==============================================={~n} +{#has_idx $idx="7"} + $idx is {$idx}{~n} + $len is {$len}{~n} +{/has_idx} + +Section set $idx as a parameter, new context, and explicit context{~n} +=================================================================={~n} +{#has_idx:another_idx $idx="7" $len="8"} + $idx is {$idx}{~n} + $len is {$len}{~n} + other is {other}{~n} +{/has_idx} + +Section vs Partial priority{~n} +==========================={~n} +{#some_global:explicit pet="snake"} + {pet}{~n} +{/some_global} +{>priority:explicit pet="snake"/} + +Exists vs Partial priority{~n} +=========================={~n} +{?some_global:explicit pet="snake"} + {pet}{~n} +{/some_global} +{>priority:explicit pet="snake"/} + +Section vs NotExists priority{~n} +=========================={~n} +{^some_global:explicit pet="snake"} + MAIN {pet}{~n} +{:else} + ELSE {pet}{~n} +{/some_global} +{>priority:explicit pet="snake"/} + +Section vs Partial priority Failed Walk{~n} +======================================={~n} +{#doesnotexist:explicit pet="snake"} + MAIN {pet}{~n} +{:else} + ELSE {pet}{~n} +{/doesnotexist} +{>priority:explicit pet="snake"/} + +Section Loop set $idx as a parameter{~n} +===================================={~n} +{#loop $idx="7"} + $idx is {$idx}{~n} + $len is {$len}{~n} +{/loop} + +Section Loop set $idx in explicit context{~n} +========================================={~n} +{#loop:has_idx} + $idx is {$idx}{~n} + $len is {$len}{~n} +{/loop} + +Section Loop set $idx in explicit context and parameter{~n} +======================================================={~n} +{#loop:has_idx $idx="7"} + $idx is {$idx}{~n} + $len is {$len}{~n} +{/loop} + +{! Lets check the priority order for all the tag types now that we know that sections and partials have a different order !} +Section vs Partial priority{~n} +==========================={~n} +{#some_global:explicit pet="snake"} + {pet}{~n} +{/some_global} +{>priority:explicit pet="snake"/} + +Exists vs Section priority{~n} +=========================={~n} +{?some_global:explicit pet="snake"} + {pet}{~n} +{/some_global} +{>priority:explicit pet="snake"/} + +Not Exists vs Section priority{~n} +=============================={~n} +{^empty_array:explicit pet="snake"} + {pet}{~n} +{/empty_array} +{>priority:explicit pet="snake"/} + +{! TODO: Add helpers once we have a helper that sets a variable !} + +{! Do blocks or inline partials have parameters? !} +Do blocks or inline partials have parameters{~n} +============================================{~n} +{+some_block_override pet="snake" name="cuddlebunny"} + {pet}, {name}{~n} +{/some_block_override} +{default/} +{default_explicit/} +{default/} +{xx\b&\"'\t\f\n\r\\!@#$%^&*()[]{}<>,./?:;_-+=`" +} diff --git a/js/test_cases/filters/main.dust b/js/test_cases/filters/main.dust new file mode 100644 index 0000000..202d4a2 --- /dev/null +++ b/js/test_cases/filters/main.dust @@ -0,0 +1,40 @@ +Special characters: {special_characters}{~n} +Special characters html escaping disabled: {special_characters|s}{~n} +Special characters html escaping disabled and enabled: {special_characters|s|h}{~n} +Special characters html escaping enabled and disabled: {special_characters|h|s}{~n} +Special characters html escaped once: {special_characters|h}{~n} +Special characters html escaped twice: {special_characters|h|h}{~n} + + +Object string parsed: {string|jp}{~n} +Object string parsed and stringified: {string|jp|js}{~n} +Object string stringified and parsed: {string|js|jp}{~n} + +Array: {array}{~n} +Array stringified: {array|js}{~n} +Array stringified and parsed: {array|js|jp}{~n} + +Object: {object}{~n} +Object html escaped: {object|h}{~n} +Object html escaping disabled: {object|s}{~n} +Object stringified: {object|js}{~n} +Object stringified and parsed: {object|js|jp}{~n} +Object stringified, html escaping disabled, parsed, stringified, and html escaped: {object|js|s|jp|js|h}{~n} + +Special characters: {special_characters}{~n} +Special characters html escaping disabled and javascript escaped: {special_characters|s|j}{~n} +Special characters javascript escaped and html escaping disabled: {special_characters|j|s}{~n} +Special characters javascript escaped once: {special_characters|j}{~n} +Special characters javascript escaped twice: {special_characters|j|j}{~n} + +Special characters: {special_characters}{~n} +Special characters html escaping disabled and encodeURI: {special_characters|s|u}{~n} +Special characters encodeURI and html escaping disabled: {special_characters|u|s}{~n} +Special characters encodeURI once: {special_characters|u}{~n} +Special characters encodeURI twice: {special_characters|u|u}{~n} + +Special characters: {special_characters}{~n} +Special characters html escaping disabled and encodeURIComponent: {special_characters|s|uc}{~n} +Special characters encodeURIComponent and html escaping disabled: {special_characters|uc|s}{~n} +Special characters encodeURIComponent once: {special_characters|uc}{~n} +Special characters encodeURIComponent twice: {special_characters|uc|uc}{~n} diff --git a/js/test_cases/generated_filters/array.json b/js/test_cases/generated_filters/array.json new file mode 100644 index 0000000..21ac75d --- /dev/null +++ b/js/test_cases/generated_filters/array.json @@ -0,0 +1,6 @@ +{ + "name": [ + "foo", + "bar" + ] +} diff --git a/js/test_cases/generated_filters/boolean.json b/js/test_cases/generated_filters/boolean.json new file mode 100644 index 0000000..925ccd9 --- /dev/null +++ b/js/test_cases/generated_filters/boolean.json @@ -0,0 +1,3 @@ +{ + "name": true +} diff --git a/js/test_cases/generated_filters/float.json b/js/test_cases/generated_filters/float.json new file mode 100644 index 0000000..e84feaa --- /dev/null +++ b/js/test_cases/generated_filters/float.json @@ -0,0 +1,3 @@ +{ + "name": 7.4 +} diff --git a/js/test_cases/generated_filters/integer.json b/js/test_cases/generated_filters/integer.json new file mode 100644 index 0000000..4c5ec73 --- /dev/null +++ b/js/test_cases/generated_filters/integer.json @@ -0,0 +1,3 @@ +{ + "name": 4 +} diff --git a/js/test_cases/generated_filters/main.dust b/js/test_cases/generated_filters/main.dust new file mode 100644 index 0000000..c057b22 --- /dev/null +++ b/js/test_cases/generated_filters/main.dust @@ -0,0 +1,517 @@ +Hello {name}!{~n} +Hello {name|s}!{~n} +Hello {name|h}!{~n} +Hello {name|j}!{~n} +Hello {name|u}!{~n} +Hello {name|uc}!{~n} +Hello {name|js}!{~n} +Hello {name|s|h}!{~n} +Hello {name|s|j}!{~n} +Hello {name|s|u}!{~n} +Hello {name|s|uc}!{~n} +Hello {name|s|js}!{~n} +Hello {name|h|s}!{~n} +Hello {name|h|j}!{~n} +Hello {name|h|u}!{~n} +Hello {name|h|uc}!{~n} +Hello {name|h|js}!{~n} +Hello {name|j|s}!{~n} +Hello {name|j|h}!{~n} +Hello {name|j|u}!{~n} +Hello {name|j|uc}!{~n} +Hello {name|j|js}!{~n} +Hello {name|u|s}!{~n} +Hello {name|u|h}!{~n} +Hello {name|u|j}!{~n} +Hello {name|u|uc}!{~n} +Hello {name|u|js}!{~n} +Hello {name|uc|s}!{~n} +Hello {name|uc|h}!{~n} +Hello {name|uc|j}!{~n} +Hello {name|uc|u}!{~n} +Hello {name|uc|js}!{~n} +Hello {name|js|s}!{~n} +Hello {name|js|h}!{~n} +Hello {name|js|j}!{~n} +Hello {name|js|u}!{~n} +Hello {name|js|uc}!{~n} +Hello {name|s|h|j}!{~n} +Hello {name|s|h|u}!{~n} +Hello {name|s|h|uc}!{~n} +Hello {name|s|h|js}!{~n} +Hello {name|s|j|h}!{~n} +Hello {name|s|j|u}!{~n} +Hello {name|s|j|uc}!{~n} +Hello {name|s|j|js}!{~n} +Hello {name|s|u|h}!{~n} +Hello {name|s|u|j}!{~n} +Hello {name|s|u|uc}!{~n} +Hello {name|s|u|js}!{~n} +Hello {name|s|uc|h}!{~n} +Hello {name|s|uc|j}!{~n} +Hello {name|s|uc|u}!{~n} +Hello {name|s|uc|js}!{~n} +Hello {name|s|js|h}!{~n} +Hello {name|s|js|j}!{~n} +Hello {name|s|js|u}!{~n} +Hello {name|s|js|uc}!{~n} +Hello {name|h|s|j}!{~n} +Hello {name|h|s|u}!{~n} +Hello {name|h|s|uc}!{~n} +Hello {name|h|s|js}!{~n} +Hello {name|h|j|s}!{~n} +Hello {name|h|j|u}!{~n} +Hello {name|h|j|uc}!{~n} +Hello {name|h|j|js}!{~n} +Hello {name|h|u|s}!{~n} +Hello {name|h|u|j}!{~n} +Hello {name|h|u|uc}!{~n} +Hello {name|h|u|js}!{~n} +Hello {name|h|uc|s}!{~n} +Hello {name|h|uc|j}!{~n} +Hello {name|h|uc|u}!{~n} +Hello {name|h|uc|js}!{~n} +Hello {name|h|js|s}!{~n} +Hello {name|h|js|j}!{~n} +Hello {name|h|js|u}!{~n} +Hello {name|h|js|uc}!{~n} +Hello {name|j|s|h}!{~n} +Hello {name|j|s|u}!{~n} +Hello {name|j|s|uc}!{~n} +Hello {name|j|s|js}!{~n} +Hello {name|j|h|s}!{~n} +Hello {name|j|h|u}!{~n} +Hello {name|j|h|uc}!{~n} +Hello {name|j|h|js}!{~n} +Hello {name|j|u|s}!{~n} +Hello {name|j|u|h}!{~n} +Hello {name|j|u|uc}!{~n} +Hello {name|j|u|js}!{~n} +Hello {name|j|uc|s}!{~n} +Hello {name|j|uc|h}!{~n} +Hello {name|j|uc|u}!{~n} +Hello {name|j|uc|js}!{~n} +Hello {name|j|js|s}!{~n} +Hello {name|j|js|h}!{~n} +Hello {name|j|js|u}!{~n} +Hello {name|j|js|uc}!{~n} +Hello {name|u|s|h}!{~n} +Hello {name|u|s|j}!{~n} +Hello {name|u|s|uc}!{~n} +Hello {name|u|s|js}!{~n} +Hello {name|u|h|s}!{~n} +Hello {name|u|h|j}!{~n} +Hello {name|u|h|uc}!{~n} +Hello {name|u|h|js}!{~n} +Hello {name|u|j|s}!{~n} +Hello {name|u|j|h}!{~n} +Hello {name|u|j|uc}!{~n} +Hello {name|u|j|js}!{~n} +Hello {name|u|uc|s}!{~n} +Hello {name|u|uc|h}!{~n} +Hello {name|u|uc|j}!{~n} +Hello {name|u|uc|js}!{~n} +Hello {name|u|js|s}!{~n} +Hello {name|u|js|h}!{~n} +Hello {name|u|js|j}!{~n} +Hello {name|u|js|uc}!{~n} +Hello {name|uc|s|h}!{~n} +Hello {name|uc|s|j}!{~n} +Hello {name|uc|s|u}!{~n} +Hello {name|uc|s|js}!{~n} +Hello {name|uc|h|s}!{~n} +Hello {name|uc|h|j}!{~n} +Hello {name|uc|h|u}!{~n} +Hello {name|uc|h|js}!{~n} +Hello {name|uc|j|s}!{~n} +Hello {name|uc|j|h}!{~n} +Hello {name|uc|j|u}!{~n} +Hello {name|uc|j|js}!{~n} +Hello {name|uc|u|s}!{~n} +Hello {name|uc|u|h}!{~n} +Hello {name|uc|u|j}!{~n} +Hello {name|uc|u|js}!{~n} +Hello {name|uc|js|s}!{~n} +Hello {name|uc|js|h}!{~n} +Hello {name|uc|js|j}!{~n} +Hello {name|uc|js|u}!{~n} +Hello {name|js|s|h}!{~n} +Hello {name|js|s|j}!{~n} +Hello {name|js|s|u}!{~n} +Hello {name|js|s|uc}!{~n} +Hello {name|js|h|s}!{~n} +Hello {name|js|h|j}!{~n} +Hello {name|js|h|u}!{~n} +Hello {name|js|h|uc}!{~n} +Hello {name|js|j|s}!{~n} +Hello {name|js|j|h}!{~n} +Hello {name|js|j|u}!{~n} +Hello {name|js|j|uc}!{~n} +Hello {name|js|u|s}!{~n} +Hello {name|js|u|h}!{~n} +Hello {name|js|u|j}!{~n} +Hello {name|js|u|uc}!{~n} +Hello {name|js|uc|s}!{~n} +Hello {name|js|uc|h}!{~n} +Hello {name|js|uc|j}!{~n} +Hello {name|js|uc|u}!{~n} +Hello {name|s|h|j|u}!{~n} +Hello {name|s|h|j|uc}!{~n} +Hello {name|s|h|j|js}!{~n} +Hello {name|s|h|u|j}!{~n} +Hello {name|s|h|u|uc}!{~n} +Hello {name|s|h|u|js}!{~n} +Hello {name|s|h|uc|j}!{~n} +Hello {name|s|h|uc|u}!{~n} +Hello {name|s|h|uc|js}!{~n} +Hello {name|s|h|js|j}!{~n} +Hello {name|s|h|js|u}!{~n} +Hello {name|s|h|js|uc}!{~n} +Hello {name|s|j|h|u}!{~n} +Hello {name|s|j|h|uc}!{~n} +Hello {name|s|j|h|js}!{~n} +Hello {name|s|j|u|h}!{~n} +Hello {name|s|j|u|uc}!{~n} +Hello {name|s|j|u|js}!{~n} +Hello {name|s|j|uc|h}!{~n} +Hello {name|s|j|uc|u}!{~n} +Hello {name|s|j|uc|js}!{~n} +Hello {name|s|j|js|h}!{~n} +Hello {name|s|j|js|u}!{~n} +Hello {name|s|j|js|uc}!{~n} +Hello {name|s|u|h|j}!{~n} +Hello {name|s|u|h|uc}!{~n} +Hello {name|s|u|h|js}!{~n} +Hello {name|s|u|j|h}!{~n} +Hello {name|s|u|j|uc}!{~n} +Hello {name|s|u|j|js}!{~n} +Hello {name|s|u|uc|h}!{~n} +Hello {name|s|u|uc|j}!{~n} +Hello {name|s|u|uc|js}!{~n} +Hello {name|s|u|js|h}!{~n} +Hello {name|s|u|js|j}!{~n} +Hello {name|s|u|js|uc}!{~n} +Hello {name|s|uc|h|j}!{~n} +Hello {name|s|uc|h|u}!{~n} +Hello {name|s|uc|h|js}!{~n} +Hello {name|s|uc|j|h}!{~n} +Hello {name|s|uc|j|u}!{~n} +Hello {name|s|uc|j|js}!{~n} +Hello {name|s|uc|u|h}!{~n} +Hello {name|s|uc|u|j}!{~n} +Hello {name|s|uc|u|js}!{~n} +Hello {name|s|uc|js|h}!{~n} +Hello {name|s|uc|js|j}!{~n} +Hello {name|s|uc|js|u}!{~n} +Hello {name|s|js|h|j}!{~n} +Hello {name|s|js|h|u}!{~n} +Hello {name|s|js|h|uc}!{~n} +Hello {name|s|js|j|h}!{~n} +Hello {name|s|js|j|u}!{~n} +Hello {name|s|js|j|uc}!{~n} +Hello {name|s|js|u|h}!{~n} +Hello {name|s|js|u|j}!{~n} +Hello {name|s|js|u|uc}!{~n} +Hello {name|s|js|uc|h}!{~n} +Hello {name|s|js|uc|j}!{~n} +Hello {name|s|js|uc|u}!{~n} +Hello {name|h|s|j|u}!{~n} +Hello {name|h|s|j|uc}!{~n} +Hello {name|h|s|j|js}!{~n} +Hello {name|h|s|u|j}!{~n} +Hello {name|h|s|u|uc}!{~n} +Hello {name|h|s|u|js}!{~n} +Hello {name|h|s|uc|j}!{~n} +Hello {name|h|s|uc|u}!{~n} +Hello {name|h|s|uc|js}!{~n} +Hello {name|h|s|js|j}!{~n} +Hello {name|h|s|js|u}!{~n} +Hello {name|h|s|js|uc}!{~n} +Hello {name|h|j|s|u}!{~n} +Hello {name|h|j|s|uc}!{~n} +Hello {name|h|j|s|js}!{~n} +Hello {name|h|j|u|s}!{~n} +Hello {name|h|j|u|uc}!{~n} +Hello {name|h|j|u|js}!{~n} +Hello {name|h|j|uc|s}!{~n} +Hello {name|h|j|uc|u}!{~n} +Hello {name|h|j|uc|js}!{~n} +Hello {name|h|j|js|s}!{~n} +Hello {name|h|j|js|u}!{~n} +Hello {name|h|j|js|uc}!{~n} +Hello {name|h|u|s|j}!{~n} +Hello {name|h|u|s|uc}!{~n} +Hello {name|h|u|s|js}!{~n} +Hello {name|h|u|j|s}!{~n} +Hello {name|h|u|j|uc}!{~n} +Hello {name|h|u|j|js}!{~n} +Hello {name|h|u|uc|s}!{~n} +Hello {name|h|u|uc|j}!{~n} +Hello {name|h|u|uc|js}!{~n} +Hello {name|h|u|js|s}!{~n} +Hello {name|h|u|js|j}!{~n} +Hello {name|h|u|js|uc}!{~n} +Hello {name|h|uc|s|j}!{~n} +Hello {name|h|uc|s|u}!{~n} +Hello {name|h|uc|s|js}!{~n} +Hello {name|h|uc|j|s}!{~n} +Hello {name|h|uc|j|u}!{~n} +Hello {name|h|uc|j|js}!{~n} +Hello {name|h|uc|u|s}!{~n} +Hello {name|h|uc|u|j}!{~n} +Hello {name|h|uc|u|js}!{~n} +Hello {name|h|uc|js|s}!{~n} +Hello {name|h|uc|js|j}!{~n} +Hello {name|h|uc|js|u}!{~n} +Hello {name|h|js|s|j}!{~n} +Hello {name|h|js|s|u}!{~n} +Hello {name|h|js|s|uc}!{~n} +Hello {name|h|js|j|s}!{~n} +Hello {name|h|js|j|u}!{~n} +Hello {name|h|js|j|uc}!{~n} +Hello {name|h|js|u|s}!{~n} +Hello {name|h|js|u|j}!{~n} +Hello {name|h|js|u|uc}!{~n} +Hello {name|h|js|uc|s}!{~n} +Hello {name|h|js|uc|j}!{~n} +Hello {name|h|js|uc|u}!{~n} +Hello {name|j|s|h|u}!{~n} +Hello {name|j|s|h|uc}!{~n} +Hello {name|j|s|h|js}!{~n} +Hello {name|j|s|u|h}!{~n} +Hello {name|j|s|u|uc}!{~n} +Hello {name|j|s|u|js}!{~n} +Hello {name|j|s|uc|h}!{~n} +Hello {name|j|s|uc|u}!{~n} +Hello {name|j|s|uc|js}!{~n} +Hello {name|j|s|js|h}!{~n} +Hello {name|j|s|js|u}!{~n} +Hello {name|j|s|js|uc}!{~n} +Hello {name|j|h|s|u}!{~n} +Hello {name|j|h|s|uc}!{~n} +Hello {name|j|h|s|js}!{~n} +Hello {name|j|h|u|s}!{~n} +Hello {name|j|h|u|uc}!{~n} +Hello {name|j|h|u|js}!{~n} +Hello {name|j|h|uc|s}!{~n} +Hello {name|j|h|uc|u}!{~n} +Hello {name|j|h|uc|js}!{~n} +Hello {name|j|h|js|s}!{~n} +Hello {name|j|h|js|u}!{~n} +Hello {name|j|h|js|uc}!{~n} +Hello {name|j|u|s|h}!{~n} +Hello {name|j|u|s|uc}!{~n} +Hello {name|j|u|s|js}!{~n} +Hello {name|j|u|h|s}!{~n} +Hello {name|j|u|h|uc}!{~n} +Hello {name|j|u|h|js}!{~n} +Hello {name|j|u|uc|s}!{~n} +Hello {name|j|u|uc|h}!{~n} +Hello {name|j|u|uc|js}!{~n} +Hello {name|j|u|js|s}!{~n} +Hello {name|j|u|js|h}!{~n} +Hello {name|j|u|js|uc}!{~n} +Hello {name|j|uc|s|h}!{~n} +Hello {name|j|uc|s|u}!{~n} +Hello {name|j|uc|s|js}!{~n} +Hello {name|j|uc|h|s}!{~n} +Hello {name|j|uc|h|u}!{~n} +Hello {name|j|uc|h|js}!{~n} +Hello {name|j|uc|u|s}!{~n} +Hello {name|j|uc|u|h}!{~n} +Hello {name|j|uc|u|js}!{~n} +Hello {name|j|uc|js|s}!{~n} +Hello {name|j|uc|js|h}!{~n} +Hello {name|j|uc|js|u}!{~n} +Hello {name|j|js|s|h}!{~n} +Hello {name|j|js|s|u}!{~n} +Hello {name|j|js|s|uc}!{~n} +Hello {name|j|js|h|s}!{~n} +Hello {name|j|js|h|u}!{~n} +Hello {name|j|js|h|uc}!{~n} +Hello {name|j|js|u|s}!{~n} +Hello {name|j|js|u|h}!{~n} +Hello {name|j|js|u|uc}!{~n} +Hello {name|j|js|uc|s}!{~n} +Hello {name|j|js|uc|h}!{~n} +Hello {name|j|js|uc|u}!{~n} +Hello {name|u|s|h|j}!{~n} +Hello {name|u|s|h|uc}!{~n} +Hello {name|u|s|h|js}!{~n} +Hello {name|u|s|j|h}!{~n} +Hello {name|u|s|j|uc}!{~n} +Hello {name|u|s|j|js}!{~n} +Hello {name|u|s|uc|h}!{~n} +Hello {name|u|s|uc|j}!{~n} +Hello {name|u|s|uc|js}!{~n} +Hello {name|u|s|js|h}!{~n} +Hello {name|u|s|js|j}!{~n} +Hello {name|u|s|js|uc}!{~n} +Hello {name|u|h|s|j}!{~n} +Hello {name|u|h|s|uc}!{~n} +Hello {name|u|h|s|js}!{~n} +Hello {name|u|h|j|s}!{~n} +Hello {name|u|h|j|uc}!{~n} +Hello {name|u|h|j|js}!{~n} +Hello {name|u|h|uc|s}!{~n} +Hello {name|u|h|uc|j}!{~n} +Hello {name|u|h|uc|js}!{~n} +Hello {name|u|h|js|s}!{~n} +Hello {name|u|h|js|j}!{~n} +Hello {name|u|h|js|uc}!{~n} +Hello {name|u|j|s|h}!{~n} +Hello {name|u|j|s|uc}!{~n} +Hello {name|u|j|s|js}!{~n} +Hello {name|u|j|h|s}!{~n} +Hello {name|u|j|h|uc}!{~n} +Hello {name|u|j|h|js}!{~n} +Hello {name|u|j|uc|s}!{~n} +Hello {name|u|j|uc|h}!{~n} +Hello {name|u|j|uc|js}!{~n} +Hello {name|u|j|js|s}!{~n} +Hello {name|u|j|js|h}!{~n} +Hello {name|u|j|js|uc}!{~n} +Hello {name|u|uc|s|h}!{~n} +Hello {name|u|uc|s|j}!{~n} +Hello {name|u|uc|s|js}!{~n} +Hello {name|u|uc|h|s}!{~n} +Hello {name|u|uc|h|j}!{~n} +Hello {name|u|uc|h|js}!{~n} +Hello {name|u|uc|j|s}!{~n} +Hello {name|u|uc|j|h}!{~n} +Hello {name|u|uc|j|js}!{~n} +Hello {name|u|uc|js|s}!{~n} +Hello {name|u|uc|js|h}!{~n} +Hello {name|u|uc|js|j}!{~n} +Hello {name|u|js|s|h}!{~n} +Hello {name|u|js|s|j}!{~n} +Hello {name|u|js|s|uc}!{~n} +Hello {name|u|js|h|s}!{~n} +Hello {name|u|js|h|j}!{~n} +Hello {name|u|js|h|uc}!{~n} +Hello {name|u|js|j|s}!{~n} +Hello {name|u|js|j|h}!{~n} +Hello {name|u|js|j|uc}!{~n} +Hello {name|u|js|uc|s}!{~n} +Hello {name|u|js|uc|h}!{~n} +Hello {name|u|js|uc|j}!{~n} +Hello {name|uc|s|h|j}!{~n} +Hello {name|uc|s|h|u}!{~n} +Hello {name|uc|s|h|js}!{~n} +Hello {name|uc|s|j|h}!{~n} +Hello {name|uc|s|j|u}!{~n} +Hello {name|uc|s|j|js}!{~n} +Hello {name|uc|s|u|h}!{~n} +Hello {name|uc|s|u|j}!{~n} +Hello {name|uc|s|u|js}!{~n} +Hello {name|uc|s|js|h}!{~n} +Hello {name|uc|s|js|j}!{~n} +Hello {name|uc|s|js|u}!{~n} +Hello {name|uc|h|s|j}!{~n} +Hello {name|uc|h|s|u}!{~n} +Hello {name|uc|h|s|js}!{~n} +Hello {name|uc|h|j|s}!{~n} +Hello {name|uc|h|j|u}!{~n} +Hello {name|uc|h|j|js}!{~n} +Hello {name|uc|h|u|s}!{~n} +Hello {name|uc|h|u|j}!{~n} +Hello {name|uc|h|u|js}!{~n} +Hello {name|uc|h|js|s}!{~n} +Hello {name|uc|h|js|j}!{~n} +Hello {name|uc|h|js|u}!{~n} +Hello {name|uc|j|s|h}!{~n} +Hello {name|uc|j|s|u}!{~n} +Hello {name|uc|j|s|js}!{~n} +Hello {name|uc|j|h|s}!{~n} +Hello {name|uc|j|h|u}!{~n} +Hello {name|uc|j|h|js}!{~n} +Hello {name|uc|j|u|s}!{~n} +Hello {name|uc|j|u|h}!{~n} +Hello {name|uc|j|u|js}!{~n} +Hello {name|uc|j|js|s}!{~n} +Hello {name|uc|j|js|h}!{~n} +Hello {name|uc|j|js|u}!{~n} +Hello {name|uc|u|s|h}!{~n} +Hello {name|uc|u|s|j}!{~n} +Hello {name|uc|u|s|js}!{~n} +Hello {name|uc|u|h|s}!{~n} +Hello {name|uc|u|h|j}!{~n} +Hello {name|uc|u|h|js}!{~n} +Hello {name|uc|u|j|s}!{~n} +Hello {name|uc|u|j|h}!{~n} +Hello {name|uc|u|j|js}!{~n} +Hello {name|uc|u|js|s}!{~n} +Hello {name|uc|u|js|h}!{~n} +Hello {name|uc|u|js|j}!{~n} +Hello {name|uc|js|s|h}!{~n} +Hello {name|uc|js|s|j}!{~n} +Hello {name|uc|js|s|u}!{~n} +Hello {name|uc|js|h|s}!{~n} +Hello {name|uc|js|h|j}!{~n} +Hello {name|uc|js|h|u}!{~n} +Hello {name|uc|js|j|s}!{~n} +Hello {name|uc|js|j|h}!{~n} +Hello {name|uc|js|j|u}!{~n} +Hello {name|uc|js|u|s}!{~n} +Hello {name|uc|js|u|h}!{~n} +Hello {name|uc|js|u|j}!{~n} +Hello {name|js|s|h|j}!{~n} +Hello {name|js|s|h|u}!{~n} +Hello {name|js|s|h|uc}!{~n} +Hello {name|js|s|j|h}!{~n} +Hello {name|js|s|j|u}!{~n} +Hello {name|js|s|j|uc}!{~n} +Hello {name|js|s|u|h}!{~n} +Hello {name|js|s|u|j}!{~n} +Hello {name|js|s|u|uc}!{~n} +Hello {name|js|s|uc|h}!{~n} +Hello {name|js|s|uc|j}!{~n} +Hello {name|js|s|uc|u}!{~n} +Hello {name|js|h|s|j}!{~n} +Hello {name|js|h|s|u}!{~n} +Hello {name|js|h|s|uc}!{~n} +Hello {name|js|h|j|s}!{~n} +Hello {name|js|h|j|u}!{~n} +Hello {name|js|h|j|uc}!{~n} +Hello {name|js|h|u|s}!{~n} +Hello {name|js|h|u|j}!{~n} +Hello {name|js|h|u|uc}!{~n} +Hello {name|js|h|uc|s}!{~n} +Hello {name|js|h|uc|j}!{~n} +Hello {name|js|h|uc|u}!{~n} +Hello {name|js|j|s|h}!{~n} +Hello {name|js|j|s|u}!{~n} +Hello {name|js|j|s|uc}!{~n} +Hello {name|js|j|h|s}!{~n} +Hello {name|js|j|h|u}!{~n} +Hello {name|js|j|h|uc}!{~n} +Hello {name|js|j|u|s}!{~n} +Hello {name|js|j|u|h}!{~n} +Hello {name|js|j|u|uc}!{~n} +Hello {name|js|j|uc|s}!{~n} +Hello {name|js|j|uc|h}!{~n} +Hello {name|js|j|uc|u}!{~n} +Hello {name|js|u|s|h}!{~n} +Hello {name|js|u|s|j}!{~n} +Hello {name|js|u|s|uc}!{~n} +Hello {name|js|u|h|s}!{~n} +Hello {name|js|u|h|j}!{~n} +Hello {name|js|u|h|uc}!{~n} +Hello {name|js|u|j|s}!{~n} +Hello {name|js|u|j|h}!{~n} +Hello {name|js|u|j|uc}!{~n} +Hello {name|js|u|uc|s}!{~n} +Hello {name|js|u|uc|h}!{~n} +Hello {name|js|u|uc|j}!{~n} +Hello {name|js|uc|s|h}!{~n} +Hello {name|js|uc|s|j}!{~n} +Hello {name|js|uc|s|u}!{~n} +Hello {name|js|uc|h|s}!{~n} +Hello {name|js|uc|h|j}!{~n} +Hello {name|js|uc|h|u}!{~n} +Hello {name|js|uc|j|s}!{~n} +Hello {name|js|uc|j|h}!{~n} +Hello {name|js|uc|j|u}!{~n} +Hello {name|js|uc|u|s}!{~n} +Hello {name|js|uc|u|h}!{~n} +Hello {name|js|uc|u|j}!{~n} diff --git a/js/test_cases/generated_filters/null.json b/js/test_cases/generated_filters/null.json new file mode 100644 index 0000000..298db8f --- /dev/null +++ b/js/test_cases/generated_filters/null.json @@ -0,0 +1,3 @@ +{ + "name": null +} diff --git a/js/test_cases/generated_filters/object.json b/js/test_cases/generated_filters/object.json new file mode 100644 index 0000000..7a016fe --- /dev/null +++ b/js/test_cases/generated_filters/object.json @@ -0,0 +1,5 @@ +{ + "name": { + "foo": "bar" + } +} diff --git a/js/test_cases/generated_filters/string.json b/js/test_cases/generated_filters/string.json new file mode 100644 index 0000000..6b66c0f --- /dev/null +++ b/js/test_cases/generated_filters/string.json @@ -0,0 +1,3 @@ +{ + "name": "{\"foo\": \"bar\"}" +} diff --git a/js/test_cases/generated_filters_two/README.md b/js/test_cases/generated_filters_two/README.md new file mode 100644 index 0000000..c32a73e --- /dev/null +++ b/js/test_cases/generated_filters_two/README.md @@ -0,0 +1 @@ +I had to split the generated_filters test into two tests because dustjs is limited to 65536 characters of output (a shortcoming duster will not have). diff --git a/js/test_cases/generated_filters_two/array.json b/js/test_cases/generated_filters_two/array.json new file mode 100644 index 0000000..21ac75d --- /dev/null +++ b/js/test_cases/generated_filters_two/array.json @@ -0,0 +1,6 @@ +{ + "name": [ + "foo", + "bar" + ] +} diff --git a/js/test_cases/generated_filters_two/boolean.json b/js/test_cases/generated_filters_two/boolean.json new file mode 100644 index 0000000..925ccd9 --- /dev/null +++ b/js/test_cases/generated_filters_two/boolean.json @@ -0,0 +1,3 @@ +{ + "name": true +} diff --git a/js/test_cases/generated_filters_two/float.json b/js/test_cases/generated_filters_two/float.json new file mode 100644 index 0000000..e84feaa --- /dev/null +++ b/js/test_cases/generated_filters_two/float.json @@ -0,0 +1,3 @@ +{ + "name": 7.4 +} diff --git a/js/test_cases/generated_filters_two/integer.json b/js/test_cases/generated_filters_two/integer.json new file mode 100644 index 0000000..4c5ec73 --- /dev/null +++ b/js/test_cases/generated_filters_two/integer.json @@ -0,0 +1,3 @@ +{ + "name": 4 +} diff --git a/js/test_cases/generated_filters_two/main.dust b/js/test_cases/generated_filters_two/main.dust new file mode 100644 index 0000000..b3a9f1c --- /dev/null +++ b/js/test_cases/generated_filters_two/main.dust @@ -0,0 +1,720 @@ +Hello {name|s|h|j|u|uc}!{~n} +Hello {name|s|h|j|u|js}!{~n} +Hello {name|s|h|j|uc|u}!{~n} +Hello {name|s|h|j|uc|js}!{~n} +Hello {name|s|h|j|js|u}!{~n} +Hello {name|s|h|j|js|uc}!{~n} +Hello {name|s|h|u|j|uc}!{~n} +Hello {name|s|h|u|j|js}!{~n} +Hello {name|s|h|u|uc|j}!{~n} +Hello {name|s|h|u|uc|js}!{~n} +Hello {name|s|h|u|js|j}!{~n} +Hello {name|s|h|u|js|uc}!{~n} +Hello {name|s|h|uc|j|u}!{~n} +Hello {name|s|h|uc|j|js}!{~n} +Hello {name|s|h|uc|u|j}!{~n} +Hello {name|s|h|uc|u|js}!{~n} +Hello {name|s|h|uc|js|j}!{~n} +Hello {name|s|h|uc|js|u}!{~n} +Hello {name|s|h|js|j|u}!{~n} +Hello {name|s|h|js|j|uc}!{~n} +Hello {name|s|h|js|u|j}!{~n} +Hello {name|s|h|js|u|uc}!{~n} +Hello {name|s|h|js|uc|j}!{~n} +Hello {name|s|h|js|uc|u}!{~n} +Hello {name|s|j|h|u|uc}!{~n} +Hello {name|s|j|h|u|js}!{~n} +Hello {name|s|j|h|uc|u}!{~n} +Hello {name|s|j|h|uc|js}!{~n} +Hello {name|s|j|h|js|u}!{~n} +Hello {name|s|j|h|js|uc}!{~n} +Hello {name|s|j|u|h|uc}!{~n} +Hello {name|s|j|u|h|js}!{~n} +Hello {name|s|j|u|uc|h}!{~n} +Hello {name|s|j|u|uc|js}!{~n} +Hello {name|s|j|u|js|h}!{~n} +Hello {name|s|j|u|js|uc}!{~n} +Hello {name|s|j|uc|h|u}!{~n} +Hello {name|s|j|uc|h|js}!{~n} +Hello {name|s|j|uc|u|h}!{~n} +Hello {name|s|j|uc|u|js}!{~n} +Hello {name|s|j|uc|js|h}!{~n} +Hello {name|s|j|uc|js|u}!{~n} +Hello {name|s|j|js|h|u}!{~n} +Hello {name|s|j|js|h|uc}!{~n} +Hello {name|s|j|js|u|h}!{~n} +Hello {name|s|j|js|u|uc}!{~n} +Hello {name|s|j|js|uc|h}!{~n} +Hello {name|s|j|js|uc|u}!{~n} +Hello {name|s|u|h|j|uc}!{~n} +Hello {name|s|u|h|j|js}!{~n} +Hello {name|s|u|h|uc|j}!{~n} +Hello {name|s|u|h|uc|js}!{~n} +Hello {name|s|u|h|js|j}!{~n} +Hello {name|s|u|h|js|uc}!{~n} +Hello {name|s|u|j|h|uc}!{~n} +Hello {name|s|u|j|h|js}!{~n} +Hello {name|s|u|j|uc|h}!{~n} +Hello {name|s|u|j|uc|js}!{~n} +Hello {name|s|u|j|js|h}!{~n} +Hello {name|s|u|j|js|uc}!{~n} +Hello {name|s|u|uc|h|j}!{~n} +Hello {name|s|u|uc|h|js}!{~n} +Hello {name|s|u|uc|j|h}!{~n} +Hello {name|s|u|uc|j|js}!{~n} +Hello {name|s|u|uc|js|h}!{~n} +Hello {name|s|u|uc|js|j}!{~n} +Hello {name|s|u|js|h|j}!{~n} +Hello {name|s|u|js|h|uc}!{~n} +Hello {name|s|u|js|j|h}!{~n} +Hello {name|s|u|js|j|uc}!{~n} +Hello {name|s|u|js|uc|h}!{~n} +Hello {name|s|u|js|uc|j}!{~n} +Hello {name|s|uc|h|j|u}!{~n} +Hello {name|s|uc|h|j|js}!{~n} +Hello {name|s|uc|h|u|j}!{~n} +Hello {name|s|uc|h|u|js}!{~n} +Hello {name|s|uc|h|js|j}!{~n} +Hello {name|s|uc|h|js|u}!{~n} +Hello {name|s|uc|j|h|u}!{~n} +Hello {name|s|uc|j|h|js}!{~n} +Hello {name|s|uc|j|u|h}!{~n} +Hello {name|s|uc|j|u|js}!{~n} +Hello {name|s|uc|j|js|h}!{~n} +Hello {name|s|uc|j|js|u}!{~n} +Hello {name|s|uc|u|h|j}!{~n} +Hello {name|s|uc|u|h|js}!{~n} +Hello {name|s|uc|u|j|h}!{~n} +Hello {name|s|uc|u|j|js}!{~n} +Hello {name|s|uc|u|js|h}!{~n} +Hello {name|s|uc|u|js|j}!{~n} +Hello {name|s|uc|js|h|j}!{~n} +Hello {name|s|uc|js|h|u}!{~n} +Hello {name|s|uc|js|j|h}!{~n} +Hello {name|s|uc|js|j|u}!{~n} +Hello {name|s|uc|js|u|h}!{~n} +Hello {name|s|uc|js|u|j}!{~n} +Hello {name|s|js|h|j|u}!{~n} +Hello {name|s|js|h|j|uc}!{~n} +Hello {name|s|js|h|u|j}!{~n} +Hello {name|s|js|h|u|uc}!{~n} +Hello {name|s|js|h|uc|j}!{~n} +Hello {name|s|js|h|uc|u}!{~n} +Hello {name|s|js|j|h|u}!{~n} +Hello {name|s|js|j|h|uc}!{~n} +Hello {name|s|js|j|u|h}!{~n} +Hello {name|s|js|j|u|uc}!{~n} +Hello {name|s|js|j|uc|h}!{~n} +Hello {name|s|js|j|uc|u}!{~n} +Hello {name|s|js|u|h|j}!{~n} +Hello {name|s|js|u|h|uc}!{~n} +Hello {name|s|js|u|j|h}!{~n} +Hello {name|s|js|u|j|uc}!{~n} +Hello {name|s|js|u|uc|h}!{~n} +Hello {name|s|js|u|uc|j}!{~n} +Hello {name|s|js|uc|h|j}!{~n} +Hello {name|s|js|uc|h|u}!{~n} +Hello {name|s|js|uc|j|h}!{~n} +Hello {name|s|js|uc|j|u}!{~n} +Hello {name|s|js|uc|u|h}!{~n} +Hello {name|s|js|uc|u|j}!{~n} +Hello {name|h|s|j|u|uc}!{~n} +Hello {name|h|s|j|u|js}!{~n} +Hello {name|h|s|j|uc|u}!{~n} +Hello {name|h|s|j|uc|js}!{~n} +Hello {name|h|s|j|js|u}!{~n} +Hello {name|h|s|j|js|uc}!{~n} +Hello {name|h|s|u|j|uc}!{~n} +Hello {name|h|s|u|j|js}!{~n} +Hello {name|h|s|u|uc|j}!{~n} +Hello {name|h|s|u|uc|js}!{~n} +Hello {name|h|s|u|js|j}!{~n} +Hello {name|h|s|u|js|uc}!{~n} +Hello {name|h|s|uc|j|u}!{~n} +Hello {name|h|s|uc|j|js}!{~n} +Hello {name|h|s|uc|u|j}!{~n} +Hello {name|h|s|uc|u|js}!{~n} +Hello {name|h|s|uc|js|j}!{~n} +Hello {name|h|s|uc|js|u}!{~n} +Hello {name|h|s|js|j|u}!{~n} +Hello {name|h|s|js|j|uc}!{~n} +Hello {name|h|s|js|u|j}!{~n} +Hello {name|h|s|js|u|uc}!{~n} +Hello {name|h|s|js|uc|j}!{~n} +Hello {name|h|s|js|uc|u}!{~n} +Hello {name|h|j|s|u|uc}!{~n} +Hello {name|h|j|s|u|js}!{~n} +Hello {name|h|j|s|uc|u}!{~n} +Hello {name|h|j|s|uc|js}!{~n} +Hello {name|h|j|s|js|u}!{~n} +Hello {name|h|j|s|js|uc}!{~n} +Hello {name|h|j|u|s|uc}!{~n} +Hello {name|h|j|u|s|js}!{~n} +Hello {name|h|j|u|uc|s}!{~n} +Hello {name|h|j|u|uc|js}!{~n} +Hello {name|h|j|u|js|s}!{~n} +Hello {name|h|j|u|js|uc}!{~n} +Hello {name|h|j|uc|s|u}!{~n} +Hello {name|h|j|uc|s|js}!{~n} +Hello {name|h|j|uc|u|s}!{~n} +Hello {name|h|j|uc|u|js}!{~n} +Hello {name|h|j|uc|js|s}!{~n} +Hello {name|h|j|uc|js|u}!{~n} +Hello {name|h|j|js|s|u}!{~n} +Hello {name|h|j|js|s|uc}!{~n} +Hello {name|h|j|js|u|s}!{~n} +Hello {name|h|j|js|u|uc}!{~n} +Hello {name|h|j|js|uc|s}!{~n} +Hello {name|h|j|js|uc|u}!{~n} +Hello {name|h|u|s|j|uc}!{~n} +Hello {name|h|u|s|j|js}!{~n} +Hello {name|h|u|s|uc|j}!{~n} +Hello {name|h|u|s|uc|js}!{~n} +Hello {name|h|u|s|js|j}!{~n} +Hello {name|h|u|s|js|uc}!{~n} +Hello {name|h|u|j|s|uc}!{~n} +Hello {name|h|u|j|s|js}!{~n} +Hello {name|h|u|j|uc|s}!{~n} +Hello {name|h|u|j|uc|js}!{~n} +Hello {name|h|u|j|js|s}!{~n} +Hello {name|h|u|j|js|uc}!{~n} +Hello {name|h|u|uc|s|j}!{~n} +Hello {name|h|u|uc|s|js}!{~n} +Hello {name|h|u|uc|j|s}!{~n} +Hello {name|h|u|uc|j|js}!{~n} +Hello {name|h|u|uc|js|s}!{~n} +Hello {name|h|u|uc|js|j}!{~n} +Hello {name|h|u|js|s|j}!{~n} +Hello {name|h|u|js|s|uc}!{~n} +Hello {name|h|u|js|j|s}!{~n} +Hello {name|h|u|js|j|uc}!{~n} +Hello {name|h|u|js|uc|s}!{~n} +Hello {name|h|u|js|uc|j}!{~n} +Hello {name|h|uc|s|j|u}!{~n} +Hello {name|h|uc|s|j|js}!{~n} +Hello {name|h|uc|s|u|j}!{~n} +Hello {name|h|uc|s|u|js}!{~n} +Hello {name|h|uc|s|js|j}!{~n} +Hello {name|h|uc|s|js|u}!{~n} +Hello {name|h|uc|j|s|u}!{~n} +Hello {name|h|uc|j|s|js}!{~n} +Hello {name|h|uc|j|u|s}!{~n} +Hello {name|h|uc|j|u|js}!{~n} +Hello {name|h|uc|j|js|s}!{~n} +Hello {name|h|uc|j|js|u}!{~n} +Hello {name|h|uc|u|s|j}!{~n} +Hello {name|h|uc|u|s|js}!{~n} +Hello {name|h|uc|u|j|s}!{~n} +Hello {name|h|uc|u|j|js}!{~n} +Hello {name|h|uc|u|js|s}!{~n} +Hello {name|h|uc|u|js|j}!{~n} +Hello {name|h|uc|js|s|j}!{~n} +Hello {name|h|uc|js|s|u}!{~n} +Hello {name|h|uc|js|j|s}!{~n} +Hello {name|h|uc|js|j|u}!{~n} +Hello {name|h|uc|js|u|s}!{~n} +Hello {name|h|uc|js|u|j}!{~n} +Hello {name|h|js|s|j|u}!{~n} +Hello {name|h|js|s|j|uc}!{~n} +Hello {name|h|js|s|u|j}!{~n} +Hello {name|h|js|s|u|uc}!{~n} +Hello {name|h|js|s|uc|j}!{~n} +Hello {name|h|js|s|uc|u}!{~n} +Hello {name|h|js|j|s|u}!{~n} +Hello {name|h|js|j|s|uc}!{~n} +Hello {name|h|js|j|u|s}!{~n} +Hello {name|h|js|j|u|uc}!{~n} +Hello {name|h|js|j|uc|s}!{~n} +Hello {name|h|js|j|uc|u}!{~n} +Hello {name|h|js|u|s|j}!{~n} +Hello {name|h|js|u|s|uc}!{~n} +Hello {name|h|js|u|j|s}!{~n} +Hello {name|h|js|u|j|uc}!{~n} +Hello {name|h|js|u|uc|s}!{~n} +Hello {name|h|js|u|uc|j}!{~n} +Hello {name|h|js|uc|s|j}!{~n} +Hello {name|h|js|uc|s|u}!{~n} +Hello {name|h|js|uc|j|s}!{~n} +Hello {name|h|js|uc|j|u}!{~n} +Hello {name|h|js|uc|u|s}!{~n} +Hello {name|h|js|uc|u|j}!{~n} +Hello {name|j|s|h|u|uc}!{~n} +Hello {name|j|s|h|u|js}!{~n} +Hello {name|j|s|h|uc|u}!{~n} +Hello {name|j|s|h|uc|js}!{~n} +Hello {name|j|s|h|js|u}!{~n} +Hello {name|j|s|h|js|uc}!{~n} +Hello {name|j|s|u|h|uc}!{~n} +Hello {name|j|s|u|h|js}!{~n} +Hello {name|j|s|u|uc|h}!{~n} +Hello {name|j|s|u|uc|js}!{~n} +Hello {name|j|s|u|js|h}!{~n} +Hello {name|j|s|u|js|uc}!{~n} +Hello {name|j|s|uc|h|u}!{~n} +Hello {name|j|s|uc|h|js}!{~n} +Hello {name|j|s|uc|u|h}!{~n} +Hello {name|j|s|uc|u|js}!{~n} +Hello {name|j|s|uc|js|h}!{~n} +Hello {name|j|s|uc|js|u}!{~n} +Hello {name|j|s|js|h|u}!{~n} +Hello {name|j|s|js|h|uc}!{~n} +Hello {name|j|s|js|u|h}!{~n} +Hello {name|j|s|js|u|uc}!{~n} +Hello {name|j|s|js|uc|h}!{~n} +Hello {name|j|s|js|uc|u}!{~n} +Hello {name|j|h|s|u|uc}!{~n} +Hello {name|j|h|s|u|js}!{~n} +Hello {name|j|h|s|uc|u}!{~n} +Hello {name|j|h|s|uc|js}!{~n} +Hello {name|j|h|s|js|u}!{~n} +Hello {name|j|h|s|js|uc}!{~n} +Hello {name|j|h|u|s|uc}!{~n} +Hello {name|j|h|u|s|js}!{~n} +Hello {name|j|h|u|uc|s}!{~n} +Hello {name|j|h|u|uc|js}!{~n} +Hello {name|j|h|u|js|s}!{~n} +Hello {name|j|h|u|js|uc}!{~n} +Hello {name|j|h|uc|s|u}!{~n} +Hello {name|j|h|uc|s|js}!{~n} +Hello {name|j|h|uc|u|s}!{~n} +Hello {name|j|h|uc|u|js}!{~n} +Hello {name|j|h|uc|js|s}!{~n} +Hello {name|j|h|uc|js|u}!{~n} +Hello {name|j|h|js|s|u}!{~n} +Hello {name|j|h|js|s|uc}!{~n} +Hello {name|j|h|js|u|s}!{~n} +Hello {name|j|h|js|u|uc}!{~n} +Hello {name|j|h|js|uc|s}!{~n} +Hello {name|j|h|js|uc|u}!{~n} +Hello {name|j|u|s|h|uc}!{~n} +Hello {name|j|u|s|h|js}!{~n} +Hello {name|j|u|s|uc|h}!{~n} +Hello {name|j|u|s|uc|js}!{~n} +Hello {name|j|u|s|js|h}!{~n} +Hello {name|j|u|s|js|uc}!{~n} +Hello {name|j|u|h|s|uc}!{~n} +Hello {name|j|u|h|s|js}!{~n} +Hello {name|j|u|h|uc|s}!{~n} +Hello {name|j|u|h|uc|js}!{~n} +Hello {name|j|u|h|js|s}!{~n} +Hello {name|j|u|h|js|uc}!{~n} +Hello {name|j|u|uc|s|h}!{~n} +Hello {name|j|u|uc|s|js}!{~n} +Hello {name|j|u|uc|h|s}!{~n} +Hello {name|j|u|uc|h|js}!{~n} +Hello {name|j|u|uc|js|s}!{~n} +Hello {name|j|u|uc|js|h}!{~n} +Hello {name|j|u|js|s|h}!{~n} +Hello {name|j|u|js|s|uc}!{~n} +Hello {name|j|u|js|h|s}!{~n} +Hello {name|j|u|js|h|uc}!{~n} +Hello {name|j|u|js|uc|s}!{~n} +Hello {name|j|u|js|uc|h}!{~n} +Hello {name|j|uc|s|h|u}!{~n} +Hello {name|j|uc|s|h|js}!{~n} +Hello {name|j|uc|s|u|h}!{~n} +Hello {name|j|uc|s|u|js}!{~n} +Hello {name|j|uc|s|js|h}!{~n} +Hello {name|j|uc|s|js|u}!{~n} +Hello {name|j|uc|h|s|u}!{~n} +Hello {name|j|uc|h|s|js}!{~n} +Hello {name|j|uc|h|u|s}!{~n} +Hello {name|j|uc|h|u|js}!{~n} +Hello {name|j|uc|h|js|s}!{~n} +Hello {name|j|uc|h|js|u}!{~n} +Hello {name|j|uc|u|s|h}!{~n} +Hello {name|j|uc|u|s|js}!{~n} +Hello {name|j|uc|u|h|s}!{~n} +Hello {name|j|uc|u|h|js}!{~n} +Hello {name|j|uc|u|js|s}!{~n} +Hello {name|j|uc|u|js|h}!{~n} +Hello {name|j|uc|js|s|h}!{~n} +Hello {name|j|uc|js|s|u}!{~n} +Hello {name|j|uc|js|h|s}!{~n} +Hello {name|j|uc|js|h|u}!{~n} +Hello {name|j|uc|js|u|s}!{~n} +Hello {name|j|uc|js|u|h}!{~n} +Hello {name|j|js|s|h|u}!{~n} +Hello {name|j|js|s|h|uc}!{~n} +Hello {name|j|js|s|u|h}!{~n} +Hello {name|j|js|s|u|uc}!{~n} +Hello {name|j|js|s|uc|h}!{~n} +Hello {name|j|js|s|uc|u}!{~n} +Hello {name|j|js|h|s|u}!{~n} +Hello {name|j|js|h|s|uc}!{~n} +Hello {name|j|js|h|u|s}!{~n} +Hello {name|j|js|h|u|uc}!{~n} +Hello {name|j|js|h|uc|s}!{~n} +Hello {name|j|js|h|uc|u}!{~n} +Hello {name|j|js|u|s|h}!{~n} +Hello {name|j|js|u|s|uc}!{~n} +Hello {name|j|js|u|h|s}!{~n} +Hello {name|j|js|u|h|uc}!{~n} +Hello {name|j|js|u|uc|s}!{~n} +Hello {name|j|js|u|uc|h}!{~n} +Hello {name|j|js|uc|s|h}!{~n} +Hello {name|j|js|uc|s|u}!{~n} +Hello {name|j|js|uc|h|s}!{~n} +Hello {name|j|js|uc|h|u}!{~n} +Hello {name|j|js|uc|u|s}!{~n} +Hello {name|j|js|uc|u|h}!{~n} +Hello {name|u|s|h|j|uc}!{~n} +Hello {name|u|s|h|j|js}!{~n} +Hello {name|u|s|h|uc|j}!{~n} +Hello {name|u|s|h|uc|js}!{~n} +Hello {name|u|s|h|js|j}!{~n} +Hello {name|u|s|h|js|uc}!{~n} +Hello {name|u|s|j|h|uc}!{~n} +Hello {name|u|s|j|h|js}!{~n} +Hello {name|u|s|j|uc|h}!{~n} +Hello {name|u|s|j|uc|js}!{~n} +Hello {name|u|s|j|js|h}!{~n} +Hello {name|u|s|j|js|uc}!{~n} +Hello {name|u|s|uc|h|j}!{~n} +Hello {name|u|s|uc|h|js}!{~n} +Hello {name|u|s|uc|j|h}!{~n} +Hello {name|u|s|uc|j|js}!{~n} +Hello {name|u|s|uc|js|h}!{~n} +Hello {name|u|s|uc|js|j}!{~n} +Hello {name|u|s|js|h|j}!{~n} +Hello {name|u|s|js|h|uc}!{~n} +Hello {name|u|s|js|j|h}!{~n} +Hello {name|u|s|js|j|uc}!{~n} +Hello {name|u|s|js|uc|h}!{~n} +Hello {name|u|s|js|uc|j}!{~n} +Hello {name|u|h|s|j|uc}!{~n} +Hello {name|u|h|s|j|js}!{~n} +Hello {name|u|h|s|uc|j}!{~n} +Hello {name|u|h|s|uc|js}!{~n} +Hello {name|u|h|s|js|j}!{~n} +Hello {name|u|h|s|js|uc}!{~n} +Hello {name|u|h|j|s|uc}!{~n} +Hello {name|u|h|j|s|js}!{~n} +Hello {name|u|h|j|uc|s}!{~n} +Hello {name|u|h|j|uc|js}!{~n} +Hello {name|u|h|j|js|s}!{~n} +Hello {name|u|h|j|js|uc}!{~n} +Hello {name|u|h|uc|s|j}!{~n} +Hello {name|u|h|uc|s|js}!{~n} +Hello {name|u|h|uc|j|s}!{~n} +Hello {name|u|h|uc|j|js}!{~n} +Hello {name|u|h|uc|js|s}!{~n} +Hello {name|u|h|uc|js|j}!{~n} +Hello {name|u|h|js|s|j}!{~n} +Hello {name|u|h|js|s|uc}!{~n} +Hello {name|u|h|js|j|s}!{~n} +Hello {name|u|h|js|j|uc}!{~n} +Hello {name|u|h|js|uc|s}!{~n} +Hello {name|u|h|js|uc|j}!{~n} +Hello {name|u|j|s|h|uc}!{~n} +Hello {name|u|j|s|h|js}!{~n} +Hello {name|u|j|s|uc|h}!{~n} +Hello {name|u|j|s|uc|js}!{~n} +Hello {name|u|j|s|js|h}!{~n} +Hello {name|u|j|s|js|uc}!{~n} +Hello {name|u|j|h|s|uc}!{~n} +Hello {name|u|j|h|s|js}!{~n} +Hello {name|u|j|h|uc|s}!{~n} +Hello {name|u|j|h|uc|js}!{~n} +Hello {name|u|j|h|js|s}!{~n} +Hello {name|u|j|h|js|uc}!{~n} +Hello {name|u|j|uc|s|h}!{~n} +Hello {name|u|j|uc|s|js}!{~n} +Hello {name|u|j|uc|h|s}!{~n} +Hello {name|u|j|uc|h|js}!{~n} +Hello {name|u|j|uc|js|s}!{~n} +Hello {name|u|j|uc|js|h}!{~n} +Hello {name|u|j|js|s|h}!{~n} +Hello {name|u|j|js|s|uc}!{~n} +Hello {name|u|j|js|h|s}!{~n} +Hello {name|u|j|js|h|uc}!{~n} +Hello {name|u|j|js|uc|s}!{~n} +Hello {name|u|j|js|uc|h}!{~n} +Hello {name|u|uc|s|h|j}!{~n} +Hello {name|u|uc|s|h|js}!{~n} +Hello {name|u|uc|s|j|h}!{~n} +Hello {name|u|uc|s|j|js}!{~n} +Hello {name|u|uc|s|js|h}!{~n} +Hello {name|u|uc|s|js|j}!{~n} +Hello {name|u|uc|h|s|j}!{~n} +Hello {name|u|uc|h|s|js}!{~n} +Hello {name|u|uc|h|j|s}!{~n} +Hello {name|u|uc|h|j|js}!{~n} +Hello {name|u|uc|h|js|s}!{~n} +Hello {name|u|uc|h|js|j}!{~n} +Hello {name|u|uc|j|s|h}!{~n} +Hello {name|u|uc|j|s|js}!{~n} +Hello {name|u|uc|j|h|s}!{~n} +Hello {name|u|uc|j|h|js}!{~n} +Hello {name|u|uc|j|js|s}!{~n} +Hello {name|u|uc|j|js|h}!{~n} +Hello {name|u|uc|js|s|h}!{~n} +Hello {name|u|uc|js|s|j}!{~n} +Hello {name|u|uc|js|h|s}!{~n} +Hello {name|u|uc|js|h|j}!{~n} +Hello {name|u|uc|js|j|s}!{~n} +Hello {name|u|uc|js|j|h}!{~n} +Hello {name|u|js|s|h|j}!{~n} +Hello {name|u|js|s|h|uc}!{~n} +Hello {name|u|js|s|j|h}!{~n} +Hello {name|u|js|s|j|uc}!{~n} +Hello {name|u|js|s|uc|h}!{~n} +Hello {name|u|js|s|uc|j}!{~n} +Hello {name|u|js|h|s|j}!{~n} +Hello {name|u|js|h|s|uc}!{~n} +Hello {name|u|js|h|j|s}!{~n} +Hello {name|u|js|h|j|uc}!{~n} +Hello {name|u|js|h|uc|s}!{~n} +Hello {name|u|js|h|uc|j}!{~n} +Hello {name|u|js|j|s|h}!{~n} +Hello {name|u|js|j|s|uc}!{~n} +Hello {name|u|js|j|h|s}!{~n} +Hello {name|u|js|j|h|uc}!{~n} +Hello {name|u|js|j|uc|s}!{~n} +Hello {name|u|js|j|uc|h}!{~n} +Hello {name|u|js|uc|s|h}!{~n} +Hello {name|u|js|uc|s|j}!{~n} +Hello {name|u|js|uc|h|s}!{~n} +Hello {name|u|js|uc|h|j}!{~n} +Hello {name|u|js|uc|j|s}!{~n} +Hello {name|u|js|uc|j|h}!{~n} +Hello {name|uc|s|h|j|u}!{~n} +Hello {name|uc|s|h|j|js}!{~n} +Hello {name|uc|s|h|u|j}!{~n} +Hello {name|uc|s|h|u|js}!{~n} +Hello {name|uc|s|h|js|j}!{~n} +Hello {name|uc|s|h|js|u}!{~n} +Hello {name|uc|s|j|h|u}!{~n} +Hello {name|uc|s|j|h|js}!{~n} +Hello {name|uc|s|j|u|h}!{~n} +Hello {name|uc|s|j|u|js}!{~n} +Hello {name|uc|s|j|js|h}!{~n} +Hello {name|uc|s|j|js|u}!{~n} +Hello {name|uc|s|u|h|j}!{~n} +Hello {name|uc|s|u|h|js}!{~n} +Hello {name|uc|s|u|j|h}!{~n} +Hello {name|uc|s|u|j|js}!{~n} +Hello {name|uc|s|u|js|h}!{~n} +Hello {name|uc|s|u|js|j}!{~n} +Hello {name|uc|s|js|h|j}!{~n} +Hello {name|uc|s|js|h|u}!{~n} +Hello {name|uc|s|js|j|h}!{~n} +Hello {name|uc|s|js|j|u}!{~n} +Hello {name|uc|s|js|u|h}!{~n} +Hello {name|uc|s|js|u|j}!{~n} +Hello {name|uc|h|s|j|u}!{~n} +Hello {name|uc|h|s|j|js}!{~n} +Hello {name|uc|h|s|u|j}!{~n} +Hello {name|uc|h|s|u|js}!{~n} +Hello {name|uc|h|s|js|j}!{~n} +Hello {name|uc|h|s|js|u}!{~n} +Hello {name|uc|h|j|s|u}!{~n} +Hello {name|uc|h|j|s|js}!{~n} +Hello {name|uc|h|j|u|s}!{~n} +Hello {name|uc|h|j|u|js}!{~n} +Hello {name|uc|h|j|js|s}!{~n} +Hello {name|uc|h|j|js|u}!{~n} +Hello {name|uc|h|u|s|j}!{~n} +Hello {name|uc|h|u|s|js}!{~n} +Hello {name|uc|h|u|j|s}!{~n} +Hello {name|uc|h|u|j|js}!{~n} +Hello {name|uc|h|u|js|s}!{~n} +Hello {name|uc|h|u|js|j}!{~n} +Hello {name|uc|h|js|s|j}!{~n} +Hello {name|uc|h|js|s|u}!{~n} +Hello {name|uc|h|js|j|s}!{~n} +Hello {name|uc|h|js|j|u}!{~n} +Hello {name|uc|h|js|u|s}!{~n} +Hello {name|uc|h|js|u|j}!{~n} +Hello {name|uc|j|s|h|u}!{~n} +Hello {name|uc|j|s|h|js}!{~n} +Hello {name|uc|j|s|u|h}!{~n} +Hello {name|uc|j|s|u|js}!{~n} +Hello {name|uc|j|s|js|h}!{~n} +Hello {name|uc|j|s|js|u}!{~n} +Hello {name|uc|j|h|s|u}!{~n} +Hello {name|uc|j|h|s|js}!{~n} +Hello {name|uc|j|h|u|s}!{~n} +Hello {name|uc|j|h|u|js}!{~n} +Hello {name|uc|j|h|js|s}!{~n} +Hello {name|uc|j|h|js|u}!{~n} +Hello {name|uc|j|u|s|h}!{~n} +Hello {name|uc|j|u|s|js}!{~n} +Hello {name|uc|j|u|h|s}!{~n} +Hello {name|uc|j|u|h|js}!{~n} +Hello {name|uc|j|u|js|s}!{~n} +Hello {name|uc|j|u|js|h}!{~n} +Hello {name|uc|j|js|s|h}!{~n} +Hello {name|uc|j|js|s|u}!{~n} +Hello {name|uc|j|js|h|s}!{~n} +Hello {name|uc|j|js|h|u}!{~n} +Hello {name|uc|j|js|u|s}!{~n} +Hello {name|uc|j|js|u|h}!{~n} +Hello {name|uc|u|s|h|j}!{~n} +Hello {name|uc|u|s|h|js}!{~n} +Hello {name|uc|u|s|j|h}!{~n} +Hello {name|uc|u|s|j|js}!{~n} +Hello {name|uc|u|s|js|h}!{~n} +Hello {name|uc|u|s|js|j}!{~n} +Hello {name|uc|u|h|s|j}!{~n} +Hello {name|uc|u|h|s|js}!{~n} +Hello {name|uc|u|h|j|s}!{~n} +Hello {name|uc|u|h|j|js}!{~n} +Hello {name|uc|u|h|js|s}!{~n} +Hello {name|uc|u|h|js|j}!{~n} +Hello {name|uc|u|j|s|h}!{~n} +Hello {name|uc|u|j|s|js}!{~n} +Hello {name|uc|u|j|h|s}!{~n} +Hello {name|uc|u|j|h|js}!{~n} +Hello {name|uc|u|j|js|s}!{~n} +Hello {name|uc|u|j|js|h}!{~n} +Hello {name|uc|u|js|s|h}!{~n} +Hello {name|uc|u|js|s|j}!{~n} +Hello {name|uc|u|js|h|s}!{~n} +Hello {name|uc|u|js|h|j}!{~n} +Hello {name|uc|u|js|j|s}!{~n} +Hello {name|uc|u|js|j|h}!{~n} +Hello {name|uc|js|s|h|j}!{~n} +Hello {name|uc|js|s|h|u}!{~n} +Hello {name|uc|js|s|j|h}!{~n} +Hello {name|uc|js|s|j|u}!{~n} +Hello {name|uc|js|s|u|h}!{~n} +Hello {name|uc|js|s|u|j}!{~n} +Hello {name|uc|js|h|s|j}!{~n} +Hello {name|uc|js|h|s|u}!{~n} +Hello {name|uc|js|h|j|s}!{~n} +Hello {name|uc|js|h|j|u}!{~n} +Hello {name|uc|js|h|u|s}!{~n} +Hello {name|uc|js|h|u|j}!{~n} +Hello {name|uc|js|j|s|h}!{~n} +Hello {name|uc|js|j|s|u}!{~n} +Hello {name|uc|js|j|h|s}!{~n} +Hello {name|uc|js|j|h|u}!{~n} +Hello {name|uc|js|j|u|s}!{~n} +Hello {name|uc|js|j|u|h}!{~n} +Hello {name|uc|js|u|s|h}!{~n} +Hello {name|uc|js|u|s|j}!{~n} +Hello {name|uc|js|u|h|s}!{~n} +Hello {name|uc|js|u|h|j}!{~n} +Hello {name|uc|js|u|j|s}!{~n} +Hello {name|uc|js|u|j|h}!{~n} +Hello {name|js|s|h|j|u}!{~n} +Hello {name|js|s|h|j|uc}!{~n} +Hello {name|js|s|h|u|j}!{~n} +Hello {name|js|s|h|u|uc}!{~n} +Hello {name|js|s|h|uc|j}!{~n} +Hello {name|js|s|h|uc|u}!{~n} +Hello {name|js|s|j|h|u}!{~n} +Hello {name|js|s|j|h|uc}!{~n} +Hello {name|js|s|j|u|h}!{~n} +Hello {name|js|s|j|u|uc}!{~n} +Hello {name|js|s|j|uc|h}!{~n} +Hello {name|js|s|j|uc|u}!{~n} +Hello {name|js|s|u|h|j}!{~n} +Hello {name|js|s|u|h|uc}!{~n} +Hello {name|js|s|u|j|h}!{~n} +Hello {name|js|s|u|j|uc}!{~n} +Hello {name|js|s|u|uc|h}!{~n} +Hello {name|js|s|u|uc|j}!{~n} +Hello {name|js|s|uc|h|j}!{~n} +Hello {name|js|s|uc|h|u}!{~n} +Hello {name|js|s|uc|j|h}!{~n} +Hello {name|js|s|uc|j|u}!{~n} +Hello {name|js|s|uc|u|h}!{~n} +Hello {name|js|s|uc|u|j}!{~n} +Hello {name|js|h|s|j|u}!{~n} +Hello {name|js|h|s|j|uc}!{~n} +Hello {name|js|h|s|u|j}!{~n} +Hello {name|js|h|s|u|uc}!{~n} +Hello {name|js|h|s|uc|j}!{~n} +Hello {name|js|h|s|uc|u}!{~n} +Hello {name|js|h|j|s|u}!{~n} +Hello {name|js|h|j|s|uc}!{~n} +Hello {name|js|h|j|u|s}!{~n} +Hello {name|js|h|j|u|uc}!{~n} +Hello {name|js|h|j|uc|s}!{~n} +Hello {name|js|h|j|uc|u}!{~n} +Hello {name|js|h|u|s|j}!{~n} +Hello {name|js|h|u|s|uc}!{~n} +Hello {name|js|h|u|j|s}!{~n} +Hello {name|js|h|u|j|uc}!{~n} +Hello {name|js|h|u|uc|s}!{~n} +Hello {name|js|h|u|uc|j}!{~n} +Hello {name|js|h|uc|s|j}!{~n} +Hello {name|js|h|uc|s|u}!{~n} +Hello {name|js|h|uc|j|s}!{~n} +Hello {name|js|h|uc|j|u}!{~n} +Hello {name|js|h|uc|u|s}!{~n} +Hello {name|js|h|uc|u|j}!{~n} +Hello {name|js|j|s|h|u}!{~n} +Hello {name|js|j|s|h|uc}!{~n} +Hello {name|js|j|s|u|h}!{~n} +Hello {name|js|j|s|u|uc}!{~n} +Hello {name|js|j|s|uc|h}!{~n} +Hello {name|js|j|s|uc|u}!{~n} +Hello {name|js|j|h|s|u}!{~n} +Hello {name|js|j|h|s|uc}!{~n} +Hello {name|js|j|h|u|s}!{~n} +Hello {name|js|j|h|u|uc}!{~n} +Hello {name|js|j|h|uc|s}!{~n} +Hello {name|js|j|h|uc|u}!{~n} +Hello {name|js|j|u|s|h}!{~n} +Hello {name|js|j|u|s|uc}!{~n} +Hello {name|js|j|u|h|s}!{~n} +Hello {name|js|j|u|h|uc}!{~n} +Hello {name|js|j|u|uc|s}!{~n} +Hello {name|js|j|u|uc|h}!{~n} +Hello {name|js|j|uc|s|h}!{~n} +Hello {name|js|j|uc|s|u}!{~n} +Hello {name|js|j|uc|h|s}!{~n} +Hello {name|js|j|uc|h|u}!{~n} +Hello {name|js|j|uc|u|s}!{~n} +Hello {name|js|j|uc|u|h}!{~n} +Hello {name|js|u|s|h|j}!{~n} +Hello {name|js|u|s|h|uc}!{~n} +Hello {name|js|u|s|j|h}!{~n} +Hello {name|js|u|s|j|uc}!{~n} +Hello {name|js|u|s|uc|h}!{~n} +Hello {name|js|u|s|uc|j}!{~n} +Hello {name|js|u|h|s|j}!{~n} +Hello {name|js|u|h|s|uc}!{~n} +Hello {name|js|u|h|j|s}!{~n} +Hello {name|js|u|h|j|uc}!{~n} +Hello {name|js|u|h|uc|s}!{~n} +Hello {name|js|u|h|uc|j}!{~n} +Hello {name|js|u|j|s|h}!{~n} +Hello {name|js|u|j|s|uc}!{~n} +Hello {name|js|u|j|h|s}!{~n} +Hello {name|js|u|j|h|uc}!{~n} +Hello {name|js|u|j|uc|s}!{~n} +Hello {name|js|u|j|uc|h}!{~n} +Hello {name|js|u|uc|s|h}!{~n} +Hello {name|js|u|uc|s|j}!{~n} +Hello {name|js|u|uc|h|s}!{~n} +Hello {name|js|u|uc|h|j}!{~n} +Hello {name|js|u|uc|j|s}!{~n} +Hello {name|js|u|uc|j|h}!{~n} +Hello {name|js|uc|s|h|j}!{~n} +Hello {name|js|uc|s|h|u}!{~n} +Hello {name|js|uc|s|j|h}!{~n} +Hello {name|js|uc|s|j|u}!{~n} +Hello {name|js|uc|s|u|h}!{~n} +Hello {name|js|uc|s|u|j}!{~n} +Hello {name|js|uc|h|s|j}!{~n} +Hello {name|js|uc|h|s|u}!{~n} +Hello {name|js|uc|h|j|s}!{~n} +Hello {name|js|uc|h|j|u}!{~n} +Hello {name|js|uc|h|u|s}!{~n} +Hello {name|js|uc|h|u|j}!{~n} +Hello {name|js|uc|j|s|h}!{~n} +Hello {name|js|uc|j|s|u}!{~n} +Hello {name|js|uc|j|h|s}!{~n} +Hello {name|js|uc|j|h|u}!{~n} +Hello {name|js|uc|j|u|s}!{~n} +Hello {name|js|uc|j|u|h}!{~n} +Hello {name|js|uc|u|s|h}!{~n} +Hello {name|js|uc|u|s|j}!{~n} +Hello {name|js|uc|u|h|s}!{~n} +Hello {name|js|uc|u|h|j}!{~n} +Hello {name|js|uc|u|j|s}!{~n} +Hello {name|js|uc|u|j|h}!{~n} diff --git a/js/test_cases/generated_filters_two/null.json b/js/test_cases/generated_filters_two/null.json new file mode 100644 index 0000000..298db8f --- /dev/null +++ b/js/test_cases/generated_filters_two/null.json @@ -0,0 +1,3 @@ +{ + "name": null +} diff --git a/js/test_cases/generated_filters_two/object.json b/js/test_cases/generated_filters_two/object.json new file mode 100644 index 0000000..7a016fe --- /dev/null +++ b/js/test_cases/generated_filters_two/object.json @@ -0,0 +1,5 @@ +{ + "name": { + "foo": "bar" + } +} diff --git a/js/test_cases/generated_filters_two/string.json b/js/test_cases/generated_filters_two/string.json new file mode 100644 index 0000000..6b66c0f --- /dev/null +++ b/js/test_cases/generated_filters_two/string.json @@ -0,0 +1,3 @@ +{ + "name": "{\"foo\": \"bar\"}" +} diff --git a/src/js/test_cases/hello_world/input1.json b/js/test_cases/hello_world/input1.json similarity index 100% rename from src/js/test_cases/hello_world/input1.json rename to js/test_cases/hello_world/input1.json diff --git a/src/js/test_cases/hello_world/input2.json b/js/test_cases/hello_world/input2.json similarity index 100% rename from src/js/test_cases/hello_world/input2.json rename to js/test_cases/hello_world/input2.json diff --git a/src/js/test_cases/hello_world/template.dust b/js/test_cases/hello_world/main.dust similarity index 100% rename from src/js/test_cases/hello_world/template.dust rename to js/test_cases/hello_world/main.dust diff --git a/js/test_cases/helpers_eq/README.md b/js/test_cases/helpers_eq/README.md new file mode 100644 index 0000000..ddcf8e1 --- /dev/null +++ b/js/test_cases/helpers_eq/README.md @@ -0,0 +1,15 @@ +Without a key parameter, neither the main block nor the else block is rendered. + +Literal values work in both keys and values. + +Can't Walk Theory +----------------- + +Assuming a missing value = cantwalk and a non-existent key = cantwalk then their equality makes sense. + +The null tests have proven that absent parameters and missing values do not equal null and therefore it is not just making all falsey values equal. + +Non-Scalar Values +----------------- + +For non-scalar values, it seems that dust does mark two values as true if they have the same path, but otherwise they are not equal. diff --git a/js/test_cases/helpers_eq/input1.json b/js/test_cases/helpers_eq/input1.json new file mode 100644 index 0000000..bfbbd94 --- /dev/null +++ b/js/test_cases/helpers_eq/input1.json @@ -0,0 +1,60 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_eq/main.dust b/js/test_cases/helpers_eq/main.dust new file mode 100644 index 0000000..ac77ac9 --- /dev/null +++ b/js/test_cases/helpers_eq/main.dust @@ -0,0 +1,42 @@ +{@eq key=str value="master"}str is equal to "master"{:else}str does not equal "master"{/eq}{~n} +{@eq key=str value="7"}str is equal to "7"{:else}str does not equal "7"{/eq}{~n} +{@eq key=int value="7"}int is equal to "7"{:else}int does not equal "7"{/eq}{~n} +{@eq key=int value=7}int is equal to 7{:else}int does not equal 7{/eq}{~n} +{@eq key=alpha value=beta}alpha is equal to beta{:else}alpha does not equal beta{/eq}{~n} +{@eq value=beta}missing key is true{:else}missing key is false{/eq}{~n} +{@eq value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/eq}{~n} +{@eq key=alpha}missing value is true{:else}missing value is false{/eq}{~n} +{@eq key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/eq}{~n} +{@eq key="master" value="master"}"master" is equal to "master"{:else}"master" does not equal "master"{/eq}{~n} +{@eq key=null}null equals a missing value{:else}null does not equal a missing value{/eq}{~n} +{@eq key=null value=gamma}null equals a non-existent value{:else}null does not equal a non-existent value{/eq}{~n} +{@eq}no parameters is true{:else}no parameters is false{/eq}{~n} +{@eq key=array_lower value=array_higher}[3,5,7] is equal to [8,9]{:else}[3,5,7] does not equal [8,9]{/eq}{~n} +{! non-scalar and copied value tests !} +{@eq key=array_lower value=array_lower}array_lower is equal to array_lower{:else}array_lower does not equal array_lower{/eq}{~n} +{@eq key=array_lower value=copy_array_lower}array_lower is equal to copy_array_lower{:else}array_lower does not equal copy_array_lower{/eq}{~n} +{@eq key=some_obj value=some_obj}some_obj is equal to some_obj{:else}some_obj does not equal some_obj{/eq}{~n} +{@eq key=some_obj value=copy_some_obj}some_obj is equal to copy_some_obj{:else}some_obj does not equal copy_some_obj{/eq}{~n} +{@eq key=some_obj value=other_obj}some_obj is equal to other_obj{:else}some_obj does not equal other_obj{/eq}{~n} +{@eq key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is equal to array_of_some_obj{:else}array_of_some_obj does not equal array_of_some_obj{/eq}{~n} +{@eq key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is equal to copy_array_of_some_obj{:else}array_of_some_obj does not equal copy_array_of_some_obj{/eq}{~n} +{@eq key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is equal to array_of_other_obj{:else}array_of_some_obj does not equal array_of_other_obj{/eq}{~n} + +Do objects with different paths referencing the same variable match?{~n} +===================================================================={~n} +{#int renamed=some_obj} + {@eq key=some_obj value=renamed}some_obj equals renamed{:else}some_obj does not equal renamed{/eq}{~n} +{/int} + +Floating point equality{~n} +======================={~n} +{@eq key=int value=7.0}int is equal to 7.0{~n}{:else}int is not equal to 7.0{~n}{/eq} + +Type cast{~n} +========={~n} +{@eq key=int value="7"}int is equal to "7"{~n}{:else}int is not equal to "7"{~n}{/eq} +{@eq key=int value="7" type="number"}int is equal to "7"::number{~n}{:else}int is not equal to "7"::number{~n}{/eq} +{@eq key=beta value=21 type="string"}beta is equal to 21::string{~n}{:else}beta is not equal to 21::string{~n}{/eq} +{@eq key=beta value="21" type="string"}beta is equal to "21"::string{~n}{:else}beta is not equal to "21"::string{~n}{/eq} +{@eq key=1 value=true_value type="number"}1 is equal to true_value::number{~n}{:else}1 is not equal to true_value::number{~n}{/eq} +{@eq key=0 value=false_value type="number"}0 is equal to false_value::number{~n}{:else}0 is not equal to false_value::number{~n}{/eq} diff --git a/js/test_cases/helpers_first/input1.json b/js/test_cases/helpers_first/input1.json new file mode 100644 index 0000000..f5fc911 --- /dev/null +++ b/js/test_cases/helpers_first/input1.json @@ -0,0 +1,23 @@ +{ + "people": [ + { + "name": "Alice", + "pet": "cat" + }, + { + "name": "Bob", + "pet": "dog" + }, + { + "name": "Chris", + "pet": "lizard" + } + ], + "toys": [ + "ball", + "bone" + ], + "scalar": 7, + "name": "global name", + "pet": "global pet" +} diff --git a/js/test_cases/helpers_first/main.dust b/js/test_cases/helpers_first/main.dust new file mode 100644 index 0000000..a7b4e1d --- /dev/null +++ b/js/test_cases/helpers_first/main.dust @@ -0,0 +1,29 @@ +Tags inside a first{~n} +==================={~n} +{#people} + {name}{@first petname="fluffy"},{pet}{petname}{/first} +{/people}{~n} + +first inside a scalar{~n} +====================={~n} +{#scalar} + {name}{@first petname="fluffy"},{pet}{petname}{/first} +{/scalar}{~n} + +Nested first inside another non-array section{~n} +============================================={~n} +{#people} + {#toys} + {name}'s pet {pet} plays with a {.}{@first}, {/first} + {/toys} +{/people}{~n} + +Else block inside a first{~n} +========================={~n} +{#people} + {name}{@first},{pet}{petname}{:else}elseblock{/first} +{/people}{~n} + +Sep outside any section{~n} +======================={~n} +{@first}first is printed outside any section{/first} diff --git a/js/test_cases/helpers_gt/README.md b/js/test_cases/helpers_gt/README.md new file mode 100644 index 0000000..527bae3 --- /dev/null +++ b/js/test_cases/helpers_gt/README.md @@ -0,0 +1,37 @@ +Based on my tests, it appears dust is sorting based on ascii-table values. This also appears to extend to unicode codepoints based on a symbols test. + +Greater than follows the same pattern for not rendering when key is omitted or null. + +Longer arrays are greater than shorter arrays if all preceding values match. + +Theory for comparing arrays: Compare the values, if theres any mismatched types then take the same action you would for non-matching scalar types. + +greater than +------------ + +All comparisons between non-matching types (for example, int vs string) appear to render the else block. + +Comparisons between non-scalar types (like arrays) appears to render the else block + + +greater than or equals to +------------------------- + +All comparisons between non-matching types (for example, int vs string) appear to render the main block. + +Comparisons between non-scalar types (like arrays) appears to render the main block + +less than +--------- + +All comparisons between non-matching types (for example, int vs string) appear to render the else block. + +Comparisons between non-scalar types (like arrays) appears to render the else block + + +less than or equal to +--------------------- + +All comparisons between non-matching types (for example, int vs string) appear to render the main block. + +Comparisons between non-scalar types (like arrays) appears to render the main block diff --git a/js/test_cases/helpers_gt/input1.json b/js/test_cases/helpers_gt/input1.json new file mode 100644 index 0000000..de2ac09 --- /dev/null +++ b/js/test_cases/helpers_gt/input1.json @@ -0,0 +1,72 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "array_lower_with_array": [ + 3, + 5, + [ + 7 + ] + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "array_higher_longer": [ + 8, + 9, + 1 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_gt/main.dust b/js/test_cases/helpers_gt/main.dust new file mode 100644 index 0000000..d90cbc1 --- /dev/null +++ b/js/test_cases/helpers_gt/main.dust @@ -0,0 +1,39 @@ +{@gt key=str value="master"}str is greater than "master"{:else}str is less than or equal to "master"{/gt}{~n} +{@gt key=str value="7"}str is greater than "7"{:else}str is less than or equal to "7"{/gt}{~n} +{@gt key=int value="7"}int is greater than "7"{:else}int is less than or equal to "7"{/gt}{~n} +{@gt key=int value=7}int is greater than 7{:else}int is less than or equal to 7{/gt}{~n} +{@gt key=int value=6}int is greater than 6{:else}int is less than or equal to 6{/gt}{~n} +{@gt key=alpha value=beta}alpha is greater than beta{:else}alpha is less than or equal to beta{/gt}{~n} +{@gt value=beta}missing key is true{:else}missing key is false{/gt}{~n} +{@gt value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/gt}{~n} +{@gt key=alpha}missing value is true{:else}missing value is false{/gt}{~n} +{@gt key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/gt}{~n} +{@gt key="master" value="master"}"master" is greater than "master"{:else}"master" is less than or equal to "master"{/gt}{~n} +{@gt key=null}null is greater than a missing value{:else}null is less than or equal to a missing value{/gt}{~n} +{@gt key=null value=gamma}null is greater than a non-existent value{:else}null is less than or equal to a non-existent value{/gt}{~n} +{@gt}no parameters is true{:else}no parameters is false{/gt}{~n} +{@gt key="a" value="]"}"a" is greater than "]"{:else}"a" is less than or equal to "]"{/gt}{~n} +{@gt key="a" value="A"}"a" is greater than "A"{:else}"a" is less than or equal to "A"{/gt}{~n} +{@gt key="a" value="}"}"a" is greater than "}"{:else}"a" is less than or equal to "}"{/gt}{~n} +{! + Commented out because unicode breaks nom + {@gt key="☃" value="☄"}"☃" is greater than "☄"{:else}"☃" is less than or equal to "☄"{/gt}{~n} + !} +{@gt key=true_value value=false_value}true is greater than false{:else}true is less than or equal to false{/gt}{~n} +{@gt key=array_lower value=array_higher}[3,5,7] is greater than [8,9]{:else}[3,5,7] is less than or equal to [8,9]{/gt}{~n} +{@gt key=array_higher value=array_lower}[8,9] is greater than [3,5,7]{:else}[8,9] is less than or equal to [3,5,7]{/gt}{~n} +{! non-scalar and copied value tests !} +{@gt key=array_lower value=array_lower}array_lower is greater than array_lower{:else}array_lower is less than or equal to array_lower{/gt}{~n} +{@gt key=array_lower value=copy_array_lower}array_lower is greater than copy_array_lower{:else}array_lower is less than or equal to copy_array_lower{/gt}{~n} +{@gt key=some_obj value=some_obj}some_obj is greater than some_obj{:else}some_obj is less than or equal to some_obj{/gt}{~n} +{@gt key=some_obj value=copy_some_obj}some_obj is greater than copy_some_obj{:else}some_obj is less than or equal to copy_some_obj{/gt}{~n} +{@gt key=some_obj value=other_obj}some_obj is greater than other_obj{:else}some_obj is less than or equal to other_obj{/gt}{~n} +{@gt key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is greater than array_of_some_obj{:else}array_of_some_obj is less than or equal to array_of_some_obj{/gt}{~n} +{@gt key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is greater than copy_array_of_some_obj{:else}array_of_some_obj is less than or equal to copy_array_of_some_obj{/gt}{~n} +{@gt key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is greater than array_of_other_obj{:else}array_of_some_obj is less than or equal to array_of_other_obj{/gt}{~n} + +{@gt key=array_higher value=array_higher_longer}array_higher is greater than array_higher_longer{:else}array_higher is less than or equal to array_higher_longer{/gt}{~n} +{@gt key=array_lower value=array_lower_with_array}array_lower is greater than array_lower_with_array{:else}array_lower is less than or equal to array_lower_with_array{/gt}{~n} +{@gt key=array_lower_with_array value=array_lower}array_lower_with_array is greater than array_lower{:else}array_lower_with_array is less than or equal to array_lower{/gt}{~n} +{@gt key=some_obj value=int}some_obj is greater than int{:else}some_obj is less than or equal to int{/gt}{~n} +{@gt key=some_obj value="[object Object]"}some_obj is greater than "[object Object]"{:else}some_obj is less than or equal to "[object Object]"{/gt}{~n} diff --git a/js/test_cases/helpers_gte/input1.json b/js/test_cases/helpers_gte/input1.json new file mode 100644 index 0000000..de2ac09 --- /dev/null +++ b/js/test_cases/helpers_gte/input1.json @@ -0,0 +1,72 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "array_lower_with_array": [ + 3, + 5, + [ + 7 + ] + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "array_higher_longer": [ + 8, + 9, + 1 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_gte/main.dust b/js/test_cases/helpers_gte/main.dust new file mode 100644 index 0000000..3db586e --- /dev/null +++ b/js/test_cases/helpers_gte/main.dust @@ -0,0 +1,38 @@ +{@gte key=str value="master"}str is greater than or equal to "master"{:else}str is less than "master"{/gte}{~n} +{@gte key=str value="7"}str is greater than or equal to "7"{:else}str is less than "7"{/gte}{~n} +{@gte key=int value="7"}int is greater than or equal to "7"{:else}int is less than "7"{/gte}{~n} +{@gte key=int value=7}int is greater than or equal to 7{:else}int is less than 7{/gte}{~n} +{@gte key=int value=6}int is greater than or equal to 6{:else}int is less than 6{/gte}{~n} +{@gte key=alpha value=beta}alpha is greater than or equal to beta{:else}alpha is less than beta{/gte}{~n} +{@gte value=beta}missing key is true{:else}missing key is false{/gte}{~n} +{@gte value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/gte}{~n} +{@gte key=alpha}missing value is true{:else}missing value is false{/gte}{~n} +{@gte key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/gte}{~n} +{@gte key="master" value="master"}"master" is greater than or equal to "master"{:else}"master" is less than "master"{/gte}{~n} +{@gte key=null}null is greater than or equal to a missing value{:else}null is less than a missing value{/gte}{~n} +{@gte key=null value=gamma}null is greater than or equal to a non-existent value{:else}null is less than a non-existent value{/gte}{~n} +{@gte}no parameters is true{:else}no parameters is false{/gte}{~n} +{@gte key="a" value="]"}"a" is greater than or equal to "]"{:else}"a" is less than "]"{/gte}{~n} +{@gte key="a" value="A"}"a" is greater than or equal to "A"{:else}"a" is less than "A"{/gte}{~n} +{@gte key="a" value="}"}"a" is greater than or equal to "}"{:else}"a" is less than "}"{/gte}{~n} +{! + Commented out because unicode breaks nom + {@gte key="☃" value="☄"}"☃" is greater than or equal to "☄"{:else}"☃" is less than "☄"{/gte}{~n} + !} +{@gte key=true_value value=false_value}true is greater than or equal to false{:else}true is less than false{/gte}{~n} +{@gte key=array_lower value=array_higher}[3,5,7] is greater than or equal to [8,9]{:else}[3,5,7] is less than [8,9]{/gte}{~n} +{@gte key=array_higher value=array_lower}[8,9] is greater than or equal to [3,5,7]{:else}[8,9] is less than [3,5,7]{/gte}{~n} +{! non-scalar and copied value tests !} +{@gte key=array_lower value=array_lower}array_lower is greater than or equal to array_lower{:else}array_lower is less than array_lower{/gte}{~n} +{@gte key=array_lower value=copy_array_lower}array_lower is greater than or equal to copy_array_lower{:else}array_lower is less than copy_array_lower{/gte}{~n} +{@gte key=some_obj value=some_obj}some_obj is greater than or equal to some_obj{:else}some_obj is less than some_obj{/gte}{~n} +{@gte key=some_obj value=copy_some_obj}some_obj is greater than or equal to copy_some_obj{:else}some_obj is less than copy_some_obj{/gte}{~n} +{@gte key=some_obj value=other_obj}some_obj is greater than or equal to other_obj{:else}some_obj is less than other_obj{/gte}{~n} +{@gte key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is greater than or equal to array_of_some_obj{:else}array_of_some_obj is less than array_of_some_obj{/gte}{~n} +{@gte key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is greater than or equal to copy_array_of_some_obj{:else}array_of_some_obj is less than copy_array_of_some_obj{/gte}{~n} +{@gte key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is greater than or equal to array_of_other_obj{:else}array_of_some_obj is less than array_of_other_obj{/gte}{~n} +{@gte key=array_higher value=array_higher_longer}array_higher is greater than or equal to array_higher_longer{:else}array_higher is less than array_higher_longer{/gte}{~n} +{@gte key=array_lower value=array_lower_with_array}array_lower is greater than or equal to array_lower_with_array{:else}array_lower is less than array_lower_with_array{/gte}{~n} +{@gte key=array_lower_with_array value=array_lower}array_lower_with_array is greater than or equal to array_lower{:else}array_lower_with_array is less than array_lower{/gte}{~n} +{@gte key=some_obj value=int}some_obj is greater than or equal to int{:else}some_obj is less than int{/gte}{~n} +{@gte key=some_obj value="[object Object]"}some_obj is greater than or equal to "[object Object]"{:else}some_obj is less than "[object Object]"{/gte}{~n} diff --git a/js/test_cases/helpers_last/input1.json b/js/test_cases/helpers_last/input1.json new file mode 100644 index 0000000..f5fc911 --- /dev/null +++ b/js/test_cases/helpers_last/input1.json @@ -0,0 +1,23 @@ +{ + "people": [ + { + "name": "Alice", + "pet": "cat" + }, + { + "name": "Bob", + "pet": "dog" + }, + { + "name": "Chris", + "pet": "lizard" + } + ], + "toys": [ + "ball", + "bone" + ], + "scalar": 7, + "name": "global name", + "pet": "global pet" +} diff --git a/js/test_cases/helpers_last/main.dust b/js/test_cases/helpers_last/main.dust new file mode 100644 index 0000000..5b6cfdd --- /dev/null +++ b/js/test_cases/helpers_last/main.dust @@ -0,0 +1,29 @@ +Tags inside a last{~n} +=================={~n} +{#people} + {name}{@last petname="fluffy"},{pet}{petname}{/last} +{/people}{~n} + +last inside a scalar{~n} +===================={~n} +{#scalar} + {name}{@last petname="fluffy"},{pet}{petname}{/last} +{/scalar}{~n} + +Nested last inside another non-array section{~n} +============================================{~n} +{#people} + {#toys} + {name}'s pet {pet} plays with a {.}{@last}, {/last} + {/toys} +{/people}{~n} + +Else block inside a last{~n} +========================{~n} +{#people} + {name}{@last},{pet}{petname}{:else}elseblock{/last} +{/people}{~n} + +Sep outside any section{~n} +======================={~n} +{@last}last is printed outside any section{/last} diff --git a/js/test_cases/helpers_lt/input1.json b/js/test_cases/helpers_lt/input1.json new file mode 100644 index 0000000..de2ac09 --- /dev/null +++ b/js/test_cases/helpers_lt/input1.json @@ -0,0 +1,72 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "array_lower_with_array": [ + 3, + 5, + [ + 7 + ] + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "array_higher_longer": [ + 8, + 9, + 1 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_lt/main.dust b/js/test_cases/helpers_lt/main.dust new file mode 100644 index 0000000..f373546 --- /dev/null +++ b/js/test_cases/helpers_lt/main.dust @@ -0,0 +1,38 @@ +{@lt key=str value="master"}str is less than "master"{:else}str is greater than or equal to "master"{/lt}{~n} +{@lt key=str value="7"}str is less than "7"{:else}str is greater than or equal to "7"{/lt}{~n} +{@lt key=int value="7"}int is less than "7"{:else}int is greater than or equal to "7"{/lt}{~n} +{@lt key=int value=7}int is less than 7{:else}int is greater than or equal to 7{/lt}{~n} +{@lt key=int value=6}int is less than 6{:else}int is greater than or equal to 6{/lt}{~n} +{@lt key=alpha value=beta}alpha is less than beta{:else}alpha is greater than or equal to beta{/lt}{~n} +{@lt value=beta}missing key is true{:else}missing key is false{/lt}{~n} +{@lt value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/lt}{~n} +{@lt key=alpha}missing value is true{:else}missing value is false{/lt}{~n} +{@lt key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/lt}{~n} +{@lt key="master" value="master"}"master" is less than "master"{:else}"master" is greater than or equal to "master"{/lt}{~n} +{@lt key=null}null is less than a missing value{:else}null is greater than or equal to a missing value{/lt}{~n} +{@lt key=null value=gamma}null is less than a non-existent value{:else}null is greater than or equal to a non-existent value{/lt}{~n} +{@lt}no parameters is true{:else}no parameters is false{/lt}{~n} +{@lt key="a" value="]"}"a" is less than "]"{:else}"a" is greater than or equal to "]"{/lt}{~n} +{@lt key="a" value="A"}"a" is less than "A"{:else}"a" is greater than or equal to "A"{/lt}{~n} +{@lt key="a" value="}"}"a" is less than "}"{:else}"a" is greater than or equal to "}"{/lt}{~n} +{! + Commented out because unicode breaks nom + {@lt key="☃" value="☄"}"☃" is less than "☄"{:else}"☃" is greater than or equal to "☄"{/lt}{~n} + !} +{@lt key=true_value value=false_value}true is less than false{:else}true is greater than or equal to false{/lt}{~n} +{@lt key=array_lower value=array_higher}[3,5,7] is less than [8,9]{:else}[3,5,7] is greater than or equal to [8,9]{/lt}{~n} +{@lt key=array_higher value=array_lower}[8,9] is less than [3,5,7]{:else}[8,9] is greater than or equal to [3,5,7]{/lt}{~n} +{! non-scalar and copied value tests !} +{@lt key=array_lower value=array_lower}array_lower is less than array_lower{:else}array_lower is greater than or equal to array_lower{/lt}{~n} +{@lt key=array_lower value=copy_array_lower}array_lower is less than copy_array_lower{:else}array_lower is greater than or equal to copy_array_lower{/lt}{~n} +{@lt key=some_obj value=some_obj}some_obj is less than some_obj{:else}some_obj is greater than or equal to some_obj{/lt}{~n} +{@lt key=some_obj value=copy_some_obj}some_obj is less than copy_some_obj{:else}some_obj is greater than or equal to copy_some_obj{/lt}{~n} +{@lt key=some_obj value=other_obj}some_obj is less than other_obj{:else}some_obj is greater than or equal to other_obj{/lt}{~n} +{@lt key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is less than array_of_some_obj{:else}array_of_some_obj is greater than or equal to array_of_some_obj{/lt}{~n} +{@lt key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is less than copy_array_of_some_obj{:else}array_of_some_obj is greater than or equal to copy_array_of_some_obj{/lt}{~n} +{@lt key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is less than array_of_other_obj{:else}array_of_some_obj is greater than or equal to array_of_other_obj{/lt}{~n} +{@lt key=array_higher value=array_higher_longer}array_higher is less than array_higher_longer{:else}array_higher is greater than or equal to array_higher_longer{/lt}{~n} +{@lt key=array_lower value=array_lower_with_array}array_lower is less than array_lower_with_array{:else}array_lower is greater than or equal to array_lower_with_array{/lt}{~n} +{@lt key=array_lower_with_array value=array_lower}array_lower_with_array is less than array_lower{:else}array_lower_with_array is greater than or equal to array_lower{/lt}{~n} +{@lt key=some_obj value=int}some_obj is less than int{:else}some_obj is greater than or equal to int{/lt}{~n} +{@lt key=some_obj value="[object Object]"}some_obj is less than "[object Object]"{:else}some_obj is greater than or equal to "[object Object]"{/lt}{~n} diff --git a/js/test_cases/helpers_lte/input1.json b/js/test_cases/helpers_lte/input1.json new file mode 100644 index 0000000..de2ac09 --- /dev/null +++ b/js/test_cases/helpers_lte/input1.json @@ -0,0 +1,72 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "array_lower_with_array": [ + 3, + 5, + [ + 7 + ] + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "array_higher_longer": [ + 8, + 9, + 1 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_lte/main.dust b/js/test_cases/helpers_lte/main.dust new file mode 100644 index 0000000..6954d16 --- /dev/null +++ b/js/test_cases/helpers_lte/main.dust @@ -0,0 +1,38 @@ +{@lte key=str value="master"}str is less than or equal to "master"{:else}str is greater than "master"{/lte}{~n} +{@lte key=str value="7"}str is less than or equal to "7"{:else}str is greater than "7"{/lte}{~n} +{@lte key=int value="7"}int is less than or equal to "7"{:else}int is greater than "7"{/lte}{~n} +{@lte key=int value=7}int is less than or equal to 7{:else}int is greater than 7{/lte}{~n} +{@lte key=int value=6}int is less than or equal to 6{:else}int is greater than 6{/lte}{~n} +{@lte key=alpha value=beta}alpha is less than or equal to beta{:else}alpha is greater than beta{/lte}{~n} +{@lte value=beta}missing key is true{:else}missing key is false{/lte}{~n} +{@lte value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/lte}{~n} +{@lte key=alpha}missing value is true{:else}missing value is false{/lte}{~n} +{@lte key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/lte}{~n} +{@lte key="master" value="master"}"master" is less than or equal to "master"{:else}"master" is greater than "master"{/lte}{~n} +{@lte key=null}null is less than or equal to a missing value{:else}null is greater than a missing value{/lte}{~n} +{@lte key=null value=gamma}null is less than or equal to a non-existent value{:else}null is greater than a non-existent value{/lte}{~n} +{@lte}no parameters is true{:else}no parameters is false{/lte}{~n} +{@lte key="a" value="]"}"a" is less than or equal to "]"{:else}"a" is greater than "]"{/lte}{~n} +{@lte key="a" value="A"}"a" is less than or equal to "A"{:else}"a" is greater than "A"{/lte}{~n} +{@lte key="a" value="}"}"a" is less than or equal to "}"{:else}"a" is greater than "}"{/lte}{~n} +{! + Commented out because unicode breaks nom + {@lte key="☃" value="☄"}"☃" is less than or equal to "☄"{:else}"☃" is greater than "☄"{/lte}{~n} + !} +{@lte key=true_value value=false_value}true is less than or equal to false{:else}true is greater than false{/lte}{~n} +{@lte key=array_lower value=array_higher}[3,5,7] is less than or equal to [8,9]{:else}[3,5,7] is greater than [8,9]{/lte}{~n} +{@lte key=array_higher value=array_lower}[8,9] is less than or equal to [3,5,7]{:else}[8,9] is greater than [3,5,7]{/lte}{~n} +{! non-scalar and copied value tests !} +{@lte key=array_lower value=array_lower}array_lower is less than or equal to array_lower{:else}array_lower is greater than array_lower{/lte}{~n} +{@lte key=array_lower value=copy_array_lower}array_lower is less than or equal to copy_array_lower{:else}array_lower is greater than copy_array_lower{/lte}{~n} +{@lte key=some_obj value=some_obj}some_obj is less than or equal to some_obj{:else}some_obj is greater than some_obj{/lte}{~n} +{@lte key=some_obj value=copy_some_obj}some_obj is less than or equal to copy_some_obj{:else}some_obj is greater than copy_some_obj{/lte}{~n} +{@lte key=some_obj value=other_obj}some_obj is less than or equal to other_obj{:else}some_obj is greater than other_obj{/lte}{~n} +{@lte key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is less than or equal to array_of_some_obj{:else}array_of_some_obj is greater than array_of_some_obj{/lte}{~n} +{@lte key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is less than or equal to copy_array_of_some_obj{:else}array_of_some_obj is greater than copy_array_of_some_obj{/lte}{~n} +{@lte key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is less than or equal to array_of_other_obj{:else}array_of_some_obj is greater than array_of_other_obj{/lte}{~n} +{@lte key=array_higher value=array_higher_longer}array_higher is less than or equal to array_higher_longer{:else}array_higher is greater than array_higher_longer{/lte}{~n} +{@lte key=array_lower value=array_lower_with_array}array_lower is less than or equal to array_lower_with_array{:else}array_lower is greater than array_lower_with_array{/lte}{~n} +{@lte key=array_lower_with_array value=array_lower}array_lower_with_array is less than or equal to array_lower{:else}array_lower_with_array is greater than array_lower{/lte}{~n} +{@lte key=some_obj value=int}some_obj is less than or equal to int{:else}some_obj is greater than int{/lte}{~n} +{@lte key=some_obj value="[object Object]"}some_obj is less than or equal to "[object Object]"{:else}some_obj is greater than "[object Object]"{/lte}{~n} diff --git a/js/test_cases/helpers_math/README.md b/js/test_cases/helpers_math/README.md new file mode 100644 index 0000000..9ae12f0 --- /dev/null +++ b/js/test_cases/helpers_math/README.md @@ -0,0 +1,9 @@ +Early termination +----------------- + +If the math helper has a body then it stops rendering conditionals after the first conditional that returns true. Rendering an else block does not cause early termination, but else blocks are rendered. + +Non-number values +----------------- + +If the math operation involves non-numbers, NaN is returned. diff --git a/js/test_cases/helpers_math/input1.json b/js/test_cases/helpers_math/input1.json new file mode 100644 index 0000000..3b5727f --- /dev/null +++ b/js/test_cases/helpers_math/input1.json @@ -0,0 +1,7 @@ +{ + "number_7": 7, + "string_7": "7", + "string_cat": "cat", + "add_operation": "add", + "a_operation": "a" +} diff --git a/js/test_cases/helpers_math/main.dust b/js/test_cases/helpers_math/main.dust new file mode 100644 index 0000000..5559821 --- /dev/null +++ b/js/test_cases/helpers_math/main.dust @@ -0,0 +1,111 @@ +Bodiless math{~n} +============={~n} +{@math key="7" method="add" operand="4" /}{~n} +{@math key=number_7 method="add" operand="4" /}{~n} +{@math key=string_7 method="add" operand="4" /}{~n} +{@math key=string_cat method="add" operand="4" /}{~n} +{@math key=string_cat method="add" operand="foo" /}{~n} + +Math with body: dot reference{~n} +============================={~n} +{@math key="7" method="add" operand="4"} + {.}{~n} +{/math} + +Math with body: eq literal{~n} +=========================={~n} +{@math key="7" method="add" operand="4"} + {@eq value=11}math result is 11{:else}math result is not 11{/eq}{~n} +{/math} + +Math with body: eq string{~n} +========================={~n} +{@math key="7" method="add" operand="4"} + {@eq value="11"}math result is "11"{:else}math result is not "11"{/eq}{~n} +{/math} + +Math with body: eq else block{~n} +============================={~n} +{@math key="7" method="add" operand="4"} + {@eq value=12}math result is 12{:else}math result is not 12{/eq}{~n} +{/math} + +Math with body: standalone eq{~n} +============================={~n} +{@math key="7" method="add" operand="4"} + {@eq key=12 value=12}12 is 12{:else}12 is not 12{/eq}{~n} +{/math} + +Math with body: standalone else block{~n} +====================================={~n} +{@math key="7" method="add" operand="4"} + {@eq key=11 value=12}11 is 12{:else}11 is not 12{/eq}{~n} +{/math} + +Math with body: early termination{~n} +============================={~n} +{@math key="7" method="add" operand="4"} + {@eq value=11}math result is 11{~n}{:else}math result is not 11{~n}{/eq} + {@eq value=11}math result is 11{~n}{:else}math result is not 11{~n}{/eq} +{/math} + +Math with body: early termination else block{~n} +============================================{~n} +{@math key="7" method="add" operand="4"} + {@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq} + {@eq value=11}math result is 11{~n}{:else}math result is not 11{~n}{/eq} + {@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq} +{/math} + + +Math with body: any{~n} +==================={~n} +{@math key="7" method="add" operand="4"} + {@eq value=10}math result is 10{~n}{:else}math result is not 10{~n}{/eq} + {@eq value=11}math result is 11{~n}{:else}math result is not 11{~n}{/eq} + {@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq} + {@any}We found the value{~n}{/any} + {@none}We did not find the value{~n}{/none} +{/math} + +Math with body: none{~n} +===================={~n} +{@math key="7" method="add" operand="4"} + {@eq value=10}math result is 10{~n}{:else}math result is not 10{~n}{/eq} + {@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq} + {@any}We found the value{~n}{/any} + {@none}We did not find the value{~n}{/none} +{/math} + +Math where method is a reference{~n} +================================{~n} +{@math key="7" method=add_operation operand="4" /}{~n} + +Math where method is a template{~n} +==============================={~n} +{@math key="7" method="{a_operation}dd" operand="4" /}{~n} + +Math where key is a template{~n} +============================{~n} +{@math key="{string_7}1" method="add" operand="4" /}{~n} + +Math where method is absent{~n} +============================{~n} +{@math key="7" operand="4" /}{~n} + +Math where method is missing{~n} +============================{~n} +{@math key="7" method=foobar operand="4" /}{~n} + +Math with unknown method{~n} +========================{~n} +{@math key="7" method="twirl" operand="4" /}{~n} + +Math with body where method is absent{~n} +===================================={~n} +{@math key="7" operand="4"} + {@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq} + {@any}We found the value{~n}{/any} + {@none}We did not find the value{~n}{/none} + plain text not inside a tag{~n} +{/math} diff --git a/js/test_cases/helpers_ne/input1.json b/js/test_cases/helpers_ne/input1.json new file mode 100644 index 0000000..bfbbd94 --- /dev/null +++ b/js/test_cases/helpers_ne/input1.json @@ -0,0 +1,60 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_ne/main.dust b/js/test_cases/helpers_ne/main.dust new file mode 100644 index 0000000..dc09b8f --- /dev/null +++ b/js/test_cases/helpers_ne/main.dust @@ -0,0 +1,23 @@ +{@ne key=str value="master"}str does not equal "master"{:else}str is equal to "master"{/ne}{~n} +{@ne key=str value="7"}str does not equal "7"{:else}str is equal to "7"{/ne}{~n} +{@ne key=int value="7"}int does not equal "7"{:else}int is equal to "7"{/ne}{~n} +{@ne key=int value=7}int does not equal 7{:else}int is equal to 7{/ne}{~n} +{@ne key=alpha value=beta}alpha does not equal beta{:else}alpha is equal to beta{/ne}{~n} +{@ne value=beta}missing key is true{:else}missing key is false{/ne}{~n} +{@ne value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/ne}{~n} +{@ne key=alpha}missing value is true{:else}missing value is false{/ne}{~n} +{@ne key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/ne}{~n} +{@ne key="master" value="master"}"master" does not equal "master"{:else}"master" is equal to "master"{/ne}{~n} +{@ne key=null}null does not equal a missing value{:else}null equals a missing value{/ne}{~n} +{@ne key=null value=gamma}null does not equal non-existent value{:else}null equals a non-existent value{/ne}{~n} +{@ne}no parameters is true{:else}no parameters is false{/ne}{~n} +{@ne key=array_lower value=array_higher}[3,5,7] does not equal [8,9]{:else}[3,5,7] is equal to [8,9]{/ne}{~n} +{! non-scalar and copied value tests !} +{@ne key=array_lower value=array_lower}array_lower does not equal array_lower{:else}array_lower is equals to array_lower{/ne}{~n} +{@ne key=array_lower value=copy_array_lower}array_lower does not equal copy_array_lower{:else}array_lower is equals to copy_array_lower{/ne}{~n} +{@ne key=some_obj value=some_obj}some_obj does not equal some_obj{:else}some_obj is equals to some_obj{/ne}{~n} +{@ne key=some_obj value=copy_some_obj}some_obj does not equal copy_some_obj{:else}some_obj is equals to copy_some_obj{/ne}{~n} +{@ne key=some_obj value=other_obj}some_obj does not equal other_obj{:else}some_obj is equals to other_obj{/ne}{~n} +{@ne key=array_of_some_obj value=array_of_some_obj}array_of_some_obj does not equal array_of_some_obj{:else}array_of_some_obj is equals to array_of_some_obj{/ne}{~n} +{@ne key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj does not equal copy_array_of_some_obj{:else}array_of_some_obj is equals to copy_array_of_some_obj{/ne}{~n} +{@ne key=array_of_some_obj value=array_of_other_obj}array_of_some_obj does not equal array_of_other_obj{:else}array_of_some_obj is equals to array_of_other_obj{/ne}{~n} diff --git a/js/test_cases/helpers_select/README.md b/js/test_cases/helpers_select/README.md new file mode 100644 index 0000000..ba85db9 --- /dev/null +++ b/js/test_cases/helpers_select/README.md @@ -0,0 +1,16 @@ +Depth +----- +Select blocks can contain any element type inside, just like most blocks. They, however, only apply their select-checking to immediate children. This prevents things like loops with conditionals inside of them. + +Early termination +----------------- + +Comparisons are done in-order and only the first matching comparison (eq/ne/gt/gte/lt/lte) is evaluated. All other non-comparison elements inside the select tag are still rendered normally. + +Matching is terminated even if the first matching comparison has its own key. + +Else blocks are rendered until the first matching comparison but else blocks after that are not rendered. + +Default vs none +--------------- +Default was deprecated as of dust 1.6.0 and no longer does anything. It was deprecated because it was essentially the same as none except that there could only be one per select block and it had to be placed after all the other conditions to make sure they were all false. None is more flexible. diff --git a/js/test_cases/helpers_select/input1.json b/js/test_cases/helpers_select/input1.json new file mode 100644 index 0000000..9b9a458 --- /dev/null +++ b/js/test_cases/helpers_select/input1.json @@ -0,0 +1,19 @@ +{ + "pet": "cat", + "person": "Alice", + "pet_names": [ + { + "type": "cat", + "pet_name": "fluffy" + }, + { + "type": "dog", + "pet_name": "rover" + }, + { + "type": "lizard", + "pet_name": "dave" + } + ], + "scalar": 7 +} diff --git a/js/test_cases/helpers_select/main.dust b/js/test_cases/helpers_select/main.dust new file mode 100644 index 0000000..6c2d49f --- /dev/null +++ b/js/test_cases/helpers_select/main.dust @@ -0,0 +1,124 @@ +Simple select{~n} +============={~n} +{@select key=pet} + {@eq value="cat"}Lets name your pet fluffy{~n}{/eq} + {@eq value="dog"}Lets name your pet rover{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{/eq} +{/select} + +non-comparison bodies inside select{~n} +==================================={~n} +{@select key=pet} + text not inside a comparison{~n} + {@eq value="cat"}Lets name your pet fluffy{~n}{/eq} + {@eq value="dog"}Lets name your pet rover{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{/eq} +{/select} + +@any{~n} +===={~n} +{@select key=pet} + {@any}{person} has a pet!{~n}{/any} + {@eq value="cat"}Lets name your pet fluffy{~n}{/eq} + {@eq value="dog"}Lets name your pet rover{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{/eq} +{/select} + +@none{~n} +====={~n} +{@select key=pet} + {@any}{person} has a pet!{~n}{/any} + {@none}I don't know what to name {person}'s pet...{~n}{/none} + {@eq value="dog"}Lets name your pet rover{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{/eq} +{/select} + +Is key added to the context?{~n} +============================{~n} +{@select key=pet} + {key}{~n} + {@any}{person} has a pet!{~n}{/any} +{/select}{~n} + +Conditionals inside loop sections{~n} +================================={~n} +{@select key=pet} + {@any}{person} has a pet!{~n}{/any} + {#pet_names} + {@eq value=type}Lets name your pet {pet_name}{~n}{/eq} + {/pet_names} + {@none}I don't know what to name {person}'s pet...{~n}{/none} +{/select}{~n} + +Conditionals inside scalar sections{~n} +==================================={~n} +{@select key=pet} + {@any}{person} has a pet!{~n}{/any} + {#scalar} + {@eq value="cat"}Lets name your pet fluffy{~n}{/eq} + {@eq value="dog"}Lets name your pet rover{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{/eq} + {/scalar} +{/select}{~n} + +Sections inside select without conditionals{~n} +==========================================={~n} +{@select key=pet} + {@any}{person} has a pet!{~n}{/any} + {#pet_names} + If your pet was a {type} we'd name it {pet_name}{~n} + {/pet_names} + {@eq value="cat"}Lets name your pet fluffy{~n}{/eq} + {@eq value="dog"}Lets name your pet rover{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{/eq} +{/select}{~n} + +Early termination{~n} +================={~n} +{@select key=pet} + {@eq value="cat"}Lets name your pet fluffy{~n}{/eq} + {@eq value="cat"}Lets name your pet whiskers{~n}{/eq} + {@eq value="dog"}Lets name your pet rover{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{/eq} + {@any}{person} has a pet!{~n}{/any} + text not inside a comparison{~n} + {#pet_names} + If your pet was a {type} we'd name it {pet_name}{~n} + {/pet_names} +{/select}{~n} + +Early termination stand-alone comparison{~n} +========================================{~n} +{@select key=pet} + {@eq key="duck" value="duck"}Lets name your pet quackers{~n}{/eq} + {@eq value="cat"}Lets name your pet whiskers{~n}{/eq} + {@eq value="dog"}Lets name your pet rover{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{/eq} + {@any}{person} has a pet!{~n}{/any} + text not inside a comparison{~n} + {#pet_names} + If your pet was a {type} we'd name it {pet_name}{~n} + {/pet_names} +{/select}{~n} + +Early termination else block{~n} +============================{~n} +{@select key=pet} + {@eq value="dog"}Lets name your pet rover{~n}{:else}Lets not name your pet rover{~n}{/eq} + {@eq value="cat"}Lets name your pet fluffy{~n}{:else}Lets not name your pet fluffy{~n}{/eq} + {@eq value="cat"}Lets name your pet whiskers{~n}{:else}Lets not name your pet whiskers{~n}{/eq} + {@eq value="lizard"}Lets name your pet dave{~n}{:else}Lets not name your pet dave{~n}{/eq} + {@any}{person} has a pet!{~n}{/any} + text not inside a comparison{~n} + {#pet_names} + If your pet was a {type} we'd name it {pet_name}{~n} + {/pet_names} +{/select}{~n} + +@any alone{~n} +=========={~n} +{@any}{person} has a pet!{~n}{/any} + +@none alone{~n} +==========={~n} +{@none}I don't know what to name {person}'s pet...{~n}{/none} diff --git a/js/test_cases/helpers_sep/input1.json b/js/test_cases/helpers_sep/input1.json new file mode 100644 index 0000000..f5fc911 --- /dev/null +++ b/js/test_cases/helpers_sep/input1.json @@ -0,0 +1,23 @@ +{ + "people": [ + { + "name": "Alice", + "pet": "cat" + }, + { + "name": "Bob", + "pet": "dog" + }, + { + "name": "Chris", + "pet": "lizard" + } + ], + "toys": [ + "ball", + "bone" + ], + "scalar": 7, + "name": "global name", + "pet": "global pet" +} diff --git a/js/test_cases/helpers_sep/main.dust b/js/test_cases/helpers_sep/main.dust new file mode 100644 index 0000000..492d43e --- /dev/null +++ b/js/test_cases/helpers_sep/main.dust @@ -0,0 +1,29 @@ +Tags inside a sep{~n} +================={~n} +{#people} + {name}{@sep petname="fluffy"},{pet}{petname}{/sep} +{/people}{~n} + +sep inside a scalar{~n} +==================={~n} +{#scalar} + {name}{@sep petname="fluffy"},{pet}{petname}{/sep} +{/scalar}{~n} + +Nested sep inside another non-array section{~n} +==========================================={~n} +{#people} + {#toys} + {name}'s pet {pet} plays with a {.}{@sep}, {/sep} + {/toys} +{/people}{~n} + +Else block inside a sep{~n} +======================={~n} +{#people} + {name}{@sep},{pet}{petname}{:else}elseblock{/sep} +{/people}{~n} + +Sep outside any section{~n} +======================={~n} +{@sep}sep is printed outside any section{/sep} diff --git a/js/test_cases/helpers_size/README.md b/js/test_cases/helpers_size/README.md new file mode 100644 index 0000000..76b797a --- /dev/null +++ b/js/test_cases/helpers_size/README.md @@ -0,0 +1,18 @@ +Excerpt from [the DustJS tutorial](https://github.com/linkedin/dustjs/wiki/Dust-Tutorial#size_keyxxx___size_helper_Available_in_Dust_V11_release): + + Array - number of elements, [1,2,3,4] has size=4 + String - length of the string, "abcdef" has size=6 + Object - Number of properties in the object, {a:4, b:8, c:15, d:16} has size=4 + Number - Value of the number, 23 has size 23 and 3.14 has size 3.14 + Undefined, 0, empty string - zero + Any other value - length after conversion to string + +This appears to be inaccurate (probably out of date because that tutorial was deprecated in favor of [dustjs.com](https://www.dustjs.com/), but the latter is incomplete and lacking any reference to the `@size` helper. + +Corrections +----------- +- Booleans are 0, not converted to strings + +Oddities +-------- +Reference parameters (like `foo="{bar}"`) are usually treated as strings but it seems if it contains ONLY a reference to a value and not anything else, then it is still treated as a number. diff --git a/js/test_cases/helpers_size/inputarray.json b/js/test_cases/helpers_size/inputarray.json new file mode 100644 index 0000000..98f6406 --- /dev/null +++ b/js/test_cases/helpers_size/inputarray.json @@ -0,0 +1,7 @@ +{ + "val": [ + "Alice", + "Bob", + "Chris" + ] +} diff --git a/js/test_cases/helpers_size/inputemptystring.json b/js/test_cases/helpers_size/inputemptystring.json new file mode 100644 index 0000000..f911fb5 --- /dev/null +++ b/js/test_cases/helpers_size/inputemptystring.json @@ -0,0 +1,3 @@ +{ + "val": "" +} diff --git a/js/test_cases/helpers_size/inputfalse.json b/js/test_cases/helpers_size/inputfalse.json new file mode 100644 index 0000000..9cc7fda --- /dev/null +++ b/js/test_cases/helpers_size/inputfalse.json @@ -0,0 +1,3 @@ +{ + "val": false +} diff --git a/js/test_cases/helpers_size/inputmissing.json b/js/test_cases/helpers_size/inputmissing.json new file mode 100644 index 0000000..95cf907 --- /dev/null +++ b/js/test_cases/helpers_size/inputmissing.json @@ -0,0 +1,3 @@ +{ + "notval": "Alice" +} diff --git a/js/test_cases/helpers_size/inputnumber.json b/js/test_cases/helpers_size/inputnumber.json new file mode 100644 index 0000000..378b4a9 --- /dev/null +++ b/js/test_cases/helpers_size/inputnumber.json @@ -0,0 +1,3 @@ +{ + "val": 7.21 +} diff --git a/js/test_cases/helpers_size/inputobject.json b/js/test_cases/helpers_size/inputobject.json new file mode 100644 index 0000000..e511117 --- /dev/null +++ b/js/test_cases/helpers_size/inputobject.json @@ -0,0 +1,6 @@ +{ + "val": { + "name": "fluffy", + "pet": "cat" + } +} diff --git a/js/test_cases/helpers_size/inputstring.json b/js/test_cases/helpers_size/inputstring.json new file mode 100644 index 0000000..fea025d --- /dev/null +++ b/js/test_cases/helpers_size/inputstring.json @@ -0,0 +1,3 @@ +{ + "val": "7.99" +} diff --git a/js/test_cases/helpers_size/inputtrue.json b/js/test_cases/helpers_size/inputtrue.json new file mode 100644 index 0000000..25d6b1f --- /dev/null +++ b/js/test_cases/helpers_size/inputtrue.json @@ -0,0 +1,3 @@ +{ + "val": true +} diff --git a/js/test_cases/helpers_size/inputzero.json b/js/test_cases/helpers_size/inputzero.json new file mode 100644 index 0000000..d6110e8 --- /dev/null +++ b/js/test_cases/helpers_size/inputzero.json @@ -0,0 +1,3 @@ +{ + "val": 0 +} diff --git a/js/test_cases/helpers_size/main.dust b/js/test_cases/helpers_size/main.dust new file mode 100644 index 0000000..52b398c --- /dev/null +++ b/js/test_cases/helpers_size/main.dust @@ -0,0 +1,10 @@ +The size of val ({val|js|s}) is {@size key=val /}{~n} +The size of "{~lb}val{~rb}" is {@size key="{val}" /}{~n} +{#val foo="{val}"} + The size of foo ({foo|js|s}) is {@size key=foo /}{~n} + The size of "{~lb}foo{~rb}" is {@size key="{foo}" /}{~n} +{:else} + The size of foo ({foo|js|s}) is {@size key=foo /}{~n} + The size of "{~lb}foo{~rb}" is {@size key="{foo}" /}{~n} +{/val} +The size with no key is {@size /}{~n} diff --git a/js/test_cases/idx_and_len/README.md b/js/test_cases/idx_and_len/README.md new file mode 100644 index 0000000..c4e8bdd --- /dev/null +++ b/js/test_cases/idx_and_len/README.md @@ -0,0 +1 @@ +$idx and $len seem to only be valid inside sections iterating over arrays, but nothing else. diff --git a/js/test_cases/idx_and_len/array.json b/js/test_cases/idx_and_len/array.json new file mode 100644 index 0000000..3f3e0ab --- /dev/null +++ b/js/test_cases/idx_and_len/array.json @@ -0,0 +1,7 @@ +{ + "things": [ + "Alice", + "Bob", + "Chris" + ] +} diff --git a/js/test_cases/idx_and_len/false.json b/js/test_cases/idx_and_len/false.json new file mode 100644 index 0000000..d51e741 --- /dev/null +++ b/js/test_cases/idx_and_len/false.json @@ -0,0 +1,3 @@ +{ + "things": false +} diff --git a/js/test_cases/idx_and_len/main.dust b/js/test_cases/idx_and_len/main.dust new file mode 100644 index 0000000..2e645b4 --- /dev/null +++ b/js/test_cases/idx_and_len/main.dust @@ -0,0 +1,40 @@ +Outside $idx: {$idx}{~n} +Outside $len: {$len}{~n} +Outside {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} +Outside {?$len}$len is true{:else}$len is false{/$len}{~n} + +{#things} + Inside Section $idx: {$idx}{~n} + Inside Section $len: {$len}{~n} + Inside Section {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} + Inside Section {?$len}$len is true{:else}$len is false{/$len}{~n} +{:else} + Else Section $idx: {$idx}{~n} + Else Section $len: {$len}{~n} + Else Section {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} + Else Section {?$len}$len is true{:else}$len is false{/$len}{~n} +{/things} + +{?things} + Inside Exists $idx: {$idx}{~n} + Inside Exists $len: {$len}{~n} + Inside Exists {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} + Inside Exists {?$len}$len is true{:else}$len is false{/$len}{~n} +{:else} + Else Exists $idx: {$idx}{~n} + Else Exists $len: {$len}{~n} + Else Exists {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} + Else Exists {?$len}$len is true{:else}$len is false{/$len}{~n} +{/things} + +{^things} + Inside Not Exists $idx: {$idx}{~n} + Inside Not Exists $len: {$len}{~n} + Inside Not Exists {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} + Inside Not Exists {?$len}$len is true{:else}$len is false{/$len}{~n} +{:else} + Else Not Exists $idx: {$idx}{~n} + Else Not Exists $len: {$len}{~n} + Else Not Exists {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} + Else Not Exists {?$len}$len is true{:else}$len is false{/$len}{~n} +{/things} diff --git a/js/test_cases/idx_and_len/number.json b/js/test_cases/idx_and_len/number.json new file mode 100644 index 0000000..585368a --- /dev/null +++ b/js/test_cases/idx_and_len/number.json @@ -0,0 +1,3 @@ +{ + "things": 7 +} diff --git a/js/test_cases/idx_and_len/single_element_array.json b/js/test_cases/idx_and_len/single_element_array.json new file mode 100644 index 0000000..97cb724 --- /dev/null +++ b/js/test_cases/idx_and_len/single_element_array.json @@ -0,0 +1,5 @@ +{ + "things": [ + "Alice" + ] +} diff --git a/js/test_cases/idx_and_len/string.json b/js/test_cases/idx_and_len/string.json new file mode 100644 index 0000000..90316e2 --- /dev/null +++ b/js/test_cases/idx_and_len/string.json @@ -0,0 +1,3 @@ +{ + "things": "foobar" +} diff --git a/js/test_cases/idx_and_len_nested/array.json b/js/test_cases/idx_and_len_nested/array.json new file mode 100644 index 0000000..322270c --- /dev/null +++ b/js/test_cases/idx_and_len_nested/array.json @@ -0,0 +1,16 @@ +{ + "things": [ + [ + "Alice", + "Andy" + ], + [ + "Bob", + "Becky" + ], + [ + "Chris", + "Cathy" + ] + ] +} diff --git a/js/test_cases/idx_and_len_nested/main.dust b/js/test_cases/idx_and_len_nested/main.dust new file mode 100644 index 0000000..7eaa5e3 --- /dev/null +++ b/js/test_cases/idx_and_len_nested/main.dust @@ -0,0 +1,19 @@ +Outside $idx: {$idx}{~n} +Outside $len: {$len}{~n} +Outside {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} +Outside {?$len}$len is true{:else}$len is false{/$len}{~n} + +{#things} + Inside Section $idx: {$idx}{~n} + Inside Section $len: {$len}{~n} + Inside Section .$idx: {.$idx}{~n} + Inside Section {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} + Inside Section {?$len}$len is true{:else}$len is false{/$len}{~n} + {#.} + Inside Nested Section $idx: {$idx}{~n} + Inside Nested Section $len: {$len}{~n} + Inside Nested Section .$idx: {.$idx}{~n} + Inside Nested Section {?$idx}$idx is true{:else}$idx is false{/$idx}{~n} + Inside Nested Section {?$len}$len is true{:else}$len is false{/$len}{~n} + {/.} +{/things} diff --git a/js/test_cases/internal_whitespace/README.md b/js/test_cases/internal_whitespace/README.md new file mode 100644 index 0000000..4d6143d --- /dev/null +++ b/js/test_cases/internal_whitespace/README.md @@ -0,0 +1,37 @@ +Through experimentation I have determined that DustJS: + +- Ignores newlines entirely (probably why there is a special character for newlines) +- Ignores spaces on empty lines +- Ignore tabs on empty lines +- Honors interior tabs +- Honors interior spaces +- Honors trailing spaces on opening, body, and closing tags +- Honors trailing spaces on tagless lines with content +- Honors trailing tabs on opening, body, and closing tags +- Honors trailing tabs on tagless lines with content +- Ignores leading spaces on opening, body, and closing tags +- Ignores leading spaces on tagless lines with content +- Ignores leading tabs on opening, body, and closing tags +- Ignores leading tabs on tagless lines with content + +Definitions +----------- + +Interior: Surounnded by tags on the same line: +``` +{foo}this is interior{/foo} +``` + +Trailing: On a line with a tag but no tag following it on that line: +``` +{stuff} +{name}this is trailing +{other stuff} +``` + +Leading: On a line with a tag but no tag preceding it on that line: +``` +{stuff} +this is leading{name} +{other stuff} +``` diff --git a/js/test_cases/internal_whitespace/input1.json b/js/test_cases/internal_whitespace/input1.json new file mode 100644 index 0000000..fee95a9 --- /dev/null +++ b/js/test_cases/internal_whitespace/input1.json @@ -0,0 +1 @@ +{"names": ["Alice", "Bob", "Chris"]} diff --git a/js/test_cases/internal_whitespace/main.dust b/js/test_cases/internal_whitespace/main.dust new file mode 100644 index 0000000..99e602c --- /dev/null +++ b/js/test_cases/internal_whitespace/main.dust @@ -0,0 +1,138 @@ +- simple -{~n} +{#names}{.}{/names} +{~n}- new lines -{~n} +{#names} +{.} +{/names} + +{~n}- interior tabs -{~n} +{#names} {.} {/names} + +{~n}- interior spaces -{~n} +{#names} {.} {/names} + +{~n}- empty line with spaces -{~n} +{#names} + +{/names} + +{~n}- empty line with tabs -{~n} +{#names} + +{/names} + +{~n}- empty line with spaces, trailing spaces on opening and closing -{~n} +{#names} + +{/names} + +{~n}- no line, trailing spaces on opening and closing -{~n} +{#names} +{/names} + +{~n}- empty line, trailing spaces on opening and closing -{~n} +{#names} + +{/names} + +{~n}- trailing spaces -{~n} +{#names} +{.} +{/names} + +{~n}- tagless line trailing spaces -{~n} +{#names} + +{/names} +{#names} +line with content but no tag +{/names} + +{~n}- empty line with tabs, trailing tabs on opening and closing -{~n} +{#names} + +{/names} + +{~n}- no line, trailing tabs on opening and closing -{~n} +{#names} +{/names} + +{~n}- empty line, trailing tabs on opening and closing -{~n} +{#names} + +{/names} + +{~n}- trailing tabs -{~n} +{#names} +{.} +{/names} + +{~n}- tagless line trailing tabs -{~n} +{#names} + +{/names} +{#names} +line with content but no tag +{/names} + + + + + + + + + +{~n}- empty line with spaces, leading spaces on opening and closing -{~n} + {#names} + + {/names} + +{~n}- no line, leading spaces on opening and closing -{~n} + {#names} + {/names} + +{~n}- empty line, leading spaces on opening and closing -{~n} + {#names} + + {/names} + +{~n}- leading spaces -{~n} + {#names} + {.} + {/names} + +{~n}- tagless line leading spaces -{~n} +{#names} + +{/names} +{#names} + line with content but no tag +{/names} + +{~n}- empty line with tabs, leading tabs on opening and closing -{~n} + {#names} + + {/names} + +{~n}- no line, leading tabs on opening and closing -{~n} + {#names} + {/names} + +{~n}- empty line, leading tabs on opening and closing -{~n} + {#names} + + {/names} + +{~n}- leading tabs -{~n} + {#names} + {.} + {/names} + +{~n}- tagless line leading tabs -{~n} +{#names} + +{/names} +{#names} + line with content but no tag +{/names} diff --git a/js/test_cases/literal_string_block/input1.json b/js/test_cases/literal_string_block/input1.json new file mode 100644 index 0000000..ad7288d --- /dev/null +++ b/js/test_cases/literal_string_block/input1.json @@ -0,0 +1 @@ +{"name": "Bob"} diff --git a/js/test_cases/literal_string_block/main.dust b/js/test_cases/literal_string_block/main.dust new file mode 100644 index 0000000..781ca76 --- /dev/null +++ b/js/test_cases/literal_string_block/main.dust @@ -0,0 +1,5 @@ +{` +This block is supposed + to preserve {name} all +newlines, whitespace, and braces +`} diff --git a/js/test_cases/missing_reference/input1.json b/js/test_cases/missing_reference/input1.json new file mode 100644 index 0000000..8bffc80 --- /dev/null +++ b/js/test_cases/missing_reference/input1.json @@ -0,0 +1 @@ +{"rank": "Admiral"} diff --git a/js/test_cases/missing_reference/main.dust b/js/test_cases/missing_reference/main.dust new file mode 100644 index 0000000..09f2e19 --- /dev/null +++ b/js/test_cases/missing_reference/main.dust @@ -0,0 +1 @@ +Hello {name}! diff --git a/js/test_cases/not_exists/input1.json b/js/test_cases/not_exists/input1.json new file mode 100644 index 0000000..69197e0 --- /dev/null +++ b/js/test_cases/not_exists/input1.json @@ -0,0 +1 @@ +{"things": ["Alice", "Bob", "Chris"]} diff --git a/js/test_cases/not_exists/input10.json b/js/test_cases/not_exists/input10.json new file mode 100644 index 0000000..08f519d --- /dev/null +++ b/js/test_cases/not_exists/input10.json @@ -0,0 +1 @@ +{"things": {}} diff --git a/js/test_cases/not_exists/input11.json b/js/test_cases/not_exists/input11.json new file mode 100644 index 0000000..b9ee8db --- /dev/null +++ b/js/test_cases/not_exists/input11.json @@ -0,0 +1 @@ +["cat", "dog"] diff --git a/js/test_cases/not_exists/input2.json b/js/test_cases/not_exists/input2.json new file mode 100644 index 0000000..4743329 --- /dev/null +++ b/js/test_cases/not_exists/input2.json @@ -0,0 +1 @@ +{"things": {"name": "Alice", "keyboard": "K-Type"}} diff --git a/js/test_cases/not_exists/input3.json b/js/test_cases/not_exists/input3.json new file mode 100644 index 0000000..8040d63 --- /dev/null +++ b/js/test_cases/not_exists/input3.json @@ -0,0 +1 @@ +{"there_are_no_things": 4} diff --git a/js/test_cases/not_exists/input4.json b/js/test_cases/not_exists/input4.json new file mode 100644 index 0000000..03d1e8a --- /dev/null +++ b/js/test_cases/not_exists/input4.json @@ -0,0 +1 @@ +{"things": "just a string"} diff --git a/js/test_cases/not_exists/input5.json b/js/test_cases/not_exists/input5.json new file mode 100644 index 0000000..4ef7571 --- /dev/null +++ b/js/test_cases/not_exists/input5.json @@ -0,0 +1 @@ +{"things": false} diff --git a/js/test_cases/not_exists/input6.json b/js/test_cases/not_exists/input6.json new file mode 100644 index 0000000..feb65c6 --- /dev/null +++ b/js/test_cases/not_exists/input6.json @@ -0,0 +1 @@ +{"things": null} diff --git a/js/test_cases/not_exists/input7.json b/js/test_cases/not_exists/input7.json new file mode 100644 index 0000000..d9fc4f8 --- /dev/null +++ b/js/test_cases/not_exists/input7.json @@ -0,0 +1 @@ +{"things": 0} diff --git a/js/test_cases/not_exists/input8.json b/js/test_cases/not_exists/input8.json new file mode 100644 index 0000000..1486a8a --- /dev/null +++ b/js/test_cases/not_exists/input8.json @@ -0,0 +1 @@ +{"things": ""} diff --git a/js/test_cases/not_exists/input9.json b/js/test_cases/not_exists/input9.json new file mode 100644 index 0000000..93babc6 --- /dev/null +++ b/js/test_cases/not_exists/input9.json @@ -0,0 +1 @@ +{"things": []} diff --git a/js/test_cases/not_exists/main.dust b/js/test_cases/not_exists/main.dust new file mode 100644 index 0000000..bea33c4 --- /dev/null +++ b/js/test_cases/not_exists/main.dust @@ -0,0 +1,5 @@ +{^things} +Thing: {things} +{:else} +No things {.} +{/things} diff --git a/js/test_cases/partial_jump_around_one/input1.json b/js/test_cases/partial_jump_around_one/input1.json new file mode 100644 index 0000000..4170048 --- /dev/null +++ b/js/test_cases/partial_jump_around_one/input1.json @@ -0,0 +1,28 @@ +{ + "v0": "0", + "level3": { + "v3": "3", + "v4": "3", + "v5": "3", + "level4": { + "v4": "4", + "v5": "4", + "level5": { + "v5": "5" + } + } + }, + "level1": { + "v1": "1", + "v2": "1", + "v3": "1", + "v4": "1", + "v5": "1", + "level2": { + "v2": "2", + "v3": "2", + "v4": "2", + "v5": "2" + } + } +} diff --git a/js/test_cases/partial_jump_around_one/main.dust b/js/test_cases/partial_jump_around_one/main.dust new file mode 100644 index 0000000..004474e --- /dev/null +++ b/js/test_cases/partial_jump_around_one/main.dust @@ -0,0 +1,3 @@ +{#level1.level2} +{>partialone v0="a" v1="a" v2="a" v3="a" v4="a" v5="a"/} +{/level1.level2} diff --git a/js/test_cases/partial_jump_around_one/partialone.dust b/js/test_cases/partial_jump_around_one/partialone.dust new file mode 100644 index 0000000..a53b2eb --- /dev/null +++ b/js/test_cases/partial_jump_around_one/partialone.dust @@ -0,0 +1,3 @@ +{#level3.level4} + {>partialtwo/} +{/level3.level4} diff --git a/js/test_cases/partial_jump_around_one/partialtwo.dust b/js/test_cases/partial_jump_around_one/partialtwo.dust new file mode 100644 index 0000000..bb751b3 --- /dev/null +++ b/js/test_cases/partial_jump_around_one/partialtwo.dust @@ -0,0 +1,8 @@ +{#level5} + {v0}{~n} + {v1}{~n} + {v2}{~n} + {v3}{~n} + {v4}{~n} + {v5}{~n} +{/level5} diff --git a/js/test_cases/partial_jump_around_two/input1.json b/js/test_cases/partial_jump_around_two/input1.json new file mode 100644 index 0000000..5b9dd75 --- /dev/null +++ b/js/test_cases/partial_jump_around_two/input1.json @@ -0,0 +1,27 @@ +{ + "level3": { + "v3": "3", + "v4": "3", + "v5": "3", + "level4": { + "v4": "4", + "v5": "4", + "level5": { + "v5": "5" + } + } + }, + "level1": { + "v1": "1", + "v2": "1", + "v3": "1", + "v4": "1", + "v5": "1", + "level2": { + "v2": "2", + "v3": "2", + "v4": "2", + "v5": "2" + } + } +} diff --git a/js/test_cases/partial_jump_around_two/main.dust b/js/test_cases/partial_jump_around_two/main.dust new file mode 100644 index 0000000..e6ad4e4 --- /dev/null +++ b/js/test_cases/partial_jump_around_two/main.dust @@ -0,0 +1,3 @@ +{#level1.level2} +{>partialone v1="a" v2="a" v3="a" v4="a" v5="a"/} +{/level1.level2} diff --git a/js/test_cases/partial_jump_around_two/partialone.dust b/js/test_cases/partial_jump_around_two/partialone.dust new file mode 100644 index 0000000..0642002 --- /dev/null +++ b/js/test_cases/partial_jump_around_two/partialone.dust @@ -0,0 +1,3 @@ +{#level3.level4} + {>partialtwo v1="b" v2="b" v3="b" v4="b" v5="b" /} +{/level3.level4} diff --git a/js/test_cases/partial_jump_around_two/partialtwo.dust b/js/test_cases/partial_jump_around_two/partialtwo.dust new file mode 100644 index 0000000..d8f7866 --- /dev/null +++ b/js/test_cases/partial_jump_around_two/partialtwo.dust @@ -0,0 +1,7 @@ +{#level5} + {v1}{~n} + {v2}{~n} + {v3}{~n} + {v4}{~n} + {v5}{~n} +{/level5} diff --git a/js/test_cases/partial_path/greeting.dust b/js/test_cases/partial_path/greeting.dust new file mode 100644 index 0000000..f5a4adb --- /dev/null +++ b/js/test_cases/partial_path/greeting.dust @@ -0,0 +1 @@ +Hello {name}{?item}, nice {item}{/item}!{~n} diff --git a/js/test_cases/partial_path/input1.json b/js/test_cases/partial_path/input1.json new file mode 100644 index 0000000..232ddbe --- /dev/null +++ b/js/test_cases/partial_path/input1.json @@ -0,0 +1,14 @@ +{ + "people": [ + { + "name": "Alice", + "item": "cat" + }, + { + "name": "Bob" + } + ], + "globals": { + "item": "couch" + } +} diff --git a/js/test_cases/partial_path/main.dust b/js/test_cases/partial_path/main.dust new file mode 100644 index 0000000..87518e2 --- /dev/null +++ b/js/test_cases/partial_path/main.dust @@ -0,0 +1,3 @@ +{#people} +{>greeting item=globals.item/} +{/people} diff --git a/js/test_cases/partial_simple/README.md b/js/test_cases/partial_simple/README.md new file mode 100644 index 0000000..d8908e5 --- /dev/null +++ b/js/test_cases/partial_simple/README.md @@ -0,0 +1 @@ +Partial parameters do not take highest priority but also do not seem to take lowest priority. Current theory is parameters are inserted 1 level above the current context (so parameters would be references before walking up). diff --git a/js/test_cases/partial_simple/greeting.dust b/js/test_cases/partial_simple/greeting.dust new file mode 100644 index 0000000..f5a4adb --- /dev/null +++ b/js/test_cases/partial_simple/greeting.dust @@ -0,0 +1 @@ +Hello {name}{?item}, nice {item}{/item}!{~n} diff --git a/js/test_cases/partial_simple/input1.json b/js/test_cases/partial_simple/input1.json new file mode 100644 index 0000000..272dfa5 --- /dev/null +++ b/js/test_cases/partial_simple/input1.json @@ -0,0 +1,4 @@ +{"people": [ + {"name": "Alice", "item": "cat"}, + {"name": "Bob"} +]} diff --git a/js/test_cases/partial_simple/input2.json b/js/test_cases/partial_simple/input2.json new file mode 100644 index 0000000..02913d0 --- /dev/null +++ b/js/test_cases/partial_simple/input2.json @@ -0,0 +1,5 @@ +{"people": [ + {"name": "Alice"}, + {"name": "Bob"} +], + "item": "cat"} diff --git a/js/test_cases/partial_simple/main.dust b/js/test_cases/partial_simple/main.dust new file mode 100644 index 0000000..5961053 --- /dev/null +++ b/js/test_cases/partial_simple/main.dust @@ -0,0 +1,3 @@ +{#people} +{>greeting item="shoes"/} +{/people} diff --git a/js/test_cases/reference_parameters/README.md b/js/test_cases/reference_parameters/README.md new file mode 100644 index 0000000..7fe6fd4 --- /dev/null +++ b/js/test_cases/reference_parameters/README.md @@ -0,0 +1 @@ +Reference parameters are evaluated at the time of render, as opposed to direct parameters which are evaluated at the time of assignment. diff --git a/js/test_cases/reference_parameters/input1.json b/js/test_cases/reference_parameters/input1.json new file mode 100644 index 0000000..7a597e1 --- /dev/null +++ b/js/test_cases/reference_parameters/input1.json @@ -0,0 +1,32 @@ +{ + "name": "Bob", + "people": [ + { + "name": "Alice", + "petname": "rover" + } + ], + "truthy": "some truthy value", + "other_petname": [ + { + "petname": "spot" + } + ], + "array_petname": [ + { + "petname": [ + "foo", + "bar" + ] + } + ], + "some_object": { + "foo": "bar" + }, + "some_same_object": { + "foo": "bar" + }, + "some_different_object": { + "foo": "baz" + } +} diff --git a/js/test_cases/reference_parameters/main.dust b/js/test_cases/reference_parameters/main.dust new file mode 100644 index 0000000..4308d5a --- /dev/null +++ b/js/test_cases/reference_parameters/main.dust @@ -0,0 +1,64 @@ +Hello {name}, nice {pet}{~n} +{#people} + Hello {name}, nice {pet}{~n} +{/people} +{#people name="chris" pet="cat"} + Hello {name}, nice {pet}{~n} +{/people} + +Direct Parameters{~n} +================={~n} +{#people name="chris" pet=petname petname="whiskers"} + Hello {name}, nice {pet}{~n} +{/people} +{#people} + {#truthy name="chris" pet=petname petname="whiskers"} + Hello {name}, nice {pet}{~n} + {/truthy} +{/people} +{#people name="chris" pet=petname petname="whiskers"} + {#other_petname} + Hello {name}, nice {pet}{~n} + {/other_petname} +{/people} + +Reference Parameters{~n} +===================={~n} +{#people name="chris" pet="{petname}" petname="whiskers"} + Hello {name}, nice {pet}{~n} +{/people} +{#people} + {#truthy name="chris" pet="{petname}" petname="whiskers"} + Hello {name}, nice {pet}{~n} + {/truthy} +{/people} +{#people name="chris" pet="{petname}" petname="whiskers"} + {#other_petname} + Hello {name}, nice {pet}{~n} + {/other_petname} +{/people} +{! Can you have additional text in reference parameters, or just the reference !} +{#people name="chris" pet="{petname}!" petname="whiskers"} + {#other_petname} + Hello {name}, nice {pet}{~n} + {/other_petname} +{/people} +{! Can you have filters !} +{#people name="chris" pet="{petname|js}" petname="whiskers"} + {#other_petname} + Hello {name}, nice {pet}{~n} + {/other_petname} +{/people} +{! Can you go through multiple levels of references !} +{#truthy name="chris" pet="{petname}" petname="{deeperpetname}" deeperpetname="fluffy"} + Hello {name}, nice {pet}{~n} +{/truthy} + +Equality{~n} +========{~n} +{@eq key=some_object value=some_object}some_object equals some_object{:else}some_object does not equal some_object{/eq}{~n} +{@eq key=some_object value=some_same_object}some_object equals some_same_object{:else}some_object does not equal some_same_object{/eq}{~n} +{@eq key=some_object value="{some_object}"}some_object equals reference(some_object){:else}some_object does not equal reference(some_object){/eq}{~n} +{@eq key="{some_object}" value="{some_object}"}reference(some_object) equals reference(some_object){:else}reference(some_object) does not equal reference(some_object){/eq}{~n} +{@eq key="{some_object}" value="{some_same_object}"}reference(some_object) equals reference(some_same_object){:else}reference(some_object) does not equal reference(some_same_object){/eq}{~n} +{@eq key="{some_object}" value="{some_different_object}"}reference(some_object) equals reference(some_different_object){:else}reference(some_object) does not equal reference(some_different_object){/eq}{~n} diff --git a/js/test_cases/remove_comments/input1.json b/js/test_cases/remove_comments/input1.json new file mode 100644 index 0000000..ad7288d --- /dev/null +++ b/js/test_cases/remove_comments/input1.json @@ -0,0 +1 @@ +{"name": "Bob"} diff --git a/js/test_cases/remove_comments/main.dust b/js/test_cases/remove_comments/main.dust new file mode 100644 index 0000000..4a7b28e --- /dev/null +++ b/js/test_cases/remove_comments/main.dust @@ -0,0 +1 @@ +Hel{! This is a {!dust} comment !}lo {name}! diff --git a/js/test_cases/render_unusual_types/input1.json b/js/test_cases/render_unusual_types/input1.json new file mode 100644 index 0000000..0a08cba --- /dev/null +++ b/js/test_cases/render_unusual_types/input1.json @@ -0,0 +1 @@ +{"name": ["Bob", "Smith"]} diff --git a/js/test_cases/render_unusual_types/input2.json b/js/test_cases/render_unusual_types/input2.json new file mode 100644 index 0000000..b81f636 --- /dev/null +++ b/js/test_cases/render_unusual_types/input2.json @@ -0,0 +1 @@ +{"name": {"first": "Alice", "last": "Jones"}} diff --git a/js/test_cases/render_unusual_types/input3.json b/js/test_cases/render_unusual_types/input3.json new file mode 100644 index 0000000..382a810 --- /dev/null +++ b/js/test_cases/render_unusual_types/input3.json @@ -0,0 +1 @@ +{"name": ["Bob", {"title": "sir"}, [1,2,3]]} diff --git a/js/test_cases/render_unusual_types/input4.json b/js/test_cases/render_unusual_types/input4.json new file mode 100644 index 0000000..9b1fcb1 --- /dev/null +++ b/js/test_cases/render_unusual_types/input4.json @@ -0,0 +1 @@ +{"name": [true, false, null]} diff --git a/js/test_cases/render_unusual_types/input5.json b/js/test_cases/render_unusual_types/input5.json new file mode 100644 index 0000000..c1f2309 --- /dev/null +++ b/js/test_cases/render_unusual_types/input5.json @@ -0,0 +1 @@ +[true, false, null] diff --git a/js/test_cases/render_unusual_types/input6.json b/js/test_cases/render_unusual_types/input6.json new file mode 100644 index 0000000..329afc2 --- /dev/null +++ b/js/test_cases/render_unusual_types/input6.json @@ -0,0 +1 @@ +{"name": false} diff --git a/js/test_cases/render_unusual_types/input7.json b/js/test_cases/render_unusual_types/input7.json new file mode 100644 index 0000000..0ab5773 --- /dev/null +++ b/js/test_cases/render_unusual_types/input7.json @@ -0,0 +1 @@ +{"name": true} diff --git a/js/test_cases/render_unusual_types/input8.json b/js/test_cases/render_unusual_types/input8.json new file mode 100644 index 0000000..c0b4ef3 --- /dev/null +++ b/js/test_cases/render_unusual_types/input8.json @@ -0,0 +1 @@ +{"name": [[], {}, true, false, ""]} diff --git a/js/test_cases/render_unusual_types/main.dust b/js/test_cases/render_unusual_types/main.dust new file mode 100644 index 0000000..09f2e19 --- /dev/null +++ b/js/test_cases/render_unusual_types/main.dust @@ -0,0 +1 @@ +Hello {name}! diff --git a/js/test_cases/sections/input1.json b/js/test_cases/sections/input1.json new file mode 100644 index 0000000..69197e0 --- /dev/null +++ b/js/test_cases/sections/input1.json @@ -0,0 +1 @@ +{"things": ["Alice", "Bob", "Chris"]} diff --git a/js/test_cases/sections/input10.json b/js/test_cases/sections/input10.json new file mode 100644 index 0000000..08f519d --- /dev/null +++ b/js/test_cases/sections/input10.json @@ -0,0 +1 @@ +{"things": {}} diff --git a/js/test_cases/sections/input11.json b/js/test_cases/sections/input11.json new file mode 100644 index 0000000..b9ee8db --- /dev/null +++ b/js/test_cases/sections/input11.json @@ -0,0 +1 @@ +["cat", "dog"] diff --git a/js/test_cases/sections/input2.json b/js/test_cases/sections/input2.json new file mode 100644 index 0000000..4743329 --- /dev/null +++ b/js/test_cases/sections/input2.json @@ -0,0 +1 @@ +{"things": {"name": "Alice", "keyboard": "K-Type"}} diff --git a/js/test_cases/sections/input3.json b/js/test_cases/sections/input3.json new file mode 100644 index 0000000..8040d63 --- /dev/null +++ b/js/test_cases/sections/input3.json @@ -0,0 +1 @@ +{"there_are_no_things": 4} diff --git a/js/test_cases/sections/input4.json b/js/test_cases/sections/input4.json new file mode 100644 index 0000000..03d1e8a --- /dev/null +++ b/js/test_cases/sections/input4.json @@ -0,0 +1 @@ +{"things": "just a string"} diff --git a/js/test_cases/sections/input5.json b/js/test_cases/sections/input5.json new file mode 100644 index 0000000..4ef7571 --- /dev/null +++ b/js/test_cases/sections/input5.json @@ -0,0 +1 @@ +{"things": false} diff --git a/js/test_cases/sections/input6.json b/js/test_cases/sections/input6.json new file mode 100644 index 0000000..feb65c6 --- /dev/null +++ b/js/test_cases/sections/input6.json @@ -0,0 +1 @@ +{"things": null} diff --git a/js/test_cases/sections/input7.json b/js/test_cases/sections/input7.json new file mode 100644 index 0000000..d9fc4f8 --- /dev/null +++ b/js/test_cases/sections/input7.json @@ -0,0 +1 @@ +{"things": 0} diff --git a/js/test_cases/sections/input8.json b/js/test_cases/sections/input8.json new file mode 100644 index 0000000..1486a8a --- /dev/null +++ b/js/test_cases/sections/input8.json @@ -0,0 +1 @@ +{"things": ""} diff --git a/js/test_cases/sections/input9.json b/js/test_cases/sections/input9.json new file mode 100644 index 0000000..93babc6 --- /dev/null +++ b/js/test_cases/sections/input9.json @@ -0,0 +1 @@ +{"things": []} diff --git a/js/test_cases/sections/main.dust b/js/test_cases/sections/main.dust new file mode 100644 index 0000000..8f5d538 --- /dev/null +++ b/js/test_cases/sections/main.dust @@ -0,0 +1,5 @@ +{#things} +Thing: {.} +{:else} +No things {.} +{/things} diff --git a/js/test_cases/surrounding_whitespace/README.md b/js/test_cases/surrounding_whitespace/README.md new file mode 100644 index 0000000..f112c8c --- /dev/null +++ b/js/test_cases/surrounding_whitespace/README.md @@ -0,0 +1 @@ +Through experimentation I have determined that DustJS erases all preceding whitespace (spaces, tabs, and newlines) but only trims trailing newlines. diff --git a/js/test_cases/surrounding_whitespace/input1.json b/js/test_cases/surrounding_whitespace/input1.json new file mode 100644 index 0000000..ad7288d --- /dev/null +++ b/js/test_cases/surrounding_whitespace/input1.json @@ -0,0 +1 @@ +{"name": "Bob"} diff --git a/js/test_cases/surrounding_whitespace/main.dust b/js/test_cases/surrounding_whitespace/main.dust new file mode 100644 index 0000000..65a50e2 --- /dev/null +++ b/js/test_cases/surrounding_whitespace/main.dust @@ -0,0 +1,9 @@ + + + + + Hello {name}! + + + + diff --git a/js/test_cases/walk_up/README.md b/js/test_cases/walk_up/README.md new file mode 100644 index 0000000..3f9a4b5 --- /dev/null +++ b/js/test_cases/walk_up/README.md @@ -0,0 +1,90 @@ +Through experimentation it seems that you can walk up to access higher levels in the context. Interestingly enough, it seems that walking up to a higher context does not unwind the context stack but instead seems to add the higher level context element to the bottom. For example: + +```js +{ + "foo": { + "f1": "f", + "f2": "ff" + }, + "bar": { + "b1": "b", + "b2": "bb" + } +} +``` + +if we walk down into bar and then into foo then our variable look ups appear to follow this pattern: +``` +(attempts to read from the context in-order starting with the first line) + +Starting access context: +{"foo":{"f1":"f","f2":"ff"},"bar":{"b1":"b","b2":"bb"}} + +After walk "bar": +{"b1":"b","b2":"bb"} +{"foo":{"f1":"f","f2":"ff"},"bar":{"b1":"b","b2":"bb"}} + +After walk "foo": +{"f1":"f","f2":"ff"} +{"b1":"b","b2":"bb"} +{"foo":{"f1":"f","f2":"ff"},"bar":{"b1":"b","b2":"bb"}} +``` + +Scoping +------- + +This appears to be using dynamic scoping instead of lexical scoping. For example, in lexical scoping a read of "b1" would fail after that final walk because you're inside the "foo" context which does not have any "b1" in or above it, however, since this is using dynamic scoping its using the invocations to build a scope tree rather than their original position. + +Itermediate scopes appear to not be added. For example: +```js +{ + "globals": { + "item": "pencil", + "things": {"color": "purple"} + }, + "people": [ + {"name": "Dave"}, + {"name": "Emily", "item": "pen"} + ] +} +``` + +If we walk into people and then into globals.things in one step, globals will not be added to the dynamic scope: +``` +(attempts to read from the context in-order starting with the first line) + +Starting access context: +{"globals":{"item":"pencil","things":{"color":"purple"}},"people":[{"name":"Dave"},{"name":"Emily","item":"pen"}]} + +After walk "people": +[{"name":"Dave"},{"name":"Emily","item":"pen"}] +{"globals":{"item":"pencil","things":{"color":"purple"}},"people":[{"name":"Dave"},{"name":"Emily","item":"pen"}]} + +After walk globals.things +{"color":"purple"} +[{"name":"Dave"},{"name":"Emily","item":"pen"}] +{"globals":{"item":"pencil","things":{"color":"purple"}},"people":[{"name":"Dave"},{"name":"Emily","item":"pen"}]} +``` + +So if we were on the "Dave" iteration in people and I attempted to read "item" it would not find a value despite "item" being a key in the lexical context above `globals.things`. + +Backtracking +------------ + +Item resolution appears to be greedy. For example if we have: +```js +{ + "clothes": { + "shirt": "t-shirt", + "pants": "jeans" + }, + "alice": { + "clothes": { + "shirt": "tank top" + } + }, + "bob": {}, +} +``` + +If we walked into `alice` and then attempted to read `clothes.pants` it will return nothing because `alice` has a `clothes` block but no `pants` element inside that. However, if we walked into `bob` and attempted to read `clothes.pants` it would return `jeans` because `bob` does not have a `clothes` block so it would walk up to the global `clothes` block. diff --git a/js/test_cases/walk_up/input1.json b/js/test_cases/walk_up/input1.json new file mode 100644 index 0000000..889e443 --- /dev/null +++ b/js/test_cases/walk_up/input1.json @@ -0,0 +1,19 @@ +{ + "company": "The Pendulum", + "globals": { + "email": "email hidden" + }, + "people": [ + {"name": "Alice", "job": "Chief Swinger"}, + {"name": "Bob", "job": "Chief Swayer"}, + {"name": "Chris", "job": "Barista", "company": "GenericCoffee", "email": "thecoffeeguy@generic.coffee"} + ], + "deep_globals": { + "item": "pencil", + "things": {"color": "purple", "deeper_item": {"style": "number 2"}} + }, + "deep_people": [ + {"name": "Dave"}, + {"name": "Emily", "item": "pen", "deeper_item": {"style": "ballpoint", "material": "plastic"}} + ] +} diff --git a/js/test_cases/walk_up/main.dust b/js/test_cases/walk_up/main.dust new file mode 100644 index 0000000..efdcfad --- /dev/null +++ b/js/test_cases/walk_up/main.dust @@ -0,0 +1,13 @@ +Directory for {company}:{~n} +{#people} +{name}: {job} at {company} (email: {globals.email}){~n} +Testing walking after entering a parent context {#globals}job: {job}, email: {email}, company: {company}{/globals}{~n} +{/people} + +Doing a deep walk to see if intermediate steps are added to the dynamic context.{~n} +{#deep_people} +{#deep_globals.things} +{name} has a {color} {item} which is {deeper_item.style} and made out of {deeper_item.material} +{/deep_globals.things} +but everyone shares one that is {deeper_item.style} and made out of {deeper_item.material}{~n} +{/deep_people} diff --git a/src/bin.rs b/src/bin.rs index c03b9ed..a614f88 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -1,9 +1,769 @@ extern crate nom; -use parser::template; + +use crate::renderer::CompareContextElement; +use parser::Filter; +use parser::OwnedLiteral; +use parser::Template; +use renderer::compare_json_numbers; +use renderer::compile_template; +use renderer::Castable; +use renderer::ComparisonNumber; +use renderer::CompileError; +use renderer::ContextElement; +use renderer::DustRenderer; +use renderer::IceResult; +use renderer::IntoContextElement; +use renderer::Loopable; +use renderer::MathNumber; +use renderer::RenderError; +use renderer::Renderable; +use renderer::Sizable; +use renderer::Truthiness; +use renderer::WalkError; +use renderer::Walkable; +use std::cmp::Ordering; +use std::convert::TryInto; +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::Path; mod parser; +mod renderer; fn main() { - let parsed_template = template("{#foo.bar}hello {name}{/foo.bar}!"); - println!("{:?}", parsed_template); + let context = read_context_from_stdin(); + + let argv: Vec = env::args().collect(); + if argv.len() < 2 { + panic!("Need to pass templates"); + } + let template_paths = &argv[1..]; + let template_contents: Vec<(String, String)> = template_paths + .iter() + .map(|p| { + let template_content = fs::read_to_string(&p).unwrap(); + (p.to_string(), template_content) + }) + .collect(); + let compiled_templates_result: Result, CompileError> = + template_contents + .iter() + .map(|(p, contents)| template_from_file(p, contents)) + .collect(); + let compiled_templates = compiled_templates_result.unwrap(); + let main_template_name = &compiled_templates + .first() + .expect("There should be more than 1 template") + .0; + let mut dust_renderer = DustRenderer::new(); + compiled_templates.iter().for_each(|(name, template)| { + dust_renderer.load_source(template, name.to_owned()); + }); + println!( + "{}", + dust_renderer + .render(main_template_name, Some(&context)) + .expect("Failed to render") + ); +} + +fn template_from_file<'a>( + file_path: &str, + file_contents: &'a str, +) -> Result<(String, Template<'a>), CompileError> { + let path: &Path = Path::new(file_path); + let name = path.file_stem().ok_or(CompileError { + message: format!("Failed to get file stem on {}", file_path), + })?; + Ok(( + name.to_string_lossy().to_string(), + compile_template(file_contents)?, + )) +} + +fn read_context_from_stdin() -> serde_json::Value { + let mut buffer = String::new(); + io::stdin() + .read_to_string(&mut buffer) + .expect("Failed to read stdin"); + + serde_json::from_str(&buffer).expect("Failed to parse json") +} + +fn html_escape(inp: &str) -> String { + // Adding 10% space from the original to avoid re-allocations by + // leaving room for escaped sequences. + let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); + inp.chars().for_each(|c| match c { + '<' => output.push_str("<"), + '>' => output.push_str(">"), + '"' => output.push_str("""), + '\'' => output.push_str("'"), + '&' => output.push_str("&"), + _ => output.push(c), + }); + output +} + +fn javascript_escape(inp: &str) -> String { + // Adding 10% space from the original to avoid re-allocations by + // leaving room for escaped sequences. + let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); + inp.chars().for_each(|c| match c { + '"' => output.push_str(r#"\""#), + '\'' => output.push_str(r#"\'"#), + '\t' => output.push_str(r#"\t"#), + '\x0C' => output.push_str(r#"\f"#), + '\n' => output.push_str(r#"\n"#), + '\r' => output.push_str(r#"\r"#), + '\\' => output.push_str(r#"\\"#), + '/' => output.push_str(r#"\/"#), + _ => output.push(c), + }); + output +} + +fn get_utf8_hex(inp: char) -> String { + let num_bytes = inp.len_utf8(); + let mut byte_buffer = [0; 4]; // UTF-8 supports up to 4 bytes per codepoint + let mut output = String::with_capacity(num_bytes * 2); + + inp.encode_utf8(&mut byte_buffer); + + for b in &byte_buffer[..num_bytes] { + output.push_str(&format!("{:02X}", b)); + } + + output +} + +fn encode_uri(inp: &str) -> String { + // Adding 10% space from the original to avoid re-allocations by + // leaving room for escaped sequences. + let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); + inp.chars().for_each(|c| match c { + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' + | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' + | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' + | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' + | 'V' | 'W' | 'X' | 'Y' | 'Z' | ';' | ',' | '/' | '?' | ':' | '@' | '&' | '=' | '+' + | '$' | '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')' | '#' => output.push(c), + _ => { + output.push('%'); + output.push_str(&get_utf8_hex(c)); + } + }); + output +} + +fn encode_uri_component(inp: &str) -> String { + // Adding 10% space from the original to avoid re-allocations by + // leaving room for escaped sequences. + let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); + inp.chars().for_each(|c| match c { + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' + | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' + | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' + | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' + | 'V' | 'W' | 'X' | 'Y' | 'Z' | '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')' => { + output.push(c) + } + _ => { + output.push('%'); + output.push_str(&get_utf8_hex(c)); + } + }); + output +} + +fn apply_filter( + json_value: &serde_json::Value, + filter: &Filter, +) -> Result { + match (json_value, filter) { + // Html escape filter + (serde_json::Value::String(string), Filter::HtmlEncode) => { + Ok(serde_json::Value::String(html_escape(string))) + } + (_, Filter::HtmlEncode) => Ok(serde_json::Value::String(html_escape( + &json_value.render(&Vec::new())?, + ))), + // Disable html escape filter + (_, Filter::DisableHtmlEncode) => panic!("The |s filter is automatically removed by the renderer since it is a no-op during rendering."), + // Parse JSON filter + (serde_json::Value::String(string), Filter::JsonParse) => { + serde_json::from_str(&string).or(Err(RenderError::InvalidJson(string.to_owned()))) + } + (_, Filter::JsonParse) => { + let rendered_value = json_value.render(&Vec::new())?; + serde_json::from_str(&rendered_value).or(Err(RenderError::InvalidJson(rendered_value))) + } + // Json Stringify filter + (_, Filter::JsonStringify) => { + Ok(serde_json::Value::String(json_value.to_string())) + } + // Javascript escape filter + (serde_json::Value::String(string), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::String(javascript_escape(string))) + } + (serde_json::Value::Bool(boolean), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Bool(*boolean)) + } + (serde_json::Value::Number(number), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Number(number.clone())) + } + (serde_json::Value::Array(arr), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Array(arr.clone())) + } + (serde_json::Value::Object(obj), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Object(obj.clone())) + } + (_, Filter::JavascriptStringEncode) => Ok(serde_json::Value::String(javascript_escape( + &json_value.render(&Vec::new())?, + ))), + // EncodeURI filter + (serde_json::Value::String(string), Filter::EncodeUri) => { + Ok(serde_json::Value::String(encode_uri(string))) + } + (_, Filter::EncodeUri) => Ok(serde_json::Value::String(encode_uri( + &json_value.render(&Vec::new())?, + ))), + // EncodeURIComponent filter + (serde_json::Value::String(string), Filter::EncodeUriComponent) => { + Ok(serde_json::Value::String(encode_uri_component(string))) + } + (_, Filter::EncodeUriComponent) => Ok(serde_json::Value::String(encode_uri_component( + &json_value.render(&Vec::new())?, + ))), + } +} + +fn apply_filters( + json_value: &serde_json::Value, + filters: &[Filter], +) -> Result { + let mut final_value: serde_json::Value = apply_filter(json_value, &filters[0])?; + + for filter in &filters[1..] { + final_value = apply_filter(&final_value, filter)?; + } + + Ok(final_value) +} + +impl ContextElement for serde_json::Value {} + +impl Truthiness for serde_json::Value { + fn is_truthy(&self) -> bool { + match self { + serde_json::Value::Null => false, + serde_json::Value::Bool(boolean) => *boolean, + serde_json::Value::Number(_num) => true, + serde_json::Value::String(string_value) => !string_value.is_empty(), + serde_json::Value::Array(array_value) => !array_value.is_empty(), + serde_json::Value::Object(_obj) => true, + } + } +} + +impl Renderable for serde_json::Value { + fn render(&self, _filters: &Vec) -> Result { + let after_apply = if _filters.is_empty() { + None + } else { + Some(apply_filters(self, _filters)?) + }; + + match after_apply.as_ref().unwrap_or(self) { + serde_json::Value::Null => Ok("".to_owned()), + serde_json::Value::Bool(boolean) => Ok(boolean.to_string()), + serde_json::Value::Number(num) => Ok(num.to_string()), + serde_json::Value::String(string) => Ok(string.to_string()), + serde_json::Value::Array(arr) => { + let rendered: Result, RenderError> = + arr.iter().map(|val| val.render(&Vec::new())).collect(); + let rendered_slice: &[String] = &rendered?; + Ok(rendered_slice.join(",")) + } + serde_json::Value::Object(_obj) => Ok("[object Object]".to_owned()), + } + } +} + +impl Walkable for serde_json::Value { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + match self { + serde_json::Value::Null => Err(WalkError::CantWalk), + serde_json::Value::Bool(_boolean) => Err(WalkError::CantWalk), + serde_json::Value::Number(_num) => Err(WalkError::CantWalk), + serde_json::Value::String(_string) => Err(WalkError::CantWalk), + serde_json::Value::Array(_arr) => Err(WalkError::CantWalk), + serde_json::Value::Object(obj) => obj + .get(segment) + .map(|val| val as _) + .ok_or(WalkError::CantWalk), + } + } +} + +impl Loopable for serde_json::Value { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + match self { + serde_json::Value::Array(array_value) => array_value.iter().map(|x| x as _).collect(), + _ => Vec::new(), + } + } +} + +impl Sizable for serde_json::Value { + fn is_castable(&self) -> bool { + match self { + serde_json::Value::Null => true, + serde_json::Value::Bool(_) => false, + serde_json::Value::Number(_) => true, + serde_json::Value::String(_) => true, + serde_json::Value::Array(_) => true, + serde_json::Value::Object(_) => true, + } + } + + fn get_size<'a>(&'a self) -> Option> { + match self { + serde_json::Value::Null => { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) + } + serde_json::Value::Bool(_boolean) => { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) + } + serde_json::Value::Number(_num) => Some(IceResult::from_borrowed(self)), + serde_json::Value::String(text) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(text.len().try_into().unwrap()), + )), + serde_json::Value::Array(arr) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(arr.len().try_into().unwrap()), + )), + serde_json::Value::Object(obj) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(obj.len().try_into().unwrap()), + )), + } + } +} + +impl Castable for serde_json::Value { + fn cast_to_type<'a>(&'a self, target: &str) -> Option> { + match (self, target) { + (serde_json::Value::String(text), "number") => text + .parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LPositiveInteger(num))) + .or_else(|_| { + text.parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LNegativeInteger(num))) + }) + .or_else(|_| { + text.parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LFloat(num))) + }) + .ok(), + (serde_json::Value::Number(_), "number") => Some(IceResult::from_borrowed(self)), + (serde_json::Value::Null, "number") => { + Some(IceResult::from_owned(serde_json::Value::Number(0.into()))) + } + (serde_json::Value::Bool(boolean), "number") => { + if *boolean { + Some(IceResult::from_owned(serde_json::Value::Number(1.into()))) + } else { + Some(IceResult::from_owned(serde_json::Value::Number(0.into()))) + } + } + (serde_json::Value::Array(_), "number") => None, + (serde_json::Value::Object(_), "number") => None, + + (serde_json::Value::String(_), "string") => Some(IceResult::from_borrowed(self)), + (serde_json::Value::Number(num), "string") => Some(IceResult::from_owned( + serde_json::Value::String(num.to_string()), + )), + (serde_json::Value::Null, "string") => Some(IceResult::from_owned( + serde_json::Value::String("null".to_owned()), + )), + (serde_json::Value::Bool(boolean), "string") => Some(IceResult::from_owned( + serde_json::Value::String(boolean.to_string()), + )), + (serde_json::Value::Array(_), "string") => Some(IceResult::from_owned( + serde_json::Value::String(self.render(&Vec::new()).unwrap_or("".to_owned())), + )), + (serde_json::Value::Object(_), "string") => Some(IceResult::from_owned( + serde_json::Value::String(self.render(&Vec::new()).unwrap_or("".to_owned())), + )), + + (serde_json::Value::String(text), "boolean") => { + if text.is_empty() { + Some(IceResult::from_owned(serde_json::Value::Bool(false))) + } else { + Some(IceResult::from_owned(serde_json::Value::Bool(true))) + } + } + (serde_json::Value::Number(json_num), "boolean") => { + Some(IceResult::from_owned(serde_json::Value::Bool( + match (json_num.as_u64(), json_num.as_i64(), json_num.as_f64()) { + (Some(num), _, _) => num != 0, + (_, Some(num), _) => num != 0, + (_, _, Some(num)) => num != 0.0 && !num.is_nan(), + _ => false, + }, + ))) + } + (serde_json::Value::Null, "boolean") => { + Some(IceResult::from_owned(serde_json::Value::Bool(false))) + } + (serde_json::Value::Bool(_), "boolean") => Some(IceResult::from_borrowed(self)), + (serde_json::Value::Array(_), "boolean") => { + Some(IceResult::from_owned(serde_json::Value::Bool(true))) + } + (serde_json::Value::Object(_), "boolean") => { + Some(IceResult::from_owned(serde_json::Value::Bool(true))) + } + + (_, _) => panic!("Unimplemented cast"), + } + } +} + +impl CompareContextElement for serde_json::Value { + fn equals(&self, other: &dyn ContextElement) -> bool { + // println!("Json equality check {:?} == {:?}", self, other); + // Handle other serde_json::Value + match other.to_any().downcast_ref::() { + None => (), + Some(other_json_value) => match (self, other_json_value) { + // Non-scalar values not caught in the renderer by the + // identical-path shortcut are always not equal. + (serde_json::Value::Array(_), _) + | (_, serde_json::Value::Array(_)) + | (serde_json::Value::Object(_), _) + | (_, serde_json::Value::Object(_)) => return false, + _ => return self == other_json_value, + }, + } + // Handle literals + match other.to_any().downcast_ref::() { + None => (), + Some(OwnedLiteral::LString(other_string)) => { + return self.as_str().map_or(false, |s| s == other_string) + } + Some(OwnedLiteral::LBoolean(boolean)) => { + return self.equals(&serde_json::Value::Bool(*boolean) as &dyn ContextElement); + } + Some(OwnedLiteral::LPositiveInteger(other_num)) => { + let other_json_num: serde_json::Number = std::convert::From::from(*other_num); + return self + .equals(&serde_json::Value::Number(other_json_num) as &dyn ContextElement); + } + Some(OwnedLiteral::LNegativeInteger(other_num)) => { + let other_json_num: serde_json::Number = std::convert::From::from(*other_num); + return self + .equals(&serde_json::Value::Number(other_json_num) as &dyn ContextElement); + } + Some(OwnedLiteral::LFloat(other_num)) => match self.as_f64() { + None => return false, + Some(self_float) => return self_float == *other_num, + }, + } + false + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // Handle type coerced objects + + // When doing a greater than or less than comparison, + // javascript coerces objects into "[object Object]". + if let serde_json::Value::Object(_) = self { + return OwnedLiteral::LString(self.render(&Vec::new()).unwrap_or("".to_owned())) + .partial_compare(other); + } + + // When doing a greater than or less than comparison + // javascript turns arrays into strings. + if let serde_json::Value::Array(_) = self { + return OwnedLiteral::LString(self.render(&Vec::new()).unwrap_or("".to_owned())) + .partial_compare(other); + } + + let maybe_json_other = other.to_any().downcast_ref::(); + let maybe_literal_other = other.to_any().downcast_ref::(); + + // If they're both strings, compare them directly + match (self, maybe_json_other, maybe_literal_other) { + // If they're both strings, compare them directly + ( + serde_json::Value::String(self_string), + Some(serde_json::Value::String(other_string)), + _, + ) => return self_string.partial_cmp(&other_string), + ( + serde_json::Value::String(self_string), + _, + Some(OwnedLiteral::LString(other_string)), + ) => return self_string.partial_cmp(&other_string), + // Otherwise, convert to numbers and compare them that way + (_, Some(json_other), _) => return compare_json_numbers(self, json_other), + (_, _, Some(literal_other)) => return compare_json_numbers(self, literal_other), + _ => panic!("Unimplemented comparison type."), + } + } + + fn math_add<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + + std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + + std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_subtract<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + - std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + - std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_multiply<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + * std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + * std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_divide<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + / std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + / std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_modulus<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + % std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + % std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_abs<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_abs() + .map(IceResult::from_owned) + } + + fn math_floor<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_floor() + .map(IceResult::from_owned) + } + + fn math_ceil<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_ceil() + .map(IceResult::from_owned) + } +} + +impl From<&serde_json::Value> for ComparisonNumber { + /// Convert from a JSON value to a number for comparison based on + /// the logic described at + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Greater_than + fn from(original: &serde_json::Value) -> Self { + match original { + serde_json::Value::Null => ComparisonNumber::UnsignedInteger(0), + serde_json::Value::Bool(boolean) => { + if *boolean { + ComparisonNumber::UnsignedInteger(1) + } else { + ComparisonNumber::UnsignedInteger(0) + } + } + serde_json::Value::Number(num) => num.into(), + serde_json::Value::String(text) => text.into(), + serde_json::Value::Array(_) => { + panic!("Only primitives should be cast to numbers for comparisons") + } + serde_json::Value::Object(_) => { + panic!("Only primitives should be cast to numbers for comparisons") + } + } + } +} + +impl From<&serde_json::Number> for ComparisonNumber { + fn from(original: &serde_json::Number) -> Self { + match original.as_u64() { + Some(num) => return ComparisonNumber::UnsignedInteger(num), + None => (), + }; + match original.as_i64() { + Some(num) => return ComparisonNumber::SignedInteger(num), + None => (), + }; + match original.as_f64() { + Some(num) => return ComparisonNumber::Decimal(num), + None => (), + }; + ComparisonNumber::Failure + } +} + +impl From<&serde_json::Value> for MathNumber { + fn from(original: &serde_json::Value) -> Self { + match original { + serde_json::Value::Null => MathNumber::Integer(0), + serde_json::Value::Bool(boolean) => { + if *boolean { + MathNumber::Integer(1) + } else { + MathNumber::Integer(0) + } + } + serde_json::Value::Number(num) => num.into(), + serde_json::Value::String(_) => { + panic!("Strings should not be cast to numbers for math") + } + serde_json::Value::Array(_) => { + panic!("Only primitives should be cast to numbers for comparisons") + } + serde_json::Value::Object(_) => { + panic!("Only primitives should be cast to numbers for comparisons") + } + } + } +} + +impl From<&serde_json::Number> for MathNumber { + fn from(original: &serde_json::Number) -> Self { + match original.as_u64() { + Some(num) => return MathNumber::Integer(num.try_into().unwrap()), + None => (), + }; + match original.as_i64() { + Some(num) => return MathNumber::Integer(num.into()), + None => (), + }; + match original.as_f64() { + Some(num) => return MathNumber::Decimal(num), + None => (), + }; + MathNumber::Failure + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nested_array_render() { + let x: serde_json::Value = + serde_json::from_str(r#"[3,5,[7,9]]"#).expect("Failed to parse json"); + assert_eq!( + x.render(&Vec::new()), + Ok::<_, RenderError>("3,5,7,9".to_owned()) + ); + } + + #[test] + fn test_html_escape() { + assert_eq!(html_escape("<>&\"'"), "<>&"'".to_owned()) + } } diff --git a/src/js/README.md b/src/js/README.md deleted file mode 100644 index 70a0fdd..0000000 --- a/src/js/README.md +++ /dev/null @@ -1,9 +0,0 @@ -* Compliance Tests -These tests run my implementation of LinkedIn's Dust and the official implementation of [LinkedIn's DustJS](https://www.dustjs.com) as compares the output to ensure they match. -** Setup -Install LinkedIn's DustJS in order to rust the compliance tests -** Running -The dustjs_shim expects a path to a template file as the first parameter, and it expects a json context over stdin. For example, to invoke it on the command line you could run: -```sh -cat test_cases/hello_world/input1.json | node dustjs_shim.js test_cases/hello_world/template.dust -``` diff --git a/src/js/dustjs_shim.js b/src/js/dustjs_shim.js deleted file mode 100644 index 83b1ba8..0000000 --- a/src/js/dustjs_shim.js +++ /dev/null @@ -1,29 +0,0 @@ -var dust = require('dustjs-linkedin'); -var fs = require('fs'); - -var argv = process.argv.slice(2); -if (argv.length != 1) { - console.error("Expecting only 1 argument (a path to a template)"); - process.exit(1); -} -var context = JSON.parse(fs.readFileSync(0, 'utf-8')); - -try { - var template_source = fs.readFileSync(argv[0], 'utf-8'); -} catch (err) { - console.error(err); - process.exit(1); -} - -var compiled_template = dust.compile(template_source, "tmpl"); -dust.loadSource(compiled_template); - -dust.render("tmpl", context, function(err, out) { - if(err) { - console.error(err); - process.exit(1); - } else { - console.log(out); - process.exit(0); - } -}); diff --git a/src/lib.rs b/src/lib.rs index a2ebd73..9299281 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ extern crate nom; -mod parser; +pub mod parser; +pub mod renderer; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 520bb61..6efae74 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,10 +1,16 @@ //! This module contains a rust implementation of LinkedIn Dust -mod node_invoker; mod parser; -pub use node_invoker::run_node_dust; -pub use node_invoker::NodeError; -pub use node_invoker::Result; pub use parser::template; +pub use parser::Body; +pub use parser::DustTag; +pub use parser::Filter; +pub use parser::KVPair; +pub use parser::OwnedLiteral; +pub use parser::PartialNameElement; +pub use parser::Path; +pub use parser::RValue; +pub use parser::Special; pub use parser::Template; +pub use parser::TemplateElement; diff --git a/src/parser/node_invoker.rs b/src/parser/node_invoker.rs deleted file mode 100644 index c554634..0000000 --- a/src/parser/node_invoker.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::error; -use std::fmt; -use std::io::Write; -use std::process::Output; -use std::process::{Command, Stdio}; - -pub type Result = std::result::Result; - -#[derive(Clone)] -pub struct NodeError { - output: Output, -} - -impl fmt::Display for NodeError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Error from node: {}", - String::from_utf8_lossy(&self.output.stderr) - ) - } -} - -impl fmt::Debug for NodeError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Error from node: {}", - String::from_utf8_lossy(&self.output.stderr) - ) - } -} - -impl error::Error for NodeError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - None - } -} - -/// Invokes Node to run the authentic LinkedIn Dust -pub fn run_node_dust(template_path: &str, context: &str) -> Result { - let mut proc = Command::new("node") - .arg("./src/js/dustjs_shim.js") - .arg(template_path) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("failed to execute process"); - proc.stdin - .take() - .unwrap() - .write_all(context.as_bytes()) - .expect("Failed to write to stdin of node process"); - let output = proc.wait_with_output().expect("Failed to wait on node"); - if output.status.success() { - Ok(String::from_utf8(output.stdout).expect("Invalid UTF-8 from node process")) - } else { - Err(NodeError { output: output }) - } -} diff --git a/src/parser/parser.rs b/src/parser/parser.rs index 295117b..e35a44b 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -2,20 +2,20 @@ use nom::branch::alt; use nom::bytes::complete::escaped_transform; use nom::bytes::complete::is_a; use nom::bytes::complete::is_not; -use nom::bytes::complete::tag; -use nom::bytes::complete::take_until; +use nom::bytes::complete::{tag, take_until, take_until_parser_matches}; +use nom::character::complete::line_ending; +use nom::character::complete::multispace0; use nom::character::complete::one_of; -use nom::character::complete::space1; +use nom::character::complete::{digit1, space0, space1}; +use nom::combinator::all_consuming; use nom::combinator::map; use nom::combinator::opt; use nom::combinator::recognize; -use nom::combinator::rest; use nom::combinator::value; use nom::combinator::verify; use nom::multi::many0; use nom::multi::many1; -use nom::multi::separated_list; -use nom::multi::separated_nonempty_list; +use nom::multi::separated_list1; use nom::sequence::delimited; use nom::sequence::preceded; use nom::sequence::separated_pair; @@ -23,27 +23,37 @@ use nom::sequence::terminated; use nom::sequence::tuple; use nom::IResult; -#[derive(Clone, Debug, PartialEq)] -enum DustTag<'a> { +#[derive(Debug, PartialEq)] +pub enum DustTag<'a> { DTSpecial(Special), DTComment(Comment<'a>), + DTLiteralStringBlock(&'a str), DTReference(Reference<'a>), - DTSection(Container<'a>), - DTExists(Container<'a>), - DTNotExists(Container<'a>), - DTBlock(NamedBlock<'a>), - DTInlinePartial(NamedBlock<'a>), + DTSection(ParameterizedBlock<'a>), + DTExists(ParameterizedBlock<'a>), + DTNotExists(ParameterizedBlock<'a>), + DTBlock(ParameterizedBlock<'a>), + DTInlinePartial(ParameterizedBlock<'a>), DTPartial(Partial<'a>), DTHelperEquals(ParameterizedBlock<'a>), DTHelperNotEquals(ParameterizedBlock<'a>), DTHelperGreaterThan(ParameterizedBlock<'a>), DTHelperLessThan(ParameterizedBlock<'a>), - DTHelperGreaterThenOrEquals(ParameterizedBlock<'a>), - DTHelperLessThenOrEquals(ParameterizedBlock<'a>), + DTHelperGreaterThanOrEquals(ParameterizedBlock<'a>), + DTHelperLessThanOrEquals(ParameterizedBlock<'a>), + DTHelperSep(ParameterizedBlock<'a>), + DTHelperFirst(ParameterizedBlock<'a>), + DTHelperLast(ParameterizedBlock<'a>), + DTHelperSelect(ParameterizedBlock<'a>), + DTHelperAny(ParameterizedBlock<'a>), + DTHelperNone(ParameterizedBlock<'a>), + DTHelperMath(ParameterizedBlock<'a>), + DTHelperSize(ParameterizedBlock<'a>), + DTHelperContextDump(ParameterizedBlock<'a>), } #[derive(Clone, Debug, PartialEq)] -enum Special { +pub enum Special { Space, NewLine, CarriageReturn, @@ -51,24 +61,32 @@ enum Special { RightCurlyBrace, } -#[derive(Clone, Debug, PartialEq)] -struct Comment<'a> { +#[derive(Debug, PartialEq)] +pub enum IgnoredWhitespace<'a> { + StartOfLine(&'a str), +} + +#[derive(Debug, PartialEq)] +pub struct Comment<'a> { value: &'a str, } -#[derive(Clone, Debug, PartialEq)] -struct Path<'a> { - keys: Vec<&'a str>, +/// A series of keys separated by '.' to reference a variable in the context +/// +/// Special case: If the path is just "." then keys will be an empty vec +#[derive(Debug, PartialEq)] +pub struct Path<'a> { + pub keys: Vec<&'a str>, +} + +#[derive(Debug, PartialEq)] +pub struct Reference<'a> { + pub path: Path<'a>, + pub filters: Vec, } #[derive(Clone, Debug, PartialEq)] -struct Reference<'a> { - path: Path<'a>, - filters: Vec, -} - -#[derive(Clone, Debug, PartialEq)] -enum Filter { +pub enum Filter { HtmlEncode, DisableHtmlEncode, JavascriptStringEncode, @@ -78,64 +96,119 @@ enum Filter { JsonParse, } -#[derive(Clone, Debug, PartialEq)] -struct Span<'a> { - contents: &'a str, +#[derive(Debug, PartialEq)] +pub struct Span<'a> { + pub contents: &'a str, } -#[derive(Clone, Debug, PartialEq)] -struct Container<'a> { - path: Path<'a>, - contents: Option>, - else_contents: Option>, +#[derive(Debug, PartialEq)] +pub struct ParameterizedBlock<'a> { + pub path: Path<'a>, + pub explicit_context: Option>, + pub params: Vec>, + pub contents: Option>, + pub else_contents: Option>, } -#[derive(Clone, Debug, PartialEq)] -struct NamedBlock<'a> { - name: &'a str, - contents: Option>, +#[derive(Debug, PartialEq)] +pub struct Partial<'a> { + pub name: Vec, + pub explicit_context: Option>, + pub params: Vec>, } -#[derive(Clone, Debug, PartialEq)] -struct ParameterizedBlock<'a> { - name: &'a str, - params: Vec>, - contents: Option>, - else_contents: Option>, +#[derive(Debug, PartialEq)] +pub enum OwnedLiteral { + LString(String), + LPositiveInteger(u64), + LNegativeInteger(i64), + LFloat(f64), + // Unlike the other OwnedLiterals, booleans cannot occur in DustJS + // templates because true/false are not reserved + // names. Regardless, they are needed here for type casting in the + // renderer. + LBoolean(bool), } -#[derive(Clone, Debug, PartialEq)] -struct Partial<'a> { - name: String, - params: Vec>, -} - -#[derive(Clone, Debug, PartialEq)] -enum RValue<'a> { +#[derive(Debug, PartialEq)] +pub enum RValue<'a> { RVPath(Path<'a>), - RVString(String), + RVTemplate(Vec), + RVLiteral(OwnedLiteral), } -#[derive(Clone, Debug, PartialEq)] -struct KVPair<'a> { - key: &'a str, - value: RValue<'a>, +#[derive(Debug, PartialEq)] +pub struct KVPair<'a> { + pub key: &'a str, + pub value: RValue<'a>, } -#[derive(Clone, Debug, PartialEq)] -struct Body<'a> { - elements: Vec>, +#[derive(Debug, PartialEq)] +pub enum PartialNameElement { + PNSpan { + contents: String, + }, + PNReference { + path: Vec, + filters: Vec, + }, } -#[derive(Clone, Debug)] +#[derive(Debug, PartialEq)] +pub struct Body<'a> { + pub elements: Vec>, +} + +#[derive(Debug, PartialEq)] pub struct Template<'a> { - contents: Body<'a>, + pub contents: Body<'a>, } -#[derive(Clone, Debug, PartialEq)] -enum TemplateElement<'a> { +#[derive(Debug, PartialEq)] +pub enum TemplateElement<'a> { TESpan(Span<'a>), TETag(DustTag<'a>), + TEIgnoredWhitespace(IgnoredWhitespace<'a>), +} + +impl From> for PartialNameElement { + fn from(original: TemplateElement) -> Self { + match original { + TemplateElement::TESpan(span) => PartialNameElement::PNSpan { + contents: span.contents.to_owned(), + }, + TemplateElement::TETag(DustTag::DTReference(reference)) => { + PartialNameElement::PNReference { + path: reference + .path + .keys + .into_iter() + .map(|s| s.to_owned()) + .collect(), + filters: reference.filters, + } + } + _ => panic!("Only spans and references can be used in partial names."), + } + } +} + +impl<'a> From<&'a PartialNameElement> for TemplateElement<'a> { + fn from(original: &'a PartialNameElement) -> Self { + match original { + PartialNameElement::PNSpan { contents } => { + TemplateElement::TESpan(Span { contents: contents }) + } + PartialNameElement::PNReference { path, filters } => { + TemplateElement::TETag(DustTag::DTReference(Reference { + path: Path { + keys: path.into_iter().map(|s| s.as_str()).collect(), + }, + filters: filters.into_iter().map(|f| f.clone()).collect(), + })) + } + } + } } /// Any element significant to dust that isn't plain text @@ -145,19 +218,89 @@ fn dust_tag(i: &str) -> IResult<&str, DustTag> { alt(( map(special, DustTag::DTSpecial), map(comment, DustTag::DTComment), + map(literal_string_block, DustTag::DTLiteralStringBlock), map(reference, DustTag::DTReference), - conditional("{#", DustTag::DTSection), - conditional("{?", DustTag::DTExists), - conditional("{^", DustTag::DTNotExists), - named_block("{+", DustTag::DTBlock), - named_block("{<", DustTag::DTInlinePartial), + map(parameterized_block("{#", path), DustTag::DTSection), + map(parameterized_block("{?", path), DustTag::DTExists), + map(parameterized_block("{^", path), DustTag::DTNotExists), + map( + parameterized_block_without_else("{+", key_to_path), + DustTag::DTBlock, + ), + map( + parameterized_block_without_else("{<", key_to_path), + DustTag::DTInlinePartial, + ), partial("{>", DustTag::DTPartial), - parameterized_block("{@", "gte", DustTag::DTHelperGreaterThenOrEquals), - parameterized_block("{@", "lte", DustTag::DTHelperLessThenOrEquals), - parameterized_block("{@", "eq", DustTag::DTHelperEquals), - parameterized_block("{@", "ne", DustTag::DTHelperNotEquals), - parameterized_block("{@", "gt", DustTag::DTHelperGreaterThan), - parameterized_block("{@", "lt", DustTag::DTHelperLessThan), + dust_tag_helper, + ))(i) +} + +/// Nom's alt() is limited to 21 possibilities, so I pushed this out +/// into its own parser. Otherwise there is no reason for this not to +/// be part of the dust_tag parser. +fn dust_tag_helper(i: &str) -> IResult<&str, DustTag> { + alt(( + map( + parameterized_block("{@", &tag_to_path("gte")), + DustTag::DTHelperGreaterThanOrEquals, + ), + map( + parameterized_block("{@", &tag_to_path("lte")), + DustTag::DTHelperLessThanOrEquals, + ), + map( + parameterized_block("{@", &tag_to_path("eq")), + DustTag::DTHelperEquals, + ), + map( + parameterized_block("{@", &tag_to_path("ne")), + DustTag::DTHelperNotEquals, + ), + map( + parameterized_block("{@", &tag_to_path("gt")), + DustTag::DTHelperGreaterThan, + ), + map( + parameterized_block("{@", &tag_to_path("lt")), + DustTag::DTHelperLessThan, + ), + map( + parameterized_block("{@", &tag_to_path("sep")), + DustTag::DTHelperSep, + ), + map( + parameterized_block("{@", &tag_to_path("first")), + DustTag::DTHelperFirst, + ), + map( + parameterized_block("{@", &tag_to_path("last")), + DustTag::DTHelperLast, + ), + map( + parameterized_block("{@", &tag_to_path("select")), + DustTag::DTHelperSelect, + ), + map( + parameterized_block("{@", &tag_to_path("any")), + DustTag::DTHelperAny, + ), + map( + parameterized_block("{@", &tag_to_path("none")), + DustTag::DTHelperNone, + ), + map( + parameterized_block("{@", &tag_to_path("math")), + DustTag::DTHelperMath, + ), + map( + parameterized_block("{@", &tag_to_path("size")), + DustTag::DTHelperSize, + ), + map( + parameterized_block("{@", &tag_to_path("contextDump")), + DustTag::DTHelperContextDump, + ), ))(i) } @@ -195,16 +338,97 @@ fn key(i: &str) -> IResult<&str, &str> { /// A series of keys separated by '.' to reference a variable in the context fn path(i: &str) -> IResult<&str, Path> { - map(separated_nonempty_list(tag("."), key), |body| Path { - keys: body, - })(i) + alt(( + map(separated_list1(tag("."), key), |body| Path { keys: body }), + map( + tuple((tag("."), separated_list1(tag("."), key))), + |(dot, mut body)| { + body.insert(0, dot); + Path { keys: body } + }, + ), + map(tag("."), |dot| Path { keys: vec![dot] }), + ))(i) +} + +fn tag_to_path<'a>(text: &'static str) -> impl Fn(&'a str) -> IResult<&str, Path<'a>> { + move |i: &'a str| map(tag(text), |t| Path { keys: vec![t] })(i) +} + +fn key_to_path<'a>(i: &'a str) -> IResult<&str, Path<'a>> { + map(key, |k| Path { keys: vec![k] })(i) +} + +/// Just digits, no signs or decimals +fn postitive_integer_literal(i: &str) -> IResult<&str, u64> { + map( + verify( + map( + recognize(tuple((opt(tag("+")), digit1))), + |number_string: &str| number_string.parse::(), + ), + |parse_result| parse_result.is_ok(), + ), + |parsed_number| parsed_number.unwrap(), + )(i) +} + +/// No decimals, just the sign and digits +fn negative_integer_literal(i: &str) -> IResult<&str, i64> { + map( + verify( + map( + recognize(tuple((tag("-"), digit1))), + |number_string: &str| number_string.parse::(), + ), + |parse_result| parse_result.is_ok(), + ), + |parsed_number| parsed_number.unwrap(), + )(i) +} + +/// A non-scientific notation float (sign, digits, decimal, and more digits) +fn float_literal(i: &str) -> IResult<&str, f64> { + map( + verify( + map( + recognize(tuple((opt(one_of("+-")), digit1, tag("."), digit1))), + |number_string: &str| number_string.parse::(), + ), + |parse_result| parse_result.is_ok(), + ), + |parsed_number| parsed_number.unwrap(), + )(i) +} + +fn template_string_rvalue(i: &str) -> IResult<&str, Vec> { + let (i, template_string) = verify(quoted_string, |s: &String| { + partial_quoted_tag(s.as_str()).is_ok() + })(i)?; + + let (_remaining, parsed_template_elements) = partial_quoted_tag(template_string.as_str()) + .expect("A successful parse was verified earlier with a call to verify()"); + let converted_template_elements = parsed_template_elements + .into_iter() + .map(|e| e.into()) + .collect(); + Ok((i, converted_template_elements)) } /// Either a literal or a path to a value fn rvalue(i: &str) -> IResult<&str, RValue> { alt(( map(path, RValue::RVPath), - map(quoted_string, RValue::RVString), + map(template_string_rvalue, RValue::RVTemplate), + map(float_literal, |num| { + RValue::RVLiteral(OwnedLiteral::LFloat(num)) + }), + map(negative_integer_literal, |num| { + RValue::RVLiteral(OwnedLiteral::LNegativeInteger(num)) + }), + map(postitive_integer_literal, |num| { + RValue::RVLiteral(OwnedLiteral::LPositiveInteger(num)) + }), ))(i) } @@ -218,7 +442,11 @@ fn key_value_pair(i: &str) -> IResult<&str, KVPair> { /// Display a value from the context fn reference(i: &str) -> IResult<&str, Reference> { - let (remaining, (p, filters)) = delimited(tag("{"), tuple((path, many0(filter))), tag("}"))(i)?; + let (remaining, (p, filters)) = delimited( + tag("{"), + tuple((path, many0(filter))), + preceded(space0, tag("}")), + )(i)?; Ok(( remaining, Reference { @@ -228,201 +456,191 @@ fn reference(i: &str) -> IResult<&str, Reference> { )) } -fn conditional<'a, F>( - open_matcher: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> -where - F: Copy + Fn(Container<'a>) -> DustTag<'a>, -{ - alt(( - conditional_with_body(open_matcher, constructor), - self_closing_conditional(open_matcher, constructor), - )) -} - -fn conditional_with_body<'a, F>( - open_matcher: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> -where - F: Fn(Container<'a>) -> DustTag<'a>, -{ - move |i: &'a str| { - let (i, (opening_name, inner, maybe_else, _closing_name)) = verify( - tuple(( - delimited(tag(open_matcher), path, tag("}")), - opt(body), - opt(preceded(tag("{:else}"), opt(body))), - delimited(tag("{/"), path, tag("}")), - )), - |(open, _inn, _maybe_else, close)| open == close, - )(i)?; - - Ok(( - i, - constructor(Container { - path: opening_name, - contents: inner, - else_contents: maybe_else.flatten(), - }), - )) - } -} - -fn self_closing_conditional<'a, F>( - open_matcher: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> -where - F: Fn(Container<'a>) -> DustTag<'a>, -{ - move |i: &'a str| { - let (i, path) = delimited(tag(open_matcher), path, tag("/}"))(i)?; - - Ok(( - i, - constructor(Container { - path: path, - contents: None, - else_contents: None, - }), - )) - } -} - -fn named_block<'a, F>( - open_matcher: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> -where - F: Copy + Fn(NamedBlock<'a>) -> DustTag<'a>, -{ - alt(( - named_block_with_body(open_matcher, constructor), - self_closing_named_block(open_matcher, constructor), - )) -} - -fn named_block_with_body<'a, F>( - open_matcher: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> -where - F: Fn(NamedBlock<'a>) -> DustTag<'a>, -{ - move |i: &'a str| { - let (i, (opening_name, inner, _closing_name)) = verify( - tuple(( - delimited(tag(open_matcher), key, tag("}")), - opt(body), - delimited(tag("{/"), key, tag("}")), - )), - |(open, _inn, close)| open == close, - )(i)?; - - Ok(( - i, - constructor(NamedBlock { - name: opening_name, - contents: inner, - }), - )) - } -} - -fn self_closing_named_block<'a, F>( - open_matcher: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> -where - F: Fn(NamedBlock<'a>) -> DustTag<'a>, -{ - move |i: &'a str| { - let (i, name) = delimited(tag(open_matcher), key, tag("/}"))(i)?; - - Ok(( - i, - constructor(NamedBlock { - name: name, - contents: None, - }), - )) - } -} - fn parameterized_block<'a, F>( open_matcher: &'static str, - tag_name: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> + name_matcher: F, +) -> impl FnMut(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> where - F: Copy + Fn(ParameterizedBlock<'a>) -> DustTag<'a>, + F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, { alt(( - parameterized_block_with_body(open_matcher, tag_name, constructor), - parameterized_self_closing_block(open_matcher, tag_name, constructor), + parameterized_block_with_body(open_matcher, name_matcher), + parameterized_self_closing_block(open_matcher, name_matcher), + )) +} + +fn parameterized_block_without_else<'a, F>( + open_matcher: &'static str, + name_matcher: F, +) -> impl FnMut(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> +where + F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, +{ + alt(( + parameterized_block_with_body_without_else(open_matcher, name_matcher), + parameterized_self_closing_block(open_matcher, name_matcher), )) } fn parameterized_block_with_body<'a, F>( open_matcher: &'static str, - tag_name: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> + name_matcher: F, +) -> impl Fn(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> where - F: Fn(ParameterizedBlock<'a>) -> DustTag<'a>, + F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, { move |i: &'a str| { - let (i, (name, params, inner, maybe_else, _closing_name)) = tuple(( - preceded(tag(open_matcher), tag(tag_name)), - terminated( - opt(preceded(space1, separated_list(space1, key_value_pair))), - tag("}"), - ), - opt(body), - opt(preceded(tag("{:else}"), opt(body))), - delimited(tag("{/"), tag(tag_name), tag("}")), - ))(i)?; + let (i, (opening_name, maybe_explicit_context, params, inner, maybe_else, _closing_name)) = + verify( + tuple(( + preceded(tag(open_matcher), name_matcher), + opt(preceded(tag(":"), path)), + terminated( + opt(preceded(space1, separated_list1(space1, key_value_pair))), + preceded(space0, tag("}")), + ), + opt(body), + opt(preceded(tag("{:else}"), opt(body))), + delimited(tag("{/"), name_matcher, tag("}")), + )), + |(open, _maybe_explicit, _params, _inn, _maybe_else, close)| open == close, + )(i)?; Ok(( i, - constructor(ParameterizedBlock { - name: name, + ParameterizedBlock { + path: opening_name, + explicit_context: maybe_explicit_context, params: params.unwrap_or(Vec::new()), contents: inner, else_contents: maybe_else.flatten(), - }), + }, + )) + } +} + +fn parameterized_block_with_body_without_else<'a, F>( + open_matcher: &'static str, + name_matcher: F, +) -> impl Fn(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> +where + F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, +{ + move |i: &'a str| { + let (i, (opening_name, maybe_explicit_context, params, inner, _closing_name)) = + verify( + tuple(( + preceded(tag(open_matcher), name_matcher), + opt(preceded(tag(":"), path)), + terminated( + opt(preceded(space1, separated_list1(space1, key_value_pair))), + preceded(space0, tag("}")), + ), + opt(body), + delimited(tag("{/"), name_matcher, tag("}")), + )), + |(open, _maybe_explicit, _params, _inn, close)| open == close, + )(i)?; + + Ok(( + i, + ParameterizedBlock { + path: opening_name, + explicit_context: maybe_explicit_context, + params: params.unwrap_or(Vec::new()), + contents: inner, + else_contents: None, + }, )) } } fn parameterized_self_closing_block<'a, F>( open_matcher: &'static str, - tag_name: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> + name_matcher: F, +) -> impl Fn(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> where - F: Fn(ParameterizedBlock<'a>) -> DustTag<'a>, + F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, { move |i: &'a str| { - let (i, (name, params)) = delimited( + let (i, (opening_name, maybe_explicit_context, params)) = delimited( tag(open_matcher), tuple(( - tag(tag_name), - opt(preceded(space1, separated_list(space1, key_value_pair))), + name_matcher, + opt(preceded(tag(":"), path)), + opt(preceded(space1, separated_list1(space1, key_value_pair))), )), - tag("/}"), + preceded(space0, tag("/}")), )(i)?; Ok(( i, - constructor(ParameterizedBlock { - name: name, + ParameterizedBlock { + path: opening_name, + explicit_context: maybe_explicit_context, params: params.unwrap_or(Vec::new()), contents: None, else_contents: None, - }), + }, + )) + } +} + +fn partial_with_plain_tag<'a>( + open_matcher: &'static str, +) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> { + move |i: &'a str| { + let (i, (name, maybe_explicit_context, params)) = delimited( + tag(open_matcher), + tuple(( + key, + opt(preceded(tag(":"), path)), + opt(preceded(space1, separated_list1(space1, key_value_pair))), + )), + preceded(space0, tag("/}")), + )(i)?; + + Ok(( + i, + Partial { + name: vec![PartialNameElement::PNSpan { + contents: name.to_owned(), + }], + explicit_context: maybe_explicit_context, + params: params.unwrap_or(Vec::new()), + }, + )) + } +} + +fn partial_quoted_tag(i: &str) -> IResult<&str, Vec> { + all_consuming(many1(alt(( + map(span, TemplateElement::TESpan), + map(map(reference, DustTag::DTReference), TemplateElement::TETag), + ))))(i) +} + +fn partial_with_quoted_tag<'a>( + open_matcher: &'static str, +) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> { + move |i: &'a str| { + let (i, (name, maybe_explicit_context, params)) = delimited( + tag(open_matcher), + tuple(( + template_string_rvalue, + opt(preceded(tag(":"), path)), + opt(preceded(space1, separated_list1(space1, key_value_pair))), + )), + preceded(space0, tag("/}")), + )(i)?; + + Ok(( + i, + Partial { + name: name, + explicit_context: maybe_explicit_context, + params: params.unwrap_or(Vec::new()), + }, )) } } @@ -434,24 +652,9 @@ fn partial<'a, F>( where F: Fn(Partial<'a>) -> DustTag<'a>, { - move |i: &'a str| { - let (i, (name, params)) = delimited( - tag(open_matcher), - tuple(( - alt((map(key, String::from), quoted_string)), - opt(preceded(space1, separated_list(space1, key_value_pair))), - )), - tag("/}"), - )(i)?; - - Ok(( - i, - constructor(Partial { - name: name, - params: params.unwrap_or(Vec::new()), - }), - )) - } + let plain = partial_with_plain_tag(open_matcher); + let quoted = partial_with_quoted_tag(open_matcher); + move |i: &'a str| map(alt((&plain, "ed)), &constructor)(i) } fn filter(i: &str) -> IResult<&str, Filter> { @@ -469,14 +672,39 @@ fn filter(i: &str) -> IResult<&str, Filter> { )(i) } -/// Any text that is not a Dust element +/// Whitespace at the beginning of lines is ignored so we are matching +/// a newline character followed by as much contiguous whitespace as +/// possible, all of which will be thrown away by other parsers. +fn ignore_new_line_leading_whitespace(i: &str) -> IResult<&str, IgnoredWhitespace> { + map( + recognize(tuple((line_ending, multispace0))), + IgnoredWhitespace::StartOfLine, + )(i) +} + +fn literal_string_block(i: &str) -> IResult<&str, &str> { + delimited(tag("{`"), take_until("`}"), tag("`}"))(i) +} + +/// Any text that is not a Dust element or ignored whitespace fn span(i: &str) -> IResult<&str, Span> { - let (remaining, body) = verify(alt((take_until("{"), rest)), |s: &str| s.len() > 0)(i)?; - Ok((remaining, Span { contents: body })) + let (remaining, line) = verify( + take_until_parser_matches(alt(( + tag("{"), + line_ending, + recognize(all_consuming(eof_whitespace)), + ))), + |s: &str| s.len() > 0, + )(i)?; + Ok((remaining, Span { contents: line })) } fn body(i: &str) -> IResult<&str, Body> { let (remaining, template_elements) = many1(alt(( + map( + ignore_new_line_leading_whitespace, + TemplateElement::TEIgnoredWhitespace, + ), map(span, TemplateElement::TESpan), map(dust_tag, TemplateElement::TETag), )))(i)?; @@ -489,7 +717,8 @@ fn body(i: &str) -> IResult<&str, Body> { } pub fn template(i: &str) -> IResult<&str, Template> { - let (remaining, contents) = body(i)?; + // DustJS ignores all preceding whitespace (tabs, newlines, spaces) but only ignores trailing newlines + let (remaining, contents) = all_consuming(delimited(multispace0, body, eof_whitespace))(i)?; Ok((remaining, Template { contents: contents })) } @@ -501,6 +730,10 @@ fn quoted_string(i: &str) -> IResult<&str, String> { )(i) } +fn eof_whitespace(i: &str) -> IResult<&str, Vec<&str>> { + many0(line_ending)(i) +} + #[cfg(test)] mod tests { use super::*; @@ -508,6 +741,13 @@ mod tests { use nom::error::ErrorKind; use nom::Err::Error; + #[test] + fn test_direct_literal() { + assert_eq!(super::float_literal("-17.4"), Ok(("", -17.4))); + assert_eq!(super::float_literal("17.1"), Ok(("", 17.1))); + assert_eq!(super::negative_integer_literal("-12"), Ok(("", -12))); + } + #[test] fn test_reference() { assert_eq!( @@ -524,6 +764,20 @@ mod tests { ); } + #[test] + fn test_reference_to_variable() { + assert_eq!( + super::reference("{$idx}"), + Ok(( + "", + Reference { + path: Path { keys: vec!["$idx"] }, + filters: Vec::new(), + } + )) + ); + } + #[test] fn test_path() { assert_eq!( @@ -577,6 +831,14 @@ mod tests { ); } + #[test] + fn test_span_end_of_line() { + assert_eq!( + super::ignore_new_line_leading_whitespace("\n \t \n\nfoo"), + Ok(("foo", IgnoredWhitespace::StartOfLine("\n \t \n\n"))) + ); + } + #[test] fn test_span() { assert_eq!( @@ -601,6 +863,48 @@ mod tests { super::span("{~lb}"), Err(Error(("{~lb}", ErrorKind::Verify))) ); + assert_eq!( + super::body("this is \t \n\n \t \n \t multiline text\n {foo}"), + Ok(( + "", + Body { + elements: vec![ + TemplateElement::TESpan(Span { + contents: "this is \t " + }), + TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( + "\n\n \t \n \t " + )), + TemplateElement::TESpan(Span { + contents: "multiline text" + }), + TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( + "\n " + )), + TemplateElement::TETag(DustTag::DTReference(Reference { + path: Path { keys: vec!["foo"] }, + filters: vec![] + })) + ] + } + )) + ); + assert_eq!( + super::body("\n leading whitespace"), + Ok(( + "", + Body { + elements: vec![ + TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( + "\n " + )), + TemplateElement::TESpan(Span { + contents: "leading whitespace" + }), + ] + } + )) + ); } #[test] @@ -617,10 +921,12 @@ mod tests { super::dust_tag("{#foo.bar}{/foo.bar}"), Ok(( "", - DustTag::DTSection(Container { + DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["foo", "bar"] }, + explicit_context: None, + params: Vec::new(), contents: None, else_contents: None, }) @@ -634,10 +940,12 @@ mod tests { super::dust_tag("{#foo.bar/}"), Ok(( "", - DustTag::DTSection(Container { + DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["foo", "bar"] }, + explicit_context: None, + params: Vec::new(), contents: None, else_contents: None, }) @@ -651,10 +959,12 @@ mod tests { super::dust_tag("{#foo.bar}hello {name}{/foo.bar}"), Ok(( "", - DustTag::DTSection(Container { + DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["foo", "bar"] }, + explicit_context: None, + params: Vec::new(), contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "hello " }), @@ -676,10 +986,12 @@ mod tests { super::dust_tag("{#greeting}hello {name}{:else}goodbye {name}{/greeting}"), Ok(( "", - DustTag::DTSection(Container { + DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["greeting"] }, + explicit_context: None, + params: Vec::new(), contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "hello " }), @@ -705,15 +1017,58 @@ mod tests { ); } + #[test] + fn test_empty_section_with_explicit_context() { + assert_eq!( + super::dust_tag("{#foo.bar:baz.ipsum}{/foo.bar}"), + Ok(( + "", + DustTag::DTSection(ParameterizedBlock { + path: Path { + keys: vec!["foo", "bar"] + }, + explicit_context: Some(Path { + keys: vec!["baz", "ipsum"] + }), + params: Vec::new(), + contents: None, + else_contents: None, + }) + )) + ); + } + + #[test] + fn test_self_closing_section_with_explicit_context() { + assert_eq!( + super::dust_tag("{#foo.bar:$idx/}"), + Ok(( + "", + DustTag::DTSection(ParameterizedBlock { + path: Path { + keys: vec!["foo", "bar"] + }, + explicit_context: Some(Path { keys: vec!["$idx"] }), + params: Vec::new(), + contents: None, + else_contents: None, + }) + )) + ); + } + #[test] fn test_self_closing_block() { assert_eq!( super::dust_tag("{+foo/}"), Ok(( "", - DustTag::DTBlock(NamedBlock { - name: "foo", - contents: None + DustTag::DTBlock(ParameterizedBlock { + path: Path { keys: vec!["foo"] }, + explicit_context: None, + params: Vec::new(), + contents: None, + else_contents: None }) )) ); @@ -725,8 +1080,10 @@ mod tests { super::dust_tag("{+foo}hello {name}{/foo}"), Ok(( "", - DustTag::DTBlock(NamedBlock { - name: "foo", + DustTag::DTBlock(ParameterizedBlock { + path: Path { keys: vec!["foo"] }, + explicit_context: None, + params: Vec::new(), contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "hello " }), @@ -735,7 +1092,54 @@ mod tests { filters: Vec::new() })) ] - }) + }), + else_contents: None + }) + )) + ); + } + + #[test] + fn test_self_closing_block_with_explicit_context() { + assert_eq!( + super::dust_tag("{+foo:bar.baz/}"), + Ok(( + "", + DustTag::DTBlock(ParameterizedBlock { + path: Path { keys: vec!["foo"] }, + explicit_context: Some(Path { + keys: vec!["bar", "baz"] + }), + params: Vec::new(), + contents: None, + else_contents: None + }) + )) + ); + } + + #[test] + fn test_block_with_explicit_context() { + assert_eq!( + super::dust_tag("{+foo:bar.baz}hello {name}{/foo}"), + Ok(( + "", + DustTag::DTBlock(ParameterizedBlock { + path: Path { keys: vec!["foo"] }, + explicit_context: Some(Path { + keys: vec!["bar", "baz"] + }), + params: Vec::new(), + contents: Some(Body { + elements: vec![ + TemplateElement::TESpan(Span { contents: "hello " }), + TemplateElement::TETag(DustTag::DTReference(Reference { + path: Path { keys: vec!["name"] }, + filters: Vec::new() + })) + ] + }), + else_contents: None }) )) ); @@ -747,9 +1151,12 @@ mod tests { super::dust_tag("{"dynamic{ref}template" bar=baz animal="cat"/}"#), + Ok(( + "", + DustTag::DTPartial(Partial { + name: vec![ + PartialNameElement::PNSpan { + contents: "dynamic".to_owned() + }, + PartialNameElement::PNReference { + path: vec!["ref".to_owned()], + filters: Vec::new() + }, + PartialNameElement::PNSpan { + contents: "template".to_owned() + } + ], + explicit_context: None, + params: vec![ + KVPair { + key: "bar", + value: RValue::RVPath(Path { keys: vec!["baz"] }) + }, + KVPair { + key: "animal", + value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { + contents: "cat".to_owned() + }]) + } + ] + }) + )) + ); + } + + #[test] + fn test_unquoted_partial_with_explicit_context() { + assert_eq!( + dust_tag(r#"{>foo:foo.bar bar=baz animal="cat"/}"#), + Ok(( + "", + DustTag::DTPartial(Partial { + name: vec![PartialNameElement::PNSpan { + contents: "foo".to_owned() + },], + explicit_context: Some(Path { + keys: vec!["foo", "bar"] + }), + params: vec![ + KVPair { + key: "bar", + value: RValue::RVPath(Path { keys: vec!["baz"] }) + }, + KVPair { + key: "animal", + value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { + contents: "cat".to_owned() + }]) + } + ] + }) + )) + ); + } + + #[test] + fn test_quoted_partial_with_explicit_context() { + assert_eq!( + dust_tag( + r#"{>"template name * with * special \" characters":foo.bar bar=baz animal="cat"/}"# + ), + Ok(( + "", + DustTag::DTPartial(Partial { + name: vec![PartialNameElement::PNSpan { + contents: r#"template name * with * special " characters"#.to_owned() + },], + explicit_context: Some(Path { + keys: vec!["foo", "bar"] + }), + params: vec![ + KVPair { + key: "bar", + value: RValue::RVPath(Path { keys: vec!["baz"] }) + }, + KVPair { + key: "animal", + value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { + contents: "cat".to_owned() + }]) + } + ] + }) + )) + ); + } + + #[test] + fn test_literals() { + assert_eq!( + dust_tag(r#"{>foo a="foo" b=179 c=17.1 d=-12 e=-17.4/}"#), + Ok(( + "", + DustTag::DTPartial(Partial { + name: vec![PartialNameElement::PNSpan { + contents: "foo".to_owned() + },], + explicit_context: None, + params: vec![ + KVPair { + key: "a", + value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { + contents: "foo".to_owned() + }]) + }, + KVPair { + key: "b", + value: RValue::RVLiteral(OwnedLiteral::LPositiveInteger(179)) + }, + KVPair { + key: "c", + value: RValue::RVLiteral(OwnedLiteral::LFloat(17.1)) + }, + KVPair { + key: "d", + value: RValue::RVLiteral(OwnedLiteral::LNegativeInteger(-12)) + }, + KVPair { + key: "e", + value: RValue::RVLiteral(OwnedLiteral::LFloat(-17.4)) } ] }) @@ -838,7 +1397,8 @@ mod tests { Ok(( "", DustTag::DTHelperEquals(ParameterizedBlock { - name: "eq", + path: Path { keys: vec!["eq"] }, + explicit_context: None, params: vec![ KVPair { key: "key", @@ -846,7 +1406,9 @@ mod tests { }, KVPair { key: "value", - value: RValue::RVString("cat".to_owned()) + value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { + contents: "cat".to_owned() + }]) } ], contents: Some(Body { @@ -874,7 +1436,8 @@ mod tests { Ok(( "", DustTag::DTHelperEquals(ParameterizedBlock { - name: "eq", + path: Path { keys: vec!["eq"] }, + explicit_context: None, params: vec![ KVPair { key: "key", @@ -882,7 +1445,9 @@ mod tests { }, KVPair { key: "value", - value: RValue::RVString("cat".to_owned()) + value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { + contents: "cat".to_owned() + }]) } ], contents: None, @@ -891,4 +1456,232 @@ mod tests { )) ); } + + #[test] + fn test_helper_with_explicit_context() { + assert_eq!( + dust_tag(r#"{@eq:foo.bar key=name value="cat"}Pet the {name}!{/eq}"#), + Ok(( + "", + DustTag::DTHelperEquals(ParameterizedBlock { + path: Path { keys: vec!["eq"] }, + explicit_context: Some(Path { + keys: vec!["foo", "bar"] + }), + params: vec![ + KVPair { + key: "key", + value: RValue::RVPath(Path { keys: vec!["name"] }) + }, + KVPair { + key: "value", + value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { + contents: "cat".to_owned() + }]) + } + ], + contents: Some(Body { + elements: vec![ + TemplateElement::TESpan(Span { + contents: "Pet the " + }), + TemplateElement::TETag(DustTag::DTReference(Reference { + path: Path { keys: vec!["name"] }, + filters: Vec::new() + })), + TemplateElement::TESpan(Span { contents: "!" }) + ] + }), + else_contents: None + }) + )) + ); + } + + #[test] + fn test_self_closing_helper_with_explicit_context() { + assert_eq!( + dust_tag(r#"{@eq:foo.bar key=name value="cat"/}"#), + Ok(( + "", + DustTag::DTHelperEquals(ParameterizedBlock { + path: Path { keys: vec!["eq"] }, + explicit_context: Some(Path { + keys: vec!["foo", "bar"] + }), + params: vec![ + KVPair { + key: "key", + value: RValue::RVPath(Path { keys: vec!["name"] }) + }, + KVPair { + key: "value", + value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { + contents: "cat".to_owned() + }]) + } + ], + contents: None, + else_contents: None + }) + )) + ); + } + + #[test] + fn test_full_document_new_line_equality() { + assert_eq!( + super::template( + "- simple -{~n} +{#names}{.}{/names} +{~n}- new lines -{~n} +{#names} +{.} +{/names}" + ), + Ok::<_, nom::Err<(&str, ErrorKind)>>(( + "", + Template { + contents: Body { + elements: vec![ + TemplateElement::TESpan(Span { + contents: "- simple -" + }), + TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)), + TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( + "\n" + )), + TemplateElement::TETag(DustTag::DTSection(ParameterizedBlock { + path: Path { + keys: vec!["names"] + }, + explicit_context: None, + params: Vec::new(), + contents: Some(Body { + elements: vec![TemplateElement::TETag(DustTag::DTReference( + Reference { + path: Path { keys: vec!["."] }, + filters: vec![] + } + ))] + }), + else_contents: None, + })), + TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( + "\n" + )), + TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)), + TemplateElement::TESpan(Span { + contents: "- new lines -" + }), + TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)), + TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( + "\n" + )), + TemplateElement::TETag(DustTag::DTSection(ParameterizedBlock { + path: Path { + keys: vec!["names"] + }, + explicit_context: None, + params: Vec::new(), + contents: Some(Body { + elements: vec![ + TemplateElement::TEIgnoredWhitespace( + IgnoredWhitespace::StartOfLine("\n") + ), + TemplateElement::TETag(DustTag::DTReference(Reference { + path: Path { keys: vec!["."] }, + filters: vec![] + })), + TemplateElement::TEIgnoredWhitespace( + IgnoredWhitespace::StartOfLine("\n") + ) + ] + }), + else_contents: None, + })), + ] + } + } + )) + ); + } + + #[test] + fn test_full_document_parameterized_partial() { + assert_eq!( + super::template( + r#"{#level3.level4}{>partialtwo v1="b" v2="b" v3="b" v4="b" v5="b" /}{/level3.level4}"# + ), + Ok::<_, nom::Err<(&str, ErrorKind)>>(( + "", + Template { + contents: Body { + elements: vec![TemplateElement::TETag(DustTag::DTSection( + ParameterizedBlock { + path: Path { + keys: vec!["level3", "level4"] + }, + explicit_context: None, + params: Vec::new(), + contents: Some(Body { + elements: vec![TemplateElement::TETag(DustTag::DTPartial( + Partial { + name: vec![PartialNameElement::PNSpan { + contents: "partialtwo".to_owned() + },], + explicit_context: None, + params: vec![ + KVPair { + key: "v1", + value: RValue::RVTemplate(vec![ + PartialNameElement::PNSpan { + contents: "b".to_owned() + } + ]) + }, + KVPair { + key: "v2", + value: RValue::RVTemplate(vec![ + PartialNameElement::PNSpan { + contents: "b".to_owned() + } + ]) + }, + KVPair { + key: "v3", + value: RValue::RVTemplate(vec![ + PartialNameElement::PNSpan { + contents: "b".to_owned() + } + ]) + }, + KVPair { + key: "v4", + value: RValue::RVTemplate(vec![ + PartialNameElement::PNSpan { + contents: "b".to_owned() + } + ]) + }, + KVPair { + key: "v5", + value: RValue::RVTemplate(vec![ + PartialNameElement::PNSpan { + contents: "b".to_owned() + } + ]) + } + ] + } + ))] + }), + else_contents: None + } + ))] + } + } + )) + ); + } } diff --git a/src/renderer/breadcrumb_tree.rs b/src/renderer/breadcrumb_tree.rs new file mode 100644 index 0000000..dcf25ba --- /dev/null +++ b/src/renderer/breadcrumb_tree.rs @@ -0,0 +1,57 @@ +use crate::renderer::context_element::IceResult; +use crate::renderer::context_element::IntoContextElement; +use std::borrow::Borrow; +use std::rc::Rc; + +#[derive(Clone, Debug)] +pub enum BreadcrumbTreeElement<'a> { + // Using Rc so that when we need to create BreadcrumbTrees with + // the same BreadcrumbTreeElement but a different parent (for + // example, when inserting behind the tail), we don't need to the + // copy the already owned/malloc'd data. + Owned(Rc), + Borrowed(&'a dyn IntoContextElement), +} + +impl<'a> BreadcrumbTreeElement<'a> { + pub fn from_owned(val: I) -> BreadcrumbTreeElement<'a> { + BreadcrumbTreeElement::Owned(Rc::new(val)) + } + + pub fn from_borrowed(val: &'a dyn IntoContextElement) -> BreadcrumbTreeElement<'a> { + BreadcrumbTreeElement::Borrowed(val) + } +} + +impl<'a> From<&'a IceResult<'a>> for BreadcrumbTreeElement<'a> { + fn from(inp: &'a IceResult<'a>) -> Self { + match inp { + IceResult::Owned(rc_ce) => { + BreadcrumbTreeElement::from_borrowed(rc_ce.from_context_element()) + } + IceResult::Borrowed(ce) => { + BreadcrumbTreeElement::from_borrowed(ce.from_context_element()) + } + } + } +} + +impl<'a> From> for BreadcrumbTreeElement<'a> { + fn from(inp: IceResult<'a>) -> Self { + match inp { + IceResult::Owned(rc_ce) => BreadcrumbTreeElement::Owned(rc_ce.into_rc_ice()), + IceResult::Borrowed(ce) => { + BreadcrumbTreeElement::from_borrowed(ce.from_context_element()) + } + } + } +} + +impl<'a> Borrow for BreadcrumbTreeElement<'a> { + fn borrow(&self) -> &(dyn IntoContextElement + 'a) { + match self { + BreadcrumbTreeElement::Owned(ice) => ice.as_ref(), + BreadcrumbTreeElement::Borrowed(ice) => *ice, + } + } +} diff --git a/src/renderer/comparison_number.rs b/src/renderer/comparison_number.rs new file mode 100644 index 0000000..d85307f --- /dev/null +++ b/src/renderer/comparison_number.rs @@ -0,0 +1,89 @@ +use std::cmp::Ordering; + +#[derive(Debug)] +pub enum ComparisonNumber { + UnsignedInteger(u64), + SignedInteger(i64), + Decimal(f64), + Failure, +} + +/// Compare json numbers +/// +/// While this function can be called with two strings, it would not +/// make sense because javascript does not use numeric comparisons for +/// strings +pub fn compare_json_numbers(self_input: S, other_input: O) -> Option +where + S: Into, + O: Into, +{ + let self_number: ComparisonNumber = self_input.into(); + let other_number: ComparisonNumber = other_input.into(); + match (self_number, other_number) { + (ComparisonNumber::Failure, _) => return None, + (_, ComparisonNumber::Failure) => return None, + ( + ComparisonNumber::UnsignedInteger(self_num), + ComparisonNumber::UnsignedInteger(other_num), + ) => return self_num.partial_cmp(&other_num), + ( + ComparisonNumber::UnsignedInteger(self_num), + ComparisonNumber::SignedInteger(other_num), + ) => { + if self_num < std::i64::MAX as u64 { + return (self_num as i64).partial_cmp(&other_num); + } else { + return Some(Ordering::Greater); + } + } + (ComparisonNumber::UnsignedInteger(self_num), ComparisonNumber::Decimal(other_num)) => { + return (self_num as f64).partial_cmp(&other_num) + } + + ( + ComparisonNumber::SignedInteger(self_num), + ComparisonNumber::UnsignedInteger(other_num), + ) => { + if other_num < std::i64::MAX as u64 { + return self_num.partial_cmp(&(other_num as i64)); + } else { + return Some(Ordering::Less); + } + } + (ComparisonNumber::SignedInteger(self_num), ComparisonNumber::SignedInteger(other_num)) => { + return self_num.partial_cmp(&other_num) + } + (ComparisonNumber::SignedInteger(self_num), ComparisonNumber::Decimal(other_num)) => { + return (self_num as f64).partial_cmp(&other_num) + } + + (ComparisonNumber::Decimal(self_num), ComparisonNumber::UnsignedInteger(other_num)) => { + return self_num.partial_cmp(&(other_num as f64)) + } + (ComparisonNumber::Decimal(self_num), ComparisonNumber::SignedInteger(other_num)) => { + return self_num.partial_cmp(&(other_num as f64)) + } + (ComparisonNumber::Decimal(self_num), ComparisonNumber::Decimal(other_num)) => { + return self_num.partial_cmp(&other_num) + } + } +} + +impl From<&String> for ComparisonNumber { + fn from(original: &String) -> Self { + match original.parse::() { + Ok(num) => return ComparisonNumber::UnsignedInteger(num), + Err(_) => (), + }; + match original.parse::() { + Ok(num) => return ComparisonNumber::SignedInteger(num), + Err(_) => (), + }; + match original.parse::() { + Ok(num) => return ComparisonNumber::Decimal(num), + Err(_) => (), + }; + ComparisonNumber::Failure + } +} diff --git a/src/renderer/context_element.rs b/src/renderer/context_element.rs new file mode 100644 index 0000000..0e4779c --- /dev/null +++ b/src/renderer/context_element.rs @@ -0,0 +1,242 @@ +use crate::parser::Filter; +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; +use crate::renderer::errors::RenderError; +use crate::renderer::errors::WalkError; +use crate::renderer::DustRenderer; +use std::any::Any; +use std::ops::Add; +use std::ops::Div; +use std::ops::Mul; +use std::ops::Rem; +use std::ops::Sub; +use std::rc::Rc; +use std::{cmp::Ordering, fmt::Debug}; + +pub trait ContextElement: + Debug + + Truthiness + + Walkable + + Renderable + + Loopable + + CompareContextElement + + FromContextElement + + IntoRcIce + + Castable + + Sizable +{ +} + +pub trait Truthiness { + fn is_truthy(&self) -> bool; +} + +pub trait Walkable { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError>; + + /// If an element contains meta information and should not be + /// returned as the final result of a walk, this function should + /// return true. + /// + /// For example, the iteration context contains $idx and $len but + /// it should not be the result of a dot-reference like `{.}`. + fn is_pseudo_element(&self) -> bool { + false + } +} + +pub trait Renderable { + fn render(&self, filters: &Vec) -> Result; +} + +pub trait Loopable { + /// Return the elements for a Dust section + /// + /// Sections in dust are accomplished with the {#path} syntax. If + /// its an array-like value then it will render n-times, once for + /// each element of the array. If this is a scalar value, then + /// return an empty array. Sections with scalar values will still + /// be rendered (only once) if their truthiness check comes back + /// true. + fn get_loop_elements(&self) -> Vec<&dyn ContextElement>; +} + +pub trait Castable { + fn cast_to_type<'a>(&'a self, target: &str) -> Option>; +} + +pub trait Sizable { + /// Special case: In DustJS the @size helper usually attempts to + /// cast to a number before calculating the size. The exception to + /// this is booleans. `Number(true) == 1` but `@size` on any + /// boolean is always 0. Make this function return false for any + /// type that casting to a number shouldn't be attempted. + /// + /// Note: Its fine for objects that cannot be cast to a number to + /// return true here. False is only needed for cases where casting + /// to a number would cause a deviation in the final value for + /// `@size`. + fn is_castable(&self) -> bool; + + fn get_size<'a>(&'a self) -> Option>; +} + +pub trait CastToAny { + fn to_any(&self) -> &dyn Any; +} + +pub trait CompareContextElement: CastToAny { + fn equals(&self, other: &dyn ContextElement) -> bool; + + fn partial_compare(&self, other: &dyn ContextElement) -> Option; + + fn math_add<'a>(&self, other: &dyn ContextElement) -> Option>; + fn math_subtract<'a>(&self, other: &dyn ContextElement) -> Option>; + fn math_multiply<'a>(&self, other: &dyn ContextElement) -> Option>; + fn math_divide<'a>(&self, other: &dyn ContextElement) -> Option>; + fn math_modulus<'a>(&self, other: &dyn ContextElement) -> Option>; + fn math_abs<'a>(&self) -> Option>; + fn math_floor<'a>(&self) -> Option>; + fn math_ceil<'a>(&self) -> Option>; +} + +impl CastToAny for C { + fn to_any(&self) -> &dyn Any { + self + } +} + +impl<'a, 'b> PartialEq<&'b dyn ContextElement> for &'a dyn ContextElement { + fn eq(&self, other: &&'b dyn ContextElement) -> bool { + self.equals(*other) + } +} + +impl<'a, 'b> PartialOrd<&'b dyn ContextElement> for &'a dyn ContextElement { + fn partial_cmp(&self, other: &&'b dyn ContextElement) -> Option { + self.partial_compare(*other) + } +} + +impl<'a> Add<&'a dyn ContextElement> for &'a dyn ContextElement { + type Output = Option>; + + fn add(self, other: &'a dyn ContextElement) -> Self::Output { + self.math_add(other) + } +} + +impl<'a> Sub<&'a dyn ContextElement> for &'a dyn ContextElement { + type Output = Option>; + + fn sub(self, other: &'a dyn ContextElement) -> Self::Output { + self.math_subtract(other) + } +} + +impl<'a> Mul<&'a dyn ContextElement> for &'a dyn ContextElement { + type Output = Option>; + + fn mul(self, other: &'a dyn ContextElement) -> Self::Output { + self.math_multiply(other) + } +} + +impl<'a> Div<&'a dyn ContextElement> for &'a dyn ContextElement { + type Output = Option>; + + fn div(self, other: &'a dyn ContextElement) -> Self::Output { + self.math_divide(other) + } +} + +impl<'a> Rem<&'a dyn ContextElement> for &'a dyn ContextElement { + type Output = Option>; + + fn rem(self, other: &'a dyn ContextElement) -> Self::Output { + self.math_modulus(other) + } +} + +pub trait FromContextElement { + fn from_context_element(&self) -> &dyn IntoContextElement; +} + +impl FromContextElement for C { + fn from_context_element(&self) -> &dyn IntoContextElement { + self + } +} + +pub trait IntoContextElement: Debug + Walkable { + fn into_context_element<'a>( + &'a self, + renderer: &DustRenderer, + breadcrumbs: &'a Vec>, + ) -> Option>; +} + +impl IntoContextElement for C { + fn into_context_element<'a>( + &'a self, + _renderer: &DustRenderer, + _breadcrumbs: &'a Vec>, + ) -> Option> { + Some(IceResult::from_borrowed(self)) + } +} + +pub trait IntoRcIce { + fn into_rc_ice(self: Rc) -> Rc; +} + +impl IntoRcIce for C { + fn into_rc_ice(self: Rc) -> Rc { + Rc::clone(&self) as Rc + } +} + +#[derive(Clone, Debug)] +pub enum IceResult<'a> { + // Using Rc so that when we need to create BreadcrumbTrees with + // the same BreadcrumbTreeElement but a different parent (for + // example, when inserting behind the tail), we don't need to the + // copy the already owned/malloc'd data. + Owned(Rc), + Borrowed(&'a dyn ContextElement), +} + +impl<'a> IceResult<'a> { + pub fn from_owned(val: C) -> IceResult<'a> { + IceResult::Owned(Rc::new(val)) + } + + pub fn from_borrowed(val: &'a dyn ContextElement) -> IceResult<'a> { + IceResult::Borrowed(val) + } + + pub fn get_context_element_reference(&self) -> &dyn ContextElement { + match self { + IceResult::Owned(rc_ce) => rc_ce.as_ref(), + IceResult::Borrowed(ce) => *ce, + } + } +} + +impl<'a> IntoContextElement for IceResult<'a> { + fn into_context_element<'b>( + &'b self, + _renderer: &DustRenderer, + _breadcrumbs: &'b Vec>, + ) -> Option> { + match self { + IceResult::Owned(rc_ce) => Some(IceResult::from_borrowed(rc_ce.as_ref())), + IceResult::Borrowed(ce) => Some(IceResult::from_borrowed(*ce)), + } + } +} + +impl<'a> Walkable for IceResult<'a> { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + self.get_context_element_reference().walk(segment) + } +} diff --git a/src/renderer/errors.rs b/src/renderer/errors.rs new file mode 100644 index 0000000..8c458d7 --- /dev/null +++ b/src/renderer/errors.rs @@ -0,0 +1,100 @@ +use std::error; +use std::fmt; + +/// Fatal errors while rendering. +/// +/// A RenderError will halt rendering. +#[derive(PartialEq)] +pub enum RenderError { + Generic(String), + TemplateNotFound(String), + InvalidJson(String), +} + +#[derive(PartialEq)] +pub enum WalkError { + CantWalk, +} + +#[derive(Clone)] +pub struct CompileError { + pub message: String, +} + +impl fmt::Display for RenderError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RenderError::Generic(msg) => write!(f, "{}", msg), + RenderError::TemplateNotFound(name) => { + write!(f, "No template named {} in context", name) + } + RenderError::InvalidJson(invalid_json) => write!( + f, + "Attempted to parse the following invalid JSON: {}", + invalid_json + ), + } + } +} + +impl fmt::Debug for RenderError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RenderError::Generic(msg) => write!(f, "{}", msg), + RenderError::TemplateNotFound(name) => { + write!(f, "No template named {} in context", name) + } + RenderError::InvalidJson(invalid_json) => write!( + f, + "Attempted to parse the following invalid JSON: {}", + invalid_json + ), + } + } +} + +impl error::Error for RenderError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} + +impl fmt::Display for WalkError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + WalkError::CantWalk => write!(f, "Failed to walk"), + } + } +} + +impl fmt::Debug for WalkError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + WalkError::CantWalk => write!(f, "Failed to walk"), + } + } +} + +impl error::Error for WalkError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} + +impl fmt::Display for CompileError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Error compiling: {}", self.message) + } +} + +impl fmt::Debug for CompileError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Error compiling: {}", self.message) + } +} + +impl error::Error for CompileError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} diff --git a/src/renderer/inline_partial_tree.rs b/src/renderer/inline_partial_tree.rs new file mode 100644 index 0000000..fffe0a8 --- /dev/null +++ b/src/renderer/inline_partial_tree.rs @@ -0,0 +1,128 @@ +use crate::parser::Body; +use crate::parser::DustTag; +use crate::parser::Template; +use crate::parser::TemplateElement; +use std::collections::HashMap; + +pub struct InlinePartialTreeElement<'a> { + parent: Option<&'a InlinePartialTreeElement<'a>>, + blocks: HashMap<&'a str, &'a Option>>, +} + +impl<'a> InlinePartialTreeElement<'a> { + pub fn new( + parent: Option<&'a InlinePartialTreeElement<'a>>, + blocks: HashMap<&'a str, &'a Option>>, + ) -> InlinePartialTreeElement<'a> { + InlinePartialTreeElement { + parent: parent, + blocks: blocks, + } + } + + pub fn get_block(&self, name: &str) -> Option<&'a Option>> { + match self.blocks.get(name) { + None => match self.parent { + None => None, + Some(parent_tree_element) => parent_tree_element.get_block(name), + }, + Some(interior) => Some(interior), + } + } +} + +pub fn extract_inline_partials<'a>( + template: &'a Template<'a>, +) -> HashMap<&'a str, &'a Option>> { + let mut blocks: HashMap<&'a str, &'a Option>> = HashMap::new(); + + extract_inline_partials_from_body(&mut blocks, &template.contents); + + blocks +} + +fn extract_inline_partials_from_body<'a, 'b>( + blocks: &'b mut HashMap<&'a str, &'a Option>>, + body: &'a Body<'a>, +) { + for elem in &body.elements { + match elem { + TemplateElement::TEIgnoredWhitespace(_) => (), + TemplateElement::TESpan(_span) => (), + TemplateElement::TETag(dt) => { + extract_inline_partials_from_tag(blocks, dt); + } + } + } +} + +fn extract_inline_partials_from_tag<'a, 'b>( + blocks: &'b mut HashMap<&'a str, &'a Option>>, + tag: &'a DustTag, +) { + match tag { + DustTag::DTComment(..) => (), + DustTag::DTSpecial(..) => (), + DustTag::DTLiteralStringBlock(..) => (), + DustTag::DTReference(..) => (), + DustTag::DTSection(container) => { + match &container.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match &container.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + DustTag::DTExists(container) => { + match &container.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match &container.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + DustTag::DTNotExists(container) => { + match &container.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match &container.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + DustTag::DTPartial(..) => (), + DustTag::DTInlinePartial(named_block) => { + blocks.insert(&named_block.path.keys[0], &named_block.contents); + } + DustTag::DTBlock(..) => (), + DustTag::DTHelperEquals(parameterized_block) + | DustTag::DTHelperNotEquals(parameterized_block) + | DustTag::DTHelperGreaterThan(parameterized_block) + | DustTag::DTHelperLessThan(parameterized_block) + | DustTag::DTHelperGreaterThanOrEquals(parameterized_block) + | DustTag::DTHelperLessThanOrEquals(parameterized_block) + | DustTag::DTHelperSep(parameterized_block) + | DustTag::DTHelperFirst(parameterized_block) + | DustTag::DTHelperLast(parameterized_block) + | DustTag::DTHelperSelect(parameterized_block) + | DustTag::DTHelperAny(parameterized_block) + | DustTag::DTHelperNone(parameterized_block) + | DustTag::DTHelperMath(parameterized_block) + | DustTag::DTHelperSize(parameterized_block) + | DustTag::DTHelperContextDump(parameterized_block) => { + match ¶meterized_block.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match ¶meterized_block.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + } +} diff --git a/src/renderer/iteration_context.rs b/src/renderer/iteration_context.rs new file mode 100644 index 0000000..0289678 --- /dev/null +++ b/src/renderer/iteration_context.rs @@ -0,0 +1,52 @@ +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; +use crate::renderer::context_element::IceResult; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::DustRenderer; +use crate::renderer::WalkError; +use crate::{parser::OwnedLiteral, renderer::Walkable}; +use std::convert::TryInto; + +/// An injected context for $idx and $len +/// +/// Functions the same as the injected parameters contexts for +/// helpers/partials with parameters but this has no need for storing +/// breadcrumbs since its simply storing two integers. +#[derive(Debug)] +pub struct IterationContext { + idx: OwnedLiteral, + len: OwnedLiteral, +} + +impl IterationContext { + pub fn new(idx: usize, len: usize) -> Self { + // TODO: it would be nice to handle usize vs u64 better + IterationContext { + idx: OwnedLiteral::LPositiveInteger(idx.try_into().unwrap()), + len: OwnedLiteral::LPositiveInteger(len.try_into().unwrap()), + } + } +} + +impl IntoContextElement for IterationContext { + fn into_context_element<'b>( + &'b self, + _renderer: &DustRenderer, + _breadcrumbs: &'b Vec>, + ) -> Option> { + panic!("into_context_element cannot be called on pseudo elements"); + } +} + +impl Walkable for IterationContext { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + match segment { + "$idx" => Ok(&self.idx), + "$len" => Ok(&self.len), + _ => Err(WalkError::CantWalk), + } + } + + fn is_pseudo_element(&self) -> bool { + true + } +} diff --git a/src/renderer/math.rs b/src/renderer/math.rs new file mode 100644 index 0000000..dd972e8 --- /dev/null +++ b/src/renderer/math.rs @@ -0,0 +1,194 @@ +use crate::parser::OwnedLiteral; +use std::convert::TryFrom; +use std::convert::TryInto; +use std::ops::Add; +use std::ops::Div; +use std::ops::Mul; +use std::ops::Rem; +use std::ops::Sub; + +#[derive(Debug)] +pub enum MathNumber { + Integer(i128), + Decimal(f64), + Failure, +} + +impl From<&OwnedLiteral> for MathNumber { + fn from(original: &OwnedLiteral) -> Self { + match original { + OwnedLiteral::LString(_) => panic!("Strings should not be cast to numbers for math"), + OwnedLiteral::LBoolean(boolean) => { + if *boolean { + MathNumber::Integer(1) + } else { + MathNumber::Integer(0) + } + } + OwnedLiteral::LPositiveInteger(num) => { + return MathNumber::Integer((*num).try_into().unwrap()) + } + OwnedLiteral::LNegativeInteger(num) => return MathNumber::Integer((*num).into()), + OwnedLiteral::LFloat(num) => return MathNumber::Decimal(*num), + } + } +} + +impl Add for MathNumber { + type Output = Option; + + fn add(self, other: MathNumber) -> Self::Output { + match (self, other) { + (MathNumber::Failure, _) | (_, MathNumber::Failure) => None, + (MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => { + math_ints(self_num, other_num, std::ops::Add::add) + } + (MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat(self_num + other_num)) + } + (MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat((self_num as f64) + other_num)) + } + (MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => { + Some(OwnedLiteral::LFloat(self_num + (other_num as f64))) + } + } + } +} + +impl Sub for MathNumber { + type Output = Option; + + fn sub(self, other: MathNumber) -> Self::Output { + match (self, other) { + (MathNumber::Failure, _) | (_, MathNumber::Failure) => None, + (MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => { + math_ints(self_num, other_num, std::ops::Sub::sub) + } + (MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat(self_num - other_num)) + } + (MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat((self_num as f64) - other_num)) + } + (MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => { + Some(OwnedLiteral::LFloat(self_num - (other_num as f64))) + } + } + } +} + +impl Mul for MathNumber { + type Output = Option; + + fn mul(self, other: MathNumber) -> Self::Output { + match (self, other) { + (MathNumber::Failure, _) | (_, MathNumber::Failure) => None, + (MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => { + math_ints(self_num, other_num, std::ops::Mul::mul) + } + (MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat(self_num * other_num)) + } + (MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat((self_num as f64) * other_num)) + } + (MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => { + Some(OwnedLiteral::LFloat(self_num * (other_num as f64))) + } + } + } +} + +impl Div for MathNumber { + type Output = Option; + + fn div(self, other: MathNumber) -> Self::Output { + match (self, other) { + (MathNumber::Failure, _) | (_, MathNumber::Failure) => None, + (MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => { + Some(OwnedLiteral::LFloat((self_num as f64) / (other_num as f64))) + } + (MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat(self_num / other_num)) + } + (MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat((self_num as f64) / other_num)) + } + (MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => { + Some(OwnedLiteral::LFloat(self_num / (other_num as f64))) + } + } + } +} + +impl Rem for MathNumber { + type Output = Option; + + fn rem(self, other: MathNumber) -> Self::Output { + match (self, other) { + (MathNumber::Failure, _) | (_, MathNumber::Failure) => None, + (MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => { + math_ints(self_num, other_num, std::ops::Rem::rem) + } + (MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat(self_num % other_num)) + } + (MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => { + Some(OwnedLiteral::LFloat((self_num as f64) % other_num)) + } + (MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => { + Some(OwnedLiteral::LFloat(self_num % (other_num as f64))) + } + } + } +} + +impl MathNumber { + pub fn math_abs(&self) -> Option { + match self { + MathNumber::Failure => None, + MathNumber::Integer(num) => num.abs().try_into().ok(), + MathNumber::Decimal(num) => Some(OwnedLiteral::LFloat(num.abs())), + } + } + pub fn math_floor(&self) -> Option { + match self { + MathNumber::Failure => None, + MathNumber::Integer(num) => (*num).try_into().ok(), + MathNumber::Decimal(num) => Some(OwnedLiteral::LFloat(num.floor())), + } + } + pub fn math_ceil(&self) -> Option { + match self { + MathNumber::Failure => None, + MathNumber::Integer(num) => (*num).try_into().ok(), + MathNumber::Decimal(num) => Some(OwnedLiteral::LFloat(num.ceil())), + } + } +} + +/// For math operations that take in integers and return integers +/// (add, subtract, multiply) +pub fn math_ints(left: L, right: R, operation: F) -> Option +where + L: Into, + R: Into, + F: Fn(i128, i128) -> i128, +{ + operation(left.into(), right.into()).try_into().ok() +} + +impl TryFrom for OwnedLiteral { + type Error = &'static str; + + fn try_from(original: i128) -> Result { + std::convert::TryInto::::try_into(original) + .map(OwnedLiteral::LPositiveInteger) + .ok() + .or(std::convert::TryInto::::try_into(original) + .map(OwnedLiteral::LNegativeInteger) + .ok()) + .ok_or("Value does not fit into either u64 or i64") + } +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs new file mode 100644 index 0000000..739eede --- /dev/null +++ b/src/renderer/mod.rs @@ -0,0 +1,33 @@ +//! This module contains a renderer for a rust implementation of LinkedIn Dust + +mod breadcrumb_tree; +mod comparison_number; +mod context_element; +mod errors; +mod inline_partial_tree; +mod iteration_context; +mod math; +mod parameters_context; +mod renderer; +mod select_context; +mod walking; + +pub use comparison_number::compare_json_numbers; +pub use comparison_number::ComparisonNumber; +pub use context_element::Castable; +pub use context_element::CompareContextElement; +pub use context_element::ContextElement; +pub use context_element::IceResult; +pub use context_element::IntoContextElement; +pub use context_element::Loopable; +pub use context_element::Renderable; +pub use context_element::Sizable; +pub use context_element::Truthiness; +pub use context_element::Walkable; +pub use errors::CompileError; +pub use errors::RenderError; +pub use errors::WalkError; +pub use math::MathNumber; +pub use renderer::compile_template; +pub use renderer::DustRenderer; +pub use select_context::SelectContext; diff --git a/src/renderer/parameters_context.rs b/src/renderer/parameters_context.rs new file mode 100644 index 0000000..a03535c --- /dev/null +++ b/src/renderer/parameters_context.rs @@ -0,0 +1,509 @@ +use crate::parser::Filter; +use crate::parser::KVPair; +use crate::parser::OwnedLiteral; +use crate::parser::RValue; +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; +use crate::renderer::comparison_number::compare_json_numbers; +use crate::renderer::comparison_number::ComparisonNumber; +use crate::renderer::context_element::CompareContextElement; +use crate::renderer::context_element::ContextElement; +use crate::renderer::context_element::IceResult; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::math::MathNumber; +use crate::renderer::walking::walk_path; +use crate::renderer::Castable; +use crate::renderer::DustRenderer; +use crate::renderer::Loopable; +use crate::renderer::RenderError; +use crate::renderer::Renderable; +use crate::renderer::Sizable; +use crate::renderer::Truthiness; +use crate::renderer::WalkError; +use crate::renderer::Walkable; +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::convert::TryInto; + +#[derive(Debug)] +pub struct ParametersContext<'a> { + parent: Option<&'a ParametersContext<'a>>, + params: HashMap<&'a str, (&'a RValue<'a>, Option>)>, +} + +impl<'a> ParametersContext<'a> { + pub fn new( + renderer: &DustRenderer, + breadcrumbs: &'a Vec>, + params: &'a Vec, + parent: Option<&'a ParametersContext<'a>>, + ) -> Self { + // If the parameter is a Path, then we resolve it immediately + // to a context element because those are resolved using the + // breadcrumbs at the time of assignment. + // + // If the parameter is a template (for example `foo="{bar}"`) + // then those are resolved at the time of access rather than + // the time of assignment, so we leave them into their + // original IntoContextElement state. + let rendered_params: HashMap<&'a str, (&'a RValue<'a>, Option>)> = + params + .iter() + .map(|kvpair| { + let k = kvpair.key; + let v: Option> = match &kvpair.value { + RValue::RVLiteral(_owned_literal) => { + Some(BreadcrumbTreeElement::from_borrowed(&kvpair.value)) + } + RValue::RVPath(_path) => kvpair + .value + .into_context_element(renderer, breadcrumbs) + .map(std::convert::From::from), + RValue::RVTemplate(_template) => { + Some(BreadcrumbTreeElement::from_borrowed(&kvpair.value)) + } + }; + (k, (&kvpair.value, v)) + }) + .collect(); + + ParametersContext { + parent: parent, + params: rendered_params, + } + } + + pub fn from_values( + parent: Option<&'a ParametersContext<'a>>, + params: HashMap<&'a str, (&'a RValue<'a>, Option>)>, + ) -> Self { + ParametersContext { + parent: parent, + params: params, + } + } + + pub fn contains_key(&self, segment: &str) -> bool { + self.params.contains_key(segment) + || self + .parent + .map(|p| p.contains_key(segment)) + .unwrap_or(false) + } + + pub fn get_original_rvalue(&self, segment: &str) -> Option<&'a RValue<'a>> { + self.params + .get(segment) + .map(|(rvalue, _bte)| *rvalue) + .or_else(|| { + self.parent + .map(|p| p.get_original_rvalue(segment)) + .flatten() + }) + } +} + +impl<'a> IntoContextElement for ParametersContext<'a> { + fn into_context_element<'b>( + &'b self, + _renderer: &DustRenderer, + _breadcrumbs: &'b Vec>, + ) -> Option> { + panic!("into_context_element cannot be called on pseudo elements"); + } +} + +impl<'a> Walkable for ParametersContext<'a> { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + match self.params.get(segment).map(|(_rvalue, bte)| bte) { + Some(Some(bte)) => Ok(bte.borrow()), + Some(None) => Err(WalkError::CantWalk), + None => self + .parent + .map(|p| p.walk(segment)) + .unwrap_or(Err(WalkError::CantWalk)), + } + } + + fn is_pseudo_element(&self) -> bool { + true + } +} + +impl<'a> IntoContextElement for RValue<'a> { + fn into_context_element<'b>( + &'b self, + renderer: &DustRenderer, + breadcrumbs: &'b Vec>, + ) -> Option> { + match self { + RValue::RVLiteral(owned_literal) => Some(IceResult::from_borrowed(owned_literal)), + RValue::RVPath(path) => walk_path(breadcrumbs, &path.keys) + .map(|ice| ice.into_context_element(renderer, breadcrumbs)) + .ok() + .flatten(), + RValue::RVTemplate(template) => renderer + .render_partial_name(template, breadcrumbs) + .map(|rendered| OwnedLiteral::LString(rendered)) + .ok() + .map(|owned_literal| IceResult::from_owned(owned_literal)), + } + } +} + +impl<'a> Walkable for RValue<'a> { + fn walk(&self, _segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + Err(WalkError::CantWalk) + } +} + +impl ContextElement for OwnedLiteral {} + +impl Truthiness for OwnedLiteral { + fn is_truthy(&self) -> bool { + match self { + OwnedLiteral::LBoolean(boolean) => *boolean, + OwnedLiteral::LString(text) => !text.is_empty(), + OwnedLiteral::LPositiveInteger(_num) => true, + OwnedLiteral::LNegativeInteger(_num) => true, + OwnedLiteral::LFloat(_num) => true, + } + } +} + +impl Renderable for OwnedLiteral { + fn render(&self, _filters: &Vec) -> Result { + match self { + OwnedLiteral::LBoolean(boolean) => Ok(boolean.to_string()), + OwnedLiteral::LString(text) => Ok(text.clone()), + OwnedLiteral::LPositiveInteger(num) => Ok(num.to_string()), + OwnedLiteral::LNegativeInteger(num) => Ok(num.to_string()), + OwnedLiteral::LFloat(num) => Ok(num.to_string()), + } + } +} + +impl Loopable for OwnedLiteral { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + Vec::new() + } +} + +impl Walkable for OwnedLiteral { + fn walk(&self, _segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + Err(WalkError::CantWalk) + } +} + +impl Sizable for OwnedLiteral { + fn is_castable(&self) -> bool { + match self { + OwnedLiteral::LBoolean(_) => false, + OwnedLiteral::LFloat(_) => true, + OwnedLiteral::LPositiveInteger(_) => true, + OwnedLiteral::LNegativeInteger(_) => true, + OwnedLiteral::LString(_) => true, + } + } + + fn get_size<'a>(&'a self) -> Option> { + match self { + OwnedLiteral::LBoolean(_) => { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) + } + OwnedLiteral::LFloat(_) => Some(IceResult::from_borrowed(self)), + OwnedLiteral::LPositiveInteger(_) => Some(IceResult::from_borrowed(self)), + OwnedLiteral::LNegativeInteger(_) => Some(IceResult::from_borrowed(self)), + OwnedLiteral::LString(text) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(text.len().try_into().unwrap()), + )), + } + } +} + +impl Castable for OwnedLiteral { + fn cast_to_type<'a>(&'a self, target: &str) -> Option> { + match (self, target) { + (OwnedLiteral::LString(text), "number") => text + .parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LPositiveInteger(num))) + .or_else(|_| { + text.parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LNegativeInteger(num))) + }) + .or_else(|_| { + text.parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LFloat(num))) + }) + .ok(), + (OwnedLiteral::LBoolean(boolean), "number") => { + if *boolean { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(1))) + } else { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) + } + } + (OwnedLiteral::LPositiveInteger(_), "number") => Some(IceResult::from_borrowed(self)), + (OwnedLiteral::LNegativeInteger(_), "number") => Some(IceResult::from_borrowed(self)), + (OwnedLiteral::LFloat(_), "number") => Some(IceResult::from_borrowed(self)), + + (OwnedLiteral::LString(_), "string") => Some(IceResult::from_borrowed(self)), + (OwnedLiteral::LBoolean(boolean), "string") => Some(IceResult::from_owned( + OwnedLiteral::LString(boolean.to_string()), + )), + (OwnedLiteral::LPositiveInteger(num), "string") => Some(IceResult::from_owned( + OwnedLiteral::LString(num.to_string()), + )), + (OwnedLiteral::LNegativeInteger(num), "string") => Some(IceResult::from_owned( + OwnedLiteral::LString(num.to_string()), + )), + (OwnedLiteral::LFloat(num), "string") => Some(IceResult::from_owned( + OwnedLiteral::LString(num.to_string()), + )), + + (OwnedLiteral::LString(text), "boolean") => { + if text.is_empty() { + Some(IceResult::from_owned(OwnedLiteral::LBoolean(false))) + } else { + Some(IceResult::from_owned(OwnedLiteral::LBoolean(true))) + } + } + (OwnedLiteral::LBoolean(_), "boolean") => Some(IceResult::from_borrowed(self)), + (OwnedLiteral::LPositiveInteger(num), "boolean") => { + if *num == 0 { + Some(IceResult::from_owned(OwnedLiteral::LBoolean(false))) + } else { + Some(IceResult::from_owned(OwnedLiteral::LBoolean(true))) + } + } + (OwnedLiteral::LNegativeInteger(num), "boolean") => { + if *num == 0 { + Some(IceResult::from_owned(OwnedLiteral::LBoolean(false))) + } else { + Some(IceResult::from_owned(OwnedLiteral::LBoolean(true))) + } + } + (OwnedLiteral::LFloat(num), "boolean") => { + if *num == 0.0 || num.is_nan() { + Some(IceResult::from_owned(OwnedLiteral::LBoolean(false))) + } else { + Some(IceResult::from_owned(OwnedLiteral::LBoolean(true))) + } + } + (_, _) => panic!("Unimplemented cast"), + } + } +} + +impl CompareContextElement for OwnedLiteral { + fn equals(&self, other: &dyn ContextElement) -> bool { + // println!("Literal equality check {:?} == {:?}", self, other); + // If its an OwnedLiteral then compare them directly, + // otherwise defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => other.equals(self), + Some(other_literal) => match (self, other_literal) { + (OwnedLiteral::LString(self_text), OwnedLiteral::LString(other_text)) => { + self_text == other_text + } + ( + OwnedLiteral::LPositiveInteger(self_num), + OwnedLiteral::LPositiveInteger(other_num), + ) => self_num == other_num, + ( + OwnedLiteral::LNegativeInteger(self_num), + OwnedLiteral::LNegativeInteger(other_num), + ) => self_num == other_num, + (OwnedLiteral::LBoolean(self_bool), OwnedLiteral::LBoolean(other_bool)) => { + self_bool == other_bool + } + (OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => false, + (OwnedLiteral::LBoolean(_), _) | (_, OwnedLiteral::LBoolean(_)) => false, + (OwnedLiteral::LFloat(self_num), OwnedLiteral::LFloat(other_num)) => { + self_num == other_num + } + (OwnedLiteral::LFloat(self_num), OwnedLiteral::LPositiveInteger(other_num)) => { + *self_num == (*other_num as f64) + } + (OwnedLiteral::LPositiveInteger(self_num), OwnedLiteral::LFloat(other_num)) => { + (*self_num as f64) == *other_num + } + (OwnedLiteral::LFloat(self_num), OwnedLiteral::LNegativeInteger(other_num)) => { + *self_num == (*other_num as f64) + } + (OwnedLiteral::LNegativeInteger(self_num), OwnedLiteral::LFloat(other_num)) => { + (*self_num as f64) == *other_num + } + ( + OwnedLiteral::LPositiveInteger(self_num), + OwnedLiteral::LNegativeInteger(other_num), + ) => { + if *self_num < std::i64::MAX as u64 { + (*self_num as i64) == *other_num + } else { + false + } + } + ( + OwnedLiteral::LNegativeInteger(self_num), + OwnedLiteral::LPositiveInteger(other_num), + ) => { + if *other_num < std::i64::MAX as u64 { + *self_num == (*other_num as i64) + } else { + false + } + } + }, + } + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // If its an OwnedLiteral then compare them directly, + // otherwise defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => match other.partial_compare(self) { + None => None, + Some(ord) => match ord { + Ordering::Equal => Some(Ordering::Equal), + Ordering::Greater => Some(Ordering::Less), + Ordering::Less => Some(Ordering::Greater), + }, + }, + Some(other_literal) => match (self, other_literal) { + // If they're both strings, compare them directly + (OwnedLiteral::LString(self_text), OwnedLiteral::LString(other_text)) => { + self_text.partial_cmp(other_text) + } + // Otherwise, convert to numbers and compare them that way + (_, _) => return compare_json_numbers(self, other_literal), + }, + } + } + + fn math_add<'a>(&self, other: &dyn ContextElement) -> Option> { + // If its an OwnedLiteral then add them directly, otherwise + // defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => other.math_add(self), + Some(other_literal) => match (self, other_literal) { + (OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None, + (_, _) => (std::convert::Into::::into(self) + + std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + }, + } + } + + fn math_subtract<'a>(&self, other: &dyn ContextElement) -> Option> { + // If its an OwnedLiteral then subtract them directly, + // otherwise defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => other.math_subtract(self), + Some(other_literal) => match (self, other_literal) { + (OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None, + (_, _) => (std::convert::Into::::into(self) + - std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + }, + } + } + + fn math_multiply<'a>(&self, other: &dyn ContextElement) -> Option> { + // If its an OwnedLiteral then multiply them directly, + // otherwise defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => other.math_multiply(self), + Some(other_literal) => match (self, other_literal) { + (OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None, + (_, _) => (std::convert::Into::::into(self) + * std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + }, + } + } + + fn math_divide<'a>(&self, other: &dyn ContextElement) -> Option> { + // If its an OwnedLiteral then divide them directly, otherwise + // defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => other.math_divide(self), + Some(other_literal) => match (self, other_literal) { + (OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None, + (_, _) => (std::convert::Into::::into(self) + / std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + }, + } + } + + fn math_modulus<'a>(&self, other: &dyn ContextElement) -> Option> { + // If its an OwnedLiteral then modulus them directly, + // otherwise defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => other.math_modulus(self), + Some(other_literal) => match (self, other_literal) { + (OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None, + (_, _) => (std::convert::Into::::into(self) + % std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + }, + } + } + + fn math_abs<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_abs() + .map(IceResult::from_owned) + } + + fn math_floor<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_floor() + .map(IceResult::from_owned) + } + + fn math_ceil<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_ceil() + .map(IceResult::from_owned) + } +} + +impl From<&OwnedLiteral> for ComparisonNumber { + fn from(original: &OwnedLiteral) -> Self { + match original { + OwnedLiteral::LBoolean(boolean) => { + if *boolean { + ComparisonNumber::UnsignedInteger(1) + } else { + ComparisonNumber::UnsignedInteger(0) + } + } + OwnedLiteral::LPositiveInteger(num) => ComparisonNumber::UnsignedInteger(*num), + OwnedLiteral::LNegativeInteger(num) => ComparisonNumber::SignedInteger(*num), + OwnedLiteral::LString(text) => text.into(), + OwnedLiteral::LFloat(num) => { + if num.is_nan() { + ComparisonNumber::Failure + } else { + ComparisonNumber::Decimal(*num) + } + } + } + } +} diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs new file mode 100644 index 0000000..023cd38 --- /dev/null +++ b/src/renderer/renderer.rs @@ -0,0 +1,1218 @@ +use crate::parser::template; +use crate::parser::Body; +use crate::parser::DustTag; +use crate::parser::Filter; +use crate::parser::OwnedLiteral; +use crate::parser::PartialNameElement; +use crate::parser::Path; +use crate::parser::RValue; +use crate::parser::Special; +use crate::parser::Template; +use crate::parser::TemplateElement; +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; +use crate::renderer::context_element::ContextElement; +use crate::renderer::context_element::IceResult; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::context_element::Walkable; +use crate::renderer::errors::CompileError; +use crate::renderer::errors::RenderError; +use crate::renderer::errors::WalkError; +use crate::renderer::inline_partial_tree::extract_inline_partials; +use crate::renderer::inline_partial_tree::InlinePartialTreeElement; +use crate::renderer::iteration_context::IterationContext; +use crate::renderer::parameters_context::ParametersContext; +use crate::renderer::select_context::SelectContext; +use crate::renderer::walking::walk_path; +use std::borrow::Borrow; +use std::collections::HashMap; + +#[derive(Clone, Debug)] +pub struct DustRenderer<'a> { + templates: HashMap>, +} + +pub fn compile_template<'a>(source: &'a str) -> Result, CompileError> { + let (_remaining, parsed_template) = template(source).map_err(|_err| CompileError { + message: "Failed to compile template".to_owned(), + })?; + Ok(parsed_template) +} + +impl<'a> DustRenderer<'a> { + pub fn new() -> DustRenderer<'a> { + DustRenderer { + templates: HashMap::new(), + } + } + + pub fn load_source(&mut self, template: &'a Template, name: String) { + self.templates.insert(name, template); + } + + pub fn render(&'a self, name: &str, context: Option<&C>) -> Result + where + C: IntoContextElement, + { + let breadcrumbs = context + .map(|ctx| vec![BreadcrumbTreeElement::from_borrowed(ctx)]) + .unwrap_or(Vec::new()); + self.render_template(name, breadcrumbs.as_ref(), None) + } + + pub fn render_template( + &'a self, + name: &str, + breadcrumbs: &'a Vec>, + blocks: Option<&'a InlinePartialTreeElement<'a>>, + ) -> Result { + let main_template = match self.templates.get(name) { + Some(tmpl) => tmpl, + None => { + return Err(RenderError::TemplateNotFound(name.to_owned())); + } + }; + let extracted_inline_partials = extract_inline_partials(main_template); + let new_blocks = InlinePartialTreeElement::new(blocks, extracted_inline_partials); + let new_block_context = BlockContext { + breadcrumbs: breadcrumbs, + blocks: &new_blocks, + }; + self.render_body( + &main_template.contents, + breadcrumbs, + &new_block_context, + &mut None, + ) + } + + fn render_maybe_body( + &'a self, + body: &'a Option, + breadcrumbs: &'a Vec>, + blocks: &'a BlockContext<'a>, + select_context: &mut Option>, + ) -> Result { + match body { + None => Ok("".to_owned()), + Some(body) => Ok(self.render_body(body, breadcrumbs, blocks, select_context)?), + } + } + + fn render_body( + &'a self, + body: &'a Body, + breadcrumbs: &'a Vec>, + blocks: &'a BlockContext<'a>, + select_context: &mut Option>, + ) -> Result { + let mut output = String::new(); + for elem in &body.elements { + match elem { + TemplateElement::TEIgnoredWhitespace(_) => {} + TemplateElement::TESpan(span) => output.push_str(span.contents), + TemplateElement::TETag(dt) => { + output.push_str(&self.render_tag(dt, breadcrumbs, blocks, select_context)?); + } + } + } + Ok(output) + } + + /// For rendering a dynamic partial's name or an rvalue template + pub fn render_partial_name( + &'a self, + body: &'a Vec, + breadcrumbs: &'a Vec>, + ) -> Result { + let converted_to_template_elements: Vec> = + body.into_iter().map(|e| e.into()).collect(); + // Simple templates like partial names and reference rvalues + // cannot contain blocks or inline partials, so we use a blank + // BlockContext. + let empty_block_context = BlockContext { + breadcrumbs: &Vec::new(), + blocks: &InlinePartialTreeElement::new(None, HashMap::new()), + }; + self.render_body( + &Body { + elements: converted_to_template_elements, + }, + breadcrumbs, + &empty_block_context, + &mut None, + ) + } + + fn render_tag( + &'a self, + tag: &'a DustTag, + breadcrumbs: &'a Vec>, + blocks: &'a BlockContext<'a>, + select_context: &mut Option>, + ) -> Result { + match tag { + DustTag::DTComment(_comment) => (), + DustTag::DTSpecial(special) => { + return Ok(match special { + Special::Space => " ", + Special::NewLine => "\n", + Special::CarriageReturn => "\r", + Special::LeftCurlyBrace => "{", + Special::RightCurlyBrace => "}", + } + .to_owned()) + } + DustTag::DTLiteralStringBlock(literal) => return Ok((*literal).to_owned()), + DustTag::DTReference(reference) => { + let val = walk_path(breadcrumbs, &reference.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match val { + Err(WalkError::CantWalk) | Ok(None) => return Ok("".to_owned()), + Ok(Some(final_val)) => { + return if final_val.get_context_element_reference().is_truthy() { + final_val + .get_context_element_reference() + .render(&Self::preprocess_filters(&reference.filters)) + } else { + Ok("".to_owned()) + }; + } + } + } + DustTag::DTSection(container) => { + let injected_context = + ParametersContext::new(self, breadcrumbs, &container.params, None); + let val = walk_path(breadcrumbs, &container.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match val { + Err(WalkError::CantWalk) | Ok(None) => { + let new_breadcrumbs = self.new_breadcrumbs_section( + breadcrumbs, + None, + Some(&injected_context), + &container.explicit_context, + None, + ); + return self.render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ); + } + Ok(Some(final_val)) => { + let context_element = final_val.get_context_element_reference(); + return if context_element.is_truthy() { + match &container.contents { + None => Ok("".to_owned()), + Some(body) => { + let loop_elements: Vec<&dyn ContextElement> = + context_element.get_loop_elements(); + if loop_elements.is_empty() { + // Scalar value + let new_breadcrumbs = self.new_breadcrumbs_section( + breadcrumbs, + None, + Some(&injected_context), + &container.explicit_context, + Some(context_element), + ); + self.render_body( + body, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ) + } else { + // Array-like value + let total_length = loop_elements.len(); + let rendered_results: Result, RenderError> = + loop_elements + .into_iter() + .enumerate() + .map(|(i, array_elem)| { + let index_context = + IterationContext::new(i, total_length); + let new_breadcrumbs = self + .new_breadcrumbs_section( + breadcrumbs, + Some(&index_context), + Some(&injected_context), + &container.explicit_context, + Some(array_elem), + ); + self.render_body( + &body, + new_breadcrumbs + .as_ref() + .unwrap_or(breadcrumbs), + blocks, + &mut None, + ) + }) + .collect(); + let rendered_slice: &[String] = &rendered_results?; + return Ok(rendered_slice.join("")); + } + } + } + } else { + // Oddly enough if the value is falsey (like + // an empty array or null), Dust uses the + // original context before walking the path as + // the context for rendering the else block + let new_breadcrumbs = self.new_breadcrumbs_section( + breadcrumbs, + None, + Some(&injected_context), + &container.explicit_context, + None, + ); + self.render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ) + }; + } + } + } + DustTag::DTExists(container) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + &container.explicit_context, + ); + let val = walk_path(breadcrumbs, &container.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + return match val { + Ok(Some(v)) if v.get_context_element_reference().is_truthy() => self + .render_maybe_body( + &container.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ), + _ => self.render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ), + }; + } + DustTag::DTNotExists(container) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + &container.explicit_context, + ); + let val = walk_path(breadcrumbs, &container.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + return match val { + Ok(Some(v)) if v.get_context_element_reference().is_truthy() => self + .render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ), + _ => self.render_maybe_body( + &container.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ), + }; + } + DustTag::DTPartial(partial) => { + let partial_name = self.render_partial_name(&partial.name, breadcrumbs)?; + if partial.params.is_empty() { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + &partial.explicit_context, + ); + let rendered_content = self.render_template( + &partial_name, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + Some(blocks.blocks), + )?; + return Ok(rendered_content); + } else { + let injected_context = + ParametersContext::new(self, breadcrumbs, &partial.params, None); + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + Some(&injected_context), + &partial.explicit_context, + ); + let rendered_content = self.render_template( + &partial_name, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + Some(blocks.blocks), + )?; + return Ok(rendered_content); + } + } + DustTag::DTInlinePartial(_named_block) => { + // Inline partials are blank during rendering (they get injected into blocks) + return Ok("".to_owned()); + } + DustTag::DTBlock(named_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + blocks.breadcrumbs, + None, + &named_block.explicit_context, + ); + return match blocks.blocks.get_block(named_block.path.keys[0]) { + None => self.render_maybe_body( + &named_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ), + Some(inline_partial) => self.render_maybe_body( + inline_partial, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ), + }; + } + DustTag::DTHelperEquals(parameterized_block) + | DustTag::DTHelperNotEquals(parameterized_block) + | DustTag::DTHelperGreaterThan(parameterized_block) + | DustTag::DTHelperLessThan(parameterized_block) + | DustTag::DTHelperGreaterThanOrEquals(parameterized_block) + | DustTag::DTHelperLessThanOrEquals(parameterized_block) => { + match select_context { + Some(sc) if sc.allowed_to_render_any_more_conditionals == false => { + return Ok("".to_owned()) + } + _ => (), + } + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + + return self + .perform_comparison_check( + tag, + breadcrumbs, + select_context.as_ref().map(|sc| sc.select_parameters), + ) + .map(|check_result| { + if check_result { + select_context.as_mut().map(|sc| { + sc.allowed_to_render_any_more_conditionals = false; + }); + self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ) + } else { + self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ) + } + }) + .unwrap_or(Ok("".to_owned())); + } + DustTag::DTHelperSep(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + + let index = self.get(breadcrumbs, &vec!["$idx"]); + let len = self.get(breadcrumbs, &vec!["$len"]); + match (index, len) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ) + } + (Ok(index_resolved), Ok(len_resolved)) => { + // Iteration contexts use OwnedLiteral::LPositiveinteger + let index_cast = index_resolved + .get_context_element_reference() + .to_any() + .downcast_ref::(); + let len_cast = len_resolved + .get_context_element_reference() + .to_any() + .downcast_ref::(); + match (index_cast, len_cast) { + ( + Some(OwnedLiteral::LPositiveInteger(index_number)), + Some(OwnedLiteral::LPositiveInteger(len_number)), + ) => { + if *index_number == len_number - 1 { + return Ok("".to_owned()); + } else { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ); + } + } + _ => return Ok("".to_owned()), + } + } + } + } + DustTag::DTHelperFirst(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + + let index = self.get(breadcrumbs, &vec!["$idx"]); + match index { + Err(_) => return Ok("".to_owned()), + Ok(index_resolved) => { + // Iteration contexts use OwnedLiteral::LPositiveinteger + let index_cast = index_resolved + .get_context_element_reference() + .to_any() + .downcast_ref::(); + match index_cast { + Some(OwnedLiteral::LPositiveInteger(index_number)) => { + if *index_number != 0 { + return Ok("".to_owned()); + } else { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ); + } + } + _ => return Ok("".to_owned()), + } + } + } + } + DustTag::DTHelperLast(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + + let index = self.get(breadcrumbs, &vec!["$idx"]); + let len = self.get(breadcrumbs, &vec!["$len"]); + match (index, len) { + (Err(_), _) | (_, Err(_)) => return Ok("".to_owned()), + (Ok(index_resolved), Ok(len_resolved)) => { + // Iteration contexts use OwnedLiteral::LPositiveinteger + let index_cast = index_resolved + .get_context_element_reference() + .to_any() + .downcast_ref::(); + let len_cast = len_resolved + .get_context_element_reference() + .to_any() + .downcast_ref::(); + match (index_cast, len_cast) { + ( + Some(OwnedLiteral::LPositiveInteger(index_number)), + Some(OwnedLiteral::LPositiveInteger(len_number)), + ) => { + if *index_number != len_number - 1 { + return Ok("".to_owned()); + } else { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ); + } + } + _ => return Ok("".to_owned()), + } + } + } + } + DustTag::DTHelperSelect(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + let new_breadcrumbs_ref = new_breadcrumbs.as_ref().unwrap_or(breadcrumbs); + match ¶meterized_block.contents { + None => return Ok("".to_owned()), + Some(body) => { + let param_map = ParametersContext::new( + self, + breadcrumbs, + ¶meterized_block.params, + None, + ); + let are_any_checks_true = body + .elements + .iter() + .filter_map(|te| match te { + TemplateElement::TETag(dt) => match dt { + DustTag::DTHelperEquals(_) + | DustTag::DTHelperNotEquals(_) + | DustTag::DTHelperGreaterThan(_) + | DustTag::DTHelperLessThan(_) + | DustTag::DTHelperGreaterThanOrEquals(_) + | DustTag::DTHelperLessThanOrEquals(_) => Some(dt), + _ => None, + }, + _ => None, + }) + .map(|dt| { + self.perform_comparison_check( + dt, + new_breadcrumbs_ref, + Some(¶m_map), + ) + }) + .any(|check_result| check_result.unwrap_or(false)); + let select_context = SelectContext::new(¶m_map, are_any_checks_true); + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs_ref, + blocks, + &mut Some(select_context), + ); + } + } + } + DustTag::DTHelperAny(parameterized_block) => match select_context { + Some(sc) if sc.were_any_true => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ); + } + _ => return Ok("".to_owned()), + }, + DustTag::DTHelperNone(parameterized_block) => match select_context { + Some(sc) if !sc.were_any_true => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + &mut None, + ); + } + _ => return Ok("".to_owned()), + }, + DustTag::DTHelperMath(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + let new_breadcrumbs_ref = new_breadcrumbs.as_ref().unwrap_or(breadcrumbs); + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params, None); + match ¶meterized_block.contents { + None => { + return self + .perform_math_operation(breadcrumbs, ¶m_map) + .map(|final_val| { + final_val + .get_context_element_reference() + .render(&Vec::new()) + }) + .unwrap_or(Ok("".to_owned())); + } + Some(body) => { + // Calculate the value + let calculated_value = + match self.perform_math_operation(breadcrumbs, ¶m_map) { + None => return Ok("".to_owned()), + Some(val) => val, + }; + + // Generate a ParametersContext with the result of the math operation as key + let converted_value: BreadcrumbTreeElement<'_> = calculated_value.into(); + let dummy_rvalue = RValue::RVLiteral(OwnedLiteral::LPositiveInteger(0)); + let calculated_param_map: HashMap< + &str, + (&RValue<'_>, Option>), + > = vec![("key", (&dummy_rvalue, Some(converted_value)))] + .into_iter() + .collect(); + let calculated_context = + ParametersContext::from_values(None, calculated_param_map); + // calculate are_any_checks_true + let are_any_checks_true = body + .elements + .iter() + .filter_map(|te| match te { + TemplateElement::TETag(dt) => match dt { + DustTag::DTHelperEquals(_) + | DustTag::DTHelperNotEquals(_) + | DustTag::DTHelperGreaterThan(_) + | DustTag::DTHelperLessThan(_) + | DustTag::DTHelperGreaterThanOrEquals(_) + | DustTag::DTHelperLessThanOrEquals(_) => Some(dt), + _ => None, + }, + _ => None, + }) + .map(|dt| { + self.perform_comparison_check( + dt, + new_breadcrumbs_ref, + Some(&calculated_context), + ) + }) + .any(|check_result| check_result.unwrap_or(false)); + // Generate a SelectContext + let select_context = + SelectContext::new(&calculated_context, are_any_checks_true); + // render_maybe_body + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs_ref, + blocks, + &mut Some(select_context), + ); + } + } + } + DustTag::DTHelperSize(parameterized_block) => { + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params, None); + let value = self.tap(breadcrumbs, ¶m_map, "key"); + let value_ce = value.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }); + match value_ce { + // If "key" is not on the @size tag at all, render 0. + None => return Ok("0".to_owned()), + // If the key value could not be found in the context, render 0. + Some(Err(_)) => return Ok("0".to_owned()), + Some(Ok(ce)) => { + // The @size helper attempts to cast values to + // numbers, and if that succeeds it uses the + // number, otherwise we'll get the size of the + // original type. + match (ce.cast_to_type("number"), ce.is_castable()) { + (Some(ice), true) => { + return ice + .get_context_element_reference() + .get_size() + .map(|ce_size| { + ce_size.get_context_element_reference().render(&Vec::new()) + }) + .unwrap_or(Ok("".to_owned())) + } + (Some(_), false) => { + return ce + .get_size() + .map(|ce_size| { + ce_size.get_context_element_reference().render(&Vec::new()) + }) + .unwrap_or(Ok("".to_owned())) + } + (None, _) => { + return ce + .get_size() + .map(|ce_size| { + ce_size.get_context_element_reference().render(&Vec::new()) + }) + .unwrap_or(Ok("".to_owned())) + } + } + } + } + } + DustTag::DTHelperContextDump(parameterized_block) => { + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params, None); + let value = self.tap(breadcrumbs, ¶m_map, "key"); + let destination = self.tap(breadcrumbs, ¶m_map, "to"); + let value_rendered = match value.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + .map(|ce| ce.render(&Vec::new())) + }) { + Some(Ok(Ok(val))) => val, + _ => "current".to_owned(), + }; + let destination_rendered = match destination.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + .map(|ce| ce.render(&Vec::new())) + }) { + Some(Ok(Ok(val))) => val, + _ => "output".to_owned(), + }; + match ( + value_rendered.as_str(), + destination_rendered.as_str(), + breadcrumbs.last(), + ) { + ("current", "output", None) => return Ok("{}".to_owned()), + ("current", "console", None) => println!("{{}}"), + ("current", "output", Some(bc)) => return Ok(format!("{:?}", bc)), + ("current", "console", Some(bc)) => println!("{:?}", bc), + ("full", "output", _) => return Ok(format!("{:?}", breadcrumbs)), + ("full", "console", _) => println!("{:?}", breadcrumbs), + _ => panic!("Unhandled contextDump parameters."), + } + } + } + + Ok("".to_owned()) + } + + /// Read a value from the context tree + pub fn get

( + &'a self, + breadcrumbs: &'a Vec>, + name: &Vec

, + ) -> Result, WalkError> + where + P: Borrow, + { + let val = + walk_path(breadcrumbs, name).map(|ice| ice.into_context_element(self, breadcrumbs)); + match val { + Ok(Some(ice_result)) => Ok(ice_result), + Ok(None) => Err(WalkError::CantWalk), + Err(walk_error) => Err(walk_error), + } + } + + /// Read a value from the parameters context + /// + /// Returns None if the key is not present at all + pub fn tap

( + &'a self, + breadcrumbs: &'a Vec>, + parameters: &'a ParametersContext<'a>, + name: &P, + ) -> Option, WalkError>> + where + P: AsRef + ?Sized, + { + if !parameters.contains_key(name.as_ref()) { + return None; + } + let val = parameters + .walk(name.as_ref()) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match val { + Ok(Some(ice_result)) => Some(Ok(ice_result)), + Ok(None) => Some(Err(WalkError::CantWalk)), + Err(walk_error) => Some(Err(walk_error)), + } + } + + fn new_breadcrumbs_section<'b>( + &'b self, + breadcrumbs: &'b Vec>, + index_context: Option<&'b dyn IntoContextElement>, + injected_context: Option<&'b dyn IntoContextElement>, + explicit_context: &Option>, + new_context_element: Option<&'b dyn ContextElement>, + ) -> Option>> { + // If none of the additional contexts are present, return None + // to signal that the original breadcrumbs should be used + // rather than incurring a copy here. + match ( + index_context, + injected_context, + explicit_context, + new_context_element, + ) { + (None, None, None, None) => return None, + _ => (), + } + + // If there is an explicit context, then drop all the current + // context + let mut new_stack = match explicit_context { + Some(_) => Vec::with_capacity(4), + None => breadcrumbs.clone(), + }; + + explicit_context.as_ref().map(|path| { + walk_path(breadcrumbs, &path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)) + .ok() + .flatten() + .map(|val| { + if val.get_context_element_reference().is_truthy() { + new_stack.push(std::convert::From::from(val)) + } + }); + }); + injected_context.map(|ctx| new_stack.push(BreadcrumbTreeElement::from_borrowed(ctx))); + new_context_element.map(|ctx| { + new_stack.push(BreadcrumbTreeElement::from_borrowed( + ctx.from_context_element(), + )) + }); + index_context.map(|ctx| new_stack.push(BreadcrumbTreeElement::from_borrowed(ctx))); + + Some(new_stack) + } + + fn new_breadcrumbs_partial<'b>( + &'b self, + breadcrumbs: &'b Vec>, + explicit_context_breadcrumbs: &'a Vec>, + injected_context: Option<&'b dyn IntoContextElement>, + explicit_context: &Option>, + ) -> Option>> { + // If none of the additional contexts are present, return None + // to signal that the original breadcrumbs should be used + // rather than incurring a copy here. + match (injected_context, explicit_context) { + (None, None) => return None, + _ => (), + }; + + // If there is an explicit context, then drop all the current + // context + let mut new_stack = match explicit_context { + Some(_) => Vec::with_capacity(3), + None => breadcrumbs.clone(), + }; + + injected_context.map(|ctx| { + // Special case: when there is no explicit context, the + // injected context gets inserted 1 spot behind the + // current context. Otherwise, the injected context gets + // added after the current context but before the explicit + // context. + match explicit_context { + None => new_stack.insert( + Self::get_index_of_first_non_pseudo_element(&new_stack).unwrap_or(0), + BreadcrumbTreeElement::from_borrowed(ctx), + ), + _ => new_stack.push(BreadcrumbTreeElement::from_borrowed(ctx)), + } + }); + + explicit_context.as_ref().map(|path| { + // TODO: should resolving the value here use + // explicit_context_maybe_breadcrumbs or + // maybe_breadcrumbs? + walk_path(explicit_context_breadcrumbs, &path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)) + .ok() + .flatten() + .map(|val| { + if val.get_context_element_reference().is_truthy() { + new_stack.push(std::convert::From::from(val)); + } + }); + }); + + Some(new_stack) + } + + fn are_paths_identical<'b>( + left_side: &Option>, + right_side: &Option>, + ) -> bool { + match (left_side, right_side) { + (Some(Ok(lce)), Some(Ok(rce))) => (*lce) as *const _ == (*rce) as *const _, + _ => false, + } + } + + fn preprocess_filters(filters: &Vec) -> Vec { + let mut final_filters: Vec = filters + .into_iter() + .filter(|f| f != &&Filter::DisableHtmlEncode) + .map(|f| f.clone()) + .collect(); + + // If the user has not specified any escaping filter (|s or + // |h), automatically add an html escape filter + if !filters.iter().any(|f| f == &Filter::DisableHtmlEncode) { + final_filters.push(Filter::HtmlEncode); + } + final_filters + } + + fn get_index_of_first_non_pseudo_element<'b>( + breadcrumbs: &'b Vec>, + ) -> Option { + breadcrumbs.iter().rposition(|b| { + !std::borrow::Borrow::::borrow(b).is_pseudo_element() + }) + } + + /// Performs a comparison (eq, ne, gt, gte, lt, lte) and returns + /// whether or not the comparison was true. + /// + /// Returns None in the special case that the "key" parameter did + /// not exist at all. This means that key was not in the original + /// dust template source, which is not the same as key referencing + /// a value which does not exist. + fn perform_comparison_check( + &'a self, + tag: &'a DustTag, + breadcrumbs: &'a Vec>, + select_parameters: Option<&'a ParametersContext<'a>>, + ) -> Option { + let param_map = match tag { + DustTag::DTHelperEquals(parameterized_block) + | DustTag::DTHelperNotEquals(parameterized_block) + | DustTag::DTHelperGreaterThan(parameterized_block) + | DustTag::DTHelperLessThan(parameterized_block) + | DustTag::DTHelperGreaterThanOrEquals(parameterized_block) + | DustTag::DTHelperLessThanOrEquals(parameterized_block) => ParametersContext::new(self, breadcrumbs, ¶meterized_block.params, select_parameters), + _ => panic!("perform_comparison_check only implemented for comparison helpers (eq, ne, gt, gte, lt, lte)") + }; + + let left_side = self.tap(breadcrumbs, ¶m_map, "key"); + let right_side = self.tap(breadcrumbs, ¶m_map, "value"); + let type_cast = self.tap(breadcrumbs, ¶m_map, "type"); + + let left_side_ce = left_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }); + let mut right_side_ce = right_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }); + let type_rendered = match type_cast.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + .map(|ce| ce.render(&Vec::new())) + }) { + Some(Ok(Ok(val))) => Some(val), + _ => None, + }; + if left_side_ce.is_none() { + // If key did not exist at all, return None + return None; + } + let right_side_cast = match (right_side_ce, type_rendered) { + (Some(Ok(ce)), Some(target_type)) => ce.cast_to_type(&target_type), + _ => None, + }; + match right_side_cast.as_ref() { + Some(ice) => { + right_side_ce = Some(Ok(ice.get_context_element_reference())); + } + None => (), + } + match tag { + // Special case: when comparing two RVPaths, if the path + // points to the same value then they are equal. This is + // particularly important for objects which compare memory + // locations rather than contents (javascript object + // equality). + DustTag::DTHelperEquals(_) => Some(Self::are_paths_identical(&left_side_ce, &right_side_ce) + || left_side_ce.unwrap_or(Err(&WalkError::CantWalk)) + == right_side_ce.unwrap_or(Err(&WalkError::CantWalk))), + DustTag::DTHelperNotEquals(_) => Some(!Self::are_paths_identical(&left_side_ce, &right_side_ce) + && left_side_ce.unwrap_or(Err(&WalkError::CantWalk)) + != right_side_ce.unwrap_or(Err(&WalkError::CantWalk))), + DustTag::DTHelperGreaterThan(_) => Some(match (left_side_ce, right_side_ce) { + (Some(Ok(left_value)), Some(Ok(right_value))) if left_value > right_value => true, + _ => false + }), + DustTag::DTHelperLessThan(_) => Some(match (left_side_ce, right_side_ce) { + (Some(Ok(left_value)), Some(Ok(right_value))) if left_value < right_value => true, + _ => false + }), + DustTag::DTHelperGreaterThanOrEquals(_) => Some(match (left_side_ce, right_side_ce) { + (Some(Ok(left_value)), Some(Ok(right_value))) if left_value >= right_value => true, + _ => false + }), + DustTag::DTHelperLessThanOrEquals(_) => Some(match (left_side_ce, right_side_ce) { + (Some(Ok(left_value)), Some(Ok(right_value))) if left_value <= right_value => true, + _ => false + }), + _ => panic!("perform_comparison_check only implemented for comparison helpers (eq, ne, gt, gte, lt, lte)") + } + } + + /// Performs a math operation (add, subtract, multiply, divide, + /// mod, abs, floor, ceil) and returns the result or None if + /// nothing should be rendered. + fn perform_math_operation( + &'a self, + breadcrumbs: &'a Vec>, + math_parameters: &'a ParametersContext<'a>, + ) -> Option> { + // Special case: if method is a template then do not render + // anything. This is to match the behavior of dustjs even + // though it works fine. + match math_parameters.get_original_rvalue("method") { + Some(RValue::RVTemplate(template)) => { + if template.iter().any(|pne| match pne { + PartialNameElement::PNReference { .. } => true, + PartialNameElement::PNSpan { .. } => false, + }) { + return None; + } + } + _ => (), + } + + let method = match self.tap(breadcrumbs, math_parameters, "method") { + None | Some(Err(_)) => return None, + Some(Ok(ice_result)) => ice_result, + }; + let method_rendered = match method.get_context_element_reference().render(&Vec::new()) { + Ok(text) => text, + Err(_) => return None, + }; + + let left_side = self.tap(breadcrumbs, math_parameters, "key"); + let right_side = self.tap(breadcrumbs, math_parameters, "operand"); + + let left_side_ce = left_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + .map(|ce| ce.cast_to_type("number")) + }); + let right_side_ce = right_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + .map(|ce| ce.cast_to_type("number")) + }); + + // println!( + // "Doing {:?} {:?} {:?}", + // left_side, method_rendered, right_side + // ); + + return match method_rendered.as_str() { + "add" => match (left_side_ce, right_side_ce) { + (None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None, + (Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned( + OwnedLiteral::LString("NaN".to_owned()), + )), + (Some(Ok(Some(l))), Some(Ok(Some(r)))) => l + .get_context_element_reference() + .math_add(r.get_context_element_reference()), + }, + "subtract" => match (left_side_ce, right_side_ce) { + (None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None, + (Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned( + OwnedLiteral::LString("NaN".to_owned()), + )), + (Some(Ok(Some(l))), Some(Ok(Some(r)))) => l + .get_context_element_reference() + .math_subtract(r.get_context_element_reference()), + }, + "multiply" => match (left_side_ce, right_side_ce) { + (None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None, + (Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned( + OwnedLiteral::LString("NaN".to_owned()), + )), + (Some(Ok(Some(l))), Some(Ok(Some(r)))) => l + .get_context_element_reference() + .math_multiply(r.get_context_element_reference()), + }, + "divide" => match (left_side_ce, right_side_ce) { + (None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None, + (Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned( + OwnedLiteral::LString("NaN".to_owned()), + )), + (Some(Ok(Some(l))), Some(Ok(Some(r)))) => l + .get_context_element_reference() + .math_divide(r.get_context_element_reference()), + }, + "mod" => match (left_side_ce, right_side_ce) { + (None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None, + (Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned( + OwnedLiteral::LString("NaN".to_owned()), + )), + (Some(Ok(Some(l))), Some(Ok(Some(r)))) => l + .get_context_element_reference() + .math_modulus(r.get_context_element_reference()), + }, + "abs" => match left_side_ce { + None | Some(Err(_)) => None, + Some(Ok(None)) => Some(IceResult::from_owned(OwnedLiteral::LString( + "NaN".to_owned(), + ))), + Some(Ok(Some(num))) => num.get_context_element_reference().math_abs(), + }, + "floor" => match left_side_ce { + None | Some(Err(_)) => None, + Some(Ok(None)) => Some(IceResult::from_owned(OwnedLiteral::LString( + "NaN".to_owned(), + ))), + Some(Ok(Some(num))) => num.get_context_element_reference().math_floor(), + }, + "ceil" => match left_side_ce { + None | Some(Err(_)) => None, + Some(Ok(None)) => Some(IceResult::from_owned(OwnedLiteral::LString( + "NaN".to_owned(), + ))), + Some(Ok(Some(num))) => num.get_context_element_reference().math_ceil(), + }, + _ => None, + }; + } +} + +struct BlockContext<'a> { + /// The breadcrumbs at the time of entering the current partial + breadcrumbs: &'a Vec>, + blocks: &'a InlinePartialTreeElement<'a>, +} diff --git a/src/renderer/select_context.rs b/src/renderer/select_context.rs new file mode 100644 index 0000000..6e76d22 --- /dev/null +++ b/src/renderer/select_context.rs @@ -0,0 +1,18 @@ +use crate::renderer::parameters_context::ParametersContext; + +#[derive(Debug)] +pub struct SelectContext<'a> { + pub select_parameters: &'a ParametersContext<'a>, + pub were_any_true: bool, + pub allowed_to_render_any_more_conditionals: bool, +} + +impl<'a> SelectContext<'a> { + pub fn new(select_parameters: &'a ParametersContext<'a>, were_any_true: bool) -> Self { + SelectContext { + select_parameters: select_parameters, + were_any_true: were_any_true, + allowed_to_render_any_more_conditionals: true, + } + } +} diff --git a/src/renderer/walking.rs b/src/renderer/walking.rs new file mode 100644 index 0000000..ebfed6c --- /dev/null +++ b/src/renderer/walking.rs @@ -0,0 +1,94 @@ +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::WalkError; +use std::borrow::Borrow; + +enum WalkResult<'a> { + NoWalk, + PartialWalk, + FullyWalked(&'a dyn IntoContextElement), +} + +fn walk_path_from_single_level<'a, P>( + context: &'a dyn IntoContextElement, + path: &[P], +) -> WalkResult<'a> +where + P: Borrow, +{ + if path.is_empty() { + return WalkResult::FullyWalked(context); + } + + let mut walk_failure = WalkResult::NoWalk; + let mut output = context; + for elem in path.iter() { + match output.walk(elem.borrow()) { + Err(WalkError::CantWalk { .. }) => { + return walk_failure; + } + Ok(new_val) => { + walk_failure = WalkResult::PartialWalk; + output = new_val; + } + } + } + + WalkResult::FullyWalked(output) +} + +fn get_first_non_pseudo_element<'a>( + breadcrumbs: &'a Vec>, +) -> Option<&'a BreadcrumbTreeElement<'a>> { + breadcrumbs + .iter() + .rev() + .filter(|b| { + !std::borrow::Borrow::::borrow(*b).is_pseudo_element() + }) + .next() +} + +pub fn walk_path<'a, P>( + breadcrumbs: &'a Vec>, + path: &Vec

, +) -> Result<&'a dyn IntoContextElement, WalkError> +where + P: Borrow, +{ + match (breadcrumbs.last(), path.first()) { + (None, _) => return Err(WalkError::CantWalk), + (Some(last_elem), None) => return Ok(last_elem.borrow()), + (Some(_), Some(path_first)) if path_first.borrow() == "." => { + let first_non_pseudo_element = get_first_non_pseudo_element(breadcrumbs); + return match first_non_pseudo_element { + None => Err(WalkError::CantWalk), + Some(current_context) => { + match walk_path_from_single_level(current_context.borrow(), &path[1..]) { + // If no walking was done at all or we partially walked + // then stop trying to find anything because '.' restricts + // us to the current scope + WalkResult::NoWalk | WalkResult::PartialWalk => Err(WalkError::CantWalk), + WalkResult::FullyWalked(new_context) => Ok(new_context), + } + } + }; + } + (Some(_), Some(_path_first)) => { + for context in breadcrumbs.iter().rev() { + match walk_path_from_single_level(context.borrow(), path) { + // If no walking was done at all, keep looping + WalkResult::NoWalk => {} + // If we partially walked then stop trying to find + // anything + WalkResult::PartialWalk => { + return Err(WalkError::CantWalk); + } + WalkResult::FullyWalked(new_context) => return Ok(new_context), + } + } + } + } + + Err(WalkError::CantWalk) +}