diff --git a/lib/ex_doc.ex b/lib/ex_doc.ex index 28b919b4d..9a5a382b7 100644 --- a/lib/ex_doc.ex +++ b/lib/ex_doc.ex @@ -35,6 +35,7 @@ defmodule ExDoc do deps: [], extra_section: nil, extras: [], + groups_for_extras: [], filter_prefix: nil, formatter: @default.formatter, formatter_opts: [], @@ -42,6 +43,7 @@ defmodule ExDoc do language: @default.language, logo: nil, main: nil, + groups_for_modules: [], output: @default.output, project: nil, retriever: @default.retriever, @@ -63,6 +65,7 @@ defmodule ExDoc do deps: [{ebin_path :: String.t, doc_url :: String.t}], extra_section: nil | String.t, extras: list(), + groups_for_extras: keyword(), filter_prefix: nil | String.t, formatter: nil | String.t, formatter_opts: Keyword.t, @@ -70,6 +73,7 @@ defmodule ExDoc do language: String.t, logo: nil | Path.t, main: nil | String.t, + groups_for_modules: keyword(), output: nil | Path.t, project: nil | String.t, retriever: :atom, diff --git a/lib/ex_doc/formatter/html.ex b/lib/ex_doc/formatter/html.ex index ec88992ab..e33721c55 100644 --- a/lib/ex_doc/formatter/html.ex +++ b/lib/ex_doc/formatter/html.ex @@ -4,7 +4,7 @@ defmodule ExDoc.Formatter.HTML do """ alias __MODULE__.{Assets, Autolink, Templates} - alias ExDoc.Markdown + alias ExDoc.{Markdown, GroupMatcher} @main "api-reference" @@ -178,32 +178,34 @@ defmodule ExDoc.Formatter.HTML do def build_extras(project_nodes, config, extension) do config.extras |> Enum.map(&Task.async(fn -> - build_extra(&1, project_nodes, extension) + build_extra(&1, project_nodes, extension, config) end)) |> Enum.map(&Task.await(&1, :infinity)) end - defp build_extra({input, options}, project_nodes, extension) do + defp build_extra({input, options}, project_nodes, extension, config) do input = to_string(input) id = options[:filename] || input |> input_to_title() |> title_to_id() - build_extra(input, id, options[:title], options[:group], project_nodes, extension) + build_extra(input, id, options[:title], options[:group], project_nodes, extension, config) end - defp build_extra(input, project_nodes, extension) do + defp build_extra(input, project_nodes, extension, config) do id = input |> input_to_title() |> title_to_id() - build_extra(input, id, nil, "", project_nodes, extension) + build_extra(input, id, nil, "", project_nodes, extension, config) end - defp build_extra(input, id, title, group, project_nodes, extension) do + defp build_extra(input, id, title, group, project_nodes, extension, config) do if valid_extension_name?(input) do content = input |> File.read!() |> Autolink.project_doc(project_nodes, nil, extension) + group_pattern = GroupMatcher.match_extra config.groups_for_extras, input + html_content = Markdown.to_html(content, file: input, line: 1) title = title || extract_title(html_content) || input_to_title(input) - %{id: id, title: title, group: group, content: html_content} + %{id: id, title: title, group: group_pattern || group, content: html_content} else raise ArgumentError, "file format not recognized, allowed format is: .md" end diff --git a/lib/ex_doc/formatter/html/templates.ex b/lib/ex_doc/formatter/html/templates.ex index ef883df42..316dbf45f 100644 --- a/lib/ex_doc/formatter/html/templates.ex +++ b/lib/ex_doc/formatter/html/templates.ex @@ -176,9 +176,9 @@ defmodule ExDoc.Formatter.HTML.Templates do |> Enum.map_join(",", &sidebar_items_by_type/1) if items == "" do - ~s/{"id":"#{module_node.id}","title":"#{module_node.title}"}/ + ~s/{"id":"#{module_node.id}","title":"#{module_node.title}","group":"#{module_node.group}"}/ else - ~s/{"id":"#{module_node.id}","title":"#{module_node.title}",#{items}}/ + ~s/{"id":"#{module_node.id}","title":"#{module_node.title}","group":"#{module_node.group}",#{items}}/ end end diff --git a/lib/ex_doc/group_matcher.ex b/lib/ex_doc/group_matcher.ex new file mode 100644 index 000000000..652eac766 --- /dev/null +++ b/lib/ex_doc/group_matcher.ex @@ -0,0 +1,48 @@ +defmodule ExDoc.GroupMatcher do + @moduledoc """ + Match modules or extra pages to groups. + + Matching does happen by explicitly matching names or using regular expressions. + """ + @type pattern :: Regex.t | module() | String.t + @type patterns :: pattern | [pattern] + @type group_patterns :: keyword(patterns) + + @doc """ + Does try to find a matching group for the given module name or id + """ + @spec match_module(group_patterns, module(), String.t) :: String.t | nil + def match_module(group_patterns, module, id) do + match_group_patterns(group_patterns, fn pattern -> + case pattern do + %Regex{} = regex -> Regex.match?(regex, id) + string when is_binary(string) -> id == string + atom -> atom == module + end + end) + end + + @doc """ + Does try to find a matching group for the given extra filename + """ + @spec match_extra(group_patterns, String.t) :: String.t | nil + def match_extra(group_patterns, filename) do + match_group_patterns(group_patterns, fn pattern -> + case pattern do + %Regex{} = regex -> Regex.match?(regex, filename) + string when is_binary(string) -> filename == string + end + end) + end + + defp match_group_patterns(group_patterns, matcher) do + Enum.find_value(group_patterns, fn {group, patterns} -> + patterns = List.wrap patterns + match_patterns(patterns, matcher) && Atom.to_string(group) + end) + end + + defp match_patterns(patterns, matcher) do + Enum.any?(patterns, matcher) || nil + end +end diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex index 1383ddf78..7e6359e50 100644 --- a/lib/ex_doc/retriever.ex +++ b/lib/ex_doc/retriever.ex @@ -3,13 +3,15 @@ defmodule ExDoc.ModuleNode do Structure that represents a *module* """ - defstruct id: nil, title: nil, module: nil, doc: nil, doc_line: nil, - docs: [], typespecs: [], source_path: nil, source_url: nil, type: nil + defstruct id: nil, title: nil, module: nil, group: nil, doc: nil, + doc_line: nil, docs: [], typespecs: [], source_path: nil, + source_url: nil, type: nil @type t :: %__MODULE__{ id: nil | String.t, title: nil | String.t, module: nil | String.t, + group: nil | String.t, docs: list(), doc: nil | String.t, doc_line: non_neg_integer(), @@ -81,6 +83,7 @@ defmodule ExDoc.Retriever do Functions to extract documentation information from modules. """ + alias ExDoc.GroupMatcher alias ExDoc.Retriever.Error alias Kernel.Typespec @@ -115,7 +118,10 @@ defmodule ExDoc.Retriever do modules |> Enum.map(&get_module(&1, config)) |> Enum.filter(&(&1)) - |> Enum.sort(&(&1.id <= &2.id)) + |> Enum.sort_by(fn module -> + group_index = Enum.find_index(config.groups_for_modules, fn {k, _v} -> Atom.to_string(k) == module.group end) + {group_index || -1, module.id} + end) end defp filename_to_module(name) do @@ -171,10 +177,13 @@ defmodule ExDoc.Retriever do {title, id} = module_title_and_id(module, type) + module_group = GroupMatcher.match_module config.groups_for_modules, module, id + %ExDoc.ModuleNode{ id: id, title: title, module: module_info.name, + group: module_group, type: type, docs: docs, doc: moduledoc, diff --git a/test/ex_doc/formatter/html/templates_test.exs b/test/ex_doc/formatter/html/templates_test.exs index 313c10573..f7d89a401 100644 --- a/test/ex_doc/formatter/html/templates_test.exs +++ b/test/ex_doc/formatter/html/templates_test.exs @@ -14,8 +14,8 @@ defmodule ExDoc.Formatter.HTML.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!, @@ -24,6 +24,8 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do source_url: source_url(), output: "test/tmp/html_templates" } + + struct(default, config) end defp get_module_page(names) do @@ -211,6 +213,15 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do assert content =~ ~r("id":"CompiledWithDocs.Nested")ms end + test "list_page outputs 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)) + content = Templates.create_sidebar_items(%{modules: nodes}, []) + + assert content =~ ~r("id":"CompiledWithDocs","title":"CompiledWithDocs","group":"Group")ms + end + ## MODULES test "module_page outputs the functions and docstrings" do diff --git a/test/ex_doc/formatter/html_test.exs b/test/ex_doc/formatter/html_test.exs index dfc0c1946..fa9b94644 100644 --- a/test/ex_doc/formatter/html_test.exs +++ b/test/ex_doc/formatter/html_test.exs @@ -249,13 +249,23 @@ defmodule ExDoc.Formatter.HTMLTest do assert content =~ ~r{Getting Started – Elixir v1.0.1} content = read_wildcard!("#{output_dir()}/dist/sidebar_items-*.js") assert content =~ ~r{"id":"readme","title":"Getting Started","group":""} - end + end test "run generates pages with custom group" do + extra_config = [ + extras: ["test/fixtures/README.md"], + groups_for_extras: ["Intro": ~r/fixtures\/READ.?/] + ] + generate_docs(doc_config(extra_config)) + content = read_wildcard!("#{output_dir()}/dist/sidebar_items-*.js") + assert content =~ ~r{"id":"readme","title":"README","group":"Intro"} + end + + test "run generates pages with custom group via the deprecated method as keyword opts" do generate_docs(doc_config(extras: ["test/fixtures/README.md": [group: "Intro"]])) content = read_wildcard!("#{output_dir()}/dist/sidebar_items-*.js") assert content =~ ~r{"id":"readme","title":"README","group":"Intro"} - end + end test "run generates with auto-extracted title" do generate_docs(doc_config(extras: ["test/fixtures/ExtraPage.md"])) diff --git a/test/ex_doc/group_matcher_test.exs b/test/ex_doc/group_matcher_test.exs new file mode 100644 index 000000000..ca9371a8b --- /dev/null +++ b/test/ex_doc/group_matcher_test.exs @@ -0,0 +1,52 @@ +defmodule ExDoc.GroupMatcherTest do + use ExUnit.Case, async: true + import ExDoc.GroupMatcher + + describe "module matching" do + test "it can match modules by their atom names" do + patterns = [ + "Group": [MyApp.SomeModule, :lists] + ] + assert "Group" == match_module(patterns, MyApp.SomeModule, "MyApp.SomeModule") + assert "Group" == match_module(patterns, :lists, ":lists") + assert nil == match_module(patterns, MyApp.SomeOtherModule, "MyApp.SomeOtherModule") + end + + test "it can match modules by their string names" do + patterns = [ + "Group": ["MyApp.SomeModule", ":lists"] + ] + assert "Group" == match_module(patterns, MyApp.SomeModule, "MyApp.SomeModule") + assert "Group" == match_module(patterns, :lists, ":lists") + assert nil == match_module(patterns, MyApp.SomeOtherModule, "MyApp.SomeOtherModule") + end + + test "it can match modules by regular expressions" do + patterns = [ + "Group": ~r/MyApp\..?/ + ] + assert "Group" == match_module(patterns, MyApp.SomeModule, "MyApp.SomeModule") + assert "Group" == match_module(patterns, MyApp.SomeOtherModule, "MyApp.SomeOtherModule") + assert nil == match_module(patterns, MyAppWeb.SomeOtherModule, "MyAppWeb.SomeOtherModule") + end + end + + describe "extras matching" do + test "it can match extra files by their string names" do + patterns = [ + "Group": ["docs/handling/testing.md"] + ] + assert "Group" == match_extra(patterns, "docs/handling/testing.md") + assert nil == match_extra(patterns, "docs/handling/setup.md") + end + + test "it can match extra files by regular expressions" do + patterns = [ + "Group": ~r/docs\/handling?/ + ] + assert "Group" == match_extra(patterns, "docs/handling/testing.md") + assert "Group" == match_extra(patterns, "docs/handling/setup.md") + assert nil == match_extra(patterns, "docs/introduction.md") + end + end +end diff --git a/test/ex_doc/retriever_test.exs b/test/ex_doc/retriever_test.exs index 1df8dc99c..6b2e9def0 100644 --- a/test/ex_doc/retriever_test.exs +++ b/test/ex_doc/retriever_test.exs @@ -3,10 +3,11 @@ defmodule ExDoc.RetrieverTest do alias ExDoc.Retriever - defp docs_from_files(names, url_pattern \\ "http://example.com/%{path}#L%{line}") do + defp docs_from_files(names, config \\ []) do files = Enum.map names, fn(n) -> "test/tmp/Elixir.#{n}.beam" end - config = %ExDoc.Config{source_url_pattern: url_pattern, source_root: File.cwd!} - Retriever.docs_from_files(files, config) + default = %ExDoc.Config{source_url_pattern: "http://example.com/%{path}#L%{line}", source_root: File.cwd!} + + Retriever.docs_from_files(files, struct(default, config)) end ## MODULES @@ -44,6 +45,29 @@ defmodule ExDoc.RetrieverTest do assert module_node.module == CompiledWithDocs end + describe "docs_from_files returns the group" do + test "atom" do + [module_node] = docs_from_files ["CompiledWithDocs"], groups_for_modules: [ + "Group": [CompiledWithDocs] + ] + assert module_node.group == "Group" + end + + test "string" do + [module_node] = docs_from_files ["CompiledWithDocs"], groups_for_modules: [ + "Group": ["CompiledWithDocs"] + ] + assert module_node.group == "Group" + end + + test "regex" do + [module_node] = docs_from_files ["CompiledWithDocs"], groups_for_modules: [ + "Group": ~r/^CompiledWith.?/ + ] + assert module_node.group == "Group" + end + end + test "docs_from_files returns the moduledoc info" do [module_node] = docs_from_files ["CompiledWithDocs"] assert module_node.doc == "moduledoc\n\n\#\# Example ☃ Unicode > escaping\n CompiledWithDocs.example\n\n### Example H3 heading\n\nexample\n" @@ -135,7 +159,7 @@ defmodule ExDoc.RetrieverTest do end test "docs_from_files returns the source" do - [module_node] = docs_from_files ["CompiledWithDocs"], "http://foo.com/bar/%{path}#L%{line}" + [module_node] = docs_from_files ["CompiledWithDocs"], source_url_pattern: "http://foo.com/bar/%{path}#L%{line}" assert module_node.source_url == "http://foo.com/bar/test/fixtures/compiled_with_docs.ex\#L1" end @@ -147,9 +171,8 @@ defmodule ExDoc.RetrieverTest do end test "docs_from_modules fails when module is not available" do - config = %ExDoc.Config{source_url_pattern: "http://example.com/%{path}#L%{line}", source_root: File.cwd!} assert_raise ExDoc.Retriever.Error, "module NotAvailable is not defined/available", fn -> - docs_from_files(["NotAvailable"], config) + docs_from_files(["NotAvailable"]) end end