@@ -2,6 +2,7 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
22 @ moduledoc false
33
44 alias Phoenix.LiveView.TagEngine.Tokenizer
5+ alias Phoenix.LiveView.TagEngine.ParseResult
56 alias Phoenix.Component.MacroComponent
67
78 defguardp is_tag_open ( tag_type ) when tag_type not in [ :close , :eex ]
@@ -30,7 +31,8 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
3031 caller: caller ,
3132 skip_macro_components: skip_macro_components ,
3233 prune_text_after_slots: prune_text_after_slots ,
33- process_buffer: process_buffer
34+ process_buffer: process_buffer ,
35+ directives: [ ]
3436 } )
3537 catch
3638 { :syntax_error , line , column , message } ->
@@ -233,8 +235,8 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
233235 # ], %{line: 0, column: 0, opt: '='}}
234236 # ]
235237 # ```
236- defp to_tree ( [ ] , buffer , [ ] , _state ) do
237- { :ok , Enum . reverse ( buffer ) }
238+ defp to_tree ( [ ] , buffer , [ ] , state ) do
239+ { :ok , % ParseResult { nodes: Enum . reverse ( buffer ) , directives: state . directives } }
238240 end
239241
240242 defp to_tree (
@@ -269,7 +271,7 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
269271 when parent_type in [ :local_component , :remote_component ] do
270272 meta = meta |> Map . put ( :special , extract_special_attrs ( attrs ) ) |> maybe_macro_component ( attrs )
271273
272- { tokens , buffer } =
274+ { tokens , buffer , state } =
273275 maybe_process_macro_component (
274276 { :self_close , :slot , name , attrs , meta } ,
275277 tokens ,
@@ -331,7 +333,7 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
331333 when is_tag_open ( type ) do
332334 meta = meta |> Map . put ( :special , extract_special_attrs ( attrs ) ) |> maybe_macro_component ( attrs )
333335
334- { tokens , buffer } =
336+ { tokens , buffer , state } =
335337 maybe_process_macro_component ( { :self_close , type , name , attrs , meta } , tokens , buffer , state )
336338
337339 to_tree ( tokens , buffer , stack , state )
@@ -353,7 +355,7 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
353355 ) do
354356 block = Enum . reverse ( reversed_buffer )
355357
356- { tokens , buffer } =
358+ { tokens , buffer , state } =
357359 maybe_process_macro_component (
358360 { :block , type , name , attrs , block , open_meta , close_meta } ,
359361 tokens ,
@@ -522,9 +524,19 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
522524
523525 defp maybe_process_macro_component ( tree_node , tokens , buffer , state )
524526 when is_macro_component ( tree_node ) and not skip_macro_components ( state ) do
525- # Remove :type from attrs for the macro component AST
527+ caller = state . caller
528+
529+ if is_nil ( caller ) do
530+ raise ArgumentError , "macro components require a caller environment"
531+ end
532+
533+ Macro.Env . required? ( caller , Phoenix.Component ) ||
534+ raise ArgumentError ,
535+ "macro components are only supported in modules that `use Phoenix.Component`"
536+
526537 tree_node =
527538 case tree_node do
539+ # Remove :type from attrs for the macro component AST
528540 { :self_close , :tag , name , attrs , meta } ->
529541 { :self_close , :tag , name , List . keydelete ( attrs , ":type" , 0 ) , meta }
530542
@@ -545,54 +557,57 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
545557 )
546558 end
547559
548- case process_macro_component ( tree_node , state ) do
549- { :text , "" , _ } ->
550- { strip_text ( tokens ) , buffer }
560+ meta = get_meta ( tree_node )
561+ module_string = meta . macro_component
562+ module = validate_module! ( module_string , meta , state )
563+
564+ case process_macro_component ( tree_node , module , state ) do
565+ { _new_node , directives } when directives != [ ] and buffer != [ ] ->
566+ throw_syntax_error! (
567+ "macro component #{ inspect ( module ) } specified directives and therefore must appear at the very beginning of the template" ,
568+ get_meta ( tree_node )
569+ )
570+
571+ { { :text , "" , _ } , directives } ->
572+ { strip_text ( tokens ) , buffer , % { state | directives: directives } }
551573
552- new_node ->
553- { strip_text ( tokens ) , [ new_node | buffer ] }
574+ { new_node , directives } ->
575+ { strip_text ( tokens ) , [ new_node | buffer ] , % { state | directives: directives } }
554576 end
555577 end
556578
557- defp maybe_process_macro_component ( tree_node , tokens , buffer , _state ) do
558- { tokens , [ tree_node | buffer ] }
579+ defp maybe_process_macro_component ( tree_node , tokens , buffer , state ) do
580+ { tokens , [ tree_node | buffer ] , state }
559581 end
560582
561583 # Process a macro component: call transform and convert result back to tree nodes
562- defp process_macro_component ( tree_node , state ) do
584+ defp process_macro_component ( tree_node , module , state ) do
563585 # Macro components work by converting the tree nodes into a macro AST
564586 # (see Phoenix.Component.MacroComponent) and then calling the transform
565587 # function on the macro component module, which can return a transformed
566588 # AST.
567589 #
568590 # The AST is limited in functionality and we convert it back to the regular
569591 # node format afterwards.
570- caller = state . caller
571-
572- if is_nil ( caller ) do
573- raise ArgumentError , "macro components require a caller environment"
574- end
575-
576- Macro.Env . required? ( caller , Phoenix.Component ) ||
577- raise ArgumentError ,
578- "macro components are only supported in modules that `use Phoenix.Component`"
579-
580592 meta = get_meta ( tree_node )
581- module_string = meta . macro_component
582- module = validate_module! ( module_string , meta , state )
583593
584- # Build the macro component AST
585- case MacroComponent . build_ast ( tree_node , caller ) do
594+ case MacroComponent . build_ast ( tree_node , state . caller ) do
586595 { :ok , macro_ast } ->
587596 try do
588597 # Call the transform function
589- case module . transform ( macro_ast , % { env: caller } ) do
598+ case module . transform ( macro_ast , % { env: state . caller } ) do
590599 { :ok , new_ast } ->
591- MacroComponent . ast_to_tree ( new_ast , meta )
600+ { MacroComponent . ast_to_tree ( new_ast , meta ) , [ ] }
592601
593602 { :ok , new_ast , data } ->
594- Module . put_attribute ( caller . module , :__macro_components__ , { module , data } )
595- MacroComponent . ast_to_tree ( new_ast , meta )
603+ Module . put_attribute ( state . caller . module , :__macro_components__ , { module , data } )
604+ { MacroComponent . ast_to_tree ( new_ast , meta ) , [ ] }
605+
606+ { :ok , new_ast , data , directives } ->
607+ Module . put_attribute ( state . caller . module , :__macro_components__ , { module , data } )
608+
609+ { MacroComponent . ast_to_tree ( new_ast , meta ) ,
610+ validate_directives! ( module , directives , meta ) }
596611
597612 other ->
598613 throw_syntax_error! (
@@ -641,6 +656,60 @@ defmodule Phoenix.LiveView.TagEngine.Parser do
641656 module
642657 end
643658
659+ defp validate_directives! ( module , directives , meta ) do
660+ Enum . each ( directives , fn { key , value } ->
661+ validate_directive! ( module , key , value , meta )
662+ end )
663+
664+ directives
665+ end
666+
667+ defp validate_directive! ( _module , :root_tag_attribute , nil , _ ) , do: :ok
668+
669+ defp validate_directive! ( module , :root_tag_attribute , { name , value } , meta )
670+ when is_binary ( name ) and ( is_binary ( value ) or value == true ) do
671+ configured_attribute = Application . get_env ( :phoenix_live_view , :root_tag_attribute )
672+
673+ if ! configured_attribute do
674+ message = """
675+ a global :root_tag_attribute must be configured for macro components to use the :root_tag_attribute directive
676+
677+ Macro Component: #{ inspect ( module ) }
678+
679+ Expected global :root_tag_attribute to be a string, got: #{ inspect ( configured_attribute ) }
680+
681+ The global :root_tag_attribute is typically used for `Phoenix.LiveView.ColocatedCSS` and
682+ is usually configured to `"phx-r"`, but it needs to be explicitly enabled in your configuration:
683+
684+ config :phoenix_live_view, root_tag_attribute: "phx-r"
685+
686+ You can also use a different value than `"phx-r"`.
687+ """
688+
689+ throw_syntax_error! ( message , meta )
690+ else
691+ :ok
692+ end
693+ end
694+
695+ defp validate_directive! ( module , :root_tag_attribute , other , meta ) do
696+ throw_syntax_error! (
697+ """
698+ expected {name, value} for :root_tag_attribute directive from macro component #{ inspect ( module ) } , got: #{ inspect ( other ) }
699+
700+ name must be a compile-time string, and value must be a compile-time string or true
701+ """ ,
702+ meta
703+ )
704+ end
705+
706+ defp validate_directive! ( module , directive , value , meta ) do
707+ throw_syntax_error! (
708+ "unknown directive #{ inspect ( { directive , value } ) } provided by macro component #{ inspect ( module ) } " ,
709+ meta
710+ )
711+ end
712+
644713 defp throw_syntax_error! ( message , meta ) do
645714 throw ( { :syntax_error , meta . line , meta . column , message } )
646715 end
0 commit comments