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