252 lines
7.2 KiB
Markdown
252 lines
7.2 KiB
Markdown
# Solid
|
|
|
|
[](https://github.com/edgurgel/solid/actions?query=workflow%3ACI)
|
|
[](https://hex.pm/packages/solid)
|
|
[](https://hexdocs.pm/solid/)
|
|
[](https://hex.pm/packages/solid)
|
|
[](https://github.com/edgurgel/solid/blob/master/LICENSE.md)
|
|
[](https://github.com/edgurgel/solid/commits/master)
|
|
|
|
Solid is an implementation in Elixir of the template language [Liquid](https://shopify.github.io/liquid/). It uses [nimble_parsec](https://github.com/plataformatec/nimble_parsec) to generate the parser.
|
|
|
|
## Basic Usage
|
|
|
|
```elixir
|
|
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é"
|
|
```
|
|
|
|
## Sigil Support
|
|
|
|
Solid provides a `~LIQUID` sigil for validating and compiling templates at compile time:
|
|
|
|
```elixir
|
|
import Solid.Sigil
|
|
|
|
# Validates syntax at compile time
|
|
template = ~LIQUID"""
|
|
Hello, {{ name }}!
|
|
"""
|
|
|
|
# Use the compiled template
|
|
Solid.render!(template, %{"name" => "World"})
|
|
```
|
|
|
|
The sigil will raise helpful CompileError messages with line numbers and context when templates contain syntax errors.
|
|
|
|
## Installation
|
|
|
|
The package can be installed with:
|
|
|
|
```elixir
|
|
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:
|
|
|
|
```elixir
|
|
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
|
|
|
|
```elixir
|
|
defmodule MyParser do
|
|
use Solid.Parser.Base, custom_tags: [MyCustomTag]
|
|
end
|
|
```
|
|
|
|
And finally pass the custom parser as an option:
|
|
|
|
```elixir
|
|
"{% my_tag %}"
|
|
|> Solid.parse!(parser: MyParser)
|
|
|> Solid.render()
|
|
```
|
|
|
|
## Custom filters
|
|
|
|
While calling `Solid.render` one can pass a module with custom filters:
|
|
|
|
```elixir
|
|
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:
|
|
|
|
```elixir
|
|
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](https://github.com/whitfin/cachex) for that such implemention would look like:
|
|
|
|
```elixir
|
|
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:
|
|
|
|
```elixir
|
|
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
|
|
```
|
|
|
|
If the `Solid.Matcher` protocol is not enough one can provide their own module like this:
|
|
|
|
```elixir
|
|
defmodule MyMatcher do
|
|
def match(data, keys), do: {:ok, 42}
|
|
end
|
|
|
|
# ...
|
|
Solid.render(template, %{"number" => 4}, matcher_module: MyMatcher)
|
|
```
|
|
|
|
## 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](https://github.com/edgurgel/solid/pull/3)
|
|
- [x] All the standard filters [#8](https://github.com/edgurgel/solid/issues/8)
|
|
- [x] Support to custom filters [#11](https://github.com/edgurgel/solid/issues/11)
|
|
- [x] Tags (if, case, unless, etc)
|
|
- [x] `for`
|
|
- [x] `else`
|
|
- [x] `break`
|
|
- [x] `continue`
|
|
- [x] `limit`
|
|
- [x] `offset`
|
|
- [x] Range (3..5)
|
|
- [x] `reversed`
|
|
- [x] `forloop` object
|
|
- [x] `raw` [#18](https://github.com/edgurgel/solid/issues/18)
|
|
- [x] `cycle` [#17](https://github.com/edgurgel/solid/issues/17)
|
|
- [x] `capture` [#19](https://github.com/edgurgel/solid/issues/19)
|
|
- [x] `increment` [#16](https://github.com/edgurgel/solid/issues/16)
|
|
- [x] `decrement` [#16](https://github.com/edgurgel/solid/issues/16)
|
|
- [x] Boolean operators [#2](https://github.com/edgurgel/solid/pull/2)
|
|
- [x] Whitespace control [#10](https://github.com/edgurgel/solid/issues/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](./LICENSE.md) file for more details.
|