View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        jan@swi-prolog.org
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2018-2026, CWI 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(prolog_help,
   37	  [ help/0,
   38	    help/1,                     % +Object
   39	    apropos/1,                  % +Search
   40	    apropos/2,                  % +Search, +Options
   41            help_apropos/4,
   42	    help_text/2                 % :PI, -Text:string
   43	  ]).   44:- use_module(library(pldoc), []).   45:- use_module(library(isub), [isub/4]).   46:- autoload(library(apply), [maplist/3]).   47:- autoload(library(error), [must_be/2]).   48:- autoload(library(lists), [append/3, sum_list/2, select/3]).   49:- autoload(library(option), [option/3]).   50:- autoload(library(pairs), [pairs_values/2]).   51:- autoload(library(porter_stem), [tokenize_atom/2]).   52:- autoload(library(process),
   53	    [process_create/3, process_which/2, process_wait/2]).   54:- autoload(library(sgml), [load_html/3]).   55:- autoload(library(solution_sequences), [distinct/1]).   56:- autoload(library(http/html_write), [html/3, print_html/1]).   57:- autoload(library(lynx/html_text), [html_text/2]).   58:- autoload(pldoc(doc_man),
   59	    [ man_page/4, pldoc_href_object/2,
   60	      man_object_uri/2, man_uri_object/2,
   61	      xpce_object_label/2
   62	    ]).   63:- autoload(library(pce), [send/3, get/3]).   64:- autoload(pldoc(doc_modes), [(mode)/2]).   65:- autoload(pldoc(doc_words), [doc_related_word/3]).   66:- autoload(pldoc(man_index), [man_object_property/2, doc_object_identifier/2]).   67:- autoload(library(prolog_code), [pi_head/2]).   68:- autoload(library(prolog_xref), [xref_source/2]).   69:- use_module(library(lynx/pldoc_style), []).   70:- autoload(library(terms), [mapsubterms/3]).   71
   72/** <module> Text based manual
   73
   74This module provides help/1 and apropos/1 that   give help on a topic or
   75searches the manual for relevant topics.
   76
   77By default the result of  help/1  is   sent  through  a  _pager_ such as
   78`less`. This behaviour is controlled by the following:
   79
   80  - The Prolog flag `help_pager`, which can be set to one of the
   81    following values:
   82
   83    - false
   84    Never use a pager.
   85    - default
   86    Use default behaviour.  This tries to determine whether Prolog
   87    is running interactively in an environment that allows for
   88    a pager.  If so it examines the environment variable =PAGER=
   89    or otherwise tries to find the `less` program.
   90    - Callable
   91    A Callable term is interpreted as program_name(Arg, ...).  For
   92    example, `less('-r')` would be the default.  Note that the
   93    program name can be an absolute path if single quotes are
   94    used.
   95*/
   96
   97:- meta_predicate
   98    with_pager(0).   99
  100:- multifile
  101    show_html_hook/1.  102
  103% one of `default`, `false`, an executable or executable(options), e.g.
  104% less('-r').
  105:- create_prolog_flag(help_pager, default,
  106		      [ type(term),
  107			keep(true)
  108		      ]).  109
  110%!  help is det.
  111%!  help(+What) is det.
  112%
  113%   Show help for What. What is a   term that describes the topics(s) to
  114%   give help for.  Notations for What are:
  115%
  116%     - Atom
  117%       This ambiguous form is most commonly used and shows all
  118%       matching documents.  For example:
  119%
  120%           ?- help(append).
  121%
  122%     - Name/Arity
  123%       Give help on predicates with matching Name/Arity.  Arity may
  124%       be unbound.
  125%     - Name//Arity
  126%       Give help on the matching DCG rule (non-terminal)
  127%     - Module:Name
  128%       Give help on predicates with Name in Module and any arity.
  129%       Used for loaded code only.
  130%     - Module:Name/Arity
  131%       Give help on predicates with Name in Module and Arity.
  132%       Used for loaded code only.
  133%     - f(Name/Arity)
  134%       Give help on the matching Prolog arithmetic functions.
  135%     - c(Name)
  136%       Give help on the matching C interface function
  137%     - section(Label)
  138%       Show the section from the manual with matching Label.
  139%     - xpce(Class, Kind, Name)
  140%       Show the documentation of an XPCE class member.
  141%
  142%   help/1 shows documentation from the manual   as  well as from loaded
  143%   user code if the code is documented   using  PlDoc. To show only the
  144%   documentatoion of the  loaded  predicate   we  may  prefix predicate
  145%   indicator with the module in which it is defined.
  146%
  147%   If an exact match fails this predicates attempts fuzzy matching and,
  148%   when successful, display the results headed   by  a warning that the
  149%   matches are based on fuzzy matching.
  150%
  151%   If possible, the results are sent  through   a  _pager_  such as the
  152%   `less` program. This behaviour is  controlled   by  the  Prolog flag
  153%   `help_pager`. See section level documentation.
  154%
  155%   If the terminal supports hyperlinks (see  the Prolog flag
  156%   `hyperlink_term`), the manual references in  the page are clickable.
  157%   In an Epilog window, clicking one quits the pager and runs help/1 on
  158%   the linked object.
  159%
  160%   @see apropos/1 for searching the manual names and summaries.
  161
  162help :-
  163    notrace(show_matches([help/1, apropos/1], exact-help)).
  164
  165help(What) :-
  166    notrace(help_no_trace(What)).
  167
  168help_no_trace(What) :-
  169    help_objects_how(What, Matches, How),
  170    !,
  171    show_matches(Matches, How-What).
  172help_no_trace(What) :-
  173    print_message(warning, help(not_found(What))).
  174
  175show_matches(Matches, HowWhat) :-
  176    help_html(Matches, HowWhat, HTML),
  177    !,
  178    show_html(HTML).
  179
  180%!  show_html_hook(+HTML:string) is semidet.
  181%
  182%   Hook called to display the  extracted   HTML  document. If this hook
  183%   fails the HTML is rendered  to  the   console  as  plain  text using
  184%   html_text/2.
  185
  186show_html(HTML) :-
  187    show_html_hook(HTML),
  188    !.
  189show_html(HTML) :-
  190    load_html(string(HTML), DOM0, []),
  191    mapsubterms(man_link, DOM0, DOM),
  192    page_width(PageWidth),
  193    LineWidth is PageWidth - 4,
  194    with_pager(html_text(DOM, [width(LineWidth)])).
  195
  196help_html(Matches, How, HTML) :-
  197    (   current_prolog_flag(epilog, true)
  198    ->  Extra = [link_scheme(man)]
  199    ;   Extra = []
  200    ),
  201    phrase(html(html([ head([]),
  202		       body([ \match_type(How),
  203			      dl(\man_pages(Matches,
  204					    [ no_manual(fail),
  205					      links(false),
  206					      link_source(false),
  207					      navtree(false),
  208					      server(false),
  209                                              qualified(always)
  210                                            | Extra
  211					    ]))
  212			    ])
  213		     ])),
  214	   Tokens),
  215    !,
  216    with_output_to(string(HTML),
  217		   print_html(Tokens)).
  218
  219match_type(exact-_) -->
  220    [].
  221match_type(dwim-For) -->
  222    html(p(class(warning),
  223	   [ 'WARNING: No matches for "', span(class('help-query'), For),
  224	     '" Showing closely related results'
  225	   ])).
  226
  227man_pages([], _) -->
  228    [].
  229man_pages([H|T], Options) -->
  230    (   man_page(H, Options)
  231    ->  []
  232    ;   html(p(class(warning),
  233               [ 'WARNING: No help for ~p'-[H]
  234               ]))
  235    ),
  236    man_pages(T, Options).
  237
  238page_width(Width) :-
  239    tty_width(W),
  240    Width is min(100,max(50,W)).
  241
  242%!  tty_width(-Width) is det.
  243%
  244%   Return the believed width of the terminal.   If we do not know Width
  245%   is bound to 80.
  246
  247tty_width(W) :-
  248    \+ running_under_emacs,
  249    catch(tty_size(_, W), _, fail),
  250    !.
  251tty_width(80).
  252
  253help_objects_how(Spec, Objects, exact) :-
  254    help_objects(Spec, exact, Objects),
  255    !.
  256help_objects_how(Spec, Objects, dwim) :-
  257    help_objects(Spec, dwim, Objects),
  258    !.
  259
  260help_objects(Spec, How, Objects) :-
  261    findall(ID-Obj, help_object(Spec, How, Obj, ID), Objects0),
  262    Objects0 \== [],
  263    sort(1, @>, Objects0, Objects1),
  264    pairs_values(Objects1, Objects2),
  265    sort(Objects2, Objects).
  266
  267help_object(Fuzzy/Arity, How, Name/Arity, ID) :-
  268    match_name(How, Fuzzy, Name),
  269    man_object_property(Name/Arity, id(ID)).
  270help_object(Fuzzy//Arity, How, Name//Arity, ID) :-
  271    match_name(How, Fuzzy, Name),
  272    man_object_property(Name//Arity, id(ID)).
  273help_object(Fuzzy/Arity, How, f(Name/Arity), ID) :-
  274    match_name(How, Fuzzy, Name),
  275    man_object_property(f(Name/Arity), id(ID)).
  276help_object(Fuzzy, How, Name/Arity, ID) :-
  277    atom(Fuzzy),
  278    match_name(How, Fuzzy, Name),
  279    man_object_property(Name/Arity, id(ID)).
  280help_object(Fuzzy, How, Name//Arity, ID) :-
  281    atom(Fuzzy),
  282    match_name(How, Fuzzy, Name),
  283    man_object_property(Name//Arity, id(ID)).
  284help_object(Fuzzy, How, f(Name/Arity), ID) :-
  285    atom(Fuzzy),
  286    match_name(How, Fuzzy, Name),
  287    man_object_property(f(Name/Arity), id(ID)).
  288help_object(Fuzzy, How, c(Name), ID) :-
  289    atom(Fuzzy),
  290    match_name(How, Fuzzy, Name),
  291    man_object_property(c(Name), id(ID)).
  292help_object(SecID, _How, section(Label), ID) :-
  293    atom(SecID),
  294    (   atom_concat('sec:', SecID, Label)
  295    ;   sub_atom(SecID, _, _, 0, '.html'),
  296	Label = SecID
  297    ),
  298    man_object_property(section(_Level,_Num,Label,_File), id(ID)).
  299help_object(Func, How, c(Name), ID) :-
  300    compound(Func),
  301    compound_name_arity(Func, Fuzzy, 0),
  302    match_name(How, Fuzzy, Name),
  303    man_object_property(c(Name), id(ID)).
  304% resolved manual objects, e.g. from a clicked hyperlink.  See man_link/2.
  305help_object(Obj, _How, Obj, ID) :-
  306    man_object_id(Obj, ID).
  307% for currently loaded predicates
  308help_object(Module, _How, Module:Name/Arity, _ID) :-
  309    atom(Module),
  310    current_module(Module),
  311    atom_concat('sec:', Module, SecLabel),
  312    \+ man_object_property(section(_,_,SecLabel,_), _), % not a section
  313    current_predicate_help(Module:Name/Arity).
  314help_object(Module:Name, _How, Module:Name/Arity, _ID) :-
  315    atom(Name),
  316    current_predicate_help(Module:Name/Arity).
  317help_object(Module:Name/Arity, _How, Module:Name/Arity, _ID) :-
  318    atom(Name),
  319    current_predicate_help(Module:Name/Arity).
  320help_object(Name/Arity, _How, Module:Name/Arity, _ID) :-
  321    atom(Name),
  322    current_predicate_help(Module:Name/Arity).
  323help_object(Fuzzy, How, Module:Name/Arity, _ID) :-
  324    atom(Fuzzy),
  325    match_name(How, Fuzzy, Name),
  326    current_predicate_help(Module:Name/Arity).
  327
  328%!  man_object_id(@Object, -ID) is semidet.
  329%
  330%   True when Object is a fully specified   manual object with identifier
  331%   ID.  Predicate indicators  are  not   included:  these  are  ambiguous
  332%   enough to be handled by the fuzzy matching clauses above.
  333
  334man_object_id(Module:Name/Arity, ID) :-
  335    atom(Module),
  336    atom(Name),
  337    integer(Arity),
  338    man_object_property(Module:Name/Arity, id(ID)).
  339man_object_id(Module:Name//Arity, ID) :-
  340    atom(Module),
  341    atom(Name),
  342    integer(Arity),
  343    man_object_property(Module:Name//Arity, id(ID)).
  344man_object_id(section(Label), ID) :-
  345    atom(Label),
  346    man_object_property(section(_Level,_Num,Label,_File), id(ID)).
  347man_object_id(f(Name/Arity), ID) :-
  348    atom(Name),
  349    integer(Arity),
  350    man_object_property(f(Name/Arity), id(ID)).
  351man_object_id(c(Name), ID) :-
  352    atom(Name),
  353    man_object_property(c(Name), id(ID)).
  354man_object_id(xpce(Class,Kind,Name), ID) :-
  355    atom(Class),
  356    atom(Kind),
  357    atom(Name),
  358    man_object_property(xpce(Class,Kind,Name), id(ID)).
  359
  360%!  current_predicate_help(?PI) is nondet.
  361%
  362%   True when we have documentation on  PI.   First  we decide we have a
  363%   definition  for  PI,  then  we  check    whether   or  not  we  have
  364%   documentation for the module in which PI  resides. If not, we switch
  365%   to documentation collect mode and reload the file that defines PI.
  366
  367current_predicate_help(M:Name/Arity) :-
  368    current_predicate(M:Name/Arity),
  369    pi_head(Name/Arity,Head),
  370    \+ predicate_property(M:Head, imported_from(_)),
  371    module_property(M, class(user)),
  372    (   mode(M:_, _)             % Some predicates are documented
  373    ->  true
  374    ;   \+ module_property(M, class(system)),
  375        main_source_file(M:Head, File),
  376	xref_source(File,[comments(store)])
  377    ),
  378    mode(M:Head, _).             % Test that our predicate is documented
  379
  380match_name(exact, Name, Name).
  381match_name(dwim,  Name, Fuzzy) :-
  382    freeze(Fuzzy, dwim_match(Fuzzy, Name)).
  383
  384%!  main_source_file(+Pred, -File) is semidet.
  385%
  386%   True when File is the main (not included) file that defines Pred.
  387
  388main_source_file(Pred, File) :-
  389    predicate_property(Pred, file(File0)),
  390    main_source(File0, File).
  391
  392main_source(File, Main) :-
  393    source_file(File),
  394    !,
  395    Main = File.
  396main_source(File, Main) :-
  397    source_file_property(File, included_in(Parent, _Time)),
  398    main_source(Parent, Main).
  399
  400
  401%!  with_pager(+Goal)
  402%
  403%   Send the current output of Goal through a  pager. If no pager can be
  404%   found we simply dump the output to the current output.  We wait for
  405%   the pager to terminate, so the toplevel does not print its prompt on
  406%   the screen the pager is using.
  407
  408with_pager(Goal) :-
  409    pager_ok(Pager, Options),
  410    !,
  411    current_output(Screen),
  412    setup_call_cleanup(
  413	pager_screen(Screen, enter),
  414	paged(Pager, Goal, Options),
  415	pager_screen(Screen, leave)).
  416with_pager(Goal) :-
  417    call(Goal).
  418
  419%!  pager(?Thread, ?PID) is nondet.
  420%
  421%   True while Thread is showing help using the pager process PID.  Used
  422%   by quit_pager/1 to get the pager out of the way if the user clicks a
  423%   hyperlink in the page it is showing.
  424
  425:- dynamic
  426    pager/2.                            % Thread, PID
  427
  428paged(Pager, Goal, Options) :-
  429    Catch = error(io_error(_,_), _),
  430    current_output(OldIn),
  431    thread_self(Me),
  432    setup_call_cleanup(
  433	( process_create(Pager, Options,
  434			 [stdin(pipe(In)), process(PID)]),
  435	  assertz(pager(Me, PID), Ref)
  436	),
  437	( set_stream(In, tty(true)),
  438	  set_output(In),
  439	  catch(Goal, Catch, true)
  440	),
  441	call_cleanup(( set_output(OldIn),
  442                       close(In, [force(true)]),
  443                       process_wait(PID, _Status)
  444                     ),
  445                     erase(Ref))).
  446
  447%!  pager_screen(+Screen, +Which) is det.
  448%
  449%   Give the pager a screen of its own, so that quitting it leaves the
  450%   terminal as it was.  Windows only: a pager there takes a screen
  451%   buffer from the console API and the console swaps back to the
  452%   previous one when the pager exits, but a pseudo console -- which is
  453%   what an Epilog window gives its children -- does not carry those
  454%   calls.  Its alternate screen is the DEC private mode and nothing
  455%   else, so the terminal is told here rather than by the pager.
  456%
  457%   Elsewhere the pager does this itself, from its terminal description,
  458%   and a pager that does not (`cat`) is one whose output should stay.
  459
  460pager_screen(_Screen, _Which) :-
  461    \+ current_prolog_flag(windows, true),
  462    !.
  463pager_screen(Screen, _Which) :-
  464    \+ stream_property(Screen, tty(true)),
  465    !.
  466pager_screen(Screen, enter) :-
  467    !,
  468    format(Screen, '\e[?1049h', []),
  469    flush_output(Screen).
  470pager_screen(Screen, leave) :-
  471    format(Screen, '\e[?1049l', []),
  472    flush_output(Screen).
  473
  474pager_ok(_Path, _Options) :-
  475    current_prolog_flag(help_pager, false),
  476    !,
  477    fail.
  478pager_ok(Path, Options) :-
  479    current_prolog_flag(help_pager, default),
  480    !,
  481    stream_property(current_output, tty(true)),
  482    \+ running_under_emacs,
  483    (   distinct((   getenv('PAGER', Pager)
  484		 ;   Pager = less
  485		 )),
  486	absolute_file_name(path(Pager), Path,
  487			   [ access(execute),
  488			     file_errors(fail)
  489			   ])
  490    ->  pager_options(Path, Options)
  491    ).
  492pager_ok(Path, Options) :-
  493    current_prolog_flag(help_pager, Term),
  494    callable(Term),
  495    compound_name_arguments(Term, Pager, Options),
  496    (   is_absolute_file_name(Pager)
  497    ->  Prog = Pager
  498    ;   Prog = path(Pager)
  499    ),
  500    process_which(Prog, Path).
  501
  502pager_options(Path, Options) :-
  503    file_base_name(Path, File),
  504    file_name_extension(Base, _, File),
  505    downcase_atom(Base, Id),
  506    pager_default_options(Id, Options),
  507    !.
  508pager_options(_, []).
  509
  510pager_default_options(less, ['-r']).
  511
  512
  513%!  running_under_emacs
  514%
  515%   True when we believe to be running  in Emacs. Unfortunately there is
  516%   no easy unambiguous way to tell.
  517
  518running_under_emacs :-
  519    current_prolog_flag(emacs_inferior_process, true),
  520    !.
  521running_under_emacs :-
  522    getenv('TERM', dumb),
  523    !.
  524running_under_emacs :-
  525    current_prolog_flag(toplevel_prompt, P),
  526    sub_atom(P, _, _, _, 'ediprolog'),
  527    !.
  528
  529%!  apropos(+Query) is det.
  530%!  apropos(+Query, +Options) is det.
  531%
  532%   Print objects from the  manual  whose   name  or  summary match with
  533%   Query. Query takes one of the following forms:
  534%
  535%     - Type:Text
  536%       Find objects matching Text and filter the results by Type.
  537%       Type matching is a case intensitive _prefix_ match.
  538%       Defined types are `section`, `cfunction`, `function`,
  539%       `iso_predicate`, `swi_builtin_predicate`, `library_predicate`,
  540%       `dcg` and aliases `chapter`, `arithmetic`, `c_function`,
  541%       `predicate`, `nonterminal` and `non_terminal`.  For example:
  542%
  543%           ?- apropos(c:close).
  544%           ?- apropos(f:min).
  545%
  546%     - Text
  547%       Text is broken into tokens.  A topic matches if all tokens
  548%       appear in the name or summary of the topic. Matching is
  549%	case insensitive.  Results are ordered depending on the
  550%	quality of the match.
  551%
  552%   Only the best `limit` matches are shown.  Options:
  553%
  554%     - limit(+Count)
  555%       Maximum number of matches to show.  Default 20.
  556%     - offset(+Skip)
  557%       Ignore the Skip best matches.  Default 0.
  558%
  559%   If the terminal supports hyperlinks (see  the Prolog flag
  560%   `hyperlink_term`), the matches are clickable  and so is the line that
  561%   reports there are more matches.  In an  Epilog window, clicking these
  562%   runs help/1 on the match or apropos/2 on the next page.
  563
  564apropos(Query) :-
  565    apropos(Query, []).
  566
  567apropos(Query, Options) :-
  568    notrace(apropos_no_trace(Query, Options)).
  569
  570apropos_no_trace(Query, Options) :-
  571    option(limit(Limit), Options, 20),
  572    option(offset(From), Options, 0),
  573    must_be(positive_integer, Limit),
  574    must_be(nonneg, From),
  575    findall(Q-(Obj-Summary), help_apropos(Query, Obj, Summary, Q), Pairs),
  576    (   Pairs == []
  577    ->  print_message(warning, help(no_apropos_match(Query)))
  578    ;   sort(1, >=, Pairs, Sorted),
  579	length(Sorted, Total),
  580	page(Sorted, From, Limit, Page),
  581	pairs_values(Page, Matches),
  582	print_message(information,
  583		      help(apropos_matches(Query, Matches, From, Total)))
  584    ).
  585
  586%!  page(+List, +From, +Limit, -Page) is det.
  587%
  588%   Page is the sub list of List that   starts at From and holds at most
  589%   Limit elements.
  590
  591page(List, From, Limit, Page) :-
  592    length(List, Len),
  593    Skip is min(From, Len),
  594    length(Prefix, Skip),
  595    append(Prefix, Rest, List),
  596    length(Rest, RestLen),
  597    Take is min(Limit, RestLen),
  598    length(Page, Take),
  599    append(Page, _, Rest).
  600
  601%!  help_apropos(+Query, -Obj, -Summary, -Score) is nondet.
  602%
  603%   Find matching documented objects in the   help  database. Obj is the
  604%   formal object identifier, Summary its  summary description and Score
  605%   is a number indicating the quality of the match.
  606
  607help_apropos(Query, Obj, Summary, Q) :-
  608    parse_query(Query, Type, Words),
  609    man_object_property(Obj, summary(Summary)),
  610    apropos_match(Type, Words, Obj, Summary, Q).
  611
  612parse_query(Type:String, Type, Words) :-
  613    !,
  614    must_be(atom, Type),
  615    must_be(text, String),
  616    tokenize_atom(String, Words).
  617parse_query(String, _Type, Words) :-
  618    must_be(text, String),
  619    tokenize_atom(String, Words).
  620
  621apropos_match(Type, Query, Object, Summary, Q) :-
  622    maplist(amatch(Object, Summary), Query, Scores),
  623    match_object_type(Type, Object),
  624    sum_list(Scores, Q).
  625
  626amatch(Object, Summary, Query, Score) :-
  627    (   doc_object_identifier(Object, String)
  628    ;   String = Summary
  629    ),
  630    amatch(Query, String, Score),
  631    !.
  632
  633amatch(Query, To, Quality) :-
  634    doc_related_word(Query, Related, Distance),
  635    sub_atom_icasechk(To, _, Related),
  636    isub(Related, To, false, Quality0),
  637    Quality is Quality0*Distance.
  638
  639match_object_type(Type, _Object) :-
  640    var(Type),
  641    !.
  642match_object_type(Type, Object) :-
  643    downcase_atom(Type, LType),
  644    object_class(Object, Class),
  645    match_object_class(LType, Class).
  646
  647match_object_class(Type, Class) :-
  648    (   TheClass = Class
  649    ;   class_alias(Class, TheClass)
  650    ),
  651    sub_atom(TheClass, 0, _, _, Type),
  652    !.
  653
  654class_alias(section,               chapter).
  655class_alias(function,              arithmetic).
  656class_alias(cfunction,             c_function).
  657class_alias(iso_predicate,         predicate).
  658class_alias(swi_builtin_predicate, predicate).
  659class_alias(library_predicate,     predicate).
  660class_alias(dcg,                   predicate).
  661class_alias(dcg,                   nonterminal).
  662class_alias(dcg,                   non_terminal).
  663
  664class_tag(section,               'SEC').
  665class_tag(function,              'F').
  666class_tag(cfunction,             'C').
  667class_tag(iso_predicate,         'ISO').
  668class_tag(swi_builtin_predicate, 'SWI').
  669class_tag(library_predicate,     'LIB').
  670class_tag(dcg,                   'DCG').
  671class_tag(xpce,                  'XPCE').
  672
  673object_class(section(_Level, _Num, _Label, _File), section).
  674object_class(c(_Name), cfunction).
  675object_class(f(_Name/_Arity), function).
  676object_class(xpce(_Class, _Kind, _Name), xpce).
  677object_class(Name/Arity, Type) :-
  678    functor(Term, Name, Arity),
  679    (   current_predicate(system:Name/Arity),
  680	predicate_property(system:Term, built_in)
  681    ->  (   predicate_property(system:Term, iso)
  682	->  Type = iso_predicate
  683	;   Type = swi_builtin_predicate
  684	)
  685    ;   Type = library_predicate
  686    ).
  687object_class(_M:_Name/_Arity, library_predicate).
  688object_class(_Name//_Arity, dcg).
  689object_class(_M:_Name//_Arity, dcg).
  690
  691%! help_text(+Predicate:term, -HelpText:string) is semidet.
  692%
  693%  When  Predicate  is  a  term  of  the  form  `Name/Arity`  for  which
  694%  documentation exists, HelpText is the documentation in textual format
  695%  (parsed from the HTML help).
  696
  697help_text(Pred, HelpText) :-
  698    help_objects(Pred, exact, Matches), !,
  699    catch(help_html(Matches, exact-exact, HtmlDoc), _, fail),
  700    setup_call_cleanup(open_string(HtmlDoc, In),
  701                       load_html(stream(In), Dom, []),
  702                       close(In)),
  703    with_output_to(string(HelpText), html_text(Dom, [])).
  704
  705
  706                /*******************************
  707                *            LINKS             *
  708                *******************************/
  709
  710%!  man_link(+Term, -Mapped) is semidet.
  711%
  712%   The `link_scheme(man)` option of man_page//2 already wrote the manual
  713%   references as ``man:`` IRIs, which a  terminal   emits as OSC8 hyperlinks
  714%   (see ansi_hyperlink/3) and tty_link_hook/2 below resolves when clicked.
  715%   This maps the remaining links, which  address   the  PlDoc server, onto
  716%   the same IRIs.  Links we cannot resolve are removed.
  717
  718man_link(element(a, Attrs0, Content), Element) :-
  719    select(href=HREF0, Attrs0, Attrs1),
  720    \+ sub_atom(HREF0, 0, _, _, 'man:'),
  721    (   current_prolog_flag(epilog, true),
  722        pldoc_href_object(HREF0, Object),
  723	man_object_uri(Object, HREF)
  724    ->  Element = element(a, [href=HREF|Attrs1], Content)
  725    ;   Element = element(b, Attrs1, Content)
  726    ).
  727
  728%!  apropos_uri(+Query, +Offset, -URI) is det.
  729%!  apropos_uri_goal(+URI, -Goal) is semidet.
  730%
  731%   Convert between an ``apropos:`` IRI  and   the  apropos/2  goal  that
  732%   continues the search  at  Offset.  Used   to  make  the  line telling
  733%   there are more matches clickable.
  734
  735apropos_uri(Query, Offset, URI) :-
  736    format(atom(URI), 'apropos:~q', [Query+Offset]).
  737
  738apropos_uri_goal(URI, apropos(Query, [offset(Offset)])) :-
  739    atom_concat('apropos:', Text, URI),
  740    catch(term_to_atom(Query+Offset, Text), error(_,_), fail),
  741    integer(Offset).
  742
  743%!  epilog:tty_link_hook(+Terminal, +Link) is semidet.
  744%
  745%   Open a ``man:`` or ``apropos:`` link  that was clicked in an Epilog
  746%   Terminal.  We quit the pager if it is  still showing the page the link
  747%   was clicked in and let the terminal run help/1 on the linked object or
  748%   continue the apropos/2 search.
  749
  750:- multifile epilog:tty_link_hook/2.  751
  752epilog:tty_link_hook(Terminal, URL) :-
  753    link_goal(URL, Goal),
  754    !,                                  % the link is ours, do not let
  755    quit_pager(Terminal),               % Epilog pass it to a browser
  756    ignore(send(Terminal, inject, Goal)).
  757
  758link_goal(URL, help(Object)) :-
  759    man_uri_object(URL, Object).
  760link_goal(URL, Goal) :-
  761    apropos_uri_goal(URL, Goal).
  762
  763%!  quit_pager(+Terminal) is det.
  764%
  765%   If the Prolog thread of Terminal is  waiting for its pager, tell the
  766%   pager to quit. All common pagers quit on `q`.
  767
  768quit_pager(Terminal) :-
  769    get(Terminal, thread, Thread),
  770    pager(Thread, _PID),
  771    !,
  772    send(Terminal, send, "q").
  773quit_pager(_).
  774
  775		 /*******************************
  776		 *            MESSAGES		*
  777		 *******************************/
  778
  779:- multifile prolog:message//1.  780
  781prolog:message(help(not_found(What))) -->
  782    [ 'No help for ~p.'-[What], nl,
  783      'Use ?- apropos(query). to search for candidates.'-[]
  784    ].
  785prolog:message(help(no_apropos_match(Query))) -->
  786    [ 'No matches for ~p'-[Query] ].
  787prolog:message(help(apropos_matches(Query, Pairs, From, Total))) -->
  788    { tty_width(W),
  789      Width is max(30,W),
  790      length(Pairs, Count),
  791      End is From+Count
  792    },
  793    matches(Pairs, Width),
  794    (   {End =:= Total, From =:= 0}
  795    ->  []
  796    ;   [nl],
  797	showing(Query, From, End, Total),
  798	(   {End =:= Total}
  799	->  []
  800	;   [ nl, nl,
  801	      'Use ?- apropos(Type:Query) or multiple words in Query '-[], nl,
  802	      'to restrict your search.  For example:'-[], nl, nl,
  803	      '  ?- apropos(iso:open).'-[], nl,
  804	      '  ?- apropos(\'open file\').'-[]
  805	    ]
  806	)
  807    ).
  808
  809%!  showing(+Query, +From, +End, +Total)// is det.
  810%
  811%   Emit the line telling which of  the   matches  are  shown. If not all
  812%   matches are shown this is a link to the next page.
  813
  814showing(Query, From, End, Total) -->
  815    { End < Total,
  816      apropos_uri(Query, End, URI),
  817      Start is From+1
  818    },
  819    !,
  820    [ ansi([fg(red), href(URI)], 'Showing ~D..~D of ~D matches',
  821	   [Start,End,Total])
  822    ].
  823showing(_Query, From, End, Total) -->
  824    { Start is From+1 },
  825    [ ansi(fg(red), 'Showing ~D..~D of ~D matches', [Start,End,Total]) ].
  826
  827matches([], _) --> [].
  828matches([H|T], Width) -->
  829    match(H, Width),
  830    (   {T == []}
  831    ->  []
  832    ;   [nl],
  833	matches(T, Width)
  834    ).
  835
  836match(Obj-Summary, Width) -->
  837    { Left is min(40, max(20, round(Width/3))),
  838      Right is Width-Left-2,
  839      man_object_summary(Obj, ObjS, Tag),
  840      format(string(TagS), '~t~w~4|', [Tag]),
  841      string_length(ObjS, LenObj),
  842      Spaces0 is Left - LenObj - 5,
  843      (   Spaces0 > 0
  844      ->  Spaces = Spaces0,
  845	  SummaryLen = Right
  846      ;   Spaces = 1,
  847	  SummaryLen is Right + Spaces0 - 1
  848      ),
  849      truncate(Summary, SummaryLen, SummaryE),
  850      match_attributes(Obj, Attrs)
  851    },
  852    [ ansi([fg(default)], '~w ', [TagS]),
  853      ansi(Attrs, '~w', [ObjS]),
  854      '~|~*+~w'-[Spaces, SummaryE]
  855%     '~*|~w'-[Spaces, SummaryE]		% Should eventually work
  856    ].
  857
  858%!  match_attributes(+Object, -Attributes) is det.
  859%
  860%   ANSI attributes for printing Object.  If  the terminal supports them,
  861%   make the match a link that runs help/1 on Object.
  862
  863match_attributes(Obj, [fg(default), href(URI)]) :-
  864    current_prolog_flag(hyperlink_term, true),
  865    man_object_uri(Obj, URI),
  866    !.
  867match_attributes(_Obj, [fg(default)]).
  868
  869truncate(Summary, Width, SummaryE) :-
  870    string_length(Summary, SL),
  871    SL > Width,
  872    !,
  873    ellipsis(Ellipsis, Len),
  874    Pre is max(0, Width-Len),
  875    sub_string(Summary, 0, Pre, _, S1),
  876    string_concat(S1, Ellipsis, SummaryE).
  877truncate(Summary, _, Summary).
  878
  879%!  ellipsis(-Ellipsis:string, -Length:integer) is det.
  880%
  881%   Ellipsis is appended to truncated  text  and   Length  is  the number
  882%   of columns it occupies.  Use  the   Unicode  horizontal  ellipsis if
  883%   the message stream can represent it.
  884
  885ellipsis(" \u2026", 2) :-
  886    stream_property(user_error, encoding(Enc)),
  887    unicode_encoding(Enc),
  888    !.
  889ellipsis(" ...", 4).
  890
  891unicode_encoding(utf8).
  892unicode_encoding(unicode_be).
  893unicode_encoding(unicode_le).
  894unicode_encoding(wchar_t).
  895
  896%!  man_object_summary(+Object, -Label:string, -Tag) is det.
  897%
  898%   Label is the text used to display  Object in the apropos output. Tag
  899%   is a short indication of the type of Object.
  900
  901man_object_summary(section(_Level, _Num, Label, _File), Text, 'SEC') :-
  902    atom_concat('sec:', Name, Label),
  903    !,
  904    format(string(Text), '~w', [Name]).
  905man_object_summary(section(0, _Num, File, _Path), Text, 'SEC') :- !,
  906    format(string(Text), '~w', [File]).
  907man_object_summary(c(Name), Text, 'C') :- !,
  908    format(string(Text), '~w()', [Name]).
  909man_object_summary(xpce(Class, Kind, Name), Text, 'XPCE') :- !,
  910    xpce_object_label(xpce(Class, Kind, Name), Label),
  911    format(string(Text), '~w', [Label]).
  912man_object_summary(f(Name/Arity), Text, 'F') :- !,
  913    format(string(Text), '~p', [Name/Arity]).
  914man_object_summary(Obj, Text, Tag) :-
  915    (   object_class(Obj, Class),
  916	class_tag(Class, Tag)
  917    ->  true
  918    ;   Tag = '?'
  919    ),
  920    format(string(Text), '~p', [Obj]).
  921
  922		 /*******************************
  923		 *            SANDBOX		*
  924		 *******************************/
  925
  926sandbox:safe_primitive(prolog_help:apropos(_)).
  927sandbox:safe_primitive(prolog_help:apropos(_,_)).
  928sandbox:safe_primitive(prolog_help:help(_))