/* Part of ClioPatria SeRQL and SPARQL server Author: Jan Wielemaker E-mail: J.Wielemaker@vu.nl WWW: http://www.swi-prolog.org Copyright (c) 2010-2018, University of Amsterdam, VU University Amsterdam All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ :- module(cp_simple_search, [ simple_search_form//0, simple_search_form//1, % +Options search_filter/2 ]). :- use_module(library(http/http_json)). :- use_module(library(http/http_parameters)). :- use_module(library(http/http_path)). :- use_module(library(http/http_dispatch)). :- use_module(library(http/http_wrapper)). :- use_module(library(http/html_write)). :- use_module(library(http/html_head)). :- use_module(library(semweb/rdf_db)). :- use_module(library(semweb/rdfs)). :- use_module(library(semweb/rdf_label)). :- use_module(library(option)). :- use_module(components(basics)). :- http_handler(api(ac_find_literal), ac_find_literal, []). /** Simple literal search */ %! simple_search_form// is det. %! simple_search_form(+Options)// is det. % % Provide a search form to find literals in the database. Options % processed: % % * id(ID) % Identifier-base for the search-box. The actual box is called % ID_ % * filter(+Filter) % Restrict results to resources that satisfy filter. Filtering % is implemented by search_filter/2. % * select_handler(+HandlerID) % ID of the handler called if the user selects a completion. The % handler is called with q= % * submit_handler(+HandlerID) % ID of the handler called if the user submits using the button. % The handler is called with q= % * label(Label) % Label of the search-button. Default is _Search_. % * value(Value) % Initial value of the search-box % * width(Width) % Width of the input box (default is =25em=). Must be a CSS % width. simple_search_form --> simple_search_form([]). simple_search_form(Options) --> { option(label(Label), Options, 'Search'), option(submit_handler(Search), Options, search) }, html(form([ id(search_form), action(location_by_id(Search)) ], [ div([ \search_box([ name(q) | Options ]), \filter(Options), \select_handler(Options), input([ type(submit), value(Label) ]) ]) ])). filter(Options) --> { option(filter(Filter), Options), !, term_to_atom(Filter, FilterAtom) }, hidden(filter, FilterAtom). filter(_) --> []. select_handler(Options) --> { option(select_handler(Handler), Options) }, !, hidden(handler, Handler). select_handler(_) --> []. max_results_displayed(100). search_box(Options) --> { max_results_displayed(Max) }, autocomplete(ac_find_literal, [ query_delay(0.2), auto_highlight(false), max_results_displayed(Max), width('30ex') | Options ]). %! autocomplete(+HandlerID, +Options)// is det. % % Insert a YUI autocomplete widget that obtains its alternatives % from HandlerID. The following Options are supported: % % * width(+Width) % Specify the width of the box. Width must satisfy the CSS % length syntax. % % * query_delay(+Seconds) % Wait until no more keys are typed for Seconds before sending % the query to the server. autocomplete(Handler, Options) --> { option(id(ID), Options, ac_find_literal), atom_concat(ID, '_complete', CompleteID), atom_concat(ID, '_input', InputID), atom_concat(ID, '_container', ContainerID), select_option(width(Width), Options, Options1, '25em'), select_option(name(Name), Options1, Options2, predicate), select_option(value(PValue), Options2, Options3, ''), expand_value(PValue, Value) }, html([ \html_requires(yui('autocomplete/autocomplete.js')), \html_requires(yui('autocomplete/assets/skins/sam/autocomplete.css')), div([ id(CompleteID), class(ac_input) ], [ input([ id(InputID), name(Name), value(Value), type(text) ]), div(id(ContainerID), []) ]), style(type('text/css'), [ '#', CompleteID, '\n', '{ width:~w; padding-bottom:0em; display:inline-block; vertical-align:top}'-[Width] ]), \autocomplete_script(Handler, InputID, ContainerID, Options3) ]). %! expand_value(ValueIn, Value) % % Allow for e.g., p(q) to use the value from the HTTP-parameter % =q=. expand_value(p(Name), Value) :- !, ( http_current_request(Request), memberchk(search(Search), Request), memberchk(Name=PValue, Search) -> Value = PValue ; Value = '' ). expand_value(Value, Value). highlight --> html(script(type('text/javascript'), \[ 'function highlighMatches(str, query, cls)\n', '{ var pat = new RegExp(query, "gi"); var sa = str.split(pat); var ma = str.match(pat); var i; var out = sa[0];\n', ' if ( !ma ) { return str; }\n', ' for(i=0; i"+ma[i++]+""; out += sa[i]; }\n', 'return out; }\n' ])). autocomplete_script(HandlerID, Input, Container, Options) --> { http_link_to_id(HandlerID, [], Path), option(filter(Filter), Options, true), term_to_atom(Filter, FilterAtom), uri_query_components(QS, [filter(FilterAtom)]) }, highlight, html(script(type('text/javascript'), \[ '{ \n', ' var oDS = new YAHOO.util.XHRDataSource("~w");\n'-[Path], ' oDS.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;\n', ' oDS.responseSchema = { resultsList:"results", \t\t\t fields:["label","count","href"] \t\t\t};\n', ' oDS.maxCacheEntries = 5;\n', ' var oAC = new YAHOO.widget.AutoComplete("~w", "~w", oDS);\n'-[Input, Container], ' oAC.resultTypeList = false;\n', ' oAC.formatResult = function(oResultData, sQuery, sResultMatch) { var sLabel = highlighMatches(oResultData.label, sQuery, "acmatch"); if ( oResultData.count > 1 ) { sLabel += " ("+oResultData.count+")"; } return sLabel; };\n', ' oAC.itemSelectEvent.subscribe(function(sType, aArgs) { var oData = aArgs[2]; window.location.href = oData.href; });\n', ' oAC.generateRequest = function(sQuery) { return "?~w&query=" + sQuery ; };\n'-[QS], \ac_options(Options), '}\n' ])). ac_options([]) --> []. ac_options([H|T]) --> ac_option(H), ac_options(T). ac_option(query_delay(Time)) --> !, html([ ' oAC.queryDelay = ~w;\n'-[Time] ]). ac_option(auto_highlight(Bool)) --> !, html([ ' oAC.autoHighlight = ~w;\n'-[Bool] ]). ac_option(max_results_displayed(Max)) --> !, html([ ' oAC.maxResultsDisplayed = ~w;\n'-[Max] ]). ac_option(_) --> []. %! ac_find_literal(+Request) % % Perform autocompletion for literals and resources. The reply is % a JSON object that is normally used in a YUI autocomplete % widget. ac_find_literal(Request) :- max_results_displayed(DefMax), http_parameters(Request, [ query(Query, [ description('Prefix for literals to find') ]), filter(FilterAtom, [ optional(true), description('Filter on raw matches (a Prolog term)') ]), handler(Handler, [ default(list_triples_with_literal), description('Callback handler on selection') ]), maxResultsDisplayed(Max, [ integer, default(DefMax), description('Maximum number of results displayed') ]) ]), ( var(FilterAtom) -> Filter = true ; atom_to_term(FilterAtom, Filter0, []), rdf_global_term(Filter0, Filter) ), autocompletions(Query, Filter, Handler, Max, Count, Completions), reply_json(json([ query = json([ count=Count ]), results = Completions ])). autocompletions(Query, Filter, Handler, Max, Count, Completions) :- autocompletions(prefix(label), Query, Filter, Handler, Max, BNC, ByName), ( BNC > Max -> Completions = ByName, Count = BNC ; TMax is Max-BNC, autocompletions(prefix(other), Query, Filter, Handler, TMax, BTC, ByToken), append(ByName, ByToken, Completions), Count is BNC+BTC ). autocompletions(How, Query, Filter, Handler, Max, Count, Completions) :- ac_objects(How, Query, Filter, Completions0), length(Completions0, Count), first_n(Max, Completions0, Completions1), maplist(obj_result(Handler), Completions1, Completions). obj_result(Handler, Text-Count, json([ label=Text, count=Count, href=Href ])) :- object_href(Handler, Text, Href). object_href(Handler, Text, Link) :- !, http_link_to_id(Handler, [ q=Text ], Link). first_n(0, _, []) :- !. first_n(_, [], []) :- !. first_n(N, [H|T0], [H|T]) :- N2 is N - 1, first_n(N2, T0, T). %! ac_objects(+How, +Query, +Filter, -Objects) % % @param Objects is a list of Text-Count pairs ac_objects(How, Query, Filter, Objects) :- findall(Pair, ac_object(How, Query, Filter, Pair), Pairs), keysort(Pairs, KSorted), group_pairs_by_key(KSorted, Grouped), maplist(hit_count, Grouped, Objects). hit_count(Text-Resources, Text-Count) :- length(Resources, Count). % duplicates? %! ac_object(+How, +Query, +Filter, -Object) ac_object(prefix(label), Query, Filter, Text-Resource) :- ac_candidate(Query, Filter, Resource, P, Literal), ( label_property(LP), rdfs_subproperty_of(P, LP) -> literal_text(Literal, Text) ). ac_object(prefix(other), Query, Filter, Text-Resource) :- ac_candidate(Query, Filter, Resource, P, Literal), ( label_property(LP), rdfs_subproperty_of(P, LP) -> fail ; literal_text(Literal, Text) ). ac_candidate(Query, Filter, R, P, literal(Literal)) :- ( sub_term(graph(Graph), Filter) -> rdf(R, P, literal(prefix(Query), Literal), Graph) ; rdf(R, P, literal(prefix(Query), Literal)) ), search_filter(Filter, R). %! search_filter(+Filter, +Resource) is semidet. % % True if Filter holds for Resource. Defined filters are: % % * true % Always true % * graph(Graph) % The triple providing the literal must reside in Graph. search_filter(true, _) :- !. search_filter(graph(_), _) :- !. % already filtered search_filter(Filter, _) :- domain_error(filter, Filter).