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