View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker and Anjo Anjewierden
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2002-2025, University of Amsterdam
    7                              VU University Amsterdam
    8                              SWI-Prolog Solutions b.v.
    9    All rights reserved.
   10
   11    Redistribution and use in source and binary forms, with or without
   12    modification, are permitted provided that the following conditions
   13    are met:
   14
   15    1. Redistributions of source code must retain the above copyright
   16       notice, this list of conditions and the following disclaimer.
   17
   18    2. Redistributions in binary form must reproduce the above copyright
   19       notice, this list of conditions and the following disclaimer in
   20       the documentation and/or other materials provided with the
   21       distribution.
   22
   23    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   24    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   25    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   26    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   27    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   28    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   29    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   30    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   31    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   32    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   33    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   34    POSSIBILITY OF SUCH DAMAGE.
   35*/
   36
   37:- module(html_write,
   38          [ reply_html_page/2,          % :Head, :Body
   39            reply_html_page/3,          % +Style, :Head, :Body
   40            reply_html_partial/1,       % +HTML
   41
   42                                        % Basic output routines
   43            page//1,                    % :Content
   44            page//2,                    % :Head, :Body
   45            page//3,                    % +Style, :Head, :Body
   46            html//1,                    % :Content
   47
   48                                        % Option processing
   49            html_set_options/1,         % +OptionList
   50            html_current_option/1,      % ?Option
   51
   52                                        % repositioning HTML elements
   53            html_post//2,               % +Id, :Content
   54            html_receive//1,            % +Id
   55            html_receive//2,            % +Id, :Handler
   56            xhtml_ns//2,                % +Id, +Value
   57            html_root_attribute//2,     % +Name, +Value
   58
   59            html/4,                     % {|html||quasi quotations|}
   60
   61                                        % Useful primitives for expanding
   62            html_begin//1,              % +EnvName[(Attribute...)]
   63            html_end//1,                % +EnvName
   64            html_quoted//1,             % +Text
   65            html_quoted_attribute//1,   % +Attribute
   66
   67                                        % Emitting the HTML code
   68            print_html/1,               % +List
   69            print_html/2,               % +Stream, +List
   70            html_print_length/2,        % +List, -Length
   71
   72                                        % Extension support
   73            (html_meta)/1,              % +Spec
   74            op(1150, fx, html_meta)
   75          ]).   76:- use_module(html_quasiquotations, [html/4]).   77:- use_module(library(debug),[debug/3]).   78:- use_module(html_decl, [(html_meta)/1, html_no_content/1, op(_,_,_)]).   79:- autoload(library(error),
   80	    [must_be/2,domain_error/2,instantiation_error/1]).   81:- autoload(library(lists),
   82	    [permutation/2,selectchk/3,append/3,select/4,list_to_set/2]).   83:- autoload(library(option),[option/2]).   84:- autoload(library(pairs),[group_pairs_by_key/2]).   85:- autoload(library(sgml),[xml_quote_cdata/3,xml_quote_attribute/3]).   86:- autoload(library(uri),[uri_encoded/3]).   87:- autoload(library(url),[www_form_encode/2]).   88
   89% Quote output
   90:- set_prolog_flag(generate_debug_info, false).   91
   92:- meta_predicate
   93    reply_html_page(+, :, :),
   94    reply_html_page(:, :),
   95    html(:, -, +),
   96    page(:, -, +),
   97    page(:, :, -, +),
   98    pagehead(+, :, -, +),
   99    pagebody(+, :, -, +),
  100    html_receive(+, 3, -, +),
  101    html_post(+, :, -, +).  102
  103:- multifile
  104    expand//1,                      % +HTMLElement
  105    expand_attribute_value//1,      % +HTMLAttributeValue
  106    html_header_hook/1.             % +Style
  107
  108
  109/** <module> Write HTML text
  110
  111Most   code   doesn't   need  to   use  this   directly;  instead   use
  112library(http/http_server),  which  combines   this  library  with   the
  113typical HTTP libraries that most servers need.
  114
  115The purpose of this library  is  to   simplify  writing  HTML  pages. Of
  116course, it is possible to  use  format/3   to  write  to the HTML stream
  117directly, but this is generally not very satisfactory:
  118
  119        * It is a lot of typing
  120        * It does not guarantee proper HTML syntax.  You have to deal
  121          with HTML quoting, proper nesting and reasonable layout.
  122        * It is hard to use satisfactory abstraction
  123
  124This module tries to remedy these problems.   The idea is to translate a
  125Prolog term into  an  HTML  document.  We   use  DCG  for  most  of  the
  126generation.
  127
  128---++ International documents
  129
  130The library supports the generation of international documents, but this
  131is currently limited to using UTF-8 encoded HTML or XHTML documents.  It
  132is strongly recommended to use the following mime-type.
  133
  134==
  135Content-type: text/html; charset=UTF-8
  136==
  137
  138When generating XHTML documents, the output stream must be in UTF-8
  139encoding.
  140*/
  141
  142
  143                 /*******************************
  144                 *            SETTINGS          *
  145                 *******************************/
  146
  147%!  html_set_options(+Options) is det.
  148%
  149%   Set options for the HTML output.   Options  are stored in prolog
  150%   flags to ensure proper multi-threaded behaviour where setting an
  151%   option is local to the thread  and   new  threads start with the
  152%   options from the parent thread. Defined options are:
  153%
  154%     * dialect(Dialect)
  155%       One of =html4=, =xhtml= or =html5= (default). For
  156%       compatibility reasons, =html= is accepted as an
  157%       alias for =html4=.
  158%
  159%     * doctype(+DocType)
  160%       Set the =|<|DOCTYPE|= DocType =|>|= line for page//1 and
  161%       page//2.
  162%
  163%     * content_type(+ContentType)
  164%       Set the =|Content-type|= for reply_html_page/3
  165%
  166%   Note that the doctype and  content_type   flags  are  covered by
  167%   distinct  prolog  flags:  =html4_doctype=,  =xhtml_doctype=  and
  168%   =html5_doctype= and similar for the   content  type. The Dialect
  169%   must be switched before doctype and content type.
  170
  171html_set_options(Options) :-
  172    must_be(list, Options),
  173    set_options(Options).
  174
  175set_options([]).
  176set_options([H|T]) :-
  177    html_set_option(H),
  178    set_options(T).
  179
  180html_set_option(dialect(Dialect0)) :-
  181    !,
  182    must_be(oneof([html,html4,xhtml,html5]), Dialect0),
  183    (   html_version_alias(Dialect0, Dialect)
  184    ->  true
  185    ;   Dialect = Dialect0
  186    ),
  187    set_prolog_flag(html_dialect, Dialect).
  188html_set_option(doctype(Atom)) :-
  189    !,
  190    must_be(atom, Atom),
  191    current_prolog_flag(html_dialect, Dialect),
  192    dialect_doctype_flag(Dialect, Flag),
  193    set_prolog_flag(Flag, Atom).
  194html_set_option(content_type(Atom)) :-
  195    !,
  196    must_be(atom, Atom),
  197    current_prolog_flag(html_dialect, Dialect),
  198    dialect_content_type_flag(Dialect, Flag),
  199    set_prolog_flag(Flag, Atom).
  200html_set_option(O) :-
  201    domain_error(html_option, O).
  202
  203html_version_alias(html, html4).
  204
  205%!  html_current_option(?Option) is nondet.
  206%
  207%   True if Option is an active option for the HTML generator.
  208
  209html_current_option(dialect(Dialect)) :-
  210    current_prolog_flag(html_dialect, Dialect).
  211html_current_option(doctype(DocType)) :-
  212    current_prolog_flag(html_dialect, Dialect),
  213    dialect_doctype_flag(Dialect, Flag),
  214    current_prolog_flag(Flag, DocType).
  215html_current_option(content_type(ContentType)) :-
  216    current_prolog_flag(html_dialect, Dialect),
  217    dialect_content_type_flag(Dialect, Flag),
  218    current_prolog_flag(Flag, ContentType).
  219
  220dialect_doctype_flag(html4, html4_doctype).
  221dialect_doctype_flag(html5, html5_doctype).
  222dialect_doctype_flag(xhtml, xhtml_doctype).
  223
  224dialect_content_type_flag(html4, html4_content_type).
  225dialect_content_type_flag(html5, html5_content_type).
  226dialect_content_type_flag(xhtml, xhtml_content_type).
  227
  228option_default(html_dialect, html5).
  229option_default(html4_doctype,
  230               'HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" \c
  231               "http://www.w3.org/TR/html4/loose.dtd"').
  232option_default(html5_doctype,
  233               'html').
  234option_default(xhtml_doctype,
  235               'html PUBLIC "-//W3C//DTD XHTML 1.0 \c
  236               Transitional//EN" \c
  237               "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"').
  238option_default(html4_content_type, 'text/html; charset=UTF-8').
  239option_default(html5_content_type, 'text/html; charset=UTF-8').
  240option_default(xhtml_content_type, 'application/xhtml+xml; charset=UTF-8').
  241
  242%!  init_options is det.
  243%
  244%   Initialise the HTML processing options.
  245
  246init_options :-
  247    (   option_default(Name, Value),
  248        (   current_prolog_flag(Name, _)
  249        ->  true
  250        ;   create_prolog_flag(Name, Value, [])
  251        ),
  252        fail
  253    ;   true
  254    ).
  255
  256:- init_options.  257
  258%!  xml_header(-Header)
  259%
  260%   First line of XHTML document.  Added by print_html/1.
  261
  262xml_header('<?xml version=\'1.0\' encoding=\'UTF-8\'?>').
  263
  264%!  ns(?Which, ?Atom)
  265%
  266%   Namespace declarations
  267
  268ns(xhtml, 'http://www.w3.org/1999/xhtml').
  269
  270
  271                 /*******************************
  272                 *             PAGE             *
  273                 *******************************/
  274
  275%!  page(+Content:dom)// is det.
  276%!  page(+Head:dom, +Body:dom)// is det.
  277%
  278%   Generate a page including the   HTML  =|<!DOCTYPE>|= header. The
  279%   actual doctype is read from the   option =doctype= as defined by
  280%   html_set_options/1.
  281
  282page(Content) -->
  283    doctype,
  284    html(html(Content)).
  285
  286page(Head, Body) -->
  287    page(default, Head, Body).
  288
  289page(Style, Head, Body) -->
  290    doctype,
  291    content_type,
  292    html_begin(html),
  293    pagehead(Style, Head),
  294    pagebody(Style, Body),
  295    html_end(html).
  296
  297%!  doctype//
  298%
  299%   Emit the =|<DOCTYPE ...|= header.  The   doctype  comes from the
  300%   option doctype(DOCTYPE) (see html_set_options/1).   Setting  the
  301%   doctype to '' (empty  atom)   suppresses  the header completely.
  302%   This is to avoid a IE bug in processing AJAX output ...
  303
  304doctype -->
  305    { html_current_option(doctype(DocType)),
  306      DocType \== ''
  307    },
  308    !,
  309    [ '<!DOCTYPE ', DocType, '>' ].
  310doctype -->
  311    [].
  312
  313content_type -->
  314    { html_current_option(content_type(Type))
  315    },
  316    !,
  317    html_post(head, meta([ 'http-equiv'('content-type'),
  318                           content(Type)
  319                         ], [])).
  320content_type -->
  321    { html_current_option(dialect(html5)) },
  322    !,
  323    html_post(head, meta('charset=UTF-8')).
  324content_type -->
  325    [].
  326
  327pagehead(_, Head) -->
  328    { functor(Head, head, _)
  329    },
  330    !,
  331    html(Head).
  332pagehead(Style, Head) -->
  333    { strip_module(Head, M, _),
  334      hook_module(M, HM, head//2)
  335    },
  336    HM:head(Style, Head),
  337    !.
  338pagehead(_, Head) -->
  339    { strip_module(Head, M, _),
  340      hook_module(M, HM, head//1)
  341    },
  342    HM:head(Head),
  343    !.
  344pagehead(_, Head) -->
  345    html(head(Head)).
  346
  347
  348pagebody(_, Body) -->
  349    { functor(Body, body, _)
  350    },
  351    !,
  352    html(Body).
  353pagebody(Style, Body) -->
  354    { strip_module(Body, M, _),
  355      hook_module(M, HM, body//2)
  356    },
  357    HM:body(Style, Body),
  358    !.
  359pagebody(_, Body) -->
  360    { strip_module(Body, M, _),
  361      hook_module(M, HM, body//1)
  362    },
  363    HM:body(Body),
  364    !.
  365pagebody(_, Body) -->
  366    html(body(Body)).
  367
  368
  369hook_module(M, M, PI) :-
  370    current_predicate(M:PI),
  371    !.
  372hook_module(_, user, PI) :-
  373    current_predicate(user:PI).
  374
  375%!  html(+Content:dom)// is det
  376%
  377%   Generate HTML from Content.  Generates a token sequence for
  378%   print_html/2.
  379
  380html(Spec) -->
  381    { strip_module(Spec, M, T) },
  382    qhtml(T, M).
  383
  384qhtml(Var, _) -->
  385    { var(Var),
  386      !,
  387      instantiation_error(Var)
  388    }.
  389qhtml([], _) -->
  390    !,
  391    [].
  392qhtml([H|T], M) -->
  393    !,
  394    html_expand(H, M),
  395    qhtml(T, M).
  396qhtml(X, M) -->
  397    html_expand(X, M).
  398
  399html_expand(Var, _) -->
  400    { var(Var),
  401      !,
  402      instantiation_error(Var)
  403    }.
  404html_expand(Term, Module) -->
  405    do_expand(Term, Module),
  406    !.
  407html_expand(Term, _Module) -->
  408    { print_message(error, html(expand_failed(Term))) }.
  409
  410
  411do_expand(Token, _) -->                 % call user hooks
  412    expand(Token),
  413    !.
  414do_expand(Fmt-Args, _) -->
  415    !,
  416    { format(string(String), Fmt, Args)
  417    },
  418    html_quoted(String).
  419do_expand(\List, Module) -->
  420    { is_list(List)
  421    },
  422    !,
  423    raw(List, Module).
  424do_expand(\Term, Module, In, Rest) :-
  425    !,
  426    call(Module:Term, In, Rest).
  427do_expand(Module:Term, _) -->
  428    !,
  429    qhtml(Term, Module).
  430do_expand(&(Entity), _) -->
  431    !,
  432    {   integer(Entity)
  433    ->  format(string(String), '&#~d;', [Entity])
  434    ;   format(string(String), '&~w;', [Entity])
  435    },
  436    [ String ].
  437do_expand(Token, _) -->
  438    { atomic(Token)
  439    },
  440    !,
  441    html_quoted(Token).
  442do_expand(element(Env, Attributes, Contents), M) -->
  443    !,
  444    (   { Contents == [],
  445          html_current_option(dialect(xhtml))
  446        }
  447    ->  xhtml_empty(Env, Attributes)
  448    ;   html_begin(Env, Attributes),
  449        qhtml(Env, Contents, M),
  450        html_end(Env)
  451    ).
  452do_expand(Term, M) -->
  453    { Term =.. [Env, Contents]
  454    },
  455    !,
  456    (   { html_no_content(Env) }
  457    ->  html_begin(Env, Contents)
  458    ;   (   { Contents == [],
  459              html_current_option(dialect(xhtml))
  460            }
  461        ->  xhtml_empty(Env, [])
  462        ;   html_begin(Env),
  463            qhtml(Env, Contents, M),
  464            html_end(Env)
  465        )
  466    ).
  467do_expand(Term, M) -->
  468    { Term =.. [Env, Attributes, Contents],
  469      check_non_empty(Contents, Env, Term)
  470    },
  471    !,
  472    (   { Contents == [],
  473          html_current_option(dialect(xhtml))
  474        }
  475    ->  xhtml_empty(Env, Attributes)
  476    ;   html_begin(Env, Attributes),
  477        qhtml(Env, Contents, M),
  478        html_end(Env)
  479    ).
  480
  481qhtml(Env, Contents, M) -->
  482    { cdata_element(Env),
  483      phrase(cdata(Contents, M), Tokens)
  484    },
  485    !,
  486    [ cdata(Env, Tokens) ].
  487qhtml(_, Contents, M) -->
  488    qhtml(Contents, M).
  489
  490
  491check_non_empty([], _, _) :- !.
  492check_non_empty(_, Tag, Term) :-
  493    html_no_content(Tag),
  494    !,
  495    print_message(warning,
  496                  format('Using empty element with content: ~p', [Term])).
  497check_non_empty(_, _, _).
  498
  499cdata(List, M) -->
  500    { is_list(List) },
  501    !,
  502    raw(List, M).
  503cdata(One, M) -->
  504    raw_element(One, M).
  505
  506%!  raw(+List, +Module)// is det.
  507%
  508%   Emit unquoted (raw) output used for scripts, etc.
  509
  510raw([], _) -->
  511    [].
  512raw([H|T], Module) -->
  513    raw_element(H, Module),
  514    raw(T, Module).
  515
  516raw_element(Var, _) -->
  517    { var(Var),
  518      !,
  519      instantiation_error(Var)
  520    }.
  521raw_element(\List, Module) -->
  522    { is_list(List)
  523    },
  524    !,
  525    raw(List, Module).
  526raw_element(\Term, Module, In, Rest) :-
  527    !,
  528    call(Module:Term, In, Rest).
  529raw_element(Module:Term, _) -->
  530    !,
  531    raw_element(Term, Module).
  532raw_element(Fmt-Args, _) -->
  533    !,
  534    { format(string(S), Fmt, Args) },
  535    [S].
  536raw_element(Value, _) -->
  537    { must_be(atomic, Value) },
  538    [Value].
  539
  540
  541%!  html_begin(+Env)// is det.
  542%!  html_end(+End)// is det
  543%
  544%   For  html_begin//1,  Env  is   a    term   Env(Attributes);  for
  545%   html_end//1  it  is  the  plain    environment  name.  Used  for
  546%   exceptional  cases.  Normal  applications    use   html//1.  The
  547%   following two fragments are identical, where we prefer the first
  548%   as it is more concise and less error-prone.
  549%
  550%   ==
  551%           html(table(border=1, \table_content))
  552%   ==
  553%   ==
  554%           html_begin(table(border=1)
  555%           table_content,
  556%           html_end(table)
  557%   ==
  558
  559html_begin(Env) -->
  560    { Env =.. [Name|Attributes]
  561    },
  562    html_begin(Name, Attributes).
  563
  564html_begin(Env, Attributes) -->
  565    pre_open(Env),
  566    [<],
  567    [Env],
  568    attributes(Env, Attributes),
  569    (   { html_no_content(Env),
  570          html_current_option(dialect(xhtml))
  571        }
  572    ->  ['/>']
  573    ;   [>]
  574    ),
  575    post_open(Env).
  576
  577html_end(Env)   -->                     % empty element or omited close
  578    { html_no_content(Env)
  579    ; layout(Env, _, -),
  580      html_current_option(dialect(html))
  581    },
  582    !,
  583    [].
  584html_end(Env)   -->
  585    pre_close(Env),
  586    ['</'],
  587    [Env],
  588    ['>'],
  589    post_close(Env).
  590
  591%!  xhtml_empty(+Env, +Attributes)// is det.
  592%
  593%   Emit element in xhtml mode with empty content.
  594
  595xhtml_empty(Env, Attributes) -->
  596    pre_open(Env),
  597    [<],
  598    [Env],
  599    attributes(Attributes),
  600    ['/>'].
  601
  602%!  xhtml_ns(+Id, +Value)//
  603%
  604%   Demand an xmlns:id=Value in the outer   html  tag. This uses the
  605%   html_post/2 mechanism to  post  to   the  =xmlns=  channel. Rdfa
  606%   (http://www.w3.org/2006/07/SWD/RDFa/syntax/), embedding RDF   in
  607%   (x)html provides a typical  usage  scenario   where  we  want to
  608%   publish the required namespaces in the header. We can define:
  609%
  610%   ==
  611%   rdf_ns(Id) -->
  612%           { rdf_global_id(Id:'', Value) },
  613%           xhtml_ns(Id, Value).
  614%   ==
  615%
  616%   After which we can use rdf_ns//1 as  a normal rule in html//1 to
  617%   publish namespaces from library(semweb/rdf_db).   Note that this
  618%   macro only has effect if  the  dialect   is  set  to =xhtml=. In
  619%   =html= mode it is silently ignored.
  620%
  621%   The required =xmlns= receiver  is   installed  by  html_begin//1
  622%   using the =html= tag and thus is   present  in any document that
  623%   opens the outer =html= environment through this library.
  624
  625xhtml_ns(Id, Value) -->
  626    { html_current_option(dialect(xhtml)) },
  627    !,
  628    html_post(xmlns, \attribute(xmlns:Id=Value)).
  629xhtml_ns(_, _) -->
  630    [].
  631
  632%!  html_root_attribute(+Name, +Value)//
  633%
  634%   Add an attribute to the  HTML  root   element  of  the page. For
  635%   example:
  636%
  637%     ==
  638%         html(div(...)),
  639%         html_root_attribute(lang, en),
  640%         ...
  641%     ==
  642
  643html_root_attribute(Name, Value) -->
  644    html_post(html_begin, \attribute(Name=Value)).
  645
  646%!  attributes(+Env, +Attributes)// is det.
  647%
  648%   Emit attributes for Env. Adds XHTML namespace declaration to the
  649%   html tag if not provided by the caller.
  650
  651attributes(html, L) -->
  652    !,
  653    (   { html_current_option(dialect(xhtml)) }
  654    ->  (   { option(xmlns(_), L) }
  655        ->  attributes(L)
  656        ;   { ns(xhtml, NS) },
  657            attributes([xmlns(NS)|L])
  658        ),
  659        html_receive(xmlns)
  660    ;   attributes(L),
  661        html_noreceive(xmlns)
  662    ),
  663    html_receive(html_begin).
  664attributes(_, L) -->
  665    attributes(L).
  666
  667attributes([]) -->
  668    !,
  669    [].
  670attributes([H|T]) -->
  671    !,
  672    attribute(H),
  673    attributes(T).
  674attributes(One) -->
  675    attribute(One).
  676
  677attribute(Name=Value) -->
  678    !,
  679    [' '], name(Name), [ '="' ],
  680    attribute_value(Value),
  681    ['"'].
  682attribute(NS:Term) -->
  683    !,
  684    { Term =.. [Name, Value]
  685    },
  686    !,
  687    attribute((NS:Name)=Value).
  688attribute(Term) -->
  689    { Term =.. [Name, Value]
  690    },
  691    !,
  692    attribute(Name=Value).
  693attribute(Atom) -->                     % Value-abbreviated attribute
  694    { atom(Atom)
  695    },
  696    [ ' ', Atom ].
  697
  698name(NS:Name) -->
  699    !,
  700    [NS, :, Name].
  701name(Name) -->
  702    [ Name ].
  703
  704%!  attribute_value(+Value) is det.
  705%
  706%   Print an attribute value. Value is either   atomic or one of the
  707%   following terms:
  708%
  709%     * A+B
  710%     Concatenation of A and B
  711%     * encode(V)
  712%     Emit URL-encoded version of V.  See www_form_encode/2.
  713%     * An option list
  714%     Emit ?Name1=encode(Value1)&Name2=encode(Value2) ...
  715%     * A term Format-Arguments
  716%     Use format/3 and emit the result as quoted value.
  717%
  718%   The hook html_write:expand_attribute_value//1 can  be defined to
  719%   provide additional `function like'   translations.  For example,
  720%   http_dispatch.pl  defines  location_by_id(ID)  to   refer  to  a
  721%   location on the current server  based   on  the  handler id. See
  722%   http_location_by_id/2.
  723
  724attribute_value(List) -->
  725    { is_list(List) },
  726    !,
  727    attribute_value_m(List).
  728attribute_value(Value) -->
  729    attribute_value_s(Value).
  730
  731% emit a single attribute value
  732
  733attribute_value_s(Var) -->
  734    { var(Var),
  735      !,
  736      instantiation_error(Var)
  737    }.
  738attribute_value_s(A+B) -->
  739    !,
  740    attribute_value(A),
  741    (   { is_list(B) }
  742    ->  (   { B == [] }
  743        ->  []
  744        ;   [?], search_parameters(B)
  745        )
  746    ;   attribute_value(B)
  747    ).
  748attribute_value_s(encode(Value)) -->
  749    !,
  750    { uri_encoded(query_value, Value, Encoded) },
  751    [ Encoded ].
  752attribute_value_s(Value) -->
  753    expand_attribute_value(Value),
  754    !.
  755attribute_value_s(Fmt-Args) -->
  756    !,
  757    { format(string(Value), Fmt, Args) },
  758    html_quoted_attribute(Value).
  759attribute_value_s(Value) -->
  760    html_quoted_attribute(Value).
  761
  762search_parameters([H|T]) -->
  763    search_parameter(H),
  764    (   {T == []}
  765    ->  []
  766    ;   ['&amp;'],
  767        search_parameters(T)
  768    ).
  769
  770search_parameter(Var) -->
  771    { var(Var),
  772      !,
  773      instantiation_error(Var)
  774    }.
  775search_parameter(Name=Value) -->
  776    { www_form_encode(Value, Encoded) },
  777    [Name, =, Encoded].
  778search_parameter(Term) -->
  779    { Term =.. [Name, Value],
  780      !,
  781      www_form_encode(Value, Encoded)
  782    },
  783    [Name, =, Encoded].
  784search_parameter(Term) -->
  785    { domain_error(search_parameter, Term)
  786    }.
  787
  788%!  attribute_value_m(+List)//
  789%
  790%   Used for multi-valued attributes, such as class-lists.  E.g.,
  791%
  792%     ==
  793%           body(class([c1, c2]), Body)
  794%     ==
  795%
  796%     Emits =|<body class="c1 c2"> ...|=
  797
  798attribute_value_m([]) -->
  799    [].
  800attribute_value_m([H|T]) -->
  801    attribute_value_s(H),
  802    (   { T == [] }
  803    ->  []
  804    ;   [' '],
  805        attribute_value_m(T)
  806    ).
  807
  808
  809                 /*******************************
  810                 *         QUOTING RULES        *
  811                 *******************************/
  812
  813%!  html_quoted(Text)// is det.
  814%
  815%   Quote  the  value  for  normal  (CDATA)  text.  Note  that  text
  816%   appearing in the document  structure   is  normally quoted using
  817%   these rules. I.e. the following emits  properly quoted bold text
  818%   regardless of the content of Text:
  819%
  820%   ==
  821%           html(b(Text))
  822%   ==
  823%
  824%   @tbd    Assumes UTF-8 encoding of the output.
  825
  826html_quoted(Text) -->
  827    { xml_quote_cdata(Text, Quoted, utf8) },
  828    [ Quoted ].
  829
  830%!  html_quoted_attribute(+Text)// is det.
  831%
  832%   Quote the value  according  to   the  rules  for  tag-attributes
  833%   included in double-quotes.  Note   that  -like  html_quoted//1-,
  834%   attributed   values   printed   through   html//1   are   quoted
  835%   automatically.
  836%
  837%   @tbd    Assumes UTF-8 encoding of the output.
  838
  839html_quoted_attribute(Text) -->
  840    { xml_quote_attribute(Text, Quoted, utf8) },
  841    [ Quoted ].
  842
  843%!  cdata_element(?Element)
  844%
  845%   True when Element contains declared CDATA   and thus only =|</|=
  846%   needs to be escaped.
  847
  848cdata_element(script).
  849cdata_element(style).
  850
  851
  852                 /*******************************
  853                 *      REPOSITIONING HTML      *
  854                 *******************************/
  855
  856%!  html_post(+Id, :HTML)// is det.
  857%
  858%   Reposition HTML to  the  receiving   Id.  The  html_post//2 call
  859%   processes HTML using html//1. Embedded   \-commands are executed
  860%   by mailman/1 from  print_html/1   or  html_print_length/2. These
  861%   commands are called in the calling   context of the html_post//2
  862%   call.
  863%
  864%   A typical usage scenario is to  get   required  CSS links in the
  865%   document head in a reusable fashion. First, we define css//1 as:
  866%
  867%   ==
  868%   css(URL) -->
  869%           html_post(css,
  870%                     link([ type('text/css'),
  871%                            rel('stylesheet'),
  872%                            href(URL)
  873%                          ])).
  874%   ==
  875%
  876%   Next we insert the _unique_ CSS links, in the pagehead using the
  877%   following call to reply_html_page/2:
  878%
  879%   ==
  880%           reply_html_page([ title(...),
  881%                             \html_receive(css)
  882%                           ],
  883%                           ...)
  884%   ==
  885
  886html_post(Id, Content) -->
  887    { strip_module(Content, M, C) },
  888    [ mailbox(Id, post(M, C)) ].
  889
  890%!  html_receive(+Id)// is det.
  891%
  892%   Receive posted HTML tokens. Unique   sequences  of tokens posted
  893%   with  html_post//2  are  inserted   at    the   location   where
  894%   html_receive//1 appears.
  895%
  896%   @see    The local predicate sorted_html//1 handles the output of
  897%           html_receive//1.
  898%   @see    html_receive//2 allows for post-processing the posted
  899%           material.
  900
  901html_receive(Id) -->
  902    html_receive(Id, sorted_html).
  903
  904%!  html_receive(+Id, :Handler)// is det.
  905%
  906%   This extended version of html_receive//1   causes  Handler to be
  907%   called to process all messages posted to the channel at the time
  908%   output  is  generated.  Handler  is    called  as  below,  where
  909%   `PostedTerms` is a list of  Module:Term   created  from calls to
  910%   html_post//2. Module is the context module of html_post and Term
  911%   is the unmodified term. Members  in   `PostedTerms`  are  in the
  912%   order posted and may contain duplicates.
  913%
  914%     ==
  915%       phrase(Handler, PostedTerms, HtmlTerms, Rest)
  916%     ==
  917%
  918%   Typically, Handler collects the posted   terms,  creating a term
  919%   suitable for html//1 and finally calls html//1.
  920
  921html_receive(Id, Handler) -->
  922    { strip_module(Handler, M, P) },
  923    [ mailbox(Id, accept(M:P, _)) ].
  924
  925%!  html_noreceive(+Id)// is det.
  926%
  927%   As html_receive//1, but discard posted messages.
  928
  929html_noreceive(Id) -->
  930    [ mailbox(Id, ignore(_,_)) ].
  931
  932%!  mailman(+Tokens) is det.
  933%
  934%   Collect  posted  tokens  and  copy    them  into  the  receiving
  935%   mailboxes. Mailboxes may produce output for  each other, but not
  936%   cyclic. The current scheme to resolve   this is rather naive: It
  937%   simply  permutes  the mailbox resolution order  until it found a
  938%   working one. Before that, it puts   =head= and =script= boxes at
  939%   the end.
  940
  941mailman(Tokens) :-
  942    (   html_token(mailbox(_, accept(_, Accepted)), Tokens)
  943    ->  true
  944    ),
  945    var(Accepted),                 % not yet executed
  946    !,
  947    mailboxes(Tokens, Boxes),
  948    keysort(Boxes, Keyed),
  949    group_pairs_by_key(Keyed, PerKey),
  950    move_last(PerKey, script, PerKey1),
  951    move_last(PerKey1, head, PerKey2),
  952    (   permutation(PerKey2, PerKeyPerm),
  953        (   mail_ids(PerKeyPerm)
  954        ->  !
  955        ;   debug(html(mailman),
  956                  'Failed mail delivery order; retrying', []),
  957            fail
  958        )
  959    ->  true
  960    ;   print_message(error, html(cyclic_mailboxes))
  961    ).
  962mailman(_).
  963
  964move_last(Box0, Id, Box) :-
  965    selectchk(Id-List, Box0, Box1),
  966    !,
  967    append(Box1, [Id-List], Box).
  968move_last(Box, _, Box).
  969
  970%!  html_token(?Token, +Tokens) is nondet.
  971%
  972%   True if Token is a token in the  token set. This is like member,
  973%   but the toplevel list may contain cdata(Elem, Tokens).
  974
  975html_token(Token, [H|T]) :-
  976    html_token_(T, H, Token).
  977
  978html_token_(_, Token, Token) :- !.
  979html_token_(_, cdata(_,Tokens), Token) :-
  980    html_token(Token, Tokens).
  981html_token_([H|T], _, Token) :-
  982    html_token_(T, H, Token).
  983
  984%!  mailboxes(+Tokens, -MailBoxes) is det.
  985%
  986%   Get all mailboxes from the token set.
  987
  988mailboxes(Tokens, MailBoxes) :-
  989    mailboxes(Tokens, MailBoxes, []).
  990
  991mailboxes([], List, List).
  992mailboxes([mailbox(Id, Value)|T0], [Id-Value|T], Tail) :-
  993    !,
  994    mailboxes(T0, T, Tail).
  995mailboxes([cdata(_Type, Tokens)|T0], Boxes, Tail) :-
  996    !,
  997    mailboxes(Tokens, Boxes, Tail0),
  998    mailboxes(T0, Tail0, Tail).
  999mailboxes([_|T0], T, Tail) :-
 1000    mailboxes(T0, T, Tail).
 1001
 1002mail_ids([]).
 1003mail_ids([H|T0]) :-
 1004    mail_id(H, NewPosts),
 1005    add_new_posts(NewPosts, T0, T),
 1006    mail_ids(T).
 1007
 1008mail_id(Id-List, NewPosts) :-
 1009    mail_handlers(List, Boxes, Content),
 1010    (   Boxes = [accept(MH:Handler, In)]
 1011    ->  extend_args(Handler, Content, Goal),
 1012        phrase(MH:Goal, In),
 1013        mailboxes(In, NewBoxes),
 1014        keysort(NewBoxes, Keyed),
 1015        group_pairs_by_key(Keyed, NewPosts)
 1016    ;   Boxes = [ignore(_, _)|_]
 1017    ->  NewPosts = []
 1018    ;   Boxes = [accept(_,_),accept(_,_)|_]
 1019    ->  print_message(error, html(multiple_receivers(Id))),
 1020        NewPosts = []
 1021    ;   print_message(error, html(no_receiver(Id))),
 1022        NewPosts = []
 1023    ).
 1024
 1025add_new_posts([], T, T).
 1026add_new_posts([Id-Posts|NewT], T0, T) :-
 1027    (   select(Id-List0, T0, Id-List, T1)
 1028    ->  append(List0, Posts, List)
 1029    ;   debug(html(mailman), 'Stuck with new posts on ~q', [Id]),
 1030        fail
 1031    ),
 1032    add_new_posts(NewT, T1, T).
 1033
 1034
 1035%!  mail_handlers(+Boxes, -Handlers, -Posters) is det.
 1036%
 1037%   Collect all post(Module,HTML) into Posters  and the remainder in
 1038%   Handlers.  Handlers  consists  of  accept(Handler,  Tokens)  and
 1039%   ignore(_,_).
 1040
 1041mail_handlers([], [], []).
 1042mail_handlers([post(Module,HTML)|T0], H, [Module:HTML|T]) :-
 1043    !,
 1044    mail_handlers(T0, H, T).
 1045mail_handlers([H|T0], [H|T], C) :-
 1046    mail_handlers(T0, T, C).
 1047
 1048extend_args(Term, Extra, NewTerm) :-
 1049    Term =.. [Name|Args],
 1050    append(Args, [Extra], NewArgs),
 1051    NewTerm =.. [Name|NewArgs].
 1052
 1053%!  sorted_html(+Content:list)// is det.
 1054%
 1055%   Default  handlers  for  html_receive//1.  It  sorts  the  posted
 1056%   objects to create a unique list.
 1057%
 1058%   @bug    Elements can differ just on the module.  Ideally we
 1059%           should phrase all members, sort the list of list of
 1060%           tokens and emit the result.  Can we do better?
 1061
 1062sorted_html(List) -->
 1063    { sort(List, Unique) },
 1064    html(Unique).
 1065
 1066%!  head_html(+Content:list)// is det.
 1067%
 1068%   Handler for html_receive(head). Unlike  sorted_html//1, it calls
 1069%   a user hook  html_write:html_head_expansion/2   to  process  the
 1070%   collected head material into a term suitable for html//1.
 1071%
 1072%   @tbd  This  has  been  added   to  facilitate  html_head.pl,  an
 1073%   experimental  library  for  dealing  with   css  and  javascript
 1074%   resources. It feels a bit like a hack, but for now I do not know
 1075%   a better solution.
 1076
 1077head_html(List) -->
 1078    { list_to_set(List, Unique),
 1079      html_expand_head(Unique, NewList)
 1080    },
 1081    html(NewList).
 1082
 1083:- multifile
 1084    html_head_expansion/2. 1085
 1086html_expand_head(List0, List) :-
 1087    html_head_expansion(List0, List1),
 1088    List0 \== List1,
 1089    !,
 1090    html_expand_head(List1, List).
 1091html_expand_head(List, List).
 1092
 1093
 1094                 /*******************************
 1095                 *             LAYOUT           *
 1096                 *******************************/
 1097
 1098pre_open(Env) -->
 1099    { layout(Env, N-_, _)
 1100    },
 1101    !,
 1102    [ nl(N) ].
 1103pre_open(_) --> [].
 1104
 1105post_open(Env) -->
 1106    { layout(Env, _-N, _)
 1107    },
 1108    !,
 1109    [ nl(N) ].
 1110post_open(_) -->
 1111    [].
 1112
 1113pre_close(head) -->
 1114    !,
 1115    html_receive(head, head_html),
 1116    { layout(head, _, N-_) },
 1117    [ nl(N) ].
 1118pre_close(Env) -->
 1119    { layout(Env, _, N-_)
 1120    },
 1121    !,
 1122    [ nl(N) ].
 1123pre_close(_) -->
 1124    [].
 1125
 1126post_close(Env) -->
 1127    { layout(Env, _, _-N)
 1128    },
 1129    !,
 1130    [ nl(N) ].
 1131post_close(_) -->
 1132    [].
 1133
 1134%!  layout(+Tag, -Open, -Close) is det.
 1135%
 1136%   Define required newlines before and after   tags.  This table is
 1137%   rather incomplete. New rules can  be   added  to  this multifile
 1138%   predicate.
 1139%
 1140%   @arg Tag      Name of the tag
 1141%   @arg Open     Tuple M-N, where M is the number of lines before
 1142%                 the tag and N after.
 1143%   @arg Close    Either as Open, or the atom - (minus) to omit the
 1144%                 close-tag.
 1145%
 1146%   @tbd    Complete table
 1147
 1148:- multifile
 1149    layout/3. 1150
 1151layout(table,      2-1, 1-2).
 1152layout(blockquote, 2-1, 1-2).
 1153layout(pre,        2-1, 0-2).
 1154layout(textarea,   1-1, 0-1).
 1155layout(center,     2-1, 1-2).
 1156layout(dl,         2-1, 1-2).
 1157layout(ul,         1-1, 1-1).
 1158layout(ol,         2-1, 1-2).
 1159layout(form,       2-1, 1-2).
 1160layout(frameset,   2-1, 1-2).
 1161layout(address,    2-1, 1-2).
 1162
 1163layout(head,       1-1, 1-1).
 1164layout(body,       1-1, 1-1).
 1165layout(script,     1-1, 1-1).
 1166layout(style,      1-1, 1-1).
 1167layout(select,     1-1, 1-1).
 1168layout(map,        1-1, 1-1).
 1169layout(html,       1-1, 1-1).
 1170layout(caption,    1-1, 1-1).
 1171layout(applet,     1-1, 1-1).
 1172
 1173layout(tr,         1-0, 0-1).
 1174layout(option,     1-0, 0-1).
 1175layout(li,         1-0, 0-1).
 1176layout(dt,         1-0, -).
 1177layout(dd,         0-0, -).
 1178layout(title,      1-0, 0-1).
 1179
 1180layout(h1,         2-0, 0-2).
 1181layout(h2,         2-0, 0-2).
 1182layout(h3,         2-0, 0-2).
 1183layout(h4,         2-0, 0-2).
 1184
 1185layout(iframe,     1-1, 1-1).
 1186
 1187layout(area,       1-0, -).
 1188layout(base,       1-1, -).
 1189layout(br,         0-1, -).
 1190layout(col,        0-0, -).
 1191layout(embed,      1-1, -).
 1192layout(hr,         1-1, -).
 1193layout(img,        0-0, -).
 1194layout(input,      1-0, -).
 1195layout(link,       1-1, -).
 1196layout(meta,       1-1, -).
 1197layout(param,      1-0, -).
 1198layout(source,     1-0, -).
 1199layout(track,	   1-0, -).
 1200layout(wbr,	   0-0, -).
 1201
 1202layout(p,          2-1, -).             % omited close
 1203layout(td,         0-0, 0-0).
 1204
 1205layout(div,        1-0, 0-1).
 1206
 1207                 /*******************************
 1208                 *           PRINTING           *
 1209                 *******************************/
 1210
 1211%!  print_html(+List) is det.
 1212%!  print_html(+Out:stream, +List) is det.
 1213%
 1214%   Print list of atoms and layout instructions.  Currently used layout
 1215%   instructions:
 1216%
 1217%           * nl(N)
 1218%           Use at minimum N newlines here.
 1219%
 1220%           * mailbox(Id, Box)
 1221%           Repositioned tokens (see html_post//2 and
 1222%           html_receive//2)
 1223
 1224print_html(List) :-
 1225    current_output(Out),
 1226    mailman(List),
 1227    write_html(List, Out).
 1228print_html(Out, List) :-
 1229    (   html_current_option(dialect(xhtml))
 1230    ->  stream_property(Out, encoding(Enc)),
 1231        (   Enc == utf8
 1232        ->  true
 1233        ;   print_message(warning, html(wrong_encoding(Out, Enc)))
 1234        ),
 1235        xml_header(Hdr),
 1236        write(Out, Hdr), nl(Out)
 1237    ;   true
 1238    ),
 1239    mailman(List),
 1240    write_html(List, Out),
 1241    flush_output(Out).
 1242
 1243write_html([], _).
 1244write_html([nl(N)|T], Out) :-
 1245    !,
 1246    join_nl(T, N, Lines, T2),
 1247    write_nl(Lines, Out),
 1248    write_html(T2, Out).
 1249write_html([mailbox(_, Box)|T], Out) :-
 1250    !,
 1251    (   Box = accept(_, Accepted),
 1252        nonvar(Accepted)
 1253    ->  write_html(Accepted, Out)
 1254    ;   true
 1255    ),
 1256    write_html(T, Out).
 1257write_html([cdata(Env, Tokens)|T], Out) :-
 1258    !,
 1259    with_output_to(string(CDATA), write_html(Tokens, current_output)),
 1260    valid_cdata(Env, CDATA),
 1261    write(Out, CDATA),
 1262    write_html(T, Out).
 1263write_html([H|T], Out) :-
 1264    write(Out, H),
 1265    write_html(T, Out).
 1266
 1267join_nl([nl(N0)|T0], N1, N, T) :-
 1268    !,
 1269    N2 is max(N0, N1),
 1270    join_nl(T0, N2, N, T).
 1271join_nl(L, N, N, L).
 1272
 1273write_nl(0, _) :- !.
 1274write_nl(N, Out) :-
 1275    nl(Out),
 1276    N1 is N - 1,
 1277    write_nl(N1, Out).
 1278
 1279%!  valid_cdata(+Env, +String) is det.
 1280%
 1281%   True when String is valid content for   a  CDATA element such as
 1282%   =|<script>|=. This implies  it   cannot  contain  =|</script/|=.
 1283%   There is no escape for this and  the script generator must use a
 1284%   work-around using features of the  script language. For example,
 1285%   when  using  JavaScript,  "</script>"   can    be   written   as
 1286%   "<\/script>".
 1287%
 1288%   @see write_json/2, js_arg//1.
 1289%   @error domain_error(cdata, String)
 1290
 1291valid_cdata(Env, String) :-
 1292    atomics_to_string(['</', Env, '>'], End),
 1293    sub_atom_icasechk(String, _, End),
 1294    !,
 1295    domain_error(cdata, String).
 1296valid_cdata(_, _).
 1297
 1298%!  html_print_length(+List, -Len) is det.
 1299%
 1300%   Determine the content length of  a   token  list  produced using
 1301%   html//1. Here is an example on  how   this  is used to output an
 1302%   HTML compatible to HTTP:
 1303%
 1304%   ==
 1305%           phrase(html(DOM), Tokens),
 1306%           html_print_length(Tokens, Len),
 1307%           format('Content-type: text/html; charset=UTF-8~n'),
 1308%           format('Content-length: ~d~n~n', [Len]),
 1309%           print_html(Tokens)
 1310%   ==
 1311
 1312html_print_length(List, Len) :-
 1313    mailman(List),
 1314    (   html_current_option(dialect(xhtml))
 1315    ->  xml_header(Hdr),
 1316        atom_length(Hdr, L0),
 1317        L1 is L0+1                  % one for newline
 1318    ;   L1 = 0
 1319    ),
 1320    html_print_length(List, L1, Len).
 1321
 1322html_print_length([], L, L).
 1323html_print_length([nl(N)|T], L0, L) :-
 1324    !,
 1325    join_nl(T, N, Lines, T1),
 1326    L1 is L0 + Lines,               % assume only \n!
 1327    html_print_length(T1, L1, L).
 1328html_print_length([mailbox(_, Box)|T], L0, L) :-
 1329    !,
 1330    (   Box = accept(_, Accepted)
 1331    ->  html_print_length(Accepted, L0, L1)
 1332    ;   L1 = L0
 1333    ),
 1334    html_print_length(T, L1, L).
 1335html_print_length([cdata(_, CDATA)|T], L0, L) :-
 1336    !,
 1337    html_print_length(CDATA, L0, L1),
 1338    html_print_length(T, L1, L).
 1339html_print_length([H|T], L0, L) :-
 1340    atom_length(H, Hlen),
 1341    L1 is L0+Hlen,
 1342    html_print_length(T, L1, L).
 1343
 1344
 1345%!  reply_html_page(:Head, :Body) is det.
 1346%!  reply_html_page(+Style, :Head, :Body) is det.
 1347%
 1348%   Provide the complete reply as required by http_wrapper.pl for a page
 1349%   constructed  from  Head  and  Body.  The  HTTP  =|Content-type|=  is
 1350%   provided by html_current_option/1.
 1351%
 1352%   @see  reply_html_partial/1  to  avoid  adding   a  ``DOCTYPE``,  and
 1353%   required outer HTML elements such as ``<html>``.
 1354
 1355reply_html_page(Head, Body) :-
 1356    reply_html_page(default, Head, Body).
 1357reply_html_page(Style, Head, Body) :-
 1358    html_current_option(content_type(Type)),
 1359    phrase(page(Style, Head, Body), HTML),
 1360    forall(html_header_hook(Style), true),
 1361    format('Content-type: ~w~n~n', [Type]),
 1362    print_html(HTML).
 1363
 1364
 1365%!  reply_html_partial(+HTML) is det.
 1366%
 1367%   Reply with partial HTML  document.  The   reply  only  contains  the
 1368%   element from HTML, i.e., this predicate   does not add a ``DOCTYPE``
 1369%   header, ``<html>``, ``<head>`` or  ``<body>``.   It  is intended for
 1370%   JavaScript handlers that request a partial  document and insert that
 1371%   somewhere into the existing page DOM.
 1372%
 1373%   @see reply_html_page/3 to reply with a complete (valid) HTML page.
 1374%   @since 9.1.20
 1375
 1376reply_html_partial(HTML) :-
 1377    html_current_option(content_type(Type)),
 1378    phrase(html(HTML), Tokens),
 1379    format('Content-type: ~w~n~n', [Type]),
 1380    print_html(Tokens).
 1381
 1382
 1383%!  html_header_hook(+Style) is nondet.
 1384%
 1385%   This multifile hook  is  called   just  before  the ``Content-type``
 1386%   header  is  emitted.  It  allows  for  emitting  additional  headers
 1387%   depending on the first argument of reply_html_page/3.
 1388
 1389
 1390:- html_meta
 1391    html(html,?,?),
 1392    page(html,?,?),
 1393    page(html,html,?,?),
 1394    page(+,html,html,?,?),
 1395    pagehead(+,html,?,?),
 1396    pagebody(+,html,?,?),
 1397    reply_html_page(html,html),
 1398    reply_html_page(+,html,html),
 1399    html_post(+,html,?,?). 1400
 1401
 1402:- multifile
 1403    prolog:hook/1. 1404
 1405prolog:hook(body(_,_,_)).
 1406prolog:hook(body(_,_,_,_)).
 1407prolog:hook(head(_,_,_)).
 1408prolog:hook(head(_,_,_,_)).
 1409
 1410
 1411                 /*******************************
 1412                 *            MESSAGES          *
 1413                 *******************************/
 1414
 1415:- multifile
 1416    prolog:message/3. 1417
 1418prolog:message(html(expand_failed(What))) -->
 1419    [ 'Failed to translate to HTML: ~p'-[What] ].
 1420prolog:message(html(wrong_encoding(Stream, Enc))) -->
 1421    [ 'XHTML demands UTF-8 encoding; encoding of ~p is ~w'-[Stream, Enc] ].
 1422prolog:message(html(multiple_receivers(Id))) -->
 1423    [ 'html_post//2: multiple receivers for: ~p'-[Id] ].
 1424prolog:message(html(no_receiver(Id))) -->
 1425    [ 'html_post//2: no receivers for: ~p'-[Id] ]