%% ===================================================================== %% This library is free software; you can redistribute it and/or modify %% it under the terms of the GNU Lesser General Public License as %% published by the Free Software Foundation; either version 2 of the %% License, or (at your option) any later version. %% %% This library is distributed in the hope that it will be useful, but %% WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %% Lesser General Public License for more details. %% %% You should have received a copy of the GNU Lesser General Public %% License along with this library; if not, write to the Free Software %% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 %% USA %% %% @copyright 2001-2003 Richard Carlsson %% @author Richard Carlsson <carlsson.richard@gmail.com> %% @see edoc %% @end %% ===================================================================== %% @doc Utility functions for EDoc. -module(edoc_lib). -export([count/2, lines/1, split_at/2, split_at_stop/1, split_at_space/1, filename/1, transpose/1, segment/2, get_first_sentence/1, is_space/1, strip_space/1, parse_expr/2, parse_contact/2, escape_uri/1, join_uri/2, is_relative_uri/1, is_name/1, to_label/1, find_doc_dirs/0, find_sources/2, find_sources/3, find_file/3, try_subdir/2, unique/1, write_file/3, write_file/4, write_file/5, write_info_file/4, read_info_file/1, get_doc_env/1, get_doc_env/4, copy_file/2, uri_get/1, run_doclet/2, run_layout/2, simplify_path/1, timestr/1, datestr/1, read_encoding/2]). -import(edoc_report, [report/2, warning/2]). -include("edoc.hrl"). -include_lib("xmerl/include/xmerl.hrl"). -define(FILE_BASE, "/"). %% --------------------------------------------------------------------- %% List and string utilities %% @private timestr({H,M,Sec}) -> lists:flatten(io_lib:fwrite("~2.2.0w:~2.2.0w:~2.2.0w",[H,M,Sec])). %% @private datestr({Y,M,D}) -> Ms = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], lists:flatten(io_lib:fwrite("~s ~w ~w",[lists:nth(M, Ms),D,Y])). %% @private read_encoding(File, Options) -> case epp:read_encoding(File, Options) of none -> epp:default_encoding(); Encoding -> Encoding end. %% @private count(X, Xs) -> count(X, Xs, 0). count(X, [X | Xs], N) -> count(X, Xs, N + 1); count(X, [_ | Xs], N) -> count(X, Xs, N); count(_X, [], N) -> N. %% @private lines(Cs) -> lines(Cs, [], []). lines([$\n | Cs], As, Ls) -> lines(Cs, [], [lists:reverse(As) | Ls]); lines([C | Cs], As, Ls) -> lines(Cs, [C | As], Ls); lines([], As, Ls) -> lists:reverse([lists:reverse(As) | Ls]). %% @private split_at(Cs, K) -> split_at(Cs, K, []). split_at([K | Cs], K, As) -> {lists:reverse(As), Cs}; split_at([C | Cs], K, As) -> split_at(Cs, K, [C | As]); split_at([], _K, As) -> {lists:reverse(As), []}. %% @private split_at_stop(Cs) -> split_at_stop(Cs, []). split_at_stop([$., $\s | Cs], As) -> {lists:reverse(As), Cs}; split_at_stop([$., $\t | Cs], As) -> {lists:reverse(As), Cs}; split_at_stop([$., $\n | Cs], As) -> {lists:reverse(As), Cs}; split_at_stop([$.], As) -> {lists:reverse(As), []}; split_at_stop([C | Cs], As) -> split_at_stop(Cs, [C | As]); split_at_stop([], As) -> {lists:reverse(As), []}. %% @private split_at_space(Cs) -> split_at_space(Cs, []). split_at_space([$\s | Cs], As) -> {lists:reverse(As), Cs}; split_at_space([$\t | Cs], As) -> {lists:reverse(As), Cs}; split_at_space([$\n | Cs], As) -> {lists:reverse(As), Cs}; split_at_space([C | Cs], As) -> split_at_space(Cs, [C | As]); split_at_space([], As) -> {lists:reverse(As), []}. %% @private is_space([$\s | Cs]) -> is_space(Cs); is_space([$\t | Cs]) -> is_space(Cs); is_space([$\n | Cs]) -> is_space(Cs); is_space([_C | _Cs]) -> false; is_space([]) -> true. %% @private strip_space([$\s | Cs]) -> strip_space(Cs); strip_space([$\t | Cs]) -> strip_space(Cs); strip_space([$\n | Cs]) -> strip_space(Cs); strip_space(Cs) -> Cs. %% @private segment(Es, N) -> segment(Es, [], [], 0, N). segment([E | Es], As, Cs, N, M) when N < M -> segment(Es, [E | As], Cs, N + 1, M); segment([_ | _] = Es, As, Cs, _N, M) -> segment(Es, [], [lists:reverse(As) | Cs], 0, M); segment([], [], Cs, _N, _M) -> lists:reverse(Cs); segment([], As, Cs, _N, _M) -> lists:reverse([lists:reverse(As) | Cs]). %% @private transpose([]) -> []; transpose([[] | Xss]) -> transpose(Xss); transpose([[X | Xs] | Xss]) -> [[X | [H || [H | _T] <- Xss]] | transpose([Xs | [T || [_H | T] <- Xss]])]. %% Note that the parser will not produce two adjacent text segments; %% thus, if a text segment ends with a period character, it marks the %% end of the summary sentence only if it is also the last segment in %% the list, or is followed by a 'p' or 'br' ("whitespace") element. %% @private get_first_sentence([#xmlElement{name = p, content = Es} | _]) -> %% Descend into initial paragraph. get_first_sentence_1(Es); get_first_sentence(Es) -> get_first_sentence_1(Es). get_first_sentence_1([E = #xmlText{value = Txt} | Es]) -> Last = case Es of [#xmlElement{name = p} | _] -> true; [#xmlElement{name = br} | _] -> true; [] -> true; _ -> false end, case end_of_sentence(Txt, Last) of {value, Txt1} -> [E#xmlText{value = Txt1}]; none -> [E | get_first_sentence_1(Es)] end; get_first_sentence_1([E | Es]) -> % Skip non-text segments - don't descend further [E | get_first_sentence_1(Es)]; get_first_sentence_1([]) -> []. end_of_sentence(Cs, Last) -> end_of_sentence(Cs, Last, []). %% We detect '.' and '!' as end-of-sentence markers. end_of_sentence([C=$., $\s | _], _, As) -> end_of_sentence_1(C, true, As); end_of_sentence([C=$., $\t | _], _, As) -> end_of_sentence_1(C, true, As); end_of_sentence([C=$., $\n | _], _, As) -> end_of_sentence_1(C, true, As); end_of_sentence([C=$.], Last, As) -> end_of_sentence_1(C, Last, As); end_of_sentence([C=$!, $\s | _], _, As) -> end_of_sentence_1(C, true, As); end_of_sentence([C=$!, $\t | _], _, As) -> end_of_sentence_1(C, true, As); end_of_sentence([C=$!, $\n | _], _, As) -> end_of_sentence_1(C, true, As); end_of_sentence([C=$!], Last, As) -> end_of_sentence_1(C, Last, As); end_of_sentence([C | Cs], Last, As) -> end_of_sentence(Cs, Last, [C | As]); end_of_sentence([], Last, As) -> end_of_sentence_1($., Last, strip_space(As)). % add a '.' end_of_sentence_1(C, true, As) -> {value, lists:reverse([C | As])}; end_of_sentence_1(_, false, _) -> none. %% For handling ISO 8859-1 (Latin-1) we use the following information: %% %% 000 - 037 NUL - US control %% 040 - 057 SPC - / punctuation %% 060 - 071 0 - 9 digit %% 072 - 100 : - @ punctuation %% 101 - 132 A - Z uppercase %% 133 - 140 [ - ` punctuation %% 141 - 172 a - z lowercase %% 173 - 176 { - ~ punctuation %% 177 DEL control %% 200 - 237 control %% 240 - 277 NBSP - ¿ punctuation %% 300 - 326 À - Ö uppercase %% 327 × punctuation %% 330 - 336 Ø - Þ uppercase %% 337 - 366 ß - ö lowercase %% 367 ÷ punctuation %% 370 - 377 ø - ÿ lowercase %% Names must begin with a lowercase letter and contain only %% alphanumerics and underscores. %% @private is_name([C | Cs]) when C >= $a, C =< $z -> is_name_1(Cs); is_name([C | Cs]) when C >= $\337, C =< $\377, C =/= $\367 -> is_name_1(Cs); is_name(_) -> false. is_name_1([C | Cs]) when C >= $a, C =< $z -> is_name_1(Cs); is_name_1([C | Cs]) when C >= $A, C =< $Z -> is_name_1(Cs); is_name_1([C | Cs]) when C >= $0, C =< $9 -> is_name_1(Cs); is_name_1([C | Cs]) when C >= $\300, C =< $\377, C =/= $\327, C =/= $\367 -> is_name_1(Cs); is_name_1([$_ | Cs]) -> is_name_1(Cs); is_name_1([]) -> true; is_name_1(_) -> false. to_atom(A) when is_atom(A) -> A; to_atom(S) when is_list(S) -> list_to_atom(S). to_list(A) when is_atom(A) -> atom_to_list(A); to_list(S) when is_list(S) -> S. %% @private unique([X | Xs]) -> [X | unique(Xs, X)]; unique([]) -> []. unique([X | Xs], X) -> unique(Xs, X); unique([X | Xs], _) -> [X | unique(Xs, X)]; unique([], _) -> []. %% --------------------------------------------------------------------- %% Parsing utilities %% @doc EDoc Erlang expression parsing. For parsing things like the %% content of <a href="overview-summary.html#ftag-equiv">`@equiv'</a> %% tags, and strings denoting file names, e.g. in @headerfile. Also used %% by {@link edoc_run}. %% @private parse_expr(S, L) -> case erl_scan:string(S ++ ".", L) of {ok, Ts, _} -> case erl_parse:parse_exprs(Ts) of {ok, [Expr]} -> Expr; {error, {999999, erl_parse, _}} -> throw_error(eof, L); {error, E} -> throw_error(E, L) end; {error, E, _} -> throw_error(E, L) end. %% @doc EDoc "contact information" parsing. This is the type of the %% content in e.g. %% <a href="overview-summary.html#mtag-author">`@author'</a> tags. %% @private %% % @type info() = #info{name = string(), %% % email = string(), %% % uri = string()} -record(info, {name = "" :: string(), email = "" :: string(), uri = "" :: string()}). parse_contact(S, L) -> I = scan_name(S, L, #info{}, []), {I#info.name, I#info.email, I#info.uri}. %% The name is taken as the first non-whitespace-only string before, %% between, or following the e-mail/URI sections. Subsequent text that %% is not e/mail or URI is ignored. scan_name([$< | Cs], L, I, As) -> case I#info.email of "" -> {Cs1, I1} = scan_email(Cs, L, set_name(I, As), []), scan_name(Cs1, L, I1, []); _ -> throw_error("multiple '<...>' sections.", L) end; scan_name([$[ | Cs], L, I, As) -> case I#info.uri of "" -> {Cs1, I1} = scan_uri(Cs, L, set_name(I, As), []), scan_name(Cs1, L, I1, []); _ -> throw_error("multiple '[...]' sections.", L) end; scan_name([$\n | Cs], L, I, As) -> scan_name(Cs, L + 1, I, [$\n | As]); scan_name([C | Cs], L, I, As) -> scan_name(Cs, L, I, [C | As]); scan_name([], _L, I, As) -> set_name(I, As). scan_uri([$] | Cs], _L, I, As) -> {Cs, I#info{uri = strip_and_reverse(As)}}; scan_uri([$\n | Cs], L, I, As) -> scan_uri(Cs, L + 1, I, [$\n | As]); scan_uri([C | Cs], L, I, As) -> scan_uri(Cs, L, I, [C | As]); scan_uri([], L, _I, _As) -> throw_error({missing, $]}, L). scan_email([$> | Cs], _L, I, As) -> {Cs, I#info{email = strip_and_reverse(As)}}; scan_email([$\n | Cs], L, I, As) -> scan_email(Cs, L + 1, I, [$\n | As]); scan_email([C | Cs], L, I, As) -> scan_email(Cs, L, I, [C | As]); scan_email([], L, _I, _As) -> throw_error({missing, $>}, L). set_name(I, As) -> case I#info.name of "" -> I#info{name = strip_and_reverse(As)}; _ -> I end. strip_and_reverse(As) -> edoc_lib:strip_space(lists:reverse(edoc_lib:strip_space(As))). %% --------------------------------------------------------------------- %% URI and Internet %% This is a conservative URI escaping, which escapes anything that may %% not appear in an NMTOKEN ([a-zA-Z0-9]|'.'|'-'|'_'), including ':'. %% Characters are first encoded in UTF-8. %% %% Note that this should *not* be applied to complete URI, but only to %% segments that may need escaping, when forming a complete URI. %% %% TODO: general utf-8 encoding for all of Unicode (0-16#10ffff) %% @private escape_uri([C | Cs]) when C >= $a, C =< $z -> [C | escape_uri(Cs)]; escape_uri([C | Cs]) when C >= $A, C =< $Z -> [C | escape_uri(Cs)]; escape_uri([C | Cs]) when C >= $0, C =< $9 -> [C | escape_uri(Cs)]; escape_uri([C = $. | Cs]) -> [C | escape_uri(Cs)]; escape_uri([C = $- | Cs]) -> [C | escape_uri(Cs)]; escape_uri([C = $_ | Cs]) -> [C | escape_uri(Cs)]; escape_uri([C | Cs]) when C > 16#7f -> %% This assumes that characters are at most 16 bits wide. escape_byte(((C band 16#c0) bsr 6) + 16#c0) ++ escape_byte(C band 16#3f + 16#80) ++ escape_uri(Cs); escape_uri([C | Cs]) -> escape_byte(C) ++ escape_uri(Cs); escape_uri([]) -> []. escape_byte(C) when C >= 0, C =< 255 -> [$%, hex_digit(C bsr 4), hex_digit(C band 15)]. hex_digit(N) when N >= 0, N =< 9 -> N + $0; hex_digit(N) when N > 9, N =< 15 -> N + $a - 10. % utf8([C | Cs]) when C > 16#7f -> % [((C band 16#c0) bsr 6) + 16#c0, C band 16#3f ++ 16#80 | utf8(Cs)]; % utf8([C | Cs]) -> % [C | utf8(Cs)]; % utf8([]) -> % []. %% Please note that URI are *not* file names. Don't use the stdlib %% 'filename' module for operations on (any parts of) URI. %% @private join_uri(Base, "") -> Base; join_uri("", Path) -> Path; join_uri(Base, Path) -> Base ++ "/" ++ Path. %% Check for relative URI; "network paths" ("//...") not included! %% @private is_relative_uri([$: | _]) -> false; is_relative_uri([$/, $/ | _]) -> false; is_relative_uri([$/ | _]) -> true; is_relative_uri([$? | _]) -> true; is_relative_uri([$# | _]) -> true; is_relative_uri([_ | Cs]) -> is_relative_uri(Cs); is_relative_uri([]) -> true. %% @private uri_get("file:///" ++ Path) -> uri_get_file(Path); uri_get("file://localhost/" ++ Path) -> uri_get_file(Path); uri_get("file://" ++ Path) -> Msg = io_lib:format("cannot handle 'file:' scheme with " "nonlocal network-path: 'file://~ts'.", [Path]), {error, Msg}; uri_get("file:/" ++ Path) -> uri_get_file(Path); uri_get("file:" ++ Path) -> Msg = io_lib:format("ignoring malformed URI: 'file:~ts'.", [Path]), {error, Msg}; uri_get("http:" ++ Path) -> uri_get_http("http:" ++ Path); uri_get("ftp:" ++ Path) -> uri_get_ftp("ftp:" ++ Path); uri_get("//" ++ Path) -> Msg = io_lib:format("cannot access network-path: '//~ts'.", [Path]), {error, Msg}; uri_get([C, $:, $/ | _]=Path) when C >= $A, C =< $Z; C >= $a, C =< $z -> uri_get_file(Path); % special case for Windows uri_get([C, $:, $\ | _]=Path) when C >= $A, C =< $Z; C >= $a, C =< $z -> uri_get_file(Path); % special case for Windows uri_get(URI) -> case is_relative_uri(URI) of true -> uri_get_file(URI); false -> Msg = io_lib:format("cannot handle URI: '~ts'.", [URI]), {error, Msg} end. uri_get_file(File0) -> File = filename:join(?FILE_BASE, File0), case read_file(File) of {ok, Text} -> {ok, Text}; {error, R} -> {error, file:format_error(R)} end. uri_get_http(URI) -> %% Try using option full_result=false case catch {ok, httpc:request(get, {URI,[]}, [], [{full_result, false}])} of {'EXIT', _} -> uri_get_http_r10(URI); Result -> uri_get_http_1(Result, URI) end. uri_get_http_r10(URI) -> %% Try most general form of request Result = (catch {ok, httpc:request(get, {URI,[]}, [], [])}), uri_get_http_1(Result, URI). uri_get_http_1(Result, URI) -> case Result of {ok, {ok, {200, Text}}} when is_list(Text) -> %% new short result format {ok, Text}; {ok, {ok, {Status, Text}}} when is_integer(Status), is_list(Text) -> %% new short result format when status /= 200 Phrase = httpd_util:reason_phrase(Status), {error, http_errmsg(Phrase, URI)}; {ok, {ok, {{_Vsn, 200, _Phrase}, _Hdrs, Text}}} when is_list(Text) -> %% new long result format {ok, Text}; {ok, {ok, {{_Vsn, _Status, Phrase}, _Hdrs, Text}}} when is_list(Text) -> %% new long result format when status /= 200 {error, http_errmsg(Phrase, URI)}; {ok, {200,_Hdrs,Text}} when is_list(Text) -> %% old result format {ok, Text}; {ok, {Status,_Hdrs,Text}} when is_list(Text) -> %% old result format when status /= 200 Phrase = httpd_util:reason_phrase(Status), {error, http_errmsg(Phrase, URI)}; {ok, {error, R}} -> Reason = inet:format_error(R), {error, http_errmsg(Reason, URI)}; {ok, R} -> Reason = io_lib:format("bad return value ~P", [R, 5]), {error, http_errmsg(Reason, URI)}; {'EXIT', R} -> Reason = io_lib:format("crashed with reason ~w", [R]), {error, http_errmsg(Reason, URI)}; R -> Reason = io_lib:format("uncaught throw: ~w", [R]), {error, http_errmsg(Reason, URI)} end. http_errmsg(Reason, URI) -> io_lib:format("http error: ~ts: '~ts'", [Reason, URI]). %% TODO: implement ftp access method uri_get_ftp(URI) -> Msg = io_lib:format("cannot access ftp scheme yet: '~ts'.", [URI]), {error, Msg}. %% @private to_label([$\s | Cs]) -> to_label(Cs); to_label([$\t | Cs]) -> to_label(Cs); to_label([$\n | Cs]) -> to_label(Cs); to_label([]) -> []; to_label(Cs) -> to_label_1(Cs). to_label_1([$\s | Cs]) -> to_label_2([$\s | Cs]); to_label_1([$\t | Cs]) -> to_label_2([$\s | Cs]); to_label_1([$\n | Cs]) -> to_label_2([$\s | Cs]); to_label_1([C | Cs]) -> [C | to_label_1(Cs)]; to_label_1([]) -> []. to_label_2(Cs) -> case to_label(Cs) of [] -> []; Cs1 -> [$_ | Cs1] end. %% --------------------------------------------------------------------- %% Files %% @private filename([C | T]) when is_integer(C), C > 0 -> [C | filename(T)]; filename([H|T]) -> filename(H) ++ filename(T); filename([]) -> []; filename(N) when is_atom(N) -> atom_to_list(N); filename(N) -> report("bad filename: `~P'.", [N, 25]), exit(error). %% @private copy_file(From, To) -> case file:copy(From, To) of {ok, _} -> ok; {error, R} -> R1 = file:format_error(R), report("error copying '~ts' to '~ts': ~ts.", [From, To, R1]), exit(error) end. list_dir(Dir, Error) -> case file:list_dir(Dir) of {ok, Fs} -> Fs; {error, R} -> F = case Error of %% true -> %% fun (S, As) -> report(S, As), exit(error) end; false -> fun (S, As) -> warning(S, As), [] end end, R1 = file:format_error(R), F("could not read directory '~ts': ~ts.", [filename(Dir), R1]) end. %% @private simplify_path(P) -> case filename:basename(P) of "." -> simplify_path(filename:dirname(P)); ".." -> simplify_path(filename:dirname(filename:dirname(P))); _ -> P end. %% The directories From and To are assumed to exist. %% copy_dir(From, To) -> %% Es = list_dir(From, true), % error if listing fails %% lists:foreach(fun (E) -> copy_dir(From, To, E) end, Es). %% copy_dir(From, To, Entry) -> %% From1 = filename:join(From, Entry), %% To1 = filename:join(To, Entry), %% case filelib:is_dir(From1) of %% true -> %% make_dir(To1), %% copy_dir(From1, To1); %% false -> %% copy_file(From1, To1) %% end. %% make_dir(Dir) -> %% case file:make_dir(Dir) of %% ok -> ok; %% {error, R} -> %% R1 = file:format_error(R), %% report("cannot create directory '~ts': ~ts.", [Dir, R1]), %% exit(error) %% end. %% @private try_subdir(Dir, Subdir) -> D = filename:join(Dir, Subdir), case filelib:is_dir(D) of true -> D; false -> Dir end. %% @spec (Text::deep_string(), Dir::edoc:filename(), %% Name::edoc:filename()) -> ok %% %% @doc Write the given `Text' to the file named by `Name' in directory %% `Dir'. If the target directory does not exist, it will be created. %% @private write_file(Text, Dir, Name) -> write_file(Text, Dir, Name, ''). %% @spec (Text::deep_string(), Dir::edoc:filename(), %% Name::edoc:filename(), Package::atom()|string()) -> ok %% @doc Like {@link write_file/3}, but adds path components to the target %% directory corresponding to the specified package. %% @private write_file(Text, Dir, Name, Package) -> write_file(Text, Dir, Name, Package, [{encoding,latin1}]). write_file(Text, Dir, Name, Package, Options) -> File = filename:join([Dir, to_list(Package), Name]), ok = filelib:ensure_dir(File), case file:open(File, [write] ++ Options) of {ok, FD} -> io:put_chars(FD, Text), ok = file:close(FD); {error, R} -> R1 = file:format_error(R), report("could not write file '~ts': ~ts.", [File, R1]), exit(error) end. %% @private write_info_file(App, Packages, Modules, Dir) -> Ts = [{packages, Packages}, {modules, Modules}], Ts1 = if App =:= ?NO_APP -> Ts; true -> [{application, App} | Ts] end, S0 = [io_lib:fwrite("~p.\n", [T]) || T <- Ts1], S = ["%% encoding: UTF-8\n" | S0], write_file(S, Dir, ?INFO_FILE, '', [{encoding,unicode}]). %% @spec (Name::edoc:filename()) -> {ok, string()} | {error, Reason} %% %% @doc Reads text from the file named by `Name'. read_file(File) -> case file:read_file(File) of {ok, Bin} -> Enc = edoc_lib:read_encoding(File, []), case catch unicode:characters_to_list(Bin, Enc) of String when is_list(String) -> {ok, String}; _ -> {error, invalid_unicode} end; {error, Reason} -> {error, Reason} end. %% --------------------------------------------------------------------- %% Info files info_file_data(Ts) -> App = proplists:get_value(application, Ts, ?NO_APP), Ps = proplists:append_values(packages, Ts), Ms = proplists:append_values(modules, Ts), {App, Ps, Ms}. %% Local file access - don't complain if file does not exist. %% @private read_info_file(Dir) -> File = filename:join(Dir, ?INFO_FILE), case filelib:is_file(File) of true -> case read_file(File) of {ok, Text} -> parse_info_file(Text, File); {error, R} -> R1 = file:format_error(R), warning("could not read '~ts': ~ts.", [File, R1]), {?NO_APP, [], []} end; false -> {?NO_APP, [], []} end. %% URI access uri_get_info_file(Base) -> URI = join_uri(Base, ?INFO_FILE), case uri_get(URI) of {ok, Text} -> parse_info_file(Text, URI); {error, Msg} -> warning("could not read '~ts': ~ts.", [URI, Msg]), {?NO_APP, [], []} end. parse_info_file(Text, Name) -> case parse_terms(Text) of {ok, Vs} -> info_file_data(Vs); {error, eof} -> warning("unexpected end of file in '~ts'.", [Name]), {?NO_APP, [], []}; {error, {_Line,Module,R}} -> warning("~ts: ~ts.", [Module:format_error(R), Name]), {?NO_APP, [], []} end. parse_terms(Text) -> case erl_scan:string(Text) of {ok, Ts, _Line} -> parse_terms_1(Ts, [], []); {error, R, _Line} -> {error, R} end. parse_terms_1([T={dot, _L} | Ts], As, Vs) -> case erl_parse:parse_term(lists:reverse([T | As])) of {ok, V} -> parse_terms_1(Ts, [], [V | Vs]); {error, R} -> {error, R} end; parse_terms_1([T | Ts], As, Vs) -> parse_terms_1(Ts, [T | As], Vs); parse_terms_1([], [], Vs) -> {ok, lists:reverse(Vs)}; parse_terms_1([], _As, _Vs) -> {error, eof}. %% --------------------------------------------------------------------- %% Source files and packages %% @private find_sources(Path, Opts) -> find_sources(Path, "", Opts). %% @doc See {@link edoc:run/3} for a description of the options %% `subpackages', `source_suffix' and `exclude_packages'. %% @private %% NEW-OPTIONS: subpackages, source_suffix, exclude_packages %% DEFER-OPTIONS: edoc:run/3 find_sources(Path, Pkg, Opts) -> Rec = proplists:get_bool(subpackages, Opts), Ext = proplists:get_value(source_suffix, Opts, ?DEFAULT_SOURCE_SUFFIX), find_sources(Path, Pkg, Rec, Ext, Opts). find_sources(Path, Pkg, Rec, Ext, Opts) -> Skip = proplists:get_value(exclude_packages, Opts, []), lists:flatten(find_sources_1(Path, to_atom(Pkg), Rec, Ext, Skip)). find_sources_1([P | Ps], Pkg, Rec, Ext, Skip) -> Dir = filename:join(P, atom_to_list(Pkg)), Fs1 = find_sources_1(Ps, Pkg, Rec, Ext, Skip), case filelib:is_dir(Dir) of true -> [find_sources_2(Dir, Pkg, Rec, Ext, Skip) | Fs1]; false -> Fs1 end; find_sources_1([], _Pkg, _Rec, _Ext, _Skip) -> []. find_sources_2(Dir, Pkg, Rec, Ext, Skip) -> case lists:member(Pkg, Skip) of false -> Es = list_dir(Dir, false), % just warn if listing fails Es1 = [{Pkg, E, Dir} || E <- Es, is_source_file(E, Ext)], case Rec of true -> [find_sources_3(Es, Dir, Pkg, Rec, Ext, Skip) | Es1]; false -> Es1 end; true -> [] end. find_sources_3(Es, Dir, Pkg, Rec, Ext, Skip) -> [find_sources_2(filename:join(Dir, E), to_atom(join(Pkg, E)), Rec, Ext, Skip) || E <- Es, is_package_dir(E, Dir)]. join('', E) -> E; join(Pkg, E) -> filename:join(Pkg, E). is_source_file(Name, Ext) -> (filename:extension(Name) == Ext) andalso is_name(filename:rootname(Name, Ext)). is_package_dir(Name, Dir) -> is_name(filename:rootname(filename:basename(Name))) andalso filelib:is_dir(filename:join(Dir, Name)). %% @private find_file([P | Ps], []=Pkg, Name) -> Pkg = [], File = filename:join(P, Name), case filelib:is_file(File) of true -> File; false -> find_file(Ps, Pkg, Name) end; find_file([], [], _Name) -> "". %% @private find_doc_dirs() -> find_doc_dirs(code:get_path()). find_doc_dirs([P0 | Ps]) -> P = filename:absname(P0), P1 = case filename:basename(P) of ?EBIN_DIR -> filename:dirname(P); _ -> P end, Dir = try_subdir(P1, ?EDOC_DIR), File = filename:join(Dir, ?INFO_FILE), case filelib:is_file(File) of true -> [Dir | find_doc_dirs(Ps)]; false -> find_doc_dirs(Ps) end; find_doc_dirs([]) -> []. %% All names with "internal linkage" are mapped to the empty string, so %% that relative references will be created. For apps, the empty string %% implies that we use the default app-path. %% NEW-OPTIONS: doc_path %% DEFER-OPTIONS: get_doc_env/4 get_doc_links(App, Packages, Modules, Opts) -> Path = proplists:append_values(doc_path, Opts) ++ find_doc_dirs(), Ds = [{P, uri_get_info_file(P)} || P <- Path], Ds1 = [{"", {App, Packages, Modules}} | Ds], D = dict:new(), make_links(Ds1, D, D, D). make_links([{Dir, {App, Ps, Ms}} | Ds], A, P, M) -> A1 = if App == ?NO_APP -> A; true -> add_new(App, Dir, A) end, F = fun (K, D) -> add_new(K, Dir, D) end, P1 = lists:foldl(F, P, Ps), M1 = lists:foldl(F, M, Ms), make_links(Ds, A1, P1, M1); make_links([], A, P, M) -> F = fun (D) -> fun (K) -> case dict:find(K, D) of {ok, V} -> V; error -> "" end end end, {F(A), F(P), F(M)}. add_new(K, V, D) -> case dict:is_key(K, D) of true -> D; false -> dict:store(K, V, D) end. %% @spec (Options::proplist()) -> edoc_env() %% @equiv get_doc_env([], [], [], Opts) %% @private get_doc_env(Opts) -> get_doc_env([], [], [], Opts). %% @spec (App, Packages, Modules, Options::proplist()) -> edoc_env() %% App = [] | atom() %% Packages = [atom()] %% Modules = [atom()] %% proplist() = [term()] %% %% @type proplist() = proplists:property(). %% @type edoc_env(). Environment information needed by EDoc for %% generating references. The data representation is not documented. %% %% @doc Creates an environment data structure used by parts of EDoc for %% generating references, etc. See {@link edoc:run/3} for a description %% of the options `file_suffix', `app_default' and `doc_path'. %% %% @see edoc_extract:source/4 %% @see edoc:get_doc/3 %% NEW-OPTIONS: file_suffix, app_default %% INHERIT-OPTIONS: get_doc_links/4 %% DEFER-OPTIONS: edoc:run/3 get_doc_env(App, Packages, Modules, Opts) -> Suffix = proplists:get_value(file_suffix, Opts, ?DEFAULT_FILE_SUFFIX), AppDefault = proplists:get_value(app_default, Opts, ?APP_DEFAULT), Includes = proplists:append_values(includes, Opts), {A, P, M} = get_doc_links(App, Packages, Modules, Opts), #env{file_suffix = Suffix, package_summary = ?PACKAGE_SUMMARY ++ Suffix, apps = A, packages = P, modules = M, app_default = AppDefault, includes = Includes }. %% --------------------------------------------------------------------- %% Plug-in modules %% @doc See {@link edoc:run/3} for a description of the `doclet' option. %% NEW-OPTIONS: doclet %% DEFER-OPTIONS: edoc:run/3 %% @private run_doclet(Fun, Opts) -> run_plugin(doclet, ?DEFAULT_DOCLET, Fun, Opts). %% @doc See {@link edoc:layout/2} for a description of the `layout' %% option. %% NEW-OPTIONS: layout %% DEFER-OPTIONS: edoc:layout/2 %% @private run_layout(Fun, Opts) -> run_plugin(layout, ?DEFAULT_LAYOUT, Fun, Opts). run_plugin(Name, Default, Fun, Opts) -> run_plugin(Name, Name, Default, Fun, Opts). run_plugin(Name, Key, Default, Fun, Opts) when is_atom(Name) -> Module = get_plugin(Key, Default, Opts), case catch {ok, Fun(Module)} of {ok, Value} -> Value; R -> report("error in ~ts '~w': ~P.", [Name, Module, R, 20]), exit(error) end. get_plugin(Key, Default, Opts) -> case proplists:get_value(Key, Opts, Default) of M when is_atom(M) -> M; Other -> report("bad value for option '~w': ~P.", [Key, Other, 10]), exit(error) end. %% --------------------------------------------------------------------- %% Error handling -type line() :: erl_scan:line(). -type err() :: 'eof' | {'missing', char()} | {line(), atom(), string()} | string(). -spec throw_error(err(), line()) -> no_return(). throw_error({missing, C}, L) -> throw_error({"missing '~c'.", [C]}, L); throw_error(eof, L) -> throw({error,L,"unexpected end of expression."}); throw_error({L, M, D}, _L) -> throw({error,L,{format_error,M,D}}); throw_error(D, L) -> throw({error, L, D}).