Current section

Files

Jump to
docsh src docsh_edoc_xmerl.erl
Raw

src/docsh_edoc_xmerl.erl

-module(docsh_edoc_xmerl).
%% xmerl:simple_export/2 API
-export(['#root#'/4,
'#element#'/5,
'#text#'/1,
'#xml-inheritance#'/0]).
%% EDoc formatter
-export([format_edoc/2]).
-export_type([xml_element_content/0]).
-include_lib("xmerl/include/xmerl.hrl").
%% @type xml_element_content(). `#xmlElement.content' as defined by `xmerl.hrl'.
-type xml_element_content() :: [#xmlElement{} | #xmlText{} | #xmlPI{} | #xmlComment{} | #xmlDecl{}].
-define(il2b(IOList), iolist_to_binary(IOList)).
-define(l2i(L), list_to_integer(L)).
-define(l2ea(L), list_to_existing_atom(L)).
%%
%%' xmerl:simple_export/2 API
%%
-spec '#xml-inheritance#'() -> list().
'#xml-inheritance#'() -> [].
%% The '#root#' tag is called when the entire structure has been
%% exported. It does not appear in the structure itself.
-spec '#root#'(any(), any(), any(), any()) -> docsh_internal:t().
'#root#'([#xmlElement{name = module} = Module], _, _, _) ->
#{name => get_module_name(Module),
description => get_module_description(Module),
items => get_functions(Module) ++ get_types(Module)}.
-spec '#element#'(any(), any(), any(), any(), any()) -> any().
'#element#'(_, _, _, _, E) -> E.
%% The '#text#' function is called for every text segment.
-spec '#text#'(any()) -> any().
'#text#'(Text) -> ?il2b(Text).
%%.
%%' xmerl:simple_export/2 helpers
%%
get_module_name(#xmlElement{attributes = Attrs}) ->
?l2ea('find_attribute!'(name, Attrs)).
get_module_description(#xmlElement{name = module} = M) ->
get_description(M).
-spec get_functions(#xmlElement{}) -> [docsh_internal:item()].
get_functions(#xmlElement{name = module} = M) ->
get_content(functions, [], fun get_functions/1, M);
get_functions(#xmlElement{name = functions, content = Content}) ->
[ get_function(Function) || #xmlElement{name = function} = Function <- Content ].
-spec get_function(#xmlElement{}) -> docsh_internal:item().
get_function(#xmlElement{attributes = Attrs} = Function) ->
#{kind => 'function',
name => ?l2ea('find_attribute!'(name, Attrs)),
arity => ?l2i('find_attribute!'(arity, Attrs)),
exported => list_to_boolean('find_attribute!'(exported, Attrs)),
description => get_function_description(Function)}.
-spec get_types(#xmlElement{}) -> [docsh_internal:item()].
get_types(#xmlElement{name = module} = M) ->
get_content(typedecls, [], fun get_types/1, M);
get_types(#xmlElement{name = typedecls, content = Content}) ->
[ get_type(Type) || #xmlElement{name = typedecl} = Type <- Content ].
-spec get_type(#xmlElement{}) -> docsh_internal:item().
get_type(#xmlElement{name = typedecl} = Type) ->
#{kind => 'type',
name => get_type_name(Type),
arity => get_type_arity(Type),
%% TODO: really always true? anyway, we want the structure for functions and types
%% to be the same
exported => true,
description => get_type_description(Type)}.
get_function_description(#xmlElement{name = function} = Function) ->
get_description(Function).
get_type_name(#xmlElement{name = typedecl} = Type) ->
get_type_def(fun get_type_name/1, Type);
get_type_name(#xmlElement{name = typedef} = TypeDef) ->
case get_content(erlangName, {error, no_erlang_name}, fun get_type_name/1, TypeDef) of
{error, no_erlang_name} -> erlang:error({not_found, erlangName});
TypeName -> TypeName
end;
get_type_name(#xmlElement{name = erlangName, attributes = Attrs}) ->
?l2ea('find_attribute!'(name, Attrs)).
get_type_arity(#xmlElement{name = typedecl} = Type) ->
get_type_def(fun get_type_arity/1, Type);
get_type_arity(#xmlElement{name = typedef} = TypeDef) ->
case get_content(argtypes, {error, no_argtypes}, fun get_type_arity/1, TypeDef) of
{error, no_argtypes} -> erlang:error({not_found, argtypes});
TypeArity -> TypeArity
end;
get_type_arity(#xmlElement{name = argtypes, content = Content}) ->
count_args(Content).
count_args(Args) ->
length([ Arg || #xmlElement{name = type} = Arg <- Args ]).
get_type_description(#xmlElement{name = typedecl} = Type) ->
get_description(Type).
get_content(Name, Default, ContinueFun, #xmlElement{content = Content} = Element) ->
case lists:keyfind(Name, #xmlElement.name, Content) of
false -> debug({not_found, Name}, Element),
Default;
#xmlElement{} = Found -> ContinueFun(Found)
end.
get_description(#xmlElement{} = Element) ->
get_content(description, none, fun get_full_description/1, Element).
get_full_description(#xmlElement{name = description} = D) ->
get_content(fullDescription, none, fun get_full_description/1, D);
get_full_description(#xmlElement{name = fullDescription, content = XmlElementContent}) ->
%% See xmerl.hrl for the definition of #xmlElement.content:
%% content = [#xmlElement()|#xmlText()|#xmlPI()|#xmlComment()|#xmlDecl()]
format_text(XmlElementContent).
format_text(TextSubtree) ->
%% Just return the EDoc subtree for storage or later processing.
TextSubtree.
get_type_def(ContinueFun, #xmlElement{name = typedecl} = Type) ->
case get_content(typedef, {error, no_typedef}, ContinueFun, Type) of
{error, no_typedef} -> erlang:error({not_found, typedef, Type});
ContinuationResult -> ContinuationResult
end.
list_to_boolean("yes") -> true;
list_to_boolean("no") -> false.
'find_attribute!'(Attr, Attrs) ->
case xmerl_lib:find_attribute(Attr, Attrs) of
false -> erlang:error({no_attribute, Attr, Attrs});
{value, Value} -> Value
end.
%% Intended only for tracing.
debug(_, _) -> ok.
%%.
%%' EDoc formatter
%%
-spec format_edoc(xml_element_content(), any()) -> iolist().
format_edoc(Content, Ctx) ->
lists:map(fun
({br}) -> "\n";
({i, Inline}) -> [Inline]
end, end_block(format_content(Content, Ctx))).
format_content(Content, Ctx) ->
lists:flatten([ format_content_(C, Ctx) || C <- Content ]).
format_content_(#xmlPI{}, _Ctx) -> [];
format_content_(#xmlComment{}, _Ctx) -> [];
format_content_(#xmlDecl{}, _Ctx) -> [];
format_content_(#xmlText{} = T, Ctx) ->
Text = T#xmlText.value,
case edoc_lib:is_space(Text) of
true -> [];
false ->
case is_preformatted(T#xmlText.parents) of
true -> cleanup_preformatted_text(Text, Ctx);
false -> cleanup_text(Text, Ctx)
end
end;
format_content_(#xmlElement{name = Name, content = Content} = E, Ctx) ->
format_element(Name, E, format_content(Content, Ctx), Ctx).
format_element(h1, #xmlElement{} = E, Lines, Ctx) -> format_header(E, Lines, Ctx);
format_element(h2, #xmlElement{} = E, Lines, Ctx) -> format_header(E, Lines, Ctx);
format_element(h3, #xmlElement{} = E, Lines, Ctx) -> format_header(E, Lines, Ctx);
format_element(h4, #xmlElement{} = E, Lines, Ctx) -> format_header(E, Lines, Ctx);
format_element(h5, #xmlElement{} = E, Lines, Ctx) -> format_header(E, Lines, Ctx);
format_element(h6, #xmlElement{} = E, Lines, Ctx) -> format_header(E, Lines, Ctx);
format_element(hgroup, _, _Lines, _Ctx) -> [];
format_element(code, #xmlElement{}, Lines, _Ctx) ->
Lines;
format_element(dl, #xmlElement{}, Lines, _Ctx) ->
end_block(Lines);
format_element(dt, #xmlElement{}, Lines, _Ctx) ->
dl_item(" ", Lines);
format_element(dd, #xmlElement{}, Lines, _Ctx) ->
dl_item(" ", Lines);
format_element(p, #xmlElement{}, Lines, _Ctx) ->
end_block(lists:dropwhile(fun
({br}) -> true;
(_) -> false
end, Lines));
format_element(pre, #xmlElement{}, Lines, _Ctx) ->
end_block(Lines);
format_element(ol, #xmlElement{} = E, ListItems, Ctx) ->
lists:all(fun ({li, _}) -> true; (_) -> false end, ListItems)
orelse erlang:error({non_list_item_children, ListItems}, [ol, E, ListItems, Ctx]),
end_block([ [{i, io_lib:format(" ~b. ", [Index])}, FirstItem, IndentedRest, {br}]
|| {Index, {li, [FirstItem | Rest]}} <- enumerate(ListItems),
IndentedRest <- [prepend(" ", Rest)] ]);
format_element(ul, #xmlElement{} = E, ListItems, Ctx) ->
lists:all(fun ({li, _}) -> true; (_) -> false end, ListItems)
orelse erlang:error({non_list_item_children, ListItems}, [ul, E, ListItems, Ctx]),
end_block([ [{i, " - "}, FirstItem, IndentedRest, {br}]
|| {li, [FirstItem | Rest]} <- ListItems,
IndentedRest <- [prepend(" ", Rest)] ]);
format_element(li, #xmlElement{}, Lines, _Ctx) ->
[{li, Lines}];
format_element(_, #xmlElement{}, Lines, _Ctx) ->
Lines.
format_header(#xmlElement{name = Name}, Lines, _Ctx) ->
Headers = #{h1 => "# ",
h2 => "## ",
h3 => "### ",
h4 => "#### ",
h5 => "##### ",
h6 => "###### "},
case Name of
hgroup -> [];
_ ->
[{i, Text}] = Lines,
end_block([{br}, {i, [maps:get(Name, Headers), Text]}])
end.
cleanup_text(Text, _Ctx) ->
lists:flatmap(fun
("\n") -> [{br}];
(T) ->
case edoc_lib:is_space(T) of
true -> [];
false -> [{i, T}]
end
end,
split(Text, "\s*(\n)\s*", [trim, {return, list}])).
cleanup_preformatted_text(Text, _Ctx) ->
lists:flatmap(fun
("\n") -> [{br}];
(T) -> [{i, T}]
end,
split(Text, "(\n)", [{return, list}])).
split(Text, Pattern, Opts) ->
re:split(Text, Pattern, Opts).
is_preformatted(Parents) ->
lists:any(fun
({pre, _}) -> true;
(_) -> false
end, Parents).
prepend(Prefix, Doc) -> prepend(Prefix, lists:reverse(Doc), []).
prepend(_Prefix, [], Acc) -> Acc;
prepend( Prefix, [{br} | Doc], [] = Acc) -> prepend(Prefix, Doc, [{br} | Acc]);
prepend( Prefix, [{br} | Doc], [{br}] = Acc) -> prepend(Prefix, Doc, [{br} | Acc]);
prepend( Prefix, [{br} | Doc], [{br}, {br}] = Acc) -> prepend(Prefix, Doc, Acc);
prepend( Prefix, [{br} | Doc], Acc) -> prepend(Prefix, Doc, [{br}, {i, Prefix} | Acc]);
prepend( Prefix, [Node | Doc], Acc) -> prepend(Prefix, Doc, [Node | Acc]).
enumerate(List) ->
lists:zip(lists:seq(1, length(List)), List).
end_block(Doc) -> end_block([{br}, {br} | lists:reverse(lists:flatten(Doc))], []).
end_block([], Acc) -> Acc;
end_block([{br} | Doc], [] = Acc) -> end_block(Doc, [{br} | Acc]);
end_block([{br} | Doc], [{br}] = Acc) -> end_block(Doc, [{br} | Acc]);
end_block([{br} | Doc], [{br}, {br}] = Acc) -> end_block(Doc, Acc);
end_block([Node | Doc], Acc) -> end_block(Doc, [Node | Acc]).
dl_item(Prefix, Lines) ->
[First | Rest] = Lines,
end_block([{i, Prefix}, First, prepend(Prefix, Rest)]).
%%. vim: foldmethod=marker foldmarker=%%',%%.