Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 107 additions & 65 deletions lib/phoenix_component/macro_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -151,90 +151,123 @@ defmodule Phoenix.Component.MacroComponent do
end

@doc false
def build_ast([{:tag, name, attrs, tag_meta} | rest], env) do
if closing = tag_meta[:closing] do
# we assert here because we don't expect any other values
true = closing in [:self, :void]
def build_ast(node, env) when is_tuple(node) do
case node do
{:self_close, :tag, name, attrs, meta} ->
closing_meta = Map.take(meta, [:closing])
{:ok, {name, attrs_to_ast(attrs, env), [], closing_meta}}

{:block, :tag, name, attrs, children, _meta, _close_meta} ->
children_ast = build_ast(children, env)
{:ok, {name, attrs_to_ast(attrs, env), children_ast, %{}}}
end

meta = Map.take(tag_meta, [:closing])
build_ast(rest, [], [{name, token_attrs_to_ast(attrs, env), meta}], env)
catch
{:ast_error, message, error_meta} ->
{:error, message, error_meta}
end

# recursive case: build_ast(tokens, acc, stack)
def build_ast(children, env) when is_list(children) do
Enum.map(children, fn
{:text, text, _meta} ->
text

# closing for final stack element -> done!
defp build_ast([{:close, :tag, _, _} | rest], acc, [{tag_name, attrs, meta}], _env) do
{:ok, {tag_name, attrs, Enum.reverse(acc), meta}, rest}
end
{:self_close, :tag, name, attrs, meta} ->
closing_meta = Map.take(meta, [:closing])
{name, attrs_to_ast(attrs, env), [], closing_meta}

# tag open (self closing or void)
defp build_ast([{:tag, name, attrs, %{closing: type}} | rest], acc, stack, env)
when type in [:self, :void] do
acc = [{name, token_attrs_to_ast(attrs, env), [], %{closing: type}} | acc]
build_ast(rest, acc, stack, env)
end
{:block, :tag, name, attrs, nested_children, _meta, _close_meta} ->
{name, attrs_to_ast(attrs, env), build_ast(nested_children, env), %{}}

# tag open
defp build_ast([{:tag, name, attrs, _tag_meta} | rest], acc, stack, env) do
build_ast(rest, [], [{name, token_attrs_to_ast(attrs, env), %{}, acc} | stack], env)
end
{:self_close, type, _name, _attrs, meta}
when type in [:local_component, :remote_component] ->
throw({:ast_error, "function components cannot be nested inside a macro component", meta})

# tag close
defp build_ast(
[{:close, :tag, name, _tag_meta} | tokens],
acc,
[{name, attrs, meta, prev_acc} | stack],
env
) do
build_ast(tokens, [{name, attrs, Enum.reverse(acc), meta} | prev_acc], stack, env)
end
{:block, type, _name, _attrs, _children, meta, _close_meta}
when type in [:local_component, :remote_component] ->
throw({:ast_error, "function components cannot be nested inside a macro component", meta})

# text
defp build_ast([{:text, text, _meta} | rest], acc, stack, env) do
build_ast(rest, [text | acc], stack, env)
end
{:self_close, :slot, _name, _attrs, meta} ->
throw({:ast_error, "slots cannot be nested inside a macro component", meta})

# unsupported token
defp build_ast([{type, _name, _attrs, meta} | _tokens], _acc, _stack, _env)
when type in [:local_component, :remote_component] do
{:error, "function components cannot be nested inside a macro component", meta}
end
{:block, :slot, _name, _attrs, _children, meta, _close_meta} ->
throw({:ast_error, "slots cannot be nested inside a macro component", meta})

defp build_ast([{:expr, _, _} | _], _acc, _stack, _env) do
# we raise here because we don't have a meta map (line + column) available
raise ArgumentError, "EEx is not currently supported in macro components"
end
{:body_expr, _expr, meta} ->
throw({:ast_error, "interpolation is not currently supported in macro components", meta})

defp build_ast([{:body_expr, _, meta} | _], _acc, _stack, _env) do
{:error, "interpolation is not currently supported in macro components", meta}
{:eex, _expr, meta} ->
throw({:ast_error, "EEx is not currently supported in macro components", meta})

{:eex_block, _expr, _blocks, meta} ->
throw({:ast_error, "EEx is not currently supported in macro components", meta})
end)
end

