From 528e7901e28e0367e455e251c62b7a44bb8e4a39 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Mon, 18 Dec 2017 18:31:58 +0100 Subject: [PATCH 1/5] Allow custom links in Autolink.elixir_functions --- lib/ex_doc/formatter/html/autolink.ex | 25 +++++++++++++------- test/ex_doc/formatter/html/autolink_test.exs | 22 +++++++++++++++-- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index ae7c5a976..2925e2af3 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -421,19 +421,28 @@ defmodule ExDoc.Formatter.HTML.Autolink do 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) + module_re = ~r{(?|(?|[A-Z][A-Za-z_\d]+)\.)*}.source + fun_re = ~r{([ct]:)?(#{module_re}([a-z_]+[A-Za-z_\d]*[\\?\\!]?|[\{\}=&\\|\\.<>~*^@\\+\\%\\!-]+)/\d+)}.source + normal_link_re = ~r{(? + {text, match, prefix, module, function, arity} = + if custom_match == "" do + {prefix, module, function, arity} = split_function(normal_match) + {"`#{module}.#{function}/#{arity}`", normal_match, prefix, module, function, arity} + else + {prefix, module, function, arity} = split_function(custom_match) + {custom_text, custom_match, prefix, module, function, arity} + end cond do match in project_funs -> - "[`#{module}.#{function}/#{arity}`](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" + "[#{text}](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" doc = lib_dirs_to_doc("Elixir." <> module, lib_dirs) -> - "[`#{module}.#{function}/#{arity}`](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" + "[#{text}](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" true -> all 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 From 07f7e4d0cfb7d57a50ac4b1b82ec5928137ac1a4 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 19 Dec 2017 16:35:41 +0100 Subject: [PATCH 2/5] Revert "Allow custom links in Autolink.elixir_functions" This reverts commit 528e7901e28e0367e455e251c62b7a44bb8e4a39. --- lib/ex_doc/formatter/html/autolink.ex | 25 +++++++------------- test/ex_doc/formatter/html/autolink_test.exs | 22 ++--------------- 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index 2925e2af3..ae7c5a976 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -421,28 +421,19 @@ defmodule ExDoc.Formatter.HTML.Autolink do 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 = ~r{(?|(?|[A-Z][A-Za-z_\d]+)\.)*}.source - fun_re = ~r{([ct]:)?(#{module_re}([a-z_]+[A-Za-z_\d]*[\\?\\!]?|[\{\}=&\\|\\.<>~*^@\\+\\%\\!-]+)/\d+)}.source - normal_link_re = ~r{(? - {text, match, prefix, module, function, arity} = - if custom_match == "" do - {prefix, module, function, arity} = split_function(normal_match) - {"`#{module}.#{function}/#{arity}`", normal_match, prefix, module, function, arity} - else - {prefix, module, function, arity} = split_function(custom_match) - {custom_text, custom_match, prefix, module, function, arity} - end + 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) cond do match in project_funs -> - "[#{text}](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" + "[`#{module}.#{function}/#{arity}`](#{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})" + "[`#{module}.#{function}/#{arity}`](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" true -> all diff --git a/test/ex_doc/formatter/html/autolink_test.exs b/test/ex_doc/formatter/html/autolink_test.exs index bc3f3b659..eb1258ccc 100644 --- a/test/ex_doc/formatter/html/autolink_test.exs +++ b/test/ex_doc/formatter/html/autolink_test.exs @@ -121,32 +121,14 @@ 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 From 36576e3faf3c3267d08a93f5ec832b4ecd991eb9 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 19 Dec 2017 16:46:37 +0100 Subject: [PATCH 3/5] First stab --- lib/ex_doc/formatter/html/autolink.ex | 34 ++++++++++++++++++-- test/ex_doc/formatter/html/autolink_test.exs | 22 +++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index ae7c5a976..eb7d84b64 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -421,11 +421,38 @@ defmodule ExDoc.Formatter.HTML.Autolink do 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) + end + + def replace_custom_links(bin, project_funs, extension, 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{(? + Regex.replace(custom_re, bin, fn all, text, match -> + {prefix, module, function, arity} = split_function(match) + + 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) + end + + def replace_normal_links(bin, project_funs, extension, 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+)}) + normal_re = ~r{(? {prefix, module, function, arity} = split_function(match) cond do @@ -458,10 +485,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 From 6a2dabf4cc7ade676604fcabea5bc067f200fca5 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 19 Dec 2017 16:53:44 +0100 Subject: [PATCH 4/5] DRY up a bit --- lib/ex_doc/formatter/html/autolink.ex | 52 +++++++++++---------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index eb7d84b64..69b104b81 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -426,46 +426,36 @@ defmodule ExDoc.Formatter.HTML.Autolink do |> replace_normal_links(project_funs, extension, lib_dirs) end - def replace_custom_links(bin, project_funs, extension, 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+)}) - custom_re = ~r{\[(.*)\]\(`(#{fun_re})`\)} - - Regex.replace(custom_re, bin, fn all, text, match -> - {prefix, module, function, arity} = split_function(match) - - 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})" + 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{(? - all - end + def replace_custom_links(bin, project_funs, extension, lib_dirs) when is_binary(bin) do + Regex.replace(@custom_re, bin, fn all, text, match -> + replacement(all, match, project_funs, extension, lib_dirs) end) end def replace_normal_links(bin, project_funs, extension, 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+)}) - normal_re = ~r{(? + replacement(all, match, project_funs, extension, lib_dirs) + end) + end - Regex.replace(normal_re, bin, fn all, match -> - {prefix, module, function, arity} = split_function(match) + def replacement(all, match, project_funs, extension, lib_dirs) do + {prefix, module, function, arity} = split_function(match) - cond do - match in project_funs -> - "[`#{module}.#{function}/#{arity}`](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" + cond do + match in project_funs -> + "[`#{module}.#{function}/#{arity}`](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" - doc = lib_dirs_to_doc("Elixir." <> module, lib_dirs) -> - "[`#{module}.#{function}/#{arity}`](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" + doc = lib_dirs_to_doc("Elixir." <> module, lib_dirs) -> + "[`#{module}.#{function}/#{arity}`](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" - true -> - all - end - end) + true -> + all + end end @doc """ From c7089b3555b9a20929fea19920ea2f5fbeaf4b17 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 19 Dec 2017 16:59:29 +0100 Subject: [PATCH 5/5] Final --- lib/ex_doc/formatter/html/autolink.ex | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index 69b104b81..7a9169096 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -415,9 +415,10 @@ 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 @@ -431,27 +432,28 @@ defmodule ExDoc.Formatter.HTML.Autolink do @custom_re ~r{\[(.*)\]\(`(#{fun_re})`\)} @normal_re ~r{(? - replacement(all, match, project_funs, extension, lib_dirs) + replacement(all, match, project_funs, extension, lib_dirs, text) end) end - def replace_normal_links(bin, project_funs, extension, lib_dirs) when is_binary(bin) do + 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) do + 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 -> - "[`#{module}.#{function}/#{arity}`](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" + "[#{text}](#{module}#{extension}##{prefix}#{enc_h function}/#{arity})" doc = lib_dirs_to_doc("Elixir." <> module, lib_dirs) -> - "[`#{module}.#{function}/#{arity}`](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" + "[#{text}](#{doc}#{module}.html##{prefix}#{enc_h function}/#{arity})" true -> all