Solid
Solid is an implementation in Elixir of the template language Liquid. It uses nimble_parsec to generate the parser.
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:
def deps do
[{:solid, "~> 0.14"}]
end
Custom tags
To implement a new tag you need to create a new module that implements the Tag
behaviour:
defmodule MyCustomTag do
import NimbleParsec
@behaviour Solid.Tag
@impl true
def spec(_parser) do
space = Solid.Parser.Literal.whitespace(min: 0)
ignore(string("{%"))
|> ignore(space)
|> ignore(string("my_tag"))
|> ignore(space)
|> ignore(string("%}"))
end
@impl true
def render(tag, _context, _options) do
[text: "my first tag"]
end
end
spec
defines how to parse your tag;render
defines how to render your tag.
Now we need to add the tag to the parser
defmodule MyParser do
use Solid.Parser.Base, custom_tags: [MyCustomTag]
end
And finally pass the custom parser as an option:
"{% my_tag %}"
|> Solid.parse!(parser: MyParser)
|> Solid.render()
Custom filters
While calling Solid.render
one can pass a module with custom filters:
defmodule MyCustomFilters do
def add_one(x), do: x + 1
end
"{{ number | add_one }}"
|> Solid.parse!()
|> Solid.render(%{ "number" => 41}, custom_filters: MyCustomFilters)
|> IO.puts()
# 42
Extra options can be passed as last argument to custom filters if an extra argument is accepted:
defmodule MyCustomFilters do
def asset_url(path, opts) do
opts[:host] <> path
end
end
opts = [custom_filters: MyCustomFilters, host: "http://example.com"]
"{{ file_path | asset_url }}"
|> Solid.parse!()
|> Solid.render(%{ "file_path" => "/styles/app.css"}, opts)
|> IO.puts()
# http://example.com/styles/app.css
Strict rendering
Solid.render/3
doesn't raise or return errors unless strict_variables: true
or strict_filters: true
are passed as options.
If there are any missing variables/filters 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:
defmodule CachexCache do
@behaviour Solid.Caching
@impl true
def get(key) do
case Cachex.get(:your_cache_name, key) do
{_, nil} -> {:error, :not_found}
{:ok, value} -> {:ok, value}
{:error, error_msg} -> {:error, error_msg}
end
end
@impl true
def put(key, value) do
case Cachex.put(:my_cache, key, value) do
{:ok, true} -> :ok
{:error, error_msg} -> {:error, error_msg}
end
end
end
And then pass it as an option to render cache_module: CachexCache
.
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:
defmodule UserProfile do
defstruct [:full_name]
defimpl Solid.Matcher do
def match(user_profile, ["full_name"]), do: {:ok, user_profile.full_name}
end
end
defmodule User do
defstruct [:email]
def load_profile(%User{} = _user) do
# implementation omitted
%UserProfile{full_name: "John Doe"}
end
defimpl Solid.Matcher do
def match(user, ["email"]), do: {:ok, user.email}
def match(user, ["profile" | keys]), do: user |> User.load_profile() |> @protocol.match(keys)
end
end
template = ~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
Contributing
When adding new functionality or fixing bugs consider adding a new test case here inside test/cases
. These cases are tested against the Ruby gem so we can try to stay as close as possible to the original implementation.
TODO
- [x] Integration tests using Liquid gem to build fixtures; #3
- [x] All the standard filters #8
- [x] Support to custom filters #11
- [x] Tags (if, case, unless, etc)
- [x] Boolean operators #2
- [x] Whitespace control #10
Copyright and License
Copyright (c) 2016-2022 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.