From 9c71dc8a2327002390a5df4011e91bbdfb13cab2 Mon Sep 17 00:00:00 2001
From: j3lte
Date: Fri, 24 Nov 2023 20:36:10 +0100
Subject: [PATCH] First setup
---
.github/ISSUE_TEMPLATE/bug_report.md | 33 ++
.github/ISSUE_TEMPLATE/feature_request.md | 20 ++
.github/workflows/npm.yml | 35 ++
.github/workflows/release.yml | 34 ++
.github/workflows/test.yml | 42 +++
.github/workflows/version.yml | 35 ++
.gitignore | 9 +
LICENSE | 21 ++
README.md | 18 ++
deno.json | 50 +++
deno.lock | 204 ++++++++++++
dev_deps.ts | 18 ++
mod.ts | 6 +
scripts/build-npm.ts | 71 ++++
scripts/check-license.ts | 60 ++++
scripts/update-version.ts | 29 ++
scripts/watch-test.ts | 67 ++++
src/lib/Context.ts | 130 ++++++++
src/lib/Mustache.ts | 67 ++++
src/lib/Scanner.ts | 80 +++++
src/lib/Writer.ts | 281 ++++++++++++++++
src/lib/parse.ts | 268 +++++++++++++++
src/lib/util.ts | 80 +++++
src/mod.ts | 8 +
test/Context.test.ts | 76 +++++
test/Scanner.test.ts | 90 ++++++
test/helpers/_files/ampersand_escape.js | 3 +
test/helpers/_files/ampersand_escape.mustache | 1 +
test/helpers/_files/ampersand_escape.txt | 1 +
test/helpers/_files/apostrophe.js | 4 +
test/helpers/_files/apostrophe.mustache | 1 +
test/helpers/_files/apostrophe.txt | 1 +
test/helpers/_files/array_of_strings.js | 3 +
test/helpers/_files/array_of_strings.mustache | 1 +
test/helpers/_files/array_of_strings.txt | 1 +
.../avoids_obj_prototype_in_view_cache.js | 4 +
...voids_obj_prototype_in_view_cache.mustache | 1 +
.../avoids_obj_prototype_in_view_cache.txt | 1 +
test/helpers/_files/backslashes.js | 3 +
test/helpers/_files/backslashes.mustache | 7 +
test/helpers/_files/backslashes.txt | 7 +
.../_files/bug_11_eating_whitespace.js | 3 +
.../_files/bug_11_eating_whitespace.mustache | 1 +
.../_files/bug_11_eating_whitespace.txt | 1 +
test/helpers/_files/bug_length_property.js | 3 +
.../_files/bug_length_property.mustache | 1 +
test/helpers/_files/bug_length_property.txt | 1 +
test/helpers/_files/changing_delimiters.js | 4 +
.../_files/changing_delimiters.mustache | 1 +
test/helpers/_files/changing_delimiters.txt | 1 +
test/helpers/_files/check_falsy.js | 7 +
test/helpers/_files/check_falsy.mustache | 1 +
test/helpers/_files/check_falsy.txt | 1 +
test/helpers/_files/comments.js | 5 +
test/helpers/_files/comments.mustache | 1 +
test/helpers/_files/comments.txt | 1 +
test/helpers/_files/complex.js | 19 ++
test/helpers/_files/complex.mustache | 16 +
test/helpers/_files/complex.txt | 6 +
test/helpers/_files/context_lookup.js | 8 +
test/helpers/_files/context_lookup.mustache | 1 +
test/helpers/_files/context_lookup.txt | 1 +
test/helpers/_files/delimiters.js | 6 +
test/helpers/_files/delimiters.mustache | 7 +
test/helpers/_files/delimiters.txt | 5 +
.../helpers/_files/disappearing_whitespace.js | 4 +
.../_files/disappearing_whitespace.mustache | 1 +
.../_files/disappearing_whitespace.txt | 1 +
test/helpers/_files/dot_notation.js | 24 ++
test/helpers/_files/dot_notation.mustache | 12 +
test/helpers/_files/dot_notation.txt | 12 +
test/helpers/_files/double_render.js | 5 +
test/helpers/_files/double_render.mustache | 1 +
test/helpers/_files/double_render.txt | 1 +
test/helpers/_files/empty_list.js | 3 +
test/helpers/_files/empty_list.mustache | 4 +
test/helpers/_files/empty_list.txt | 1 +
test/helpers/_files/empty_sections.js | 1 +
test/helpers/_files/empty_sections.mustache | 1 +
test/helpers/_files/empty_sections.txt | 1 +
test/helpers/_files/empty_string.js | 6 +
test/helpers/_files/empty_string.mustache | 1 +
test/helpers/_files/empty_string.txt | 1 +
test/helpers/_files/empty_template.js | 1 +
test/helpers/_files/empty_template.mustache | 1 +
test/helpers/_files/empty_template.txt | 1 +
test/helpers/_files/error_not_found.js | 3 +
test/helpers/_files/error_not_found.mustache | 1 +
test/helpers/_files/error_not_found.txt | 0
test/helpers/_files/escaped.js | 7 +
test/helpers/_files/escaped.mustache | 2 +
test/helpers/_files/escaped.txt | 2 +
test/helpers/_files/falsy.js | 8 +
test/helpers/_files/falsy.mustache | 12 +
test/helpers/_files/falsy.txt | 12 +
test/helpers/_files/falsy_array.js | 10 +
test/helpers/_files/falsy_array.mustache | 3 +
test/helpers/_files/falsy_array.txt | 6 +
test/helpers/_files/grandparent_context.js | 19 ++
.../_files/grandparent_context.mustache | 10 +
test/helpers/_files/grandparent_context.txt | 17 +
test/helpers/_files/higher_order_sections.js | 9 +
.../_files/higher_order_sections.mustache | 1 +
test/helpers/_files/higher_order_sections.txt | 1 +
test/helpers/_files/implicit_iterator.js | 8 +
.../helpers/_files/implicit_iterator.mustache | 7 +
test/helpers/_files/implicit_iterator.txt | 3 +
test/helpers/_files/included_tag.js | 3 +
test/helpers/_files/included_tag.mustache | 1 +
test/helpers/_files/included_tag.txt | 1 +
test/helpers/_files/inverted_section.js | 3 +
test/helpers/_files/inverted_section.mustache | 3 +
test/helpers/_files/inverted_section.txt | 3 +
.../helpers/_files/keys_with_questionmarks.js | 5 +
.../_files/keys_with_questionmarks.mustache | 3 +
.../_files/keys_with_questionmarks.txt | 1 +
test/helpers/_files/malicious_template.js | 1 +
.../_files/malicious_template.mustache | 5 +
test/helpers/_files/malicious_template.txt | 2 +
test/helpers/_files/multiline_comment.js | 1 +
.../helpers/_files/multiline_comment.mustache | 6 +
test/helpers/_files/multiline_comment.txt | 1 +
test/helpers/_files/nested_dot.js | 1 +
test/helpers/_files/nested_dot.mustache | 1 +
test/helpers/_files/nested_dot.txt | 1 +
.../_files/nested_higher_order_sections.js | 8 +
.../nested_higher_order_sections.mustache | 1 +
.../_files/nested_higher_order_sections.txt | 1 +
test/helpers/_files/nested_iterating.js | 8 +
test/helpers/_files/nested_iterating.mustache | 1 +
test/helpers/_files/nested_iterating.txt | 1 +
test/helpers/_files/nesting.js | 7 +
test/helpers/_files/nesting.mustache | 5 +
test/helpers/_files/nesting.txt | 3 +
test/helpers/_files/nesting_same_name.js | 8 +
.../helpers/_files/nesting_same_name.mustache | 1 +
test/helpers/_files/nesting_same_name.txt | 1 +
test/helpers/_files/null_lookup_array.js | 9 +
.../helpers/_files/null_lookup_array.mustache | 3 +
test/helpers/_files/null_lookup_array.txt | 3 +
test/helpers/_files/null_lookup_object.js | 31 ++
.../_files/null_lookup_object.mustache | 9 +
test/helpers/_files/null_lookup_object.txt | 7 +
test/helpers/_files/null_string.js | 10 +
test/helpers/_files/null_string.mustache | 6 +
test/helpers/_files/null_string.txt | 6 +
test/helpers/_files/null_view.js | 4 +
test/helpers/_files/null_view.mustache | 1 +
test/helpers/_files/null_view.txt | 1 +
test/helpers/_files/partial_array.js | 3 +
test/helpers/_files/partial_array.mustache | 1 +
test/helpers/_files/partial_array.partial | 4 +
test/helpers/_files/partial_array.txt | 5 +
.../_files/partial_array_of_partials.js | 8 +
.../_files/partial_array_of_partials.mustache | 4 +
.../_files/partial_array_of_partials.partial | 1 +
.../_files/partial_array_of_partials.txt | 5 +
.../partial_array_of_partials_implicit.js | 3 +
...artial_array_of_partials_implicit.mustache | 4 +
...partial_array_of_partials_implicit.partial | 1 +
.../partial_array_of_partials_implicit.txt | 5 +
test/helpers/_files/partial_empty.js | 3 +
test/helpers/_files/partial_empty.mustache | 2 +
test/helpers/_files/partial_empty.partial | 0
test/helpers/_files/partial_empty.txt | 1 +
test/helpers/_files/partial_template.js | 6 +
test/helpers/_files/partial_template.mustache | 2 +
test/helpers/_files/partial_template.partial | 1 +
test/helpers/_files/partial_template.txt | 2 +
test/helpers/_files/partial_view.js | 14 +
test/helpers/_files/partial_view.mustache | 3 +
test/helpers/_files/partial_view.partial | 5 +
test/helpers/_files/partial_view.txt | 5 +
test/helpers/_files/partial_whitespace.js | 14 +
.../_files/partial_whitespace.mustache | 3 +
.../helpers/_files/partial_whitespace.partial | 5 +
test/helpers/_files/partial_whitespace.txt | 5 +
.../_files/recursion_with_same_names.js | 8 +
.../_files/recursion_with_same_names.mustache | 7 +
.../_files/recursion_with_same_names.txt | 7 +
test/helpers/_files/reuse_of_enumerables.js | 6 +
.../_files/reuse_of_enumerables.mustache | 8 +
test/helpers/_files/reuse_of_enumerables.txt | 8 +
test/helpers/_files/section_as_context.js | 10 +
.../_files/section_as_context.mustache | 9 +
test/helpers/_files/section_as_context.txt | 6 +
.../_files/section_functions_in_partials.js | 7 +
.../section_functions_in_partials.mustache | 3 +
.../section_functions_in_partials.partial | 1 +
.../_files/section_functions_in_partials.txt | 3 +
test/helpers/_files/simple.js | 8 +
test/helpers/_files/simple.mustache | 5 +
test/helpers/_files/simple.txt | 3 +
test/helpers/_files/string_as_context.js | 4 +
.../helpers/_files/string_as_context.mustache | 5 +
test/helpers/_files/string_as_context.txt | 5 +
test/helpers/_files/two_in_a_row.js | 4 +
test/helpers/_files/two_in_a_row.mustache | 1 +
test/helpers/_files/two_in_a_row.txt | 1 +
test/helpers/_files/two_sections.js | 1 +
test/helpers/_files/two_sections.mustache | 4 +
test/helpers/_files/two_sections.txt | 0
test/helpers/_files/unescaped.js | 6 +
test/helpers/_files/unescaped.mustache | 1 +
test/helpers/_files/unescaped.txt | 1 +
.../_files/uses_props_from_view_prototype.js | 30 ++
.../uses_props_from_view_prototype.mustache | 1 +
.../_files/uses_props_from_view_prototype.txt | 1 +
test/helpers/_files/whitespace.js | 4 +
test/helpers/_files/whitespace.mustache | 4 +
test/helpers/_files/whitespace.txt | 4 +
test/helpers/_files/zero_view.js | 1 +
test/helpers/_files/zero_view.mustache | 1 +
test/helpers/_files/zero_view.txt | 1 +
test/helpers/render-helper.ts | 61 ++++
test/parse.test.ts | 188 +++++++++++
test/partial.test.ts | 175 ++++++++++
test/render.test.ts | 305 ++++++++++++++++++
218 files changed, 3396 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md
create mode 100644 .github/workflows/npm.yml
create mode 100644 .github/workflows/release.yml
create mode 100644 .github/workflows/test.yml
create mode 100644 .github/workflows/version.yml
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 deno.json
create mode 100644 deno.lock
create mode 100644 dev_deps.ts
create mode 100644 mod.ts
create mode 100644 scripts/build-npm.ts
create mode 100644 scripts/check-license.ts
create mode 100644 scripts/update-version.ts
create mode 100644 scripts/watch-test.ts
create mode 100644 src/lib/Context.ts
create mode 100644 src/lib/Mustache.ts
create mode 100644 src/lib/Scanner.ts
create mode 100644 src/lib/Writer.ts
create mode 100644 src/lib/parse.ts
create mode 100644 src/lib/util.ts
create mode 100644 src/mod.ts
create mode 100644 test/Context.test.ts
create mode 100644 test/Scanner.test.ts
create mode 100644 test/helpers/_files/ampersand_escape.js
create mode 100644 test/helpers/_files/ampersand_escape.mustache
create mode 100644 test/helpers/_files/ampersand_escape.txt
create mode 100644 test/helpers/_files/apostrophe.js
create mode 100644 test/helpers/_files/apostrophe.mustache
create mode 100644 test/helpers/_files/apostrophe.txt
create mode 100644 test/helpers/_files/array_of_strings.js
create mode 100644 test/helpers/_files/array_of_strings.mustache
create mode 100644 test/helpers/_files/array_of_strings.txt
create mode 100644 test/helpers/_files/avoids_obj_prototype_in_view_cache.js
create mode 100644 test/helpers/_files/avoids_obj_prototype_in_view_cache.mustache
create mode 100644 test/helpers/_files/avoids_obj_prototype_in_view_cache.txt
create mode 100644 test/helpers/_files/backslashes.js
create mode 100644 test/helpers/_files/backslashes.mustache
create mode 100644 test/helpers/_files/backslashes.txt
create mode 100644 test/helpers/_files/bug_11_eating_whitespace.js
create mode 100644 test/helpers/_files/bug_11_eating_whitespace.mustache
create mode 100644 test/helpers/_files/bug_11_eating_whitespace.txt
create mode 100644 test/helpers/_files/bug_length_property.js
create mode 100644 test/helpers/_files/bug_length_property.mustache
create mode 100644 test/helpers/_files/bug_length_property.txt
create mode 100644 test/helpers/_files/changing_delimiters.js
create mode 100644 test/helpers/_files/changing_delimiters.mustache
create mode 100644 test/helpers/_files/changing_delimiters.txt
create mode 100644 test/helpers/_files/check_falsy.js
create mode 100644 test/helpers/_files/check_falsy.mustache
create mode 100644 test/helpers/_files/check_falsy.txt
create mode 100644 test/helpers/_files/comments.js
create mode 100644 test/helpers/_files/comments.mustache
create mode 100644 test/helpers/_files/comments.txt
create mode 100644 test/helpers/_files/complex.js
create mode 100644 test/helpers/_files/complex.mustache
create mode 100644 test/helpers/_files/complex.txt
create mode 100644 test/helpers/_files/context_lookup.js
create mode 100644 test/helpers/_files/context_lookup.mustache
create mode 100644 test/helpers/_files/context_lookup.txt
create mode 100644 test/helpers/_files/delimiters.js
create mode 100644 test/helpers/_files/delimiters.mustache
create mode 100644 test/helpers/_files/delimiters.txt
create mode 100644 test/helpers/_files/disappearing_whitespace.js
create mode 100644 test/helpers/_files/disappearing_whitespace.mustache
create mode 100644 test/helpers/_files/disappearing_whitespace.txt
create mode 100644 test/helpers/_files/dot_notation.js
create mode 100644 test/helpers/_files/dot_notation.mustache
create mode 100644 test/helpers/_files/dot_notation.txt
create mode 100644 test/helpers/_files/double_render.js
create mode 100644 test/helpers/_files/double_render.mustache
create mode 100644 test/helpers/_files/double_render.txt
create mode 100644 test/helpers/_files/empty_list.js
create mode 100644 test/helpers/_files/empty_list.mustache
create mode 100644 test/helpers/_files/empty_list.txt
create mode 100644 test/helpers/_files/empty_sections.js
create mode 100644 test/helpers/_files/empty_sections.mustache
create mode 100644 test/helpers/_files/empty_sections.txt
create mode 100644 test/helpers/_files/empty_string.js
create mode 100644 test/helpers/_files/empty_string.mustache
create mode 100644 test/helpers/_files/empty_string.txt
create mode 100644 test/helpers/_files/empty_template.js
create mode 100644 test/helpers/_files/empty_template.mustache
create mode 100644 test/helpers/_files/empty_template.txt
create mode 100644 test/helpers/_files/error_not_found.js
create mode 100644 test/helpers/_files/error_not_found.mustache
create mode 100644 test/helpers/_files/error_not_found.txt
create mode 100644 test/helpers/_files/escaped.js
create mode 100644 test/helpers/_files/escaped.mustache
create mode 100644 test/helpers/_files/escaped.txt
create mode 100644 test/helpers/_files/falsy.js
create mode 100644 test/helpers/_files/falsy.mustache
create mode 100644 test/helpers/_files/falsy.txt
create mode 100644 test/helpers/_files/falsy_array.js
create mode 100644 test/helpers/_files/falsy_array.mustache
create mode 100644 test/helpers/_files/falsy_array.txt
create mode 100644 test/helpers/_files/grandparent_context.js
create mode 100644 test/helpers/_files/grandparent_context.mustache
create mode 100644 test/helpers/_files/grandparent_context.txt
create mode 100644 test/helpers/_files/higher_order_sections.js
create mode 100644 test/helpers/_files/higher_order_sections.mustache
create mode 100644 test/helpers/_files/higher_order_sections.txt
create mode 100644 test/helpers/_files/implicit_iterator.js
create mode 100644 test/helpers/_files/implicit_iterator.mustache
create mode 100644 test/helpers/_files/implicit_iterator.txt
create mode 100644 test/helpers/_files/included_tag.js
create mode 100644 test/helpers/_files/included_tag.mustache
create mode 100644 test/helpers/_files/included_tag.txt
create mode 100644 test/helpers/_files/inverted_section.js
create mode 100644 test/helpers/_files/inverted_section.mustache
create mode 100644 test/helpers/_files/inverted_section.txt
create mode 100644 test/helpers/_files/keys_with_questionmarks.js
create mode 100644 test/helpers/_files/keys_with_questionmarks.mustache
create mode 100644 test/helpers/_files/keys_with_questionmarks.txt
create mode 100644 test/helpers/_files/malicious_template.js
create mode 100644 test/helpers/_files/malicious_template.mustache
create mode 100644 test/helpers/_files/malicious_template.txt
create mode 100644 test/helpers/_files/multiline_comment.js
create mode 100644 test/helpers/_files/multiline_comment.mustache
create mode 100644 test/helpers/_files/multiline_comment.txt
create mode 100644 test/helpers/_files/nested_dot.js
create mode 100644 test/helpers/_files/nested_dot.mustache
create mode 100644 test/helpers/_files/nested_dot.txt
create mode 100644 test/helpers/_files/nested_higher_order_sections.js
create mode 100644 test/helpers/_files/nested_higher_order_sections.mustache
create mode 100644 test/helpers/_files/nested_higher_order_sections.txt
create mode 100644 test/helpers/_files/nested_iterating.js
create mode 100644 test/helpers/_files/nested_iterating.mustache
create mode 100644 test/helpers/_files/nested_iterating.txt
create mode 100644 test/helpers/_files/nesting.js
create mode 100644 test/helpers/_files/nesting.mustache
create mode 100644 test/helpers/_files/nesting.txt
create mode 100644 test/helpers/_files/nesting_same_name.js
create mode 100644 test/helpers/_files/nesting_same_name.mustache
create mode 100644 test/helpers/_files/nesting_same_name.txt
create mode 100644 test/helpers/_files/null_lookup_array.js
create mode 100644 test/helpers/_files/null_lookup_array.mustache
create mode 100644 test/helpers/_files/null_lookup_array.txt
create mode 100644 test/helpers/_files/null_lookup_object.js
create mode 100644 test/helpers/_files/null_lookup_object.mustache
create mode 100644 test/helpers/_files/null_lookup_object.txt
create mode 100644 test/helpers/_files/null_string.js
create mode 100644 test/helpers/_files/null_string.mustache
create mode 100644 test/helpers/_files/null_string.txt
create mode 100644 test/helpers/_files/null_view.js
create mode 100644 test/helpers/_files/null_view.mustache
create mode 100644 test/helpers/_files/null_view.txt
create mode 100644 test/helpers/_files/partial_array.js
create mode 100644 test/helpers/_files/partial_array.mustache
create mode 100644 test/helpers/_files/partial_array.partial
create mode 100644 test/helpers/_files/partial_array.txt
create mode 100644 test/helpers/_files/partial_array_of_partials.js
create mode 100644 test/helpers/_files/partial_array_of_partials.mustache
create mode 100644 test/helpers/_files/partial_array_of_partials.partial
create mode 100644 test/helpers/_files/partial_array_of_partials.txt
create mode 100644 test/helpers/_files/partial_array_of_partials_implicit.js
create mode 100644 test/helpers/_files/partial_array_of_partials_implicit.mustache
create mode 100644 test/helpers/_files/partial_array_of_partials_implicit.partial
create mode 100644 test/helpers/_files/partial_array_of_partials_implicit.txt
create mode 100644 test/helpers/_files/partial_empty.js
create mode 100644 test/helpers/_files/partial_empty.mustache
create mode 100644 test/helpers/_files/partial_empty.partial
create mode 100644 test/helpers/_files/partial_empty.txt
create mode 100644 test/helpers/_files/partial_template.js
create mode 100644 test/helpers/_files/partial_template.mustache
create mode 100644 test/helpers/_files/partial_template.partial
create mode 100644 test/helpers/_files/partial_template.txt
create mode 100644 test/helpers/_files/partial_view.js
create mode 100644 test/helpers/_files/partial_view.mustache
create mode 100644 test/helpers/_files/partial_view.partial
create mode 100644 test/helpers/_files/partial_view.txt
create mode 100644 test/helpers/_files/partial_whitespace.js
create mode 100644 test/helpers/_files/partial_whitespace.mustache
create mode 100644 test/helpers/_files/partial_whitespace.partial
create mode 100644 test/helpers/_files/partial_whitespace.txt
create mode 100644 test/helpers/_files/recursion_with_same_names.js
create mode 100644 test/helpers/_files/recursion_with_same_names.mustache
create mode 100644 test/helpers/_files/recursion_with_same_names.txt
create mode 100644 test/helpers/_files/reuse_of_enumerables.js
create mode 100644 test/helpers/_files/reuse_of_enumerables.mustache
create mode 100644 test/helpers/_files/reuse_of_enumerables.txt
create mode 100644 test/helpers/_files/section_as_context.js
create mode 100644 test/helpers/_files/section_as_context.mustache
create mode 100644 test/helpers/_files/section_as_context.txt
create mode 100644 test/helpers/_files/section_functions_in_partials.js
create mode 100644 test/helpers/_files/section_functions_in_partials.mustache
create mode 100644 test/helpers/_files/section_functions_in_partials.partial
create mode 100644 test/helpers/_files/section_functions_in_partials.txt
create mode 100644 test/helpers/_files/simple.js
create mode 100644 test/helpers/_files/simple.mustache
create mode 100644 test/helpers/_files/simple.txt
create mode 100644 test/helpers/_files/string_as_context.js
create mode 100644 test/helpers/_files/string_as_context.mustache
create mode 100644 test/helpers/_files/string_as_context.txt
create mode 100644 test/helpers/_files/two_in_a_row.js
create mode 100644 test/helpers/_files/two_in_a_row.mustache
create mode 100644 test/helpers/_files/two_in_a_row.txt
create mode 100644 test/helpers/_files/two_sections.js
create mode 100644 test/helpers/_files/two_sections.mustache
create mode 100644 test/helpers/_files/two_sections.txt
create mode 100644 test/helpers/_files/unescaped.js
create mode 100644 test/helpers/_files/unescaped.mustache
create mode 100644 test/helpers/_files/unescaped.txt
create mode 100644 test/helpers/_files/uses_props_from_view_prototype.js
create mode 100644 test/helpers/_files/uses_props_from_view_prototype.mustache
create mode 100644 test/helpers/_files/uses_props_from_view_prototype.txt
create mode 100644 test/helpers/_files/whitespace.js
create mode 100644 test/helpers/_files/whitespace.mustache
create mode 100644 test/helpers/_files/whitespace.txt
create mode 100644 test/helpers/_files/zero_view.js
create mode 100644 test/helpers/_files/zero_view.mustache
create mode 100644 test/helpers/_files/zero_view.txt
create mode 100644 test/helpers/render-helper.ts
create mode 100644 test/parse.test.ts
create mode 100644 test/partial.test.ts
create mode 100644 test/render.test.ts
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..28661dd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,33 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: j3lte
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Runtime info:**
+- [ ] Deno
+- [ ] Node
+ - OS: [e.g. iOS]
+ - Version [e.g. 22]
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml
new file mode 100644
index 0000000..1dc6d37
--- /dev/null
+++ b/.github/workflows/npm.yml
@@ -0,0 +1,35 @@
+name: Build NPM package (On Demand)
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Release version (x.y.z):"
+ required: true
+
+jobs:
+ release:
+ name: Build NPM package (On Demand)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup Deno
+ uses: denoland/setup-deno@v1
+
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Run Deno dnt
+ run: deno task npm ${{ github.event.inputs.version }}
+
+ - name: Check Version
+ run: cat ./npm/package.json | jq .version
+
+ # - name: Publish to NPM
+ # run: cd ./npm && yarn publish --verbose --access public --new-version ${{ github.event.inputs.version }}
+ # env:
+ # NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..8627fbf
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,34 @@
+name: Build NPM package (On Release)
+
+on:
+ release:
+ types:
+ - published
+
+jobs:
+ release:
+ name: Build NPM package (On Demand)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup Deno
+ uses: denoland/setup-deno@v1
+
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Run Deno dnt
+ run: deno task npm ${{ github.ref_name }}
+
+ - name: Check Version
+ run: cat ./npm/package.json | jq .version
+
+ # - name: Publish to NPM
+ # run: cd ./npm && yarn publish --verbose --access public --new-version ${{ github.ref_name }}
+ # env:
+ # NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..0a1dfd0
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,42 @@
+name: Deno CI (test)
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ name: Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup Deno
+ uses: denoland/setup-deno@v1
+
+ - name: Check format
+ run: deno fmt --check
+
+ - name: Check linting
+ run: deno lint
+
+ - name: Check license
+ run: deno task check:license
+
+ - name: Tests
+ run: deno task test
+
+ - name: Coverage
+ run: deno task coverage
+
+ # - name: Upload coverage reports to Codecov
+ # uses: codecov/codecov-action@v3
+ # with:
+ # token: ${{ secrets.CODECOV_TOKEN }}
+ # file: ./.coverage/coverage.lcov
+ # flags: unittests
+ # name: codecov-umbrella
+ # fail_ci_if_error: true
diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml
new file mode 100644
index 0000000..43761a0
--- /dev/null
+++ b/.github/workflows/version.yml
@@ -0,0 +1,35 @@
+name: Update version (On Demand)
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version (x.y.z):"
+ required: true
+permissions:
+ contents: write
+
+jobs:
+ release:
+ name: Update version (On Demand)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Setup Deno
+ uses: denoland/setup-deno@v1
+ with:
+ deno-version: v1.38.2
+
+ - name: Run Update Version
+ run: deno task update:version ${{ github.event.inputs.version }}
+
+ - name: Commit changes
+ id: auto-commit-action
+ uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: Update Version (On Demand)
+ commit_options: '--no-verify'
+ # tagging_message: ${{ github.event.inputs.version }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb139ab
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.env*
+*.json
+!deno.json
+.DS_Store
+node_modules
+npm/
+_local_testing.ts
+.coverage/
+coverage.lcov
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..73eb6ca
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright © J.W. Lagendijk 2023. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8d25644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+# mustachio
+
+[![Build Status](https://travis-ci.org/j3lte/mustachio.svg?branch=master)](https://travis-ci.org/j3lte/mustachio)
+[![Coverage Status](https://coveralls.io/repos/github/j3lte/mustachio/badge.svg?branch=master)](https://coveralls.io/github/j3lte/mustachio?branch=master)
+
+> -------------------------------------
+>
+> Work in progress
+>
+> -------------------------------------
+
+## License
+
+[MIT](LICENSE)
+
+---
+
+[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/j3lte)
diff --git a/deno.json b/deno.json
new file mode 100644
index 0000000..ffa8f28
--- /dev/null
+++ b/deno.json
@@ -0,0 +1,50 @@
+{
+ "tasks": {
+ "check:license": "deno run -A ./scripts/check-license.ts --check",
+ "format": "deno fmt ./src/ ./test/*.ts ./test/helpers/*.ts",
+ "lint": "deno lint ./src/ ./test/*.ts ./test/helpers/*.ts",
+ "test": "deno test --allow-read --coverage=.coverage",
+ "coverage": "deno coverage .coverage --lcov --exclude=/src/node/ --exclude=/test/ --exclude=/scripts/ > ./.coverage/coverage.lcov",
+ "test:watch": "deno test --watch",
+ "update:version": "deno run --allow-read --allow-write ./scripts/update-version.ts",
+ "update:deno_deps": "deno run -A https://deno.land/x/udd/main.ts dev_deps.ts ./scripts/build-npm.ts ./src/lib/Pastebin.ts ./src/lib/Scraper.ts",
+ "update:deps": "deno task update:deno_deps",
+ "localTest": "deno run --allow-read --allow-write --allow-run ./scripts/watch-test.ts",
+ "clean": "rm -r ./coverage",
+ "npm": "deno run -A ./scripts/build-npm.ts"
+ },
+ "fmt": {
+ "indentWidth": 2,
+ "lineWidth": 100,
+ "singleQuote": false,
+ "useTabs": false,
+ "proseWrap": "preserve",
+ "exclude": [
+ ".coverage/",
+ "npm/",
+ "./test/helpers/_files/",
+ ".github/",
+ "README.md"
+ ]
+ },
+ "lint": {
+ "rules": {
+ "include": [
+ "ban-untagged-todo",
+ "explicit-function-return-type"
+ ]
+ },
+ "exclude": [
+ "./_local_testing.ts",
+ "./test/helpers/_files/",
+ "npm/"
+ ]
+ },
+ "test": {
+ "exclude": [
+ "npm/",
+ "src/node/",
+ ".coverage/"
+ ]
+ }
+}
diff --git a/deno.lock b/deno.lock
new file mode 100644
index 0000000..c4e70ea
--- /dev/null
+++ b/deno.lock
@@ -0,0 +1,204 @@
+{
+ "version": "3",
+ "remote": {
+ "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
+ "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49",
+ "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
+ "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9",
+ "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
+ "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
+ "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f",
+ "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d",
+ "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
+ "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3",
+ "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09",
+ "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b",
+ "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633",
+ "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee",
+ "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d",
+ "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44",
+ "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
+ "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757",
+ "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21",
+ "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
+ "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
+ "https://deno.land/std@0.181.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
+ "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32",
+ "https://deno.land/std@0.181.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688",
+ "https://deno.land/std@0.181.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40",
+ "https://deno.land/std@0.181.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a",
+ "https://deno.land/std@0.181.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32",
+ "https://deno.land/std@0.181.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
+ "https://deno.land/std@0.181.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
+ "https://deno.land/std@0.181.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
+ "https://deno.land/std@0.181.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
+ "https://deno.land/std@0.181.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
+ "https://deno.land/std@0.181.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c",
+ "https://deno.land/std@0.181.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
+ "https://deno.land/std@0.181.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
+ "https://deno.land/std@0.181.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
+ "https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9",
+ "https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48",
+ "https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
+ "https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee",
+ "https://deno.land/std@0.208.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c",
+ "https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9",
+ "https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227",
+ "https://deno.land/std@0.208.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7",
+ "https://deno.land/std@0.208.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6",
+ "https://deno.land/std@0.208.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63",
+ "https://deno.land/std@0.208.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c",
+ "https://deno.land/std@0.208.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c",
+ "https://deno.land/std@0.208.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b",
+ "https://deno.land/std@0.208.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4",
+ "https://deno.land/std@0.208.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848",
+ "https://deno.land/std@0.208.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b",
+ "https://deno.land/std@0.208.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754",
+ "https://deno.land/std@0.208.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22",
+ "https://deno.land/std@0.208.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0",
+ "https://deno.land/std@0.208.0/assert/assert_not_strict_equals.ts": "4cdef83df17488df555c8aac1f7f5ec2b84ad161b6d0645ccdbcc17654e80c99",
+ "https://deno.land/std@0.208.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54",
+ "https://deno.land/std@0.208.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057",
+ "https://deno.land/std@0.208.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265",
+ "https://deno.land/std@0.208.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c",
+ "https://deno.land/std@0.208.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd",
+ "https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56",
+ "https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece",
+ "https://deno.land/std@0.208.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278",
+ "https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085",
+ "https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a",
+ "https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536",
+ "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2",
+ "https://deno.land/std@0.208.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978",
+ "https://deno.land/std@0.208.0/fs/copy.ts": "ca19e4837965914471df38fbd61e16f9e8adfe89f9cffb0c83615c83ea3fc2bf",
+ "https://deno.land/std@0.208.0/fs/empty_dir.ts": "7fba29ef2d03f3503cd512616efc0535984cf1bbe7ca9d098e8b4d0d88910120",
+ "https://deno.land/std@0.208.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40",
+ "https://deno.land/std@0.208.0/fs/ensure_file.ts": "39ac83cc283a20ec2735e956adf5de3e8a3334e0b6820547b5772f71c49ae083",
+ "https://deno.land/std@0.208.0/fs/ensure_link.ts": "c15e69c48556d78aae31b83e0c0ece04b7b8bc0951412f5b759aceb6fde7f0ac",
+ "https://deno.land/std@0.208.0/fs/ensure_symlink.ts": "b389c8568f0656d145ac7ece472afe710815cccbb2ebfd19da7978379ae143fe",
+ "https://deno.land/std@0.208.0/fs/eol.ts": "8565e1e076c5baced170236617150a7833668658e000205d896fc54084309ce1",
+ "https://deno.land/std@0.208.0/fs/exists.ts": "cb59a853d84871d87acab0e7936a4dac11282957f8e195102c5a7acb42546bb8",
+ "https://deno.land/std@0.208.0/fs/expand_glob.ts": "4f98c508fc9e40d6311d2f7fd88aaad05235cc506388c22dda315e095305811d",
+ "https://deno.land/std@0.208.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898",
+ "https://deno.land/std@0.208.0/fs/move.ts": "b4f8f46730b40c32ea3c0bc8eb0fd0e8139249a698883c7b3756424cf19785c9",
+ "https://deno.land/std@0.208.0/fs/walk.ts": "c1e6b43f72a46e89b630140308bd51a4795d416a416b4cfb7cd4bd1e25946723",
+ "https://deno.land/std@0.208.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946",
+ "https://deno.land/std@0.208.0/path/_common/basename.ts": "0d978ff818f339cd3b1d09dc914881f4d15617432ae519c1b8fdc09ff8d3789a",
+ "https://deno.land/std@0.208.0/path/_common/common.ts": "9e4233b2eeb50f8b2ae10ecc2108f58583aea6fd3e8907827020282dc2b76143",
+ "https://deno.land/std@0.208.0/path/_common/constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
+ "https://deno.land/std@0.208.0/path/_common/dirname.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397",
+ "https://deno.land/std@0.208.0/path/_common/format.ts": "11aa62e316dfbf22c126917f5e03ea5fe2ee707386555a8f513d27ad5756cf96",
+ "https://deno.land/std@0.208.0/path/_common/from_file_url.ts": "ef1bf3197d2efbf0297a2bdbf3a61d804b18f2bcce45548ae112313ec5be3c22",
+ "https://deno.land/std@0.208.0/path/_common/glob_to_reg_exp.ts": "5c3c2b79fc2294ec803d102bd9855c451c150021f452046312819fbb6d4dc156",
+ "https://deno.land/std@0.208.0/path/_common/normalize.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397",
+ "https://deno.land/std@0.208.0/path/_common/normalize_string.ts": "88c472f28ae49525f9fe82de8c8816d93442d46a30d6bb5063b07ff8a89ff589",
+ "https://deno.land/std@0.208.0/path/_common/relative.ts": "1af19d787a2a84b8c534cc487424fe101f614982ae4851382c978ab2216186b4",
+ "https://deno.land/std@0.208.0/path/_common/strip_trailing_separators.ts": "7ffc7c287e97bdeeee31b155828686967f222cd73f9e5780bfe7dfb1b58c6c65",
+ "https://deno.land/std@0.208.0/path/_common/to_file_url.ts": "a8cdd1633bc9175b7eebd3613266d7c0b6ae0fb0cff24120b6092ac31662f9ae",
+ "https://deno.land/std@0.208.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
+ "https://deno.land/std@0.208.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2",
+ "https://deno.land/std@0.208.0/path/basename.ts": "04bb5ef3e86bba8a35603b8f3b69537112cdd19ce64b77f2522006da2977a5f3",
+ "https://deno.land/std@0.208.0/path/common.ts": "f4d061c7d0b95a65c2a1a52439edec393e906b40f1caf4604c389fae7caa80f5",
+ "https://deno.land/std@0.208.0/path/dirname.ts": "88a0a71c21debafc4da7a4cd44fd32e899462df458fbca152390887d41c40361",
+ "https://deno.land/std@0.208.0/path/extname.ts": "2da4e2490f3b48b7121d19fb4c91681a5e11bd6bd99df4f6f47d7a71bb6ecdf2",
+ "https://deno.land/std@0.208.0/path/format.ts": "3457530cc85d1b4bab175f9ae73998b34fd456c830d01883169af0681b8894fb",
+ "https://deno.land/std@0.208.0/path/from_file_url.ts": "e7fa233ea1dff9641e8d566153a24d95010110185a6f418dd2e32320926043f8",
+ "https://deno.land/std@0.208.0/path/glob.ts": "a00a81a55c02bbe074ab21a50b6495c6f7795f54cd718c824adaa92c6c9b7419",
+ "https://deno.land/std@0.208.0/path/glob_to_regexp.ts": "74d7448c471e293d03f05ccb968df4365fed6aaa508506b6325a8efdc01d8271",
+ "https://deno.land/std@0.208.0/path/is_absolute.ts": "67232b41b860571c5b7537f4954c88d86ae2ba45e883ee37d3dec27b74909d13",
+ "https://deno.land/std@0.208.0/path/is_glob.ts": "567dce5c6656bdedfc6b3ee6c0833e1e4db2b8dff6e62148e94a917f289c06ad",
+ "https://deno.land/std@0.208.0/path/join.ts": "98d3d76c819af4a11a81d5ba2dbb319f1ce9d63fc2b615597d4bcfddd4a89a09",
+ "https://deno.land/std@0.208.0/path/join_globs.ts": "9b84d5103b63d3dbed4b2cf8b12477b2ad415c7d343f1488505162dc0e5f4db8",
+ "https://deno.land/std@0.208.0/path/mod.ts": "3defabebc98279e62b392fee7a6937adc932a8f4dcd2471441e36c15b97b00e0",
+ "https://deno.land/std@0.208.0/path/normalize.ts": "aa95be9a92c7bd4f9dc0ba51e942a1973e2b93d266cd74f5ca751c136d520b66",
+ "https://deno.land/std@0.208.0/path/normalize_glob.ts": "674baa82e1c00b6cb153bbca36e06f8e0337cb8062db6d905ab5de16076ca46b",
+ "https://deno.land/std@0.208.0/path/parse.ts": "d87ff0deef3fb495bc0d862278ff96da5a06acf0625ca27769fc52ac0d3d6ece",
+ "https://deno.land/std@0.208.0/path/posix/_util.ts": "ecf49560fedd7dd376c6156cc5565cad97c1abe9824f4417adebc7acc36c93e5",
+ "https://deno.land/std@0.208.0/path/posix/basename.ts": "a630aeb8fd8e27356b1823b9dedd505e30085015407caa3396332752f6b8406a",
+ "https://deno.land/std@0.208.0/path/posix/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b",
+ "https://deno.land/std@0.208.0/path/posix/dirname.ts": "f48c9c42cc670803b505478b7ef162c7cfa9d8e751b59d278b2ec59470531472",
+ "https://deno.land/std@0.208.0/path/posix/extname.ts": "ee7f6571a9c0a37f9218fbf510c440d1685a7c13082c348d701396cc795e0be0",
+ "https://deno.land/std@0.208.0/path/posix/format.ts": "b94876f77e61bfe1f147d5ccb46a920636cd3cef8be43df330f0052b03875968",
+ "https://deno.land/std@0.208.0/path/posix/from_file_url.ts": "b97287a83e6407ac27bdf3ab621db3fccbf1c27df0a1b1f20e1e1b5acf38a379",
+ "https://deno.land/std@0.208.0/path/posix/glob_to_regexp.ts": "6ed00c71fbfe0ccc35977c35444f94e82200b721905a60bd1278b1b768d68b1a",
+ "https://deno.land/std@0.208.0/path/posix/is_absolute.ts": "159900a3422d11069d48395568217eb7fc105ceda2683d03d9b7c0f0769e01b8",
+ "https://deno.land/std@0.208.0/path/posix/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f",
+ "https://deno.land/std@0.208.0/path/posix/join.ts": "0c0d84bdc344876930126640011ec1b888e6facf74153ffad9ef26813aa2a076",
+ "https://deno.land/std@0.208.0/path/posix/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121",
+ "https://deno.land/std@0.208.0/path/posix/mod.ts": "f1b08a7f64294b7de87fc37190d63b6ce5b02889af9290c9703afe01951360ae",
+ "https://deno.land/std@0.208.0/path/posix/normalize.ts": "11de90a94ab7148cc46e5a288f7d732aade1d616bc8c862f5560fa18ff987b4b",
+ "https://deno.land/std@0.208.0/path/posix/normalize_glob.ts": "10a1840c628ebbab679254d5fa1c20e59106102354fb648a1765aed72eb9f3f9",
+ "https://deno.land/std@0.208.0/path/posix/parse.ts": "199208f373dd93a792e9c585352bfc73a6293411bed6da6d3bc4f4ef90b04c8e",
+ "https://deno.land/std@0.208.0/path/posix/relative.ts": "e2f230608b0f083e6deaa06e063943e5accb3320c28aef8d87528fbb7fe6504c",
+ "https://deno.land/std@0.208.0/path/posix/resolve.ts": "51579d83159d5c719518c9ae50812a63959bbcb7561d79acbdb2c3682236e285",
+ "https://deno.land/std@0.208.0/path/posix/separator.ts": "0b6573b5f3269a3164d8edc9cefc33a02dd51003731c561008c8bb60220ebac1",
+ "https://deno.land/std@0.208.0/path/posix/to_file_url.ts": "08d43ea839ee75e9b8b1538376cfe95911070a655cd312bc9a00f88ef14967b6",
+ "https://deno.land/std@0.208.0/path/posix/to_namespaced_path.ts": "c9228a0e74fd37e76622cd7b142b8416663a9b87db643302fa0926b5a5c83bdc",
+ "https://deno.land/std@0.208.0/path/relative.ts": "23d45ede8b7ac464a8299663a43488aad6b561414e7cbbe4790775590db6349c",
+ "https://deno.land/std@0.208.0/path/resolve.ts": "5b184efc87155a0af9fa305ff68a109e28de9aee81fc3e77cd01380f19daf867",
+ "https://deno.land/std@0.208.0/path/separator.ts": "40a3e9a4ad10bef23bc2cd6c610291b6c502a06237c2c4cd034a15ca78dedc1f",
+ "https://deno.land/std@0.208.0/path/to_file_url.ts": "edaafa089e0bce386e1b2d47afe7c72e379ff93b28a5829a5885e4b6c626d864",
+ "https://deno.land/std@0.208.0/path/to_namespaced_path.ts": "cf8734848aac3c7527d1689d2adf82132b1618eff3cc523a775068847416b22a",
+ "https://deno.land/std@0.208.0/path/windows/_util.ts": "f32b9444554c8863b9b4814025c700492a2b57ff2369d015360970a1b1099d54",
+ "https://deno.land/std@0.208.0/path/windows/basename.ts": "8a9dbf7353d50afbc5b221af36c02a72c2d1b2b5b9f7c65bf6a5a2a0baf88ad3",
+ "https://deno.land/std@0.208.0/path/windows/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b",
+ "https://deno.land/std@0.208.0/path/windows/dirname.ts": "5c2aa541384bf0bd9aca821275d2a8690e8238fa846198ef5c7515ce31a01a94",
+ "https://deno.land/std@0.208.0/path/windows/extname.ts": "07f4fa1b40d06a827446b3e3bcc8d619c5546b079b8ed0c77040bbef716c7614",
+ "https://deno.land/std@0.208.0/path/windows/format.ts": "343019130d78f172a5c49fdc7e64686a7faf41553268961e7b6c92a6d6548edf",
+ "https://deno.land/std@0.208.0/path/windows/from_file_url.ts": "d53335c12b0725893d768be3ac6bf0112cc5b639d2deb0171b35988493b46199",
+ "https://deno.land/std@0.208.0/path/windows/glob_to_regexp.ts": "290755e18ec6c1a4f4d711c3390537358e8e3179581e66261a0cf348b1a13395",
+ "https://deno.land/std@0.208.0/path/windows/is_absolute.ts": "245b56b5f355ede8664bd7f080c910a97e2169972d23075554ae14d73722c53c",
+ "https://deno.land/std@0.208.0/path/windows/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f",
+ "https://deno.land/std@0.208.0/path/windows/join.ts": "e6600bf88edeeef4e2276e155b8de1d5dec0435fd526ba2dc4d37986b2882f16",
+ "https://deno.land/std@0.208.0/path/windows/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121",
+ "https://deno.land/std@0.208.0/path/windows/mod.ts": "d7040f461465c2c21c1c68fc988ef0bdddd499912138cde3abf6ad60c7fb3814",
+ "https://deno.land/std@0.208.0/path/windows/normalize.ts": "9deebbf40c81ef540b7b945d4ccd7a6a2c5a5992f791e6d3377043031e164e69",
+ "https://deno.land/std@0.208.0/path/windows/normalize_glob.ts": "344ff5ed45430495b9a3d695567291e50e00b1b3b04ea56712a2acf07ab5c128",
+ "https://deno.land/std@0.208.0/path/windows/parse.ts": "120faf778fe1f22056f33ded069b68e12447668fcfa19540c0129561428d3ae5",
+ "https://deno.land/std@0.208.0/path/windows/relative.ts": "026855cd2c36c8f28f1df3c6fbd8f2449a2aa21f48797a74700c5d872b86d649",
+ "https://deno.land/std@0.208.0/path/windows/resolve.ts": "5ff441ab18a2346abadf778121128ee71bda4d0898513d4639a6ca04edca366b",
+ "https://deno.land/std@0.208.0/path/windows/separator.ts": "ae21f27015f10510ed1ac4a0ba9c4c9c967cbdd9d9e776a3e4967553c397bd5d",
+ "https://deno.land/std@0.208.0/path/windows/to_file_url.ts": "8e9ea9e1ff364aa06fa72999204229952d0a279dbb876b7b838b2b2fea55cce3",
+ "https://deno.land/std@0.208.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d",
+ "https://deno.land/std@0.208.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c",
+ "https://deno.land/std@0.208.0/testing/bdd.ts": "c41f019786c4a9112aadb7e5a7bbcc711f58429ac5904b3855fa248ba5fa0ba6",
+ "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5",
+ "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff",
+ "https://deno.land/x/deno_cache@0.6.2/auth_tokens.ts": "5d1d56474c54a9d152e44d43ea17c2e6a398dd1e9682c69811a313567c01ee1e",
+ "https://deno.land/x/deno_cache@0.6.2/cache.ts": "58b53c128b742757efcad10af9a3871f23b4e200674cb5b0ddf61164fb9b2fe7",
+ "https://deno.land/x/deno_cache@0.6.2/deno_dir.ts": "1ea355b8ba11c630d076b222b197cfc937dd81e5a4a260938997da99e8ff93a0",
+ "https://deno.land/x/deno_cache@0.6.2/deps.ts": "12cca94516cf2d3ed42fccd4b721ecd8060679253f077d83057511045b0081aa",
+ "https://deno.land/x/deno_cache@0.6.2/dirs.ts": "009c6f54e0b610914d6ce9f72f6f6ccfffd2d47a79a19061e0a9eb4253836069",
+ "https://deno.land/x/deno_cache@0.6.2/disk_cache.ts": "66a1e604a8d564b6dd0500326cac33d08b561d331036bf7272def80f2f7952aa",
+ "https://deno.land/x/deno_cache@0.6.2/file_fetcher.ts": "4f3e4a2c78a5ca1e4812099e5083f815a8525ab20d389b560b3517f6b1161dd6",
+ "https://deno.land/x/deno_cache@0.6.2/http_cache.ts": "407135eaf2802809ed373c230d57da7ef8dff923c4abf205410b9b99886491fd",
+ "https://deno.land/x/deno_cache@0.6.2/lib/deno_cache_dir.generated.js": "59f8defac32e8ebf2a30f7bc77e9d88f0e60098463fb1b75e00b9791a4bbd733",
+ "https://deno.land/x/deno_cache@0.6.2/lib/snippets/deno_cache_dir-a2aecaa9536c9402/fs.js": "cbe3a976ed63c72c7cb34ef845c27013033a3b11f9d8d3e2c4aa5dda2c0c7af6",
+ "https://deno.land/x/deno_cache@0.6.2/mod.ts": "b4004287e1c6123d7f07fe9b5b3e94ce6d990c4102949a89c527c68b19627867",
+ "https://deno.land/x/deno_cache@0.6.2/util.ts": "f3f5a0cfc60051f09162942fb0ee87a0e27b11a12aec4c22076e3006be4cc1e2",
+ "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66",
+ "https://deno.land/x/dnt@0.39.0/lib/compiler.ts": "7f4447531581896348b8a379ab94730856b42ae50d99043f2468328360293cb1",
+ "https://deno.land/x/dnt@0.39.0/lib/compiler_transforms.ts": "f21aba052f5dcf0b0595c734450842855c7f572e96165d3d34f8fed2fc1f7ba1",
+ "https://deno.land/x/dnt@0.39.0/lib/mod.deps.ts": "8d6123c8e1162037e58aa8126686a03d1e2cffb250a8757bf715f80242097597",
+ "https://deno.land/x/dnt@0.39.0/lib/npm_ignore.ts": "57fbb7e7b935417d225eec586c6aa240288905eb095847d3f6a88e290209df4e",
+ "https://deno.land/x/dnt@0.39.0/lib/package_json.ts": "607b0a4f44acad071a4c8533b312a27d6671eac8e6a23625c8350ce29eadb2ba",
+ "https://deno.land/x/dnt@0.39.0/lib/pkg/dnt_wasm.generated.js": "4f9c59b3ca6c875adabb10df256e273fff1129fca3a1557eb8936bddd7da7b18",
+ "https://deno.land/x/dnt@0.39.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "aba69a019a6da6f084898a6c7b903b8b583bc0dbd82bfb338449cf0b5bce58fd",
+ "https://deno.land/x/dnt@0.39.0/lib/shims.ts": "60fd285ad433c6944544595e7b885eab3eab09253252891380654f4cd3addaaa",
+ "https://deno.land/x/dnt@0.39.0/lib/test_runner/get_test_runner_code.ts": "4dc7a73a13b027341c0688df2b29a4ef102f287c126f134c33f69f0339b46968",
+ "https://deno.land/x/dnt@0.39.0/lib/test_runner/test_runner.ts": "4d0da0500ec427d5f390d9a8d42fb882fbeccc92c92d66b6f2e758606dbd40e6",
+ "https://deno.land/x/dnt@0.39.0/lib/transform.deps.ts": "2e159661e1c5c650de9a573babe0e319349fe493105157307ec2ad2f6a52c94e",
+ "https://deno.land/x/dnt@0.39.0/lib/types.ts": "b8e228b2fac44c2ae902fbb73b1689f6ab889915bd66486c8a85c0c24255f5fb",
+ "https://deno.land/x/dnt@0.39.0/lib/utils.ts": "224f15f33e7226a2fd991e438d0291d7ed8c7889807efa2e1ecb67d2d1db6720",
+ "https://deno.land/x/dnt@0.39.0/mod.ts": "9df36a862161d9eb376472b699f6cb08ba0ad1704e0826fbe13be766bd3c01da",
+ "https://deno.land/x/dnt@0.39.0/transform.ts": "f68743a14cf9bf53bfc9c81073871d69d447a7f9e3453e0447ca2fb78926bb1d",
+ "https://deno.land/x/ts_morph@20.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9",
+ "https://deno.land/x/ts_morph@20.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06",
+ "https://deno.land/x/ts_morph@20.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d",
+ "https://deno.land/x/ts_morph@20.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed",
+ "https://deno.land/x/ts_morph@20.0.0/common/ts_morph_common.js": "2325f94f61dc5f3f98a1dab366dc93048d11b1433d718b10cfc6ee5a1cfebe8f",
+ "https://deno.land/x/ts_morph@20.0.0/common/typescript.js": "b9edf0a451685d13e0467a7ed4351d112b74bd1e256b915a2b941054e31c1736",
+ "https://deno.land/x/wasmbuild@0.15.1/cache.ts": "9d01b5cb24e7f2a942bbd8d14b093751fa690a6cde8e21709ddc97667e6669ed",
+ "https://deno.land/x/wasmbuild@0.15.1/loader.ts": "8c2fc10e21678e42f84c5135d8ab6ab7dc92424c3f05d2354896a29ccfd02a63"
+ }
+}
diff --git a/dev_deps.ts b/dev_deps.ts
new file mode 100644
index 0000000..1639c7a
--- /dev/null
+++ b/dev_deps.ts
@@ -0,0 +1,18 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+export { emptyDir } from "https://deno.land/std@0.208.0/fs/mod.ts";
+export { walk } from "https://deno.land/std@0.208.0/fs/walk.ts";
+export {
+ assert,
+ assertEquals,
+ assertNotEquals,
+ assertStrictEquals,
+ assertThrows,
+} from "https://deno.land/std@0.208.0/assert/mod.ts";
+export {
+ afterEach,
+ beforeAll,
+ beforeEach,
+ describe,
+ it,
+} from "https://deno.land/std@0.208.0/testing/bdd.ts";
diff --git a/mod.ts b/mod.ts
new file mode 100644
index 0000000..10e2ff3
--- /dev/null
+++ b/mod.ts
@@ -0,0 +1,6 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import mustache from "./src/mod.ts";
+export type { Config, Context, Mustache } from "./src/mod.ts";
+
+export default mustache;
diff --git a/scripts/build-npm.ts b/scripts/build-npm.ts
new file mode 100644
index 0000000..97df0bc
--- /dev/null
+++ b/scripts/build-npm.ts
@@ -0,0 +1,71 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { build, emptyDir } from "https://deno.land/x/dnt@0.39.0/mod.ts";
+
+const cleanupTypes = async (dir: string) => {
+ for await (const dirEntry of Deno.readDir(dir)) {
+ const entryPath = `${dir}/${dirEntry.name}`;
+ if (dirEntry.isDirectory) {
+ await cleanupTypes(entryPath);
+ } else {
+ const file = await Deno.readTextFile(entryPath);
+ const newFile = file.replaceAll('.js"', '"');
+ await Deno.writeTextFile(entryPath, newFile);
+ }
+ }
+};
+
+await emptyDir("./npm");
+
+await build({
+ entryPoints: ["./src/mod.ts"],
+ outDir: "./npm",
+ mappings: {},
+ declaration: "separate",
+ skipSourceOutput: true,
+ // scriptModule: false,
+ shims: {
+ // deno: true,
+ },
+ test: false,
+ typeCheck: false,
+ compilerOptions: {
+ importHelpers: true,
+ target: "ES2021",
+ lib: ["ESNext"],
+ },
+ package: {
+ // package.json properties
+ name: "mustachio",
+ version: Deno.args[0] || "1.0.0",
+ description: "Mustache template engine for Deno/Node",
+ license: "MIT",
+ publishConfiig: {
+ access: "public",
+ },
+ keywords: [
+ "mustachio",
+ "mustache",
+ "template",
+ "templates",
+ "ejs",
+ ],
+ author: {
+ name: "J.W. Lagendijk",
+ email: "jwlagendijk@gmail.com",
+ },
+ repository: {
+ type: "git",
+ url: "git+https://github.com/j3lte/mustachio.git",
+ },
+ bugs: {
+ url: "https://github.com/j3lte/mustachio/issues",
+ },
+ },
+ async postBuild(): Promise {
+ // steps to run after building and before running the tests
+ await Deno.copyFile("./LICENSE", "npm/LICENSE");
+ await Deno.copyFile("./README.md", "npm/README.md");
+ await cleanupTypes("./npm/types");
+ },
+});
diff --git a/scripts/check-license.ts b/scripts/check-license.ts
new file mode 100644
index 0000000..2e966c0
--- /dev/null
+++ b/scripts/check-license.ts
@@ -0,0 +1,60 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { walk } from "../dev_deps.ts";
+
+const EXTENSIONS = [".ts"];
+
+const ROOT = new URL("../", import.meta.url);
+const CHECK = Deno.args.includes("--check");
+const CURRENT_YEAR = new Date().getFullYear();
+const RX_COPYRIGHT = new RegExp(
+ `// Copyright ([0-9]{4}) J.W. Lagendijk\\. All rights reserved\\. MIT license\\.\n`,
+);
+const COPYRIGHT = `// Copyright ${CURRENT_YEAR} J.W. Lagendijk. All rights reserved. MIT license.`;
+
+let failed = false;
+
+for await (
+ const { path } of walk(ROOT, {
+ exts: EXTENSIONS,
+ skip: [
+ /\.coverage/,
+ /node_modules/,
+ /npm/,
+ /_local_testing\.ts/,
+ ],
+ includeDirs: false,
+ })
+) {
+ console.log("Checking " + path);
+ const content = await Deno.readTextFile(path);
+ const match = content.match(RX_COPYRIGHT);
+
+ if (!match) {
+ if (CHECK) {
+ console.error(`Missing copyright header: ${path}`);
+ failed = true;
+ } else {
+ const contentWithCopyright = COPYRIGHT + "\n" + content;
+ await Deno.writeTextFile(path, contentWithCopyright);
+ console.log("Copyright header automatically added to " + path);
+ }
+ } else if (parseInt(match[1]) !== CURRENT_YEAR) {
+ if (CHECK) {
+ console.error(`Incorrect copyright year: ${path}`);
+ failed = true;
+ } else {
+ const index = match.index ?? 0;
+ const contentWithoutCopyright = content.replace(match[0], "");
+ const contentWithCopyright = contentWithoutCopyright.substring(0, index) +
+ COPYRIGHT + "\n" + contentWithoutCopyright.substring(index);
+ await Deno.writeTextFile(path, contentWithCopyright);
+ console.log("Copyright header automatically updated in " + path);
+ }
+ }
+}
+
+if (failed) {
+ console.info(`Copyright header should be "${COPYRIGHT}"`);
+ Deno.exit(1);
+}
diff --git a/scripts/update-version.ts b/scripts/update-version.ts
new file mode 100644
index 0000000..868724e
--- /dev/null
+++ b/scripts/update-version.ts
@@ -0,0 +1,29 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+async function update(args: string[]): Promise {
+ const version = args[0];
+ if (!version) {
+ console.error("No version provided.");
+ Deno.exit(1);
+ }
+
+ const filePath = new URL(import.meta.url).pathname;
+ const dirPath = filePath.split("/").slice(0, -1).join("/");
+
+ const paths = [
+ `${dirPath}/../src/lib/Mustache.ts`,
+ ];
+
+ for (const path of paths) {
+ const file = await Deno.readTextFile(path);
+ const updatedFile = file.replace(
+ /static version = ".*";/,
+ `static version = "${version}";`,
+ );
+ await Deno.writeTextFile(path, updatedFile);
+ }
+
+ console.log(`Updated version to ${version}.`);
+}
+
+update(Deno.args);
diff --git a/scripts/watch-test.ts b/scripts/watch-test.ts
new file mode 100644
index 0000000..0b8562a
--- /dev/null
+++ b/scripts/watch-test.ts
@@ -0,0 +1,67 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { emptyDir } from "../dev_deps.ts";
+
+const watcher = Deno.watchFs([
+ "./src/",
+ "./test/",
+], { recursive: true });
+
+const runCmd = async (args: string[], deno = false) => {
+ let cmd: Deno.Command;
+ if (deno) {
+ cmd = new Deno.Command(Deno.execPath(), {
+ args,
+ });
+ } else {
+ cmd = new Deno.Command(args[0], {
+ args: args.slice(1),
+ });
+ }
+ const { code, stdout, stderr } = await cmd.output();
+ return {
+ output: new TextDecoder().decode(stdout),
+ error: new TextDecoder().decode(stderr),
+ code,
+ };
+};
+
+// Debounce runner
+let timeout: number | undefined;
+
+const runTest = (path: string) => {
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ timeout = setTimeout(async () => {
+ console.log(">>>>> runTest", path);
+ await emptyDir("./.coverage/");
+ const test = await runCmd(["test", "--allow-read", "--coverage=./.coverage", "./test/"], true);
+ console.log(test.output);
+
+ if (test.code !== 0) {
+ console.log(test.error);
+ return;
+ }
+ const cov = await runCmd([
+ "coverage",
+ "./.coverage/",
+ "--lcov",
+ "--exclude=/test|scripts/",
+ ], true);
+ await Deno.writeTextFile("./.coverage/coverageFile.lcov", cov.output);
+
+ await runCmd(["genhtml", "-o", "./.coverage/", "./.coverage/coverageFile.lcov"]);
+ }, 100);
+};
+
+for await (const event of watcher) {
+ // console.log(">>>>> event", event);
+ const { kind, paths } = event;
+ if (["modify", "create", "delete"].includes(kind) && paths[0]) {
+ const [path] = paths;
+ if (path.endsWith(".ts")) {
+ runTest(path);
+ }
+ }
+}
diff --git a/src/lib/Context.ts b/src/lib/Context.ts
new file mode 100644
index 0000000..2c4e9d1
--- /dev/null
+++ b/src/lib/Context.ts
@@ -0,0 +1,130 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { hasProperty, isFunction, primitiveHasOwnProperty } from "./util.ts";
+
+/**
+ * Represents a rendering context by wrapping a view object and
+ * maintaining a reference to the parent context.
+ */
+export class Context {
+ #view: Record;
+ private cache: Record;
+ #parent: Context | undefined;
+
+ constructor(view: Record, parentContext?: Context) {
+ this.#view = view;
+ this.cache = { ".": this.#view };
+ this.#parent = parentContext;
+ }
+
+ /**
+ * Creates a new context using the given view with this context
+ * as the parent.
+ */
+ push(view: Record): Context {
+ return new Context(view, this);
+ }
+
+ /**
+ * Returns the value of the given name in this context, traversing
+ * up the context hierarchy if the value is absent in this context's view.
+ */
+ lookup(name: string): unknown {
+ const cache = this.cache;
+
+ let value;
+
+ if (Object.prototype.hasOwnProperty.call(cache, name)) {
+ value = cache[name];
+ } else {
+ // deno-lint-ignore no-this-alias
+ let context: Context | undefined = this;
+ let intermediateValue: object | Record;
+ let names: string[];
+ let index: number;
+ let lookupHit = false;
+
+ while (context) {
+ if (name.indexOf(".") > 0) {
+ intermediateValue = context.view;
+ names = name.split(".");
+ index = 0;
+
+ /**
+ * Using the dot notion path in `name`, we descend through the
+ * nested objects.
+ *
+ * To be certain that the lookup has been successful, we have to
+ * check if the last object in the path actually has the property
+ * we are looking for. We store the result in `lookupHit`.
+ *
+ * This is specially necessary for when the value has been set to
+ * `undefined` and we want to avoid looking up parent contexts.
+ *
+ * In the case where dot notation is used, we consider the lookup
+ * to be successful even if the last "object" in the path is
+ * not actually an object but a primitive (e.g., a string, or an
+ * integer), because it is sometimes useful to access a property
+ * of an autoboxed primitive, such as the length of a string.
+ */
+ while (intermediateValue != null && index < names.length) {
+ if (index === names.length - 1) {
+ lookupHit = hasProperty(intermediateValue, names[index]) ||
+ primitiveHasOwnProperty(intermediateValue, names[index]);
+ }
+
+ // @ts-ignore Need to investigate this error
+ intermediateValue = intermediateValue[names[index++]];
+ }
+ } else {
+ // @ts-ignore Need to investigate this error
+ intermediateValue = context.view[name];
+
+ /**
+ * Only checking against `hasProperty`, which always returns `false` if
+ * `context.view` is not an object. Deliberately omitting the check
+ * against `primitiveHasOwnProperty` if dot notation is not used.
+ *
+ * Consider this example:
+ * ```
+ * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"})
+ * ```
+ *
+ * If we were to check also against `primitiveHasOwnProperty`, as we do
+ * in the dot notation case, then render call would return:
+ *
+ * "The length of a football field is 9."
+ *
+ * rather than the expected:
+ *
+ * "The length of a football field is 100 yards."
+ */
+ lookupHit = hasProperty(context.view, name);
+ }
+
+ if (lookupHit) {
+ value = intermediateValue;
+ break;
+ }
+
+ context = context.parent;
+ }
+
+ cache[name] = value;
+ }
+
+ if (isFunction(value)) {
+ value = value.call(this.#view);
+ }
+
+ return value;
+ }
+
+ get view(): Record {
+ return this.#view;
+ }
+
+ get parent(): Context | undefined {
+ return this.#parent;
+ }
+}
diff --git a/src/lib/Mustache.ts b/src/lib/Mustache.ts
new file mode 100644
index 0000000..dce7408
--- /dev/null
+++ b/src/lib/Mustache.ts
@@ -0,0 +1,67 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { Context } from "./Context.ts";
+import { Config, Writer } from "./Writer.ts";
+import { Token } from "./parse.ts";
+import { escapeHtml, typeStr } from "./util.ts";
+
+export class Mustache {
+ static name = "mustache.deno";
+ static version = "0.1.0";
+ tags = ["{{", "}}"];
+ escape = escapeHtml;
+
+ writer: Writer;
+
+ /**
+ * The name of the module.
+ * `name = "mustache.deno"`
+ */
+ readonly name = Mustache.name;
+ /**
+ * The version of the module.
+ * `version = "1.0.0"`
+ */
+ readonly version = Mustache.version;
+
+ constructor() {
+ this.writer = new Writer();
+ }
+
+ set templateCache(cache: Map | undefined) {
+ this.writer.templateCache = cache;
+ }
+
+ clearCache(): void {
+ return this.writer.clearCache();
+ }
+
+ parse(template: string, tags: string[] = this.tags): Token[] {
+ return this.writer.parse(template, tags);
+ }
+
+ render(
+ template: string,
+ view: Context | unknown,
+ partials: Record = {},
+ config: Partial | string[] = {
+ tags: this.tags,
+ escape: escapeHtml,
+ },
+ ): string {
+ if (typeof template !== "string") {
+ throw new TypeError(
+ 'Invalid template! Template should be a "string" ' +
+ 'but "' +
+ typeStr(template) +
+ '" was given as the first ' +
+ "argument for mustache#render(template, view, partials)",
+ );
+ }
+ return this.writer.render(template, view, partials, config);
+ }
+}
+
+const mustache = new Mustache();
+
+export default mustache;
diff --git a/src/lib/Scanner.ts b/src/lib/Scanner.ts
new file mode 100644
index 0000000..db174d7
--- /dev/null
+++ b/src/lib/Scanner.ts
@@ -0,0 +1,80 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+/**
+ * A simple string scanner that is used by the template parser to find
+ * tokens in template strings.
+ */
+export class Scanner {
+ #string;
+ #tail;
+ #pos = 0;
+
+ constructor(string: string) {
+ this.#string = string;
+ this.#tail = string;
+ }
+
+ /**
+ * Returns `true` if the tail is empty (end of string).
+ */
+ eos(): boolean {
+ return this.#tail === "";
+ }
+
+ /**
+ * Tries to match the given regular expression at the current position.
+ * Returns the matched text if it can match, the empty string otherwise.
+ */
+ scan(re: RegExp): string {
+ const match = this.#tail.match(re);
+
+ if (!match || match.index !== 0) {
+ return "";
+ }
+
+ const string = match[0];
+
+ this.#tail = this.#tail.substring(string.length);
+ this.#pos += string.length;
+
+ return string;
+ }
+
+ /**
+ * Skips all text until the given regular expression can be matched. Returns
+ * the skipped string, which is the entire tail if no match can be made.
+ */
+ scanUntil(re: RegExp): string {
+ const index = this.#tail.search(re);
+ let match;
+
+ switch (index) {
+ case -1:
+ match = this.#tail;
+ this.#tail = "";
+ break;
+ case 0:
+ match = "";
+ break;
+ default:
+ match = this.#tail.substring(0, index);
+ this.#tail = this.#tail.substring(index);
+ }
+
+ this.#pos += match.length;
+
+ return match;
+ }
+
+ get pos(): number {
+ return this.#pos;
+ }
+
+ get string(): string {
+ return this.#string;
+ }
+
+ get tail(): string {
+ return this.#tail;
+ }
+}
diff --git a/src/lib/Writer.ts b/src/lib/Writer.ts
new file mode 100644
index 0000000..5f1c654
--- /dev/null
+++ b/src/lib/Writer.ts
@@ -0,0 +1,281 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { Context } from "./Context.ts";
+import { parseTemplate, Token } from "./parse.ts";
+import { escapeHtml, isArray, isFunction } from "./util.ts";
+
+const defaultTags = ["{{", "}}"];
+
+export type Config = {
+ tags: string[];
+ escape: (value: string) => string;
+};
+
+type Partials = (name: string) => string | Record;
+
+export class Writer {
+ templateCache: Map | undefined = new Map();
+
+ clearCache(): void {
+ if (typeof this.templateCache !== "undefined") {
+ this.templateCache.clear();
+ }
+ }
+
+ /**
+ * Parses and caches the given `template` according to the given `tags` or
+ * `mustache.tags` if `tags` is omitted, and returns the array of tokens
+ * that is generated from the parse.
+ */
+ parse(template: string, tags: string[] = defaultTags): Token[] {
+ const cache = this.templateCache;
+ const cacheKey = template + ":" + tags.join(":");
+ const isCacheEnabled = typeof cache !== "undefined";
+ let tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;
+
+ if (typeof tokens === "undefined") {
+ tokens = parseTemplate(template, tags);
+ isCacheEnabled && cache.set(cacheKey, tokens);
+ }
+ return tokens;
+ }
+
+ /**
+ * High-level method that is used to render the given `template` with
+ * the given `view`.
+ *
+ * The optional `partials` argument may be an object that contains the
+ * names and templates of partials that are used in the template. It may
+ * also be a function that is used to load partial templates on the fly
+ * that takes a single argument: the name of the partial.
+ *
+ * If the optional `config` argument is given here, then it should be an
+ * object with a `tags` attribute or an `escape` attribute or both.
+ * If an array is passed, then it will be interpreted the same way as
+ * a `tags` attribute on a `config` object.
+ *
+ * The `tags` attribute of a `config` object must be an array with two
+ * string values: the opening and closing tags used in the template (e.g.
+ * [ "<%", "%>" ]). The default is to mustache.tags.
+ *
+ * The `escape` attribute of a `config` object must be a function which
+ * accepts a string as input and outputs a safely escaped string.
+ * If an `escape` function is not provided, then an HTML-safe string
+ * escaping function is used as the default.
+ */
+ // render(template, view, partials, config) {
+ render(
+ template: string,
+ view: Context | unknown,
+ partials: Partials | Record,
+ config: Partial | string[],
+ ): string {
+ const tags = this.getConfigTags(config);
+ const tokens = this.parse(template, tags);
+ const context = (view instanceof Context) ? view : new Context(view as Record);
+ return this.renderTokens(tokens, context, partials, template, config);
+ }
+
+ /**
+ * Low-level method that renders the given array of `tokens` using
+ * the given `context` and `partials`.
+ *
+ * Note: The `originalTemplate` is only ever used to extract the portion
+ * of the original template that was contained in a higher-order section.
+ * If the template doesn't use higher-order sections, this argument may
+ * be omitted.
+ */
+ // renderTokens(tokens, context, partials, originalTemplate, config) {
+ renderTokens(
+ tokens: Token[],
+ context: Context,
+ partials: Partials | Record,
+ originalTemplate: string,
+ config: Partial | string[],
+ ): string {
+ let buffer = "";
+
+ // var token, symbol, value;
+ let token: Token;
+ let symbol: string;
+ let value: unknown;
+
+ for (let i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+ value = undefined;
+ token = tokens[i];
+ symbol = token[0];
+
+ if (symbol === "#") {
+ value = this.renderSection(token, context, partials, originalTemplate, config);
+ } else if (symbol === "^") {
+ value = this.renderInverted(token, context, partials, originalTemplate, config);
+ } else if (symbol === ">") value = this.renderPartial(token, context, partials, config);
+ else if (symbol === "&") value = this.unescapedValue(token, context);
+ else if (symbol === "name") value = this.escapedValue(token, context, config);
+ else if (symbol === "text") value = this.rawValue(token);
+
+ if (value !== undefined) {
+ buffer += value;
+ }
+ }
+
+ return buffer;
+ }
+
+ // renderSection(token, context, partials, originalTemplate, config) {
+ renderSection(
+ token: Token,
+ context: Context,
+ partials: Partials | Record,
+ originalTemplate: string,
+ config: Partial | string[],
+ ): string | undefined {
+ let buffer = "";
+ let value = context.lookup(token[1]);
+
+ // This function is used to render an arbitrary template
+ // in the current context by higher-order sections.
+ const subRender = (template: string): string => {
+ return this.render(template, context, partials, config);
+ };
+
+ if (!value) return;
+
+ if (isArray(value)) {
+ for (let j = 0; j < value.length; ++j) {
+ buffer += this.renderTokens(
+ token[4] as Token[],
+ context.push(value[j]),
+ partials,
+ originalTemplate,
+ config,
+ );
+ }
+ } else if (
+ typeof value === "object" || typeof value === "string" || typeof value === "number"
+ ) {
+ buffer += this.renderTokens(
+ token[4] as Token[],
+ context.push(value as Record),
+ partials,
+ originalTemplate,
+ config,
+ );
+ } else if (isFunction(value)) {
+ if (typeof originalTemplate !== "string") {
+ throw new Error("Cannot use higher-order sections without the original template");
+ }
+
+ // Extract the portion of the original template that the section contains.
+ value = value.call(
+ context.view,
+ originalTemplate.slice(token[3], token[5]),
+ subRender,
+ ) as string;
+
+ if (value != null) {
+ buffer += value;
+ }
+ } else {
+ buffer += this.renderTokens(token[4] as Token[], context, partials, originalTemplate, config);
+ }
+ return buffer;
+ }
+
+ renderInverted(
+ token: Token,
+ context: Context,
+ partials: Partials | Record,
+ originalTemplate: string,
+ config: Partial | string[],
+ ): string | undefined {
+ const value = context.lookup(token[1]);
+
+ // Use JavaScript's definition of falsy. Include empty arrays.
+ // See https://github.com/janl/mustache.js/issues/186
+ if (!value || (isArray(value) && value.length === 0)) {
+ return this.renderTokens(token[4] as Token[], context, partials, originalTemplate, config);
+ }
+ }
+
+ indentPartial(partial: string, indentation: string, lineHasNonSpace: boolean): string {
+ const filteredIndentation = indentation.replace(/[^ \t]/g, "");
+ const partialByNl = partial.split("\n");
+ for (let i = 0; i < partialByNl.length; i++) {
+ if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) {
+ partialByNl[i] = filteredIndentation + partialByNl[i];
+ }
+ }
+ return partialByNl.join("\n");
+ }
+
+ renderPartial(
+ token: Token,
+ context: Context,
+ partials: Partials | Record,
+ config: Partial | string[],
+ ): string | undefined {
+ if (!partials) return;
+ const tags = this.getConfigTags(config);
+
+ const value =
+ (isFunction(partials)
+ ? partials(token[1])
+ : partials[token[1]] || partials[token[1] + (tags as string[])[1]]) as string;
+
+ if (value != null) {
+ const lineHasNonSpace = token[6] as boolean;
+ const tagIndex = token[5];
+ const indentation = token[4] as string;
+ let indentedValue = value;
+ if (tagIndex == 0 && indentation) {
+ indentedValue = this.indentPartial(value, indentation, lineHasNonSpace);
+ }
+ const tokens = this.parse(indentedValue, tags);
+ return this.renderTokens(tokens, context, partials, indentedValue, config);
+ }
+ }
+
+ unescapedValue(token: Token, context: Context): string | undefined {
+ const value = context.lookup(token[1]);
+ if (value != null) {
+ return value as string;
+ }
+ }
+
+ escapedValue(
+ token: Token,
+ context: Context,
+ config: Partial | string[],
+ ): string | undefined {
+ const escape = this.getConfigEscape(config) || escapeHtml;
+ const value = context.lookup(token[1]);
+ if (value != null) {
+ return (typeof value === "number" && escape === escapeHtml)
+ ? String(value)
+ : escape(value as string);
+ }
+ }
+
+ rawValue(token: Token): string {
+ return token[1];
+ }
+
+ getConfigTags(config: string[] | Partial): string[] | undefined {
+ if (isArray(config)) {
+ return config;
+ }
+ if (config && typeof config === "object") {
+ return config.tags;
+ }
+ return;
+ }
+
+ getConfigEscape(config: string[] | Partial): ((value: string) => string) | undefined {
+ if (config && typeof config === "object" && !isArray(config)) {
+ return config.escape;
+ } else {
+ return undefined;
+ }
+ }
+}
diff --git a/src/lib/parse.ts b/src/lib/parse.ts
new file mode 100644
index 0000000..e78421a
--- /dev/null
+++ b/src/lib/parse.ts
@@ -0,0 +1,268 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { Scanner } from "./Scanner.ts";
+import { escapeRegExp, isArray, isWhitespace } from "./util.ts";
+
+const whiteRe = /\s*/;
+const spaceRe = /\s+/;
+const equalsRe = /\s*=/;
+const curlyRe = /\s*\}/;
+const tagRe = /#|\^|\/|>|\{|&|=|!/;
+
+export type Token = [string, string, number, number, (string | Token[])?, number?, boolean?];
+
+/**
+ * Forms the given array of `tokens` into a nested tree structure where
+ * tokens that represent a section have two additional items: 1) an array of
+ * all tokens that appear in that section and 2) the index in the original
+ * template that represents the end of that section.
+ */
+function nestTokens(tokens: Token[]): Token[] {
+ const nestedTokens: Token[] = [];
+ let collector: Token[] = nestedTokens;
+ const sections: Token[] = [];
+
+ // var token, section;
+ let token: Token;
+ let section: Token;
+
+ for (let i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+ token = tokens[i];
+
+ switch (token[0]) {
+ case "#":
+ case "^":
+ collector.push(token);
+ sections.push(token);
+ collector = token[4] = [];
+ break;
+ case "/":
+ section = sections.pop() as Token;
+ section[5] = token[2];
+ collector =
+ (sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens) as Token[];
+ break;
+ default:
+ collector.push(token);
+ }
+ }
+
+ return nestedTokens;
+}
+
+/**
+ * Combines the values of consecutive text tokens in the given `tokens` array
+ * to a single token.
+ */
+function squashTokens(tokens: Token[]): Token[] {
+ const squashedTokens: Token[] = [];
+
+ // var token, lastToken;
+ let token: Token;
+ let lastToken: Token | undefined;
+
+ for (let i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+ token = tokens[i];
+
+ if (token) {
+ if (token[0] === "text" && lastToken && lastToken[0] === "text") {
+ lastToken[1] += token[1];
+ lastToken[3] = token[3];
+ } else {
+ squashedTokens.push(token);
+ lastToken = token;
+ }
+ }
+ }
+
+ return squashedTokens;
+}
+
+function compileTags(tagsToCompile: string | string[]): {
+ openingTagRe: RegExp;
+ closingTagRe: RegExp;
+ closingCurlyRe: RegExp;
+} {
+ if (typeof tagsToCompile === "string") {
+ tagsToCompile = tagsToCompile.split(spaceRe, 2);
+ }
+
+ if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) {
+ throw new Error("Invalid tags: " + tagsToCompile);
+ }
+
+ const openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + "\\s*");
+ const closingTagRe = new RegExp("\\s*" + escapeRegExp(tagsToCompile[1]));
+ const closingCurlyRe = new RegExp("\\s*" + escapeRegExp("}" + tagsToCompile[1]));
+
+ return { openingTagRe, closingTagRe, closingCurlyRe };
+}
+
+/**
+ * Breaks up the given `template` string into a tree of tokens. If the `tags`
+ * argument is given here it must be an array with two string values: the
+ * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
+ * course, the default is to use mustaches (i.e. mustache.tags).
+ *
+ * A token is an array with at least 4 elements. The first element is the
+ * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
+ * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
+ * all text that appears outside a symbol this element is "text".
+ *
+ * The second element of a token is its "value". For mustache tags this is
+ * whatever else was inside the tag besides the opening symbol. For text tokens
+ * this is the text itself.
+ *
+ * The third and fourth elements of the token are the start and end indices,
+ * respectively, of the token in the original template.
+ *
+ * Tokens that are the root node of a subtree contain two more elements: 1) an
+ * array of tokens in the subtree and 2) the index in the original template at
+ * which the closing tag for that section begins.
+ *
+ * Tokens for partials also contain two more elements: 1) a string value of
+ * indendation prior to that tag and 2) the index of that tag on that line -
+ * eg a value of 2 indicates the partial is the third tag on this line.
+ */
+
+export function parseTemplate(template: string, tags: string[]): Token[] {
+ if (!template) {
+ return [];
+ }
+ let lineHasNonSpace = false;
+ const sections = []; // Stack to hold section tokens
+ const tokens: Array = []; // Buffer to hold the tokens
+
+ let spaces: number[] = []; // Indices of whitespace tokens on the current line
+ let hasTag = false; // Is there a {{tag}} on the current line?
+ let nonSpace = false; // Is there a non-space char on the current line?
+ let indentation = ""; // Tracks indentation for tags that use it
+ let tagIndex = 0; // Stores a count of number of tags encountered on a line
+
+ // Strips all whitespace tokens array for the current line
+ // if there was a {{#tag}} on it and otherwise only space.
+ function stripSpace(): void {
+ if (hasTag && !nonSpace) {
+ while (spaces.length) {
+ delete tokens[spaces.pop() as number];
+ }
+ } else {
+ spaces = [];
+ }
+
+ hasTag = false;
+ nonSpace = false;
+ }
+
+ let { openingTagRe, closingTagRe, closingCurlyRe } = compileTags(tags || ["{{", "}}"]);
+
+ const scanner = new Scanner(template);
+
+ let start: number;
+ let type: string;
+ let value: string;
+ let chr: string;
+ let token: Token;
+ let openSection;
+
+ while (!scanner.eos()) {
+ start = scanner.pos;
+
+ // Match any text between tags.
+ value = scanner.scanUntil(openingTagRe);
+
+ if (value) {
+ for (let i = 0, valueLength = value.length; i < valueLength; ++i) {
+ chr = value.charAt(i);
+
+ if (isWhitespace(chr)) {
+ spaces.push(tokens.length);
+ indentation += chr;
+ } else {
+ nonSpace = true;
+ lineHasNonSpace = true;
+ indentation += " ";
+ }
+
+ tokens.push(["text", chr, start, start + 1]);
+ start += 1;
+
+ // Check for whitespace on the current line.
+ if (chr === "\n") {
+ stripSpace();
+ indentation = "";
+ tagIndex = 0;
+ lineHasNonSpace = false;
+ }
+ }
+ }
+
+ // Match the opening tag.
+ if (!scanner.scan(openingTagRe)) {
+ break;
+ }
+
+ hasTag = true;
+
+ // Get the tag type.
+ type = scanner.scan(tagRe) || "name";
+ scanner.scan(whiteRe);
+
+ // Get the tag value.
+ if (type === "=") {
+ value = scanner.scanUntil(equalsRe);
+ scanner.scan(equalsRe);
+ scanner.scanUntil(closingTagRe);
+ } else if (type === "{") {
+ value = scanner.scanUntil(closingCurlyRe);
+ scanner.scan(curlyRe);
+ scanner.scanUntil(closingTagRe);
+ type = "&";
+ } else {
+ value = scanner.scanUntil(closingTagRe);
+ }
+
+ // Match the closing tag.
+ if (!scanner.scan(closingTagRe)) {
+ throw new Error("Unclosed tag at " + scanner.pos);
+ }
+
+ if (type == ">") {
+ token = [type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace];
+ } else {
+ token = [type, value, start, scanner.pos];
+ }
+ tagIndex++;
+ tokens.push(token);
+
+ if (type === "#" || type === "^") {
+ sections.push(token);
+ } else if (type === "/") {
+ // Check section nesting.
+ openSection = sections.pop();
+
+ if (!openSection) {
+ throw new Error('Unopened section "' + value + '" at ' + start);
+ }
+
+ if (openSection[1] !== value) {
+ throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
+ }
+ } else if (type === "name" || type === "{" || type === "&") {
+ nonSpace = true;
+ } else if (type === "=") {
+ ({ openingTagRe, closingTagRe, closingCurlyRe } = compileTags(value));
+ }
+ }
+
+ stripSpace();
+
+ // Make sure there are no open sections when we're done.
+ openSection = sections.pop();
+
+ if (openSection) {
+ throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
+ }
+
+ return nestTokens(squashTokens(tokens));
+}
diff --git a/src/lib/util.ts b/src/lib/util.ts
new file mode 100644
index 0000000..8978569
--- /dev/null
+++ b/src/lib/util.ts
@@ -0,0 +1,80 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+// deno-lint-ignore-file ban-types no-explicit-any
+
+export function isFunction(obj: any): obj is Function {
+ return typeof obj === "function";
+}
+
+export function objectToString(o: any): string {
+ return Object.prototype.toString.call(o);
+}
+
+export function isArray(a: any): a is Array {
+ return Array.isArray(a) || objectToString(a) === "[object Array]";
+}
+
+export function typeStr(obj: any): string {
+ return isArray(obj) ? "array" : typeof obj;
+}
+
+export function escapeRegExp(string: string): string {
+ return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+}
+
+/**
+ * Null safe way of checking whether or not an object,
+ * including its prototype, has a given property
+ *
+ * @param obj The object to check
+ * @param propName The property name to check for
+ * @returns true if the object has the property, false otherwise
+ */
+export function hasProperty(obj: any, propName: string): boolean {
+ return obj != null && typeof obj === "object" && (propName in obj);
+}
+
+/**
+ * Safe way of detecting whether or not the given thing is a primitive and
+ * whether it has the given property
+ *
+ * @param primitive The thing to check
+ * @param propName The property name to check for
+ * @returns true if the thing is a primitive and has the property, false otherwise
+ */
+export function primitiveHasOwnProperty(primitive: any, propName: string): boolean {
+ return (
+ primitive != null &&
+ typeof primitive !== "object" &&
+ primitive.hasOwnProperty &&
+ // deno-lint-ignore no-prototype-builtins
+ primitive.hasOwnProperty(propName)
+ );
+}
+
+const regExpTest = RegExp.prototype.test;
+export function testRegExp(re: RegExp, string: string): boolean {
+ return regExpTest.call(re, string);
+}
+
+const nonSpaceRe = /\S/;
+export function isWhitespace(string: string): boolean {
+ return !testRegExp(nonSpaceRe, string);
+}
+
+const entityMap = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
+ "/": "/",
+ "`": "`",
+ "=": "=",
+};
+
+export function escapeHtml(string: string): string {
+ return String(string).replace(/[&<>"'`=\/]/g, (s) => {
+ return entityMap[s as keyof typeof entityMap] as string;
+ });
+}
diff --git a/src/mod.ts b/src/mod.ts
new file mode 100644
index 0000000..5845c99
--- /dev/null
+++ b/src/mod.ts
@@ -0,0 +1,8 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import mustache, { Mustache } from "./lib/Mustache.ts";
+export type { Mustache };
+export type { Config } from "./lib/Writer.ts";
+export type { Context } from "./lib/Context.ts";
+
+export default mustache;
diff --git a/test/Context.test.ts b/test/Context.test.ts
new file mode 100644
index 0000000..a579d10
--- /dev/null
+++ b/test/Context.test.ts
@@ -0,0 +1,76 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { assertEquals, beforeEach, describe, it } from "../dev_deps.ts";
+
+import { Context } from "../src/lib/Context.ts";
+
+describe("A new Mustache.Context", () => {
+ let context: Context;
+
+ beforeEach(() => {
+ context = new Context({ name: "parent", message: "hi", a: { b: "b" } });
+ });
+
+ it("is able to lookup properties of its own view", () => {
+ assertEquals(context.lookup("name"), "parent");
+ });
+
+ it("is able to lookup nested properties of its own view", () => {
+ assertEquals(context.lookup("a.b"), "b");
+ });
+
+ describe("when pushed", () => {
+ beforeEach(() => {
+ context = context.push({ name: "child", c: { d: "d" } });
+ });
+
+ describe("returns the child context", () => {
+ it("has child name", () => {
+ assertEquals(context.view.name, "child");
+ });
+
+ it("has parent name", () => {
+ assertEquals(context.parent?.view.name, "parent");
+ });
+
+ it("is able to lookup properties of its own view", () => {
+ assertEquals(context.lookup("name"), "child");
+ });
+
+ it("is able to lookup properties of the parent context's view", () => {
+ assertEquals(context.lookup("message"), "hi");
+ });
+
+ it("is able to lookup nested properties of its own view", () => {
+ assertEquals(context.lookup("c.d"), "d");
+ });
+ });
+
+ it("is able to lookup nested properties of its parent view", () => {
+ assertEquals(context.lookup("a.b"), "b");
+ });
+ });
+});
+
+describe("A Mustache.Context", () => {
+ let context: Context;
+
+ describe("with an empty string in the lookup chain", () => {
+ let view: Record>;
+
+ beforeEach(() => {
+ view = {
+ a: {
+ b: "b",
+ "": "empty",
+ },
+ };
+ (view.a as Record).b = "value";
+ context = new Context(view);
+ });
+
+ it("is able to lookup a nested property", () => {
+ assertEquals(context.lookup("a.b"), (view.a as Record).b);
+ });
+ });
+});
diff --git a/test/Scanner.test.ts b/test/Scanner.test.ts
new file mode 100644
index 0000000..73653ab
--- /dev/null
+++ b/test/Scanner.test.ts
@@ -0,0 +1,90 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { assert, assertEquals, beforeEach, describe, it } from "../dev_deps.ts";
+
+import { Scanner } from "../src/lib/Scanner.ts";
+
+describe("A new Mustache.Scanner", () => {
+ describe("for an empty string", () => {
+ it("is at the end", () => {
+ const scanner = new Scanner("");
+ assert(scanner.eos());
+ });
+ });
+
+ describe("for a non-empty string", () => {
+ let scanner: Scanner;
+ beforeEach(() => {
+ scanner = new Scanner("a b c");
+ });
+
+ describe("scan", () => {
+ describe("has a string property", () => {
+ it("returns the string", () => {
+ assertEquals(scanner.string, "a b c");
+ });
+ it("returns the tail", () => {
+ assertEquals(scanner.tail, "a b c");
+ });
+ });
+
+ describe("when the RegExp matches the entire string", () => {
+ it("returns the entire string", () => {
+ const match = scanner.scan(/a b c/);
+ assertEquals(match, scanner.string);
+ assert(scanner.eos());
+ });
+ });
+
+ describe("when the RegExp matches at index 0", () => {
+ it("returns the portion of the string that matched", () => {
+ const match = scanner.scan(/a/);
+ assertEquals(match, "a");
+ assertEquals(scanner.pos, 1);
+ });
+ });
+
+ describe("when the RegExp matches at some index other than 0", () => {
+ it("returns the empty string", () => {
+ const match = scanner.scan(/b/);
+ assertEquals(match, "");
+ assertEquals(scanner.pos, 0);
+ });
+ });
+
+ describe("when the RegExp does not match", () => {
+ it("returns the empty string", () => {
+ const match = scanner.scan(/z/);
+ assertEquals(match, "");
+ assertEquals(scanner.pos, 0);
+ });
+ });
+ }); // scan
+
+ describe("scanUntil", () => {
+ describe("when the RegExp matches at index 0", () => {
+ it("returns the empty string", () => {
+ const match = scanner.scanUntil(/a/);
+ assertEquals(match, "");
+ assertEquals(scanner.pos, 0);
+ });
+ });
+
+ describe("when the RegExp matches at some index other than 0", () => {
+ it("returns the string up to that index", () => {
+ const match = scanner.scanUntil(/b/);
+ assertEquals(match, "a ");
+ assertEquals(scanner.pos, 2);
+ });
+ });
+
+ describe("when the RegExp does not match", () => {
+ it("returns the entire string", () => {
+ const match = scanner.scanUntil(/z/);
+ assertEquals(match, scanner.string);
+ assert(scanner.eos());
+ });
+ });
+ }); // scanUntil
+ }); // for a non-empty string
+});
diff --git a/test/helpers/_files/ampersand_escape.js b/test/helpers/_files/ampersand_escape.js
new file mode 100644
index 0000000..07da0c7
--- /dev/null
+++ b/test/helpers/_files/ampersand_escape.js
@@ -0,0 +1,3 @@
+({
+ message: 'Some '
+});
diff --git a/test/helpers/_files/ampersand_escape.mustache b/test/helpers/_files/ampersand_escape.mustache
new file mode 100644
index 0000000..6501a48
--- /dev/null
+++ b/test/helpers/_files/ampersand_escape.mustache
@@ -0,0 +1 @@
+{{&message}}
diff --git a/test/helpers/_files/ampersand_escape.txt b/test/helpers/_files/ampersand_escape.txt
new file mode 100644
index 0000000..2ed3fd3
--- /dev/null
+++ b/test/helpers/_files/ampersand_escape.txt
@@ -0,0 +1 @@
+Some
diff --git a/test/helpers/_files/apostrophe.js b/test/helpers/_files/apostrophe.js
new file mode 100644
index 0000000..7acfce0
--- /dev/null
+++ b/test/helpers/_files/apostrophe.js
@@ -0,0 +1,4 @@
+({
+ 'apos': "'",
+ 'control': 'X'
+});
diff --git a/test/helpers/_files/apostrophe.mustache b/test/helpers/_files/apostrophe.mustache
new file mode 100644
index 0000000..e8687aa
--- /dev/null
+++ b/test/helpers/_files/apostrophe.mustache
@@ -0,0 +1 @@
+{{apos}}{{control}}
diff --git a/test/helpers/_files/apostrophe.txt b/test/helpers/_files/apostrophe.txt
new file mode 100644
index 0000000..4427c30
--- /dev/null
+++ b/test/helpers/_files/apostrophe.txt
@@ -0,0 +1 @@
+'X
diff --git a/test/helpers/_files/array_of_strings.js b/test/helpers/_files/array_of_strings.js
new file mode 100644
index 0000000..6eb9e63
--- /dev/null
+++ b/test/helpers/_files/array_of_strings.js
@@ -0,0 +1,3 @@
+({
+ array_of_strings: ['hello', 'world']
+});
diff --git a/test/helpers/_files/array_of_strings.mustache b/test/helpers/_files/array_of_strings.mustache
new file mode 100644
index 0000000..4d65738
--- /dev/null
+++ b/test/helpers/_files/array_of_strings.mustache
@@ -0,0 +1 @@
+{{#array_of_strings}}{{.}} {{/array_of_strings}}
diff --git a/test/helpers/_files/array_of_strings.txt b/test/helpers/_files/array_of_strings.txt
new file mode 100644
index 0000000..4a1f475
--- /dev/null
+++ b/test/helpers/_files/array_of_strings.txt
@@ -0,0 +1 @@
+hello world
diff --git a/test/helpers/_files/avoids_obj_prototype_in_view_cache.js b/test/helpers/_files/avoids_obj_prototype_in_view_cache.js
new file mode 100644
index 0000000..ae88d2b
--- /dev/null
+++ b/test/helpers/_files/avoids_obj_prototype_in_view_cache.js
@@ -0,0 +1,4 @@
+({
+ valueOf: 'Avoids methods',
+ watch: 'in Object.prototype'
+});
diff --git a/test/helpers/_files/avoids_obj_prototype_in_view_cache.mustache b/test/helpers/_files/avoids_obj_prototype_in_view_cache.mustache
new file mode 100644
index 0000000..f6e469b
--- /dev/null
+++ b/test/helpers/_files/avoids_obj_prototype_in_view_cache.mustache
@@ -0,0 +1 @@
+{{valueOf}} {{watch}}
\ No newline at end of file
diff --git a/test/helpers/_files/avoids_obj_prototype_in_view_cache.txt b/test/helpers/_files/avoids_obj_prototype_in_view_cache.txt
new file mode 100644
index 0000000..2621d99
--- /dev/null
+++ b/test/helpers/_files/avoids_obj_prototype_in_view_cache.txt
@@ -0,0 +1 @@
+Avoids methods in Object.prototype
\ No newline at end of file
diff --git a/test/helpers/_files/backslashes.js b/test/helpers/_files/backslashes.js
new file mode 100644
index 0000000..0a5d845
--- /dev/null
+++ b/test/helpers/_files/backslashes.js
@@ -0,0 +1,3 @@
+({
+ value: '\\abc'
+});
diff --git a/test/helpers/_files/backslashes.mustache b/test/helpers/_files/backslashes.mustache
new file mode 100644
index 0000000..fe7745b
--- /dev/null
+++ b/test/helpers/_files/backslashes.mustache
@@ -0,0 +1,7 @@
+* {{value}}
+* {{{value}}}
+* {{&value}}
+
diff --git a/test/helpers/_files/backslashes.txt b/test/helpers/_files/backslashes.txt
new file mode 100644
index 0000000..038dd37
--- /dev/null
+++ b/test/helpers/_files/backslashes.txt
@@ -0,0 +1,7 @@
+* \abc
+* \abc
+* \abc
+
diff --git a/test/helpers/_files/bug_11_eating_whitespace.js b/test/helpers/_files/bug_11_eating_whitespace.js
new file mode 100644
index 0000000..3a91f96
--- /dev/null
+++ b/test/helpers/_files/bug_11_eating_whitespace.js
@@ -0,0 +1,3 @@
+({
+ tag: 'yo'
+});
diff --git a/test/helpers/_files/bug_11_eating_whitespace.mustache b/test/helpers/_files/bug_11_eating_whitespace.mustache
new file mode 100644
index 0000000..8d5cd92
--- /dev/null
+++ b/test/helpers/_files/bug_11_eating_whitespace.mustache
@@ -0,0 +1 @@
+{{tag}} foo
diff --git a/test/helpers/_files/bug_11_eating_whitespace.txt b/test/helpers/_files/bug_11_eating_whitespace.txt
new file mode 100644
index 0000000..f5bbc85
--- /dev/null
+++ b/test/helpers/_files/bug_11_eating_whitespace.txt
@@ -0,0 +1 @@
+yo foo
diff --git a/test/helpers/_files/bug_length_property.js b/test/helpers/_files/bug_length_property.js
new file mode 100644
index 0000000..c63899e
--- /dev/null
+++ b/test/helpers/_files/bug_length_property.js
@@ -0,0 +1,3 @@
+({
+ length: 'hello'
+});
diff --git a/test/helpers/_files/bug_length_property.mustache b/test/helpers/_files/bug_length_property.mustache
new file mode 100644
index 0000000..b000887
--- /dev/null
+++ b/test/helpers/_files/bug_length_property.mustache
@@ -0,0 +1 @@
+{{#length}}The length variable is: {{length}}{{/length}}
diff --git a/test/helpers/_files/bug_length_property.txt b/test/helpers/_files/bug_length_property.txt
new file mode 100644
index 0000000..f5355d3
--- /dev/null
+++ b/test/helpers/_files/bug_length_property.txt
@@ -0,0 +1 @@
+The length variable is: hello
diff --git a/test/helpers/_files/changing_delimiters.js b/test/helpers/_files/changing_delimiters.js
new file mode 100644
index 0000000..0bb3b13
--- /dev/null
+++ b/test/helpers/_files/changing_delimiters.js
@@ -0,0 +1,4 @@
+({
+ 'foo': 'foooooooooooooo',
+ 'bar': 'bar!'
+});
diff --git a/test/helpers/_files/changing_delimiters.mustache b/test/helpers/_files/changing_delimiters.mustache
new file mode 100644
index 0000000..0cd044c
--- /dev/null
+++ b/test/helpers/_files/changing_delimiters.mustache
@@ -0,0 +1 @@
+{{=<% %>=}}<% foo %> {{foo}} <%{bar}%> {{{bar}}}
diff --git a/test/helpers/_files/changing_delimiters.txt b/test/helpers/_files/changing_delimiters.txt
new file mode 100644
index 0000000..1b1510d
--- /dev/null
+++ b/test/helpers/_files/changing_delimiters.txt
@@ -0,0 +1 @@
+foooooooooooooo {{foo}} bar! {{{bar}}}
diff --git a/test/helpers/_files/check_falsy.js b/test/helpers/_files/check_falsy.js
new file mode 100644
index 0000000..aeb7422
--- /dev/null
+++ b/test/helpers/_files/check_falsy.js
@@ -0,0 +1,7 @@
+({
+ number: function (text, render) {
+ return function (text, render) {
+ return +render(text);
+ };
+ }
+});
diff --git a/test/helpers/_files/check_falsy.mustache b/test/helpers/_files/check_falsy.mustache
new file mode 100644
index 0000000..30e2547
--- /dev/null
+++ b/test/helpers/_files/check_falsy.mustache
@@ -0,0 +1 @@
+{{#number}}0{{/number}}
diff --git a/test/helpers/_files/check_falsy.txt b/test/helpers/_files/check_falsy.txt
new file mode 100644
index 0000000..3bb2f51
--- /dev/null
+++ b/test/helpers/_files/check_falsy.txt
@@ -0,0 +1 @@
+0
diff --git a/test/helpers/_files/comments.js b/test/helpers/_files/comments.js
new file mode 100644
index 0000000..af291cb
--- /dev/null
+++ b/test/helpers/_files/comments.js
@@ -0,0 +1,5 @@
+({
+ title: function () {
+ return 'A Comedy of Errors';
+ }
+});
diff --git a/test/helpers/_files/comments.mustache b/test/helpers/_files/comments.mustache
new file mode 100644
index 0000000..5036801
--- /dev/null
+++ b/test/helpers/_files/comments.mustache
@@ -0,0 +1 @@
+{{title}}{{! just something interesting... or not... }}
diff --git a/test/helpers/_files/comments.txt b/test/helpers/_files/comments.txt
new file mode 100644
index 0000000..0133517
--- /dev/null
+++ b/test/helpers/_files/comments.txt
@@ -0,0 +1 @@
+A Comedy of Errors
diff --git a/test/helpers/_files/complex.js b/test/helpers/_files/complex.js
new file mode 100644
index 0000000..d6c3ae2
--- /dev/null
+++ b/test/helpers/_files/complex.js
@@ -0,0 +1,19 @@
+({
+ header: function () {
+ return 'Colors';
+ },
+ item: [
+ {name: 'red', current: true, url: '#Red'},
+ {name: 'green', current: false, url: '#Green'},
+ {name: 'blue', current: false, url: '#Blue'}
+ ],
+ link: function () {
+ return this['current'] !== true;
+ },
+ list: function () {
+ return this.item.length !== 0;
+ },
+ empty: function () {
+ return this.item.length === 0;
+ }
+});
diff --git a/test/helpers/_files/complex.mustache b/test/helpers/_files/complex.mustache
new file mode 100644
index 0000000..869a4f0
--- /dev/null
+++ b/test/helpers/_files/complex.mustache
@@ -0,0 +1,16 @@
+{{header}}
+{{#list}}
+
+ {{#item}}
+ {{#current}}
+ - {{name}}
+ {{/current}}
+ {{#link}}
+ - {{name}}
+ {{/link}}
+ {{/item}}
+
+{{/list}}
+{{#empty}}
+ The list is empty.
+{{/empty}}
diff --git a/test/helpers/_files/complex.txt b/test/helpers/_files/complex.txt
new file mode 100644
index 0000000..596d3f6
--- /dev/null
+++ b/test/helpers/_files/complex.txt
@@ -0,0 +1,6 @@
+Colors
+
diff --git a/test/helpers/_files/context_lookup.js b/test/helpers/_files/context_lookup.js
new file mode 100644
index 0000000..b9f891c
--- /dev/null
+++ b/test/helpers/_files/context_lookup.js
@@ -0,0 +1,8 @@
+({
+ 'outer': {
+ 'id': 1,
+ 'second': {
+ 'nothing': 2
+ }
+ }
+});
diff --git a/test/helpers/_files/context_lookup.mustache b/test/helpers/_files/context_lookup.mustache
new file mode 100644
index 0000000..3c7b767
--- /dev/null
+++ b/test/helpers/_files/context_lookup.mustache
@@ -0,0 +1 @@
+{{#outer}}{{#second}}{{id}}{{/second}}{{/outer}}
diff --git a/test/helpers/_files/context_lookup.txt b/test/helpers/_files/context_lookup.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/test/helpers/_files/context_lookup.txt
@@ -0,0 +1 @@
+1
diff --git a/test/helpers/_files/delimiters.js b/test/helpers/_files/delimiters.js
new file mode 100644
index 0000000..b4c9f50
--- /dev/null
+++ b/test/helpers/_files/delimiters.js
@@ -0,0 +1,6 @@
+({
+ first: 'It worked the first time.',
+ second: 'And it worked the second time.',
+ third: 'Then, surprisingly, it worked the third time.',
+ fourth: 'Fourth time also fine!.'
+});
diff --git a/test/helpers/_files/delimiters.mustache b/test/helpers/_files/delimiters.mustache
new file mode 100644
index 0000000..7fac846
--- /dev/null
+++ b/test/helpers/_files/delimiters.mustache
@@ -0,0 +1,7 @@
+{{=<% %>=}}*
+<% first %>
+* <% second %>
+<%=| |=%>
+* | third |
+|={{ }}=|
+* {{ fourth }}
diff --git a/test/helpers/_files/delimiters.txt b/test/helpers/_files/delimiters.txt
new file mode 100644
index 0000000..698a6bb
--- /dev/null
+++ b/test/helpers/_files/delimiters.txt
@@ -0,0 +1,5 @@
+*
+It worked the first time.
+* And it worked the second time.
+* Then, surprisingly, it worked the third time.
+* Fourth time also fine!.
diff --git a/test/helpers/_files/disappearing_whitespace.js b/test/helpers/_files/disappearing_whitespace.js
new file mode 100644
index 0000000..1d2f392
--- /dev/null
+++ b/test/helpers/_files/disappearing_whitespace.js
@@ -0,0 +1,4 @@
+({
+ bedrooms: true,
+ total: 1
+});
diff --git a/test/helpers/_files/disappearing_whitespace.mustache b/test/helpers/_files/disappearing_whitespace.mustache
new file mode 100644
index 0000000..16c16e0
--- /dev/null
+++ b/test/helpers/_files/disappearing_whitespace.mustache
@@ -0,0 +1 @@
+{{#bedrooms}}{{total}}{{/bedrooms}} BED
diff --git a/test/helpers/_files/disappearing_whitespace.txt b/test/helpers/_files/disappearing_whitespace.txt
new file mode 100644
index 0000000..66e98ef
--- /dev/null
+++ b/test/helpers/_files/disappearing_whitespace.txt
@@ -0,0 +1 @@
+1 BED
diff --git a/test/helpers/_files/dot_notation.js b/test/helpers/_files/dot_notation.js
new file mode 100644
index 0000000..8dbef43
--- /dev/null
+++ b/test/helpers/_files/dot_notation.js
@@ -0,0 +1,24 @@
+({
+ name: 'A Book',
+ authors: ['John Power', 'Jamie Walsh'],
+ price: {
+ value: 200,
+ vat: function () {
+ return this.value * 0.2;
+ },
+ currency: {
+ symbol: '$',
+ name: 'USD'
+ }
+ },
+ availability: {
+ status: true,
+ text: 'In Stock'
+ },
+ // And now, some truthy false values
+ truthy: {
+ zero: 0,
+ notTrue: false
+ },
+ singletonList: [{singletonItem: 'singleton item'}]
+});
diff --git a/test/helpers/_files/dot_notation.mustache b/test/helpers/_files/dot_notation.mustache
new file mode 100644
index 0000000..1ef2209
--- /dev/null
+++ b/test/helpers/_files/dot_notation.mustache
@@ -0,0 +1,12 @@
+
+{{name}}
+Authors:
{{#authors}}- {{.}}
{{/authors}}
+Price: {{{price.currency.symbol}}}{{price.value}} {{#price.currency}}{{name}} {{availability.text}}{{/price.currency}}
+VAT: {{{price.currency.symbol}}}{{#price}}{{vat}}{{/price}}
+
+Test truthy false values:
+Zero: {{truthy.zero}}
+False: {{truthy.notTrue}}
+length of string should be rendered: {{price.currency.name.length}}
+length of string in a list should be rendered: {{#singletonList}}{{singletonItem.length}}{{/singletonList}}
+length of an array should be rendered: {{authors.length}}
diff --git a/test/helpers/_files/dot_notation.txt b/test/helpers/_files/dot_notation.txt
new file mode 100644
index 0000000..c71ab27
--- /dev/null
+++ b/test/helpers/_files/dot_notation.txt
@@ -0,0 +1,12 @@
+
+A Book
+Authors:
+Price: $200 USD In Stock
+VAT: $40
+
+Test truthy false values:
+Zero: 0
+False: false
+length of string should be rendered: 3
+length of string in a list should be rendered: 14
+length of an array should be rendered: 2
diff --git a/test/helpers/_files/double_render.js b/test/helpers/_files/double_render.js
new file mode 100644
index 0000000..d8ade03
--- /dev/null
+++ b/test/helpers/_files/double_render.js
@@ -0,0 +1,5 @@
+({
+ foo: true,
+ bar: '{{win}}',
+ win: 'FAIL'
+});
diff --git a/test/helpers/_files/double_render.mustache b/test/helpers/_files/double_render.mustache
new file mode 100644
index 0000000..4500fd7
--- /dev/null
+++ b/test/helpers/_files/double_render.mustache
@@ -0,0 +1 @@
+{{#foo}}{{bar}}{{/foo}}
diff --git a/test/helpers/_files/double_render.txt b/test/helpers/_files/double_render.txt
new file mode 100644
index 0000000..b6e652d
--- /dev/null
+++ b/test/helpers/_files/double_render.txt
@@ -0,0 +1 @@
+{{win}}
diff --git a/test/helpers/_files/empty_list.js b/test/helpers/_files/empty_list.js
new file mode 100644
index 0000000..c40f6d8
--- /dev/null
+++ b/test/helpers/_files/empty_list.js
@@ -0,0 +1,3 @@
+({
+ jobs: []
+});
diff --git a/test/helpers/_files/empty_list.mustache b/test/helpers/_files/empty_list.mustache
new file mode 100644
index 0000000..4fdf13d
--- /dev/null
+++ b/test/helpers/_files/empty_list.mustache
@@ -0,0 +1,4 @@
+These are the jobs:
+{{#jobs}}
+{{.}}
+{{/jobs}}
diff --git a/test/helpers/_files/empty_list.txt b/test/helpers/_files/empty_list.txt
new file mode 100644
index 0000000..d9b4a67
--- /dev/null
+++ b/test/helpers/_files/empty_list.txt
@@ -0,0 +1 @@
+These are the jobs:
diff --git a/test/helpers/_files/empty_sections.js b/test/helpers/_files/empty_sections.js
new file mode 100644
index 0000000..b3bf1dd
--- /dev/null
+++ b/test/helpers/_files/empty_sections.js
@@ -0,0 +1 @@
+({});
diff --git a/test/helpers/_files/empty_sections.mustache b/test/helpers/_files/empty_sections.mustache
new file mode 100644
index 0000000..b6065db
--- /dev/null
+++ b/test/helpers/_files/empty_sections.mustache
@@ -0,0 +1 @@
+{{#foo}}{{/foo}}foo{{#bar}}{{/bar}}
diff --git a/test/helpers/_files/empty_sections.txt b/test/helpers/_files/empty_sections.txt
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/test/helpers/_files/empty_sections.txt
@@ -0,0 +1 @@
+foo
diff --git a/test/helpers/_files/empty_string.js b/test/helpers/_files/empty_string.js
new file mode 100644
index 0000000..59de15d
--- /dev/null
+++ b/test/helpers/_files/empty_string.js
@@ -0,0 +1,6 @@
+({
+ description: 'That is all!',
+ child: {
+ description: ''
+ }
+});
diff --git a/test/helpers/_files/empty_string.mustache b/test/helpers/_files/empty_string.mustache
new file mode 100644
index 0000000..f568441
--- /dev/null
+++ b/test/helpers/_files/empty_string.mustache
@@ -0,0 +1 @@
+{{description}}{{#child}}{{description}}{{/child}}
diff --git a/test/helpers/_files/empty_string.txt b/test/helpers/_files/empty_string.txt
new file mode 100644
index 0000000..22e2a6e
--- /dev/null
+++ b/test/helpers/_files/empty_string.txt
@@ -0,0 +1 @@
+That is all!
diff --git a/test/helpers/_files/empty_template.js b/test/helpers/_files/empty_template.js
new file mode 100644
index 0000000..b3bf1dd
--- /dev/null
+++ b/test/helpers/_files/empty_template.js
@@ -0,0 +1 @@
+({});
diff --git a/test/helpers/_files/empty_template.mustache b/test/helpers/_files/empty_template.mustache
new file mode 100644
index 0000000..bb2367a
--- /dev/null
+++ b/test/helpers/_files/empty_template.mustache
@@ -0,0 +1 @@
+Test
\ No newline at end of file
diff --git a/test/helpers/_files/empty_template.txt b/test/helpers/_files/empty_template.txt
new file mode 100644
index 0000000..bb2367a
--- /dev/null
+++ b/test/helpers/_files/empty_template.txt
@@ -0,0 +1 @@
+Test
\ No newline at end of file
diff --git a/test/helpers/_files/error_not_found.js b/test/helpers/_files/error_not_found.js
new file mode 100644
index 0000000..6dcb14f
--- /dev/null
+++ b/test/helpers/_files/error_not_found.js
@@ -0,0 +1,3 @@
+({
+ bar: 2
+});
diff --git a/test/helpers/_files/error_not_found.mustache b/test/helpers/_files/error_not_found.mustache
new file mode 100644
index 0000000..24369f7
--- /dev/null
+++ b/test/helpers/_files/error_not_found.mustache
@@ -0,0 +1 @@
+{{foo}}
\ No newline at end of file
diff --git a/test/helpers/_files/error_not_found.txt b/test/helpers/_files/error_not_found.txt
new file mode 100644
index 0000000..e69de29
diff --git a/test/helpers/_files/escaped.js b/test/helpers/_files/escaped.js
new file mode 100644
index 0000000..8345ecb
--- /dev/null
+++ b/test/helpers/_files/escaped.js
@@ -0,0 +1,7 @@
+({
+ title: function () {
+ return 'Bear > Shark';
+ },
+ symbol: null,
+ entities: "" \"'<>`=/"
+});
diff --git a/test/helpers/_files/escaped.mustache b/test/helpers/_files/escaped.mustache
new file mode 100644
index 0000000..5c44268
--- /dev/null
+++ b/test/helpers/_files/escaped.mustache
@@ -0,0 +1,2 @@
+{{title}}{{symbol}}
+And even {{entities}}, but not {{{entities}}}.
diff --git a/test/helpers/_files/escaped.txt b/test/helpers/_files/escaped.txt
new file mode 100644
index 0000000..182f99e
--- /dev/null
+++ b/test/helpers/_files/escaped.txt
@@ -0,0 +1,2 @@
+Bear > Shark
+And even " "'<>`=/, but not " "'<>`=/.
diff --git a/test/helpers/_files/falsy.js b/test/helpers/_files/falsy.js
new file mode 100644
index 0000000..e3fd422
--- /dev/null
+++ b/test/helpers/_files/falsy.js
@@ -0,0 +1,8 @@
+({
+ 'emptyString': '',
+ 'emptyArray': [],
+ 'zero': 0,
+ 'null': null,
+ 'undefined': undefined,
+ 'NaN': 0/0
+});
\ No newline at end of file
diff --git a/test/helpers/_files/falsy.mustache b/test/helpers/_files/falsy.mustache
new file mode 100644
index 0000000..f3698da
--- /dev/null
+++ b/test/helpers/_files/falsy.mustache
@@ -0,0 +1,12 @@
+{{#emptyString}}empty string{{/emptyString}}
+{{^emptyString}}inverted empty string{{/emptyString}}
+{{#emptyArray}}empty array{{/emptyArray}}
+{{^emptyArray}}inverted empty array{{/emptyArray}}
+{{#zero}}zero{{/zero}}
+{{^zero}}inverted zero{{/zero}}
+{{#null}}null{{/null}}
+{{^null}}inverted null{{/null}}
+{{#undefined}}undefined{{/undefined}}
+{{^undefined}}inverted undefined{{/undefined}}
+{{#NaN}}NaN{{/NaN}}
+{{^NaN}}inverted NaN{{/NaN}}
diff --git a/test/helpers/_files/falsy.txt b/test/helpers/_files/falsy.txt
new file mode 100644
index 0000000..9b7cde3
--- /dev/null
+++ b/test/helpers/_files/falsy.txt
@@ -0,0 +1,12 @@
+
+inverted empty string
+
+inverted empty array
+
+inverted zero
+
+inverted null
+
+inverted undefined
+
+inverted NaN
diff --git a/test/helpers/_files/falsy_array.js b/test/helpers/_files/falsy_array.js
new file mode 100644
index 0000000..209c589
--- /dev/null
+++ b/test/helpers/_files/falsy_array.js
@@ -0,0 +1,10 @@
+({
+ 'list': [
+ ['', 'emptyString'],
+ [[], 'emptyArray'],
+ [0, 'zero'],
+ [null, 'null'],
+ [undefined, 'undefined'],
+ [0/0, 'NaN']
+ ]
+});
\ No newline at end of file
diff --git a/test/helpers/_files/falsy_array.mustache b/test/helpers/_files/falsy_array.mustache
new file mode 100644
index 0000000..2be7b37
--- /dev/null
+++ b/test/helpers/_files/falsy_array.mustache
@@ -0,0 +1,3 @@
+{{#list}}
+{{#.}}{{#.}}{{.}}{{/.}}{{^.}}inverted {{/.}}{{/.}}
+{{/list}}
\ No newline at end of file
diff --git a/test/helpers/_files/falsy_array.txt b/test/helpers/_files/falsy_array.txt
new file mode 100644
index 0000000..6f24529
--- /dev/null
+++ b/test/helpers/_files/falsy_array.txt
@@ -0,0 +1,6 @@
+inverted emptyString
+inverted emptyArray
+inverted zero
+inverted null
+inverted undefined
+inverted NaN
diff --git a/test/helpers/_files/grandparent_context.js b/test/helpers/_files/grandparent_context.js
new file mode 100644
index 0000000..4d30400
--- /dev/null
+++ b/test/helpers/_files/grandparent_context.js
@@ -0,0 +1,19 @@
+({
+ grand_parent_id: 'grand_parent1',
+ parent_contexts: [
+ {
+ parent_id: 'parent1',
+ child_contexts: [
+ { child_id: 'parent1-child1' },
+ { child_id: 'parent1-child2' }
+ ]
+ },
+ {
+ parent_id: 'parent2',
+ child_contexts: [
+ { child_id: 'parent2-child1' },
+ { child_id: 'parent2-child2' }
+ ]
+ }
+ ]
+});
diff --git a/test/helpers/_files/grandparent_context.mustache b/test/helpers/_files/grandparent_context.mustache
new file mode 100644
index 0000000..e6c07a2
--- /dev/null
+++ b/test/helpers/_files/grandparent_context.mustache
@@ -0,0 +1,10 @@
+{{grand_parent_id}}
+{{#parent_contexts}}
+{{grand_parent_id}}
+{{parent_id}}
+{{#child_contexts}}
+{{grand_parent_id}}
+{{parent_id}}
+{{child_id}}
+{{/child_contexts}}
+{{/parent_contexts}}
diff --git a/test/helpers/_files/grandparent_context.txt b/test/helpers/_files/grandparent_context.txt
new file mode 100644
index 0000000..64996ad
--- /dev/null
+++ b/test/helpers/_files/grandparent_context.txt
@@ -0,0 +1,17 @@
+grand_parent1
+grand_parent1
+parent1
+grand_parent1
+parent1
+parent1-child1
+grand_parent1
+parent1
+parent1-child2
+grand_parent1
+parent2
+grand_parent1
+parent2
+parent2-child1
+grand_parent1
+parent2
+parent2-child2
diff --git a/test/helpers/_files/higher_order_sections.js b/test/helpers/_files/higher_order_sections.js
new file mode 100644
index 0000000..6a4a310
--- /dev/null
+++ b/test/helpers/_files/higher_order_sections.js
@@ -0,0 +1,9 @@
+({
+ name: 'Tater',
+ helper: 'To tinker?',
+ bolder: function () {
+ return function (text, render) {
+ return text + ' => ' + render(text) + ' ' + this.helper;
+ };
+ }
+});
diff --git a/test/helpers/_files/higher_order_sections.mustache b/test/helpers/_files/higher_order_sections.mustache
new file mode 100644
index 0000000..04f5318
--- /dev/null
+++ b/test/helpers/_files/higher_order_sections.mustache
@@ -0,0 +1 @@
+{{#bolder}}Hi {{name}}.{{/bolder}}
diff --git a/test/helpers/_files/higher_order_sections.txt b/test/helpers/_files/higher_order_sections.txt
new file mode 100644
index 0000000..be50ad7
--- /dev/null
+++ b/test/helpers/_files/higher_order_sections.txt
@@ -0,0 +1 @@
+Hi {{name}}. => Hi Tater. To tinker?
diff --git a/test/helpers/_files/implicit_iterator.js b/test/helpers/_files/implicit_iterator.js
new file mode 100644
index 0000000..47a6b68
--- /dev/null
+++ b/test/helpers/_files/implicit_iterator.js
@@ -0,0 +1,8 @@
+({
+ data: {
+ author: {
+ twitter_id: 819606,
+ name: 'janl'
+ }
+ }
+});
diff --git a/test/helpers/_files/implicit_iterator.mustache b/test/helpers/_files/implicit_iterator.mustache
new file mode 100644
index 0000000..ae31f34
--- /dev/null
+++ b/test/helpers/_files/implicit_iterator.mustache
@@ -0,0 +1,7 @@
+{{# data.author.twitter_id }}
+
+{{/ data.author.twitter_id }}
+
+{{# data.author.name }}
+
+{{/ data.author.name }}
diff --git a/test/helpers/_files/implicit_iterator.txt b/test/helpers/_files/implicit_iterator.txt
new file mode 100644
index 0000000..0fccefd
--- /dev/null
+++ b/test/helpers/_files/implicit_iterator.txt
@@ -0,0 +1,3 @@
+
+
+
diff --git a/test/helpers/_files/included_tag.js b/test/helpers/_files/included_tag.js
new file mode 100644
index 0000000..2dfc0c3
--- /dev/null
+++ b/test/helpers/_files/included_tag.js
@@ -0,0 +1,3 @@
+({
+ html: 'I like {{mustache}}'
+});
diff --git a/test/helpers/_files/included_tag.mustache b/test/helpers/_files/included_tag.mustache
new file mode 100644
index 0000000..70631c2
--- /dev/null
+++ b/test/helpers/_files/included_tag.mustache
@@ -0,0 +1 @@
+You said "{{{html}}}" today
diff --git a/test/helpers/_files/included_tag.txt b/test/helpers/_files/included_tag.txt
new file mode 100644
index 0000000..1af4556
--- /dev/null
+++ b/test/helpers/_files/included_tag.txt
@@ -0,0 +1 @@
+You said "I like {{mustache}}" today
diff --git a/test/helpers/_files/inverted_section.js b/test/helpers/_files/inverted_section.js
new file mode 100644
index 0000000..40fcba8
--- /dev/null
+++ b/test/helpers/_files/inverted_section.js
@@ -0,0 +1,3 @@
+({
+ 'repos': []
+});
diff --git a/test/helpers/_files/inverted_section.mustache b/test/helpers/_files/inverted_section.mustache
new file mode 100644
index 0000000..b0a183b
--- /dev/null
+++ b/test/helpers/_files/inverted_section.mustache
@@ -0,0 +1,3 @@
+{{#repos}}{{name}}{{/repos}}
+{{^repos}}No repos :({{/repos}}
+{{^nothin}}Hello!{{/nothin}}
diff --git a/test/helpers/_files/inverted_section.txt b/test/helpers/_files/inverted_section.txt
new file mode 100644
index 0000000..b421582
--- /dev/null
+++ b/test/helpers/_files/inverted_section.txt
@@ -0,0 +1,3 @@
+
+No repos :(
+Hello!
diff --git a/test/helpers/_files/keys_with_questionmarks.js b/test/helpers/_files/keys_with_questionmarks.js
new file mode 100644
index 0000000..345e97f
--- /dev/null
+++ b/test/helpers/_files/keys_with_questionmarks.js
@@ -0,0 +1,5 @@
+({
+ 'person?': {
+ name: 'Jon'
+ }
+});
diff --git a/test/helpers/_files/keys_with_questionmarks.mustache b/test/helpers/_files/keys_with_questionmarks.mustache
new file mode 100644
index 0000000..417f17f
--- /dev/null
+++ b/test/helpers/_files/keys_with_questionmarks.mustache
@@ -0,0 +1,3 @@
+{{#person?}}
+ Hi {{name}}!
+{{/person?}}
diff --git a/test/helpers/_files/keys_with_questionmarks.txt b/test/helpers/_files/keys_with_questionmarks.txt
new file mode 100644
index 0000000..0f69b94
--- /dev/null
+++ b/test/helpers/_files/keys_with_questionmarks.txt
@@ -0,0 +1 @@
+ Hi Jon!
diff --git a/test/helpers/_files/malicious_template.js b/test/helpers/_files/malicious_template.js
new file mode 100644
index 0000000..b3bf1dd
--- /dev/null
+++ b/test/helpers/_files/malicious_template.js
@@ -0,0 +1 @@
+({});
diff --git a/test/helpers/_files/malicious_template.mustache b/test/helpers/_files/malicious_template.mustache
new file mode 100644
index 0000000..b956867
--- /dev/null
+++ b/test/helpers/_files/malicious_template.mustache
@@ -0,0 +1,5 @@
+{{"+(function () {throw "evil"})()+"}}
+{{{"+(function () {throw "evil"})()+"}}}
+{{> "+(function () {throw "evil"})()+"}}
+{{# "+(function () {throw "evil"})()+"}}
+{{/ "+(function () {throw "evil"})()+"}}
diff --git a/test/helpers/_files/malicious_template.txt b/test/helpers/_files/malicious_template.txt
new file mode 100644
index 0000000..139597f
--- /dev/null
+++ b/test/helpers/_files/malicious_template.txt
@@ -0,0 +1,2 @@
+
+
diff --git a/test/helpers/_files/multiline_comment.js b/test/helpers/_files/multiline_comment.js
new file mode 100644
index 0000000..b3bf1dd
--- /dev/null
+++ b/test/helpers/_files/multiline_comment.js
@@ -0,0 +1 @@
+({});
diff --git a/test/helpers/_files/multiline_comment.mustache b/test/helpers/_files/multiline_comment.mustache
new file mode 100644
index 0000000..dff0893
--- /dev/null
+++ b/test/helpers/_files/multiline_comment.mustache
@@ -0,0 +1,6 @@
+{{!
+
+This is a multi-line comment.
+
+}}
+Hello world!
diff --git a/test/helpers/_files/multiline_comment.txt b/test/helpers/_files/multiline_comment.txt
new file mode 100644
index 0000000..cd08755
--- /dev/null
+++ b/test/helpers/_files/multiline_comment.txt
@@ -0,0 +1 @@
+Hello world!
diff --git a/test/helpers/_files/nested_dot.js b/test/helpers/_files/nested_dot.js
new file mode 100644
index 0000000..72493f6
--- /dev/null
+++ b/test/helpers/_files/nested_dot.js
@@ -0,0 +1 @@
+({ name: 'Bruno' });
diff --git a/test/helpers/_files/nested_dot.mustache b/test/helpers/_files/nested_dot.mustache
new file mode 100644
index 0000000..12b0728
--- /dev/null
+++ b/test/helpers/_files/nested_dot.mustache
@@ -0,0 +1 @@
+{{#name}}Hello {{.}}{{/name}}
\ No newline at end of file
diff --git a/test/helpers/_files/nested_dot.txt b/test/helpers/_files/nested_dot.txt
new file mode 100644
index 0000000..58df047
--- /dev/null
+++ b/test/helpers/_files/nested_dot.txt
@@ -0,0 +1 @@
+Hello Bruno
\ No newline at end of file
diff --git a/test/helpers/_files/nested_higher_order_sections.js b/test/helpers/_files/nested_higher_order_sections.js
new file mode 100644
index 0000000..3ccf4d3
--- /dev/null
+++ b/test/helpers/_files/nested_higher_order_sections.js
@@ -0,0 +1,8 @@
+({
+ bold: function () {
+ return function (text, render) {
+ return '' + render(text) + '';
+ };
+ },
+ person: { name: 'Jonas' }
+});
diff --git a/test/helpers/_files/nested_higher_order_sections.mustache b/test/helpers/_files/nested_higher_order_sections.mustache
new file mode 100644
index 0000000..e312fe7
--- /dev/null
+++ b/test/helpers/_files/nested_higher_order_sections.mustache
@@ -0,0 +1 @@
+{{#bold}}{{#person}}My name is {{name}}!{{/person}}{{/bold}}
diff --git a/test/helpers/_files/nested_higher_order_sections.txt b/test/helpers/_files/nested_higher_order_sections.txt
new file mode 100644
index 0000000..0ee6a40
--- /dev/null
+++ b/test/helpers/_files/nested_higher_order_sections.txt
@@ -0,0 +1 @@
+My name is Jonas!
diff --git a/test/helpers/_files/nested_iterating.js b/test/helpers/_files/nested_iterating.js
new file mode 100644
index 0000000..333d925
--- /dev/null
+++ b/test/helpers/_files/nested_iterating.js
@@ -0,0 +1,8 @@
+({
+ inner: [{
+ foo: 'foo',
+ inner: [{
+ bar: 'bar'
+ }]
+ }]
+});
diff --git a/test/helpers/_files/nested_iterating.mustache b/test/helpers/_files/nested_iterating.mustache
new file mode 100644
index 0000000..1a3bb1a
--- /dev/null
+++ b/test/helpers/_files/nested_iterating.mustache
@@ -0,0 +1 @@
+{{#inner}}{{foo}}{{#inner}}{{bar}}{{/inner}}{{/inner}}
diff --git a/test/helpers/_files/nested_iterating.txt b/test/helpers/_files/nested_iterating.txt
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/test/helpers/_files/nested_iterating.txt
@@ -0,0 +1 @@
+foobar
diff --git a/test/helpers/_files/nesting.js b/test/helpers/_files/nesting.js
new file mode 100644
index 0000000..d0eab3a
--- /dev/null
+++ b/test/helpers/_files/nesting.js
@@ -0,0 +1,7 @@
+({
+ foo: [
+ {a: {b: 1}},
+ {a: {b: 2}},
+ {a: {b: 3}}
+ ]
+});
diff --git a/test/helpers/_files/nesting.mustache b/test/helpers/_files/nesting.mustache
new file mode 100644
index 0000000..551366d
--- /dev/null
+++ b/test/helpers/_files/nesting.mustache
@@ -0,0 +1,5 @@
+{{#foo}}
+ {{#a}}
+ {{b}}
+ {{/a}}
+{{/foo}}
diff --git a/test/helpers/_files/nesting.txt b/test/helpers/_files/nesting.txt
new file mode 100644
index 0000000..7db34b1
--- /dev/null
+++ b/test/helpers/_files/nesting.txt
@@ -0,0 +1,3 @@
+ 1
+ 2
+ 3
diff --git a/test/helpers/_files/nesting_same_name.js b/test/helpers/_files/nesting_same_name.js
new file mode 100644
index 0000000..84bc5c0
--- /dev/null
+++ b/test/helpers/_files/nesting_same_name.js
@@ -0,0 +1,8 @@
+({
+ items: [
+ {
+ name: 'name',
+ items: [1, 2, 3, 4]
+ }
+ ]
+});
diff --git a/test/helpers/_files/nesting_same_name.mustache b/test/helpers/_files/nesting_same_name.mustache
new file mode 100644
index 0000000..777dbd6
--- /dev/null
+++ b/test/helpers/_files/nesting_same_name.mustache
@@ -0,0 +1 @@
+{{#items}}{{name}}{{#items}}{{.}}{{/items}}{{/items}}
diff --git a/test/helpers/_files/nesting_same_name.txt b/test/helpers/_files/nesting_same_name.txt
new file mode 100644
index 0000000..34fcfd3
--- /dev/null
+++ b/test/helpers/_files/nesting_same_name.txt
@@ -0,0 +1 @@
+name1234
diff --git a/test/helpers/_files/null_lookup_array.js b/test/helpers/_files/null_lookup_array.js
new file mode 100644
index 0000000..40efeea
--- /dev/null
+++ b/test/helpers/_files/null_lookup_array.js
@@ -0,0 +1,9 @@
+({
+ 'name': 'David',
+ 'twitter': '@dasilvacontin',
+ 'farray': [
+ ['Flor', '@florrts'],
+ ['Miquel', null],
+ ['Chris', undefined]
+ ]
+});
diff --git a/test/helpers/_files/null_lookup_array.mustache b/test/helpers/_files/null_lookup_array.mustache
new file mode 100644
index 0000000..0543895
--- /dev/null
+++ b/test/helpers/_files/null_lookup_array.mustache
@@ -0,0 +1,3 @@
+{{#farray}}
+{{#.}}{{#.}}{{.}} {{/.}}{{^.}}no twitter{{/.}}{{/.}}
+{{/farray}}
diff --git a/test/helpers/_files/null_lookup_array.txt b/test/helpers/_files/null_lookup_array.txt
new file mode 100644
index 0000000..94edf99
--- /dev/null
+++ b/test/helpers/_files/null_lookup_array.txt
@@ -0,0 +1,3 @@
+Flor @florrts
+Miquel no twitter
+Chris no twitter
diff --git a/test/helpers/_files/null_lookup_object.js b/test/helpers/_files/null_lookup_object.js
new file mode 100644
index 0000000..6f6b0e5
--- /dev/null
+++ b/test/helpers/_files/null_lookup_object.js
@@ -0,0 +1,31 @@
+({
+ 'name': 'David',
+ 'twitter': '@dasilvacontin',
+ 'fobject': [
+ {
+ 'name': 'Flor',
+ 'twitter': '@florrts'
+ },
+ {
+ 'name': 'Miquel',
+ 'twitter': null
+ },
+ {
+ 'name': 'Chris',
+ 'twitter': undefined
+ }
+ ],
+ 'favorites': {
+ 'color': 'blue',
+ 'president': 'Bush',
+ 'show': 'Futurama'
+ },
+ 'mascot': {
+ 'name': 'Squid',
+ 'favorites': {
+ 'color': 'orange',
+ 'president': undefined,
+ 'show': null
+ }
+ }
+});
diff --git a/test/helpers/_files/null_lookup_object.mustache b/test/helpers/_files/null_lookup_object.mustache
new file mode 100644
index 0000000..243e218
--- /dev/null
+++ b/test/helpers/_files/null_lookup_object.mustache
@@ -0,0 +1,9 @@
+{{#fobject}}
+{{name}}'s twitter: {{#twitter}}{{.}}{{/twitter}}{{^twitter}}unknown{{/twitter}}.
+{{/fobject}}
+
+{{#mascot}}
+{{name}}'s favorite color: {{#favorites.color}}{{.}}{{/favorites.color}}{{^favorites.color}}no one{{/favorites.color}}.
+{{name}}'s favorite president: {{#favorites.president}}{{.}}{{/favorites.president}}{{^favorites.president}}no one{{/favorites.president}}.
+{{name}}'s favorite show: {{#favorites.show}}{{.}}{{/favorites.show}}{{^favorites.show}}none{{/favorites.show}}.
+{{/mascot}}
diff --git a/test/helpers/_files/null_lookup_object.txt b/test/helpers/_files/null_lookup_object.txt
new file mode 100644
index 0000000..0f71bdb
--- /dev/null
+++ b/test/helpers/_files/null_lookup_object.txt
@@ -0,0 +1,7 @@
+Flor's twitter: @florrts.
+Miquel's twitter: unknown.
+Chris's twitter: unknown.
+
+Squid's favorite color: orange.
+Squid's favorite president: no one.
+Squid's favorite show: none.
diff --git a/test/helpers/_files/null_string.js b/test/helpers/_files/null_string.js
new file mode 100644
index 0000000..4fc1033
--- /dev/null
+++ b/test/helpers/_files/null_string.js
@@ -0,0 +1,10 @@
+({
+ name: 'Elise',
+ glytch: true,
+ binary: false,
+ value: null,
+ undef: undefined,
+ numeric: function () {
+ return NaN;
+ }
+});
diff --git a/test/helpers/_files/null_string.mustache b/test/helpers/_files/null_string.mustache
new file mode 100644
index 0000000..a6f3300
--- /dev/null
+++ b/test/helpers/_files/null_string.mustache
@@ -0,0 +1,6 @@
+Hello {{name}}
+glytch {{glytch}}
+binary {{binary}}
+value {{value}}
+undef {{undef}}
+numeric {{numeric}}
diff --git a/test/helpers/_files/null_string.txt b/test/helpers/_files/null_string.txt
new file mode 100644
index 0000000..bcabe0a
--- /dev/null
+++ b/test/helpers/_files/null_string.txt
@@ -0,0 +1,6 @@
+Hello Elise
+glytch true
+binary false
+value
+undef
+numeric NaN
diff --git a/test/helpers/_files/null_view.js b/test/helpers/_files/null_view.js
new file mode 100644
index 0000000..7f82792
--- /dev/null
+++ b/test/helpers/_files/null_view.js
@@ -0,0 +1,4 @@
+({
+ name: 'Joe',
+ friends: null
+});
diff --git a/test/helpers/_files/null_view.mustache b/test/helpers/_files/null_view.mustache
new file mode 100644
index 0000000..115b376
--- /dev/null
+++ b/test/helpers/_files/null_view.mustache
@@ -0,0 +1 @@
+{{name}}'s friends: {{#friends}}{{name}}, {{/friends}}
\ No newline at end of file
diff --git a/test/helpers/_files/null_view.txt b/test/helpers/_files/null_view.txt
new file mode 100644
index 0000000..15ed2ab
--- /dev/null
+++ b/test/helpers/_files/null_view.txt
@@ -0,0 +1 @@
+Joe's friends:
\ No newline at end of file
diff --git a/test/helpers/_files/partial_array.js b/test/helpers/_files/partial_array.js
new file mode 100644
index 0000000..30b3c77
--- /dev/null
+++ b/test/helpers/_files/partial_array.js
@@ -0,0 +1,3 @@
+({
+ array: ['1', '2', '3', '4']
+});
diff --git a/test/helpers/_files/partial_array.mustache b/test/helpers/_files/partial_array.mustache
new file mode 100644
index 0000000..7a336fe
--- /dev/null
+++ b/test/helpers/_files/partial_array.mustache
@@ -0,0 +1 @@
+{{>partial}}
\ No newline at end of file
diff --git a/test/helpers/_files/partial_array.partial b/test/helpers/_files/partial_array.partial
new file mode 100644
index 0000000..0ba652c
--- /dev/null
+++ b/test/helpers/_files/partial_array.partial
@@ -0,0 +1,4 @@
+Here's a non-sense array of values
+{{#array}}
+ {{.}}
+{{/array}}
diff --git a/test/helpers/_files/partial_array.txt b/test/helpers/_files/partial_array.txt
new file mode 100644
index 0000000..892837c
--- /dev/null
+++ b/test/helpers/_files/partial_array.txt
@@ -0,0 +1,5 @@
+Here's a non-sense array of values
+ 1
+ 2
+ 3
+ 4
diff --git a/test/helpers/_files/partial_array_of_partials.js b/test/helpers/_files/partial_array_of_partials.js
new file mode 100644
index 0000000..4f21b75
--- /dev/null
+++ b/test/helpers/_files/partial_array_of_partials.js
@@ -0,0 +1,8 @@
+({
+ numbers: [
+ {i: '1'},
+ {i: '2'},
+ {i: '3'},
+ {i: '4'}
+ ]
+});
diff --git a/test/helpers/_files/partial_array_of_partials.mustache b/test/helpers/_files/partial_array_of_partials.mustache
new file mode 100644
index 0000000..1af6d68
--- /dev/null
+++ b/test/helpers/_files/partial_array_of_partials.mustache
@@ -0,0 +1,4 @@
+Here is some stuff!
+{{#numbers}}
+{{>partial}}
+{{/numbers}}
diff --git a/test/helpers/_files/partial_array_of_partials.partial b/test/helpers/_files/partial_array_of_partials.partial
new file mode 100644
index 0000000..bdde77d
--- /dev/null
+++ b/test/helpers/_files/partial_array_of_partials.partial
@@ -0,0 +1 @@
+{{i}}
diff --git a/test/helpers/_files/partial_array_of_partials.txt b/test/helpers/_files/partial_array_of_partials.txt
new file mode 100644
index 0000000..f622375
--- /dev/null
+++ b/test/helpers/_files/partial_array_of_partials.txt
@@ -0,0 +1,5 @@
+Here is some stuff!
+1
+2
+3
+4
diff --git a/test/helpers/_files/partial_array_of_partials_implicit.js b/test/helpers/_files/partial_array_of_partials_implicit.js
new file mode 100644
index 0000000..350ad17
--- /dev/null
+++ b/test/helpers/_files/partial_array_of_partials_implicit.js
@@ -0,0 +1,3 @@
+({
+ numbers: ['1', '2', '3', '4']
+});
diff --git a/test/helpers/_files/partial_array_of_partials_implicit.mustache b/test/helpers/_files/partial_array_of_partials_implicit.mustache
new file mode 100644
index 0000000..1af6d68
--- /dev/null
+++ b/test/helpers/_files/partial_array_of_partials_implicit.mustache
@@ -0,0 +1,4 @@
+Here is some stuff!
+{{#numbers}}
+{{>partial}}
+{{/numbers}}
diff --git a/test/helpers/_files/partial_array_of_partials_implicit.partial b/test/helpers/_files/partial_array_of_partials_implicit.partial
new file mode 100644
index 0000000..12f7159
--- /dev/null
+++ b/test/helpers/_files/partial_array_of_partials_implicit.partial
@@ -0,0 +1 @@
+{{.}}
diff --git a/test/helpers/_files/partial_array_of_partials_implicit.txt b/test/helpers/_files/partial_array_of_partials_implicit.txt
new file mode 100644
index 0000000..f622375
--- /dev/null
+++ b/test/helpers/_files/partial_array_of_partials_implicit.txt
@@ -0,0 +1,5 @@
+Here is some stuff!
+1
+2
+3
+4
diff --git a/test/helpers/_files/partial_empty.js b/test/helpers/_files/partial_empty.js
new file mode 100644
index 0000000..03db6fd
--- /dev/null
+++ b/test/helpers/_files/partial_empty.js
@@ -0,0 +1,3 @@
+({
+ foo: 1
+});
diff --git a/test/helpers/_files/partial_empty.mustache b/test/helpers/_files/partial_empty.mustache
new file mode 100644
index 0000000..a710047
--- /dev/null
+++ b/test/helpers/_files/partial_empty.mustache
@@ -0,0 +1,2 @@
+hey {{foo}}
+{{>partial}}
diff --git a/test/helpers/_files/partial_empty.partial b/test/helpers/_files/partial_empty.partial
new file mode 100644
index 0000000..e69de29
diff --git a/test/helpers/_files/partial_empty.txt b/test/helpers/_files/partial_empty.txt
new file mode 100644
index 0000000..1a67907
--- /dev/null
+++ b/test/helpers/_files/partial_empty.txt
@@ -0,0 +1 @@
+hey 1
diff --git a/test/helpers/_files/partial_template.js b/test/helpers/_files/partial_template.js
new file mode 100644
index 0000000..a527657
--- /dev/null
+++ b/test/helpers/_files/partial_template.js
@@ -0,0 +1,6 @@
+({
+ title: function () {
+ return 'Welcome';
+ },
+ again: 'Goodbye'
+});
diff --git a/test/helpers/_files/partial_template.mustache b/test/helpers/_files/partial_template.mustache
new file mode 100644
index 0000000..6a7492e
--- /dev/null
+++ b/test/helpers/_files/partial_template.mustache
@@ -0,0 +1,2 @@
+{{title}}
+{{>partial}}
diff --git a/test/helpers/_files/partial_template.partial b/test/helpers/_files/partial_template.partial
new file mode 100644
index 0000000..a404529
--- /dev/null
+++ b/test/helpers/_files/partial_template.partial
@@ -0,0 +1 @@
+Again, {{again}}!
diff --git a/test/helpers/_files/partial_template.txt b/test/helpers/_files/partial_template.txt
new file mode 100644
index 0000000..692698f
--- /dev/null
+++ b/test/helpers/_files/partial_template.txt
@@ -0,0 +1,2 @@
+Welcome
+Again, Goodbye!
diff --git a/test/helpers/_files/partial_view.js b/test/helpers/_files/partial_view.js
new file mode 100644
index 0000000..6a83f54
--- /dev/null
+++ b/test/helpers/_files/partial_view.js
@@ -0,0 +1,14 @@
+({
+ greeting: function () {
+ return 'Welcome';
+ },
+ farewell: function () {
+ return 'Fair enough, right?';
+ },
+ name: 'Chris',
+ value: 10000,
+ taxed_value: function () {
+ return this.value - (this.value * 0.4);
+ },
+ in_ca: true
+});
diff --git a/test/helpers/_files/partial_view.mustache b/test/helpers/_files/partial_view.mustache
new file mode 100644
index 0000000..f8f6a5b
--- /dev/null
+++ b/test/helpers/_files/partial_view.mustache
@@ -0,0 +1,3 @@
+{{greeting}}
+{{>partial}}
+{{farewell}}
diff --git a/test/helpers/_files/partial_view.partial b/test/helpers/_files/partial_view.partial
new file mode 100644
index 0000000..03df206
--- /dev/null
+++ b/test/helpers/_files/partial_view.partial
@@ -0,0 +1,5 @@
+Hello {{name}}
+You have just won ${{value}}!
+{{#in_ca}}
+Well, ${{ taxed_value }}, after taxes.
+{{/in_ca}}
\ No newline at end of file
diff --git a/test/helpers/_files/partial_view.txt b/test/helpers/_files/partial_view.txt
new file mode 100644
index 0000000..c09147c
--- /dev/null
+++ b/test/helpers/_files/partial_view.txt
@@ -0,0 +1,5 @@
+Welcome
+Hello Chris
+You have just won $10000!
+Well, $6000, after taxes.
+Fair enough, right?
diff --git a/test/helpers/_files/partial_whitespace.js b/test/helpers/_files/partial_whitespace.js
new file mode 100644
index 0000000..6a83f54
--- /dev/null
+++ b/test/helpers/_files/partial_whitespace.js
@@ -0,0 +1,14 @@
+({
+ greeting: function () {
+ return 'Welcome';
+ },
+ farewell: function () {
+ return 'Fair enough, right?';
+ },
+ name: 'Chris',
+ value: 10000,
+ taxed_value: function () {
+ return this.value - (this.value * 0.4);
+ },
+ in_ca: true
+});
diff --git a/test/helpers/_files/partial_whitespace.mustache b/test/helpers/_files/partial_whitespace.mustache
new file mode 100644
index 0000000..48bd1ff
--- /dev/null
+++ b/test/helpers/_files/partial_whitespace.mustache
@@ -0,0 +1,3 @@
+{{ greeting }}
+{{> partial }}
+{{ farewell }}
diff --git a/test/helpers/_files/partial_whitespace.partial b/test/helpers/_files/partial_whitespace.partial
new file mode 100644
index 0000000..30de8f6
--- /dev/null
+++ b/test/helpers/_files/partial_whitespace.partial
@@ -0,0 +1,5 @@
+Hello {{ name}}
+You have just won ${{value }}!
+{{# in_ca }}
+Well, ${{ taxed_value }}, after taxes.
+{{/ in_ca }}
\ No newline at end of file
diff --git a/test/helpers/_files/partial_whitespace.txt b/test/helpers/_files/partial_whitespace.txt
new file mode 100644
index 0000000..c09147c
--- /dev/null
+++ b/test/helpers/_files/partial_whitespace.txt
@@ -0,0 +1,5 @@
+Welcome
+Hello Chris
+You have just won $10000!
+Well, $6000, after taxes.
+Fair enough, right?
diff --git a/test/helpers/_files/recursion_with_same_names.js b/test/helpers/_files/recursion_with_same_names.js
new file mode 100644
index 0000000..fd0f7d0
--- /dev/null
+++ b/test/helpers/_files/recursion_with_same_names.js
@@ -0,0 +1,8 @@
+({
+ name: 'name',
+ description: 'desc',
+ terms: [
+ {name: 't1', index: 0},
+ {name: 't2', index: 1}
+ ]
+});
diff --git a/test/helpers/_files/recursion_with_same_names.mustache b/test/helpers/_files/recursion_with_same_names.mustache
new file mode 100644
index 0000000..c331d04
--- /dev/null
+++ b/test/helpers/_files/recursion_with_same_names.mustache
@@ -0,0 +1,7 @@
+{{ name }}
+{{ description }}
+
+{{#terms}}
+ {{name}}
+ {{index}}
+{{/terms}}
diff --git a/test/helpers/_files/recursion_with_same_names.txt b/test/helpers/_files/recursion_with_same_names.txt
new file mode 100644
index 0000000..cb15d75
--- /dev/null
+++ b/test/helpers/_files/recursion_with_same_names.txt
@@ -0,0 +1,7 @@
+name
+desc
+
+ t1
+ 0
+ t2
+ 1
diff --git a/test/helpers/_files/reuse_of_enumerables.js b/test/helpers/_files/reuse_of_enumerables.js
new file mode 100644
index 0000000..b59b4a8
--- /dev/null
+++ b/test/helpers/_files/reuse_of_enumerables.js
@@ -0,0 +1,6 @@
+({
+ terms: [
+ {name: 't1', index: 0},
+ {name: 't2', index: 1}
+ ]
+});
diff --git a/test/helpers/_files/reuse_of_enumerables.mustache b/test/helpers/_files/reuse_of_enumerables.mustache
new file mode 100644
index 0000000..cc0cb7a
--- /dev/null
+++ b/test/helpers/_files/reuse_of_enumerables.mustache
@@ -0,0 +1,8 @@
+{{#terms}}
+ {{name}}
+ {{index}}
+{{/terms}}
+{{#terms}}
+ {{name}}
+ {{index}}
+{{/terms}}
diff --git a/test/helpers/_files/reuse_of_enumerables.txt b/test/helpers/_files/reuse_of_enumerables.txt
new file mode 100644
index 0000000..6d05d96
--- /dev/null
+++ b/test/helpers/_files/reuse_of_enumerables.txt
@@ -0,0 +1,8 @@
+ t1
+ 0
+ t2
+ 1
+ t1
+ 0
+ t2
+ 1
diff --git a/test/helpers/_files/section_as_context.js b/test/helpers/_files/section_as_context.js
new file mode 100644
index 0000000..aeb7b1f
--- /dev/null
+++ b/test/helpers/_files/section_as_context.js
@@ -0,0 +1,10 @@
+({
+ a_object: {
+ title: 'this is an object',
+ description: 'one of its attributes is a list',
+ a_list: [
+ {label: 'listitem1'},
+ {label: 'listitem2'}
+ ]
+ }
+});
diff --git a/test/helpers/_files/section_as_context.mustache b/test/helpers/_files/section_as_context.mustache
new file mode 100644
index 0000000..59990f6
--- /dev/null
+++ b/test/helpers/_files/section_as_context.mustache
@@ -0,0 +1,9 @@
+{{#a_object}}
+ {{title}}
+ {{description}}
+
+ {{#a_list}}
+ - {{label}}
+ {{/a_list}}
+
+{{/a_object}}
diff --git a/test/helpers/_files/section_as_context.txt b/test/helpers/_files/section_as_context.txt
new file mode 100644
index 0000000..d834e80
--- /dev/null
+++ b/test/helpers/_files/section_as_context.txt
@@ -0,0 +1,6 @@
+ this is an object
+ one of its attributes is a list
+
+ - listitem1
+ - listitem2
+
diff --git a/test/helpers/_files/section_functions_in_partials.js b/test/helpers/_files/section_functions_in_partials.js
new file mode 100644
index 0000000..ece5fc9
--- /dev/null
+++ b/test/helpers/_files/section_functions_in_partials.js
@@ -0,0 +1,7 @@
+({
+ bold: function (){
+ return function (text, render) {
+ return '' + render(text) + '';
+ };
+ }
+});
diff --git a/test/helpers/_files/section_functions_in_partials.mustache b/test/helpers/_files/section_functions_in_partials.mustache
new file mode 100644
index 0000000..8164932
--- /dev/null
+++ b/test/helpers/_files/section_functions_in_partials.mustache
@@ -0,0 +1,3 @@
+{{> partial}}
+
+some more text
diff --git a/test/helpers/_files/section_functions_in_partials.partial b/test/helpers/_files/section_functions_in_partials.partial
new file mode 100644
index 0000000..3e90b00
--- /dev/null
+++ b/test/helpers/_files/section_functions_in_partials.partial
@@ -0,0 +1 @@
+{{#bold}}Hello There{{/bold}}
diff --git a/test/helpers/_files/section_functions_in_partials.txt b/test/helpers/_files/section_functions_in_partials.txt
new file mode 100644
index 0000000..2f5955c
--- /dev/null
+++ b/test/helpers/_files/section_functions_in_partials.txt
@@ -0,0 +1,3 @@
+Hello There
+
+some more text
diff --git a/test/helpers/_files/simple.js b/test/helpers/_files/simple.js
new file mode 100644
index 0000000..6c6b1f0
--- /dev/null
+++ b/test/helpers/_files/simple.js
@@ -0,0 +1,8 @@
+({
+ name: 'Chris',
+ value: 10000,
+ taxed_value: function () {
+ return this.value - (this.value * 0.4);
+ },
+ in_ca: true
+});
diff --git a/test/helpers/_files/simple.mustache b/test/helpers/_files/simple.mustache
new file mode 100644
index 0000000..2fea632
--- /dev/null
+++ b/test/helpers/_files/simple.mustache
@@ -0,0 +1,5 @@
+Hello {{name}}
+You have just won ${{value}}!
+{{#in_ca}}
+Well, ${{ taxed_value }}, after taxes.
+{{/in_ca}}
diff --git a/test/helpers/_files/simple.txt b/test/helpers/_files/simple.txt
new file mode 100644
index 0000000..5d75d65
--- /dev/null
+++ b/test/helpers/_files/simple.txt
@@ -0,0 +1,3 @@
+Hello Chris
+You have just won $10000!
+Well, $6000, after taxes.
diff --git a/test/helpers/_files/string_as_context.js b/test/helpers/_files/string_as_context.js
new file mode 100644
index 0000000..118e2b5
--- /dev/null
+++ b/test/helpers/_files/string_as_context.js
@@ -0,0 +1,4 @@
+({
+ a_string: 'aa',
+ a_list: ['a','b','c']
+});
diff --git a/test/helpers/_files/string_as_context.mustache b/test/helpers/_files/string_as_context.mustache
new file mode 100644
index 0000000..00f7181
--- /dev/null
+++ b/test/helpers/_files/string_as_context.mustache
@@ -0,0 +1,5 @@
+
+{{#a_list}}
+ - {{a_string}}/{{.}}
+{{/a_list}}
+
\ No newline at end of file
diff --git a/test/helpers/_files/string_as_context.txt b/test/helpers/_files/string_as_context.txt
new file mode 100644
index 0000000..8bd87ff
--- /dev/null
+++ b/test/helpers/_files/string_as_context.txt
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/test/helpers/_files/two_in_a_row.js b/test/helpers/_files/two_in_a_row.js
new file mode 100644
index 0000000..272f860
--- /dev/null
+++ b/test/helpers/_files/two_in_a_row.js
@@ -0,0 +1,4 @@
+({
+ name: 'Joe',
+ greeting: 'Welcome'
+});
diff --git a/test/helpers/_files/two_in_a_row.mustache b/test/helpers/_files/two_in_a_row.mustache
new file mode 100644
index 0000000..b23f29e
--- /dev/null
+++ b/test/helpers/_files/two_in_a_row.mustache
@@ -0,0 +1 @@
+{{greeting}}, {{name}}!
diff --git a/test/helpers/_files/two_in_a_row.txt b/test/helpers/_files/two_in_a_row.txt
new file mode 100644
index 0000000..c6d6a9b
--- /dev/null
+++ b/test/helpers/_files/two_in_a_row.txt
@@ -0,0 +1 @@
+Welcome, Joe!
diff --git a/test/helpers/_files/two_sections.js b/test/helpers/_files/two_sections.js
new file mode 100644
index 0000000..b3bf1dd
--- /dev/null
+++ b/test/helpers/_files/two_sections.js
@@ -0,0 +1 @@
+({});
diff --git a/test/helpers/_files/two_sections.mustache b/test/helpers/_files/two_sections.mustache
new file mode 100644
index 0000000..a4b9f2a
--- /dev/null
+++ b/test/helpers/_files/two_sections.mustache
@@ -0,0 +1,4 @@
+{{#foo}}
+{{/foo}}
+{{#bar}}
+{{/bar}}
diff --git a/test/helpers/_files/two_sections.txt b/test/helpers/_files/two_sections.txt
new file mode 100644
index 0000000..e69de29
diff --git a/test/helpers/_files/unescaped.js b/test/helpers/_files/unescaped.js
new file mode 100644
index 0000000..3b4d3cb
--- /dev/null
+++ b/test/helpers/_files/unescaped.js
@@ -0,0 +1,6 @@
+({
+ title: function () {
+ return 'Bear > Shark';
+ },
+ symbol: null
+});
diff --git a/test/helpers/_files/unescaped.mustache b/test/helpers/_files/unescaped.mustache
new file mode 100644
index 0000000..1a27f58
--- /dev/null
+++ b/test/helpers/_files/unescaped.mustache
@@ -0,0 +1 @@
+{{{title}}}{{{symbol}}}
diff --git a/test/helpers/_files/unescaped.txt b/test/helpers/_files/unescaped.txt
new file mode 100644
index 0000000..089ad79
--- /dev/null
+++ b/test/helpers/_files/unescaped.txt
@@ -0,0 +1 @@
+Bear > Shark
diff --git a/test/helpers/_files/uses_props_from_view_prototype.js b/test/helpers/_files/uses_props_from_view_prototype.js
new file mode 100644
index 0000000..6754348
--- /dev/null
+++ b/test/helpers/_files/uses_props_from_view_prototype.js
@@ -0,0 +1,30 @@
+var Aaa = (function () {
+ function Aaa (x, y) {
+ this.x = x;
+ this._y = y;
+ }
+ Object.defineProperty(Aaa.prototype, 'y', {
+ get: function () {
+ return this._y;
+ },
+ set: function (value) {
+ this._y = value;
+ },
+ enumerable: true,
+ configurable: true
+ });
+ return Aaa;
+})();
+var Bbb = (function () {
+ function Bbb () {
+ }
+ return Bbb;
+})();
+
+var b = new Bbb();
+b.item = new Aaa('0', '00');
+b.items = [];
+b.items.push({ a: new Aaa('1', '2') });
+b.items.push({ a: new Aaa('3', '4') });
+
+(b);
diff --git a/test/helpers/_files/uses_props_from_view_prototype.mustache b/test/helpers/_files/uses_props_from_view_prototype.mustache
new file mode 100644
index 0000000..874f0c9
--- /dev/null
+++ b/test/helpers/_files/uses_props_from_view_prototype.mustache
@@ -0,0 +1 @@
+[{{ item.x }};{{ item.y }}]||{{#items}}[{{ a.x }};{{ a.y }} {{#a}}{{y}}{{/a}}]{{/items}}
\ No newline at end of file
diff --git a/test/helpers/_files/uses_props_from_view_prototype.txt b/test/helpers/_files/uses_props_from_view_prototype.txt
new file mode 100644
index 0000000..f416840
--- /dev/null
+++ b/test/helpers/_files/uses_props_from_view_prototype.txt
@@ -0,0 +1 @@
+[0;00]||[1;2 2][3;4 4]
\ No newline at end of file
diff --git a/test/helpers/_files/whitespace.js b/test/helpers/_files/whitespace.js
new file mode 100644
index 0000000..dc12fe2
--- /dev/null
+++ b/test/helpers/_files/whitespace.js
@@ -0,0 +1,4 @@
+({
+ tag1: 'Hello',
+ tag2: 'World'
+});
diff --git a/test/helpers/_files/whitespace.mustache b/test/helpers/_files/whitespace.mustache
new file mode 100644
index 0000000..aa76e08
--- /dev/null
+++ b/test/helpers/_files/whitespace.mustache
@@ -0,0 +1,4 @@
+{{tag1}}
+
+
+{{tag2}}.
diff --git a/test/helpers/_files/whitespace.txt b/test/helpers/_files/whitespace.txt
new file mode 100644
index 0000000..851fa74
--- /dev/null
+++ b/test/helpers/_files/whitespace.txt
@@ -0,0 +1,4 @@
+Hello
+
+
+World.
diff --git a/test/helpers/_files/zero_view.js b/test/helpers/_files/zero_view.js
new file mode 100644
index 0000000..d584042
--- /dev/null
+++ b/test/helpers/_files/zero_view.js
@@ -0,0 +1 @@
+({ nums: [0, 1, 2] });
diff --git a/test/helpers/_files/zero_view.mustache b/test/helpers/_files/zero_view.mustache
new file mode 100644
index 0000000..1cdc190
--- /dev/null
+++ b/test/helpers/_files/zero_view.mustache
@@ -0,0 +1 @@
+{{#nums}}{{.}},{{/nums}}
\ No newline at end of file
diff --git a/test/helpers/_files/zero_view.txt b/test/helpers/_files/zero_view.txt
new file mode 100644
index 0000000..2aee585
--- /dev/null
+++ b/test/helpers/_files/zero_view.txt
@@ -0,0 +1 @@
+0,1,2,
\ No newline at end of file
diff --git a/test/helpers/render-helper.ts b/test/helpers/render-helper.ts
new file mode 100644
index 0000000..5dc668a
--- /dev/null
+++ b/test/helpers/render-helper.ts
@@ -0,0 +1,61 @@
+const getDirPath = (): string => {
+ const filePath = new URL(import.meta.url).pathname;
+ const dirPath = filePath.split("/").slice(0, -1).join("/");
+ return dirPath;
+};
+
+const filesPath = `${getDirPath()}/_files`;
+
+const getContens = async (testName: string, ext: string): Promise => {
+ try {
+ const contents = await Deno.readTextFile(`${filesPath}/${testName}.${ext}`);
+ return contents;
+ } catch (_ex) {
+ return null;
+ }
+};
+
+const getView = async (testName: string): Promise => {
+ let view = await getContens(testName, "js");
+ if (!view) view = await getContens(testName, "cjs");
+ if (!view) throw new Error(`Cannot find view for test "${testName}"`);
+ return view;
+};
+
+const getPartial = async (testName: string): Promise => {
+ try {
+ const partial = await getContens(testName, "partial");
+ return partial;
+ } catch (_ex) {
+ return null;
+ }
+};
+
+export interface Test {
+ name: string;
+ view: string;
+ template: string | null;
+ partial: string | null;
+ expect: string | null;
+}
+
+const getTest = async (testName: string): Promise => {
+ const view = await getView(testName);
+ const template = await getContens(testName, "mustache");
+ const partial = await getPartial(testName);
+ const expect = await getContens(testName, "txt");
+ return { name: testName, view, template, partial, expect };
+};
+
+export const getTests = async (): Promise => {
+ const testNames = Deno.readDir(filesPath);
+ const tests: Test[] = [];
+
+ for await (const file of testNames) {
+ if (!file.name.endsWith(".js") && !file.name.endsWith(".cjs")) continue;
+ const testName = file.name.split(".")[0];
+ const test = await getTest(testName);
+ tests.push(test);
+ }
+ return tests;
+};
diff --git a/test/parse.test.ts b/test/parse.test.ts
new file mode 100644
index 0000000..9ceef73
--- /dev/null
+++ b/test/parse.test.ts
@@ -0,0 +1,188 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import {
+ assertEquals,
+ assertNotEquals,
+ assertThrows,
+ beforeAll,
+ beforeEach,
+ describe,
+ it,
+} from "../dev_deps.ts";
+
+import mustache from "../src/lib/Mustache.ts";
+
+import { Token } from "../src/lib/parse.ts";
+
+// A map of templates to their expected token output. Tokens are in the format:
+// [type, value, startIndex, endIndex, subTokens].
+
+// @deno-fmt-ignore
+const expectations = {
+ '' : [],
+ '{{hi}}' : [ [ 'name', 'hi', 0, 6 ] ],
+ '{{hi.world}}' : [ [ 'name', 'hi.world', 0, 12 ] ],
+ '{{hi . world}}' : [ [ 'name', 'hi . world', 0, 14 ] ],
+ '{{ hi}}' : [ [ 'name', 'hi', 0, 7 ] ],
+ '{{hi }}' : [ [ 'name', 'hi', 0, 7 ] ],
+ '{{ hi }}' : [ [ 'name', 'hi', 0, 8 ] ],
+ '{{{hi}}}' : [ [ '&', 'hi', 0, 8 ] ],
+ '{{!hi}}' : [ [ '!', 'hi', 0, 7 ] ],
+ '{{! hi}}' : [ [ '!', 'hi', 0, 8 ] ],
+ '{{! hi }}' : [ [ '!', 'hi', 0, 9 ] ],
+ '{{ !hi}}' : [ [ '!', 'hi', 0, 8 ] ],
+ '{{ ! hi}}' : [ [ '!', 'hi', 0, 9 ] ],
+ '{{ ! hi }}' : [ [ '!', 'hi', 0, 10 ] ],
+ 'a\n b' : [ [ 'text', 'a\n b', 0, 4 ] ],
+ 'a{{hi}}' : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ] ],
+ 'a {{hi}}' : [ [ 'text', 'a ', 0, 2 ], [ 'name', 'hi', 2, 8 ] ],
+ ' a{{hi}}' : [ [ 'text', ' a', 0, 2 ], [ 'name', 'hi', 2, 8 ] ],
+ ' a {{hi}}' : [ [ 'text', ' a ', 0, 3 ], [ 'name', 'hi', 3, 9 ] ],
+ 'a{{hi}}b' : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', 'b', 7, 8 ] ],
+ 'a{{hi}} b' : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', ' b', 7, 9 ] ],
+ 'a{{hi}}b ' : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', 'b ', 7, 9 ] ],
+ 'a\n{{hi}} b \n' : [ [ 'text', 'a\n', 0, 2 ], [ 'name', 'hi', 2, 8 ], [ 'text', ' b \n', 8, 12 ] ],
+ 'a\n {{hi}} \nb' : [ [ 'text', 'a\n ', 0, 3 ], [ 'name', 'hi', 3, 9 ], [ 'text', ' \nb', 9, 12 ] ],
+ 'a\n {{!hi}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '!', 'hi', 3, 10 ], [ 'text', 'b', 12, 13 ] ],
+ 'a\n{{#a}}{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [], 8 ], [ 'text', 'b', 15, 16 ] ],
+ 'a\n {{#a}}{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 9 ], [ 'text', 'b', 16, 17 ] ],
+ 'a\n {{#a}}{{/a}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 9 ], [ 'text', 'b', 17, 18 ] ],
+ 'a\n{{#a}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [], 9 ], [ 'text', 'b', 16, 17 ] ],
+ 'a\n {{#a}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 10 ], [ 'text', 'b', 17, 18 ] ],
+ 'a\n {{#a}}\n{{/a}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 10 ], [ 'text', 'b', 18, 19 ] ],
+ 'a\n{{#a}}\n{{/a}}\n{{#b}}\n{{/b}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [], 9 ], [ '#', 'b', 16, 22, [], 23 ], [ 'text', 'b', 30, 31 ] ],
+ 'a\n {{#a}}\n{{/a}}\n{{#b}}\n{{/b}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 10 ], [ '#', 'b', 17, 23, [], 24 ], [ 'text', 'b', 31, 32 ] ],
+ 'a\n {{#a}}\n{{/a}}\n{{#b}}\n{{/b}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 10 ], [ '#', 'b', 17, 23, [], 24 ], [ 'text', 'b', 32, 33 ] ],
+ 'a\n{{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [ [ '#', 'b', 9, 15, [], 16 ] ], 23 ], [ 'text', 'b', 30, 31 ] ],
+ 'a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [ [ '#', 'b', 10, 16, [], 17 ] ], 24 ], [ 'text', 'b', 31, 32 ] ],
+ 'a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [ [ '#', 'b', 10, 16, [], 17 ] ], 24 ], [ 'text', 'b', 32, 33 ] ],
+ '{{>abc}}' : [ [ '>', 'abc', 0, 8, '', 0, false ] ],
+ '{{> abc }}' : [ [ '>', 'abc', 0, 10, '', 0, false ] ],
+ '{{ > abc }}' : [ [ '>', 'abc', 0, 11, '', 0, false ] ],
+ ' {{> abc }}\n' : [ [ '>', 'abc', 2, 12, ' ', 0, false ] ],
+ ' {{> abc }} {{> abc }}\n' : [ [ '>', 'abc', 2, 12, ' ', 0, false ], [ '>', 'abc', 13, 23, ' ', 1, false ] ],
+ '{{=<% %>=}}' : [ [ '=', '<% %>', 0, 11 ] ],
+ '{{= <% %> =}}' : [ [ '=', '<% %>', 0, 13 ] ],
+ '{{=<% %>=}}<%={{ }}=%>' : [ [ '=', '<% %>', 0, 11 ], [ '=', '{{ }}', 11, 22 ] ],
+ '{{=<% %>=}}<%hi%>' : [ [ '=', '<% %>', 0, 11 ], [ 'name', 'hi', 11, 17 ] ],
+ '{{#a}}{{/a}}hi{{#b}}{{/b}}\n' : [ [ '#', 'a', 0, 6, [], 6 ], [ 'text', 'hi', 12, 14 ], [ '#', 'b', 14, 20, [], 20 ], [ 'text', '\n', 26, 27 ] ],
+ '{{a}}\n{{b}}\n\n{{#c}}\n{{/c}}\n' : [ [ 'name', 'a', 0, 5 ], [ 'text', '\n', 5, 6 ], [ 'name', 'b', 6, 11 ], [ 'text', '\n\n', 11, 13 ], [ '#', 'c', 13, 19, [], 20 ] ],
+ '{{#foo}}\n {{#a}}\n {{b}}\n {{/a}}\n{{/foo}}\n'
+ : [ [ '#', 'foo', 0, 8, [ [ '#', 'a', 11, 17, [ [ 'text', ' ', 18, 22 ], [ 'name', 'b', 22, 27 ], [ 'text', '\n', 27, 28 ] ], 30 ] ], 37 ] ]
+};
+
+let originalTemplateCache: Map;
+
+describe("mustache.parse", () => {
+ beforeAll(() => {
+ originalTemplateCache = mustache.templateCache as Map;
+ });
+
+ beforeEach(() => {
+ mustache.clearCache();
+ mustache.templateCache = originalTemplateCache;
+ });
+
+ for (const template in expectations) {
+ if (Object.prototype.hasOwnProperty.call(expectations, template)) {
+ ((template, tokens) => {
+ it("knows how to parse " + JSON.stringify(template), () => {
+ assertEquals(mustache.parse(template), tokens as Token[]);
+ });
+ })(template, expectations[template as keyof typeof expectations]);
+ }
+ }
+
+ describe("when there is an unclosed tag", () => {
+ it("throws an error", () => {
+ assertThrows(() => {
+ mustache.parse("My name is {{name");
+ });
+ });
+ });
+
+ describe("when there is an unclosed section", () => {
+ it("throws an error", () => {
+ assertThrows(() => {
+ mustache.parse("A list: {{#people}}{{name}}");
+ });
+ });
+ });
+
+ describe("when there is an unopened section", () => {
+ it("throws an error", () => {
+ assertThrows(() => {
+ mustache.parse("The end of the list! {{/people}}");
+ });
+ });
+ });
+
+ describe("when invalid tags are given as an argument", () => {
+ it("throws an error", () => {
+ assertThrows(() => {
+ mustache.parse("A template <% name %>", ["<%"]);
+ });
+ });
+ });
+
+ describe("when the template contains invalid tags", () => {
+ it("throws an error", () => {
+ assertThrows(() => {
+ mustache.parse("A template {{=<%=}}");
+ });
+ });
+ });
+
+ describe("when parsing a template without tags specified followed by the same template with tags specified", () => {
+ it("returns different tokens for the latter parse", () => {
+ const template = "{{foo}}[bar]";
+ const parsedWithBraces = mustache.parse(template);
+ const parsedWithBrackets = mustache.parse(template, ["[", "]"]);
+ assertNotEquals(parsedWithBrackets, parsedWithBraces);
+ });
+ });
+
+ describe("when parsing a template with tags specified followed by the same template with different tags specified", () => {
+ it("returns different tokens for the latter parse", () => {
+ const template = "(foo)[bar]";
+ const parsedWithParens = mustache.parse(template, ["(", ")"]);
+ const parsedWithBrackets = mustache.parse(template, ["[", "]"]);
+ assertNotEquals(parsedWithBrackets, parsedWithParens);
+ });
+ });
+
+ describe("when parsing a template after already having parsed that template with a different mustache.tags", () => {
+ it("returns different tokens for the latter parse", () => {
+ const template = "{{foo}}[bar]";
+ const parsedWithBraces = mustache.parse(template);
+
+ const oldTags = mustache.tags;
+ mustache.tags = ["[", "]"];
+ const parsedWithBrackets = mustache.parse(template);
+ mustache.tags = oldTags;
+
+ assertNotEquals(parsedWithBrackets, parsedWithBraces);
+ });
+ });
+
+ describe("when parsing a template with the same tags second time, return the cached tokens", () => {
+ it("returns the same tokens for the latter parse", () => {
+ const template = "{{foo}}[bar]";
+ const parsedResult1 = mustache.parse(template);
+ const parsedResult2 = mustache.parse(template);
+
+ assertEquals(parsedResult1, parsedResult2);
+ });
+ });
+
+ describe("when parsing a template with caching disabled and the same tags second time, do not return the cached tokens", () => {
+ it("returns different tokens for the latter parse", () => {
+ mustache.templateCache = undefined;
+ const template = "{{foo}}[bar]";
+ const parsedResult1 = mustache.parse(template);
+ const parsedResult2 = mustache.parse(template);
+
+ assertEquals(parsedResult1, parsedResult2);
+ });
+ });
+});
diff --git a/test/partial.test.ts b/test/partial.test.ts
new file mode 100644
index 0000000..ec658a8
--- /dev/null
+++ b/test/partial.test.ts
@@ -0,0 +1,175 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { assertEquals, beforeEach, describe, it } from "../dev_deps.ts";
+
+import mustache from "../src/lib/Mustache.ts";
+
+describe("Partials spec", () => {
+ beforeEach(() => {
+ mustache.clearCache();
+ });
+
+ it("The greater-than operator should expand to the named partial.", () => {
+ const template = '"{{>text}}"';
+ const data = {};
+ const partials = { "text": "from partial" };
+ const expected = '"from partial"';
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+ it("The empty string should be used when the named partial is not found.", () => {
+ const template = '"{{>text}}"';
+ const data = {};
+ const partials = {};
+ const expected = '""';
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+ it("The greater-than operator should operate within the current context.", () => {
+ const template = '"{{>partial}}"';
+ const data = { "text": "content" };
+ const partials = { "partial": "*{{text}}*" };
+ const expected = '"*content*"';
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+ it("Inline partials should not be indented", () => {
+ const template = " {{> partial}}
";
+ const data = {};
+ const partials = { "partial": "This is a partial." };
+ const expected = " This is a partial.
";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+
+ it("Inline partials should not be indented (multiline)", () => {
+ const template = " {{> partial}}
";
+ const data = {};
+ const partials = { "partial": "This is a\npartial." };
+ const expected = " This is a\n partial.
";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+
+ it("The greater-than operator should properly recurse.", () => {
+ const template = "{{>node}}";
+ const data = { "content": "X", "nodes": [{ "content": "Y", "nodes": [] }] };
+ const partials = { "node": "{{content}}<{{#nodes}}{{>node}}{{/nodes}}>" };
+ const expected = "X>";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+ it("The greater-than operator should not alter surrounding whitespace.", () => {
+ const template = "| {{>partial}} |";
+ const data = {};
+ const partials = { "partial": "\t|\t" };
+ const expected = "| \t|\t |";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+ it('"\r\n" should be considered a newline for standalone tags.', () => {
+ const template = "|\r\n{{>partial}}\r\n|";
+ const data = {};
+ const partials = { "partial": ">" };
+ const expected = "|\r\n>|";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+ it("Standalone tags should not require a newline to precede them.", () => {
+ const template = " {{>partial}}\n>";
+ const data = {};
+ const partials = { "partial": ">\n>" };
+ const expected = " >\n >>";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+ it("Superfluous in-tag whitespace should be ignored.", () => {
+ const template = "|{{> partial }}|";
+ const data = { "boolean": true };
+ const partials = { "partial": "[]" };
+ const expected = "|[]|";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+ it("Each line of the partial should be indented before rendering.", () => {
+ const template = "\\\n {{>partial}}\n/\n";
+ const data = {
+ "content": "<\n->",
+ };
+ const partials = {
+ "partial": "|\n{{{content}}}\n|\n",
+ };
+ const expected = "\\\n |\n <\n->\n |\n/\n";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+
+ it("Standalone tags should not require a newline to follow them.", () => {
+ const template = ">\n {{>partial}}";
+ const data = {};
+ const partials = {
+ "partial": ">\n>",
+ };
+ const expected = ">\n >\n >";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+
+ it("Whitespace should be left untouched.", () => {
+ const template = " {{data}} {{> partial}}\n";
+ const data = {
+ "data": "|",
+ };
+ const partials = {
+ "partial": ">\n>",
+ };
+ const expected = " | >\n>\n";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+
+ it("Partial without indentation should inherit functions.", () => {
+ const template = "{{> partial }}";
+ const data = {
+ toUpperCase: () => {
+ return function (label: string): string {
+ return label.toUpperCase();
+ };
+ },
+ };
+ const partials = { partial: "aA-{{ #toUpperCase }}Input{{ /toUpperCase }}-Aa" };
+ const expected = "aA-INPUT-Aa";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+
+ it("Partial with indentation should inherit functions.", () => {
+ const template = " {{> partial }}";
+ const data = {
+ toUpperCase: () => {
+ return function (label: string): string {
+ return label.toUpperCase();
+ };
+ },
+ };
+ const partials = { partial: "aA-{{ #toUpperCase }}Input{{ /toUpperCase }}-Aa" };
+ const expected = " aA-INPUT-Aa";
+ const renderResult = mustache.render(template, data, partials);
+ assertEquals(renderResult, expected);
+ });
+
+ it("Nested partials should support custom delimiters.", () => {
+ const tags = ["[[", "]]"];
+ const template = "[[> level1 ]]";
+ const partials = {
+ level1: "partial 1\n[[> level2]]",
+ level2: "partial 2\n[[> level3]]",
+ level3: "partial 3\n[[> level4]]",
+ level4: "partial 4\n[[> level5]]",
+ level5: "partial 5",
+ };
+ const expected = "partial 1\npartial 2\npartial 3\npartial 4\npartial 5";
+ const renderResult = mustache.render(template, {}, partials, tags);
+ assertEquals(renderResult, expected);
+ });
+});
diff --git a/test/render.test.ts b/test/render.test.ts
new file mode 100644
index 0000000..7d6b527
--- /dev/null
+++ b/test/render.test.ts
@@ -0,0 +1,305 @@
+// Copyright 2023 J.W. Lagendijk. All rights reserved. MIT license.
+
+import { assertEquals, assertThrows, beforeEach, describe, it } from "../dev_deps.ts";
+import { getTests } from "./helpers/render-helper.ts";
+
+import mustache from "../src/lib/Mustache.ts";
+
+const tests = await getTests();
+
+describe("mustache.render", () => {
+ beforeEach(() => {
+ mustache.clearCache();
+ });
+
+ it("requires template to be a string", () => {
+ assertThrows(
+ () => {
+ // @ts-ignore This should be caught by the type system, but for JS users we need to check at runtime
+ mustache.render(["dummy template"], ["foo", "bar"]);
+ },
+ TypeError,
+ 'Invalid template! Template should be a "string" but ' +
+ '"array" was given as the first argument ' +
+ "for mustache#render(template, view, partials)",
+ );
+ });
+
+ describe("custom tags", () => {
+ it("uses tags argument instead of mustache.tags when given", () => {
+ const template = "<>bar{{placeholder}}";
+
+ mustache.tags = ["{{", "}}"];
+ assertEquals(
+ mustache.render(template, { placeholder: "foo" }, {}, ["<<", ">>"]),
+ "foobar{{placeholder}}",
+ );
+ });
+
+ it("uses config.tags argument instead of mustache.tags when given", () => {
+ const template = "<>bar{{placeholder}}";
+
+ mustache.tags = ["{{", "}}"];
+ assertEquals(
+ mustache.render(template, { placeholder: "foo" }, {}, { tags: ["<<", ">>"] }),
+ "foobar{{placeholder}}",
+ );
+ });
+
+ it("uses tags argument instead of mustache.tags when given, even when it previously rendered the template using mustache.tags", () => {
+ const template = "((placeholder))bar{{placeholder}}";
+
+ mustache.tags = ["{{", "}}"];
+ mustache.render(template, { placeholder: "foo" });
+ assertEquals(
+ mustache.render(template, { placeholder: "foo" }, {}, ["((", "))"]),
+ "foobar{{placeholder}}",
+ );
+ });
+
+ it("uses config.tags argument instead of mustache.tags when given, even when it previously rendered the template using mustache.tags", () => {
+ const template = "((placeholder))bar{{placeholder}}";
+
+ mustache.tags = ["{{", "}}"];
+ mustache.render(template, { placeholder: "foo" });
+ assertEquals(
+ mustache.render(template, { placeholder: "foo" }, {}, { tags: ["((", "))"] }),
+ "foobar{{placeholder}}",
+ );
+ });
+
+ it("uses tags argument instead of mustache.tags when given, even when it previously rendered the template using different tags", () => {
+ const template = "[[placeholder]]bar<>";
+
+ mustache.render(template, { placeholder: "foo" }, {}, ["<<", ">>"]);
+ assertEquals(
+ mustache.render(template, { placeholder: "foo" }, {}, ["[[", "]]"]),
+ "foobar<>",
+ );
+ });
+
+ it("uses config.tags argument instead of mustache.tags when given, even when it previously rendered the template using different tags", () => {
+ const template = "[[placeholder]]bar<>";
+
+ mustache.render(template, { placeholder: "foo" }, {}, ["<<", ">>"]);
+ assertEquals(
+ mustache.render(template, { placeholder: "foo" }, {}, { tags: ["[[", "]]"] }),
+ "foobar<>",
+ );
+ });
+
+ it("does not mutate mustache.tags when given tags argument", () => {
+ const correctMustacheTags = ["{{", "}}"];
+ mustache.tags = correctMustacheTags;
+
+ mustache.render("((placeholder))", { placeholder: "foo" }, {}, ["((", "))"]);
+
+ assertEquals(mustache.tags, correctMustacheTags);
+ assertEquals(mustache.tags, ["{{", "}}"]);
+ });
+
+ it("does not mutate mustache.tags when given config.tags argument", () => {
+ const correctMustacheTags = ["{{", "}}"];
+ mustache.tags = correctMustacheTags;
+
+ mustache.render(
+ "((placeholder))",
+ { placeholder: "foo" },
+ {},
+ { tags: ["((", "))"] },
+ );
+
+ assertEquals(mustache.tags, correctMustacheTags);
+ assertEquals(mustache.tags, ["{{", "}}"]);
+ });
+
+ it("uses provided tags when rendering partials", () => {
+ const output = mustache.render("<%> partial %>", { name: "Santa Claus" }, {
+ partial: "<% name %>",
+ }, ["<%", "%>"]);
+
+ assertEquals(output, "Santa Claus");
+ });
+
+ it("uses provided config.tags when rendering partials", () => {
+ const output = mustache.render("<%> partial %>", { name: "Santa Claus" }, {
+ partial: "<% name %>",
+ }, { tags: ["<%", "%>"] });
+
+ assertEquals(output, "Santa Claus");
+ });
+
+ it("uses config.escape argument instead of mustache.escape when given", () => {
+ const template = "Hello, {{placeholder}}";
+
+ function escapeBang(text: string): string {
+ return text + "!";
+ }
+ assertEquals(
+ mustache.render(template, { placeholder: "world" }, {}, { escape: escapeBang }),
+ "Hello, world!",
+ );
+ });
+
+ it("uses config.escape argument instead of mustache.escape when given, even when it previously rendered the template using mustache.escape", () => {
+ const template = "Hello, {{placeholder}}";
+
+ function escapeQuestion(text: string): string {
+ return text + "?";
+ }
+ mustache.render(template, { placeholder: "world" });
+ assertEquals(
+ mustache.render(
+ template,
+ { placeholder: "world" },
+ {},
+ { escape: escapeQuestion },
+ ),
+ "Hello, world?",
+ );
+ });
+
+ it("uses config.escape argument instead of mustache.escape when given, even when it previously rendered the template using a different escape function", () => {
+ const template = "Hello, {{placeholder}}";
+
+ function escapeQuestion(text: string): string {
+ return text + "?";
+ }
+ function escapeBang(text: string): string {
+ return text + "!";
+ }
+ mustache.render(template, { placeholder: "foo" }, {}, { escape: escapeQuestion });
+ assertEquals(
+ mustache.render(template, { placeholder: "foo" }, {}, { escape: escapeBang }),
+ "Hello, foo!",
+ );
+ });
+
+ it("does not mutate mustache.escape when given config.escape argument", () => {
+ const correctMustacheEscape = mustache.escape;
+
+ function escapeNone(text: string): string {
+ return text;
+ }
+ mustache.render(
+ "((placeholder))",
+ { placeholder: "foo" },
+ {},
+ { escape: escapeNone },
+ );
+
+ assertEquals(mustache.escape, correctMustacheEscape);
+ assertEquals(mustache.escape(">&"), ">&");
+ });
+
+ it("uses provided config.escape when rendering partials", () => {
+ function escapeDoubleAmpersand(text: string): string {
+ return text.replace("&", "&&");
+ }
+ const output = mustache.render("{{> partial }}", { name: "Ampersand &" }, {
+ partial: "{{ name }}",
+ }, { escape: escapeDoubleAmpersand });
+
+ assertEquals(output, "Ampersand &&");
+ });
+
+ it("uses config.tags and config.escape arguments instead of mustache.tags and mustache.escape when given", () => {
+ const template = "Hello, {{placeholder}} [[placeholder]]";
+
+ function escapeTwoBangs(text: string): string {
+ return text + "!!";
+ }
+ const config = {
+ tags: ["[[", "]]"],
+ escape: escapeTwoBangs,
+ };
+ assertEquals(
+ mustache.render(template, { placeholder: "world" }, {}, config),
+ "Hello, {{placeholder}} world!!",
+ );
+ });
+
+ it("uses provided config.tags and config.escape when rendering partials", () => {
+ function escapeDoubleAmpersand(text: string): string {
+ return text.replace("&", "&&");
+ }
+ const config = {
+ tags: ["[[", "]]"],
+ escape: escapeDoubleAmpersand,
+ };
+ const output = mustache.render("[[> partial ]]", { name: "Ampersand &" }, {
+ partial: "[[ name ]]",
+ }, config);
+
+ assertEquals(output, "Ampersand &&");
+ });
+
+ it("uses provided config.tags and config.escape when rendering sections", () => {
+ const template = "<[[&value-raw]]: " +
+ "[[#test-1]][[value-1]][[/test-1]]" +
+ "[[^test-2]][[value-2]][[/test-2]], " +
+ "[[#test-lambda]][[value-lambda]][[/test-lambda]]" +
+ ">";
+
+ function escapeQuotes(text: string): string {
+ return '"' + text + '"';
+ }
+ const config = {
+ tags: ["[[", "]]"],
+ escape: escapeQuotes,
+ };
+ const viewTestTrue = {
+ "value-raw": "foo",
+ "test-1": true,
+ "value-1": "abc",
+ "test-2": true,
+ "value-2": "123",
+ "test-lambda": () => {
+ return function (text: string, render: (text: string) => string): string {
+ return "lambda: " + render(text);
+ };
+ },
+ "value-lambda": "bar",
+ };
+ const viewTestFalse = {
+ "value-raw": "foo",
+ "test-1": false,
+ "value-1": "abc",
+ "test-2": false,
+ "value-2": "123",
+ "test-lambda": () => {
+ return function (text: string, render: (text: string) => string): string {
+ return "lambda: " + render(text);
+ };
+ },
+ "value-lambda": "bar",
+ };
+ const outputTrue = mustache.render(template, viewTestTrue, {}, config);
+ const outputFalse = mustache.render(template, viewTestFalse, {}, config);
+
+ assertEquals(outputTrue, '');
+ assertEquals(outputFalse, '');
+ });
+ });
+
+ tests.forEach((test) => {
+ const view = eval(test.view);
+
+ if (test.template === null) {
+ console.log("Skipping " + test.name + " because no template was found.");
+ return;
+ }
+
+ it("knows how to render " + test.name, () => {
+ let output;
+ if (test.partial) {
+ output = mustache.render(test.template as string, view, { partial: test.partial });
+ } else {
+ output = mustache.render(test.template as string, view);
+ }
+
+ // output.should.equal(test.expect);
+ assertEquals(output, test.expect);
+ });
+ });
+});