lib/phoenix_storybook/stories/doc.ex

defmodule PhoenixStorybook.Stories.Doc do
  @moduledoc """
  Functions to fetch component documentation and render it at HTML.
  """

  require Logger

  @doc """
  Fetch component documentation from component source and format it as HTML.
  - For a live_component, fetches @moduledoc content
  - For a function component, fetches @doc content of the relevant function

  Output HTML is splitted in paragraphs and returned as a list of paragraphs.
  """
  def fetch_doc_as_html(story) do
    case fetch_component_doc(story.storybook_type(), story) do
      :error -> nil
      doc -> doc |> strip_attributes() |> split_paragraphs() |> Enum.map(&format/1)
    end
  end

  defp fetch_component_doc(:component, module) do
    info = Function.info(module.function())
    fetch_function_doc(info[:module], {info[:name], info[:arity]})
  end

  defp fetch_component_doc(:live_component, module) do
    fetch_module_doc(module.component())
  end

  defp fetch_function_doc(module, {fun, arity}) do
    case Code.fetch_docs(module) do
      {_, _, _, _, _, _, function_docs} ->
        case find_function_doc(function_docs, fun, arity) do
          map when is_map(map) -> map |> Map.values() |> Enum.at(0)
          _ -> nil
        end

      _ ->
        Logger.warn("could not fetch function docs from #{inspect(module)}")
        :error
    end
  end

  defp find_function_doc(docs, fun, arity) do
    Enum.find_value(
      docs,
      %{},
      fn
        {{:function, item_fun, item_arity}, _, _, doc, _} ->
          if fun == item_fun && arity == item_arity, do: doc, else: false

        _ ->
          false
      end
    )
  end

  defp fetch_module_doc(module) do
    case Code.fetch_docs(module) do
      {_, _, _, _, module_doc, _, _} ->
        case module_doc do
          map when is_map(map) -> map |> Map.values() |> Enum.at(0)
          _ -> nil
        end

      _ ->
        Logger.warn("could not fetch module doc from #{inspect(module)}")
        :error
    end
  end

  defp strip_attributes(nil), do: nil

  defp strip_attributes(doc) do
    doc |> String.split("## Attributes\n\n") |> hd()
  end

  defp split_paragraphs(nil), do: []

  defp split_paragraphs(doc) do
    doc |> String.split("\n\n") |> Enum.reject(&(String.trim(&1) == ""))
  end

  defp format(doc) do
    doc |> Earmark.as_html!() |> String.trim()
  end
end