%%%---------------------------------------------------------------------- %%% File : ejabberd_doc.erl %%% Purpose : Options documentation generator %%% %%% ejabberd, Copyright (C) 2002-2020 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_doc). %% API -export([man/0, man/1, have_a2x/0]). -include("translate.hrl"). %%%=================================================================== %%% API %%%=================================================================== man() -> man(<<"en">>). man(Lang) when is_list(Lang) -> man(list_to_binary(Lang)); man(Lang) -> {ModDoc, SubModDoc} = lists:foldl( fun(M, {Mods, SubMods} = Acc) -> case lists:prefix("mod_", atom_to_list(M)) orelse lists:prefix("Elixir.Mod", atom_to_list(M)) of true -> try M:mod_doc() of #{desc := Descr} = Map -> DocOpts = maps:get(opts, Map, []), Example = maps:get(example, Map, []), {[{M, Descr, DocOpts, #{example => Example}}|Mods], SubMods}; #{opts := DocOpts} -> {ParentMod, Backend} = strip_backend_suffix(M), {Mods, dict:append(ParentMod, {M, Backend, DocOpts}, SubMods)} catch _:undef -> case erlang:function_exported( M, mod_options, 1) of true -> warn("module ~s is not documented", [M]); false -> ok end, Acc end; false -> Acc end end, {[], dict:new()}, ejabberd_config:beams(all)), Doc = lists:flatmap( fun(M) -> try M:doc() catch _:undef -> [] end end, ejabberd_config:callback_modules(all)), Options = ["TOP LEVEL OPTIONS", "-----------------", tr(Lang, ?T("This section describes top level options of ejabberd.")), io_lib:nl()] ++ lists:flatmap( fun(Opt) -> opt_to_man(Lang, Opt, 1) end, lists:keysort(1, Doc)), ModDoc1 = lists:map( fun({M, Descr, DocOpts, Ex}) -> case dict:find(M, SubModDoc) of {ok, Backends} -> {M, Descr, DocOpts, Backends, Ex}; error -> {M, Descr, DocOpts, [], Ex} end end, ModDoc), ModOptions = [io_lib:nl(), "MODULES", "-------", "[[modules]]", tr(Lang, ?T("This section describes options of all ejabberd modules.")), io_lib:nl()] ++ lists:flatmap( fun({M, Descr, DocOpts, Backends, Example}) -> ModName = atom_to_list(M), [io_lib:nl(), ModName, lists:duplicate(length(atom_to_list(M)), $~), "[[" ++ ModName ++ "]]", io_lib:nl()] ++ tr_multi(Lang, Descr) ++ [io_lib:nl()] ++ opts_to_man(Lang, [{M, '', DocOpts}|Backends]) ++ format_example(0, Lang, Example) end, lists:keysort(1, ModDoc1)), ListenOptions = [io_lib:nl(), "LISTENERS", "-------", "[[listeners]]", tr(Lang, ?T("This section describes options of all ejabberd listeners.")), io_lib:nl(), "TODO"], AsciiData = [[unicode:characters_to_binary(Line), io_lib:nl()] || Line <- man_header(Lang) ++ Options ++ [io_lib:nl()] ++ ModOptions ++ ListenOptions ++ man_footer(Lang)], warn_undocumented_modules(ModDoc1), warn_undocumented_options(Doc), write_man(AsciiData). %%%=================================================================== %%% Internal functions %%%=================================================================== opts_to_man(Lang, [{_, _, []}]) -> Text = tr(Lang, ?T("The module has no options.")), [Text, io_lib:nl()]; opts_to_man(Lang, Backends) -> lists:flatmap( fun({_, Backend, DocOpts}) when DocOpts /= [] -> Text = if Backend == '' -> tr(Lang, ?T("Available options")); true -> lists:flatten( io_lib:format( tr(Lang, ?T("Available options for '~s' backend")), [Backend])) end, [Text ++ ":", lists:duplicate(length(Text)+1, $^)| lists:flatmap( fun(Opt) -> opt_to_man(Lang, Opt, 1) end, lists:keysort(1, DocOpts))] ++ [io_lib:nl()]; (_) -> [] end, Backends). opt_to_man(Lang, {Option, Options}, Level) -> [format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++ format_example(Level, Lang, Options); opt_to_man(Lang, {Option, Options, Children}, Level) -> [format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++ lists:append( [[H ++ ":"|T] || [H|T] <- lists:map( fun(Opt) -> opt_to_man(Lang, Opt, Level+1) end, lists:keysort(1, Children))]) ++ [io_lib:nl()|format_example(Level, Lang, Options)]. format_option(Lang, Option, #{value := Val}) -> "*" ++ atom_to_list(Option) ++ "*: 'pass:[" ++ tr(Lang, Val) ++ "]'::"; format_option(_Lang, Option, #{}) -> "*" ++ atom_to_list(Option) ++ "*::". format_desc(Lang, #{desc := Desc}) -> tr_multi(Lang, Desc). format_example(Level, Lang, #{example := [_|_] = Example}) -> case lists:all(fun is_list/1, Example) of true -> if Level == 0 -> ["*Example*:", "^^^^^^^^^^"]; true -> ["+", "*Example*:", "+"] end ++ format_yaml(Example); false when Level == 0 -> ["Examples:", "^^^^^^^^^"] ++ lists:flatmap( fun({Text, Lines}) -> [tr(Lang, Text)] ++ format_yaml(Lines) end, Example); false -> lists:flatmap( fun(Block) -> ["+", "''''", "+"|Block] end, lists:map( fun({Text, Lines}) -> [tr(Lang, Text), "+"] ++ format_yaml(Lines) end, Example)) end; format_example(_, _, _) -> []. format_yaml(Lines) -> ["==========================", "[source,yaml]", "----"|Lines] ++ ["----", "=========================="]. man_header(Lang) -> ["ejabberd.yml(5)", "===============", ":doctype: manpage", ":version: " ++ binary_to_list(ejabberd_config:version()), io_lib:nl(), "NAME", "----", "ejabberd.yml - " ++ tr(Lang, ?T("main configuration file for ejabberd.")), io_lib:nl(), "SYNOPSIS", "--------", "ejabberd.yml", io_lib:nl(), "DESCRIPTION", "-----------", tr(Lang, ?T("The configuration file is written in " "https://en.wikipedia.org/wiki/YAML[YAML] language.")), io_lib:nl(), tr(Lang, ?T("WARNING: YAML is indentation sensitive, so make sure you respect " "indentation, or otherwise you will get pretty cryptic " "configuration errors.")), io_lib:nl(), tr(Lang, ?T("Logically, configuration options are splitted into 3 main categories: " "'Modules', 'Listeners' and everything else called 'Top Level' options. " "Thus this document is splitted into 3 main chapters describing each " "category separately. So, the contents of ejabberd.yml will typically " "look like this:")), io_lib:nl(), "==========================", "[source,yaml]", "----", "hosts:", " - example.com", " - domain.tld", "loglevel: info", "...", "listen:", " -", " port: 5222", " module: ejabberd_c2s", " ...", "modules:", " mod_roster: {}", " ...", "----", "==========================", io_lib:nl(), tr(Lang, ?T("Any configuration error (such as syntax error, unknown option " "or invalid option value) is fatal in the sense that ejabberd will " "refuse to load the whole configuration file and will not start or will " "abort configuration reload.")), io_lib:nl(), tr(Lang, ?T("All options can be changed in runtime by running 'ejabberdctl " "reload-config' command. Configuration reload is atomic: either all options " "are accepted and applied simultaneously or the new configuration is " "refused without any impact on currently running configuration.")), io_lib:nl(), tr(Lang, ?T("Some options can be specified for particular virtual host(s) only " "using 'host_config' or 'append_host_config' options. Such options " "are called 'local'. Examples are 'modules', 'auth_method' and 'default_db'. " "The options that cannot be defined per virtual host are called 'global'. " "Examples are 'loglevel', 'certfiles' and 'listen'. It is a configuration " "mistake to put 'global' options under 'host_config' or 'append_host_config' " "section - ejabberd will refuse to load such configuration.")), io_lib:nl(), str:format( tr(Lang, ?T("It is not recommended to write ejabberd.yml from scratch. Instead it is " "better to start from \"default\" configuration file available at ~s. " "Once you get ejabberd running you can start changing configuration " "options to meet your requirements.")), [default_config_url()]), io_lib:nl(), str:format( tr(Lang, ?T("Note that this document is intended to provide comprehensive description of " "all configuration options that can be consulted to understand the meaning " "of a particular option, its format and possible values. It will be quite " "hard to understand how to configure ejabberd by reading this document only " "- for this purpose the reader is recommended to read online Configuration " "Guide available at ~s.")), [configuration_guide_url()]), io_lib:nl()]. man_footer(Lang) -> {Year, _, _} = date(), [io_lib:nl(), "AUTHOR", "------", "https://www.process-one.net[ProcessOne].", io_lib:nl(), "VERSION", "-------", str:format( tr(Lang, ?T("This document describes the configuration file of ejabberd ~ts. " "Configuration options of other ejabberd versions " "may differ significantly.")), [ejabberd_config:version()]), io_lib:nl(), "REPORTING BUGS", "--------------", tr(Lang, ?T("Report bugs to ")), io_lib:nl(), "SEE ALSO", "---------", tr(Lang, ?T("Default configuration file")) ++ ": " ++ default_config_url(), io_lib:nl(), tr(Lang, ?T("Main site")) ++ ": ", io_lib:nl(), tr(Lang, ?T("Documentation")) ++ ": ", io_lib:nl(), tr(Lang, ?T("Configuration Guide")) ++ ": " ++ configuration_guide_url(), io_lib:nl(), tr(Lang, ?T("Source code")) ++ ": ", io_lib:nl(), "COPYING", "-------", "Copyright (c) 2002-" ++ integer_to_list(Year) ++ " https://www.process-one.net[ProcessOne]."]. tr(Lang, {Format, Args}) -> unicode:characters_to_list( str:format( translate:translate(Lang, iolist_to_binary(Format)), Args)); tr(Lang, Txt) -> unicode:characters_to_list(translate:translate(Lang, iolist_to_binary(Txt))). tr_multi(Lang, Txt) when is_binary(Txt) -> tr_multi(Lang, [Txt]); tr_multi(Lang, {Format, Args}) -> tr_multi(Lang, [{Format, Args}]); tr_multi(Lang, Lines) when is_list(Lines) -> [tr(Lang, Txt) || Txt <- Lines]. write_man(AsciiData) -> case file:get_cwd() of {ok, Cwd} -> AsciiDocFile = filename:join(Cwd, "ejabberd.yml.5.txt"), ManPage = filename:join(Cwd, "ejabberd.yml.5"), case file:write_file(AsciiDocFile, AsciiData) of ok -> Ret = run_a2x(Cwd, AsciiDocFile), %%file:delete(AsciiDocFile), case Ret of ok -> {ok, lists:flatten( io_lib:format( "The manpage saved as ~ts", [ManPage]))}; {error, Error} -> {error, lists:flatten( io_lib:format( "Failed to generate manpage: ~ts", [Error]))} end; {error, Reason} -> {error, lists:flatten( io_lib:format( "Failed to write to ~ts: ~s", [AsciiDocFile, file:format_error(Reason)]))} end; {error, Reason} -> {error, lists:flatten( io_lib:format("Failed to get current directory: ~s", [file:format_error(Reason)]))} end. have_a2x() -> case os:find_executable("a2x") of false -> false; Path -> {true, Path} end. run_a2x(Cwd, AsciiDocFile) -> case have_a2x() of false -> {error, "a2x was not found: do you have 'asciidoc' installed?"}; {true, Path} -> Cmd = lists:flatten( io_lib:format("~ts -f manpage ~ts -D ~ts", [Path, AsciiDocFile, Cwd])), case os:cmd(Cmd) of "" -> ok; Ret -> {error, Ret} end end. warn_undocumented_modules(Docs) -> lists:foreach( fun({M, _, DocOpts, Backends, _}) -> warn_undocumented_module(M, DocOpts), lists:foreach( fun({SubM, _, SubOpts}) -> warn_undocumented_module(SubM, SubOpts) end, Backends) end, Docs). warn_undocumented_module(M, DocOpts) -> try M:mod_options(ejabberd_config:get_myname()) of Defaults -> lists:foreach( fun(OptDefault) -> Opt = case OptDefault of O when is_atom(O) -> O; {O, _} -> O end, case lists:keymember(Opt, 1, DocOpts) of false -> warn("~s: option ~s is not documented", [M, Opt]); true -> ok end end, Defaults) catch _:undef -> ok end. warn_undocumented_options(Docs) -> Opts = lists:flatmap( fun(M) -> try M:options() of Defaults -> lists:map( fun({O, _}) -> O; (O) when is_atom(O) -> O end, Defaults) catch _:undef -> [] end end, ejabberd_config:callback_modules(all)), lists:foreach( fun(Opt) -> case lists:keymember(Opt, 1, Docs) of false -> warn("option ~s is not documented", [Opt]); true -> ok end end, Opts). warn(Format, Args) -> io:format(standard_error, "Warning: " ++ Format ++ "~n", Args). strip_backend_suffix(M) -> [H|T] = lists:reverse(string:tokens(atom_to_list(M), "_")), {list_to_atom(string:join(lists:reverse(T), "_")), list_to_atom(H)}. default_config_url() -> "". configuration_guide_url() -> "".