diff options
Diffstat (limited to 'lib/edoc/src/edoc_macros.erl')
-rw-r--r-- | lib/edoc/src/edoc_macros.erl | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/lib/edoc/src/edoc_macros.erl b/lib/edoc/src/edoc_macros.erl new file mode 100644 index 0000000000..2874e2940c --- /dev/null +++ b/lib/edoc/src/edoc_macros.erl @@ -0,0 +1,327 @@ +%% ===================================================================== +%% 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-2005 Richard Carlsson +%% @author Richard Carlsson <[email protected]> +%% @see edoc +%% @end +%% ===================================================================== + +%% @doc EDoc macro expansion + +-module(edoc_macros). + +-export([expand_tags/3, std_macros/1, check_defs/1]). + +-import(edoc_report, [report/2, error/3, warning/4]). + +-include("edoc.hrl"). +-include("edoc_types.hrl"). + +-define(DEFAULT_XML_EXPORT, xmerl_html). + + +std_macros(Env) -> + (if Env#env.module =:= [] -> []; + true -> [{module, atom_to_list(Env#env.module)}] + end + ++ + if Env#env.package =:= [] -> []; + true -> [{package, atom_to_list(Env#env.package)}] + end + ++ + [{date, fun date_macro/3}, + {docRoot, Env#env.root}, + {link, fun link_macro/3}, + {section, fun section_macro/3}, + {time, fun time_macro/3}, + {type, fun type_macro/3}, + {version, fun version_macro/3}]). + + +%% Check well-formedness of user-specified list of macro definitions. + +check_defs([{K, D} | Ds]) when is_atom(K), is_list(D) -> + check_defs(Ds); +check_defs([X | _Ds]) -> + report("bad macro definition: ~P.", [X, 10]), + exit(error); +check_defs([]) -> + ok. + +%% Code for special macros should throw {error, Line, Reason} for error +%% reporting, where Reason and Line are passed to edoc_report:error(...) +%% together with the file name etc. The expanded text must be flat! + +date_macro(_S, _Line, _Env) -> + edoc_lib:datestr(date()). + +time_macro(_S, _Line, _Env) -> + edoc_lib:timestr(time()). + +version_macro(S, Line, Env) -> + date_macro(S, Line, Env) + ++ " " ++ time_macro(S, Line, Env). + +link_macro(S, Line, Env) -> + {S1, S2} = edoc_lib:split_at_stop(S), + Ref = edoc_parser:parse_ref(S1, Line), + URI = edoc_refs:get_uri(Ref, Env), + Txt = if S2 =:= [] -> "<code>" ++ S1 ++ "</code>"; + true -> S2 + end, + Target = case edoc_refs:is_top(Ref, Env) of + true -> " target=\"_top\""; % note the initial space + false -> "" + end, + lists:flatten(io_lib:fwrite("<a href=\"~s\"~s>~s</a>", + [URI, Target, Txt])). + +section_macro(S, _Line, _Env) -> + S1 = lists:reverse(edoc_lib:strip_space( + lists:reverse(edoc_lib:strip_space(S)))), + lists:flatten(io_lib:format("<a href=\"#~s\">~s</a>", + [edoc_lib:to_label(S1), S1])). + +type_macro(S, Line, Env) -> + S1 = "t()=" ++ S, + Def = edoc_parser:parse_typedef(S1, Line), + {#t_typedef{type = T}, _} = Def, + Txt = edoc_layout:type(edoc_data:type(T, Env)), + lists:flatten(io_lib:fwrite("<code>~s</code>", [Txt])). + + +%% Expand inline macros in tag content. + +expand_tags(Ts, Env, Where) -> + Defs = dict:from_list(lists:reverse(Env#env.macros)), + expand_tags(Ts, Defs, Env, Where). + +expand_tags([#tag{data = Cs, line = L} = T | Ts], Defs, Env, Where) -> + [T#tag{data = expand_tag(Cs, L, Defs, Env, Where)} + | expand_tags(Ts, Defs, Env, Where)]; +expand_tags([T | Ts], Defs, Env, Where) -> + [T | expand_tags(Ts, Defs, Env, Where)]; +expand_tags([], _, _, _) -> + []. + +expand_tag(Cs, L, Defs, Env, Where) -> + case catch {ok, expand_text(Cs, L, Defs, Env, Where)} of + {ok, Cs1} -> + lists:reverse(Cs1); + {'EXIT', R} -> + exit(R); + {error, L1, Error} -> + error(L1, Where, Error), + exit(error); + Other -> + throw(Other) + end. + +%% Expand macros in arbitrary lines of text. +%% The result is in reverse order. + +-record(state, {where, env, seen}). + +expand_text(Cs, L, Defs, Env, Where) -> + St = #state{where = Where, + env = Env, + seen = sets:new()}, + expand(Cs, L, Defs, St, []). + +%% Inline macro syntax: "{@name content}" +%% where 'content' is optional, and separated from 'name' by one or +%% more whitespace characters. The content is bound to the '{@?}' +%% parameter variable, and the macro definition is expanded and +%% substituted for the call. Recursion is detected and reported as an +%% error, since there are (currently) no control flow operators. +%% Escape sequences: +%% "@{" -> "{" +%% "@}" -> "}" +%% "@@" -> "@" + +expand([$@, $@ | Cs], L, Defs, St, As) -> + expand(Cs, L, Defs, St, [$@ | As]); +expand([$@, ${ | Cs], L, Defs, St, As) -> + expand(Cs, L, Defs, St, [${ | As]); +expand([$@, $} | Cs], L, Defs, St, As) -> + expand(Cs, L, Defs, St, [$} | As]); +expand([${, $@ | Cs], L, Defs, St, As) -> + expand_macro(Cs, L, Defs, St, As); +expand([$\n = C | Cs], L, Defs, St, As) -> + expand(Cs, L + 1, Defs, St, [C | As]); +expand([C | Cs], L, Defs, St, As) -> + expand(Cs, L, Defs, St, [C | As]); +expand([], _, _, _, As) -> + As. + +expand_macro(Cs, L, Defs, St, As) -> + {M, Cs1, L1} = macro_name(Cs, L), + {Arg, Cs2, L2} = macro_content(Cs1, L1), + As1 = expand_macro_def(M, Arg, L, Defs, St, As), + expand(Cs2, L2, Defs, St, As1). + +%% The macro argument (the "content") is expanded in the environment of +%% the call, and the result is bound to the '{@?}' parameter. The result +%% of the macro expansion is then expanded again. This allows macro +%% definitions to contain calls to other macros, avoids name capture of +%% '{@?}', and makes it easier to write handler functions for special +%% macros such as '{@link ...}', since the argument is already expanded. + +expand_macro_def(M, Arg, L, Defs, St, As) -> + Seen = St#state.seen, + case sets:is_element(M, Seen) of + true -> + throw_error(L, {"recursive macro expansion of {@~s}.", + [M]}); + false -> + Arg1 = lists:reverse(expand(Arg, L, Defs, St, [])), + Defs1 = dict:store('?', Arg1, Defs), + St1 = St#state{seen = sets:add_element(M, Seen)}, + case dict:find(M, Defs) of + {ok, Def} -> + Txt = if is_function(Def) -> + Def(Arg1, L, St1#state.env); + is_list(Def) -> + Def + end, + expand(Txt, L, Defs1, St1, As); + error -> + warning(L, St1#state.where, + "undefined macro {@~s}.", [M]), + "??" + end + end. + +%% The macro name ends at the first whitespace or '}' character. The +%% content, if any, starts at the next non-whitespace character. + +%% See edoc_tags:scan_tag/is_name/1 for details on what is a valid +%% name. In macro names we also allow '?' as the initial character. + +macro_name(Cs, L) -> + macro_name(Cs, [], L). + +macro_name([C | Cs], As, L) when C >= $a, C =< $z -> + macro_name_1(Cs, [C | As], L); +macro_name([C | Cs], As, L) when C >= $A, C =< $Z -> + macro_name_1(Cs, [C | As], L); +macro_name([C | Cs], As, L) when C >= $\300, C =< $\377, + C =/= $\327, C =/= $\367 -> + macro_name_1(Cs, [C | As], L); +macro_name([$_ | Cs], As, L) -> + macro_name_1(Cs, [$_ | As], L); +macro_name([$? | Cs], As, L) -> + macro_name_1(Cs, [$? | As], L); +macro_name([$\s | _Cs], _As, L) -> + throw_error(L, macro_name); +macro_name([$\t | _Cs], _As, L) -> + throw_error(L, macro_name); +macro_name([$\n | _Cs], _As, L) -> + throw_error(L, macro_name); +macro_name([C | _Cs], As, L) -> + throw_error(L, {macro_name, [C | As]}); +macro_name([], _As, L) -> + throw_error(L, macro_name). + +macro_name_1([C | Cs], As, L) when C >= $a, C =< $z -> + macro_name_1(Cs, [C | As], L); +macro_name_1([C | Cs], As, L) when C >= $A, C =< $Z -> + macro_name_1(Cs, [C | As], L); +macro_name_1([C | Cs], As, L) when C >= $0, C =< $9 -> + macro_name_1(Cs, [C | As], L); +macro_name_1([C | Cs], As, L) when C >= $\300, C =< $\377, + C =/= $\327, C =/= $\367 -> + macro_name_1(Cs, [C | As], L); +macro_name_1([$_ | Cs], As, L) -> + macro_name_1(Cs, [$_ | As], L); +macro_name_1([$\s | Cs], As, L) -> + macro_name_2(Cs, As, L); +macro_name_1([$\t | Cs], As, L) -> + macro_name_2(Cs, As, L); +macro_name_1([$\n | Cs], As, L) -> + macro_name_2(Cs, As, L + 1); +macro_name_1([$} | _] = Cs, As, L) -> + macro_name_3(Cs, As, L); +macro_name_1([C | _Cs], As, L) -> + throw_error(L, {macro_name, [C | As]}); +macro_name_1([], _As, L) -> + throw_error(L, unterminated_macro). + +macro_name_2([$\s | Cs], As, L) -> + macro_name_2(Cs, As, L); +macro_name_2([$\t | Cs], As, L) -> + macro_name_2(Cs, As, L); +macro_name_2([$\n | Cs], As, L) -> + macro_name_2(Cs, As, L + 1); +macro_name_2([_ | _] = Cs, As, L) -> + macro_name_3(Cs, As, L); +macro_name_2([], _As, L) -> + throw_error(L, unterminated_macro). + +macro_name_3(Cs, As, L) -> + {list_to_atom(lists:reverse(As)), Cs, L}. + + +%% The macro content ends at the first non-escaped '}' character that is +%% not balanced by a corresponding non-escaped '{@' sequence. +%% Escape sequences are those defined above. + +macro_content(Cs, L) -> + %% If there is an error, we report the start line, not the end line. + case catch {ok, macro_content(Cs, [], L, 0)} of + {ok, X} -> + X; + {'EXIT', R} -> + exit(R); + 'end' -> + throw_error(L, unterminated_macro); + Other -> + throw(Other) + end. + +%% @throws 'end' + +macro_content([$@, $@ | Cs], As, L, N) -> + macro_content(Cs, [$@, $@ | As], L, N); % escaped '@' +macro_content([$@, $} | Cs], As, L, N) -> + macro_content(Cs, [$}, $@ | As], L, N); % escaped '}' +macro_content([$@, ${ | Cs], As, L, N) -> + macro_content(Cs, [${, $@ | As], L, N); % escaped '{' +macro_content([${, $@ | Cs], As, L, N) -> + macro_content(Cs, [$@, ${ | As], L, N + 1); +macro_content([$} | Cs], As, L, 0) -> + {lists:reverse(As), Cs, L}; +macro_content([$} | Cs], As, L, N) -> + macro_content(Cs, [$} | As], L, N - 1); +macro_content([$\n = C | Cs], As, L, N) -> + macro_content(Cs, [C | As], L + 1, N); +macro_content([C | Cs], As, L, N) -> + macro_content(Cs, [C | As], L, N); +macro_content([], _As, _L, _N) -> + throw('end'). + +throw_error(L, unterminated_macro) -> + throw_error(L, {"unexpected end of macro.", []}); +throw_error(L, macro_name) -> + throw_error(L, {"missing macro name.", []}); +throw_error(L, {macro_name, S}) -> + throw_error(L, {"bad macro name: '@~s...'.", [lists:reverse(S)]}); +throw_error(L, D) -> + throw({error, L, D}). |