diff --git a/lib/ex_doc.ex b/lib/ex_doc.ex index 9591ffed8..053e9b58a 100644 --- a/lib/ex_doc.ex +++ b/lib/ex_doc.ex @@ -3,12 +3,34 @@ defmodule ExDoc do Elixir Documentation System. ExDoc produces documentation for Elixir projects """ + defmodule Default do + @moduledoc false + + @config %{ + :formatter => "html", + :output => "./doc", + :source_ref => "master", + :retriever => ExDoc.Retriever, + } + + @spec config :: map + def config do + @config + end + + @spec config(atom) :: term + def config(field) do + Map.get(config(), field) + end + end + defmodule Config do @moduledoc """ Configuration structure that holds all the available options for ExDoc You can find more details about these options in the `ExDoc.CLI` module. """ + defstruct [ assets: nil, canonical: nil, @@ -16,15 +38,16 @@ defmodule ExDoc do extra_section: nil, extras: [], filter_prefix: nil, - formatter: "html", + formatter: ExDoc.Default.config(:formatter), formatter_opts: [], homepage_url: nil, logo: nil, main: nil, - output: "doc", + output: ExDoc.Default.config(:output), project: nil, - retriever: ExDoc.Retriever, + retriever: ExDoc.Default.config(:retriever), source_beam: nil, + source_ref: ExDoc.Default.config(:source_ref), source_root: nil, source_url: nil, source_url_pattern: nil, @@ -48,6 +71,7 @@ defmodule ExDoc do project: nil | String.t, retriever: :atom, source_beam: nil | String.t, + source_ref: nil | String.t, source_root: nil | String.t, source_url: nil | String.t, source_url_pattern: nil | String.t, @@ -78,17 +102,46 @@ defmodule ExDoc do # Builds configuration by merging `options`, and normalizing the options. @spec build_config(String.t, String.t, Keyword.t) :: ExDoc.Config.t defp build_config(project, vsn, options) do - options = normalize_options(options) + options = + options + |> normalize_directory([:assets, :output, :source_root]) + |> normalize_source_url_pattern() + preconfig = %Config{ project: project, version: vsn, - main: options[:main], - homepage_url: options[:homepage_url], source_root: options[:source_root] || File.cwd!, } struct(preconfig, options) end + @doc """ + Replaces all empty string values in `options` with `nil`. + """ + @spec nilify_options(keyword) :: keyword + def nilify_options(options) do + for {field, value} <- options do + case value do + "" -> {field, nil} + _ -> {field, value} + end + end + end + + @doc """ + Updates `field` in `options` if its value is `nil`. + """ + @spec normalize_options(keyword, atom, term) :: keyword + def normalize_options(options, field, default) do + if is_nil(options[field]) do + Keyword.put(options, field, default) + else + options + end + end + + # Helpers + # Short path for programmatic interface defp find_formatter(modname) when is_atom(modname), do: modname @@ -111,14 +164,26 @@ defmodule ExDoc do modname end - # Helpers + @doc false + def normalize_directory(options, fields) when is_list(fields) do + Enum.reduce(fields, options, fn(field, options_acc) -> + normalize_directory(options_acc, field) + end) + end - defp normalize_options(options) do - pattern = options[:source_url_pattern] || guess_url(options[:source_url], options[:source_ref] || "master") - options = Keyword.put(options, :source_url_pattern, pattern) + def normalize_directory(options, field) when is_atom(field) do + if is_binary(options[field]) do + Keyword.put(options, field, String.trim_trailing(options[field], "/")) + else + options + end + end - if is_bitstring(options[:output]) do - Keyword.put(options, :output, String.trim_trailing(options[:output], "/")) + @doc false + def normalize_source_url_pattern(options, default_source_ref \\ ExDoc.Default.config(:source_ref)) do + if !options[:source_url_pattern] && options[:source_url] do + source_ref = options[:source_ref] || default_source_ref + Keyword.put(options, :source_url_pattern, guess_url(options[:source_url], source_ref)) else options end diff --git a/lib/ex_doc/cli.ex b/lib/ex_doc/cli.ex index 2cc7679fd..55d818696 100644 --- a/lib/ex_doc/cli.ex +++ b/lib/ex_doc/cli.ex @@ -52,6 +52,7 @@ defmodule ExDoc.CLI do opts = opts |> Keyword.put(:source_beam, source_beam) + |> ExDoc.nilify_options() |> merge_config() generator.(project, version, opts) end diff --git a/lib/mix/tasks/docs.ex b/lib/mix/tasks/docs.ex index 3b1f1fea1..593e34372 100644 --- a/lib/mix/tasks/docs.ex +++ b/lib/mix/tasks/docs.ex @@ -124,7 +124,11 @@ defmodule Mix.Tasks.Docs do config |> get_docs_opts() |> Keyword.merge(cli_opts) - |> normalize_source_url(config) + |> ExDoc.nilify_options() + |> ExDoc.normalize_options(:source_ref, config[:source_ref] || ExDoc.Default.config(:source_ref)) + |> ExDoc.normalize_options(:source_url, config[:source_url]) + |> ExDoc.normalize_options(:source_url_pattern, config[:source_url_pattern]) + |> ExDoc.normalize_source_url_pattern(config[:source_ref] || ExDoc.Default.config(:source_ref)) |> normalize_source_beam(config) |> normalize_main() |> normalize_deps() @@ -157,14 +161,6 @@ defmodule Mix.Tasks.Docs do Mix.shell.info [:green, "View them at #{inspect index}."] end - defp normalize_source_url(options, config) do - if source_url = config[:source_url] do - Keyword.put(options, :source_url, source_url) - else - options - end - end - defp normalize_source_beam(options, config) do compile_path = if Mix.Project.umbrella?(config) do diff --git a/test/ex_doc_test.exs b/test/ex_doc_test.exs index aff051510..0e7500ef7 100644 --- a/test/ex_doc_test.exs +++ b/test/ex_doc_test.exs @@ -49,6 +49,20 @@ defmodule ExDocTest do fn -> ExDoc.generate_docs project, version, options end end + test "default configuration" do + assert Map.get(%ExDoc.Config{}, :formatter) == "html" + assert ExDoc.Default.config(:formatter) == "html" + + assert Map.get(%ExDoc.Config{}, :output) == "./doc" + assert ExDoc.Default.config(:output) == "./doc" + + assert Map.get(%ExDoc.Config{}, :source_ref) == "master" + assert ExDoc.Default.config(:source_ref) == "master" + + assert Map.get(%ExDoc.Config{}, :retriever) == ExDoc.Retriever + assert ExDoc.Default.config(:retriever) == ExDoc.Retriever + end + test "version" do assert ExDoc.version =~ ~r{\d+\.\d+\.\d+} end diff --git a/test/mix/tasks/docs_test.exs b/test/mix/tasks/docs_test.exs index a0fdaaeac..8df1d3f1c 100644 --- a/test/mix/tasks/docs_test.exs +++ b/test/mix/tasks/docs_test.exs @@ -10,75 +10,75 @@ defmodule Mix.Tasks.DocsTest do end test "inflects values from app and version" do - assert [{"ex_doc", "0.1.0", [formatter: "html", deps: _, source_beam: _]}] = + assert [{"ex_doc", "0.1.0", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _]}] = run([], [app: :ex_doc, version: "0.1.0"]) end test "accepts multiple formatters from CLI" do - assert [{"ex_doc", "0.1.0", [formatter: "html", deps: _, source_beam: _]}, - {"ex_doc", "0.1.0", [formatter: "epub", deps: _, source_beam: _]}] = + assert [{"ex_doc", "0.1.0", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _]}, + {"ex_doc", "0.1.0", [formatter: "epub", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _]}] = run(["-f", "html", "-f", "epub"], [app: :ex_doc, version: "0.1.0"]) end test "accepts multiple formatters from config" do - assert [{"ex_doc", "0.1.0", [formatter: "html", deps: _, source_beam: _, formatters: _]}, - {"ex_doc", "0.1.0", [formatter: "epub", deps: _, source_beam: _, formatters: _]}] = + assert [{"ex_doc", "0.1.0", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _ , formatters: _]}, + {"ex_doc", "0.1.0", [formatter: "epub", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _ , formatters: _]}] = run([], [app: :ex_doc, version: "0.1.0", docs: [formatters: ["html", "epub"]]]) end test "uses the given name" do - assert [{"ExDoc", "0.1.0", [formatter: "html", deps: _, source_beam: _]}] = + assert [{"ExDoc", "0.1.0", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _]}] = run([], [app: :ex_doc, version: "0.1.0", name: "ExDoc"]) end test "accepts modules in :main" do - assert [{"ex_doc", "dev", [formatter: "html", deps: _, main: "Sample", source_beam: _, ]}] = + assert [{"ex_doc", "dev", [formatter: "html", deps: _, main: "Sample", source_beam: _, source_url_pattern: _, source_url: _, source_ref: _]}] = run([], [app: :ex_doc, docs: [main: Sample]]) end test "accepts files in :main" do - assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, main: "another"]}] = + assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _ , main: "another"]}] = run([], [app: :ex_doc, docs: [main: "another"]]) end test "accepts output in :output" do - assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, output: "hello"]}] = + assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _ , output: "hello"]}] = run([], [app: :ex_doc, docs: [output: "hello"]]) end test "parses output with lower preference than options" do - assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, output: "world"]}] = + assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _ , output: "world"]}] = run(~w(-o world), [app: :ex_doc, docs: [output: "world"]]) end test "includes dependencies" do - assert [{"ex_doc", "dev", [formatter: "html", deps: deps, source_beam: _]}] = + assert [{"ex_doc", "dev", [formatter: "html", deps: deps, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _]}] = run([], [app: :ex_doc, docs: []]) assert List.keyfind(deps, Application.app_dir(:earmark), 0) == {Application.app_dir(:earmark), "https://hexdocs.pm/earmark/#{Application.spec(:earmark, :vsn)}/"} end test "allows custom dependency paths" do - assert [{"ex_doc", "dev", [formatter: "html", deps: deps, source_beam: _]}] = + assert [{"ex_doc", "dev", [formatter: "html", deps: deps, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _]}] = run([], [app: :ex_doc, docs: [deps: [earmark: "foo"]]]) assert List.keyfind(deps, Application.app_dir(:earmark), 0) == {Application.app_dir(:earmark), "foo"} end test "accepts lazy docs" do - assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, main: "another"]}] = + assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _, main: "another"]}] = run([], [app: :ex_doc, docs: fn -> [main: "another"] end]) end test "accepts source_url from root" do - assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, - source_url: "http://github.com/elixir-lang/ex_doc"]}] = + assert [{"ex_doc", "dev", [formatter: "html", deps: _, source_beam: _, source_url_pattern: _, + source_url: "http://github.com/elixir-lang/ex_doc", source_ref: _]}] = run([], [app: :ex_doc, source_url: "http://github.com/elixir-lang/ex_doc"]) end test "supports umbrella project" do Mix.Project.in_project(:umbrella, "test/fixtures/umbrella", fn _mod -> - assert [{"umbrella", "dev", [formatter: "html", deps: deps, source_beam: _]}] = + assert [{"umbrella", "dev", [formatter: "html", deps: deps, source_beam: _, source_url_pattern: _, source_url: _, source_ref: _]}] = run([], [app: :umbrella, apps_path: "apps/"]) assert List.keyfind(deps, Application.app_dir(:foo), 0) ==