diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index 9cc04a51f..8790eb3a8 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -214,10 +214,16 @@ defmodule ExDoc.Formatter.HTML.Autolink do end defp format_typespec(ast, typespecs, aliases, lib_dirs) do + {formatted, placeholders} = format_and_extract_typespec_placeholders(ast, typespecs, aliases, lib_dirs) + replace_placeholders(formatted, placeholders) + end + + @doc false + def format_and_extract_typespec_placeholders(ast, typespecs, aliases, lib_dirs) do ref = make_ref() elixir_source = get_source(Kernel, aliases, lib_dirs) - {ast, placeholders} = + {formatted_ast, placeholders} = Macro.prewalk(ast, %{}, fn {:::, _, [{name, meta, args}, right]}, placeholders when is_atom(name) and is_list(args) -> {{:::, [], [{{ref, name}, meta, args}, right]}, placeholders} @@ -232,19 +238,16 @@ defmodule ExDoc.Formatter.HTML.Autolink do cond do {name, arity} in @basic_types -> url = elixir_source <> @basic_types_page - string = format_typespec_form(form, url) - put_placeholder(form, string, placeholders) + put_placeholder(form, url, placeholders) {name, arity} in @built_in_types -> url = elixir_source <> @built_in_types_page - string = format_typespec_form(form, url) - put_placeholder(form, string, placeholders) + put_placeholder(form, url, placeholders) {name, arity} in typespecs -> n = enc_h("#{name}") url = "#t:#{n}/#{arity}" - string = format_typespec_form(form, url) - put_placeholder(form, string, placeholders) + put_placeholder(form, url, placeholders) true -> {form, placeholders} @@ -254,9 +257,8 @@ defmodule ExDoc.Formatter.HTML.Autolink do alias = expand_alias(alias) if source = get_source(alias, aliases, lib_dirs) do - url = remote_url(source, alias, name, args) - string = format_typespec_form(form, url) - put_placeholder(form, string, placeholders) + url = type_remote_url(source, alias, name, args) + put_placeholder(form, url, placeholders) else {form, placeholders} end @@ -265,40 +267,52 @@ defmodule ExDoc.Formatter.HTML.Autolink do {form, placeholders} end) - ast - |> format_ast() - |> replace_placeholders(placeholders) + {format_ast(formatted_ast), placeholders} end - defp remote_url(@erlang_docs = source, module, name, _args) do + defp type_remote_url(@erlang_docs = source, module, name, _args) do module = enc_h("#{module}") name = enc_h("#{name}") "#{source}#{module}.html#type-#{name}" end - defp remote_url(source, alias, name, args) do + defp type_remote_url(source, alias, name, args) do name = enc_h("#{name}") "#{source}#{enc_h(inspect alias)}.html#t:#{name}/#{length(args)}" end - defp format_typespec_form(form, url) do - string = Macro.to_string(form) + defp typespec_string_to_link(string, url) do {string_to_link, _string_with_parens} = split_string_to_link(string) ~s[#{h(string_to_link)}] end - defp put_placeholder(form, string, placeholders) do - count = map_size(placeholders) + 1 - type_size = form |> Macro.to_string() |> byte_size() - int_size = count |> Integer.to_string() |> byte_size() - parens_size = 2 - pad = String.duplicate("p", max(type_size - int_size - parens_size, 1)) - placeholder = :"#{pad}#{count}" - form = put_elem(form, 0, placeholder) - {form, Map.put(placeholders, Atom.to_string(placeholder), string)} + defp put_placeholder(form, url, placeholders) do + string = Macro.to_string(form) + link = typespec_string_to_link(string, url) + + case Enum.find(placeholders, fn {_key, value} -> value == link end) do + {placeholder, _} -> + form = put_elem(form, 0, placeholder) + {form, placeholders} + + nil -> + count = map_size(placeholders) + 1 + placeholder = placeholder(string, count) + form = put_elem(form, 0, placeholder) + {form, Map.put(placeholders, placeholder, link)} + end + end + + defp placeholder(string, count) do + [name | _] = String.split(string, "(", trim: true) + name_size = String.length(name) + int_size = count |> Integer.digits() |> length() + underscores_size = 2 + pad = String.duplicate("p", max(name_size - int_size - underscores_size, 1)) + :"_#{pad}#{count}_" end defp replace_placeholders(string, placeholders) do - Regex.replace(~r"p+\d+", string, &Map.fetch!(placeholders, &1)) + Regex.replace(~r"_p+\d+_", string, &Map.fetch!(placeholders, String.to_atom(&1))) end defp format_ast(ast) do @@ -313,31 +327,6 @@ defmodule ExDoc.Formatter.HTML.Autolink do end end - # TODO: remove when we require Elixir v1.6+ - defp formatter_available? do - function_exported?(Code, :format_string!, 2) - end - - defp split_string_to_link(string) do - case :binary.split(string, "(") do - [head, tail] -> {head, "(" <> tail} - [head] -> {head, ""} - end - end - - defp expand_alias({:__aliases__, _, [h|t]}) when is_atom(h), do: Module.concat([h|t]) - defp expand_alias(atom) when is_atom(atom), do: atom - defp expand_alias(_), do: nil - - defp get_source(alias, aliases, lib_dirs) do - cond do - is_nil(alias) -> nil - alias in aliases -> "" - doc = lib_dirs_to_doc(alias, lib_dirs) -> doc - true -> nil - end - end - @doc """ Create links to locally defined functions, specified in `locals` as a list of `fun/arity` strings. @@ -584,4 +573,29 @@ defmodule ExDoc.Formatter.HTML.Autolink do lib_dirs end end + + defp split_string_to_link(string) do + case :binary.split(string, "(") do + [head, tail] -> {head, "(" <> tail} + [head] -> {head, ""} + end + end + + defp expand_alias({:__aliases__, _, [h|t]}) when is_atom(h), do: Module.concat([h|t]) + defp expand_alias(atom) when is_atom(atom), do: atom + defp expand_alias(_), do: nil + + defp get_source(alias, aliases, lib_dirs) do + cond do + is_nil(alias) -> nil + alias in aliases -> "" + doc = lib_dirs_to_doc(alias, lib_dirs) -> doc + true -> nil + end + end + + # TODO: remove when we require Elixir v1.6+ + defp formatter_available? do + function_exported?(Code, :format_string!, 2) + end end diff --git a/test/ex_doc/formatter/html/autolink_test.exs b/test/ex_doc/formatter/html/autolink_test.exs index c97c830ad..a48ee3cc3 100644 --- a/test/ex_doc/formatter/html/autolink_test.exs +++ b/test/ex_doc/formatter/html/autolink_test.exs @@ -318,6 +318,52 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do ~s[t() :: %{foo: bar(), really_long_name_that_will_trigger_multiple_line_breaks: String.t()}] end + test "autolink types that look like formatter placeholders" do + assert Autolink.typespec(quote(do: p1() :: foo()), [foo: 0], []) == + ~s[p1() :: foo()] + end + + @tag :formatter + test "placeholders" do + assert_typespec_placeholders( + "t()", + "_p1_()", + [t: 0] + ) + + assert_typespec_placeholders( + "foobar()", + "_ppp1_()", + [foobar: 0] + ) + + assert_typespec_placeholders( + "Mod.foobar()", + "_ppppppp1_()", + [], + [Mod] + ) + + assert_typespec_placeholders( + "foobar(barbaz())", + "_ppp1_(_ppp2_())", + [foobar: 1, barbaz: 0] + ) + + assert_typespec_placeholders( + "Mod.foobar(Mod.barbaz())", + "_ppppppp1_(_ppppppp2_())", + [], + [Mod] + ) + + assert_typespec_placeholders( + "foobar(foobar(barbaz()))", + "_ppp1_(_ppp1_(_ppp2_()))", + [foobar: 1, barbaz: 0] + ) + end + test "autolink Elixir types in typespecs" do assert Autolink.typespec(quote(do: String.t), [], []) == ~s[String.t()] @@ -387,4 +433,10 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do ~s[list(] <> ~s[function())] 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 end