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
8var binding = require('binding').Binding.create('omnibox');
9
10var eventBindings = require('event_bindings');
11var sendRequest = require('sendRequest').sendRequest;
12
13// Remove invalid characters from |text| so that it is suitable to use
14// for |AutocompleteMatch::contents|.
15function 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.
28function 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
84binding.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
114eventBindings.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
124exports.binding = binding.generate();
125