View source with raw comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2014-2022, VU University Amsterdam
    7                              SWI-Prolog Solutions b.v.
    8    All rights reserved.
    9
   10    Redistribution and use in source and binary forms, with or without
   11    modification, are permitted provided that the following conditions
   12    are met:
   13
   14    1. Redistributions of source code must retain the above copyright
   15       notice, this list of conditions and the following disclaimer.
   16
   17    2. Redistributions in binary form must reproduce the above copyright
   18       notice, this list of conditions and the following disclaimer in
   19       the documentation and/or other materials provided with the
   20       distribution.
   21
   22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   33    POSSIBILITY OF SUCH DAMAGE.
   34*/
   35
   36:- module(term_html,
   37          [ term//2                             % +Term, +Options
   38          ]).   39:- use_module(library(http/html_write)).   40:- use_module(library(option)).   41:- use_module(library(error)).   42:- use_module(library(debug)).   43:- use_module(library(http/json)).   44
   45:- multifile
   46    blob_rendering//3,              % +Type, +Blob, +Options
   47    portray//2,                     % +Term, +Options
   48    layout/3.                       % +Term, -Layout, +Options

Represent Prolog terms as HTML

This file is primarily designed to support running Prolog applications over the web. It provides a replacement for write_term/2 which renders terms as structured HTML. */

 term(@Term, +Options)// is det
Render a Prolog term as a structured HTML tree. Options are passed to write_term/3. In addition, the following options are processed:
format(+Format)
Used for atomic values. Typically this is used to render a single value.
float_format(+Format)
If a float is rendered, it is rendered using format(string(S), Format, [Float])
To be done
- Cyclic terms.
- Attributed terms.
- Portray
- Test with Ulrich's write test set.
- Deal with numbervars and canonical.
   76term(Term, Options) -->
   77    { must_be(acyclic, Term),
   78      merge_options(Options,
   79                    [ priority(1200),
   80                      max_depth(1 000 000 000),
   81                      depth(0)
   82                    ],
   83                    Options1),
   84      dict_options(Dict, Options1)
   85    },
   86    any(Term, Dict),
   87    finalize_term(Term, Dict).
   88
   89:- html_meta
   90    embrace(html,?,?).   91
   92any(_, Options) -->
   93    { Options.depth >= Options.max_depth },
   94    !,
   95    html(span(class('pl-ellipsis'), ...)).
   96any(Term, Options) -->
   97    (   {   nonvar(Term)
   98        ;   attvar(Term)
   99        }
  100    ->  portray(Term, Options)
  101    ),
  102    !.
  103any(Term, Options) -->
  104    { primitive(Term, Class0),
  105      !,
  106      quote_atomic(Term, S, Options),
  107      primitive_class(Class0, Term, S, Class)
  108    },
  109    html(span([class(Class)], S)).
  110any(Term, Options) -->
  111    { blob(Term,Type), Term \== [] },
  112    !,
  113    (   blob_rendering(Type,Term,Options)
  114    ->  []
  115    ;   html(span(class('pl-blob'),['<',Type,'>']))
  116    ).
  117any(Term, Options) -->
  118    { is_dict(Term), !
  119    },
  120    dict(Term, Options).
  121any(Term, Options) -->
  122    { assertion((compound(Term);Term==[]))
  123    },
  124    compound(Term, Options).
 compound(+Compound, +Options)// is det
