@@ -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
0 commit comments