defmodule Iris.ExDoc.Config do @moduledoc false # Defaults @default_source_ref "main" def default_group_for_doc(metadata), do: metadata[:group] def filter_modules(_module, _metadata), do: true def before_closing_head_tag(_), do: "" def before_closing_footer_tag(_), do: "" def before_closing_body_tag(_), do: "" def annotations_for_docs(_), do: [] def skip_undefined_reference_warnings_on(_string), do: false def skip_code_autolink_to(_string), do: false def source_url_pattern(_, _), do: nil defstruct annotations_for_docs: &__MODULE__.annotations_for_docs/1, api_reference: true, apps: [], assets: %{}, authors: nil, before_closing_body_tag: &__MODULE__.before_closing_body_tag/1, before_closing_footer_tag: &__MODULE__.before_closing_footer_tag/1, before_closing_head_tag: &__MODULE__.before_closing_head_tag/1, canonical: nil, cover: nil, deps: [], docs_groups: [], extra_section: nil, extras: [], favicon: nil, filter_modules: &__MODULE__.filter_modules/2, formatter: "html", formatters: [], group_for_doc: &__MODULE__.default_group_for_doc/1, groups_for_extras: [], groups_for_modules: [], homepage_url: nil, language: "en", logo: nil, main: nil, nest_modules_by_prefix: [], output: "./doc", package: nil, proglang: :elixir, project: nil, redirects: %{}, retriever: ExDoc.Retriever, skip_undefined_reference_warnings_on: &__MODULE__.skip_undefined_reference_warnings_on/1, skip_code_autolink_to: &__MODULE__.skip_code_autolink_to/1, source_beam: nil, source_ref: @default_source_ref, source_url: nil, source_url_pattern: &__MODULE__.source_url_pattern/2, title: nil, version: nil, warnings_as_errors: false, verbose: false @type t :: %__MODULE__{ annotations_for_docs: (map() -> list()), api_reference: boolean(), apps: [atom()], assets: %{binary() => binary()}, authors: nil | [String.t()], before_closing_body_tag: (atom() -> String.t()) | mfa() | map(), before_closing_footer_tag: (atom() -> String.t()) | mfa() | map(), before_closing_head_tag: (atom() -> String.t()) | mfa() | map(), canonical: nil | String.t(), cover: nil | Path.t(), deps: [{ebin_path :: String.t(), doc_url :: String.t()}], docs_groups: [String.t()], extra_section: nil | String.t(), extras: list(), favicon: nil | Path.t(), filter_modules: (module, map -> boolean), formatter: nil | String.t(), formatters: [String.t()], group_for_doc: (keyword() -> String.t() | nil), groups_for_extras: [{binary(), term()}], groups_for_modules: [{binary(), term()}], homepage_url: nil | String.t(), language: String.t(), logo: nil | Path.t(), main: nil | String.t(), nest_modules_by_prefix: [String.t()], output: nil | Path.t(), package: :atom | nil, project: nil | String.t(), redirects: %{optional(String.t()) => String.t()} | [{String.t(), String.t()}], retriever: atom(), skip_undefined_reference_warnings_on: (String.t() -> boolean), skip_code_autolink_to: (String.t() -> boolean), source_beam: nil | String.t(), source_ref: nil | String.t(), source_url: nil | String.t(), source_url_pattern: (String.t(), integer() -> String.t() | nil), title: nil | String.t(), version: nil | String.t(), warnings_as_errors: boolean(), verbose: boolean() } def build(project, vsn, options) do {output, options} = Keyword.pop(options, :output, "./doc") {nest_modules_by_prefix, options} = Keyword.pop(options, :nest_modules_by_prefix, []) {proglang, options} = Keyword.pop(options, :proglang, :elixir) {filter_modules, options} = Keyword.pop(options, :filter_modules, &filter_modules/2) options = if groups_for_functions = options[:groups_for_functions] do IO.warn(":groups_for_functions is deprecated, please use :groups_for_docs instead") Keyword.put_new(options, :groups_for_docs, groups_for_functions) else options end apps = Keyword.get(options, :apps, []) {groups_for_docs, options} = Keyword.pop(options, :groups_for_docs, []) {groups_for_extras, options} = Keyword.pop(options, :groups_for_extras, []) {groups_for_modules, options} = Keyword.pop(options, :groups_for_modules, default_groups_for_modules(apps)) {default_group_for_doc, options} = Keyword.pop(options, :default_group_for_doc, &default_group_for_doc/1) {skip_undefined_reference_warnings_on, options} = Keyword.pop( options, :skip_undefined_reference_warnings_on, &skip_undefined_reference_warnings_on/1 ) {skip_code_autolink_to, options} = Keyword.pop(options, :skip_code_autolink_to, &skip_code_autolink_to/1) {source_url_pattern, options} = Keyword.pop_lazy(options, :source_url_pattern, fn -> guess_url(options[:source_url], options[:source_ref] || @default_source_ref) end) preconfig = %__MODULE__{ filter_modules: normalize_filter_modules(filter_modules), docs_groups: for({group, _} <- groups_for_docs, do: to_string(group)), group_for_doc: normalize_groups_for_docs(groups_for_docs, default_group_for_doc), groups_for_extras: normalize_groups(groups_for_extras), groups_for_modules: normalize_groups( # TODO: The default module groups must be returned by the language groups_for_modules ++ [Deprecated: &deprecated?/1, Exceptions: &exception?/1] ), homepage_url: options[:homepage_url], main: options[:main], nest_modules_by_prefix: normalize_nest_modules_by_prefix(nest_modules_by_prefix), output: normalize_output(output), proglang: normalize_proglang(proglang), project: project, skip_undefined_reference_warnings_on: normalize_skip_list_function(skip_undefined_reference_warnings_on), skip_code_autolink_to: normalize_skip_list_function(skip_code_autolink_to), source_url_pattern: normalize_source_url_pattern(source_url_pattern), version: vsn } struct(preconfig, options) end defp normalize_output(output) do String.trim_trailing(output, "/") end defp normalize_proglang(binary) when is_binary(binary) do binary |> String.to_atom() |> normalize_proglang() end defp normalize_proglang(proglang) when proglang in [:elixir, :erlang] do proglang end defp normalize_proglang(proglang) do raise ArgumentError, "#{inspect(proglang)} is not supported" end defp normalize_groups_for_docs(groups, default) do groups = normalize_groups(groups) fn metadata -> Enum.find_value(groups, fn {group, function} -> function.(metadata) && group end) || default.(metadata) end end defp normalize_groups(groups) do for {k, v} <- groups, do: {to_string(k), v} end defp deprecated?(metadata), do: metadata[:deprecated] != nil defp exception?(metadata), do: metadata[:kind] == :exception defp normalize_nest_modules_by_prefix(nest_modules_by_prefix) do nest_modules_by_prefix |> Enum.map(&inspect_atoms/1) |> Enum.sort() |> Enum.reverse() end defp inspect_atoms(atom) when is_atom(atom), do: inspect(atom) defp inspect_atoms(binary) when is_binary(binary), do: binary defp normalize_filter_modules(string) when is_binary(string), do: normalize_filter_modules(Regex.compile!(string)) defp normalize_filter_modules(%Regex{} = regex), do: fn module, _ -> Atom.to_string(module) =~ regex end defp normalize_filter_modules(fun) when is_function(fun, 2), do: fun defp normalize_skip_list_function(strings) when is_list(strings), do: &(&1 in strings) defp normalize_skip_list_function(fun) when is_function(fun, 1), do: fun defp normalize_source_url_pattern(function) when is_function(function, 2), do: function defp normalize_source_url_pattern(nil), do: &source_url_pattern/2 defp normalize_source_url_pattern(binary) when is_binary(binary) do case :binary.split(binary, "%{path}") do [left, right] -> case :binary.split(left, "%{line}") do [line_left, line_right] -> fn path, line -> line_left <> Integer.to_string(line) <> line_right <> path <> right end [_] -> case :binary.split(right, "%{line}") do [line_left, line_right] -> fn path, line -> left <> path <> line_left <> Integer.to_string(line) <> line_right end [_] -> fn path, _ -> left <> path <> right end end end [_] -> case :binary.split(binary, "%{line}") do [left, right] -> fn _, line -> left <> Integer.to_string(line) <> right end [_] -> fn _, _ -> binary end end end end defp normalize_source_url_pattern(other) do raise ArgumentError, ":source_url_pattern must be a string, a two-arity function or nil, got: #{inspect(other)}" end defp guess_url(url, ref) do with {:ok, host_with_path} <- http_or_https(url), {:ok, pattern} <- known_pattern(host_with_path, ref) do "https://" <> append_slash(host_with_path) <> pattern else _ -> url end end defp http_or_https("http://" <> rest), do: {:ok, rest} defp http_or_https("https://" <> rest), do: {:ok, rest} defp http_or_https(_), do: :error defp known_pattern("github.com/" <> _, ref), do: {:ok, "blob/#{ref}/%{path}#L%{line}"} defp known_pattern("gitlab.com/" <> _, ref), do: {:ok, "blob/#{ref}/%{path}#L%{line}"} defp known_pattern("bitbucket.org/" <> _, ref), do: {:ok, "src/#{ref}/%{path}#cl-%{line}"} defp known_pattern(_host_with_path, _ref), do: :error defp append_slash(url) do if :binary.last(url) == ?/, do: url, else: url <> "/" end defp default_groups_for_modules([_app]) do [] end defp default_groups_for_modules(apps) do Enum.map(apps, fn app -> Application.load(app) {app, Application.spec(app, :modules)} end) end end