Process a compound term.
  130compound('$VAR'(Var), Options) -->
  131    { Options.get(numbervars) == true,
  132      !,
  133      format(string(S), '~W', ['$VAR'(Var), [numbervars(true)]]),
  134      (   S == "_"
  135      ->  Class = 'pl-anon'
  136      ;   Class = 'pl-var'
  137      )
  138    },
  139    html(span([class(Class)], S)).
  140compound(List, Options) -->
  141    { (   List == []
  142      ;   List = [_|_]                              % May have unbound tail
  143      ),
  144      !,
  145      arg_options(Options, _{priority:999}, ArgOptions)
  146    },
  147    list(List, ArgOptions).
  148compound({X}, Options) -->
  149    !,
  150    { arg_options(Options, _{priority:1200}, ArgOptions) },
  151    html(span(class('pl-curl'), [ '{', \any(X, ArgOptions), '}' ])).
  152compound(OpTerm, Options) -->
  153    { compound_name_arity(OpTerm, Name, 1),
  154      is_op1(Name, Type, Pri, ArgPri, Options),
  155      \+ Options.get(ignore_ops) == true
  156    },
  157    !,
  158    op1(Type, Pri, OpTerm, ArgPri, Options).
  159compound(OpTerm, Options) -->
  160    { compound_name_arity(OpTerm, Name, 2),
  161      is_op2(Name, Type, LeftPri, Pri, RightPri, Options),
  162      \+ Options.get(ignore_ops) == true
  163    },
  164    !,
  165    op2(Pri, OpTerm, Type, LeftPri, RightPri, Options).
  166compound(Compound, Options) -->
  167    { compound_name_arity(Compound, Name, Arity),
  168      quote_atomic(Name, S, Options.put(embrace, never)),
  169      arg_options(Options, _{priority:999}, ArgOptions),
  170      extra_classes(Compound, Classes, Attrs, Options)
  171    },
  172    html(span([ class(['pl-compound','pl-adaptive'|Classes]),
  173                'data-arity'(Arity),
  174                'data-name'(Name)
  175              | Attrs
  176              ],
  177              [ span(class(['pl-functor', 'pl-trigger']),
  178                     [ S, \punct('(') ]),
  179                span(class('pl-compound-args'),
  180                     [ \args(0, Arity, Compound, ArgOptions)
  181                     ])
  182              ])).
  183
  184extra_classes(Term, Classes, OAttrs, Options) :-
  185    findall(A, extra_attr(Term, A, Options), Attrs),
  186    partition(is_class_attr, Attrs, CAttrs, OAttrs),
  187    maplist(arg(1), CAttrs, Classes).
  188
  189is_class_attr(class(_)).
  190
  191extra_attr(_, class('pl-level-0'), Options) :-
  192    Options.depth == 0.
  193extra_attr(Term, 'data-layout'(Data), Options) :-
  194    layout(Term, Layout, Options),
  195    (   is_dict(Layout)
  196    ->  atom_json_dict(Data, Layout, [])
  197    ;   Data = Layout
  198    ).
 arg_options(+Options, -OptionsOut) is det
 arg_options(+Options, +Extra, -OptionsOut) is det
Increment depth in Options.
  206arg_options(Options, Options.put(depth, NewDepth)) :-
  207    NewDepth is Options.depth+1.
  208arg_options(Options, Extra, Options.put(depth, NewDepth).put(Extra)) :-
  209    NewDepth is Options.depth+1.
 args(+Arg0, +Arity, +Compound, +Options)//
Emit arguments of a compound term.
  215args(Arity, Arity, _, _) --> !.
  216args(I, Arity, Compound, ArgOptions) -->
  217    { NI is I + 1,
  218      arg(NI, Compound, Arg)
  219    },
  220    (   {NI == Arity}
  221    ->  html([ span(class('pl-compound-arg'), \any(Arg, ArgOptions)),
  222               span(class(['pl-compound-close', 'pl-punct']), ')')
  223             ])
  224    ;   html(span(class('pl-compound-arg'),
  225                  [ \any(Arg, ArgOptions), \punct(',') ])),
  226        args(NI, Arity, Compound, ArgOptions)
  227    ).
  228
  229punct(Punct) -->
  230    html(span(class('pl-punct'), Punct)).
 list(+List, +Options)//
