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)  2006-2018, University of Amsterdam
    7                              CWI, Amsterdam
    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(pldoc_search,
   37          [ search_form//1,             % +Options, //
   38            search_reply//2,            % +Search, +Options, //
   39            matching_object_table//2    % +Objects, +Options, //
   40          ]).   41:- use_module(library(http/html_write)).   42:- use_module(library(http/html_head)).   43:- use_module(library(dcg/basics)).   44:- use_module(library(option)).   45:- use_module(library(pairs)).   46:- use_module(library(uri)).   47:- use_module(library(debug)).   48:- use_module(library(apply)).   49:- use_module(library(lists)).   50:- use_module(library(atom)).   51:- use_module(library(porter_stem)).   52
   53:- use_module(doc_process).   54:- use_module(doc_html).   55:- use_module(doc_index).   56:- use_module(doc_util).   57:- use_module(doc_words).   58:- use_module(man_index).   59
   60:- include(hooks).   61
   62/** <module> Search form and reply
   63
   64@tbd    Advanced search field
   65
   66                * Limit to a directory
   67                * Whole-word search
   68*/
   69
   70:- predicate_options(search_form//1, 1,
   71                     [ for(atom),
   72                       search_in(oneof([all,noapp,app,man])),
   73                       search_match(oneof([name,summary])),
   74                       search_options(boolean)
   75                     ]).   76:- predicate_options(search_reply//2, 2,
   77                     [ resultFormat(oneof([summary,long])),
   78                       search_in(oneof([all,noapp,app,man])),
   79                       search_match(oneof([name,summary])),
   80                       header(boolean),
   81                       private(boolean),
   82                       edit(boolean),
   83                       page(positive_integer),
   84                       per_page(positive_integer),
   85                       pass_to(pldoc_index:doc_links//2, 2)
   86                     ]).   87
   88%!  search_form(+Options)//
   89%
   90%   Create  a  search  input  field.  The   input  field  points  to
   91%   =|/search?for=String|= on the current server.  Options:
   92%
   93%           * title(Title)
   94
   95search_form(Options) -->
   96    { (   option(for(Value), Options)
   97      ->  Extra = [value(Value)]
   98      ;   Extra = []
   99      ),
  100      option(search_in(In), Options, all),
  101      option(search_match(Match), Options, summary)
  102    },
  103    html(form([ id('search-form'),
  104                action(location_by_id(pldoc_search))
  105              ],
  106              [ div([ \search_field([ name(for),
  107                                      id(for)
  108                                    | Extra
  109                                    ])
  110                    ]),
  111                \search_options(In, Match, Options)
  112              ])).
  113
  114search_options(In, Match, Options) -->
  115    { option(search_options(false), Options) },
  116    !,
  117    hidden(in, In),
  118    hidden(match, Match).
  119search_options(In, Match, _Options) -->
  120    html(div(class('search-options'),
  121             [ span(class('search-in'),
  122                    [ \radio(in, all, 'All', In),
  123                      \radio(in, app, 'Application', In),
  124                      \radio(in, man, 'Manual', In)
  125                    ]),
  126               span(class('search-match'),
  127                    [ \radio(match, name, 'Name', Match),
  128                      \radio(match, summary, 'Summary', Match)
  129                    ]),
  130               span(class('search-help'),
  131                    [ a(href(location_by_id(pldoc_package)+'pldoc.html#sec:browser'),
  132                        'Help')
  133                    ])
  134             ])).
  135
  136
  137%!  search_field(+Options)// is det.
  138%
  139%   Hookable predicate to display the   search field. Hookability is
  140%   provided  to  experiment  with    auto-completion  outside  this
  141%   package.
  142
  143search_field(Options) -->
  144    prolog:doc_search_field(Options),
  145    !.
  146search_field(Options) -->
  147    html([ input(Options, []),
  148           input([ id('submit-for'),
  149                   type(submit),
  150                   value('Search')
  151                 ])
  152         ]).
  153
  154radio(Radio, Field, Label, In) -->
  155    {   Field == In
  156    ->  Extra = [checked]
  157    ;   Extra = []
  158    },
  159    html([ input([ type(radio),
  160                   name(Radio),
  161                   value(Field)
  162                 | Extra
  163                 ]),
  164           Label
  165         ]).
  166
  167hidden(Name, Value) -->
  168    html(input([type(hidden), name(Name), value(Value)])).
  169
  170%!  search_reply(+For, +Options)// is det.
  171%
  172%   Generate a reply searching for For.  Options include
  173%
  174%           * resultFormat(Format)
  175%           If =summary= (default), produce a summary-table.  If
  176%           =long=, produce full object descriptions.
  177%
  178%           * search_in(In)
  179%           Determine which databases to search.  One of
  180%           =all=, =app=, =man=
  181%
  182%           * private(Boolean)
  183%           If `false` (default `true`), hide private predicates
  184%           from results.
  185%
  186%           * search_match(Match)
  187%           What part of the object to match. One of =name=,
  188%           =summary=
  189%
  190%           * header(+Boolean)
  191%           If =false=, suppress the header.
  192%
  193%           * per_page(+positive_integer)
  194%           Number of results per page (default 25).
  195%
  196%           * page(+positive_integer)
  197%           Page number to show results for (default 1).
  198
  199:- html_meta
  200    search_header(+, html, +, ?, ?).  201
  202search_reply(For, Options) -->
  203    { var(For) ; For == '' },
  204    !,
  205    search_header('', 'Using PlDoc search', Options),
  206    html([ ul( class('search-help'),
  207               [ li([ 'If you pause typing, the search box will display ',
  208                      'an auto completion list.  Selecting an object jumps ',
  209                      'immediately to the corresponding documentation.'
  210                    ]),
  211                 li([ 'Searching for ', i('Name/Arity'), ', ',
  212                      i('Name//Arity'), ', ', i('Name'), ' or ',
  213                      i('C-function()'), ' ensures that ',
  214                      'matching definitions appear first in the search ',
  215                      'results'
  216                    ]),
  217                 li([ 'Other searches search through the name and summary ',
  218                      'descriptions in the manual.'
  219                    ])
  220               ])
  221         ]).
  222search_reply(For, Options) -->
  223    { cached_search(For, PerCategory, Time, Options),
  224      PerCategory \== [],
  225      page_location(PerCategory, NPages, Offset, Limit, Options),
  226      option(resultFormat(Format), Options, summary)
  227    },
  228    !,
  229    search_header(For, [ 'Search results for ',
  230                         span(class(for), ['"', For, '"'])
  231                       ],
  232                  Options),
  233    { DisplayOptions = [ for(For),
  234                         cputime(Time),
  235                         page_count(NPages)
  236                       | Options
  237                       ]
  238    },
  239    indexed_matches(Format, PerCategory, Offset, Limit, DisplayOptions),
  240    search_pagination(DisplayOptions).
  241search_reply(For, Options) -->
  242    search_header(For, 'No matches', Options),
  243    html(div(class('search-no-matches'), 'No matches')).
  244
  245:- dynamic
  246    cached_search_result/4.  247
  248cached_search(For, Result, Time, Options) :-
  249    option(search_in(In), Options, all),
  250    option(search_match(Match), Options, summary),
  251    cached_search_result(For, In, Match, Result),
  252    !,
  253    Time = cached.
  254cached_search(For, Result, Time, Options) :-
  255    option(search_in(In), Options, all),
  256    option(search_match(Match), Options, summary),
  257    statistics(cputime, T0),
  258    search_doc(For, PerCategory0, Options),
  259    order_matches(PerCategory0, Result),
  260    statistics(cputime, T1),
  261    Time is T1-T0,
  262    assertz(cached_search_result(For, In, Match, Result)),
  263    prune_search_cache.
  264
  265prune_search_cache :-
  266    (   predicate_property(cached_search_result(_,_,_,_),
  267                           number_of_clauses(Count)),
  268        Del is Count - 25,
  269        Del > 0
  270    ->  forall(between(1,Del,_), retract(cached_search_result(_,_,_,_)))
  271    ;   true
  272    ).
  273
  274page_location(PerCategory, NPages, Offset, Limit, Options) :-
  275    option(page(Page), Options, 1),
  276    option(per_page(Limit), Options, 25),
  277    Offset is (Page-1)*Limit,
  278    count_matches(PerCategory, Total),
  279    NPages is (Total+Limit-1)//Limit.
  280
  281search_pagination(Options) -->
  282    { option(page(Page), Options, 1),
  283      option(page_count(NPages), Options, 1)
  284    },
  285    html(div(class(pagination),
  286             [ \search_prev(Page, Options),
  287               span(class(current), ['Page ', Page, ' of ', NPages]),
  288               \search_next(NPages, Page, Options)
  289             ])).
  290
  291search_prev(Page, _) -->
  292    { Page =:= 1 },
  293    !.
  294search_prev(Page, Options) -->
  295    { Prev is Page - 1,
  296      page_link(Prev, Link, Options)
  297    },
  298    html(a(href(Link), '< Prev')).
  299
  300search_next(NPages, Page, _) -->
  301    { Page =:= NPages, ! }, [].
  302search_next(_NPages, Page, Options) -->
  303    { Next is Page + 1,
  304      page_link(Next, Link, Options)
  305    },
  306    html(a(href(Link), 'Next >')).
  307
  308page_link(Page, '?'+QueryString, Options) :-
  309    option(for(For), Options),
  310    option(search_in(In), Options, all),
  311    option(search_match(Match), Options, summary),
  312    option(resultFormat(Format), Options, summary),
  313    uri_query_components(QueryString,
  314                         [ for(For),
  315                           in(In),
  316                           match(Match),
  317                           resultFormat(Format),
  318                           page(Page)
  319                         ]).
  320
  321search_header(_For, _Title, Options) -->
  322    { option(header(false), Options) },
  323    !,
  324    html_requires(pldoc).
  325search_header(For, Title, Options) -->
  326    html_requires(pldoc),
  327    doc_links('', [for(For)|Options]),
  328    html(h1(class('search-results'), Title)).
  329
  330%!  matching_object_table(+Objects, +Options)// is det.
  331%
  332%   Show a list of matching objects,   similar  to a result-set from
  333%   search.
  334
  335matching_object_table(Objects, Options) -->
  336    { maplist(obj_cat_sec, Objects, Pairs),
  337      group_hits(Pairs, Organized),
  338      option(format(Format), Options, summary)
  339    },
  340    indexed_matches(Format, Organized, Options).
  341
  342obj_cat_sec(Object, Cat-(Section-Object)) :-
  343    prolog:doc_object_summary(Object, Cat, Section, _Summary).
  344
  345indexed_matches(Format, PerCategory, Offset, Limit, Options) -->
  346    { cat_offset(Offset, _,PerCategory, PerCategory1),
  347      cat_limit(Limit, _, PerCategory1, PerCategory2)
  348    },
  349    (   { PerCategory2 == PerCategory }
  350    ->  indexed_matches(Format, PerCategory, Options)
  351    ;   category_counts(PerCategory,
  352                        [ showing('Total'),
  353                          link(category)
  354                        | Options
  355                        ]),
  356        { delete(Options, cputime(_), Options1) },
  357        category_counts(PerCategory2,
  358                        [ showing('Showing'),
  359                          class([showing])
  360                        | Options1
  361                        ]),
  362        search_pagination(Options),
  363        matches(Format, PerCategory2, Options)
  364    ).
  365
  366%!  cat_offset(+Offset, -Remaining, +PerCat0, -PerCat) is det.
  367%!  cat_limit(+Limit, -Remaining, +PerCat0, -PerCat) is det.
  368
  369cat_offset(0, 0, PerCat, PerCat) :-
  370    !.
  371cat_offset(N, R, [C-H|T], PerCat) :-
  372    H = [_-[_|_]|_],
  373    !,
  374    cat_offset(N, R1, H, H1),
  375    (   H1 == []
  376    ->  !, cat_offset(R1, R, T, PerCat)
  377    ;   PerCat = [C-H1|T]
  378    ).
  379cat_offset(N, R, [_C-L|T0], PerCat) :-
  380    length(L, Len),
  381    Left is N-Len,
  382    Left > 0,
  383    !,
  384    cat_offset(Left, R, T0, PerCat).
  385cat_offset(N, 0, [C-L0|T], [C-L|T]) :-
  386    !,
  387    length(Skip, N),
  388    append(Skip, L, L0).
  389cat_offset(N, N, Obj, Obj).
  390
  391cat_limit(0, 0, _PerCat, []) :-
  392    !.
  393cat_limit(N, R, [C-H|T], PerCat) :-
  394    H = [_-[_|_]|_],
  395    !,
  396    cat_limit(N, R1, H, H1),
  397    (   R1 == 0
  398    ->  PerCat = [C-H1]
  399    ;   PerCat = [C-H|T1],
  400        cat_limit(R1, R, T, T1)
  401    ).
  402cat_limit(N, R, [C-L|T0], [C-L|T]) :-
  403    length(L, Len),
  404    More is N - Len,
  405    More >= 0,
  406    !,
  407    cat_limit(More, R, T0, T).
  408cat_limit(N, 0, [C-L0|_], [C-L]) :-
  409    !,
  410    length(L, N),
  411    append(L, _, L0).
  412cat_limit(N, N, [], []).
  413
  414
  415%!  order_matches(+PerCat, -Ordered) is det.
  416%
  417%   Order matches per category. Each low level   object  is of the shape
  418%   q(Q, Object), where Q is a number  between   0  and  1, 1 implying a
  419%   perfect fit.
  420
  421order_matches(PerCat0, PerCat) :-
  422    maplist(order_category, PerCat0, PerCat).
  423
  424order_category(Cat-PerSection0, Cat-PerSection) :-
  425    maplist(order_section, PerSection0, PerSectionTagged),
  426    sort(1, >=, PerSectionTagged, Ordered),
  427    pairs_values(Ordered, PerSection).
  428
  429order_section(Section-Objects0, Q-(Section-Objects)) :-
  430    sort(1, >=, Objects0, Objects),
  431    maplist(arg(1), Objects, QList),
  432    join_quality(QList, Q).
  433
  434join_quality([], 0).
  435join_quality([Q], Q).
  436join_quality([QH|QL], Q) :-
  437    join_quality(QL, QT),
  438    Q is 1-(1-QH)*(1-QT).
  439
  440
  441%!  indexed_matches(+Format, +PerCategory, +Options)//
  442%
  443%   Emit the matches.
  444
  445indexed_matches(Format, PerCategory, Options) -->
  446    category_counts(PerCategory, Options),
  447    matches(Format, PerCategory, Options).
  448
  449category_counts(PerCategory, Options) -->
  450    { count_matches(PerCategory, Matches),
  451      option(class(Classes), Options, []),
  452      (   PerCategory = [_]
  453      ->  merge_options([link(false)], Options, Options1)
  454      ;   Options1 = Options
  455      )
  456    },
  457    html([ div(class(['search-counts'|Classes]),
  458               [ \category_showing(Options1),
  459                 Matches,
  460                 \count_by_category(PerCategory, Options1),
  461                 \search_time(Options1)
  462               ])
  463         ]).
  464
  465count_by_category([Cat-_PerFile], Options) -->
  466    !,
  467    html(' matches from '),
  468    category_link(Cat, Options).
  469count_by_category(PerCategory, Options) -->
  470    html(' matches; '),
  471    count_by_category_list(PerCategory, Options).
  472
  473count_by_category_list([], _) -->
  474    [].
  475count_by_category_list([Cat-PerFile|T], Options) -->
  476    { count_category(PerFile, Count) },
  477    html([ \category_link(Cat, Options), ': ', Count ]),
  478    (   {T == []}
  479    ->  []
  480    ;   html(', '),
  481        count_by_category_list(T, Options)
  482    ).
  483
  484count_matches([], 0).
  485count_matches([_-Cat|T], Count) :-
  486    count_matches(T, Count0),
  487    count_category(Cat, N),
  488    Count is Count0 + N.
  489
  490count_category([], 0).
  491count_category([_-Objs|T], Count) :-
  492    count_category(T, Count0),
  493    length(Objs, N),
  494    Count is Count0 + N.
  495
  496category_showing(Options) -->
  497    { option(showing(Showing), Options) },
  498    html(span(class('search-showing'), [Showing, :])).
  499category_showing(_) -->
  500    [].
  501
  502search_time(Options) -->
  503    { option(cputime(Time), Options) },
  504    !,
  505    (   { number(Time) }
  506    ->  html(span(class('search-time'), '(~2f sec.)'-[Time]))
  507    ;   html(span(class('search-time'), '(~w)'-[Time]))
  508    ).
  509search_time(_) -->
  510    [].
  511
  512%!  matches(+Format, +PerCategory, +Options)// is det
  513%
  514%   Display search matches according to Format.
  515%
  516%   @param PerCategory List of File-Objects
  517
  518matches(long, PerCategory, Options) -->
  519    long_matches_by_type(PerCategory, Options).
  520matches(summary, PerCategory, Options) -->
  521    html(table(class(summary),
  522               \short_matches_by_type(PerCategory, 1, Options))).
  523
  524
  525long_matches_by_type([], _) -->
  526    [].
  527long_matches_by_type([Category-PerFile|T], Options) -->
  528    category_header(Category, Options),
  529    long_matches(PerFile, Options),
  530    long_matches_by_type(T, Options).
  531
  532
  533long_matches([], _) -->
  534    [].
  535long_matches([File-Objs|T], Options) -->
  536    file_header(File, Options),
  537    objects(Objs, Options),
  538    long_matches(T, Options).
  539
  540category_header(Category, _Options) -->
  541    html(h1(class(category), \category_title(Category))).
  542
  543short_matches_by_type([], _, _) -->
  544    [].
  545short_matches_by_type([Category-PerFile|T], Nth, Options) -->
  546    category_index_header(Category, Nth, Options),
  547    short_matches(PerFile, Options),
  548    { succ(Nth, Nth1) },
  549    short_matches_by_type(T, Nth1, Options).
  550
  551short_matches([], _) -->
  552    [].
  553short_matches([File-Objs|T], Options) -->
  554    file_index_header(File, Options),
  555    object_summaries(Objs, File, Options),
  556    short_matches(T, Options).
  557
  558
  559category_index_header(Category, Nth, _Options) -->
  560    (   { Nth > 1 }
  561    ->  category_sep('category-top-sep')
  562    ;   []
  563    ),
  564    html(tr(th([class(category), colspan(3)],
  565               a(name(Category), \category_title(Category))))),
  566    category_sep('category-bottom-sep').
  567
  568category_sep(Which) -->
  569    html(tr(th([class(Which), colspan(3)],
  570               &(nbsp)))).
  571
  572category_link(Category, Options) -->
  573    { option(link(false), Options) },
  574    !,
  575    category_title(Category).
  576category_link(Category, Options) -->
  577    { option(link(category), Options),
  578      category_link(Category, HREF, Options)
  579    },
  580    !,
  581    html(a(href(HREF), \category_title(Category))).
  582category_link(Category, _Options) -->
  583    { atom_concat(#, Category, HREF) },
  584    html(a(href(HREF), \category_title(Category))).
  585
  586category_link(Category, '?'+QueryString, Options) :-
  587    (   category_abbreviation(Category, Abbrev)
  588    ->  true
  589    ;   Abbrev = Category
  590    ),
  591    option(for(For), Options),
  592    option(search_match(Match), Options, summary),
  593    option(resultFormat(Format), Options, summary),
  594    uri_query_components(QueryString,
  595                         [ for(For),
  596                           in(Abbrev),
  597                           match(Match),
  598                           resultFormat(Format)
  599                         ]).
  600
  601category_title(Category) -->
  602    {   prolog:doc_category(Category, _Order, Title)
  603    ->  true
  604    ;   Title = Category
  605    },
  606    html(Title).
  607
  608%!  search_doc(+SearchString, -PerType:list, +Options) is det.
  609%
  610%   Return matches of SearchString as Type-PerFile tuples, where PerFile
  611%   is a list File-ListOfObjects.
  612
  613search_doc(Search, PerType, Options) :-
  614    findall(Tuples, matching_object(Search, Tuples, Options), Tuples0),
  615    sort(Tuples0, Tuples),
  616    group_hits(Tuples, PerType0),
  617    prune_library(PerType0, PerType).
  618
  619group_hits(Tuples, PerType) :-
  620    keysort(Tuples, Tuples1),
  621    group_pairs_by_key(Tuples1, PerCat0),
  622    key_sort_order(PerCat0, PerCat1),
  623    keysort(PerCat1, PerCat2),
  624    pairs_values(PerCat2, PerCat),
  625    group_by_file(PerCat, PerType).
  626
  627key_sort_order([], []).
  628key_sort_order([Cat-ByCat|T0], [Order-(Cat-ByCat)|T]) :-
  629    (   prolog:doc_category(Cat, Order, _Title)
  630    ->  true
  631    ;   Order = 99
  632    ),
  633    key_sort_order(T0, T).
  634
  635
  636group_by_file([], []).
  637group_by_file([Type-Tuples0|T0], [Type-ByFile|T]) :-
  638    keysort(Tuples0, Tuples),
  639    group_pairs_by_key(Tuples, ByFile),
  640    group_by_file(T0, T).
  641
  642
  643%!  prune_library(+PerCat0, -PerCat) is det.
  644%
  645%   Prune objects from the libary that also appear in the manual.
  646
  647prune_library(PerCat0, PerCat) :-
  648    selectchk(library-InLib0, PerCat0, library-InLib, PerCat1),
  649    !,
  650    (   cat_objects(manual, PerCat0, Manual),
  651        cat_objects(packages, PerCat0, Packages),
  652        append(Manual, Packages, Objects),
  653        sort(Objects, OSet0),
  654        maplist(arg(2), OSet0, OSet),       % get rid of q(Q,Obj)
  655        convlist(prune_section(OSet), InLib0, InLib),
  656        InLib \== []
  657    ->  PerCat = PerCat1
  658    ;   selectchk(library-_, PerCat0, PerCat)
  659    ).
  660prune_library(PerCat, PerCat).
  661
  662cat_objects(Cat, PerCat, Objects) :-
  663    memberchk(Cat-Sections, PerCat),
  664    !,
  665    pairs_values(Sections, NestedObjects),
  666    append(NestedObjects, Objects).
  667cat_objects(_, _, []).
  668
  669prune_section(Prune, Section-Objects0, Section-Objects) :-
  670    exclude(in_set(Prune), Objects0, Objects),
  671    Objects \== [].                     % specific library becomes empty
  672
  673in_set(Prune, q(_Q,Obj)) :-
  674    memberchk(Obj, Prune),
  675    !.
  676in_set(Prune, q(_Q,_Module:Obj)) :-
  677    memberchk(Obj, Prune).
  678
  679
  680%!  matching_object(+SearchString, -Object, +Options) is nondet.
  681%
  682%   Object matches SearchString.  Options include
  683%
  684%     - search_in(In)
  685%       One of `all`, `app`, `man`.
  686%
  687%     - search_match(Match)
  688%       One of `name`, `summary`
  689%
  690%   @param Object   Term of the form File-Item
  691%   @tbd Deal with search syntax
  692
  693matching_object(Search, Type-(Section-q(1,Obj)), Options) :-
  694    atom_concat(Function, '()', Search),
  695    Obj = c(Function),
  696    option(search_in(In), Options, all),
  697    prolog:doc_object_summary(Obj, Type, Section, _),
  698    matching_category(In, Type).
  699matching_object(Search, Type-(Section-q(1,Obj)), Options) :-
  700    (   atom_pi(Search, Obj0)
  701    ->  ground(Obj0)
  702    ;   catch(atom_to_term(Search, Obj0, _), _, fail),
  703        nonvar(Obj0)
  704    ),
  705    opt_qualify(Obj0, Obj),
  706    option(search_in(In), Options, all),
  707    prolog:doc_object_summary(Obj, Type, Section, _),
  708    matching_category(In, Type).
  709matching_object(Search, Match, Options) :-
  710    atom_codes(Search, Codes),
  711    phrase(search_spec(For0), Codes),
  712    (   For0 = not(_)
  713    ->  throw(error(bad_search(only_not), _))
  714    ;   optimise_search(For0, For),
  715        exec_search(For, Match, Options)
  716    ).
  717
  718opt_qualify(Obj0, Obj) :-
  719    Obj0 = _:_,
  720    !,
  721    Obj = Obj0.
  722opt_qualify(Obj, Obj).
  723opt_qualify(Obj, _:Obj).
  724
  725
  726%!  optimise_search(+Spec, -Optimised)
  727%
  728%   Optimise a search specification. Currently   only deals with the
  729%   simple case of  first  searching  for   a  negation  and  then a
  730%   positive term.
  731
  732optimise_search(and(not(A0), B0), and(B, not(A))) :-
  733    !,
  734    optimise_search(A0, A),
  735    optimise_search(B0, B).
  736optimise_search(A, A).
  737
  738
  739%!  exec_search(+Spec, -Match, +Options) is nondet.
  740%
  741%   Spec is one of
  742%
  743%     - and(Spec, Spec)
  744%       Intersection of the specification
  745%     - not(Spec)
  746%       Negation of the specification
  747%     - quoted(Tokens)
  748%       A quoted list of tokens.
  749
  750exec_search(Spec, Match, Options) :-
  751    exec_search(Spec, Match0, Q, Options),
  752    add_quality(Match0, Q, Match).
  753
  754add_quality(Type-(Section-Obj), Q, Type-(Section-q(Q,Obj))).
  755
  756exec_search(and(A, B), Match, Q, Options) :-
  757    !,
  758    exec_search(A, Match, Q1, Options),
  759    exec_search(B, Match, Q2, Options),
  760    Q is 1-((1-Q1)*(1-Q2)).
  761exec_search(Search, Type-(Section-Obj), Q, Options) :-
  762    option(search_in(In), Options, all),
  763    option(search_match(Match), Options, summary),
  764    option(private(Public), Options, true),
  765    prolog:doc_object_summary(Obj, Type, Section, Summary),
  766    matching_category(In, Type),
  767    match_private(Public, Obj),
  768    (   Search = not(For)
  769    ->  State = s(0),
  770        \+ ( match_object(For, Obj, Summary, Match, Q),
  771             nb_setarg(1, State, Q)
  772           ),
  773        arg(1, State, Q)
  774    ;   match_object(Search, Obj, Summary, Match, Q)
  775    ).
  776
  777
  778matching_category(all, _).
  779matching_category(noapp, Category) :-
  780    !,
  781    Category \== application.
  782matching_category(Category, Category).
  783matching_category(Abbrev, Category) :-
  784    category_abbreviation(Category, Abbrev).
  785
  786category_abbreviation(application, app).
  787category_abbreviation(manual,      man).
  788category_abbreviation(library,     lib).
  789category_abbreviation(packages,    pack).
  790category_abbreviation(wiki,        wiki).
  791
  792match_private(true, _).
  793match_private(false, Object) :-
  794    (   Object = (Module:PI)
  795    ->  current_module(Module),
  796        pi_head(PI, Head),
  797        (   predicate_property(Module:Head, exported)
  798        ->  true
  799        ;   predicate_property(Module:Head, multifile)
  800        ->  true
  801        ;   predicate_property(Module:Head, public)
  802        )
  803    ;   true
  804    ).
  805
  806pi_head(Name/Arity, Head) :-
  807    functor(Head, Name, Arity).
  808pi_head(Name//Arity, Head) :-
  809    Arity1 is Arity+ 2,
  810    functor(Head, Name, Arity1).
  811
  812%!  search_spec(-Search)// is det.
  813%
  814%   Break a search string from the user into a logical expression.
  815
  816search_spec(Spec) -->
  817    blanks,
  818    prim_search_spec(A),
  819    blanks,
  820    (   eos
  821    ->  { Spec = A }
  822    ;   search_spec(B)
  823    ->  { Spec = and(A,B) }
  824    ).
  825
  826prim_search_spec(quoted(Quoted)) -->
  827    "\"", string(Codes), "\"",
  828    !,
  829    { tokenize_atom(Codes, Quoted)
  830    }.
  831prim_search_spec(Spec) -->
  832    nonblanks(Codes),
  833    {   Codes = [0'-,C0|Rest],
  834        code_type(C0, csym)
  835    ->  atom_codes(Word, [C0|Rest]),
  836        Spec = not(Word)
  837    ;   Codes \== [],
  838        atom_codes(Spec, Codes)
  839    }.
  840
  841
  842%!  object_summary(?Object, ?Category, ?Section, ?Summary) is nondet.
  843%
  844%   True  if  Object  is  summarised   by  Summary.  This  multifile
  845%   predicate can be extended  with   other  search  mechanisms. The
  846%   returned objects must be  handled   by  object_summaries//2  and
  847%   objects//2.
  848%
  849%   @param Category Atom describing the source.
  850%   @param Section  Reference to the context of Object.
  851
  852prolog:doc_object_summary(Obj, Category, File, Summary) :-
  853    once(prolog_object(Obj)),
  854    current_prolog_flag(home, SWI),
  855    doc_comment(Obj0, File:_Line, Summary, _Comment),
  856    (   is_list(Obj0)
  857    ->  member(Obj, Obj0)
  858    ;   Obj = Obj0
  859    ),
  860    Obj \= _:module(_Title),                % HACK.  See ref_object//1
  861    (   sub_atom(File, 0, _, _, SWI)
  862    ->  Category = library
  863    ;   Category = application
  864    ).
  865
  866prolog_object(Var) :- var(Var), !.
  867prolog_object(_/_).
  868prolog_object(_//_).
  869prolog_object(_:_/_).
  870prolog_object(_:_//_).
  871prolog_object(module(_)).
  872
  873
  874%!  doc_category(Name, SortOrder, Description) is nondet.
  875%
  876%   Describe the various  categories  of   search  results.  Used to
  877%   create the category headers  as  well   as  the  advanced search
  878%   dialog.
  879%
  880%   @param SortOrder        Ranges 0..100.  Lower values come first
  881
  882prolog:doc_category(application, 20, 'Application').
  883prolog:doc_category(library,     80, 'System Libraries').
  884
  885
  886                 /*******************************
  887                 *             UTIL             *
  888                 *******************************/
  889
  890%!  match_object(+For, +Object, +Summary, +How, -Quality) is semidet.
  891%
  892%   True when Object with summary text Summary matches For acording to
  893%   How.
  894%
  895%   @arg For is either a token (atom) or a term quoted(Tokens), where
  896%   `Tokens` is a list of atoms.
  897%   @arg How is one of `name` or `summary`
  898%   @arg Quality is a number in the range 0..1, where 1 means a strong
  899%   match.
  900
  901match_object(For, Object, Summary, How, Quality) :-
  902    (   doc_object_identifier(Object, Identitier),
  903        identifier_match_quality(For, Identitier, Quality)
  904    ->  debug(search(rank), 'Rank "~w" in identifier "~w": ~q',
  905              [For, Identitier, Quality])
  906    ;   How == summary,
  907        summary_match_quality(For, Summary, Quality),
  908        debug(search(rank), 'Rank "~w" in summary "~w": ~q',
  909              [For, Summary, Quality])
  910    ).
  911
  912summary_match_quality(For, Summary, Q) :-
  913    tokenize_atom(Summary, Tokens0),
  914    exclude(is_punctuation, Tokens0, Tokens),
  915    Tokens \== [],
  916    token_match_quality(summary, For, Tokens, Q0),
  917    Q is Q0/2.
  918
  919is_punctuation(Token) :-
  920    atom_length(Token, 1),
  921    char_type(Token, punct).
  922
  923
  924identifier_match_quality(Identifier, Identifier, 1) :-
  925    !.
  926identifier_match_quality(For, Identifier, Q) :-
  927    dwim_match(For, Identifier, _),
  928    !,
  929    Q = 0.8.
  930identifier_match_quality(For, Identifier, Q) :-
  931    identifier_parts(Identifier, Parts),
  932    Parts \== [],
  933    token_match_quality(identifier, For, Parts, Q).
  934
  935token_match_quality(_How, quoted(Tokens), Parts, Q) :-
  936    !,
  937    append(Tokens, _, All),
  938    append(_, All, Parts),
  939    Q = 1.
  940token_match_quality(How, For, Parts, Q) :-
  941    length(Parts, Len),
  942    (   memberchk(For, Parts)
  943    ->  Q0 = 1
  944    ;   porter_stem(For, Stem),
  945        member(Part, Parts),
  946        porter_stem(Part, Stem)
  947    ->  Q0 = 0.9
  948    ;   How == summary,
  949        member(Part, Parts),
  950        sub_atom_icasechk(Part, _, For),
  951        identifier_parts(Part, SubParts),
  952        token_match_quality(identifier, For, SubParts, Q00)
  953    ->  Q0 is Q00/2
  954    ;   How == summary,
  955        member(Part, Parts),
  956        sub_atom_icasechk(Part, 0, For),
  957        is_numbered_var(Part, For)
  958    ->  Q0 is 0.9
  959    ;   doc_related_word(For, Word, Distance),
  960        memberchk(Word, Parts)
  961    ->  Q0 = Distance
  962    ),
  963    Q is Q0/Len.
  964
  965is_numbered_var(VarName, Search) :-
  966    atom_length(Search, Len),
  967    sub_string(VarName, Len, _, 0, NS),
  968    number_string(_, NS),
  969    sub_atom(VarName, 0, 1, _, First),
  970    char_type(First, prolog_var_start)