Skip to content

Commit

Permalink
Merge pull request #25 from vkatsuba/rework-edoc-backends-chaining
Browse files Browse the repository at this point in the history
Rework how user-specified `doclet` and `layout` modules are managed
  • Loading branch information
dumbbell authored Jun 3, 2022
2 parents 7305e56 + 540fd53 commit 53de439
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 122 deletions.
246 changes: 154 additions & 92 deletions src/rebar3_edoc_extensions_prv.erl
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
{rebar_state, command_parsed_args, 1}]).

-if(?OTP_RELEASE >= 24).
-dialyzer({no_underspecs, [chain_edoc_backends/1,
chain_edoc_backends/2]}).
-dialyzer({no_underspecs, [override_edoc_backends/1,
override_edoc_backends/2]}).
-else.
-dialyzer({nowarn_function, [chain_edoc_backends/1,
chain_edoc_backends/2]}).
-dialyzer({nowarn_function, [override_edoc_backends/1,
override_edoc_backends/2]}).
-endif.

-define(PROVIDER, edoc).
Expand Down Expand Up @@ -55,6 +55,7 @@ init(State) ->
{deps, ?DEPS}, % The list of dependencies
{example, "rebar3 edoc"}, % How to use the plugin
{opts, []}, % list of options understood by the plugin
{profiles, [docs]},
{short_desc, "Override \"edoc\" command to improve EDoc documentation"},
{desc,
"The EDoc-generated documentation is improved "
Expand All @@ -65,59 +66,126 @@ init(State) ->
do(State) ->
EdocOpts = rebar_state:get(State, edoc_opts, []),
DirOpt = proplists:get_value(dir, EdocOpts, "doc"),
EdocOptsWithCSS = lists:keystore(
stylesheet, 1, EdocOpts,
{stylesheet, ?GENERATED_CSS}),
GlobalUserStylesheet = proplists:get_value(stylesheet, EdocOpts),

ProjectApps = rebar_state:project_apps(State),
PrismVersion = rebar_state:get(State, prismjs_version, ?PRISMJS_DEFAULT_VERSION),
PrismTheme = rebar_state:get(State, prismjs_theme, ?PRISMJS_DEFAULT_THEME),
PrismLangs = rebar_state:get(State, prismjs_languages, ?PRISMJS_DEFAULT_LANGS),
Ret = lists:foldl(fun (AppInfo, ok) ->
AppName =
rebar_utils:to_list(
rebar_app_info:name(AppInfo)),
AppDir = rebar_app_info:dir(AppInfo),
DocDir = filename:join(AppDir, DirOpt),
try
case file:make_dir(DocDir) of
ok ->
ok;
{error, eexist} ->
ok
end,
{ok, _} = application:ensure_all_started(ssl),
{ok, _} = application:ensure_all_started(inets),
download_github_markdown_css(AppName, DocDir),
download_prismjs(AppName,
DocDir,
PrismVersion,
PrismTheme,
PrismLangs),

ok = prepare_stylesheets(DocDir, EdocOpts),

ok
catch
_Class:Reason:_Stacktrace ->
{app_failed, AppName, Reason}
end;
(_, Error) ->
Error
end,
ok,
ProjectApps),
State1 =
case Ret of
ok ->
EdocOpts1 = chain_edoc_backends(EdocOptsWithCSS),
?DEBUG("Overriden edoc options: ~p", [EdocOpts1]),
rebar_state:set(State, edoc_opts, EdocOpts1);
{app_failed, AppName, Reason} ->
?ERROR("Failed to fetch JS+CSS resources for ~ts: ~p", [AppName, Reason]),
State
end,
rebar_prv_edoc:do(State1).
Ret = lists:foldl(
fun
(_, {app_failed, _, _, _} = Error) ->
Error;
(AppInfo, {StateAcc, NeedsTwoPasses0}) ->
AppName = rebar_app_info:name(AppInfo),
AppDir = rebar_app_info:dir(AppInfo),
try
DocDir = filename:join(AppDir, DirOpt),
case file:make_dir(DocDir) of
ok -> ok;
{error, eexist} -> ok
end,
{ok, _} = application:ensure_all_started(ssl),
{ok, _} = application:ensure_all_started(inets),
download_github_markdown_css(AppName, DocDir),
download_prismjs(
AppName, DocDir,
PrismVersion, PrismTheme, PrismLangs),

AppOpts = rebar_app_info:opts(AppInfo),
AppEdocOpts = rebar_opts:get(AppOpts, edoc_opts, []),
?DEBUG(
"Initial app edoc options for ~s: ~p",
[AppName, AppEdocOpts]),

NeedsTwoPasses1 = are_edoc_backends_non_default(
AppEdocOpts),

%% Generate the wrapping stylesheet.
UserStylesheet = proplists:get_value(
stylesheet, AppEdocOpts,
GlobalUserStylesheet),
ok = prepare_stylesheets(DocDir, UserStylesheet),

AppEdocOpts1 = lists:keystore(
stylesheet, 1, AppEdocOpts,
{stylesheet, ?GENERATED_CSS}),

%% Override backend modules.
AppEdocOpts2 = override_edoc_backends(AppEdocOpts1),

AppOpts1 = rebar_opts:set(
AppOpts, edoc_opts, AppEdocOpts2),
AppInfo1 = rebar_app_info:opts(AppInfo, AppOpts1),

StateAcc1 = rebar_state:project_apps(
StateAcc, AppInfo1),
?DEBUG(
"Overriden app edoc options for ~s: ~p",
[AppName,
rebar_opts:get(
rebar_app_info:opts(AppInfo1),
edoc_opts)]),
{StateAcc1, NeedsTwoPasses0 orelse NeedsTwoPasses1}
catch
_Class:Reason:Stacktrace ->
{app_failed, AppName, Reason, Stacktrace}
end
end, {State, false}, ProjectApps),
case Ret of
{app_failed, AppName, Reason, Stacktrace} ->
?ERROR(
"Failed to fetch JS+CSS resources for ~ts: ~p~n~p",
[AppName, Reason, Stacktrace]),
rebar_prv_edoc:do(State);
{State1, NeedsTwoPasses} ->
%% We clear the global settings we override in each apps.
%% Otherwise they are duplicated in the merged options in the
%% `edoc' Rebar command.
EdocOpts1 = lists:foldl(
fun(Option, EdocOptsAcc) ->
lists:keydelete(Option, 1, EdocOptsAcc)
end,
EdocOpts,
[stylesheet, xml_export, doclet, layout]),
State2 = rebar_state:set(State1, edoc_opts, EdocOpts1),
?DEBUG(
"Overriden global edoc options: ~p",
[rebar_state:get(State2, edoc_opts)]),

%% We may call the `edoc' Rebar command twice:
%% 1. Once with the updated configuration for this plugin. We
%% assert that the state was not modified by this first
%% pass.
%% 2. Once with the initial configuration if the user configured
%% his own EDoc backend modules.
?DEBUG("EDoc run #1 for `rebar3_edoc_extension`", []),
case rebar_prv_edoc:do(State2) of
{ok, State2} ->
ok;
{ok, _State3} ->
?WARN(
"Dropping modified state in "
"`rebar3_edoc_extensions_wrapper`",
[]),
ok;
Error ->
?ERROR(
"Failed to run `rebar3_edoc_extensions_wrapper` pass: "
"~p",
[Error]),
ok
end,
case NeedsTwoPasses of
true ->
?DEBUG(
"EDoc run #2 with user-configured backend modules",
[]),
rebar_prv_edoc:do(State);
false ->
{ok, State}
end
end.

-spec format_error(any()) -> iolist().
format_error(Reason) ->
Expand Down Expand Up @@ -239,17 +307,17 @@ download(Url, Filename) ->
{ok, saved_to_file} = httpc:request(get, {Url, []}, HTTPOptions, Options, default),
ok.

-spec prepare_stylesheets(DocDir, EdocOpts) -> ok when
-spec prepare_stylesheets(DocDir, UserStylesheet) -> ok when
DocDir :: file:filename(),
EdocOpts :: [tuple()].
prepare_stylesheets(DocDir, EdocOpts) ->
UserStylesheet :: string().
prepare_stylesheets(DocDir, UserStylesheet) ->
%% We first want to import the two CSS files we downloaded. We then import
%% the user-provided CSS file, if any.
Stylesheets0 = ["github-markdown.css",
"prism.css"],
Stylesheets = case proplists:get_value(stylesheet, EdocOpts) of
undefined -> Stylesheets0;
Stylesheet -> Stylesheets0 ++ [Stylesheet]
Stylesheets = case UserStylesheet of
undefined -> Stylesheets0;
_ -> Stylesheets0 ++ [UserStylesheet]
end,
generate_wrapping_css(DocDir, Stylesheets).

Expand All @@ -267,47 +335,41 @@ generate_wrapping_css(DocDir, Stylesheets) ->
ok = file:write_file(Filename, Content),
ok.

-spec chain_edoc_backends(EdocOpts) -> EdocOpts when
-spec are_edoc_backends_non_default(EdocOpts) -> AreNonDefault when
EdocOpts :: [tuple()],
AreNonDefault :: boolean().
are_edoc_backends_non_default(EdocOpts) ->
lists:any(
fun
({doclet, edoc_doclet}) -> false;
({layout, edoc_layout}) -> false;
({_, undefined}) -> false;
(_) -> true
end,
[{Option, proplists:get_value(Option, EdocOpts)}
|| Option <- [doclet, layout]]).

-spec override_edoc_backends(EdocOpts) -> EdocOpts when
EdocOpts :: [tuple()].
chain_edoc_backends(EdocOpts) ->
override_edoc_backends(EdocOpts) ->
Options = [xml_export,
layout,
doclet],
chain_edoc_backends(Options, EdocOpts).
override_edoc_backends(Options, EdocOpts).

-spec chain_edoc_backends(Options, EdocOpts) -> EdocOpts when
-spec override_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])),
override_edoc_backends([Option | Rest], EdocOpts) ->
Mod = case Option of
doclet -> rebar3_edoc_extensions_wrapper;
layout -> rebar3_edoc_extensions_wrapper;
xml_export -> rebar3_edoc_extensions_export
end,
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) ->
{Option, Mod}),
override_edoc_backends(Rest, EdocOpts1);
override_edoc_backends([], EdocOpts) ->
EdocOpts.
33 changes: 3 additions & 30 deletions src/rebar3_edoc_extensions_wrapper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,16 @@

