defmodule Sanity.Components.PortableText do use Phoenix.Component require Logger defmodule Behaviour do @callback block(map()) :: Phoenix.LiveView.Rendered.t() @callback mark(map()) :: Phoenix.LiveView.Rendered.t() @callback type(map()) :: Phoenix.LiveView.Rendered.t() end @behaviour Behaviour defmacro __using__([]) do quote do @behaviour Sanity.Components.PortableText.Behaviour def block(assigns), do: Sanity.Components.PortableText.block(assigns) def mark(assigns), do: Sanity.Components.PortableText.mark(assigns) def type(assigns), do: Sanity.Components.PortableText.type(assigns) defoverridable Sanity.Components.PortableText.Behaviour end end def portable_text(assigns) do mod = Map.get(assigns, :mod, __MODULE__) ~H""" <%= for block <- @value do %><.render_with mod={mod} func={:type} value={block} /> <% end %> """ end defp render_with(assigns) do {func, assigns} = Map.pop!(assigns, :func) apply(assigns.mod, func, [assigns]) end defp shared_props(assigns), do: Map.take(assigns, [:mod, :value]) @doc false @impl true def type(%{value: %{_type: "block"}} = assigns) do ~H""" <.render_with func={:block} {shared_props(assigns)}> <%= for child <- @value.children do %><.marks marks={child.marks} {shared_props(assigns)}><%= child.text %><% end %> """ end @doc false @impl true def block(%{value: %{_type: "block", style: style}} = assigns) when style in ["blockquote", "h1", "h2", "h3", "h4", "h5", "h6"] do ~H""" <%= Phoenix.HTML.Tag.content_tag style do %><%= render_slot(@inner_block) %><% end %> """ end def block(%{value: %{_type: "block", style: "normal"}} = assigns) do ~H"""

<%= render_slot(@inner_block) %>

""" end def block(%{value: %{_type: "block", style: style}} = assigns) do Logger.warn("unknown block style: #{inspect(style)}") ~H"""

<%= render_slot(@inner_block) %>

""" end defp marks(%{marks: []} = assigns) do ~H""" <%= render_slot(@inner_block) %> """ end defp marks(%{marks: [mark | remaining_marks]} = assigns) do mark_props = case Enum.find(assigns.value.mark_defs, &(&1._key == mark)) do nil -> %{ mark_key: mark, mark_type: mark, value: nil } %{_type: type} = mark_def -> %{ mark_key: mark, mark_type: type, value: mark_def } end ~H""" <.render_with mod={@mod} func={:mark} {mark_props}><.marks marks={remaining_marks} {shared_props(assigns)}><%= render_slot(@inner_block) %> """ end @doc false @impl true def mark(%{mark_type: "em"} = assigns) do ~H""" <%= render_slot(@inner_block) %> """ end def mark(%{mark_type: "strong"} = assigns) do ~H""" <%= render_slot(@inner_block) %> """ end def mark(%{mark_type: "link", value: value} = assigns) do ~H""" <%= render_slot(@inner_block) %> """ end def mark(%{mark_type: mark_type} = assigns) do Logger.warn("unknown mark type: #{inspect(mark_type)}") ~H""" <%= render_slot(@inner_block) %> """ end end