- <%= for type_node <- summary_map.types do
- detail_template(type_node, module)
- end %>
+
+ <%= for node <- nodes, do: detail_template(node, module) %>
<% end %>
-
- <%= if summary_map.guards != [] do %>
-
- <% end %>
<%= footer_template(config) %>
diff --git a/lib/ex_doc/formatter/html/templates/summary_entry_template.eex b/lib/ex_doc/formatter/html/templates/summary_entry_template.eex
index 7056fd9c0..ffb3bd9e1 100644
--- a/lib/ex_doc/formatter/html/templates/summary_entry_template.eex
+++ b/lib/ex_doc/formatter/html/templates/summary_entry_template.eex
@@ -1,11 +1,11 @@
diff --git a/lib/ex_doc/formatter/html/templates/summary_template.eex b/lib/ex_doc/formatter/html/templates/summary_template.eex
index 1a3ccd3b1..89cd3ebe5 100644
--- a/lib/ex_doc/formatter/html/templates/summary_template.eex
+++ b/lib/ex_doc/formatter/html/templates/summary_template.eex
@@ -1,8 +1,8 @@
<%= unless Enum.empty?(nodes) do %>
-
+
- <%= for module_node <- nodes, do: summary_entry_template(module_node) %>
+ <%= for node <- nodes, do: summary_entry_template(node) %>
<% end %>
diff --git a/lib/ex_doc/nodes.ex b/lib/ex_doc/nodes.ex
index 144f7a8a8..ab9c28b60 100644
--- a/lib/ex_doc/nodes.ex
+++ b/lib/ex_doc/nodes.ex
@@ -10,6 +10,7 @@ defmodule ExDoc.ModuleNode do
deprecated: nil,
doc: nil,
doc_line: nil,
+ function_groups: [],
docs: [],
typespecs: [],
source_path: nil,
@@ -22,6 +23,7 @@ defmodule ExDoc.ModuleNode do
module: nil | String.t(),
group: nil | String.t(),
deprecated: nil | String.t(),
+ function_groups: list(String.t()),
docs: list(),
doc: nil | String.t(),
doc_line: non_neg_integer(),
@@ -47,6 +49,7 @@ defmodule ExDoc.FunctionNode do
signature: nil,
specs: [],
annotations: [],
+ group: nil,
doc_line: nil,
source_path: nil,
source_url: nil
@@ -60,6 +63,7 @@ defmodule ExDoc.FunctionNode do
doc_line: non_neg_integer,
source_path: nil | String.t(),
source_url: nil | String.t(),
+ group: nil | String.t(),
type: nil | String.t(),
signature: nil | String.t(),
specs: list(),
diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex
index 3161e0d07..32598bdb6 100644
--- a/lib/ex_doc/retriever.ex
+++ b/lib/ex_doc/retriever.ex
@@ -103,7 +103,8 @@ defmodule ExDoc.Retriever do
{doc_line, moduledoc, metadata} = get_module_docs(module_data)
line = find_module_line(module_data) || doc_line
- docs = get_docs(module_data, source) ++ get_callbacks(module_data, source)
+ {function_groups, function_docs} = get_docs(module_data, source, config)
+ docs = function_docs ++ get_callbacks(module_data, source)
types = get_types(module_data, source)
{title, id} = module_title_and_id(module_data)
module_group = GroupMatcher.match_module(config.groups_for_modules, module, id)
@@ -115,6 +116,7 @@ defmodule ExDoc.Retriever do
group: module_group,
type: module_data.type,
deprecated: metadata[:deprecated],
+ function_groups: function_groups,
docs: Enum.sort_by(docs, & &1.id),
doc: moduledoc,
doc_line: doc_line,
@@ -160,11 +162,8 @@ defmodule ExDoc.Retriever do
end
end
- defp get_module_docs(%{docs: docs}) do
- case docs do
- {:docs_v1, anno, _, _, %{"en" => doc}, metadata, _} -> {anno_line(anno), doc, metadata}
- {:docs_v1, anno, _, _, _, metadata, _} -> {anno_line(anno), nil, metadata}
- end
+ defp get_module_docs(%{docs: {:docs_v1, anno, _, _, moduledoc, metadata, _}}) do
+ {anno_line(anno), docstring(moduledoc), metadata}
end
defp get_abstract_code(module) do
@@ -178,12 +177,20 @@ defmodule ExDoc.Retriever do
## Function helpers
- defp get_docs(%{type: type, docs: docs} = module_data, source) do
+ defp get_docs(%{type: type, docs: docs} = module_data, source, config) do
{:docs_v1, _, _, _, _, _, docs} = docs
- for doc <- docs, doc?(doc, type) do
- get_function(doc, source, module_data)
- end
+ groups_for_functions =
+ Enum.map(config.groups_for_functions, fn {group, filter} ->
+ {Atom.to_string(group), filter}
+ end) ++ [{"Functions", fn _ -> true end}]
+
+ function_docs =
+ for doc <- docs, doc?(doc, type) do
+ get_function(doc, source, module_data, groups_for_functions)
+ end
+
+ {Enum.map(groups_for_functions, &elem(&1, 0)), function_docs}
end
# We are only interested in functions and macros for now
@@ -211,7 +218,7 @@ defmodule ExDoc.Retriever do
true
end
- defp get_function(function, source, module_data) do
+ defp get_function(function, source, module_data, groups_for_functions) do
{{type, name, arity}, anno, signature, doc, metadata} = function
actual_def = actual_def(name, arity, type)
doc_line = anno_line(anno)
@@ -240,6 +247,11 @@ defmodule ExDoc.Retriever do
_ -> annotations
end
+ group =
+ Enum.find_value(groups_for_functions, fn {group, filter} ->
+ filter.(metadata) && group
+ end)
+
%ExDoc.FunctionNode{
id: "#{name}/#{arity}",
name: name,
@@ -252,7 +264,8 @@ defmodule ExDoc.Retriever do
specs: specs,
source_path: source.path,
source_url: source_link(source, line),
- type: if(metadata[:guard], do: :guard, else: type),
+ type: type,
+ group: group,
annotations: annotations
}
end
@@ -262,8 +275,9 @@ defmodule ExDoc.Retriever do
with {:docs_v1, _, _, _, _, _, docs} <- Code.fetch_docs(behaviour),
key = {definition_to_callback(type), name, arity},
- {_, _, _, %{"en" => doc}, _} <- List.keyfind(docs, key, 0) do
- "#{doc}\n\n#{info}"
+ {_, _, _, doc, _} <- List.keyfind(docs, key, 0),
+ docstring when is_binary(docstring) <- docstring(doc) do
+ "#{docstring}\n\n#{info}"
else
_ -> info
end
@@ -326,7 +340,7 @@ defmodule ExDoc.Retriever do
## Typespecs
- # Returns a map of {name, arity} -> spec.
+ # Returns a map of {name, arity} => spec.
defp get_specs(module) do
case Code.Typespec.fetch_specs(module) do
{:ok, specs} -> Map.new(specs)
@@ -334,7 +348,7 @@ defmodule ExDoc.Retriever do
end
end
- # Returns a map of {name, arity} -> behaviour.
+ # Returns a map of {name, arity} => behaviour.
defp get_impls(module) do
for behaviour <- behaviours_implemented_by(module),
callback <- callbacks_defined_by(behaviour),
diff --git a/lib/mix/tasks/docs.ex b/lib/mix/tasks/docs.ex
index fa58f43b6..df142eab5 100644
--- a/lib/mix/tasks/docs.ex
+++ b/lib/mix/tasks/docs.ex
@@ -83,7 +83,7 @@ defmodule Mix.Tasks.Docs do
* `:formatters` - Formatter to use; default: ["html"], options: "html", "epub".
- * `:groups_for_extras`, `:groups_for_modules` - See next section
+ * `:groups_for_extras`, `:groups_for_modules`, `:groups_for_functions` - See next sections
* `:language` - Identify the primary language of the documents, its value must be
a valid [BCP 47](https://tools.ietf.org/html/bcp47) language tag; default: "en"
@@ -111,7 +111,7 @@ defmodule Mix.Tasks.Docs do
* `:output` - Output directory for the generated docs; default: "doc".
May be overridden by command line argument.
-
+
*`:ignore_apps` - Apps to be ignored when generating documentation in an umbrella project.
Receives a list of atoms. Example: `[:first_app, :second_app]`.
@@ -155,6 +155,41 @@ defmodule Mix.Tasks.Docs do
A regex or the string name of the module is also supported.
+ ## Grouping functions
+
+ Functions inside a module can also be organized in groups. This is done via
+ the `:groups_for_functions` configuration which is a keyword list of group
+ titles and filtering functions that receive the documentation metadata of
+ functions as argument.
+
+ For example, imagine that you have an API client library with a large surface
+ area for all the API endpoints you need to support. It would be helpful to
+ group the functions with similar responsibilities together. In this case in
+ your module you might have:
+
+ defmodule APIClient do
+ @doc section: :auth
+ def refresh_token(params \\ [])
+
+ @doc subject: :object
+ def update_status(id, new_status)
+
+ @doc permission: :grant
+ def grant_privilege(resource, privilege)
+ end
+
+ And then in the congiruation you can group these with:
+
+ groups_for_functions: [
+ Authentication: & &1[:section] == :auth,
+ Resource: & &1[:subject] == :object,
+ Admin: & &1[:permission] in [:grant, :write]
+ ]
+
+ A function can belong to a single group only. If multiple group filters match,
+ the first will take precedence. Functions that don't have a custom group will
+ be listed under the default "Functions" group.
+
## Umbrella project
ExDoc can be used in an umbrella project and generates a single documentation
diff --git a/test/ex_doc/formatter/epub/templates_test.exs b/test/ex_doc/formatter/epub/templates_test.exs
index 72a554684..95f193730 100644
--- a/test/ex_doc/formatter/epub/templates_test.exs
+++ b/test/ex_doc/formatter/epub/templates_test.exs
@@ -12,8 +12,8 @@ defmodule ExDoc.Formatter.EPUB.TemplatesTest do
"http://elixir-lang.org"
end
- defp doc_config do
- %ExDoc.Config{
+ defp doc_config(config \\ []) do
+ default = %ExDoc.Config{
project: "Elixir",
version: "1.0.1",
source_root: File.cwd!(),
@@ -22,12 +22,15 @@ defmodule ExDoc.Formatter.EPUB.TemplatesTest do
source_url: source_url(),
output: "test/tmp/epub_templates"
}
+
+ struct(default, config)
end
- defp get_module_page(names) do
- mods = ExDoc.Retriever.docs_from_modules(names, doc_config())
+ defp get_module_page(names, config \\ []) do
+ config = doc_config(config)
+ mods = ExDoc.Retriever.docs_from_modules(names, config)
mods = HTML.Autolink.all(mods, HTML.Autolink.compile(mods, ".xhtml", []))
- Templates.module_page(doc_config(), hd(mods))
+ Templates.module_page(config, hd(mods))
end
setup_all do
@@ -68,6 +71,23 @@ defmodule ExDoc.Formatter.EPUB.TemplatesTest do
assert content =~ ~s{example(foo, bar \\\\ Baz)}
end
+ test "outputs function groups" do
+ content =
+ get_module_page([CompiledWithDocs],
+ groups_for_functions: [
+ "Example functions": &(&1[:purpose] == :example),
+ Legacy: &is_binary(&1[:deprecated])
+ ]
+ )
+
+ assert content =~ ~r{id="example-functions".*href="#example-functions".*Example functions}ms
+ assert content =~ ~r{id="legacy".*href="#legacy".*Legacy}ms
+ assert content =~ ~r{id="example-functions".*id="example/2"}ms
+ refute content =~ ~r{id="legacy".*id="example/2"}ms
+ refute content =~ ~r{id="functions".*id="example/2"}ms
+ assert content =~ ~r{id="functions".*id="example_1/0"}ms
+ end
+
test "outputs summaries" do
content = get_module_page([CompiledWithDocs])
assert content =~ ~r{
\s*
}
diff --git a/test/ex_doc/formatter/html/templates_test.exs b/test/ex_doc/formatter/html/templates_test.exs
index 35c59bbc7..c266bc3ca 100644
--- a/test/ex_doc/formatter/html/templates_test.exs
+++ b/test/ex_doc/formatter/html/templates_test.exs
@@ -28,10 +28,11 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do
struct(default, config)
end
- defp get_module_page(names) do
- mods = ExDoc.Retriever.docs_from_modules(names, doc_config())
+ defp get_module_page(names, config \\ []) do
+ config = doc_config(config)
+ mods = ExDoc.Retriever.docs_from_modules(names, config)
mods = HTML.Autolink.all(mods, HTML.Autolink.compile(mods, ".html", []))
- Templates.module_page(hd(mods), @empty_nodes_map, doc_config())
+ Templates.module_page(hd(mods), @empty_nodes_map, config)
end
setup_all do
@@ -41,20 +42,6 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do
:ok
end
- describe "header_to_id" do
- test "id generation" do
- assert Templates.header_to_id("“Stale”") == "stale"
- assert Templates.header_to_id("José") == "josé"
- assert Templates.header_to_id(" a - b ") == "a-b"
- assert Templates.header_to_id(" ☃ ") == ""
- assert Templates.header_to_id(" ² ") == ""
- assert Templates.header_to_id(" ⏜ ") == ""
-
- assert Templates.header_to_id("Git Options (:git
)") ==
- "git-options-git"
- end
- end
-
describe "link_headings" do
test "generates headers with hovers" do
assert Templates.link_headings("Foo
Bar
") == """
@@ -204,12 +191,34 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do
content = Templates.create_sidebar_items(%{modules: nodes}, [])
assert content =~ ~r("modules":\[\{"id":"CompiledWithDocs","title":"CompiledWithDocs")ms
- assert content =~ ~r("id":"CompiledWithDocs".*"functions":.*"example/2")ms
- assert content =~ ~r("id":"CompiledWithDocs".*"functions":.*"example_without_docs/0")ms
+ assert content =~ ~r("id":"CompiledWithDocs".*"key":"functions".*"example/2")ms
+ assert content =~ ~r("id":"CompiledWithDocs".*"key":"functions".*"example_without_docs/0")ms
assert content =~ ~r("id":"CompiledWithDocs.Nested")ms
end
- test "outputs groups for the given nodes" do
+ test "outputs nodes grouped based on metadata" do
+ nodes =
+ ExDoc.Retriever.docs_from_modules(
+ [CompiledWithDocs, CompiledWithDocs.Nested],
+ doc_config(
+ groups_for_functions: [
+ "Example functions": &(&1[:purpose] == :example),
+ Legacy: &is_binary(&1[:deprecated])
+ ]
+ )
+ )
+
+ content = Templates.create_sidebar_items(%{modules: nodes}, [])
+
+ assert content =~ ~s("modules":\[\{"id":"CompiledWithDocs","title":"CompiledWithDocs")
+ assert content =~ ~r("key":"example-functions".*"example/2")ms
+ refute content =~ ~r("key":"legacy".*"example/2")ms
+ refute content =~ ~r("key":"functions".*"example/2")ms
+ assert content =~ ~r("key":"functions".*"example_1/0")ms
+ assert content =~ ~r("key":"legacy".*"example_without_docs/0")ms
+ end
+
+ test "outputs module groups for the given nodes" do
names = [CompiledWithDocs, CompiledWithDocs.Nested]
group_mapping = [groups_for_modules: [Group: [CompiledWithDocs]]]
nodes = ExDoc.Retriever.docs_from_modules(names, doc_config(group_mapping))
@@ -280,6 +289,23 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do
~r{\s*\s*Link to this function\s*}ms
end
+ test "outputs function groups" do
+ content =
+ get_module_page([CompiledWithDocs],
+ groups_for_functions: [
+ "Example functions": &(&1[:purpose] == :example),
+ Legacy: &is_binary(&1[:deprecated])
+ ]
+ )
+
+ assert content =~ ~r{id="example-functions".*href="#example-functions".*Example functions}ms
+ assert content =~ ~r{id="legacy".*href="#legacy".*Legacy}ms
+ assert content =~ ~r{id="example-functions".*id="example/2"}ms
+ refute content =~ ~r{id="legacy".*id="example/2"}ms
+ refute content =~ ~r{id="functions".*id="example/2"}ms
+ assert content =~ ~r{id="functions".*id="example_1/0"}ms
+ end
+
test "outputs deprecation information" do
content = get_module_page([CompiledWithDocs])
diff --git a/test/ex_doc/formatter/html_test.exs b/test/ex_doc/formatter/html_test.exs
index 3bd4e0216..70efab76f 100644
--- a/test/ex_doc/formatter/html_test.exs
+++ b/test/ex_doc/formatter/html_test.exs
@@ -2,6 +2,7 @@ defmodule ExDoc.Formatter.HTMLTest do
use ExUnit.Case
import ExUnit.CaptureIO
+ alias ExDoc.Formatter.HTML
alias ExDoc.Markdown.DummyProcessor
setup do
@@ -75,6 +76,26 @@ defmodule ExDoc.Formatter.HTMLTest do
fn -> generate_docs(config) end
end
+ describe "strip_tags" do
+ test "removes html tags from text leaving the content" do
+ assert HTML.strip_tags("
Hello World!
") == "Hello World!"
+ assert HTML.strip_tags("Go
back") == "Go back"
+ assert HTML.strip_tags("Git opts (
:git
)") == "Git opts (:git)"
+ end
+ end
+
+ describe "text_to_id" do
+ test "id generation" do
+ assert HTML.text_to_id("“Stale”") == "stale"
+ assert HTML.text_to_id("José") == "josé"
+ assert HTML.text_to_id(" a - b ") == "a-b"
+ assert HTML.text_to_id(" ☃ ") == ""
+ assert HTML.text_to_id(" ² ") == ""
+ assert HTML.text_to_id(" ⏜ ") == ""
+ assert HTML.text_to_id("Git opts (
:git
)") == "git-opts-git"
+ end
+ end
+
test "warns when generating an index.html file with an invalid redirect" do
output =
capture_io(:stderr, fn ->
@@ -147,7 +168,7 @@ defmodule ExDoc.Formatter.HTMLTest do
content = read_wildcard!("#{output_dir()}/dist/sidebar_items-*.js")
assert content =~ ~r{"id":"CompiledWithDocs","title":"CompiledWithDocs"}ms
- assert content =~ ~r("id":"CompiledWithDocs".*"functions":.*"example/2")ms
+ assert content =~ ~r("id":"CompiledWithDocs".*"key":"functions".*"example/2")ms
assert content =~ ~r{"id":"CompiledWithDocs\.Nested","title":"CompiledWithDocs\.Nested"}ms
assert content =~ ~r{"id":"UndefParent\.Nested","title":"UndefParent\.Nested"}ms
diff --git a/test/ex_doc/retriever_test.exs b/test/ex_doc/retriever_test.exs
index da6fc16f4..10cda3220 100644
--- a/test/ex_doc/retriever_test.exs
+++ b/test/ex_doc/retriever_test.exs
@@ -67,9 +67,15 @@ defmodule ExDoc.RetrieverTest do
end
test "returns the function nodes for each module" do
- [module_node] = docs_from_files(["CompiledWithDocs"])
-
- [struct, example, example_1, _example_with_h3, example_without_docs, is_zero] =
+ [module_node] =
+ docs_from_files(["CompiledWithDocs"],
+ groups_for_functions: [
+ Example: &(&1[:purpose] == :example),
+ Legacy: &is_binary(&1[:deprecated])
+ ]
+ )
+
+ [struct, example, example_1, example_with_h3, example_without_docs, is_zero] =
module_node.docs
assert struct.id == "__struct__/0"
@@ -77,6 +83,7 @@ defmodule ExDoc.RetrieverTest do
assert struct.type == :function
assert struct.defaults == []
assert struct.signature == "%CompiledWithDocs{}"
+ assert struct.group == "Functions"
assert example.id == "example/2"
assert example.doc == "Some example"
@@ -84,23 +91,27 @@ defmodule ExDoc.RetrieverTest do
assert example.defaults == ["example/1"]
assert example.signature == "example(foo, bar \\\\ Baz)"
assert example.deprecated == "Use something else instead"
+ assert example.group == "Example"
assert example_1.id == "example_1/0"
assert example_1.type == :macro
assert example_1.defaults == []
assert example_1.annotations == ["macro", "since 1.3.0"]
+ assert example_with_h3.id == "example_with_h3/0"
+ assert example_with_h3.group == "Example"
+
assert example_without_docs.id == "example_without_docs/0"
assert example_without_docs.doc == nil
assert example_without_docs.defaults == []
+ assert example_without_docs.group == "Legacy"
assert example_without_docs.source_url ==
- "http://example.com/test/fixtures/compiled_with_docs.ex\#L34"
+ "http://example.com/test/fixtures/compiled_with_docs.ex\#L38"
assert is_zero.id == "is_zero/1"
assert is_zero.doc == "A simple guard"
- # TODO: Remove :macro when ~> 1.8
- assert is_zero.type in [:guard, :macro]
+ assert is_zero.type == :macro
assert is_zero.defaults == []
end
diff --git a/test/fixtures/compiled_with_docs.ex b/test/fixtures/compiled_with_docs.ex
index d4704a874..a2cf2f870 100644
--- a/test/fixtures/compiled_with_docs.ex
+++ b/test/fixtures/compiled_with_docs.ex
@@ -14,6 +14,7 @@ defmodule CompiledWithDocs do
defstruct [:field]
@doc "Some example"
+ @doc purpose: :example
@deprecated "Use something else instead"
def example(foo, bar \\ Baz), do: bar.baz(foo)
@@ -22,6 +23,7 @@ defmodule CompiledWithDocs do
defmacro example_1, do: 1
@doc "A simple guard"
+ # TODO: remove explicit guard: true when ~> 1.8
defguard is_zero(number) when number == 0
@doc """
@@ -29,8 +31,10 @@ defmodule CompiledWithDocs do
### Examples
"""
+ @doc purpose: :example
def example_with_h3, do: 1
+ @deprecated "Use something else instead"
def example_without_docs, do: nil
defmodule Nested do