-spec module(term(), list()) -> term().
module(Element, Options) ->
LayoutMod = get_chained_mod(layout, Options),
LayoutMod:module(Element, Options).
edoc_layout:module(Element, Options).

-spec overview(term(), list()) -> [binary() | list()].
overview(Element, Options) ->
LayoutMod = get_chained_mod(layout, Options),
Overview = LayoutMod:overview(Element, Options),
Overview = edoc_layout:overview(Element, Options),
patch_html(Overview).

-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),
ok = edoc_doclet:run(Cmd, Ctxt),
File = filename:join(Dir, "modules-frame.html"),
{ok, Content0} = file:read_file(File),
Content1 = add_toc(App, Content0, Dir),
Expand All @@ -49,30 +46,6 @@ run(#doclet_gen{app = App} = Cmd, #?RECORD{dir = Dir} = 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 = Option, Options) when is_list(Options) ->
get_chained_mod(proplists:get_value(chained_doclet, Options), Option, edoc_doclet);
get_chained_mod(layout = Option, Options) when is_list(Options) ->
get_chained_mod(proplists:get_value(chained_layout, Options), Option, edoc_layout);
get_chained_mod(Option, Ctxt) when is_tuple(Ctxt) ->
#?RECORD{opts = Options} = Ctxt,
get_chained_mod(Option, Options).

-spec get_chained_mod(Option1, Option2, DefaultOption) -> Value when
Option1 :: atom(),
Option2 :: doclet | layout,
DefaultOption :: atom(),
Value :: atom().
get_chained_mod(undefined, Option, DefaultOption) ->
?DEBUG("Used default option '~p' value for '~p'", [DefaultOption, Option]),
DefaultOption;
get_chained_mod(Option, _, _) ->
Option.

-spec patch_html(list()) -> list().
patch_html(Html) ->
Html2 = re:replace(
Expand Down

0 comments on commit 53de439

Please sign in to comment.