diff --git a/lib/ex_doc/formatter/epub/templates/module_template.eex b/lib/ex_doc/formatter/epub/templates/module_template.eex
index 2639baddd..2242051de 100644
--- a/lib/ex_doc/formatter/epub/templates/module_template.eex
+++ b/lib/ex_doc/formatter/epub/templates/module_template.eex
@@ -18,15 +18,20 @@
<%= if summary != [] do %>
Summary
- <%= for {name, nodes} <- summary, do: H.summary_template(name, nodes) %>
+ <%= for group <- summary, do: H.summary_template(group.title, group.docs) %>
<% end %>
- <%= for {name, nodes} <- summary, key = text_to_id(name) do %>
+ <%= for group <- summary, key = text_to_id(group.title) do %>
- <%=h to_string(name) %>
+ <%=h to_string(group.title) %>
+ <%= if doc = group.rendered_doc do %>
+
+ <%= H.link_group_headings(doc, key) %>
+
+ <% end %>
- <%= for node <- nodes, do: H.detail_template(node, module) %>
+ <%= for node <- group.docs, do: H.detail_template(node, module) %>
<% end %>
diff --git a/lib/ex_doc/formatter/html.ex b/lib/ex_doc/formatter/html.ex
index bdc3420e8..e7584367c 100644
--- a/lib/ex_doc/formatter/html.ex
+++ b/lib/ex_doc/formatter/html.ex
@@ -93,27 +93,32 @@ defmodule ExDoc.Formatter.HTML do
language: language
] ++ base
- docs =
- for child_node <- node.docs do
- id = id(node, child_node)
-
- autolink_opts =
- autolink_opts ++
- [
- id: id,
- line: child_node.doc_line,
- file: child_node.doc_file,
- current_kfa: {child_node.type, child_node.name, child_node.arity}
- ]
-
- specs = Enum.map(child_node.specs, &language.autolink_spec(&1, autolink_opts))
- child_node = %{child_node | specs: specs}
- render_doc(child_node, language, autolink_opts, opts)
+ docs_groups =
+ for group <- node.docs_groups do
+ docs =
+ for child_node <- group.docs do
+ id = id(node, child_node)
+
+ autolink_opts =
+ autolink_opts ++
+ [
+ id: id,
+ line: child_node.doc_line,
+ file: child_node.doc_file,
+ current_kfa: {child_node.type, child_node.name, child_node.arity}
+ ]
+
+ specs = Enum.map(child_node.specs, &language.autolink_spec(&1, autolink_opts))
+ child_node = %{child_node | specs: specs}
+ render_doc(child_node, language, autolink_opts, opts)
+ end
+
+ %{render_doc(group, language, autolink_opts, opts) | docs: docs}
end
%{
render_doc(node, language, [{:id, node.id} | autolink_opts], opts)
- | docs: docs
+ | docs_groups: docs_groups
}
end,
timeout: :infinity
diff --git a/lib/ex_doc/formatter/html/templates.ex b/lib/ex_doc/formatter/html/templates.ex
index 80d180548..de6897b9c 100644
--- a/lib/ex_doc/formatter/html/templates.ex
+++ b/lib/ex_doc/formatter/html/templates.ex
@@ -115,9 +115,9 @@ defmodule ExDoc.Formatter.HTML.Templates do
{id, modules}
end
- defp sidebar_entries({group, nodes}) do
+ defp sidebar_entries(group) do
nodes =
- for node <- nodes do
+ for node <- group.docs do
id =
if "struct" in node.annotations do
node.signature
@@ -134,7 +134,7 @@ defmodule ExDoc.Formatter.HTML.Templates do
%{id: id, title: node.signature, anchor: URI.encode(node.id), deprecated: deprecated?}
end
- %{key: text_to_id(group), name: group, nodes: nodes}
+ %{key: text_to_id(group.title), name: group.title, nodes: nodes}
end
defp module_sections(%ExDoc.ModuleNode{rendered_doc: nil}), do: [sections: []]
@@ -167,10 +167,7 @@ defmodule ExDoc.Formatter.HTML.Templates do
|> Enum.map(&%{id: &1, anchor: URI.encode(text_to_id(&1))})
end
- def module_summary(module_node) do
- # TODO: Maybe it should be moved to retriever and it already returned grouped metadata
- ExDoc.GroupMatcher.group_by(module_node.docs_groups, module_node.docs, & &1.group)
- end
+ def module_summary(module_node), do: module_node.docs_groups
defp favicon_path(%{favicon: nil}), do: nil
defp favicon_path(%{favicon: favicon}), do: "assets/favicon#{Path.extname(favicon)}"
@@ -281,6 +278,10 @@ defmodule ExDoc.Formatter.HTML.Templates do
link_headings(content, prefix <> "-")
end
+ def link_group_headings(content, key) do
+ link_headings(content, "group-#{key}-")
+ end
+
templates = [
detail_template: [:node, :module],
footer_template: [:config, :node],
diff --git a/lib/ex_doc/formatter/html/templates/module_template.eex b/lib/ex_doc/formatter/html/templates/module_template.eex
index e14af45bf..944b746d3 100644
--- a/lib/ex_doc/formatter/html/templates/module_template.eex
+++ b/lib/ex_doc/formatter/html/templates/module_template.eex
@@ -39,20 +39,25 @@
Summary
- <%= for {name, nodes} <- summary, do: summary_template(name, nodes) %>
+ <%= for group <- summary, do: summary_template(group.title, group.docs) %>
<% end %>
-<%= for {name, nodes} <- summary, key = text_to_id(name) do %>
+<%= for group <- summary, key = text_to_id(group.title) do %>
- <%= name %>
+ <%= group.title %>
+ <%= if doc = group.rendered_doc do %>
+
+ <%= link_group_headings(doc, key) %>
+
+ <% end %>
- <%= for node <- nodes, do: detail_template(node, module) %>
+ <%= for node <- group.docs, do: detail_template(node, module) %>
<% end %>
diff --git a/lib/ex_doc/group_matcher.ex b/lib/ex_doc/group_matcher.ex
index 6cda47c61..1ba306f11 100644
--- a/lib/ex_doc/group_matcher.ex
+++ b/lib/ex_doc/group_matcher.ex
@@ -14,23 +14,6 @@ defmodule ExDoc.GroupMatcher do
Enum.find_index(groups, fn {k, _v} -> k == group end) || -1
end
- @doc """
- Group the following entries and while preserving the order in `groups`.
- """
- def group_by(groups, entries, by) do
- entries = Enum.group_by(entries, by)
-
- {groups, leftovers} =
- Enum.flat_map_reduce(groups, entries, fn group, grouped_nodes ->
- case Map.pop(grouped_nodes, group, []) do
- {[], grouped_nodes} -> {[], grouped_nodes}
- {entries, grouped_nodes} -> {[{group, entries}], grouped_nodes}
- end
- end)
-
- groups ++ Enum.sort(leftovers)
- end
-
@doc """
Finds a matching group for the given module name, id, and metadata.
"""
diff --git a/lib/ex_doc/nodes.ex b/lib/ex_doc/nodes.ex
index 068e9f848..38d9d5750 100644
--- a/lib/ex_doc/nodes.ex
+++ b/lib/ex_doc/nodes.ex
@@ -43,7 +43,7 @@ defmodule ExDoc.ModuleNode do
moduledoc_file: String.t(),
source_path: String.t() | nil,
source_url: String.t() | nil,
- docs_groups: [atom()],
+ docs_groups: [ExDoc.DocGroupNode.t()],
docs: [ExDoc.DocNode.t()],
typespecs: [ExDoc.DocNode.t()],
type: atom(),
@@ -87,11 +87,23 @@ defmodule ExDoc.DocNode do
rendered_doc: String.t() | nil,
type: atom(),
signature: String.t(),
- specs: [ExDoc.Language.spec_ast()],
+ specs: [ExDoc.Language.spec_ast() | String.t()],
annotations: [annotation()],
- group: atom() | nil,
+ group: String.t() | nil,
doc_file: String.t(),
doc_line: non_neg_integer(),
source_url: String.t() | nil
}
end
+
+defmodule ExDoc.DocGroupNode do
+ defstruct title: nil, description: nil, doc: nil, rendered_doc: nil, docs: []
+
+ @type t :: %__MODULE__{
+ title: String.t() | atom(),
+ description: String.t() | nil,
+ doc: ExDoc.DocAST.t() | nil,
+ rendered_doc: String.t() | nil,
+ docs: [ExDoc.DocNode.t()]
+ }
+end
diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex
index 128b4a18d..5b308b38c 100644
--- a/lib/ex_doc/retriever.ex
+++ b/lib/ex_doc/retriever.ex
@@ -140,7 +140,18 @@ defmodule ExDoc.Retriever do
group_for_doc = config.group_for_doc
annotations_for_docs = config.annotations_for_docs
- docs = get_docs(module_data, source, group_for_doc, annotations_for_docs)
+ {docs, nodes_groups} = get_docs(module_data, source, group_for_doc, annotations_for_docs)
+ docs = ExDoc.Utils.natural_sort_by(docs, &"#{&1.name}/#{&1.arity}")
+
+ moduledoc_groups = Map.get(metadata, :groups, [])
+
+ docs_groups =
+ get_docs_groups(
+ moduledoc_groups ++ config.docs_groups ++ module_data.default_groups,
+ nodes_groups,
+ docs
+ )
+
metadata = Map.put(metadata, :kind, module_data.type)
group = GroupMatcher.match_module(config.groups_for_modules, module, module_data.id, metadata)
{nested_title, nested_context} = module_data.nesting_info || {nil, nil}
@@ -154,8 +165,8 @@ defmodule ExDoc.Retriever do
module: module,
type: module_data.type,
deprecated: metadata[:deprecated],
- docs_groups: config.docs_groups ++ module_data.default_groups,
- docs: ExDoc.Utils.natural_sort_by(docs, &"#{&1.name}/#{&1.arity}"),
+ docs_groups: docs_groups,
+ docs: docs,
doc_format: format,
doc: doc,
source_doc: source_doc,
@@ -189,13 +200,15 @@ defmodule ExDoc.Retriever do
defp get_docs(module_data, source, group_for_doc, annotations_for_docs) do
{:docs_v1, _, _, _, _, _, docs} = module_data.docs
- nodes =
+ {nodes, groups} =
for doc <- docs,
doc_data = module_data.language.doc_data(doc, module_data) do
- get_doc(doc, doc_data, module_data, source, group_for_doc, annotations_for_docs)
+ {_node, _group} =
+ get_doc(doc, doc_data, module_data, source, group_for_doc, annotations_for_docs)
end
+ |> Enum.unzip()
- filter_defaults(nodes)
+ {filter_defaults(nodes), groups}
end
defp get_doc(doc, doc_data, module_data, source, group_for_doc, annotations_for_docs) do
@@ -222,9 +235,9 @@ defmodule ExDoc.Retriever do
(source_doc && doc_ast(content_type, source_doc, file: doc_file, line: doc_line + 1)) ||
doc_data.doc_fallback.()
- group = group_for_doc.(metadata) || doc_data.default_group
+ group = normalize_group(group_for_doc.(metadata) || doc_data.default_group)
- %ExDoc.DocNode{
+ doc_node = %ExDoc.DocNode{
id: doc_data.id_key <> nil_or_name(name, arity),
name: name,
arity: arity,
@@ -238,9 +251,11 @@ defmodule ExDoc.Retriever do
specs: doc_data.specs,
source_url: source_url,
type: doc_data.type,
- group: group,
+ group: group.title,
annotations: annotations
}
+
+ {doc_node, group}
end
defp get_defaults(_name, _arity, 0), do: []
@@ -261,6 +276,57 @@ defmodule ExDoc.Retriever do
end)
end
+ defp get_docs_groups(module_groups, nodes_groups, doc_nodes) do
+ module_groups = Enum.map(module_groups, &normalize_group/1)
+
+ # Doc nodes already have normalized groups
+ nodes_groups_descriptions = Map.new(nodes_groups, &{&1.title, &1.description})
+
+ normal_groups = module_groups ++ nodes_groups
+ nodes_by_group_title = Enum.group_by(doc_nodes, & &1.group)
+
+ {docs_groups, _} =
+ Enum.flat_map_reduce(normal_groups, %{}, fn
+ group, seen when is_map_key(seen, group.title) ->
+ {[], seen}
+
+ group, seen ->
+ seen = Map.put(seen, group.title, true)
+
+ case Map.get(nodes_by_group_title, group.title, []) do
+ [] ->
+ {[], seen}
+
+ child_nodes ->
+ group = finalize_group(group, child_nodes, nodes_groups_descriptions)
+ {[group], seen}
+ end
+ end)
+
+ docs_groups
+ end
+
+ defp finalize_group(group, doc_nodes, description_fallbacks) do
+ description =
+ case group.description do
+ nil -> Map.get(description_fallbacks, group.title)
+ text -> text
+ end
+
+ doc_ast =
+ case description do
+ nil -> nil
+ text -> doc_ast("text/markdown", %{"en" => text}, [])
+ end
+
+ %ExDoc.DocGroupNode{
+ title: group.title,
+ description: description,
+ doc: doc_ast,
+ docs: doc_nodes
+ }
+ end
+
## General helpers
defp nil_or_name(name, arity) do
@@ -314,4 +380,19 @@ defmodule ExDoc.Retriever do
defp source_link(%{url_pattern: url_pattern, relative_path: path}, line) do
url_pattern.(path, line)
end
+
+ defp normalize_group(group) do
+ case group do
+ %{title: title, description: description}
+ when is_binary(title) and (is_binary(description) or is_nil(description)) ->
+ %{group | title: title, description: description}
+
+ kw when is_list(kw) ->
+ true = Keyword.keyword?(kw)
+ %{title: to_string(Keyword.fetch!(kw, :title)), description: kw[:description]}
+
+ title when is_binary(title) when is_atom(title) ->
+ %{title: to_string(title), description: nil}
+ end
+ end
end
diff --git a/test/ex_doc/formatter/epub/templates_test.exs b/test/ex_doc/formatter/epub/templates_test.exs
index 724ea3400..ff93d13e5 100644
--- a/test/ex_doc/formatter/epub/templates_test.exs
+++ b/test/ex_doc/formatter/epub/templates_test.exs
@@ -145,6 +145,42 @@ defmodule ExDoc.Formatter.EPUB.TemplatesTest do
assert content =~ ~r{id="functions".*id="example_1/0"}ms
end
+ test "outputs groups descriptions" do
+ content =
+ get_module_page([CompiledWithDocs],
+ group_for_doc: fn metadata ->
+ if metadata[:purpose] == :example do
+ [
+ title: "Example functions",
+ description: """
+ ### A section heading example
+
+ A content example.
+
+ See `example/1` or `example/2`.
+ A link to `flatten/1`.
+ """
+ ]
+ else
+ "Functions"
+ end
+ end
+ )
+
+ doc = LazyHTML.from_document(content)
+
+ assert Enum.count(doc["div.group-description"]) == 1
+ assert Enum.count(doc["#group-description-example-functions"]) == 1
+ assert Enum.count(doc["#group-description-example-functions h3"]) == 1
+ assert Enum.count(doc["#group-example-functions-a-section-heading-example"]) == 1
+ assert Enum.count(doc["#example-functions .group-description a[href='#example/1']"]) == 1
+ assert Enum.count(doc["#example-functions .group-description a[href='#example/2']"]) == 1
+ assert Enum.count(doc["#example-functions .group-description a[href='#flatten/1']"]) == 1
+
+ assert content =~ ~s[A section heading example]
+ assert content =~ "
A content example.
"
+ end
+
test "outputs summaries" do
content = get_module_page([CompiledWithDocs])
diff --git a/test/ex_doc/formatter/html/templates_test.exs b/test/ex_doc/formatter/html/templates_test.exs
index f13e21260..d0b7c404a 100644
--- a/test/ex_doc/formatter/html/templates_test.exs
+++ b/test/ex_doc/formatter/html/templates_test.exs
@@ -469,6 +469,42 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do
assert Enum.count(doc["#functions [id='example/2']"]) == 0
end
+ test "outputs groups descriptions", context do
+ content =
+ get_module_page([CompiledWithDocs], context,
+ group_for_doc: fn metadata ->
+ if metadata[:purpose] == :example do
+ [
+ title: "Example functions",
+ description: """
+ ### A section heading example
+
+ A content example.
+
+ See `example/1` or `example/2`.
+ A link to `flatten/1`.
+ """
+ ]
+ else
+ "Functions"
+ end
+ end
+ )
+
+ doc = LazyHTML.from_document(content)
+
+ assert Enum.count(doc["div.group-description"]) == 1
+ assert Enum.count(doc["#group-description-example-functions"]) == 1
+ assert Enum.count(doc["#group-description-example-functions h3"]) == 1
+ assert Enum.count(doc["#group-example-functions-a-section-heading-example"]) == 1
+ assert Enum.count(doc["#example-functions .group-description a[href='#example/1']"]) == 1
+ assert Enum.count(doc["#example-functions .group-description a[href='#example/2']"]) == 1
+ assert Enum.count(doc["#example-functions .group-description a[href='#flatten/1']"]) == 1
+
+ assert content =~ ~s[A section heading example]
+ assert content =~ "A content example.
"
+ end
+
test "outputs deprecation information", context do
content = get_module_page([CompiledWithDocs], context)
diff --git a/test/ex_doc/group_matcher_test.exs b/test/ex_doc/group_matcher_test.exs
index 9f5832570..a73f4363d 100644
--- a/test/ex_doc/group_matcher_test.exs
+++ b/test/ex_doc/group_matcher_test.exs
@@ -2,16 +2,6 @@ defmodule ExDoc.GroupMatcherTest do
use ExUnit.Case, async: true
import ExDoc.GroupMatcher
- describe "group_by" do
- test "group by given data with leftovers" do
- assert group_by([1, 3, 5], [%{key: 1}, %{key: 3}, %{key: 2}], & &1.key) == [
- {1, [%{key: 1}]},
- {3, [%{key: 3}]},
- {2, [%{key: 2}]}
- ]
- end
- end
-
describe "module matching" do
test "by atom names" do
patterns = [
diff --git a/test/ex_doc/retriever/erlang_test.exs b/test/ex_doc/retriever/erlang_test.exs
index 671fb329f..56f10126f 100644
--- a/test/ex_doc/retriever/erlang_test.exs
+++ b/test/ex_doc/retriever/erlang_test.exs
@@ -59,7 +59,7 @@ defmodule ExDoc.Retriever.ErlangTest do
moduledoc_line: 2,
moduledoc_file: moduledoc_file,
docs: [equiv_function2, function1, function2],
- docs_groups: ["Types", "Callbacks", "Functions"],
+ docs_groups: [%{title: "Functions"}],
group: nil,
id: "mod",
language: ExDoc.Language.Erlang,
@@ -156,7 +156,7 @@ defmodule ExDoc.Retriever.ErlangTest do
moduledoc_line: 6,
moduledoc_file: moduledoc_file,
docs: [type, callback, function],
- docs_groups: ["Types", "Callbacks", "Functions"],
+ docs_groups: [%{title: "Types"}, %{title: "Callbacks"}, %{title: "Functions"}],
group: nil,
id: "mod",
language: ExDoc.Language.Erlang,
@@ -397,7 +397,7 @@ defmodule ExDoc.Retriever.ErlangTest do
deprecated: nil,
moduledoc_line: _,
docs: [function1, function2],
- docs_groups: ["Types", "Callbacks", "Functions"],
+ docs_groups: [%{title: "Functions"}],
group: nil,
id: "mod",
language: ExDoc.Language.Erlang,
diff --git a/test/ex_doc/retriever_test.exs b/test/ex_doc/retriever_test.exs
index 80588962a..243726d33 100644
--- a/test/ex_doc/retriever_test.exs
+++ b/test/ex_doc/retriever_test.exs
@@ -108,6 +108,87 @@ defmodule ExDoc.RetrieverTest do
assert %{id: "baz/0", group: "c"} = baz
end
+ test "default_group_for_doc can return group description from @moduledoc", c do
+ elixirc(c, ~S"""
+ defmodule A do
+
+ @moduledoc groups: [
+ "c",
+ %{title: "b", description: "predefined b"}
+ ]
+
+ @doc test_group: "a"
+ @callback foo() :: :ok
+
+ @doc test_group: "b"
+ def bar(), do: :ok
+
+ @doc test_group: "c"
+ def baz(), do: :ok
+ end
+ """)
+
+ config = %ExDoc.Config{
+ group_for_doc: fn meta ->
+ case meta[:test_group] do
+ "a" -> [title: "a", description: "for a"]
+ "b" -> [title: "b", description: "ignored description"]
+ "c" -> [title: "c", description: "for c"]
+ end
+ end
+ }
+
+ {[mod], []} = Retriever.docs_from_modules([A], config)
+
+ assert [c, b, a] = mod.docs_groups
+
+ # Description returned by the function should override nil
+ assert %{title: "c", description: "for c"} = c
+
+ # Description returned by the function should not override a
+ # description from @moduledoc
+ assert %{title: "b", description: "predefined b"} = b
+
+ # Description returned by th function should define a description
+ # for leftover groups
+ assert %{title: "a", description: "for a"} = a
+
+ [bar, baz, foo] = mod.docs
+
+ assert %{id: "c:foo/0", group: "a"} = foo
+ assert %{id: "bar/0", group: "b"} = bar
+ assert %{id: "baz/0", group: "c"} = baz
+ end
+
+ test "function groups description use moduledoc :groups metadata", c do
+ elixirc(c, ~S"""
+ defmodule A do
+ @moduledoc groups: [
+ "c",
+ %{title: "b", description: "text for b"}
+ ]
+
+ @doc group: "a"
+ @callback foo() :: :ok
+
+ @doc group: "b"
+ def bar(), do: :ok
+
+ @doc group: "c"
+ def baz(), do: :ok
+ end
+ """)
+
+ config = %ExDoc.Config{}
+ {[mod], []} = Retriever.docs_from_modules([A], config)
+
+ assert [
+ %{description: nil, title: "c"},
+ %{description: "text for b", title: "b"},
+ %{description: nil, title: "a"}
+ ] = mod.docs_groups
+ end
+
test "function annotations", c do
elixirc(c, ~S"""
defmodule A do