#!/usr/bin/env escript %% -*- erlang -*- %%! -smp disable %% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2010. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% %% %%%------------------------------------------------------------------- %%% File : emd2exml.in %%% Author : Rickard Green %%% Description : Erlangish MarkDown To Erlangish XML %%% %%% Note that this script has *one and only one* %%% specific purpose. That purpose is to generate XML %%% that fits into the Erlang/OTP documentation, so that %%% the Erlang/OTP "readme" files also can be part of %%% the Erlang/OTP HTML and Erlang/OTP PDF %%% documentation. Nothing more, nothing less. It is %%% *not* intended as some kind of a generic Markdown to %%% XML tool, and never will be. %%% %%% Created : 25 Feb 2010 by Rickard Green %%%------------------------------------------------------------------- -define(MAX_HEADING, 3). -define(DELAYED_COPYRIGHT_IX, 0). -define(DELAYED_TOC_IX, 1). -define(DELAYED_START_IX, 2). -compile({no_auto_import,[error/2]}). -mode(compile). -export([main/1]). -record(state,{h = 0, p = false, c = no, note = false, warning = false, emphasis = no, code = false, code_blank = [], bq_lvl = 0, mlist = [], list_p = false, list_type_stack = [], list_p_stack = [], list_lvl = 0, line_no = -1, line = "", type = blank, bq_type = 0, prev_line = "", prev_type = blank, next_line = "", next_type = blank, bq_next_type = 0, delayed_array, delayed_array_ix = ?DELAYED_START_IX, delayed_tree, out = [], copyright = false, copyright_data = [], have_h1 = false, smarker_ix = false, toc = [], ifile, ofile}). main([I, O]) -> IFD = file_ropen(I), S0 = get_line(get_line(#state{ifile = IFD, ofile = {O, undefined}, delayed_array = array:new(), delayed_tree = gb_trees:empty()})), S1 = complete_output(parse(S0)), file_close(IFD), OFD = file_wopen(O), write_output(OFD, S1#state.out), file_close(OFD), stop(); main(_) -> io:format(standard_error, "Usage: emd2exml <Input Markdown File> <Output XML file>~n",[]), stop(1). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Parser %% %% %% Eof %% parse(#state{h = H, line = eof} = S) -> sections(0, H, end_all(S)); %% %% Note %% parse(#state{line = "> *NOTE*: " ++ Line, note = false, type = {bquote, 0, 1}} = S0) -> S1 = put_line(end_all(S0#state{note = true, bq_lvl = 1}), "<note>"), parse(S1#state{line = Line, type = type(Line), bq_type = 1}); parse(#state{bq_lvl = 1, note = true, bq_type = 0} = S) -> parse(put_line(end_all(S#state{bq_lvl = 0, note = false}), "</note>")); %% %% Warning %% parse(#state{line = "> *WARNING*: " ++ Line, warning = false, type = {bquote, 0, 1}} = S0) -> S1 = put_line(end_all(S0#state{warning = true, bq_lvl = 1}), "<warning>"), parse(S1#state{line = Line, type = type(Line), bq_type = 1}); parse(#state{bq_lvl = 1, warning = true, bq_type = 0} = S) -> parse(put_line(end_all(S#state{bq_lvl = 0, warning = false}), "</warning>")); %% %% Block quote %% parse(#state{note = false, warning = false, line = BQLine, type = {bquote, TxtLvl, BQLvl}} = S0) -> Line = rm_bquote(TxtLvl, BQLvl, BQLine), S1 = chg_bq_lvl(BQLvl, S0), parse(S1#state{line = Line, type = type(Line), bq_type = BQLvl}); % This clause is also used by warning and note parse(#state{bq_lvl = BQLvl, next_line = BQLine, next_type = {bquote, TxtLvl, BQLvl}} = S) -> Line = rm_bquote(TxtLvl, BQLvl, BQLine), parse(S#state{next_line = Line, next_type = type(Line), bq_next_type = BQLvl}); parse(#state{note = false, warning = false, bq_lvl = Lvl, bq_type = 0} = S) when Lvl /= 0-> parse(chg_bq_lvl(0, S)); %% %% Multiple blank lines %% % Keep multiple blank lines in code otherwise get rid of them... parse(#state{code = false, type = blank, next_type = blank} = S) -> parse(get_line(S)); %% %% Heading %% parse(#state{type = {text, 0}, next_type = {marker, h1}} = S) -> parse(get_line(get_line(setext_heading(1, end_all(S))))); parse(#state{type = {text, 0}, next_type = {marker, h2}} = S) -> parse(get_line(get_line(setext_heading(2, end_all(S))))); parse(#state{line = "#" ++ _} = S) -> parse(get_line(atx_heading(end_all(S)))); %% %% List %% parse(#state{code = false, list_lvl = Lvl, type = blank, next_type = {text, 0}} = S) when Lvl /= 0 -> parse(get_line(chg_list_lvl(S, 0))); parse(#state{code = false, list_lvl = Lvl, type = blank, next_type = {text, TxtLvl}} = S) when Lvl /= 0, TxtLvl >= Lvl -> parse(get_line(put_line(list_use_p(S, yes), ""))); parse(#state{code = false, list_lvl = Lvl, type = blank, next_type = {uolist, ListLvl}} = S) when Lvl /= 0, ListLvl >= Lvl-1 -> parse(get_line(put_line(list_use_p(S, yes), ""))); parse(#state{code = false, list_lvl = Lvl, type = blank, next_type = {olist, ListLvl}} = S) when Lvl /= 0, ListLvl >= Lvl-1 -> parse(get_line(put_line(list_use_p(S, yes), ""))); parse(#state{code = false, type = {ListType, ListLvl}, line = Line} = S0) when ListType == uolist; ListType == olist -> S1 = list_item(end_p(S0), ListType, ListLvl), S2 = list_chk_nxt_line(put_text(put_list_p(S1, start), list_strip(ListType, Line))), parse(get_line(S2)); parse(#state{code = false, list_lvl = Lvl, list_p = true, type = {text, _}, line = Line} = S0) when Lvl /= 0 -> S1 = list_chk_nxt_line(put_text(S0, list_strip(Line))), parse(get_line(S1)); parse(#state{code = false, list_lvl = Lvl, list_p = false, type = {text, NewLvl}, line = Line} = S0) when Lvl /= 0, NewLvl < Lvl -> S1 = chg_list_lvl(S0, NewLvl), S2 = list_chk_nxt_line(put_text(put_list_p(S1, start), list_strip(Line))), parse(get_line(S2)); parse(#state{code = false, list_lvl = Lvl, list_p = false, type = {text, Lvl}, line = Line} = S0) when Lvl /= 0 -> S1 = list_chk_nxt_line(put_text(put_list_p(S0, start), list_strip(Line))), parse(get_line(S1)); %% %% Code %% parse(#state{code = false, list_lvl = Lvl, p = false, prev_type = blank, type = {text, TxtLvl}, line = Line} = S) when TxtLvl > Lvl -> Data = code(strip_lvls(Lvl+1, Line)), parse(get_line(put_chars(S#state{code = true}, ["<code type=\"none\">", nl(), Data]))); parse(#state{code = true, type = blank, line = CB, code_blank = CBs} = S) -> parse(get_line(S#state{code_blank = [CB | CBs]})); parse(#state{code = true, code_blank = CB, list_lvl = Lvl, type = {Type, TxtLvl}, line = Line} = S) when TxtLvl > Lvl, (Type == text orelse Type == uolist orelse Type == olist) -> Data = code(strip_lvls(Lvl+1, Line)), parse(get_line(put_chars(S#state{code_blank = []}, [strip_code_blank(Lvl+1, CB), Data]))); parse(#state{code = true, prev_type = blank, type = {text, 0}} = S) -> parse(chg_list_lvl(end_code(S), 0)); parse(#state{code = true} = S) -> parse(end_code(S)); %% %% Paragraph %% parse(#state{p = false, prev_type = blank, type = {text, 0}, next_type = blank, line = Line} = S) -> parse(get_line(put_line(put_text(put_line(S, "<p>"), Line), "</p>"))); parse(#state{p = false, prev_type = blank, type = {text, 0}, line = Line} = S) -> parse(get_line(put_text(put_line(S#state{p = true}, "<p>"), Line))); parse(#state{p = true, type = {text, 0}, next_type = blank, line = Line} = S) -> parse(get_line(put_line(put_text(S#state{p = false}, Line), "</p>"))); %% %% Resolve link %% parse(#state{type = resolve_link} = S) -> parse(get_line(resolve_link(S))); %% %% Plain text %% parse(#state{line = Line} = S) -> parse(get_line(put_text(S, Line))). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Auxilary functions %%% %% %% Text %% end_p(#state{p = false} = S) -> S; end_p(#state{p = true} = S) -> put_line(S#state{p = false}, "</p>"). text(Line) -> text(Line, []). text("%ERTS-VSN%" ++ Cs, Acc) -> text(Cs, ["@ERTS_VSN@"|Acc]); text("%OTP-REL%" ++ Cs, Acc) -> text(Cs, ["@OTP_REL@"|Acc]); text([$\\,C|Cs], Acc) -> text(Cs, [C|Acc]); text([$<|Cs], Acc) -> text(Cs, ["<"|Acc]); text([$>|Cs], Acc) -> text(Cs, [">"|Acc]); text([$&|Cs], Acc) -> text(Cs, ["&"|Acc]); text([$'|Cs], Acc) -> text(Cs, ["'"|Acc]); text([C|Cs], Acc) -> text(Cs, [C|Acc]); text([], Acc) -> lists:reverse(Acc). put_text(#state{c = CTag, emphasis = EmTag} = S, Line) -> put_text(S, Line, CTag, EmTag, []). put_text(S, "%ERTS-VSN%"++Cs, CTag, EmTag, Acc) -> put_text(S, Cs, CTag, EmTag, ["@ERTS_VSN@"|Acc]); put_text(S, "%OTP-REL%"++Cs, CTag, EmTag, Acc) -> put_text(S, Cs, CTag, EmTag, ["@OTP_REL@"|Acc]); put_text(S, [$\\,C|Cs], no, EmTag, Acc) -> put_text(S, Cs, no, EmTag, [C|Acc]); put_text(S, [C,C|Cs], no, b, Acc) when C == $*; C == $_ -> put_text(S, Cs, no, no, ["</b>"|Acc]); put_text(S, [C,C|Cs], no, no, Acc) when C == $*; C == $_ -> put_text(S, Cs, no, b, ["<b>"|Acc]); put_text(S, [C|Cs], no, em, Acc) when C == $*; C == $_ -> put_text(S, Cs, no, no, ["</em>"|Acc]); put_text(S, [C|Cs], no, no, Acc) when C == $*; C == $_ -> put_text(S, Cs, no, em, ["<em>"|Acc]); put_text(S, [$`,$`|Cs], double, EmTag, Acc) -> put_text(S, Cs, no, EmTag, ["</c>"|Acc]); put_text(S, [$`,$`|Cs], no, EmTag, Acc) -> put_text(S, Cs, double, EmTag, ["<c>"|Acc]); put_text(S, [$`|Cs], single, EmTag, Acc) -> put_text(S, Cs, no, EmTag, ["</c>"|Acc]); put_text(S, [$`|Cs], no, EmTag, Acc) -> put_text(S, Cs, single, EmTag, ["<c>"|Acc]); put_text(S, [$<|Cs], CTag, EmTag, Acc) when CTag /= no -> put_text(S, Cs, CTag, EmTag, ["<"|Acc]); put_text(S, [$>|Cs], CTag, EmTag, Acc) when CTag /= no -> put_text(S, Cs, CTag, EmTag, [">"|Acc]); put_text(S, [$&|Cs], CTag, EmTag, Acc) when CTag /= no -> put_text(S, Cs, CTag, EmTag, ["&"|Acc]); put_text(S, [$&, $ |Cs], CTag, EmTag, Acc) -> %Workaround for INSTALL-WIN32.md put_text(S, Cs, CTag, EmTag, ["& "|Acc]); put_text(S, [$'|Cs], CTag, EmTag, Acc)when CTag /= no -> put_text(S, Cs, CTag, EmTag, ["'"|Acc]); put_text(S, [$<|Cs], no, EmTag, Acc) -> case auto_link(Cs) of no -> put_text(S, Cs, no, EmTag, [$<|Acc]); {URL, NewCs} -> put_text(S, NewCs, no, EmTag, [URL|Acc]) end; put_text(S0, "![" ++ Cs, no, EmTag, Acc) -> put_text_link_or_image(S0, image, "![", Cs, EmTag, Acc); put_text(S0, [$[|Cs], no, EmTag, Acc) -> put_text_link_or_image(S0, link, $[, Cs, EmTag, Acc); put_text(S, " \n", CTag, EmTag, Acc) -> put_text(S, [], CTag, EmTag, ["<br/>\n"|Acc]); put_text(S, " \r", CTag, EmTag, Acc) -> put_text(S, [], CTag, EmTag, ["<br/>\r"|Acc]); put_text(S, " \r\n", CTag, EmTag, Acc) -> put_text(S, [], CTag, EmTag, ["<br/>\r\n"|Acc]); put_text(S, [C|Cs], CTag, EmTag, Acc) -> put_text(S, Cs, CTag, EmTag, [C|Acc]); put_text(S0, [], CTag, EmTag, Acc) -> S1 = put_chars(S0, [lists:reverse(Acc)]), S1#state{c = CTag, emphasis = EmTag}. put_text_link_or_image(S0, Type, Head, Tail0, EmTag, Acc) -> case link_or_image(Tail0, Type) of no -> put_text(S0, Tail0, no, EmTag, [Head|Acc]); {LnkOrImg, Tail1} -> put_text(S0, Tail1, no, EmTag, [LnkOrImg|Acc]); {delayed, Type, Key, Text, Tail1} -> chk_key(Type, Key, S0), S1 = put_chars(S0, [lists:reverse(Acc)]), S2 = put_delayed(S1, Key, {Type, Text, S1#state.line_no}), put_text(S2, Tail1, no, EmTag, []) end. %% %% Links %% chk_key(Type, [$?|_] = Key, #state{ifile = {File, _}, line_no = Line}) -> error(File, Line, "~s definition name `~ts' begin with a `?' character", [case Type of image -> "Image"; _ -> "Link" end, Key]); chk_key(_, _, _) -> ok. auto_link(Str) -> case url(Str) of no -> no; Res -> Res end. url(Str) -> url(Str, false, []). url([$\\,C|Cs], Bool, Acc) -> url(Cs, Bool, [C|Acc]); url([$>|_Cs], false, _Acc) -> no; url([$>|Cs], true, Acc) -> Url = text(lists:reverse(Acc)), {["<url href=\"", Url, "\">", Url, "</url>"], Cs}; url("://" ++ _Cs, true, _Acc) -> no; url("://" ++ Cs, false, Acc) -> url(Cs, true, [$/,$/,$:|Acc]); url([C|Cs], Bool, Acc) -> url(Cs, Bool, [C|Acc]). link_or_image(Str, Type) -> case link_or_image_text(Str, "") of no -> no; {Text, Cont1} -> case link_or_image_data(Cont1, none, Type, "", "") of no -> no; {url, Url, _Title, Cont2} -> {["<url href=\"", text(Url), "\">", text(Text), "</url>"], Cont2}; {seealso, SeeAlso, _Title, Cont2} -> {["<seealso marker=\"", text(SeeAlso), "\">", text(Text), "</seealso>"], Cont2}; {image, Image, Title, Cont2} -> {["<image file=\"", text(Image), "\"><icaption>", text(Title), "</icaption></image>"], Cont2}; {delayed, text, Cont2} -> {delayed, Type, Text, Text, Cont2}; {delayed, Key, Cont2} -> {delayed, Type, Key, Text, Cont2} end end. link_or_image_text([$\\,C|Cs], Acc) -> link_or_image_text(Cs, [C|Acc]); link_or_image_text([$]|_Cs], "") -> no; link_or_image_text([$]|Cs], Acc) -> {lists:reverse(Acc), Cs}; link_or_image_text([C|Cs], Acc) -> link_or_image_text(Cs, [C|Acc]); link_or_image_text([], _Acc) -> no. link_or_image_data([C|Cs], none, link, "", "") when C == $ ; C == $\t -> link_or_image_data(Cs, none, link, "", ""); link_or_image_data([C|Cs], none, image, "", "") when C == $ ; C == $\t -> link_or_image_data(Cs, none, image, "", ""); link_or_image_data([$\\,C|Cs], How, Type, Eltit, Lru) when How /= none -> link_or_image_data(Cs, How, Type, Eltit, [C|Lru]); link_or_image_data([$(|Cs], none, link, "", "") -> link_or_image_data(Cs, {inline, read}, seealso, "", ""); link_or_image_data([$(|Cs], none, image, "", "") -> link_or_image_data(Cs, {inline, read}, image, "", ""); link_or_image_data([$)|_Cs], {inline, _}, _Type, "", "") -> no; link_or_image_data([$)|Cs], {inline, _}, Type, Eltit, Lru) -> {Type, lists:reverse(ws_strip(Lru)), lists:reverse(ws_strip(Eltit)), Cs}; link_or_image_data("://" ++Cs, {inline, read} = IR, seealso, Eltit, Lru) -> link_or_image_data(Cs, IR, url, Eltit, [$/,$/,$:|Lru]); link_or_image_data([$"|Cs], {inline, read}, Type, "", Lru) -> link_or_image_data(Cs, {inline, read_title}, Type, "", Lru); link_or_image_data([$"|Cs], {inline, read_title}, Type, Eltit, Lru) -> link_or_image_data(Cs, {inline, drop}, Type, Eltit, Lru); link_or_image_data([C|Cs], {inline, read} = IR, Type, "", Lru) -> link_or_image_data(Cs, IR, Type, "", [C|Lru]); link_or_image_data([C|Cs], {inline, read_title} = IRT, Type, Eltit, Lru) -> link_or_image_data(Cs, IRT, Type, [C|Eltit], Lru); link_or_image_data([_|Cs], {inline, drop}, Type, Eltit, Lru) -> link_or_image_data(Cs, {inline, drop}, Type, Eltit, Lru); link_or_image_data("[]"++Cs, none, _Type, "", "") -> {delayed, text, Cs}; link_or_image_data([$[|Cs], none, Type, "", "") -> link_or_image_data(Cs, reference, Type, "", ""); link_or_image_data(_, none, _Type, "", "") -> no; link_or_image_data([$]|_Cs], reference, _Type, "", "") -> no; link_or_image_data([$]|Cs], reference, _Type, "", Fer) -> {delayed, lists:reverse(ws_strip(Fer)), Cs}; link_or_image_data([C|Cs], reference, Type, "", Fer) -> link_or_image_data(Cs, reference, Type, "", [C|Fer]). mk_image(Title, Url) -> ["<image file=\"", text(Url), "\"><icaption>", text(Title), "</icaption></image>"]. mk_link(Text, Url) -> case chk_proto(Url) of true -> ["<url href=\"", text(Url), "\">", text(Text), "</url>"]; false -> ["<seealso marker=\"", text(Url), "\">", text(Text), "</seealso>"] end. chk_proto("://" ++ _) -> true; chk_proto([]) -> false; chk_proto([_|Cs]) -> chk_proto(Cs). %% %% Code %% code(Line) -> code(Line, []). code("%ERTS-VSN%" ++ Cs, Acc) -> code(Cs, ["@ERTS_VSN@"|Acc]); code("%OTP-REL%" ++ Cs, Acc) -> code(Cs, ["@OTP_REL@"|Acc]); code([$<|Cs], Acc) -> code(Cs, ["<"|Acc]); code([$>|Cs], Acc) -> code(Cs, [">"|Acc]); code([$&|Cs], Acc) -> code(Cs, ["&"|Acc]); code([$'|Cs], Acc) -> code(Cs, ["'"|Acc]); code([C|Cs], Acc) -> code(Cs, [C|Acc]); code([], Acc) -> lists:reverse(Acc). end_code(#state{code = true} = S) -> put_line(S#state{code = false, code_blank = []}, "</code>"); end_code(S) -> S. strip_code_blank(_Lvls, []) -> []; strip_code_blank(Lvls, CBs) -> strip_code_blank(Lvls, CBs, []). strip_code_blank(_Lvls, [], Acc) -> Acc; strip_code_blank(Lvls, [CB|CBs], Acc) -> strip_code_blank(Lvls, CBs, [strip_lvls(Lvls, CB)|Acc]). strip_lvls(0, Str) -> Str; strip_lvls(N, " " ++ Str) -> strip_lvls(N-1, Str); strip_lvls(N, [$\t | Str]) -> strip_lvls(N-1, Str); strip_lvls(_N, Str) -> Str. %% %% Titles and sections %% put_title(S, 1, Title) -> header(chk_h1(1, S#state{h = 1, mlist = [top]}), Title); put_title(#state{mlist = MList0, toc = TOC, smarker_ix = SMarkerIX} = S0, H, Title) -> TitleStr = text(Title), MList1 = [mk_lvl_marker(Title) | MList0], Marker = mk_marker(MList1), S1 = chk_h1(H, S0#state{toc = [TOC, lists:duplicate(H," "), " ", "<seealso marker=\"#",Marker,"\">", TitleStr,"</seealso>",nl()], h = H, mlist = MList1, smarker_ix = false}), true = is_integer(SMarkerIX), S2 = write_delayed(S1, SMarkerIX, ["<marker id=\"", Marker, "\"/>",nl()]), {STag, ETag} = case H > ?MAX_HEADING of true -> {"<p><b>", "</b></p>"}; false -> {"<title>", "</title>"} end, put_chars(S2, [STag, TitleStr, ETag, nl()]). setext_heading(H, #state{line = Line, h = OldH} = S0) -> S1 = sections(H, OldH, S0), put_title(S1, H, ws_strip(Line)). atx_heading(#state{line = Line, h = OldH} = S0) -> {H, Title} = get_atx_title(Line), S1 = sections(H, OldH, S0), put_title(S1, H, ws_strip(Title)). get_atx_title(Line) -> get_atx_title(Line, 0). get_atx_title("#" ++ Rest, H) -> get_atx_title(Rest, H+1); get_atx_title(Rest, H) -> {H, atx_strip(Rest)}. atx_strip(S) -> strip(S, [$ ,$\t,$\n,$\r,$#]). chk_h1(1, #state{have_h1 = true, ifile = {File, _}, line_no = Line}) -> error(File, Line, "Multiple H1 headings~n", []); chk_h1(1, #state{have_h1 = false} = S) -> S#state{have_h1 = true}; chk_h1(_H, S) -> S. sections(0, 0, S) -> S; sections(H, H, #state{mlist = [_|ML], toc = TOC} = S0) -> S1 = S0#state{mlist = ML, toc = [TOC, lists:duplicate(H," "), "</item><item>",nl()]}, S2 = end_section(H, S1), begin_section(H, S2); sections(H, OldH, S) -> sections_change(H, OldH, S). sections_change(H, H, S) -> S; sections_change(H, OldH, #state{ifile = {File, _}, line_no = Line}) when H > OldH+1 -> error(File, Line, "Level ~p heading without preceding level " "~p heading~n", [H, H-1]); sections_change(H, OldH, #state{toc = TOC} = S0) when H == OldH+1 -> S1 = case H > 1 of false -> S0; true-> S0#state{toc = [TOC, lists:duplicate(H-1," "), "<list type=\"ordered\"><item>",nl()]} end, S2 = begin_section(H, S1), sections_change(H, OldH+1, S2); sections_change(H, OldH, #state{mlist = [_|ML], toc = TOC} = S0) -> S1 = case OldH > 1 of false -> S0; true-> S0#state{toc = [TOC, lists:duplicate(OldH-1," "), "</item></list>",nl()]} end, S2 = end_section(OldH, S1), sections(H, OldH-1, S2#state{mlist = ML}). begin_section(1, S) -> put_line(S, "<chapter>"); begin_section(H, #state{delayed_array_ix = IX} = S0) when H > ?MAX_HEADING -> false = S0#state.smarker_ix, true = is_integer(IX), put_delayed(S0#state{smarker_ix = IX, delayed_array_ix = IX+1}, IX); begin_section(H, #state{delayed_array_ix = IX} = S0) when H > 1 -> false = S0#state.smarker_ix, true = is_integer(IX), S1 = put_delayed(S0#state{smarker_ix = IX, delayed_array_ix = IX+1}, IX), put_line(S1, "<section>"); begin_section(_H, S) -> S. end_section(1, S) -> put_chars(S, ["</chapter>",nl(),nl()]); end_section(H, S) when H < 1; ?MAX_HEADING < H -> S; end_section(_H, S) -> put_chars(S, ["</section>",nl(),nl()]). mk_lvl_marker([C|Cs]) when $a =< C, C =< $z -> [C|mk_lvl_marker(Cs)]; mk_lvl_marker([C|Cs]) when $A =< C, C =< $Z -> [C|mk_lvl_marker(Cs)]; mk_lvl_marker([C|Cs]) when C == $ ; C == $\t -> [$-|mk_lvl_marker(Cs)]; mk_lvl_marker([_|Cs]) -> mk_lvl_marker(Cs); mk_lvl_marker([]) -> []. mk_marker([L|Ls]) -> mk_marker(Ls, L). mk_marker([top], Res) -> Res; mk_marker([L|Ls], Res) -> mk_marker(Ls, [L,$_|Res]). header(#state{ofile = {File, _}} = S0, Title) -> {Year, Month, Day} = erlang:date(), S1 = put_line(S0, "<header>"), S2 = put_delayed(S1, ?DELAYED_COPYRIGHT_IX), S3 = put_chars(S2, ["<title>", text(Title), "</title>", nl(), "<prepared>emd2exml</prepared>", nl(), "<responsible>emd2exml</responsible>", nl(), "<docno>1</docno>", nl(), "<approved>yes</approved>", nl(), "<checked>yes</checked>", nl(), "<date>", integer_to_list(Year), "-", integer_to_list(Month), "-", integer_to_list(Day), "</date>", nl(), "<rev>1</rev>", nl(), "<file>",File,"</file>", nl(), "</header>", nl()]), put_delayed(S3, ?DELAYED_TOC_IX). create_copyright_notice(#state{copyright_data = CRD} = S) -> {From, To, Holder, Legal} = parse_crd(CRD, [], [], [], []), write_delayed(S, ?DELAYED_COPYRIGHT_IX, ["<copyright>", nl(), "<year>", From, "</year>", nl(), "<year>", To, "</year>", nl(), "<holder>", code(Holder),"</holder>", nl(), "</copyright>", nl(), "<legalnotice>", nl(), code(Legal), "</legalnotice>", nl(), nl()]). create_toc(#state{toc = TOC} = S) -> case read_delayed(S, "?TOC") of {value,{"true",[]}} -> write_delayed(S, ?DELAYED_TOC_IX, ["<p><b>Table of Contents</b></p>", nl(), TOC]); _ -> write_delayed(S, ?DELAYED_TOC_IX, "") end. parse_crd([], From, To, Holder, Legal) -> {Year, _, _} = erlang:date(), {case From of [] -> integer_to_list(Year); _ -> From end, case To of [] -> integer_to_list(Year); _ -> To end, case Holder of [] -> "Ericsson AB. All Rights Reserved."; _ -> Holder end, case Legal of [] -> ["The contents of this file are subject to the Erlang Public License,", nl(), "Version 1.1, (the \"License\"); you may not use this file except in", nl(), "compliance with the License. You should have received a copy of the", nl(), "Erlang Public License along with this software. If not, it can be", nl(), "retrieved online at http://www.erlang.org/.", nl(), nl(), "Software distributed under the License is distributed on an \"AS IS\"", nl(), "basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See", nl(), "the License for the specific language governing rights and limitations", nl(), "under the License.", nl()]; _ -> Legal end}; parse_crd([Line|Lines], OldFrom, OldTo, OldHolder, Legal) -> case copyright_line(Line) of no -> parse_crd(Lines, OldFrom, OldTo, OldHolder, [code(Line)|Legal]); {From, To, Holder} -> parse_crd(Lines, From, To, code(Holder), Legal) end. copyright_line(Line) -> case copyright_line(Line, start, [], []) of {From, To} -> {From, To, Line}; _ -> no end. copyright_line([], start, _, _) -> no; copyright_line([], _, [], []) -> {[], []}; copyright_line([], _, F, []) -> Year = lists:reverse(F), {Year, Year}; copyright_line("opyright "++Cs, start, [], []) -> copyright_line(Cs, find_from, [], []); copyright_line([_|Cs], start, [], []) -> copyright_line(Cs, start, [], []); copyright_line([C|Cs], find_from, [], []) when $0 =< C, C =< $9 -> copyright_line(Cs, from, [C|[]], []); copyright_line([_|Cs], find_from, [], []) -> copyright_line(Cs, find_from, [], []); copyright_line([C|Cs], from, F, []) when $0 =< C, C =< $9 -> copyright_line(Cs, from, [C|F], []); copyright_line([_|Cs], from, F, []) -> copyright_line(Cs, find_to, F, []); copyright_line([C|Cs], find_to, F, []) when $0 =< C, C =< $9 -> copyright_line(Cs, to, F, [C|[]]); copyright_line([_|Cs], find_to, F, []) -> copyright_line(Cs, find_to, F, []); copyright_line([C|Cs], to, F, T) when $0 =< C, C =< $9 -> copyright_line(Cs, to, F, [C|T]); copyright_line(_, to, F, T) -> {lists:reverse(F), lists:reverse(T)}. %% %% Lists %% put_list_p(#state{list_p_stack = [{_, IX} | _]} = S, start) -> put_delayed(S#state{list_p = true}, IX); put_list_p(#state{list_p_stack = [{_, IX} | _]} = S, stop) -> put_delayed(S#state{list_p = false}, IX+1). list_use_p(#state{list_p_stack = [{unknown, IX} | LPS]} = S0, yes) -> S1 = write_delayed(S0, IX, "<p>"), S2 = write_delayed(S1, IX+1, "</p>"), S2#state{list_p_stack = [{yes, IX} | LPS]}; list_use_p(#state{list_p_stack = [{unknown, IX} | LPS]} = S0, no) -> S1 = write_delayed(S0, IX, ""), S2 = write_delayed(S1, IX+1, ""), S2#state{list_p_stack = [{no, IX} | LPS]}; list_use_p(S, _) -> S. list_chk_nxt_line(#state{list_p = false} = S) -> S; list_chk_nxt_line(#state{next_type = {text, _}} = S) -> S; list_chk_nxt_line(S) -> put_line(put_list_p(S, stop), ""). list_item(#state{list_lvl = OldLvl} = S0, ListType, ListLvl) -> NewLvl = case ListLvl > OldLvl of true -> OldLvl+1; false -> ListLvl+1 end, S1 = chg_list_lvl(S0, ListType, NewLvl), case NewLvl =< OldLvl of false -> S1; true -> case S1#state.list_type_stack of [ListType|_] -> %% New item in old list put_chars(S1, ["</item>", nl(), "<item>", nl()]); [_|LTS] -> %% New item in new list [_ | LPS] = S1#state.list_p_stack, IX = S1#state.delayed_array_ix, S2 = list_use_p(S1, no), % Ignored if already determined S3 = put_line(S2, list_end_tags()), S4 = put_line(S3, list_begin_tags(ListType)), S4#state{list_type_stack = [ListType|LTS], list_p_stack = [{unknown, IX} | LPS], delayed_array_ix = IX+2} end end. list_begin_tags(uolist) -> "<list type=\"bulleted\"><item>"; list_begin_tags(olist) -> "<list type=\"ordered\"><item>". list_end_tags() -> "</item></list>". chg_list_lvl(#state{list_lvl = Lvl} = S, Lvl) -> S; chg_list_lvl(#state{list_lvl = OldLvl, list_type_stack = [ListType|_]} = S, NewLvl) when OldLvl > NewLvl -> chg_list_lvl(S, ListType, NewLvl). chg_list_lvl(#state{list_lvl = Lvl} = S, _ListType, Lvl) -> S; chg_list_lvl(#state{list_p_stack = LPS, list_type_stack = LTS, delayed_array_ix = IX, list_lvl = OldLvl} = S, ListType, Lvl) when OldLvl < Lvl -> chg_list_lvl(put_line(S#state{list_type_stack = [ListType|LTS], list_p_stack = [{unknown, IX} | LPS], delayed_array_ix = IX+2, list_lvl = OldLvl+1}, list_begin_tags(ListType)), Lvl); chg_list_lvl(#state{list_p_stack = [_ | LPS], list_type_stack = [_|LTS], list_lvl = OldLvl} = S0, ListType, Lvl) -> S1 = list_use_p(S0, no), % Ignored if already determined chg_list_lvl(put_line(S1#state{list_p_stack = LPS, list_type_stack = LTS, list_lvl = OldLvl-1}, list_end_tags()), ListType, Lvl). list_strip(uolist, Str) -> case list_strip(Str) of [C | Cs] when C == $*; C == $+; C == $- -> list_strip(Cs); _ -> exit({unexpected_unordered_list_item, Str}) end; list_strip(olist, Str) -> case strip_head(list_strip(Str), lists:seq($0, $9)) of [$. | Cs] -> list_strip(Cs); _ -> exit({unexpected_ordered_list_item, Str}) end. list_strip(S) -> strip_head(S, [$ ,$\t]). %% %% Block quote %% rm_bquote(0, 0, Str) -> Str; rm_bquote(0, BQLvl, [$>|Cs]) -> rm_bquote(0, BQLvl-1, Cs); rm_bquote(0, BQLvl, [C|Cs]) when C == $ ; C == $\t -> rm_bquote(0, BQLvl, Cs); rm_bquote(TxtLvl, BQLvl, " " ++ Cs) -> " " ++ rm_bquote(TxtLvl-1, BQLvl, Cs); rm_bquote(TxtLvl, BQLvl, [$\t|Cs]) -> [$\t | rm_bquote(TxtLvl-1, BQLvl, Cs)]. chg_bq_lvl(Lvl, #state{bq_lvl = Lvl} = S) -> S; chg_bq_lvl(NewLvl, #state{bq_lvl = Lvl} = S) when NewLvl > Lvl -> chg_bq_lvl(NewLvl, put_line(end_p(end_code(S#state{bq_lvl = Lvl+1})), "<blockquote>")); chg_bq_lvl(NewLvl, #state{bq_lvl = Lvl} = S) -> chg_bq_lvl(NewLvl, put_line(end_p(end_code(S#state{bq_lvl = Lvl-1})), "</blockquote>")). %% %% Resolve link %% resolve_link(#state{line = Line} = S) -> {Key, Url, Title} = resolve_link(Line, start, "", "", ""), write_delayed(S, Key, {Url, Title}). resolve_link("[" ++ Rest, start, "", "", "") -> resolve_link(Rest, key, "", "", ""); resolve_link([_|Cs], start, "", "", "") -> resolve_link(Cs, start, "", "", ""); resolve_link("]:" ++ Rest, key, Yek, "", "") -> resolve_link(Rest, url, Yek, "", ""); resolve_link([C|Cs], url, Yek, Lru, "") when C == $"; C == $' -> %" resolve_link(Cs, {title, C}, Yek, Lru, ""); resolve_link([$(|Cs], url, Yek, Lru, "") -> resolve_link(Cs, {title, $)}, Yek, Lru, ""); resolve_link([C|Cs], {title, C}, Yek, Lru, Eltit) -> resolve_link(Cs, drop, Yek, Lru, Eltit); resolve_link([C|Cs], key, Yek, "", "") -> resolve_link(Cs, key, [C|Yek], "", ""); resolve_link([C|Cs], url, Yek, Lru, "") -> resolve_link(Cs, url, Yek, [C|Lru], ""); resolve_link([C|Cs], {title, _} = T, Yek, Lru, Eltit) -> resolve_link(Cs, T, Yek, Lru, [C|Eltit]); resolve_link([_|Cs], drop, Yek, Lru, Eltit) -> resolve_link(Cs, drop, Yek, Lru, Eltit); resolve_link([], _, Yek, Lru, Eltit) -> {ws_strip(lists:reverse(Yek)), ws_strip(md_strip_n_reverse(Lru)), ws_strip(lists:reverse(Eltit))}. %% Remove .md at end of references. md_strip_n_reverse(Lru) -> md_strip_n_reverse(Lru,[]). md_strip_n_reverse("\ndm."++Lru,Acc) -> md_strip_n_reverse(Lru,Acc); md_strip_n_reverse("#dm."++Lru,Acc) -> md_strip_n_reverse(Lru,[$#|Acc]); md_strip_n_reverse([C|T],Acc) -> md_strip_n_reverse(T,[C|Acc]); md_strip_n_reverse([], Acc) -> Acc. %% %% Misc %% ws_strip(S) -> strip(S, [$ ,$\t,$\n,$\r]). %ws_strip_head(S) -> % strip_head(S, [$ ,$\t,$\n,$\r]). %hws_strip_head(S) -> % strip_head(S, [$ ,$\t]). %hws_strip(S) -> % strip(S, [$ ,$\t]). strip([], _StripList) -> []; strip([C|Cs] = Str, StripList) -> case lists:member(C, StripList) of true -> strip(Cs, StripList); false -> lists:reverse(pirts(lists:reverse(Str), StripList)) end. pirts([], _StripList) -> []; pirts([C|Cs] = Str, StripList) -> case lists:member(C, StripList) of true -> pirts(Cs, StripList); false -> Str end. strip_head([C|Cs] = Str, StripList) -> case lists:member(C, StripList) of true -> strip_head(Cs, StripList); false -> Str end. type("[" ++ Cs) -> type_resolve_link(Cs); type(" [" ++ Cs) -> type_resolve_link(Cs); type("==" ++ Cs) -> case type_same(Cs, $=) of true -> {marker, h1}; _ -> {text, 0} end; type("--" ++ Cs) -> case type_same(Cs, $-) of true -> {marker, h2}; _ -> {text, 0} end; type(Str) -> type_cont(Str). type_resolve_link([]) -> {text, 0}; type_resolve_link("]:" ++ _) -> resolve_link; type_resolve_link([_|Cs]) -> type_resolve_link(Cs). type_bquote(Str, N) -> case type(Str) of {bquote, N, M} -> {bquote, N, M+1}; _ -> {bquote, N, 1} end. type_same([C|Cs], C) -> type_same(Cs, C); type_same([_|Cs], _) -> case type_cont(Cs) of blank -> true; _ -> false end. type_cont(Str) -> type_cont(Str, 0, true). type_cont(" " ++ Str, N, true) -> type_cont(Str, N+1, true); type_cont([$\t| Str], N, true) -> type_cont(Str, N+1, true); type_cont([C|Str], N, _) when C == $ ; C == $\t; C == $\n; C == $\r -> type_cont(Str, N, false); type_cont([], _, _) -> blank; type_cont(">" ++ Cs, N, _) -> type_bquote(Cs, N); type_cont(" >" ++ Cs, N, _) -> type_bquote(Cs, N); type_cont(" >" ++ Cs, N, _) -> type_bquote(Cs, N); type_cont(" >" ++ Cs, N, _) -> type_bquote(Cs, N); type_cont([C,$ |_], N, _) when C == $*; C == $+; C == $- -> {uolist, N}; type_cont([C,$\t |_], N, _) when C == $*; C == $+; C == $- -> {uolist, N}; type_cont([C|Cs], N, _) when $0 =< C, C =< $9 -> case type_olist(Cs) of true -> {olist, N}; false -> {text, N} end; type_cont("%CopyrightBegin%" ++ _, _, _) -> copyright_begin; type_cont("%CopyrightEnd%" ++ _, _, _) -> copyright_end; type_cont(_, N, _) -> {text, N}. type_olist([C|Cs]) when $0 =< C, C =< $9 -> type_olist(Cs); type_olist([$.,$ |_]) -> true; type_olist([$.,$\t|_]) -> true; type_olist(_) -> false. end_all(S) -> chg_list_lvl(end_p(end_code(S)), 0). get_line(#state{next_line = eof} = S) -> S#state{prev_line = S#state.line, prev_type = S#state.type, line = eof, type = blank, bq_type = 0}; get_line(#state{ifile = IFile, line_no = LNO, copyright = CR} = S0) -> NewLine = file_read_line(IFile, LNO), S1 = case {CR, type(NewLine)} of {false, copyright_begin} -> S0#state{copyright = true, next_line = "", next_type = blank}; {true, copyright_end} -> S0#state{copyright = false, next_line = "", next_type = blank}; {true, NextType} -> S0#state{next_line = NewLine, next_type = NextType, copyright_data = [NewLine|S0#state.copyright_data]}; {false, NextType} -> S0#state{next_line = NewLine, next_type = NextType} end, S1#state{line_no = S0#state.line_no + 1, line = S0#state.next_line, type = S0#state.next_type, bq_type = S0#state.bq_next_type, prev_line = S0#state.line, prev_type = S0#state.type, bq_next_type = 0}. write_delayed(#state{delayed_array = DA} = S, IX, Value) when is_integer(IX) -> S#state{delayed_array = array:set(IX, Value, DA)}; write_delayed(#state{delayed_tree = DT} = S, Key, Value) -> S#state{delayed_tree = gb_trees:enter(Key, Value, DT)}. read_delayed(#state{delayed_array = DA}, IX) when is_integer(IX) -> array:get(IX, DA); read_delayed(#state{delayed_tree = DT}, Key) -> gb_trees:lookup(Key, DT). put_delayed(#state{out = Out} = S, Key) -> S#state{out = [{delayed, Key} | Out]}. put_delayed(#state{out = Out} = S, Key, Data) -> S#state{out = [{delayed, Key, Data} | Out]}. put_chars(#state{out = Out} = S, Chars) -> S#state{out = [[Chars] | Out]}. put_line(#state{out = Out} = S, String) -> S#state{out = [[String, nl()] | Out]}. complete_output(#state{out = Out} = S) -> complete_output(create_copyright_notice(create_toc(S)), Out, []). complete_output(S, [], Out) -> S#state{delayed_array = [], out = ["<?xml version=\"1.0\" encoding=\"utf8\" ?>", nl(), "<!DOCTYPE chapter SYSTEM \"chapter.dtd\">", nl(), Out]}; complete_output(S, [{delayed, IX}|Rest], Out) -> complete_output(S, Rest, [read_delayed(S, IX)|Out]); complete_output(S, [{delayed, Key, {link, Text, Line}}|Rest], Out) -> case read_delayed(S, Key) of {value, {Url, _Title}} -> complete_output(S, Rest, [mk_link(Text, Url)|Out]); none -> {File, _} = S#state.ifile, error(File, Line, "Link definition name `~ts' not found~n", [Key]) end; complete_output(S, [{delayed, Key, {image, _Text, Line}}|Rest], Out) -> case read_delayed(S, Key) of {value, {Url, Title}} -> complete_output(S, Rest, [mk_image(Title, Url)|Out]); none -> {File, _} = S#state.ifile, error(File, Line, "Image definition name `~ts' not found~n", [Key]) end; complete_output(S, [Next|Rest], Out) -> complete_output(S, Rest, [Next|Out]). write_output(_OFD, []) -> ok; write_output(OFD, [O|Os]) -> file_write(OFD, O), write_output(OFD, Os). error(FStr, Args) -> io:format(standard_error, "ERROR: " ++ FStr ++ "~n", Args), stop(1). error(File, Line, FStr, Args) -> io:format(standard_error, "~s:~p: ERROR: " ++ FStr ++ "~n", [File | [Line | Args]]), stop(1). stop() -> init:stop(), receive after infinity -> ok end. stop(Status) -> init:stop(Status), receive after infinity -> ok end. %% %% I/O %% nl() -> io_lib:nl(). file_wopen(File) -> case file:open(File, [write, delayed_write]) of {ok, FD} -> {File, FD}; {error, Reason} -> error("Failed to open `~s' for writing: ~p~n", [File, Reason]) end. file_ropen(File) -> case file:open(File, [read, binary]) of {ok, FD} -> {File, FD}; {error, Reason} -> error("Failed to open `~s' for reading: ~p~n", [File, Reason]) end. file_read_line({File, FD}, LineNo) -> case file:read_line(FD) of {ok, Line} -> unicode:characters_to_list(Line, utf8); eof -> eof; {error, Error} -> error(File,LineNo,"Reading line failed: ~p~n",[Error]) end. file_write({File, FD}, Data) -> case file:write(FD, unicode:characters_to_binary(Data, unicode, utf8)) of ok -> ok; {error, Reason} -> error("Writing to file `~s' failed: ~p~n", [File, Reason]) end. file_close({File, FD}) -> case file:close(FD) of ok -> ok; {error, Reason} -> error("Closing file `~s' failed: ~p~n",[File,Reason]) end. %% %% EOF %%