aboutsummaryrefslogtreecommitdiffstats
path: root/lib/edoc/src/edoc_lib.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/edoc/src/edoc_lib.erl')
-rw-r--r--lib/edoc/src/edoc_lib.erl998
1 files changed, 998 insertions, 0 deletions
diff --git a/lib/edoc/src/edoc_lib.erl b/lib/edoc/src/edoc_lib.erl
new file mode 100644
index 0000000000..47e61f7932
--- /dev/null
+++ b/lib/edoc/src/edoc_lib.erl
@@ -0,0 +1,998 @@
+%% =====================================================================
+%% 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
+%%
+%% $Id$
+%%
+%% @private
+%% @copyright 2001-2003 Richard Carlsson
+%% @author Richard Carlsson <[email protected]>
+%% @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_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]).
+
+-import(edoc_report, [report/2, warning/2]).
+
+-include("edoc.hrl").
+-include("xmerl.hrl").
+
+-define(FILE_BASE, "/").
+
+
+%% ---------------------------------------------------------------------
+%% List and string utilities
+
+timestr({H,M,Sec}) ->
+ lists:flatten(io_lib:fwrite("~2.2.0w:~2.2.0w:~2.2.0w",[H,M,Sec])).
+
+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])).
+
+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.
+
+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]).
+
+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), []}.
+
+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), []}.
+
+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), []}.
+
+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.
+
+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.
+
+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]).
+
+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.
+
+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.
+
+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).
+
+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}.
+
+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.
+
+%% @type info() = #info{name = string(),
+%% mail = string(),
+%% uri = string()}
+
+-record(info, {name = "", email = "", uri = ""}).
+
+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)
+
+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) ->
+ "%" ++ hex_octet(C).
+
+% 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([]) ->
+% [].
+
+hex_octet(N) when N =< 9 ->
+ [$0 + N];
+hex_octet(N) when N > 15 ->
+ hex_octet(N bsr 4) ++ hex_octet(N band 15);
+hex_octet(N) ->
+ [N - 10 + $a].
+
+%% Please note that URI are *not* file names. Don't use the stdlib
+%% 'filename' module for operations on (any parts of) URI.
+
+join_uri(Base, "") ->
+ Base;
+join_uri("", Path) ->
+ Path;
+join_uri(Base, Path) ->
+ Base ++ "/" ++ Path.
+
+%% Check for relative URI; "network paths" ("//...") not included!
+
+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.
+
+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://~s'.",
+ [Path]),
+ {error, Msg};
+uri_get("file:/" ++ Path) ->
+ uri_get_file(Path);
+uri_get("file:" ++ Path) ->
+ Msg = io_lib:format("ignoring malformed URI: 'file:~s'.", [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: '//~s'.", [Path]),
+ {error, Msg};
+uri_get(URI) ->
+ case is_relative_uri(URI) of
+ true ->
+ uri_get_file(URI);
+ false ->
+ Msg = io_lib:format("cannot handle URI: '~s'.", [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, http: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, http: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: ~s: '~s'", [Reason, URI]).
+
+%% TODO: implement ftp access method
+
+uri_get_ftp(URI) ->
+ Msg = io_lib:format("cannot access ftp scheme yet: '~s'.", [URI]),
+ {error, Msg}.
+
+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
+
+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).
+
+copy_file(From, To) ->
+ case file:copy(From, To) of
+ {ok, _} -> ok;
+ {error, R} ->
+ R1 = file:format_error(R),
+ report("error copying '~s' to '~s': ~s.", [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 '~s': ~s.", [filename(Dir), R1])
+ end.
+
+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 '~s': ~s.", [Dir, R1]),
+%% exit(error)
+%% end.
+
+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.
+
+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.
+
+write_file(Text, Dir, Name, Package) ->
+ Dir1 = filename:join([Dir | packages:split(Package)]),
+ File = filename:join(Dir1, Name),
+ ok = filelib:ensure_dir(File),
+ case file:open(File, [write]) of
+ {ok, FD} ->
+ io:put_chars(FD, Text),
+ ok = file:close(FD);
+ {error, R} ->
+ R1 = file:format_error(R),
+ report("could not write file '~s': ~s.", [File, R1]),
+ exit(error)
+ end.
+
+write_info_file(App, Packages, Modules, Dir) ->
+ Ts = [{packages, Packages},
+ {modules, Modules}],
+ Ts1 = if App =:= ?NO_APP -> Ts;
+ true -> [{application, App} | Ts]
+ end,
+ S = [io_lib:fwrite("~p.\n", [T]) || T <- Ts1],
+ write_file(S, Dir, ?INFO_FILE).
+
+%% @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} -> {ok, binary_to_list(Bin)};
+ {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.
+
+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 '~s': ~s.", [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 '~s': ~s.", [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 '~s'.", [Name]),
+ {?NO_APP, [], []};
+ {error, {_Line,Module,R}} ->
+ warning("~s: ~s.", [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
+
+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'.
+
+%% 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, filename:join(packages:split(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(packages:concat(Pkg, E)), Rec, Ext, Skip)
+ || E <- Es, is_package_dir(E, Dir)].
+
+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)).
+
+find_file([P | Ps], Pkg, Name) ->
+ Dir = filename:join(P, filename:join(packages:split(Pkg))),
+ File = filename:join(Dir, Name),
+ case filelib:is_file(File) of
+ true ->
+ File;
+ false ->
+ find_file(Ps, Pkg, Name)
+ end;
+find_file([], _Pkg, _Name) ->
+ "".
+
+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)
+
+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 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
+
+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
+
+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 ~s '~w': ~W.", [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
+
+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}).