Skip to content

pjlast/heml

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

82 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

heml

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:

asciicast

Table of Contents

Install

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))

Usage

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>

Syntax

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>|}

Using OCaml variables

You can use OCaml variables directly in your templates.

In HTML body

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>|}

In HTML tags

Variables are inserted into HTML tags using the {variable} syntax.

let button_with_class ~cls = {%heml|<button class={cls}>Click me!</button>|}

Using OCaml code

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; %>|}

Components and layouts

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>|}

Validation

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.

Editor support

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.

Neovim

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"))

How it works

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.

Known limitations

<script></script> tags don't support interpolation inside of the tag (yet).

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages