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 © 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