Skip to content

Repository files navigation

Solid

Module VersionHex DocsTotal DownloadLicenseLast Updated

Solid is an implementation in Elixir of the Liquid template language with strict parsing.

Basic Usage

iex>template="My name is {{ user.name }}"iex>{:ok,template}=Solid.parse(template)iex>Solid.render!(template,%{"user"=>%{"name"=>"José"}})|>to_string"My name is José"

Installation

The package can be installed with:

defdepsdo[{:solid,"~> 1.0"}]end

Custom tags

To implement a new tag you need to create a new module that implements the Tag behaviour. It must implement a parse/3 function that returns a struct that implements Solid.Renderable. Here is a simple example:

defmoduleCurrentYeardo@enforce_keys[:loc]defstruct[:loc]@behaviourSolid.Tag@impltruedefparse("get_current_year",loc,context)dowith{:ok,[{:end,_}],context}<-Solid.Lexer.tokenize_tag_end(context)do{:ok,%__MODULE__{loc: loc},context}endenddefimplSolid.Renderabledodefrender(_tag,context,_options)do{[to_string(Date.utc_today().year)],context}endendend

Now to use it simply pass a :tags option to Solid.parse/2 including your custom tag:

tags=Map.put(Solid.Tag.default_tags(),"get_current_year",CurrentYear)Solid.parse!("{{ get_current_year }}",tags: tags)

One can also pass a subset of the default tags if a more restricted environment is necessary:

# No comment tags allowedtags=Map.delete(Solid.Tag.default_tags(),"comment")Solid.parse!("{% comment %} {% endcomment %}",tags: tags)

An error will be presented as comment is not part of the allowed tags:

**(Solid.TemplateError) Unexpected tag'comment'1: {%comment%}{%endcomment%}^Unexpectedtag'endcomment'1: {%comment%}{%endcomment%}^(solid1.0.0-rc1) lib/solid.ex:51: Solid.parse!/2iex:2: (file)

Custom filters

While calling Solid.render one can pass a module with custom filters:

defmoduleMyCustomFiltersdodefadd_one(x),do: x+1end"{{ number | add_one }}"|>Solid.parse!()|>Solid.render!(%{"number"=>41},custom_filters: MyCustomFilters)|>IO.puts()# 42

Alternatively, you can pass a function to the custom_filters option. This allows your filters to access predefined state. An example use-case could be to allow the filters to render strings in the user's default locale (or to override it by argument).

user_locale="en""{{ number | format_number }}"|>Solid.parse!()|>Solid.render!(%{"number"=>41},custom_filters: fn"format_number",[num]->{:ok,Cldr.Number.to_string(num,locale: user_locale)}"format_number",[num,locale]->{:ok,Cldr.Number.to_string(num,locale: locale)}_,_->:errorend)|>IO.puts()

The callback must return either {:ok, value} or :error.

Strict rendering

If there are any missing variables/filters and strict_variables: true or strict_filters: true are passed as options Solid.render/3 returns {:error, errors, result} where errors is the list of collected errors and result is the rendered template.

Solid.render!/3 raises if strict_variables: true is passed and there are missing variables. Solid.render!/3 raises if strict_filters: true is passed and there are missing filters.

Caching

In order to cache render-ed templates, you can write your own cache adapter. It should implement behaviour Solid.Caching. By default it uses Solid.Caching.NoCache trivial adapter.

If you want to use for example Cachex for that such implemention would look like:

defmoduleCachexCachedo@behaviourSolid.Caching@impltruedefget(key)docaseCachex.get(:your_cache_name,key)do{_,nil}->{:error,:not_found}{:ok,value}->{:ok,value}{:error,error_msg}->{:error,error_msg}endend@impltruedefput(key,value)docaseCachex.put(:my_cache,key,value)do{:ok,true}->:ok{:error,error_msg}->{:error,error_msg}endendend

And then pass it as an option to render cache_module: CachexCache. Now while using {% render 'etc' %} this custom cache will be used

Using structs in context

In order to pass structs to context you need to implement protocol Solid.Matcher for that. That protocol consist of one function def match(data, keys). First argument is struct being provided and second is list of string, which are keys passed after . to the struct.

For example:

defmoduleUserProfiledodefstruct[:full_name]defimplSolid.Matcherdodefmatch(user_profile,["full_name"]),do: {:ok,user_profile.full_name}endenddefmoduleUserdodefstruct[:email]defload_profile(%User{}=_user)do# implementation omitted%UserProfile{full_name: "John Doe"}enddefimplSolid.Matcherdodefmatch(user,["email"]),do: {:ok,user.email}defmatch(user,["profile"|keys]),do: user|>User.load_profile()|>@protocol.match(keys)endendtemplate=~s({{ user.email}}: {{ user.profile.full_name }})context=%{"user"=>%User{email: "test@example.com"}}template|>Solid.parse!()|>Solid.render!(context)|>to_string()# => test@example.com: John Doe

If the Solid.Matcher protocol is not enough one can provide a module like this:

defmoduleMyMatcherdodefmatch(_data,_keys),do: {:ok,42}end# ...Solid.render!(template,%{"number"=>4},matcher_module: MyMatcher)

Sigil Support

Solid provides a ~LIQUID sigil for validating and compiling templates at compile time:

importSolid.Sigil# Validates syntax at compile timetemplate=~LIQUID"""Hello, {{ name }}!"""# Use the compiled templateSolid.render!(template,%{"name"=>"World"})

The sigil will raise helpful CompileError messages with line numbers and context when templates contain syntax errors. Experimental VSCode syntax highlighting is available with the Liquid Sigil extension.

Contributing

When adding new functionality or fixing bugs consider adding a new test case here inside test/solid/integration/scenarios. These scenarios are tested against the Ruby gem so we can try to stay as close as possible to the original implementation.

Copyright and License

Copyright (c) 2016-2025 Eduardo Gurgel Pinho

This work is free. You can redistribute it and/or modify it under the terms of the MIT License. See the LICENSE.md file for more details.

About

Liquid template engine in Elixir

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages