1%%--------------------------------------------------------------------
2%%
3%% %CopyrightBegin%
4%%
5%% Copyright Ericsson AB 2009-2020. 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
34group_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
48top_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
57top_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));
60top_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));
63top_index(RootDir)  when is_atom(RootDir) ->
64    {_,RelName} = init:script_id(),
65    top_index(rel, RootDir, filename:join(RootDir, "doc"), RelName).
66
67
68
69top_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
84top_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
97process_templates([], _DestDir, _Groups) ->
98    report("\n", []);
99process_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
114process_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
123process_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);
127process_multi_template_1([], _, _, _, _, _) -> ok.
128
129subst_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
149find_templates(SearchPaths) ->
150    find_templates(SearchPaths, SearchPaths).
151
152find_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;
159find_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
167find_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
184find_application_paths(_, []) ->
185    [];
186find_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
193get_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);
215get_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
226get_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
232is_app_with_doc({"." ++ _ADir, _APath}) ->
233    false;
234is_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
252find_application_infos([]) ->
253    [];
254find_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
294form_groups(Apps) ->
295    group_apps(lists:sort(Apps)).
296
297group_apps([{Group,Heading,AppInfo} | Info]) ->
298    group_apps(Info, Group, Heading, [AppInfo]);
299group_apps([]) ->
300    [].
301
302% First description
303group_apps([{Group,"",AppInfo} | Info], Group, Heading, AppInfos) ->
304    group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
305group_apps([{Group,Heading,AppInfo} | Info], Group, "", AppInfos) ->
306    group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
307% Exact match
308group_apps([{Group,Heading,AppInfo} | Info], Group, Heading, AppInfos) ->
309    group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
310% Different descriptions
311group_apps([{Group,_OtherHeading,AppInfo} | Info], Group, Heading, AppInfos) ->
312    warning("Group ~w descriptions differ",[Group]),
313    group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
314group_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
324combine_apps(Apps) ->
325    combine_apps(Apps,[],[]).
326
327combine_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);
332combine_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]);
338combine_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
350subst_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
360subst_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
369get_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
382subst("#otp_base_vsn#", _Info, _Group) ->
383    get(otp_base_vsn);
384subst("#version#", Info, _Group) ->
385    get_version(Info);
386subst("#copyrightyear#", _Info, _Group) ->
387    {Year,_,_} = erlang:date(),
388    integer_to_list(Year);
389subst("#copyright#", _Info, _Group) ->
390    "copyright  Copyright &copy; 1991-2004";
391subst("#groups#", Info, _Group) ->
392    [
393     subst_groups(Info)
394    ];
395subst("#applinks#", Info, Group) ->
396    subst_applinks(Info, Group);
397subst(KeyWord, Info, _Group) ->
398    case search_appname(KeyWord -- "##", Info) of
399	{ok,URL} ->
400	    URL;
401	_ ->
402	    warning("Can't substitute keyword ~s~n",[KeyWord]),
403	    ""
404    end.
405
406search_appname(App, [{_Group,_,Apps} | Groups]) ->
407    case lists:keysearch(App, 1, Apps) of
408	{value, {_,[{_Vers,_Path,URL,_Text} | _]}} ->
409	    {ok,lists:sublist(URL, length(URL) - length("/index.html"))};
410	_ ->
411	    search_appname(App, Groups)
412    end;
413search_appname(_App, []) ->
414    {error, noapp}.
415
416subst_applinks(Info, Group) ->
417    subst_applinks_1(group_order(), Info, Group).
418
419subst_applinks_1([{G, Heading}|Gs], Info0, Group) ->
420    case lists:keysearch(G, 1, Info0) of
421	{value,{G,_Heading,Apps}} ->
422	    Info = lists:keydelete(G, 1, Info0),
423	    ["\n<li>",Heading, "\n<ul>\n",
424	     html_applinks(Apps), "\n</ul></li>\n"|
425	     subst_applinks_1(Gs, Info, Group)];
426	false ->
427	    warning("No applications in group ~w\n", [G]),
428	    subst_applinks_1(Gs, Info0, Group)
429    end;
430subst_applinks_1([], [], _) -> [];
431subst_applinks_1([], Info, _) ->
432    local_error("Info left: ~p\n", [Info]),
433    [].
434
435html_applinks([{Name,[{_,_,URL,_}|_]}|AppNames]) ->
436    ["<li><a href=\"",URL,"\">",Name,
437     "</a></li>\n"|html_applinks(AppNames)];
438html_applinks([]) -> [].
439
440
441% Info: [{Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ..]} | ..]} ..]
442
443subst_groups(Info0) ->
444    {Html1,Info1} = subst_known_groups(group_order(), Info0, ""),
445    {Html2,Info}  = subst_unknown_groups(Info1, Html1, []),
446    Fun = fun({_Group,_GText,Applist}, Acc) -> Applist ++ Acc end,
447    case lists:foldl(Fun, [], Info) of
448	[] ->
449	    Html2;
450	Apps ->
451	    [Html2,group_table("Misc Applications",Apps)]
452    end.
453
454
455subst_known_groups([], Info, Text) ->
456    {Text,Info};
457subst_known_groups([{Group, Heading} | Groups], Info0, Text0) ->
458    case lists:keysearch(Group, 1, Info0) of
459	{value,{_,_Heading,Apps}} ->
460	    Text = group_table(Heading,Apps),
461	    Info = lists:keydelete(Group, 1, Info0),
462	    subst_known_groups(Groups, Info, Text0 ++ Text);
463	false ->
464	    warning("No applications in group ~w~n",[Group]),
465	    subst_known_groups(Groups, Info0, Text0)
466    end.
467
468
469subst_unknown_groups([], Text0, Left) ->
470    {Text0,Left};
471subst_unknown_groups([{Group,"",Apps} | Groups], Text0, Left) ->
472    warning("No text describes ~w",[Group]),
473    subst_unknown_groups(Groups, Text0, [{Group,"",Apps} | Left]);
474subst_unknown_groups([{_Group,Heading,Apps} | Groups], Text0, Left) ->
475    Text = group_table(Heading,Apps),
476    subst_unknown_groups(Groups, Text0 ++ Text, Left).
477
478
479group_table(Heading,Apps) ->
480    ["<h2>",Heading,"</h2>",
481     "<table class=\"group-table\">\n",
482     subst_apps(Apps),
483     "</table>\n"
484    ].
485
486% Count and split the applications in half to get the right sort
487% order in the table.
488
489subst_apps([{App,VersionInfo} | Apps]) ->
490    [subst_app(App, VersionInfo) | subst_apps(Apps)];
491subst_apps([]) ->
492    [].
493
494
495subst_app(App, [{VSN,_Path,Link,Text}]) ->
496    [
497     "  <tr class=app>\n",
498     "    <td>\n",
499     "            <a href=\"",Link,"\" target=\"_top\">",uc(App),"</a>\n",
500     "            <a href=\"",Link,"\" target=\"_top\">",VSN,"</a>\n",
501     "    </td>\n",
502     "    <td>\n",
503     Text,"\n",
504     "    </td>\n",
505     "  </tr>\n"
506    ];
507subst_app(App, [{VSN,_Path,Link,Text} | VerInfos]) ->
508    [
509     "  <tr class=app>\n",
510     "    <td>\n",
511     "            <a href=\"",Link,"\" target=\"_top\">",uc(App),
512     "</a>\n",
513     "            <a href=\"",Link,"\" target=\"_top\">",VSN,"</a>\n",
514     "                <br/>\n",
515     subst_vsn(VerInfos),
516     "    </td>\n",
517     "    <td>\n",
518     Text,"\n",
519     "    </td>\n",
520     "  </tr>\n"
521    ].
522
523
524subst_vsn([{VSN,_Path,Link,_Text} | VSNs]) ->
525    [
526     "      <font size=\"2\"><a class=anum href=\"",Link,"\" target=\"_top\">",
527     VSN,
528     "</a></font><br/>\n",
529     subst_vsn(VSNs)
530    ];
531subst_vsn([]) ->
532    "".
533
534
535% Yes, this is very inefficient an is done for every comarision
536% in the sort but it doesn't matter in this case.
537
538vsncmp({Vsn1,_,_,_}, {Vsn2,_,_,_}) ->
539    L1 = [list_to_integer(N1) || N1 <- string:tokens(Vsn1, ".")],
540    L2 = [list_to_integer(N2) || N2 <- string:tokens(Vsn2, ".")],
541    L1 > L2.
542
543
544%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
545%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
546%
547%  GENERIC FUNCTIONS, NOT SPECIFIC FOR GENERATING INDEX.HTML
548%
549%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
550%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
551
552%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
553% Read the "info" file into a list of Key/Value pairs
554%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
555
556read_info(File) ->
557    case file:open(File, read) of
558	{ok,Stream} ->
559	    Res =
560		case file:read(Stream,10000) of
561		    {ok, Text} ->
562			Lines = string:tokens(Text, "\n\r"),
563			KeyValues0 = lines_to_key_value(Lines),
564			combine_key_value(KeyValues0);
565		    {error, Reason} ->
566			{error, Reason}
567		end,
568	    file:close(Stream),
569	    Res;
570	{error,Reason} ->
571	    {error,Reason}
572    end.
573
574combine_key_value([{Key,Value1},{Key,Value2} | KeyValues]) ->
575    combine_key_value([{Key,Value1 ++ "\n" ++ Value2} | KeyValues]);
576combine_key_value([KeyValue | KeyValues]) ->
577    [KeyValue | combine_key_value(KeyValues)];
578combine_key_value([]) ->
579    [].
580
581lines_to_key_value([]) ->
582    [];
583lines_to_key_value([Line | Lines]) ->
584    case re:run(Line, "^[a-zA-Z_\\-]+:") of
585	nomatch ->
586	    case re:run(Line, "[\041-\377]") of
587		nomatch ->
588		    lines_to_key_value(Lines);
589		_ ->
590		    warning("skipping line \"~s\"",[Line]),
591		    lines_to_key_value(Lines)
592	    end;
593	{match, [{0, Length} |_]} ->
594	    Value0 = lists:sublist(Line, Length+1, length(Line) - Length),
595	    Value1 = re:replace(Value0, "^[ \t]*", "",
596					 [{return, list}]),
597	    Value = re:replace(Value1, "[ \t]*$", "",
598                                         [{return, list}]),
599            Key = lists:sublist(Line, Length-1),
600	    [{Key, Value} | lines_to_key_value(Lines)]
601    end.
602
603%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
604% Regular expression helpers.
605%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
606
607%% -type gsub(String, RegExp, Fun, Acc) -> subres().
608%%  Substitute every match of the regular expression RegExp with the
609%%  string returned from the function Fun(Match, Acc). Accept pre-parsed
610%%  regular expressions. Acc is an argument to the Fun. The Fun should return
611%%  a tuple {Replacement, NewAcc}.
612
613gsub(String, RegExp, Fun, Acc) when is_list(RegExp) ->
614    case re:compile(RegExp) of
615        {ok, RE} ->
616	    gsub(String, RE, Fun, Acc);
617	{error, E} ->
618	    {error, E}
619    end;
620gsub(String, RE, Fun, Acc) ->
621    {match, Ss} = re:run(String, RE, [global]),
622    {NewString, NewAcc} = sub_repl(Ss, Fun, Acc, String, 0),
623    {ok, NewString, NewAcc}.
624
625
626% New code that uses fun for finding the replacement. Also uses accumulator
627% to pass argument between the calls to the fun.
628sub_repl([[{St, L}] |Ss], Fun, Acc0, S, Pos) ->
629        Match = string:substr(S, St+1, L),
630        {Rep, Acc} = Fun(Match, Acc0),
631        {Rs, NewAcc} = sub_repl(Ss, Fun, Acc, S, St+L),
632        {string:substr(S, Pos+1, St-Pos) ++ Rep ++ Rs, NewAcc};
633sub_repl([], _Fun, Acc, S, Pos) -> {string:substr(S, Pos+1), Acc}.
634
635
636
637%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
638% Error and warnings
639%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
640
641local_error(Format, Args) ->
642    io:format("ERROR: " ++ Format ++ "\n", Args),
643    exit(1).
644
645warning(Format, Args) ->
646    case get(silent) of
647	true -> ok;
648	_ -> io:format("WARNING: " ++ Format ++ "\n", Args)
649    end.
650
651report(Format, Args) ->
652    case get(silent) of
653	true -> ok;
654	_ -> io:format(Format ++ "\n", Args)
655    end.
656
657
658%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
659% Extensions to the 'string' module.
660%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
661
662uc(String) ->
663    lists:reverse(uc(String, [])).
664
665uc([], Acc) ->
666    Acc;
667uc([H | T], Acc) when is_integer(H), [97] =< H, H =< $z ->
668    uc(T, [H - 32 | Acc]);
669uc([H | T], Acc) ->
670    uc(T, [H | Acc]).
671
672