blob: 74b733ce399790d5aa19c0987299a228e0cc1d40 [file] [log] [blame]
/*
* 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;