diff --git a/lib/surface.ex b/lib/surface.ex
index ea51e382..3983909d 100644
--- a/lib/surface.ex
+++ b/lib/surface.ex
@@ -109,6 +109,7 @@ defmodule Surface do
indentation = meta[:indentation] || 0
column = meta[:column] || 1
+ debug_annotations? = Module.get_attribute(__CALLER__.module, :__debug_annotations__)
component_type = Module.get_attribute(__CALLER__.module, :component_type)
string
@@ -120,7 +121,9 @@ defmodule Surface do
|> Surface.Compiler.to_live_struct(
debug: Enum.member?(opts, ?d),
file: __CALLER__.file,
- line: line
+ line: line,
+ caller: __CALLER__,
+ annotate_content: debug_annotations? && (&Phoenix.LiveView.HTMLEngine.annotate_tagged_content/1)
)
end
@@ -148,15 +151,12 @@ defmodule Surface do
if File.exists?(file) do
name = file |> Path.rootname() |> Path.basename()
- body =
- file
- |> File.read!()
- |> Surface.Compiler.compile(1, __CALLER__, file)
- |> Surface.Compiler.to_live_struct()
+ quote bind_quoted: [file: file, name: name] do
+ @external_resource file
+ @file file
+
+ body = Surface.__compile_sface__(name, file, __ENV__)
- quote do
- @external_resource unquote(file)
- @file unquote(file)
def unquote(String.to_atom(name))(var!(assigns)) do
_ = var!(assigns)
unquote(body)
@@ -172,6 +172,24 @@ defmodule Surface do
end
end
+ @doc false
+ def __compile_sface__(name, file, env) do
+ debug_annotations? =
+ Module.get_attribute(
+ env.module,
+ :__debug_annotations__,
+ Application.get_env(:phoenix_live_view, :debug_heex_annotations, false)
+ )
+
+ file
+ |> File.read!()
+ |> Surface.Compiler.compile(1, env, file)
+ |> Surface.Compiler.to_live_struct(
+ caller: %Macro.Env{env | file: file, line: 1, function: {String.to_atom(name), 1}},
+ annotate_content: debug_annotations? && (&Phoenix.LiveView.HTMLEngine.annotate_tagged_content/1)
+ )
+ end
+
@doc """
Converts the given code into Surface's AST.
diff --git a/lib/surface/compiler/eex_engine.ex b/lib/surface/compiler/eex_engine.ex
index 4b21ea8c..9afd2709 100644
--- a/lib/surface/compiler/eex_engine.ex
+++ b/lib/surface/compiler/eex_engine.ex
@@ -7,6 +7,7 @@ defmodule Surface.Compiler.EExEngine do
for information on this). Finally, it passes these tokens into the engine sequentially in the same
manner as EEx.Compiler.compile/2
"""
+ alias Surface.Compiler.Helpers
alias Surface.AST
alias Surface.IOHelper
alias Surface.Components.Context
@@ -27,10 +28,12 @@ defmodule Surface.Compiler.EExEngine do
engine: opts[:engine] || @default_engine,
depth: 0,
context_vars: %{count: 0, changed: []},
- scope: []
+ scope: [],
+ root_tag?: root_tag?(nodes)
}
nodes
+ |> maybe_annotate_content(opts[:annotate_content], opts[:caller])
|> to_token_sequence()
|> generate_buffer(state.engine.init(opts), state)
|> maybe_print_expression(
@@ -40,6 +43,31 @@ defmodule Surface.Compiler.EExEngine do
)
end
+ defp maybe_annotate_content(nodes, annotate_content, caller) do
+ if annotate_content do
+ {before_comment, after_comment} = annotate_content.(caller)
+ [%AST.Literal{value: before_comment}] ++ nodes ++ [%AST.Literal{value: after_comment}]
+ else
+ nodes
+ end
+ end
+
+ defp root_tag?(nodes) do
+ Enum.reduce_while(nodes, false, fn
+ %AST.Tag{}, false ->
+ {:cont, true}
+
+ %AST.Tag{}, true ->
+ {:halt, false}
+
+ %AST.Literal{value: value}, acc ->
+ if Helpers.blank?(value), do: {:cont, acc}, else: {:halt, false}
+
+ _node, _acc ->
+ {:halt, false}
+ end)
+ end
+
defp to_token_sequence(nodes) do
nodes
|> to_dynamic_nested_html()
@@ -48,7 +76,7 @@ defmodule Surface.Compiler.EExEngine do
end
defp generate_buffer([], buffer, state) do
- ast = state.engine.handle_body(buffer, root: true)
+ ast = state.engine.handle_body(buffer, root: state.root_tag?)
quote do
require Phoenix.LiveView.TagEngine
diff --git a/lib/surface/component.ex b/lib/surface/component.ex
index cb4f0344..2f5d561d 100644
--- a/lib/surface/component.ex
+++ b/lib/surface/component.ex
@@ -39,7 +39,7 @@ defmodule Surface.Component do
@before_compile Surface.Renderer
@before_compile unquote(__MODULE__)
- use Phoenix.Component
+ use Phoenix.Component, unquote(Keyword.drop(opts, [:slot]))
import Phoenix.Component, except: [slot: 1, slot: 2]
@behaviour unquote(__MODULE__)
diff --git a/lib/surface/components/form.ex b/lib/surface/components/form.ex
index e622b7f8..da75b7bf 100644
--- a/lib/surface/components/form.ex
+++ b/lib/surface/components/form.ex
@@ -99,6 +99,7 @@ defmodule Surface.Components.Form do
assigns = assign(assigns, opts: opts)
~F"""
+
<.form :let={form} for={@for} action={@action} {...@opts}>
<#slot {@default, form: form} context_put={__MODULE__, form: form}/>
diff --git a/lib/surface/components/form/error_tag.ex b/lib/surface/components/form/error_tag.ex
index 762dca71..08ae3fae 100644
--- a/lib/surface/components/form/error_tag.ex
+++ b/lib/surface/components/form/error_tag.ex
@@ -103,18 +103,21 @@ defmodule Surface.Components.Form.ErrorTag do
prop feedback_for, :string
def render(assigns) do
- translate_error = assigns.translator || translator_from_config() || (&translate_error/1)
- class = assigns.class || get_config(:default_class)
+ assigns = assign(assigns, :class, assigns.class || get_config(:default_class))
~F"""
{translate_error.(error)}
+ >{translator(assigns.translator).(error)}
"""
end
+ defp translator(translator) do
+ translator || translator_from_config() || (&translate_error/1)
+ end
+
@doc """
Translates an error message.
diff --git a/lib/surface/renderer.ex b/lib/surface/renderer.ex
index 1cd48646..e3f86841 100644
--- a/lib/surface/renderer.ex
+++ b/lib/surface/renderer.ex
@@ -10,12 +10,20 @@ defmodule Surface.Renderer do
template_ast =
if File.exists?(template) do
- env = Map.put(env, :function, {:render, 1})
+ env =
+ env
+ |> Map.put(:function, {:render, 1})
+ |> Map.put(:file, template)
+
+ debug_annotations? = Module.get_attribute(__CALLER__.module, :__debug_annotations__)
template
|> File.read!()
|> Surface.Compiler.compile(1, env, template)
- |> Surface.Compiler.to_live_struct()
+ |> Surface.Compiler.to_live_struct(
+ caller: env,
+ annotate_content: debug_annotations? && (&Phoenix.LiveView.HTMLEngine.annotate_tagged_content/1)
+ )
else
nil
end
diff --git a/lib/surface/type_handler.ex b/lib/surface/type_handler.ex
index e0a598d6..fdd7f9dd 100644
--- a/lib/surface/type_handler.ex
+++ b/lib/surface/type_handler.ex
@@ -177,7 +177,12 @@ defmodule Surface.TypeHandler do
{:ok, [~S( ), to_string(name)]}
{:ok, val} ->
- {:ok, Phoenix.HTML.attributes_escape([{name, val}])}
+ attr_value =
+ [{name, val}]
+ |> Phoenix.HTML.attributes_escape()
+ |> Phoenix.HTML.safe_to_string()
+
+ {:ok, attr_value}
{:error, message} ->
{:error, message}
diff --git a/test/mix/tasks/compile/surface/validate_components_test.exs b/test/mix/tasks/compile/surface/validate_components_test.exs
index 05c2d9c4..e44287c7 100644
--- a/test/mix/tasks/compile/surface/validate_components_test.exs
+++ b/test/mix/tasks/compile/surface/validate_components_test.exs
@@ -222,11 +222,18 @@ defmodule Mix.Tasks.Compile.Surface.ValidateComponentsTest do
use Surface.Component
prop list, :list, required: true
+ data item, :any
+ data rest, :list
def render(%{list: [item | rest]} = assigns) do
+ assigns =
+ assigns
+ |> assign(:item, item)
+ |> assign(:rest, rest)
+
~F"""
- {item}
-