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