A compile-time Haml templating engine for Crystal. Inspired by the Ruby Haml gem (source, rubygems).
A template contains Haml markup and Crystal expressions, compiled ahead of time into a Crystal macro that writes to an IO (Haml.embed(filename, io_name)) or returns a string (Haml.render(filename)).
- Quick demo
- Interface
- Syntax
- Installation
- Key differences from Ruby Haml
- Issues and pull requests
- Author
- Development note
- Create a template file
haml_demo.html.haml:
- # I'm a comment and won't be rendered into the output HTML!
%section.container
%h1= post.title
%h2= post.subtitle
.content
= post.content- Call
Haml.renderfrom your Crystal codehaml_demo.cr:
require "haml"
record Post, title : String, subtitle : String, content : String
example_post = Post.new(title: "Welcome to Haml", subtitle: "A nice way to write templates", content: "Ruby & Crystal love Haml!")
def my_view(post : Post) : String
Haml.render("#{__DIR__}/haml_demo.html.haml")
end
puts my_view(example_post)Run it with crystal run haml_demo.cr. Output:
<section class="container">
<h1>Welcome to Haml</h1>
<h2>A nice way to write templates</h2>
<div class="content">
Ruby & Crystal love Haml!
</div>
</section>Please observe:
- The
&is automatically HTML-escaped to&.- (If you are viewing this README.md with a Markdown renderer, it may or may not show that, so look at the raw Markdown, or run the example yourself.)
- Just like Crystal's stdlib ECR:
- The template file is fully compiled into the binary at Crystal compile time: the template file is not read and not needed at runtime.
- The
Haml.rendercompiles the template file into a Crystal macro, so it can access in-scope variables likepost, and run other arbitrary Crystal code.
Just like Crystal's stdlib ECR, there are three supported macros:
Haml.embed(filename, io_name)- writes to io_nameHaml.render(filename)- returns a String (i.e. it wrapsHaml.embedinString.build)Haml.def_to_s(filename)- defines a#to_s(io)method
As shown above, using Haml.render(filename) from your view method is probably the most straightforward way to use Haml templates in your Crystal code.
A Haml.render_string(s) macro can be used for quick testing (but can become confusing because #{...} interpolation may happen before the string is passed to the macro):
crystal eval 'require "haml" ; puts Haml.render_string("%h1 Hello World\n%h2\n from Crystal\n = Crystal::VERSION")'<h1>Hello World</h1>
<h2>
from Crystal
1.21.1
</h2>In this directory, shards build will build a hamlc binary which compiles a .haml file into a Crystal macro:
hamlc --no-locations examples/haml_demo.html.hamloutputs:
__haml_io << "<section class=\"container\">\n<h1>"
::HTML.escape((post.title
).to_s, __haml_io)
__haml_io << "</h1>\n<h2>"
::HTML.escape((post.subtitle
).to_s, __haml_io)
__haml_io << "</h2>\n<div class=\"content\">\n"
::HTML.escape((post.content
).to_s, __haml_io)
__haml_io << "\n</div>\n</section>\n"
nilIn general you won't need this: just use Haml.render in your code as shown above.
See SYNTAX.md. Quick overview:
=escapes every dynamic value usingHTML.escape.!=inserts explicitly trusted raw markup.- There is no
html_safebypass like in Ruby on Rails. - Attribute values always escape.
str = "A&B"| Haml input | HTML output | Notes |
|---|---|---|
%p Hello A&B |
<p>Hello A&B</p> |
your text passes through unescaped |
%p Hello A&B |
<p>Hello A&B</p> |
|
%p Hello #{str} |
<p>Hello A&B</p> |
#{...} is interpolated and escaped |
<p>Hello #{str}</p> |
<p>Hello A&B</p> |
your own HTML tags + escaped interpolated expressions |
%p& Hello A&B |
<p>Hello A&B</p> |
the & forces escaping even on your own text |
%p= str |
<p>A&B</p> |
|
%p!= str |
<p>A&B</p> |
!= unsafely inserts raw output. (Caution: XSS risk.) |
The #{str} and = str forms will be the ones you use most frequently. Save != for when you want to insert pre-escaped content (such as raw HTML).
%p
Coin flip:
%strong
- if Random.rand >= 0.5
Heads
- else
Tails%ul
- entries.each do |entry|
%li= entry.title%a{href: entry.url, target: "_blank"}
= entry.titleMulti-line attributes are supported:
%a{
href: entry.url,
target: "_blank"
}
= entry.title%h2#my_id Subheading with an ID
%h2.mb-0.fw-bold#another_id With classes and ID
%h2{class: ["mb-0", "fw-bold", dynamic_class], id: dynamic_id} With Crystal expressions to set class and ID
%div.container
-# is the same as
.containerMultiline Crystal interpolated strings:
- value = "Crystal expression"
.example-1
=%(
This entire line, including its dynamically interpolated #{value}, will be HTML-escaped.
And this #{4 - 3} too.
)
.example-2
!=%(
This entire line, including its dynamically interpolated #{value}, will NOT be HTML-escaped. (XSS risk!)
And this #{4 - 3} too.
)Multiline inline Crystal code (notice = vs. != vs. -):
.example-1
= (
now_1 = Time.utc
# The value will be HTML-escaped and inserted into the div:
now_1 + 1.hour
)
.example-2
!= (
now_2 = Time.utc
# The value NOT be HTML-escaped, and will be inserted RAW into the div: (XSS risk!)
now_2 + 1.hour
)
.example-3
- (
now_3 = Time.utc
# The value will not be inserted.
now_3 + 1.hour
)
= now_3 # but the value can be used later (will not have 1.hour added to it)In fruits.html.haml:
- fruits.each do |fruit|
!= Haml.render("#{__DIR__}/_fruit.html.haml")
In _fruit.html.haml:
.btn.mb-2{id: "fruit_#{fruit}"}
= fruit
Render it:
require "haml"
fruits = ["apple", "banana", "peach"]
puts Haml.render("#{__DIR__}/fruits.html.haml")Outputs:
<div class="btn mb-2" id="fruit_apple">
apple
</div>
<div class="btn mb-2" id="fruit_banana">
banana
</div>
<div class="btn mb-2" id="fruit_peach">
peach
</div>Note that __DIR__ in Haml.render("#{__DIR__}/fruits.html.haml") is used to tell the compiler that fruits.html.haml is in the same directory as this file. In your project, you can just specify paths from the build root, such as Haml.render("src/templates/fruits.html.haml").
(Advanced: to reduce String allocations, it is possible to avoid having the partial build its own intermediate String by instead passing it the same IO name as the parent, and using, for example, != Haml.embed("_fruit.html.haml", __haml_io) in place of != Haml.render(...).)
-
Add the dependency to your
shard.yml:dependencies: haml: github: compumike/haml.cr
-
Run
shards install -
Add
require "haml" -
Call
Haml.render("src/templates/my_template.html.haml")
See supported syntax for details and the Ruby Haml reference for comparison.
Some important differences:
| Feature | Difference |
|---|---|
| HTML escaping | = always HTML-escapes. != always emits raw HTML. (There is no Rails-style html_safe bypass.) |
| Compilation | Templates compile ahead of time. Runtime template evaluation is not supported. |
| Attributes | False data-* and aria-* values render as "false". |
| Object references | Object references such as %div[object] (which pull class and id from object) are unsupported. |
| HTML output | HTML5 only. Whitespace handling is simpler and may differ slightly from Ruby Haml. |
| Filters | Only :plain, :escaped, :preserve, :css, and :javascript are supported. CSS and JavaScript filters do not interpolate. |
- This is a hobby side project.
- Issues and PRs are unlikely to be accepted.
- If you find a reproducible bug, you may file a brief issue.
- compumike - creator and maintainer
This project was written almost entirely by AI coding agents, including an extensive test suite. Use at your own risk.