Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion lib/phoenix_component/macro_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,29 @@ defmodule Phoenix.Component.MacroComponent do
# LiveView's end to end tests: a macro component that performs
# [syntax highlighting at compile time](https://github.com/phoenixframework/phoenix_live_view/blob/38851d943f3280c5982d75679291dccb8c442534/test/e2e/support/colocated_live.ex#L4-L35)
# using the [Makeup](https://hexdocs.pm/makeup/Makeup.html) library.
#
# ## Directives
#
# Macro components may return directives from `transform/2` which can be used to influence
# other elements in the template outside of the macro component at compile-time. For example:
#
# ```elixir
# defmodule MyAppWeb.TagRootSampleComponent do
# @behaviour Phoenix.Component.MacroComponent
#
# @impl true
# def transform(_ast, _meta) do
# {:ok, "", %{}, [root_tag_attribute: {"phx-sample-one", "test"}, root_tag_attribute: {"phx-sample-two", true}]}
# end
# end
# ```
#
# The following directives are currently supported:
#
# * `:root_tag_attribute` - A `{name, value}` tuple to apply as an attribute to all root tags during template compilation.
# Requires that a global `:root_tag_attribute` is configured for the application. The attribute name must be a string and the attribute value must be a string or `true`.
# May be provided multiple times to apply multiple attributes.
#

@type tag :: binary()
@type attribute :: {binary(), Macro.t()}
Expand All @@ -128,9 +151,13 @@ defmodule Phoenix.Component.MacroComponent do
@type tag_meta :: %{closing: :self | :void}
@type heex_ast :: {tag(), attributes(), children(), tag_meta()} | binary()
@type transform_meta :: %{env: Macro.Env.t()}
@type directive :: {:root_tag_attribute, {name :: String.t(), value :: String.t() | true}}
@type directives :: [directive]

@callback transform(heex_ast :: heex_ast(), meta :: transform_meta()) ::
{:ok, heex_ast()} | {:ok, heex_ast(), data :: term()}
{:ok, heex_ast()}
| {:ok, heex_ast(), data :: term()}
| {:ok, heex_ast(), data :: term(), directives :: directives()}

@doc """
Returns the stored data from macro components that returned `{:ok, ast, data}`.
Expand Down
4 changes: 2 additions & 2 deletions lib/phoenix_live_view/html_formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ defmodule Phoenix.LiveView.HTMLFormatter do
process_buffer: &process_buffer/1
)
|> case do
{:ok, nodes} ->
nodes
{:ok, result} ->
result.nodes
|> transform_tree(source, newlines)
|> HTMLAlgebra.build(opts)
|> Inspect.Algebra.format(line_length)
Expand Down
58 changes: 46 additions & 12 deletions lib/phoenix_live_view/tag_engine/compiler.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Phoenix.LiveView.TagEngine.Compiler do
@moduledoc false

alias Phoenix.LiveView.TagEngine.ParseResult
alias Phoenix.LiveView.TagEngine.Tokenizer.ParseError

@doc """
Expand All @@ -15,7 +16,7 @@ defmodule Phoenix.LiveView.TagEngine.Compiler do
text and expression parts and properly invoking the engine
with the correct code for features like components and slots.
"""
def compile(nodes, opts) do
def compile(%ParseResult{nodes: nodes, directives: directives}, opts) do
{engine, opts} = Keyword.pop(opts, :engine, Phoenix.LiveView.Engine)
tag_handler = Keyword.fetch!(opts, :tag_handler)

Expand All @@ -26,8 +27,11 @@ defmodule Phoenix.LiveView.TagEngine.Compiler do
caller: Keyword.fetch!(opts, :caller),
source: Keyword.fetch!(opts, :source),
tag_handler: tag_handler,
# slots is the only key that is updated when traversing nodes
slots: []
root_tag_attribute: Application.get_env(:phoenix_live_view, :root_tag_attribute),
root_tag_attributes: Keyword.get_values(directives, :root_tag_attribute),
# The following keys are updated when traversing nodes
slots: [],
local_root?: true
}

# Live components require a single, static root tag.
Expand Down Expand Up @@ -328,7 +332,7 @@ defmodule Phoenix.LiveView.TagEngine.Compiler do

with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->
substate = handle_tag_and_attrs(name, attrs, ">", to_location(meta), substate, state)
{_child_state, substate} = handle_node(children, substate, state)
{_child_state, substate} = handle_node(children, substate, %{state | local_root?: false})
substate = state.engine.handle_text(substate, [to_location(close_meta)], "</#{name}>")
{state, substate}
end)
Expand All @@ -353,7 +357,7 @@ defmodule Phoenix.LiveView.TagEngine.Compiler do
meta,
close_meta,
substate,
state
%{state | local_root?: true}
)

ast =
Expand Down Expand Up @@ -383,7 +387,10 @@ defmodule Phoenix.LiveView.TagEngine.Compiler do

with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->
{assigns, attr_info, slot_info} =
build_component_assigns(ref, attrs, children, meta, close_meta, substate, state)
build_component_assigns(ref, attrs, children, meta, close_meta, substate, %{
state
| local_root?: true
})

store_component_call({mod, fun}, attr_info, slot_info, line, state)
call_meta = [line: line, column: column]
Expand Down Expand Up @@ -420,7 +427,10 @@ defmodule Phoenix.LiveView.TagEngine.Compiler do
with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->
# Process children in a new nesting
{assigns, attr_info, slot_info} =
build_component_assigns(ref, attrs, children, meta, close_meta, substate, state)
build_component_assigns(ref, attrs, children, meta, close_meta, substate, %{
state
| local_root?: true
})

store_component_call({mod, fun}, attr_info, slot_info, line, state)
call_meta = [line: line, column: column + mod_size]
Expand Down Expand Up @@ -479,17 +489,41 @@ defmodule Phoenix.LiveView.TagEngine.Compiler do

defp handle_tag_and_attrs(name, attrs, suffix, meta, substate, state) do
text =
if debug_attributes?(state.caller) do
"<#{name} data-phx-loc=\"#{meta[:line]}\""
else
"<#{name}"
end
"<#{name}"
|> maybe_add_phx_loc(state, meta)
|> maybe_add_root_tag_attributes(state, meta)

substate = state.engine.handle_text(substate, meta, text)
substate = handle_tag_attrs(meta, attrs, substate, state)
state.engine.handle_text(substate, meta, suffix)
end

defp maybe_add_phx_loc(text, %{caller: caller}, meta) do
if debug_attributes?(caller) do
"#{text} data-phx-loc=\"#{meta[:line]}\""
else
text
end
end

defp maybe_add_root_tag_attributes(text, %{local_root?: true} = state, _meta) do
case state do
%{root_tag_attribute: root_tag_attribute} when is_binary(root_tag_attribute) ->
attrs =
[{root_tag_attribute, true} | state.root_tag_attributes]
|> Phoenix.HTML.attributes_escape()
|> Phoenix.HTML.safe_to_string()

# Phoenix.HTML.attributes_escape/1 adds a leading space automatically
"#{text}#{attrs}"

%{root_tag_attribute: _} ->
text
end
end

defp maybe_add_root_tag_attributes(text, _state, _meta), do: text

defp handle_tag_attrs(meta, attrs, substate, state) do
Enum.reduce(attrs, substate, fn
{:root, {:expr, _, _} = expr, _attr_meta}, substate ->
Expand Down
Loading
Loading