defp token_attrs_to_ast(attrs, env) do
Enum.map(attrs, fn {name, value, _meta} ->
defp attrs_to_ast(attrs, env) do
Enum.map(attrs, fn
# for now, we don't support root expressions (<div {@foo}>)
if name == :root do
{:root, value, attr_meta} ->
format_attr = fn
{:string, binary, _meta} -> binary
{:expr, code, _meta} -> code
nil -> "nil"
end

raise ArgumentError,
"dynamic attributes are not supported in macro components, got: #{format_attr.(value)}"
end
throw(
{:ast_error,
"dynamic attributes are not supported in macro components, got: #{format_attr.(value)}",
attr_meta}
)

case value do
{:string, binary, _meta} ->
{name, binary}
{name, {:string, binary, _meta}, _attr_meta} ->
{name, binary}

{:expr, code, meta} ->
ast = Code.string_to_quoted!(code, line: meta.line, column: meta.column, file: env.file)
{name, ast}
{name, {:expr, code, expr_meta}, _attr_meta} ->
ast =
Code.string_to_quoted!(code,
line: expr_meta.line,
column: expr_meta.column,
file: env.file
)

nil ->
{name, nil}
end
{name, ast}

{name, nil, _attr_meta} ->
{name, nil}
end)
end

@doc false
# Convert macro AST back to tree nodes (parser format)
# We keep reuse the original line + column metadata from the original tag
def ast_to_tree({tag, attrs, [], %{closing: _closing} = meta}, original_meta) do
tree_attrs = attrs_to_tree(attrs, original_meta)
{:self_close, :tag, tag, tree_attrs, Map.merge(original_meta, meta)}
end

def ast_to_tree({tag, attrs, children, _meta}, original_meta) do
tree_attrs = attrs_to_tree(attrs, original_meta)
tree_children = Enum.map(children, &ast_to_tree(&1, original_meta))
{:block, :tag, tag, tree_attrs, tree_children, original_meta, %{}}
end

def ast_to_tree(text, _original_meta) when is_binary(text) do
{:text, text, %{}}
end

defp attrs_to_tree(attrs, meta) do
Enum.map(attrs, fn
{name, nil} ->
{name, nil, meta}

{name, value} when is_binary(value) ->
delimiter = attr_quotes(name, value)
{name, {:string, value, Map.put(meta, :delimiter, delimiter)}, meta}

{name, ast} ->
# Convert quoted AST back to string for the tree node format
code = Macro.to_string(ast)
{name, {:expr, code, meta}, meta}
end)
end

Expand Down Expand Up @@ -307,14 +340,23 @@ defmodule Phoenix.Component.MacroComponent do
end)
end

