aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/completion.js626
-rw-r--r--lib/dom_utils.js (renamed from lib/domUtils.js)0
-rw-r--r--lib/keyboard_utils.js (renamed from lib/keyboardUtils.js)0
3 files changed, 0 insertions, 626 deletions
diff --git a/lib/completion.js b/lib/completion.js
deleted file mode 100644
index b31c4918..00000000
--- a/lib/completion.js
+++ /dev/null
@@ -1,626 +0,0 @@
-/*
- * This contains the definition of the completers used for the Vomnibox's suggestion UI. A complter will take
- * a query (whatever the user typed into the Vomnibox) and return a list of matches, e.g. bookmarks, domains,
- * URLs from history.
- *
- * The Vomnibox frontend script makes a "filterCompleter" request to the background page, which in turn calls
- * filter() on each these completers.
- *
- * A completer is a class which has two functions:
- * - refresh(): refreshes the completer's data source (e.g. refetches the list of bookmarks from Chrome).
- * - filter(query, callback): "query" will be whatever the user typed into the Vomnibox. "callback" is a
- * function which will be invoked with a list of LazyCompletionResults as its first argument.
- *
- * A completer's filter() function returns a list of LazyCompletionResults. This contains a relevancy score
- * for the result, as well as a function to build the full result (e.g. the HTML representing this result).
- *
- * The MultiCompleter collects a big list of LazyCompletionResults from many completers by calling each of
- * their filter functions in turn, sorts the results by relevancy, and then calls build() on the top N
- * results. This allows us to avoid generating HTML for all of the results we're not going to use.
- * The objects returned from build() are sent to the Vomnibox frontend script to be shown in the UI.
- */
-var completion = (function() {
-
- /*
- * An object which contains a relevancy score for the given completion, and a function which can be
- * invoked to build its HTML.
- * Calling build() should return an object of the form:
- * { html: "", action: { functionName: "", args: [] } }
- * This object is eventually sent back to the Vomnibox frontend script. "action" contains the action to
- * be performed by the frontend script if this result is chosen (user selects it and hits enter).
- * "action" includes the function the frontendScript should execute (e.g. "navigateToUrl") along with any
- * arguments (like the URL).
- * "html" is the HTML representation of this result, with some characters emphasized to higlight the query.
- *
- * This is called "lazy" because it takes in a function to lazily compute a result's html. That operation
- * can be kind of expensive, so you only want to do it to the top completion results, after you've sorted
- * them by relevancy.
- */
- var LazyCompletionResult = function(relevancy, buildFunction) {
- this.relevancy = relevancy;
- this.build = buildFunction;
- }
-
- /*
- * A completer which takes in a list of keyword commands (like "wiki" for "search wikipedia") and will
- * decide if your query is a command, a URL that you want to visit, or a search term.
- */
- var SmartKeywordCompleter = Class.extend({
- /*
- * - commands: a list of commands of the form: { keyword: [title, url] }, e.g.:
- * { "wiki ": ["Wikipedia (en)", "http://en.wikipedia.org/wiki/%s" ]
- */
- init: function(commands) {
- this.commands = commands || {};
- this.commandKeys = Object.keys(commands);
- },
-
- refresh: function() { },
-
- /** Returns the suggestions matching the user-defined commands */
- getCommandSuggestions: function(query, suggestions) {
- return this.commandKeys.filter(function(cmd) { return query.indexOf(cmd) == 0 }).map(function(cmd) {
- var term = query.slice(cmd.length);
- var desc = this.commands[cmd][0];
- var pattern = this.commands[cmd][1];
- var url = (typeof pattern == "function") ? pattern(term) : pattern.replace(/%s/g, term);
-
- // this will appear even before the URL/search suggestion
- return new LazyCompletionResult(-2, function() {
- return {
- html: createCompletionHtml(desc, term),
- action: { functionName: "navigateToUrl", args: [utils.createFullUrl(url)] },
- }})
- }.proxy(this));
- },
-
- /** Checks if the input is a URL. If yes, returns a suggestion to visit it. If no, returns a suggestion
- * to start a web search. */
- getUrlOrSearchSuggestion: function(query, suggestions) {
- // trim query
- query = query.replace(/^\s+|\s+$/g, '');
- var isUrl = utils.isUrl(query);
- return new LazyCompletionResult(-1, function() {
- return {
- html: createCompletionHtml(isUrl ? "goto" : "search", query),
- action: { functionName: "navigateToUrl",
- args: isUrl ? [utils.createFullUrl(query)] : [utils.createSearchUrl(query)] }
- }});
- },
-
- filter: function(query, callback) {
- suggestions = this.getCommandSuggestions(query);
- suggestions.push(this.getUrlOrSearchSuggestion(query));
- callback(suggestions);
- }
- });
-
- /*
- * A generic asynchronous completer which is used by completers which have asynchronous data sources,
- * like history or bookmarks.
- */
- var AsyncCompleter = Class.extend({
- init: function() {
- this.id = utils.createUniqueId();
- this.reset();
- this.resultsReady = this.fallbackReadyCallback = function(results) { this.completions = results; }
- },
-
- reset: function() {
- fuzzyMatcher.invalidateFilterCache(this.id);
- this.completions = null;
- },
-
- /*
- * This creates an intermediate representation of a completion which will later be called with a specific
- * query.
- * - type: the type of item we're completing against, e.g. "bookmark", "history", "tab".
- * - item: the item itself. This should include a url and title property (bookmark, history and tab
- * objects include both of these).
- * - action: the action to take in the Vomnibox frontend
- *
- * It's used to save us work -- we call this on every bookmark in your bookmarks list when we first fetch
- * them, for instance, and we don't want to do some the same work again every time a new query is
- * processed.
- *
- * TODO(philc): It would be nice if this could be removed; it's confusing.
- * */
- createUnrankedCompletion: function(type, item, action) {
- var url = item.url;
- var parts = [type, url, item.title];
- var str = parts.join(" ");
- action = action || { functionName: "navigateToUrl", args: [url] };
-
- function createLazyCompletion(query) {
- var relevancy = url.length / fuzzyMatcher.calculateRelevancy(query, str)
- return new LazyCompletionResult(relevancy, function() {
- return {
- html: renderFuzzy(query, createCompletionHtml.apply(null, parts)),
- action: action,
- }});
- }
-
- // add one more layer of indirection: For filtering, we only need the string to match.
- // Only after we reduced the number of possible results, we call :bind on them to get
- // an actual completion object
- return {
- completionString: parts.join(" "),
- createLazyCompletion: createLazyCompletion,
- }
- },
-
- processResults: function(query, results) {
- results = fuzzyMatcher.filter(query, results,
- function(match) { return match.completionString }, this.id);
- return results.map(function(result) { return result.createLazyCompletion(query); });
- },
-
- filter: function(query, callback) {
- var handler = function(results) { callback(this.processResults(query, results)); }.proxy(this);
-
- // are the results ready?
- if (this.completions !== null) {
- // yes: call the callback synchronously
- handler(this.completions);
- } else {
- // no: register the handler as a callback
- this.resultsReady = function(results) {
- handler(results);
- this.resultsReady = this.fallbackReadyCallback;
- this.resultsReady(results);
- }.proxy(this);
- }
- }
- });
-
- var FuzzyBookmarkCompleter = Class.extend({
- init: function() { this.asyncCompleter = new AsyncCompleter(); },
- filter: function(query, callback) { return this.asyncCompleter.filter(query, callback); },
-
- // Traverses the bookmark hierarhcy and retuns a list of all bookmarks in the tree.
- traverseBookmarkTree: function(bookmarks) {
- var results = [];
- var toVisit = bookmarks;
- while (toVisit.length > 0) {
- var bookmark = toVisit.shift();
- results.push(bookmark);
- if (bookmark.children)
- toVisit.push.apply(toVisit, bookmark.children);
- }
- return results;
- },
-
- refresh: function() {
- this.asyncCompleter.reset();
- chrome.bookmarks.getTree(function(bookmarks) {
- var results = this.traverseBookmarkTree(bookmarks);
- var validResults = results.filter(function(b) { return b.url !== undefined; });
- var matches = validResults.map(function(bookmark) {
- return this.asyncCompleter.createUnrankedCompletion("bookmark", bookmark);
- }.proxy(this));
- this.asyncCompleter.resultsReady(matches);
- }.proxy(this));
- }
- });
-
- var FuzzyHistoryCompleter = Class.extend({
- init: function(maxResults) {
- this.asyncCompleter = new AsyncCompleter();
- this.maxResults = maxResults;
- },
-
- filter: function(query, callback) { return this.asyncCompleter.filter(query, callback); },
-
- refresh: function() {
- this.asyncCompleter.reset();
-
- historyCache.use(function(history) {
- this.asyncCompleter.resultsReady(history.slice(-this.maxResults).map(function(item) {
- return this.asyncCompleter.createUnrankedCompletion("history", item);
- }.proxy(this)));
- }.proxy(this));
- }
- });
-
- var FuzzyTabCompleter = Class.extend({
- init: function() { this.asyncCompleter = new AsyncCompleter(); },
-
- filter: function(query, callback) { return this.asyncCompleter.filter(query, callback); },
-
- refresh: function() {
- this.asyncCompleter.reset();
- chrome.tabs.getAllInWindow(null, function(tabs) {
- this.asyncCompleter.resultsReady(tabs.map(function(tab) {
- return this.asyncCompleter.createUnrankedCompletion("tab", tab,
- { functionName: "switchToTab", args: [tab.id] });
- }.proxy(this)));
- }.proxy(this));
- }
- });
-
- /*
- * A completer which matches only domains from sites in your history with the current query.
- */
- var DomainCompleter = Class.extend({
- // A mapping of doamin => useHttps, where useHttps is a boolean.
- domains: null,
-
- withDomains: function(callback) {
- var self = this;
- function buildResult() {
- return Object.keys(self.domains).map(function(dom) {
- return [dom, self.domains[dom]];
- });
- }
- if (self.domains !== null)
- return callback(buildResult());
-
- self.domains = {};
-
- function processDomain(domain, https) {
- // non-www version is preferrable, so check if we have it already
- if (domain.indexOf('www.') == 0 && self.domains.hasOwnProperty(domain.slice(4)))
- domain = domain.slice(4);
-
- // HTTPS is preferrable
- https = https || self.domains[domain] || self.domains['www.' + domain];
-
- self.domains[domain] = !!https;
- delete self.domains['www.' + domain];
- }
-
- function processUrl(url) {
- parts = url.split('/');
- processDomain(parts[2], parts[0] == 'https:');
- }
-
- historyCache.use(function(history) {
- history.forEach(function(item) { processUrl(item.url); });
- });
-
- chrome.history.onVisited.addListener(function(item) { processUrl(item.url); });
-
- callback(buildResult());
- },
-
- refresh: function() { },
-
- filter: function(query, callback) {
- var best = null;
- this.withDomains(function(domains) {
- var bestOffset = 1000;
- domains.forEach(function(result) {
- var domain = result[0];
- var protocol = result[1] ? 'https' : 'http';
-
- var offset = domain.indexOf(query);
- if (offset < 0 || offset >= bestOffset)
- return;
-
- // found a new optimum
- bestOffset = offset;
- best = new LazyCompletionResult(-1.5, function() {
- return {
- html: createCompletionHtml("site", domain),
- action: { functionName: "navigateToUrl", args: [protocol + "://" + domain] },
- }});
- });
- });
- callback(best ? [best] : []);
- }
- });
-
- /** A meta-completer that delegates queries and merges and sorts the results of a collection of other
- * completer instances given in :sources. The optional argument :queryThreshold determines how long a
- * query has to be to trigger a search. */
- var MultiCompleter = Class.extend({
- init: function(sources, queryThreshold) {
- if (queryThreshold === undefined)
- queryThreshold = 1; // default
- this.sources = sources;
- this.queryThreshold = queryThreshold;
- },
-
- refresh: function() { this.sources.forEach(function(source) { source.refresh(); }); },
-
- filter: function(query, maxResults, callback) {
- if (query.length < this.queryThreshold) {
- callback([]);
- return;
- }
-
- var allResults = [];
- var counter = this.sources.length;
-
- this.sources.forEach(function(source) {
- source.filter(query, function(results) {
- allResults = allResults.concat(results);
- if (--counter > 0)
- return;
-
- // all sources have provided results by now, so we can sort and return
- allResults.sort(function(a,b) { return a.relevancy - b.relevancy; });
- // evalulate lazy completions for the top n results
- callback(allResults.slice(0, maxResults).map(function(result) { return result.build(); }));
- });
- });
- }
- });
-
- /** Singleton object that provides helpers and caching for fuzzy completion. */
- var fuzzyMatcher = (function() {
- var self = {};
-
- self.timeToClean = 0;
- self.cacheSize = 1000;
- self.regexNonWord = /[\W_]/ig;
-
- // cache generated regular expressions
- self.regexpCache = {};
- // cache filtered results from recent queries
- self.filterCache = {};
- self.normalizationCache = {};
-
- /** Normalizes the string specified in :query. Strips any non-word characters and converts
- * to lower case. */
- self.normalize = function(query) {
- if (!(query in self.normalizationCache))
- self.normalizationCache[query] = query.replace(self.regexNonWord, '').toLowerCase();
- return self.normalizationCache[query];
- }
-
- /** Returns the non-matching and matching string parts, in alternating order (starting with a
- * non-matching part) or null, if the string doesn't match the query.
- *
- * Sample: match("codecodec","code.google.com/codec") would yield ["", "code", ".google.com/", "codec"]
- *
- * Note that this function matches the longest possible parts of a string and is therefore not very
- * efficient. There it falls back to a more performant, but less accurate regex matching if the
- * normalized query is longer than 10 characters.
- *
- * _Don't use this to check if a string matches a query_. Use `getRegexp(query).test(str)` instead.
- */
- self.getMatchGroups = function(query, str) {
- query = self.normalize(query);
- if (query.length == 0)
- return str.length ? [str] : [];
- if (query.length > 15) {
- // for long query strings, the method is much too inefficient, so fall
- // back to the less accurate regex matching
- return self.getRegexp(query).exec(str).slice(1);
- }
-
- for (var i = query.length; i >= 1; --i) {
- var part = query.slice(0, i);
- var partOffset = str.toLowerCase().indexOf(part);
- if (partOffset < 0)
- continue;
-
- // we use recursive backtracking here, this is why it's slow.
- rest = self.getMatchGroups(query.slice(i), str.slice(partOffset + i));
- if (!rest) continue;
-
- return [
- str.slice(0, partOffset),
- part,
- ].concat(rest);
- }
- return null;
- }
-
- /** Calculates a very simple similarity value between a :query and a :string */
- self.calculateRelevancy = function(query, str) {
- query = self.normalize(query);
- str = self.normalize(str);
- var sum = 0;
-
- // only iterate over slices of the query starting at an offset up to 10 to save resources
- for (var start = 0; start < 20 && start < query.length; ++start) {
- for (var i = query.length; i >= start; --i) {
- if (str.indexOf(query.slice(start, i)) >= 0) {
- var length = i - start;
- sum += length * length;
- break;
- }
- }
- }
- return sum * sum * sum;
- }
-
- /** Trims the size of the caches to the configured size using a FIFO algorithm. */
- self.cleanCache = function() {
- // remove old cached regexes
- Object.keys(self.regexpCache).slice(self.cacheSize).forEach(function(query) {
- delete self.regexpCache[query];
- });
- // remove old cached normalization results
- Object.keys(self.normalizationCache).slice(self.cacheSize).forEach(function(query) {
- delete self.normalizationCache[query];
- });
- }
-
- /** Returns a regex that matches a string using a fuzzy :query. Example: The :query "abc" would result
- * in a regex like /^([^a])*(a)([^b])*(b)([^c])*(c)(.*)$/
- */
- self.getRegexp = function(query) {
- query = self.normalize(query);
- if (!(query in self.regexpCache)) {
- // build up a regex for fuzzy matching. This is the fastest method I checked (faster than:
- // string building, splice, concat, multi-level join)
- var regex = ['^'];
- for (var i = 0; i < query.length; ++i) {
- regex.push('([^');
- regex.push(query[i]);
- regex.push(']*)(');
- regex.push(query[i]);
- regex.push(')');
- }
- regex.push('(.*)$');
- self.regexpCache[query] = new RegExp(regex.join(''), 'i');
- }
- return self.regexpCache[query];
- }
-
- /** Clear the cache for the given source, e.g. for refreshing */
- self.invalidateFilterCache = function(id) {
- self.filterCache[id] = {};
- }
-
- /** Filters a collection :source using fuzzy matching against an input string :query. If a query with
- * a less specific query was issued before (e.g. if the user added a letter to the query), the cached
- * results of the last filtering are used as a starting point, instead of :source.
- */
- self.filter = function(query, source, getValue, id) {
- if (!(id in self.filterCache))
- self.filterCache[id] = {};
-
- // find the most narrow list of results in the cache
- var optSpecificity = source.length;
- var specificity;
- for (key in self.filterCache[id]) {
- if (!self.filterCache[id].hasOwnProperty(key))
- continue;
-
- if ((query.indexOf(key) != 0 && key.indexOf(query) != 0) || key.length > query.length) {
- // cache entry no longer needed
- delete self.filterCache[id][key];
- continue;
- }
-
- // is this a plausible result set to use as a source?
- if (query.indexOf(key) < 0)
- continue;
-
- // is this cache entry the most specific so far?
- specificity = self.filterCache[id][key].length;
- if (specificity < optSpecificity) {
- source = self.filterCache[id][key];
- optSpecificity = specificity;
- }
- }
-
- // don't clean up the caches every iteration
- if (++self.timeToClean > 100) {
- self.timeToClean = 0;
- self.cleanCache();
- }
-
- var regexp = self.getRegexp(query);
- var filtered = source.filter(function(x) { return regexp.test(getValue(x)) });
- self.filterCache[id][query] = filtered;
- return filtered;
- }
-
- return self;
- })();
-
- var htmlRegex = /<[^>]*>|&[a-z]+;/gi;
-
- /** Strips HTML tags and entities using a naive regex replacement. Optionally, saves the stripped
- * HTML tags in a dictionary indexed by the position where the tag should be reinserted. */
- function stripHtmlTags(str, positions) {
- if (!positions)
- return str.replace(htmlRegex, '');
-
- var match = str.match(htmlRegex);
- if (!match) return;
- match.reverse();
- var split = str.split(htmlRegex);
- var offset = 0;
- var i = 0;
- split.forEach(function(text) {
- if (match.length > 0)
- positions[offset += text.length] = match.pop();
- });
-
- return split.join('');
- }
-
- /** Creates an file-internal representation of a URL match with the given paramters */
- function createCompletionHtml(type, str, title) {
- title = title || '';
- // sanitize input, it could come from a malicious web site
- title = title.length > 0 ? ' <span class="title">' + utils.escapeHtml(title) + '</span>' : '';
- return '<em>' + type + '</em> ' + utils.escapeHtml(str) + title;
- }
-
- /** Renders a completion by marking fuzzy-matched parts. */
- function renderFuzzy(query, html) {
- // we want to match the content in HTML tags, but not the HTML tags themselves, so we remove the
- // tags and reinsert them after the matching process
- var htmlTags = {};
- var groups = fuzzyMatcher.getMatchGroups(query, stripHtmlTags(html, htmlTags));
-
- html = [];
- var htmlOffset = 0;
-
- // this helper function adds the HTML generated _for one single character_ to the HTML output
- // and reinserts HTML tags stripped before, if they were at this position
- function addToHtml(str) {
- if (htmlOffset in htmlTags)
- html.push(htmlTags[htmlOffset]);
- html.push(str);
- ++htmlOffset;
- }
-
- function addCharsWithDecoration(str, before, after) {
- before = before || '';
- after = after || '';
- for (var i = 0; i < str.length; ++i)
- addToHtml(before + str[i] + after);
- }
-
- // iterate over the match groups. They are non-matched and matched string parts, in alternating order
- for (var i = 0; i < groups.length; ++i) {
- if (i % 2 == 0)
- // we have a non-matched part, it could have several characters. We need to insert them character
- // by character, so that addToHtml can keep track of the position in the original string
- addCharsWithDecoration(groups[i]);
- else
- // we have a matched part. In addition to the characters themselves, we add some decorating HTML.
- addCharsWithDecoration(groups[i], '<span class="fuzzyMatch">', '</span>');
- };
-
- // call it another time so that a tag at the very last position is reinserted
- addToHtml('');
- return html.join('');
- }
-
- /** Singleton object that provides fast access to the Chrome history */
- var historyCache = (function() {
- var size = 20000;
- var cachedHistory = null;
-
- function use(callback) {
- if (cachedHistory !== null)
- return callback(cachedHistory);
-
- chrome.history.search({ text: '', maxResults: size, startTime: 0 }, function(history) {
- // sorting in ascending order, so we can push new items to the end later
- history.sort(function(a, b) {
- return (a.lastVisitTime|| 0) - (b.lastVisitTime || 0);
- });
- cachedHistory = history;
- callback(history);
- });
-
- chrome.history.onVisited.addListener(function(item) {
- // only cache newly visited sites
- if (item.visitCount === 1)
- cachedHistory.push(item);
- });
- }
-
- return { use: use };
- })();
-
- // public interface
- return {
- FuzzyBookmarkCompleter: FuzzyBookmarkCompleter,
- FuzzyHistoryCompleter: FuzzyHistoryCompleter,
- FuzzyTabCompleter: FuzzyTabCompleter,
- SmartKeywordCompleter: SmartKeywordCompleter,
- DomainCompleter: DomainCompleter,
- MultiCompleter: MultiCompleter
- };
-})()
diff --git a/lib/domUtils.js b/lib/dom_utils.js
index 4ab92682..4ab92682 100644
--- a/lib/domUtils.js
+++ b/lib/dom_utils.js
diff --git a/lib/keyboardUtils.js b/lib/keyboard_utils.js
index 52544c73..52544c73 100644
--- a/lib/keyboardUtils.js
+++ b/lib/keyboard_utils.js