Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 // Custom binding for the omnibox API. Only injected into the v8 contexts
      6 // for extensions which have permission for the omnibox API.
      7 
      8 var binding = require('binding').Binding.create('omnibox');
      9 
     10 var eventBindings = require('event_bindings');
     11 var sendRequest = require('sendRequest').sendRequest;
     12 
     13 // Remove invalid characters from |text| so that it is suitable to use
     14 // for |AutocompleteMatch::contents|.
     15 function sanitizeString(text, shouldTrim) {
     16   // NOTE: This logic mirrors |AutocompleteMatch::SanitizeString()|.
     17   // 0x2028 = line separator; 0x2029 = paragraph separator.
     18   var kRemoveChars = /(\r|\n|\t|\u2028|\u2029)/gm;
     19   if (shouldTrim)
     20     text = text.trimLeft();
     21   return text.replace(kRemoveChars, '');
     22 }
     23 
     24 // Parses the xml syntax supported by omnibox suggestion results. Returns an
     25 // object with two properties: 'description', which is just the text content,
     26 // and 'descriptionStyles', which is an array of style objects in a format
     27 // understood by the C++ backend.
     28 function parseOmniboxDescription(input) {
     29   var domParser = new DOMParser();
     30 
     31   // The XML parser requires a single top-level element, but we want to
     32   // support things like 'hello, <match>world</match>!'. So we wrap the
     33   // provided text in generated root level element.
     34   var root = domParser.parseFromString(
     35       '<fragment>' + input + '</fragment>', 'text/xml');
     36 
     37   // DOMParser has a terrible error reporting facility. Errors come out nested
     38   // inside the returned document.
     39   var error = root.querySelector('parsererror div');
     40   if (error) {
     41     throw new Error(error.textContent);
     42   }
     43 
     44   // Otherwise, it's valid, so build up the result.
     45   var result = {
     46     description: '',
     47     descriptionStyles: []
     48   };
     49 
     50   // Recursively walk the tree.
     51   function walk(node) {
     52     for (var i = 0, child; child = node.childNodes[i]; i++) {
     53       // Append text nodes to our description.
     54       if (child.nodeType == Node.TEXT_NODE) {
     55         var shouldTrim = result.description.length == 0;
     56         result.description += sanitizeString(child.nodeValue, shouldTrim);
     57         continue;
     58       }
     59 
     60       // Process and descend into a subset of recognized tags.
     61       if (child.nodeType == Node.ELEMENT_NODE &&
     62           (child.nodeName == 'dim' || child.nodeName == 'match' ||
     63            child.nodeName == 'url')) {
     64         var style = {
     65           'type': child.nodeName,
     66           'offset': result.description.length
     67         };
     68         $Array.push(result.descriptionStyles, style);
     69         walk(child);
     70         style.length = result.description.length - style.offset;
     71         continue;
     72       }
     73 
     74       // Descend into all other nodes, even if they are unrecognized, for
     75       // forward compat.
     76       walk(child);
     77     }
     78   };
     79   walk(root);
     80 
     81   return result;
     82 }
     83 
     84 binding.registerCustomHook(function(bindingsAPI) {
     85   var apiFunctions = bindingsAPI.apiFunctions;
     86 
     87   apiFunctions.setUpdateArgumentsPreValidate('setDefaultSuggestion',
     88                                              function(suggestResult) {
     89     if (suggestResult.content != undefined) {  // null, etc.
     90       throw new Error(
     91           'setDefaultSuggestion cannot contain the "content" field');
     92     }
     93     return [suggestResult];
     94   });
     95 
     96   apiFunctions.setHandleRequest('setDefaultSuggestion', function(details) {
     97     var parseResult = parseOmniboxDescription(details.description);
     98     sendRequest(this.name, [parseResult], this.definition.parameters);
     99   });
    100 
    101   apiFunctions.setUpdateArgumentsPostValidate(
    102       'sendSuggestions', function(requestId, userSuggestions) {
    103     var suggestions = [];
    104     for (var i = 0; i < userSuggestions.length; i++) {
    105       var parseResult = parseOmniboxDescription(
    106           userSuggestions[i].description);
    107       parseResult.content = userSuggestions[i].content;
    108       $Array.push(suggestions, parseResult);
    109     }
    110     return [requestId, suggestions];
    111   });
    112 });
    113 
    114 eventBindings.registerArgumentMassager('omnibox.onInputChanged',
    115     function(args, dispatch) {
    116   var text = args[0];
    117   var requestId = args[1];
    118   var suggestCallback = function(suggestions) {
    119     chrome.omnibox.sendSuggestions(requestId, suggestions);
    120   };
    121   dispatch([text, suggestCallback]);
    122 });
    123 
    124 exports.binding = binding.generate();
    125