diff options
Diffstat (limited to 'lib/edoc/src')
-rw-r--r-- | lib/edoc/src/Makefile | 91 | ||||
-rw-r--r-- | lib/edoc/src/edoc.app.src | 24 | ||||
-rw-r--r-- | lib/edoc/src/edoc.appup.src | 1 | ||||
-rw-r--r-- | lib/edoc/src/edoc.erl | 771 | ||||
-rw-r--r-- | lib/edoc/src/edoc.hrl | 100 | ||||
-rw-r--r-- | lib/edoc/src/edoc_data.erl | 545 | ||||
-rw-r--r-- | lib/edoc/src/edoc_doclet.erl | 521 | ||||
-rw-r--r-- | lib/edoc/src/edoc_extract.erl | 584 | ||||
-rw-r--r-- | lib/edoc/src/edoc_layout.erl | 875 | ||||
-rw-r--r-- | lib/edoc/src/edoc_lib.erl | 998 | ||||
-rw-r--r-- | lib/edoc/src/edoc_macros.erl | 327 | ||||
-rw-r--r-- | lib/edoc/src/edoc_parser.yrl | 423 | ||||
-rw-r--r-- | lib/edoc/src/edoc_refs.erl | 217 | ||||
-rw-r--r-- | lib/edoc/src/edoc_report.erl | 96 | ||||
-rw-r--r-- | lib/edoc/src/edoc_run.erl | 225 | ||||
-rw-r--r-- | lib/edoc/src/edoc_scanner.erl | 358 | ||||
-rw-r--r-- | lib/edoc/src/edoc_tags.erl | 373 | ||||
-rw-r--r-- | lib/edoc/src/edoc_types.erl | 204 | ||||
-rw-r--r-- | lib/edoc/src/edoc_types.hrl | 130 | ||||
-rw-r--r-- | lib/edoc/src/edoc_wiki.erl | 456 | ||||
-rw-r--r-- | lib/edoc/src/otpsgml_layout.erl | 853 |
21 files changed, 8172 insertions, 0 deletions
diff --git a/lib/edoc/src/Makefile b/lib/edoc/src/Makefile new file mode 100644 index 0000000000..fd0fbac37d --- /dev/null +++ b/lib/edoc/src/Makefile @@ -0,0 +1,91 @@ +# +# Copyright (C) 2004, Ericsson Telecommunications +# Author: Richard Carlsson, Bertil Karlsson +# +include $(ERL_TOP)/make/target.mk +include $(ERL_TOP)/make/$(TARGET)/otp.mk + +# ---------------------------------------------------- +# Application version +# ---------------------------------------------------- +include ../vsn.mk +VSN=$(EDOC_VSN) + +# ---------------------------------------------------- +# Release directory specification +# ---------------------------------------------------- +RELSYSDIR = $(RELEASE_PATH)/lib/edoc-$(VSN) + + +# +# Common Macros +# + +EBIN = ../ebin +XMERL = ../../xmerl +ERL_COMPILE_FLAGS += -I../include -I$(XMERL)/include +warn_unused_vars +nowarn_shadow_vars +warn_unused_import +warn_deprecated_guard + +SOURCES= \ + edoc.erl edoc_data.erl edoc_doclet.erl edoc_extract.erl \ + edoc_layout.erl edoc_lib.erl edoc_macros.erl edoc_parser.erl \ + edoc_refs.erl edoc_report.erl edoc_run.erl edoc_scanner.erl \ + edoc_tags.erl edoc_types.erl edoc_wiki.erl otpsgml_layout.erl + +OBJECTS=$(SOURCES:%.erl=$(EBIN)/%.$(EMULATOR)) $(APP_TARGET) $(APPUP_TARGET) + +HRL_FILES = edoc.hrl edoc_types.hrl ../include/edoc_doclet.hrl + +YRL_FILE = edoc_parser.yrl + +APP_FILE= edoc.app +APP_SRC= $(APP_FILE).src +APP_TARGET= $(EBIN)/$(APP_FILE) + +APPUP_FILE= edoc.appup +APPUP_SRC= $(APPUP_FILE).src +APPUP_TARGET= $(EBIN)/$(APPUP_FILE) + +# ---------------------------------------------------- +# Targets +# ---------------------------------------------------- + +debug opt: $(OBJECTS) + +all: $(OBJECTS) + +$(OBJECTS): $(HRL_FILES) $(XMERL)/include/xmerl.hrl + +clean: + rm -f $(OBJECTS) edoc_parser.erl + rm -f core *~ + +distclean: clean + +realclean: clean + +$(EBIN)/%.$(EMULATOR):%.erl + erlc -W $(ERL_COMPILE_FLAGS) -o$(EBIN) $< + +# ---------------------------------------------------- +# Special Build Targets +# ---------------------------------------------------- + +$(APP_TARGET): $(APP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +$(APPUP_TARGET): $(APPUP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +# ---------------------------------------------------- +# Release Target +# ---------------------------------------------------- +include $(ERL_TOP)/make/otp_release_targets.mk + +release_spec: opt + $(INSTALL_DIR) $(RELSYSDIR)/ebin + $(INSTALL_DATA) $(OBJECTS) $(RELSYSDIR)/ebin + $(INSTALL_DIR) $(RELSYSDIR)/src + $(INSTALL_DATA) $(SOURCES) $(HRL_FILES) $(YRL_FILE) $(RELSYSDIR)/src + +release_docs_spec: + diff --git a/lib/edoc/src/edoc.app.src b/lib/edoc/src/edoc.app.src new file mode 100644 index 0000000000..2177533441 --- /dev/null +++ b/lib/edoc/src/edoc.app.src @@ -0,0 +1,24 @@ +% This is an -*- erlang -*- file. + +{application, edoc, + [{description, "EDoc"}, + {vsn, "%VSN%"}, + {modules, [edoc, + edoc_data, + edoc_doclet, + edoc_extract, + edoc_layout, + edoc_lib, + edoc_macros, + edoc_parser, + edoc_refs, + edoc_report, + edoc_run, + edoc_scanner, + edoc_tags, + edoc_types, + edoc_wiki, + otpsgml_layout]}, + {registered,[]}, + {applications, [compiler,kernel,stdlib,syntax_tools]}, + {env, []}]}. diff --git a/lib/edoc/src/edoc.appup.src b/lib/edoc/src/edoc.appup.src new file mode 100644 index 0000000000..54a63833e6 --- /dev/null +++ b/lib/edoc/src/edoc.appup.src @@ -0,0 +1 @@ +{"%VSN%",[],[]}. diff --git a/lib/edoc/src/edoc.erl b/lib/edoc/src/edoc.erl new file mode 100644 index 0000000000..ec452a5929 --- /dev/null +++ b/lib/edoc/src/edoc.erl @@ -0,0 +1,771 @@ +%% ===================================================================== +%% 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$ +%% +%% @copyright 2001-2007 Richard Carlsson +%% @author Richard Carlsson <[email protected]> +%% @version {@version} +%% @end +%% ===================================================================== + +%% TODO: check weirdness in name generation for @spec f(TypeName, ...) -> ... +%% TODO: option for ignoring functions matching some pattern ('..._test_'/0) +%% TODO: @private_type tag, opaque unless generating private docs? +%% TODO: document the record type syntax +%% TODO: some 'skip' option for ignoring particular modules/packages? +%% TODO: intermediate-level packages: document even if no local sources. +%% TODO: multiline comment support (needs modified comment representation) +%% TODO: config-file for default settings +%% TODO: config: locations of all local docdirs; generate local doc-index page +%% TODO: config: URL:s of offline packages/apps +%% TODO: config: default stylesheet +%% TODO: config: default header/footer, etc. +%% TODO: offline linkage +%% TODO: including source code, explicitly and/or automatically + +%% @doc EDoc - the Erlang program documentation generator. +%% +%% This module provides the main user interface to EDoc. +%% <ul> +%% <li><a href="overview-summary.html">EDoc User Manual</a></li> +%% <li><a href="overview-summary.html#Running_EDoc">Running EDoc</a></li> +%% </ul> + +-module(edoc). + +-export([packages/1, packages/2, files/1, files/2, + application/1, application/2, application/3, + toc/1, toc/2, toc/3, + run/3, + file/1, file/2, + read/1, read/2, + layout/1, layout/2, + get_doc/1, get_doc/2, get_doc/3, + read_comments/1, read_comments/2, + read_source/1, read_source/2]). + +-import(edoc_report, [report/2, report/3, error/1, error/3]). + +-include("edoc.hrl"). + + +%% @spec (Name::filename()) -> ok +%% @equiv file(Name, []) +%% @deprecated See {@link file/2} for details. + +file(Name) -> + file(Name, []). + +%% @spec file(filename(), proplist()) -> ok +%% +%% @type filename() = //kernel/file:filename() +%% @type proplist() = [term()] +%% +%% @deprecated This is part of the old interface to EDoc and is mainly +%% kept for backwards compatibility. The preferred way of generating +%% documentation is through one of the functions {@link application/2}, +%% {@link packages/2} and {@link files/2}. +%% +%% @doc Reads a source code file and outputs formatted documentation to +%% a corresponding file. +%% +%% Options: +%% <dl> +%% <dt>{@type {dir, filename()@}} +%% </dt> +%% <dd>Specifies the output directory for the created file. (By +%% default, the output is written to the directory of the source +%% file.) +%% </dd> +%% <dt>{@type {source_suffix, string()@}} +%% </dt> +%% <dd>Specifies the expected suffix of the input file. The default +%% value is `".erl"'. +%% </dd> +%% <dt>{@type {file_suffix, string()@}} +%% </dt> +%% <dd>Specifies the suffix for the created file. The default value is +%% `".html"'. +%% </dd> +%% </dl> +%% +%% See {@link get_doc/2} and {@link layout/2} for further +%% options. +%% +%% For running EDoc from a Makefile or similar, see +%% {@link edoc_run:file/1}. +%% +%% @see read/2 + +%% NEW-OPTIONS: source_suffix, file_suffix, dir +%% INHERIT-OPTIONS: read/2 + +file(Name, Options) -> + Text = read(Name, Options), + SrcSuffix = proplists:get_value(source_suffix, Options, + ?DEFAULT_SOURCE_SUFFIX), + BaseName = filename:basename(Name, SrcSuffix), + Suffix = proplists:get_value(file_suffix, Options, + ?DEFAULT_FILE_SUFFIX), + Dir = proplists:get_value(dir, Options, filename:dirname(Name)), + edoc_lib:write_file(Text, Dir, BaseName ++ Suffix). + + +%% TODO: better documentation of files/1/2, packages/1/2, application/1/2/3 + +%% @spec (Files::[filename() | {package(), [filename()]}]) -> ok +%% @equiv packages(Packages, []) + +files(Files) -> + files(Files, []). + +%% @spec (Files::[filename() | {package(), [filename()]}], +%% Options::proplist()) -> ok +%% @doc Runs EDoc on a given set of source files. See {@link run/3} for +%% details, including options. +%% @equiv run([], Files, Options) + +files(Files, Options) -> + run([], Files, Options). + +%% @spec (Packages::[package()]) -> ok +%% @equiv packages(Packages, []) + +packages(Packages) -> + packages(Packages, []). + +%% @spec (Packages::[package()], Options::proplist()) -> ok +%% @type package() = atom() | string() +%% +%% @doc Runs EDoc on a set of packages. The `source_path' option is used +%% to locate the files; see {@link run/3} for details, including +%% options. This function automatically appends the current directory to +%% the source path. +%% +%% @equiv run(Packages, [], Options) + +packages(Packages, Options) -> + run(Packages, [], Options ++ [{source_path, [?CURRENT_DIR]}]). + +%% @spec (Application::atom()) -> ok +%% @equiv application(Application, []) + +application(App) -> + application(App, []). + +%% @spec (Application::atom(), Options::proplist()) -> ok +%% @doc Run EDoc on an application in its default app-directory. See +%% {@link application/3} for details. +%% @see application/1 + +application(App, Options) when is_atom(App) -> + case code:lib_dir(App) of + Dir when is_list(Dir) -> + application(App, Dir, Options); + _ -> + report("cannot find application directory for '~s'.", + [App]), + exit(error) + end. + +%% @spec (Application::atom(), Dir::filename(), Options::proplist()) +%% -> ok +%% @doc Run EDoc on an application located in the specified directory. +%% Tries to automatically set up good defaults. Unless the user +%% specifies otherwise: +%% <ul> +%% <li>The `doc' subdirectory will be used as the target directory, if +%% it exists; otherwise the application directory is used. +%% </li> +%% <li>The source code is assumed to be located in the `src' +%% subdirectory, if it exists, or otherwise in the application +%% directory itself. +%% </li> +%% <li>The {@link run/3. `subpackages'} option is turned on. All found +%% source files will be processed. +%% </li> +%% <li>The `include' subdirectory is automatically added to the +%% include path. (Only important if {@link read_source/2. +%% preprocessing} is turned on.) +%% </li> +%% </ul> +%% +%% See {@link run/3} for details, including options. +%% +%% @see application/2 + +application(App, Dir, Options) when is_atom(App) -> + Src = edoc_lib:try_subdir(Dir, ?SOURCE_DIR), + Overview = filename:join(edoc_lib:try_subdir(Dir, ?EDOC_DIR), + ?OVERVIEW_FILE), + Opts = Options ++ [{source_path, [Src]}, + subpackages, + {title, io_lib:fwrite("The ~s application", [App])}, + {overview, Overview}, + {dir, filename:join(Dir, ?EDOC_DIR)}, + {includes, [filename:join(Dir, "include")]}], + Opts1 = set_app_default(App, Dir, Opts), + %% Recursively document all subpackages of '' - i.e., everything. + run([''], [], [{application, App} | Opts1]). + +%% Try to set up a default application base URI in a smart way if the +%% user has not specified it explicitly. + +set_app_default(App, Dir0, Opts) -> + case proplists:get_value(app_default, Opts) of + undefined -> + AppName = atom_to_list(App), + Dir = edoc_lib:simplify_path(filename:absname(Dir0)), + AppDir = case filename:basename(Dir) of + AppName -> + filename:dirname(Dir); + _ -> + ?APP_DEFAULT + end, + [{app_default, AppDir} | Opts]; + _ -> + Opts + end. + +%% If no source files are found for a (specified) package, no package +%% documentation will be generated either (even if there is a +%% package-documentation file). This is the way it should be. For +%% specified files, use empty package (unless otherwise specified). The +%% assumed package is always used for creating the output. If the actual +%% module or package of the source differs from the assumption gathered +%% from the path and file name, a warning should be issued (since links +%% are likely to be incorrect). + +opt_defaults() -> + [packages]. + +opt_negations() -> + [{no_preprocess, preprocess}, + {no_subpackages, subpackages}, + {no_packages, packages}]. + +%% @spec run(Packages::[package()], +%% Files::[filename() | {package(), [filename()]}], +%% Options::proplist()) -> ok +%% @doc Runs EDoc on a given set of source files and/or packages. Note +%% that the doclet plugin module has its own particular options; see the +%% `doclet' option below. +%% +%% Also see {@link layout/2} for layout-related options, and +%% {@link get_doc/2} for options related to reading source +%% files. +%% +%% Options: +%% <dl> +%% <dt>{@type {app_default, string()@}} +%% </dt> +%% <dd>Specifies the default base URI for unknown applications. +%% </dd> +%% <dt>{@type {application, App::atom()@}} +%% </dt> +%% <dd>Specifies that the generated documentation describes the +%% application `App'. This mainly affects generated references. +%% </dd> +%% <dt>{@type {dir, filename()@}} +%% </dt> +%% <dd>Specifies the target directory for the generated documentation. +%% </dd> +%% <dt>{@type {doc_path, [string()]@}} +%% </dt> +%% <dd>Specifies a list of URI:s pointing to directories that contain +%% EDoc-generated documentation. URI without a `scheme://' part are +%% taken as relative to `file://'. (Note that such paths must use +%% `/' as separator, regardless of the host operating system.) +%% </dd> +%% <dt>{@type {doclet, Module::atom()@}} +%% </dt> +%% <dd>Specifies a callback module to be used for creating the +%% documentation. The module must export a function `run(Cmd, Ctxt)'. +%% The default doclet module is {@link edoc_doclet}; see {@link +%% edoc_doclet:run/2} for doclet-specific options. +%% </dd> +%% <dt>{@type {exclude_packages, [package()]@}} +%% </dt> +%% <dd>Lists packages to be excluded from the documentation. Typically +%% used in conjunction with the `subpackages' option. +%% </dd> +%% <dt>{@type {file_suffix, string()@}} +%% </dt> +%% <dd>Specifies the suffix used for output files. The default value is +%% `".html"'. Note that this also affects generated references. +%% </dd> +%% <dt>{@type {new, bool()@}} +%% </dt> +%% <dd>If the value is `true', any existing `edoc-info' file in the +%% target directory will be ignored and overwritten. The default +%% value is `false'. +%% </dd> +%% <dt>{@type {packages, bool()@}} +%% </dt> +%% <dd>If the value is `true', it it assumed that packages (module +%% namespaces) are being used, and that the source code directory +%% structure reflects this. The default value is `true'. (Usually, +%% this does the right thing even if all the modules belong to the +%% top-level "empty" package.) `no_packages' is an alias for +%% `{packages, false}'. See the `subpackages' option below for +%% further details. +%% +%% If the source code is organized in a hierarchy of +%% subdirectories although it does not use packages, use +%% `no_packages' together with the recursive-search `subpackages' +%% option (on by default) to automatically generate documentation +%% for all the modules. +%% </dd> +%% <dt>{@type {source_path, [filename()]@}} +%% </dt> +%% <dd>Specifies a list of file system paths used to locate the source +%% code for packages. +%% </dd> +%% <dt>{@type {source_suffix, string()@}} +%% </dt> +%% <dd>Specifies the expected suffix of input files. The default +%% value is `".erl"'. +%% </dd> +%% <dt>{@type {subpackages, bool()@}} +%% </dt> +%% <dd>If the value is `true', all subpackages of specified packages +%% will also be included in the documentation. The default value is +%% `false'. `no_subpackages' is an alias for `{subpackages, +%% false}'. See also the `exclude_packages' option. +%% +%% Subpackage source files are found by recursively searching +%% for source code files in subdirectories of the known source code +%% root directories. (Also see the `source_path' option.) Directory +%% names must begin with a lowercase letter and contain only +%% alphanumeric characters and underscore, or they will be ignored. +%% (For example, a subdirectory named `test-files' will not be +%% searched.) +%% </dd> +%% </dl> +%% +%% @see files/2 +%% @see packages/2 +%% @see application/2 + +%% NEW-OPTIONS: source_path, application +%% INHERIT-OPTIONS: init_context/1 +%% INHERIT-OPTIONS: expand_sources/2 +%% INHERIT-OPTIONS: target_dir_info/5 +%% INHERIT-OPTIONS: edoc_lib:find_sources/3 +%% INHERIT-OPTIONS: edoc_lib:run_doclet/2 +%% INHERIT-OPTIONS: edoc_lib:get_doc_env/4 + +run(Packages, Files, Opts0) -> + Opts = expand_opts(Opts0), + Ctxt = init_context(Opts), + Dir = Ctxt#context.dir, + Path = proplists:append_values(source_path, Opts), + Ss = sources(Path, Packages, Opts), + {Ss1, Ms} = expand_sources(expand_files(Files) ++ Ss, Opts), + Ps = [P || {_, P, _, _} <- Ss1], + App = proplists:get_value(application, Opts, ?NO_APP), + {App1, Ps1, Ms1} = target_dir_info(Dir, App, Ps, Ms, Opts), + %% The "empty package" is never included in the list of packages. + Ps2 = edoc_lib:unique(lists:sort(Ps1)) -- [''], + Ms2 = edoc_lib:unique(lists:sort(Ms1)), + Fs = package_files(Path, Ps2), + Env = edoc_lib:get_doc_env(App1, Ps2, Ms2, Opts), + Ctxt1 = Ctxt#context{env = Env}, + Cmd = #doclet_gen{sources = Ss1, + app = App1, + packages = Ps2, + modules = Ms2, + filemap = Fs + }, + F = fun (M) -> + M:run(Cmd, Ctxt1) + end, + edoc_lib:run_doclet(F, Opts). + +expand_opts(Opts0) -> + proplists:substitute_negations(opt_negations(), + Opts0 ++ opt_defaults()). + +%% NEW-OPTIONS: dir +%% DEFER-OPTIONS: run/3 + +init_context(Opts) -> + #context{dir = proplists:get_value(dir, Opts, ?CURRENT_DIR), + opts = Opts + }. + +%% INHERIT-OPTIONS: edoc_lib:find_sources/3 + +sources(Path, Packages, Opts) -> + lists:foldl(fun (P, Xs) -> + edoc_lib:find_sources(Path, P, Opts) ++ Xs + end, + [], Packages). + +package_files(Path, Packages) -> + Name = ?PACKAGE_FILE, % this is hard-coded for now + D = lists:foldl(fun (P, D) -> + F = edoc_lib:find_file(Path, P, Name), + dict:store(P, F, D) + end, + dict:new(), Packages), + fun (P) -> + case dict:find(P, D) of + {ok, F} -> F; + error -> "" + end + end. + +%% Expand user-specified sets of files. + +expand_files([{P, Fs1} | Fs]) -> + [{P, filename:basename(F), filename:dirname(F)} || F <- Fs1] + ++ expand_files(Fs); +expand_files([F | Fs]) -> + [{'', filename:basename(F), filename:dirname(F)} | + expand_files(Fs)]; +expand_files([]) -> + []. + +%% Create the (assumed) full module names. Keep only the first source +%% for each module, but preserve the order of the list. + +%% NEW-OPTIONS: source_suffix, packages +%% DEFER-OPTIONS: run/3 + +expand_sources(Ss, Opts) -> + Suffix = proplists:get_value(source_suffix, Opts, + ?DEFAULT_SOURCE_SUFFIX), + Ss1 = case proplists:get_bool(packages, Opts) of + true -> Ss; + false -> [{'',F,D} || {_P,F,D} <- Ss] + end, + expand_sources(Ss1, Suffix, sets:new(), [], []). + +expand_sources([{P, F, D} | Fs], Suffix, S, As, Ms) -> + M = list_to_atom(packages:concat(P, filename:rootname(F, Suffix))), + case sets:is_element(M, S) of + true -> + expand_sources(Fs, Suffix, S, As, Ms); + false -> + S1 = sets:add_element(M, S), + expand_sources(Fs, Suffix, S1, [{M, P, F, D} | As], + [M | Ms]) + end; +expand_sources([], _Suffix, _S, As, Ms) -> + {lists:reverse(As), lists:reverse(Ms)}. + +%% NEW-OPTIONS: new + +target_dir_info(Dir, App, Ps, Ms, Opts) -> + case proplists:get_bool(new, Opts) of + true -> + {App, Ps, Ms}; + false -> + {App1, Ps1, Ms1} = edoc_lib:read_info_file(Dir), + {if App == ?NO_APP -> App1; + true -> App + end, + Ps ++ Ps1, + Ms ++ Ms1} + end. + + +%% @hidden Not official yet + +toc(Dir) -> + toc(Dir, []). + +%% @equiv toc(Dir, Paths, []) +%% @hidden Not official yet + +%% NEW-OPTIONS: doc_path + +toc(Dir, Opts) -> + Paths = proplists:append_values(doc_path, Opts) + ++ edoc_lib:find_doc_dirs(), + toc(Dir, Paths, Opts). + +%% @doc Create a meta-level table of contents. +%% @hidden Not official yet + +%% INHERIT-OPTIONS: init_context/1 +%% INHERIT-OPTIONS: edoc_lib:run_doclet/2 +%% INHERIT-OPTIONS: edoc_lib:get_doc_env/4 + +toc(Dir, Paths, Opts0) -> + Opts = expand_opts(Opts0 ++ [{dir, Dir}]), + Ctxt = init_context(Opts), + Env = edoc_lib:get_doc_env('', [], [], Opts), + Ctxt1 = Ctxt#context{env = Env}, + F = fun (M) -> + M:run(#doclet_toc{paths=Paths}, Ctxt1) + end, + edoc_lib:run_doclet(F, Opts). + + +%% @spec read(File::filename()) -> string() +%% @equiv read(File, []) + +read(File) -> + read(File, []). + +%% @spec read(File::filename(), Options::proplist()) -> string() +%% +%% @doc Reads and processes a source file and returns the resulting +%% EDoc-text as a string. See {@link get_doc/2} and {@link layout/2} for +%% options. +%% +%% @see file/2 + +%% INHERIT-OPTIONS: get_doc/2, layout/2 + +read(File, Opts) -> + {_ModuleName, Doc} = get_doc(File, Opts), + layout(Doc, Opts). + + +%% @spec layout(Doc::edoc_module()) -> string() +%% @equiv layout(Doc, []) + +layout(Doc) -> + layout(Doc, []). + +%% @spec layout(Doc::edoc_module(), Options::proplist()) -> string() +%% +%% @doc Transforms EDoc module documentation data to text. The default +%% layout creates an HTML document. +%% +%% Options: +%% <dl> +%% <dt>{@type {layout, Module::atom()@}} +%% </dt> +%% <dd>Specifies a callback module to be used for formatting. The +%% module must export a function `module(Doc, Options)'. The +%% default callback module is {@link edoc_layout}; see {@link +%% edoc_layout:module/2} for layout-specific options. +%% </dd> +%% </dl> +%% +%% @see layout/1 +%% @see run/3 +%% @see read/2 +%% @see file/2 + +%% INHERIT-OPTIONS: edoc_lib:run_layout/2 + +layout(Doc, Opts) -> + F = fun (M) -> + M:module(Doc, Opts) + end, + edoc_lib:run_layout(F, Opts). + + +%% @spec (File) -> [comment()] +%% @equiv read_comments(File, []) + +read_comments(File) -> + read_comments(File, []). + +%% @spec read_comments(File::filename(), Options::proplist()) -> +%% [comment()] +%% where +%% comment() = {Line, Column, Indentation, Text}, +%% Line = integer(), +%% Column = integer(), +%% Indentation = integer(), +%% Text = [string()] +%% +%% @doc Extracts comments from an Erlang source code file. See the +%% module {@link //syntax_tools/erl_comment_scan} for details on the +%% representation of comments. Currently, no options are avaliable. + +read_comments(File, _Opts) -> + erl_comment_scan:file(File). + + +%% @spec (File) -> [syntaxTree()] +%% @equiv read_source(File, []) + +read_source(Name) -> + read_source(Name, []). + +%% @spec read_source(File::filename(), Options::proplist()) -> +%% [syntaxTree()] +%% +%% @type syntaxTree() = //syntax_tools/erl_syntax:syntaxTree() +%% +%% @doc Reads an Erlang source file and returns the list of "source code +%% form" syntax trees. +%% +%% Options: +%% <dl> +%% <dt>{@type {preprocess, bool()@}} +%% </dt> +%% <dd>If the value is `true', the source file will be read via the +%% Erlang preprocessor (`epp'). The default value is `false'. +%% `no_preprocess' is an alias for `{preprocess, false}'. +%% +%% Normally, preprocessing is not necessary for EDoc to work, but +%% if a file contains too exotic definitions or uses of macros, it +%% will not be possible to read it without preprocessing. <em>Note: +%% comments in included files will not be available to EDoc, even +%% with this option enabled.</em> +%% </dd> +%% <dt>{@type {includes, Path::[string()]@}} +%% </dt> +%% <dd>Specifies a list of directory names to be searched for include +%% files, if the `preprocess' option is turned on. Also used with +%% the `@headerfile' tag. The default value is the empty list. The +%% directory of the source file is always automatically appended to +%% the search path. +%% </dd> +%% <dt>{@type {macros, [{atom(), term()@}]@}} +%% </dt> +%% <dd>Specifies a list of pre-defined Erlang preprocessor (`epp') +%% macro definitions, used if the `preprocess' option is turned on. +%% The default value is the empty list.</dd> +%% </dl> +%% +%% @see get_doc/2 +%% @see //syntax_tools/erl_syntax + +%% NEW-OPTIONS: [no_]preprocess (preprocess -> includes, macros) + +read_source(Name, Opts0) -> + Opts = expand_opts(Opts0), + case read_source_1(Name, Opts) of + {ok, Forms} -> + check_forms(Forms, Name), + Forms; + {error, R} -> + error({"error reading file '~s'.", + [edoc_lib:filename(Name)]}), + exit({error, R}) + end. + +read_source_1(Name, Opts) -> + case proplists:get_bool(preprocess, Opts) of + true -> + read_source_2(Name, Opts); + false -> + epp_dodger:quick_parse_file(Name, Opts ++ [{no_fail, false}]) + end. + +read_source_2(Name, Opts) -> + Includes = proplists:append_values(includes, Opts) + ++ [filename:dirname(Name)], + Macros = proplists:append_values(macros, Opts), + epp:parse_file(Name, Includes, Macros). + +check_forms(Fs, Name) -> + Fun = fun (F) -> + case erl_syntax:type(F) of + error_marker -> + case erl_syntax:error_marker_info(F) of + {L, M, D} -> + error(L, Name, {format_error, M, D}); + + Other -> + report(Name, "unknown error in " + "source code: ~w.", [Other]) + end, + exit(error); + _ -> + ok + end + end, + lists:foreach(Fun, Fs). + + +%% @spec get_doc(File::filename()) -> {ModuleName, edoc_module()} +%% @equiv get_doc(File, []) + +get_doc(File) -> + get_doc(File, []). + +%% @spec get_doc(File::filename(), Options::proplist()) -> +%% {ModuleName, edoc_module()} +%% ModuleName = atom() +%% +%% @type edoc_module(). The EDoc documentation data for a module, +%% expressed as an XML document in {@link //xmerl. XMerL} format. See +%% the file <a href="../priv/edoc.dtd">`edoc.dtd'</a> for details. +%% +%% @doc Reads a source code file and extracts EDoc documentation data. +%% Note that without an environment parameter (see {@link get_doc/3}), +%% hypertext links may not be correct. +%% +%% Options: +%% <dl> +%% <dt>{@type {def, Macros@}} +%% </dt> +%% <dd><ul> +%% <li>`Macros' = {@type Macro | [Macro]}</li> +%% <li>`Macro' = {@type {Name::atom(), Text::string()@}}</li> +%% </ul> +%% Specifies a set of EDoc macro definitions. See +%% <a href="overview-summary.html#Macro_expansion">Inline macro expansion</a> +%% for details. +%% </dd> +%% <dt>{@type {hidden, bool()@}} +%% </dt> +%% <dd>If the value is `true', documentation of hidden functions will +%% also be included. The default value is `false'. +%% </dd> +%% <dt>{@type {private, bool()@}} +%% </dt> +%% <dd>If the value is `true', documentation of private functions will +%% also be included. The default value is `false'. +%% </dd> +%% <dt>{@type {todo, bool()@}} +%% </dt> +%% <dd>If the value is `true', To-Do notes written using `@todo' or +%% `@TODO' tags will be included in the documentation. The default +%% value is `false'. +%% </dd> +%% </dl> +%% +%% See {@link read_source/2}, {@link read_comments/2} and {@link +%% edoc_lib:get_doc_env/4} for further options. +%% +%% @see get_doc/3 +%% @see run/3 +%% @see edoc_extract:source/5 +%% @see read/2 +%% @see layout/2 + +%% INHERIT-OPTIONS: get_doc/3 +%% INHERIT-OPTIONS: edoc_lib:get_doc_env/4 + +get_doc(File, Opts) -> + Env = edoc_lib:get_doc_env(Opts), + get_doc(File, Env, Opts). + +%% @spec get_doc(File::filename(), Env::edoc_lib:edoc_env(), +%% Options::proplist()) -> {ModuleName, edoc_module()} +%% ModuleName = atom() +%% +%% @doc Like {@link get_doc/2}, but for a given environment +%% parameter. `Env' is an environment created by {@link +%% edoc_lib:get_doc_env/4}. + +%% INHERIT-OPTIONS: read_source/2, read_comments/2, edoc_extract:source/5 +%% DEFER-OPTIONS: get_doc/2 + +get_doc(File, Env, Opts) -> + edoc_extract:source(File, Env, Opts). diff --git a/lib/edoc/src/edoc.hrl b/lib/edoc/src/edoc.hrl new file mode 100644 index 0000000000..71cc1a52b9 --- /dev/null +++ b/lib/edoc/src/edoc.hrl @@ -0,0 +1,100 @@ +%% ===================================================================== +%% Header file for EDoc +%% +%% Copyright (C) 2001-2004 Richard Carlsson +%% +%% 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 +%% +%% Author contact: [email protected] +%% ===================================================================== + +%% Note: Documentation in this file is included by edoc_extract.erl + +-define(APPLICATION, edoc). +-define(INFO_FILE, "edoc-info"). +-define(PACKAGE_FILE, "package.edoc"). +-define(OVERVIEW_FILE, "overview.edoc"). +-define(PACKAGE_SUMMARY, "package-summary"). +-define(DEFAULT_SOURCE_SUFFIX, ".erl"). +-define(DEFAULT_FILE_SUFFIX, ".html"). +-define(DEFAULT_DOCLET, edoc_doclet). +-define(DEFAULT_LAYOUT, edoc_layout). +-define(APP_DEFAULT, "http://www.erlang.org/edoc/doc"). +-define(CURRENT_DIR, "."). +-define(SOURCE_DIR, "src"). +-define(EBIN_DIR, "ebin"). +-define(EDOC_DIR, "doc"). + +-include("edoc_doclet.hrl"). + +%% Module information + +%% @type module() = #module{name = [] | atom(), +%% parameters = none | [atom()], +%% functions = ordset(function_name()), +%% exports = ordset(function_name()), +%% attributes = ordset({atom(), term()}), +%% records = [{atom(), [{atom(), term()}]}]} +%% ordset(T) = sets:ordset(T) +%% function_name(T) = {atom(), integer()} + +-record(module, {name = [], + parameters = none, + functions = [], + exports = [], + attributes = [], + records = [] + }). + +%% Environment for generating documentation data + +-record(env, {module = [], + package = [], + root = "", + file_suffix, + package_summary, + apps, + modules, + packages, + app_default, + macros = [], + includes = [] + }). + +%% Simplified comment data + +%% @type comment() = #comment{line = integer(), +%% text = string()} + +-record(comment, {line = 0, text}). + +%% Module Entries (one per function, plus module header and footer) + +%% @type entry() = #entry{name = atom(), +%% args = [string()], +%% line = integer(), +%% export = bool(), +%% data = term()} + +-record(entry, {name, args = [], line = 0, export, data}). + +%% Generic tag information + +%% @type tag() = #tag{name = atom(), +%% line = integer(), +%% data = term()} + +-record(tag, {name, line = 0, data}). diff --git a/lib/edoc/src/edoc_data.erl b/lib/edoc/src/edoc_data.erl new file mode 100644 index 0000000000..124f8eb9a1 --- /dev/null +++ b/lib/edoc/src/edoc_data.erl @@ -0,0 +1,545 @@ +%% ===================================================================== +%% 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 2003 Richard Carlsson +%% @author Richard Carlsson <[email protected]> +%% @see edoc +%% @end +%% ===================================================================== + +%% @doc Building the EDoc external data structure. See the file +%% <a href="../priv/edoc.dtd">`edoc.dtd'</a> for details. + +-module(edoc_data). + +-export([module/4, package/4, overview/4, type/2]). + +-include("edoc.hrl"). + +%% TODO: report multiple definitions of the same type in the same module. +%% TODO: check that variables in @equiv are found in the signature +%% TODO: copy types from target (if missing) when using @equiv + +%% <!ELEMENT module (args?, description?, author*, copyright?, +%% version?, since?, deprecated?, see*, reference*, +%% todo?, behaviour*, callbacks?, typedecls?, +%% functions)> +%% <!ATTLIST module +%% name CDATA #REQUIRED +%% private NMTOKEN(yes | no) #IMPLIED +%% hidden NMTOKEN(yes | no) #IMPLIED +%% root CDATA #IMPLIED> +%% <!ELEMENT args (arg*)> +%% <!ELEMENT arg (argName, description?)> +%% <!ELEMENT argName (#PCDATA)> +%% <!ELEMENT description (briefDescription, fullDescription?)> +%% <!ELEMENT briefDescription (#PCDATA)> +%% <!ELEMENT fullDescription (#PCDATA)> +%% <!ELEMENT author EMPTY> +%% <!ATTLIST author +%% name CDATA #REQUIRED +%% email CDATA #IMPLIED +%% website CDATA #IMPLIED> +%% <!ELEMENT version (#PCDATA)> +%% <!ELEMENT since (#PCDATA)> +%% <!ELEMENT copyright (#PCDATA)> +%% <!ELEMENT deprecated (description)> +%% <!ELEMENT see (#PCDATA)> +%% <!ATTLIST see +%% name CDATA #REQUIRED +%% href CDATA #IMPLIED> +%% <!ELEMENT reference (#PCDATA)> +%% <!ELEMENT todo (#PCDATA)> +%% <!ELEMENT behaviour (#PCDATA)> +%% <!ATTLIST behaviour +%% href CDATA #IMPLIED> +%% <!ELEMENT callbacks (callback+)> +%% <!ELEMENT typedecls (typedecl+)> +%% <!ELEMENT typedecl (typedef, description?)> +%% <!ELEMENT functions (function+)> + +%% NEW-OPTIONS: private, hidden, todo +%% DEFER-OPTIONS: edoc_extract:source/4 + +module(Module, Entries, Env, Opts) -> + Name = atom_to_list(Module#module.name), + HeaderEntry = get_entry(module, Entries), + HeaderTags = HeaderEntry#entry.data, + AllTags = get_all_tags(Entries), + Functions = function_filter(Entries, Opts), + Out = {module, ([{name, Name}, + {root, Env#env.root}] + ++ case is_private(HeaderTags) of + true -> [{private, "yes"}]; + false -> [] + end + ++ case is_hidden(HeaderTags) of + true -> [{hidden, "yes"}]; + false -> [] + end), + (module_args(Module#module.parameters) + ++ behaviours(Module#module.attributes, Env) + ++ get_doc(HeaderTags) + ++ authors(HeaderTags) + ++ get_version(HeaderTags) + ++ get_since(HeaderTags) + ++ get_copyright(HeaderTags) + ++ get_deprecated(HeaderTags) + ++ sees(HeaderTags, Env) + ++ references(HeaderTags) + ++ todos(HeaderTags, Opts) + ++ [{typedecls, types(AllTags, Env)}, + {functions, functions(Functions, Env, Opts)} + | callbacks(Functions, Module, Env, Opts)]) + }, + xmerl_lib:expand_element(Out). + +get_all_tags(Es) -> + lists:flatmap(fun (#entry{data = Ts}) -> Ts end, Es). + +is_private(Ts) -> + get_tags(private, Ts) =/= []. + +description([]) -> + []; +description(Desc) -> + ShortDesc = edoc_lib:get_first_sentence(Desc), + [{description, + [{briefDescription, ShortDesc}, + {fullDescription, Desc}]}]. + +module_args(none) -> + []; +module_args(Vs) -> + [{args, [{arg, [{argName, [atom_to_list(V)]}]} || V <- Vs]}]. + +types(Tags, Env) -> + [{typedecl, [{label, edoc_types:to_label(Def)}], + [edoc_types:to_xml(Def, Env)] ++ description(Doc)} + || #tag{name = type, data = {Def, Doc}} <- Tags]. + +functions(Es, Env, Opts) -> + [function(N, As, Export, Ts, Env, Opts) + || #entry{name = {_,_}=N, args = As, export = Export, data = Ts} + <- Es]. + +function_filter(Es, Opts) -> + Private = proplists:get_bool(private, Opts), + Hidden = proplists:get_bool(hidden, Opts), + [E || E <- Es, function_filter(E, Private, Hidden)]. + +%% Note that only entries whose names have the form {_,_} are functions. +function_filter(#entry{name = {_,_}, export = Export, data = Ts}, + Private, Hidden) -> + ((Export andalso not is_private(Ts)) orelse Private) + andalso ((not is_hidden(Ts)) orelse Hidden); +function_filter(_, _, _) -> + false. + +is_hidden(Ts) -> + get_tags(hidden, Ts) =/= []. + +callbacks(Es, Module, Env, Opts) -> + case lists:any(fun (#entry{name = {behaviour_info, 1}}) -> true; + (_) -> false + end, + Es) of + true -> + try (Module#module.name):behaviour_info(callbacks) of + Fs -> + Fs1 = [{F,A} || {F,A} <- Fs, is_atom(F), is_integer(A)], + if Fs1 =:= [] -> + []; + true -> + [{callbacks, + [callback(F, Env, Opts) || F <- Fs1]}] + end + catch + _:_ -> [] + end; + false -> [] + end. + +%% <!ELEMENT callback EMPTY> +%% <!ATTLIST callback +%% name CDATA #REQUIRED +%% arity CDATA #REQUIRED> + +callback({N, A}, _Env, _Opts) -> + {callback, [{name, atom_to_list(N)}, + {arity, integer_to_list(A)}], + []}. + +%% <!ELEMENT function (args, typespec?, returns?, throws?, equiv?, +%% description?, since?, deprecated?, see*, todo?)> +%% <!ATTLIST function +%% name CDATA #REQUIRED +%% arity CDATA #REQUIRED +%% exported NMTOKEN(yes | no) #REQUIRED +%% label CDATA #IMPLIED> +%% <!ELEMENT args (arg*)> +%% <!ELEMENT arg (argName, description?)> +%% <!ELEMENT argName (#PCDATA)> +%% <!ELEMENT returns (description)> +%% <!ELEMENT throws (type, localdef*)> +%% <!ELEMENT equiv (expr, see?)> +%% <!ELEMENT expr (#PCDATA)> + +function({N, A}, As, Export, Ts, Env, Opts) -> + {Args, Ret, Spec} = signature(Ts, As, Env), + {function, [{name, atom_to_list(N)}, + {arity, integer_to_list(A)}, + {exported, case Export of + true -> "yes"; + false -> "no" + end}, + {label, edoc_refs:to_label(edoc_refs:function(N, A))}], + [{args, [{arg, [{argName, [atom_to_list(A)]}] ++ description(D)} + || {A, D} <- Args]}] + ++ Spec + ++ case Ret of + [] -> []; + _ -> [{returns, description(Ret)}] + end + ++ get_throws(Ts, Env) + ++ get_equiv(Ts, Env) + ++ get_doc(Ts) + ++ get_since(Ts) + ++ get_deprecated(Ts, N, A, Env) + ++ sees(Ts, Env) + ++ todos(Ts, Opts) + }. + +get_throws(Ts, Env) -> + case get_tags(throws, Ts) of + [Throws] -> + Type = Throws#tag.data, + [edoc_types:to_xml(Type, Env)]; + [] -> + [] + end. + +get_equiv(Ts, Env) -> + case get_tags(equiv, Ts) of + [Equiv] -> + Expr = Equiv#tag.data, + See = case get_expr_ref(Equiv#tag.data) of + none -> []; + Ref -> + [see(Ref, [edoc_refs:to_string(Ref)], Env)] + end, + [{equiv, [{expr, [erl_prettypr:format(Expr)]} | See]}]; + [] -> + [] + end. + +get_doc(Ts) -> + case get_tags(doc, Ts) of + [T] -> + description(T#tag.data); + [] -> + [] + end. + +get_copyright(Ts) -> + get_pcdata_tag(copyright, Ts). + +get_version(Ts) -> + get_pcdata_tag(version, Ts). + +get_since(Ts) -> + get_pcdata_tag(since, Ts). + +get_pcdata_tag(Tag, Ts) -> + case get_tags(Tag, Ts) of + [T] -> + [{Tag, [T#tag.data]}]; + [] -> + [] + end. + +%% Deprecation declarations for xref: +%% +%% -deprecated(Info). +%% Info = Spec | [Spec] +%% Spec = module | {F,A} | {F,A,Details}} +%% Details = next_version | next_major_release | eventually +%% (EXTENSION: | string() | {M1,F1,A1}} +%% TODO: use info from '-deprecated(...)' (xref-)declarations. + +get_deprecated(Ts) -> + case get_tags(deprecated, Ts) of + [T] -> + [{deprecated, description(T#tag.data)}]; + [] -> + [] + end. + +get_deprecated(Ts, F, A, Env) -> + case get_deprecated(Ts) of + [] -> + M = Env#env.module, + case otp_internal:obsolete(M, F, A) of + {Tag, Text} when Tag =:= deprecated; Tag =:= removed -> + deprecated([Text]); + {Tag, Repl, _Rel} when Tag =:= deprecated; Tag =:= removed -> + deprecated(Repl, Env); + _ -> + [] + end; + Es -> + Es + end. + +deprecated(Repl, Env) -> + {Text, Ref} = replacement_function(Env#env.module, Repl), + Desc = ["Use ", {a, href(Ref, Env), [{code, [Text]}]}, " instead."], + deprecated(Desc). + +deprecated(Desc) -> + [{deprecated, description(Desc)}]. + +replacement_function(M0, {M,F,A}) when is_list(A) -> + %% refer to the largest listed arity - the most general version + replacement_function(M0, {M,F,lists:last(lists:sort(A))}); +replacement_function(M, {M,F,A}) -> + {io_lib:fwrite("~w/~w", [F, A]), edoc_refs:function(F, A)}; +replacement_function(_, {M,F,A}) -> + {io_lib:fwrite("~w:~w/~w", [M, F, A]), edoc_refs:function(M, F, A)}. + +get_expr_ref(Expr) -> + case catch {ok, erl_syntax_lib:analyze_application(Expr)} of + {ok, {F, A}} when is_atom(F), is_integer(A) -> + edoc_refs:function(F, A); + {ok, {M, {F, A}}} when is_atom(M), is_atom(F), is_integer(A) -> + edoc_refs:function(M, F, A); + _ -> + none + end. + +authors(Ts) -> + [author(Info) || #tag{data = Info} <- get_tags(author, Ts)]. + +%% <!ATTLIST author +%% name CDATA #REQUIRED +%% email CDATA #IMPLIED +%% website CDATA #IMPLIED> + +author({Name, Mail, URI}) -> + %% At least one of Name and Mail must be nonempty in the tag. + {author, ([{name, if Name =:= "" -> Mail; + true -> Name + end}] + ++ if Mail =:= "" -> + case lists:member($@, Name) of + true -> [{email, Name}]; + false -> [] + end; + true -> [{email, Mail}] + end + ++ if URI =:= "" -> []; + true -> [{website, URI}] + end), []}. + +behaviours(As, Env) -> + [{behaviour, href(edoc_refs:module(B), Env), [atom_to_list(B)]} + || {behaviour, B} <- As, is_atom(B)]. + +sees(Tags, Env) -> + Ts = get_tags(see, Tags), + Rs = lists:keysort(1, [Data || #tag{data = Data} <- Ts]), + [see(Ref, XML, Env) || {Ref, XML} <- Rs]. + +see(Ref, [], Env) -> + see(Ref, [edoc_refs:to_string(Ref)], Env); +see(Ref, XML, Env) -> + {see, [{name, edoc_refs:to_string(Ref)}] ++ href(Ref, Env), XML}. + +href(Ref, Env) -> + [{href, edoc_refs:get_uri(Ref, Env)}] + ++ case edoc_refs:is_top(Ref, Env) of + true -> + [{target, "_top"}]; + false -> + [] + end. + +references(Tags) -> + [{reference, XML} || #tag{data = XML} <- get_tags(reference, Tags)]. + +todos(Tags, Opts) -> + case proplists:get_bool(todo, Opts) of + true -> + [{todo, XML} || #tag{data = XML} <- get_tags('todo', Tags)]; + false -> + [] + end. + +signature(Ts, As, Env) -> + case get_tags(spec, Ts) of + [T] -> + Spec = T#tag.data, + R = merge_returns(Spec, Ts), + As0 = edoc_types:arg_names(Spec), + Ds0 = edoc_types:arg_descs(Spec), + %% choose names in spec before names in code + P = dict:from_list(params(Ts)), + As1 = merge_args(As0, As, Ds0, P), + %% check_params(As1, P), + Spec1 = edoc_types:set_arg_names(Spec, [A || {A,_} <- As1]), + {As1, R, [edoc_types:to_xml(Spec1, Env)]}; + [] -> + S = sets:new(), + {[{A, ""} || A <- fix_argnames(As, S, 1)], [], []} + end. + +params(Ts) -> + [T#tag.data || T <- get_tags(param, Ts)]. + +%% check_params(As, P) -> +%% case dict:keys(P) -- [N || {N,_} <- As] of +%% [] -> ok; +%% Ps -> error %% TODO: report @param declarations with no match +%% end. + +merge_returns(Spec, Ts) -> + case get_tags(returns, Ts) of + [] -> + case edoc_types:range_desc(Spec) of + "" -> []; + Txt -> [Txt] + end; + [T] -> T#tag.data + end. + +%% Names are chosen from the first list (the specification) if possible. +%% Descriptions specified with @param (in P dict) override descriptions +%% from the spec (in Ds). + +merge_args(As, As1, Ds, P) -> + merge_args(As, As1, Ds, [], P, sets:new(), 1). + +merge_args(['_' | As], ['_' | As1], [D | Ds], Rs, P, S, N) -> + merge_args(As, As1, Ds, Rs, P, S, N, make_name(N, S), D); +merge_args(['_' | As], [A | As1], [D | Ds], Rs, P, S, N) -> + merge_args(As, As1, Ds, Rs, P, S, N, A, D); +merge_args([A | As], [_ | As1], [D | Ds], Rs, P, S, N) -> + merge_args(As, As1, Ds, Rs, P, S, N, A, D); +merge_args([], [], [], Rs, _P, _S, _N) -> + lists:reverse(Rs). + +merge_args(As, As1, Ds, Rs, P, S, N, A, D0) -> + D = case dict:find(A, P) of + {ok, D1} -> D1; + error when D0 =:= [] -> []; % no description + error -> [D0] % a simple-xml text element + end, + merge_args(As, As1, Ds, [{A, D} | Rs], P, + sets:add_element(A, S), N + 1). + +fix_argnames(['_' | As], S, N) -> + A = make_name(N, S), + [A | fix_argnames(As, sets:add_element(A, S), N + 1)]; +fix_argnames([A | As], S, N) -> + [A | fix_argnames(As, sets:add_element(A, S), N + 1)]; +fix_argnames([], _S, _N) -> + []. + +make_name(N, S) -> + make_name(N, S, "X"). + +make_name(N, S, Base) -> + A = list_to_atom(Base ++ integer_to_list(N)), + case sets:is_element(A, S) of + true -> + make_name(N, S, Base ++ "x"); + false -> + A + end. + +get_entry(Name, [#entry{name = Name} = E | _Es]) -> E; +get_entry(Name, [_ | Es]) -> get_entry(Name, Es). + +get_tags(Tag, [#tag{name = Tag} = T | Ts]) -> [T | get_tags(Tag, Ts)]; +get_tags(Tag, [_ | Ts]) -> get_tags(Tag, Ts); +get_tags(_, []) -> []. + +%% --------------------------------------------------------------------- + +type(T, Env) -> + xmerl_lib:expand_element({type, [edoc_types:to_xml(T, Env)]}). + +%% <!ELEMENT package (description?, author*, copyright?, version?, +%% since?, deprecated?, see*, reference*, todo?, +%% modules)> +%% <!ATTLIST package +%% name CDATA #REQUIRED +%% root CDATA #IMPLIED> +%% <!ELEMENT modules (module+)> + +package(Package, Tags, Env, Opts) -> + Env1 = Env#env{package = Package, + root = edoc_refs:relative_package_path('', Package)}, + xmerl_lib:expand_element(package_1(Package, Tags, Env1, Opts)). + +package_1(Package, Tags, Env, Opts) -> + {package, [{root, Env#env.root}], + ([{packageName, [atom_to_list(Package)]}] + ++ get_doc(Tags) + ++ authors(Tags) + ++ get_copyright(Tags) + ++ get_version(Tags) + ++ get_since(Tags) + ++ get_deprecated(Tags) + ++ sees(Tags, Env) + ++ references(Tags) + ++ todos(Tags, Opts)) + }. + +%% <!ELEMENT overview (title, description?, author*, copyright?, version?, +%% since?, see*, reference*, todo?, packages, modules)> +%% <!ATTLIST overview +%% root CDATA #IMPLIED> +%% <!ELEMENT title (#PCDATA)> + +overview(Title, Tags, Env, Opts) -> + Env1 = Env#env{package = '', + root = ""}, + xmerl_lib:expand_element(overview_1(Title, Tags, Env1, Opts)). + +overview_1(Title, Tags, Env, Opts) -> + {overview, [{root, Env#env.root}], + ([{title, [get_title(Tags, Title)]}] + ++ get_doc(Tags) + ++ authors(Tags) + ++ get_copyright(Tags) + ++ get_version(Tags) + ++ get_since(Tags) + ++ sees(Tags, Env) + ++ references(Tags) + ++ todos(Tags, Opts)) + }. + +get_title(Ts, Default) -> + case get_tags(title, Ts) of + [T] -> + T#tag.data; + [] -> + Default + end. diff --git a/lib/edoc/src/edoc_doclet.erl b/lib/edoc/src/edoc_doclet.erl new file mode 100644 index 0000000000..f1d876d593 --- /dev/null +++ b/lib/edoc/src/edoc_doclet.erl @@ -0,0 +1,521 @@ +%% ===================================================================== +%% 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$ +%% +%% @copyright 2003-2006 Richard Carlsson +%% @author Richard Carlsson <[email protected]> +%% @see edoc +%% @end +%% ===================================================================== + +%% @doc Standard doclet module for EDoc. + +%% Note that this is written so that it is *not* depending on edoc.hrl! + +%% TODO: copy "doc-files" subdirectories, recursively. +%% TODO: generate summary page of TODO-notes +%% TODO: generate summary page of deprecated things +%% TODO: generate decent indexes over modules, methods, records, etc. + +-module(edoc_doclet). + +-export([run/2]). + +-import(edoc_report, [report/2, warning/2]). + +%% @headerfile "edoc_doclet.hrl" +-include("../include/edoc_doclet.hrl"). + +-define(EDOC_APP, edoc). +-define(DEFAULT_FILE_SUFFIX, ".html"). +-define(INDEX_FILE, "index.html"). +-define(OVERVIEW_FILE, "overview.edoc"). +-define(PACKAGE_SUMMARY, "package-summary.html"). +-define(OVERVIEW_SUMMARY, "overview-summary.html"). +-define(PACKAGES_FRAME, "packages-frame.html"). +-define(MODULES_FRAME, "modules-frame.html"). +-define(STYLESHEET, "stylesheet.css"). +-define(IMAGE, "erlang.png"). +-define(NL, "\n"). + +-include("xmerl.hrl"). + +%% Sources is the list of inputs in the order they were found. Packages +%% and Modules are sorted lists of atoms without duplicates. (They +%% usually include the data from the edoc-info file in the target +%% directory, if it exists.) Note that the "empty package" is never +%% included in Packages! + +%% @spec (Command::doclet_gen() | doclet_toc(), edoc_context()) -> ok +%% @doc Main doclet entry point. See the file <a +%% href="../include/edoc_doclet.hrl">`edoc_doclet.hrl'</a> for the data +%% structures used for passing parameters. +%% +%% Also see {@link edoc:layout/2} for layout-related options, and +%% {@link edoc:get_doc/2} for options related to reading source +%% files. +%% +%% Options: +%% <dl> +%% <dt>{@type {file_suffix, string()@}} +%% </dt> +%% <dd>Specifies the suffix used for output files. The default value is +%% `".html"'. +%% </dd> +%% <dt>{@type {hidden, bool()@}} +%% </dt> +%% <dd>If the value is `true', documentation of hidden modules and +%% functions will also be included. The default value is `false'. +%% </dd> +%% <dt>{@type {overview, edoc:filename()@}} +%% </dt> +%% <dd>Specifies the name of the overview-file. By default, this doclet +%% looks for a file `"overview.edoc"' in the target directory. +%% </dd> +%% <dt>{@type {private, bool()@}} +%% </dt> +%% <dd>If the value is `true', documentation of private modules and +%% functions will also be included. The default value is `false'. +%% </dd> +%% <dt>{@type {stylesheet, string()@}} +%% </dt> +%% <dd>Specifies the URI used for referencing the stylesheet. The +%% default value is `"stylesheet.css"'. If an empty string is +%% specified, no stylesheet reference will be generated. +%% </dd> +%% <dt>{@type {stylesheet_file, edoc:filename()@}} +%% </dt> +%% <dd>Specifies the name of the stylesheet file. By default, this +%% doclet uses the file `"stylesheet.css"' in the `priv' +%% subdirectory of the EDoc installation directory. The named file +%% will be copied to the target directory. +%% </dd> +%% <dt>{@type {title, string()@}} +%% </dt> +%% <dd>Specifies the title of the overview-page. +%% </dd> +%% </dl> + +%% INHERIT-OPTIONS: title/2 +%% INHERIT-OPTIONS: sources/5 +%% INHERIT-OPTIONS: overview/4 +%% INHERIT-OPTIONS: copy_stylesheet/2 +%% INHERIT-OPTIONS: stylesheet/1 + +run(#doclet_gen{}=Cmd, Ctxt) -> + gen(Cmd#doclet_gen.sources, + Cmd#doclet_gen.app, + Cmd#doclet_gen.packages, + Cmd#doclet_gen.modules, + Cmd#doclet_gen.filemap, + Ctxt); +run(#doclet_toc{}=Cmd, Ctxt) -> + toc(Cmd#doclet_toc.paths, Ctxt). + +gen(Sources, App, Packages, Modules, FileMap, Ctxt) -> + Dir = Ctxt#context.dir, + Env = Ctxt#context.env, + Options = Ctxt#context.opts, + Title = title(App, Options), + CSS = stylesheet(Options), + {Modules1, Error} = sources(Sources, Dir, Modules, Env, Options), + modules_frame(Dir, Modules1, Title, CSS), + packages(Packages, Dir, FileMap, Env, Options), + packages_frame(Dir, Packages, Title, CSS), + overview(Dir, Title, Env, Options), + index_file(Dir, length(Packages) > 1, Title), + edoc_lib:write_info_file(App, Packages, Modules1, Dir), + copy_stylesheet(Dir, Options), + copy_image(Dir), + %% handle postponed error during processing of source files + case Error of + true -> exit(error); + false -> ok + end. + + +%% NEW-OPTIONS: title +%% DEFER-OPTIONS: run/2 + +title(App, Options) -> + proplists:get_value(title, Options, + if App == ?NO_APP -> + "Overview"; + true -> + io_lib:fwrite("Application: ~s", [App]) + end). + + +%% Processing the individual source files. + +%% NEW-OPTIONS: file_suffix, private, hidden +%% INHERIT-OPTIONS: edoc:layout/2 +%% INHERIT-OPTIONS: edoc:get_doc/3 +%% DEFER-OPTIONS: run/2 + +sources(Sources, Dir, Modules, Env, Options) -> + Suffix = proplists:get_value(file_suffix, Options, + ?DEFAULT_FILE_SUFFIX), + Private = proplists:get_bool(private, Options), + Hidden = proplists:get_bool(hidden, Options), + {Ms, E} = lists:foldl(fun (Src, {Set, Error}) -> + source(Src, Dir, Suffix, Env, Set, + Private, Hidden, Error, Options) + end, + {sets:new(), false}, Sources), + {[M || M <- Modules, sets:is_element(M, Ms)], E}. + + +%% Generating documentation for a source file, adding its name to the +%% set if it was successful. Errors are just flagged at this stage, +%% allowing all source files to be processed even if some of them fail. + +source({M, P, Name, Path}, Dir, Suffix, Env, Set, Private, Hidden, + Error, Options) -> + File = filename:join(Path, Name), + case catch {ok, edoc:get_doc(File, Env, Options)} of + {ok, {Module, Doc}} -> + check_name(Module, M, P, File), + case ((not is_private(Doc)) orelse Private) + andalso ((not is_hidden(Doc)) orelse Hidden) of + true -> + Text = edoc:layout(Doc, Options), + Name1 = packages:last(M) ++ Suffix, + edoc_lib:write_file(Text, Dir, Name1, P), + {sets:add_element(Module, Set), Error}; + false -> + {Set, Error} + end; + R -> + report("skipping source file '~s': ~W.", [File, R, 15]), + {Set, true} + end. + +check_name(M, M0, P0, File) -> + P = list_to_atom(packages:strip_last(M)), + N = packages:last(M), + N0 = packages:last(M0), + case N of + [$? | _] -> + %% A module name of the form '?...' is assumed to be caused + %% by the epp_dodger parser when the module declaration has + %% the form '-module(?MACRO).'; skip the filename check. + ok; + _ -> + if N =/= N0 -> + warning("file '~s' actually contains module '~s'.", + [File, M]); + true -> + ok + end + end, + if P =/= P0 -> + warning("file '~s' belongs to package '~s', not '~s'.", + [File, P, P0]); + true -> + ok + end. + + +%% Generating the summary files for packages. + +%% INHERIT-OPTIONS: read_file/4 +%% INHERIT-OPTIONS: edoc_lib:run_layout/2 + +packages(Packages, Dir, FileMap, Env, Options) -> + lists:foreach(fun (P) -> + package(P, Dir, FileMap, Env, Options) + end, + Packages). + +package(P, Dir, FileMap, Env, Opts) -> + Tags = case FileMap(P) of + "" -> + []; + File -> + read_file(File, package, Env, Opts) + end, + Data = edoc_data:package(P, Tags, Env, Opts), + F = fun (M) -> + M:package(Data, Opts) + end, + Text = edoc_lib:run_layout(F, Opts), + edoc_lib:write_file(Text, Dir, ?PACKAGE_SUMMARY, P). + + +%% Creating an index file, with some frames optional. +%% TODO: get rid of frames, or change doctype to Frameset + +index_file(Dir, Packages, Title) -> + Frame1 = {frame, [{src,?PACKAGES_FRAME}, + {name,"packagesFrame"},{title,""}], + []}, + Frame2 = {frame, [{src,?MODULES_FRAME}, + {name,"modulesFrame"},{title,""}], + []}, + Frame3 = {frame, [{src,?OVERVIEW_SUMMARY}, + {name,"overviewFrame"},{title,""}], + []}, + Frameset = {frameset, [{cols,"20%,80%"}], + case Packages of + true -> + [?NL, + {frameset, [{rows,"30%,70%"}], + [?NL, Frame1, ?NL, Frame2, ?NL]} + ]; + false -> + [?NL, Frame2, ?NL] + end + ++ [?NL, Frame3, ?NL, + {noframes, + [?NL, + {h2, ["This page uses frames"]}, + ?NL, + {p, ["Your browser does not accept frames.", + ?NL, br, + "You should go to the ", + {a, [{href, ?OVERVIEW_SUMMARY}], + ["non-frame version"]}, + " instead.", ?NL]}, + ?NL]}, + ?NL]}, + XML = xhtml_1(Title, [], Frameset), + Text = xmerl:export_simple([XML], xmerl_html, []), + edoc_lib:write_file(Text, Dir, ?INDEX_FILE). + +packages_frame(Dir, Ps, Title, CSS) -> + Body = [?NL, + {h2, [{class, "indextitle"}], ["Packages"]}, + ?NL, + {table, [{width, "100%"}, {border, 0}, + {summary, "list of packages"}], + lists:concat( + [[?NL, + {tr, [{td, [], [{a, [{href, package_ref(P)}, + {target,"overviewFrame"}, + {class, "package"}], + [atom_to_list(P)]}]}]}] + || P <- Ps])}, + ?NL], + XML = xhtml(Title, CSS, Body), + Text = xmerl:export_simple([XML], xmerl_html, []), + edoc_lib:write_file(Text, Dir, ?PACKAGES_FRAME). + +modules_frame(Dir, Ms, Title, CSS) -> + Body = [?NL, + {h2, [{class, "indextitle"}], ["Modules"]}, + ?NL, + {table, [{width, "100%"}, {border, 0}, + {summary, "list of modules"}], + lists:concat( + [[?NL, + {tr, [{td, [], + [{a, [{href, module_ref(M)}, + {target, "overviewFrame"}, + {class, "module"}], + [atom_to_list(M)]}]}]}] + || M <- Ms])}, + ?NL], + XML = xhtml(Title, CSS, Body), + Text = xmerl:export_simple([XML], xmerl_html, []), + edoc_lib:write_file(Text, Dir, ?MODULES_FRAME). + +module_ref(M) -> + edoc_refs:relative_package_path(M, '') ++ ?DEFAULT_FILE_SUFFIX. + +package_ref(P) -> + edoc_lib:join_uri(edoc_refs:relative_package_path(P, ''), + ?PACKAGE_SUMMARY). + +xhtml(Title, CSS, Content) -> + xhtml_1(Title, CSS, {body, [{bgcolor, "white"}], Content}). + +xhtml_1(Title, CSS, Body) -> + {html, [?NL, + {head, [?NL, {title, [Title]}, ?NL] ++ CSS}, + ?NL, + Body, + ?NL] + }. + +%% NEW-OPTIONS: overview +%% INHERIT-OPTIONS: read_file/4 +%% INHERIT-OPTIONS: edoc_lib:run_layout/2 +%% INHERIT-OPTIONS: edoc_extract:file/4 +%% DEFER-OPTIONS: run/2 + +overview(Dir, Title, Env, Opts) -> + File = proplists:get_value(overview, Opts, + filename:join(Dir, ?OVERVIEW_FILE)), + Tags = read_file(File, overview, Env, Opts), + Data = edoc_data:overview(Title, Tags, Env, Opts), + F = fun (M) -> + M:overview(Data, Opts) + end, + Text = edoc_lib:run_layout(F, Opts), + edoc_lib:write_file(Text, Dir, ?OVERVIEW_SUMMARY). + + +copy_image(Dir) -> + case code:priv_dir(?EDOC_APP) of + PrivDir when is_list(PrivDir) -> + From = filename:join(PrivDir, ?IMAGE), + edoc_lib:copy_file(From, filename:join(Dir, ?IMAGE)); + _ -> + report("cannot find default image file.", []), + exit(error) + end. + +%% NEW-OPTIONS: stylesheet_file +%% DEFER-OPTIONS: run/2 + +copy_stylesheet(Dir, Options) -> + case proplists:get_value(stylesheet, Options) of + undefined -> + From = case proplists:get_value(stylesheet_file, Options) of + File when is_list(File) -> + File; + _ -> + case code:priv_dir(?EDOC_APP) of + PrivDir when is_list(PrivDir) -> + filename:join(PrivDir, ?STYLESHEET); + _ -> + report("cannot find default " + "stylesheet file.", []), + exit(error) + end + end, + edoc_lib:copy_file(From, filename:join(Dir, ?STYLESHEET)); + _ -> + ok + end. + +%% NEW-OPTIONS: stylesheet +%% DEFER-OPTIONS: run/2 + +stylesheet(Options) -> + case proplists:get_value(stylesheet, Options) of + "" -> + []; + S -> + Ref = case S of + undefined -> + ?STYLESHEET; + "" -> + ""; % no stylesheet + S when is_list(S) -> + S; + _ -> + report("bad value for option 'stylesheet'.", + []), + exit(error) + end, + [{link, [{rel, "stylesheet"}, + {type, "text/css"}, + {href, Ref}, + {title, "EDoc"}], []}, + ?NL] + end. + +is_private(E) -> + case get_attrval(private, E) of + "yes" -> true; + _ -> false + end. + +is_hidden(E) -> + case get_attrval(hidden, E) of + "yes" -> true; + _ -> false + end. + +get_attrval(Name, #xmlElement{attributes = As}) -> + case get_attr(Name, As) of + [#xmlAttribute{value = V}] -> + V; + [] -> "" + end. + +get_attr(Name, [#xmlAttribute{name = Name} = A | As]) -> + [A | get_attr(Name, As)]; +get_attr(Name, [_ | As]) -> + get_attr(Name, As); +get_attr(_, []) -> + []. + +%% Read external source file. Fails quietly, returning empty tag list. + +%% INHERIT-OPTIONS: edoc_extract:file/4 + +read_file(File, Context, Env, Opts) -> + case edoc_extract:file(File, Context, Env, Opts) of + {ok, Tags} -> + Tags; + {error, _} -> + [] + end. + + +%% TODO: FIXME: meta-level index generation + +%% Creates a Table of Content from a list of Paths (ie paths to applications) +%% and an overview file. + +-define(EDOC_DIR, "doc"). +-define(INDEX_DIR, "doc/index"). +-define(CURRENT_DIR, "."). + +toc(Paths, Ctxt) -> + Opts = Ctxt#context.opts, + Dir = Ctxt#context.dir, + Env = Ctxt#context.env, + app_index_file(Paths, Dir, Env, Opts). + +%% TODO: FIXME: it's unclear how much of this is working at all + +%% NEW-OPTIONS: title +%% INHERIT-OPTIONS: overview/4 + +app_index_file(Paths, Dir, Env, Options) -> + Title = proplists:get_value(title, Options,"Overview"), +% Priv = proplists:get_bool(private, Options), + CSS = stylesheet(Options), + Apps1 = [{filename:dirname(A),filename:basename(A)} || A <- Paths], + index_file(Dir, false, Title), + application_frame(Dir, Apps1, Title, CSS), + modules_frame(Dir, [], Title, CSS), + overview(Dir, Title, Env, Options), +% edoc_lib:write_info_file(Prod, [], Modules1, Dir), + copy_stylesheet(Dir, Options). + +application_frame(Dir, Apps, Title, CSS) -> + Body = [?NL, + {h2, ["Applications"]}, + ?NL, + {table, [{width, "100%"}, {border, 0}], + lists:concat( + [[{tr, [{td, [], [{a, [{href,app_ref(Path,App)}, + {target,"_top"}], + [App]}]}]}] + || {Path,App} <- Apps])}, + ?NL], + XML = xhtml(Title, CSS, Body), + Text = xmerl:export_simple([XML], xmerl_html, []), + edoc_lib:write_file(Text, Dir, ?MODULES_FRAME). + +app_ref(Path,M) -> + filename:join([Path,M,?EDOC_DIR,?INDEX_FILE]). diff --git a/lib/edoc/src/edoc_extract.erl b/lib/edoc/src/edoc_extract.erl new file mode 100644 index 0000000000..ea2755f7aa --- /dev/null +++ b/lib/edoc/src/edoc_extract.erl @@ -0,0 +1,584 @@ +%% ===================================================================== +%% 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$ +%% +%% @copyright 2001-2003 Richard Carlsson +%% @author Richard Carlsson <[email protected]> +%% @see edoc +%% @end +%% ===================================================================== + +%% @doc EDoc documentation extraction. + +-module(edoc_extract). + +-export([source/3, source/4, source/5, header/3, header/4, header/5, + file/4, text/4]). + +-import(edoc_report, [report/3, warning/3]). + +%% %% @headerfile "edoc.hrl" (disabled until it can be made private) +-include("edoc.hrl"). + +%% @type filename() = file:filename() + +%% @spec source(File::filename(), Env::edoc_env(), Options::proplist()) +%% -> {ModuleName, edoc_module()} +%% ModuleName = atom() +%% proplist() = [term()] +%% +%% @doc Like {@link source/5}, but reads the syntax tree and the +%% comments from the specified file. +%% +%% @see edoc:read_comments/2 +%% @see edoc:read_source/2 +%% @see source/4 + +source(File, Env, Opts) -> + Forms = edoc:read_source(File, Opts), + Comments = edoc:read_comments(File, Opts), + source(Forms, Comments, File, Env, Opts). + +%% @spec source(Forms, Comments::[comment()], File::filename(), +%% Env::edoc_env(), Options::proplist()) -> +%% {ModuleName, edoc_module()} +%% +%% Forms = syntaxTree() | [syntaxTree()] +%% comment() = {Line, Column, Indentation, Text} +%% Line = integer() +%% Column = integer() +%% Indentation = integer() +%% Text = [string()] +%% ModuleName = atom() +%% +%% @doc Like {@link source/4}, but first inserts the given comments in +%% the syntax trees. The syntax trees must contain valid position +%% information. (Cf. {@link edoc:read_comments/2}.) +%% +%% @see edoc:read_comments/2 +%% @see edoc:read_source/2 +%% @see source/3 +%% @see source/4 +%% @see //syntax_tools/erl_recomment + +source(Forms, Comments, File, Env, Opts) when is_list(Forms) -> + Forms1 = erl_syntax:form_list(Forms), + source(Forms1, Comments, File, Env, Opts); +source(Forms, Comments, File, Env, Opts) -> + Tree = erl_recomment:quick_recomment_forms(Forms, Comments), + source(Tree, File, Env, Opts). + +%% @spec source(Forms, File::filename(), Env::edoc_env(), +%% Options::proplist()) -> +%% {ModuleName, edoc_module()} +%% +%% Forms = syntaxTree() | [syntaxTree()] +%% ModuleName = atom() +%% edoc_module() = edoc:edoc_module() +%% @type edoc_env() = edoc_lib:edoc_env() +%% +%% @doc Extracts EDoc documentation from commented source code syntax +%% trees. The given `Forms' must be a single syntax tree of +%% type `form_list', or a list of syntax trees representing +%% "program forms" (cf. {@link edoc:read_source/2}. +%% `Env' is an environment created by {@link +%% edoc_lib:get_doc_env/4}. The `File' argument is used for +%% error reporting and output file name generation only. +%% +%% See {@link edoc:get_doc/2} for descriptions of the `def', +%% `hidden', `private', and `todo' options. +%% +%% @see edoc:read_comments/2 +%% @see edoc:read_source/2 +%% @see source/5 +%% @see //syntax_tools/erl_recomment + +%% Note that the actual module name found in the source file will be +%% used for generating the documentation, creating relative links, etc. + +%% INHERIT-OPTIONS: add_macro_defs/3 +%% INHERIT-OPTIONS: edoc_data:module/4 + +source(Forms, File, Env, Opts) when is_list(Forms) -> + source(erl_syntax:form_list(Forms), File, Env, Opts); +source(Tree, File0, Env, Opts) -> + Forms = preprocess_forms(Tree), + File = edoc_lib:filename(File0), + Module = get_module_info(Tree, File), + {Header, Footer, Entries} = collect(Forms, Module), + Name = Module#module.name, + Package = list_to_atom(packages:strip_last(Name)), + Env1 = Env#env{module = Name, + package = Package, + root = edoc_refs:relative_package_path('', Package)}, + Env2 = add_macro_defs(module_macros(Env1), Opts, Env1), + Entries1 = get_tags([Header, Footer | Entries], Env2, File), + Data = edoc_data:module(Module, Entries1, Env2, Opts), + {Name, Data}. + + +%% @spec header(File::filename(), Env::edoc_env(), Options::proplist()) +%% -> {ok, Tags} | {error, Reason} +%% Tags = [term()] +%% Reason = term() +%% +%% @doc Similar to {@link header/5}, but reads the syntax tree and the +%% comments from the specified file. +%% +%% @see edoc:read_comments/2 +%% @see edoc:read_source/2 +%% @see header/4 + +header(File, Env, Opts) -> + Forms = edoc:read_source(File), + Comments = edoc:read_comments(File), + header(Forms, Comments, File, Env, Opts). + +%% @spec header(Forms, Comments::[comment()], File::filename(), +%% Env::edoc_env(), Options::proplist()) -> +%% {ok, Tags} | {error, Reason} +%% Forms = syntaxTree() | [syntaxTree()] +%% Tags = [term()] +%% Reason = term() +%% +%% @doc Similar to {@link header/4}, but first inserts the given +%% comments in the syntax trees. The syntax trees must contain valid +%% position information. (Cf. {@link edoc:read_comments/2}.) +%% +%% @see header/3 +%% @see header/4 +%% @see //syntax_tools/erl_recomment + +header(Forms, Comments, File, Env, Opts) when is_list(Forms) -> + Forms1 = erl_syntax:form_list(Forms), + header(Forms1, Comments, File, Env, Opts); +header(Forms, Comments, File, Env, Opts) -> + Tree = erl_recomment:quick_recomment_forms(Forms, Comments), + header(Tree, File, Env, Opts). + +%% @spec header(Forms, File::filename(), Env::edoc_env(), +%% Options::proplist()) -> +%% {ok, Tags} | {error, Reason} +%% Forms = syntaxTree() | [syntaxTree()] +%% Tags = [term()] +%% Reason = term() +%% +%% @doc Extracts EDoc documentation from commented header file syntax +%% trees. Similar to {@link source/5}, but ignores any documentation +%% that occurs before a module declaration or a function definition. +%% (Warning messages are printed if content may be ignored.) `Env' is +%% assumed to already be set up with a suitable module context. +%% +%% @see header/5 +%% @see //syntax_tools/erl_recomment + +header(Forms, File, Env, Opts) when is_list(Forms) -> + header(erl_syntax:form_list(Forms), File, Env, Opts); +header(Tree, File0, Env, _Opts) -> + Forms = preprocess_forms(Tree), + File = edoc_lib:filename(File0), + Module = #module{name = Env#env.module}, % a dummy module record + %% We take only "footer" tags, i.e., any kind of definition will + %% kill all the information above it up to that point. Then we call + %% this the 'header' to make error reports make better sense. + {Header, Footer, Entries} = collect(Forms, Module), + if Header#entry.data /= [] -> + warning(File, "documentation before module declaration is ignored by @headerfile", []); + true -> ok + end, + if Entries /= [] -> + warning(File, "documentation before function definitions is ignored by @headerfile", []); + true -> ok + end, + [Entry] = get_tags([Footer#entry{name = header}], Env, File), + Entry#entry.data. + +%% NEW-OPTIONS: def +%% DEFER-OPTIONS: source/4 + +add_macro_defs(Defs0, Opts, Env) -> + Defs = proplists:append_values(def, Opts), + edoc_macros:check_defs(Defs), + Env#env{macros = Defs ++ Defs0 ++ Env#env.macros}. + + +%% @spec file(File::filename(), Context, Env::edoc_env(), +%% Options::proplist()) -> {ok, Tags} | {error, Reason} +%% Context = overview | package +%% Tags = [term()] +%% Reason = term() +%% +%% @doc Reads a text file and returns the list of tags in the file. Any +%% lines of text before the first tag are ignored. `Env' is an +%% environment created by {@link edoc_lib:get_doc_env/4}. Upon error, +%% `Reason' is an atom returned from the call to {@link +%% //kernel/file:read_file/1}. +%% +%% See {@link text/4} for options. + +%% INHERIT-OPTIONS: text/4 + +file(File, Context, Env, Opts) -> + case file:read_file(File) of + {ok, Bin} -> + {ok, text(binary_to_list(Bin), Context, Env, Opts, File)}; + {error, _R} = Error -> + Error + end. + + +%% @spec (Text::string(), Context, Env::edoc_env(), +%% Options::proplist()) -> Tags +%% Context = overview | package +%% Tags = [term()] +%% +%% @doc Returns the list of tags in the text. Any lines of text before +%% the first tag are ignored. `Env' is an environment created by {@link +%% edoc_lib:get_doc_env/4}. +%% +%% See {@link source/4} for a description of the `def' option. + +%% INHERIT-OPTIONS: add_macro_defs/3 +%% DEFER-OPTIONS: source/4 + +text(Text, Context, Env, Opts) -> + text(Text, Context, Env, Opts, ""). + +text(Text, Context, Env, Opts, Where) -> + Env1 = add_macro_defs(file_macros(Context, Env), Opts, Env), + Cs = edoc_lib:lines(Text), + Ts0 = edoc_tags:scan_lines(Cs, 1), + Tags = sets:from_list(edoc_tags:tag_names()), + Ts1 = edoc_tags:filter_tags(Ts0, Tags, Where), + Single = sets:from_list(edoc_tags:tags(single)), + Allow = sets:from_list(edoc_tags:tags(Context)), + case edoc_tags:check_tags(Ts1, Allow, Single, Where) of + true -> + exit(error); + false -> + Ts2 = edoc_macros:expand_tags(Ts1, Env1, Where), + How = dict:from_list(edoc_tags:tag_parsers()), + edoc_tags:parse_tags(Ts2, How, Env1, Where) + end. + + +%% @spec (Forms::[syntaxTree()], File::filename()) -> moduleInfo() +%% @doc Initialises a module-info record with data about the module +%% represented by the list of forms. Exports are guaranteed to exist in +%% the set of defined names. + +get_module_info(Forms, File) -> + L = case catch {ok, erl_syntax_lib:analyze_forms(Forms)} of + {ok, L1} -> + L1; + syntax_error -> + report(File, "syntax error in input.", []), + exit(error); + {'EXIT', R} -> + exit(R); + R -> + throw(R) + end, + {Name, Vars} = case lists:keyfind(module, 1, L) of + {module, N} when is_atom(N) -> + {N, none}; + {module, {N, _Vs} = NVs} when is_atom(N) -> + NVs; + _ -> + report(File, "module name missing.", []), + exit(error) + end, + Functions = ordsets:from_list(get_list_keyval(functions, L)), + Exports = ordsets:from_list(get_list_keyval(exports, L)), + Attributes = ordsets:from_list(get_list_keyval(attributes, L)), + Records = get_list_keyval(records, L), + #module{name = Name, + parameters = Vars, + functions = Functions, + exports = ordsets:intersection(Exports, Functions), + attributes = Attributes, + records = Records}. + +get_list_keyval(Key, L) -> + case lists:keyfind(Key, 1, L) of + {Key, As} -> + ordsets:from_list(As); + _ -> + [] + end. + +%% @spec (Forms::[syntaxTree()]) -> [syntaxTree()] +%% @doc Preprocessing: copies any precomments on forms to standalone +%% comments, and removes "invisible" forms from the list. + +preprocess_forms(Tree) -> + preprocess_forms_1(erl_syntax:form_list_elements( + erl_syntax:flatten_form_list(Tree))). + +preprocess_forms_1([F | Fs]) -> + case erl_syntax:get_precomments(F) of + [] -> + preprocess_forms_2(F, Fs); + Cs -> + Cs ++ preprocess_forms_2(F, Fs) + end; +preprocess_forms_1([]) -> + []. + +preprocess_forms_2(F, Fs) -> + case erl_syntax_lib:analyze_form(F) of + comment -> + [F | preprocess_forms_1(Fs)]; + {function, _} -> + [F | preprocess_forms_1(Fs)]; + {rule, _} -> + [F | preprocess_forms_1(Fs)]; + {attribute, {module, _}} -> + [F | preprocess_forms_1(Fs)]; + text -> + [F | preprocess_forms_1(Fs)]; + _ -> + preprocess_forms_1(Fs) + end. + +%% This collects the data for the header and the functions of the +%% module. Note that the list of forms is assumed to have been +%% preprocessed first, so that all "invisible" forms are removed, and +%% the only interesting comments are those that are standalone comments +%% in the list. + +collect(Fs, Mod) -> + collect(Fs, [], [], undefined, Mod). + +collect([F | Fs], Cs, As, Header, Mod) -> + case erl_syntax_lib:analyze_form(F) of + comment -> + collect(Fs, [F | Cs], As, Header, Mod); + {function, Name} -> + L = erl_syntax:get_pos(F), + Export = ordsets:is_element(Name, Mod#module.exports), + Args = parameters(erl_syntax:function_clauses(F)), + collect(Fs, [], [#entry{name = Name, args = Args, line = L, + export = Export, + data = comment_text(Cs)} | As], + Header, Mod); + {rule, Name} -> + L = erl_syntax:get_pos(F), + Export = ordsets:is_element(Name, Mod#module.exports), + Args = parameters(erl_syntax:rule_clauses(F)), + collect(Fs, [], [#entry{name = Name, args = Args, line = L, + export = Export, + data = comment_text(Cs)} | As], + Header, Mod); + {attribute, {module, _}} when Header =:= undefined -> + L = erl_syntax:get_pos(F), + collect(Fs, [], As, #entry{name = module, line = L, + data = comment_text(Cs)}, + Mod); + _ -> + %% Drop current seen comments. + collect(Fs, [], As, Header, Mod) + end; +collect([], Cs, As, Header, _Mod) -> + Footer = #entry{name = footer, data = comment_text(Cs)}, + As1 = lists:reverse(As), + if Header =:= undefined -> + {#entry{name = module, data = []}, Footer, As1}; + true -> + {Header, Footer, As1} + end. + +%% Returns a list of simplified comment information (position and text) +%% for a list of abstract comments. The order of elements is reversed. + +comment_text(Cs) -> + comment_text(Cs, []). + +comment_text([C | Cs], Ss) -> + L = erl_syntax:get_pos(C), + comment_text(Cs, [#comment{line = L, + text = [remove_percent_chars(S) + || S <- erl_syntax:comment_text(C)]} + | Ss]); +comment_text([], Ss) -> + Ss. + +%% @spec (string()) -> string() +%% +%% @doc Replaces leading `%' characters by spaces. For example, `"%%% +%% foo" -> "\s\s\s foo"', but `"% % foo" -> "\s % foo"', since the +%% second `%' is preceded by whitespace. + +remove_percent_chars([$% | Cs]) -> [$\s | remove_percent_chars(Cs)]; +remove_percent_chars(Cs) -> Cs. + +%% Extracting possible parameter names from Erlang clause patterns. The +%% atom '_' is used when no name can be found. (Better names are made up +%% later, when we also may have typespecs available; see edoc_data.) + +parameters(Clauses) -> + select_names([find_names(Ps) || Ps <- patterns(Clauses)]). + +patterns(Cs) -> + edoc_lib:transpose([erl_syntax:clause_patterns(C) || C <- Cs]). + +find_names(Ps) -> + find_names(Ps, []). + +find_names([P | Ps], Ns) -> + case erl_syntax:type(P) of + variable -> + find_names(Ps, [tidy_name(erl_syntax:variable_name(P)) | Ns]); + match_expr -> + %% Right-hand side gets priority over left-hand side! + %% Note that the list is reversed afterwards. + P1 = erl_syntax:match_expr_pattern(P), + P2 = erl_syntax:match_expr_body(P), + find_names([P1, P2 | Ps], Ns); + list -> + P1 = erl_syntax:list_tail(P), + find_names([P1 | Ps], Ns); + record_expr -> + A = erl_syntax:record_expr_type(P), + N = list_to_atom(capitalize(erl_syntax:atom_name(A))), + find_names(Ps, [N | Ns]); + infix_expr -> + %% this can only be a '++' operation + P1 = erl_syntax:infix_expr_right(P), + find_names([P1 | Ps], Ns); + _ -> + find_names(Ps, Ns) + end; +find_names([], Ns) -> + lists:reverse(Ns). + +select_names(Ls) -> + select_names(Ls, [], sets:new()). + +select_names([Ns | Ls], As, S) -> + A = select_name(Ns, S), + select_names(Ls, [A | As], sets:add_element(A, S)); +select_names([], As, _) -> + lists:reverse(As). + +select_name([A | Ns], S) -> + case sets:is_element(A, S) of + true -> + select_name(Ns, S); + false -> + A + end; +select_name([], _S) -> + '_'. + +%% Strip leading underscore characters from parameter names. If the +%% result does not begin with an uppercase character, we add a single +%% leading underscore. If the result would be empty, the atom '_' is +%% returned. + +tidy_name(A) -> + case atom_to_list(A) of + [$_ | Cs] -> + list_to_atom(tidy_name_1(Cs)); + _ -> + A + end. + +tidy_name_1([$_ | Cs]) -> tidy_name_1(Cs); +tidy_name_1([C | _]=Cs) when C >= $A, C =< $Z -> Cs; +tidy_name_1([C | _]=Cs) when C >= $\300, C =< $\336, C =/= $\327-> Cs; +tidy_name_1(Cs) -> [$_ | Cs]. + +%% Change initial character from lowercase to uppercase. + +capitalize([C | Cs]) when C >= $a, C =< $z -> [C - 32 | Cs]; +capitalize(Cs) -> Cs. + +%% Collects the tags belonging to each entry, checks them, expands +%% macros and parses the content. + +%% %This is commented out until it can be made private +%% %@type tags() = #tags{names = set(atom()), +%% % single = set(atom()), +%% % module = set(atom()), +%% % footer = set(atom()), +%% % function = set(atom())} +%% % set(T) = sets:set(T) + +-record(tags, {names,single,module,function,footer}). + +get_tags(Es, Env, File) -> + %% Cache this stuff for quick lookups. + Tags = #tags{names = sets:from_list(edoc_tags:tag_names()), + single = sets:from_list(edoc_tags:tags(single)), + module = sets:from_list(edoc_tags:tags(module)), + footer = sets:from_list(edoc_tags:tags(footer)), + function = sets:from_list(edoc_tags:tags(function))}, + How = dict:from_list(edoc_tags:tag_parsers()), + get_tags(Es, Tags, Env, How, File). + +get_tags([#entry{name = Name, data = Cs} = E | Es], Tags, Env, + How, File) -> + Where = {File, Name}, + Ts0 = scan_tags(Cs), + Ts1 = check_tags(Ts0, Tags, Where), + Ts2 = edoc_macros:expand_tags(Ts1, Env, Where), + Ts = edoc_tags:parse_tags(Ts2, How, Env, Where), + [E#entry{data = Ts} | get_tags(Es, Tags, Env, How, File)]; +get_tags([], _, _, _, _) -> + []. + +%% Scanning a list of separate comments for tags. + +scan_tags([#comment{line = L, text = Ss} | Es]) -> + edoc_tags:scan_lines(Ss, L) ++ scan_tags(Es); +scan_tags([]) -> + []. + +%% Check the set of found tags (depending on context). +%% Completely unknown tags are filtered out with a warning. + +check_tags(Ts0, Tags, Where) -> + Ts = edoc_tags:filter_tags(Ts0, Tags#tags.names, Where), + case check_tags_1(Ts, Tags, Where) of + false -> Ts; + true -> exit(error) + end. + +check_tags_1(Ts, Tags, {_, module} = Where) -> + Allow = Tags#tags.module, + Single = Tags#tags.single, + edoc_tags:check_tags(Ts, Allow, Single, Where); +check_tags_1(Ts, Tags, {_, footer} = Where) -> + Allow = Tags#tags.footer, + Single = Tags#tags.single, + edoc_tags:check_tags(Ts, Allow, Single, Where); +check_tags_1(Ts, Tags, Where) -> + Allow = Tags#tags.function, + Single = Tags#tags.single, + edoc_tags:check_tags(Ts, Allow, Single, Where). + +%% Macros for modules + +module_macros(Env) -> + [{module, atom_to_list(Env#env.module)}] + ++ edoc_macros:std_macros(Env). + +%% Macros for reading auxiliary edoc-files + +file_macros(_Context, Env) -> + edoc_macros:std_macros(Env). diff --git a/lib/edoc/src/edoc_layout.erl b/lib/edoc/src/edoc_layout.erl new file mode 100644 index 0000000000..900f0b3040 --- /dev/null +++ b/lib/edoc/src/edoc_layout.erl @@ -0,0 +1,875 @@ +%% ===================================================================== +%% 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$ +%% +%% @author Richard Carlsson <[email protected]> +%% @copyright 2001-2006 Richard Carlsson +%% @see edoc +%% @end +%% ===================================================================== + +%% @doc The standard HTML layout module for EDoc. See the {@link edoc} +%% module for details on usage. + +%% Note that this is written so that it is *not* depending on edoc.hrl! + +-module(edoc_layout). + +-export([module/2, package/2, overview/2, type/1]). + +-import(edoc_report, [report/2]). + +-include("xmerl.hrl"). + +-define(HTML_EXPORT, xmerl_html). +-define(DEFAULT_XML_EXPORT, ?HTML_EXPORT). +-define(OVERVIEW_SUMMARY, "overview-summary.html"). +-define(STYLESHEET, "stylesheet.css"). +-define(NL, "\n"). +-define(DESCRIPTION_TITLE, "Description"). +-define(DESCRIPTION_LABEL, "description"). +-define(DATA_TYPES_TITLE, "Data Types"). +-define(DATA_TYPES_LABEL, "types"). +-define(FUNCTION_INDEX_TITLE, "Function Index"). +-define(FUNCTION_INDEX_LABEL, "index"). +-define(FUNCTIONS_TITLE, "Function Details"). +-define(FUNCTIONS_LABEL, "functions"). + + +%% @doc The layout function. +%% +%% Options to the standard layout: +%% <dl> +%% <dt>{@type {index_columns, integer()@}} +%% </dt> +%% <dd>Specifies the number of column pairs used for the function +%% index tables. The default value is 1. +%% </dd> +%% <dt>{@type {stylesheet, string()@}} +%% </dt> +%% <dd>Specifies the URI used for referencing the stylesheet. The +%% default value is `"stylesheet.css"'. If an empty string is +%% specified, no stylesheet reference will be generated. +%% </dd> +%% <dt>{@type {sort_functions, bool()@}} +%% </dt> +%% <dd>If `true', the detailed function descriptions are listed by +%% name, otherwise they are listed in the order of occurrence in +%% the source file. The default value is `true'. +%% </dd> +%% <dt>{@type {xml_export, Module::atom()@}} +%% </dt> +%% <dd>Specifies an {@link //xmerl. `xmerl'} callback module to be +%% used for exporting the documentation. See {@link +%% //xmerl/xmerl:export_simple/3} for details. +%% </dd> +%% </dl> +%% +%% @see edoc:layout/2 + +%% NEW-OPTIONS: xml_export, index_columns, stylesheet + +module(Element, Options) -> + XML = layout_module(Element, init_opts(Element, Options)), + Export = proplists:get_value(xml_export, Options, + ?DEFAULT_XML_EXPORT), + xmerl:export_simple(XML, Export, []). + +% Put layout options in a data structure for easier access. + +%% %Commented out until it can be made private +%% %@type opts() = #opts{root = string(), +%% % stylesheet = string(), +%% % index_columns = integer()} + +-record(opts, {root, stylesheet, index_columns, sort_functions}). + +init_opts(Element, Options) -> + R = #opts{root = get_attrval(root, Element), + index_columns = proplists:get_value(index_columns, + Options, 1), + sort_functions = proplists:get_value(sort_functions, + Options, true) + }, + case proplists:get_value(stylesheet, Options) of + undefined -> + S = edoc_lib:join_uri(R#opts.root, ?STYLESHEET), + R#opts{stylesheet = S}; + "" -> + R; % don't use any stylesheet + S when is_list(S) -> + R#opts{stylesheet = S}; + _ -> + report("bad value for option `stylesheet'.", []), + exit(error) + end. + + +%% ===================================================================== +%% XML-BASED LAYOUT ENGINE +%% ===================================================================== + +%% We assume that we have expanded XML data. + +%% <!ELEMENT module (behaviour*, description?, author*, copyright?, +%% version?, since?, deprecated?, see*, reference*, +%% todo?, typedecls?, functions)> +%% <!ATTLIST module +%% name CDATA #REQUIRED +%% private NMTOKEN(yes | no) #IMPLIED +%% root CDATA #IMPLIED> +%% <!ELEMENT behaviour (#PCDATA)> +%% <!ATTLIST behaviour +%% href CDATA #IMPLIED> +%% <!ELEMENT description (briefDescription, fullDescription?)> +%% <!ELEMENT briefDescription (#PCDATA)> +%% <!ELEMENT fullDescription (#PCDATA)> +%% <!ELEMENT author EMPTY> +%% <!ATTLIST author +%% name CDATA #REQUIRED +%% email CDATA #IMPLIED +%% website CDATA #IMPLIED> +%% <!ELEMENT version (#PCDATA)> +%% <!ELEMENT since (#PCDATA)> +%% <!ELEMENT copyright (#PCDATA)> +%% <!ELEMENT deprecated (description)> +%% <!ELEMENT see (#PCDATA)> +%% <!ATTLIST see +%% name CDATA #REQUIRED +%% href CDATA #IMPLIED> +%% <!ELEMENT reference (#PCDATA)> +%% <!ELEMENT todo (#PCDATA)> +%% <!ELEMENT typedecls (typedecl+)> +%% <!ELEMENT functions (function+)> + +%% TODO: improve layout of parameterized modules + +layout_module(#xmlElement{name = module, content = Es}=E, Opts) -> + Args = module_params(get_content(args, Es)), + Name = get_attrval(name, E), + Title = case get_elem(args, Es) of + [] -> ["Module ", Name]; + _ -> ["Abstract module ", Name, " [", {Args}, "]"] + end, + Desc = get_content(description, Es), + ShortDesc = get_content(briefDescription, Desc), + FullDesc = get_content(fullDescription, Desc), + Functions = [{function_name(E), E} || E <- get_content(functions, Es)], + Types = [{type_name(E), E} || E <- get_content(typedecls, Es)], + SortedFs = lists:sort(Functions), + Body = (navigation("top") + ++ [?NL, hr, ?NL, ?NL, {h1, Title}, ?NL] + ++ doc_index(FullDesc, Functions, Types) + ++ ShortDesc + ++ [?NL] + ++ copyright(Es) + ++ deprecated(Es, "module") + ++ [?NL] + ++ version(Es) + ++ since(Es) + ++ behaviours(Es, Name) + ++ authors(Es) + ++ references(Es) + ++ sees(Es) + ++ todos(Es) + ++ if FullDesc == [] -> []; + true -> [?NL, + {h2, [{a, [{name, "description"}], + ["Description"]}]} + | FullDesc] + end + ++ types(lists:sort(Types)) + ++ function_index(SortedFs, Opts#opts.index_columns) + ++ if Opts#opts.sort_functions -> functions(SortedFs); + true -> functions(Functions) + end + ++ [hr, ?NL] + ++ navigation("bottom") + ++ timestamp()), + xhtml(Title, stylesheet(Opts), Body). + +module_params(Es) -> + As = [{get_text(argName, Es1), + get_content(fullDescription, get_content(description, Es1))} + || #xmlElement{content = Es1} <- Es], + case As of + [] -> []; + [First | Rest] -> + [element(1, First) | [ {[", ",A]} || {A, _D} <- Rest]] + end. + +timestamp() -> + [?NL, {p, [{i, [io_lib:fwrite("Generated by EDoc, ~s, ~s.", + [edoc_lib:datestr(date()), + edoc_lib:timestr(time())]) + ]}]}, + ?NL]. + +stylesheet(Opts) -> + case Opts#opts.stylesheet of + undefined -> + []; + CSS -> + [{link, [{rel, "stylesheet"}, + {type, "text/css"}, + {href, CSS}, + {title, "EDoc"}], []}, + ?NL] + end. + +navigation(Where) -> + [?NL, + {'div', [{class, "navbar"}], + [{a, [{name, "#navbar_" ++ Where}], []}, + {table, [{width, "100%"}, {border,0}, + {cellspacing, 0}, {cellpadding, 2}, + {summary, "navigation bar"}], + [{tr, + [{td, [{a, [{href, ?OVERVIEW_SUMMARY}, {target,"overviewFrame"}], + ["Overview"]}]}, + {td, [{a, [{href, "http://www.erlang.org/"}], + [{img, [{src, "erlang.png"}, {align, "right"}, + {border, 0}, {alt, "erlang logo"}], + []}]} + ]} + ]} + ]} + ]} + ]. + +doc_index(FullDesc, Functions, Types) -> + case doc_index_rows(FullDesc, Functions, Types) of + [] -> []; + Rs -> + [{ul, [{class, "index"}], + [{li, [{a, [{href, local_label(R)}], [T]}]} + || {T, R} <- Rs]}] + end. + +doc_index_rows(FullDesc, Functions, Types) -> + (if FullDesc == [] -> []; + true -> [{?DESCRIPTION_TITLE, ?DESCRIPTION_LABEL}] + end + ++ if Types == [] -> []; + true -> [{?DATA_TYPES_TITLE, ?DATA_TYPES_LABEL}] + end + ++ if Functions == [] -> []; + true -> [{?FUNCTION_INDEX_TITLE, ?FUNCTION_INDEX_LABEL}, + {?FUNCTIONS_TITLE, ?FUNCTIONS_LABEL}] + end). + +function_index(Fs, Cols) -> + case function_index_rows(Fs, Cols, []) of + [] -> []; + Rows -> + [?NL, + {h2, [{a, [{name, ?FUNCTION_INDEX_LABEL}], + [?FUNCTION_INDEX_TITLE]}]}, + ?NL, + {table, [{width, "100%"}, {border, 1}, + {cellspacing,0}, {cellpadding,2}, + {summary, "function index"}], + Rows}, + ?NL] + end. + +function_index_rows(Fs, Cols, Title) -> + Rows = (length(Fs) + (Cols - 1)) div Cols, + (if Title == [] -> []; + true -> [{tr, [{th, [{colspan, Cols * 2}, {align, left}], + [Title]}]}, + ?NL] + end + ++ lists:flatmap(fun index_row/1, + edoc_lib:transpose(edoc_lib:segment(Fs, Rows)))). + +index_row(Fs) -> + [{tr, lists:flatmap(fun index_col/1, Fs)}, ?NL]. + +index_col({Name, F=#xmlElement{content = Es}}) -> + [{td, [{valign, "top"}], + label_href(function_header(Name, F, "*"), F)}, + {td, index_desc(Es)}]. + +index_desc(Es) -> + Desc = get_content(description, Es), + (case get_content(deprecated, Es) of + [] -> []; + _ -> ["(", {em, ["Deprecated"]}, ".) "] + end + ++ case get_content(briefDescription, Desc) of + [] -> + equiv(Es); % no description at all if no equiv + ShortDesc -> + ShortDesc + end). + +label_href(Content, F) -> + case get_attrval(label, F) of + "" -> Content; + Ref -> [{a, [{href, local_label(Ref)}], Content}] + end. + +%% <!ELEMENT function (args, typespec?, returns?, throws?, equiv?, +%% description?, since?, deprecated?, see*, todo?)> +%% <!ATTLIST function +%% name CDATA #REQUIRED +%% arity CDATA #REQUIRED +%% exported NMTOKEN(yes | no) #REQUIRED +%% label CDATA #IMPLIED> +%% <!ELEMENT args (arg*)> +%% <!ELEMENT equiv (expr, see?)> +%% <!ELEMENT expr (#PCDATA)> + +functions(Fs) -> + Es = lists:flatmap(fun ({Name, E}) -> function(Name, E) end, Fs), + if Es == [] -> []; + true -> + [?NL, + {h2, [{a, [{name, ?FUNCTIONS_LABEL}], [?FUNCTIONS_TITLE]}]}, + ?NL | Es] + end. + +function(Name, E=#xmlElement{content = Es}) -> + ([?NL, + {h3, [{class, "function"}], + label_anchor(function_header(Name, E, " *"), E)}, + ?NL] + ++ [{'div', [{class, "spec"}], + [?NL, + {p, + case typespec(get_content(typespec, Es)) of + [] -> + signature(get_content(args, Es), + get_attrval(name, E)); + Spec -> Spec + end}, + ?NL] + ++ case params(get_content(args, Es)) of + [] -> []; + Ps -> [{p, Ps}, ?NL] + end + ++ case returns(get_content(returns, Es)) of + [] -> []; + Rs -> [{p, Rs}, ?NL] + end}] + ++ throws(Es) + ++ equiv_p(Es) + ++ deprecated(Es, "function") + ++ fulldesc(Es) + ++ since(Es) + ++ sees(Es) + ++ todos(Es)). + +function_name(E) -> + atom(get_attrval(name, E)) ++ "/" ++ get_attrval(arity, E). + +function_header(Name, E, Private) -> + case is_exported(E) of + true -> [Name]; + false -> [Name, Private] + end. + +is_exported(E) -> + case get_attrval(exported, E) of + "yes" -> true; + _ -> false + end. + +label_anchor(Content, E) -> + case get_attrval(label, E) of + "" -> Content; + Ref -> [{a, [{name, Ref}], Content}] + end. + +%% <!ELEMENT args (arg*)> +%% <!ELEMENT arg (argName, description?)> +%% <!ELEMENT argName (#PCDATA)> + +%% This is currently only done for functions without type spec. + +signature(Es, Name) -> + [{tt, [Name, "("] ++ seq(fun arg/1, Es) ++ [") -> any()"]}]. + +arg(#xmlElement{content = Es}) -> + [get_text(argName, Es)]. + +%% parameter and return value descriptions (if any) + +params(Es) -> + As = [{get_text(argName, Es1), + get_content(fullDescription, get_content(description, Es1))} + || #xmlElement{content = Es1} <- Es], + As1 = [A || A <- As, element(2, A) /= []], + if As1 == [] -> + []; + true -> + [ { [{tt, [A]}, ": "] ++ D ++ [br, ?NL] } + || {A, D} <- As1] + end. + +returns(Es) -> + case get_content(fullDescription, get_content(description, Es)) of + [] -> + []; + D -> + ["returns: "] ++ D + end. + +%% <!ELEMENT throws (type, localdef*)> + +throws(Es) -> + case get_content(throws, Es) of + [] -> []; + Es1 -> + [{p, (["throws ", {tt, t_utype(get_elem(type, Es1))}] + ++ local_defs(get_elem(localdef, Es1)))}, + ?NL] + end. + +%% <!ELEMENT typespec (erlangName, type, localdef*)> + +typespec([]) -> []; +typespec(Es) -> + [{tt, ([t_name(get_elem(erlangName, Es))] + ++ t_utype(get_elem(type, Es)))}] + ++ local_defs(get_elem(localdef, Es)). + +%% <!ELEMENT typedecl (typedef, description?)> +%% <!ELEMENT typedef (erlangName, argtypes, type?, localdef*)> + +types([]) -> []; +types(Ts) -> + Es = lists:flatmap(fun ({Name, E}) -> typedecl(Name, E) end, Ts), + [?NL, + {h2, [{a, [{name, ?DATA_TYPES_LABEL}], + [?DATA_TYPES_TITLE]}]}, + ?NL | Es]. + +typedecl(Name, E=#xmlElement{content = Es}) -> + ([?NL, {h3, [{class, "typedecl"}], label_anchor([Name, "()"], E)}, ?NL] + ++ [{p, typedef(get_content(typedef, Es))}, ?NL] + ++ fulldesc(Es)). + +type_name(#xmlElement{content = Es}) -> + t_name(get_elem(erlangName, get_content(typedef, Es))). + +typedef(Es) -> + Name = ([t_name(get_elem(erlangName, Es)), "("] + ++ seq(fun t_utype_elem/1, get_content(argtypes, Es), [")"])), + (case get_elem(type, Es) of + [] -> [{b, ["abstract datatype"]}, ": ", {tt, Name}]; + Type -> + [{tt, Name ++ [" = "] ++ t_utype(Type)}] + end + ++ local_defs(get_elem(localdef, Es))). + +local_defs([]) -> []; +local_defs(Es) -> + [?NL, + {ul, [{class, "definitions"}], + lists:concat([[{li, [{tt, localdef(E)}]}, ?NL] || E <- Es])}]. + +localdef(E = #xmlElement{content = Es}) -> + (case get_elem(typevar, Es) of + [] -> + label_anchor(t_abstype(get_content(abstype, Es)), E); + [V] -> + t_var(V) + end + ++ [" = "] ++ t_utype(get_elem(type, Es))). + +fulldesc(Es) -> + case get_content(fullDescription, get_content(description, Es)) of + [] -> [?NL]; + Desc -> [{p, Desc}, ?NL] + end. + +sees(Es) -> + case get_elem(see, Es) of + [] -> []; + Es1 -> + [{p, [{b, ["See also:"]}, " "] ++ seq(fun see/1, Es1, ["."])}, + ?NL] + end. + +see(E=#xmlElement{content = Es}) -> + see(E, Es). + +see(E, Es) -> + case href(E) of + [] -> Es; + Ref -> + [{a, Ref, Es}] + end. + +href(E) -> + case get_attrval(href, E) of + "" -> []; + URI -> + T = case get_attrval(target, E) of + "" -> []; + S -> [{target, S}] + end, + [{href, URI} | T] + end. + +equiv_p(Es) -> + equiv(Es, true). + +equiv(Es) -> + equiv(Es, false). + +equiv(Es, P) -> + case get_content(equiv, Es) of + [] -> []; + Es1 -> + case get_content(expr, Es1) of + [] -> []; + [Expr] -> + Expr1 = [{tt, [Expr]}], + Expr2 = case get_elem(see, Es1) of + [] -> + Expr1; + [E=#xmlElement{}] -> + see(E, Expr1) + end, + Txt = ["Equivalent to "] ++ Expr2 ++ ["."], + (case P of + true -> [{p, Txt}]; + false -> Txt + end + ++ [?NL]) + end + end. + +copyright(Es) -> + case get_content(copyright, Es) of + [] -> []; + Es1 -> + [{p, ["Copyright \251 " | Es1]}, ?NL] + end. + +version(Es) -> + case get_content(version, Es) of + [] -> []; + Es1 -> + [{p, [{b, ["Version:"]}, " " | Es1]}, ?NL] + end. + +since(Es) -> + case get_content(since, Es) of + [] -> []; + Es1 -> + [{p, [{b, ["Introduced in:"]}, " " | Es1]}, ?NL] + end. + +deprecated(Es, S) -> + Es1 = get_content(description, get_content(deprecated, Es)), + case get_content(fullDescription, Es1) of + [] -> []; + Es2 -> + [{p, [{b, ["This " ++ S ++ " is deprecated:"]}, " " | Es2]}, + ?NL] + end. + +behaviours(Es, Name) -> + (case get_elem(behaviour, Es) of + [] -> []; + Es1 -> + [{p, ([{b, ["Behaviours:"]}, " "] + ++ seq(fun behaviour/1, Es1, ["."]))}, + ?NL] + end + ++ + case get_content(callbacks, Es) of + [] -> []; + Es1 -> + [{p, ([{b, ["This module defines the ", {tt, [Name]}, + " behaviour."]}, + br, " Required callback functions: "] + ++ seq(fun callback/1, Es1, ["."]))}, + ?NL] + end). + +behaviour(E=#xmlElement{content = Es}) -> + see(E, [{tt, Es}]). + +callback(E=#xmlElement{}) -> + Name = get_attrval(name, E), + Arity = get_attrval(arity, E), + [{tt, [Name, "/", Arity]}]. + +authors(Es) -> + case get_elem(author, Es) of + [] -> []; + Es1 -> + [{p, [{b, ["Authors:"]}, " "] ++ seq(fun author/1, Es1, ["."])}, + ?NL] + end. + +atom(String) -> + io_lib:write_atom(list_to_atom(String)). + +%% <!ATTLIST author +%% name CDATA #REQUIRED +%% email CDATA #IMPLIED +%% website CDATA #IMPLIED> + +author(E=#xmlElement{}) -> + Name = get_attrval(name, E), + Mail = get_attrval(email, E), + URI = get_attrval(website, E), + (if Name == Mail -> + [{a, [{href, "mailto:" ++ Mail}],[{tt, [Mail]}]}]; + true -> + if Mail == "" -> [Name]; + true -> [Name, " (", {a, [{href, "mailto:" ++ Mail}], + [{tt, [Mail]}]}, ")"] + end + end + ++ if URI == "" -> + []; + true -> + [" [", {em, ["web site:"]}, " ", + {tt, [{a, [{href, URI}, {target, "_top"}], [URI]}]}, + "]"] + end). + +references(Es) -> + case get_elem(reference, Es) of + [] -> []; + Es1 -> + [{p, [{b, ["References"]}, + {ul, [{li, C} || #xmlElement{content = C} <- Es1]}]}, + ?NL] + end. + +todos(Es) -> + case get_elem(todo, Es) of + [] -> []; + Es1 -> + Todos = [{li, [{font, [{color,red}], C}]} + || #xmlElement{content = C} <- Es1], + [{p, [{b, [{font, [{color,red}], ["To do"]}]}, + {ul, Todos}]}, + ?NL] + end. + +t_name([E]) -> + N = get_attrval(name, E), + case get_attrval(module, E) of + "" -> atom(N); + M -> + S = atom(M) ++ ":" ++ atom(N), + case get_attrval(app, E) of + "" -> S; + A -> "//" ++ atom(A) ++ "/" ++ S + end + end. + +t_utype([E]) -> + t_utype_elem(E). + +t_utype_elem(E=#xmlElement{content = Es}) -> + case get_attrval(name, E) of + "" -> t_type(Es); + Name -> + T = t_type(Es), + case T of + [Name] -> T; % avoid generating "Foo::Foo" + T -> [Name] ++ ["::"] ++ T + end + end. + +t_type([E=#xmlElement{name = typevar}]) -> + t_var(E); +t_type([E=#xmlElement{name = atom}]) -> + t_atom(E); +t_type([E=#xmlElement{name = integer}]) -> + t_integer(E); +t_type([E=#xmlElement{name = float}]) -> + t_float(E); +t_type([#xmlElement{name = nil}]) -> + t_nil(); +t_type([#xmlElement{name = list, content = Es}]) -> + t_list(Es); +t_type([#xmlElement{name = tuple, content = Es}]) -> + t_tuple(Es); +t_type([#xmlElement{name = 'fun', content = Es}]) -> + t_fun(Es); +t_type([#xmlElement{name = record, content = Es}]) -> + t_record(Es); +t_type([E = #xmlElement{name = abstype, content = Es}]) -> + T = t_abstype(Es), + see(E, T); +t_type([#xmlElement{name = union, content = Es}]) -> + t_union(Es). + +t_var(E) -> + [get_attrval(name, E)]. + +t_atom(E) -> + [get_attrval(value, E)]. + +t_integer(E) -> + [get_attrval(value, E)]. + +t_float(E) -> + [get_attrval(value, E)]. + +t_nil() -> + ["[]"]. + +t_list(Es) -> + ["["] ++ t_utype(get_elem(type, Es)) ++ ["]"]. + +t_tuple(Es) -> + ["{"] ++ seq(fun t_utype_elem/1, Es, ["}"]). + +t_fun(Es) -> + ["("] ++ seq(fun t_utype_elem/1, get_content(argtypes, Es), + [") -> "] ++ t_utype(get_elem(type, Es))). + +t_record(Es) -> + ["#"] ++ t_type(get_elem(atom, Es)) ++ ["{"] + ++ seq(fun t_field/1, get_elem(field, Es), ["}"]). + +t_field(#xmlElement{content = Es}) -> + t_type(get_elem(atom, Es)) ++ [" = "] ++ t_utype(get_elem(type, Es)). + +t_abstype(Es) -> + ([t_name(get_elem(erlangName, Es)), "("] + ++ seq(fun t_utype_elem/1, get_elem(type, Es), [")"])). + +t_union(Es) -> + seq(fun t_utype_elem/1, Es, " | ", []). + +seq(F, Es) -> + seq(F, Es, []). + +seq(F, Es, Tail) -> + seq(F, Es, ", ", Tail). + +seq(F, [E], _Sep, Tail) -> + F(E) ++ Tail; +seq(F, [E | Es], Sep, Tail) -> + F(E) ++ [Sep] ++ seq(F, Es, Sep, Tail); +seq(_F, [], _Sep, Tail) -> + Tail. + +get_elem(Name, [#xmlElement{name = Name} = E | Es]) -> + [E | get_elem(Name, Es)]; +get_elem(Name, [_ | Es]) -> + get_elem(Name, Es); +get_elem(_, []) -> + []. + +get_attr(Name, [#xmlAttribute{name = Name} = A | As]) -> + [A | get_attr(Name, As)]; +get_attr(Name, [_ | As]) -> + get_attr(Name, As); +get_attr(_, []) -> + []. + +get_attrval(Name, #xmlElement{attributes = As}) -> + case get_attr(Name, As) of + [#xmlAttribute{value = V}] -> + V; + [] -> "" + end. + +get_content(Name, Es) -> + case get_elem(Name, Es) of + [#xmlElement{content = Es1}] -> + Es1; + [] -> [] + end. + +get_text(Name, Es) -> + case get_content(Name, Es) of + [#xmlText{value = Text}] -> + Text; + [] -> "" + end. + +local_label(R) -> + "#" ++ R. + +xhtml(Title, CSS, Body) -> + [{html, [?NL, + {head, [?NL, + {title, Title}, + ?NL] ++ CSS}, + ?NL, + {body, [{bgcolor, "white"}], Body}, + ?NL] + }, + ?NL]. + +%% --------------------------------------------------------------------- + +type(E) -> + type(E, []). + +type(E, Ds) -> + xmerl:export_simple_content(t_utype_elem(E) ++ local_defs(Ds), + ?HTML_EXPORT). + +package(E=#xmlElement{name = package, content = Es}, Options) -> + Opts = init_opts(E, Options), + Name = get_text(packageName, Es), + Title = ["Package ", Name], + Desc = get_content(description, Es), +% ShortDesc = get_content(briefDescription, Desc), + FullDesc = get_content(fullDescription, Desc), + Body = ([?NL, {h1, [Title]}, ?NL] +% ++ ShortDesc + ++ copyright(Es) + ++ deprecated(Es, "package") + ++ version(Es) + ++ since(Es) + ++ authors(Es) + ++ references(Es) + ++ sees(Es) + ++ todos(Es) + ++ FullDesc), + XML = xhtml(Title, stylesheet(Opts), Body), + xmerl:export_simple(XML, ?HTML_EXPORT, []). + +overview(E=#xmlElement{name = overview, content = Es}, Options) -> + Opts = init_opts(E, Options), + Title = [get_text(title, Es)], + Desc = get_content(description, Es), +% ShortDesc = get_content(briefDescription, Desc), + FullDesc = get_content(fullDescription, Desc), + Body = (navigation("top") + ++ [?NL, {h1, [Title]}, ?NL] +% ++ ShortDesc + ++ copyright(Es) + ++ version(Es) + ++ since(Es) + ++ authors(Es) + ++ references(Es) + ++ sees(Es) + ++ todos(Es) + ++ FullDesc + ++ [?NL, hr] + ++ navigation("bottom") + ++ timestamp()), + XML = xhtml(Title, stylesheet(Opts), Body), + xmerl:export_simple(XML, ?HTML_EXPORT, []). 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}). 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}). diff --git a/lib/edoc/src/edoc_parser.yrl b/lib/edoc/src/edoc_parser.yrl new file mode 100644 index 0000000000..0eea8ae66f --- /dev/null +++ b/lib/edoc/src/edoc_parser.yrl @@ -0,0 +1,423 @@ +%% ========================== -*-Erlang-*- ============================= +%% EDoc type specification grammar for the Yecc parser generator, +%% adapted from Sven-Olof Nystr�m's type specification parser. +%% +%% Also contains entry points for parsing things like typedefs, +%% references, and throws-declarations. +%% +%% Copyright (C) 2002-2005 Richard Carlsson +%% +%% 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 +%% +%% Author contact: [email protected] +%% +%% $Id$ +%% +%% ===================================================================== + +Nonterminals +start spec func_type utype_list utype_tuple utypes utype ptypes ptype +nutype function_name where_defs defs def typedef etype throws qname ref +aref mref lref pref var_list vars fields field. + +Terminals +atom float integer var string start_spec start_typedef start_throws +start_ref + +'(' ')' ',' '.' '->' '{' '}' '[' ']' '|' '+' ':' '::' '=' '/' '//' '*' +'#' 'where'. + +Rootsymbol start. + +start -> start_spec spec: '$2'. +start -> start_throws throws: '$2'. +start -> start_typedef typedef: '$2'. +start -> start_ref ref: '$2'. + +%% Produced in reverse order. +qname -> atom: [tok_val('$1')]. +qname -> qname '.' atom: [tok_val('$3') | '$1']. + +spec -> func_type where_defs: + #t_spec{type = '$1', defs = lists:reverse('$2')}. +spec -> function_name func_type where_defs: + #t_spec{name = '$1', type = '$2', defs = lists:reverse('$3')}. + +where_defs -> 'where' defs: '$2'. +where_defs -> defs: '$1'. + +function_name -> atom: #t_name{name = tok_val('$1')}. + +func_type -> utype_list '->' utype: + #t_fun{args = element(1, '$1'), range = '$3'}. + + +%% Paired with line number, for later error reporting +utype_list -> '(' ')' : {[], tok_line('$1')}. +utype_list -> '(' utypes ')' : {lists:reverse('$2'), tok_line('$1')}. + +utype_tuple -> '{' '}' : []. +utype_tuple -> '{' utypes '}' : lists:reverse('$2'). + +%% Produced in reverse order. +utypes -> utype : ['$1']. +utypes -> utypes ',' utype : ['$3' | '$1']. + +utype -> nutype string: annotate('$1', tok_val('$2')). +utype -> nutype: '$1'. + +nutype -> var '::' ptypes: annotate(union('$3'), tok_val('$1')). +nutype -> ptypes: union('$1'). + +%% Produced in reverse order. +ptypes -> ptype : ['$1']. +ptypes -> ptypes '+' ptype : ['$3' | '$1']. +ptypes -> ptypes '|' ptype : ['$3' | '$1']. + +ptype -> var : #t_var{name = tok_val('$1')}. +ptype -> atom : #t_atom{val = tok_val('$1')}. +ptype -> integer: #t_integer{val = tok_val('$1')}. +ptype -> float: #t_float{val = tok_val('$1')}. +ptype -> utype_tuple : #t_tuple{types = '$1'}. +ptype -> '[' ']' : #t_nil{}. +ptype -> '[' utype ']' : #t_list{type = '$2'}. +ptype -> utype_list: + if length(element(1, '$1')) == 1 -> + %% there must be exactly one utype in the list + hd(element(1, '$1')); + length(element(1, '$1')) == 0 -> + return_error(element(2, '$1'), "syntax error before: ')'"); + true -> + return_error(element(2, '$1'), "syntax error before: ','") + end. +ptype -> utype_list '->' ptype: + #t_fun{args = element(1, '$1'), range = '$3'}. +ptype -> '#' atom '{' '}' : + #t_record{name = #t_atom{val = tok_val('$2')}}. +ptype -> '#' atom '{' fields '}' : + #t_record{name = #t_atom{val = tok_val('$2')}, + fields = lists:reverse('$4')}. +ptype -> atom utype_list: + #t_type{name = #t_name{name = tok_val('$1')}, + args = element(1, '$2')}. +ptype -> qname ':' atom utype_list : + #t_type{name = #t_name{module = qname('$1'), + name = tok_val('$3')}, + args = element(1, '$4')}. +ptype -> '//' atom '/' qname ':' atom utype_list : + #t_type{name = #t_name{app = tok_val('$2'), + module = qname('$4'), + name = tok_val('$6')}, + args = element(1, '$7')}. + +%% Produced in reverse order. +fields -> field : ['$1']. +fields -> fields ',' field : ['$3' | '$1']. + +field -> atom '=' utype : + #t_field{name = #t_atom{val = tok_val('$1')}, type = '$3'}. + +%% Produced in reverse order. +defs -> '$empty' : []. +defs -> defs def : ['$2' | '$1']. +defs -> defs ',' def : ['$3' | '$1']. + +def -> var '=' utype: + #t_def{name = #t_var{name = tok_val('$1')}, + type = '$3'}. +def -> atom var_list '=' utype: + #t_def{name = #t_type{name = #t_name{name = tok_val('$1')}, + args = '$2'}, + type = '$4'}. + +var_list -> '(' ')' : []. +var_list -> '(' vars ')' : lists:reverse('$2'). + +%% Produced in reverse order. +vars -> var : [#t_var{name = tok_val('$1')}]. +vars -> vars ',' var : [#t_var{name = tok_val('$3')} | '$1']. + +typedef -> atom var_list where_defs: + #t_typedef{name = #t_name{name = tok_val('$1')}, + args = '$2', + defs = lists:reverse('$3')}. +typedef -> atom var_list '=' utype where_defs: + #t_typedef{name = #t_name{name = tok_val('$1')}, + args = '$2', + type = '$4', + defs = lists:reverse('$5')}. + +%% References + +ref -> aref: '$1'. +ref -> mref: '$1'. +ref -> lref: '$1'. +ref -> pref: '$1'. + +aref -> '//' atom: + edoc_refs:app(tok_val('$2')). +aref -> '//' atom '/' mref: + edoc_refs:app(tok_val('$2'), '$4'). +aref -> '//' atom '/' pref: + edoc_refs:app(tok_val('$2'), '$4'). + +mref -> qname ':' atom '/' integer: + edoc_refs:function(qname('$1'), tok_val('$3'), tok_val('$5')). +mref -> qname ':' atom '(' ')': + edoc_refs:type(qname('$1'), tok_val('$3')). +mref -> qname: + edoc_refs:module(qname('$1')). + +pref -> qname '.' '*': + edoc_refs:package(qname('$1')). + +lref -> atom '/' integer: + edoc_refs:function(tok_val('$1'), tok_val('$3')). +lref -> atom '(' ')': + edoc_refs:type(tok_val('$1')). + +%% Exception declarations + +etype -> utype: '$1'. + +throws -> etype where_defs: + #t_throws{type = '$1', + defs = lists:reverse('$2')}. + +%% (commented out for now) +%% Header +%% "%% ========================== -*-Erlang-*- =============================" +%% "%% EDoc function specification parser, generated from the file" +%% "%% \"edoc_parser.yrl\" by the Yecc parser generator." +%% "%%" +%% "%% Copyright (C) 2002-2005 Richard Carlsson" +%% "%%" +%% "%% 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" +%% "%%" +%% "%% @private" +%% "%% @author Richard Carlsson <[email protected]>" +%% "%% ====================================================================" +%% . + +Erlang code. + +%% ========================== -*-Erlang-*- ============================= +%% EDoc function specification parser, generated from the file +%% "edoc_parser.yrl" by the Yecc parser generator. +%% +%% Copyright (C) 2002-2005 Richard Carlsson +%% +%% 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 +%% ==================================================================== + +-export([parse_spec/2, parse_typedef/2, parse_throws/2, parse_ref/2, + parse_see/2, parse_param/2]). + +-include("edoc_types.hrl"). + +%% Multiple entry point hack: + +start_spec(Ts, L) -> run_parser(Ts, L, start_spec). + +start_typedef(Ts, L) -> run_parser(Ts, L, start_typedef). + +start_throws(Ts, L) -> run_parser(Ts, L, start_throws). + +start_ref(Ts, L) -> run_parser(Ts, L, start_ref). + +%% Error reporting fix + +run_parser(Ts, L, Start) -> + case parse([{Start,L} | Ts]) of + {error, {999999,?MODULE,_}} -> + What = case Start of + start_spec -> "specification"; + start_typedef -> "type definition"; + start_throws -> "exception declaration"; + start_ref -> "reference" + end, + {error, {L,?MODULE,["unexpected end of ", What]}}; + Other -> Other + end. + +%% Utility functions: + +tok_val(T) -> element(3, T). + +tok_line(T) -> element(2, T). + +qname([A]) -> + A; % avoid unnecessary call to packages:concat/1. +qname(List) -> + list_to_atom(packages:concat(lists:reverse(List))). + +union(Ts) -> + case Ts of + [T] -> T; + _ -> #t_union{types = lists:reverse(Ts)} + end. + +annotate(T, A) -> ?add_t_ann(T, A). + +%% --------------------------------------------------------------------- + +%% @doc EDoc type specification parsing. Parses the content of +%% <a href="overview-summary.html#ftag-spec">`@spec'</a> declarations. + +parse_spec(S, L) -> + case edoc_scanner:string(S, L) of + {ok, Ts, _} -> + case start_spec(Ts, L) of + {ok, Spec} -> + Spec; + {error, E} -> + throw_error(E, L) + end; + {error, E, _} -> + throw_error(E, L) + end. + +%% --------------------------------------------------------------------- + +%% @doc EDoc type definition parsing. Parses the content of +%% <a href="overview-summary.html#gtag-type">`@type'</a> declarations. + +parse_typedef(S, L) -> + {S1, S2} = edoc_lib:split_at_stop(S), + N = edoc_lib:count($\n, S1), + L1 = L + N, + Text = edoc_lib:strip_space(S2), + {parse_typedef_1(S1, L), edoc_wiki:parse_xml(Text, L1)}. + +parse_typedef_1(S, L) -> + case edoc_scanner:string(S, L) of + {ok, Ts, _} -> + case start_typedef(Ts, L) of + {ok, T} -> + T; + {error, E} -> + throw_error({parse_typedef, E}, L) + end; + {error, E, _} -> + throw_error({parse_typedef, E}, L) + end. + +%% --------------------------------------------------------------------- + +%% @doc Parses a <a +%% href="overview-summary.html#References">reference</a> to a module, +%% package, function, type, or application + +parse_ref(S, L) -> + case edoc_scanner:string(S, L) of + {ok, Ts, _} -> + case start_ref(Ts, L) of + {ok, T} -> + T; + {error, E} -> + throw_error({parse_ref, E}, L) + end; + {error, E, _} -> + throw_error({parse_ref, E}, L) + end. + +%% --------------------------------------------------------------------- + +%% @doc Parses the content of +%% <a href="overview-summary.html#ftag-see">`@see'</a> references. +parse_see(S, L) -> + {S1, S2} = edoc_lib:split_at_stop(S), + N = edoc_lib:count($\n, S1), + L1 = L + N, + Text = edoc_lib:strip_space(S2), + {parse_ref(S1, L), edoc_wiki:parse_xml(Text, L1)}. + +%% --------------------------------------------------------------------- + +%% @doc Parses the content of +%% <a href="overview-summary.html#ftag-param">`@param'</a> tags. +parse_param(S, L) -> + {S1, S2} = edoc_lib:split_at_space(edoc_lib:strip_space(S)), + case edoc_lib:strip_space(S1) of + "" -> throw_error(parse_param, L); + Name -> + Text = edoc_lib:strip_space(S2), + {list_to_atom(Name), edoc_wiki:parse_xml(Text, L)} + end. + +%% --------------------------------------------------------------------- + +%% @doc EDoc exception specification parsing. Parses the content of +%% <a href="overview-summary.html#ftag-throws">`@throws'</a> declarations. + +parse_throws(S, L) -> + case edoc_scanner:string(S, L) of + {ok, Ts, _} -> + case start_throws(Ts, L) of + {ok, Spec} -> + Spec; + {error, E} -> + throw_error({parse_throws, E}, L) + end; + {error, E, _} -> + throw_error({parse_throws, E}, L) + end. + +%% --------------------------------------------------------------------- + +throw_error({L, M, D}, _L0) -> + throw({error,L,{format_error,M,D}}); +throw_error({parse_spec, E}, L) -> + throw_error({"specification", E}, L); +throw_error({parse_typedef, E}, L) -> + throw_error({"type definition", E}, L); +throw_error({parse_ref, E}, L) -> + throw_error({"reference", E}, L); +throw_error({parse_throws, E}, L) -> + throw_error({"throws-declaration", E}, L); +throw_error(parse_param, L) -> + throw({error, L, "missing parameter name"}); +throw_error({Where, E}, L) when is_list(Where) -> + throw({error,L,{"unknown error parsing ~s: ~P.",[Where,E,15]}}); +throw_error(E, L) -> + %% Just in case. + throw({error,L,{"unknown parse error: ~P.",[E,15]}}). diff --git a/lib/edoc/src/edoc_refs.erl b/lib/edoc/src/edoc_refs.erl new file mode 100644 index 0000000000..c2146bbe02 --- /dev/null +++ b/lib/edoc/src/edoc_refs.erl @@ -0,0 +1,217 @@ +%% ===================================================================== +%% 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 2003 Richard Carlsson +%% @author Richard Carlsson <[email protected]> +%% @see edoc +%% @see edoc_parse_ref +%% @end +%% ===================================================================== + +%% @doc Representation and handling of EDoc object references. See +%% {@link edoc_parse_ref} for more details. + +-module(edoc_refs). + +-export([app/1, app/2, package/1, module/1, module/2, module/3, + function/2, function/3, function/4, type/1, type/2, type/3, + to_string/1, to_label/1, get_uri/2, is_top/2, + relative_module_path/2, relative_package_path/2]). + +-import(edoc_lib, [join_uri/2, escape_uri/1]). + +-include("edoc.hrl"). + +-define(INDEX_FILE, "index.html"). + + +%% Creating references: + +app(App) -> + {app, App}. + +app(App, Ref) -> + {app, App, Ref}. + +module(M) -> + {module, M}. + +module(M, Ref) -> + {module, M, Ref}. + +module(App, M, Ref) -> + app(App, module(M, Ref)). + +package(P) -> + {package, P}. + +function(F, A) -> + {function, F, A}. + +function(M, F, A) -> + module(M, function(F, A)). + +function(App, M, F, A) -> + module(App, M, function(F, A)). + +type(T) -> + {type, T}. + +type(M, T) -> + module(M, type(T)). + +type(App, M, T) -> + module(App, M, type(T)). + + +%% Creating a print string for a reference + +to_string({app, A}) -> + "//" ++ atom_to_list(A); +to_string({app, A, Ref}) -> + "//" ++ atom_to_list(A) ++ "/" ++ to_string(Ref); +to_string({module, M}) -> + atom_to_list(M) ; +to_string({module, M, Ref}) -> + atom_to_list(M) ++ ":" ++ to_string(Ref); +to_string({package, P}) -> + atom_to_list(P) ++ ".*"; +to_string({function, F, A}) -> + atom_to_list(F) ++ "/" ++ integer_to_list(A); +to_string({type, T}) -> + atom_to_list(T) ++ "()". + + +%% Creating URIs and anchors. + +to_label({function, F, A}) -> + escape_uri(atom_to_list(F)) ++ "-" ++ integer_to_list(A); +to_label({type, T}) -> + "type-" ++ escape_uri(atom_to_list(T)). + +get_uri({app, App}, Env) -> + join_uri(app_ref(App, Env), ?INDEX_FILE); +get_uri({app, App, Ref}, Env) -> + app_ref(App, Ref, Env); +get_uri({module, M, Ref}, Env) -> + module_ref(M, Env) ++ "#" ++ to_label(Ref); +get_uri({module, M}, Env) -> + module_ref(M, Env); +get_uri({package, P}, Env) -> + package_ref(P, Env); +get_uri(Ref, _Env) -> + "#" ++ to_label(Ref). + +abs_uri({module, M}, Env) -> + module_absref(M, Env); +abs_uri({module, M, Ref}, Env) -> + module_absref(M, Env) ++ "#" ++ to_label(Ref); +abs_uri({package, P}, Env) -> + package_absref(P, Env). + +module_ref(M, Env) -> + case (Env#env.modules)(M) of + "" -> + File = packages:last(M) ++ Env#env.file_suffix, + Path = relative_module_path(M, Env#env.package), + join_uri(Path, escape_uri(File)); + Base -> + join_uri(Base, module_absref(M, Env)) + end. + +module_absref(M, Env) -> + join_segments(packages:split(M)) + ++ escape_uri(Env#env.file_suffix). + +package_ref(P, Env) -> + case (Env#env.packages)(P) of + "" -> + join_uri(relative_package_path(P, Env#env.package), + escape_uri(Env#env.package_summary)); + Base -> + join_uri(Base, package_absref(P, Env)) + end. + +package_absref(P, Env) -> + join_uri(join_segments(packages:split(P)), + escape_uri(Env#env.package_summary)). + +app_ref(A, Env) -> + case (Env#env.apps)(A) of + "" -> + join_uri(Env#env.app_default, + join_uri(escape_uri(atom_to_list(A)), ?EDOC_DIR)); + Base -> + Base + end. + +app_ref(A, Ref, Env) -> + join_uri(app_ref(A, Env), abs_uri(Ref, Env)). + +is_top({app, _App}, _Env) -> + true; +is_top(_Ref, _Env) -> + false. + +%% Each segment of a path must be separately escaped before joining. + +join_segments([S]) -> + escape_uri(S); +join_segments([S | Ss]) -> + join_uri(escape_uri(S), join_segments(Ss)). + +%% 'From' is always the "current package" here: + +%% The empty string is returned if the To module has only one segment, +%% implying a local reference. + +relative_module_path(To, From) -> + case first(packages:split(To)) of + [] -> ""; + P -> relative_path(P, packages:split(From)) + end. + +relative_package_path(To, From) -> + relative_path(packages:split(To), packages:split(From)). + +%% This takes two lists of path segments (From, To). Note that an empty +%% string will be returned if the paths are the same. Empty leading +%% segments are stripped from both paths. + +relative_path(Ts, ["" | Fs]) -> + relative_path(Ts, Fs); +relative_path(["" | Ts], Fs) -> + relative_path(Ts, Fs); +relative_path(Ts, Fs) -> + relative_path_1(Ts, Fs). + +relative_path_1([T | Ts], [F | Fs]) when F == T -> + relative_path_1(Ts, Fs); +relative_path_1(Ts, Fs) -> + relative_path_2(Fs, Ts). + +relative_path_2([_F | Fs], Ts) -> + relative_path_2(Fs, [".." | Ts]); +relative_path_2([], []) -> + ""; +relative_path_2([], Ts) -> + join_segments(Ts). + +first([H | T]) when T /= [] -> [H | first(T)]; +first(_) -> []. diff --git a/lib/edoc/src/edoc_report.erl b/lib/edoc/src/edoc_report.erl new file mode 100644 index 0000000000..b87c58dde3 --- /dev/null +++ b/lib/edoc/src/edoc_report.erl @@ -0,0 +1,96 @@ +%% ===================================================================== +%% 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 EDoc verbosity/error reporting. + +-module(edoc_report). + +-export([error/1, + error/2, + error/3, + report/2, + report/3, + report/4, + warning/1, + warning/2, + warning/3, + warning/4]). + +-include("edoc.hrl"). + + +error(What) -> + error([], What). + +error(Where, What) -> + error(0, Where, What). + +error(Line, Where, S) when is_list(S) -> + report(Line, Where, S, []); +error(Line, Where, {S, D}) when is_list(S) -> + report(Line, Where, S, D); +error(Line, Where, {format_error, M, D}) -> + report(Line, Where, M:format_error(D), []). + +warning(S) -> + warning(S, []). + +warning(S, Vs) -> + warning([], S, Vs). + +warning(Where, S, Vs) -> + warning(0, Where, S, Vs). + +warning(L, Where, S, Vs) -> + report(L, Where, "warning: " ++ S, Vs). + +report(S, Vs) -> + report([], S, Vs). + +report(Where, S, Vs) -> + report(0, Where, S, Vs). + +report(L, Where, S, Vs) -> + io:put_chars(where(Where)), + if is_integer(L), L > 0 -> + io:fwrite("at line ~w: ", [L]); + true -> + ok + end, + io:fwrite(S, Vs), + io:nl(). + +where({File, module}) -> + io_lib:fwrite("~s, in module header: ", [File]); +where({File, footer}) -> + io_lib:fwrite("~s, in module footer: ", [File]); +where({File, header}) -> + io_lib:fwrite("~s, in header file: ", [File]); +where({File, {F, A}}) -> + io_lib:fwrite("~s, function ~s/~w: ", [File, F, A]); +where([]) -> + io_lib:fwrite("~s: ", [?APPLICATION]); +where(File) when is_list(File) -> + File ++ ": ". diff --git a/lib/edoc/src/edoc_run.erl b/lib/edoc/src/edoc_run.erl new file mode 100644 index 0000000000..37025d6621 --- /dev/null +++ b/lib/edoc/src/edoc_run.erl @@ -0,0 +1,225 @@ +%% ===================================================================== +%% 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$ +%% +%% @copyright 2003 Richard Carlsson +%% @author Richard Carlsson <[email protected]> +%% @see edoc +%% @end +%% ===================================================================== + +%% @doc Interface for calling EDoc from Erlang startup options. +%% +%% The following is an example of typical usage in a Makefile: +%% ```docs: +%% erl -noshell -run edoc_run application "'$(APP_NAME)'" \ +%% '"."' '[{def,{vsn,"$(VSN)"}}]' +%% ''' +%% (note the single-quotes to avoid shell expansion, and the +%% double-quotes enclosing the strings). +%% +%% <strong>New feature in version 0.6.9</strong>: It is no longer +%% necessary to write `-s init stop' last on the command line in order +%% to make the execution terminate. The termination (signalling success +%% or failure to the operating system) is now built into these +%% functions. + +-module(edoc_run). + +-export([file/1, application/1, packages/1, files/1, toc/1]). + +-import(edoc_report, [report/2, error/1]). + + +%% @spec application([string()]) -> none() +%% +%% @doc Calls {@link edoc:application/3} with the corresponding +%% arguments. The strings in the list are parsed as Erlang constant +%% terms. The list can be either `[App]', `[App, Options]' or `[App, +%% Dir, Options]'. In the first case {@link edoc:application/1} is +%% called instead; in the second case, {@link edoc:application/2} is +%% called. +%% +%% The function call never returns; instead, the emulator is +%% automatically terminated when the call has completed, signalling +%% success or failure to the operating system. + +application(Args) -> + F = fun () -> + case parse_args(Args) of + [App] -> edoc:application(App); + [App, Opts] -> edoc:application(App, Opts); + [App, Dir, Opts] -> edoc:application(App, Dir, Opts); + _ -> + invalid_args("edoc_run:application/1", Args) + end + end, + run(F). + +%% @spec files([string()]) -> none() +%% +%% @doc Calls {@link edoc:files/2} with the corresponding arguments. The +%% strings in the list are parsed as Erlang constant terms. The list can +%% be either `[Files]' or `[Files, Options]'. In the first case, {@link +%% edoc:files/1} is called instead. +%% +%% The function call never returns; instead, the emulator is +%% automatically terminated when the call has completed, signalling +%% success or failure to the operating system. + +files(Args) -> + F = fun () -> + case parse_args(Args) of + [Files] -> edoc:files(Files); + [Files, Opts] -> edoc:files(Files, Opts); + _ -> + invalid_args("edoc_run:files/1", Args) + end + end, + run(F). + +%% @spec packages([string()]) -> none() +%% +%% @doc Calls {@link edoc:application/2} with the corresponding +%% arguments. The strings in the list are parsed as Erlang constant +%% terms. The list can be either `[Packages]' or `[Packages, Options]'. +%% In the first case {@link edoc:application/1} is called instead. +%% +%% The function call never returns; instead, the emulator is +%% automatically terminated when the call has completed, signalling +%% success or failure to the operating system. + +packages(Args) -> + F = fun () -> + case parse_args(Args) of + [Packages] -> edoc:packages(Packages); + [Packages, Opts] -> edoc:packages(Packages, Opts); + _ -> + invalid_args("edoc_run:packages/1", Args) + end + end, + run(F). + +%% @hidden Not official yet +toc(Args) -> + F = fun () -> + case parse_args(Args) of + [Dir, Paths] -> edoc:toc(Dir,Paths); + [Dir, Paths, Opts] -> edoc:toc(Dir,Paths,Opts); + _ -> + invalid_args("edoc_run:toc/1", Args) + end + end, + run(F). + + +%% @spec file([string()]) -> none() +%% +%% @deprecated This is part of the old interface to EDoc and is mainly +%% kept for backwards compatibility. The preferred way of generating +%% documentation is through one of the functions {@link application/1}, +%% {@link packages/1} and {@link files/1}. +%% +%% @doc Calls {@link edoc:file/2} with the corresponding arguments. The +%% strings in the list are parsed as Erlang constant terms. The list can +%% be either `[File]' or `[File, Options]'. In the first case, an empty +%% list of options is passed to {@link edoc:file/2}. +%% +%% The following is an example of typical usage in a Makefile: +%% ```$(DOCDIR)/%.html:%.erl +%% erl -noshell -run edoc_run file '"$<"' '[{dir,"$(DOCDIR)"}]' \ +%% -s init stop''' +%% +%% The function call never returns; instead, the emulator is +%% automatically terminated when the call has completed, signalling +%% success or failure to the operating system. + +file(Args) -> + F = fun () -> + case parse_args(Args) of + [File] -> edoc:file(File, []); + [File, Opts] -> edoc:file(File, Opts); + _ -> + invalid_args("edoc_run:file/1", Args) + end + end, + run(F). + +-spec invalid_args(string(), list()) -> no_return(). + +invalid_args(Where, Args) -> + report("invalid arguments to ~s: ~w.", [Where, Args]), + shutdown_error(). + +run(F) -> + wait_init(), + case catch {ok, F()} of + {ok, _} -> + shutdown_ok(); + {'EXIT', E} -> + report("edoc terminated abnormally: ~P.", [E, 10]), + shutdown_error(); + Thrown -> + report("internal error: throw without catch in edoc: ~P.", + [Thrown, 15]), + shutdown_error() + end. + +wait_init() -> + case erlang:whereis(code_server) of + undefined -> + erlang:yield(), + wait_init(); + _ -> + ok + end. + +%% When and if a function init:stop/1 becomes generally available, we +%% can use that instead of delay-and-pray when there is an error. + +shutdown_ok() -> + %% shut down emulator nicely, signalling "normal termination" + init:stop(). + +shutdown_error() -> + %% delay 1 second to allow I/O to finish + receive after 1000 -> ok end, + %% stop emulator the hard way with a nonzero exit value + halt(1). + +parse_args([A | As]) when is_atom(A) -> + [parse_arg(atom_to_list(A)) | parse_args(As)]; +parse_args([A | As]) -> + [parse_arg(A) | parse_args(As)]; +parse_args([]) -> + []. + +parse_arg(A) -> + case catch {ok, edoc_lib:parse_expr(A, 1)} of + {ok, Expr} -> + case catch erl_parse:normalise(Expr) of + {'EXIT', _} -> + report("bad argument: '~s':", [A]), + exit(error); + Term -> + Term + end; + {error, _, D} -> + report("error parsing argument '~s'", [A]), + error(D), + exit(error) + end. diff --git a/lib/edoc/src/edoc_scanner.erl b/lib/edoc/src/edoc_scanner.erl new file mode 100644 index 0000000000..d3dff64682 --- /dev/null +++ b/lib/edoc/src/edoc_scanner.erl @@ -0,0 +1,358 @@ +%% ``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 express 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, Ericsson +%% Utvecklings AB. All Rights Reserved.'' +%% +%% $Id$ +%% +%% @private +%% @copyright Richard Carlsson 2001-2003. Portions created by Ericsson +%% are Copyright 1999, Ericsson Utvecklings AB. All Rights Reserved. +%% @author Richard Carlsson <[email protected]> +%% @see edoc +%% @end + +%% @doc Tokeniser for EDoc. Based on the Erlang standard library module +%% {@link //stdlib/erl_scan}. + +-module(edoc_scanner). + +%% NOTE: the interface to this module is ancient and should be updated. +%% Please do not regard these exported functions as stable. Their +%% behaviour is described in the documentation of the module `erl_scan'. +%% +%% Since there are no `full stop' tokens in EDoc specifications, the +%% `tokens' function *always* returns `{more, Continuation}' unless an +%% error occurs. + +-export([string/1,string/2,format_error/1]). + +-import(lists, [reverse/1]). + +string(Cs) -> string(Cs, 1). + +string(Cs, StartPos) -> + case scan(Cs, StartPos) of + {ok,Toks} -> {ok,Toks,StartPos}; + {error,E} -> {error,E,StartPos} + end. + +%% format_error(Error) +%% Return a string describing the error. + +format_error({string,Quote,Head}) -> + ["unterminated string starting with " ++ io_lib:write_string(Head,Quote)]; +format_error({illegal,Type}) -> io_lib:fwrite("illegal ~w", [Type]); +format_error(char) -> "unterminated character"; +format_error(scan) -> "premature end"; +format_error({base,Base}) -> io_lib:fwrite("illegal base '~w'", [Base]); +format_error(float) -> "bad float"; + +format_error(Other) -> io_lib:write(Other). + +%% Reserved words, not atoms: +reserved('where') -> true; +reserved(_) -> false. + +%% scan(CharList, StartPos) +%% This takes a list of characters and tries to tokenise them. +%% +%% The token list is built in reverse order (in a stack) to save appending +%% and then reversed when all the tokens have been collected. Most tokens +%% are built in the same way. +%% +%% Returns: +%% {ok,[Tok]} +%% {error,{ErrorPos,edoc_scanner,What}} + +scan(Cs, Pos) -> + scan1(Cs, [], Pos). + +%% scan1(Characters, TokenStack, Position) +%% Scan a list of characters into tokens. + +scan1([$\n|Cs], Toks, Pos) -> % Newline + scan1(Cs, Toks, Pos+1); +scan1([C|Cs], Toks, Pos) when C >= 0, C =< $ -> % Skip blanks + scan1(Cs, Toks, Pos); +scan1([C|Cs], Toks, Pos) when C >= $a, C =< $z -> % Unquoted atom + scan_atom(C, Cs, Toks, Pos); +scan1([C|Cs], Toks, Pos) when C >= $0, C =< $9 -> % Numbers + scan_number(C, Cs, Toks, Pos); +scan1([$-,C| Cs], Toks, Pos) when C >= $0, C =< $9 -> % Signed numbers + scan_signed_number($-, C, Cs, Toks, Pos); +scan1([$+,C| Cs], Toks, Pos) when C >= $0, C =< $9 -> % Signed numbers + scan_signed_number($+, C, Cs, Toks, Pos); +scan1([C|Cs], Toks, Pos) when C >= $A, C =< $Z -> % Variables + scan_variable(C, Cs, Toks, Pos); +scan1([$_|Cs], Toks, Pos) -> % Variables + scan_variable($_, Cs, Toks, Pos); +scan1([$$|Cs], Toks, Pos) -> % Character constant + case scan_char_const(Cs, Toks, Pos) of + {ok, Result} -> + {ok, Result}; + {error, truncated_char} -> + scan_error(char, Pos); + {error, illegal_character} -> + scan_error({illegal, char}, Pos) + end; +scan1([$'|Cs0], Toks, Pos) -> % Quoted atom + case scan_string(Cs0, $', Pos) of + {S,Cs1,Pos1} -> + case catch list_to_atom(S) of + A when is_atom(A) -> + scan1(Cs1, [{atom,Pos,A}|Toks], Pos1); + _Error -> scan_error({illegal,atom}, Pos) + end; + {error, premature_end} -> + scan_error({string,$',Cs0}, Pos); + {error, truncated_char} -> + scan_error(char, Pos); + {error, illegal_character} -> + scan_error({illegal, atom}, Pos) + end; +scan1([$"|Cs0], Toks, Pos) -> % String + case scan_string(Cs0, $", Pos) of + {S,Cs1,Pos1} -> + case Toks of + [{string, Pos0, S0} | Toks1] -> + scan1(Cs1, [{string, Pos0, S0 ++ S} | Toks1], + Pos1); + _ -> + scan1(Cs1, [{string,Pos,S}|Toks], Pos1) + end; + {error, premature_end} -> + scan_error({string,$",Cs0}, Pos); + {error, truncated_char} -> + scan_error(char, Pos); + {error, illegal_character} -> + scan_error({illegal, string}, Pos) + end; +%% Punctuation characters and operators, first recognise multiples. +scan1([$-,$>|Cs], Toks, Pos) -> + scan1(Cs, [{'->',Pos}|Toks], Pos); +scan1([$:,$:|Cs], Toks, Pos) -> + scan1(Cs, [{'::',Pos}|Toks], Pos); +scan1([$/,$/|Cs], Toks, Pos) -> + scan1(Cs, [{'//',Pos}|Toks], Pos); +scan1([C|Cs], Toks, Pos) -> % Punctuation character + P = list_to_atom([C]), + scan1(Cs, [{P,Pos}|Toks], Pos); +scan1([], Toks0, _Pos) -> + Toks = reverse(Toks0), + {ok,Toks}. + +%% Note that `_' is not accepted as a variable token. +scan_variable(C, Cs, Toks, Pos) -> + {Wcs,Cs1} = scan_name(Cs, []), + W = [C|reverse(Wcs)], + case W of + "_" -> + scan_error({illegal,token}, Pos); + _ -> + case catch list_to_atom(W) of + A when is_atom(A) -> + scan1(Cs1, [{var,Pos,A}|Toks], Pos); + _ -> + scan_error({illegal,variable}, Pos) + end + end. + +scan_atom(C, Cs, Toks, Pos) -> + {Wcs,Cs1} = scan_name(Cs, []), + W = [C|reverse(Wcs)], + case catch list_to_atom(W) of + A when is_atom(A) -> + case reserved(A) of + true -> + scan1(Cs1, [{A,Pos}|Toks], Pos); + false -> + scan1(Cs1, [{atom,Pos,A}|Toks], Pos) + end; + _ -> + scan_error({illegal,token}, Pos) + end. + +%% scan_name(Cs) -> lists:splitwith(fun (C) -> name_char(C) end, Cs). + +scan_name([C|Cs], Ncs) -> + case name_char(C) of + true -> + scan_name(Cs, [C|Ncs]); + false -> + {Ncs,[C|Cs]} % Must rebuild here, sigh! + end; +scan_name([], Ncs) -> + {Ncs,[]}. + +name_char(C) when C >= $a, C =< $z -> true; +name_char(C) when C >= $\337, C =< $\377, C /= $\367 -> true; +name_char(C) when C >= $A, C =< $Z -> true; +name_char(C) when C >= $\300, C =< $\336, C /= $\327 -> true; +name_char(C) when C >= $0, C =< $9 -> true; +name_char($_) -> true; +name_char($@) -> true; +name_char(_) -> false. + +%% scan_string(CharList, QuoteChar, Pos) -> +%% {StringChars,RestChars, NewPos} + +scan_string(Cs, Quote, Pos) -> + scan_string(Cs, [], Quote, Pos). + +scan_string([Quote|Cs], Scs, Quote, Pos) -> + {reverse(Scs),Cs,Pos}; +scan_string([], _Scs, _Quote, _Pos) -> + {error, premature_end}; +scan_string(Cs0, Scs, Quote, Pos) -> + case scan_char(Cs0, Pos) of + {C,Cs,Pos1} -> + %% Only build the string here + scan_string(Cs, [C|Scs], Quote, Pos1); + Error -> + Error + end. + +%% Note that space characters are not allowed +scan_char_const([$\040 | _Cs0], _Toks, _Pos) -> + {error, illegal_character}; +scan_char_const(Cs0, Toks, Pos) -> + case scan_char(Cs0, Pos) of + {C,Cs,Pos1} -> + scan1(Cs, [{char,Pos,C}|Toks], Pos1); + Error -> + Error + end. + +%% {Character,RestChars,NewPos} = scan_char(Chars, Pos) +%% Read a single character from a string or character constant. The +%% pre-scan phase has checked for errors here. +%% Note that control characters are not allowed. + +scan_char([$\\|Cs], Pos) -> + scan_escape(Cs, Pos); +scan_char([C | _Cs], _Pos) when C =< 16#1f -> + {error, illegal_character}; +scan_char([C|Cs], Pos) -> + {C,Cs,Pos}; +scan_char([], _Pos) -> + {error, truncated_char}. + +%% The following conforms to Standard Erlang escape sequences. + +scan_escape([O1, O2, O3 | Cs], Pos) when % \<1-3> octal digits + O1 >= $0, O1 =< $3, O2 >= $0, O2 =< $7, O3 >= $0, O3 =< $7 -> + Val = (O1*8 + O2)*8 + O3 - 73*$0, + {Val,Cs,Pos}; +scan_escape([O1, O2 | Cs], Pos) when + O1 >= $0, O1 =< $7, O2 >= $0, O2 =< $7 -> + Val = (O1*8 + O2) - 9*$0, + {Val,Cs,Pos}; +scan_escape([O1 | Cs], Pos) when + O1 >= $0, O1 =< $7 -> + {O1 - $0,Cs,Pos}; +scan_escape([$^, C | Cs], Pos) -> % \^X -> CTL-X + if C >= $\100, C =< $\137 -> + {C - $\100,Cs,Pos}; + true -> {error, illegal_control_character} + end; +scan_escape([C | Cs], Pos) -> + case escape_char(C) of + C1 when C1 > $\000 -> {C1,Cs,Pos}; + _ -> {error, undefined_escape_sequence} + end; +scan_escape([], _Pos) -> + {error, truncated_char}. + +%% Note that we return $\000 for undefined escapes. +escape_char($b) -> $\010; % \b = BS +escape_char($d) -> $\177; % \d = DEL +escape_char($e) -> $\033; % \e = ESC +escape_char($f) -> $\014; % \f = FF +escape_char($n) -> $\012; % \n = LF +escape_char($r) -> $\015; % \r = CR +escape_char($s) -> $\040; % \s = SPC +escape_char($t) -> $\011; % \t = HT +escape_char($v) -> $\013; % \v = VT +escape_char($\\) -> $\134; % \\ = \ +escape_char($') -> $\047; % \' = ' +escape_char($") -> $\042; % \" = " +escape_char(_C) -> $\000. + +%% scan_number(Char, CharList, TokenStack, Pos) +%% We handle sign and radix notation: +%% [+-]<digits> - the digits in base [+-]10 +%% [+-]<digits>.<digits> +%% [+-]<digits>.<digits>E+-<digits> +%% [+-]<digits>#<digits> - the digits read in base [+-]B +%% +%% Except for explicitly based integers we build a list of all the +%% characters and then use list_to_integer/1 or list_to_float/1 to +%% generate the value. + +%% SPos == Start position +%% CPos == Current position + +scan_number(C, Cs0, Toks, Pos) -> + {Ncs,Cs,Pos1} = scan_integer(Cs0, [C], Pos), + scan_after_int(Cs, Ncs, Toks, Pos, Pos1). + +scan_signed_number(S, C, Cs0, Toks, Pos) -> + {Ncs,Cs,Pos1} = scan_integer(Cs0, [C, S], Pos), + scan_after_int(Cs, Ncs, Toks, Pos, Pos1). + +scan_integer([C|Cs], Stack, Pos) when C >= $0, C =< $9 -> + scan_integer(Cs, [C|Stack], Pos); +scan_integer(Cs, Stack, Pos) -> + {Stack,Cs,Pos}. + +scan_after_int([$.,C|Cs0], Ncs0, Toks, SPos, CPos) when C >= $0, C =< $9 -> + {Ncs,Cs,CPos1} = scan_integer(Cs0, [C,$.|Ncs0], CPos), + scan_after_fraction(Cs, Ncs, Toks, SPos, CPos1); +scan_after_int(Cs, Ncs, Toks, SPos, CPos) -> + N = list_to_integer(reverse(Ncs)), + scan1(Cs, [{integer,SPos,N}|Toks], CPos). + +scan_after_fraction([$E|Cs], Ncs, Toks, SPos, CPos) -> + scan_exponent(Cs, [$E|Ncs], Toks, SPos, CPos); +scan_after_fraction([$e|Cs], Ncs, Toks, SPos, CPos) -> + scan_exponent(Cs, [$e|Ncs], Toks, SPos, CPos); +scan_after_fraction(Cs, Ncs, Toks, SPos, CPos) -> + case catch list_to_float(reverse(Ncs)) of + N when is_float(N) -> + scan1(Cs, [{float,SPos,N}|Toks], CPos); + _Error -> scan_error({illegal,float}, SPos) + end. + +%% scan_exponent(CharList, NumberCharStack, TokenStack, StartPos, CurPos) +%% Generate an error here if E{+|-} not followed by any digits. + +scan_exponent([$+|Cs], Ncs, Toks, SPos, CPos) -> + scan_exponent1(Cs, [$+|Ncs], Toks, SPos, CPos); +scan_exponent([$-|Cs], Ncs, Toks, SPos, CPos) -> + scan_exponent1(Cs, [$-|Ncs], Toks, SPos, CPos); +scan_exponent(Cs, Ncs, Toks, SPos, CPos) -> + scan_exponent1(Cs, Ncs, Toks, SPos, CPos). + +scan_exponent1([C|Cs0], Ncs0, Toks, SPos, CPos) when C >= $0, C =< $9 -> + {Ncs,Cs,CPos1} = scan_integer(Cs0, [C|Ncs0], CPos), + case catch list_to_float(reverse(Ncs)) of + N when is_float(N) -> + scan1(Cs, [{float,SPos,N}|Toks], CPos1); + _Error -> scan_error({illegal,float}, SPos) + end; +scan_exponent1(_, _, _, _, CPos) -> + scan_error(float, CPos). + +scan_error(In, Pos) -> + {error,{Pos,edoc_scanner,In}}. diff --git a/lib/edoc/src/edoc_tags.erl b/lib/edoc/src/edoc_tags.erl new file mode 100644 index 0000000000..1f2cb99c75 --- /dev/null +++ b/lib/edoc/src/edoc_tags.erl @@ -0,0 +1,373 @@ +%% ===================================================================== +%% 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 EDoc tag scanning. + +%% TODO: tag/macro for including the code of a function as `<pre>'-text. +%% TODO: consider new tag: @license text + +-module(edoc_tags). + +-export([tags/0, tags/1, tag_names/0, tag_parsers/0, scan_lines/2, + filter_tags/3, check_tags/4, parse_tags/4]). + +-import(edoc_report, [report/4, warning/4, error/3]). + +-include("edoc.hrl"). +-include("edoc_types.hrl"). + + +%% Tags are described by {Name, Parser, Flags}. +%% Name = atom() +%% Parser = text | xml | (Text,Line,Where) -> term() +%% Flags = [Flag] +%% Flag = module | function | package | overview | single +%% +%% Note that the pseudo-tag '@clear' is not listed here. +%% (Cf. the function 'filter_tags'.) +%% +%% Rejected tag suggestions: +%% - @keywords (never up to date; free text search is better) +%% - @uses [modules] (never up to date; false dependencies) +%% - @maintainer (never up to date; duplicates author info) +%% - @contributor (unnecessary; mention in normal documentation) +%% - @creator (unnecessary; already have copyright/author) +%% - @history (never properly updated; use version control etc.) +%% - @category (useless; superseded by keywords or free text search) + +tags() -> + All = [module,footer,function,package,overview], + [{author, fun parse_contact/4, [module,package,overview]}, + {copyright, text, [module,package,overview,single]}, + {deprecated, xml, [module,function,package,single]}, + {doc, xml, [module,function,package,overview,single]}, + {docfile, fun parse_file/4, All}, + {'end', text, All}, + {equiv, fun parse_expr/4, [function,single]}, + {headerfile, fun parse_header/4, All}, + {hidden, text, [module,function,single]}, + {param, fun parse_param/4, [function]}, + {private, text, [module,function,single]}, + {reference, xml, [module,footer,package,overview]}, + {returns, xml, [function,single]}, + {see, fun parse_see/4, [module,function,package,overview]}, + {since, text, [module,function,package,overview,single]}, + {spec, fun parse_spec/4, [function,single]}, + {throws, fun parse_throws/4, [function,single]}, + {title, text, [overview,single]}, + {'TODO', xml, All}, + {todo, xml, All}, + {type, fun parse_typedef/4, [module,footer,function]}, + {version, text, [module,package,overview,single]}]. + +aliases('TODO') -> todo; +aliases(return) -> returns; +aliases(T) -> T. + +%% Selecting tags based on flags. +tags(Flag) -> + [T || {T,_,Fs} <- tags(), lists:member(Flag, Fs)]. + +%% The set of known tags. +tag_names() -> + [T || {T,_,_} <- tags()]. + +%% The pairs of tags and their parsers. +tag_parsers() -> + [{T,F} || {T,F,_} <- tags()]. + + +%% Scanning lines of comment text. + +scan_lines(Ss, L) -> + lists:reverse(scan_lines(Ss, L, [])). + +scan_lines([S | Ss], L, As) -> + scan_lines(S, Ss, L, As); +scan_lines([], _L, As) -> + As. + +%% Looking for a leading '@', skipping whitespace. +%% Also accept "TODO:" at start of line as equivalent to "@TODO". + +scan_lines([$\s | Cs], Ss, L, As) -> scan_lines(Cs, Ss, L, As); +scan_lines([$\t | Cs], Ss, L, As) -> scan_lines(Cs, Ss, L, As); +scan_lines([$@ | Cs], Ss, L, As) -> scan_tag(Cs, Ss, L, As, []); +scan_lines(("TODO:"++_)=Cs, Ss, L, As) -> scan_tag(Cs, Ss, L, As, []); +scan_lines(_, Ss, L, As) -> scan_lines(Ss, L + 1, As). + +%% Scanning chars following '@', accepting only nonempty valid names. +%% See edoc_lib:is_name/1 for details on what is a valid name. In tags +%% we also allow the initial letter to be uppercase or underscore. + +scan_tag([C | Cs], Ss, L, As, Ts) when C >= $a, C =< $z -> + scan_tag_1(Cs, Ss, L, As, [C | Ts]); +scan_tag([C | Cs], Ss, L, As, Ts) when C >= $A, C =< $Z -> + scan_tag_1(Cs, Ss, L, As, [C | Ts]); +scan_tag([C | Cs], Ss, L, As, Ts) when C >= $\300, C =< $\377, + C =/= $\327, C =/= $\367 -> + scan_tag_1(Cs, Ss, L, As, [C | Ts]); +scan_tag([$_ | Cs], Ss, L, As, Ts) -> + scan_tag_1(Cs, Ss, L, As, [$_ | Ts]); +scan_tag(_Cs, Ss, L, As, _Ts) -> + scan_lines(Ss, L + 1, As). % not a valid name + +scan_tag_1([C | Cs], Ss, L, As, Ts) when C >= $a, C =< $z -> + scan_tag_1(Cs, Ss, L, As, [C | Ts]); +scan_tag_1([C | Cs], Ss, L, As, Ts) when C >= $A, C =< $Z -> + scan_tag_1(Cs, Ss, L, As, [C | Ts]); +scan_tag_1([C | Cs], Ss, L, As, Ts) when C >= $0, C =< $9 -> + scan_tag_1(Cs, Ss, L, As, [C | Ts]); +scan_tag_1([C | Cs], Ss, L, As, Ts) when C >= $\300, C =< $\377, + C =/= $\327, C =/= $\367 -> + scan_tag_1(Cs, Ss, L, As, [C | Ts]); +scan_tag_1([$_ | Cs], Ss, L, As, Ts) -> + scan_tag_1(Cs, Ss, L, As, [$_ | Ts]); +scan_tag_1(Cs, Ss, L, As, Ts) -> + scan_tag_2(Cs, Ss, L, As, {Ts, L}). + +%% Check that the tag is followed by whitespace, linebreak, or colon. + +scan_tag_2([$\s | Cs], Ss, L, As, T) -> + scan_tag_lines(Ss, T, [Cs], L + 1, As); +scan_tag_2([$\t | Cs], Ss, L, As, T) -> + scan_tag_lines(Ss, T, [Cs], L + 1, As); +scan_tag_2([$: | Cs], Ss, L, As, T) -> + scan_tag_lines(Ss, T, [Cs], L + 1, As); +scan_tag_2([], Ss, L, As, T) -> + scan_tag_lines(Ss, T, [[]], L + 1, As); +scan_tag_2(_, Ss, L, As, _T) -> + scan_lines(Ss, L + 1, As). + +%% Scanning lines after a tag is found. + +scan_tag_lines([S | Ss], T, Ss1, L, As) -> + scan_tag_lines(S, S, Ss, T, Ss1, L, As); +scan_tag_lines([], {Ts, L1}, Ss1, _L, As) -> + [make_tag(Ts, L1, Ss1) | As]. + +%% Collecting tag text lines until end of comment or next tagged line. + +scan_tag_lines([$\s | Cs], S, Ss, T, Ss1, L, As) -> + scan_tag_lines(Cs, S, Ss, T, Ss1, L, As); +scan_tag_lines([$\t | Cs], S, Ss, T, Ss1, L, As) -> + scan_tag_lines(Cs, S, Ss, T, Ss1, L, As); +scan_tag_lines([$@, C | _Cs], S, Ss, {Ts, L1}, Ss1, L, As) + when C >= $a, C =< $z -> + scan_lines(S, Ss, L, [make_tag(Ts, L1, Ss1) | As]); +scan_tag_lines([$@, C | _Cs], S, Ss, {Ts, L1}, Ss1, L, As) + when C >= $A, C =< $Z -> + scan_lines(S, Ss, L, [make_tag(Ts, L1, Ss1) | As]); +scan_tag_lines([$@, C | _Cs], S, Ss, {Ts, L1}, Ss1, L, As) + when C >= $\300, C =< $\377, C =/= $\327, C =/= $\367 -> + scan_lines(S, Ss, L, [make_tag(Ts, L1, Ss1) | As]); +scan_tag_lines("TODO:"++_, S, Ss, {Ts, L1}, Ss1, L, As) -> + scan_lines(S, Ss, L, [make_tag(Ts, L1, Ss1) | As]); +scan_tag_lines(_Cs, S, Ss, T, Ss1, L, As) -> + scan_tag_lines(Ss, T, [S | Ss1], L + 1, As). + +make_tag(Cs, L, Ss) -> + #tag{name = aliases(list_to_atom(lists:reverse(Cs))), + line = L, + data = append_lines(lists:reverse(Ss))}. + +%% Flattening lines of text and inserting line breaks. + +append_lines([L]) -> L; +append_lines([L | Ls]) -> L ++ [$\n | append_lines(Ls)]; +append_lines([]) -> []. + +%% Filtering out unknown tags. + +filter_tags(Ts, Tags, Where) -> + filter_tags(Ts, Tags, Where, []). + +filter_tags([#tag{name = clear} | Ts], Tags, Where, _Ts1) -> + filter_tags(Ts, Tags, Where); +filter_tags([#tag{name = N, line = L} = T | Ts], Tags, Where, Ts1) -> + case sets:is_element(N, Tags) of + true -> + filter_tags(Ts, Tags, Where, [T | Ts1]); + false -> + warning(L, Where, "tag @~s not recognized.", [N]), + filter_tags(Ts, Tags, Where, Ts1) + end; +filter_tags([], _, _, Ts) -> + lists:reverse(Ts). + +%% Check occurrances of tags. + +check_tags(Ts, Allow, Single, Where) -> + check_tags(Ts, Allow, Single, Where, false, sets:new()). + +check_tags([#tag{name = T, line = L} | Ts], Allow, Single, Where, Error, Seen) -> + case sets:is_element(T, Seen) of + true -> + case sets:is_element(T, Single) of + false -> + check_tags(Ts, Allow, Single, Where, Error, Seen); + true -> + report(L, Where, "multiple @~s tag.", [T]), + check_tags(Ts, Allow, Single, Where, true, Seen) + end; + false -> + Seen1 = sets:add_element(T, Seen), + case sets:is_element(T, Allow) of + true -> + check_tags(Ts, Allow, Single, Where, Error, Seen1); + false -> + report(L, Where, "tag @~s not allowed here.", [T]), + check_tags(Ts, Allow, Single, Where, true, Seen1) + end + end; +check_tags([], _, _, _, Error, _) -> + Error. + + +%% Parses tag contents for specific tags. + +parse_tags(Ts, How, Env, Where) -> + parse_tags(Ts, How, Env, Where, []). + +parse_tags([#tag{name = Name} = T | Ts], How, Env, Where, Ts1) -> + case dict:fetch(Name, How) of + text -> + parse_tags(Ts, How, Env, Where, [T | Ts1]); + xml -> + [T1] = parse_tag(T, fun parse_xml/4, Env, Where), + parse_tags(Ts, How, Env, Where, [T1 | Ts1]); + F when is_function(F) -> + Ts2 = parse_tag(T, F, Env, Where), + parse_tags(Ts, How, Env, Where, lists:reverse(Ts2, Ts1)) + end; +parse_tags([], _How, _Env, _Where, Ts) -> + lists:reverse(Ts). + +parse_tag(T, F, Env, Where) -> + case catch {ok, F(T#tag.data, T#tag.line, Env, Where)} of + {ok, Data} -> + [T#tag{data = Data}]; + {expand, Ts} -> + Ts; + {error, L, Error} -> + error(L, Where, Error), + exit(error); + {'EXIT', R} -> exit(R); + Other -> throw(Other) + end. + +%% parser functions for the built-in content types. They also perform +%% some sanity checks on the results. + +parse_xml(Data, Line, _Env, _Where) -> + edoc_wiki:parse_xml(Data, Line). + +parse_see(Data, Line, _Env, _Where) -> + edoc_parser:parse_see(Data, Line). + +parse_expr(Data, Line, _Env, _Where) -> + edoc_lib:parse_expr(Data, Line). + +parse_spec(Data, Line, _Env, {_, {F, A}} = _Where) -> + Spec = edoc_parser:parse_spec(Data, Line), + #t_spec{name = N, type = #t_fun{args = As}} = Spec, + if length(As) /= A -> + throw_error(Line, "@spec arity does not match."); + true -> + case N of + undefined -> + Spec#t_spec{name = #t_name{module = [], name = F}}; + #t_name{module = [], name = F} -> + Spec; + _ -> + throw_error(Line, "@spec name does not match.") + end + end. + +parse_param(Data, Line, _Env, {_, {_F, _A}} = _Where) -> + edoc_parser:parse_param(Data, Line). + +parse_throws(Data, Line, _Env, {_, {_F, _A}} = _Where) -> + edoc_parser:parse_throws(Data, Line). + +parse_contact(Data, Line, _Env, _Where) -> + case edoc_lib:parse_contact(Data, Line) of + {"", "", _URI} -> + throw_error(Line, "must specify name or e-mail."); + Info -> + Info + end. + +parse_typedef(Data, Line, _Env, _Where) -> + Def = edoc_parser:parse_typedef(Data, Line), + {#t_typedef{name = #t_name{name = T}}, _} = Def, + case edoc_types:is_predefined(T) of + true -> + throw_error(Line, {"redefining built-in type '~w'.", [T]}); + false -> + Def + end. + +parse_file(Data, Line, Env, _Where) -> + case edoc_lib:parse_expr(Data, Line) of + {string, _, File0} -> + File = edoc_lib:strip_space(File0), + case edoc_extract:file(File, module, Env, []) of + {ok, Ts} -> + throw({expand, Ts}); + {error, R} -> + throw_error(Line, {read_file, File, R}) + end; + _ -> + throw_error(Line, file_not_string) + end. + +parse_header(Data, Line, Env, {Where, _}) -> + parse_header(Data, Line, Env, Where); +parse_header(Data, Line, Env, Where) when is_list(Where) -> + case edoc_lib:parse_expr(Data, Line) of + {string, _, File} -> + Dir = filename:dirname(Where), + Path = Env#env.includes ++ [Dir], + case edoc_lib:find_file(Path, "", File) of + "" -> + throw_error(Line, {file_not_found, File}); + File1 -> + Ts = edoc_extract:header(File1, Env, []), + throw({expand, Ts}) + end; + _ -> + throw_error(Line, file_not_string) + end. + +throw_error(L, {read_file, File, R}) -> + throw_error(L, {"error reading file '~s': ~w", + [edoc_lib:filename(File), R]}); +throw_error(L, {file_not_found, F}) -> + throw_error(L, {"file not found: ~s", [F]}); +throw_error(L, file_not_string) -> + throw_error(L, "expected file name as a string"); +throw_error(L, D) -> + throw({error, L, D}). diff --git a/lib/edoc/src/edoc_types.erl b/lib/edoc/src/edoc_types.erl new file mode 100644 index 0000000000..85c9ee6f2a --- /dev/null +++ b/lib/edoc/src/edoc_types.erl @@ -0,0 +1,204 @@ +%% ===================================================================== +%% 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 Datatype representation for EDoc. + +-module(edoc_types). + +-export([is_predefined/1, to_ref/1, to_xml/2, to_label/1, arg_names/1, + set_arg_names/2, arg_descs/1, range_desc/1]). + +%% @headerfile "edoc_types.hrl" + +-include("edoc_types.hrl"). +-include("xmerl.hrl"). + + +is_predefined(any) -> true; +is_predefined(atom) -> true; +is_predefined(binary) -> true; +is_predefined(bool) -> true; +is_predefined(char) -> true; +is_predefined(cons) -> true; +is_predefined(deep_string) -> true; +is_predefined(float) -> true; +is_predefined(function) -> true; +is_predefined(integer) -> true; +is_predefined(list) -> true; +is_predefined(nil) -> true; +is_predefined(none) -> true; +is_predefined(number) -> true; +is_predefined(pid) -> true; +is_predefined(port) -> true; +is_predefined(reference) -> true; +is_predefined(string) -> true; +is_predefined(term) -> true; +is_predefined(tuple) -> true; +is_predefined(_) -> false. + +to_ref(#t_typedef{name = N}) -> + to_ref(N); +to_ref(#t_def{name = N}) -> + to_ref(N); +to_ref(#t_type{name = N}) -> + to_ref(N); +to_ref(#t_name{module = [], name = N}) -> + edoc_refs:type(N); +to_ref(#t_name{app = [], module = M, name = N}) -> + edoc_refs:type(M, N); +to_ref(#t_name{app = A, module = M, name = N}) -> + edoc_refs:type(A, M, N). + +to_label(N) -> + edoc_refs:to_label(to_ref(N)). + +get_uri(Name, Env) -> + edoc_refs:get_uri(to_ref(Name), Env). + +to_xml(#t_var{name = N}, _Env) -> + {typevar, [{name, atom_to_list(N)}], []}; +to_xml(#t_name{module = [], name = N}, _Env) -> + {erlangName, [{name, atom_to_list(N)}], []}; +to_xml(#t_name{app = [], module = M, name = N}, _Env) -> + {erlangName, [{module, atom_to_list(M)}, + {name, atom_to_list(N)}], []}; +to_xml(#t_name{app = A, module = M, name = N}, _Env) -> + {erlangName, [{app, atom_to_list(A)}, + {module, atom_to_list(M)}, + {name, atom_to_list(N)}], []}; +to_xml(#t_type{name = N, args = As}, Env) -> + Predef = case N of + #t_name{module = [], name = T} -> + is_predefined(T); + _ -> + false + end, + HRef = case Predef of + true -> []; + false -> [{href, get_uri(N, Env)}] + end, + {abstype, HRef, [to_xml(N, Env) | map(fun wrap_utype/2, As, Env)]}; +to_xml(#t_fun{args = As, range = T}, Env) -> + {'fun', [{argtypes, map(fun wrap_utype/2, As, Env)}, + wrap_utype(T, Env)]}; +to_xml(#t_tuple{types = Ts}, Env) -> + {tuple, map(fun wrap_utype/2, Ts, Env)}; +to_xml(#t_list{type = T}, Env) -> + {list, [wrap_utype(T, Env)]}; +to_xml(#t_nil{}, _Env) -> + nil; +to_xml(#t_atom{val = V}, _Env) -> + {atom, [{value, io_lib:write(V)}], []}; +to_xml(#t_integer{val = V}, _Env) -> + {integer, [{value, integer_to_list(V)}], []}; +to_xml(#t_float{val = V}, _Env) -> + {float, [{value, io_lib:write(V)}], []}; +to_xml(#t_union{types = Ts}, Env) -> + {union, map(fun wrap_type/2, Ts, Env)}; +to_xml(#t_record{name = N = #t_atom{}, fields = Fs}, Env) -> + {record, [to_xml(N, Env) | map(fun to_xml/2, Fs, Env)]}; +to_xml(#t_field{name = N = #t_atom{}, type = T}, Env) -> + {field, [to_xml(N, Env), wrap_type(T, Env)]}; +to_xml(#t_def{name = N = #t_var{}, type = T}, Env) -> + {localdef, [to_xml(N, Env), wrap_type(T, Env)]}; +to_xml(#t_def{name = N, type = T}, Env) -> + {localdef, [{label, to_label(N)}], + [to_xml(N, Env), wrap_type(T, Env)]}; +to_xml(#t_spec{name = N, type = T, defs = Ds}, Env) -> + {typespec, [to_xml(N, Env), wrap_utype(T, Env) + | map(fun to_xml/2, Ds, Env)]}; +to_xml(#t_typedef{name = N, args = As, type = undefined, defs = Ds}, + Env) -> + {typedef, [to_xml(N, Env), + {argtypes, map(fun wrap_utype/2, As, Env)} + | map(fun to_xml/2, Ds, Env)]}; +to_xml(#t_typedef{name = N, args = As, type = T, defs = Ds}, Env) -> + {typedef, [to_xml(N, Env), + {argtypes, map(fun wrap_utype/2, As, Env)}, + wrap_type(T, Env) + | map(fun to_xml/2, Ds, Env)]}; +to_xml(#t_throws{type = T, defs = Ds}, Env) -> + {throws, [wrap_type(T, Env) + | map(fun to_xml/2, Ds, Env)]}. + +wrap_type(T, Env) -> + {type, [to_xml(T, Env)]}. + +wrap_utype(T, Env) -> + E = to_xml(T, Env), + case arg_name(T) of + '_' -> {type, [E]}; + A -> {type, [{name, atom_to_list(A)}], [E]} + end. + +map(F, Xs, Env) -> + [F(X, Env) || X <- Xs]. + +is_name(A) when is_atom(A) -> true; +is_name(_) -> false. + +is_desc(A) when is_list(A) -> true; +is_desc(_) -> false. + +arg_name(T) -> + find(?t_ann(T), fun is_name/1, '_'). + +arg_names(S) -> + arg_anns(S, fun is_name/1, '_'). + +arg_descs(S) -> + arg_anns(S, fun is_desc/1, ""). + +range_desc(#t_spec{type = #t_fun{range = T}}) -> + find(?t_ann(T), fun is_desc/1, ""). + +arg_anns(#t_spec{type = #t_fun{args = As}}, F, Def) -> + [find(?t_ann(A), F, Def) || A <- As]. + +find([A| As], F, Def) -> + case F(A) of + true -> A; + false -> find(As, F, Def) + end; +find([], _, Def) -> Def. + +set_arg_names(S, Ns) -> + set_arg_anns(S, Ns, fun is_name/1). + +%% set_arg_descs(S, Ns) -> +%% set_arg_anns(S, Ns, fun is_desc/1). + +set_arg_anns(#t_spec{type = #t_fun{args = As}=T}=S, Ns, F) -> + Zip = fun (A, N) -> + ?set_t_ann(A, update(?t_ann(A), N, F)) + end, + S#t_spec{type = T#t_fun{args = lists:zipwith(Zip, As, Ns)}}. + +update([A| As], N, F) -> + case F(A) of + true -> [N | As]; + false -> [A| update(As, N, F)] + end; +update([], N, _) -> [N]. diff --git a/lib/edoc/src/edoc_types.hrl b/lib/edoc/src/edoc_types.hrl new file mode 100644 index 0000000000..1dcbdd9493 --- /dev/null +++ b/lib/edoc/src/edoc_types.hrl @@ -0,0 +1,130 @@ +%% ===================================================================== +%% Header file for EDoc Type Representations +%% +%% Copyright (C) 2001-2005 Richard Carlsson +%% +%% 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 +%% +%% Author contact: [email protected] +%% ===================================================================== + +%% Type specification data structures + +%% @type t_spec() = #t_spec{name = t_name(), +%% type = t_type(), +%% defs = [t_def()]} + +-record(t_spec, {name, type, defs=[]}). % function specification + +%% @type type() = t_atom() | t_fun() | t_integer() | t_list() | t_nil() +%% | t_tuple() | t_type() | t_union() | t_var() + +%% @type t_typedef() = #t_typedef{name = t_name(), +%% args = [type()], +%% type = type(), +%% defs = [t_def()]} + +-record(t_typedef, {name, args, type, + defs=[]}). % type declaration/definition + +%% @type t_throws() = #t_throws{type = type(), +%% defs = [t_def()]} + +-record(t_throws, {type, defs=[]}). % exception declaration + +%% @type t_def() = #t_def{name = t_name(), +%% type = type()} + +-record(t_def, {name, type}). % local definition 'name = type' +%% @type t_name() = #t_name{app = [] | atom(), +%% module = [] | atom(), +%% name = [] | atom()} + +-record(t_name, {app = [], % app = [] if module = [] + module=[], % unqualified if module = [] + name=[]}). + +%% The following records all have 'a=[]' as their first field. +%% This is used for name and usage annotations; in particular, the +%% fun-argument types of a function specification (t_spec) are often +%% annotated with the names of the corresponding formal parameters, +%% and/or usage summaries. + +-define(t_ann(X), element(2, X)). +-define(set_t_ann(X, Y), setelement(2, X, Y)). +-define(add_t_ann(X, Y), ?set_t_ann(X, [Y | ?t_ann(X)])). + +%% @type t_var() = #t_var{a = list(), name = [] | atom()} + +-record(t_var, {a=[], name=[]}). % type variable + +%% @type t_type() = #t_type{a = list(), +%% name = t_name(), +%% args = [type()]} + +-record(t_type, {a=[], name, args = []}). % abstract type 'name(...)' + +%% @type t_union() = #t_union{a = list(), +%% types = [type()]} + +-record(t_union, {a=[], types = []}). % union type 't1|...|tN' + +%% @type t_fun() = #t_fun{a = list(), +%% args = [type()], +%% range = type()} + +-record(t_fun, {a=[], args, range}). % function '(t1,...,tN) -> range' + +%% @type t_tuple() = #t_tuple{a = list(), +%% types = [type()]} + +-record(t_tuple, {a=[], types = []}). % tuple type '{t1,...,tN}' + +%% @type t_list() = #t_list{a = list(), +%% type = type()} + +-record(t_list, {a=[], type}). % list type '[type]' + +%% @type t_nil() = #t_nil{a = list()} + +-record(t_nil, {a=[]}). % empty-list constant '[]' + +%% @type t_atom() = #t_atom{a = list(), +%% val = atom()} + +-record(t_atom, {a=[], val}). % atom constant + +%% @type t_integer() = #t_integer{a = list(), +%% val = integer()} + +-record(t_integer, {a=[], val}). % integer constant + +%% @type t_float() = #t_float{a = list(), +%% val = float()} + +-record(t_float, {a=[], val}). % floating-point constant + +%% @type t_record() = #t_list{a = list(), +%% name = type(), +%% fields = [field()]} + +-record(t_record, {a=[], name, fields = []}). % record type '#r{f1,...,fN}' + +%% @type t_field() = #t_field{a = list(), +%% name = type(), +%% type = type()} + +-record(t_field, {a=[], name, type}). % named field 'n1=t1' diff --git a/lib/edoc/src/edoc_wiki.erl b/lib/edoc/src/edoc_wiki.erl new file mode 100644 index 0000000000..e4a3d74734 --- /dev/null +++ b/lib/edoc/src/edoc_wiki.erl @@ -0,0 +1,456 @@ +%% ===================================================================== +%% 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 EDoc wiki expansion, parsing and postprocessing of XML text. +%% Uses {@link //xmerl. XMerL}. +%% @end + +%% Notes: +%% +%% * Whatever happens in this module, it must interact nicely with the +%% actual XML-parsing. It is not acceptable to break any existing and +%% legal XML markup so that it does not parse or is rendered wrong. +%% +%% * The focus should always be on making *documentation* easier to +%% write. No wiki notation should be introduced unless it is clear that +%% it is better than using plain XHTML, making typing less cumbersome +%% and the resulting text easier to read. The wiki notation should be a +%% small bag of easy-to-remember tricks for making XHTML documentation +%% easier to write, not a complete markup language in itself. As a +%% typical example, it is hardly worthwile to introduce a special +%% notation like say, ""..."" for emphasized text, since <em>...</em> is +%% not much harder to write, not any less readable, and no more +%% difficult to remember, especially since emphasis is not very often +%% occurring in normal documentation. +%% +%% * The central reasoning for the code-quoting goes like this: I don't +%% want to have special escape characters within the quotes (like +%% backslash in C), to allow quoting of the quote characters themselves. +%% I also don't want to use the "`" character both for opening and +%% closing quotes. Therefore, you can either use `...' - and then you +%% cannot use the "'" character without ending the quote - or you can +%% use ``...'' - which allows single but not double "'" characters +%% within the quote. Whitespace is automatically removed from the +%% beginning and the end of the quoted strings; this allows you to write +%% things like "`` 'foo@bar' ''". Text that contains "''" has to be +%% written within <code>...</code>. +%% +%% To produce a single "`" character without starting a quote, write +%% "`'" (no space between "`" and "'"). +%% +%% For verbatim/preformatted text, the ```...'''-quotes expand to +%% "<pre><![CDATA[...]]></pre>". The indentation at the start of the +%% quoted string is preserved; whitespace is stripped only at the end. +%% Whole leading lines of whitespace are however skipped. + +-module(edoc_wiki). + +-export([parse_xml/2, expand_text/2]). + +-include("edoc.hrl"). +-include("xmerl.hrl"). + +-define(BASE_HEADING, 3). + + +%% Parsing Wiki-XML with pre-and post-expansion. + +parse_xml(Data, Line) -> + par(parse_xml_1(expand_text(Data, Line), Line)). + +parse_xml_1(Text, Line) -> + Text1 = "<doc>" ++ Text ++ "</doc>", + case catch {ok, xmerl_scan:string(Text1, [{line, Line}])} of + {ok, {E, _}} -> + E#xmlElement.content; + {'EXIT', {fatal, {Reason, L, _C}}} -> + throw_error(L, {"XML parse error: ~p.", [Reason]}); + {'EXIT', Reason} -> + throw_error(Line, {"error in XML parser: ~P.", [Reason, 10]}); + Other -> + throw_error(Line, {"nocatch in XML parser: ~P.", [Other, 10]}) + end. + +%% Expand wiki stuff in arbitrary text. + +expand_text(Cs, L) -> + lists:reverse(expand_new_line(Cs, L, [])). + +%% Interestingly, the reverse of "code" is "edoc". :-) + +expand_new_line([$\s = C | Cs], L, As) -> + expand_new_line(Cs, L, [C | As]); +expand_new_line([$\t = C | Cs], L, As) -> + expand_new_line(Cs, L, [C | As]); +expand_new_line([$\n = C | Cs], L, As) -> + expand_new_line(Cs, L + 1, [C | As]); +expand_new_line([$=, $=, $=, $= | Cs], L, As) -> + expand_heading(Cs, 2, L, As); +expand_new_line([$=, $=, $= | Cs], L, As) -> + expand_heading(Cs, 1, L, As); +expand_new_line([$=, $= | Cs], L, As) -> + expand_heading(Cs, 0, L, As); +expand_new_line(Cs, L, As) -> + expand(Cs, L, As). + +expand([$`, $' | Cs], L, As) -> + expand(Cs, L, [$` | As]); % produce "`" - don't start a new quote +expand([$`, $`, $` | Cs], L, As) -> + %% If this is the first thing on the line, compensate for the + %% indentation, unless we had to skip one or more empty lines. + {Cs1, Skipped} = strip_empty_lines(Cs), % avoid vertical space + N = if Skipped > 0 -> + 0; + true -> + {As1, _} = edoc_lib:split_at(As, $\n), + case edoc_lib:is_space(As1) of + true -> 3 + length(As1); + false -> 2 % nice default - usually right. + end + end, + Ss = lists:duplicate(N, $\s), + expand_triple(Cs1, L + Skipped, Ss ++ "[ATADC[!<>erp<" ++ As); +expand([$`, $` | Cs], L, As) -> + expand_double(edoc_lib:strip_space(Cs), L, ">edoc<" ++ As); +expand([$` | Cs], L, As) -> + expand_single(edoc_lib:strip_space(Cs), L, ">edoc<" ++ As); +expand([$[ | Cs], L, As) -> + expand_uri(Cs, L, As); +expand([$\n = C | Cs], L, As) -> + expand_new_line(Cs, L + 1, [C | As]); +expand([C | Cs], L, As) -> + expand(Cs, L, [C | As]); +expand([], _, As) -> + As. + +%% == Heading == +%% === SubHeading === +%% ==== SubSubHeading ==== + +expand_heading([$= | _] = Cs, N, L, As) -> + expand_heading_1(Cs, N, L, As); +expand_heading(Cs, N, L, As) -> + {Cs1, Cs2} = edoc_lib:split_at(Cs, $\n), + case edoc_lib:strip_space(lists:reverse(Cs1)) of + [$=, $= | Cs3] -> + {Es, Ts} = lists:splitwith(fun (X) -> X =:= $= end, Cs3), + if length(Es) =:= N -> + Ts1 = edoc_lib:strip_space( + lists:reverse(edoc_lib:strip_space(Ts))), + expand_heading_2(Ts1, Cs2, N, L, As); + true -> + H1 = lists:duplicate(N+2, $=), + H2 = "==" ++ Es, + throw_error(L, {"heading end marker mismatch: " + "~s...~s", [H1, H2]}) + end; + _ -> + expand_heading_1(Cs, N, L, As) + end. + +expand_heading_1(Cs, N, L, As) -> + expand(Cs, L, lists:duplicate(N + 2, $=) ++ As). + +expand_heading_2(Ts, Cs, N, L, As) -> + H = ?BASE_HEADING + N, + Ts1 = io_lib:format("<h~w><a name=\"~s\">~s</a></h~w>\n", + [H, make_label(Ts), Ts, H]), + expand_new_line(Cs, L + 1, lists:reverse(lists:flatten(Ts1), As)). + +make_label([$\s | Cs]) -> + [$_ | make_label(edoc_lib:strip_space(Cs))]; +make_label([$\t | Cs]) -> + [$_ | make_label(edoc_lib:strip_space(Cs))]; +make_label([$\n | Cs]) -> + [$_ | make_label(edoc_lib:strip_space(Cs))]; +make_label([C | Cs]) -> + [C | make_label(Cs)]; +make_label([]) -> + []. + +%% `...' + +expand_single(Cs, L, As) -> + expand_single(Cs, L, As, L). + +expand_single([$' | Cs], L, As, _L0) -> + expand(Cs, L, ">edoc/<" ++ edoc_lib:strip_space(As)); +expand_single([$< | Cs], L, As, L0) -> + expand_single(Cs, L, ";tl&" ++ As, L0); +expand_single([$> | Cs], L, As, L0) -> + expand_single(Cs, L, ";tg&" ++ As, L0); +expand_single([$& | Cs], L, As, L0) -> + expand_single(Cs, L, ";pma&" ++ As, L0); +expand_single([$\n = C | Cs], L, As, L0) -> + expand_single(Cs, L + 1, [C | As], L0); +expand_single([C | Cs], L, As, L0) -> + expand_single(Cs, L, [C | As], L0); +expand_single([], L, _, L0) -> + throw_error(L0, {"`-quote ended unexpectedly at line ~w", [L]}). + +%% ``...'' + +expand_double(Cs, L, As) -> + expand_double(Cs, L, As, L). + +expand_double([$', $' | Cs], L, As, _L0) -> + expand(Cs, L, ">edoc/<" ++ edoc_lib:strip_space(As)); +expand_double([$< | Cs], L, As, L0) -> + expand_double(Cs, L, ";tl&" ++ As, L0); +expand_double([$> | Cs], L, As, L0) -> + expand_double(Cs, L, ";tg&" ++ As, L0); +expand_double([$& | Cs], L, As, L0) -> + expand_double(Cs, L, ";pma&" ++ As, L0); +expand_double([$\n = C | Cs], L, As, L0) -> + expand_double(Cs, L + 1, [C | As], L0); +expand_double([C | Cs], L, As, L0) -> + expand_double(Cs, L, [C | As], L0); +expand_double([], L, _, L0) -> + throw_error(L0, {"``-quote ended unexpectedly at line ~w", [L]}). + +%% ```...''' + +expand_triple(Cs, L, As) -> + expand_triple(Cs, L, As, L). + +expand_triple([$', $', $' | Cs], L, As, _L0) -> % ' stupid emacs + expand(Cs, L, ">erp/<>]]" ++ edoc_lib:strip_space(As)); +expand_triple([$], $], $> | Cs], L, As, L0) -> + expand_triple(Cs, L, ";tg&]]" ++ As, L0); +expand_triple([$\n = C | Cs], L, As, L0) -> + expand_triple(Cs, L + 1, [C | As], L0); +expand_triple([C | Cs], L, As, L0) -> + expand_triple(Cs, L, [C | As], L0); +expand_triple([], L, _, L0) -> + throw_error(L0, {"```-quote ended unexpectedly at line ~w", [L]}). + +%% e.g. [file:/...] or [http://... LinkText] + +expand_uri("http:/" ++ Cs, L, As) -> + expand_uri(Cs, L, "/:ptth", As); +expand_uri("ftp:/" ++ Cs, L, As) -> + expand_uri(Cs, L, "/:ptf", As); +expand_uri("file:/" ++ Cs, L, As) -> + expand_uri(Cs, L, "/:elif", As); +expand_uri(Cs, L, As) -> + expand(Cs, L, [$[ | As]). + +expand_uri([$] | Cs], L, Us, As) -> + expand(Cs, L, push_uri(Us, ">tt/<" ++ Us ++ ">tt<", As)); +expand_uri([$\s = C | Cs], L, Us, As) -> + expand_uri(Cs, 0, L, [C], Us, As); +expand_uri([$\t = C | Cs], L, Us, As) -> + expand_uri(Cs, 0, L, [C], Us, As); +expand_uri([$\n = C | Cs], L, Us, As) -> + expand_uri(Cs, 1, L, [C], Us, As); +expand_uri([C | Cs], L, Us, As) -> + expand_uri(Cs, L, [C | Us], As); +expand_uri([], L, Us, _As) -> + expand_uri_error(Us, L). + +expand_uri([$] | Cs], N, L, Ss, Us, As) -> + Ss1 = lists:reverse(edoc_lib:strip_space( + lists:reverse(edoc_lib:strip_space(Ss)))), + expand(Cs, L + N, push_uri(Us, Ss1, As)); +expand_uri([$\n = C | Cs], N, L, Ss, Us, As) -> + expand_uri(Cs, N + 1, L, [C | Ss], Us, As); +expand_uri([C | Cs], N, L, Ss, Us, As) -> + expand_uri(Cs, N, L, [C | Ss], Us, As); +expand_uri([], _, L, _Ss, Us, _As) -> + expand_uri_error(Us, L). + +-spec expand_uri_error(list(), pos_integer()) -> no_return(). + +expand_uri_error(Us, L) -> + {Ps, _} = edoc_lib:split_at(lists:reverse(Us), $:), + throw_error(L, {"reference '[~s:...' ended unexpectedly", [Ps]}). + + +push_uri(Us, Ss, As) -> + ">a/<" ++ Ss ++ ">\"pot_\"=tegrat \"" ++ Us ++ "\"=ferh a<" ++ As. + + +strip_empty_lines(Cs) -> + strip_empty_lines(Cs, 0). + +strip_empty_lines(Cs, N) -> + {Cs1, Cs2} = edoc_lib:split_at(Cs, $\n), + case edoc_lib:is_space(Cs1) of + true -> + strip_empty_lines(Cs2, N + 1); + false -> + {Cs, N} + end. + + +%% Scanning element content for paragraph breaks (empty lines). +%% Paragraphs are flushed by block level elements. + +par(Es) -> + par(Es, [], []). + +par([E=#xmlText{value = Value} | Es], As, Bs) -> + par_text(Value, As, Bs, E, Es); +par([E=#xmlElement{name = Name} | Es], As, Bs) -> + %% (Note that paragraphs may not contain any further block-level + %% elements, including other paragraphs. Tables get complicated.) + case Name of + 'p' -> par_flush(Es, [E | As], Bs); + 'hr' -> par_flush(Es, [E | As], Bs); + 'h1' -> par_flush(Es, [E | As], Bs); + 'h2' -> par_flush(Es, [E | As], Bs); + 'h3' -> par_flush(Es, [E | As], Bs); + 'h4' -> par_flush(Es, [E | As], Bs); + 'h5' -> par_flush(Es, [E | As], Bs); + 'h6' -> par_flush(Es, [E | As], Bs); + 'pre' -> par_flush(Es, [E | As], Bs); + 'address' -> par_flush(Es, [E | As], Bs); + 'div' -> par_flush(Es, [par_elem(E) | As], Bs); + 'blockquote' -> par_flush(Es, [par_elem(E) | As], Bs); + 'form' -> par_flush(Es, [par_elem(E) | As], Bs); + 'fieldset' -> par_flush(Es, [par_elem(E) | As], Bs); + 'noscript' -> par_flush(Es, [par_elem(E) | As], Bs); + 'ul' -> par_flush(Es, [par_subelem(E) | As], Bs); + 'ol' -> par_flush(Es, [par_subelem(E) | As], Bs); + 'dl' -> par_flush(Es, [par_subelem(E) | As], Bs); + 'table' -> par_flush(Es, [par_subelem(E) | As], Bs); + _ -> par(Es, [E | As], Bs) + end; +par([E | Es], As, Bs) -> + par(Es, [E | As], Bs); +par([], As, Bs) -> + lists:reverse(As ++ Bs). + +par_text(Cs, As, Bs, E, Es) -> + case ptxt(Cs) of + none -> + %% no blank lines: keep this element as it is + par(Es, [E | As], Bs); + {Cs1, Ss, Cs2} -> + Es1 = case Cs1 of + [] -> lists:reverse(As); + _ -> lists:reverse(As, [E#xmlText{value = Cs1}]) + end, + Bs0 = case Es1 of + [] -> Bs; + _ -> [#xmlElement{name = p, content = Es1} | Bs] + end, + Bs1 = case Ss of + [] -> Bs0; + _ -> [#xmlText{value = Ss} | Bs0] + end, + case Cs2 of + [] -> + par(Es, [], Bs1); + _ -> + par_text(Cs2, [], Bs1, #xmlText{value = Cs2}, Es) + end + end. + +par_flush(Es, As, Bs) -> + par(Es, [], As ++ Bs). + +par_elem(E) -> + E#xmlElement{content = par(E#xmlElement.content)}. + +%% Only process content of subelements; ignore immediate content. +par_subelem(E) -> + E#xmlElement{content = par_subelem_1(E#xmlElement.content)}. + +par_subelem_1([E=#xmlElement{name = Name} | Es]) -> + E1 = case par_skip(Name) of + true -> + E; + false -> + case par_sub(Name) of + true -> + par_subelem(E); + false -> + par_elem(E) + end + end, + [E1 | par_subelem_1(Es)]; +par_subelem_1([E | Es]) -> + [E | par_subelem_1(Es)]; +par_subelem_1([]) -> + []. + +par_skip('caption') -> true; +par_skip('col') -> true; +par_skip('colgroup') -> true; +par_skip(_) -> false. + +par_sub(tr) -> true; +par_sub(thead) -> true; +par_sub(tfoot) -> true; +par_sub(tbody) -> true; +par_sub(_) -> false. + + +%% scanning text content for a blank line + +ptxt(Cs) -> + ptxt(Cs, []). + +ptxt([$\n | Cs], As) -> + ptxt_1(Cs, As, [$\n]); +ptxt([C | Cs], As) -> + ptxt(Cs, [C | As]); +ptxt([], _As) -> + none. + +%% scanning text following an initial newline +ptxt_1([C=$\s | Cs], As, Ss) -> + ptxt_1(Cs, As, [C | Ss]); +ptxt_1([C=$\t | Cs], As, Ss) -> + ptxt_1(Cs, As, [C | Ss]); +ptxt_1([C=$\n | Cs], As, Ss) -> + %% blank line detected + ptxt_2(Cs, As, [C | Ss]); +ptxt_1(Cs, As, Ss) -> + %% not a blank line + ptxt(Cs, lists:reverse(Ss, As)). + +%% collecting whitespace following a blank line +ptxt_2([C=$\s | Cs], As, Ss) -> + ptxt_2(Cs, As, [C | Ss]); +ptxt_2([C=$\t | Cs], As, Ss) -> + ptxt_2(Cs, As, [C | Ss]); +ptxt_2([C=$\n | Cs], As, Ss) -> + ptxt_2(Cs, As, [C | Ss]); +ptxt_2(Cs, As, Ss) -> + %% ended by non-whitespace or end of element + case edoc_lib:is_space(As) of + true -> + {[], lists:reverse(Ss ++ As), Cs}; + false -> + {lists:reverse(As), lists:reverse(Ss), Cs} + end. + + +-spec throw_error(non_neg_integer(), {string(), [_]}) -> no_return(). + +throw_error(L, D) -> + throw({error, L, D}). diff --git a/lib/edoc/src/otpsgml_layout.erl b/lib/edoc/src/otpsgml_layout.erl new file mode 100644 index 0000000000..45f74b299e --- /dev/null +++ b/lib/edoc/src/otpsgml_layout.erl @@ -0,0 +1,853 @@ +%% ===================================================================== +%% 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$ +%% +%% @author Richard Carlsson <[email protected]> +%% @author Kenneth Lundin <[email protected]> +%% @copyright 2001-2004 Richard Carlsson +%% @see edoc_layout +%% @end +%% ===================================================================== + +%% @doc The OTP SGML layout module for EDoc. See the module {@link edoc} +%% for details on usage. + +%% Note that this is written so that it is *not* depending on edoc.hrl! + +-module(otpsgml_layout). + +-export([module/2, package/2, overview/2,type/1]). + +-import(edoc_report, [report/2]). + +-include("xmerl.hrl"). + +-define(SGML_EXPORT, xmerl_otpsgml). +-define(DEFAULT_XML_EXPORT, ?SGML_EXPORT). +-define(STYLESHEET, "stylesheet.css"). +-define(NL, "\n"). +-define(DESCRIPTION_TITLE, "Description"). +-define(DESCRIPTION_LABEL, "description"). +-define(DATA_TYPES_TITLE, "Data Types"). +-define(DATA_TYPES_LABEL, "types"). +-define(FUNCTION_INDEX_TITLE, "Function Index"). +-define(FUNCTION_INDEX_LABEL, "index"). +-define(FUNCTIONS_TITLE, "Function Details"). +-define(FUNCTIONS_LABEL, "functions"). + + +%% @doc The layout function. +%% +%% Options: +%% <dl> +%% <dt>{@type {index_columns, integer()@}} +%% </dt> +%% <dd>Specifies the number of column pairs used for the function +%% index tables. The default value is 1. +%% </dd> +%% <dt>{@type {stylesheet, string()@}} +%% </dt> +%% <dd>Specifies the URI used for referencing the stylesheet. The +%% default value is `"stylesheet.css"'. If an empty string is +%% specified, no stylesheet reference will be generated. +%% </dd> +%% <dt>{@type {xml_export, Module::atom()@}} +%% </dt> +%% <dd>Specifies an {@link //xmerl. `xmerl'} callback module to be +%% used for exporting the documentation. See {@link +%% //xmerl/xmerl:export_simple/3} for details. +%% </dd> +%% </dl> +%% +%% @see edoc:layout/2 + +-record(opts, {root, stylesheet, index_columns}). + +module(Element, Options) -> + XML = layout_module(Element, init_opts(Element, Options)), + Export = proplists:get_value(xml_export, Options, + ?DEFAULT_XML_EXPORT), + xmerl:export_simple([XML], Export, []). + +% Put layout options in a data structure for easier access. + +init_opts(Element, Options) -> + R = #opts{root = get_attrval(root, Element), + index_columns = proplists:get_value(index_columns, + Options, 1) + }, + case proplists:get_value(stylesheet, Options) of + undefined -> + S = edoc_lib:join_uri(R#opts.root, ?STYLESHEET), + R#opts{stylesheet = S}; + "" -> + R; % don't use any stylesheet + S when is_list(S) -> + R#opts{stylesheet = S}; + _ -> + report("bad value for option `stylesheet'.", []), + exit(error) + end. + + +%% ===================================================================== +%% XML-BASED LAYOUT ENGINE +%% ===================================================================== + +%% We assume that we have expanded XML data. + +%% <!ELEMENT module (moduleName, moduleFullName, behaviour*, description?, +%% author*, version?, since?, copyright?, deprecated?, +%% see*, reference*, typedecls?, functions)> +%% <!ATTLIST module +%% root CDATA #IMPLIED> +%% <!ELEMENT moduleName (#PCDATA)> +%% <!ELEMENT moduleFullName (#PCDATA)> +%% <!ELEMENT behaviour (#PCDATA)> +%% <!ATTLIST behaviour +%% href CDATA #IMPLIED> +%% <!ELEMENT description (briefDescription, fullDescription?)> +%% <!ELEMENT briefDescription (#PCDATA)> +%% <!ELEMENT fullDescription (#PCDATA)> +%% <!ELEMENT author EMPTY> +%% <!ATTLIST author +%% name CDATA #REQUIRED +%% email CDATA #IMPLIED +%% website CDATA #IMPLIED> +%% <!ELEMENT version (#PCDATA)> +%% <!ELEMENT since (#PCDATA)> +%% <!ELEMENT copyright (#PCDATA)> +%% <!ELEMENT deprecated (description)> +%% <!ELEMENT see (#PCDATA)> +%% <!ATTLIST see +%% name CDATA #REQUIRED +%% href CDATA #IMPLIED> +%% <!ELEMENT reference (#PCDATA)> +%% <!ELEMENT typedecls (typedecl+)> +%% <!ELEMENT functions (function+)> + +layout_module(#xmlElement{name = module, content = Es}=E, _Opts) -> + Name = get_attrval(name, E), + Desc = get_content(description, Es), + ShortDesc = get_content(briefDescription, Desc), + FullDesc = get_content(fullDescription, Desc), + Functions = [E || E <- get_content(functions, Es)], + SortedFs = lists:sort([{function_name(E), E} || E <- Functions]), + Types = get_content(typedecls, Es), + SortedTs = lists:sort([{type_name(E), E} || E <- Types]), + Header = {header, [ + ?NL,{title, [Name]}, + ?NL,{prepared, [""]}, + ?NL,{responsible, [""]}, + ?NL,{docno, ["1"]}, + ?NL,{approved, [""]}, + ?NL,{checked, [""]}, + ?NL,{date, [""]}, + ?NL,{rev, ["A"]}, + ?NL,{file, [Name++".sgml"]} + ]}, + Module = {module, [Name]}, + ModuleSummary = {modulesummary, ShortDesc}, + {Short,Long} = find_first_p(FullDesc,[]), + Description = {description, [?NL,{p,Short}|Long]++[?NL|types(SortedTs)]}, + Funcs = functions(SortedFs), + Authors = {authors, authors(Es)}, + See = sees1(Es), + {erlref, [ + ?NL,Header, + ?NL,Module, + ?NL,ModuleSummary, + ?NL,Description, + ?NL,Funcs, + ?NL,See, + ?NL,Authors + ] + }. + +stylesheet(Opts) -> + case Opts#opts.stylesheet of + undefined -> + []; + CSS -> + [{link, [{rel, "stylesheet"}, + {type, "text/css"}, + {href, CSS}], []}, + ?NL] + end. + +% doc_index(FullDesc, Functions, Types) -> +% case doc_index_rows(FullDesc, Functions, Types) of +% [] -> []; +% Rs -> +% [{ul, [{li, [{a, [{href, local_label(R)}], [T]}]} +% || {T, R} <- Rs]}] +% end. + +% doc_index_rows(FullDesc, Functions, Types) -> +% (if FullDesc == [] -> []; +% true -> [{?DESCRIPTION_TITLE, ?DESCRIPTION_LABEL}] +% end +% ++ if Types == [] -> []; +% true -> [{?DATA_TYPES_TITLE, ?DATA_TYPES_LABEL}] +% end +% ++ if Functions == [] -> []; +% true -> [{?FUNCTION_INDEX_TITLE, ?FUNCTION_INDEX_LABEL}, +% {?FUNCTIONS_TITLE, ?FUNCTIONS_LABEL}] +% end). + +% function_index(Fs, Cols) -> +% case function_index_rows(Fs, Cols, []) of +% [] -> []; +% Rows -> +% [?NL, +% {h2, [{a, [{name, ?FUNCTION_INDEX_LABEL}], +% [?FUNCTION_INDEX_TITLE]}]}, +% ?NL, +% {table, [{width, "100%"}, {border, 1}], Rows}, +% ?NL] +% end. + +% function_index_rows(Fs, Cols, Title) -> +% Rows = (length(Fs) + (Cols - 1)) div Cols, +% (if Title == [] -> []; +% true -> [{tr, [{th, [{colspan, Cols * 2}, {align, left}], +% [Title]}]}, +% ?NL] +% end +% ++ lists:flatmap(fun index_row/1, +% edoc_lib:transpose(edoc_lib:segment(Fs, Rows)))). + +% index_row(Fs) -> +% [{tr, lists:flatmap(fun index_col/1, Fs)}, ?NL]. + +% index_col({Name, F=#xmlElement{content = Es}}) -> +% [{td, [{valign, "top"}], label_href([Name], F)}, +% {td, index_desc(Es)}]. + +index_desc(Es) -> + Desc = get_content(description, Es), + case get_content(briefDescription, Desc) of + [] -> + equiv(Es); % no description at all if no equiv + ShortDesc -> + ShortDesc + end. + +% label_href(Content, F) -> +% case get_attrval(label, F) of +% "" -> Content; +% Ref -> [{a, [{href, local_label(Ref)}], Content}] +% end. + + +%% <!ELEMENT function (args, typespec?, equiv?, description?, since?, +%% deprecated?, see*)> +%% <!ATTLIST function +%% name CDATA #REQUIRED +%% arity CDATA #REQUIRED +%% exported NMTOKEN(yes | no) #REQUIRED +%% label CDATA #IMPLIED> +%% <!ELEMENT args (arg*)> +%% <!ELEMENT arg description?> +%% <!ATTLIST arg name CDATA #REQUIRED> + + +%% <!ELEMENT equiv (expr, see?)> +%% <!ELEMENT expr (#PCDATA)> + +% functions(Fs) -> +% Es = lists:flatmap(fun ({Name, E}) -> function(Name, E) end, Fs), +% if Es == [] -> []; +% true -> +% [?NL, +% {h2, [{a, [{name, ?FUNCTIONS_LABEL}], [?FUNCTIONS_TITLE]}]}, +% ?NL | Es] +% end. + +functions(Fs) -> + Es = lists:flatmap(fun ({Name, E}) -> function(Name, E) end, Fs), + if Es == [] -> []; + true -> + {funcs, Es} + end. + +% is_exported(E) -> +% case get_attrval(exported, E) of +% "yes" -> true; +% _ -> false +% end. + +% function(Name, E=#xmlElement{content = Es}) -> +% ([?NL, {h3, label_anchor([Name], E)}, ?NL] +% ++ case typespec(get_content(typespec, Es)) of +% [] -> +% signature(get_content(arguments, Es), +% get_text(functionName, Es)); +% Spec -> Spec +% end +% ++ equiv(Es) +% ++ deprecated(Es, "function") +% ++ fulldesc(Es) +% ++ since(Es) +% ++ sees(Es)). + +function(_Name, E=#xmlElement{content = Es}) -> + TypeSpec = get_content(typespec, Es), + [?NL,{func, [ ?NL, + {name, +% case typespec(get_content(typespec, Es)) of + case funcheader(TypeSpec) of + [] -> + signature(get_content(args, Es), + get_attrval(name, E)); + Spec -> Spec + end + }, + ?NL,{fsummary, fsummary(Es)}, +% ?NL,{type, local_types(TypeSpec)}, + ?NL,local_types(TypeSpec), + ?NL,{desc, label_anchor(E)++fulldesc(Es)++sees(Es)} + ]}]. + +fsummary([]) -> ["\s"]; +fsummary(Es) -> + Desc = get_content(description, Es), + case get_content(briefDescription, Desc) of + [] -> + fsummary_equiv(Es); % no description at all if no equiv + ShortDesc -> + ShortDesc + end. + + +fsummary_equiv(Es) -> + case get_content(equiv, Es) of + [] -> ["\s"]; + Es1 -> + case get_content(expr, Es1) of + [] -> ["\s"]; + [Expr] -> + ["Equivalent to ", Expr, ".",?NL] + end + end. + + +function_name(E) -> + get_attrval(name, E) ++ "/" ++ get_attrval(arity, E). + +label_anchor(E) -> + case get_attrval(label, E) of + "" -> []; + Ref -> [{marker, [{id, Ref}],[]},?NL] + end. + +label_anchor(Content, E) -> + case get_attrval(label, E) of + "" -> Content; + Ref -> {p,[{marker, [{id, Ref}],[]}, + {em, Content}]} + end. + +%% <!ELEMENT args (arg*)> +%% <!ELEMENT arg (argName, description?)> +%% <!ELEMENT argName (#PCDATA)> + +%% This is currently only done for functions without type spec. + +signature(Es, Name) -> +% [{tt, [Name, "("] ++ seq(fun arg/1, Es) ++ [") -> term()", ?NL]}]. + [Name, "("] ++ seq(fun arg/1, Es) ++ [") -> term()", ?NL]. + +arg(#xmlElement{content = Es}) -> + [get_text(argName, Es)]. + +%% <!ELEMENT typespec (erlangName, type, localdef*)> + +% typespec([]) -> []; +% typespec(Es) -> +% [{p, ([{tt, ([t_name(get_elem(qualifiedName, Es))] +% ++ t_type(get_content(type, Es)))}] +% ++ local_defs(get_elem(definition, Es)))}, +% ?NL]. + +funcheader([]) -> []; +funcheader(Es) -> + [t_name(get_elem(erlangName, Es))] ++ t_utype(get_elem(type, Es)). + +local_types([]) -> []; +local_types(Es) -> + local_defs2(get_elem(localdef, Es)). + +local_defs2([]) -> []; +local_defs2(Es) -> + {type,[?NL | [{v, localdef(E)} || E <- Es]]}. + +%% <!ELEMENT typedecl (typedef, description?)> +%% <!ELEMENT typedef (erlangName, argtypes, type?, localdef*)> + +types([]) -> []; +types(Ts) -> + Es = lists:flatmap(fun ({Name, E}) -> typedecl(Name, E) end, Ts), + [?NL, +% {h2, [{a, [{name, ?DATA_TYPES_LABEL}], +% [?DATA_TYPES_TITLE]}]}, +% ?NL | Es] + {p,[{marker, [{id, ?DATA_TYPES_LABEL}],[]}, + {em,[?DATA_TYPES_TITLE]}]}, + ?NL, {taglist,[?NL|Es]}]. + +%%type(Name, E=#xmlElement{content = Es}) -> +%% ([?NL, {h3, label_anchor([Name, "()"], E)}, ?NL] +%% ++ [{p, typedef(get_content(typedef, Es))}, ?NL] +%% ++ fulldesc(Es)). +typedecl(_Name, #xmlElement{content = Es}) -> + [{tag, typedef(get_content(typedef, Es))},?NL,{item,fulldesc(Es)},?NL]. + + +type_name(#xmlElement{content = Es}) -> + t_name(get_elem(erlangName, get_content(typedef, Es))). + +typedef(Es) -> + Name = ([t_name(get_elem(erlangName, Es)), "("] + ++ seq(fun t_utype_elem/1, get_content(argtypes, Es), [")"])), + (case get_elem(type, Es) of + [] -> [{b, ["abstract datatype"]}, ": ", {tt, Name}]; + Type -> + [{tt, Name ++ [" = "] ++ t_utype(Type)}] + end + ++ local_defs(get_elem(localdef, Es))). + +local_defs([]) -> []; +local_defs(Es) -> + [?NL, {ul, [{li, [{tt, localdef(E)}]} || E <- Es]}]. + +localdef(E = #xmlElement{content = Es}) -> + (case get_elem(typevar, Es) of + [] -> + label_anchor(t_abstype(get_content(abstype, Es)), E); + [V] -> + t_var(V) + end + ++ [" = "] ++ t_utype(get_elem(type, Es))). + +fulldesc(Es) -> + case get_content(fullDescription, get_content(description, Es)) of +% [] -> [?NL]; + [] -> index_desc(Es); +% Desc -> [{p, Desc}, ?NL] + Desc -> + {Short,Long} = find_first_p(Desc,[]), + [?NL,{p,Short}|Long] ++[?NL] + end. + +find_first_p([#xmlElement{name=p}|_]=Long,Short) -> + {lists:reverse(Short),Long}; +find_first_p([H|T],Short) -> + find_first_p(T,[H|Short]); +find_first_p([],Short) -> + {lists:reverse(Short),[]}. + + +sees1(Es) -> + case get_elem(see, Es) of + [] -> []; + Es1 -> + {section,[{title,["See also"]},{p,seq(fun see/1, Es1, [])}]} + end. + +sees(Es) -> + case get_elem(see, Es) of + [] -> []; + Es1 -> + [{p, [{em, ["See also:"]}, " "] ++ seq(fun see/1, Es1, ["."])}, + ?NL] + end. + +see(E=#xmlElement{content = Es}) -> + see(E,Es). + +see(E, Es) -> + case get_attrval(href, E) of + "" -> Es; + Ref -> + case lists:reverse(Ref) of + "lmgs.ppa_"++Ppa -> + App = lists:reverse(Ppa), + [{seealso, [{marker, App++"_app"}], [App]},"(6)"]; + "lmgs."++Dom -> + Mod = lists:reverse(Dom), + [{seealso, [{marker, Mod}], [Mod]},"(3)"]; + _ -> + [{seealso, [{marker, Ref}], Es}] + end + end. + +equiv(Es) -> + case get_content(equiv, Es) of + [] -> ["\s"]; + Es1 -> + case get_content(expr, Es1) of + [] -> []; + [Expr] -> +% Expr1 = {tt, [Expr]}, +% Expr1 = {c, [Expr]}, + Expr1 = [Expr], + Expr2 = case get_elem(see, Es1) of + [] -> + {c,Expr1}; + [E=#xmlElement{}] -> +% see(E,Expr1) + case get_attrval(href, E) of + "" -> + {c,Expr1}; + Ref -> + {seealso, [{marker, Ref}], Expr1} + end + end, + [{p, ["Equivalent to ", Expr2, "."]}, ?NL] + end + end. + +% replace_minus_with_percent([$-|T]) -> +% [$%|T]; +% replace_minus_with_percent([H|T]) -> +% [H|replace_minus_with_percent(T)]. + +copyright(Es) -> + case get_content(copyright, Es) of + [] -> []; + Es1 -> + [{p, ["Copyright \251 " | Es1]}, ?NL] + end. + +version(Es) -> + case get_content(version, Es) of + [] -> []; + Es1 -> + [{p, [{b, ["Version:"]}, " " | Es1]}, ?NL] + end. + +since(Es) -> + case get_content(since, Es) of + [] -> []; + Es1 -> + [{p, [{b, ["Introduced in:"]}, " " | Es1]}, ?NL] + end. + +deprecated(Es, S) -> + Es1 = get_content(description, get_content(deprecated, Es)), + case get_content(fullDescription, Es1) of + [] -> []; + Es2 -> + [{p, [{b, ["This " ++ S ++ " is deprecated:"]}, " " | Es2]}, + ?NL] + end. + +% behaviours(Es) -> +% case get_elem(behaviour, Es) of +% [] -> []; +% Es1 -> +% [{p, [{b, ["Behaviour:"]}, " "] ++ seq(fun behaviour/1, Es1, ["."])}, +% ?NL] +% end. + +% behaviour(E=#xmlElement{content = Es}) -> +% case get_attrval(href, E) of +% "" -> [{tt, Es}]; +% Ref -> [{a, [{href, Ref}], [{tt, Es}]}] +% end. + +authors(Es) -> + case get_elem(author, Es) of + [] -> [?NL,{aname,["\s"]},?NL,{email,["\s"]}]; + Es1 -> [?NL|seq(fun author/1, Es1, [])] +% +% [{p, [{b, ["Authors:"]}, " "] ++ seq(fun author/1, Es1, ["."])}, +% ?NL] + end. + + +%% <!ATTLIST author +%% name CDATA #REQUIRED +%% email CDATA #IMPLIED +%% website CDATA #IMPLIED> + +author(E=#xmlElement{}) -> + Name = case get_attrval(name, E) of + [] -> "\s"; + N -> N + end, + Mail = case get_attrval(email, E) of + [] -> "\s"; + M -> M + end, + [?NL,{aname,[Name]},?NL,{email,[Mail]}]. + +% author(E=#xmlElement{}) -> +% Name = get_attrval(name, E), +% Mail = get_attrval(email, E), +% URI = get_attrval(website, E), +% (if Name == Mail -> +% [{a, [{href, "mailto:" ++ Mail}],[{tt, [Mail]}]}]; +% true -> +% if Mail == "" -> [Name]; +% true -> [Name, " (", {a, [{href, "mailto:" ++ Mail}], +% [{tt, [Mail]}]}, ")"] +% end +% end +% ++ if URI == "" -> []; +% true -> [" [", {em, ["web site:"]}, " ", +% {tt, [{a, [{href, URI}], [URI]}]}, "]"] +% end). + +references(Es) -> + case get_elem(reference, Es) of + [] -> []; + Es1 -> + [{p, [{b, ["References"]}, + {ul, [{li, C} || #xmlElement{content = C} <- Es1]}]}, + ?NL] + end. + +t_name([E]) -> + N = get_attrval(name, E), + case get_attrval(module, E) of + "" -> N; + M -> + S = M ++ ":" ++ N, + case get_attrval(app, E) of + "" -> S; + A -> "//" ++ A ++ "/" ++ S + end + end. + +t_utype([E]) -> + t_utype_elem(E). + +t_utype_elem(E=#xmlElement{content = Es}) -> + case get_attrval(name, E) of + "" -> t_type(Es); + Name -> + T = t_type(Es), + case T of + [Name] -> T; % avoid generating "Foo::Foo" + T -> [Name] ++ ["::"] ++ T + end + end. + +t_type([E=#xmlElement{name = typevar}]) -> + t_var(E); +t_type([E=#xmlElement{name = atom}]) -> + t_atom(E); +t_type([E=#xmlElement{name = integer}]) -> + t_integer(E); +t_type([E=#xmlElement{name = float}]) -> + t_float(E); +t_type([#xmlElement{name = nil}]) -> + t_nil(); +t_type([#xmlElement{name = list, content = Es}]) -> + t_list(Es); +t_type([#xmlElement{name = tuple, content = Es}]) -> + t_tuple(Es); +t_type([#xmlElement{name = 'fun', content = Es}]) -> + t_fun(Es); +t_type([E = #xmlElement{name = abstype, content = Es}]) -> + T = t_abstype(Es), +% see(E,T); + case get_attrval(href, E) of + "" -> T; + % Ref -> [{seealso, [{marker, Ref}], T}] + _Ref -> T + end; +t_type([#xmlElement{name = union, content = Es}]) -> + t_union(Es). + +t_var(E) -> + [get_attrval(name, E)]. + + +t_atom(E) -> + [get_attrval(value, E)]. + +t_integer(E) -> + [get_attrval(value, E)]. + +t_float(E) -> + [get_attrval(value, E)]. + +t_nil() -> + ["[]"]. + +t_list(Es) -> + ["["] ++ t_utype(get_elem(type, Es)) ++ ["]"]. + +t_tuple(Es) -> + ["{"] ++ seq(fun t_utype_elem/1, Es, ["}"]). + +t_fun(Es) -> + ["("] ++ seq(fun t_utype_elem/1, get_content(argtypes, Es), + [") -> "] ++ t_utype(get_elem(type, Es))). + +t_abstype(Es) -> +% ([t_name(get_elem(qualifiedName, Es)), "("] +% ++ seq(fun t_type_elem/1, get_elem(type, Es), [")"])). + case split_at_colon(t_name(get_elem(erlangName, Es)),[]) of + {Mod,Type} -> + [Type, "("] ++ + seq(fun t_utype_elem/1, get_elem(type, Es), [")"]) ++ + [" (see module ", Mod, ")"]; + Type -> + [Type, "("] ++ + seq(fun t_utype_elem/1, get_elem(type, Es), [")"]) + end. + +%% Split at one colon, but not at two (or more) +split_at_colon([$:,$:|_]=Rest,Acc) -> + lists:reverse(Acc)++Rest; +split_at_colon([$:|Type],Acc) -> + {lists:reverse(Acc),Type}; +split_at_colon([Char|Rest],Acc) -> + split_at_colon(Rest,[Char|Acc]); +split_at_colon([],Acc) -> + lists:reverse(Acc). + +% t_par(Es) -> +% T = t_type(get_content(type, Es)), +% case get_elem(variable, Es) of +% [] -> T; +% [V0] -> case t_variable(V0) of +% T -> T; +% V -> V ++ ["::"] ++ T +% end +% end. + +% t_par_elem(#xmlElement{content = Es}) -> t_par(Es). + +t_union(Es) -> + seq(fun t_utype_elem/1, Es, " | ", []). + +seq(F, Es) -> + seq(F, Es, []). + +seq(F, Es, Tail) -> + seq(F, Es, ", ", Tail). + +seq(F, [E], _Sep, Tail) -> + F(E) ++ Tail; +seq(F, [E | Es], Sep, Tail) -> + F(E) ++ [Sep] ++ seq(F, Es, Sep, Tail); +seq(_F, [], _Sep, Tail) -> + Tail. + +get_elem(Name, [#xmlElement{name = Name} = E | Es]) -> + [E | get_elem(Name, Es)]; +get_elem(Name, [_ | Es]) -> + get_elem(Name, Es); +get_elem(_, []) -> + []. + +get_attr(Name, [#xmlAttribute{name = Name} = A | As]) -> + [A | get_attr(Name, As)]; +get_attr(Name, [_ | As]) -> + get_attr(Name, As); +get_attr(_, []) -> + []. + +get_attrval(Name, #xmlElement{attributes = As}) -> + case get_attr(Name, As) of + [#xmlAttribute{value = V}] -> + V; + [] -> "" + end. + +get_content(Name, Es) -> + case get_elem(Name, Es) of + [#xmlElement{content = Es1}] -> + Es1; + [] -> [] + end. + +get_text(Name, Es) -> + case get_content(Name, Es) of + [#xmlText{value = Text}] -> + Text; + [] -> "" + end. + +% local_label(R) -> +% "#" ++ R. + +xml(Title, CSS, Body) -> + {html, [?NL, + {head, [?NL, + {title, [Title]}, + ?NL] ++ CSS}, + ?NL, + {body, [{bgcolor, "white"}], Body}, + ?NL] + }. + +%% --------------------------------------------------------------------- + + type(E) -> + type(E, []). + +% type(E, Ds) -> +% xmerl:export_simple_content(t_utype_elem(E) ++ local_defs(Ds), +% ?HTML_EXPORT). + type(E, Ds) -> + xmerl:export_simple_content(t_utype_elem(E) ++ local_defs(Ds), + ?SGML_EXPORT). + + +package(E=#xmlElement{name = package, content = Es}, Options) -> + Opts = init_opts(E, Options), + Name = get_text(packageName, Es), + Title = io_lib:fwrite("Package ~s", [Name]), + Desc = get_content(description, Es), +% ShortDesc = get_content(briefDescription, Desc), + FullDesc = get_content(fullDescription, Desc), + Body = ([?NL, {h1, [Title]}, ?NL] +% ++ ShortDesc + ++ copyright(Es) + ++ deprecated(Es, "package") + ++ version(Es) + ++ since(Es) + ++ authors(Es) + ++ references(Es) + ++ sees(Es) + ++ FullDesc), + XML = xml(Title, stylesheet(Opts), Body), + xmerl:export_simple([XML], ?SGML_EXPORT, []). + +overview(E=#xmlElement{name = overview, content = Es}, Options) -> + Opts = init_opts(E, Options), + Title = get_text(title, Es), + Desc = get_content(description, Es), +% ShortDesc = get_content(briefDescription, Desc), + FullDesc = get_content(fullDescription, Desc), + Body = ([?NL, {h1, [Title]}, ?NL] +% ++ ShortDesc + ++ copyright(Es) + ++ version(Es) + ++ since(Es) + ++ authors(Es) + ++ references(Es) + ++ sees(Es) + ++ FullDesc), + XML = xml(Title, stylesheet(Opts), Body), + xmerl:export_simple([XML], ?SGML_EXPORT, []). |