├── spec
├── fixtures
│ ├── welcome.html
│ ├── import.html
│ ├── hello_world.html.rendered
│ ├── hello_world.html
│ └── layout.html
├── integration
│ ├── compat-suite
│ │ ├── expected
│ │ │ ├── basic.html
│ │ │ ├── global_fn.html
│ │ │ ├── empty_loop.html
│ │ │ ├── comment.html
│ │ │ ├── indexing.html
│ │ │ ├── raw.html
│ │ │ ├── magical_variable.html
│ │ │ ├── loop_with_filters.html
│ │ │ ├── comment_alignment.html
│ │ │ ├── filters.html
│ │ │ ├── loops_set_dot_access.html
│ │ │ ├── variables.html
│ │ │ ├── basic_inheritance.html
│ │ │ ├── variable_tests.html
│ │ │ ├── include.html
│ │ │ ├── many_variable_blocks.html
│ │ │ ├── conditions.html
│ │ │ ├── use_macros.html
│ │ │ └── loops.html
│ │ ├── templates
│ │ │ ├── basic.html
│ │ │ ├── value_render.html
│ │ │ ├── global_fn.html
│ │ │ ├── macro_included.html
│ │ │ ├── comment.html
│ │ │ ├── comment_alignment.html
│ │ │ ├── raw.html
│ │ │ ├── empty_loop.html
│ │ │ ├── loop_with_filters.html
│ │ │ ├── indexing.html
│ │ │ ├── magical_variable.html
│ │ │ ├── include.html
│ │ │ ├── loops_set_dot_access.html
│ │ │ ├── included.html
│ │ │ ├── base.html
│ │ │ ├── use_macros.html
│ │ │ ├── filters.html
│ │ │ ├── basic_inheritance.html
│ │ │ ├── variables.html
│ │ │ ├── macros.html
│ │ │ ├── conditions.html
│ │ │ ├── variable_tests.html
│ │ │ ├── loops.html
│ │ │ └── many_variable_blocks.html
│ │ ├── parser-failures
│ │ │ ├── invalid_number.html
│ │ │ ├── unterminated.html
│ │ │ ├── invalid_operator.html
│ │ │ ├── unexpected_terminator.html
│ │ │ ├── missing_not_expression.html
│ │ │ ├── invalid_extends.html
│ │ │ ├── missing_endblock_name.html
│ │ │ ├── invalid_else.html
│ │ │ ├── wrong_endblock.html
│ │ │ ├── invalid_elif.html
│ │ │ ├── invalid_content_macro.html
│ │ │ └── duplicate_block.html
│ │ └── render-failures
│ │ │ ├── field_unknown.html
│ │ │ ├── non_math_operation.html
│ │ │ ├── value_render_non_object.html
│ │ │ ├── inexisting_include.html
│ │ │ ├── filter_section_invalid.html
│ │ │ ├── iterate_on_non_array.html
│ │ │ ├── field_unknown_forloop.html
│ │ │ ├── macro_self_inexisting.html
│ │ │ ├── error-location
│ │ │ ├── macros.html
│ │ │ ├── base.html
│ │ │ ├── error_in_macro.html
│ │ │ ├── error_in_parent.html
│ │ │ ├── error_in_child.html
│ │ │ ├── error_in_grand_child.html
│ │ │ └── base_error.html
│ │ │ ├── macro_wrong_args.html
│ │ │ └── macros.html
│ ├── if_test_spec.cr
│ ├── json_spec.cr
│ ├── yaml_spec.cr
│ ├── hello_world_spec.cr
│ ├── readme_sample_spec.cr
│ ├── spec_helper.cr
│ └── autoescape_spec.cr
├── functions
│ ├── dict_spec.cr
│ ├── range_spec.cr
│ ├── joiner_spec.cr
│ └── cycler_spec.cr
├── all_specs.cr
├── server_spec.cr
├── tags
│ ├── autoescape_spec.cr
│ ├── filter_spec.cr
│ ├── raw_spec.cr
│ ├── with_spec.cr
│ ├── do_spec.cr
│ ├── set_spec.cr
│ ├── import_spec.cr
│ ├── if_spec.cr
│ └── from_spec.cr
├── server
│ └── server_spec.cr
├── visitor
│ └── source_spec.cr
├── expression
│ ├── expression_spec.cr
│ ├── dict_spec.cr
│ ├── lexer_spec.cr
│ ├── comparator_spec.cr
│ └── identifiers_spec.cr
├── template_spec.cr
├── parser
│ ├── base_spec.cr
│ ├── error_spec.cr
│ ├── expression_parser_spec.cr
│ └── location_spec.cr
├── lib
│ └── feature_library_spec.cr
├── loader_spec.cr
├── util
│ ├── bindings_spec.cr
│ ├── string_trimmer_spec.cr
│ ├── pyobject_spec.cr
│ └── template_cache_spec.cr
├── crinja_spec.cr
├── config_spec.cr
├── runtime
│ └── value_spec.cr
├── interpreter
│ ├── error_location_spec.cr
│ └── value_spec.cr
└── spec_helper.cr
├── examples
├── kilt
│ ├── .gitignore
│ ├── pages
│ │ └── test.j2
│ ├── shard.yml
│ └── kilt.cr
├── kemal
│ ├── .gitignore
│ ├── pages
│ │ ├── index.html
│ │ └── layout.html
│ ├── shard.yml
│ ├── public
│ │ └── source.css
│ └── kemal.cr
├── rwbench
│ ├── shard.yml
│ ├── crinja
│ │ ├── helpers.html
│ │ ├── layout.html
│ │ └── index.html
│ └── rwbench.cr
├── config
│ ├── shard.yml
│ └── config.cr
├── server
│ ├── shard.yml
│ ├── pages
│ │ ├── index.html
│ │ ├── helpers.html
│ │ ├── layout.html
│ │ ├── rwbench.html
│ │ └── play.html
│ ├── public
│ │ ├── play.css
│ │ ├── source.css
│ │ └── vendor
│ │ │ └── ace-1.2.7
│ │ │ └── theme-tomorrow.js
│ └── server.cr
├── Makefile
└── integration_test.bats
├── src
├── lib
│ ├── function
│ │ ├── dict.cr
│ │ ├── joiner.cr
│ │ ├── super.cr
│ │ ├── debug.cr
│ │ ├── cycler.cr
│ │ └── range.cr
│ ├── operator
│ │ ├── tilde.cr
│ │ ├── divide.cr
│ │ ├── modulo.cr
│ │ ├── int_divide.cr
│ │ ├── multiply.cr
│ │ ├── power.cr
│ │ ├── minus.cr
│ │ ├── logic.cr
│ │ ├── plus.cr
│ │ └── comparator.cr
│ ├── filter
│ │ ├── escape.cr
│ │ ├── liquid.cr
│ │ ├── join.cr
│ │ ├── var.cr
│ │ ├── sort.cr
│ │ ├── html.cr
│ │ └── number.cr
│ ├── tag
│ │ ├── raw.cr
│ │ ├── end_tag.cr
│ │ ├── do.cr
│ │ ├── extends.cr
│ │ ├── block.cr
│ │ ├── autoescape.cr
│ │ ├── import.cr
│ │ ├── with.cr
│ │ ├── filter.cr
│ │ ├── set.cr
│ │ ├── if.cr
│ │ ├── include.cr
│ │ ├── from.cr
│ │ └── call.cr
│ ├── function.cr
│ ├── operator.cr
│ ├── test.cr
│ └── test
│ │ └── tests.cr
├── docs.cr
├── visitor
│ ├── visitor.cr
│ ├── source.cr
│ └── html.cr
├── yaml.cr
├── server
│ ├── template_handler.cr
│ ├── render_handler.cr
│ ├── source_handler.cr
│ └── play_handler.cr
├── json.cr
├── runtime
│ ├── undefined.cr
│ ├── tuple.cr
│ ├── output.cr
│ └── finalizer.cr
├── util
│ ├── template_cache.cr
│ ├── scope_map.cr
│ ├── string_trimmer.cr
│ └── json_builder.cr
├── liquid.cr
├── parser
│ ├── token_stream.cr
│ ├── symbol.cr
│ ├── token.cr
│ ├── character_stream.cr
│ └── parser_helper.cr
├── server.cr
├── loader
│ └── baked_file_loader.cr
├── crinja.cr
└── arguments.cr
├── .ameba.yml
├── scripts
├── feature-comparison.sh
├── jinja
│ └── default_lib.py
├── coverage
└── generate-docs.sh
├── .editorconfig
├── renovate.json
├── .gitignore
├── shard.yml
├── LICENSE
├── .travis.yml
├── playground
├── features.md
└── objects.md
├── .overcommit.yml
├── Makefile
└── .github
└── workflows
└── ci.yml
/spec/fixtures/welcome.html:
--------------------------------------------------------------------------------
1 | Hello {{ name }}
2 |
--------------------------------------------------------------------------------
/spec/fixtures/import.html:
--------------------------------------------------------------------------------
1 | {% include "welcome.html" %}!
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/basic.html:
--------------------------------------------------------------------------------
1 |
Hello
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/global_fn.html:
--------------------------------------------------------------------------------
1 | vincent.is
--------------------------------------------------------------------------------
/examples/kilt/.gitignore:
--------------------------------------------------------------------------------
1 | /doc/
2 | /lib/
3 | /bin/
4 | /.shards/
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/empty_loop.html:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/examples/kemal/.gitignore:
--------------------------------------------------------------------------------
1 | /doc/
2 | /lib/
3 | /bin/
4 | /.shards/
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/comment.html:
--------------------------------------------------------------------------------
1 |
2 | Hello
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/basic.html:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/indexing.html:
--------------------------------------------------------------------------------
1 | My review
2 | 1
3 | 22
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/invalid_number.html:
--------------------------------------------------------------------------------
1 | {{1.2.2
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/unterminated.html:
--------------------------------------------------------------------------------
1 | {{ hello
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/invalid_operator.html:
--------------------------------------------------------------------------------
1 | {{ hello =!
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/value_render.html:
--------------------------------------------------------------------------------
1 | {{ manufacturer }}
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/unexpected_terminator.html:
--------------------------------------------------------------------------------
1 | {{ 1 + }}
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/field_unknown.html:
--------------------------------------------------------------------------------
1 | Hello {{ hey }}
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/global_fn.html:
--------------------------------------------------------------------------------
1 | {{ url_for(name="home") }}
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/macro_included.html:
--------------------------------------------------------------------------------
1 | {{ greeting }} humans
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/non_math_operation.html:
--------------------------------------------------------------------------------
1 | {{ username + 1 }}
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/value_render_non_object.html:
--------------------------------------------------------------------------------
1 | {{ hello }}
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/comment.html:
--------------------------------------------------------------------------------
1 | {# Hello comment #}
2 | Hello
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/raw.html:
--------------------------------------------------------------------------------
1 | Hello
2 |
3 | Hey there {{ name }}
4 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/inexisting_include.html:
--------------------------------------------------------------------------------
1 | {% include "hola" %}
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/magical_variable.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/function/dict.cr:
--------------------------------------------------------------------------------
1 | Crinja.function(:dict) do
2 | Crinja.dictionary arguments.kwargs
3 | end
4 |
--------------------------------------------------------------------------------
/.ameba.yml:
--------------------------------------------------------------------------------
1 | Lint/UnusedArgument:
2 | IgnoreProcs: true
3 |
4 | Style/WhileTrue:
5 | Enabled: false
6 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/loop_with_filters.html:
--------------------------------------------------------------------------------
1 |
2 | My review
3 |
4 | My review
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/missing_not_expression.html:
--------------------------------------------------------------------------------
1 | {% if not %}
2 | {% endif %}
3 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/comment_alignment.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Hello
4 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/invalid_extends.html:
--------------------------------------------------------------------------------
1 | Hello
2 | {% extends "something.html" %}
3 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/filter_section_invalid.html:
--------------------------------------------------------------------------------
1 | {% filter round %}hello{% endfilter %}
2 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/missing_endblock_name.html:
--------------------------------------------------------------------------------
1 | {% block hello %}
2 | Hello
3 | {% endblock %}
4 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/comment_alignment.html:
--------------------------------------------------------------------------------
1 |
2 | {# comment #}
3 |
Hello
4 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/invalid_else.html:
--------------------------------------------------------------------------------
1 | {% if true %}
2 | {% else %}
3 | {% else %}
4 | {% endif %}
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/wrong_endblock.html:
--------------------------------------------------------------------------------
1 | {% block hello %}
2 | Hello
3 | {% endblock goodbye %}
4 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/iterate_on_non_array.html:
--------------------------------------------------------------------------------
1 | {% for i in username %}
2 | {{i}}
3 | {% endfor %}
4 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/raw.html:
--------------------------------------------------------------------------------
1 | Hello
2 | {% raw %}
3 | Hey there {{ name }}
4 | {% endraw %}
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/invalid_elif.html:
--------------------------------------------------------------------------------
1 | {% if true %}
2 | {% else %}
3 | {% elif false %}
4 | {% endif %}
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/empty_loop.html:
--------------------------------------------------------------------------------
1 | Hello
2 | {% for i in empty %}
3 | {{loop.index}}{{i}}
4 | {% endfor %}
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/field_unknown_forloop.html:
--------------------------------------------------------------------------------
1 | Hello
2 | {% for r in reviews %}
3 | {{ r.random }}
4 | {% endfor %}
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/macro_self_inexisting.html:
--------------------------------------------------------------------------------
1 | {% import "macros.html" as macros %}
2 | {{ macros::hello_world2() }}
3 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/loop_with_filters.html:
--------------------------------------------------------------------------------
1 | {% for item in reviews | reverse %}
2 | {{ item.title }}
3 | {% endfor %}
4 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/error-location/macros.html:
--------------------------------------------------------------------------------
1 | {% macro cause_error() %}
2 | {{ hey }}
3 | {% endmacro cause_error %}
4 |
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/macro_wrong_args.html:
--------------------------------------------------------------------------------
1 | {% import "macros.html" as macros %}
2 | {{ macros::input(greeting="Hello World") }}
3 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/indexing.html:
--------------------------------------------------------------------------------
1 | {{ reviews.1.title }}
2 | {{ a_tuple.0 }}
3 | {% for u in an_array_of_tuple %}{{ u.1 }}{% endfor %}
4 |
--------------------------------------------------------------------------------
/examples/kilt/pages/test.j2:
--------------------------------------------------------------------------------
1 | Greetings from {{ from }} through {{ messenger }}!
2 |
3 | {{ debug() }}
4 |
5 | This was rendered by Crinja {{ crinja.version }}
6 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/error-location/base.html:
--------------------------------------------------------------------------------
1 | Hello
2 |
3 | {% block content %}
4 | Some base content
5 | {% endblock content %}
6 |
--------------------------------------------------------------------------------
/scripts/feature-comparison.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | diff -B <( ./scripts/jinja/default_lib.py ) <( crystal ./src/cli.cr -- --library-defaults --only-names )
4 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/error-location/error_in_macro.html:
--------------------------------------------------------------------------------
1 | {% import "error-location/macros.html" as macros %}
2 |
3 | {{ macros::cause_error() }}
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cr]
2 | charset = utf-8
3 | end_of_line = lf
4 | insert_final_newline = true
5 | indent_style = space
6 | indent_size = 2
7 | trim_trailing_whitespace = true
8 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/magical_variable.html:
--------------------------------------------------------------------------------
1 | {% import "macros.html" as macros %}
2 |
3 | {{ __tera_context }}
4 |
5 |
6 | {{ macros.dump(name="bob") }}
7 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",
5 | "helpers:pinGitHubActionDigests"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/invalid_content_macro.html:
--------------------------------------------------------------------------------
1 | {% macro input(label, type) %}
2 | {% macro nested() %}
3 | {% endmacro nested %}
4 | {% endmacro input %}
5 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/error-location/error_in_parent.html:
--------------------------------------------------------------------------------
1 | {% extends "error-location/base_error.html" %}
2 |
3 | {% block content %}
4 | Hello
5 | {% endblock content %}
6 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/error-location/error_in_child.html:
--------------------------------------------------------------------------------
1 | {% extends "error-location/base.html" %}
2 |
3 | {% block content %}
4 | {{ unknown }}
5 | {% endblock content %}
6 |
--------------------------------------------------------------------------------
/src/docs.cr:
--------------------------------------------------------------------------------
1 | # This file requires all optional files besides the main entry point in order to
2 | # have all included in the API docs.
3 |
4 | require "./cli"
5 | require "./loader/baked_file_loader"
6 |
--------------------------------------------------------------------------------
/examples/rwbench/shard.yml:
--------------------------------------------------------------------------------
1 | name: crinja-rwbench-example
2 | version: 0.0.1
3 |
4 | targets:
5 | rwbench:
6 | main: rwbench.cr
7 |
8 | dependencies:
9 | crinja:
10 | path: ../../
11 |
--------------------------------------------------------------------------------
/examples/config/shard.yml:
--------------------------------------------------------------------------------
1 | name: crinja-config-example
2 | version: 0.0.1
3 |
4 | targets:
5 | config-example:
6 | main: config.cr
7 |
8 | dependencies:
9 | crinja:
10 | path: ../../
11 |
--------------------------------------------------------------------------------
/examples/server/shard.yml:
--------------------------------------------------------------------------------
1 | name: crinja-server-example
2 | version: 0.0.1
3 |
4 | targets:
5 | server-example:
6 | main: server.cr
7 |
8 | dependencies:
9 | crinja:
10 | path: ../../
11 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/filters.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | MOTO G
4 |
5 |
6 | Moto G - M ...
7 | 2 reviewes
8 |
9 |
--------------------------------------------------------------------------------
/spec/functions/dict_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper.cr"
2 |
3 | describe "function dict" do
4 | it "creates dict" do
5 | evaluate_expression(%(dict(foo="bar"))).should eq(%({'foo' => 'bar'}))
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/error-location/error_in_grand_child.html:
--------------------------------------------------------------------------------
1 | {% extends "error-location/error_in_child.html" %}
2 |
3 | {% block content %}
4 | {{ unknown2 }}
5 | {% endblock content %}
6 |
--------------------------------------------------------------------------------
/spec/all_specs.cr:
--------------------------------------------------------------------------------
1 | require "./crinja_spec.cr"
2 | require "./expression/*"
3 | require "./functions/*"
4 | require "./integration/*"
5 | require "./lib/*"
6 | require "./parser/*"
7 | require "./tags/*"
8 | require "./util/*"
9 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/parser-failures/duplicate_block.html:
--------------------------------------------------------------------------------
1 | {% extends "base" %}
2 |
3 | {% block hello %}
4 | Hello
5 | {% endblock hello %}
6 |
7 | {% block hello %}
8 | Hello
9 | {% endblock hello %}
10 |
--------------------------------------------------------------------------------
/examples/kemal/pages/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Crinja {{ crinja.version }}
5 |
6 |
7 | Crinja {{ crinja.version }}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/spec/functions/range_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe "function range" do
4 | it "negative range" do
5 | render(%({% for i in range(10, 5, -1) %}{{ i }} {% endfor %})).should eq "10 9 8 7 6 "
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/functions/joiner_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper.cr"
2 |
3 | describe "function joiner" do
4 | it "joins" do
5 | render(%({% set pipe = joiner('|') %}{{ pipe() }}-{{ pipe() }}-{{ pipe() }})).should eq "-|-|"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/include.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Products
4 |
5 |
6 |
7 | {% include "included.html" %}
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/lib/operator/tilde.cr:
--------------------------------------------------------------------------------
1 | class Crinja::Operator
2 | class Tilde < Operator
3 | include Binary
4 | name "~"
5 |
6 | def value(env : Crinja, op1, op2)
7 | op1.to_s + op2.to_s
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/error-location/base_error.html:
--------------------------------------------------------------------------------
1 | Hello
2 |
3 | {% block title %}
4 | {{ tite }}
5 | {% endblock title %}
6 |
7 | {% block content %}
8 | Some base content
9 | {% endblock content %}
10 |
--------------------------------------------------------------------------------
/examples/server/pages/index.html:
--------------------------------------------------------------------------------
1 | Crinja Examples Server
2 |
3 | Crinja Playground
4 |
5 |
6 | {% for name in crinja.server.templates %}
7 | - {{ name }}
8 | {% endfor %}
9 |
10 |
--------------------------------------------------------------------------------
/spec/fixtures/hello_world.html.rendered:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Value with <unsafe> data
5 |
6 |
7 | 1,
8 | 2,
9 | 3,
10 | 4,
11 | 5,
12 | 6
13 |
14 |
15 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/loops_set_dot_access.html:
--------------------------------------------------------------------------------
1 | {% for review in reviews %}
2 | {{review.title}}
3 | {% set rev = review %}
4 | {% for paragraph in rev.paragraphs %}
5 | {{ paragraph }}
6 | {% endfor %}
7 | {% endfor %}
8 |
--------------------------------------------------------------------------------
/examples/kemal/shard.yml:
--------------------------------------------------------------------------------
1 | name: crinja-kemal-example
2 | version: 0.0.1
3 |
4 | targets:
5 | kemal-example:
6 | main: kemal.cr
7 |
8 | dependencies:
9 | kemal:
10 | github: kemalcr/kemal
11 | branch: master
12 | crinja:
13 | path: ../../
14 |
--------------------------------------------------------------------------------
/spec/fixtures/hello_world.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ variable }}
5 |
6 |
7 | {%- for item in item_list -%}
8 | {{ item }}{% if not loop.last %},{% endif %}
9 | {%- endfor -%}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/included.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | | User |
4 | Number of reviews |
5 |
6 |
7 | | {{ username }} |
8 | {{ number_reviews }} |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/examples/Makefile:
--------------------------------------------------------------------------------
1 | SHARDS ::= shards
2 | BATS ::= bats
3 |
4 | .PHONY: all
5 | all:
6 | $(BATS) integration_test.bats
7 |
8 | .PHONY: update
9 | update:
10 | for i in $(find . -maxdepth 1 -mindepth 1 -type d); do
11 | pushd $i
12 | $(SHARDS) update
13 | popd
14 | done
15 |
--------------------------------------------------------------------------------
/spec/server_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 | require "../src/server"
3 |
4 | describe Crinja::Server do
5 | it do
6 | env = Crinja.new
7 | server = Crinja::Server.new(env)
8 | server.template_dir = File.join(__DIR__, "fixtures")
9 | server.setup
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/lib/function/joiner.cr:
--------------------------------------------------------------------------------
1 | Crinja.function({sep: ", "}, :joiner) do
2 | called = false
3 | sep = arguments["sep"]
4 | ->(_args : Crinja::Arguments) do
5 | if called
6 | sep
7 | else
8 | called = true
9 | Crinja::Value.new ""
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/loops_set_dot_access.html:
--------------------------------------------------------------------------------
1 |
2 | My review
3 |
4 |
5 | A
6 |
7 | B
8 |
9 | C
10 |
11 |
12 | My review
13 |
14 |
15 | A
16 |
17 | B
18 |
19 | C
20 |
21 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/render-failures/macros.html:
--------------------------------------------------------------------------------
1 | {% macro input(label, type) %}
2 |
6 | {% endmacro input %}
7 |
8 |
9 | {% macro hello_world2() %}
10 | {{ self::inexisting() }}
11 | {% endmacro hello_world2 %}
12 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% block title %}{{ product.name }}{% endblock title %}
4 | {% block extrahead %}{% endblock extrahead %}
5 |
6 |
7 | {% block content %}{% endblock content %}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/kilt/shard.yml:
--------------------------------------------------------------------------------
1 | name: crinja-kilt-example
2 | version: 0.0.1
3 |
4 | targets:
5 | kilt-example:
6 | main: kilt.cr
7 |
8 | dependencies:
9 | kilt:
10 | github: jeromegn/kilt
11 | baked_file_system:
12 | github: schovi/baked_file_system
13 | branch: master
14 | crinja:
15 | path: ../../
16 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/use_macros.html:
--------------------------------------------------------------------------------
1 | {% import "macros.html" as macros %}
2 | {{ macros.input(label="Name", type="text") }}
3 | {{ macros.input(label="Age", type="number") }}
4 | {{ macros.input(label=username, type="text") }}
5 |
6 | {{ macros.factorial(n=7) }}
7 |
8 | {{ macros.include(greeting="Bonjour") }}
9 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/variables.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Moto G
4 |
5 |
6 | Moto G - Motorala
7 | A phone
8 | £120 (VAT inc.)
9 | Look at reviews from your friends bob
10 |
11 |
12 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/basic_inheritance.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Moto G
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
Moto G - Motorala
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/visitor/visitor.cr:
--------------------------------------------------------------------------------
1 | abstract class Crinja::Visitor(T)
2 | macro visit(*node_types)
3 | def visit(node : {{
4 | (node_types.map do |type|
5 | "AST::#{type.id}"
6 | end).join(" | ").id
7 | }})
8 | {{ yield }}
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /docs/
2 | /tmp/
3 | /lib/
4 | /build/
5 | /bin/
6 | .shards/
7 | /examples/*/lib/
8 | /examples/*/docs/
9 | /examples/*/bin/
10 | /examples/*/shard.lock
11 |
12 | *.rendered.html
13 | /.cache
14 | __pycache__
15 |
16 | # Libraries don't need dependency lock
17 | # Dependencies will be locked in application that uses them
18 | /shard.lock
19 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/expected/variable_tests.html:
--------------------------------------------------------------------------------
1 |
2 | Hello!
3 |
4 |
5 |
6 | Hello again!
7 |
8 |
9 |
10 | Blabla
11 |
12 |
13 |
14 | String
15 |
16 |
17 |
18 | Odd
19 |
20 |
21 |
22 | Even
23 |
24 |
25 |
26 | Number
27 |
28 |
29 |
30 | Divisible
31 |
32 |
33 |
34 | Iterable
35 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/filters.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ product.name | upper | trim }}
4 |
5 |
6 | {{ product.name | trim }} - {{ product.manufacturer | trim | truncate(length=4) }}
7 | {{ number_reviews | round(method="floor") | int }} reviewes
8 |
9 |
10 |
--------------------------------------------------------------------------------
/spec/integration/if_test_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe "if_test" do
4 | it "renders template" do
5 | render("Hello {{name}}!
6 |
7 | {% if test -%}
8 | How are you?
9 | {%- endif %}", {
10 | "name" => "John",
11 | "test" => true,
12 | }).should eq("Hello John!
13 |
14 | How are you?
15 | ")
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/integration/compat-suite/templates/basic_inheritance.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block extrahead %}
4 |
5 |
8 | {% endblock extrahead %}
9 |
10 | {% block content %}
11 | {{ product.name }} - {{ product.manufacturer }}
12 | {% endblock content %}
13 |
--------------------------------------------------------------------------------
/spec/tags/autoescape_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper.cr"
2 |
3 | describe "autoescape" do
4 | it "autescape true" do
5 | render(%({% autoescape true %}{{ "
39 | {% endfor %}
40 |
41 |
68 |
--------------------------------------------------------------------------------
/spec/parser/expression_parser_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper.cr"
2 |
3 | describe Crinja::Parser::ExpressionParser do
4 | it "parses string literals" do
5 | expression = parse_expression(%( "foo"))
6 | expression.should be_a(Crinja::AST::StringLiteral)
7 | end
8 |
9 | it "parses binary expressions" do
10 | expression = parse_expression(%(1 + 2))
11 | expression.should be_a(Crinja::AST::BinaryExpression)
12 | Crinja.new.evaluate(expression).should eq 3
13 | end
14 |
15 | it "parses member operator" do
16 | expression = parse_expression(%(foo.bar))
17 | expression.should be_a(Crinja::AST::MemberExpression)
18 | end
19 |
20 | it "parses single parenthesis tuple" do
21 | expression = parse_expression(%(("foo", 1)))
22 | expression.should be_a(Crinja::AST::TupleLiteral)
23 | end
24 |
25 | it "parses integer as identifier member" do
26 | expression = parse_expression(%(foo.1))
27 | expression.should be_a(Crinja::AST::MemberExpression)
28 | end
29 |
30 | it "parse double parenthesis" do
31 | expression = parse_expression("dict(foo=(1, 2))")
32 | expression.should be_a(Crinja::AST::CallExpression)
33 | end
34 |
35 | it "parses expression as named argument value" do
36 | expression = parse_expression("self(n=n-1)")
37 | expression.should be_a(Crinja::AST::CallExpression)
38 | end
39 |
40 | it "parses integer as member access" do
41 | expression = parse_expression("foo.1.bar")
42 | expression.should be_a(Crinja::AST::MemberExpression)
43 | end
44 |
45 | it "parses escaped backslashes" do
46 | expression = parse_expression(%q("foo\\bar"))
47 | expression.should be_a(Crinja::AST::StringLiteral)
48 | expression.as(Crinja::AST::StringLiteral).value.should eq %q(foo\bar)
49 | end
50 |
51 | it "parses escaped newlines" do
52 | expression = parse_expression(%q("foo\nbar"))
53 | expression.should be_a(Crinja::AST::StringLiteral)
54 | expression.as(Crinja::AST::StringLiteral).value.should eq "foo\nbar"
55 | end
56 |
57 | it "parses escaped quotes" do
58 | expression = parse_expression(%q("\"foo\"\'bar\'"))
59 | expression.should be_a(Crinja::AST::StringLiteral)
60 | expression.as(Crinja::AST::StringLiteral).value.should eq %q("foo"'bar')
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/src/server/play_handler.cr:
--------------------------------------------------------------------------------
1 | require "yaml"
2 |
3 | class Crinja::Server::PlayHandler
4 | include HTTP::Handler
5 |
6 | DEFAULT_TEMPLATE = <<-'END'
7 |
8 | {%- for user in users | sort(attribute="name") -%}
9 | - {{ user.name }}
10 | {%- endfor -%}
11 |
12 | END
13 |
14 | DEFAULT_VARIABLES = <<-'END'
15 | users:
16 | - id: 1
17 | name: "john"
18 | - id: 2
19 | name: "james"
20 | - id: 3
21 | name: "mike"
22 | - id: 4
23 | name: "mat"
24 | END
25 |
26 | def initialize(@env : Crinja)
27 | end
28 |
29 | def call(context)
30 | return call_next(context) unless context.request.path == "/play"
31 |
32 | template_source = nil
33 | variables = nil
34 |
35 | if context.request.method == "POST"
36 | if body = context.request.body
37 | params = HTTP::Params.parse(body.gets_to_end)
38 |
39 | template_source = params["template_source"]?
40 | variables = params["variables"]?
41 | else
42 | context.response.status_code = 400
43 | end
44 |
45 | unless template_source && variables
46 | context.response.status_code = 400
47 | return
48 | end
49 | end
50 |
51 | template_source ||= DEFAULT_TEMPLATE
52 | variables ||= DEFAULT_VARIABLES
53 | bindings = YAML.parse(variables)
54 |
55 | rendered_result = ""
56 | begin
57 | template = @env.from_string template_source
58 |
59 | begin
60 | rendered_result = template.render(bindings.as_h)
61 | rescue exc : Crinja::Error
62 | exc.template = template
63 | rendered_result = exc.to_s
64 | @env.logger.error(exception: exc) { "Play handler render failed" }
65 | end
66 | rescue exc : Crinja::TemplateError
67 | rendered_result = exc.to_s
68 | @env.logger.error(exception: exc) { "Play handler template loading failed" }
69 | end
70 |
71 | context.response.content_type = "text/html"
72 | @env.get_template("play.html").render(context.response, {
73 | template_source: template_source,
74 | variables: variables,
75 | rendered_result: SafeString.new(rendered_result),
76 | })
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/spec/parser/location_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe Crinja::Parser::TemplateParser do
4 | it "fixed string" do
5 | parser = Crinja::Parser::TemplateParser.new(Crinja.new, "Hallo Welt!")
6 | tree = parser.parse
7 |
8 | fixed_string = tree.children.first
9 | fixed_string.location_start.should eq({1, 1, 0})
10 | fixed_string.location_end.should eq({1, 12, 11})
11 | end
12 |
13 | it "fixed string and expression" do
14 | parser = Crinja::Parser::TemplateParser.new(Crinja.new, "Hallo Welt {{ name }}!")
15 | tree = parser.parse
16 |
17 | hello = tree.children[0]
18 | hello.location_start.should eq({1, 1, 0})
19 | hello.location_end.should eq({1, 12, 11})
20 |
21 | print = tree.children[1].as(Crinja::AST::PrintStatement)
22 | print.location_start.should eq({1, 12, 11})
23 | print.location_end.should eq({1, 22, 21})
24 |
25 | literal = print.expression
26 | literal.location_start.should eq({1, 15, 14})
27 | literal.location_end.should eq({1, 19, 18})
28 |
29 | bang = tree.children[2]
30 | bang.location_start.should eq({1, 22, 21})
31 | bang.location_end.should eq({1, 23, 22})
32 | end
33 | end
34 |
35 | describe Crinja::Parser::ExpressionParser do
36 | it "parse double parenthesis" do
37 | expression = parse_expression("dict(foo=(1, 2))").as(Crinja::AST::CallExpression)
38 |
39 | expression.location_start.should eq({1, 1, 0})
40 | expression.location_end.should eq({1, 17, 16})
41 |
42 | identifier = expression.identifier
43 | identifier.location_start.should eq({1, 1, 0})
44 | identifier.location_end.should eq({1, 5, 4})
45 |
46 | keywords = expression.keyword_arguments
47 |
48 | key_id = keywords.first_key
49 | key_id.location_start.should eq({1, 6, 5})
50 | key_id.location_end.should eq({1, 9, 8})
51 |
52 | value = keywords.first_value.as(Crinja::AST::TupleLiteral)
53 | value.location_start.should eq({1, 10, 9})
54 | value.location_end.should eq({1, 16, 15})
55 |
56 | one = value.children[0]
57 | one.location_start.should eq({1, 11, 10})
58 | one.location_end.should eq({1, 12, 11})
59 |
60 | one = value.children[1]
61 | one.location_start.should eq({1, 14, 13})
62 | one.location_end.should eq({1, 15, 14})
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/src/lib/tag/include.cr:
--------------------------------------------------------------------------------
1 | # The include statement is useful to include a template and return the rendered contents of that
2 | # file into the current namespace:
3 | #
4 | # ```
5 | # {% include 'header.html' %}
6 | # Body
7 | # {% include 'footer.html' %}
8 | # ```
9 | #
10 | # See [Jinja2 Template Documentation](http://jinja.pocoo.org/docs/2.9/templates/#include) for details.
11 | class Crinja::Tag::Include < Crinja::Tag
12 | name "include"
13 |
14 | private def interpret(io : IO, renderer : Crinja::Renderer, tag_node : TagNode)
15 | env = renderer.env
16 | parser = Parser.new(tag_node.arguments, renderer.env.config)
17 | source_expression, ignore_missing, with_context = parser.parse_include_tag
18 |
19 | begin
20 | source = env.evaluate(source_expression)
21 | rescue error : UndefinedError
22 | if renderer.env.config.liquid_compatibility_mode
23 | # enables use of `{% include file.name %}` => `source = "file.name"`
24 | source = Value.new(String.build do |str|
25 | Visitor::Source.new(str).visit(tag_node.arguments)
26 | end.strip)
27 | else
28 | raise error
29 | end
30 | end
31 |
32 | # FIXME
33 | source = source.not_nil!
34 |
35 | if source.iterable?
36 | include_name = source.map &.to_s
37 | else
38 | include_name = source.to_s
39 | end
40 |
41 | context = env.global_context unless with_context
42 |
43 | begin
44 | env.logger.debug { "loading include #{include_name}" }
45 | template = env.get_template(include_name)
46 | template.render(io, context)
47 | rescue error : TemplateNotFoundError
48 | raise error unless ignore_missing
49 | end
50 | end
51 |
52 | private class Parser < ArgumentsParser
53 | def parse_include_tag
54 | source_expression = parse_expression
55 |
56 | ignore_missing = false
57 |
58 | if_identifier "ignore" do
59 | next_token
60 | expect_identifier "missing"
61 |
62 | ignore_missing = true
63 | end
64 |
65 | with_context = current_token.value != "without"
66 |
67 | if_identifier ["with", "without"] do
68 | next_token
69 | expect_identifier "context"
70 | end
71 |
72 | close
73 |
74 | {source_expression, ignore_missing, with_context}
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/spec/integration/autoescape_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe "autoescape" do
4 | it "renders template *.html" do
5 | render_file("hello_world.html", {
6 | "variable" => "Value with data",
7 | "item_list" => [1, 2, 3, 4, 5, 6],
8 | }, autoescape: true).should eq(rendered_file("hello_world.html"))
9 | end
10 |
11 | it "renders template *.xml" do
12 | env = Crinja.new
13 | env.config.autoescape.disabled_extensions = ["txt"]
14 | env.config.autoescape.default = false
15 | Crinja::Template.new("{{ variable }}", env, filename: "hello_world.XML").render({
16 | "variable" => "Value with data",
17 | }).should eq("Value with <unsafe> data")
18 | end
19 |
20 | it "renders template *.xml.jinja" do
21 | env = Crinja.new
22 | env.config.autoescape.disabled_extensions = ["txt"]
23 | env.config.autoescape.default = false
24 | Crinja::Template.new("{{ variable }}", env, filename: "hello_world.xml.jinja").render({
25 | "variable" => "Value with data",
26 | }).should eq("Value with <unsafe> data")
27 | end
28 |
29 | it "renders template *.xml.j2" do
30 | env = Crinja.new
31 | env.config.autoescape.disabled_extensions = ["txt"]
32 | Crinja::Template.new("{{ variable }}", env, filename: "hello_world.xml.j2").render({
33 | "variable" => "Value with data",
34 | }).should eq("Value with <unsafe> data")
35 | end
36 |
37 | it "renders template *.txt" do
38 | env = Crinja.new
39 | env.config.autoescape.disabled_extensions = ["txt"]
40 | Crinja::Template.new("{{ variable }}", env, filename: "hello_world.txt").render({
41 | "variable" => "Value with data",
42 | }).should eq("Value with data")
43 | end
44 |
45 | it "renders template *.txt.jinja" do
46 | env = Crinja.new
47 | env.config.autoescape.disabled_extensions = ["txt"]
48 | Crinja::Template.new("{{ variable }}", env, filename: "hello_world.txt.jinja").render({
49 | "variable" => "Value with data",
50 | }).should eq("Value with data")
51 | end
52 |
53 | it "renders template *.txt.j2" do
54 | env = Crinja.new
55 | env.config.autoescape.disabled_extensions = ["txt"]
56 | Crinja::Template.new("{{ variable }}", env, filename: "hello_world.TXT.J2").render({
57 | "variable" => "Value with data",
58 | }).should eq("Value with data")
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | branches:
7 | - master
8 | # Branches from forks have the form 'user:branch-name' so we only run
9 | # this job on pull_request events for branches that look like fork
10 | # branches. Without this we would end up running this job twice for non
11 | # forked PRs, once for the push and then once for opening the PR.
12 | - "**:**"
13 | schedule:
14 | - cron: '0 6 * * 1' # Every monday 6 AM
15 |
16 | env:
17 | SHARDS_OPTS: --ignore-crystal-version
18 |
19 | jobs:
20 | test:
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | include:
25 | - os: ubuntu-latest
26 | crystal: latest
27 | - os: ubuntu-latest
28 | crystal: 1.0.0
29 | - os: ubuntu-latest
30 | crystal: nightly
31 | - os: macos-latest
32 | runs-on: ${{ matrix.os }}
33 |
34 | steps:
35 | - name: Download source
36 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4
37 | - name: Install Crystal
38 | uses: crystal-lang/install-crystal@v1
39 | with:
40 | crystal: ${{ matrix.crystal }}
41 | - name: Install shards
42 | run: shards update
43 | - name: Run specs
44 | run: crystal spec --error-trace
45 |
46 | integration_test:
47 | strategy:
48 | fail-fast: false
49 | matrix:
50 | os: [ubuntu-latest]
51 | crystal: [1.0.0, latest, nightly]
52 | runs-on: ${{ matrix.os }}
53 |
54 | steps:
55 | - name: Download source
56 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4
57 | - name: Install Crystal
58 | uses: crystal-lang/install-crystal@v1
59 | with:
60 | crystal: ${{ matrix.crystal }}
61 | - name: Setup BATS
62 | uses: mig4/setup-bats@af9a00deb21b5d795cabfeaa8d9060410377686d # tag=v1
63 | with:
64 | bats-version: 1.2.1
65 | - name: Run integration specs
66 | run: make test/integration
67 |
68 | format:
69 | runs-on: ubuntu-latest
70 |
71 | steps:
72 | - name: Download source
73 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4
74 | - name: Install Crystal
75 | uses: crystal-lang/install-crystal@v1
76 | with:
77 | crystal: latest
78 | - name: Check formatting
79 | run: crystal tool format; git diff --exit-code
80 |
--------------------------------------------------------------------------------
/src/lib/tag/from.cr:
--------------------------------------------------------------------------------
1 | # Crinja supports putting often used code into macros. These macros can go into different templates
2 | # and get imported from there. It’s important to know that imports can be cached and imported templates
3 | # don’t have access to the current template variables, just the globals by default.
4 | #
5 | # See [Jinja2 Template Documentation](http://jinja.pocoo.org/docs/2.9/templates/#import) for details.
6 | class Crinja::Tag::From < Crinja::Tag
7 | name "from"
8 |
9 | private def interpret(io : IO, renderer : Renderer, tag_node : TagNode)
10 | env = renderer.env
11 | parser = Parser.new(tag_node.arguments, renderer.env.config)
12 | source_expression, with_context, imports = parser.parse_from_tag
13 |
14 | template_name = env.evaluate(source_expression).to_s
15 |
16 | child = if with_context
17 | Crinja.new(env)
18 | else
19 | Crinja.new(config: env.config, loader: env.loader)
20 | end
21 |
22 | template = child.get_template(template_name)
23 |
24 | template.render(child)
25 |
26 | env.errors += child.errors
27 |
28 | imports.each do |from_name, import_name|
29 | if template.macros.has_key?(from_name)
30 | env.context.macros[import_name] = template.macros[from_name]
31 | elsif child.context.has_key?(from_name)
32 | env.context[import_name] = child.context[from_name]
33 | else
34 | raise RuntimeError.new("Unknown import `#{from_name}` in #{template}").at(tag_node)
35 | end
36 | end
37 | end
38 |
39 | private class Parser < ArgumentsParser
40 | def parse_from_tag
41 | source_expression = parse_expression
42 |
43 | expect_identifier "import"
44 |
45 | imports = Hash(String, String).new
46 |
47 | while true
48 | from_name = expect_identifier
49 |
50 | import_name = from_name
51 |
52 | if_identifier "as" do
53 | next_token
54 | import_name = expect_identifier
55 | end
56 |
57 | imports[from_name] = import_name
58 |
59 | break unless current_token.kind == Kind::COMMA
60 | next_token
61 | end
62 |
63 | with_context = false
64 | if_identifier ["with", "without"] do
65 | with_context = current_token.value != "without"
66 | next_token
67 | expect_identifier "context"
68 | end
69 |
70 | close
71 |
72 | {source_expression, with_context, imports}
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/src/runtime/finalizer.cr:
--------------------------------------------------------------------------------
1 | require "html"
2 |
3 | # This class is used to process the result of a variable expression before it is output.
4 | # It tries to convert values to a meaningful string represenation similar to what `Object#to_s` does
5 | # but with a few adjustments compared to Crystal standard `to_s` methods.
6 | struct Crinja::Finalizer
7 | def self.stringify(raw, escape = false, in_struct = false)
8 | String.build do |io|
9 | stringify(io, raw, escape, in_struct)
10 | end
11 | end
12 |
13 | def self.stringify(io : IO, raw, escape = false, in_struct = false)
14 | new(io, escape, in_struct).stringify(raw)
15 | end
16 |
17 | # :nodoc:
18 | protected def initialize(@io : IO, @escape = false, @inside_struct = false)
19 | end
20 |
21 | # Convert a `Value` to string.
22 | protected def stringify(value : Value)
23 | stringify(value.raw)
24 | end
25 |
26 | # Convert any type to string.
27 | protected def stringify(raw)
28 | raw.to_s(@io)
29 | end
30 |
31 | # Convert a `nil` to `"none"`.
32 | protected def stringify(raw : Nil)
33 | @io << "none"
34 | end
35 |
36 | # Convert a `SafeString` to string.
37 | protected def stringify(safe : SafeString)
38 | quote { safe.to_s(@io) }
39 | end
40 |
41 | # Convert a `SafeString` to string.
42 | protected def stringify(string : String)
43 | quote do
44 | if @escape
45 | HTML.escape(string).to_s(@io)
46 | else
47 | string.to_s(@io)
48 | end
49 | end
50 | end
51 |
52 | # Convert an `Array` to string.
53 | protected def stringify(array : Array)
54 | @inside_struct = true
55 | @io << "["
56 | array.join(@io, ", ") { |item| stringify(item) }
57 | @io << "]"
58 | end
59 |
60 | # Convert an `Hash` to string.
61 | protected def stringify(hash : Hash)
62 | @inside_struct = true
63 | @io << "{"
64 | found_one = false
65 | hash.each do |key, value|
66 | @io << ", " if found_one
67 | stringify(key)
68 | @io << " => "
69 | stringify(value)
70 | found_one = true
71 | end
72 | @io << "}"
73 | end
74 |
75 | # Convert an `Crinja::Tuple` to string.
76 | protected def stringify(array : Crinja::Tuple)
77 | @inside_struct = true
78 | @io << "("
79 | array.join(@io, ", ") { |item| stringify(item) }
80 | @io << ")"
81 | end
82 |
83 | private def quote(&)
84 | quotes = @inside_struct
85 | @io << '\'' if quotes
86 | yield
87 | @io << '\'' if quotes
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/examples/server/public/vendor/ace-1.2.7/theme-tomorrow.js:
--------------------------------------------------------------------------------
1 | define("ace/theme/tomorrow",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-tomorrow",t.cssText=".ace-tomorrow .ace_gutter {background: #f6f6f6;color: #4D4D4C}.ace-tomorrow .ace_print-margin {width: 1px;background: #f6f6f6}.ace-tomorrow {background-color: #FFFFFF;color: #4D4D4C}.ace-tomorrow .ace_cursor {color: #AEAFAD}.ace-tomorrow .ace_marker-layer .ace_selection {background: #D6D6D6}.ace-tomorrow.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #FFFFFF;}.ace-tomorrow .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-tomorrow .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #D1D1D1}.ace-tomorrow .ace_marker-layer .ace_active-line {background: #EFEFEF}.ace-tomorrow .ace_gutter-active-line {background-color : #dcdcdc}.ace-tomorrow .ace_marker-layer .ace_selected-word {border: 1px solid #D6D6D6}.ace-tomorrow .ace_invisible {color: #D1D1D1}.ace-tomorrow .ace_keyword,.ace-tomorrow .ace_meta,.ace-tomorrow .ace_storage,.ace-tomorrow .ace_storage.ace_type,.ace-tomorrow .ace_support.ace_type {color: #8959A8}.ace-tomorrow .ace_keyword.ace_operator {color: #3E999F}.ace-tomorrow .ace_constant.ace_character,.ace-tomorrow .ace_constant.ace_language,.ace-tomorrow .ace_constant.ace_numeric,.ace-tomorrow .ace_keyword.ace_other.ace_unit,.ace-tomorrow .ace_support.ace_constant,.ace-tomorrow .ace_variable.ace_parameter {color: #F5871F}.ace-tomorrow .ace_constant.ace_other {color: #666969}.ace-tomorrow .ace_invalid {color: #FFFFFF;background-color: #C82829}.ace-tomorrow .ace_invalid.ace_deprecated {color: #FFFFFF;background-color: #8959A8}.ace-tomorrow .ace_fold {background-color: #4271AE;border-color: #4D4D4C}.ace-tomorrow .ace_entity.ace_name.ace_function,.ace-tomorrow .ace_support.ace_function,.ace-tomorrow .ace_variable {color: #4271AE}.ace-tomorrow .ace_support.ace_class,.ace-tomorrow .ace_support.ace_type {color: #C99E00}.ace-tomorrow .ace_heading,.ace-tomorrow .ace_markup.ace_heading,.ace-tomorrow .ace_string {color: #718C00}.ace-tomorrow .ace_entity.ace_name.ace_tag,.ace-tomorrow .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow .ace_meta.ace_tag,.ace-tomorrow .ace_string.ace_regexp,.ace-tomorrow .ace_variable {color: #C82829}.ace-tomorrow .ace_comment {color: #8E908C}.ace-tomorrow .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
2 |
--------------------------------------------------------------------------------
/src/lib/tag/call.cr:
--------------------------------------------------------------------------------
1 | # In some cases it can be useful to pass a `Macro` to another macro. For this purpose, you can use
2 | # the special call block. The following example shows a macro that takes advantage of the call
3 | # functionality and how it can be used:
4 | #
5 | # ```
6 | # {% macro render_dialog(title, class='dialog') -%}
7 | #
8 | #
{{ title }}
9 | #
10 | # {{ caller() }}
11 | #
12 | #
13 | # {%- endmacro %}
14 |
15 | # {% call render_dialog('Hello World') %}
16 | # This is a simple dialog rendered by using a macro and
17 | # a call block.
18 | # {% endcall %}
19 | # ```
20 | #
21 | # See [Jinja2 Template Documentation](http://jinja.pocoo.org/docs/2.9/templates/#call) for details.
22 | class Crinja::Tag::Call < Crinja::Tag
23 | name "call", "endcall"
24 |
25 | def interpret_output(renderer : Renderer, tag_node : TagNode)
26 | env = renderer.env
27 | parser = Parser.new(tag_node.arguments, renderer.env.config)
28 | defaults, call = parser.parse_call_tag
29 |
30 | instance = Tag::Macro::MacroFunction.new "caller", tag_node.block, renderer, caller: true
31 | defaults.each do |key, value|
32 | instance.defaults[key] = if value.is_a?(AST::ExpressionNode)
33 | env.evaluate(value)
34 | else
35 | Value.new value
36 | end
37 | end
38 |
39 | env.context.register_macro instance
40 |
41 | Renderer::RenderedOutput.new(env.evaluate(call).to_s)
42 | end
43 |
44 | private class Parser < ArgumentsParser
45 | def parse_call_tag
46 | defaults = Hash(String, AST::ExpressionNode | Nil).new
47 |
48 | if_token Kind::LEFT_PAREN do
49 | location = current_token.location
50 | next_token
51 | args = parse_call_expression(AST::IdentifierLiteral.new("call").at(location))
52 |
53 | args.argumentlist.children.each do |arg_expr|
54 | if (arg = arg_expr).is_a?(AST::IdentifierLiteral)
55 | defaults[arg.name] = nil
56 | else
57 | raise TemplateSyntaxError.new(arg_expr, "Invalid statement #{arg_expr} in call def")
58 | end
59 | end
60 |
61 | args.keyword_arguments.each do |arg, value|
62 | defaults[arg.name] = value
63 | end
64 | end
65 |
66 | identifier = parse_identifier
67 | expect Kind::LEFT_PAREN
68 | call = parse_call_expression(identifier)
69 |
70 | close
71 |
72 | {defaults, call}
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/src/crinja.cr:
--------------------------------------------------------------------------------
1 | # This class represents the core component of the Crinja template engine.
2 | #
3 | # It contains the *runtime environment* including configuration, global variables
4 | # as well as loading and rendering templates.
5 | #
6 | # Instances of this class may be modified if they are not shared and if no template
7 | # was loaded so far. Modifications on environments after the first template was
8 | # loaded will lead to surprising effects and undefined behavior.
9 | #
10 | # It also contains macros to easily define custom template features such as filters, tests
11 | # and functions.
12 | class Crinja
13 | VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }}
14 |
15 | # Render Crinja template *template* to a String.
16 | #
17 | # Variables for the template can be assigned as parameter *variables*.
18 | #
19 | # This uses default *loader* and *config* unless these are provided as
20 | # optional parameters.
21 | #
22 | # A new `Crinja` instance will be created for each invocation and it will
23 | # parse the *template*. To parse the same template once and invoke it multiple
24 | # times, it needs to be created directly (using `Crinja#from_string` or
25 | # `Template.new`) and stored in a variable.
26 | def self.render(template, variables = nil, loader = nil, config = nil) : String
27 | String.build do |io|
28 | render io, template, variables, loader, config
29 | end
30 | end
31 |
32 | # Render Crinja template *template* to an `IO` *io*.
33 | #
34 | # Variables for the template can be assigned as parameter *variables*.
35 | #
36 | # This uses default *loader* and *config* unless these are provided as
37 | # optional parameters.
38 | #
39 | # A new `Crinja` instance will be created for each invocation and it will
40 | # parse the *template*. To parse the same template once and invoke it multiple
41 | # times, it needs to be created directly (using `Crinja#from_string` or
42 | # `Template.new`) and stored in a variable.
43 | def self.render(io : IO, template, variables = nil, loader = nil, config = nil)
44 | env = Crinja.new
45 | env.loader = loader unless loader.nil?
46 | env.config = config unless config.nil?
47 |
48 | env.from_string(template.to_s).render(io, variables)
49 | end
50 | end
51 |
52 | require "./util/*"
53 | require "./config"
54 | require "./environment"
55 | require "./error"
56 | require "./loader"
57 | require "./template"
58 | require "./parser/*"
59 | require "./runtime/*"
60 | require "./lib/feature_library"
61 | require "./lib/tag"
62 | require "./lib/function"
63 | require "./lib/filter"
64 | require "./lib/operator"
65 | require "./lib/test"
66 | require "./visitor/*"
67 |
--------------------------------------------------------------------------------
/src/lib/test.cr:
--------------------------------------------------------------------------------
1 | # Beside filters, there are also so-called “tests” available.
2 | # Tests can be used to test a variable against a common expression. To test a variable or expression,
3 | # you add `is` plus the name of the test after the variable. For example, to find out if a variable
4 | # is defined, you can do `name is defined`, which will then return true or false depending on
5 | # whether name is defined in the current template context.
6 | #
7 | # Tests can accept arguments, too. If the test only takes one argument, you can leave out the
8 | # parentheses. For example, the following two expressions do the same thing:
9 | #
10 | # ```crinja
11 | # {% if loop.index is divisibleby 3 %}
12 | # {% if loop.index is divisibleby(3) %}
13 | # ```
14 | #
15 | # ## Builtin Tests
16 | #
17 | # The following tests are available in the default library:
18 | #
19 | # * `**[callable](http://jinja.pocoo.org/docs/2.9/templates/#callable)**()`
20 | # * `**[defined](http://jinja.pocoo.org/docs/2.9/templates/#defined)**()`
21 | # * `**[divisibleby](http://jinja.pocoo.org/docs/2.9/templates/#divisibleby)**(num)`
22 | # * `**[equalto](http://jinja.pocoo.org/docs/2.9/templates/#equalto)**(other)`
23 | # * `**[escaped](http://jinja.pocoo.org/docs/2.9/templates/#escaped)**()`
24 | # * `**[even](http://jinja.pocoo.org/docs/2.9/templates/#even)**()`
25 | # * `**[greaterthan](http://jinja.pocoo.org/docs/2.9/templates/#greaterthan)**(other=0)`
26 | # * `**[in](http://jinja.pocoo.org/docs/2.9/templates/#in)**(seq=[])`
27 | # * `**[iterable](http://jinja.pocoo.org/docs/2.9/templates/#iterable)**()`
28 | # * `**[lessthan](http://jinja.pocoo.org/docs/2.9/templates/#lessthan)**(other=0)`
29 | # * `**[lower](http://jinja.pocoo.org/docs/2.9/templates/#lower)**()`
30 | # * `**[mapping](http://jinja.pocoo.org/docs/2.9/templates/#mapping)**()`
31 | # * `**[nil](http://jinja.pocoo.org/docs/2.9/templates/#nil)**()`
32 | # * `**[none](http://jinja.pocoo.org/docs/2.9/templates/#none)**()`
33 | # * `**[number](http://jinja.pocoo.org/docs/2.9/templates/#number)**()`
34 | # * `**[odd](http://jinja.pocoo.org/docs/2.9/templates/#odd)**()`
35 | # * `**[sameas](http://jinja.pocoo.org/docs/2.9/templates/#sameas)**(other)`
36 | # * `**[sequence](http://jinja.pocoo.org/docs/2.9/templates/#sequence)**()`
37 | # * `**[string](http://jinja.pocoo.org/docs/2.9/templates/#string)**()`
38 | # * `**[undefined](http://jinja.pocoo.org/docs/2.9/templates/#undefined)**()`
39 | # * `**[upper](http://jinja.pocoo.org/docs/2.9/templates/#upper)**()`
40 | module Crinja::Test
41 | class Library < FeatureLibrary(Callable)
42 | end
43 | end
44 |
45 | require "./test/*"
46 |
--------------------------------------------------------------------------------
/src/parser/parser_helper.cr:
--------------------------------------------------------------------------------
1 | require "log"
2 | require "./token_stream"
3 |
4 | module Crinja::Parser::ParserHelper
5 | # :nodoc:
6 | alias Kind = Parser::Token::Kind
7 |
8 | getter :token_stream
9 | delegate :next_token, :next_token?, :peek_token, :peek_token?, :current_token, to: token_stream
10 |
11 | def initialize(lexer : BaseLexer)
12 | initialize(TokenStream.new(lexer))
13 | end
14 |
15 | def initialize(@token_stream : TokenStream)
16 | end
17 |
18 | def raise(message : String)
19 | ::raise(Crinja::TemplateSyntaxError.new(current_token, message))
20 | end
21 |
22 | # :nodoc:
23 | private def expect(type : Token::Kind)
24 | unless current_token.kind == type
25 | unexpected_token type
26 | end
27 |
28 | next_token
29 | end
30 |
31 | # :nodoc:
32 | private def expect(type : Token::Kind, value : String)
33 | unless current_token.kind == type && current_token.value == value
34 | unexpected_token type, value
35 | end
36 |
37 | next_token
38 | end
39 |
40 | # :nodoc:
41 | private def unexpected_token(expected : Token::Kind? = nil, value : String? = nil)
42 | if current_token.kind != Token::Kind::EOF
43 | if expected && value
44 | error_message = "Expected #{value}, got #{current_token.kind}"
45 | elsif expected
46 | error_message = "Expected #{expected}, got #{current_token.kind}"
47 | else
48 | error_message = "Unexpected #{current_token.kind}"
49 | end
50 | else
51 | if value
52 | error_message = "Unexpected end of file, expected #{value}"
53 | elsif expected
54 | error_message = "Unexpected end of file, expected #{expected}"
55 | else
56 | error_message = "Unexpected end of file"
57 | end
58 | end
59 |
60 | raise TemplateSyntaxError.new(current_token, error_message)
61 | end
62 |
63 | # :nodoc:
64 | private def assert_token(type : Token::Kind, &)
65 | unless current_token.kind == type
66 | unexpected_token type
67 | end
68 |
69 | yield
70 | end
71 |
72 | # :nodoc:
73 | private def assert_token(type : Token::Kind, value : String, &)
74 | unless current_token.kind == type && current_token.value == value
75 | unexpected_token type, value
76 | end
77 |
78 | yield
79 | end
80 |
81 | # :nodoc:
82 | private def if_token(type : Kind, &)
83 | yield if current_token.kind == type
84 | end
85 |
86 | # :nodoc:
87 | private def if_token(type : Kind, value : String, &)
88 | yield if current_token.kind == type && current_token.value == value
89 | end
90 |
91 | def close
92 | if next_token? && current_token.kind != Kind::EOF
93 | raise TemplateSyntaxError.new(current_token, "Did not expect any more tokens, found: #{current_token}")
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/spec/tags/from_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper.cr"
2 |
3 | private def test_loader_from
4 | Crinja::Loader::HashLoader.new({
5 | "foomacro.html" => <<-'TPL',
6 | {% macro foomacro() %}foo{%endmacro%}
7 | TPL
8 | "foobarmacros.html" => <<-'TPL',
9 | {% macro foomacro() %}foo{%endmacro%}
10 | {% macro barmacro() %}bar{%endmacro%}
11 | TPL
12 | })
13 | end
14 |
15 | describe Crinja::Tag::From do
16 | it "imports macro" do
17 | render("{% from 'foomacro.html' import foomacro %}{{ foomacro() }}", loader: test_loader_from).should eq "foo"
18 | end
19 | it "imports macro" do
20 | render("{% from 'foobarmacros.html' import foomacro %}{{ foomacro() }}", loader: test_loader_from).should eq "foo"
21 | end
22 | it "fails with non-existant macro" do
23 | expect_raises(Crinja::RuntimeError, "Unknown import `barmacro`") do
24 | render("{% from 'foomacro.html' import barmacro %}", loader: test_loader_from).should eq "foo"
25 | end
26 | end
27 | describe "other macros" do
28 | it "are undefined" do
29 | render("{% from 'foobarmacros.html' import foomacro %}{{ barmacro is callable }}", loader: test_loader_from).should eq "false"
30 | end
31 | it "cannot be called" do
32 | expect_raises(Crinja::TypeError) do
33 | render("{% from 'foobarmacros.html' import foomacro %}{{ barmacro() }}", loader: test_loader_from)
34 | end
35 | end
36 | end
37 | it "overwrites existing macros" do
38 | render("{% macro foomacro() %}local{% endmacro %}{% from 'foomacro.html' import foomacro %}{{ foomacro() }}", loader: test_loader_from).should eq "foo"
39 | end
40 | it "local macro overwrites from" do
41 | render("{% from 'foomacro.html' import foomacro %}{% macro foomacro() %}local{% endmacro %}{{ foomacro() }}", loader: test_loader_from).should eq "local"
42 | end
43 | it "imports macro with alias" do
44 | render("{% from 'foomacro.html' import foomacro as aliasmacro %}{{ aliasmacro() }}", loader: test_loader_from).should eq "foo"
45 | end
46 | it "imports multiple macros" do
47 | render("{% from 'foobarmacros.html' import foomacro, barmacro %}{{ foomacro() }}|{{ barmacro() }}", loader: test_loader_from).should eq "foo|bar"
48 | end
49 | it "imports multiple macros with alias" do
50 | render("{% from 'foobarmacros.html' import foomacro as foo, barmacro as bar %}{{ foo() }}|{{ bar() }}", loader: test_loader_from).should eq "foo|bar"
51 | end
52 | it "imports multiple macros with alias" do
53 | render("{% from 'foobarmacros.html' import foomacro as barmacro, barmacro as foomacro %}{{ foomacro() }}|{{ barmacro() }}", loader: test_loader_from).should eq "bar|foo"
54 | end
55 | it "imports macro from variable source file" do
56 | render("{% from source import foomacro %}{{ foomacro() }}", {source: "foomacro.html"}, loader: test_loader_from).should eq "foo"
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/spec/spec_helper.cr:
--------------------------------------------------------------------------------
1 | require "spec"
2 | require "../src/crinja"
3 |
4 | alias Kind = Crinja::Parser::Token::Kind
5 |
6 | def parse(string)
7 | Crinja::Template.new(string)
8 | end
9 |
10 | def parse_expression(string)
11 | env = Crinja.new
12 | begin
13 | lexer = Crinja::Parser::ExpressionLexer.new(env.config, string)
14 | parser = Crinja::Parser::ExpressionParser.new(lexer)
15 | parser.parse
16 | rescue e : Crinja::TemplateError
17 | e.template = Crinja::Template.new(string, env, run_parser: false)
18 | raise e
19 | end
20 | end
21 |
22 | def render(string, bindings = nil, autoescape = nil, loader = nil, trim_blocks = nil, lstrip_blocks = nil)
23 | env = Crinja.new
24 | env.loader = loader unless loader.nil?
25 | env.config.autoescape.default_for_string = autoescape unless autoescape.nil?
26 | env.config.trim_blocks = trim_blocks unless trim_blocks.nil?
27 | env.config.lstrip_blocks = lstrip_blocks unless lstrip_blocks.nil?
28 | template = env.from_string(string)
29 |
30 | template.render(bindings)
31 | end
32 |
33 | def load(name, autoescape = nil, loader = nil, trim_blocks = nil, lstrip_blocks = nil)
34 | env = Crinja.new
35 | env.loader = loader unless loader.nil?
36 | env.context.autoescape = autoescape unless autoescape.nil?
37 | env.config.trim_blocks = trim_blocks unless trim_blocks.nil?
38 | env.config.lstrip_blocks = lstrip_blocks unless lstrip_blocks.nil?
39 | env.get_template(name)
40 | end
41 |
42 | def render_load(name, bindings = nil, autoescape = nil, loader = nil, trim_blocks = nil, lstrip_blocks = nil)
43 | load(name, autoescape, loader, trim_blocks, lstrip_blocks).render(bindings)
44 | end
45 |
46 | def evaluate_expression(string, bindings = nil, autoescape = nil)
47 | env = Crinja.new
48 | env.config.autoescape.default_for_string = autoescape unless autoescape.nil?
49 |
50 | env.evaluate(string, bindings)
51 | end
52 |
53 | def evaluate_expression_raw(string, bindings = nil, autoescape = nil)
54 | env = Crinja.new
55 | env.context.autoescape = autoescape unless autoescape.nil?
56 | lexer = Crinja::Parser::ExpressionLexer.new(env.config, string)
57 | parser = Crinja::Parser::ExpressionParser.new(lexer)
58 |
59 | expression = parser.parse
60 |
61 | env.evaluate(expression, bindings).raw
62 | end
63 |
64 | module Spec
65 | # :nodoc:
66 | struct BeInExpectation(T)
67 | def initialize(@expected_container : T)
68 | end
69 |
70 | def match(actual_value)
71 | @expected_container.includes?(actual_value)
72 | end
73 |
74 | def failure_message(actual_value)
75 | "Expected: #{@expected_container.inspect}\nto include: #{actual_value.inspect}"
76 | end
77 |
78 | def negative_failure_message(actual_value)
79 | "Expected: value #{@expected_container.inspect}\nto not include: #{actual_value.inspect}"
80 | end
81 | end
82 |
83 | module Expectations
84 | def be_in(value)
85 | Spec::BeInExpectation.new(value)
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/src/lib/filter/html.cr:
--------------------------------------------------------------------------------
1 | require "uri"
2 | require "html"
3 | require "../../util/json_builder"
4 |
5 | module Crinja::Filter
6 | Crinja.filter({trim_url_limit: nil, nofollow: false, target: nil, rel: nil}, :urlize) do
7 | rel = arguments["rel"].to_s.split(' ')
8 |
9 | rel << "nofollow" if arguments["nofollow"].truthy?
10 | rel |= env.policies.fetch("urlize.rel", "noopener").to_s.split(' ')
11 | rel = rel.reject(&.empty?).to_set
12 |
13 | target_attr = arguments.fetch("target") { env.policies.fetch("urlize.target", nil) }.raw.as(String?)
14 | trim_url_limit = arguments["trim_url_limit"].raw.as(Int32?)
15 |
16 | Crinja::Util.urlize(target.to_s, trim_url_limit, rel, target_attr)
17 | end
18 |
19 | Crinja.filter(:urlencode) do
20 | if target.iterable?
21 | target.map do |item|
22 | if item.iterable? && item.size == 2
23 | [URI.encode_www_form(item[0].to_s), "=", URI.encode_www_form(item[1].to_s)].join
24 | else
25 | URI.encode_www_form(item.to_s)
26 | end
27 | end.join("&")
28 | else
29 | URI.encode_www_form(target.to_s, space_to_plus: false)
30 | end
31 | end
32 |
33 | # TODO: This is still a draft implementation.
34 | # `responds_to?(:to_json)` is true for every object, because to_json.cr adds the wrappers everywhere.
35 | Crinja.filter({indent: nil}, :tojson) do
36 | raw = target.raw
37 |
38 | indent = arguments.fetch("indent", 0).to_i
39 |
40 | SafeString.escape do |io|
41 | JsonBuilder.to_json(io, raw, indent)
42 | end
43 | end
44 |
45 | Crinja.filter({autoescape: true}, :xmlattr) do
46 | string = SafeString.build do |io|
47 | target.as_h.each do |key, value|
48 | next if value.none? || value.undefined?
49 |
50 | io << sprintf %( %s="%s"), HTML.escape(key.to_s), HTML.escape(value.to_s)
51 | end
52 | end
53 |
54 | if string.size > 0 && !arguments["autoescape"].truthy?
55 | string = string[1..-1]
56 | end
57 |
58 | string
59 | end
60 | end
61 |
62 | module Crinja::Util
63 | # https://github.com/tenderlove/rails_autolink/blob/master/lib/rails_autolink/helpers.rb
64 | AUTO_LINK_RE = %r{
65 | (?: ([0-9A-Za-z+.:-]+:)// | www\. )
66 | [^\s<]+
67 | }x
68 |
69 | def self.urlize(text, trim_url_limit, rel, target)
70 | rel_attr = ""
71 | rel_attr = %( rel="%s") % HTML.escape(rel.join(' ')) unless rel.empty?
72 |
73 | target_attr = ""
74 | target_attr = %( target="%s") % HTML.escape(target) unless target.nil? || target.empty?
75 |
76 | SafeString.build do |io|
77 | text.each_line do |line|
78 | io << line.gsub(AUTO_LINK_RE) do
79 | all = $~
80 | url = all[0]
81 | display = url
82 | unless trim_url_limit.nil? || display.size < trim_url_limit
83 | display = display[0..(trim_url_limit - 3)] + "..."
84 | end
85 | %(#{display})
86 | end
87 | end
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/src/arguments.cr:
--------------------------------------------------------------------------------
1 | require "./crinja"
2 | require "./error"
3 |
4 | # This holds arguments and environment information for function, filter, test and macro calls.
5 | struct Crinja::Arguments
6 | # Returns the variable arguments of the call.
7 | getter varargs : Array(Value)
8 |
9 | # Returns the target of the call (if any).
10 | getter target : Value?
11 |
12 | # Returns the keyword arguments of the call.
13 | getter kwargs : Hash(String, Value)
14 |
15 | # Default argument values defined by the call implementation.
16 | getter defaults : Variables
17 |
18 | # :nodoc:
19 | setter defaults : Variables
20 |
21 | # Returns the crinja environment.
22 | getter env : Crinja
23 |
24 | def initialize(@env, @varargs = [] of Value, @kwargs = Hash(String, Value).new, @defaults = Variables.new, @target = nil)
25 | end
26 |
27 | def [](name : String) : Value
28 | if kwargs.has_key?(name)
29 | kwargs[name]
30 | elsif index = defaults.index { |k, v| k == name }
31 | if varargs.size > index
32 | varargs[index]
33 | else
34 | default(name)
35 | end
36 | else
37 | raise UnknownArgumentError.new(name, self)
38 | end
39 | end
40 |
41 | def fetch(name, default : Value)
42 | fetch(name) { default }
43 | end
44 |
45 | def fetch(name, default = nil)
46 | fetch name, Value.new(default)
47 | end
48 |
49 | def fetch(name, &)
50 | value = self[name]
51 | if value.raw.nil?
52 | Value.new(yield)
53 | else
54 | value
55 | end
56 | end
57 |
58 | def target!
59 | if (t = target).nil?
60 | raise UndefinedError.new("undefined target")
61 | else
62 | t
63 | end
64 | end
65 |
66 | def to_h
67 | [@kwargs.keys, @defaults.keys].flatten.uniq.each_with_object(Hash(String, Value).new) do |key, hash|
68 | hash[key] = self[key]
69 | end
70 | end
71 |
72 | def is_set?(name : Symbol)
73 | is_set?(name.to_s)
74 | end
75 |
76 | def is_set?(name : String)
77 | kwargs.has_key?(name) || (index = defaults.index { |k, v| k == name }) && varargs.size > index
78 | end
79 |
80 | def default(name : Symbol)
81 | default(name.to_s)
82 | end
83 |
84 | def default(name : String)
85 | Value.new defaults[name]
86 | end
87 |
88 | class UnknownArgumentError < RuntimeError
89 | def initialize(name, arguments)
90 | super "unknown argument \"#{name}\" for #{arguments.inspect}"
91 | end
92 | end
93 |
94 | class Error < RuntimeError
95 | property callee
96 | property argument : String?
97 |
98 | def self.new(argument : Symbol | String, msg = nil, cause = nil)
99 | new nil, msg, cause, argument: argument
100 | end
101 |
102 | def initialize(@callee : Callable | Callable::Proc | Operator?, msg = nil, cause = nil, @argument = nil)
103 | super msg, cause
104 | end
105 |
106 | def message
107 | arg = ""
108 | arg = " argument: #{argument}" unless argument.nil?
109 | "#{super} (called: #{callee}#{arg})"
110 | end
111 | end
112 | end
113 |
--------------------------------------------------------------------------------
/src/lib/filter/number.cr:
--------------------------------------------------------------------------------
1 | module Crinja::Filter
2 | Crinja.filter :abs do
3 | if target.number?
4 | target.as_number.abs
5 | else
6 | raise Arguments::Error.new("abs", "Cannot render abs value for #{target.raw.class}, only accepts numbers")
7 | end
8 | end
9 |
10 | Crinja.filter({default: 0.0}, :float) do
11 | raw = target.raw
12 | if raw.responds_to?(:to_f?) && (result = raw.to_f?)
13 | return Value.new result
14 | end
15 |
16 | arguments["default"].to_f
17 | end
18 |
19 | Crinja.filter({default: 0, base: 10}, :int) do
20 | raw = target.raw
21 | raw = raw.to_s if raw.is_a?(SafeString)
22 | if raw.is_a?(String)
23 | if raw['.']?
24 | result = raw.to_f?.try &.to_i
25 | else
26 | result = raw.to_i?(arguments["base"].to_i, prefix: true)
27 | end
28 | elsif raw.responds_to?(:to_i?)
29 | result = raw.to_i?
30 | elsif raw.responds_to?(:to_i)
31 | result = raw.to_i
32 | end
33 |
34 | if result
35 | Value.new result
36 | else
37 | arguments["default"].to_i
38 | end
39 | end
40 |
41 | Crinja.filter({binary: false}, :filesizeformat) do
42 | Crinja::Filter::Filesizeformat.filesize_to_human(target.to_f, arguments["binary"].truthy?)
43 | end
44 |
45 | class Filesizeformat
46 | def self.filesize_to_human(size, binary = false)
47 | if binary
48 | {
49 | "Bytes" => 1024_i64,
50 | "KiB" => 1024_i64 ** 2,
51 | "MiB" => 1024_i64 ** 3,
52 | "GiB" => 1024_i64 ** 4,
53 | "TiB" => 1024_i64 ** 5,
54 | "PiB" => 1024_i64 ** 6,
55 | }
56 | else
57 | {
58 | "Bytes" => 1000_i64,
59 | "kB" => 1000_i64 ** 2,
60 | "MB" => 1000_i64 ** 3,
61 | "GB" => 1000_i64 ** 4,
62 | "TB" => 1000_i64 ** 5,
63 | "PB" => 1000_i64 ** 6,
64 | }
65 | end.each do |unit, magnitude|
66 | if size < magnitude
67 | return String.build do |io|
68 | converted = (size / (magnitude // (binary ? 1024 : 1000)))
69 | if unit == "Bytes"
70 | io << converted.to_i
71 | else
72 | io << converted.round(1)
73 | end
74 | io << " "
75 | io << unit
76 | end
77 | end
78 | end
79 | end
80 | end
81 |
82 | Crinja.filter({precision: 0, method: "common", base: 10}, :round) do
83 | precision = arguments["precision"].to_i
84 | value = target.as_number
85 | base = arguments["base"].as_number
86 | base = base.to_f if precision < 0
87 |
88 | case arguments["method"].as_s
89 | when "common"
90 | value.round(precision, base)
91 | when "ceil"
92 | multi = base ** precision
93 | (value * multi).ceil.to_f / multi
94 | when "floor"
95 | multi = base ** precision
96 | (value * multi).floor.to_f / multi
97 | else
98 | raise Arguments::Error.new("method", "argument `method` for filter `round` must be 'common', 'ceil' or 'floor'")
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/src/lib/test/tests.cr:
--------------------------------------------------------------------------------
1 | # Return whether the object is callable.
2 | Crinja.test(:callable) { target.callable? }
3 |
4 | # Returns `true` if the variable is defined.
5 | # See the `default()` filter for a simple way to set undefined variables.
6 | Crinja.test(:defined) { !target.undefined? }
7 |
8 | # Returns `true` if the variable is undefined.
9 | Crinja.test(:undefined) { target.undefined? }
10 |
11 | # Returns `true` if the variable is nil.
12 | Crinja.test(:none) { target.raw.nil? }
13 |
14 | # Returns `true` if the variable is nil.
15 | Crinja.test(:nil) { target.raw.nil? }
16 |
17 | # Returns `true` if the object is a mapping (dict etc.).
18 | Crinja.test(:mapping) { target.mapping? }
19 |
20 | # Check if a variable is divisible by a number.
21 | Crinja.test({num: Crinja::UNDEFINED}, :divisibleby) { target.to_i % arguments["num"].to_i == 0 }
22 |
23 | # Check if an object has the same value as another object:
24 | # ```
25 | # {% if foo.expression is equalto 42 %}
26 | # the foo attribute evaluates to the constant 42
27 | # {% endif %}
28 | # ```
29 | # This appears to be a useless test as it does exactly the same as the == operator, but it can be useful when used together with the selectattr function:
30 | # ```
31 | # {{ users | selectattr("email", "equalto", "foo@bar.invalid") }}
32 | # ```
33 | Crinja.test({other: Crinja::UNDEFINED}, :equalto) { target == arguments["other"] }
34 |
35 | # Checks if an object points to the same memory address than another object:
36 | Crinja.test({other: Crinja::UNDEFINED}, :sameas) { target.sameas? arguments["other"] }
37 |
38 | # Returns `true` if the variable is lowercased.
39 | Crinja.test(:lower) { target.to_s.chars.all?(&.lowercase?) }
40 |
41 | # Returns `true` if the variable is upcased.
42 | Crinja.test(:upper) { target.to_s.chars.all?(&.uppercase?) }
43 |
44 | # Returns `true` if the variable is a string.
45 | Crinja.test(:string) { target.string? }
46 |
47 | # Returns `true` if the variable is a string.
48 | Crinja.test(:escaped) { target.raw.is_a?(Crinja::SafeString) }
49 |
50 | # Returns `true` if the variable is a number.
51 | Crinja.test(:number) { target.number? }
52 |
53 | # Returns `true` if the variable is a sequence. Sequences are variables that are iterable.
54 | Crinja.test(:sequence) { target.sequence? }
55 |
56 | # Returns `true` if the variable is iterable.
57 | Crinja.test(:iterable) { target.iterable? }
58 |
59 | # This tests an integer if it is even.
60 | Crinja.test(:even) { target.to_i.even? }
61 |
62 | # This test an integer if it is odd.
63 | Crinja.test(:odd) { target.to_i.odd? }
64 |
65 | # Checks if value is less than other.
66 | Crinja.test({other: 0}, :lessthan) { target.to_i.<(arguments["other"].to_i) }
67 |
68 | # Checks if value is greater than other.
69 | Crinja.test({other: 0}, :greaterthan) { target.to_i.>(arguments["other"].to_i) }
70 |
71 | # Check if value is in seq.
72 | Crinja.test({seq: Array(Crinja::Value).new}, :in) {
73 | seq = arguments["seq"]
74 | raw = seq.raw
75 | case raw
76 | when Hash
77 | raw.has_key? target.raw
78 | when Enumerable
79 | raw.includes? target.raw
80 | else
81 | if seq.string?
82 | seq.to_s.includes?(target.to_s)
83 | else
84 | raise seq.inspect
85 | end
86 | end
87 | }
88 |
--------------------------------------------------------------------------------
/playground/objects.md:
--------------------------------------------------------------------------------
1 | # Using custom objects
2 |
3 | To make custom objects usable in Crinja, they need to include `Crinja::Object`.
4 |
5 | > This module does not define any methods or requires a specific interface, it is just necessary to have a dedicated
6 | type for this because Crystal cannot use `Object` as type of an instance variable (yet).
7 |
8 | Types *may* implement the following methods to make properties accessbile:
9 |
10 | * `#crinja_attribute(name : Crinja::Value) : Crinja::Value`:
11 | Access an attribute (e.g. an instance property) of this type.
12 | * `#crinja_call(name : String) : Crinja::Callable | Callable::Proc | Nil`:
13 | Expose a callable as method of this type.
14 |
15 | `crinja_attribute` *must* return an `Crinja::Undefined` if there is no attribute or item of that name. `crinja_call` returns `nil` in that case.
16 |
17 | ## Example
18 |
19 | ```playground
20 | require "./crinja"
21 |
22 | class User
23 | include Crinja::Object
24 |
25 | property name : String
26 | property dob : Time
27 |
28 | def initialize(@name, @dob)
29 | end
30 |
31 | def age
32 | (Time.now - @dob).years
33 | end
34 |
35 | def crinja_attribute(attr : Crinja::Value)
36 | value = case attr.to_string
37 | when "name"
38 | name
39 | when "age"
40 | age
41 | else
42 | Crinja::Undefined.new(attr.to_s)
43 | end
44 |
45 | Crinja::Value.new(value)
46 | end
47 | end
48 |
49 | users = [
50 | User.new("john", Time.new(1982, 10, 10)),
51 | User.new("bob", Time.new(1997, 9, 16)),
52 | User.new("peter", Time.new(2002, 4, 1))
53 | ]
54 |
55 | Crinja.render STDOUT, <<-'TEMPLATE', {users: users}
56 | {%- for user in users -%}
57 | * {{ user.name }} ({{ user.age }})
58 | {% endfor -%}
59 | TEMPLATE
60 | ```
61 |
62 | # Automatic exposure
63 |
64 | The method definition of `crinja_attribute` is often pretty boring as it usually just maps names of methods to the respective method calls.
65 |
66 | This can easily be generated automatically by the use of `Crinja::Object::Auto`. This module defines an automatically generated `crinja_attribute` method that exposes the types method as attributes.
67 |
68 | A method will be exposed if it is annotated with `@[Crystal::Attribute]`.
69 |
70 | A type annotated with `@[Crystal::Attributes]` exposes all methods defined on that type and matching the signature (no argument, no block).
71 | This annotation take an optional `expose` argument which whitelist methods to expose.
72 |
73 | ```playground
74 | @[Crinja::Attributes(expose: [name, age])]
75 | class User
76 | include Crinja::Object::Auto
77 |
78 | property name : String
79 | property dob : Time
80 |
81 | def initialize(@name, @dob)
82 | end
83 |
84 | def age
85 | (Time.now - @dob).years
86 | end
87 | end
88 |
89 | users = [
90 | User.new("john", Time.new(1982, 10, 10)),
91 | User.new("bob", Time.new(1997, 9, 16)),
92 | User.new("peter", Time.new(2002, 4, 1))
93 | ]
94 |
95 | Crinja.render STDOUT, <<-'TEMPLATE', {users: users}
96 | {%- for user in users -%}
97 | * {{ user.name }} ({{ user.age }})
98 | {% endfor -%}
99 | TEMPLATE
100 | ```
101 |
--------------------------------------------------------------------------------