diff options
| author | Stephen Blott | 2015-05-05 16:43:48 +0100 |
|---|---|---|
| committer | Stephen Blott | 2015-05-05 16:43:48 +0100 |
| commit | 28807bd25b27e5404228a638f2ab5e6c00f606cc (patch) | |
| tree | ce0d1f8aef6911cc250a7e1b57d4b553333eeb90 /background_scripts/completion_engines.coffee | |
| parent | 43bdd2787f2bffc4fc9c3397937a0ce9a183beda (diff) | |
| download | vimium-28807bd25b27e5404228a638f2ab5e6c00f606cc.tar.bz2 | |
Search completion; misc.
Diffstat (limited to 'background_scripts/completion_engines.coffee')
| -rw-r--r-- | background_scripts/completion_engines.coffee | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee new file mode 100644 index 00000000..0177806a --- /dev/null +++ b/background_scripts/completion_engines.coffee @@ -0,0 +1,219 @@ + +# A completion engine provides search suggestions for a search engine. A search engine is identified by a +# "searchUrl", e.g. Settings.get("searchUrl"), or a custom search engine. +# +# Each completion engine defines three functions: +# +# 1. "match" - This takes a searchUrl, and returns a boolean indicating whether this completion engine can +# perform completion for the given search engine. +# +# 2. "getUrl" - This takes a list of query terms (queryTerms) and generates a completion URL, that is, a URL +# which will provide completions for this completion engine. +# +# 3. "parse" - This takes a successful XMLHttpRequest object (the request has completed successfully), and +# returns a list of suggestions (a list of strings). +# +# The main completion entry point is CompletionEngines.complete(). This implements all lookup and caching +# logic. It is possible to add new completion engines without changing the CompletionEngines infrastructure +# itself. + +# A base class for common regexp-based matching engines. +class RegexpEngine + constructor: (@regexps) -> + match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl + +# Several Google completion engines package responses in this way. +class GoogleXMLRegexpEngine extends RegexpEngine + parse: (xhr) -> + for suggestion in xhr.responseXML.getElementsByTagName "suggestion" + continue unless suggestion = suggestion.getAttribute "data" + suggestion + +class Google extends GoogleXMLRegexpEngine + # Example search URL: http://www.google.com/search?q=%s + constructor: -> + super [ + # We match the major English-speaking TLDs. + new RegExp "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/" + new RegExp "localhost/cgi-bin/booky" # Only for smblott. + ] + + getUrl: (queryTerms) -> + "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}" + +class Youtube extends GoogleXMLRegexpEngine + # Example search URL: http://www.youtube.com/results?search_query=%s + constructor: -> + super [ new RegExp "^https?://[a-z]+\.youtube\.com/results" ] + + getUrl: (queryTerms) -> + "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=#{Utils.createSearchQuery queryTerms}" + +class Wikipedia extends RegexpEngine + # Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s + constructor: -> + super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ] + + getUrl: (queryTerms) -> + "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=#{Utils.createSearchQuery queryTerms}" + + parse: (xhr) -> + JSON.parse(xhr.responseText)[1] + +## class GoogleMaps extends RegexpEngine +## # Example search URL: https://www.google.com/maps/search/%s +## constructor: -> +## super [ new RegExp "^https?://www\.google\.com/maps/search/" ] +## +## getUrl: (queryTerms) -> +## console.log "xxxxxxxxxxxxxxxxxxxxx" +## "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}" +## +## parse: (xhr) -> +## console.log "yyy", xhr.responseText +## data = JSON.parse xhr.responseText +## console.log "zzz" +## console.log data +## [] + +class Bing extends RegexpEngine + # Example search URL: https://www.bing.com/search?q=%s + constructor: -> super [ new RegExp "^https?://www\.bing\.com/search" ] + getUrl: (queryTerms) -> "http://api.bing.com/osjson.aspx?query=#{Utils.createSearchQuery queryTerms}" + parse: (xhr) -> JSON.parse(xhr.responseText)[1] + +class Amazon extends RegexpEngine + # Example search URL: http://www.amazon.com/s/?field-keywords=%s + constructor: -> super [ new RegExp "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/" ] + getUrl: (queryTerms) -> "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=#{Utils.createSearchQuery queryTerms}" + parse: (xhr) -> JSON.parse(xhr.responseText)[1] + +class DuckDuckGo extends RegexpEngine + # Example search URL: https://duckduckgo.com/?q=%s + constructor: -> super [ new RegExp "^https?://([a-z]+\.)?duckduckgo\.com/" ] + getUrl: (queryTerms) -> "https://duckduckgo.com/ac/?q=#{Utils.createSearchQuery queryTerms}" + parse: (xhr) -> + suggestion.phrase for suggestion in JSON.parse xhr.responseText + +# A dummy search engine which is guaranteed to match any search URL, but never produces completions. This +# allows the rest of the logic to be written knowing that there will be a search engine match. +class DummySearchEngine + match: -> true + # We return a useless URL which we know will succeed, but which won't generate any network traffic. + getUrl: -> chrome.runtime.getURL "content_scripts/vimium.css" + parse: -> [] + +completionEngines = [ + Youtube + Google + DuckDuckGo + Wikipedia + Bing + Amazon + DummySearchEngine +] + +CompletionEngines = + debug: true + + get: (searchUrl, url, callback) -> + xhr = new XMLHttpRequest() + xhr.open "GET", url, true + xhr.timeout = 1000 + xhr.ontimeout = xhr.onerror = -> callback null + xhr.send() + + xhr.onreadystatechange = -> + if xhr.readyState == 4 + callback(if xhr.status == 200 then xhr else null) + + # Look up the search-completion engine for this searchUrl. Because of DummySearchEngine, above, we know + # there will always be a match. Imagining that there may be many completion engines, and knowing that this + # is called for every input event in the vomnibar, we cache the result. + lookupEngine: (searchUrl) -> + @engineCache ?= new SimpleCache 30 * 60 * 60 * 1000 # 30 hours (these are small, we can keep them longer). + if @engineCache.has searchUrl + @engineCache.get searchUrl + else + for engine in completionEngines + engine = new engine() + return @engineCache.set searchUrl, engine if engine.match searchUrl + + # This is the main (actually, the only) entry point. + # - searchUrl is the search engine's URL, e.g. Settings.get("searchUrl"), or a custome search engine's URL. + # This is only used as a key for determining the relevant completion engine. + # - queryTerms are the queryTerms. + # - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes + # wrong). + complete: (searchUrl, queryTerms, callback) -> + @mostRecentHandler = null + + # We can't complete empty queries. + return callback [] unless 0 < queryTerms.length + + if 1 == queryTerms.length + # We don't complete URLs. + return callback [] if Utils.isUrl queryTerms[0] + # We don't complete less then three characters: the results are usually useless. This also prevents + # one- and two-character custom search engine keywords from being sent to the default completer (e.g. + # the initial "w" before typing "w something"). + return callback [] unless 2 < queryTerms[0].length + + # We don't complete Javascript URLs. + return callback [] if Utils.hasJavascriptPrefix queryTerms[0] + + # Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need + # to generate a key. We mix in some junk generated by pwgen. A key clash is possible, but vanishingly + # unlikely. + junk = "//Zi?ei5;o//" + completionCacheKey = searchUrl + junk + queryTerms.join junk + @completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries. + if @completionCache.has completionCacheKey + console.log "hit", completionCacheKey if @debug + return callback @completionCache.get completionCacheKey + + fetchSuggestions = (callback) => + engine = @lookupEngine searchUrl + url = engine.getUrl queryTerms + console.log "get", url if @debug + query = queryTerms.join(" ").toLowerCase() + @get searchUrl, url, (xhr = null) => + # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In + # all cases, we fall back to the catch clause, below. + try + suggestions = engine.parse xhr + # Make sure we really do have an iterable of strings. + suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion) + # Filter out the query itself. It's not adding anything. + suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query) + catch + suggestions = [] + # We cache failures, but remove them after just ten minutes. This (it is hoped) avoids repeated + # XMLHttpRequest failures over a short period of time. + removeCompletionCacheKey = => @completionCache.set completionCacheKey, null + setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes. + + callback suggestions + + # We pause in case the user is still typing. + Utils.setTimeout 200, handler = @mostRecentHandler = => + if handler != @mostRecentHandler # Bail if another completion has begun, or the user is typing. + console.log "bail", completionCacheKey if @debug + return callback [] + # Don't allow duplicate identical active requests. This can happen, for example, when the user enters or + # removes a space, or when they enter a character and immediately delete it. + @inTransit ?= {} + unless @inTransit[completionCacheKey]?.push callback + queue = @inTransit[completionCacheKey] = [] + fetchSuggestions (suggestions) => + callback @completionCache.set completionCacheKey, suggestions + delete @inTransit[completionCacheKey] + console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length + callback suggestions for callback in queue + + userIsTyping: -> + console.log "reset (typing)" if @debug and @mostRecentHandler? + @mostRecentHandler = null + +root = exports ? window +root.CompletionEngines = CompletionEngines |
