diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex
index 3469d87b3..7a070b504 100644
--- a/lib/ex_doc/formatter/html/autolink.ex
+++ b/lib/ex_doc/formatter/html/autolink.ex
@@ -2,9 +2,13 @@ defmodule ExDoc.Formatter.HTML.Autolink do
@moduledoc """
Conveniences for autolinking.
"""
-
import ExDoc.Formatter.HTML.Templates, only: [h: 1, enc_h: 1]
+ @type language :: :elixir | :erlang | :markdown
+ @type kind :: :function | :module
+ @type link_type :: :normal | :custom
+
+ @backtick_token ""
@elixir_docs "https://hexdocs.pm/"
@erlang_docs "http://www.erlang.org/doc/man/"
@basic_types_page "typespecs.html#basic-types"
@@ -71,7 +75,7 @@ defmodule ExDoc.Formatter.HTML.Autolink do
@special_form_strings for {f, a} <- special_form_exports, do: "#{f}/#{a}"
@doc """
- Compiles information used during autolinks.
+ Compiles information used during autolinking.
"""
def compile(modules, extension, extra_lib_dirs) do
aliases = Enum.map(modules, & &1.module)
@@ -112,25 +116,19 @@ defmodule ExDoc.Formatter.HTML.Autolink do
end
defp project_doc(bin, module_id, locals, compiled) when is_binary(bin) do
- %{
- aliases: aliases,
- docs_refs: docs_refs,
- extension: extension,
- lib_dirs: lib_dirs,
- modules_refs: modules_refs
- } = compiled
+ options =
+ Map.merge(compiled, %{
+ module_id: module_id,
+ locals: locals
+ })
- bin
- |> locals(locals, aliases, extension, lib_dirs)
- |> elixir_functions(docs_refs, extension, lib_dirs)
- |> elixir_modules(modules_refs, module_id, extension, lib_dirs)
- |> erlang_functions()
+ link_everything(bin, options)
end
@doc """
- Autolinks all modules nodes.
+ Autolinks all module nodes.
- This is the main API to autolink any modules nodes.
+ This is the main API to autolink any module nodes.
"""
def all(modules, compiled) do
opts = [timeout: :infinity]
@@ -311,6 +309,20 @@ defmodule ExDoc.Formatter.HTML.Autolink do
|> IO.iodata_to_binary()
end
+ @kinds [:module, :function]
+ @languages [:elixir, :erlang]
+ @link_types [:custom, :normal]
+
+ @regexes (for link_type <- @link_types,
+ language <- @languages,
+ kind <- @kinds do
+ %{
+ kind: kind,
+ language: language,
+ link_type: link_type
+ }
+ end)
+
@doc """
Helper function for autolinking locals.
@@ -322,40 +334,34 @@ defmodule ExDoc.Formatter.HTML.Autolink do
or trailing `]`, e.g. `[my link link/1 is here](url)`, the fun/arity
will get translated to the new href of the function.
"""
- def locals(bin, locals, aliases \\ [], extension \\ ".html", lib_dirs \\ elixir_lib_dirs()) do
- fun_re =
- Regex.source(
- ~r{(([ct]:)?([a-z_]+[A-Za-z_\d]*[\\?\\!]?|[\{\}=&\\|\\.<>~*^@\\+\\%\\!-\/]+)/\d+)}
- )
-
- regex = ~r{(?
- {prefix, _, function, arity} = split_function(match)
- text = "`#{function}/#{arity}`"
-
- cond do
- match in locals ->
- "[#{text}](##{prefix}#{enc_h(function)}/#{arity})"
-
- match in @basic_type_strings ->
- "[#{text}](#{elixir_docs}#{@basic_types_page})"
-
- match in @built_in_type_strings ->
- "[#{text}](#{elixir_docs}#{@built_in_types_page})"
+ def locals(string, locals, aliases \\ [], extension \\ ".html", lib_dirs \\ elixir_lib_dirs()) do
+ options = %{
+ locals: locals,
+ aliases: aliases,
+ extension: extension,
+ lib_dirs: lib_dirs
+ }
- match in @kernel_function_strings ->
- "[#{text}](#{elixir_docs}Kernel#{extension}##{prefix}#{enc_h(function)}/#{arity})"
+ link(string, :elixir, :function, options)
+ end
- match in @special_form_strings ->
- "[#{text}](#{elixir_docs}Kernel.SpecialForms" <>
- "#{extension}##{prefix}#{enc_h(function)}/#{arity})"
+ @doc false
+ def elixir_modules(
+ string,
+ module_refs,
+ module_id \\ nil,
+ extension \\ ".html",
+ lib_dirs \\ elixir_lib_dirs()
+ )
+ when is_binary(string) do
+ options = %{
+ module_refs: module_refs,
+ module_id: module_id,
+ extension: extension,
+ lib_dirs: lib_dirs
+ }
- true ->
- all
- end
- end)
+ link(string, :elixir, :module, options)
end
@doc """
@@ -370,112 +376,288 @@ defmodule ExDoc.Formatter.HTML.Autolink do
`[my link Module.link/1 is here](url)`, the Module.fun/arity
will get translated to the new href of the function.
"""
- def elixir_functions(bin, project_funs, extension \\ ".html", lib_dirs \\ elixir_lib_dirs())
- when is_binary(bin) do
- bin
- |> replace_custom_links(project_funs, extension, lib_dirs)
- |> replace_normal_links(project_funs, extension, lib_dirs)
+ def elixir_functions(string, docs_refs, extension \\ ".html", lib_dirs \\ elixir_lib_dirs())
+ when is_binary(string) do
+ options = %{
+ docs_refs: docs_refs,
+ extension: extension,
+ lib_dirs: lib_dirs
+ }
+
+ link(string, :elixir, :function, options)
end
- module_re = Regex.source(~r{(([A-Z][A-Za-z_\d]+)\.)+})
+ @doc """
+ Helper function for autolinking elixir modules.
- fun_re =
- Regex.source(
- ~r{([ct]:)?((#{module_re})?(([a-z_]+[A-Za-z_\d]*[\\?\\!]?)|[\{\}=&\\|\\.<>~*^@\\+\\%\\!-\/]+)/\d+)}
- )
+ Ignores modules which are already wrapped in markdown url syntax,
+ e.g. `[Module](url)`. If the module name doesn't touch the leading
+ or trailing `]`, e.g. `[my link Module is here](url)`, the Module
+ will get translated to the new href of the module.
+ """
+ def erlang_modules(string) when is_binary(string) do
+ link(string, :erlang, :module, %{})
+ end
- @custom_re ~r{\[(.*?)\]\(`(#{fun_re})`\)}
- @normal_re ~r{(?
- replacement(all, match, project_funs, extension, lib_dirs, text)
- end)
+ Only links modules that are in the Erlang distribution `lib_dir`
+ and only link functions in those modules that export a function of the
+ same name and arity.
+
+ Ignores functions which are already wrapped in markdown url syntax,
+ e.g. `[:module.test/1](url)`. If the function doesn't touch the leading
+ or trailing `]`, e.g. `[my link :module.link/1 is here](url)`, the :module.fun/arity
+ will get translated to the new href of the function.
+ """
+ def erlang_functions(string) when is_binary(string) do
+ link(string, :erlang, :function, %{})
+ end
+
+ # Helper function for autolinking functions and modules.
+ #
+ # It autolinks all links for a certain `language` and of a certain `kind`.
+ #
+ # `language` can be: `:elixir`, `:erlang` or `:markdown`.
+ #
+ # `kind` is either `:function` or `:module`.
+ #
+ # It accepts a list of `options` used in the replacement functions.
+ # - `:aliases
+ # - `:docs_refs`
+ # - `:extension` - Default value is `".html"`
+ # - `:lib_dirs`
+ # - `:locals` - A list of local functions
+ # - `:module_id` - Module of the current doc. Default value is `nil`
+ # - `:modules_refs` - List of modules available
+ #
+ # Internal options:
+ # - `:preprocess?` - `true` or `false`. Do preprocessing and postprocessing, such as replacing backticks
+ # with a token
+ defp link(string, language, kind, options) when is_map(options) do
+ options = Map.put_new(options, :preprocess?, true)
+
+ string
+ |> link_process(:preprocess, options.preprocess?)
+ |> link(language, kind, :custom, options)
+ |> link(language, kind, :normal, options)
+ |> link_process(:postprocess, options.preprocess?)
+ end
+
+ defp link(string, language, kind, link_type, options) when is_map(options) do
+ regex = regex_link_type(language, kind, link_type)
+ replace_fun = replace_fun(language, kind, link_type, options)
+
+ result = link_process(string, :preprocess, options.preprocess?)
+ result = Regex.replace(regex, result, replace_fun)
+ result = link_process(result, :postprocess, options.preprocess?)
+
+ result
end
- defp replace_normal_links(bin, project_funs, extension, lib_dirs) do
- Regex.replace(@normal_re, bin, fn all, match ->
- replacement(all, match, project_funs, extension, lib_dirs)
+ @doc false
+ def link_everything(string, options \\ %{}) when is_map(options) do
+ # disable preprocess every time we run link/4,
+ # and transform the string manually before and after Enum.reduce
+ options = Map.put_new(options, :preprocess?, false)
+
+ string = preprocess(string)
+
+ string =
+ Enum.reduce(@regexes, string, fn %{
+ kind: kind,
+ language: language,
+ link_type: link_type
+ },
+ acc ->
+ link(acc, language, kind, link_type, options)
+ end)
+
+ postprocess(string)
+ end
+
+ defp link_process(string, _, false),
+ do: string
+
+ defp link_process(string, :preprocess, true),
+ do: preprocess(string)
+
+ defp link_process(string, :postprocess, true),
+ do: postprocess(string)
+
+ @doc false
+ # Replaces all backticks inside the text of custom links with @backtick_token
+ def preprocess(string) do
+ regex = ~r{
+ \[([^\]]*?`[^\]]*?)\]
+ \(([^\)]*?)\)
+ }x
+
+ Regex.replace(regex, string, fn _all, text, link ->
+ new_text = String.replace(text, :binary.compile_pattern("`"), @backtick_token)
+ "[#{new_text}](#{link})"
end)
end
- defp replacement(all, match, project_funs, extension, lib_dirs, text \\ nil) do
- {prefix, module, function, arity} = split_function(match)
- text = text || "`#{module}.#{function}/#{arity}`"
+ @doc false
+ # Reverts the changes done by `preprocess/1`.
+ def postprocess(string) do
+ String.replace(string, :binary.compile_pattern(@backtick_token), "`")
+ end
- aliases = []
- elixir_docs = get_elixir_docs(aliases, lib_dirs)
+ defp replace_fun(language, kind, :normal, options) do
+ fn all, match ->
+ replacement(all, language, kind, match, options)
+ end
+ end
- cond do
- match in project_funs ->
- "[#{text}](#{module}#{extension}##{prefix}#{enc_h(function)}/#{arity})"
+ defp replace_fun(language, kind, :custom, options) do
+ fn all, text, match ->
+ replacement(all, language, kind, match, text, options)
+ end
+ end
+
+ # The heart of the autolinking logic
+ defp replacement(string, language, kind, match, text \\ nil, options)
- match in @kernel_function_strings ->
- "[#{text}](#{elixir_docs}Kernel#{extension}##{prefix}#{enc_h(function)}/#{arity})"
+ defp replacement(string, :erlang, kind, match, text, options) do
+ lib_dirs = Map.get(options, :lib_dirs, default_lib_dirs(:erlang))
- match in @special_form_strings ->
- "[#{text}](#{elixir_docs}Kernel.SpecialForms#{extension}##{prefix}#{enc_h(function)}/#{
- arity
- })"
+ pmfa = {_prefix, module, function, arity} = split_function(match)
+ text = text || default_text(:erlang, kind, match, pmfa)
- doc = lib_dirs_to_doc("Elixir." <> module, lib_dirs) ->
- "[#{text}](#{doc}#{module}.html##{prefix}#{enc_h(function)}/#{arity})"
+ if doc = module_docs(:erlang, module, lib_dirs) do
+ case kind do
+ :module ->
+ "[#{text}](#{doc}#{module}.html)"
- true ->
- all
+ :function ->
+ "[#{text}](#{doc}#{module}.html##{function}-#{arity})"
+ end
+ else
+ string
end
end
- @doc """
- Helper function for autolinking elixir modules.
+ defp replacement(string, :elixir, kind, match, text, options) do
+ aliases = Map.get(options, :aliases, [])
+ docs_refs = Map.get(options, :docs_refs, [])
+ extension = Map.get(options, :extension, ".html")
+ lib_dirs = Map.get(options, :lib_dirs, default_lib_dirs(:elixir))
+ locals = Map.get(options, :locals, [])
+ module_id = Map.get(options, :module_id, nil)
+ modules_refs = Map.get(options, :modules_refs, [])
- Ignores modules which are already wrapped in markdown url syntax,
- e.g. `[Module](url)`. If the module name doesn't touch the leading
- or trailing `]`, e.g. `[my link Module is here](url)`, the Module
- will get translated to the new href of the module.
- """
- def elixir_modules(
- bin,
- modules,
- module_id \\ nil,
- extension \\ ".html",
- lib_dirs \\ elixir_lib_dirs()
- )
- when is_binary(bin) do
- regex = ~r{(?
- cond do
- match == module_id ->
- "[`#{match}`](#{match}#{extension}#content)"
+ elixir_docs = get_elixir_docs(aliases, lib_dirs)
- match in modules ->
- "[`#{match}`](#{match}#{extension})"
+ case kind do
+ :module ->
+ cond do
+ match == module_id ->
+ "[`#{match}`](#{match}#{extension}#content)"
- doc = lib_dirs_to_doc("Elixir." <> match, lib_dirs) ->
- "[`#{match}`](#{doc}#{match}.html)"
+ match in modules_refs ->
+ "[`#{match}`](#{match}#{extension})"
- true ->
- all
- end
- end)
+ doc = module_docs(:elixir, match, lib_dirs) ->
+ "[`#{match}`](#{doc}#{match}.html)"
+
+ true ->
+ string
+ end
+
+ :function ->
+ cond do
+ match in locals ->
+ "[#{text}](##{prefix}#{enc_h(function)}/#{arity})"
+
+ match in docs_refs ->
+ "[#{text}](#{module}#{extension}##{prefix}#{enc_h(function)}/#{arity})"
+
+ match in @basic_type_strings ->
+ "[#{text}](#{elixir_docs}#{@basic_types_page})"
+
+ match in @built_in_type_strings ->
+ "[#{text}](#{elixir_docs}#{@built_in_types_page})"
+
+ match in @kernel_function_strings ->
+ "[#{text}](#{elixir_docs}Kernel#{extension}##{prefix}#{enc_h(function)}/#{arity})"
+
+ match in @special_form_strings ->
+ "[#{text}](#{elixir_docs}Kernel.SpecialForms#{extension}##{prefix}#{enc_h(function)}/#{
+ arity
+ })"
+
+ doc = module_docs(:elixir, module, lib_dirs) ->
+ "[#{text}](#{doc}#{module}.html##{prefix}#{enc_h(function)}/#{arity})"
+
+ true ->
+ string
+ end
+ end
end
- defp split_function("c:" <> bin) do
- {_, mod, fun, arity} = split_function(bin)
+ ## Helpers
+
+ defp default_text(:erlang, _kind, match, {_prefix, _module, _function, _arity}),
+ do: "`#{match}`"
+
+ defp default_text(:elixir, _kind, _match, {_prefix, module, function, arity}) do
+ if module == "" do
+ # local
+ "`#{function}/#{arity}`"
+ else
+ "`#{module}.#{function}/#{arity}`"
+ end
+ end
+
+ defp default_lib_dirs(:elixir),
+ do: elixir_lib_dirs()
+
+ defp default_lib_dirs(:erlang),
+ do: erlang_lib_dirs()
+
+ defp module_docs(:elixir, module, lib_dirs),
+ do: lib_dirs_to_doc("Elixir." <> module, lib_dirs)
+
+ defp module_docs(:erlang, module, lib_dirs),
+ do: lib_dirs_to_doc(module, lib_dirs)
+
+ @doc false
+ def split_function(string) when is_binary(string),
+ do: split_function_string(string)
+
+ defp split_function_string("c:" <> string) do
+ {_, mod, fun, arity} = split_function(string)
{"c:", mod, fun, arity}
end
- defp split_function("t:" <> bin) do
- {_, mod, fun, arity} = split_function(bin)
+ defp split_function_string("t:" <> string) do
+ {_, mod, fun, arity} = split_function_string(string)
{"t:", mod, fun, arity}
end
- defp split_function(bin) when is_binary(bin) do
- split_function(String.split(bin, "/"))
+ defp split_function_string(":" <> string) do
+ {_, mod, fun, arity} = split_function_string(string)
+ {":", mod, fun, arity}
end
- defp split_function([modules, arity]) do
+ defp split_function_string(string) do
+ string
+ |> String.split("/")
+ |> split_function_list()
+ end
+
+ # handles a single module
+ defp split_function_list([module]) do
+ {"", module, "", ""}
+ end
+
+ defp split_function_list([modules, arity]) do
{mod, name} =
modules
# this handles the case of the ".." function
@@ -487,39 +669,10 @@ defmodule ExDoc.Formatter.HTML.Autolink do
end
# handles "/" function
- defp split_function([modules, "", arity]) do
- split_function([modules <> "/", arity])
+ defp split_function_list([modules, "", arity]) when is_binary(modules) do
+ split_function_list([modules <> "/", arity])
end
- @doc """
- Helper function for autolinking erlang functions.
-
- Only links modules that are in the Erlang distribution `lib_dir`
- and only link functions in those modules that export a function of the
- same name and arity.
-
- Ignores functions which are already wrapped in markdown url syntax,
- e.g. `[:module.test/1](url)`. If the function doesn't touch the leading
- or trailing `]`, e.g. `[my link :module.link/1 is here](url)`, the :module.fun/arity
- will get translated to the new href of the function.
- """
- def erlang_functions(bin) when is_binary(bin) do
- lib_dirs = erlang_lib_dirs()
- regex = ~r{(?
- {_, module, function, arity} = split_function(match)
-
- if doc = lib_dirs_to_doc(module, lib_dirs) do
- "[`:#{match}`](#{doc}#{module}.html##{function}-#{arity})"
- else
- all
- end
- end)
- end
-
- ## Helpers
-
defp doc_prefix(%{type: c}) when c in [:callback, :macrocallback], do: "c:"
defp doc_prefix(%{type: _}), do: ""
@@ -613,4 +766,150 @@ defmodule ExDoc.Formatter.HTML.Autolink do
defp get_elixir_docs(aliases, lib_dirs) do
get_source(Kernel, aliases, lib_dirs)
end
+
+ @doc false
+ def backtick_token(), do: @backtick_token
+
+ ## REGULAR EXPRESSION HELPERS
+
+ # Returns a the string source of a regular expression,
+ # given the `name` and `language`
+ defp re_source(name, language \\ :elixir) do
+ Regex.source(re(name, language))
+ end
+
+ # Returns a regular expression
+ # given the `name` and `language`
+ defp re(:prefix, :elixir) do
+ ~r{
+ [ct]: # c:, t:
+ }x
+ end
+
+ defp re(:m, :elixir) do
+ ~r{
+ ( [A-Z] # start with uppercase letter
+ [_a-zA-Z0-9]*\.? # followed by optional letter, number or underscore
+ )+ # this pattern could be repeated
+ (?~*^@\\+\\%\\!-\/]+ # special_form
+ }x
+ end
+
+ defp re(:f, :erlang) do
+ ~r{
+# TODO: revise the erlang rules for function names
+ [0-9a-zA-Z_!\\?]+
+ }x
+ end
+
+ defp re(:fa, language) when language in [:elixir, :erlang] do
+ ~r{
+ (#{re_source(:f, language)}) # function_name
+ /\d+ # /arity
+ }x
+ end
+
+ defp re(:mfa, :elixir) do
+ ~r{
+ (#{re_source(:prefix)})? # optional callback/type identifier or ":"
+ (
+ (#{re_source(:m)}\.)
+ #{re_source(:fa)}
+ )
+ }x
+ end
+
+ defp re(:mfa, :erlang) do
+ ~r{
+ #{re_source(:m, :erlang)} # module_name
+ \. # "."
+ #{re_source(:fa, :erlang)} # function_name/arity
+ }x
+ end
+
+ defp re(:local, :elixir) do
+ ~r{
+ (#{re_source(:prefix)})? # optional callback or type identifier
+ #{re_source(:fa)} # function_name/arity
+ }x
+ end
+
+ defp re(:modules, :elixir) do
+ ~r{
+ #{re_source(:m)}
+ }x
+ end
+
+ defp re(:modules, :erlang) do
+ ~r{
+ #{re_source(:m, :erlang)}
+ }x
+ end
+
+ defp re(:functions, :elixir) do
+ ~r{
+ (#{re_source(:local)}) | (#{re_source(:mfa)})
+ }x
+ end
+
+ defp re(:functions, :erlang) do
+ ~r{
+ #{re_source(:mfa, :erlang)}
+ }x
+ end
+
+ defp re({:normal_link, function_re_source}, :markdown) do
+ ~r{
+ (? :functions
+ :module -> :modules
+ end
+
+ case link_type do
+ :normal ->
+ re({:normal_link, re_source(group, language)}, :markdown)
+
+ :custom ->
+ re({:custom_link, re_source(group, language)}, :markdown)
+ end
+ end
end
diff --git a/test/ex_doc/formatter/html/autolink_test.exs b/test/ex_doc/formatter/html/autolink_test.exs
index 2eaba1a21..26f3268eb 100644
--- a/test/ex_doc/formatter/html/autolink_test.exs
+++ b/test/ex_doc/formatter/html/autolink_test.exs
@@ -160,7 +160,7 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
end
test "autolinks types" do
- # use the same approach for elixir_functions as for localss
+ # use the same approach for elixir_functions as for locals
assert Autolink.elixir_functions(
"`t:MyModule.my_type/0`",
["t:MyModule.my_type/0"]
@@ -191,6 +191,18 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
"[the `Mod.example/1`](foo)"
end
+ test "supports normal links" do
+ assert Autolink.elixir_functions("`Mod.example/1`", ["Mod.example/1"]) ==
+ "[`Mod.example/1`](Mod.html#example/1)"
+
+ assert Autolink.elixir_functions("(`Mod.example/1`)", ["Mod.example/1"]) ==
+ "([`Mod.example/1`](Mod.html#example/1))"
+
+ # It ignores links preceded by "]("
+ assert Autolink.elixir_functions("](`Mod.example/1`)", ["Mod.example/1"]) ==
+ "](`Mod.example/1`)"
+ end
+
test "supports custom links" do
assert Autolink.elixir_functions("[`example`](`Mod.example/1`)", ["Mod.example/1"]) ==
"[`example`](Mod.html#example/1)"
@@ -224,6 +236,29 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
assert Autolink.elixir_functions("[`is_boolean`](`is_boolean/1`)", []) ==
"[`is_boolean`](#{@elixir_docs}elixir/Kernel.html#is_boolean/1)"
+
+ assert Autolink.elixir_functions("[term()](`t:term/0`)", []) ==
+ "[term()](#{@elixir_docs}elixir/typespecs.html#built-in-types)"
+
+ assert Autolink.elixir_functions("[term\(\)](`t:term/0`)", []) ==
+ "[term\(\)](#{@elixir_docs}elixir/typespecs.html#built-in-types)"
+
+ assert Autolink.elixir_functions("[`term()`](`t:term/0`)", []) ==
+ "[`term()`](#{@elixir_docs}elixir/typespecs.html#built-in-types)"
+
+ assert Autolink.elixir_functions("[`term()`](`t:term/0`)", []) ==
+ "[`term()`](#{@elixir_docs}elixir/typespecs.html#built-in-types)"
+
+ assert Autolink.elixir_functions("[version](`t:Version.version/0`)", ["t:Version.version/0"]) ==
+ "[version](Version.html#t:version/0)"
+
+ assert Autolink.link_everything("[version](`t:Version.version/0`)",
+ %{docs_refs: ["t:Version.version/0"]}
+ ) == "[version](Version.html#t:version/0)"
+
+ # assert Autolink.link_everything("[version](`t:version/0`)", %{locals: ["t:Version.version/0"]}) ==
+ assert Autolink.link_everything("[version](`t:version/0`)", %{locals: ["t:version/0"]}) ==
+ "[version](#t:version/0)"
end
end
@@ -279,6 +314,45 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
assert Autolink.elixir_modules("[the `Mod.Nested`](other.html)", ["Mod.Nested"]) ==
"[the `Mod.Nested`](other.html)"
+
+ assert Autolink.elixir_modules("[in the `Kernel` module](Kernel.html#guards)", ["Kernel"]) ==
+ "[in the `Kernel` module](Kernel.html#guards)"
+
+ assert Autolink.elixir_modules("[in the `Kernel` module](Kernel.html#guards)", []) ==
+ "[in the `Kernel` module](Kernel.html#guards)"
+
+ assert Autolink.link_everything("[in the `Kernel` module](Kernel.html#guards)") ==
+ "[in the `Kernel` module](Kernel.html#guards)"
+ end
+ end
+
+ describe "Erlang modules" do
+ test "autolinks to Erlang modules" do
+ assert Autolink.erlang_modules("`:erlang`") == "[`:erlang`](#{@erlang_docs}erlang.html)"
+
+ assert Autolink.erlang_modules("`:erl_prim_loader`") ==
+ "[`:erl_prim_loader`](#{@erlang_docs}erl_prim_loader.html)"
+ end
+
+ test "autolinks to Erlang modules with custom links" do
+ assert Autolink.erlang_modules("[`example`](`:lists`)") ==
+ "[`example`](#{@erlang_docs}lists.html)"
+
+ assert Autolink.erlang_modules("[example](`:lists`)") ==
+ "[example](#{@erlang_docs}lists.html)"
+ end
+
+ test "does not autolink pre-linked docs" do
+ assert Autolink.erlang_modules("[`:erlang`](other.html)") == "[`:erlang`](other.html)"
+
+ assert Autolink.erlang_modules("[the `:erlang` module](other.html)") ==
+ "[the `:erlang` module](other.html)"
+
+ assert Autolink.erlang_modules("`:erlang`") == "[`:erlang`](#{@erlang_docs}erlang.html)"
+ end
+
+ test "does not autolink functions that aren't part of the Erlang distribution" do
+ assert Autolink.erlang_modules("`:unknown.foo/0`") == "`:unknown.foo/0`"
end
end
@@ -302,6 +376,14 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
"[`:zlib.deflateInit/2`](#{@erlang_docs}zlib.html#deflateInit-2)"
end
+ test "autolinks to Erlang functions with custom links" do
+ assert Autolink.erlang_functions("[`example`](`:lists.reverse/1`)") ==
+ "[`example`](#{@erlang_docs}lists.html#reverse-1)"
+
+ assert Autolink.erlang_functions("[example](`:lists.reverse/1`)") ==
+ "[example](#{@erlang_docs}lists.html#reverse-1)"
+ end
+
test "does not autolink pre-linked docs" do
assert Autolink.erlang_functions("[`:erlang.apply/2`](other.html)") ==
"[`:erlang.apply/2`](other.html)"
@@ -309,6 +391,15 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
assert Autolink.erlang_functions("[the `:erlang.apply/2`](other.html)") ==
"[the `:erlang.apply/2`](other.html)"
+ assert Autolink.erlang_functions("[the `:erlang.apply/2` function](`Kernel.apply/2`)") ==
+ "[the `:erlang.apply/2` function](`Kernel.apply/2`)"
+
+ assert Autolink.erlang_functions("[the :erlang.apply/2 function](`Kernel.apply/2`)") ==
+ "[the :erlang.apply/2 function](`Kernel.apply/2`)"
+
+ assert Autolink.erlang_functions("[the `:erlang.apply/2` function](other.html)") ==
+ "[the `:erlang.apply/2` function](other.html)"
+
assert Autolink.erlang_functions("`:erlang`") == "`:erlang`"
end
@@ -472,9 +563,59 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
end
end
+ describe "corner-cases" do
+ test "accepts functions around () and []" do
+ assert Autolink.locals("`===/2`", [], [Kernel]) === "[`===/2`](Kernel.html#===/2)"
+ assert Autolink.locals("(`===/2`)", [], [Kernel]) === "([`===/2`](Kernel.html#===/2))"
+ assert Autolink.locals("[`===/2`]", [], [Kernel]) === "[[`===/2`](Kernel.html#===/2)]"
+
+ output = Autolink.link_everything("`===/2`")
+ assert output === "[`===/2`](#{@elixir_docs}elixir/Kernel.html#===/2)"
+ assert Autolink.link_everything("(`===/2`)") === "(" <> output <> ")"
+ assert Autolink.link_everything("[`===/2`]") === "[" <> output <> "]"
+ end
+ end
+
defp assert_typespec_placeholders(original, expected, typespecs, aliases \\ []) do
ast = Code.string_to_quoted!(original)
{actual, _} = Autolink.format_and_extract_typespec_placeholders(ast, typespecs, aliases, [])
assert actual == expected, "Original: #{original}\nExpected: #{expected}\nActual: #{actual}"
end
+
+ describe "backtick preprocessing" do
+ test "replace backticks" do
+ assert Autolink.preprocess("[`===/2`](foo)") ===
+ "[#{Autolink.backtick_token()}===/2#{Autolink.backtick_token()}](foo)"
+ end
+
+ test "do not touch backticks" do
+ assert Autolink.preprocess("`===/2`") === "`===/2`"
+ assert Autolink.preprocess("(`===/2`)") === "(`===/2`)"
+ assert Autolink.preprocess("(foo)[`Module`]") === "(foo)[`Module`]"
+
+ # this tests a bug in the regex that was being too greedy and stretching for several links
+ string = """
+ A [version](`t:version/0`) is a [string](`t:String.t/0`) in a specific
+ format or a [version](`t:Version.t/0`) struct
+ generated after parsing a version string with `Version.parse/1`.
+ """
+
+ assert Autolink.preprocess(string) === string
+ end
+
+ test "replace backtick tokens" do
+ assert Autolink.postprocess(
+ "[#{Autolink.backtick_token()}===/2#{Autolink.backtick_token()}](foo)"
+ ) === "[`===/2`](foo)"
+
+ string = """
+ [A `version` is](`t:version/0`) a [beautiful `string` in a](`t:String.t/0`) specific
+ format or a [`version`](`t:Version.t/0`) struct
+ generated after parsing a version string with `Version.parse/1`.
+ """
+
+ refute Autolink.preprocess(string) === string
+ assert string |> Autolink.preprocess() |> Autolink.postprocess() === string
+ end
+ end
end