"Fossies" - the Fresh Open Source Software Archive

Member "doc/docbuild/erl_html_tools.erl" (17 Sep 2019, 21246 Bytes) of package /linux/misc/otp_doc_html_22.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Erlang source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "erl_html_tools.erl": 21.2_vs_21.3.

    1 %%--------------------------------------------------------------------
    2 %%
    3 %% %CopyrightBegin%
    4 %%
    5 %% Copyright Ericsson AB 2009-2016. All Rights Reserved.
    6 %%
    7 %% Licensed under the Apache License, Version 2.0 (the "License");
    8 %% you may not use this file except in compliance with the License.
    9 %% You may obtain a copy of the License at
   10 %%
   11 %%     http://www.apache.org/licenses/LICENSE-2.0
   12 %%
   13 %% Unless required by applicable law or agreed to in writing, software
   14 %% distributed under the License is distributed on an "AS IS" BASIS,
   15 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   16 %% See the License for the specific language governing permissions and
   17 %% limitations under the License.
   18 %%
   19 %% %CopyrightEnd%
   20 %%
   21 %%-----------------------------------------------------------------
   22 %% File: erl_html_tools.erl
   23 %% 
   24 %% Description:
   25 %%    This file generates the top index of the documentation.
   26 %%
   27 %%-----------------------------------------------------------------
   28 -module(erl_html_tools). 
   29 
   30 -export([top_index/0,top_index/1,top_index/4,top_index_silent/3]).
   31 
   32 -include_lib("kernel/include/file.hrl").
   33 
   34 group_order() ->
   35     [
   36      {basic, "Basic"},
   37      {dat, "Database"},
   38      {oam, "Operation & Maintenance"},
   39      {comm, "Interface and Communication"},
   40      {tools, "Tools"},
   41      {test, "Test"},
   42      {doc, "Documentation"},
   43      {orb, "Object Request Broker & IDL"},
   44      {misc, "Miscellaneous"},
   45      {eric, "Ericsson Internal"}
   46     ].
   47 
   48 top_index() ->
   49     case os:getenv("ERL_TOP") of
   50     false ->
   51         io:format("Variable ERL_TOP is required\n",[]);
   52     Value ->
   53         {_,RelName} = init:script_id(),     
   54         top_index(src, Value, filename:join(Value, "doc"), RelName)
   55     end.
   56 
   57 top_index([src, RootDir, DestDir, OtpBaseVsn])
   58   when is_atom(RootDir), is_atom(DestDir), is_atom(OtpBaseVsn) ->
   59     top_index(src, atom_to_list(RootDir), atom_to_list(DestDir), atom_to_list(OtpBaseVsn));
   60 top_index([rel, RootDir, DestDir, OtpBaseVsn])
   61   when is_atom(RootDir), is_atom(DestDir), is_atom(OtpBaseVsn) ->
   62     top_index(rel, atom_to_list(RootDir), atom_to_list(DestDir), atom_to_list(OtpBaseVsn));
   63 top_index(RootDir)  when is_atom(RootDir) -> 
   64     {_,RelName} = init:script_id(),
   65     top_index(rel, RootDir, filename:join(RootDir, "doc"), RelName).
   66 
   67 
   68 
   69 top_index(Source, RootDir, DestDir, OtpBaseVsn) ->
   70     report("****\nRootDir: ~p", [RootDir]),
   71     report("****\nDestDir: ~p", [DestDir]),
   72     report("****\nOtpBaseVsn: ~p", [OtpBaseVsn]),
   73 
   74     put(otp_base_vsn, OtpBaseVsn),
   75 
   76     Templates = find_templates(["","templates",DestDir]),
   77     report("****\nTemplates: ~p", [Templates]),
   78     Bases = [{"../lib/", filename:join(RootDir,"lib")},
   79          {"../",     RootDir}],
   80     Groups = find_information(Source, Bases),
   81     report("****\nGroups: ~p", [Groups]),
   82     process_templates(Templates, DestDir, Groups).
   83 
   84 top_index_silent(RootDir, DestDir, OtpBaseVsn) ->
   85     put(silent,true),
   86     Result = top_index(rel, RootDir, DestDir, OtpBaseVsn),
   87     erase(silent),
   88     Result.
   89 
   90 
   91 
   92 
   93 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
   94 % Main loop - process templates
   95 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
   96 
   97 process_templates([], _DestDir, _Groups) ->
   98     report("\n", []);
   99 process_templates([Template | Templates], DestDir, Groups) ->
  100     report("****\nIN-FILE: ~s", [Template]),
  101     BaseName = filename:basename(Template, ".src"),
  102     case lists:reverse(filename:rootname(BaseName)) of
  103     "_"++_ ->
  104         %% One template expands to several output files.
  105         process_multi_template(BaseName, Template, DestDir, Groups);
  106     _ ->
  107         %% Standard one-to-one template.
  108         OutFile = filename:join(DestDir, BaseName),
  109         subst_file("", OutFile, Template, Groups)
  110     end,
  111     process_templates(Templates, DestDir, Groups).
  112 
  113 
  114 process_multi_template(BaseName0, Template, DestDir, Info) ->
  115     Ext = filename:extension(BaseName0),
  116     BaseName1 = filename:basename(BaseName0, Ext),
  117     [_|BaseName2] = lists:reverse(BaseName1),
  118     BaseName = lists:reverse(BaseName2),
  119     Groups0 = [{[$_|atom_to_list(G)],G} || {G, _} <- group_order()],
  120     Groups = [{"",basic}|Groups0],
  121     process_multi_template_1(Groups, BaseName, Ext, Template, DestDir, Info).
  122 
  123 process_multi_template_1([{Suffix,Group}|Gs], BaseName, Ext, Template, DestDir, Info) ->
  124     OutFile = filename:join(DestDir, BaseName++Suffix++Ext),
  125     subst_file(Group, OutFile, Template, Info),
  126     process_multi_template_1(Gs, BaseName, Ext, Template, DestDir, Info);
  127 process_multi_template_1([], _, _, _, _, _) -> ok.
  128 
  129 subst_file(Group, OutFile, Template, Info) ->
  130     report("\nOUTFILE: ~s", [OutFile]),
  131     case subst_template(Group, Template, Info) of
  132     {ok,Text,_NewInfo} ->
  133         case file:open(OutFile, [write]) of
  134         {ok, Stream} ->
  135             file:write(Stream, Text),
  136             file:close(Stream);
  137         Error ->
  138             local_error("Can't write to file ~s: ~w", [OutFile,Error])
  139         end;
  140     Error ->
  141         local_error("Can't write to file ~s: ~w", [OutFile,Error])
  142     end.
  143 
  144 
  145 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  146 % Find the templates
  147 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  148 
  149 find_templates(SearchPaths) ->
  150     find_templates(SearchPaths, SearchPaths).
  151 
  152 find_templates([SearchPath | SearchPaths], AllSearchPaths) ->
  153     case filelib:wildcard(filename:join(SearchPath, "*.html.src")) of
  154     [] ->
  155         find_templates(SearchPaths, AllSearchPaths);
  156     Result ->
  157         Result
  158     end;
  159 find_templates([], AllSearchPaths) ->
  160     local_error("No templates found in ~p",[AllSearchPaths]).
  161 
  162 
  163 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  164 % This function read all application names and if present all "info" files.
  165 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  166 
  167 find_information(Source, Bases) ->
  168     Paths = find_application_paths(Source, Bases),
  169 %    report("****\nPaths: ~p", [Paths]),
  170     Apps = find_application_infos(Paths),
  171 %    report("****\nApps: ~p", [Apps]),
  172     form_groups(Apps).
  173 
  174 % The input is a list of tuples of the form
  175 %
  176 %   IN: [{BaseURL,SearchDir}, ...]
  177 %
  178 % and the output is a list
  179 %
  180 %  OUT: [{Appname,AppVersion,AppPath,IndexUTL}, ...]
  181 %
  182 % We know URL ends in a slash.
  183 
  184 find_application_paths(_, []) ->
  185     [];
  186 find_application_paths(Source, [{URL, Dir} | Paths]) ->
  187 
  188     AppDirs = get_app_dirs(Dir),   
  189     AppPaths = get_app_paths(Source, AppDirs, URL),
  190     AppPaths ++ find_application_paths(Source, Paths).
  191 
  192 
  193 get_app_paths(src, AppDirs, URL) ->
  194     Sub1 = "doc/html/index.html",
  195 %%     Sub2 = "doc/index.html",
  196     lists:map(
  197       fun({App, AppPath}) -> 
  198           VsnFile = filename:join(AppPath, "vsn.mk"),
  199           VsnStr = 
  200           case file:read_file(VsnFile) of
  201               {ok, Bin} ->
  202               case re:run(Bin, ".*VSN\s*=\s*([0-9\.]+).*",[{capture,[1],list}]) of
  203                   {match, [V]} ->
  204                   V;
  205                   nomatch ->
  206                   exit(io_lib:format("No VSN variable found in ~s\n",
  207                              [VsnFile]))
  208               end;
  209               {error, Reason} ->
  210               exit(io_lib:format("~p : ~s\n", [Reason, VsnFile]))
  211           end,
  212           AppURL = URL ++ App ++ "-" ++ VsnStr,
  213           {App, VsnStr, AppPath, AppURL ++ "/" ++ Sub1}
  214       end, AppDirs);
  215 get_app_paths(rel, AppDirs, URL) ->
  216     Sub1 = "doc/html/index.html",
  217 %%     Sub2 = "doc/index.html",
  218     lists:map(
  219       fun({App, AppPath}) -> 
  220           [AppName, VsnStr] = string:tokens(App, "-"),
  221           AppURL = URL ++ App,
  222           {AppName, VsnStr, AppPath, AppURL ++ "/" ++ Sub1}
  223       end, AppDirs).
  224 
  225 
  226 get_app_dirs(Dir) ->
  227     {ok, Files} = file:list_dir(Dir),
  228     AFiles = 
  229     lists:map(fun(File) -> {File, filename:join([Dir, File])} end, Files),
  230     lists:zf(fun is_app_with_doc/1, AFiles).
  231 
  232 is_app_with_doc({"." ++ _ADir, _APath}) ->
  233     false;
  234 is_app_with_doc({ADir, APath}) ->
  235     case file:read_file_info(filename:join([APath, "info"])) of
  236     {ok, _FileInfo} ->
  237         {true, {ADir, APath}};
  238     _ ->
  239         false
  240     end.
  241 
  242 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  243 % Find info for one application.
  244 % Read the "info" file for each application. Look at "group" and "short".
  245 % key words.
  246 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  247 
  248 %   IN: [{Appname,AppVersion,AppPath,IndexUTL}, ...]
  249 %  OUT: [{Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]}
  250 %                        | ...]}, ...]
  251 
  252 find_application_infos([]) ->
  253     [];
  254 find_application_infos([{App, Vsn, AppPath, IndexURL} | Paths]) ->
  255     case read_info(filename:join(AppPath,"info")) of
  256     {error,_Reason} ->
  257         warning("No info for app ~p", [AppPath]),
  258         find_application_infos(Paths);
  259     Db ->
  260         {Group,_Heading} =
  261         case lists:keysearch("group", 1, Db) of
  262             {value, {_, G0}} ->
  263             % This value may be in two parts, 
  264                 % tag and desciption
  265             case string:str(G0, " ") of
  266                 0 ->
  267                 {list_to_atom(G0), ""};
  268                 N ->
  269                 {list_to_atom(string:substr(G0,1,N-1)),
  270                  string:substr(G0,N+1)}
  271             end;
  272             false ->
  273             local_error("No group given",[])
  274         end,
  275         Text =
  276         case lists:keysearch("short", 1, Db) of
  277             {value, {_, G1}} ->
  278             G1;
  279             false ->
  280             ""
  281         end,
  282 %%      [{Group, Heading, {App, {Vsn, AppPath, IndexURL, Text}}}
  283         [{Group, "", {App, {Vsn, AppPath, IndexURL, Text}}}
  284          | find_application_infos(Paths)]
  285     end.
  286 
  287 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  288 % Group into one list element for each group name.
  289 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  290 
  291 % IN : {Group,Heading,{AppName,{AppVersion,Path,URL,Text}}}
  292 % OUT: {Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]} | ...]}
  293 
  294 form_groups(Apps) ->
  295     group_apps(lists:sort(Apps)).
  296 
  297 group_apps([{Group,Heading,AppInfo} | Info]) ->
  298     group_apps(Info, Group, Heading, [AppInfo]);
  299 group_apps([]) ->
  300     [].
  301 
  302 % First description
  303 group_apps([{Group,"",AppInfo} | Info], Group, Heading, AppInfos) ->
  304     group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
  305 group_apps([{Group,Heading,AppInfo} | Info], Group, "", AppInfos) ->
  306     group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
  307 % Exact match
  308 group_apps([{Group,Heading,AppInfo} | Info], Group, Heading, AppInfos) ->
  309     group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
  310 % Different descriptions
  311 group_apps([{Group,_OtherHeading,AppInfo} | Info], Group, Heading, AppInfos) ->
  312     warning("Group ~w descriptions differ",[Group]),
  313     group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
  314 group_apps(Info, Group, Heading, AppInfos) ->
  315     [{Group,Heading,combine_apps(AppInfos)} | group_apps(Info)].
  316 
  317 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  318 % Group into one list element for each application name.
  319 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  320 
  321 % IN : {AppName,{AppVersion,Path,URL,Text}}
  322 % OUT: {AppName,[{AppVersion,Path,URL,Text} | ...]}
  323 
  324 combine_apps(Apps) ->
  325     combine_apps(Apps,[],[]).
  326 
  327 combine_apps([{AppName,{Vsn1,Path1,URL1,Text1}}, 
  328           {AppName,{Vsn2,Path2,URL2,Text2}} | Apps], AppAcc, Acc) ->
  329     combine_apps([{AppName,{Vsn2,Path2,URL2,Text2}} | Apps],
  330          [{Vsn1,Path1,URL1,Text1} | AppAcc],
  331          Acc);
  332 combine_apps([{AppName,{Vsn1,Path1,URL1,Text1}}, 
  333           {NewAppName,{Vsn2,Path2,URL2,Text2}} | Apps], AppAcc, Acc) ->
  334     App = lists:sort(fun vsncmp/2,[{Vsn1,Path1,URL1,Text1}|AppAcc]),
  335     combine_apps([{NewAppName,{Vsn2,Path2,URL2,Text2}} | Apps],
  336          [],
  337          [{AppName,App}|Acc]);
  338 combine_apps([{AppName,{Vsn,Path,URL,Text}}], AppAcc, Acc) ->
  339     App = lists:sort(fun vsncmp/2,[{Vsn,Path,URL,Text}|AppAcc]),
  340     [{AppName,App}|Acc].
  341 
  342 
  343 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  344 % Open a template and fill in the missing parts
  345 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  346 
  347 % IN : {Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]} | ...]}
  348 % OUT: String that is the HTML code
  349 
  350 subst_template(Group, File, Info) ->
  351     case file:open(File, read) of
  352     {ok,Stream} ->
  353         Res = subst_template_1(Group, Stream, Info),
  354         file:close(Stream),
  355         Res;
  356     {error,Reason} ->
  357         {error, Reason}
  358     end.
  359 
  360 subst_template_1(Group, Stream, Info) ->
  361     case file:read(Stream, 100000) of
  362     {ok, Template} ->
  363         Fun = fun(Match, _) -> {subst(Match, Info, Group),Info} end,
  364         gsub(Template, "#[A-Za-z_0-9]+#", Fun, Info);
  365     {error, Reason} ->
  366         {error, Reason}
  367     end.
  368 
  369 get_version(Info) ->
  370     case lists:keysearch('runtime', 1, Info) of
  371     {value, {_,_,Apps}} ->
  372         case lists:keysearch("erts", 1, Apps) of
  373         {value, {_,[{Vers,_,_,_} | _]}} ->
  374             Vers;
  375         _ ->
  376             ""
  377         end;
  378     _ ->
  379         ""
  380     end.
  381             
  382 subst("#otp_base_vsn#", _Info, _Group) ->
  383     get(otp_base_vsn);
  384 subst("#version#", Info, _Group) ->
  385     get_version(Info);
  386 subst("#copyright#", _Info, _Group) ->
  387     "copyright  Copyright &copy; 1991-2004";
  388 subst("#groups#", Info, _Group) ->
  389     [
  390      subst_groups(Info)
  391     ];
  392 subst("#applinks#", Info, Group) ->
  393     subst_applinks(Info, Group);
  394 subst(KeyWord, Info, _Group) ->
  395     case search_appname(KeyWord -- "##", Info) of
  396     {ok,URL} ->
  397         URL;
  398     _ ->
  399         warning("Can't substitute keyword ~s~n",[KeyWord]),
  400         ""
  401     end.
  402 
  403 search_appname(App, [{_Group,_,Apps} | Groups]) ->
  404     case lists:keysearch(App, 1, Apps) of
  405     {value, {_,[{_Vers,_Path,URL,_Text} | _]}} ->
  406         {ok,lists:sublist(URL, length(URL) - length("/index.html"))}; 
  407     _ ->
  408         search_appname(App, Groups)
  409     end;
  410 search_appname(_App, []) ->
  411     {error, noapp}.
  412 
  413 subst_applinks(Info, Group) ->
  414     subst_applinks_1(group_order(), Info, Group).
  415 
  416 subst_applinks_1([{G, Heading}|Gs], Info0, Group) ->
  417     case lists:keysearch(G, 1, Info0) of
  418     {value,{G,_Heading,Apps}} ->
  419         Info = lists:keydelete(G, 1, Info0),
  420         ["\n<li>",Heading, "\n<ul>\n",
  421          html_applinks(Apps), "\n</ul></li>\n"|
  422          subst_applinks_1(Gs, Info, Group)];
  423     false ->
  424         warning("No applications in group ~w\n", [G]),
  425         subst_applinks_1(Gs, Info0, Group)
  426     end;
  427 subst_applinks_1([], [], _) -> [];
  428 subst_applinks_1([], Info, _) ->
  429     local_error("Info left: ~p\n", [Info]),
  430     [].
  431 
  432 html_applinks([{Name,[{_,_,URL,_}|_]}|AppNames]) ->
  433     ["<li><a href=\"",URL,"\">",Name,
  434      "</a></li>\n"|html_applinks(AppNames)];
  435 html_applinks([]) -> [].
  436 
  437 
  438 % Info: [{Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ..]} | ..]} ..]
  439 
  440 subst_groups(Info0) ->
  441     {Html1,Info1} = subst_known_groups(group_order(), Info0, ""),
  442     {Html2,Info}  = subst_unknown_groups(Info1, Html1, []),
  443     Fun = fun({_Group,_GText,Applist}, Acc) -> Applist ++ Acc end,
  444     case lists:foldl(Fun, [], Info) of
  445     [] ->
  446         Html2;
  447     Apps ->
  448         [Html2,group_table("Misc Applications",Apps)]
  449     end.
  450     
  451 
  452 subst_known_groups([], Info, Text) ->
  453     {Text,Info};
  454 subst_known_groups([{Group, Heading} | Groups], Info0, Text0) ->    
  455     case lists:keysearch(Group, 1, Info0) of
  456     {value,{_,_Heading,Apps}} ->
  457         Text = group_table(Heading,Apps),
  458         Info = lists:keydelete(Group, 1, Info0),
  459         subst_known_groups(Groups, Info, Text0 ++ Text);
  460     false ->
  461         warning("No applications in group ~w~n",[Group]),
  462         subst_known_groups(Groups, Info0, Text0)
  463     end.
  464 
  465 
  466 subst_unknown_groups([], Text0, Left) ->
  467     {Text0,Left};
  468 subst_unknown_groups([{Group,"",Apps} | Groups], Text0, Left) ->
  469     warning("No text describes ~w",[Group]),
  470     subst_unknown_groups(Groups, Text0, [{Group,"",Apps} | Left]);
  471 subst_unknown_groups([{_Group,Heading,Apps} | Groups], Text0, Left) ->    
  472     Text = group_table(Heading,Apps),
  473     subst_unknown_groups(Groups, Text0 ++ Text, Left).
  474 
  475 
  476 group_table(Heading,Apps) ->
  477     ["<h2>",Heading,"</h2>",
  478      "<table class=\"group-table\">\n",
  479      subst_apps(Apps),
  480      "</table>\n"
  481     ].
  482 
  483 % Count and split the applications in half to get the right sort
  484 % order in the table.
  485 
  486 subst_apps([{App,VersionInfo} | Apps]) ->
  487     [subst_app(App, VersionInfo) | subst_apps(Apps)];
  488 subst_apps([]) ->
  489     [].
  490 
  491 
  492 subst_app(App, [{VSN,_Path,Link,Text}]) ->
  493     [
  494      "  <tr class=app>\n",
  495      "    <td>\n",
  496      "            <a href=\"",Link,"\" target=\"_top\">",uc(App),"</a>\n",
  497      "            <a href=\"",Link,"\" target=\"_top\">",VSN,"</a>\n",
  498      "    </td>\n",
  499      "    <td>\n",
  500      Text,"\n",
  501      "    </td>\n",
  502      "  </tr>\n"
  503     ];
  504 subst_app(App, [{VSN,_Path,Link,Text} | VerInfos]) ->
  505     [
  506      "  <tr class=app>\n",
  507      "    <td>\n",
  508      "            <a href=\"",Link,"\" target=\"_top\">",uc(App),
  509      "</a>\n",
  510      "            <a href=\"",Link,"\" target=\"_top\">",VSN,"</a>\n",
  511      "                <br/>\n",
  512      subst_vsn(VerInfos),
  513      "    </td>\n",
  514      "    <td>\n",
  515      Text,"\n",
  516      "    </td>\n",
  517      "  </tr>\n"
  518     ].
  519 
  520 
  521 subst_vsn([{VSN,_Path,Link,_Text} | VSNs]) ->
  522     [
  523      "      <font size=\"2\"><a class=anum href=\"",Link,"\" target=\"_top\">",
  524      VSN,
  525      "</a></font><br/>\n",
  526      subst_vsn(VSNs)
  527     ];
  528 subst_vsn([]) ->
  529     "".
  530     
  531 
  532 % Yes, this is very inefficient an is done for every comarision
  533 % in the sort but it doesn't matter in this case.
  534 
  535 vsncmp({Vsn1,_,_,_}, {Vsn2,_,_,_}) ->
  536     L1 = [list_to_integer(N1) || N1 <- string:tokens(Vsn1, ".")],
  537     L2 = [list_to_integer(N2) || N2 <- string:tokens(Vsn2, ".")],
  538     L1 > L2.
  539 
  540 
  541 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  542 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  543 %
  544 %  GENERIC FUNCTIONS, NOT SPECIFIC FOR GENERATING INDEX.HTML
  545 %
  546 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  547 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  548 
  549 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  550 % Read the "info" file into a list of Key/Value pairs
  551 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  552 
  553 read_info(File) ->
  554     case file:open(File, read) of
  555     {ok,Stream} ->
  556         Res =
  557         case file:read(Stream,10000) of
  558             {ok, Text} ->
  559             Lines = string:tokens(Text, "\n\r"),
  560             KeyValues0 = lines_to_key_value(Lines),
  561             combine_key_value(KeyValues0);
  562             {error, Reason} ->
  563             {error, Reason}
  564         end,
  565         file:close(Stream),
  566         Res;
  567     {error,Reason} ->
  568         {error,Reason}
  569     end.
  570 
  571 combine_key_value([{Key,Value1},{Key,Value2} | KeyValues]) ->
  572     combine_key_value([{Key,Value1 ++ "\n" ++ Value2} | KeyValues]);
  573 combine_key_value([KeyValue | KeyValues]) ->
  574     [KeyValue | combine_key_value(KeyValues)];
  575 combine_key_value([]) ->
  576     [].
  577 
  578 lines_to_key_value([]) ->
  579     [];
  580 lines_to_key_value([Line | Lines]) ->
  581     case re:run(Line, "^[a-zA-Z_\\-]+:") of
  582     nomatch ->
  583         case re:run(Line, "[\041-\377]") of
  584         nomatch ->
  585             lines_to_key_value(Lines);
  586         _ ->
  587             warning("skipping line \"~s\"",[Line]),
  588             lines_to_key_value(Lines)
  589         end;
  590     {match, [{0, Length} |_]} ->
  591         Value0 = lists:sublist(Line, Length+1, length(Line) - Length),
  592         Value1 = re:replace(Value0, "^[ \t]*", "", 
  593                      [{return, list}]),
  594         Value = re:replace(Value1, "[ \t]*$", "", 
  595                                          [{return, list}]),
  596             Key = lists:sublist(Line, Length-1),
  597         [{Key, Value} | lines_to_key_value(Lines)]
  598     end.
  599 
  600 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  601 % Regular expression helpers.
  602 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  603 
  604 %% -type gsub(String, RegExp, Fun, Acc) -> subres().
  605 %%  Substitute every match of the regular expression RegExp with the
  606 %%  string returned from the function Fun(Match, Acc). Accept pre-parsed
  607 %%  regular expressions. Acc is an argument to the Fun. The Fun should return
  608 %%  a tuple {Replacement, NewAcc}.
  609  
  610 gsub(String, RegExp, Fun, Acc) when is_list(RegExp) ->
  611     case re:compile(RegExp) of
  612         {ok, RE} -> 
  613         gsub(String, RE, Fun, Acc);
  614     {error, E} -> 
  615         {error, E}
  616     end;
  617 gsub(String, RE, Fun, Acc) ->
  618     {match, Ss} = re:run(String, RE, [global]),
  619     {NewString, NewAcc} = sub_repl(Ss, Fun, Acc, String, 0),
  620     {ok, NewString, NewAcc}.
  621 
  622 
  623 % New code that uses fun for finding the replacement. Also uses accumulator
  624 % to pass argument between the calls to the fun.
  625 sub_repl([[{St, L}] |Ss], Fun, Acc0, S, Pos) ->
  626         Match = string:substr(S, St+1, L),
  627         {Rep, Acc} = Fun(Match, Acc0),
  628         {Rs, NewAcc} = sub_repl(Ss, Fun, Acc, S, St+L),
  629         {string:substr(S, Pos+1, St-Pos) ++ Rep ++ Rs, NewAcc};
  630 sub_repl([], _Fun, Acc, S, Pos) -> {string:substr(S, Pos+1), Acc}.
  631 
  632 
  633 
  634 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  635 % Error and warnings
  636 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  637 
  638 local_error(Format, Args) ->
  639     io:format("ERROR: " ++ Format ++ "\n", Args),
  640     exit(1).
  641 
  642 warning(Format, Args) ->
  643     case get(silent) of
  644     true -> ok;
  645     _ -> io:format("WARNING: " ++ Format ++ "\n", Args)
  646     end.
  647 
  648 report(Format, Args) ->
  649     case get(silent) of
  650     true -> ok;
  651     _ -> io:format(Format ++ "\n", Args)
  652     end.
  653 
  654 
  655 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  656 % Extensions to the 'string' module.
  657 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  658 
  659 uc(String) ->
  660     lists:reverse(uc(String, [])).
  661 
  662 uc([], Acc) ->
  663     Acc;
  664 uc([H | T], Acc) when is_integer(H), [97] =< H, H =< $z ->
  665     uc(T, [H - 32 | Acc]);
  666 uc([H | T], Acc) ->
  667     uc(T, [H | Acc]).
  668