defmodule Solid do
@moduledoc """
Main module to interact with Solid
"""
alias Solid.{Object, Tag, Context}
@type errors :: %Solid.UndefinedVariableError{} | %Solid.UndefinedFilterError{}
defmodule Template do
@type rendered_data :: {:text, iodata()} | {:object, keyword()} | {:tag, list()}
@type t :: %__MODULE__{parsed_template: list(rendered_data())}
@enforce_keys [:parsed_template]
defstruct [:parsed_template]
end
defmodule TemplateError do
defexception [:message, :line, :reason, :header]
@impl true
def exception([reason, line, header]) do
%__MODULE__{
message: "Reason: #{reason}, line: #{elem(line, 0)}, header: #{header}",
reason: reason,
line: line,
header: header
}
end
end
defmodule RenderError do
defexception [:message, :errors, :result]
@impl true
def message(exception) do
"#{length(exception.errors)} error(s) found while rendering"
end
end
@doc """
It generates the compiled template
This function returns `{:ok, template}` if successfully parses the template, `{:error, template_error}` otherwise
# Options
* `parser` - a custom parser module can be passed. See `Solid.Tag` for more information
"""
@spec parse(String.t(), Keyword.t()) :: {:ok, %Template{}} | {:error, %TemplateError{}}
def parse(text, opts \\ []) do
parser = Keyword.get(opts, :parser, Solid.Parser)
case parser.parse(text) do
{:ok, result, _, _, _, _} ->
{:ok, %Template{parsed_template: result}}
{:error, reason, _, _, line, _} ->
{:error, TemplateError.exception([reason, line, String.slice(text, 0..20)])}
end
end
@doc """
It generates the compiled template
This function returns the compiled template or raises an error. Same options as `parse/2`
"""
@spec parse!(String.t(), Keyword.t()) :: Template.t() | no_return
def parse!(text, opts \\ []) do
case parse(text, opts) do
{:ok, template} -> template
{:error, template_error} -> raise template_error
end
end
@doc """
It renders the compiled template using a map with vars
See `render/3` for more details
It returns the rendered template or it raises an exception
with the accumulated errors and a partial result
"""
@spec render!(Solid.Template.t(), map, Keyword.t()) :: iolist
def render!(%Template{} = template, hash, options \\ []) do
case render(template, hash, options) do
{:ok, result} ->
result
{:error, errors, result} ->
raise RenderError, errors: errors, result: result
end
end
@doc """
It renders the compiled template using a map with vars
## Options
- `file_system`: a tuple of {FileSystemModule, options}. If this option is not specified, `Solid` uses `Solid.BlankFileSystem` which raises an error when the `render` tag is used. `Solid.LocalFileSystem` can be used or a custom module may be implemented. See `Solid.FileSystem` for more details.
- `custom_filters`: a module name where additional filters are defined. The base filters (thos from `Solid.Filter`) still can be used, however, custom filters always take precedence.
## Example
fs = Solid.LocalFileSystem.new("/path/to/template/dir/")
Solid.render(template, vars, [file_system: {Solid.LocalFileSystem, fs}])
"""
def render(template_or_text, values, options \\ [])
@spec render(%Template{}, map, Keyword.t()) :: {:ok, iolist} | {:error, list(errors), iolist}
@spec render(list, %Context{}, Keyword.t()) :: {iolist, %Context{}}
def render(%Template{parsed_template: parsed_template}, hash, options) do
context = %Context{counter_vars: hash}
{result, context} = render(parsed_template, context, options)
process_result(result, context)
catch
{exp, result, context} when exp in [:break_exp, :continue_exp] ->
process_result(result, context)
end
def render(text, context = %Context{}, options) do
{result, context} =
Enum.reduce(text, {[], context}, fn entry, {acc, context} ->
try do
{result, context} = do_render(entry, context, options)
{[result | acc], context}
catch
{:break_exp, result, context} ->
throw({:break_exp, Enum.reverse([result | acc]), context})
{:continue_exp, result, context} ->
throw({:continue_exp, Enum.reverse([result | acc]), context})
end
end)
{Enum.reverse(result), context}
end
defp process_result(result, context) do
if context.errors == [] do
{:ok, result}
else
# Errors are accumulated by prepending to the errors list
{:error, Enum.reverse(context.errors), result}
end
end
defp do_render({:text, string}, context, _options), do: {string, context}
defp do_render({:object, object}, context, options) do
{:ok, object_text, context} = Object.render(object, context, options)
{object_text, context}
end
defp do_render({:tag, tag}, context, options) do
render_tag(tag, context, options)
end
defp render_tag(tag, context, options) do
{result, context} = Tag.eval(tag, context, options)
if result do
render(result, context, options)
else
{"", context}
end
end
end