Warning
This library is still early in development and things may still change quite a bit.
heml
(HTML + Embedded ML) is a ppx extension for easy-to-use HTML templating directly in OCaml.
It's a direct conversion of Phoenix's HEEx templates.
Here's a quick demo of what it looks like in Neovim:
This package is not yet available on opam, so to use it you're going to have to manually pin and install it:
opam pin ppx_heml.dev git+https://github.com/pjlast/heml.git
opam install ppx_heml
After which you can use it in your project by adding the following to your dune
file:
(preprocess
(pps ppx_heml))
A template is written using the {%heml|... |%}
syntax, which will return a string.
Here's a quick demo program:
input |
output |
---|---|
(* bin/layouts.ml *)
let layout ~title contents =
{%heml|<!DOCTYPE html>
<head>
<title><%s= title %></title>
</head>
<body>
<%raw= contents %>
</body>
|} (* bin/main.ml *)
type user =
{ name: string
; age: int }
let user_list ~users =
{%heml|<ul id="list">
<%= List.iter (fun user -> %>
<li id={string_of_int user.age}>
<%s= user.name %> is <%i= user.age %> years old.
</li>
<%= ) users; %>
</ul>|}
let () =
let users = [{name = "John"; age = 22}
;{name = "Jane"; age = 23}]
in
let my_class = "title" in
let page =
{%heml|<Layouts.layout title="Users">
<h1 class={my_class}>
Users
</h1>
<.user_list users={users} />
</Layouts.layout>|}
in
print_endline page |
<!DOCTYPE html>
<head>
<title>Users</title>
</head>
<body>
<h1 class="title">
Users
</h1>
<ul id="list">
<li id="22">
John is 22 years old.
</li>
<li id="23">
Jane is 23 years old.
</li>
</ul>
</body> |
For the most part you can just write regular HTML.
However, for void elements, such as <img ...>
or <br>
that don't have a closing tag, you need to self-close the element using />
. They will be rendered as <img ...>
and <br>
.
{%heml|<!DOCTYPE html>
<html lang="en">
<head>
<title>My webpage</title>
</head>
<body>
<h1>Hello!</h1>
<br />
<p>Welcome to my web page!</p>
<img src="https://example.com/image.png" />
</body>
</html>|}
You can use OCaml variables directly in your templates.
Variables are inserted into the HTML body using the <%s= string_variable %>
and <%i= int_variable %>
tags.
let custom_button ~text = {%heml|<button><%s= text %></button>|}
Variables are inserted into HTML tags using the {variable}
syntax.
let button_with_class ~cls = {%heml|<button class={cls}>Click me!</button>|}
OCaml code can be used throughout templates by using the <%= (* code *) %>
tags. Code can be started in one tag and ended in another.
This is useful for iteration and conditionals.
let render_users_if_true ~users ~should_render =
{%heml|<%= if should_render then (%>
<%= List.iter (fun user -> %>
<%s= user.name %> is <%i= user.age %> years old.
<%= ) users; %>
<%= ); %>
You'll notice that all OCaml code should be unit statements and terminated with a semicolon.
If, for whatever reason, you want to render something withing a code block, you can use the write
function:
let render_text ~text = {%heml|<%= write text; %>|}
It's also possible to use your own components or layouts directly in the template.
A component from another module can be used directly:
{%heml|<Components.button text="Click me!" />|}
But if you're using a component from the same module, you must prefix it with a .
(otherwise we can't distinguish between normal HTML and components).
let button ~text = {%heml|<button><%s= text %></button>|}
let () = print_endline {%heml|<.button text="Click me!" />|}
Components can also contain HTML, in which case you need to render the contents using the <%raw= %>
tag.
The <%raw= %>
tag will render the contents as-is, without doing any HTML escaping, and assumes that the contents are safe.
The contents are passed as the final unlabelled argument of the function.
let custom_div ~cls contents =
{%heml|<div class={cls}>
<%raw= contents %>
</div>|}
Similarly, you can use the <%raw= %>
to create something like a layout:
(* layouts.ml *)
let base_layout ~title contents = {%heml|<!DOCTYPE html>
<html lang="en">
<head>
<title><%s= title %></title>
<!-- other meta tags -->
</head>
<body>
<%raw= contents %>
</body>
</html>|}
(* main.ml *)
let () = print_endline {%heml|<Layouts.base_layout title="My webpage">
<h1>Hello!</h1>
<p>Welcome to my web page!</p>
</Layouts.base_layout>|}
heml
does basic HTML validation. It won't allow you to have mismatched start and end tags, or to have unclosed tags. It also makes sure attributes and such are formatted correctly.
It does NOT do any kind of HTML element validation. I've tried to hit the sweet spot between being strict and being helpful.
So heml
will ensure that your HTML is well-formed, but it won't check that your HTML is semantically correct.
heml
leverages the OCaml LSP for feedback directly in your editor. No special LSP or plugin is required other than the standard OCaml LSP.
Since heml
is basically HEEx, you can use the HEEx treesitter grammar for syntax highlighting.
To get nice highlighting in Neovim, add a queries/ocaml/injections.scm
file to your .config/nvim
with the following contents:
((quoted_extension
(attribute_id) @_attid
(quoted_string_content) @injection.content)
(#contains? @_attid "heml")
(#set! injection.language "heex"))
ppx_heml
parses the templates into a sort-of abstract syntax tree (AST). This AST is then transformed into a series of Buffer writes, and ends with the Buffer contents being returned. I went with this approach because the arbitrary OCaml code blocks need to be compiled. This templates still need to be parsed into a valid AST, but they're immediately converted into a statement that outputs a string, as opposed to converting the AST to a string at runtime.
<script></script>
tags don't support interpolation inside of the tag (yet).