Skip to content

Commit 8375f4f

Browse files
committed
Add Phoenix.LiveView.TagCompiler.Compiler
This commit introduces a compiler for the new unified tree structure introduced in the previous commit. It changes the TagEngine to use the new compiler and also moves macro component handling to the parser. For backwards compatibility, the TagEngine still implements the EEx.Engine behaviour, but it is basically a no-op (it will still parse Elixir expressions twice!) and calls the new compiler at the end. The benefit of this is that we can now have the full tokenized template and handle macro components before compiling inner parts of the template. This allows us to more easily implement directives as explored by #4114 #4116
1 parent 1d473db commit 8375f4f

8 files changed

Lines changed: 1734 additions & 1559 deletions

File tree

lib/phoenix_component/macro_component.ex

Lines changed: 110 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -151,90 +151,138 @@ defmodule Phoenix.Component.MacroComponent do
151151
end
152152

153153
@doc false
154-
def build_ast([{:tag, name, attrs, tag_meta} | rest], env) do
155-
if closing = tag_meta[:closing] do
156-
# we assert here because we don't expect any other values
157-
true = closing in [:self, :void]
154+
def build_ast(node, env) when is_tuple(node) do
155+
case node do
156+
{:self_close, :tag, name, attrs, meta} ->
157+
closing_meta = Map.take(meta, [:closing])
158+
{:ok, {name, attrs_to_ast(attrs, env), [], closing_meta}}
159+
160+
{:block, :tag, name, attrs, children, _meta} ->
161+
children_ast = build_ast(children, env)
162+
{:ok, {name, attrs_to_ast(attrs, env), children_ast, %{}}}
158163
end
159-
160-
meta = Map.take(tag_meta, [:closing])
161-
build_ast(rest, [], [{name, token_attrs_to_ast(attrs, env), meta}], env)
164+
catch
165+
{:ast_error, message, error_meta} ->
166+
{:error, message, error_meta}
162167
end
163168

164-
# recursive case: build_ast(tokens, acc, stack)
169+
def build_ast(children, env) when is_list(children) do
170+
Enum.map(children, fn
171+
{:text, text, _meta} ->
172+
text
165173

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

171-
# tag open (self closing or void)
172-
defp build_ast([{:tag, name, attrs, %{closing: type}} | rest], acc, stack, env)
173-
when type in [:self, :void] do
174-
acc = [{name, token_attrs_to_ast(attrs, env), [], %{closing: type}} | acc]
175-
build_ast(rest, acc, stack, env)
176-
end
178+
{:block, :tag, name, attrs, nested_children, _meta} ->
179+
{name, attrs_to_ast(attrs, env), build_ast(nested_children, env), %{}}
177180

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

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

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

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

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

209-
defp build_ast([{:body_expr, _, meta} | _], _acc, _stack, _env) do
210-
{:error, "interpolation is not currently supported in macro components", meta}
201+
{:eex_block, _expr, _blocks, meta} ->
202+
throw({:ast_error, "EEx is not currently supported in macro components", meta})
203+
end)
211204
end
212205

213-
defp token_attrs_to_ast(attrs, env) do
214-
Enum.map(attrs, fn {name, value, _meta} ->
215-
# for now, we don't support root expressions (<div {@foo}>)
216-
if name == :root do
206+
defp attrs_to_ast(attrs, env) do
207+
Enum.map(attrs, fn
208+
{:root, value, attr_meta} ->
217209
format_attr = fn
218210
{:string, binary, _meta} -> binary
219211
{:expr, code, _meta} -> code
220212
nil -> "nil"
221213
end
222214

223-
raise ArgumentError,
224-
"dynamic attributes are not supported in macro components, got: #{format_attr.(value)}"
225-
end
215+
throw(
216+
{:ast_error,
217+
"dynamic attributes are not supported in macro components, got: #{format_attr.(value)}",
218+
attr_meta}
219+
)
226220

227-
case value do
228-
{:string, binary, _meta} ->
229-
{name, binary}
221+
{name, {:string, binary, _meta}, _attr_meta} ->
222+
{name, binary}
230223

231-
{:expr, code, meta} ->
232-
ast = Code.string_to_quoted!(code, line: meta.line, column: meta.column, file: env.file)
233-
{name, ast}
224+
{name, {:expr, code, expr_meta}, _attr_meta} ->
225+
ast =
226+
Code.string_to_quoted!(code,
227+
line: expr_meta.line,
228+
column: expr_meta.column,
229+
file: env.file
230+
)
234231

235-
nil ->
236-
{name, nil}
237-
end
232+
{name, ast}
233+
234+
{name, nil, _attr_meta} ->
235+
{name, nil}
236+
end)
237+
end
238+
239+
@doc false
240+
# Convert macro AST back to tree nodes (parser format)
241+
def ast_to_tree({tag, attrs, [], %{closing: _closing} = meta}, parent_meta) do
242+
tree_attrs = attrs_to_tree(attrs, parent_meta)
243+
{:self_close, :tag, tag, tree_attrs, Map.merge(parent_meta, meta)}
244+
end
245+
246+
def ast_to_tree({tag, attrs, children, _meta}, parent_meta) do
247+
tree_attrs = attrs_to_tree(attrs, parent_meta)
248+
tree_children = Enum.map(children, &ast_to_tree(&1, parent_meta))
249+
{:block, :tag, tag, tree_attrs, tree_children, parent_meta}
250+
end
251+
252+
def ast_to_tree(text, parent_meta) when is_binary(text) do
253+
{:text, text, parent_meta}
254+
end
255+
256+
defp attrs_to_tree(attrs, meta) do
257+
Enum.map(attrs, fn
258+
{name, nil} ->
259+
{name, nil, meta}
260+
261+
{name, value} when is_binary(value) ->
262+
# Determine delimiter based on content (same logic as encode_binary_attribute)
263+
delimiter =
264+
case {:binary.match(value, ~s["]), :binary.match(value, "'")} do
265+
{:nomatch, _} ->
266+
?"
267+
268+
{_, :nomatch} ->
269+
?'
270+
271+
_ ->
272+
raise ArgumentError, """
273+
invalid attribute value for "#{name}".
274+
Attribute values must not contain single and double quotes at the same time.
275+
276+
You need to escape your attribute before using it in the MacroComponent AST. You can use `Phoenix.HTML.attributes_escape/1` to do so.
277+
"""
278+
end
279+
280+
{name, {:string, value, Map.put(meta, :delimiter, delimiter)}, meta}
281+
282+
{name, ast} ->
283+
# Convert quoted AST back to string for the tree node format
284+
code = Macro.to_string(ast)
285+
{name, {:expr, code, meta}, meta}
238286
end)
239287
end
240288

lib/phoenix_live_view/html_engine.ex

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ defmodule Phoenix.LiveView.HTMLEngine do
2222
trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true)
2323
source = File.read!(path)
2424

25-
EEx.compile_string(source,
26-
engine: Phoenix.LiveView.TagEngine,
27-
line: 1,
25+
options = [
26+
engine: Phoenix.LiveView.Engine,
2827
file: path,
29-
trim: trim,
28+
line: 1,
3029
caller: __CALLER__,
31-
source: source,
32-
tag_handler: __MODULE__
33-
)
30+
tag_handler: __MODULE__,
31+
trim: trim
32+
]
33+
34+
Phoenix.LiveView.TagEngine.compile(source, options)
3435
end
3536

3637
@behaviour Phoenix.LiveView.TagEngine

lib/phoenix_live_view/tag_compiler.ex

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)