chromium/chrome/renderer/resources/extensions/omnibox_custom_bindings.js

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Custom binding for the omnibox API. Only injected into the v8 contexts
// for extensions which have permission for the omnibox API.

var inServiceWorker = requireNative('utils').isInServiceWorker();

// Remove invalid characters from |text| so that it is suitable to use
// for |AutocompleteMatch::contents|.
function sanitizeString(text, shouldTrim) {
  // NOTE: This logic mirrors |AutocompleteMatch::SanitizeString()|.
  // 0x2028 = line separator; 0x2029 = paragraph separator.
  var kRemoveChars = /(\r|\n|\t|\u2028|\u2029)/gm;
  if (shouldTrim)
    text = text.trimLeft();
  return text.replace(kRemoveChars, '');
}

// Parses the xml syntax supported by omnibox suggestion results. Returns an
// object with two properties: 'description', which is just the text content,
// and 'descriptionStyles', which is an array of style objects in a format
// understood by the C++ backend.
function parseOmniboxDescription(input) {
  var domParser = new DOMParser();

  // The XML parser requires a single top-level element, but we want to
  // support things like 'hello, <match>world</match>!'. So we wrap the
  // provided text in generated root level element.
  var root = domParser.parseFromString(
      '<fragment>' + input + '</fragment>', 'text/xml');

  // DOMParser has a terrible error reporting facility. Errors come out nested
  // inside the returned document.
  var error = root.querySelector('parsererror div');
  if (error) {
    throw new Error(error.textContent);
  }

  // Otherwise, it's valid, so build up the result.
  var result = {
    description: '',
    descriptionStyles: []
  };

  // Recursively walk the tree.
  function walk(node) {
    for (var i = 0, child; child = node.childNodes[i]; i++) {
      // Append text nodes to our description.
      if (child.nodeType === Node.TEXT_NODE) {
        var shouldTrim = result.description.length === 0;
        result.description += sanitizeString(child.nodeValue, shouldTrim);
        continue;
      }

      // Process and descend into a subset of recognized tags.
      if (child.nodeType === Node.ELEMENT_NODE &&
          (child.nodeName === 'dim' || child.nodeName === 'match' ||
           child.nodeName === 'url')) {
        var style = {
          'type': child.nodeName,
          'offset': result.description.length
        };
        $Array.push(result.descriptionStyles, style);
        walk(child);
        style.length = result.description.length - style.offset;
        continue;
      }

      // Descend into all other nodes, even if they are unrecognized, for
      // forward compat.
      walk(child);
    }
  };
  walk(root);

  return result;
}

// The following custom hooks are only registered in non-service worker
// contexts. In service worker contexts, we instead parse the description and
// styles from the browser process.
// TODO(devlin): Migrate DOM-based contexts to also use the browser-side
// parsing so that there's only one implementation. We have them separate for
// now to ensure things don't break.
if (!inServiceWorker) {
  apiBridge.registerCustomHook(function(bindingsAPI) {
    var apiFunctions = bindingsAPI.apiFunctions;

    apiFunctions.setHandleRequest('setDefaultSuggestion',
                                  function(details, callback) {
      var parseResult = parseOmniboxDescription(details.description);
      bindingUtil.sendRequest('omnibox.setDefaultSuggestion',
                              [parseResult, callback],
                              undefined);
    });

    apiFunctions.setUpdateArgumentsPostValidate(
        'sendSuggestions', function(requestId, userSuggestions) {
      var suggestions = [];
      for (var i = 0; i < userSuggestions.length; i++) {
        var parseResult = parseOmniboxDescription(
            userSuggestions[i].description);
        parseResult.content = userSuggestions[i].content;
        parseResult.deletable = userSuggestions[i].deletable;
        $Array.push(suggestions, parseResult);
      }
      return [requestId, suggestions];
    });
  });
}

bindingUtil.registerEventArgumentMassager('omnibox.onInputChanged',
                                          function(args, dispatch) {
  var text = args[0];
  var requestId = args[1];
  var suggestCallback = function(suggestions) {
    chrome.omnibox.sendSuggestions(requestId, suggestions);
  };
  dispatch([text, suggestCallback]);
});