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("#copyright#", _Info, _Group) ->
387    "copyright  Copyright &copy; 1991-2004";
388subst("#groups#", Info, _Group) ->
389    [
390     subst_groups(Info)
391    ];
392subst("#applinks#", Info, Group) ->
393    subst_applinks(Info, Group);
394subst(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
403search_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;
410search_appname(_App, []) ->
411    {error, noapp}.
412
413subst_applinks(Info, Group) ->
414    subst_applinks_1(group_order(), Info, Group).
415
416subst_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;
427subst_applinks_1([], [], _) -> [];
428subst_applinks_1([], Info, _) ->
429    local_error("Info left: ~p\n", [Info]),
430    [].
431
432html_applinks([{Name,[{_,_,URL,_}|_]}|AppNames]) ->
433    ["<li><a href=\"",URL,"\">",Name,
434     "</a></li>\n"|html_applinks(AppNames)];
435html_applinks([]) -> [].
436
437
438% Info: [{Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ..]} | ..]} ..]
439
440subst_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
452subst_known_groups([], Info, Text) ->
453    {Text,Info};
454subst_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
466subst_unknown_groups([], Text0, Left) ->
467    {Text0,Left};
468subst_unknown_groups([{Group,"",Apps} | Groups], Text0, Left) ->
469    warning("No text describes ~w",[Group]),
470    subst_unknown_groups(Groups, Text0, [{Group,"",Apps} | Left]);
471subst_unknown_groups([{_Group,Heading,Apps} | Groups], Text0, Left) ->
472    Text = group_table(Heading,Apps),
473    subst_unknown_groups(Groups, Text0 ++ Text, Left).
474
475
476group_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
486subst_apps([{App,VersionInfo} | Apps]) ->
487    [subst_app(App, VersionInfo) | subst_apps(Apps)];
488subst_apps([]) ->
489    [].
490
491
492subst_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    ];
504subst_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
521subst_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    ];
528subst_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
535vsncmp({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
553read_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
571combine_key_value([{Key,Value1},{Key,Value2} | KeyValues]) ->
572    combine_key_value([{Key,Value1 ++ "\n" ++ Value2} | KeyValues]);
573combine_key_value([KeyValue | KeyValues]) ->
574    [KeyValue | combine_key_value(KeyValues)];
575combine_key_value([]) ->
576    [].
577
578lines_to_key_value([]) ->
579    [];
580lines_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
610gsub(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;
617gsub(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.
625sub_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};
630sub_repl([], _Fun, Acc, S, Pos) -> {string:substr(S, Pos+1), Acc}.
631
632
633
634%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
635% Error and warnings
636%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
637
638local_error(Format, Args) ->
639    io:format("ERROR: " ++ Format ++ "\n", Args),
640    exit(1).
641
642warning(Format, Args) ->
643    case get(silent) of
644	true -> ok;
645	_ -> io:format("WARNING: " ++ Format ++ "\n", Args)
646    end.
647
648report(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
659uc(String) ->
660    lists:reverse(uc(String, [])).
661
662uc([], Acc) ->
663    Acc;
664uc([H | T], Acc) when is_integer(H), [97] =< H, H =< $z ->
665    uc(T, [H - 32 | Acc]);
666uc([H | T], Acc) ->
667    uc(T, [H | Acc]).
668
669