ute
Ute
A lightweight template engine in pure crystal. Built for my own entertainment.
Probably somewhat unfinished as when I write these lines I have done what I wanted to with this pet project.
It handles:
- Nested parameters
- Simple conditionals
- Loops
- Inclusion of other templates (composition)
- Usage of base templates (kind of inhertiance)
- Escapement
- Whitespace handling (WIP)
- Caching
- Low hassle parameters
- Compile time caching
It would like to handle but is unlikely because im growing bored of this project:
- More complex conditonals
- Default values
- Better whitespace handling
- Disablable whitespace handling
- Default body for anchor tags
- Loose and strict modes
- Compatibility with
YAML::Any
as parameters
Syntax and features
Basic
Tags are denoted by ${...}
.
Simple tags with an identifier will output the value of the parameters named this way:
Hello ${name}
Will render as:
Hello Bob
Nested
Parameters can be nested and accessed through a chain:
The template:
Hello ${user.name}.
Will render as:
Hello Bob.
Escapement
In case you need to output tags literaly, they can be escaped:
Hello ${name}
Hello \${name}
Hello \\${name}
Hello \\\${name}
Escapement is not needed in raw text: \\\
Will render as:
Hello Bob
Hello ${name}
Hello \Bob
Hello \${name}
Escapement is not needed in raw text: \\\
Conditonals
Simpel conditions are supported:
Hello ${if user.is_nice}magnificient ${end}${user.name}
Might render as:
Hello Bob
or
Hello magnificient Bob
depending on the truthyness of the user.is_nice
parameter.
If the parameter is missing entierly, it will be considered falsey.
Loops
List:${for item of items} ${item}${end}.
Will render as:
List: lentils rice.
Loops supports keys and indexes too:
${for key, value of items}
${key}: ${item}
${end}
With array parameters:
{"items" => ["lentils", "rice"]}
:
1: lentils
2: rice
With hash parameters:
{"items" => {"some" => "lentils", "much" => "rice"}}
:
some: lentils
much: rice
Inclusion
A template can include another template:
web.ut:
...
<body>
${include "content.ut"}
</body>
content.ut:
Some cool informations
The web.ut template will render as:
...
<body>
Some cool informations
</body>
Parameters
Included templates receive the same parameters as the including template by default. This can be changed with an optionnal parameter name after the template name:
${include "some_template.ut" the_parameter.it_will_receive}
Dynamic template names
The included template name can be dynamic and stored in a parameter. In this case the template name must be replaced buy the parameter holding it and quotes must be ommitted:
${include some.template.name}
Generic templating
It is possible to defines a base template with reserved spaces to be filled by another template later. The inheriting template will specify which mother template to use, and the content to put in each empty space.
A base template:
Some header
${anchor body_content}
Some footer
An inheriting template:
${use "mother_template_path"}
${as body_content}
This is the content that will appear instead of the \${anchor body_content},
while the content of the mother will appear instead of \${use "mother_template_path"}
${end}
${end}
The inheriting template will render as:
Some header
This is the content that will appear instaed of the \${anchor_body_content},
while the content of the mother will appear instead of \${use "mother_template_path"}
Some footer
Similarly to inclusion, inherited template names can be dynamic and provided as a parameter. A second parameters can be added to specify the root of the parameters used by the rendering templats.
[!NOTE] There might be several 'anchor' in a template, and so several 'as' tags in a 'use' tag body.
There might be several 'use' tag within the same template.
Templates can be nested both ways: a template can use both 'use' tags and 'anchor' tags.
[!WARNING] When changing the parameters given to the base template, those new parameters will be applied within the body of the
${as ...}
tags of the inheriting template.This is subject to future changes.
Whitespace handling
In order to make template easier to write and read, when a control (that does not output anything by itself) template tag is the only none whitespace text in the line, the line is not outputed. It allows for a nicer structuring of the templates:
The template
List:
${for item in items}
- ${item}
${end}
End of the list.
Will render:
List:
- a
- b
End of the list.
and not:
List:
- a
- b
End of the list
[!CAUTION] This may not work perfectly well, it might consider the tag alone in the line even if there are another tag, or content 'hidden' by another tag, previously in the line. This is due to the method of scanning the whole file for tags, instead of working line per line.
This is subject to future changes.
Parameters
Parameters can be provided in several ways:
Provided format
There is a Template::Parameters
type that can be used to holds parameters:
parameters = Template::Parameters.new({
"name" => Template::Parameters.new("Bob"),
"friends" => Template::Parameters.new([
Template::Parameters.new("John")
])
})
However native crystal types can be used as is, they will be converted internally:
parameters = {
"name" => "Bob",
"friends" => [
"John"
]
}
[!NOTE] When using native types, the following are alloweds:
- String
- Bool
- Array()
- Hash(String, )
JSON::Any
The parameters can also be provided as a JSON::Any
:
require "json"
require "ute"
manager = Template::Manager.new
manager.render "template", JSON.parse <<-JSON
{
"name": "Bob",
"friends": [
"John"
]
}
JSON
YAML::Any
While it is desired, using YAML::Any
as parameters is not yet supported.
The reasons is that YAML::Any
hashes use YAML::Any
as keys and not String
, which require some adaptation to handle.
Anything else you want
Any custom type can be given as parameters, as long as it implements the following constructor and methods:
abstract class YourCustomType
abstract def initialize(raw : Whatever)
abstract def [](key : String) : self
abstract def []?(key : String) : self?
abstract def raw : Whatever
end
Loops tags expect the given parameter raw
value to be one of Array(self)
or Hash(String, self)
.
The method to_s
will be called when inserting a parameter into the result.
As for conditonal, the raw
method will be used as is, so crystal truthy/falsey rules applies.
Caching
A Template::Manager
will cache an intermediary representation of each template it renders. The cache will be invalidated if the orignal template file exists and has a modification time later than the one of the version cached.
The cache is easily accessible as Template::Manager#cache
.
The entries in the cache are indexed through the name of the template, with the base path of the template manager if it has one. You can consider the bath path to only be a quality of life when all the templates are in a single directory.
At compile time
It is possible to create a cache of the intermediary representation of templates at compile time (thus embedding the "templates" into the compiled binary).
This cache wil behave exactly as usual, which mean it is possible to compile a program with his template and run it elswhere without them, then later add one of the template back to update it if needed.
Note that using the compile time cache will increase compilation times (especially when compiling with it for the first time).
require "ute"
# Important note: `#build_with_cache` is a macro
# which mean the parameter MUST be a literal string
# It also means that you should avoid using it for the same dir in several codebase locations.
mgr = Template::Manager.build_with_cache "spec"
# mgr will have all the templates within ./spec in its cache immediately at runtime, even if the template on the filesystem are gone.
For a finer control, you can use
Template::Manager#compile_time_cache(*patterns, base = nil)
Which will generate the cache literal for all the templates matched by the patterns. The template names will be made relative to base
if given.
Installation
-
Add the dependency to your
shard.yml
:dependencies: ute: github: globoplox/ute
-
Run
shards install
Usage
require "ute"
template_dir = ENV["TEMPLATE_DIR"]?
# The directory parameter is optional. It defaults to the current parameter.
mgr = Template::Manager.new template_dir
# Render a template:
rendered = mgr.render "example.ut"
# With parameters:
parameters = {"some" => "parameters"}
rendered = mgr.render "exemple.ut", parameters
# Render into an IO:
dest = IO::Memory.new
mgr.render "example.ut", parameters, dest
rendered = dest.tap(&.rewind).gets_to_end
You can find more examples in the spec
folder.
ute
- 0
- 0
- 0
- 0
- 0
- 10 months ago
- February 21, 2024
MIT License
Sat, 21 Dec 2024 22:23:01 GMT