summaryrefslogtreecommitdiffstats
path: root/chrome/renderer/resources/extensions/omnibox_custom_bindings.js
blob: 04ae18113f6600084e0a6191b72cdf16d7620237 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

var chromeHidden = requireNative('chrome_hidden').GetChromeHidden();
var sendRequest = require('sendRequest').sendRequest;

// 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
        };
        result.descriptionStyles.push(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;
}

chromeHidden.registerCustomHook('omnibox', function(bindingsAPI) {
  var apiFunctions = bindingsAPI.apiFunctions;

  apiFunctions.setHandleRequest('setDefaultSuggestion', function(details) {
    var parseResult = parseOmniboxDescription(details.description);
    sendRequest(this.name, [parseResult], this.definition.parameters);
  });

  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;
      suggestions.push(parseResult);
    }
    return [requestId, suggestions];
  });
});

chromeHidden.Event.registerArgumentMassager('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]);
});