Skip to content

Commit 0d3ba3f

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 0d3ba3f

7 files changed

Lines changed: 1728 additions & 1528 deletions

File tree

lib/phoenix_component/macro_component.ex

Lines changed: 111 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -151,90 +151,139 @@ 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),
180+
%{}}
177181

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

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

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

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

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
196+
{:body_expr, _expr, meta} ->
197+
throw({:ast_error, "interpolation is not currently supported in macro components", meta})
198+
199+
{:eex, _expr, meta} ->
200+
throw({:ast_error, "EEx is not currently supported in macro components", meta})
208201

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

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
207+
defp attrs_to_ast(attrs, env) do
208+
Enum.map(attrs, fn
209+
{:root, value, attr_meta} ->
217210
format_attr = fn
218211
{:string, binary, _meta} -> binary
219212
{:expr, code, _meta} -> code
220213
nil -> "nil"
221214
end
222215

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

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

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

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

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)