1%% =====================================================================
2%% Licensed under the Apache License, Version 2.0 (the "License"); you may
3%% not use this file except in compliance with the License. You may obtain
4%% a copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>
5%%
6%% Unless required by applicable law or agreed to in writing, software
7%% distributed under the License is distributed on an "AS IS" BASIS,
8%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9%% See the License for the specific language governing permissions and
10%% limitations under the License.
11%%
12%% Alternatively, you may use this file under the terms of the GNU Lesser
13%% General Public License (the "LGPL") as published by the Free Software
14%% Foundation; either version 2.1, or (at your option) any later version.
15%% If you wish to allow use of your version of this file only under the
16%% terms of the LGPL, you should delete the provisions above and replace
17%% them with the notice and other provisions required by the LGPL; see
18%% <http://www.gnu.org/licenses/>. If you do not delete the provisions
19%% above, a recipient may use your version of this file under the terms of
20%% either the Apache License or the LGPL.
21%%
22%% @copyright 2003-2006 Richard Carlsson
23%% @author Richard Carlsson <carlsson.richard@gmail.com>
24%% @see edoc
25%% @end
26%% =====================================================================
27
28%% @doc Standard doclet module for EDoc.
29
30%% Note that this is written so that it is *not* depending on edoc.hrl!
31
32%% TODO: copy "doc-files" subdirectories, recursively.
33%% TODO: generate summary page of TODO-notes
34%% TODO: generate summary page of deprecated things
35%% TODO: generate decent indexes over modules, methods, records, etc.
36
37-module(edoc_doclet).
38
39-export([run/2]).
40
41-import(edoc_report, [report/2, warning/2]).
42
43%% @headerfile "../include/edoc_doclet.hrl"
44-include("../include/edoc_doclet.hrl").
45
46-define(EDOC_APP, edoc).
47-define(DEFAULT_FILE_SUFFIX, ".html").
48-define(INDEX_FILE, "index.html").
49-define(OVERVIEW_FILE, "overview.edoc").
50-define(OVERVIEW_SUMMARY, "overview-summary.html").
51-define(MODULES_FRAME, "modules-frame.html").
52-define(STYLESHEET, "stylesheet.css").
53-define(IMAGE, "erlang.png").
54-define(NL, "\n").
55
56-include_lib("xmerl/include/xmerl.hrl").
57
58%% Sources is the list of inputs in the order they were found.
59%% Modules are sorted lists of atoms without duplicates. (They
60%% usually include the data from the edoc-info file in the target
61%% directory, if it exists.)
62
63%% @spec (Command::doclet_gen() | doclet_toc(), edoc_context()) -> ok
64%% @doc Main doclet entry point. See the file <a
65%% href="edoc_doclet.hrl">`edoc_doclet.hrl'</a> for the data
66%% structures used for passing parameters.
67%%
68%% Also see {@link edoc:layout/2} for layout-related options, and
69%% {@link edoc:get_doc/2} for options related to reading source
70%% files.
71%%
72%% Options:
73%% <dl>
74%%  <dt>{@type {file_suffix, string()@}}
75%%  </dt>
76%%  <dd>Specifies the suffix used for output files. The default value is
77%%      `".html"'.
78%%  </dd>
79%%  <dt>{@type {hidden, boolean()@}}
80%%  </dt>
81%%  <dd>If the value is `true', documentation of hidden modules and
82%%      functions will also be included. The default value is `false'.
83%%  </dd>
84%%  <dt>{@type {overview, edoc:filename()@}}
85%%  </dt>
86%%  <dd>Specifies the name of the overview-file. By default, this doclet
87%%      looks for a file `"overview.edoc"' in the target directory.
88%%  </dd>
89%%  <dt>{@type {private, boolean()@}}
90%%  </dt>
91%%  <dd>If the value is `true', documentation of private modules and
92%%      functions will also be included. The default value is `false'.
93%%  </dd>
94%%  <dt>{@type {stylesheet, string()@}}
95%%  </dt>
96%%  <dd>Specifies the URI used for referencing the stylesheet. The
97%%      default value is `"stylesheet.css"'. If an empty string is
98%%      specified, no stylesheet reference will be generated.
99%%  </dd>
100%%  <dt>{@type {stylesheet_file, edoc:filename()@}}
101%%  </dt>
102%%  <dd>Specifies the name of the stylesheet file. By default, this
103%%      doclet uses the file `"stylesheet.css"' in the `priv'
104%%      subdirectory of the EDoc installation directory. The named file
105%%      will be copied to the target directory.
106%%  </dd>
107%%  <dt>{@type {title, string()@}}
108%%  </dt>
109%%  <dd>Specifies the title of the overview-page.
110%%  </dd>
111%% </dl>
112
113%% INHERIT-OPTIONS: title/2
114%% INHERIT-OPTIONS: sources/5
115%% INHERIT-OPTIONS: overview/4
116%% INHERIT-OPTIONS: copy_stylesheet/2
117%% INHERIT-OPTIONS: stylesheet/1
118
119run(#doclet_gen{}=Cmd, Ctxt) ->
120    gen(Cmd#doclet_gen.sources,
121	Cmd#doclet_gen.app,
122	Cmd#doclet_gen.modules,
123	Ctxt);
124run(#doclet_toc{}=Cmd, Ctxt) ->
125    toc(Cmd#doclet_toc.paths, Ctxt).
126
127gen(Sources, App, Modules, Ctxt) ->
128    Dir = Ctxt#context.dir,
129    Env = Ctxt#context.env,
130    Options = Ctxt#context.opts,
131    Title = title(App, Options),
132    CSS = stylesheet(Options),
133    {Modules1, Error} = sources(Sources, Dir, Modules, Env, Options),
134    modules_frame(Dir, Modules1, Title, CSS),
135    overview(Dir, Title, Env, Options),
136    index_file(Dir, Title),
137    edoc_lib:write_info_file(App, Modules1, Dir),
138    copy_stylesheet(Dir, Options),
139    copy_image(Dir),
140    %% handle postponed error during processing of source files
141    case Error of
142	true -> exit(error);
143	false -> ok
144    end.
145
146
147%% NEW-OPTIONS: title
148%% DEFER-OPTIONS: run/2
149
150title(App, Options) ->
151    proplists:get_value(title, Options,
152			if App == ?NO_APP ->
153				"Overview";
154			   true ->
155				io_lib:fwrite("Application: ~ts", [App])
156			end).
157
158
159%% Processing the individual source files.
160
161%% NEW-OPTIONS: file_suffix, private, hidden
162%% INHERIT-OPTIONS: edoc:layout/2
163%% INHERIT-OPTIONS: edoc:get_doc/3
164%% DEFER-OPTIONS: run/2
165
166sources(Sources, Dir, Modules, Env, Options) ->
167    Suffix = proplists:get_value(file_suffix, Options,
168				 ?DEFAULT_FILE_SUFFIX),
169    Private = proplists:get_bool(private, Options),
170    Hidden = proplists:get_bool(hidden, Options),
171    {Ms, E} = lists:foldl(fun (Src, {Set, Error}) ->
172				  source(Src, Dir, Suffix, Env, Set,
173					 Private, Hidden, Error, Options)
174			  end,
175			  {sets:new(), false}, Sources),
176    {[M || M <- Modules, sets:is_element(M, Ms)], E}.
177
178
179%% Generating documentation for a source file, adding its name to the
180%% set if it was successful. Errors are just flagged at this stage,
181%% allowing all source files to be processed even if some of them fail.
182
183source({M, Name, Path}, Dir, Suffix, Env, Set, Private, Hidden,
184       Error, Options) ->
185    File = filename:join(Path, Name),
186    case catch {ok, edoc:get_doc(File, Env, Options)} of
187	{ok, {Module, Doc}} ->
188	    check_name(Module, M, File),
189	    case ((not is_private(Doc)) orelse Private)
190		andalso ((not is_hidden(Doc)) orelse Hidden) of
191		true ->
192		    Text = edoc:layout(Doc, Options),
193		    Name1 = atom_to_list(M) ++ Suffix,
194                    Encoding = [{encoding,encoding(Doc)}],
195		    edoc_lib:write_file(Text, Dir, Name1, Encoding),
196		    {sets:add_element(Module, Set), Error};
197		false ->
198		    {Set, Error}
199	    end;
200	R ->
201	    report("skipping source file '~ts': ~tP.", [File, R, 15]),
202	    {Set, true}
203    end.
204
205check_name(M, M0, File) ->
206    N = M,
207    N0 = M0,
208    case N of
209	[$? | _] ->
210	    %% A module name of the form '?...' is assumed to be caused
211	    %% by the epp_dodger parser when the module declaration has
212	    %% the form '-module(?MACRO).'; skip the filename check.
213	    ok;
214	_ ->
215	    if N =/= N0 ->
216		    warning("file '~ts' actually contains module '~s'.",
217			    [File, M]);
218	       true ->
219		    ok
220	    end
221    end,
222	ok.
223
224%% Creating an index file, with some frames optional.
225%% TODO: get rid of frames, or change doctype to Frameset
226
227index_file(Dir, Title) ->
228    Frame2 = {frame, [{src,?MODULES_FRAME},
229		      {name,"modulesFrame"},{title,""}],
230	      []},
231    Frame3 = {frame, [{src,?OVERVIEW_SUMMARY},
232		      {name,"overviewFrame"},{title,""}],
233	      []},
234    Frameset = {frameset, [{cols,"20%,80%"}],
235    	[?NL, Frame2, ?NL, ?NL, Frame3, ?NL,
236		    {noframes,
237		     [?NL,
238		      {h2, ["This page uses frames"]},
239		      ?NL,
240		      {p, ["Your browser does not accept frames.",
241			   ?NL, br,
242			   "You should go to the ",
243			   {a, [{href, ?OVERVIEW_SUMMARY}],
244			    ["non-frame version"]},
245			   " instead.", ?NL]},
246		      ?NL]},
247		    ?NL]},
248    XML = xhtml_1(Title, [], Frameset),
249    Text = xmerl:export_simple([XML], xmerl_html, []),
250    edoc_lib:write_file(Text, Dir, ?INDEX_FILE).
251
252modules_frame(Dir, Ms, Title, CSS) ->
253    Body = [?NL,
254	    {h2, [{class, "indextitle"}], ["Modules"]},
255	    ?NL,
256	    {table, [{width, "100%"}, {border, 0},
257		     {summary, "list of modules"}],
258	     lists:append(
259	       [[?NL,
260		 {tr, [{td, [],
261			[{a, [{href, module_ref(M)},
262			      {target, "overviewFrame"},
263			      {class, "module"}],
264			  [atom_to_list(M)]}]}]}]
265		 || M <- Ms])},
266	    ?NL],
267    XML = xhtml(Title, CSS, Body),
268    Text = xmerl:export_simple([XML], xmerl_html, []),
269    edoc_lib:write_file(Text, Dir, ?MODULES_FRAME).
270
271module_ref(M) ->
272    atom_to_list(M) ++ ?DEFAULT_FILE_SUFFIX.
273
274xhtml(Title, CSS, Content) ->
275    xhtml_1(Title, CSS, {body, [{bgcolor, "white"}], Content}).
276
277xhtml_1(Title, CSS, Body) ->
278    {html, [?NL,
279	    {head, [?NL, {title, [Title]}, ?NL] ++ CSS},
280	    ?NL,
281	    Body,
282	    ?NL]
283    }.
284
285%% NEW-OPTIONS: overview
286%% INHERIT-OPTIONS: read_file/4
287%% INHERIT-OPTIONS: edoc_lib:run_layout/2
288%% INHERIT-OPTIONS: edoc_extract:file/4
289%% DEFER-OPTIONS: run/2
290
291overview(Dir, Title, Env, Opts) ->
292    File = proplists:get_value(overview, Opts,
293			       filename:join(Dir, ?OVERVIEW_FILE)),
294    Encoding = edoc_lib:read_encoding(File, [{in_comment_only, false}]),
295    Tags = read_file(File, overview, Env, Opts),
296    Data0 = edoc_data:overview(Title, Tags, Env, Opts),
297    EncodingAttribute = #xmlAttribute{name = encoding,
298                                      value = atom_to_list(Encoding)},
299    #xmlElement{attributes = As} = Data0,
300    Data = Data0#xmlElement{attributes = [EncodingAttribute | As]},
301    F = fun (M) ->
302		M:overview(Data, Opts)
303	end,
304    Text = edoc_lib:run_layout(F, Opts),
305    EncOpts = [{encoding,Encoding}],
306    edoc_lib:write_file(Text, Dir, ?OVERVIEW_SUMMARY, EncOpts).
307
308copy_image(Dir) ->
309    case code:priv_dir(?EDOC_APP) of
310	PrivDir when is_list(PrivDir) ->
311	    From = filename:join(PrivDir, ?IMAGE),
312	    edoc_lib:copy_file(From, filename:join(Dir, ?IMAGE));
313	_ ->
314	    report("cannot find default image file.", []),
315	    exit(error)
316    end.
317
318%% NEW-OPTIONS: stylesheet_file
319%% DEFER-OPTIONS: run/2
320
321copy_stylesheet(Dir, Options) ->
322    case proplists:get_value(stylesheet, Options) of
323	undefined ->
324	    From = case proplists:get_value(stylesheet_file, Options) of
325		       File when is_list(File) ->
326			   File;
327		       _ ->
328			   case code:priv_dir(?EDOC_APP) of
329			       PrivDir when is_list(PrivDir) ->
330				   filename:join(PrivDir, ?STYLESHEET);
331			       _ ->
332				   report("cannot find default "
333					  "stylesheet file.", []),
334				   exit(error)
335			   end
336		   end,
337	    edoc_lib:copy_file(From, filename:join(Dir, ?STYLESHEET));
338	_ ->
339	    ok
340    end.
341
342%% NEW-OPTIONS: stylesheet
343%% DEFER-OPTIONS: run/2
344
345stylesheet(Options) ->
346    case proplists:get_value(stylesheet, Options) of
347	"" ->
348	    [];
349	S ->
350	    Ref = case S of
351		      undefined ->
352			  ?STYLESHEET;
353		      "" ->
354			  "";    % no stylesheet
355		      S when is_list(S) ->
356			  S;
357		      _ ->
358			  report("bad value for option 'stylesheet'.",
359				 []),
360			  exit(error)
361		  end,
362	    [{link, [{rel, "stylesheet"},
363		     {type, "text/css"},
364		     {href, Ref},
365		     {title, "EDoc"}], []},
366	     ?NL]
367    end.
368
369is_private(E) ->
370    case get_attrval(private, E) of
371 	"yes" -> true;
372 	_ -> false
373    end.
374
375is_hidden(E) ->
376    case get_attrval(hidden, E) of
377 	"yes" -> true;
378 	_ -> false
379    end.
380
381encoding(E) ->
382    case get_attrval(encoding, E) of
383        "latin1" -> latin1;
384        _ -> utf8
385    end.
386
387get_attrval(Name, #xmlElement{attributes = As}) ->
388    case get_attr(Name, As) of
389	[#xmlAttribute{value = V}] ->
390	    V;
391	[] -> ""
392    end.
393
394get_attr(Name, [#xmlAttribute{name = Name} = A | As]) ->
395    [A | get_attr(Name, As)];
396get_attr(Name, [_ | As]) ->
397    get_attr(Name, As);
398get_attr(_, []) ->
399    [].
400
401%% Read external source file. Fails quietly, returning empty tag list.
402
403%% INHERIT-OPTIONS: edoc_extract:file/4
404
405read_file(File, Context, Env, Opts) ->
406    case edoc_extract:file(File, Context, Env, Opts) of
407	{ok, Tags} ->
408	    Tags;
409	{error, _} ->
410	    []
411    end.
412
413
414%% TODO: FIXME: meta-level index generation
415
416%% Creates a Table of Content from a list of Paths (ie paths to applications)
417%% and an overview file.
418
419-define(EDOC_DIR, "doc").
420-define(INDEX_DIR, "doc/index").
421-define(CURRENT_DIR, ".").
422
423toc(Paths, Ctxt) ->
424    Opts = Ctxt#context.opts,
425    Dir = Ctxt#context.dir,
426    Env = Ctxt#context.env,
427    app_index_file(Paths, Dir, Env, Opts).
428
429%% TODO: FIXME: it's unclear how much of this is working at all
430
431%% NEW-OPTIONS: title
432%% INHERIT-OPTIONS: overview/4
433
434app_index_file(Paths, Dir, Env, Options) ->
435    Title = proplists:get_value(title, Options,"Overview"),
436%    Priv = proplists:get_bool(private, Options),
437    CSS = stylesheet(Options),
438    Apps1 = [{filename:dirname(A),filename:basename(A)} || A <- Paths],
439    index_file(Dir, Title),
440    application_frame(Dir, Apps1, Title, CSS),
441    modules_frame(Dir, [], Title, CSS),
442    overview(Dir, Title, Env, Options),
443%    edoc_lib:write_info_file(Prod, [], Modules1, Dir),
444    copy_stylesheet(Dir, Options).
445
446application_frame(Dir, Apps, Title, CSS) ->
447    Body = [?NL,
448	    {h2, ["Applications"]},
449	    ?NL,
450	    {table, [{width, "100%"}, {border, 0}],
451	     lists:append(
452	       [[{tr, [{td, [], [{a, [{href,app_ref(Path,App)},
453				      {target,"_top"}],
454				  [App]}]}]}]
455		|| {Path,App} <- Apps])},
456	    ?NL],
457    XML = xhtml(Title, CSS, Body),
458    Text = xmerl:export_simple([XML], xmerl_html, []),
459    edoc_lib:write_file(Text, Dir, ?MODULES_FRAME).
460
461app_ref(Path,M) ->
462    filename:join([Path,M,?EDOC_DIR,?INDEX_FILE]).
463