Skip to content

Commit

Permalink
Allow custom links for Elixir functions (#813)
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach committed Dec 19, 2017
1 parent c823727 commit 2728963
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 19 deletions.
56 changes: 39 additions & 17 deletions lib/ex_doc/formatter/html/autolink.ex
Expand Up @@ -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{(?<!\[)`\s*(#{fun_re})\s*`(?!\])}

Regex.replace(regex, bin, fn all, match ->
{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{(?<!\[)`\s*(#{fun_re})\s*`(?!\])}

doc = lib_dirs_to_doc("Elixir." <> 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.
Expand All @@ -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
Expand Down
22 changes: 20 additions & 2 deletions test/ex_doc/formatter/html/autolink_test.exs
Expand Up @@ -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
Expand Down

0 comments on commit 2728963

Please sign in to comment.