Emit a list. The List may have an unbound tail.
  236list(List, Options) -->
  237    { '$skip_list'(Length, List, Tail),
  238      (   Tail == []
  239      ->  Attr = ['data-length'(Length)]
  240      ;   Attr = ['data-length'(Length), 'data-partial'(true)]
  241      )
  242    },
  243    html(span([ class(['pl-list','pl-adaptive'])
  244              | Attr
  245              ],
  246              [ span(class(['pl-list-open', 'pl-trigger', 'pl-punct']), '['),
  247                \list_content(List, Options),
  248                span(class(['pl-list-close', 'pl-punct']), ']')
  249              ])).
  250
  251list_content([], _Options) -->
  252    !,
  253    [].
  254list_content([H|T], Options) -->
  255    !,
  256    { arg_options(Options, ArgOptions),
  257      (   T == []
  258      ->  Sep = [],
  259          Next = end
  260      ;   Options.depth + 1 >= Options.max_depth
  261      ->  Sep = [span(class('pl-punct'), '|')],
  262          Next = depth_limit
  263      ;   (var(T) ; \+ T = [_|_])
  264      ->  Sep = [span(class('pl-punct'), '|')],
  265          Next = tail
  266      ;   Sep = [span(class('pl-punct'), [',', ' '])],
  267          Next = list
  268      )
  269    },
  270    html(span(class('pl-list-el'),
  271              [ \any(H, Options) | Sep ])),
  272    list_next(Next, T, ArgOptions).
  273
  274list_next(end, _, _) --> !.
  275list_next(depth_limit, _, _) -->
  276    !,
  277    html(span(class('pl-ellipsis'), ...)).
  278list_next(tail, Value, Options) -->
  279    {   var(Value)
  280    ->  Class = 'pl-var-tail'
  281    ;   Class = 'pl-nonvar-tail'
  282    },
  283    html(span(class(Class), \any(Value, Options))).
  284list_next(list, Tail, Options) -->
  285    list_content(Tail, Options).
 is_op1(+Name, -Type, -Priority, -ArgPriority, +Options) is semidet
True if Name is an operator taking one argument of Type.
  291is_op1(Name, Type, Pri, ArgPri, Options) :-
  292    operator_module(Module, Options),
  293    current_op(Pri, OpType, Module:Name),
  294    argpri(OpType, Type, Pri, ArgPri),
  295    !.
  296
  297argpri(fx, prefix,  Pri0, Pri) :- Pri is Pri0 - 1.
  298argpri(fy, prefix,  Pri,  Pri).
  299argpri(xf, postfix, Pri0, Pri) :- Pri is Pri0 - 1.
  300argpri(yf, postfix, Pri,  Pri).
  301
  302% ! is_op2(+Name, -Type, -LeftPri, -Pri, -RightPri, +Options) is semidet.
  303%
  304%   True if Name is an operator taking two arguments of Type.
  305
  306is_op2(Name, Type, LeftPri, Pri, RightPri, Options) :-
  307    operator_module(Module, Options),
  308    current_op(Pri, Type, Module:Name),
  309    infix_argpri(Type, LeftPri, Pri, RightPri),
  310    !.
  311
  312infix_argpri(xfx, ArgPri, Pri, ArgPri) :- ArgPri is Pri - 1.
  313infix_argpri(yfx, Pri, Pri, ArgPri) :- ArgPri is Pri - 1.
  314infix_argpri(xfy, ArgPri, Pri, Pri) :- ArgPri is Pri - 1.
 operator_module(-Module, +Options) is det
