crinja
Crinja is an implementation of the Jinja2 template engine written in Crystal. Templates are parsed and evaluated at runtime (see Background). It includes a script runtime for evaluation of dynamic python-like expressions used by the Jinja2 syntax.
API Documentation · Github Repo · Template Syntax
Features
Crinja tries to stay close to the Jinja2 language design and implementation. It currently provides most features of the original template language, such as:
- all basic language features like control structures and expressions
- template inheritance
- block scoping
- custom tags, filters, functions, operators and tests
- autoescape by default
- template cache
From Jinja2 all builtin control structures (tags), tests, global functions, operators and filters have been ported to Crinja. See Crinja::Filter
, Crinja::Test
, Crinja::Function
, Crinja::Tag
, Crinja::Operator
for lists of builtin features.
Currently, template errors fail fast raising an exception. It is considered to change this behaviour to collect multiple errors, similar to what Jinjava does.
Installation
Add this to your application's shard.yml
:
dependencies:
crinja:
github: straight-shoota/crinja
Usage
Simple string template
require "crinja"
Crinja.render("Hello, {{ name }}!", {"name" => "John"}) # => "Hello, John!"
File loader
With this template file:
# views/index.html.j2
<p>Hello {{ name | default('World') }}</p>
It can be loaded with a FileSystemLoader
:
require "crinja"
env = Crinja.new
env.loader = Crinja::Loader::FileSystemLoader.new("views/")
template = env.get_template("index.html.j2")
template.render # => "Hello, World!"
template.render({ "name" => "John" }) # => "Hello, John!"
Crystal Playground
Run the Crystal playground inside this repostitory and the server is prepared with examples of using Crinja's API (check the Workbooks
section).
$ crystal play
You can also browse the examples and documentation online (without the interactive playground): objects & features
Crinja Playground
The Crinja Example Server in examples/server
is an HTTP server which renders Crinja templates from examples/server/pages
. It has also an interactive playground for Crinja template testing at /play
.
$ cd examples/server && crystal server.cr
Other examples can be found in the examples
folder.
Template Syntax
The following is a quick overview of the template language to get you started.
More details can be found in the template guide. The original Jinja2 template reference can also be helpful, Crinja templates are mostly similar.
Expressions
In a template, expressions inside double curly braces ({{
... }}
) will be evaluated and printed to the template output.
Assuming there is a variable name
with value "World"
, the following template renders Hello, World!
.
Hello, {{ name }}!
Properties of an object can be accessed by dot (.
) or square brackets ([]
). Filters modify the value of an expression.
Hello, {{ current_user.name | default("World") | titelize }}!
Tests are similar to filters, but are used in the context of a boolean expression, for example as condition of an if
tag.
{% if current_user is logged_in %}
Hello, {{ current_user.name }}!
{% else %}
Hey, stranger!
{% end %}
Tags
Tags control the logic of the template. They are enclosed in {%
and %}
.
{% if is_morning %}
Good Morning, {{ name }}!
{% else %}
Hello, {{ name }}!
{% end %}
The for
tag allows looping over a collection.
{% for name in users %}
{{ user.name }}
{% endfor %}
Other templates can be included using the include
tag:
{% include "header.html" %}
<main>
Content
</main>
{% include "footer.html" %}
Macros
Macros are similar to functions in other programming languages.
{% macro say_hello(name) %}Hello, {{ name | default("stranger") }}!{% endmacro %}
{{ say_hello('Peter') }}
{{ say_hello('Paul') }}
Template Inheritance
Template inheritance enables the use of block
tags in parent templates that can be overwritten by child templates. This is useful for implementating layouts:
{# layout.html #}
<h1>{% block page_title %}{% endblock %}</h1>
<main>
{% block body %}
{# This block is typically overwritten by child templates #}
{% endblock %}
</main>
{% block footer %}
{% include "footer.html" %}
{% endblock %}
{# page.html #}
{% extends "layout.html" %}
{% block page_title %}Blog Index{% endblock %}
{% block body %}
<ul>
{% for article in articles if article.published %}
<div class="article">
<li>
<a href="{{ article.href | escape }}">{{ article.title | escape }}</a>
written by <a href="{{ article.user.href | escape}}">{{ article.user.username | escape }}</a>
</li>
{%- endfor %}
</ul>
{% endblock %}
Crystal API
The API tries to stick ot the original Jinja2 API which is written in Python.
Configuration
Currently the following configuration options for Config
are supported:
- autoescape
-
This config allows the same settings as
select_autoescape
in Jinja 2.9.It intelligently sets the initial value of autoescaping based on the filename of the template.
When set to a boolean value,
false
deactivates any autoescape andtrue
activates autoescape for any template. It also allows more detailed configuration:- enabled_extensions
- List of filename extensions that autoescape should be enabled for. Default:
["html", "htm", "xml"]
- disabled_extensions
- List of filename extensions that autoescape should be disabled for. Default:
[] of String
- default_for_string
- Determines autoescape default value for templates loaded from a string (without a filename). Default:
false
- default
- If nothing matches, this will be the default autoescape value. Default:
false
Note: The default configuration of Crinja differs from that of Jinja 2.9, that autoescape is activated by default for HTML and XML files. This will most likely be changed by Jinja2 in the future, too.
- disabled_filters
- A list of *disabled_filters* that will raise a `SecurityError` when invoked.
- disabled_functions
- A list of *disabled_functions* that will raise a `SecurityError` when invoked.
- disabled_operators
- A list of *disabled_operators* that will raise a `SecurityError` when invoked.
- disabled_tags
- A list of *disabled_tags* that will raise a `SecurityError` when invoked.
- disabled_tests
- A list of *disabled_tests* that will raise a `SecurityError` when invoked.
- keep_trailing_newline
- Preserve the trailing newline when rendering templates. If set to `false`, a single newline, if present, will be stripped from the end of the template. Default:
false
- trim_blocks
- If this is set to
true
, the first newline after a block is removed. This only applies to blocks, not expression tags. Default:false
. - lstrip_blocks
- If this is set to
true
, leading spaces and tabs are stripped from the start of a line to a block. Default:false
. - If
register_defaults
is set totrue
, all feature libraries will be populated with the defaults (Crinja standards and registered custom features). Otherwise the libraries will be empty. They can be manually populated withlibrary.register_defaults
. This setting needs to be set at the creation of an environment.
See also the original Jinja2 API Documentation.
Custom features
You can provide custom tags, filters, functions, operators and tests. Create an implementation using the macros Crinja.filter
, Crinja.function
, Crinja.test
. They need to be passed a block which will be converted to a Proc. Optional arguments are a Hash
or NamedTuple
with default arguments and a name. If a name is provided, it will be added to the feature library defaults and available in every environment which uses the registered defaults.
Example with macro Crinja.filter
:
env = Crinja.new
myfilter = Crinja.filter({ attribute: nil }) do
"#{target} is #{arguments["attribute"]}!"
end
env.filters["customfilter"] = myfilter
template = env.from_string(%({{ "Hello World" | customfilter(attribute="super") }}))
template.render # => "Hello World is super!"
Or you can define a class for more complex features:
class Customfilter
include Crinja::Callable
getter name = "customfilter"
getter defaults = Crinja.variables({
"attribute" => "great"
})
def call(arguments)
"#{arguments.target} is #{arguments["attribute"]}!"
end
end
env = Crinja.new
env.filters << Customfilter.new
template = env.from_string(%({{ "Hello World" | customfilter(attribute="super") }}))
template.render # => "Hello World is super!"
Custom tags and operator can be implemented through subclassing Crinja::Operator
and Crinja:Tag
and adding an instance to the feature library defaults (Crinja::Operator::Library.defaults << MyTag.new
) or to a specific environment (env.tags << MyTag.new
).
Differences from Jinja2
This is an incomplete list of Differences to the original Jinja2:
- Python expressions: Because templates are evaluated inside a compiled Crystal program, it's not possible to use ordinary Python expressions in Crinja. But it might be considered to implement some of the Python stdlib like
Dict#iteritems()
which is often used to make dicts iterable. - Line statements and line comments: Are not supported, because their usecase is negligible.
- String representation: Some objects will have slightly different representation as string or JSON. Crinja uses Crystal internals, while Jinja uses Python internals. For example, an array with strings like
{{ ["foo", "bar"] }}
will render as[u'foo', u'bar']
in Jinja2 and as['foo', 'bar']
in Crinja. - Double escape:
{{ '<html>'|escape|escape }}
will render as<html>
in Jinja2, but&lt;html&gt;
. Should we change that behaviour? - Complex numbers: Complex numbers are not supported yet.
- Configurable syntax: It is not possible to reconfigure the syntax symbols. This makes the parser less complex and faster.
The following features are not yet fully implemented, but on the roadmap:
- Sandboxed execution.
- Some in-depth features like extended macro reflection, reusable blocks.
Background
Crystal is a great programming language with a clean syntax inspired by Ruby, but it is compiled and runs incredibly fast.
There are already some template engines for crystal. But if you want control structures and dynamic expressions without some sort of Domain Specific Language, there is only Embedded Crystal (ECR), which is a part of Crystal's standard library. It uses macros to convert templates to Crystal code and embed them into the source at compile time. So for every change in a template, you have to recompile the binary. This approach is certainly applicable for many projects and provides very fast template rendering. The downside is, you need a crystal build stack for template design. This makes it impossible to render dynamic, user defined templates, that can be changed at runtime.
Jinja2 is a powerful, mature template engine with a great syntax and proven language design. Its philosophy is:
Application logic is for the controller, but don't make the template designer's life difficult by restricting functionality too much.
Jinja derived from the Django Template Language. While it comes from web development and is heavily used there (Flask) Ansible and Salt use it for dynamic enhancements of configuration data. It has quite a number of implementations and adaptations in other languages:
- Jinjava - Jinja2 implementation in Java using Unified Expression Language (
javaex.el
) for expression resolving. It served as an inspiration for some parts of Crinja. - Liquid - Jinja2-inspired template engine in Ruby
- Liquid.cr - Liquid implementation in Crystal
- Twig - Jinja2-inspired template engine in PHP
- ginger - Jinja2 implementation in Haskell
- Jinja-Js - Jinja2-inspired template engin in Javascript
- jigo - Jinja2 implementation in Go
- tera - Jinja2 implementation in Rust
- jingoo - Jinja2 implementation in OCaml
- nunjucks - Jinja2 inspired template engine in Javascript
Contributing
- Fork it (https://github.com/straight-shoota/crinja/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Contributors
- straight-shoota Johannes Müller - creator, maintainer