[ANN] Liquid ML - A templating language used by Shopify, Github Pages and more!

Hello everyone, I am excited to announce the release of my first OPAM package liquid_ml. Shopify’s Liquid Templating language for OCaml. Check it out here: https://github.com/benfaerber/liquid-ml

Learn Liquid syntax here: https://shopify.github.io/liquid/

The Liquid templating language is used all over including Shopify and Github pages. The Rust port of the language is very popular for static site and documentation generation. If anyone has any suggestions or wants to help improve the project I welcome pull requests!

19 Likes

Really appreciate you releasing this. Excellent!

One thing I noticed in the README was the use of Obj.t below:

type value =
| Bool of bool
| String of string
| Number of float
| Var of string list
| List of value list
| Date of Date.t
| Object of liquid_object
| Nil
and liquid_object = value Obj.t (* <--- HERE *)

I associate Obj.t with “unsafe”, and it took me a while of digging to see that liquid_ml shadows the built-in module Obj with its own module Obj = Stdlib.Map.Make(LiquidObject).

If it makes sense to you, could the module Obj be renamed Object? That would make it symmetric with | Date of Date.t and it would help people like me who pay extra scrutiny to code that uses Obj. I would send a PR myself, but I won’t be able to get to it for a few weeks, and this is one of those API things that is better changed early rather than late.

3 Likes

Sounds good! I wasn’t aware of that module. I just submitted a pull request for version 0.1.2 of the library which fixes this.

3 Likes

Thanks for this awesome contribution. I’m curious: what would it take or look like to allow for custom tags? I haven’t seen it done in too many templating libraries, but I’m interested in how one might render partials/subsections of a template. I’m thinking (but again, I don’t really know) that if Liquid ML allowed for custom tags, one could extend render/render_text to extract the desired inline.

This desire stems from a way to create a fuller template without breaking it up into too many included/rendered sub-templates. But then being able to target a smaller part of that. The only other prior art that I’m aware of is something in Django called django-template-partials. This pattern has proven itself handy with HTMX.

Interesting! I basically only ported all the features Shopify uses that are not directly coupled to Shopify features. I definitely would consider adding it as I’ve heard HTMX is gaining a fair bit of popularity in the ecosystem.

How I would go about doing it is similar to the filters (aka functions). How they work is it searches the standard library for a valid filter, if it doesn’t find one it searches the user provided custom filters, if it doesn’t find one it throws a syntax error. Basically you would need to add the same logic for keywords, search for standard tag, then search for user provided tag. Right now I don’t expose many things publicly in the API but am planning to add lots more customization next release. The lexer, parser, standard library are all seperate packages so someone could always modify one of those and then stitch them back together. Also this feature would require a bit of tweaking in the lexer / parser because it matches against a syntax tree that only allows certain types with behavior tied to each of them.

When the file was rendered, the parser would basically lex everything, find the specific part you want to use, render it and then discard the rest.

If you want to use the library right now, a work around for this feature is using render file, params, render with params / aliases or render file for collection as item and a switch case. Here’s an example:

# link.liquid
{% capture color_class &}
  {% case color %}
    {% when "red" %} text-red-100
    {% when "green" %} text-green-100
    {% else %} text-green-100
  {% endcase %}
{% endcapture %}

<a href="{{ href }}" class="{{ color_class }}">{{ text }}</a>

# header.liquid
{% render 'link', text: "Hello", href: "https://discuss.ocaml.org", color: "green" %}
{% render 'link', text: "Goodbye", href: "https://discuss.ocaml.org", color: "red" %}

Imagine writing large but related parts of your UI in the switch case. You can include the variable you want to switch on in the global variable context as seen in the readme. Then you could change that variable based on route / request params or something like that.

If you interested in working on these sorts of things I would definitely accept a PR, if not I will consider it for the next release.

1 Like