Find the module for evaluating operators.
  320operator_module(Module, Options) :-
  321    Module = Options.get(module),
  322    !.
  323operator_module(TypeIn, _) :-
  324    '$module'(TypeIn, TypeIn).
 op1(+Type, +Pri, +Term, +ArgPri, +Options)// is det
  328op1(Type, Pri, Term, ArgPri, Options) -->
  329    { Pri > Options.priority },
  330    !,
  331    embrace(\op1(Type, Term, ArgPri, Options)).
  332op1(Type, _, Term, ArgPri, Options) -->
  333    op1(Type, Term, ArgPri, Options).
  334
  335op1(prefix, Term, ArgPri, Options) -->
  336    { Term =.. [Functor,Arg],
  337      arg_options(Options, DepthOptions),
  338      FuncOptions = DepthOptions.put(embrace, never),
  339      ArgOptions  = DepthOptions.put(priority, ArgPri),
  340      quote_atomic(Functor, S, FuncOptions),
  341      extra_classes(Term, Classes, Attrs, Options.put(op, prefix))
  342    },
  343    html(span([ class(['pl-compound', 'pl-op', 'pl-prefix-op'|Classes]),
  344                'data-arity'(1),
  345                'data-name'(Functor)
  346              | Attrs
  347              ],
  348              [ span(class('pl-functor'), S),
  349                \space(Functor, Arg, o, a, FuncOptions, ArgOptions),
  350                \op_arg(Arg, ArgOptions)
  351              ])).
  352op1(postfix, Term, ArgPri, Options) -->
  353    { Term =.. [Functor,Arg],
  354      arg_options(Options, DepthOptions),
  355      ArgOptions = DepthOptions.put(priority, ArgPri),
  356      FuncOptions = DepthOptions.put(embrace, never),
  357      quote_atomic(Functor, S, FuncOptions),
  358      extra_classes(Term, Classes, Attrs, Options.put(op, postfix))
  359    },
  360    html(span([ class(['pl-compound', 'pl-op', 'pl-postfix-op'|Classes]),
  361                'data-arity'(1),
  362                'data-name'(Functor)
  363              | Attrs
  364              ],
  365              [ \op_arg(Arg, ArgOptions),
  366                \space(Arg, Functor, a, o, ArgOptions, FuncOptions),
  367                span(class('pl-functor'), S)
  368              ])).
 op2(+Pri, +Term, +Type, +LeftPri, +RightPri, +Options)// is det
  372op2(Pri, Term, Type, LeftPri, RightPri, Options) -->
  373    { Pri > Options.priority },
  374    !,
  375    embrace(\op2(Term, Type, LeftPri, RightPri, Options)).
  376op2(_, Term, Type, LeftPri, RightPri, Options) -->
  377    op2(Term, Type, LeftPri, RightPri, Options).
  378
  379op2(Term, xfy, LeftPri, RightPri, Options) -->
  380    { functor(Term, Functor, 2),
  381      quote_op(Functor, S, Options),
  382      xfy_list(Term, Functor, List),
  383      arg_options(Options, DepthOptions),
  384      ArgOptions  = DepthOptions.put(#{priority:LeftPri, quoted_op:S}),
  385      extra_classes(Term, Classes, Attrs, Options.put(op, infix))
  386    },
  387    html(span([ class(['pl-op-seq', 'pl-adaptive'|Classes])
  388              | Attrs
  389              ],
  390              \op_seq(List, Functor, RightPri, ArgOptions))).
  391op2(Term, _Type, LeftPri, RightPri, Options) -->
  392    { Term =.. [Functor,Left,Right],
  393      arg_options(Options, DepthOptions),
  394      LeftOptions  = DepthOptions.put(priority, LeftPri),
  395      FuncOptions  = DepthOptions.put(embrace, never),
  396      RightOptions = DepthOptions.put(priority, RightPri),
  397      (   (   need_space(Left, Functor, a, o, LeftOptions, FuncOptions)
  398          ;   need_space(Functor, Right, o, a, FuncOptions, RightOptions)
  399          )
  400      ->  Space = ' '
  401      ;   Space = ''
  402      ),
  403      quote_op(Functor, S, Options),
  404      extra_classes(Term, Classes, Attrs, Options.put(op, infix))
  405    },
  406    html(span([ class(['pl-compound', 'pl-op', 'pl-infix-op'|Classes]),
  407                'data-arity'(2),
  408                'data-name'(Functor)
  409              | Attrs
  410              ],
  411              [ \op_arg(Left, LeftOptions),
  412                Space,
  413                span(class('pl-functor'), S),
  414                Space,
  415                \op_arg(Right, RightOptions)
  416              ])).
 op_arg(+Term, +Options)// is det
  420op_arg(Atom, Options) -->
  421    { atom(Atom),
  422      operator_module(Module, Options),
  423      current_op(_,_,Module:Atom)
  424    }, !,
  425    embrace(\any(Atom, Options.put(embrace, never))).
  426op_arg(Any, Options) -->
  427    any(Any, Options).
  428
  429op_seq([Last], _Functor, LastPri, Options) -->
  430    !,
  431    { LastOptions = Options.put(priority, LastPri)
  432    },
  433    html(span(class('pl-op-seq-el'), \op_arg(Last, LastOptions))).
  434op_seq([H|T], Functor, LastPri, Options) -->
  435    html(span(class('pl-op-seq-el'),
  436              [ \op_arg(H, Options),
  437                \left_space(H, Functor, Options),
  438                span(class('pl-infix'), Options.quoted_op)
  439              ])),
  440    op_seq(T, Functor, LastPri, Options).
  441
  442left_space(Left, Functor, Options) -->
  443    { need_space(Left, Functor, a, o, Options, Options.put(embrace, never))
  444    },
  445    !,
  446    html(' ').
  447left_space(_,_,_) -->
  448    [].
  449
  450xfy_list(Term, Name, List),
  451    compound(Term),
  452    compound_name_arguments(Term, _, [A,B]) =>
  453    List = [A|T],
  454    xfy_list(B, Name, T).
  455xfy_list(Term, _, List) =>
  456    List = [Term].
 embrace(+HTML)//
Place parenthesis around HTML with a DOM that allows to easily justify the height of the parenthesis.
  463embrace(HTML) -->
  464    html(span(class('pl-embrace'),
  465              [ span(class('pl-parenthesis'), '('),
  466                span(class('pl-embraced'),\html(HTML)),
  467                span(class('pl-parenthesis'), ')')
  468              ])).
 space(@T1, @T2, +C1, +C2, +Options)//
Emit a space if omitting a space between T1 and T2 would cause the two terms to join.
  475space(T1, T2, C1, C2, LeftOptions, RightOptions) -->
  476    { need_space(T1, T2, C1, C2, LeftOptions, RightOptions) },
  477    html(' ').
  478space(_, _, _, _, _, _) -->
  479    [].
  480
  481need_space(T1, T2, _, _, _, _) :-
  482    (   is_solo(T1)
  483    ;   is_solo(T2)
  484    ),
  485    !,
  486    fail.
  487need_space(T1, T2, C1, C2, LeftOptions, RightOptions) :-
  488    end_code_type(T1, C1, TypeR, LeftOptions.put(side, right)),
  489    end_code_type(T2, C2, TypeL, RightOptions.put(side, left)),
  490    \+ no_space(TypeR, TypeL).
  491
  492no_space(punct, _).
  493no_space(_, punct).
  494no_space(quote(R), quote(L)) :-
  495    !,
  496    R \== L.
  497no_space(alnum, symbol).
  498no_space(symbol, alnum).
 end_code_type(+Term, +Class, -Code, Options)
True when code is the first/last character code that is emitted by printing Term using Options.
  505end_code_type(Atom, a, Type, Options) :-
  506    atom(Atom),
  507    operator_module(Module, Options),
  508    current_op(_,_,Module:Atom),
  509    !,
  510    Type = punct.
  511end_code_type(Atom, _, Type, Options) :-
  512    end_code_type(Atom, Type, Options).
  513
  514end_code_type(_, Type, Options) :-
  515    Options.depth >= Options.max_depth,
  516    !,
  517    Type = symbol.
  518end_code_type(Term, Type, Options) :-
  519    primitive(Term, _),
  520    !,
  521    quote_atomic(Term, S, Options),
  522    end_type(S, Type, Options).
  523end_code_type(Dict, Type, Options) :-
  524    is_dict(Dict, Tag),
  525    !,
  526    (   Options.side == left
  527    ->  end_code_type(Tag, Type, Options)
  528    ;   Type = punct
  529    ).
  530end_code_type('$VAR'(Var), Type, Options) :-
  531    Options.get(numbervars) == true,
  532    !,
  533    format(string(S), '~W', ['$VAR'(Var), [numbervars(true)]]),
  534    end_type(S, Type, Options).
  535end_code_type(List, Type, _) :-
  536    (   List == []
  537    ;   List = [_|_]
  538    ),
  539    !,
  540    Type = punct.
  541end_code_type(OpTerm, Type, Options) :-
  542    compound_name_arity(OpTerm, Name, 1),
  543    is_op1(Name, OpType, Pri, ArgPri, Options),
  544    \+ Options.get(ignore_ops) == true,
  545    !,
  546    (   Pri > Options.priority
  547    ->  Type = punct
  548    ;   (   OpType == prefix, Options.side == left
  549        ->  end_code_type(Name, Type, Options)
  550        ;   OpType == postfix, Options.side == right
  551        ->  end_code_type(Name, Type, Options)
  552        ;   arg(1, OpTerm, Arg),
  553            arg_options(Options, ArgOptions),
  554            op_end_code_type(Arg, Type, ArgOptions.put(priority, ArgPri))
  555        )
  556    ).
  557end_code_type(OpTerm, Type, Options) :-
  558    compound_name_arity(OpTerm, Name, 2),
  559    is_op2(Name, _Type, LeftPri, Pri, RightPri, Options),
  560    \+ Options.get(ignore_ops) == true,
  561    !,
  562    (   Pri > Options.priority
  563    ->  Type = punct
  564    ;   Options.side == left
  565    ->  arg(1, OpTerm, Arg),
  566        arg_options(Options, ArgOptions),
  567        op_end_code_type(Arg, Type, ArgOptions.put(priority, LeftPri))
  568    ;   Options.side == right
  569    ->  arg(2, OpTerm, Arg),
  570        arg_options(Options, ArgOptions),
  571        op_end_code_type(Arg, Type, ArgOptions.put(priority, RightPri))
  572    ).
  573end_code_type(Compound, Type, Options) :-
  574    compound_name_arity(Compound, Name, _),
  575    end_code_type(Name, Type, Options).
  576
  577op_end_code_type(Atom, Type, Options) :-
  578    end_code_type(Atom, a, Type, Options).
  579
  580end_type(S, Type, Options) :-
  581    number(S),
  582    !,
  583    (   (S < 0 ; S == -0.0),
  584        Options.side == left
  585    ->  Type = symbol
  586    ;   Type = alnum
  587    ).
  588end_type(S, Type, Options) :-
  589    Options.side == left,
  590    !,
  591    sub_string(S, 0, 1, _, Start),
  592    syntax_type(Start, Type).
  593end_type(S, Type, _) :-
  594    sub_string(S, _, 1, 0, End),
  595    syntax_type(End, Type).
  596
  597syntax_type("\"", quote(double)) :- !.
  598syntax_type("\'", quote(single)) :- !.
  599syntax_type("\`", quote(back))   :- !.
  600syntax_type(S, Type) :-
  601    string_code(1, S, C),
  602    (   code_type(C, prolog_identifier_continue)
  603    ->  Type = alnum
  604    ;   code_type(C, prolog_symbol)
  605    ->  Type = symbol
  606    ;   code_type(C, space)
  607    ->  Type = layout
  608    ;   Type = punct
  609    ).
 dict(+Term, +Options)//
  614dict(Term, Options) -->
  615    { dict_pairs(Term, Tag, Pairs),
  616      quote_atomic(Tag, S, Options.put(embrace, never)),
  617      arg_options(Options, ArgOptions)
  618    },
  619    html(span(class(['pl-dict', 'pl-adaptive']),
  620              [ span(class(['pl-tag', 'pl-trigger']), S),
  621                span(class(['pl-dict-open', 'pl-punct']), '{'),
  622                span(class('pl-dict-body'),
  623                     [ span(class('pl-dict-kvs'),
  624                            \dict_kvs(Pairs, ArgOptions)),
  625                       span(class(['pl-dict-close', 'pl-punct']), '}')
  626                     ])
  627              ])).
  628
  629dict_kvs([], _) --> [].
  630dict_kvs(_, Options) -->
  631    { Options.depth >= Options.max_depth },
  632    !,
  633    html(span(class('pl-ellipsis'), ...)).
  634dict_kvs(KVs, Options) -->
  635    dict_kvs2(KVs, Options).
  636
  637dict_kvs2([], _) -->
  638    [].
  639dict_kvs2([K-V|T], Options) -->
  640    { quote_atomic(K, S, Options),
  641      end_code_type(V, VType, Options.put(side, left)),
  642      (   VType == symbol
  643      ->  VSpace = ' '
  644      ;   VSpace = ''
  645      ),
  646      arg_options(Options, ArgOptions),
  647      (   T == []
  648      ->  Sep = []
  649      ;   Sep = [\punct(','), ' ']
  650      )
  651    },
  652    html(span(class('pl-dict-kv'),
  653              [ span(class('pl-key'), [S, \punct(:)]),
  654                VSpace,
  655                span(class('pl-dict-value'),
  656                     [ \any(V, ArgOptions)
  657                     | Sep
  658                     ])
  659              ])),
  660    dict_kvs2(T, Options).
  661
  662quote_atomic(Float, String, Options) :-
  663    float(Float),
  664    Format = Options.get(float_format),
  665    !,
  666    format(string(String), Format, [Float]).
  667quote_atomic(Plain, String, Options) :-
  668    atomic(Plain),
  669    Format = Options.get(format),
  670    !,
  671    format(string(String), Format, [Plain]).
  672quote_atomic(Plain, String, Options) :-
  673    rational(Plain),
  674    \+ integer(Plain),
  675    !,
  676    operator_module(Module, Options),
  677    format(string(String), '~W', [Plain, [module(Module)]]).
  678quote_atomic(Plain, Plain, _) :-
  679    number(Plain),
  680    !.
  681quote_atomic(Plain, String, Options) :-
  682    Options.get(quoted) == true,
  683    !,
  684    (   Options.get(embrace) == never
  685    ->  format(string(String), '~q', [Plain])
  686    ;   format(string(String), '~W', [Plain, Options])
  687    ).
  688quote_atomic(Var, String, Options) :-
  689    var(Var),
  690    !,
  691    format(string(String), '~W', [Var, Options]).
  692quote_atomic(Plain, Plain, _).
  693
  694quote_op(Op, S, _Options) :-
  695    is_solo(Op),
  696    !,
  697    S = Op.
  698quote_op(Op, S, Options) :-
  699    quote_atomic(Op, S, Options.put(embrace,never)).
  700
  701is_solo(Var) :-
  702    var(Var), !, fail.
  703is_solo(',').
  704is_solo(';').
  705is_solo('!').
 primitive(+Term, -Class) is semidet
True if Term is a primitive term, rendered using the CSS class Class.
  712primitive(Term, Type) :- var(Term),      !, Type = 'pl-avar'.
  713primitive(Term, Type) :- atom(Term),     !, Type = 'pl-atom'.
  714primitive(Term, Type) :- string(Term),   !, Type = 'pl-string'.
  715primitive(Term, Type) :- integer(Term),  !, Type = 'pl-int'.
  716primitive(Term, Type) :- rational(Term), !, Type = 'pl-rational'.
  717primitive(Term, Type) :- float(Term),    !, Type = 'pl-float'.
 primitive_class(+Class0, +Value, -String, -Class) is det
Fixup the CSS class for lexical variations. Used to find quoted atoms.
  724primitive_class('pl-atom', Atom, String, Class) :-
  725    \+ atom_string(Atom, String),
  726    !,
  727    Class = 'pl-quoted-atom'.
  728primitive_class(Class, _, _, Class).
 finalize_term(+Term, +Dict)// is det
Handle the full_stop(Bool) and nl(Bool) options.
  734finalize_term(Term, Dict) -->
  735    (   { true == Dict.get(full_stop) }
  736    ->  space(Term, '.', o, o, Dict, Dict),
  737        (   { true == Dict.get(nl) }
  738        ->  html(['.', br([])])
  739        ;   html('. ')
  740        )
  741    ;   (   { true == Dict.get(nl) }
  742        ->  html(br([]))
  743        ;   []
  744        )
  745    ).
  746
  747
  748                 /*******************************
  749                 *             HOOKS            *
  750                 *******************************/
 blob_rendering(+BlobType, +Blob, +WriteOptions)// is semidet
Hook to render blob atoms as HTML. This hook is called whenever a blob atom is encountered while rendering a compound term as HTML. The blob type is provided to allow efficient indexing without having to examine the blob. If this predicate fails, the blob is rendered as an HTML SPAN with class 'pl-blob' containing BlobType as text.