diff --git a/elvis.config b/elvis.config index 6d9fab1..63b5ba8 100644 --- a/elvis.config +++ b/elvis.config @@ -8,7 +8,8 @@ {elvis_style, function_naming_convention, disable}, {elvis_style, dont_repeat_yourself, disable}, {elvis_style, no_spec_with_records, disable}, - {elvis_style, no_block_expressions, disable}]}, + {elvis_style, no_block_expressions, disable}, + {elvis_style, invalid_dynamic_call, disable}]}, #{dirs => ["test/**"], filter => "*.erl", ruleset => erl_files}, diff --git a/src/rebar3_edoc_extensions_prv.erl b/src/rebar3_edoc_extensions_prv.erl index c8b8a4a..c2b6d70 100644 --- a/src/rebar3_edoc_extensions_prv.erl +++ b/src/rebar3_edoc_extensions_prv.erl @@ -19,6 +19,14 @@ {rebar_state, add_provider, 2}, {rebar_state, command_parsed_args, 1}]). +-if(?OTP_RELEASE >= 24). +-dialyzer({no_underspecs, [chain_edoc_backends/1, + chain_edoc_backends/2]}). +-else. +-dialyzer({nowarn_function, [chain_edoc_backends/1, + chain_edoc_backends/2]}). +-endif. + -define(PROVIDER, edoc). -define(DEPS, [compile]). -define(GITHUB_URL_BASE, "https://raw.githubusercontent.com"). @@ -106,12 +114,7 @@ do(State) -> State1 = case Ret of ok -> - %% FIXME: Clear conflicting options. - EdocOpts1 = - [{xml_export, rebar3_edoc_extensions_export}, - {layout, rebar3_edoc_extensions_wrapper}, - {doclet, rebar3_edoc_extensions_wrapper} - | EdocOptsWithCSS], + EdocOpts1 = chain_edoc_backends(EdocOptsWithCSS), ?DEBUG("Overriden edoc options: ~p", [EdocOpts1]), rebar_state:set(State, edoc_opts, EdocOpts1); {app_failed, AppName, Reason} -> @@ -267,3 +270,48 @@ generate_wrapping_css(DocDir, Stylesheets) -> Filename = filename:join(DocDir, ?GENERATED_CSS), ok = file:write_file(Filename, Content), ok. + +-spec chain_edoc_backends(EdocOpts) -> EdocOpts when + EdocOpts :: [tuple()]. +chain_edoc_backends(EdocOpts) -> + Options = [xml_export, + layout, + doclet], + chain_edoc_backends(Options, EdocOpts). + +-spec chain_edoc_backends(Options, EdocOpts) -> EdocOpts when + Options :: [xml_export | doclet | layout], + EdocOpts :: [tuple()]. +chain_edoc_backends([xml_export | Rest], EdocOpts) -> + %% For `xml_export', we override the module, regardless of what the user + %% provided. I don't know if we can support module chaining here as there + %% is no options passed to the module functions. Perhaps we could use a + %% `persistent_term', but anyway, I don't have a use case currently to + %% test this. + EdocOpts1 = lists:keystore( + xml_export, 1, EdocOpts, + {xml_export, rebar3_edoc_extensions_export}), + chain_edoc_backends(Rest, EdocOpts1); +chain_edoc_backends([Option | Rest], EdocOpts) + when Option =:= doclet orelse Option =:= layout -> + %% For d`doclet' and `layout', we override the user-configured module by + %% our own. However, we store the former in another option. When + %% processing the documentation, our own module will called the + %% user-configured module and patch its output. + %% + %% If there is no user-configured module, we default to `edoc_doclet' and + %% `edoc_layout'. + DefaultMod = list_to_atom(io_lib:format("edoc_~s", [Option])), + ChainedMod = proplists:get_value(Option, EdocOpts, DefaultMod), + ChainedOption = list_to_atom( + io_lib:format("chained_~s", [Option])), + EdocOpts1 = lists:keystore( + Option, 1, EdocOpts, + {ChainedOption, ChainedMod}), + + EdocOpts2 = lists:keystore( + Option, 1, EdocOpts1, + {Option, rebar3_edoc_extensions_wrapper}), + chain_edoc_backends(Rest, EdocOpts2); +chain_edoc_backends([], EdocOpts) -> + EdocOpts. diff --git a/src/rebar3_edoc_extensions_wrapper.erl b/src/rebar3_edoc_extensions_wrapper.erl index 308e761..a4608b0 100644 --- a/src/rebar3_edoc_extensions_wrapper.erl +++ b/src/rebar3_edoc_extensions_wrapper.erl @@ -10,30 +10,36 @@ -include_lib("edoc/include/edoc_doclet.hrl"). +%% doclet's Ctxt is a #context{} record in Erlang 23 and #doclet_context{} in +%% Erlang 24+. +-if(?OTP_RELEASE >= 24). +-define(RECORD, doclet_context). +-else. +-define(RECORD, context). +-endif. + -include("rebar3_edoc_extensions.hrl"). --export([module/2, overview/2, type/1]). +%% `edoc_layout` API. +-export([module/2, overview/2]). +%% `edoc_doclet` API. -export([run/2]). --spec module(term(), term()) -> term(). +-spec module(term(), list()) -> term(). module(Element, Options) -> - edoc_layout:module(Element, Options). + LayoutMod = get_chained_mod(layout, Options), + LayoutMod:module(Element, Options). --spec overview(term(), term()) -> [binary() | list()]. +-spec overview(term(), list()) -> [binary() | list()]. overview(Element, Options) -> - Overview = edoc_layout:overview(Element, Options), + LayoutMod = get_chained_mod(layout, Options), + Overview = LayoutMod:overview(Element, Options), patch_html(Overview). --spec type(term()) -> term(). -type(Element) -> - edoc_layout:type(Element). - --spec run(#doclet_gen{}, tuple()) -> ok | no_return(). -run(#doclet_gen{app = App} = Cmd, Ctxt) -> - ok = edoc_doclet:run(Cmd, Ctxt), - %% Ctxt is a #context{} record in Erlang 23 and #doclet_context{} in Erlang - %% 24. The directory is the second field in that record in both cases. - Dir = element(2, Ctxt), +-spec run(#doclet_gen{}, #?RECORD{}) -> ok | no_return(). +run(#doclet_gen{app = App} = Cmd, #?RECORD{dir = Dir} = Ctxt) -> + DocletMod = get_chained_mod(doclet, Ctxt), + ok = DocletMod:run(Cmd, Ctxt), File = filename:join(Dir, "modules-frame.html"), {ok, Content0} = file:read_file(File), Content1 = add_toc(App, Content0, Dir), @@ -43,6 +49,19 @@ run(#doclet_gen{app = App} = Cmd, Ctxt) -> {error, Reason} -> exit({error, Reason}) end. +-spec get_chained_mod(Option, Options | Ctxt) -> Value when + Option :: doclet | layout, + Options :: [tuple()], + Ctxt :: #?RECORD{}, + Value :: any(). +get_chained_mod(doclet, Options) when is_list(Options) -> + proplists:get_value(chained_doclet, Options); +get_chained_mod(layout, Options) when is_list(Options) -> + proplists:get_value(chained_layout, Options); +get_chained_mod(Option, Ctxt) when is_tuple(Ctxt) -> + #?RECORD{opts = Options} = Ctxt, + get_chained_mod(Option, Options). + -spec patch_html(list()) -> list(). patch_html(Html) -> Html2 = re:replace(