Skip to content

Commit 6acdad0

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 6acdad0

7 files changed

Lines changed: 1732 additions & 1528 deletions

File tree

lib/phoenix_component/macro_component.ex

Lines changed: 115 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -151,90 +151,143 @@ 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]
158-
end
154+
# Build macro AST from a tree node
155+
# For self-closing: {:self_close, :tag, name, attrs, meta}
156+
# For block: {:block, :tag, name, attrs, children, meta}
157+
def build_ast({:self_close, :tag, name, attrs, meta}, env) do
158+
closing_meta = Map.take(meta, [:closing])
159+
{:ok, {name, do_attrs_to_macro_ast(attrs, env), [], closing_meta}}
160+
catch
161+
{:ast_error, message, error_meta} ->
162+
{:error, message, error_meta}
163+
end
159164

160-
meta = Map.take(tag_meta, [:closing])
161-
build_ast(rest, [], [{name, token_attrs_to_ast(attrs, env), meta}], env)
165+
def build_ast({:block, :tag, name, attrs, children, _meta}, env) do
166+
children_ast = do_build_children_ast(children, env)
167+
{:ok, {name, do_attrs_to_macro_ast(attrs, env), children_ast, %{}}}
168+
catch
169+
{:ast_error, message, error_meta} ->
170+
{:error, message, error_meta}
162171
end
163172

164-
# recursive case: build_ast(tokens, acc, stack)
173+
defp do_build_children_ast(children, env) do
174+
Enum.map(children, fn
175+
{:text, text, _meta} ->
176+
text
165177

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
178+
{:self_close, :tag, name, attrs, meta} ->
179+
closing_meta = Map.take(meta, [:closing])
180+
{name, do_attrs_to_macro_ast(attrs, env), [], closing_meta}
170181

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
182+
{:block, :tag, name, attrs, nested_children, _meta} ->
183+
{name, do_attrs_to_macro_ast(attrs, env), do_build_children_ast(nested_children, env),
184+
%{}}
177185

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
186+
{:self_close, type, _name, _attrs, meta}
187+
when type in [:local_component, :remote_component] ->
188+
throw({:ast_error, "function components cannot be nested inside a macro component", meta})
182189

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
190+
{:block, type, _name, _attrs, _children, meta}
191+
when type in [:local_component, :remote_component] ->
192+
throw({:ast_error, "function components cannot be nested inside a macro component", meta})
192193

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

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
197+
{:block, :slot, _name, _attrs, _children, meta} ->
198+
throw({:ast_error, "slots cannot be nested inside a macro component", meta})
203199

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
200+
{:body_expr, _expr, meta} ->
201+
throw({:ast_error, "interpolation is not currently supported in macro components", meta})
208202

209-
defp build_ast([{:body_expr, _, meta} | _], _acc, _stack, _env) do
210-
{:error, "interpolation is not currently supported in macro components", meta}
203+
{:eex, _expr, meta} ->
204+
throw({:ast_error, "EEx is not currently supported in macro components", meta})
205+
206+
{:eex_block, _expr, _blocks, meta} ->
207+
throw({:ast_error, "EEx is not currently supported in macro components", meta})
208+
end)
211209
end
212210

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
211+
defp do_attrs_to_macro_ast(attrs, env) do
212+
Enum.map(attrs, fn
213+
{:root, value, attr_meta} ->
217214
format_attr = fn
218215
{:string, binary, _meta} -> binary
219216
{:expr, code, _meta} -> code
220217
nil -> "nil"
221218
end
222219

223-
raise ArgumentError,
224-
"dynamic attributes are not supported in macro components, got: #{format_attr.(value)}"
225-
end
220+
throw(
221+
{:ast_error,
222+
"dynamic attributes are not supported in macro components, got: #{format_attr.(value)}",
223+
attr_meta}
224+
)
226225

227-
case value do
228-
{:string, binary, _meta} ->
229-
{name, binary}
226+
{name, {:string, binary, _meta}, _attr_meta} ->
227+
{name, binary}
230228

231-
{:expr, code, meta} ->
232-
ast = Code.string_to_quoted!(code, line: meta.line, column: meta.column, file: env.file)
233-
{name, ast}
229+
{name, {:expr, code, expr_meta}, _attr_meta} ->
230+
ast =
231+
Code.string_to_quoted!(code,
232+
line: expr_meta.line,
233+
column: expr_meta.column,
234+
file: env.file
235+
)
234236

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

lib/phoenix_live_view/html_engine.ex

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ 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__,
3130
source: source,
32-
tag_handler: __MODULE__
33-
)
31+
tag_handler: __MODULE__,
32+
trim: trim
33+
]
34+
35+
Phoenix.LiveView.TagCompiler.compile(source, options)
3436
end
3537

3638
@behaviour Phoenix.LiveView.TagEngine

0 commit comments

Comments
 (0)