├── 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 | 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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
UserNumber of reviews
{{ username }}{{ number_reviews }}
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 | 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 | --------------------------------------------------------------------------------