%% ``The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved via the world wide web at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999-2000, Ericsson
%% Utvecklings AB. All Rights Reserved.''
%%
%% $Id$
%%
-module(docb_main).
-export([process/2,
parse/2, parse1/2,
pp/2,
insert_after/3,
transform/5, pp/5,
include_file/2, include/3,
eval_str/1,
validate_html/1
]).
-export([do_parse_sgmls/1]).
%%----------------------------------------------------------------------
%% process(File, Opts) -> errors | ok
%% Parses the source file File and transforms the result to html,
%% latex and/or man page format.
process(File, Opts) ->
File1 = File ++ ".tmpconv",
os:cmd("sed -e 's/xi:include[ \t]*href/include file/g' -e 's/xmlns:xi=\"http:\\/\\/www.w3.org\\/2001\\/XInclude\"//g' < " ++
File ++ ".xml > " ++ File1 ++ ".xml"), %LATH
case parse1(File1, Opts) of
errors ->
file:delete(File1 ++ ".xml"),
errors;
{ok, Tree} ->
From = element(1, Tree),
Tos0 =
lists:foldl(
fun(latex, Acc) -> [latex|Acc];
(html, Acc) -> [html|Acc];
({man, _Section}, Acc) -> [man|Acc];
(_, Acc) -> Acc
end, [], Opts),
%% If no target format is specified, assume HTML:
Tos = if
Tos0==[] -> [html];
true -> Tos0
end,
Result = [transform(From, To, Opts, File, Tree)||To <- Tos],
case lists:member(transformation_error,Result) of
true ->
file:delete(File1 ++ ".xml"),
errors;
_ ->
file:delete(File1 ++ ".xml"),
ok
end
end.
%%----------------------------------------------------------------------
%% parse(File, Opts) -> {ok, Tree} | errors
%% Parses the source file File, resulting in a tree structure.
parse(File, Opts) ->
case docb_util:lookup_option(src_type, Opts) of
".xml" ->
parse_xml(File++".xml", Opts);
".sgml" ->
parse_sgml(File, Opts)
end.
%% parse1(File, Opts) -> {ok, Tree} | errors
%% Like parse/2, but in the SGML case also prints the parse errors
%% (in File.html.sgmls_errs) information to stdout.
parse1(File, Opts) ->
parse(File, [{print_parse_errs, true}|Opts]).
validate_html(InFile) ->
ScanOpts = [{validation,true}, {fetch_fun, fun fetch_dtd/2}],
case xmerl_scan:file(InFile, ScanOpts) of
{_XMLTuple,[]} -> % ok
{InFile,ok};
{'EXIT',Reason} ->
{InFile,Reason}
end.
fetch_dtd({public,_,"http://www.w3.org/TR/xhtml1/DTD/"++ Rest},GlobalState) ->
Filename = filename:join(docb_util:dtd_dir(),Rest),
{ok,{file,Filename},GlobalState};
fetch_dtd({public,_,Str},GlobalState) ->
{ok,{file,filename:join(docb_util:dtd_dir(),Str)},GlobalState}.
parse_xml(InFile, Opts) ->
DtdDir = docb_util:dtd_dir(),
ScanOpts = [{validation,true}, {fetch_path, [DtdDir]}],
PrintP = docb_util:lookup_option(print_parse_errs, Opts),
case catch xmerl_scan:file(InFile, ScanOpts) of
{'EXIT', Error} when PrintP ->
docb_util:message(error,
"XML validation error:~n~p", [Error]),
errors;
{'EXIT', _Error} ->
errors;
{error, Reason} -> % probably file error
docb_util:message(error, "XML scan error: ~p", [Reason]),
errors;
{Doc, []} ->
case catch xmerl:export([Doc], docb_xmerl_tree_cb) of
[Tree] ->
verify(Tree),
{ok, Tree};
{'EXIT', Error} ->
docb_util:message(error,
"XML export error:~n~p", [Error]),
errors
end
end.
parse_sgml(InFile, Opts) ->
Pfx = tmp_file_prefix(InFile, Opts),
OutFile = Pfx ++ "sgmls_output",
ErrFile = Pfx ++ "sgmls_errs",
EntVals = lists:usort(docb_util:lookup_options(ent, Opts)),
Ents = lists:flatten([" -ent " ++ Val || Val <- EntVals]),
Cmd = docb_util:old_docb_dir() ++ "/bin/docb_sgmls_run " ++
Ents ++ " " ++ InFile ++ ".sgml " ++ OutFile ++ " " ++ ErrFile,
case os:cmd(Cmd) of
[] ->
PrintP = docb_util:lookup_option(print_parse_errs, Opts),
case filelib:file_size(ErrFile) of
0 -> % implies no errors
parse_sgmls(InFile, OutFile);
_ when PrintP ->
cat(ErrFile),
errors;
_ ->
errors
end;
Msg ->
docb_util:message(error, "~p", [Msg]),
errors
end.
tmp_file_prefix(File, Opts) ->
lists:concat(
[File, "." | lists:foldl(
fun(latex, Acc) -> ["latex."|Acc];
(html, Acc) -> ["html."|Acc];
({man, Section}, Acc) -> ["man", Section, "."|Acc];
(_, Acc) -> Acc
end, [], Opts)]).
parse_sgmls(InFile, SgmlsFile) ->
case file:open(SgmlsFile, [read]) of
{ok, Fd} ->
Res = case (catch do_parse_sgmls(Fd)) of
{ok, Tree} ->
{ok, Tree};
{'EXIT', Reason} ->
docb_util:message(
error,
"Cannot parse sgmls output file "
"~s, obtained from parsing ~s, "
"reason: ~w",
[SgmlsFile, InFile, Reason]),
errors;
{error, Reason} ->
docb_util:message(
error,
"Cannot parse sgmls output file "
"~s, obtained from parsing ~s, "
"reason: ~w",
[SgmlsFile, InFile, Reason]),
errors
end,
file:close(Fd),
case Res of
{ok, Tree0} ->
verify(Tree0),
{ok, Tree0};
_Other ->
errors
end;
{error, Reason} ->
docb_util:message(error,
"Cannot open sgmls output file ~s, "
"obtained from parsing ~s, reason: ~w",
[SgmlsFile, InFile, Reason]),
errors
end.
do_parse_sgmls(Fd) ->
do_parse_sgmls(Fd, []).
do_parse_sgmls(Fd, Attrs) ->
case get_line(Fd) of
{attrs, A} ->
do_parse_sgmls(Fd, [A|Attrs]);
{startTag, Tag} ->
{ok, {Tag, Attrs, get_args(Fd)}};
Other ->
{error, Other}
end.
get_args(Fd) ->
case get_line(Fd) of
{startTag, Tag} ->
H = {Tag, [], get_args(Fd)},
[H|get_args(Fd)];
{dataTag, Str} ->
[{pcdata, [], Str}|get_args(Fd)];
{attrs, A} ->
get_args_attr(Fd, [A]);
close ->
[];
ok ->
[]
end.
get_args_attr(Fd, Attrs) ->
case get_line(Fd) of
{startTag, Tag} ->
H = {Tag, lists:reverse(Attrs), get_args(Fd)},
[H|get_args(Fd)];
{dataTag, Str} ->
[{pcdata, lists:reverse(Attrs), Str}|get_args(Fd)];
{attrs, A} ->
get_args_attr(Fd, [A|Attrs]);
close ->
[];
ok ->
[]
end.
get_line(Fd) ->
Str = io:get_line(Fd, ''),
case Str of
[$(|T] ->
{startTag, tag_name(T)};
[$-|T] ->
{dataTag, T};
[$)|_T] ->
close;
[$A|T] ->
{attrs, attrs(remove_nl(T))};
[$?|_T] ->
get_line(Fd);
[$C|_] ->
ok
end.
remove_nl([$\n|_]) -> [];
remove_nl([H|T]) -> [H|remove_nl(T)];
remove_nl([]) -> [].
%% attrs
%% splits a string like
%% AAAAA BBBBB ......
%% into {"AAA", "BBB", Rest}
attrs(T) ->
{X, T1} = get_item(T),
{Y, T2} = get_item(T1),
T3 = skip_blanks(T2),
{X, Y, T3}.
get_item(T) -> get_item(skip_blanks(T), []).
get_item([$ |T], L) -> {lists:reverse(L), [$ |T]};
get_item([H|T], L) -> get_item(T, [H|L]);
get_item([], L) -> {lists:reverse(L), []}.
skip_blanks([$ |T]) -> skip_blanks(T);
skip_blanks(T) -> T.
tag_name(Str) -> tag_name(Str, []).
tag_name([H|T], L) when $A =< H, H =< $Z ->
tag_name(T, [H-$A+$a|L]);
tag_name([$\n], L) ->
list_to_atom(lists:reverse(L));
tag_name([H|T], L) ->
tag_name(T, [H|L]).
cat(File) ->
case file:open(File, [read]) of
{ok, Fd} ->
cat1(Fd),
file:close(Fd);
Other ->
Other
end.
cat1(Fd) ->
case io:get_line(Fd, '') of
eof ->
eof;
Str ->
io:format("~s", [Str]),
cat1(Fd)
end.
%%----------------------------------------------------------------------
verify(Tree) -> verify(Tree, [], 1).
verify({pcdata, Optional, _}, Path, Level) ->
verify_optional(Optional, Path, Level);
verify({Tag, Optional, Args}, Path, Level) when is_list(Args) ->
case verify_optional(Optional, Path, Level) of
true ->
verify_list(Args, [Tag|Path], Level);
false ->
false
end;
verify(Other, Path, Level) ->
verify_error(Other, Path, Level).
verify_error(X, Path, Level) ->
docb_util:message(error, "Invalid object found at: ~p level:~w~n~s",
[Path, Level, docb_pretty_format:term(X)]),
false.
verify_list([H|T], Path, Level) ->
case verify(H, Path, Level) of
true ->
verify_list(T, Path, Level +1);
false ->
false
end;
verify_list([], _, _) ->
true.
verify_optional([{_, _, _}|T], Path, Level) ->
verify_optional(T, Path, Level);
verify_optional([], _Path, _Level) ->
true;
verify_optional(X, Path, Level) ->
verify_error(X, Path, Level).
%%----------------------------------------------------------------------
%% pp(File, Opts) -> {ok, OutFile} | errors
%% Parses the source file and, if successful, prints the resulting tree
%% structure to a file with the extension ".pp".
pp(File, Opts) ->
case parse(File, Opts) of
{ok, Tree} ->
OutFile = File ++ ".pp",
dump(OutFile, Tree),
{ok, OutFile};
errors ->
errors
end.
dump(File, Struct) ->
{ok, Stream} = file:open(File, [write]),
io:format("Info: Dump on ~p ...", [File]),
io:format(Stream, "~n~s~n", [docb_pretty_format:term(Struct)]),
io:format(" done.\n"),
file:close(Stream).
%%----------------------------------------------------------------------
%% insert_after(Tag, Tree, Obj) -> Tree | {'EXIT', Reason}
%% Insert an element in a tree structure
insert_after(Tag, Tree, Obj) ->
edit(Tag, Tree, {insert_after, Obj}).
%% edit Op = delete, insert_before, insert_after
edit(Tag, Tree, Op) ->
case catch edit1(Tag, Tree, Op) of
error ->
docb_util:message(error, "Cannot do ~p to ~w", [Op, Tag]),
Tree;
Other ->
Other
end.
edit1(Tag, {Tag, _O, _A}, _Op) ->
throw(error);
edit1(Tag, {Tag1, O, A}, Op) ->
{Tag1, O, edit1_list(Tag, A, Op)};
edit1(_, _, _) ->
throw(error).
edit1_list(Tag, [{pcdata, Str}|T], Op) ->
[{pcdata, Str}|edit1_list(Tag, T, Op)];
edit1_list(Tag, [{Tag, O, A}|T], {insert_after, Obj}) ->
[{Tag, O, A}, Obj|T];
edit1_list(Tag, [H|T], Op) ->
[H|edit1_list(Tag, T, Op)];
edit1_list(_Tag, [], _Op) ->
[].
%%______________________________________________________________________
%% transform(From, To, Opts, File, Tree) -> void()
%% Actual transformation of tree structure to desired format.
transform(From, To, Opts, File, Tree) ->
Filter = if
To==html; To==kwic ->
list_to_atom("docb_tr_" ++ atom_to_list(From) ++
[$2|atom_to_list(To)]);
true ->
list_to_atom("sgml_tr_" ++ atom_to_list(From) ++
[$2|atom_to_list(To)])
end,
case catch apply(Filter, transform, [File, Tree, Opts]) of
%% R5C
{'EXIT', {undef, [{Filter, transform, [File, Tree, Opts]}|_]}}->
%% No transformation defined
finish_transform(Tree, File, Opts, Filter);
{'EXIT', {undef, {Filter, transform, [File, Tree, Opts]}}} ->
%% No transformation defined
finish_transform(Tree, File, Opts, Filter);
{'EXIT', What} ->
docb_util:message(error,
"Transformation trouble in ~P", [What, 9]),
transformation_error;
{error, Reason} ->
docb_util:message(error, Reason),
transformation_error;
{Tree1, Opts1} ->
%% transformation returning both new parse and new options
finish_transform(Tree1, File, Opts1, Filter);
Tree1 ->
%% transformation returning only new parse
finish_transform(Tree1, File, Opts, Filter)
end.
finish_transform(Tree, File, Opts, Filter) ->
{Str, NewOpts} = pp(Tree, [], 1, Filter, Opts),
Extension =
case catch apply(Filter, extension, [NewOpts]) of
{'EXIT', _} ->
apply(Filter, extension, []);
Others ->
Others
end,
{ok, Out} =
file:open(docb_util:outfile(File, Extension, NewOpts), [write]),
put_chars(Out, Str),
file:close(Out).
put_chars(Out, Str) -> put_chars(Out, Str, 0).
put_chars(Out, [$\n|Cs], _Pos) ->
io:put_chars(Out, [$\n]),
put_chars(Out, Cs, 0);
put_chars(Out, [$\011|Cs], Pos) -> % tab
TabbedPos = 8*((Pos div 8)+1),
Nblanks = TabbedPos - Pos,
io:put_chars(Out, lists:duplicate(Nblanks, $ )),
put_chars(Out, Cs, Pos+Nblanks);
put_chars(Out, [C|Cs], Pos) when is_integer(C) ->
io:put_chars(Out, [C]),
put_chars(Out, Cs, Pos+1);
put_chars(Out, [L|Cs], Pos) when is_list(L) ->
put_chars(Out, Cs, put_chars(Out, L, Pos));
put_chars(_Out, [], Pos) ->
Pos.
pp({Tag, Optional, Args}, TagPath, Level, Filter, Opts) ->
TagPath1 = [Tag|TagPath],
Optional1 = reduce_optional(Optional),
%% First try Filter:rule/3. It returns {Return, NewOpts}
%% where Return is as from rule/2:
Rule_3_result =
case catch Filter:rule(TagPath1, {Level,Optional1,Args},Opts) of
%% R5C
{'EXIT', {undef, [{_, rule, _}|_]}} -> % No rule/3 defined
failed;
{'EXIT', {undef, {_, rule, _}}} -> % No rule/3 defined
failed;
%% R5C
{'EXIT', {function_clause, [{_, rule, _}|_]}} -> % No MATCHING rule/3
failed;
{'EXIT', {function_clause, {_, rule, _}}} -> % No MATCHING rule/3
failed;
{'EXIT', What} ->
docb_util:message(error,
"Serious Error: ~P", [What, 9]);
Others ->
Others
end,
handle_rule_call_result({r3, Rule_3_result}, Filter, TagPath1, Tag,
Level, Optional1, Args, Opts).
handle_rule_call_result({r3, failed}, Filter, TagPath1, Tag, Level, Optional1,
Args, Opts) ->
%% Hmmm, try Filter:rule/2
Rule_2_result = (catch Filter:rule(TagPath1, {Level, Optional1, Args})),
handle_rule_call_result({r2, Rule_2_result}, Filter, TagPath1, Tag,
Level, Optional1, Args, Opts);
handle_rule_call_result({r3, {Result, NewOpts}}, Filter, TagPath1, Tag, Level,
Optional1, Args, _Opts) ->
handle_rule_call_result({r2, Result}, Filter, TagPath1, Tag, Level,
Optional1, Args, NewOpts);
handle_rule_call_result({_, {func, F}}, _Filter, _TagPath1, _Tag, _Level,
_Optional1, Args, Opts) ->
{F(Args), Opts};
handle_rule_call_result({_, {'EXIT', Why}}, _Filter, TagPath1, _Tag, Level,
Optional1, Args, Opts) ->
report_error(TagPath1, Why, {Level, Optional1, Args}),
{[], Opts};
handle_rule_call_result({_, {drop, Str}}, _Filter, _TagPath1, _Tag, _Level,
_Optional1, _Args, Opts) ->
{[Str], Opts};
handle_rule_call_result({_, {newargs, NewArgs}}, Filter, TagPath1, _Tag, _Level,
_Optional1, _Args, Opts) ->
{List, NewOpts} = pp_list(NewArgs, TagPath1, 1, Filter, Opts),
{[List], NewOpts};
handle_rule_call_result({_, {newargs, Before, NewArgs, After}}, Filter, TagPath1, _Tag, _Level,
_Optional1, _Args, Opts) ->
{List, NewOpts} = pp_list(NewArgs, TagPath1, 1, Filter, Opts),
{[Before, List, After], NewOpts};
handle_rule_call_result({_, {Before, After}}, Filter, TagPath1, _Tag, _Level,
_Optional1, Args, Opts) when is_list(Before) ->
{List, NewOpts} = pp_list(Args, TagPath1, 1, Filter, Opts),
{[Before, List, After], NewOpts}.
pp_list([H|T], TagPath, Level, Rules, Opts) ->
{Hpp, Hopts} = pp(H, TagPath, Level, Rules, Opts),
{Tpp, Tops} = pp_list(T, TagPath, Level + 1, Rules, Hopts),
{[Hpp|Tpp], Tops};
pp_list([], _, _, _, Opts) ->
{[], Opts}.
reduce_optional([{_, _, H}|T]) -> [H|reduce_optional(T)];
reduce_optional([]) -> [].
report_error(Arg1, Cause, Arg2) ->
[Tag|_] = Arg1,
docb_util:message(error,
"Formatting trouble in ~p: ~p", [Tag, Cause]),
docb_util:message(error, "Failure in rule(~p, ~p)", [Arg1, Arg2]).
%%----------------------------------------------------------------------
%% include_file(File, Tag) -> {ok, String} | error
include_file(File, Tag) ->
include(File, "%S" ++ Tag, "%E" ++ Tag).
%% include(File, StartTag, StopTag) -> {ok, String} | error
include(File, "", "") ->
case file:open(File, [read]) of
{ok, Fd} ->
String = include_all(Fd),
file:close(Fd),
{ok, String};
_ ->
docb_util:message(error,
"Include file ~s not found", [File]),
error
end;
include(File, StartTag, StopTag) ->
case file:open(File, [read]) of
{ok, Fd} ->
String = extract(File, Fd, StartTag, StopTag, searching),
file:close(Fd),
{ok, lists:flatten(String)};
_ ->
docb_util:message(error,
"Include file ~s not found", [File]),
error
end.
include_all(Fd) ->
case io:get_line(Fd, '') of
eof ->
[];
ListOfChars ->
lists:append(ListOfChars, include_all(Fd))
end.
extract(File, Fd, StartTag, StopTag, State) ->
Line=io:get_line(Fd, ''),
extract(File, Fd, StartTag, StopTag, State, Line).
extract(File, _, _, _, _, eof) ->
docb_util:message(error,
"Premature end of file in include file ~p",
[File]),
[];
extract(File, Fd, StartTag, StopTag, searching, Line) ->
case regexp:match(Line, "^" ++ StartTag) of
{match, _Start, _Length} ->
extract(File, Fd, StartTag, StopTag, copying);
nomatch ->
extract(File, Fd, StartTag, StopTag, searching);
{error, _Error} ->
docb_util:message(error, "Bad syntax in ~s", [File]),
[]
end;
extract(File, Fd, StartTag, StopTag, copying, Line) ->
case regexp:match(Line, "^" ++ StopTag) of
{match, _Start, _Length} ->
[];
nomatch ->
[Line|extract(File, Fd, StartTag, StopTag, copying)];
{error, _Error} ->
docb_util:message(error, "Bad syntax in ~s", [File]),
[]
end.
%%----------------------------------------------------------------------
eval_str(Str) ->
case lib:eval_str(Str) of
{error, Report} ->
docb_util:message(error,
"ErlEval failed: ~s (~s)", [Str, Report]);
{ok, S} ->
io_lib:format("~p~n", [S])
end.