Home | History | Annotate | Download | only in extension-docs
      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 &lt;match&gt; 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('');
     77           style.push(name.substring(start, end));
     78           style.push('');
     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(['', match['type'], ''].join(''));
    327         }
    328         if (match['description']) {
    329           suggestion.push(['', match['description'], ''].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