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