Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework how user-specified doclet and layout modules are managed #25

Merged
merged 1 commit into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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