This repository has been archived on 2023-08-07. You can view files and clone it, but cannot push or open issues or pull requests.
temple/lib/temple/renderer.ex

372 lines
9.5 KiB
Elixir
Raw Normal View History

defmodule Temple.Renderer do
@moduledoc false
alias Temple.Ast.ElementList
alias Temple.Ast.Text
alias Temple.Ast.Components
alias Temple.Ast.Slot
alias Temple.Ast.Slottable
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.VoidElementsAliases
alias Temple.Ast.AnonymousFunctions
alias Temple.Ast.RightArrow
alias Temple.Ast.DoExpressions
alias Temple.Ast.Match
alias Temple.Ast.Default
alias Temple.Ast.Empty
alias Temple.Ast.Utils
Dynamic Attributes (#190) * Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
2023-01-21 11:44:29 +00:00
@engine Application.compile_env(:temple, :engine, Phoenix.HTML.Engine)
@doc false
def engine(), do: @engine
Dynamic Attributes (#190) * Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
2023-01-21 11:44:29 +00:00
defmacro compile(do: block) do
block
|> Temple.Parser.parse()
Dynamic Attributes (#190) * Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
2023-01-21 11:44:29 +00:00
|> Temple.Renderer.render(engine: @engine)
# |> Temple.Ast.Utils.inspect_ast()
end
def render(asts, opts \\ [])
def render(asts, opts) when is_list(asts) and is_list(opts) do
Dynamic Attributes (#190) * Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
2023-01-21 11:44:29 +00:00
engine = Keyword.get(opts, :engine, Phoenix.HTML.Engine)
state = %{
engine: engine,
indentation: 0,
terminal_node: false
}
buffer = engine.init([])
buffer =
for ast <- asts, reduce: buffer do
buffer ->
render(buffer, state, ast)
end
if function_exported?(engine, :handle_body, 2) do
engine.handle_body(buffer, root: length(asts) == 1)
else
engine.handle_body(buffer)
end
end
def render(buffer, state, %Text{text: text}) do
t = Utils.indent(state.indentation) <> text <> new_line(state)
unless t == "" do
state.engine.handle_text(
buffer,
[],
t
)
end
end
def render(buffer, state, asts) when is_list(asts) do
for ast <- asts, reduce: buffer do
buffer ->
render(buffer, state, ast)
end
end
def render(buffer, state, %Components{
function: function,
arguments: arguments,
slots: slots
}) do
slot_quotes =
Enum.group_by(slots, & &1.name, fn %Slottable{} = slot ->
slot_buffer = state.engine.handle_begin(buffer)
slot_buffer =
for child <- children(slot.content), reduce: slot_buffer do
slot_buffer ->
render(slot_buffer, state, child)
end
ast = state.engine.handle_end(slot_buffer)
inner_block =
quote do
inner_block unquote(slot.name) do
unquote(slot.parameter || quote(do: _)) ->
unquote(ast)
end
end
Dynamic Attributes (#190) * Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
2023-01-21 11:44:29 +00:00
{rest, attributes} = Keyword.pop(slot.attributes, :rest!, [])
slot =
{:%{}, [],
[
__slot__: slot.name,
inner_block: inner_block
] ++ attributes}
quote do
Map.merge(unquote(slot), Map.new(unquote(rest)))
end
end)
Dynamic Attributes (#190) * Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
2023-01-21 11:44:29 +00:00
{rest, arguments} = Keyword.pop(arguments, :rest!, [])
component_arguments =
{:%{}, [],
arguments
|> Map.new()
|> Map.merge(slot_quotes)
|> Enum.to_list()}
Dynamic Attributes (#190) * Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
2023-01-21 11:44:29 +00:00
component_arguments =
quote do
Map.merge(unquote(component_arguments), Map.new(unquote(rest)))
end
expr =
quote do
component(
unquote(function),
unquote(component_arguments),
{__MODULE__, __ENV__.function, __ENV__.file, __ENV__.line}
)
end
state.engine.handle_expr(buffer, "=", expr)
end
def render(buffer, state, %Slot{} = ast) do
render_slot_func =
quote do
Dynamic Attributes (#190) * Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
2023-01-21 11:44:29 +00:00
{rest, args} = Map.pop(Map.new(unquote(ast.args)), :rest!, [])
args = Map.merge(args, Map.new(rest))
render_slot(unquote(ast.name), args)
end
state.engine.handle_expr(buffer, "=", render_slot_func)
end
def render(buffer, state, %ElementList{} = ast) do
render(buffer, state, ast.children)
end
def render(buffer, state, %NonvoidElementsAliases{} = ast) do
current_indent = Utils.indent(state.indentation)
inside_new_lines = if ast.meta.whitespace == :tight, do: "", else: "\n"
new_indent = if ast.meta.whitespace == :tight, do: nil, else: state.indentation + 1
buffer =
state.engine.handle_text(
buffer,
[],
"#{current_indent}<#{ast.name}"
)
buffer =
for node <- Utils.compile_attrs(ast.attrs), reduce: buffer do
buffer ->
case node do
{:text, text} ->
state.engine.handle_text(buffer, [], text)
{:expr, expr} ->
state.engine.handle_expr(buffer, "=", expr)
end
end
buffer =
state.engine.handle_text(
buffer,
[],
">#{inside_new_lines}"
)
buffer =
if Enum.any?(children(ast.children)) do
for {child, index} <- Enum.with_index(children(ast.children), 1), reduce: buffer do
buffer ->
render(
buffer,
%{
state
| indentation: new_indent,
terminal_node: index == length(children(ast.children))
},
child
)
end
else
buffer
end
state.engine.handle_text(
buffer,
[],
"#{inside_new_lines}#{Utils.indent(if(ast.meta.whitespace == :loose, do: state.indentation, else: nil))}</#{ast.name}>#{new_line(state)}\n"
)
end
def render(buffer, state, %VoidElementsAliases{} = ast) do
current_indent = Utils.indent(state.indentation)
buffer =
state.engine.handle_text(
buffer,
[],
"#{current_indent}<#{ast.name}"
)
buffer =
for node <- Utils.compile_attrs(ast.attrs), reduce: buffer do
buffer ->
case node do
{:text, text} ->
state.engine.handle_text(buffer, [], text)
{:expr, expr} ->
state.engine.handle_expr(buffer, "=", expr)
end
end
state.engine.handle_text(buffer, [], ">\n")
end
def render(buffer, state, %AnonymousFunctions{} = ast) do
new_buffer = state.engine.handle_begin(buffer)
new_buffer =
for child <- children(ast.children), child != nil, reduce: new_buffer do
new_buffer ->
render(new_buffer, state, child)
end
new_buffer = state.engine.handle_text(new_buffer, [], "\n")
inner_quoted = state.engine.handle_end(new_buffer)
{name, meta, args} = ast.elixir_ast
{args, {func, fmeta, [{arrow, arrowmeta, [first, _block]}]}, args2} =
Temple.Ast.Utils.split_on_fn(args, {[], nil, []})
full_ast =
{name, meta, args ++ [{func, fmeta, [{arrow, arrowmeta, [first, inner_quoted]}]}] ++ args2}
state.engine.handle_expr(buffer, "=", full_ast)
end
def render(buffer, state, %RightArrow{elixir_ast: elixir_ast} = ast) do
new_buffer = state.engine.handle_begin(buffer)
new_buffer =
for child <- children(ast.children), child != nil, reduce: new_buffer do
new_buffer ->
render(new_buffer, state, child)
end
inner_quoted = state.engine.handle_end(new_buffer)
{func, meta, [first]} = elixir_ast
full_ast = {func, meta, [first | [inner_quoted]]}
state.engine.handle_expr(buffer, "", full_ast)
end
def render(buffer, state, %DoExpressions{} = ast) do
{func, meta, args} = ast.elixir_ast
new_buffer = state.engine.handle_begin(buffer)
[do_block, else_block] = ast.children
do_inner_quoted =
case do_block do
[%RightArrow{} | _] = bodies ->
for b <- bodies do
new_buffer = state.engine.handle_begin(buffer)
new_buffer = render(new_buffer, state, b)
{:__block__, _, [quoted | _]} = state.engine.handle_end(new_buffer)
quoted
end
block ->
for child <- children(block), child != nil, reduce: new_buffer do
new_buffer ->
render(new_buffer, state, child)
end
|> state.engine.handle_end()
end
else_inner_quoted =
if else_block do
case else_block do
[%RightArrow{} | _] = bodies ->
for b <- bodies do
new_buffer = state.engine.handle_begin(buffer)
new_buffer = render(new_buffer, state, b)
{:__block__, _, [quoted | _]} = state.engine.handle_end(new_buffer)
quoted
end
block ->
for child <- children(block), child != nil, reduce: new_buffer do
new_buffer ->
render(new_buffer, state, child)
end
|> state.engine.handle_end()
end
end
new_args =
then([do: do_inner_quoted], fn args ->
if else_inner_quoted do
Keyword.put(args, :else, else_inner_quoted) |> Enum.reverse()
else
args
end
end)
full_ast = {func, meta, args ++ [new_args]}
state.engine.handle_expr(buffer, "=", full_ast)
end
def render(buffer, state, %Match{elixir_ast: elixir_ast}) do
state.engine.handle_expr(buffer, "", elixir_ast)
end
def render(buffer, state, %Default{elixir_ast: elixir_ast}) do
buffer =
if state.indentation && state.indentation > 0 do
state.engine.handle_text(buffer, [], Utils.indent(state.indentation))
else
buffer
end
buffer = state.engine.handle_expr(buffer, "=", elixir_ast)
if not state.terminal_node do
state.engine.handle_text(buffer, [], "\n")
else
buffer
end
end
def render(buffer, _state, %Empty{}), do: buffer
defp children(%ElementList{children: children}), do: children
defp children(list) when is_list(list), do: list
def new_line(%{terminal_node: false}), do: "\n"
def new_line(%{terminal_node: true}), do: ""
end