lib/mix/tasks/spark.replace_doc_links.ex

defmodule Mix.Tasks.Spark.ReplaceDocLinks do
  @moduledoc """
  Replaces any documentation links with text appropriate for hex docs.

  This makes projects support
  """
  use Mix.Task

  @auto_prefixes ~w(
    ash ash_postgres ash_graphql ash_phoenix ash_authentication ash_archival
    ash_json_api ash_admin ash_state_machine ash_oban ash_geo ash_appsignal ash_rbac ash_query_builder
    ash_uuid ash_csv pyro smokestack ash_thrift ash_double_entry ash_ulid
  )

  @prefixes @auto_prefixes
            |> Map.new(&{Macro.camelize(&1), &1})
            |> Map.merge(%{
              "AshAuthentication.Phoenix" => "ash_authentication_phoenix"
            })

  @shortdoc "Replaces any spark dsl specific doc links with text appropriate for hex docs."
  def run(_argv) do
    mix_project = Mix.Project.get!()
    module_prefix = mix_project |> Module.split() |> Enum.at(0)

    "doc/**/*.html"
    |> Path.wildcard()
    |> Enum.each(fn file ->
      new_contents =
        file
        |> File.read!()
        |> String.replace(~r/\>d\:[a-zA-Z0-9|_\.]*\</, fn ">d:" <> contents ->
          contents =
            contents
            |> String.trim_trailing("<")
            |> String.replace("|", ".")

          module_name =
            contents
            |> String.split(".")
            |> Enum.take_while(&capitalized?/1)
            |> Enum.join(".")

          url_prefix =
            if String.starts_with?(module_name, module_prefix <> ".") do
              case Code.ensure_compiled(Module.concat([module_name])) do
                {:module, _} ->
                  {:ok, ""}

                {:error, error} ->
                  raise "Expected #{module_name} to be compiled because the link \"d:#{contents}\" was used, but it was not available: #{inspect(error)}"
              end
            else
              @prefixes
              |> Enum.filter(fn {key, _value} -> String.starts_with?(module_name, key) end)
              |> Enum.sort_by(fn {key, _} -> String.length(key) end)
              |> Enum.reverse()
              |> Enum.at(0)
              |> case do
                nil ->
                  :error

                {_, package_name} ->
                  {:ok, "https://hexdocs.pm/#{package_name}/"}
              end
            end

          case url_prefix do
            {:ok, prefix} ->
              name =
                module_name
                |> String.trim_trailing(".Dsl")
                |> String.split(".")
                |> Enum.map_join("-", &String.downcase/1)

              rest =
                contents |> String.trim_leading(module_name <> ".") |> String.replace(".", "-")

              "><a href=\"#{prefix}dsl-#{name}.html##{rest}\">#{contents}</a><"

            :error ->
              ">#{contents}<"
          end
        end)

      File.write!(file, new_contents)
    end)
  end

  defp capitalized?(string) do
    first =
      string
      |> String.graphemes()
      |> Enum.at(0)

    String.downcase(first) != first
  end
end