Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 66 additions & 52 deletions lib/ex_doc/formatter/html/autolink.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function is too complex (CC is 10, max is 9).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is CC? I don't get this message

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}
Expand All @@ -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}
Expand All @@ -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
Expand All @@ -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[<a href="#{url}">#{h(string_to_link)}</a>]
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
Expand All @@ -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.
Expand Down Expand Up @@ -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+

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO found

defp formatter_available? do
function_exported?(Code, :format_string!, 2)
end
end
52 changes: 52 additions & 0 deletions test/ex_doc/formatter/html/autolink_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,52 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
~s[t() :: %{foo: bar(), really_long_name_that_will_trigger_multiple_line_breaks: <a href=\"#{@elixir_docs}elixir/String.html#t:t/0\">String.t</a>()}]
end

test "autolink types that look like formatter placeholders" do
assert Autolink.typespec(quote(do: p1() :: foo()), [foo: 0], []) ==
~s[p1() :: <a href=\"#t:foo/0\">foo</a>()]
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[<a href="#{@elixir_docs}elixir/String.html#t:t/0">String.t</a>()]
Expand Down Expand Up @@ -387,4 +433,10 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
~s[<a href=\"#{@elixir_docs}elixir/typespecs.html#basic-types\">list</a>(] <>
~s[<a href=\"#{@elixir_docs}elixir/typespecs.html#built-in-types\">function</a>())]
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