%%-------------------------------------------------------------------- %% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2009-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %%----------------------------------------------------------------- %% File: erl_html_tools.erl %% %% Description: %% This file generates the top index of the documentation. %% %%----------------------------------------------------------------- -module(erl_html_tools). -export([top_index/0,top_index/1,top_index/4,top_index_silent/3]). -include_lib("kernel/include/file.hrl"). group_order() -> [ {basic, "Basic"}, {dat, "Database"}, {oam, "Operation & Maintenance"}, {comm, "Interface and Communication"}, {tools, "Tools"}, {test, "Test"}, {doc, "Documentation"}, {orb, "Object Request Broker & IDL"}, {misc, "Miscellaneous"}, {eric, "Ericsson Internal"} ]. top_index() -> case os:getenv("ERL_TOP") of false -> io:format("Variable ERL_TOP is required\n",[]); Value -> {_,RelName} = init:script_id(), top_index(src, Value, filename:join(Value, "doc"), RelName) end. top_index([src, RootDir, DestDir, OtpBaseVsn]) when is_atom(RootDir), is_atom(DestDir), is_atom(OtpBaseVsn) -> top_index(src, atom_to_list(RootDir), atom_to_list(DestDir), atom_to_list(OtpBaseVsn)); top_index([rel, RootDir, DestDir, OtpBaseVsn]) when is_atom(RootDir), is_atom(DestDir), is_atom(OtpBaseVsn) -> top_index(rel, atom_to_list(RootDir), atom_to_list(DestDir), atom_to_list(OtpBaseVsn)); top_index(RootDir) when is_atom(RootDir) -> {_,RelName} = init:script_id(), top_index(rel, RootDir, filename:join(RootDir, "doc"), RelName). top_index(Source, RootDir, DestDir, OtpBaseVsn) -> report("****\nRootDir: ~p", [RootDir]), report("****\nDestDir: ~p", [DestDir]), report("****\nOtpBaseVsn: ~p", [OtpBaseVsn]), put(otp_base_vsn, OtpBaseVsn), Templates = find_templates(["","templates",DestDir]), report("****\nTemplates: ~p", [Templates]), Bases = [{"../lib/", filename:join(RootDir,"lib")}, {"../", RootDir}], Groups = find_information(Source, Bases), report("****\nGroups: ~p", [Groups]), process_templates(Templates, DestDir, Groups). top_index_silent(RootDir, DestDir, OtpBaseVsn) -> put(silent,true), Result = top_index(rel, RootDir, DestDir, OtpBaseVsn), erase(silent), Result. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Main loop - process templates %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% process_templates([], _DestDir, _Groups) -> report("\n", []); process_templates([Template | Templates], DestDir, Groups) -> report("****\nIN-FILE: ~s", [Template]), BaseName = filename:basename(Template, ".src"), case lists:reverse(filename:rootname(BaseName)) of "_"++_ -> %% One template expands to several output files. process_multi_template(BaseName, Template, DestDir, Groups); _ -> %% Standard one-to-one template. OutFile = filename:join(DestDir, BaseName), subst_file("", OutFile, Template, Groups) end, process_templates(Templates, DestDir, Groups). process_multi_template(BaseName0, Template, DestDir, Info) -> Ext = filename:extension(BaseName0), BaseName1 = filename:basename(BaseName0, Ext), [_|BaseName2] = lists:reverse(BaseName1), BaseName = lists:reverse(BaseName2), Groups0 = [{[$_|atom_to_list(G)],G} || {G, _} <- group_order()], Groups = [{"",basic}|Groups0], process_multi_template_1(Groups, BaseName, Ext, Template, DestDir, Info). process_multi_template_1([{Suffix,Group}|Gs], BaseName, Ext, Template, DestDir, Info) -> OutFile = filename:join(DestDir, BaseName++Suffix++Ext), subst_file(Group, OutFile, Template, Info), process_multi_template_1(Gs, BaseName, Ext, Template, DestDir, Info); process_multi_template_1([], _, _, _, _, _) -> ok. subst_file(Group, OutFile, Template, Info) -> report("\nOUTFILE: ~s", [OutFile]), case subst_template(Group, Template, Info) of {ok,Text,_NewInfo} -> case file:open(OutFile, [write]) of {ok, Stream} -> file:write(Stream, Text), file:close(Stream); Error -> local_error("Can't write to file ~s: ~w", [OutFile,Error]) end; Error -> local_error("Can't write to file ~s: ~w", [OutFile,Error]) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Find the templates %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% find_templates(SearchPaths) -> find_templates(SearchPaths, SearchPaths). find_templates([SearchPath | SearchPaths], AllSearchPaths) -> case filelib:wildcard(filename:join(SearchPath, "*.html.src")) of [] -> find_templates(SearchPaths, AllSearchPaths); Result -> Result end; find_templates([], AllSearchPaths) -> local_error("No templates found in ~p",[AllSearchPaths]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % This function read all application names and if present all "info" files. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% find_information(Source, Bases) -> Paths = find_application_paths(Source, Bases), % report("****\nPaths: ~p", [Paths]), Apps = find_application_infos(Paths), % report("****\nApps: ~p", [Apps]), form_groups(Apps). % The input is a list of tuples of the form % % IN: [{BaseURL,SearchDir}, ...] % % and the output is a list % % OUT: [{Appname,AppVersion,AppPath,IndexUTL}, ...] % % We know URL ends in a slash. find_application_paths(_, []) -> []; find_application_paths(Source, [{URL, Dir} | Paths]) -> AppDirs = get_app_dirs(Dir), AppPaths = get_app_paths(Source, AppDirs, URL), AppPaths ++ find_application_paths(Source, Paths). get_app_paths(src, AppDirs, URL) -> Sub1 = "doc/html/index.html", %% Sub2 = "doc/index.html", lists:map( fun({App, AppPath}) -> VsnFile = filename:join(AppPath, "vsn.mk"), VsnStr = case file:read_file(VsnFile) of {ok, Bin} -> case re:run(Bin, ".*VSN\s*=\s*([0-9\.]+).*",[{capture,[1],list}]) of {match, [V]} -> V; nomatch -> exit(io_lib:format("No VSN variable found in ~s\n", [VsnFile])) end; {error, Reason} -> exit(io_lib:format("~p : ~s\n", [Reason, VsnFile])) end, AppURL = URL ++ App ++ "-" ++ VsnStr, {App, VsnStr, AppPath, AppURL ++ "/" ++ Sub1} end, AppDirs); get_app_paths(rel, AppDirs, URL) -> Sub1 = "doc/html/index.html", %% Sub2 = "doc/index.html", lists:map( fun({App, AppPath}) -> [AppName, VsnStr] = string:tokens(App, "-"), AppURL = URL ++ App, {AppName, VsnStr, AppPath, AppURL ++ "/" ++ Sub1} end, AppDirs). get_app_dirs(Dir) -> {ok, Files} = file:list_dir(Dir), AFiles = lists:map(fun(File) -> {File, filename:join([Dir, File])} end, Files), lists:zf(fun is_app_with_doc/1, AFiles). is_app_with_doc({"." ++ _ADir, _APath}) -> false; is_app_with_doc({ADir, APath}) -> case file:read_file_info(filename:join([APath, "info"])) of {ok, _FileInfo} -> {true, {ADir, APath}}; _ -> false end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Find info for one application. % Read the "info" file for each application. Look at "group" and "short". % key words. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % IN: [{Appname,AppVersion,AppPath,IndexUTL}, ...] % OUT: [{Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]} % | ...]}, ...] find_application_infos([]) -> []; find_application_infos([{App, Vsn, AppPath, IndexURL} | Paths]) -> case read_info(filename:join(AppPath,"info")) of {error,_Reason} -> warning("No info for app ~p", [AppPath]), find_application_infos(Paths); Db -> {Group,_Heading} = case lists:keysearch("group", 1, Db) of {value, {_, G0}} -> % This value may be in two parts, % tag and desciption case string:str(G0, " ") of 0 -> {list_to_atom(G0), ""}; N -> {list_to_atom(string:substr(G0,1,N-1)), string:substr(G0,N+1)} end; false -> local_error("No group given",[]) end, Text = case lists:keysearch("short", 1, Db) of {value, {_, G1}} -> G1; false -> "" end, %% [{Group, Heading, {App, {Vsn, AppPath, IndexURL, Text}}} [{Group, "", {App, {Vsn, AppPath, IndexURL, Text}}} | find_application_infos(Paths)] end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Group into one list element for each group name. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % IN : {Group,Heading,{AppName,{AppVersion,Path,URL,Text}}} % OUT: {Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]} | ...]} form_groups(Apps) -> group_apps(lists:sort(Apps)). group_apps([{Group,Heading,AppInfo} | Info]) -> group_apps(Info, Group, Heading, [AppInfo]); group_apps([]) -> []. % First description group_apps([{Group,"",AppInfo} | Info], Group, Heading, AppInfos) -> group_apps(Info, Group, Heading, [AppInfo | AppInfos]); group_apps([{Group,Heading,AppInfo} | Info], Group, "", AppInfos) -> group_apps(Info, Group, Heading, [AppInfo | AppInfos]); % Exact match group_apps([{Group,Heading,AppInfo} | Info], Group, Heading, AppInfos) -> group_apps(Info, Group, Heading, [AppInfo | AppInfos]); % Different descriptions group_apps([{Group,_OtherHeading,AppInfo} | Info], Group, Heading, AppInfos) -> warning("Group ~w descriptions differ",[Group]), group_apps(Info, Group, Heading, [AppInfo | AppInfos]); group_apps(Info, Group, Heading, AppInfos) -> [{Group,Heading,combine_apps(AppInfos)} | group_apps(Info)]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Group into one list element for each application name. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % IN : {AppName,{AppVersion,Path,URL,Text}} % OUT: {AppName,[{AppVersion,Path,URL,Text} | ...]} combine_apps(Apps) -> combine_apps(Apps,[],[]). combine_apps([{AppName,{Vsn1,Path1,URL1,Text1}}, {AppName,{Vsn2,Path2,URL2,Text2}} | Apps], AppAcc, Acc) -> combine_apps([{AppName,{Vsn2,Path2,URL2,Text2}} | Apps], [{Vsn1,Path1,URL1,Text1} | AppAcc], Acc); combine_apps([{AppName,{Vsn1,Path1,URL1,Text1}}, {NewAppName,{Vsn2,Path2,URL2,Text2}} | Apps], AppAcc, Acc) -> App = lists:sort(fun vsncmp/2,[{Vsn1,Path1,URL1,Text1}|AppAcc]), combine_apps([{NewAppName,{Vsn2,Path2,URL2,Text2}} | Apps], [], [{AppName,App}|Acc]); combine_apps([{AppName,{Vsn,Path,URL,Text}}], AppAcc, Acc) -> App = lists:sort(fun vsncmp/2,[{Vsn,Path,URL,Text}|AppAcc]), [{AppName,App}|Acc]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Open a template and fill in the missing parts %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % IN : {Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]} | ...]} % OUT: String that is the HTML code subst_template(Group, File, Info) -> case file:open(File, read) of {ok,Stream} -> Res = subst_template_1(Group, Stream, Info), file:close(Stream), Res; {error,Reason} -> {error, Reason} end. subst_template_1(Group, Stream, Info) -> case file:read(Stream, 100000) of {ok, Template} -> Fun = fun(Match, _) -> {subst(Match, Info, Group),Info} end, gsub(Template, "#[A-Za-z_0-9]+#", Fun, Info); {error, Reason} -> {error, Reason} end. get_version(Info) -> case lists:keysearch('runtime', 1, Info) of {value, {_,_,Apps}} -> case lists:keysearch("erts", 1, Apps) of {value, {_,[{Vers,_,_,_} | _]}} -> Vers; _ -> "" end; _ -> "" end. subst("#otp_base_vsn#", _Info, _Group) -> get(otp_base_vsn); subst("#version#", Info, _Group) -> get_version(Info); subst("#copyright#", _Info, _Group) -> "copyright Copyright © 1991-2004"; subst("#groups#", Info, _Group) -> [ subst_groups(Info) ]; subst("#applinks#", Info, Group) -> subst_applinks(Info, Group); subst(KeyWord, Info, _Group) -> case search_appname(KeyWord -- "##", Info) of {ok,URL} -> URL; _ -> warning("Can't substitute keyword ~s~n",[KeyWord]), "" end. search_appname(App, [{_Group,_,Apps} | Groups]) -> case lists:keysearch(App, 1, Apps) of {value, {_,[{_Vers,_Path,URL,_Text} | _]}} -> {ok,lists:sublist(URL, length(URL) - length("/index.html"))}; _ -> search_appname(App, Groups) end; search_appname(_App, []) -> {error, noapp}. subst_applinks(Info, Group) -> subst_applinks_1(group_order(), Info, Group). subst_applinks_1([{G, Heading}|Gs], Info0, Group) -> case lists:keysearch(G, 1, Info0) of {value,{G,_Heading,Apps}} -> Info = lists:keydelete(G, 1, Info0), ["\n
  • ",Heading, "\n
  • \n"| subst_applinks_1(Gs, Info, Group)]; false -> warning("No applications in group ~w\n", [G]), subst_applinks_1(Gs, Info0, Group) end; subst_applinks_1([], [], _) -> []; subst_applinks_1([], Info, _) -> local_error("Info left: ~p\n", [Info]), []. html_applinks([{Name,[{_,_,URL,_}|_]}|AppNames]) -> ["
  • ",Name, "
  • \n"|html_applinks(AppNames)]; html_applinks([]) -> []. % Info: [{Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ..]} | ..]} ..] subst_groups(Info0) -> {Html1,Info1} = subst_known_groups(group_order(), Info0, ""), {Html2,Info} = subst_unknown_groups(Info1, Html1, []), Fun = fun({_Group,_GText,Applist}, Acc) -> Applist ++ Acc end, case lists:foldl(Fun, [], Info) of [] -> Html2; Apps -> [Html2,group_table("Misc Applications",Apps)] end. subst_known_groups([], Info, Text) -> {Text,Info}; subst_known_groups([{Group, Heading} | Groups], Info0, Text0) -> case lists:keysearch(Group, 1, Info0) of {value,{_,_Heading,Apps}} -> Text = group_table(Heading,Apps), Info = lists:keydelete(Group, 1, Info0), subst_known_groups(Groups, Info, Text0 ++ Text); false -> warning("No applications in group ~w~n",[Group]), subst_known_groups(Groups, Info0, Text0) end. subst_unknown_groups([], Text0, Left) -> {Text0,Left}; subst_unknown_groups([{Group,"",Apps} | Groups], Text0, Left) -> warning("No text describes ~w",[Group]), subst_unknown_groups(Groups, Text0, [{Group,"",Apps} | Left]); subst_unknown_groups([{_Group,Heading,Apps} | Groups], Text0, Left) -> Text = group_table(Heading,Apps), subst_unknown_groups(Groups, Text0 ++ Text, Left). group_table(Heading,Apps) -> ["

    ",Heading,"

    ", "\n", subst_apps(Apps), "
    \n" ]. % Count and split the applications in half to get the right sort % order in the table. subst_apps([{App,VersionInfo} | Apps]) -> [subst_app(App, VersionInfo) | subst_apps(Apps)]; subst_apps([]) -> []. subst_app(App, [{VSN,_Path,Link,Text}]) -> [ " \n", " \n", " ",uc(App),"\n", " ",VSN,"\n", " \n", " \n", Text,"\n", " \n", " \n" ]; subst_app(App, [{VSN,_Path,Link,Text} | VerInfos]) -> [ " \n", " \n", " ",uc(App), "\n", " ",VSN,"\n", "
    \n", subst_vsn(VerInfos), " \n", " \n", Text,"\n", " \n", " \n" ]. subst_vsn([{VSN,_Path,Link,_Text} | VSNs]) -> [ " ", VSN, "
    \n", subst_vsn(VSNs) ]; subst_vsn([]) -> "". % Yes, this is very inefficient an is done for every comarision % in the sort but it doesn't matter in this case. vsncmp({Vsn1,_,_,_}, {Vsn2,_,_,_}) -> L1 = [list_to_integer(N1) || N1 <- string:tokens(Vsn1, ".")], L2 = [list_to_integer(N2) || N2 <- string:tokens(Vsn2, ".")], L1 > L2. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % GENERIC FUNCTIONS, NOT SPECIFIC FOR GENERATING INDEX.HTML % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Read the "info" file into a list of Key/Value pairs %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% read_info(File) -> case file:open(File, read) of {ok,Stream} -> Res = case file:read(Stream,10000) of {ok, Text} -> Lines = string:tokens(Text, "\n\r"), KeyValues0 = lines_to_key_value(Lines), combine_key_value(KeyValues0); {error, Reason} -> {error, Reason} end, file:close(Stream), Res; {error,Reason} -> {error,Reason} end. combine_key_value([{Key,Value1},{Key,Value2} | KeyValues]) -> combine_key_value([{Key,Value1 ++ "\n" ++ Value2} | KeyValues]); combine_key_value([KeyValue | KeyValues]) -> [KeyValue | combine_key_value(KeyValues)]; combine_key_value([]) -> []. lines_to_key_value([]) -> []; lines_to_key_value([Line | Lines]) -> case re:run(Line, "^[a-zA-Z_\\-]+:") of nomatch -> case re:run(Line, "[\041-\377]") of nomatch -> lines_to_key_value(Lines); _ -> warning("skipping line \"~s\"",[Line]), lines_to_key_value(Lines) end; {match, [{0, Length} |_]} -> Value0 = lists:sublist(Line, Length+1, length(Line) - Length), Value1 = re:replace(Value0, "^[ \t]*", "", [{return, list}]), Value = re:replace(Value1, "[ \t]*$", "", [{return, list}]), Key = lists:sublist(Line, Length-1), [{Key, Value} | lines_to_key_value(Lines)] end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Regular expression helpers. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% -type gsub(String, RegExp, Fun, Acc) -> subres(). %% Substitute every match of the regular expression RegExp with the %% string returned from the function Fun(Match, Acc). Accept pre-parsed %% regular expressions. Acc is an argument to the Fun. The Fun should return %% a tuple {Replacement, NewAcc}. gsub(String, RegExp, Fun, Acc) when is_list(RegExp) -> case re:compile(RegExp) of {ok, RE} -> gsub(String, RE, Fun, Acc); {error, E} -> {error, E} end; gsub(String, RE, Fun, Acc) -> {match, Ss} = re:run(String, RE, [global]), {NewString, NewAcc} = sub_repl(Ss, Fun, Acc, String, 0), {ok, NewString, NewAcc}. % New code that uses fun for finding the replacement. Also uses accumulator % to pass argument between the calls to the fun. sub_repl([[{St, L}] |Ss], Fun, Acc0, S, Pos) -> Match = string:substr(S, St+1, L), {Rep, Acc} = Fun(Match, Acc0), {Rs, NewAcc} = sub_repl(Ss, Fun, Acc, S, St+L), {string:substr(S, Pos+1, St-Pos) ++ Rep ++ Rs, NewAcc}; sub_repl([], _Fun, Acc, S, Pos) -> {string:substr(S, Pos+1), Acc}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Error and warnings %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% local_error(Format, Args) -> io:format("ERROR: " ++ Format ++ "\n", Args), exit(1). warning(Format, Args) -> case get(silent) of true -> ok; _ -> io:format("WARNING: " ++ Format ++ "\n", Args) end. report(Format, Args) -> case get(silent) of true -> ok; _ -> io:format(Format ++ "\n", Args) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Extensions to the 'string' module. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% uc(String) -> lists:reverse(uc(String, [])). uc([], Acc) -> Acc; uc([H | T], Acc) when is_integer(H), [97] =< H, H =< $z -> uc(T, [H - 32 | Acc]); uc([H | T], Acc) -> uc(T, [H | Acc]).