From 5eee0455a29e8c1210ac2d2c3cd04b243c744ee2 Mon Sep 17 00:00:00 2001 From: "Paulo F. Oliveira" Date: Mon, 30 Oct 2023 06:57:42 +0000 Subject: [PATCH] Restore warning on missing references and `.md` links for Erlang (#1798) --- lib/ex_doc/autolink.ex | 406 ++++++++++++++++++++++++++ lib/ex_doc/language/elixir.ex | 418 +-------------------------- lib/ex_doc/language/erlang.ex | 12 +- test/ex_doc/language/erlang_test.exs | 28 ++ 4 files changed, 449 insertions(+), 415 deletions(-) diff --git a/lib/ex_doc/autolink.ex b/lib/ex_doc/autolink.ex index f75d6e967..c0ebbc0f5 100644 --- a/lib/ex_doc/autolink.ex +++ b/lib/ex_doc/autolink.ex @@ -25,6 +25,8 @@ defmodule ExDoc.Autolink do # # * `:filtered_modules` - A list of module nodes that were filtered by the retriever + alias ExDoc.Refs + defstruct [ :current_module, :module_id, @@ -136,6 +138,410 @@ defmodule ExDoc.Autolink do end end + @ref_regex ~r/^`(.+)`$/ + + def custom_link(attrs, config) do + case Keyword.fetch(attrs, :href) do + {:ok, href} -> + case Regex.scan(@ref_regex, href) do + [[_, custom_link]] -> + custom_link + |> url(:custom_link, config) + |> remove_and_warn_if_invalid(custom_link, config) + + [] -> + build_extra_link(href, config) + end + + _ -> + nil + end + end + + def url(string = "mix help " <> name, mode, config) do + name |> mix_task(string, mode, config) |> maybe_remove_link(mode) + end + + def url(string = "mix " <> name, mode, config) do + name |> mix_task(string, mode, config) |> maybe_remove_link(mode) + end + + def url(string, mode, config) do + if Enum.any?(config.skip_code_autolink_to, &(&1 == string)) do + nil + else + parse_url(string, mode, config) + end + end + + defp remove_and_warn_if_invalid(nil, reference, config) do + warn( + ~s[documentation references "#{reference}" but it is invalid], + {config.file, config.line}, + config.id + ) + + :remove_link + end + + defp remove_and_warn_if_invalid(result, _, _), do: result + + defp build_extra_link(link, config) do + with %{scheme: nil, host: nil, path: path} = uri <- URI.parse(link), + true <- is_binary(path) and path != "" and not (path =~ @ref_regex), + true <- Path.extname(path) in [".livemd", ".md", ".txt", ""] do + if file = config.extras[Path.basename(path)] do + fragment = (uri.fragment && "#" <> uri.fragment) || "" + file <> config.ext <> fragment + else + maybe_warn(nil, config, nil, %{file_path: path, original_text: link}) + nil + end + else + _ -> nil + end + end + + defp maybe_remove_link(nil, :custom_link) do + :remove_link + end + + defp maybe_remove_link(result, _mode) do + result + end + + defp mix_task(name, string, mode, config) do + {module, url, visibility} = + if name =~ ~r/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/ do + parts = name |> String.split(".") |> Enum.map(&Macro.camelize/1) + module = Module.concat([Mix, Tasks | parts]) + + {module, module_url(module, :regular_link, config, string), + Refs.get_visibility({:module, module})} + else + {nil, nil, :undefined} + end + + if url in [nil, :remove_link] and mode == :custom_link do + maybe_warn({:module, module}, config, visibility, %{ + mix_task: true, + original_text: string + }) + end + + url + end + + defp module_url(module, mode, config, string) do + ref = {:module, module} + + case {mode, Refs.get_visibility(ref)} do + {_link_type, visibility} when visibility in [:public, :limited] -> + app_module_url(tool(module, config), module, config) + + {:regular_link, :undefined} -> + nil + + {:custom_link, visibility} when visibility in [:hidden, :undefined] -> + maybe_warn(ref, config, visibility, %{original_text: string}) + :remove_link + + {_link_type, visibility} -> + maybe_warn(ref, config, visibility, %{original_text: string}) + nil + end + end + + defp parse_url(string, mode, config) do + case Regex.run(~r{^(.+)/(\d+)$}, string) do + [_, left, right] -> + with {:ok, arity} <- parse_arity(right) do + {kind, rest} = kind(left) + + case parse_module_function(rest) do + {:local, function} -> + kind + |> local_url(function, arity, config, string, mode: mode) + |> maybe_remove_link(mode) + + {:remote, module, function} -> + {kind, module, function, arity} + |> remote_url(config, string, mode: mode) + |> maybe_remove_link(mode) + + :error -> + nil + end + else + _ -> + nil + end + + nil -> + case parse_module(string, mode) do + {:module, module} -> + module_url(module, mode, config, string) + + :error -> + nil + end + + _ -> + nil + end + end + + defp parse_arity(string) do + case Integer.parse(string) do + {arity, ""} -> {:ok, arity} + _ -> :error + end + end + + defp parse_module_function(string) do + case string |> String.split(".") |> Enum.reverse() do + [string] -> + with {:function, function} <- parse_function(string) do + {:local, function} + end + + ["", "", ""] -> + {:local, :..} + + ["//", "", ""] -> + {:local, :"..//"} + + ["", ""] -> + {:local, :.} + + ["", "", "" | rest] -> + module_string = rest |> Enum.reverse() |> Enum.join(".") + + with {:module, module} <- parse_module(module_string, :custom_link) do + {:remote, module, :..} + end + + ["", "" | rest] -> + module_string = rest |> Enum.reverse() |> Enum.join(".") + + with {:module, module} <- parse_module(module_string, :custom_link) do + {:remote, module, :.} + end + + [function_string | rest] -> + module_string = rest |> Enum.reverse() |> Enum.join(".") + + with {:module, module} <- parse_module(module_string, :custom_link), + {:function, function} <- parse_function(function_string) do + {:remote, module, function} + end + end + end + + # There are special forms that are forbidden by the tokenizer + defp parse_function("__aliases__"), do: {:function, :__aliases__} + defp parse_function("__block__"), do: {:function, :__block__} + defp parse_function("%"), do: {:function, :%} + + defp parse_function(string) do + case Code.string_to_quoted("& #{string}/0") do + {:ok, {:&, _, [{:/, _, [{function, _, _}, 0]}]}} when is_atom(function) -> + {:function, function} + + _ -> + :error + end + end + + defp parse_module(<> <> _ = string, _mode) when first in ?A..?Z do + if string =~ ~r/^[A-Za-z0-9_.]+$/ do + do_parse_module(string) + else + :error + end + end + + defp parse_module(":" <> _ = string, :custom_link) do + do_parse_module(string) + end + + defp parse_module(_, _) do + :error + end + + defp do_parse_module(string) do + case Code.string_to_quoted(string, warn_on_unnecessary_quotes: false) do + {:ok, module} when is_atom(module) -> + {:module, module} + + {:ok, {:__aliases__, _, parts}} -> + if Enum.all?(parts, &is_atom/1) do + {:module, Module.concat(parts)} + else + :error + end + + _ -> + :error + end + end + + defp kind("c:" <> rest), do: {:callback, rest} + defp kind("t:" <> rest), do: {:type, rest} + defp kind(rest), do: {:function, rest} + + @basic_types [ + any: 0, + none: 0, + atom: 0, + map: 0, + pid: 0, + port: 0, + reference: 0, + struct: 0, + tuple: 0, + float: 0, + integer: 0, + neg_integer: 0, + non_neg_integer: 0, + pos_integer: 0, + list: 1, + nonempty_list: 1, + maybe_improper_list: 2, + nonempty_improper_list: 2, + nonempty_maybe_improper_list: 2 + ] + + @built_in_types [ + term: 0, + arity: 0, + as_boolean: 1, + binary: 0, + bitstring: 0, + boolean: 0, + byte: 0, + char: 0, + charlist: 0, + nonempty_charlist: 0, + fun: 0, + function: 0, + identifier: 0, + iodata: 0, + iolist: 0, + keyword: 0, + keyword: 1, + list: 0, + nonempty_list: 0, + maybe_improper_list: 0, + nonempty_maybe_improper_list: 0, + mfa: 0, + module: 0, + no_return: 0, + node: 0, + number: 0, + struct: 0, + timeout: 0 + ] + + def local_url(kind, name, arity, config, original_text, options \\ []) + + def local_url(:type, name, arity, config, _original_text, _options) + when {name, arity} in @basic_types do + ex_doc_app_url(Kernel, config, "typespecs", config.ext, "#basic-types") + end + + def local_url(:type, name, arity, config, _original_text, _options) + when {name, arity} in @built_in_types do + ex_doc_app_url(Kernel, config, "typespecs", config.ext, "#built-in-types") + end + + def local_url(kind, name, arity, config, original_text, options) do + module = config.current_module + ref = {kind, module, name, arity} + mode = Keyword.get(options, :mode, :regular_link) + visibility = Refs.get_visibility(ref) + + case {kind, visibility} do + {_kind, :public} -> + fragment(tool(module, config), kind, name, arity) + + {:function, _visibility} -> + try_autoimported_function(name, arity, mode, config, original_text) + + {:type, :hidden} -> + nil + + {:type, _} -> + nil + + _ -> + maybe_warn(ref, config, visibility, %{original_text: original_text}) + nil + end + end + + defp fragment(:ex_doc, kind, name, arity) do + "#" <> prefix(kind) <> "#{URI.encode(Atom.to_string(name))}/#{arity}" + end + + defp fragment(_, kind, name, arity) do + case kind do + :function -> "##{name}-#{arity}" + :callback -> "#Module:#{name}-#{arity}" + :type -> "#type-#{name}" + end + end + + defp prefix(kind) + defp prefix(:function), do: "" + defp prefix(:callback), do: "c:" + defp prefix(:type), do: "t:" + + @autoimported_modules [Kernel, Kernel.SpecialForms] + + defp try_autoimported_function(name, arity, mode, config, original_text) do + Enum.find_value(@autoimported_modules, fn module -> + remote_url({:function, module, name, arity}, config, original_text, + warn?: false, + mode: mode + ) + end) + end + + def remote_url({kind, module, name, arity} = ref, config, original_text, opts \\ []) do + warn? = Keyword.get(opts, :warn?, true) + mode = Keyword.get(opts, :mode, :regular_link) + same_module? = module == config.current_module + + case {mode, Refs.get_visibility({:module, module}), Refs.get_visibility(ref)} do + {_mode, _module_visibility, :public} -> + tool = tool(module, config) + + if same_module? do + fragment(tool, kind, name, arity) + else + app_module_url(tool, module, config) <> fragment(tool, kind, name, arity) + end + + {:regular_link, module_visibility, :undefined} + when module_visibility == :public + when module_visibility == :limited and kind != :type -> + if warn?, + do: maybe_warn(ref, config, :undefined, %{original_text: original_text}) + + nil + + {:regular_link, _module_visibility, :undefined} when not same_module? -> + nil + + {_mode, _module_visibility, visibility} -> + if warn?, + do: maybe_warn(ref, config, visibility, %{original_text: original_text}) + + nil + end + end + defp warn(message, {file, line}, id) do warning = IO.ANSI.format([:yellow, "warning: ", :reset]) diff --git a/lib/ex_doc/language/elixir.ex b/lib/ex_doc/language/elixir.ex index e64ac42ad..a0c070185 100644 --- a/lib/ex_doc/language/elixir.ex +++ b/lib/ex_doc/language/elixir.ex @@ -4,7 +4,6 @@ defmodule ExDoc.Language.Elixir do @behaviour ExDoc.Language alias ExDoc.Autolink - alias ExDoc.Refs alias ExDoc.Language.Erlang @impl true @@ -392,10 +391,6 @@ defmodule ExDoc.Language.Elixir do defp process_type_ast({:"::", _, [d | _]}, :opaque), do: d defp process_type_ast(ast, _), do: ast - ## Autolinking - - @autoimported_modules [Kernel, Kernel.SpecialForms] - defp walk_doc(list, config) when is_list(list) do Enum.map(list, &walk_doc(&1, config)) end @@ -409,7 +404,7 @@ defmodule ExDoc.Language.Elixir do end defp walk_doc({:a, attrs, inner, meta} = ast, config) do - case custom_link(attrs, config) do + case Autolink.custom_link(attrs, config) do :remove_link -> remove_link(ast) @@ -422,7 +417,7 @@ defmodule ExDoc.Language.Elixir do end defp walk_doc({:code, attrs, [code], meta} = ast, config) do - if url = url(code, :regular_link, config) do + if url = Autolink.url(code, :regular_link, config) do code = remove_prefix(code) {:a, [href: url], [{:code, attrs, [code], meta}], %{}} else @@ -438,38 +433,6 @@ defmodule ExDoc.Language.Elixir do inner end - @ref_regex ~r/^`(.+)`$/ - - defp custom_link(attrs, config) do - case Keyword.fetch(attrs, :href) do - {:ok, href} -> - case Regex.scan(@ref_regex, href) do - [[_, custom_link]] -> - custom_link - |> url(:custom_link, config) - |> remove_and_warn_if_invalid(custom_link, config) - - [] -> - build_extra_link(href, config) - end - - _ -> - nil - end - end - - defp remove_and_warn_if_invalid(nil, reference, config) do - warn( - ~s[documentation references "#{reference}" but it is invalid], - {config.file, config.line}, - config.id - ) - - :remove_link - end - - defp remove_and_warn_if_invalid(result, _, _), do: result - defp warn(message, {file, line}, id) do warning = IO.ANSI.format([:yellow, "warning: ", :reset]) @@ -481,264 +444,10 @@ defmodule ExDoc.Language.Elixir do IO.puts(:stderr, [warning, message, ?\n, stacktrace, ?\n]) end - defp build_extra_link(link, config) do - with %{scheme: nil, host: nil, path: path} = uri <- URI.parse(link), - true <- is_binary(path) and path != "" and not (path =~ @ref_regex), - true <- Path.extname(path) in [".livemd", ".md", ".txt", ""] do - if file = config.extras[Path.basename(path)] do - fragment = (uri.fragment && "#" <> uri.fragment) || "" - file <> config.ext <> fragment - else - Autolink.maybe_warn(nil, config, nil, %{file_path: path, original_text: link}) - nil - end - else - _ -> nil - end - end - - @basic_types [ - any: 0, - none: 0, - atom: 0, - map: 0, - pid: 0, - port: 0, - reference: 0, - struct: 0, - tuple: 0, - float: 0, - integer: 0, - neg_integer: 0, - non_neg_integer: 0, - pos_integer: 0, - list: 1, - nonempty_list: 1, - maybe_improper_list: 2, - nonempty_improper_list: 2, - nonempty_maybe_improper_list: 2 - ] - - @built_in_types [ - term: 0, - arity: 0, - as_boolean: 1, - binary: 0, - bitstring: 0, - boolean: 0, - byte: 0, - char: 0, - charlist: 0, - nonempty_charlist: 0, - fun: 0, - function: 0, - identifier: 0, - iodata: 0, - iolist: 0, - keyword: 0, - keyword: 1, - list: 0, - nonempty_list: 0, - maybe_improper_list: 0, - nonempty_maybe_improper_list: 0, - mfa: 0, - module: 0, - no_return: 0, - node: 0, - number: 0, - struct: 0, - timeout: 0 - ] - - defp url(string = "mix help " <> name, mode, config) do - name |> mix_task(string, mode, config) |> maybe_remove_link(mode) - end - - defp url(string = "mix " <> name, mode, config) do - name |> mix_task(string, mode, config) |> maybe_remove_link(mode) - end - - defp url(string, mode, config) do - if Enum.any?(config.skip_code_autolink_to, &(&1 == string)) do - nil - else - parse_url(string, mode, config) - end - end - - defp parse_url(string, mode, config) do - case Regex.run(~r{^(.+)/(\d+)$}, string) do - [_, left, right] -> - with {:ok, arity} <- parse_arity(right) do - {kind, rest} = kind(left) - - case parse_module_function(rest) do - {:local, function} -> - kind - |> local_url(function, arity, config, string, mode: mode) - |> maybe_remove_link(mode) - - {:remote, module, function} -> - {kind, module, function, arity} - |> remote_url(config, string, mode: mode) - |> maybe_remove_link(mode) - - :error -> - nil - end - else - _ -> - nil - end - - nil -> - case parse_module(string, mode) do - {:module, module} -> - module_url(module, mode, config, string) - - :error -> - nil - end - - _ -> - nil - end - end - - # Remove link when we fail to parse reference so we don't warn twice - defp maybe_remove_link(nil, :custom_link) do - :remove_link - end - - defp maybe_remove_link(result, _mode) do - result - end - - defp kind("c:" <> rest), do: {:callback, rest} - defp kind("t:" <> rest), do: {:type, rest} - defp kind(rest), do: {:function, rest} - defp remove_prefix("c:" <> rest), do: rest defp remove_prefix("t:" <> rest), do: rest defp remove_prefix(rest), do: rest - defp parse_arity(string) do - case Integer.parse(string) do - {arity, ""} -> {:ok, arity} - _ -> :error - end - end - - defp parse_module_function(string) do - case string |> String.split(".") |> Enum.reverse() do - [string] -> - with {:function, function} <- parse_function(string) do - {:local, function} - end - - ["", "", ""] -> - {:local, :..} - - ["//", "", ""] -> - {:local, :"..//"} - - ["", ""] -> - {:local, :.} - - ["", "", "" | rest] -> - module_string = rest |> Enum.reverse() |> Enum.join(".") - - with {:module, module} <- parse_module(module_string, :custom_link) do - {:remote, module, :..} - end - - ["", "" | rest] -> - module_string = rest |> Enum.reverse() |> Enum.join(".") - - with {:module, module} <- parse_module(module_string, :custom_link) do - {:remote, module, :.} - end - - [function_string | rest] -> - module_string = rest |> Enum.reverse() |> Enum.join(".") - - with {:module, module} <- parse_module(module_string, :custom_link), - {:function, function} <- parse_function(function_string) do - {:remote, module, function} - end - end - end - - defp parse_module(<> <> _ = string, _mode) when first in ?A..?Z do - if string =~ ~r/^[A-Za-z0-9_.]+$/ do - do_parse_module(string) - else - :error - end - end - - defp parse_module(":" <> _ = string, :custom_link) do - do_parse_module(string) - end - - defp parse_module(_, _) do - :error - end - - defp do_parse_module(string) do - case Code.string_to_quoted(string, warn_on_unnecessary_quotes: false) do - {:ok, module} when is_atom(module) -> - {:module, module} - - {:ok, {:__aliases__, _, parts}} -> - if Enum.all?(parts, &is_atom/1) do - {:module, Module.concat(parts)} - else - :error - end - - _ -> - :error - end - end - - # There are special forms that are forbidden by the tokenizer - defp parse_function("__aliases__"), do: {:function, :__aliases__} - defp parse_function("__block__"), do: {:function, :__block__} - defp parse_function("%"), do: {:function, :%} - - defp parse_function(string) do - case Code.string_to_quoted("& #{string}/0") do - {:ok, {:&, _, [{:/, _, [{function, _, _}, 0]}]}} when is_atom(function) -> - {:function, function} - - _ -> - :error - end - end - - defp mix_task(name, string, mode, config) do - {module, url, visibility} = - if name =~ ~r/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/ do - parts = name |> String.split(".") |> Enum.map(&Macro.camelize/1) - module = Module.concat([Mix, Tasks | parts]) - - {module, module_url(module, :regular_link, config, string), - Refs.get_visibility({:module, module})} - else - {nil, nil, :undefined} - end - - if url in [nil, :remove_link] and mode == :custom_link do - Autolink.maybe_warn({:module, module}, config, visibility, %{ - mix_task: true, - original_text: string - }) - end - - url - end - defp safe_format_string!(string) do try do string @@ -795,9 +504,9 @@ defmodule ExDoc.Language.Elixir do url = if module do - remote_url({:type, module, name, arity}, config, original_text) + Autolink.remote_url({:type, module, name, arity}, config, original_text) else - local_url(:type, name, arity, config, original_text) + Autolink.local_url(:type, name, arity, config, original_text) end if url do @@ -829,123 +538,4 @@ defmodule ExDoc.Language.Elixir do defp count_args("," <> rest, 1, acc), do: count_args(rest, 1, acc + 1) defp count_args(<<_>> <> rest, counter, acc), do: count_args(rest, counter, acc) defp count_args("", _counter, acc), do: acc - - ## Internals - - defp module_url(module, mode, config, string) do - ref = {:module, module} - - case {mode, Refs.get_visibility(ref)} do - {_link_type, visibility} when visibility in [:public, :limited] -> - Autolink.app_module_url(Autolink.tool(module, config), module, config) - - {:regular_link, :undefined} -> - nil - - {:custom_link, visibility} when visibility in [:hidden, :undefined] -> - Autolink.maybe_warn(ref, config, visibility, %{original_text: string}) - :remove_link - - {_link_type, visibility} -> - Autolink.maybe_warn(ref, config, visibility, %{original_text: string}) - nil - end - end - - defp local_url(kind, name, arity, config, original_text, options \\ []) - - defp local_url(:type, name, arity, config, _original_text, _options) - when {name, arity} in @basic_types do - Autolink.ex_doc_app_url(Kernel, config, "typespecs", config.ext, "#basic-types") - end - - defp local_url(:type, name, arity, config, _original_text, _options) - when {name, arity} in @built_in_types do - Autolink.ex_doc_app_url(Kernel, config, "typespecs", config.ext, "#built-in-types") - end - - defp local_url(kind, name, arity, config, original_text, options) do - module = config.current_module - ref = {kind, module, name, arity} - mode = Keyword.get(options, :mode, :regular_link) - visibility = Refs.get_visibility(ref) - - case {kind, visibility} do - {_kind, :public} -> - fragment(Autolink.tool(module, config), kind, name, arity) - - {:function, _visibility} -> - try_autoimported_function(name, arity, mode, config, original_text) - - {:type, :hidden} -> - nil - - {:type, _} -> - nil - - _ -> - Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text}) - nil - end - end - - defp try_autoimported_function(name, arity, mode, config, original_text) do - Enum.find_value(@autoimported_modules, fn module -> - remote_url({:function, module, name, arity}, config, original_text, - warn?: false, - mode: mode - ) - end) - end - - defp remote_url({kind, module, name, arity} = ref, config, original_text, opts \\ []) do - warn? = Keyword.get(opts, :warn?, true) - mode = Keyword.get(opts, :mode, :regular_link) - same_module? = module == config.current_module - - case {mode, Refs.get_visibility({:module, module}), Refs.get_visibility(ref)} do - {_mode, _module_visibility, :public} -> - tool = Autolink.tool(module, config) - - if same_module? do - fragment(tool, kind, name, arity) - else - Autolink.app_module_url(tool, module, config) <> fragment(tool, kind, name, arity) - end - - {:regular_link, module_visibility, :undefined} - when module_visibility == :public - when module_visibility == :limited and kind != :type -> - if warn?, - do: Autolink.maybe_warn(ref, config, :undefined, %{original_text: original_text}) - - nil - - {:regular_link, _module_visibility, :undefined} when not same_module? -> - nil - - {_mode, _module_visibility, visibility} -> - if warn?, - do: Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text}) - - nil - end - end - - defp prefix(kind) - defp prefix(:function), do: "" - defp prefix(:callback), do: "c:" - defp prefix(:type), do: "t:" - - defp fragment(:ex_doc, kind, name, arity) do - "#" <> prefix(kind) <> "#{URI.encode(Atom.to_string(name))}/#{arity}" - end - - defp fragment(_, kind, name, arity) do - case kind do - :function -> "##{name}-#{arity}" - :callback -> "#Module:#{name}-#{arity}" - :type -> "#type-#{name}" - end - end end diff --git a/lib/ex_doc/language/erlang.ex b/lib/ex_doc/language/erlang.ex index 0d1ce48da..bb9db6520 100644 --- a/lib/ex_doc/language/erlang.ex +++ b/lib/ex_doc/language/erlang.ex @@ -303,7 +303,7 @@ defmodule ExDoc.Language.Erlang do inner _ -> - ast + handle_custom_link(ast, config) end end @@ -311,6 +311,16 @@ defmodule ExDoc.Language.Erlang do {tag, attrs, walk_doc(ast, config), meta} end + defp handle_custom_link({:a, attrs, inner, meta} = ast, config) do + case Autolink.custom_link(attrs, config) do + nil -> + ast + + url -> + {:a, Keyword.put(attrs, :href, url), inner, meta} + end + end + defp extract_fragment(url) do case String.split(url, "#", parts: 2) do [url] -> {"", url} diff --git a/test/ex_doc/language/erlang_test.exs b/test/ex_doc/language/erlang_test.exs index 8a1cc231d..725a14b90 100644 --- a/test/ex_doc/language/erlang_test.exs +++ b/test/ex_doc/language/erlang_test.exs @@ -5,6 +5,10 @@ defmodule ExDoc.Language.ErlangTest do @moduletag :otp_has_docs @moduletag :tmp_dir + defp sigil_m(text, []) do + ExDoc.Markdown.to_ast(text, []) + end + describe "autolink_doc/2" do test "module", c do assert autolink_doc("{@link erlang_bar}", c) == @@ -225,6 +229,30 @@ defmodule ExDoc.Language.ErlangTest do assert autolink_extra("`does_not_exist`", c) == ~s|does_not_exist| end + + test "extras" do + opts = [extras: %{"Foo Bar.md" => "foo-bar", "Bar Baz.livemd" => "bar-baz"}] + + assert ExDoc.Language.Erlang.autolink_doc(~m"[Foo](Foo Bar.md)", opts) == + ~m"[Foo](foo-bar.html)" + + assert ExDoc.Language.Erlang.autolink_doc(~m"[Bar](Bar Baz.livemd)", opts) == + ~m"[Bar](bar-baz.html)" + + assert ExDoc.Language.Erlang.autolink_doc(~m"[Foo](Foo Bar.md)", [ext: ".xhtml"] ++ opts) == + ~m"[Foo](foo-bar.xhtml)" + + assert ExDoc.Language.Erlang.autolink_doc(~m"[Foo](Foo Bar.md#baz)", opts) == + ~m"[Foo](foo-bar.html#baz)" + + assert ExDoc.Language.Erlang.autolink_doc(~m"[Foo](../guide/Foo Bar.md)", opts) == + ~m"[Foo](foo-bar.html)" + + assert ExDoc.Language.Erlang.autolink_doc(~m"[Foo](http://example.com/foo.md)", opts) == + ~m"[Foo](http://example.com/foo.md)" + + assert ExDoc.Language.Erlang.autolink_doc(~m"[Foo](#baz)", opts) == ~m"[Foo](#baz)" + end end describe "autolink_spec/2" do