Current section

Files

Jump to
live_view_native lib live_view_native renderer.ex
Raw

lib/live_view_native/renderer.ex

defmodule LiveViewNative.Renderer do
@moduledoc """
This module contains logic on how template rendering and embedding is implemented
"""
import LiveViewNative.Utils, only: [
get_interface: 1
]
@doc false
defmacro __before_compile__(%{module: module, file: file}) do
opts = Module.get_attribute(module, :native_opts)
pattern = template_filename(module, opts[:format])
file_path = Path.dirname(file)
# TODO: change to have co-location by default for 0.5.0
# {:ok, nil} -> file_path
# :error -> file_path
root = case Map.fetch(opts, :root) do
{:ok, nil} -> Path.join(file_path, Atom.to_string(opts[:format]))
{:ok, root} -> Path.join(file_path, root)
:error -> Path.join(file_path, Atom.to_string(opts[:format]))
end
quote do
embed_templates(unquote(pattern), root: unquote(root), name: unquote(opts[:as]))
end
end
@doc false
defmacro __inject_mix_recompile__(_env) do
quote location: :keep do
@template_file_hash @template_files |> Enum.sort() |> :erlang.md5()
@doc false
def __mix_recompile__? do
files =
@embeded_templates_opts
|> Enum.reduce([], fn({root, pattern, name}, templates_acc) ->
root
|> LiveViewNative.Renderer.find_templates(pattern, __MODULE__, name)
|> Enum.reduce(templates_acc, fn
{:no_embed, _reason}, templates_acc -> templates_acc
{:embed, templates}, templates_acc -> templates_acc ++ templates
end)
end)
file_hash =
files
|> Enum.sort()
|> :erlang.md5()
!(file_hash == @template_file_hash)
end
end
end
@doc false
defmacro delegate_to_target(name, opts \\ []) do
%{module: module} = env = __CALLER__
render? = Module.defines?(module, {name, 1})
suppress_render_warning? = opts[:supress_warning] || Application.get_env(:live_view_native, :suppress_render_warning, false)
if render? and !suppress_render_warning? do
IO.warn(
"#{module}.#{name}/1 is already defined. If you want to define your own and suppress this warning add:\n" <>
"`config :live_view_native, suppress_render_warning: true`\nto your application config.",
Macro.Env.stacktrace(env)
)
[]
else
quote do
@doc false
def unquote(name)(var!(assigns)) do
interface = get_interface(var!(assigns))
apply(__MODULE__, unquote(name), [var!(assigns), interface])
end
end
end
end
@doc """
Embeds templates as render functions
This function is mostly identical to `Phoenix.Component.embed_templates/2` but deviates in a few ways
## Options
* `:format` - the format from the client. This will be used for template discovery, for the format
`:swiftui` templates such as `home_live.swiftui.neex` will collected.
* `:name` - the name of the generated function. Defaults to the name of the template, for example
`app.swiftui.neex` will generate `app/2`. If this value is passed in there must only be a single template
name matched. So if the pattern results in `['home_live.swiftui.neex', 'user_list.swiftui.neex']` and
you pass `name: :render` there is ambiguity and will `raise`. The exception is when there are multiple
targets for the same name: `['home_live.swiftui+watchos.neex', 'home_live.swiftui+tvos.neex', 'home_live.swiftui.neex']`
if `name: :render` this will result:
def render(assings, %{"target" => "watchos"} = interface)
def render(assigns, %{"target" => "tvos"} = interface)
def render(assigns, interface)
the fallback function will always sort to render last. This allows for a greedy matcher if more than one target
will be used for the same template.
* `:root` - the location from which the `pattern` is applied to for finding templates. This is relative
to the rendering module.
"""
@doc type: :macro
defmacro embed_templates(pattern, opts \\ []) do
%{module: module, file: file} = env = __CALLER__
native_opts = Module.get_attribute(module, :native_opts)
format = native_opts[:format]
root = Keyword.get(opts, :root, Path.dirname(file))
name = opts[:name]
attr_ast = quote do
Module.put_attribute(__MODULE__, :embeded_templates_opts, {
unquote(root),
unquote(pattern),
unquote(name)
})
end
templates_ast =
root
|> find_templates(pattern, module, name)
|> Enum.map(&(__embed_templates__(&1,
format: format,
name: opts[:name],
env: env,
root: root,
pattern: pattern
)))
[attr_ast | templates_ast]
end
@doc false
def find_templates(root, pattern, module, default_name) do
root
|> Phoenix.Template.find_all(pattern)
|> Enum.chunk_by(&chunk_name(&1))
|> ensure_naming_uniq(pattern, default_name)
|> Enum.map(fn(templates) ->
name = build_name(templates, default_name)
render? = case Code.ensure_compiled(module) do
{:error, _} -> Module.defines?(module, {name, 2})
{:module, _} -> false
end
case {render?, templates} do
{true, [_template | _templates]} -> {:no_embed, :render_defined_with_templates}
{true, []} -> {:no_embed, :render_defined_no_templates}
{false, []} -> {:no_embed, :no_render_no_templates}
{false, templates} -> {:embed, templates}
end
end)
end
# this function ensures there is a single template group when applying a custom render function name
# if there is more than one grouping of templates and a custom render function name
# then the naming is ambiguous and we `raise`
defp ensure_naming_uniq([], _pattern, _name), do: []
defp ensure_naming_uniq([_template_group] = templates, _pattern, name) when not is_nil(name) and is_atom(name), do: templates
defp ensure_naming_uniq(templates, _pattern, nil), do: templates
defp ensure_naming_uniq(templates, pattern, name) when not is_nil(name) and is_atom(name) do
chunk_names =
templates
|> Enum.map(fn([template | _templates]) ->
"* #{chunk_name(template)}"
end)
|> Enum.join("\n")
raise ArgumentError,
"cannot apply custom render function name `#{inspect(name)} " <>
"when the following template groupings matched the pattern `#{inspect(pattern)}\n" <>
chunk_names
end
defp chunk_name(template) do
template
|> Path.basename()
|> String.split(".")
|> List.first()
end
defp __embed_templates__({:no_embed, reason}, opts) do
%{module: module} = env = opts[:env]
name = build_name([], opts[:name])
case reason do
:render_defined_with_templates ->
IO.warn(
"You have #{module}.#{name}/2 defined as well as at least one template file. You must remove " <>
" #{module}.#{name}/2 if you wish to use any template files.",
Macro.Env.stacktrace(env)
)
[]
:render_defined_no_templates -> []
:no_render_no_templates ->
IO.warn(
"You do not have any templates or any `render/2` functions defined for #{module}.",
Macro.Env.stacktrace(env)
)
[]
_unmatched_reason -> []
end
end
defp __embed_templates__({:embed, templates}, opts) do
%{module: module} = opts[:env]
format = opts[:format]
name = build_name(templates, opts[:name])
filename = template_filename(module, format)
templates
|> Enum.sort(&(String.length(&1) >= String.length(&2)))
|> Enum.map(fn(template) ->
engine = Map.fetch!(LiveViewNative.Template.engines(), format)
ast = engine.compile(template, filename)
case extract_target(template, format) do
nil ->
quote do
@file unquote(template)
@external_resource unquote(template)
@template_files unquote(template)
@doc false
def unquote(name)(var!(assigns), _interface) do
unquote(ast)
end
end
target ->
quote do
@file unquote(template)
@external_resource unquote(template)
@template_files unquote(template)
@doc false
def unquote(name)(var!(assigns), %{"target" => unquote(target)}) do
unquote(ast)
end
end
end
end)
|> List.insert_at(-1, quote do
delegate_to_target unquote(name)
end)
end
defp extract_target(template, format) do
Regex.scan(~r/#{format}\+(\w+)/, template)
|> case do
[[_, target] | _] -> target
_ -> nil
end
end
defp build_name([template | _tail], nil) do
template
|> chunk_name()
|> Macro.underscore()
|> String.to_atom()
end
defp build_name(_templates, name), do: name
defp template_filename(module, format) do
module
|> parent_module(format)
|> Kernel.<>(".#{format}*")
end
defp parent_module(module, format) do
case LiveViewNative.fetch_plugin(format) do
{:ok, plugin} ->
module_suffix =
plugin.module_suffix
|> Atom.to_string()
module
|> Module.split()
|> Enum.reject(&(&1 == module_suffix))
|> List.last()
|> Macro.underscore()
:error ->
IO.warn("missing LiveViewNative plugin for format #{inspect(format)}")
end
end
end