View source with formatted 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)  2018-2020, CWI, Amsterdam
    7    All rights reserved.
    8
    9    Redistribution and use in source and binary forms, with or without
   10    modification, are permitted provided that the following conditions
   11    are met:
   12
   13    1. Redistributions of source code must retain the above copyright
   14       notice, this list of conditions and the following disclaimer.
   15
   16    2. Redistributions in binary form must reproduce the above copyright
   17       notice, this list of conditions and the following disclaimer in
   18       the documentation and/or other materials provided with the
   19       distribution.
   20
   21    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   22    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   23    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   24    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   25    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   26    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   27    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   28    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   29    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   30    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   31    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   32    POSSIBILITY OF SUCH DAMAGE.
   33*/
   34
   35
   36:- module(html_text,
   37          [ html_text/1,                        % +FileName
   38            html_text/2                         % +FileName, Options
   39          ]).   40:- use_module(library(debug),[debug/3]).   41:- autoload(library(ansi_term),[ansi_format/3]).   42:- autoload(library(apply),[foldl/4,maplist/3,maplist/2]).   43:- autoload(library(error),[must_be/2]).   44:- autoload(library(lists),
   45	    [ append/3, list_to_set/2, reverse/2, delete/3, sum_list/2,
   46	      nth1/3, max_list/2
   47	    ]).   48:- autoload(library(option),[select_option/4,merge_options/3,option/3]).   49:- autoload(library(sgml),[xml_is_dom/1,load_html/3]).   50:- autoload(library(lynx/format),[format_paragraph/2,trim_line/2]).   51:- autoload(library(lynx/html_style),
   52	    [ element_css/3, css_block_options/5, css_inline_options/3,
   53	      attrs_classes/2, style_css_attrs/2
   54	    ]).   55
   56:- predicate_options(html_text/2, 2,
   57                     [ margin_left(integer),
   58                       margin_right(integer),
   59                       width(integer),
   60                       text_align(oneof([justify, left]))
   61                     ]).   62
   63/** <module> Render HTML as plain text
   64
   65This module renders HTML markup as plain text, just like the open _lynx_
   66program does. It is (as yet),  limited   to  and spacialized for dealing
   67with the SWI-Prolog documentation. This library   first  of all supports
   68help/1.
   69*/
   70
   71%!  html_text(+Input) is det.
   72%!  html_text(+Input, +Options) is det.
   73%
   74%   Render HTML from Input to `current_output`.  Input is either an HTML
   75%   DOM or a valid input for load_html/3. Options defined are:
   76%
   77%     - margin_left(+N)
   78%     - margin_right(+N)
   79%       Initial margins.
   80%     - width(+N)
   81%       Total preceived line width.
   82%     - text_align(+Align)
   83%       One of `justify` or `left`.  Default is `justify`.
   84
   85html_text(Input) :-
   86    html_text(Input, []).
   87
   88html_text(Input, Options) :-
   89    (   xml_is_dom(Input)
   90    ->  DOM = Input
   91    ;   load_html(Input, DOM, Options)
   92    ),
   93    default_state(State0),
   94    state_options(Options, State0, State),
   95    init_nl,
   96    format_dom(DOM, State).
   97
   98state_options([], State, State).
   99state_options([H|T], State0, State) :-
  100    H =.. [Key,Value],
  101    (   fmt_option(Key, Type, _Default)
  102    ->  must_be(Type, Value),
  103        State1 = State0.put(Key,Value)
  104    ;   State1 = State0
  105    ),
  106    state_options(T, State1, State).
  107
  108fmt_option(margin_left,  integer, 0).
  109fmt_option(margin_right, integer, 0).
  110fmt_option(text_align,   oneof([justify, left]), justify).
  111fmt_option(width,        between(10,1000), 72).
  112
  113default_state(State) :-
  114    findall(Key-Value, fmt_option(Key, _, Value), Pairs),
  115    dict_pairs(Dict, _, Pairs),
  116    State = Dict.put(_{ style:[], list:[]}).
  117
  118%!  format_dom(+DOM, +State) is det.
  119%
  120%   Format the given HTML DOM to `current_output` according to State.
  121
  122format_dom([], _) :-
  123    !.
  124format_dom([H|T], State) :-
  125    format_dom(H, State),
  126    !,
  127    format_dom(T, State).
  128format_dom(Content, State) :-
  129    Content = [H0|_],
  130    \+ is_block_element(H0),
  131    !,
  132    (   append(Inline, [H|T], Content),
  133        is_block_element(H)
  134    ->  true
  135    ;   Inline = Content
  136    ),
  137    format_dom(element(p, [], Inline), State),
  138    format_dom([H|T], State).
  139format_dom(element(html, _, Content), State) :-
  140    !,
  141    format_dom(Content, State).
  142format_dom(element(head, _, _), _) :-
  143    !.
  144format_dom(element(body, _, Content), State) :-
  145    !,
  146    format_dom(Content, State).
  147format_dom(element(E, Attrs, Content), State) :-
  148    !,
  149    (   format_element(E, Attrs, Content, State)
  150    ->  true
  151    ;   debug(format(html), 'Skipped block element ~q', [E])
  152    ).
  153
  154format_element(pre, Attrs, [Content], State) :-
  155    !,
  156    block_element(pre, Attrs, Top-Bottom, BlockAttrs, Style),
  157    update_style(Style, State, State1),
  158    ask_nl(Top),
  159    emit_code(Content, BlockAttrs, State1),
  160    ask_nl(Bottom).
  161format_element(table, Attrs, Content, State) :-
  162    !,
  163    block_element(table, Attrs, Top-Bottom, BlockAttrs, Style),
  164    update_style(Style, State, State1),
  165    state_par_properties(State1, BlockAttrs, BlockOptions),
  166    ask_nl(Top),
  167    emit_nl,
  168    format_table(Content, Attrs, BlockOptions, State1),
  169    ask_nl(Bottom).
  170format_element(hr, Attrs, _, State) :-
  171    !,
  172    block_element(hr, Attrs, Top-Bottom, BlockAttrs, Style),
  173    update_style(Style, State, State1),
  174    state_par_properties(State1, BlockAttrs, BlockOptions),
  175    ask_nl(Top),
  176    emit_nl,
  177    emit_hr(Attrs, BlockOptions, State1),
  178    ask_nl(Bottom).
  179format_element(Elem, Attrs, Content, State) :-
  180    block_element(Elem, Attrs, Top-Bottom, BlockAttrs, Style),
  181    !,
  182    update_style(Style, State, State1),
  183    block_words(Content, SubBlocks, Words, State1),
  184    (   Words == []
  185    ->  true
  186    ;   ask_nl(Top),
  187        emit_block(Words, BlockAttrs, State1),
  188        ask_nl(Bottom)
  189    ),
  190    (   SubBlocks \== []
  191    ->  update_state_par_properties(BlockAttrs, State1, State2),
  192        format_dom(SubBlocks, State2)
  193    ;   true
  194    ).
  195format_element(Elem, Attrs, Content, State) :-
  196    list_element(Elem, Attrs, Top-Bottom, State, State1),
  197    !,
  198    open_list(Elem, State1, State2),
  199    ask_nl(Top),
  200    format_list(Content, Elem, 1, State2),
  201    ask_nl(Bottom).
  202format_element(Elem, Attrs, Content, State) :-
  203    format_list_element(element(Elem, Attrs, Content), none, 0, State).
  204
  205%!  block_element(+El, +Attrs, -Margin, -ParOPtions, -Style)
  206%
  207%   Describe a block element
  208
  209block_element(El, Attrs, Margins, ParOptions, Style) :-
  210    block_element(El, Margins0, ParOptions0, Style0),
  211    (   nonvar(Attrs),
  212        element_css(El, Attrs, CSS)
  213    ->  css_block_options(CSS, Margins0, Margins, ParOptions, Style1),
  214        append(Style1, Style0, Style2),
  215        list_to_set(Style2, Style)
  216    ;   Margins = Margins0,
  217        ParOptions = ParOptions0,
  218        Style = Style0
  219    ).
  220
  221block_element(p,          1-2, [],                                []).
  222block_element(div,        1-1, [],                                []).
  223block_element(hr,         1-1, [],                                []).
  224block_element(h1,         2-2, [],                                [bold]).
  225block_element(h2,         2-2, [],                                [bold]).
  226block_element(h3,         2-2, [],                                [bold]).
  227block_element(h4,         2-2, [],                                [bold]).
  228block_element(pre,        2-2, [],                                []).
  229block_element(blockquote, 2-2, [margin_left(4), margin_right(4)], []).
  230block_element(table,      2-2, [],                                []).
  231
  232list_element(ul, _, Margins, State0, State) :-
  233    margins(4, 4, State0, State),
  234    list_level_margins(State, Margins).
  235list_element(ol, _, Margins, State0, State) :-
  236    margins(4, 4, State0, State),
  237    list_level_margins(State, Margins).
  238list_element(dl, _, 2-2, State, State).
  239
  240list_element(ul).
  241list_element(ol).
  242list_element(dl).
  243
  244list_level_margins(State, 2-2) :-
  245    nonvar(State),
  246    block_list_context(State.get(list)),
  247    !.
  248list_level_margins(_, 0-0).
  249
  250%!  block_list_context(+OpenLists) is semidet.
  251%
  252%   True when a list opened while OpenLists   are open acts as a normal
  253%   block element and thus  needs   surrounding  blank  lines. This is the
  254%   case if the list is not nested in an item of an itemized list. As the
  255%   body of a `dd` element is a  normal   block  context, a list inside a
  256%   definition list is a block element as well.
  257
  258block_list_context([]).
  259block_list_context([dl|_]).
  260
  261format_list([], _, _, _).
  262format_list([H|T], Type, Nth, State) :-
  263    format_list_element(H, Type, Nth, State),
  264    (   T == []
  265    ->  true
  266    ;   Nth1 is Nth + 1,
  267        format_list(T, Type, Nth1, State)
  268    ).
  269
  270format_list_element(element(LE, Attrs, Content), Type, Nth, State) :-
  271    setup_list_element(LE, Attrs, Type, Nth, ListParProps, State, State1),
  272    block_words(Content, Blocks, Words, State1),
  273    emit_block(Words, ListParProps, State1),
  274    (   Blocks \== []
  275    ->  ask_nl(2),                              % empty line before next par
  276        update_state_par_properties(ListParProps, State1, State2),
  277        format_dom(Blocks, State2)
  278    ;   true
  279    ).
  280
  281setup_list_element(li, _Attrs, _Type, Nth, ListParProps, State, State) :-
  282    list_par_properties(State.list, Nth, ListParProps).
  283setup_list_element(dt, _Attrs, _Type, _Nth, [], State, State2) :-
  284    margins(0, 0, State, State1),
  285    update_style([bold], State1, State2).
  286setup_list_element(dd, _Attrs, _Type, _Nth, [], State, State1) :-
  287    margins(4, 0, State, State1).
  288
  289list_item_element(li).
  290list_item_element(dt).
  291list_item_element(dd).
  292
  293list_par_properties([ul|_More], _, [bullet('\u2022')]).
  294list_par_properties([ol|_More], N, [bullet(N)]).
  295
  296
  297%!  block_words(+Content, -RestContent, -Words, +State)
  298%
  299%   Turn Content into a list of words with attributes and spaces.
  300
  301block_words(Content, RC, Words, State) :-
  302    phrase(bwords(Content, RC, State), Words0),
  303    join_whitespace(Words0, Words1),
  304    trim_line(Words1, Words).
  305
  306bwords([], [], _) -->
  307    !.
  308bwords([H|T], Rest, _State) -->
  309    { var(Rest),
  310      is_block_element(H),
  311      !,
  312      Rest = [H|T]
  313    }.
  314bwords([H|T], Rest, State) -->
  315    !,
  316    bwordsel(H, State),
  317    bwords(T, Rest, State).
  318
  319is_block_element(element(E,_,_)) :-
  320    (   block_element(E, _, _, _)
  321    ;   list_element(E)
  322    ;   list_item_element(E)
  323    ),
  324    debug(format(html), 'Found block ~q', [E]),
  325    !.
  326
  327bwordsel(element(Elem, Attrs, Content), State) -->
  328    { styled_inline(Elem, Attrs, Margins, Style),
  329      !,
  330      update_style(Style, State, State1)
  331    },
  332    left_margin(Margins),
  333    bwords(Content, [], State1),
  334    right_margin(Margins).
  335bwordsel(element(br, _, _), _State) -->
  336    [br([])].
  337bwordsel(CDATA, State) -->
  338    { atomic(CDATA),
  339      !,
  340      split_string(CDATA, " \n\t\r", "", Words)
  341    },
  342    words(Words, State).
  343bwordsel(element(Elem, _Attrs, _Content), _State) -->
  344    { debug(format(html), 'Skipped inline element ~q', [Elem]) }.
  345
  346left_margin(0-_) --> !.
  347left_margin(N-_) --> [b(N,_)].
  348
  349right_margin(_-0) --> !.
  350right_margin(_-N) --> [b(N,_)].
  351
  352styled_inline(El, Attrs, Margins, Style) :-
  353    styled_inline(El, Style0),
  354    (   nonvar(Attrs),
  355        element_css(El, Attrs, CSS)
  356    ->  css_inline_options(CSS, Margins, Style1),
  357        append(Style1, Style0, Style2),
  358        list_to_set(Style2, Style)
  359    ;   Style = Style0
  360    ).
  361
  362styled_inline(b,      [bold]).
  363styled_inline(strong, [bold]).
  364styled_inline(em,     [bold]).
  365styled_inline(span,   []).
  366styled_inline(i,      [underline]).
  367styled_inline(a,      [underline]).
  368styled_inline(var,    []).
  369styled_inline(code,   []).
  370
  371%!  words(+Tokens, +State)//
  372%
  373%   Generate a list of w(Word,Len,Attrs) and   b(Len,_)  terms for words
  374%   and (breakable) white space.
  375
  376words([], _) --> [].
  377words([""|T0], State) -->
  378    !,
  379    { skip_leading_spaces(T0, T) },
  380    space,
  381    words(T, State).
  382words([H|T], State) -->
  383    word(H, State),
  384    (   {T==[]}
  385    ->  []
  386    ;   { skip_leading_spaces(T, T1) },
  387        space,
  388        words(T1, State)
  389    ).
  390
  391skip_leading_spaces([""|T0], T) :-
  392    !,
  393    skip_leading_spaces(T0, T).
  394skip_leading_spaces(L, L).
  395
  396word(W, State) -->
  397    { string_length(W, Len),
  398      (   Style = State.get(style)
  399      ->  true
  400      ;   Style = []
  401      )
  402    },
  403    [w(W, Len, Style)].
  404
  405space -->
  406    [b(1,_)].
  407
  408%!  join_whitespace(Elements, Joined)
  409%
  410%   Join consequtive space elements into a single white space element.
  411
  412join_whitespace([], []).
  413join_whitespace([H0|T0], [H|T]) :-
  414    join_whitespace(H0, H, T0, T1),
  415    !,
  416    join_whitespace(T1, T).
  417join_whitespace([H|T0], [H|T]) :-
  418    join_whitespace(T0, T).
  419
  420join_whitespace(b(Len0,_), b(Len,_), T0, T) :-
  421    take_whitespace(T0, T, Len0, Len).
  422
  423take_whitespace([b(Len1,_)|T0], T, Len0, Len) :-
  424    !,
  425    Len2 is max(Len1,Len0),
  426    take_whitespace(T0, T, Len2, Len).
  427take_whitespace(L, L, Len, Len).
  428
  429
  430		 /*******************************
  431		 *       STATE MANAGEMENT	*
  432		 *******************************/
  433
  434%!  update_style(+Style:list, +State0, -State)
  435%
  436%   Add Style to the current state.
  437
  438update_style([], State, State) :-
  439    !.
  440update_style(Extra, State0, State) :-
  441    (   get_dict(style, State0, Style0, State, Style)
  442    ->  add_style(Extra, Style0, Style)
  443    ;   add_style(Extra, [], Style),
  444        put_dict(style, State0, Style, State)
  445    ).
  446
  447add_style(Extra, Style0, Style) :-
  448    reverse(Extra, RevExtra),
  449    foldl(add1_style, RevExtra, Style0, Style).
  450
  451%!  add1_style(+New, +Style0, -Style) is det.
  452%
  453%   Modify the current text style.
  454
  455add1_style(New, Style0, Style) :-
  456    (   style_overrides(New, Add, Overrides)
  457    ->  delete_all(Overrides, Style0, Style1),
  458        append(Add, Style1, Style)
  459    ;   Style = [New|Style0]
  460    ).
  461
  462delete_all([], List, List).
  463delete_all([H|T], List0, List) :-
  464    delete(List0, H, List1),
  465    delete_all(T, List1, List).
  466
  467style_overrides(normal,           [],      [bold]).
  468style_overrides(fg(C),            [fg(C)], [fg(_), hfg(_)]).
  469style_overrides(bg(C),            [bg(C)], [bg(_), hbg(_)]).
  470style_overrides(underline(false), [],      [underline]).
  471
  472margins(Left, Right, State0, State) :-
  473    _{ margin_left:ML0, margin_right:MR0 } >:< State0,
  474    ML is ML0 + Left,
  475    MR is MR0 + Right,
  476    State = State0.put(_{margin_left:ML, margin_right:MR}).
  477
  478open_list(Type, State0, State) :-
  479    get_dict(list, State0, Lists, State, [Type|Lists]).
  480
  481update_state_par_properties([], State, State).
  482update_state_par_properties([H|T], State0, State) :-
  483    H =.. [ Key, Value ],
  484    State1 = State0.put(Key,Value),
  485    update_state_par_properties(T, State1, State).
  486
  487%!  state_par_properties(+State, -ParProps)
  488%
  489%   Get the paragraph shape properties from  State. Eventually these two
  490%   should be merged!
  491
  492state_par_properties(State, Props) :-
  493    Props0 = [ margin_left(LM),
  494               margin_right(RM),
  495               text_align(TA),
  496               width(W),
  497               pad(Pad)
  498             ],
  499    _{margin_left:LM, margin_right:RM, text_align:TA, width:W,
  500      pad:Pad} >:< State,
  501    filled_par_props(Props0, Props).
  502
  503filled_par_props([], []).
  504filled_par_props([H|T0], [H|T]) :-
  505    arg(1, H, A),
  506    nonvar(A),
  507    !,
  508    filled_par_props(T0, T).
  509filled_par_props([_|T0], T) :-
  510    filled_par_props(T0, T).
  511
  512
  513state_par_properties(State, Options, BlockOptions) :-
  514    state_par_properties(State, Options0),
  515    foldl(merge_par_option, Options, Options0, BlockOptions).
  516
  517merge_par_option(margin_left(ML0), Options0, [margin_left(ML)|Options1]) :-
  518    !,
  519    select_option(margin_left(ML1), Options0, Options1, 0),
  520    ML is ML0+ML1.
  521merge_par_option(margin_right(MR0), Options0, [margin_right(MR)|Options1]) :-
  522    !,
  523    select_option(margin_right(MR1), Options0, Options1, 0),
  524    MR is MR0+MR1.
  525merge_par_option(Opt, Options0, Options) :-
  526    merge_options([Opt], Options0, Options).
  527
  528%!  emit_block(+Words, +Options, +State) is det.
  529%
  530%   Format a block given Words inline elements, Options and State. Calls
  531%   format_paragraph/2 after finalizing the paragraph   shape  and using
  532%   the newline logic.
  533
  534emit_block([], _, _) :-
  535    !.
  536emit_block(Words, Options, State) :-
  537    state_par_properties(State, Options, BlockOptions),
  538    use_current_position(BlockOptions, BlockOptions1),
  539    ask_nl(1),
  540    emit_nl,
  541    format_paragraph(Words, BlockOptions1),
  542    ask_nl(1).
  543
  544use_current_position(Options0, Options) :-
  545    nb_current(nl_pending, start),
  546    line_position(current_output, Pos),
  547    Pos > 0,
  548    !,
  549    Hang is -Pos,
  550    Options = [hang(Hang)|Options0].
  551use_current_position(Options, Options).
  552
  553
  554%!  init_nl is det.
  555%!  init_nl(-State) is det.
  556%!  exit_nl(+State) is det.
  557%
  558%   Initialize/finalize the newline logic.
  559
  560init_nl :-
  561    nb_setval(nl_pending, start).
  562
  563init_nl(Old) :-
  564    (   nb_current(nl_pending, Old)
  565    ->  true
  566    ;   Old = []
  567    ),
  568    nb_setval(nl_pending, start).
  569exit_nl(Old) :-
  570    nb_setval(nl_pending, Old).
  571
  572ask_nl(N) :-
  573    (   nb_current(nl_pending, N0)
  574    ->  (   N0 == start
  575        ->  true
  576        ;   integer(N0)
  577        ->  N1 is max(N0, N),
  578            nb_setval(nl_pending, N1)
  579        ;   nb_setval(nl_pending, N)
  580        )
  581    ;   nb_setval(nl_pending, N)
  582    ).
  583
  584emit_nl :-
  585    (   nb_current(nl_pending, N),
  586        integer(N)
  587    ->  forall(between(1,N,_), nl)
  588    ;   true
  589    ),
  590    nb_setval(nl_pending, 0).
  591
  592
  593		 /*******************************
  594		 *             PRE		*
  595		 *******************************/
  596
  597%!  emit_code(+Content, +BlockAttrs, +State)
  598
  599emit_code(Content, BlockAttrs, State) :-
  600    Style = State.style,
  601    split_string(Content, "\n", "", Lines),
  602    option(margin_left(LM0), BlockAttrs, 4),
  603    LM is LM0+State.margin_left,
  604    ask_nl(1),
  605    emit_nl,
  606    emit_code_lines(Lines, 1, LM, Style),
  607    ask_nl(1).
  608
  609emit_code_lines([], _, _, _).
  610emit_code_lines([H|T], LineNo, LM, Style) :-
  611    emit_code_line(H, LineNo, LM, Style),
  612    LineNo1 is LineNo + 1,
  613    emit_code_lines(T, LineNo1, LM, Style).
  614
  615emit_code_line(Line, _LineNo, LM, Style) :-
  616    emit_nl,
  617    emit_indent(LM),
  618    (   Style == []
  619    ->  write(Line)
  620    ;   ansi_format(Style, '~s', [Line])
  621    ),
  622    ask_nl(1).
  623
  624emit_indent(N) :-
  625    forall(between(1, N, _),
  626           put_char(' ')).
  627
  628
  629		 /*******************************
  630		 *            TABLES		*
  631		 *******************************/
  632
  633%!  format_table(+Content, +Attrs, +BlockAttrs, +State) is det.
  634
  635format_table(Content, Attrs, BlockAttrs, State) :-
  636    tty_state(TTY),
  637    option(margin_left(ML), BlockAttrs, 0),
  638    option(margin_right(MR), BlockAttrs, 0),
  639    MaxTableWidth is State.width - ML - MR,
  640    table_cell_state(Attrs, State, CellState0),
  641    CellState = CellState0.put(tty, TTY),
  642    phrase(rows(Content), Rows),
  643    columns(Rows, Columns),
  644    maplist(auto_column_width(CellState), Columns, Widths),
  645    column_widths(Widths, MaxTableWidth, ColWidths),
  646    maplist(format_row(ColWidths, CellState, ML), Rows).
  647
  648tty_state(TTY) :-
  649    stream_property(current_output, tty(true)),
  650    !,
  651    TTY = true.
  652tty_state(false).
  653
  654
  655%!  column_widths(+AutoWidths, +MaxTableWidth, -Widths) is det.
  656%
  657%   Establish the widths of the columns. AutoWidths  is a list of widths
  658%   for each of the columns if no folding is applied.
  659
  660column_widths(Widths, MaxTableWidth, Widths) :-
  661    sum_list(Widths, AutoWidth),
  662    AutoWidth =< MaxTableWidth,
  663    !.
  664column_widths(AutoWidths, MaxTableWidth, Widths) :-
  665    sort(0, >=, AutoWidths, Sorted),
  666    append(Wrapped, Keep, Sorted),
  667    sum_list(Keep, KeepWidth),
  668    KeepWidth < MaxTableWidth/2,
  669    length(Wrapped, NWrapped),
  670    WideWidth is round((MaxTableWidth-KeepWidth)/NWrapped),
  671    (   [KeepW|_] = Keep
  672    ->  true
  673    ;   KeepW = 0
  674    ),
  675    !,
  676    maplist(truncate_column(KeepW,WideWidth), AutoWidths, Widths).
  677
  678truncate_column(Keep, WideWidth, AutoWidth, Width) :-
  679    (   AutoWidth =< Keep
  680    ->  Width = AutoWidth
  681    ;   Width = WideWidth
  682    ).
  683
  684table_cell_state(Attrs, State, CellState) :-
  685    (   element_css(table, Attrs, CSS)
  686    ->  true
  687    ;   CSS = []
  688    ),
  689    option(padding_left(PL), CSS, 1),
  690    option(padding_right(PR), CSS, 1),
  691    CellState = State.put(_{margin_left:PL, margin_right:PR}).
  692
  693
  694%!  rows(+Content, -Rows) is det.
  695
  696rows([]) --> [].
  697rows([H|T]) --> rows(H), rows(T).
  698rows([element(tbody,_,Content)|T]) --> rows(Content), rows(T).
  699rows([element(tr,Attrs,Columns)|T]) --> [row(Columns, Attrs)], rows(T).
  700
  701%!  columns(+Rows, -Columns) is det.
  702%
  703%   Transpose the table, filling missing  columns   with  an  empty `td`
  704%   element as needed.
  705
  706columns(Rows, Columns) :-
  707    columns(Rows, 1, Columns).
  708
  709columns(Rows, I, Columns) :-
  710    maplist(row_column(I, Found), Rows, H),
  711    (   Found == true
  712    ->  Columns = [H|T],
  713        I2 is I + 1,
  714        columns(Rows, I2, T)
  715    ;   Columns = []
  716    ).
  717
  718row_column(I, Found, row(Columns, _Attrs), Cell) :-
  719    (   nth1(I, Columns, Cell)
  720    ->  Found = true
  721    ;   Cell = element(td,[],[])
  722    ).
  723
  724auto_column_width(State, Col, Width) :-
  725    maplist(auto_cell_width(State), Col, Widths),
  726    max_list(Widths, Width).
  727
  728auto_cell_width(State, Cell, Width) :-
  729    cell_colspan(Cell, 1),
  730    !,
  731    format_cell_to_string(Cell, 1_000, State, String),
  732    write_size(String, Width0, _Height, []),
  733    Width is Width0 + State.margin_right.
  734auto_cell_width(_, _, 0).
  735
  736%!  format_row(+ColWidths, +State, +MarginLeft, +Row)
  737%
  738%   Format a single row.
  739
  740format_row(ColWidths, State, MarginLeft, Row) :-
  741    hrule(Row, ColWidths, MarginLeft),
  742    format_cells(ColWidths, CWSpanned, 1, Row, State, Cells),
  743    format_row_lines(1, CWSpanned, Cells, MarginLeft).
  744
  745hrule(row(_, Attrs), ColWidths, MarginLeft) :-
  746    attrs_classes(Attrs, Classes),
  747    memberchk(hline, Classes),
  748    !,
  749    sum_list(ColWidths, RuleLen),
  750    format('~N~t~*|~`-t~*+', [MarginLeft, RuleLen]).
  751hrule(_, _, _).
  752
  753format_row_lines(LineNo, Widths, Cells, MarginLeft) :-
  754    nth_row_line(Widths, 1, LineNo, Cells, CellLines, Found),
  755    (   Found == true
  756    ->  emit_nl,
  757        emit_indent(MarginLeft),
  758        maplist(emit_cell_line, CellLines),
  759        ask_nl(1),
  760        LineNo1 is LineNo + 1,
  761        format_row_lines(LineNo1, Widths, Cells, MarginLeft)
  762    ;   true
  763    ).
  764
  765emit_cell_line(Line-Pad) :-
  766    write(Line),
  767    forall(between(1,Pad,_), put_char(' ')).
  768
  769nth_row_line([], _, _, _, [], _).
  770nth_row_line([ColW|CWT], CellNo, LineNo, Cells, [CellLine-Pad|ColLines],
  771             Found) :-
  772    nth1(CellNo, Cells, CellLines),
  773    (   nth1(LineNo, CellLines, CellLine)
  774    ->  Found = true,
  775        Pad = 0
  776    ;   CellLine = '', Pad = ColW
  777    ),
  778    CellNo1 is CellNo + 1,
  779    nth_row_line(CWT, CellNo1, LineNo, Cells, ColLines, Found).
  780
  781
  782%!  format_cells(+ColWidths, -CWSpanned, +Col0, +Row, +State, -Cells)
  783%
  784%   Format the cells for Row. The  resulting   Cells  list  is a list of
  785%   cells, where each cell is a  list   of  strings, each representing a
  786%   line.
  787
  788format_cells([], [], _, _, _, []) :- !.
  789format_cells(CWidths, [HW|TW], Column, Row, State, [HC|TC]) :-
  790    Row = row(Columns, _Attrs),
  791    nth1(Column, Columns, Cell),
  792    cell_colspan(Cell, CWidths, HW, TW0),
  793    cell_align(Cell, Align),
  794    format_cell_to_string(Cell, HW, State.put(_{pad:' ', text_align:Align}), String),
  795    split_string(String, "\n", "", HC),
  796    Column1 is Column+1,
  797    format_cells(TW0, TW, Column1, Row, State, TC).
  798
  799cell_colspan(Cell, CWidths, HW, TW) :-
  800    cell_colspan(Cell, Span),
  801    length(SpanW, Span),
  802    append(SpanW, TW, CWidths),
  803    sum_list(SpanW, HW).
  804
  805cell_colspan(element(_,Attrs,_), Span) :-
  806    (   memberchk(colspan=SpanA, Attrs),
  807        atom_number(SpanA, SpanN)
  808    ->  Span = SpanN
  809    ;   Span = 1
  810    ).
  811
  812%!  cell_align(+Cell, -Align) is det.
  813%
  814%   Determine the cell alignment. Currently   supports  the (deprecated)
  815%   HTML4  `align=Align`  possibility  and  very    naively  parsed  CSS
  816%   ``text-align:center``, etc.
  817
  818cell_align(element(_,Attrs,_), Align) :-
  819    (   memberchk(align=AlignA, Attrs)
  820    ->  Align = AlignA
  821    ;   memberchk(style=Style, Attrs),
  822        style_css_attrs(Style, Props),
  823        memberchk('text-align'(AlignA), Props)
  824    ->  Align = AlignA
  825    ;   Align = left
  826    ).
  827
  828
  829%!  format_cell_to_string(+Cell, +ColWidth, +State, -String) is det.
  830%
  831%   Format Cell to a String, given the state and column width.
  832
  833format_cell_to_string(element(_,_,[]), ColWidth, State, String) :-
  834    Pad = State.get(pad),
  835    !,
  836    length(Chars, ColWidth),
  837    maplist(=(Pad), Chars),
  838    atomics_to_string(Chars, String).
  839format_cell_to_string(Cell, ColWidth, State, String) :-
  840    setup_call_cleanup(
  841        init_nl(NlState),
  842        with_output_to(
  843            string(String),
  844            format_cell(Cell, ColWidth, State)),
  845        exit_nl(NlState)).
  846
  847format_cell(element(E, _Attrs, Content), ColWidth, State) :-
  848    set_stream(current_output, tty(State.tty)),
  849    cell_element(E, Style),
  850    update_style(Style, State.put(width, ColWidth), CellState),
  851    block_words(Content, Blocks, Words, CellState),
  852    emit_block(Words, [], CellState),
  853    (   Blocks \== []
  854    ->  format_dom(Blocks, CellState)
  855    ;   true
  856    ).
  857
  858cell_element(td, [normal]).
  859cell_element(th, [bold]).
  860
  861
  862%!  emit_hr(+Attrs, +BlockOptions, +State)
  863%
  864%   Emit a horizontal rule.
  865
  866emit_hr(_Attrs, BlockAttrs, State) :-
  867    option(margin_left(ML), BlockAttrs, 0),
  868    option(margin_right(MR), BlockAttrs, 0),
  869    RuleWidth is State.width - ML - MR,
  870    Style = State.style,
  871    emit_indent(ML),
  872    (   Style == []
  873    ->  format('~|~*t~*+', [0'-, RuleWidth])
  874    ;   ansi_format(Style, '~|~*t~*+', [0'-, RuleWidth])
  875    )