| /* |
| * Copyright (C) 2011 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| /** |
| * @implements {UI.Searchable} |
| */ |
| export class JSONView extends UI.VBox { |
| /** |
| * @param {!ParsedJSON} parsedJSON |
| */ |
| constructor(parsedJSON) { |
| super(); |
| this._initialized = false; |
| this.registerRequiredCSS('source_frame/jsonView.css'); |
| this._parsedJSON = parsedJSON; |
| this.element.classList.add('json-view'); |
| |
| /** @type {?UI.SearchableView} */ |
| this._searchableView; |
| /** @type {!ObjectUI.ObjectPropertiesSection} */ |
| this._treeOutline; |
| /** @type {number} */ |
| this._currentSearchFocusIndex = 0; |
| /** @type {!Array.<!UI.TreeElement>} */ |
| this._currentSearchTreeElements = []; |
| /** @type {?RegExp} */ |
| this._searchRegex = null; |
| } |
| |
| /** |
| * @param {string} content |
| * @return {!Promise<?UI.SearchableView>} |
| */ |
| static async createView(content) { |
| // We support non-strict JSON parsing by parsing an AST tree which is why we offload it to a worker. |
| const parsedJSON = await JSONView._parseJSON(content); |
| if (!parsedJSON || typeof parsedJSON.data !== 'object') { |
| return null; |
| } |
| |
| const jsonView = new JSONView(parsedJSON); |
| const searchableView = new UI.SearchableView(jsonView); |
| searchableView.setPlaceholder(Common.UIString('Find')); |
| jsonView._searchableView = searchableView; |
| jsonView.show(searchableView.element); |
| return searchableView; |
| } |
| |
| /** |
| * @param {?Object} obj |
| * @return {!UI.SearchableView} |
| */ |
| static createViewSync(obj) { |
| const jsonView = new JSONView(new ParsedJSON(obj, '', '')); |
| const searchableView = new UI.SearchableView(jsonView); |
| searchableView.setPlaceholder(Common.UIString('Find')); |
| jsonView._searchableView = searchableView; |
| jsonView.show(searchableView.element); |
| jsonView.element.setAttribute('tabIndex', 0); |
| return searchableView; |
| } |
| |
| /** |
| * @param {?string} text |
| * @return {!Promise<?ParsedJSON>} |
| */ |
| static _parseJSON(text) { |
| let returnObj = null; |
| if (text) { |
| returnObj = JSONView._extractJSON(/** @type {string} */ (text)); |
| } |
| if (!returnObj) { |
| return Promise.resolve(/** @type {?ParsedJSON} */ (null)); |
| } |
| return Formatter.formatterWorkerPool().parseJSONRelaxed(returnObj.data).then(handleReturnedJSON); |
| |
| /** |
| * @param {*} data |
| * @return {?ParsedJSON} |
| */ |
| function handleReturnedJSON(data) { |
| if (!data) { |
| return null; |
| } |
| returnObj.data = data; |
| return returnObj; |
| } |
| } |
| |
| /** |
| * @param {string} text |
| * @return {?ParsedJSON} |
| */ |
| static _extractJSON(text) { |
| // Do not treat HTML as JSON. |
| if (text.startsWith('<')) { |
| return null; |
| } |
| let inner = JSONView._findBrackets(text, '{', '}'); |
| const inner2 = JSONView._findBrackets(text, '[', ']'); |
| inner = inner2.length > inner.length ? inner2 : inner; |
| |
| // Return on blank payloads or on payloads significantly smaller than original text. |
| if (inner.length === -1 || text.length - inner.length > 80) { |
| return null; |
| } |
| |
| const prefix = text.substring(0, inner.start); |
| const suffix = text.substring(inner.end + 1); |
| text = text.substring(inner.start, inner.end + 1); |
| |
| // Only process valid JSONP. |
| if (suffix.trim().length && !(suffix.trim().startsWith(')') && prefix.trim().endsWith('('))) { |
| return null; |
| } |
| |
| return new ParsedJSON(text, prefix, suffix); |
| } |
| |
| /** |
| * @param {string} text |
| * @param {string} open |
| * @param {string} close |
| * @return {{start: number, end: number, length: number}} |
| */ |
| static _findBrackets(text, open, close) { |
| const start = text.indexOf(open); |
| const end = text.lastIndexOf(close); |
| let length = end - start - 1; |
| if (start === -1 || end === -1 || end < start) { |
| length = -1; |
| } |
| return {start: start, end: end, length: length}; |
| } |
| |
| /** |
| * @override |
| */ |
| wasShown() { |
| this._initialize(); |
| } |
| |
| _initialize() { |
| if (this._initialized) { |
| return; |
| } |
| this._initialized = true; |
| |
| const obj = SDK.RemoteObject.fromLocalObject(this._parsedJSON.data); |
| const title = this._parsedJSON.prefix + obj.description + this._parsedJSON.suffix; |
| this._treeOutline = new ObjectUI.ObjectPropertiesSection( |
| obj, title, undefined, undefined, undefined, undefined, true /* showOverflow */); |
| this._treeOutline.enableContextMenu(); |
| this._treeOutline.setEditable(false); |
| this._treeOutline.expand(); |
| this.element.appendChild(this._treeOutline.element); |
| this._treeOutline.firstChild().select(true /* omitFocus */, false /* selectedByUser */); |
| } |
| |
| /** |
| * @param {number} index |
| */ |
| _jumpToMatch(index) { |
| if (!this._searchRegex) { |
| return; |
| } |
| const previousFocusElement = this._currentSearchTreeElements[this._currentSearchFocusIndex]; |
| if (previousFocusElement) { |
| previousFocusElement.setSearchRegex(this._searchRegex); |
| } |
| |
| const newFocusElement = this._currentSearchTreeElements[index]; |
| if (newFocusElement) { |
| this._updateSearchIndex(index); |
| newFocusElement.setSearchRegex(this._searchRegex, UI.highlightedCurrentSearchResultClassName); |
| newFocusElement.reveal(); |
| } else { |
| this._updateSearchIndex(0); |
| } |
| } |
| |
| /** |
| * @param {number} count |
| */ |
| _updateSearchCount(count) { |
| if (!this._searchableView) { |
| return; |
| } |
| this._searchableView.updateSearchMatchesCount(count); |
| } |
| |
| /** |
| * @param {number} index |
| */ |
| _updateSearchIndex(index) { |
| this._currentSearchFocusIndex = index; |
| if (!this._searchableView) { |
| return; |
| } |
| this._searchableView.updateCurrentMatchIndex(index); |
| } |
| |
| /** |
| * @override |
| */ |
| searchCanceled() { |
| this._searchRegex = null; |
| this._currentSearchTreeElements = []; |
| |
| for (let element = this._treeOutline.rootElement(); element; element = element.traverseNextTreeElement(false)) { |
| if (!(element instanceof ObjectUI.ObjectPropertyTreeElement)) { |
| continue; |
| } |
| element.revertHighlightChanges(); |
| } |
| this._updateSearchCount(0); |
| this._updateSearchIndex(0); |
| } |
| |
| /** |
| * @override |
| * @param {!UI.SearchableView.SearchConfig} searchConfig |
| * @param {boolean} shouldJump |
| * @param {boolean=} jumpBackwards |
| */ |
| performSearch(searchConfig, shouldJump, jumpBackwards) { |
| let newIndex = this._currentSearchFocusIndex; |
| const previousSearchFocusElement = this._currentSearchTreeElements[newIndex]; |
| this.searchCanceled(); |
| this._searchRegex = searchConfig.toSearchRegex(true); |
| |
| for (let element = this._treeOutline.rootElement(); element; element = element.traverseNextTreeElement(false)) { |
| if (!(element instanceof ObjectUI.ObjectPropertyTreeElement)) { |
| continue; |
| } |
| const hasMatch = element.setSearchRegex(this._searchRegex); |
| if (hasMatch) { |
| this._currentSearchTreeElements.push(element); |
| } |
| if (previousSearchFocusElement === element) { |
| const currentIndex = this._currentSearchTreeElements.length - 1; |
| if (hasMatch || jumpBackwards) { |
| newIndex = currentIndex; |
| } else { |
| newIndex = currentIndex + 1; |
| } |
| } |
| } |
| this._updateSearchCount(this._currentSearchTreeElements.length); |
| |
| if (!this._currentSearchTreeElements.length) { |
| this._updateSearchIndex(0); |
| return; |
| } |
| newIndex = mod(newIndex, this._currentSearchTreeElements.length); |
| |
| this._jumpToMatch(newIndex); |
| } |
| |
| /** |
| * @override |
| */ |
| jumpToNextSearchResult() { |
| if (!this._currentSearchTreeElements.length) { |
| return; |
| } |
| const newIndex = mod(this._currentSearchFocusIndex + 1, this._currentSearchTreeElements.length); |
| this._jumpToMatch(newIndex); |
| } |
| |
| /** |
| * @override |
| */ |
| jumpToPreviousSearchResult() { |
| if (!this._currentSearchTreeElements.length) { |
| return; |
| } |
| const newIndex = mod(this._currentSearchFocusIndex - 1, this._currentSearchTreeElements.length); |
| this._jumpToMatch(newIndex); |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| supportsCaseSensitiveSearch() { |
| return true; |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| supportsRegexSearch() { |
| return true; |
| } |
| } |
| |
| |
| /** |
| * @unrestricted |
| */ |
| export class ParsedJSON { |
| /** |
| * @param {*} data |
| * @param {string} prefix |
| * @param {string} suffix |
| */ |
| constructor(data, prefix, suffix) { |
| this.data = data; |
| this.prefix = prefix; |
| this.suffix = suffix; |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.SourceFrame = self.SourceFrame || {}; |
| |
| /* Legacy exported object */ |
| SourceFrame = SourceFrame || {}; |
| |
| /** @constructor */ |
| SourceFrame.JSONView = JSONView; |
| |
| /** @constructor */ |
| SourceFrame.ParsedJSON = ParsedJSON; |