177 lines
5.5 KiB
Elixir
177 lines
5.5 KiB
Elixir
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 returns the rendered template or it raises an exception
|
|
with the accumulated errors and a partial result
|
|
|
|
See `render/3` for more details
|
|
"""
|
|
@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.
|
|
|
|
- `strict_variables`: if `true`, it collects an error when a variable is referenced in the template, but not given in the map
|
|
|
|
- `strict_filters`: if `true`, it collects an error when a filter is referenced in the template, but not built-in or provided via `custom_filters`
|
|
|
|
- `matcher_module`: a module to replace `Solid.Matcher` when resolving variables.
|
|
|
|
## 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
|
|
matcher_module = Keyword.get(options, :matcher_module, Solid.Matcher)
|
|
context = %Context{counter_vars: hash, matcher_module: matcher_module}
|
|
|
|
{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
|