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