Current section

Files

Jump to
phoenix_kit_publishing lib phoenix_kit_publishing page_builder renderer.ex
Raw

lib/phoenix_kit_publishing/page_builder/renderer.ex

defmodule PhoenixKit.Modules.Publishing.PageBuilder.Renderer do
@moduledoc """
Renders AST nodes to HTML by delegating to component modules.
"""
@doc """
Renders an AST node to HTML.
"""
def render(ast, assigns) when is_map(ast) do
case resolve_component(ast.type) do
{:ok, component_module} ->
render_component(component_module, ast, assigns)
{:error, :not_found} ->
# Fallback for unknown components
render_unknown(ast, assigns)
end
end
def render(ast, _assigns) when is_list(ast) do
{:ok,
Phoenix.HTML.raw(
Enum.map_join(ast, fn node ->
case render(node, %{}) do
{:ok, html} -> Phoenix.HTML.safe_to_string(html)
{:error, _} -> ""
end
end)
)}
end
def render(content, _assigns) when is_binary(content) do
{:ok, Phoenix.HTML.raw(content)}
end
# Resolve component type to module
defp resolve_component(:page), do: {:ok, PhoenixKit.Modules.Shared.Components.Page}
defp resolve_component(:hero), do: {:ok, PhoenixKit.Modules.Shared.Components.Hero}
defp resolve_component(:headline), do: {:ok, PhoenixKit.Modules.Shared.Components.Headline}
defp resolve_component(:subheadline),
do: {:ok, PhoenixKit.Modules.Shared.Components.Subheadline}
defp resolve_component(:cta), do: {:ok, PhoenixKit.Modules.Shared.Components.CTA}
defp resolve_component(:image), do: {:ok, PhoenixKit.Modules.Shared.Components.Image}
defp resolve_component(:video), do: {:ok, PhoenixKit.Modules.Shared.Components.Video}
defp resolve_component(:entityform),
do: {:ok, PhoenixKitEntities.Components.EntityForm}
defp resolve_component(_), do: {:error, :not_found}
# Render using the component module
defp render_component(component_module, ast, assigns) do
component_assigns = build_component_assigns(ast, assigns)
try do
html = component_module.render(component_assigns)
{:ok, html}
rescue
e ->
{:error, {:render_error, e}}
end
end
# Build assigns map for component
defp build_component_assigns(ast, parent_assigns) do
base_assigns = %{
__changed__: nil,
variant: Map.get(ast.attributes, "variant", "default"),
attributes: ast.attributes,
content: ast[:content],
children: ast[:children] || []
}
Map.merge(parent_assigns, base_assigns)
end
# Fallback renderer for unknown components. Builds the wrapper `<div>` as
# a safe iolist so the literal class attribute never gets interpolated
# from data — only the AST-derived `content` (admin-trusted) is raw'd.
defp render_unknown(ast, assigns) do
content =
cond do
ast[:content] ->
ast.content
ast[:children] ->
Enum.map_join(ast.children, &render_child_to_string(&1, assigns))
true ->
""
end
{:ok,
Phoenix.HTML.raw([
~s(<div class="unknown-component">),
content,
"</div>"
])}
end
defp render_child_to_string(child, assigns) do
case render(child, assigns) do
{:ok, html} -> Phoenix.HTML.safe_to_string(html)
_ -> ""
end
end
end