%% -*- erlang -*- %% %CopyrightBegin% %% %% Copyright Ericsson AB 2020. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %%---------------------------------------------------------------------- %% File : docgen_xml_to_chunk %% %% Created : 1 Nov 2018 by Kenneth Lundin %% %% Does translation of Erlang XML docs to EEP-48 doc chunks. %%---------------------------------------------------------------------- -module(docgen_xml_to_chunk). -export([main/1, func_to_tuple/1]). -include_lib("kernel/include/eep48.hrl"). main([_Application, FromBeam, _Escript, ToChunk]) -> %% The given module is not documented, generate a hidden beam chunk file Name = filename:basename(filename:rootname(FromBeam)) ++ ".erl", EmptyDocs = #docs_v1{ anno = erl_anno:set_file(Name, erl_anno:new(0)), module_doc = hidden, docs = []}, ok = file:write_file(ToChunk, term_to_binary(EmptyDocs,[compressed])), ok; main([Application, FromXML, FromBeam, _Escript, ToChunk]) -> _ = erlang:process_flag(max_heap_size,20 * 1000 * 1000), case docs(Application, FromXML, FromBeam) of {error, Reason} -> io:format("Failed to create chunks: ~p~n",[Reason]), erlang:halt(1); {docs_v1,_,_,_,_,#{ source := S },[]} when %% This is a list of all modules that do are known not have any functions S =/= "../xml/gen_fsm.xml", S =/= "../xml/shell_default.xml", S =/= "../xml/user.xml", S =/= "../xml/wxClipboardTextEvent.xml", S =/= "../xml/wxDisplayChangedEvent.xml", S =/= "../xml/wxGBSizerItem.xml", S =/= "../xml/wxGraphicsBrush.xml", S =/= "../xml/wxGraphicsFont.xml", S =/= "../xml/wxGraphicsPen.xml", S =/= "../xml/wxInitDialogEvent.xml", S =/= "../xml/wxMaximizeEvent.xml", S =/= "../xml/wxMouseCaptureLostEvent.xml", S =/= "../xml/wxPaintEvent.xml", S =/= "../xml/wxPreviewCanvas.xml", S =/= "../xml/wxSysColourChangedEvent.xml", S =/= "../xml/wxTaskBarIconEvent.xml", S =/= "../xml/wxWindowCreateEvent.xml", S =/= "../xml/wxWindowDestroyEvent.xml", S =/= "../xml/wxDataObject.xml" -> io:format("Failed to create chunks: no functions found ~s~n",[S]), erlang:halt(1), ok; Docs -> ok = file:write_file(ToChunk, term_to_binary(Docs,[compressed])) end. %% Error handling %%---------------------------------------------------------------------- -define(error(Reason), throw({dom_error, Reason})). %%---------------------------------------------------------------------- %%====================================================================== %% Records %%====================================================================== %%---------------------------------------------------------------------- %% State record for the validator %%---------------------------------------------------------------------- -record(state, { tags=[], %% Tag stack cno=[], %% Current node number namespaces = [], %% NameSpace stack dom=[] %% DOM structure }). %%====================================================================== %% External functions %%====================================================================== %%---------------------------------------------------------------------- %% Function: initial_state() -> Result %% Parameters: %% Result: %% Description: %%---------------------------------------------------------------------- initial_state() -> #state{}. %%---------------------------------------------------------------------- %% Function: get_dom(State) -> Result %% Parameters: %% Result: %% Description: %%---------------------------------------------------------------------- get_dom(#state{dom=Dom}) -> Dom. %%---------------------------------------------------------------------- %% Function: event(Event, LineNo, State) -> Result %% Parameters: %% Result: %% Description: %%---------------------------------------------------------------------- event(Event, _LineNo, State) -> build_dom(Event, State). %%====================================================================== %% Internal functions %%====================================================================== %%---------------------------------------------------------------------- %% Function : build_dom(Event, State) -> Result %% Parameters: Event = term() %% State = #xmerl_sax_simple_dom_state{} %% Result : #xmerl_sax_simple_dom_state{} | %% Description: %%---------------------------------------------------------------------- %% Document %%---------------------------------------------------------------------- build_dom(startDocument, State) -> State#state{dom=[startDocument]}; build_dom(endDocument, #state{dom=[{Tag, Attributes, Content} |D]} = State) -> case D of [startDocument] -> State#state{dom=[{Tag, Attributes, lists:reverse(Content)}]}; [Decl, startDocument] -> State#state{dom=[Decl, {Tag, Attributes, lists:reverse(Content)}]}; _ -> %% endDocument is also sent by the parser when a fault occur to tell %% the event receiver that no more input will be sent State end; %% Element %%---------------------------------------------------------------------- build_dom({startElement, _Uri, LocalName, _QName, Attributes}, #state{tags=T, dom=D} = State) -> A = parse_attributes(LocalName, Attributes), CName = list_to_atom(LocalName), State#state{tags=[CName |T], dom=[{CName, lists:reverse(A), [] } | D]}; build_dom({endElement, _Uri, LocalName, _QName}, #state{tags=[_ |T], dom=[{CName, CAttributes, CContent}, {PName, PAttributes, PContent} = _Parent | D]} = State) -> case list_to_atom(LocalName) of CName -> SectionDepth = length([E || E <- T, E =:= section]), MappedCName = case CName of title -> lists:nth(SectionDepth+1,[h1,h2,h3,h4,h5,h6]); section when SectionDepth > 0 -> 'div'; CName -> CName end, State#state{tags=T, dom=[{PName, PAttributes, [{MappedCName, CAttributes, lists:reverse(CContent)} |PContent] } | D]}; _ -> ?error("Got end of element: " ++ LocalName ++ " but expected: " ++ CName) end; %% Text %%---------------------------------------------------------------------- build_dom({characters, String}, #state{dom=[{Name, Attributes, Content}| D]} = State) -> HtmlEnts = [{" ",[160]}, {"×",[215]}, {"±",[177]}, {"ö","ö"}, {"ä","ä"}, {"å","å"} ], NoHtmlEnt = lists:foldl( fun({Pat,Sub},Str) -> re:replace(Str,Pat,Sub,[global,unicode]) end,String,HtmlEnts), case re:run(NoHtmlEnt,"&[a-z]*;",[{capture,first,binary},unicode]) of nomatch -> ok; {match,[<<"<">>]} -> ok; {match,[<<">">>]} -> ok; Else -> throw({found_illigal_thing,Else,String}) end, NewContent = [unicode:characters_to_binary(NoHtmlEnt,utf8)| Content], State#state{dom=[{Name, Attributes, NewContent} | D]}; build_dom({ignorableWhitespace, String}, #state{dom=[{Name,_,_} = _E|_]} = State) -> case lists:member(Name, [p,pre,input,code,quote,warning, note,dont,do,c,b,i,em,strong, seemfa,seeerl,seetype,seeapp, seecom,seecref,seefile,seeguide, tag,item]) of true -> % io:format("Keep ign white: ~p ~p~n",[String, _E]), build_dom({characters, String}, State); false -> State end; build_dom({startEntity, SysId}, State) -> io:format("startEntity:~p~n",[SysId]), State; %% Default %%---------------------------------------------------------------------- build_dom(_E, State) -> State. %%---------------------------------------------------------------------- %% Function : parse_attributes(ElName, Attributes) -> Result %% Parameters: %% Result : %% Description: %%---------------------------------------------------------------------- parse_attributes(ElName, Attributes) -> parse_attributes(ElName, Attributes, 1, []). parse_attributes(_, [], _, Acc) -> Acc; parse_attributes(ElName, [{_Uri, _Prefix, LocalName, AttrValue} |As], N, Acc) -> parse_attributes(ElName, As, N+1, [{list_to_atom(LocalName), AttrValue} |Acc]). docs(Application, OTPXml, FromBEAM)-> case xmerl_sax_parser:file(OTPXml, [skip_external_dtd, {event_fun,fun event/3}, {event_state,initial_state()}]) of {ok,Tree,_} -> {ok, {Module, Chunks}} = beam_lib:chunks(FromBEAM,[exports,abstract_code]), Dom = get_dom(Tree), put(application, Application), put(module, filename:basename(filename:rootname(FromBEAM))), NewDom = transform(Dom,[]), Chunk = to_chunk(NewDom, OTPXml, Module, proplists:get_value(abstract_code, Chunks)), verify_chunk(Module,proplists:get_value(exports, Chunks), Chunk), Chunk; Else -> {error,Else} end. verify_chunk(M, Exports, #docs_v1{ docs = Docs } = Doc) -> %% Make sure that each documented function actually is exported Exported = [begin FA = {F,A}, {M,F,A,lists:member(FA,Exports)} end || {{function,F,A},_,_,_,_} <- Docs], lists:foreach(fun({_M,_F,_A,true}) -> ok end,Exported), try shell_docs:validate(Doc) catch Err -> throw({maps:get(<<"en">>,Doc#docs_v1.module_doc), Err}) end. %% skip but transform and keep its content transform([{erlref,_Attr,Content}|T],Acc) -> Module = [Mod || Mod = {module,_,_} <- Content], NewContent = Content -- Module, [{module,SinceAttr,[Mname]}] = Module, Since = case proplists:get_value(since,SinceAttr) of undefined -> []; [] -> []; Vsn -> [{since,Vsn}] end, transform([{module,[{name,Mname}|Since],NewContent}|T],Acc); %% skip
and all of its content transform([{header,_Attr,_Content}|T],Acc) -> transform(T,Acc); transform([{section,Attr,Content}|T],Acc) -> transform(T,[{section,Attr,transform(Content,[])}|Acc]); %% transform to