1<!DOCTYPE html> 2<!-- 3 * Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this 4 * source code is governed by a BSD-style license that can be found in the 5 * LICENSE file. 6--> 7<html> 8 <head> 9 </head> 10 <body> 11 <script> 12 /** 13 * Allows for binding callbacks to a specific scope. 14 * @param {Object} scope Scope to bind to. 15 * @returns {Function} A wrapped call to this function. 16 */ 17 Function.prototype.bind = function(scope) { 18 var func = this; 19 return function() { 20 return func.apply(scope, arguments); 21 }; 22 }; 23 24 ////////////////////////////////////////////////////////////////////////// 25 26 /** 27 * Holds the search index and exposes operations to search the API docs. 28 * @constructor 29 */ 30 function APISearchCorpus() { 31 this.corpus_ = []; 32 }; 33 34 /** 35 * Adds an entry to the index. 36 * @param {String} name Name of the function (e.g. chrome.tabs.get). 37 * @param {String} url Url to the documentation. 38 * @param {String} desc Description (optional). 39 * @param {String} type The type of entry (e.g. method, event). 40 */ 41 APISearchCorpus.prototype.addEntry = function(name, url, desc, type) { 42 this.corpus_.push({ 43 'name' : name, 44 'url' : url, 45 'style' : name, 46 'description' : desc, 47 'type' : type 48 }); 49 }; 50 51 /** 52 * Locates a match from the supplied keywords against text. 53 * 54 * Keywords are matched in the order supplied, and a non-overlapping 55 * search is used. The matches are returned in a styled string that 56 * can be passed directly to the omnibox API. 57 * 58 * @param {Array.<String>} keywords A list of keywords to check. 59 * @param {String} name The name to search against. 60 * @returns {String|null} A string containing <match> markup 61 * corresponding to the matched text, or null if no match was found. 62 */ 63 APISearchCorpus.prototype.findMatch_ = function(keywords, name) { 64 var style = []; 65 var indexFrom = 0; 66 var lowerName = name.toLowerCase(); 67 for (var i = 0; i < keywords.length; i++) { 68 var keyword = keywords[i].toLowerCase(); 69 var start = lowerName.indexOf(keyword, indexFrom); 70 if (start == -1) { 71 return null; 72 } 73 var end = start + keyword.length + 1; 74 75 style.push(name.substring(indexFrom, start)) 76 style.push('<match>'); 77 style.push(name.substring(start, end)); 78 style.push('</match>'); 79 80 indexFrom = end; 81 } 82 style.push(name.substring(indexFrom)); 83 return style.join(''); 84 }; 85 86 /** 87 * Searches this corpus for the supplied text. 88 * @param {String} text Query text. 89 * @param {Number} limit Max results to return. 90 * @returns {Array.<Object>} A list of entries corresponding with 91 * matches (@see APISearchCorpus.findMatch_ for keyword search 92 * algorithm. Results are returned in a sorted order, first by 93 * length, then alphabetically by name. An exact match will be 94 * returned first. 95 */ 96 APISearchCorpus.prototype.search = function(text, limit) { 97 var results = []; 98 var match = null; 99 if (!text || text.length == 0) { 100 return this.corpus_.slice(0, limit); // No text, start listing APIs. 101 } 102 var searchText = text.toLowerCase(); 103 var keywords = searchText.split(' '); 104 for (var i = 0; i < this.corpus_.length; i++) { 105 var name = this.corpus_[i]['name']; 106 if (results.length < limit) { 107 var result = this.findMatch_(keywords, name); 108 if (result) { 109 this.corpus_[i]['style'] = result; 110 results.push(this.corpus_[i]); 111 } 112 } 113 if (!match && searchText == name) { 114 match = this.corpus_[i]; // An exact match. 115 } 116 if (match && results.length >= limit) { 117 break; // Have an exact match and have reached the search limit. 118 } 119 } 120 if (match) { 121 results.unshift(match); // Add any exact match to the front. 122 } 123 return results; 124 }; 125 126 /** 127 * Sorts the corpus according to name length, then name alphabetically. 128 */ 129 APISearchCorpus.prototype.sort = function() { 130 function compareLength(a, b) { 131 return a['name'].length - b['name'].length; 132 }; 133 134 function compareAlpha(a, b) { 135 if (a['name'] < b['name']) return -1; 136 if (a['name'] > b['name']) return 1; 137 return 0; 138 }; 139 140 function compare(a, b) { 141 var result = compareLength(a, b); 142 if (result == 0) result = compareAlpha(a, b); 143 return result; 144 }; 145 146 this.corpus_.sort(compare); 147 }; 148 149 ////////////////////////////////////////////////////////////////////////// 150 151 /** 152 * Provides an interface to the Chrome Extensions documentation site. 153 * @param {APISearchCorpus} corpus The search corpus to populate. 154 * @constructor 155 */ 156 function DocsManager(corpus) { 157 this.CODE_URL_PREFIX = 'http://code.google.com/chrome/extensions/'; 158 this.API_MANIFEST_URL = [ 159 'http://src.chromium.org/viewvc/chrome/trunk/src/', 160 'chrome/common/extensions/api/extension_api.json' 161 ].join(''); 162 this.corpus_ = corpus; 163 }; 164 165 /** 166 * Initiates a fetch of the docs and populates the corpus. 167 */ 168 DocsManager.prototype.fetch = function() { 169 this.fetchApi_(this.onApi_.bind(this)); 170 }; 171 172 /** 173 * Retrieves the API manifest from cache or fetches a new one if none. 174 * @param {Function} callback The function to pass the parsed manifest 175 * data to. 176 */ 177 DocsManager.prototype.fetchApi_ = function(callback) { 178 var currentCacheTime = this.getCacheTime_(); 179 if (localStorage['cache-time'] && localStorage['cache']) { 180 var cacheTime = JSON.parse(localStorage['cache-time']); 181 if (cacheTime < currentCacheTime) { 182 callback(JSON.parse(localStorage['cache'])); 183 return; 184 } 185 } 186 var xhr = new XMLHttpRequest(); 187 xhr.addEventListener('readystatechange', function(evt) { 188 if (xhr.readyState == 4 && xhr.responseText) { 189 localStorage['cache-time'] = JSON.stringify(currentCacheTime); 190 localStorage['cache'] = xhr.responseText; 191 var json = JSON.parse(xhr.responseText); 192 callback(json); 193 } 194 }); 195 xhr.open('GET', this.API_MANIFEST_URL, true); 196 xhr.send(); 197 }; 198 199 /** 200 * Returns a time which can be used to cache a manifest response. 201 * @returns {Number} A timestamp divided by the number of ms in a day, 202 * rounded to the nearest integer. This means the number should 203 * change only once per day, invalidating the cache that often. 204 */ 205 DocsManager.prototype.getCacheTime_ = function() { 206 var time = new Date().getTime(); 207 time = Math.round(time / (1000 * 60 * 60 * 24)); 208 return time; 209 }; 210 211 /** 212 * Returns an URL for the documentation given an API element. 213 * @param {String} namespace The namespace (e.g. tabs, windows). 214 * @param {String} type The type of element (e.g. event, method, type). 215 * @param {String} name The name of the element (e.g. onRemoved). 216 * @returns {String} An URL corresponding with the documentation for the 217 * given element. 218 */ 219 DocsManager.prototype.getDocLink_ = function(namespace, type, name) { 220 var linkparts = [ this.CODE_URL_PREFIX, namespace, '.html' ]; 221 if (type && name) { 222 linkparts.push('#', type, '-', name); 223 } 224 return linkparts.join(''); 225 }; 226 227 /** 228 * Returns a qualified name for an API element. 229 * @param {String} namespace The namespace (e.g. tabs, windows). 230 * @param {String} name The name of the element (e.g. onRemoved). 231 * @returns {String} A qualified API name (e.g. chrome.tabs.onRemoved). 232 */ 233 DocsManager.prototype.getName_ = function(namespace, name) { 234 var nameparts = [ 'chrome', namespace ]; 235 if (name) { 236 nameparts.push(name); 237 } 238 return nameparts.join('.'); 239 }; 240 241 /** 242 * Parses an API manifest data structure and populates the search index. 243 * @param {Object} api The api manifest, as a JSON-parsed object. 244 */ 245 DocsManager.prototype.onApi_ = function(api) { 246 for (var i = 0; i < api.length; i++) { 247 var module = api[i]; 248 if (module.nodoc) { 249 continue; 250 } 251 var ns = module.namespace; 252 var nsName = this.getName_(ns); 253 var nsUrl = this.getDocLink_(ns); 254 this.corpus_.addEntry(nsName, nsUrl, null, 'namespace'); 255 this.parseAPIArray_('method', ns, module.functions); 256 this.parseAPIArray_('event', ns, module.events); 257 this.parseAPIArray_('type', ns, module.types); 258 this.parseAPIObject_('property', ns, module.properties); 259 this.corpus_.sort(); 260 } 261 }; 262 263 /** 264 * Parses an API manifest subsection which is formatted as an Array. 265 * @param {String} type The type of data (e.g. method, event, type). 266 * @param {String} ns The namespace (e.g. tabs, windows). 267 * @param {Array} list The list of API elements. 268 */ 269 DocsManager.prototype.parseAPIArray_ = function(type, ns, list) { 270 if (!list) return; 271 for (var j = 0; j < list.length; j++) { 272 var item = list[j]; 273 if (item.nodoc) continue; 274 var name = item.name || item.id; 275 var fullname = this.getName_(ns, name); 276 var url = this.getDocLink_(ns, type, name); 277 var description = item.description; 278 this.corpus_.addEntry(fullname, url, description, type); 279 } 280 }; 281 282 /** 283 * Parses an API manifest subsection which is formatted as an Object. 284 * @param {String} type The type of data (e.g. property). 285 * @param {String} ns The namespace (e.g. tabs, windows). 286 * @param {Object} list The object containing API elements. 287 */ 288 DocsManager.prototype.parseAPIObject_ = function(type, ns, list) { 289 for (var prop in list) { 290 if (list.hasOwnProperty(prop)) { 291 var name = this.getName_(ns, prop); 292 var url = this.getDocLink_(ns, type, prop); 293 var description = list[prop].description; 294 this.corpus_.addEntry(name, url, description, type); 295 } 296 } 297 }; 298 299 ////////////////////////////////////////////////////////////////////////// 300 301 /** 302 * Manages text input into the omnibox and returns search results. 303 * @param {APISearchCorpus} Populated search corpus. 304 * @param {TabManager} Manager to use to open tabs. 305 * @constructor 306 */ 307 function OmniboxManager(corpus, tabManager) { 308 this.SEPARATOR = ' - '; 309 this.corpus_ = corpus; 310 this.tabManager_ = tabManager; 311 chrome.omnibox.onInputChanged.addListener( 312 this.onChanged_.bind(this)); 313 chrome.omnibox.onInputEntered.addListener( 314 this.onEntered_.bind(this)); 315 }; 316 317 /** 318 * Converts a corpus match to an object suitable for the omnibox API. 319 * @param {Object} match The match to convert. 320 * @returns {Object} A suggestion object formatted for the omnibox API. 321 */ 322 OmniboxManager.prototype.convertMatchToSuggestion_ = function(match) { 323 var suggestion = [ match['style'] ]; 324 if (match['type']) { 325 // Abusing the URL style a little, but want this to stand out. 326 suggestion.push(['<url>', match['type'], '</url>'].join('')); 327 } 328 if (match['description']) { 329 suggestion.push(['<dim>', match['description'], '</dim>'].join('')); 330 } 331 return { 332 'content' : match['name'], 333 'description' : suggestion.join(' - ') 334 } 335 }; 336 337 /** 338 * Suggests a list of possible matches when omnibox text changes. 339 * @param {String} text Text input from the omnibox. 340 * @param {Function} suggest Callback to execute with a list of 341 * suggestion objects, if any matches were found. 342 */ 343 OmniboxManager.prototype.onChanged_ = function(text, suggest) { 344 var matches = this.corpus_.search(text, 10); 345 var suggestions = []; 346 for (var i = 0; i < matches.length; i++) { 347 var suggestion = this.convertMatchToSuggestion_(matches[i]); 348 suggestions.push(suggestion); 349 } 350 suggest(suggestions); 351 }; 352 353 /** 354 * Opens the most appropriate URL when enter is pressed in the omnibox. 355 * 356 * Note that the entered text does not have to be exact - the first 357 * search result is automatically opened when enter is pressed. 358 * 359 * @param {String} text The text entered. 360 */ 361 OmniboxManager.prototype.onEntered_ = function(text) { 362 var matches = this.corpus_.search(text, 1); 363 if (matches.length > 0) { 364 this.tabManager_.open(matches[0]['url']); 365 } 366 }; 367 368 ////////////////////////////////////////////////////////////////////////// 369 370 /** 371 * Manages opening urls in tabs. 372 * @constructor 373 */ 374 function TabManager() { 375 this.tab_ = null; 376 chrome.tabs.onRemoved.addListener(this.onRemoved_.bind(this)); 377 }; 378 379 /** 380 * When a tab is removed, see if it was opened by us and null out if yes. 381 * @param {Number} tabid ID of the removed tab. 382 */ 383 TabManager.prototype.onRemoved_ = function(tabid) { 384 if (this.tab_ && tabid == this.tab_.id) this.tab_ = null; 385 }; 386 387 /** 388 * When a tab opened by us is created, store it for future updates. 389 * @param {Tab} tab The tab which was just opened. 390 */ 391 TabManager.prototype.onTab_ = function(tab) { 392 this.tab_ = tab; 393 }; 394 395 /** 396 * Opens the supplied URL. 397 * 398 * The first time this method is called a new tab is created. Subsequent 399 * times this is called, the opened tab will be updated. If that tab 400 * is ever closed, then a new tab will be created for the next call. 401 * 402 * @param {String} url The URL to open. 403 */ 404 TabManager.prototype.open = function(url) { 405 if (url) { 406 var args = { 'url' : url, 'selected' : true }; 407 if (this.tab_) { 408 chrome.tabs.update(this.tab_.id, args); 409 } else { 410 chrome.tabs.create(args, this.onTab_.bind(this)); 411 } 412 } 413 }; 414 415 ////////////////////////////////////////////////////////////////////////// 416 417 var corpus = new APISearchCorpus(); 418 var docsManager = new DocsManager(corpus); 419 docsManager.fetch(); 420 var tabManager = new TabManager(); 421 var omnibox = new OmniboxManager(corpus, tabManager); 422 </script> 423 </body> 424</html> 425