| // 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. |
| const inServiceWorker = requireNative('utils').isInServiceWorker(); |
| const SetIconCommon = requireNative('setIcon').SetIconCommon; |
| |
| const imageUtil = require('imageUtil'); |
| |
| const kMaxActionIconSize = 160; |
| |
| // 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. |
| const 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) { |
| const 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. |
| const root = domParser.parseFromString( |
| '<fragment>' + input + '</fragment>', 'text/xml'); |
| |
| // DOMParser has a terrible error reporting facility. Errors come out nested |
| // inside the returned document. |
| const error = root.querySelector('parsererror div'); |
| if (error) { |
| throw new Error(error.textContent); |
| } |
| |
| // Otherwise, it's valid, so build up the result. |
| const result = {description: '', descriptionStyles: []}; |
| |
| // Recursively walk the tree. |
| function walk(node) { |
| for (let i = 0, child; child = node.childNodes[i]; i++) { |
| // Append text nodes to our description. |
| if (child.nodeType === Node.TEXT_NODE) { |
| const 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')) { |
| const 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; |
| } |
| |
| |
| function verifyImageSize(imageData) { |
| if (imageData.width > kMaxActionIconSize || |
| imageData.height > kMaxActionIconSize) { |
| throw new Error('Icons must be smaller 160 px wide and tall.'); |
| } |
| } |
| |
| // Register custom hook to update action icons to a format that can be parsed by |
| // the browser. |
| apiBridge.registerCustomHook(function(bindingsAPI) { |
| const apiFunctions = bindingsAPI.apiFunctions; |
| apiFunctions.setUpdateArgumentsPreValidate( |
| 'sendSuggestions', function(requestId, userSuggestions) { |
| for (let i = 0; i < userSuggestions.length; i++) { |
| const suggestion = userSuggestions[i]; |
| |
| if (!suggestion.actions) { |
| continue; |
| } |
| const actions = []; |
| for (let j = 0; j < suggestion.actions.length; j++) { |
| if (!suggestion.actions[j].icon) { |
| $Array.push(actions, suggestion.actions[j]); |
| continue; |
| } |
| // Deep copy is needed in case many actions point to the same icon. |
| const icon = new ImageData( |
| new Uint8ClampedArray(suggestion.actions[j].icon.data), |
| suggestion.actions[j].icon.width, |
| suggestion.actions[j].icon.height); |
| const action = { |
| name: suggestion.actions[j].name, |
| label: suggestion.actions[j].label, |
| tooltipText: suggestion.actions[j].tooltipText, |
| icon: {}, |
| }; |
| verifyImageSize(icon); |
| imageUtil.verifyImageData(icon); |
| const details = {imageData: {}}; |
| details.imageData = {__proto__: null}; |
| details.imageData[icon.width.toString()] = icon; |
| action.icon = SetIconCommon(details); |
| $Array.push(actions, action); |
| } |
| suggestion.actions = actions; |
| } |
| return [requestId, userSuggestions]; |
| }); |
| }); |
| |
| // 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) { |
| const apiFunctions = bindingsAPI.apiFunctions; |
| |
| apiFunctions.setHandleRequest( |
| 'setDefaultSuggestion', function(details, callback) { |
| const parseResult = parseOmniboxDescription(details.description); |
| bindingUtil.sendRequest( |
| 'omnibox.setDefaultSuggestion', [parseResult, callback], |
| undefined); |
| }); |
| |
| apiFunctions.setUpdateArgumentsPostValidate( |
| 'sendSuggestions', function(requestId, userSuggestions) { |
| const suggestions = []; |
| for (let i = 0; i < userSuggestions.length; i++) { |
| const 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) { |
| const text = args[0]; |
| const requestId = args[1]; |
| const suggestCallback = function(suggestions) { |
| chrome.omnibox.sendSuggestions(requestId, suggestions); |
| }; |
| dispatch([text, suggestCallback]); |
| }); |