@doc false
def encode_binary_attribute(key, value) when is_binary(key) and is_binary(value) do
defp encode_binary_attribute(key, value) when is_binary(key) and is_binary(value) do
case attr_quotes(key, value) do
?" ->
~s( #{key}="#{value}")

?' ->
~s( #{key}='#{value}')
end
end

defp attr_quotes(key, value) do
case {:binary.match(value, ~s["]), :binary.match(value, "'")} do
{:nomatch, _} ->
~s( #{key}="#{value}")
?"

{_, :nomatch} ->
~s( #{key}='#{value}')
?'

_ ->
raise ArgumentError, """
Expand Down
36 changes: 23 additions & 13 deletions lib/phoenix_live_view/html_algebra.ex
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,11 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
end
end

defp tag_block?({:tag_block, _, _, _, _}), do: true
defp tag_block?({:block, _, _, _, _, _, _}), do: true
defp tag_block?(_node), do: false

defp tag?({:tag_block, _, _, _, _}), do: true
defp tag?({:tag_self_close, _, _}), do: true
defp tag?({:block, _, _, _, _, _, _}), do: true
defp tag?({:self_close, _, _, _, _}), do: true
defp tag?(_node), do: false

defp text?({:text, _, _}), do: true
Expand All @@ -195,7 +195,7 @@ defmodule Phoenix.LiveView.HTMLAlgebra do

defp text_ends_with_space?(_node), do: false

defp block_preserve?({:tag_block, _, _, _, %{mode: :preserve}}), do: true
defp block_preserve?({:block, _, _, _, _, %{mode: :preserve}, _}), do: true
defp block_preserve?({:body_expr, _, _}), do: true
defp block_preserve?({:eex, _, _}), do: true
defp block_preserve?(_node), do: false
Expand All @@ -208,7 +208,11 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
{:block, group(nest(children, :reset))}
end

defp to_algebra({:tag_block, name, attrs, block, meta}, context) when name in @languages do
defp to_algebra(
{:block, _type, _name, attrs, block, %{tag_name: name} = meta, _close_meta},
context
)
when name in @languages do
children = block_to_algebra(block, %{context | mode: :preserve})

# Convert the whole block to text as there are no
Expand Down Expand Up @@ -253,7 +257,10 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
{:block, group}
end

defp to_algebra({:tag_block, name, attrs, block, meta}, %{mode: :preserve} = context) do
defp to_algebra(
{:block, _type, _name, attrs, block, %{tag_name: name} = meta, _close_meta},
%{mode: :preserve} = context
) do
children = block_to_algebra(block, context)

children =
Expand All @@ -270,11 +277,14 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
{:inline, tag}
end

defp to_algebra({:tag_block, _name, _attrs, _block, %{mode: :preserve}} = doc, context) do
defp to_algebra(
{:block, _type, _name, _attrs, _block, %{mode: :preserve}, _close_meta} = doc,
context
) do
to_algebra(doc, %{context | mode: :preserve})
end

defp to_algebra({:tag_block, name, attrs, block, _meta}, context) do
defp to_algebra({:block, _type, _name, attrs, block, %{tag_name: name}, _close_meta}, context) do
inline? = inline?(name, context)
{block, force_newline?} = trim_block_newlines(block, inline?)
inline? = inline? and not force_newline?
Expand Down Expand Up @@ -304,7 +314,7 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
end
end

defp to_algebra({:tag_self_close, name, attrs}, context) do
defp to_algebra({:self_close, _type, _name, attrs, %{tag_name: name}}, context) do
doc =
concat([
"<#{name}",
Expand All @@ -318,7 +328,7 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
# Handle EEX blocks within preserve tags
defp to_algebra({:eex_block, expr, block, meta}, %{mode: :preserve} = context) do
doc =
Enum.reduce(block, empty(), fn {block, expr}, doc ->
Enum.reduce(block, empty(), fn {block, expr, _clause_meta}, doc ->
children = block_to_algebra(block, context)
expr = "<% #{expr} %>"
concat([doc, children, expr])
Expand All @@ -330,7 +340,7 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
# Handle EEX blocks
defp to_algebra({:eex_block, expr, block, meta}, context) do
{doc, _stab} =
Enum.reduce(block, {empty(), false}, fn {block, expr}, {doc, stab?} ->
Enum.reduce(block, {empty(), false}, fn {block, expr, _clause_meta}, {doc, stab?} ->
{block, _force_newline?} = trim_block_newlines(block, false)
{next_doc, stab?} = eex_block_to_algebra(expr, block, stab?, context)
{concat(doc, force_unfit(next_doc)), stab?}
Expand Down Expand Up @@ -529,8 +539,8 @@ defmodule Phoenix.LiveView.HTMLAlgebra do

# Handle EEx clauses
#
# {[], "something ->"}
# {[{:tag_block, "p", [], [text: "do something"]}], "else"}
# {[], "something ->", %{...}}
# {[{:block, :tag, "p", [], [...], %{...}, %{...}}], "else", %{...}}
defp eex_block_to_algebra(expr, block, stab?, context) when is_list(block) do
indent = if stab?, do: 4, else: 2

Expand Down
15 changes: 8 additions & 7 deletions lib/phoenix_live_view/html_engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ defmodule Phoenix.LiveView.HTMLEngine do
trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true)
source = File.read!(path)

EEx.compile_string(source,
engine: Phoenix.LiveView.TagEngine,
line: 1,
options = [
engine: Phoenix.LiveView.Engine,
file: path,
trim: trim,
line: 1,
caller: __CALLER__,
source: source,
tag_handler: __MODULE__
)
tag_handler: __MODULE__,
trim: trim
]

Phoenix.LiveView.TagEngine.compile(source, options)
end

@behaviour Phoenix.LiveView.TagEngine
Expand Down
Loading
Loading