diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index ae7c5a976..7a9169096 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -415,32 +415,51 @@ defmodule ExDoc.Formatter.HTML.Autolink do Project functions are specified in `project_funs` as a list of `Module.fun/arity` tuples. - 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 + Functions wrapped in markdown url syntax can link to other docs if + the url is wrapped in backticks, otherwise the url is used as is. + 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 elixir_functions(bin, project_funs, extension \\ ".html", lib_dirs \\ elixir_lib_dirs()) when is_binary(bin) do - module_re = Regex.source(~r{(([A-Z][A-Za-z_\d]+)\.)+}) - fun_re = Regex.source(~r{([ct]:)?(#{module_re}([a-z_]+[A-Za-z_\d]*[\\?\\!]?|[\{\}=&\\|\\.<>~*^@\\+\\%\\!-]+)/\d+)}) - regex = ~r{(? - {prefix, module, function, arity} = split_function(match) + bin + |> replace_custom_links(project_funs, extension, lib_dirs) + |> replace_normal_links(project_funs, extension, lib_dirs) + end - cond do - match in project_funs -> - "[`#{module}.#{function}/#{arity}`](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" + module_re = Regex.source(~r{(([A-Z][A-Za-z_\d]+)\.)+}) + fun_re = Regex.source(~r{([ct]:)?(#{module_re}([a-z_]+[A-Za-z_\d]*[\\?\\!]?|[\{\}=&\\|\\.<>~*^@\\+\\%\\!-]+)/\d+)}) + @custom_re ~r{\[(.*)\]\(`(#{fun_re})`\)} + @normal_re ~r{(? module, lib_dirs) -> - "[`#{module}.#{function}/#{arity}`](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" + def replace_custom_links(bin, project_funs, extension, lib_dirs) do + Regex.replace(@custom_re, bin, fn all, text, match -> + replacement(all, match, project_funs, extension, lib_dirs, text) + end) + end - true -> - all - end + def 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) end) end + def replacement(all, match, project_funs, extension, lib_dirs, text \\ nil) do + {prefix, module, function, arity} = split_function(match) + text = text || "`#{module}.#{function}/#{arity}`" + + cond do + match in project_funs -> + "[#{text}](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" + + doc = lib_dirs_to_doc("Elixir." <> module, lib_dirs) -> + "[#{text}](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" + + true -> + all + end + end + @doc """ Create links to elixir modules defined in the project and in Elixir itself. @@ -458,10 +477,13 @@ defmodule ExDoc.Formatter.HTML.Autolink do cond do match == module_id -> "[`#{match}`](#{match}#{extension}#content)" + match in modules -> "[`#{match}`](#{match}#{extension})" + doc = lib_dirs_to_doc("Elixir." <> match, lib_dirs) -> "[`#{match}`](#{doc}#{match}.html)" + true -> all end diff --git a/test/ex_doc/formatter/html/autolink_test.exs b/test/ex_doc/formatter/html/autolink_test.exs index eb1258ccc..bc3f3b659 100644 --- a/test/ex_doc/formatter/html/autolink_test.exs +++ b/test/ex_doc/formatter/html/autolink_test.exs @@ -121,14 +121,32 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do test "autolink functions doesn't create links for pre-linked Mod.functions docs" do assert Autolink.elixir_functions("[`Mod.example/1`]()", ["Mod.example/1"]) == "[`Mod.example/1`]()" assert Autolink.elixir_functions("[the `Mod.example/1`]()", ["Mod.example/1"]) == "[the `Mod.example/1`]()" + + assert Autolink.elixir_functions("[`Mod.example/1`](foo)", ["Mod.example/1"]) == "[`Mod.example/1`](foo)" + assert Autolink.elixir_functions("[the `Mod.example/1`](foo)", ["Mod.example/1"]) == "[the `Mod.example/1`](foo)" + end + + test "autolink functions create custom links" do + assert Autolink.elixir_functions("[`example`](`Mod.example/1`)", ["Mod.example/1"]) == + "[`example`](Mod.html#example/1)" + assert Autolink.elixir_functions("[the `example`](`Mod.example/1`)", ["Mod.example/1"]) == + "[the `example`](Mod.html#example/1)" + assert Autolink.elixir_functions("[the `Mod.example/1`](`Mod.example/1`)", ["Mod.example/1"]) == + "[the `Mod.example/1`](Mod.html#example/1)" + + assert Autolink.elixir_functions("[`callback(a)`](`c:Foo.foo/1`)", ["c:Foo.foo/1"]) == + "[`callback(a)`](Foo.html#c:foo/1)" + + assert Autolink.elixir_functions("[the `upcase`](`String.upcase/1`)", []) == + "[the `upcase`](#{@elixir_docs}elixir/String.html#upcase/1)" end test "autolink functions to types in the project" do # use the same approach for elixir_functions as for local_docs assert Autolink.elixir_functions("`t:MyModule.my_type/0`", - ["t:MyModule.my_type/0"]) == "[`MyModule.my_type/0`](MyModule.html#t:my_type/0)" + ["t:MyModule.my_type/0"]) == "[`MyModule.my_type/0`](MyModule.html#t:my_type/0)" assert Autolink.elixir_functions("`t:MyModule.my_type`", - ["t:MyModule.my_type/0"]) == "`t:MyModule.my_type`" + ["t:MyModule.my_type/0"]) == "`t:MyModule.my_type`" end # elixir_modules