Packages
ex_doc
0.16.3
0.40.3
0.40.2
0.40.1
0.40.0
0.39.3
0.39.2
0.39.1
0.39.0
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.3
0.37.2
0.37.1
0.37.0
0.37.0-rc.2
0.37.0-rc.1
0.37.0-rc.0
0.36.1
0.36.0
0.35.1
0.35.0
0.34.2
0.34.1
0.34.0
0.33.0
0.32.2
0.32.1
0.32.0
0.31.2
0.31.1
0.31.0
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.6
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.3
0.27.2
0.27.1
0.27.0
0.26.0
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.2
0.24.1
0.24.0
0.23.0
0.22.6
0.22.5
0.22.4
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.19.0-rc
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
retired
0.17.1
0.17.0
retired
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.2
0.13.1
0.13.0
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
ExDoc is a documentation generation tool for Elixir
Current section
Files
Jump to
Current section
Files
lib/ex_doc/formatter/html/autolink.ex
defmodule ExDoc.Formatter.HTML.Autolink do
@moduledoc """
Conveniences for autolinking locals, types and more.
"""
import ExDoc.Formatter.HTML.Templates, only: [h: 1, enc_h: 1]
@elixir_docs "https://hexdocs.pm/"
@erlang_docs "http://www.erlang.org/doc/man/"
@doc """
Receives a list of module nodes and autolink all docs and typespecs.
"""
def all(modules, extension, extra_lib_dirs) do
aliases = Enum.map modules, &(&1.module)
lib_dirs = extra_lib_dirs ++ elixir_lib_dirs()
modules
|> Enum.map(&Task.async(fn -> process_module(&1, modules, aliases, extension, lib_dirs) end))
|> Enum.map(&Task.await(&1, :infinity))
end
defp process_module(module, modules, aliases, extension, lib_dirs) do
module
|> all_docs(modules, extension, lib_dirs)
|> all_typespecs(aliases, lib_dirs)
end
defp module_to_string(module) do
inspect module.module
end
defp all_docs(module, modules, extension, lib_dirs) do
locals =
for doc <- module.docs,
prefix = doc_prefix(doc),
entry <- [doc.id | doc.defaults],
do: prefix <> entry,
into: Enum.map(module.typespecs, &("t:" <> &1.id))
moduledoc =
if module.doc do
module.doc
|> local_doc(locals)
|> project_doc(modules, module.id, extension, lib_dirs)
end
docs = for module_node <- module.docs do
doc =
if module_node.doc do
module_node.doc
|> local_doc(locals)
|> project_doc(modules, module.id, extension, lib_dirs)
end
%{module_node | doc: doc}
end
typedocs = for module_node <- module.typespecs do
doc =
if module_node.doc do
module_node.doc
|> local_doc(locals)
|> project_doc(modules, module.id, extension, lib_dirs)
end
%{module_node | doc: doc}
end
%{module | doc: moduledoc, docs: docs, typespecs: typedocs}
end
defp all_typespecs(module, aliases, lib_dirs) do
locals = Enum.map module.typespecs, fn
%ExDoc.TypeNode{name: name, arity: arity} -> {name, arity}
end
typespecs = for typespec <- module.typespecs do
%{typespec | spec: typespec(typespec.spec, locals, aliases, lib_dirs)}
end
docs = for module_node <- module.docs do
%{module_node | specs: Enum.map(module_node.specs, &typespec(&1, locals, aliases, lib_dirs))}
end
%{module | typespecs: typespecs, docs: docs}
end
@doc """
Converts the given `ast` to string while linking the locals
given by `typespecs` as HTML.
"""
def typespec(ast, typespecs, aliases, lib_dirs \\ elixir_lib_dirs())
def typespec({:when, _, [{:::, _, [left, {:|, _, _} = center]}, right]} = ast, typespecs, aliases, lib_dirs) do
if short_typespec?(ast) do
normalize_left(ast, typespecs, aliases, lib_dirs)
else
normalize_left(left, typespecs, aliases, lib_dirs) <>
" ::\n " <> typespec_with_new_line(center, typespecs, aliases, lib_dirs) <>
" when " <> String.slice(typespec_to_string(right, typespecs, aliases, lib_dirs), 1..-2)
end
end
def typespec({:::, _, [left, {:|, _, _} = center]} = ast, typespecs, aliases, lib_dirs) do
if short_typespec?(ast) do
normalize_left(ast, typespecs, aliases, lib_dirs)
else
normalize_left(left, typespecs, aliases, lib_dirs) <>
" ::\n " <> typespec_with_new_line(center, typespecs, aliases, lib_dirs)
end
end
def typespec(other, typespecs, aliases, lib_dirs) do
normalize_left(other, typespecs, aliases, lib_dirs)
end
defp typespec_with_new_line({:|, _, [left, right]}, typespecs, aliases, lib_dirs) do
typespec_to_string(left, typespecs, aliases, lib_dirs) <>
" |\n " <> typespec_with_new_line(right, typespecs, aliases, lib_dirs)
end
defp typespec_with_new_line(other, typespecs, aliases, lib_dirs) do
typespec_to_string(other, typespecs, aliases, lib_dirs)
end
defp normalize_left({:::, _, [{name, meta, args}, right]}, typespecs, aliases, lib_dirs) do
new_args =
Enum.map(args, &[self(), typespec_to_string(&1, typespecs, aliases, lib_dirs)])
new_left =
Macro.to_string {name, meta, new_args}, fn
[pid, string], _ when pid == self() -> string
_, string -> string
end
new_left <> " :: " <> typespec_to_string(right, typespecs, aliases, lib_dirs)
end
defp normalize_left({:when, _, [{:::, _, _} = left, right]}, typespecs, aliases, lib_dirs) do
normalize_left(left, typespecs, aliases, lib_dirs) <>
" when " <> String.slice(typespec_to_string(right, typespecs, aliases, lib_dirs), 1..-2)
end
defp normalize_left(ast, typespecs, aliases, lib_dirs) do
typespec_to_string(ast, typespecs, aliases, lib_dirs)
end
defp typespec_to_string(ast, typespecs, aliases, lib_dirs) do
Macro.to_string(ast, fn
{name, _, args}, string when is_atom(name) and is_list(args) ->
string = strip_parens(string, args)
arity = length(args)
if {name, arity} in typespecs do
n = enc_h("#{name}")
{string_to_link, string_with_parens} = split_string_to_link(string)
~s[<a href="#t:#{n}/#{arity}">#{h(string_to_link)}</a>#{string_with_parens}]
else
string
end
{{:., _, [alias, name]}, _, args}, string when is_atom(name) and is_list(args) ->
string = strip_parens(string, args)
alias = expand_alias(alias)
if source = get_source(alias, aliases, lib_dirs) do
n = enc_h("#{name}")
{string_to_link, string_with_parens} = split_string_to_link(string)
~s[<a href="#{source}#{enc_h(inspect alias)}.html#t:#{n}/#{length(args)}">#{h(string_to_link)}</a>#{string_with_parens}]
else
string
end
_, string ->
string
end)
end
defp short_typespec?(ast) do
byte_size(Macro.to_string(ast)) <= 70
end
defp strip_parens(string, []) do
if :binary.last(string) == ?) do
:binary.part(string, 0, byte_size(string) - 2)
else
string
end
end
defp strip_parens(string, _), do: string
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.
Ignores functions which are already wrapped in markdown url syntax,
e.g. `[test/1](url)`. If the function doesn't touch the leading
or trailing `]`, e.g. `[my link link/1 is here](url)`, the fun/arity
will get translated to the new href of the function.
"""
def local_doc(bin, locals) when is_binary(bin) do
fun_re = Regex.source(~r{(([ct]:)?([a-z_]+[A-Za-z_\d]*[\\?\\!]?|[\{\}=&\\|\\.<>~*^@\\+\\%\\!-]+)/\d+)})
regex = ~r{(?<!\[)`\s*(#{fun_re})\s*`(?!\])}
Regex.replace(regex, bin, fn all, match ->
if match in locals do
{prefix, _, function, arity} = split_function(match)
"[`#{function}/#{arity}`](##{prefix}#{enc_h function}/#{arity})"
else
all
end
end)
end
@doc """
Creates links to modules and functions defined in the project.
"""
def project_doc(bin, modules, module_id \\ nil,
extension \\ ".html", lib_dirs \\ elixir_lib_dirs()) when is_binary(bin) do
project_types =
for module <- modules,
type <- module.typespecs,
do: "t:" <> module.id <> "." <> type.id
project_docs =
for module <- modules,
doc <- module.docs,
prefix = doc_prefix(doc),
entry <- [doc.id | doc.defaults],
do: prefix <> module.id <> "." <> entry,
into: project_types
project_modules =
modules
|> Enum.map(&module_to_string/1)
|> Enum.uniq()
bin
|> elixir_functions(project_docs, extension, lib_dirs)
|> elixir_modules(project_modules, module_id, extension, lib_dirs)
|> erlang_functions()
end
defp doc_prefix(%{type: c}) when c in [:callback, :macrocallback], do: "c:"
defp doc_prefix(%{type: _}), do: ""
@doc """
Create links to elixir functions defined in the project and Elixir itself.
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
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)
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})"
true ->
all
end
end)
end
@doc """
Create links to elixir modules defined in the project and
in Elixir itself.
Ignores modules which are already wrapped in markdown url syntax,
e.g. `[Module](url)`. If the module name doesn't touch the leading
or trailing `]`, e.g. `[my link Module is here](url)`, the Module
will get translated to the new href of the module.
"""
def elixir_modules(bin, modules, module_id \\ nil,
extension \\ ".html", lib_dirs \\ elixir_lib_dirs()) when is_binary(bin) do
regex = ~r{(?<!\[)`\s*(([A-Z][A-Za-z_\d]+\.?)+)\s*`(?!\])}
Regex.replace(regex, bin, fn all, match ->
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
end)
end
defp split_function("c:" <> bin) do
{_, mod, fun, arity} = split_function(bin)
{"c:", mod, fun, arity}
end
defp split_function("t:" <> bin) do
{_, mod, fun, arity} = split_function(bin)
{"t:", mod, fun, arity}
end
defp split_function(bin) do
[modules, arity] = String.split(bin, "/")
{mod, name} =
modules
|> String.replace(~r{([^\.])\.}, "\\1 ") # this handles the case of the ".." function
|> String.split(" ")
|> Enum.split(-1)
{"", Enum.join(mod, "."), hd(name), arity}
end
@doc """
Create links to Erlang functions in code blocks.
Only links modules that are in the Erlang distribution `lib_dir`
and only link functions in those modules that export a function of the
same name and arity.
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
will get translated to the new href of the function.
"""
def erlang_functions(bin) when is_binary(bin) do
lib_dirs = erlang_lib_dirs()
regex = ~r{(?<!\[)`\s*:([a-z_]+\.[0-9a-zA-Z_!\\?]+/\d+)\s*`(?!\])}
Regex.replace(regex, bin, fn all, match ->
{_, module, function, arity} = split_function(match)
if doc = lib_dirs_to_doc(module, lib_dirs) do
"[`:#{match}`](#{doc}#{module}.html##{function}-#{arity})"
else
all
end
end)
end
## Helpers
defp lib_dirs_to_doc(module, lib_dirs) do
case :code.where_is_file('#{module}.beam') do
:non_existing ->
nil
path ->
path = List.to_string(path)
Enum.find_value(lib_dirs, fn {lib_dir, doc} ->
String.starts_with?(path, lib_dir) and doc
end)
end
end
defp elixir_lib_dirs do
case Application.fetch_env(:ex_doc, :elixir_lib_dirs) do
{:ok, lib_dirs} ->
lib_dirs
:error ->
lib_dir =
case :code.where_is_file('Elixir.Kernel.beam') do
:non_existing ->
[0]
path ->
path
|> Path.dirname()
|> Path.dirname()
|> Path.dirname()
end
lib_dirs =
for app <- ~w(elixir eex iex logger mix ex_unit) do
{lib_dir <> "/" <> app <> "/ebin", @elixir_docs <> app <> "/"}
end
Application.put_env(:ex_doc, :elixir_lib_dirs, lib_dirs)
lib_dirs
end
end
defp erlang_lib_dirs do
case Application.fetch_env(:ex_doc, :erlang_lib_dirs) do
{:ok, lib_dirs} ->
lib_dirs
:error ->
lib_dirs = [{Path.expand(:code.lib_dir), @erlang_docs}]
Application.put_env(:ex_doc, :erlang_lib_dirs, lib_dirs)
lib_dirs
end
end
end