blob: e49356679f97d94240b2ace2b0033e8ff832c96e [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}
*/
SourceFrame.JSONView = class extends UI.VBox {
/**
* @param {!SourceFrame.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 SourceFrame.JSONView._parseJSON(content);
if (!parsedJSON || typeof parsedJSON.data !== 'object')
return null;
const jsonView = new SourceFrame.JSONView(parsedJSON);
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 {?Object} obj
* @return {!UI.SearchableView}
*/
static createViewSync(obj) {
const jsonView = new SourceFrame.JSONView(new SourceFrame.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<?SourceFrame.ParsedJSON>}
*/
static _parseJSON(text) {
let returnObj = null;
if (text)
returnObj = SourceFrame.JSONView._extractJSON(/** @type {string} */ (text));
if (!returnObj)
return Promise.resolve(/** @type {?SourceFrame.ParsedJSON} */ (null));
return Formatter.formatterWorkerPool().parseJSONRelaxed(returnObj.data).then(handleReturnedJSON);
/**
* @param {*} data
* @return {?SourceFrame.ParsedJSON}
*/
function handleReturnedJSON(data) {
if (!data)
return null;
returnObj.data = data;
return returnObj;
}
}
/**
* @param {string} text
* @return {?SourceFrame.ParsedJSON}
*/
static _extractJSON(text) {
// Do not treat HTML as JSON.
if (text.startsWith('<'))
return null;
let inner = SourceFrame.JSONView._findBrackets(text, '{', '}');
const inner2 = SourceFrame.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 SourceFrame.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);
this._treeOutline.enableContextMenu();
this._treeOutline.setEditable(false);
this._treeOutline.expand();
this.element.appendChild(this._treeOutline.element);
}
/**
* @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
*/
SourceFrame.ParsedJSON = class {
/**
* @param {*} data
* @param {string} prefix
* @param {string} suffix
*/
constructor(data, prefix, suffix) {
this.data = data;
this.prefix = prefix;
this.suffix = suffix;
}
};