Files
react-native-devtools-frontend/front_end/source_frame/JSONView.js
T
Tim van der Lippe 77e0c895b2 Remove RelaxedJSONParser
We should not be parsing our own JSON. Instead, we should be using
`JSON.parse`. However, `JSON.parse` is of course spec-compliant and
not "relaxed". Thus, it would fail on invalid JSON. But, we do show
invalid JSON as plain text. E.g., if you were to inspect an invalid
JSON response in the network tab and it is actually not valid JSON,
we don't show the fancy object-tree-view, but instead the raw content.

Given that having our own JSON parser is not ideal and we still
gracefully fall back to a usable JSON preview, we should remove the
RelaxedJSONParser.

After this change, we can handle valid JSON, valid JSONP and show a text
preview for invalid JSON(P).

R=aerotwist@chromium.org

Bug: 1098730
Change-Id: I3c0513518e36b29f35124b2810a119fd3828ff93
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2264560
Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Paul Lewis <aerotwist@chromium.org>
2020-06-29 16:55:23 +00:00

352 lines
10 KiB
JavaScript

/*
* 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.
*/
import * as Common from '../common/common.js';
import * as ObjectUI from '../object_ui/object_ui.js';
import * as Platform from '../platform/platform.js';
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';
/**
* @implements {UI.SearchableView.Searchable}
*/
export class JSONView extends UI.Widget.VBox {
/**
* @param {!ParsedJSON} parsedJSON
* @param {boolean=} startCollapsed
*/
constructor(parsedJSON, startCollapsed) {
super();
this._initialized = false;
this.registerRequiredCSS('source_frame/jsonView.css');
this._parsedJSON = parsedJSON;
this._startCollapsed = !!startCollapsed;
this.element.classList.add('json-view');
/** @type {?UI.SearchableView.SearchableView} */
this._searchableView;
/** @type {!ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection} */
this._treeOutline;
/** @type {number} */
this._currentSearchFocusIndex = 0;
/** @type {!Array.<!UI.TreeOutline.TreeElement>} */
this._currentSearchTreeElements = [];
/** @type {?RegExp} */
this._searchRegex = null;
}
/**
* @param {string} content
* @return {!Promise<?UI.SearchableView.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.SearchableView(jsonView);
searchableView.setPlaceholder(Common.UIString.UIString('Find'));
jsonView._searchableView = searchableView;
jsonView.show(searchableView.element);
return searchableView;
}
/**
* @param {?Object} obj
* @return {!UI.SearchableView.SearchableView}
*/
static createViewSync(obj) {
const jsonView = new JSONView(new ParsedJSON(obj, '', ''));
const searchableView = new UI.SearchableView.SearchableView(jsonView);
searchableView.setPlaceholder(Common.UIString.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(null);
}
try {
const json = JSON.parse(returnObj.data);
if (!json) {
return Promise.resolve(null);
}
returnObj.data = json;
} catch (e) {
returnObj = null;
}
return Promise.resolve(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.RemoteObject.fromLocalObject(this._parsedJSON.data);
const title = this._parsedJSON.prefix + obj.description + this._parsedJSON.suffix;
this._treeOutline = new ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection(
obj, title, undefined, undefined, undefined, undefined, true /* showOverflow */);
this._treeOutline.enableContextMenu();
this._treeOutline.setEditable(false);
if (!this._startCollapsed) {
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.UIUtils.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.ObjectPropertiesSection.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.ObjectPropertiesSection.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 = Platform.NumberUtilities.mod(newIndex, this._currentSearchTreeElements.length);
this._jumpToMatch(newIndex);
}
/**
* @override
*/
jumpToNextSearchResult() {
if (!this._currentSearchTreeElements.length) {
return;
}
const newIndex =
Platform.NumberUtilities.mod(this._currentSearchFocusIndex + 1, this._currentSearchTreeElements.length);
this._jumpToMatch(newIndex);
}
/**
* @override
*/
jumpToPreviousSearchResult() {
if (!this._currentSearchTreeElements.length) {
return;
}
const newIndex =
Platform.NumberUtilities.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;
}
}