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)  2009-2023, University of Amsterdam
    7                              CWI, 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(portray_text,
   38          [ portray_text/1,             % +Bool
   39            set_portray_text/2,         % +Name, +Value
   40            set_portray_text/3          % +Name, ?Old, +Value
   41          ]).   42:- autoload(library(error), [must_be/2, domain_error/2]).   43
   44:- multifile
   45    is_text_code/1.                     % +Integer
   46
   47/** <module> Portray text
   48
   49SWI-Prolog has the special string data   type.  However, in Prolog, text
   50may be represented more traditionally as a list of character-codes, i.e.
   51(small) integers (in SWI-Prolog  specifically,   those  are Unicode code
   52points). This results in  output  like   the  following  (here using the
   53backquote notation which maps text to a list of codes):
   54
   55```
   56?- writeln(`hello`).
   57[104, 101, 108, 108, 111]
   58
   59?- atom_codes("hello",X).
   60X = [104,101,108,108,111].
   61```
   62
   63Unless you know the Unicode tables by   heart, this is pretty unpleasant
   64for debugging. Loading library(portray_text)  makes   the  toplevel  and
   65debugger consider certain lists of integers as   text  and print them as
   66text.  This  is  called  "portraying".   Of  course,  interpretation  is
   67imperfect as there is no way to tell in general whether `[65,66]` should
   68written as =|`AB`|= or as `[65,66]`. Therefore  it is important that the
   69user be aware of the fact that this   conversion is enabled. This is why
   70this library must be loaded explicitly.
   71
   72To be able to copy the printed representation and paste it back, printed
   73text is enclosed in _back quotes_  if current_prolog_flag/2 for the flag
   74`back_quotes` is `codes` (the default), and  enclosed in _double quotes_
   75otherwise.   Certain   control   characters   are     printed   out   in
   76backslash-escaped form.
   77
   78The default heuristic only considers list of  codes as text if the codes
   79are all from the  set  of  7-bit   ASCII  without  most  of  the control
   80characters. A code is classified as text   by text_code/1, which in turn
   81calls is_text_code/1. Define portray_text:is_text_code/1   to succeed on
   82additional codes for  more  flexibility   (by  default,  that  predicate
   83succeeds nowhere). For example:
   84
   85```
   86?- maplist([C,R]>>(portray_text:text_code(C)->R=y;R=n),
   87           `G\u00e9n\u00e9rateur`,Results).
   88Results = [y,n,y,n,y,y,y,y,y,y].
   89```
   90
   91Now make is_text_code/1 accept anything:
   92
   93```
   94?- [user].
   95|: portray_text:is_text_code(_).
   96|: ^D
   97% user://3 compiled 0.00 sec, 1 clauses
   98true.
   99```
  100
  101Then:
  102
  103```
  104?- maplist([C,R]>>(portray_text:text_code(C)->R=y;R=n),
  105           `G\u00e9n\u00e9rateur`,Results).
  106Results = [y,y,y,y,y,y,y,y,y,y].
  107```
  108*/
  109
  110:- dynamic
  111    portray_text_option/2.  112
  113portray_text_option(enabled, true).
  114portray_text_option(min_length, 3).
  115portray_text_option(ellipsis,  30).
  116
  117pt_option(enabled,    boolean).
  118pt_option(min_length, nonneg).
  119pt_option(ellipsis,   nonneg).
  120
  121%!  portray_text(+OnOff:boolean) is det.
  122%
  123%   Switch portraying on or off. If   `true`, consider lists of integers
  124%   as list of Unicode code points and  print them as corresponding text
  125%   inside quotes: =|`text`|= or  =|"text"|=.   Quoting  depends  on the
  126%   value of current_prolog_flag/2 `back_quotes`.  Same as
  127%
  128%       ?- set_portray_text(enabled, true).
  129
  130portray_text(OnOff) :-
  131    set_portray_text(enabled, OnOff).
  132
  133%!  set_portray_text(+Key, +Value) is det.
  134%!  set_portray_text(+Key, ?Old, +New) is det.
  135%
  136%   Set options for portraying.  Defined Keys are:
  137%
  138%     - enabled
  139%       Enable/disable portray text
  140%     - min_length
  141%       Only consider for conversion lists of integers
  142%       that have a length of at least Value. Default is 3.
  143%     - ellipsis
  144%       When converting a list that is longer than Value, display
  145%       the output as ``start...end``.
  146
  147set_portray_text(Key, New) :-
  148    set_portray_text(Key, _, New).
  149set_portray_text(Key, Old, New) :-
  150    nonvar(Key),
  151    pt_option(Key, Type),
  152    !,
  153    portray_text_option(Key, Old),
  154    (   Old == New
  155    ->  true
  156    ;   must_be(Type, New),
  157        retractall(portray_text_option(Key, _)),
  158        assert(portray_text_option(Key, New))
  159    ).
  160set_portray_text(Key, _, _) :-
  161    domain_error(portray_text_option, Key).
  162
  163
  164:- multifile
  165    user:portray/1.  166:- dynamic
  167    user:portray/1.  168
  169user:portray(Codes) :-
  170    portray_text_option(enabled, true),
  171    '$skip_list'(Length, Codes, _Tail),
  172    portray_text_option(min_length, MinLen),
  173    Length >= MinLen,
  174    mostly_codes(Codes, 0.9),
  175    portray_text_option(ellipsis, IfLonger),
  176    quote(C),
  177    put_code(C),
  178    (   Length > IfLonger
  179    ->  First is IfLonger - 5,
  180        Skip is Length - 5,
  181        skip_first(Skip, Codes, Rest),
  182        put_n_codes(First, Codes, C),
  183        format('...', [])
  184    ;   Rest = Codes
  185    ),
  186    put_var_codes(Rest, C),
  187    put_code(C).
  188
  189quote(0'`) :-
  190    current_prolog_flag(back_quotes, codes),
  191    !.
  192quote(0'").
  193
  194put_n_codes(N, [H|T], C) :-
  195    N > 0,
  196    !,
  197    emit_code(H, C),
  198    N2 is N - 1,
  199    put_n_codes(N2, T, C).
  200put_n_codes(_, _, _).
  201
  202skip_first(N, [_|T0], T) :-
  203    succ(N2, N),
  204    !,
  205    skip_first(N2, T0, T).
  206skip_first(_, L, L).
  207
  208put_var_codes(Var, _) :-
  209    var_or_numbered(Var),
  210    !,
  211    format('|~p', [Var]).
  212put_var_codes([], _).
  213put_var_codes([H|T], C) :-
  214    emit_code(H, C),
  215    put_var_codes(T, C).
  216
  217emit_code(Q, Q)    :- !, format('\\~c', [Q]).
  218emit_code(0'\b, _) :- !, format('\\b').
  219emit_code(0'\r, _) :- !, format('\\r').
  220emit_code(0'\n, _) :- !, format('\\n').
  221emit_code(0'\t, _) :- !, format('\\t').
  222emit_code(C, _) :- put_code(C).
  223
  224mostly_codes(Codes, MinFactor) :-
  225    mostly_codes(Codes, 0, 0, MinFactor).
  226
  227mostly_codes(Var, _, _, _) :-
  228    var_or_numbered(Var),
  229    !.
  230mostly_codes([], Yes, No, MinFactor) :-
  231    Yes >= (Yes+No)*MinFactor.
  232mostly_codes([H|T], Yes, No, MinFactor) :-
  233    integer(H),
  234    H >= 0,
  235    H =< 0x1ffff,
  236    (   text_code(H)
  237    ->  Yes1 is Yes+1,
  238        mostly_codes(T, Yes1, No, MinFactor)
  239    ;   catch(code_type(H, print),error(_,_),fail),
  240        No1 is No+1,
  241        mostly_codes(T, Yes, No1, MinFactor),
  242        (   Yes+No1 > 100
  243        ->  Yes >= (Yes+No1)*MinFactor
  244        ;   true
  245        )
  246    ).
  247
  248% Idea: Maybe accept anything and hex-escape anything non-printable?
  249%       In particular, I could imaging 0 and ESC appearing in text of interest.
  250%       Currently we really accept only 7-bit ASCII so even latin-1 text
  251%       precludes recognition.
  252% Bug?: emit_code/2 can emit backspace but backspace (8) is not accepted below
  253
  254text_code(Code) :-
  255    is_text_code(Code),
  256    !.
  257text_code(9).      % horizontal tab, \t
  258text_code(10).     % newline \n
  259text_code(13).     % carriage return \r
  260text_code(C) :-    % space to tilde (127 is DEL)
  261    between(32, 126, C).
  262
  263var_or_numbered(Var) :-
  264    var(Var),
  265    !.
  266var_or_numbered('$VAR'(_)).
  267
  268%!  is_text_code(+Code:nonneg) is semidet.
  269%
  270%   Multifile hook that can be used to extend the set of character codes
  271%   that is recognised as likely text.  By default, is_text_code/1 fails
  272%   everywhere  and  internally,  only    non-control  ASCII  characters
  273%   (32-126) and the the control codes (9,10,13) are accepted.
  274%
  275%   @tbd we might be able  to  use   the  current  locale to include the
  276%   appropriate code page. (Does that really make sense?)