UNPKG

chrome-devtools-frontend

Version:
302 lines (259 loc) • 9.36 kB
// Copyright 2011 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-imperative-dom-api */ import * as i18n from '../../../../core/i18n/i18n.js'; import * as SDK from '../../../../core/sdk/sdk.js'; import {html, render} from '../../../lit/lit.js'; import * as VisualLogging from '../../../visual_logging/visual_logging.js'; import * as UI from '../../legacy.js'; import * as ObjectUI from '../object_ui/object_ui.js'; import jsonViewStyles from './jsonView.css.js'; const UIStrings = { /** * @description Text to find an item */ find: 'Find', } as const; const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/source_frame/JSONView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export interface ViewInput { objectTree: ObjectUI.ObjectPropertiesSection.ObjectTree; parsedJSON: ParsedJSON; } export type ViewOutput = undefined; const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTMLElement): void => { const obj = SDK.RemoteObject.RemoteObject.fromLocalObject(input.parsedJSON.data); const titleText = input.parsedJSON.prefix + obj.description + input.parsedJSON.suffix; const title = html`<span>${titleText}</span>`; render(html` <style>${jsonViewStyles}</style> ${ObjectUI.ObjectPropertiesSection.renderObjectPropertiesSection(input.objectTree, title)} `, target, { container: { classes: ['json-view'], attributes: { jslog: VisualLogging.section('json-view'), }, }, }); }; type View = typeof DEFAULT_VIEW; export class JSONView extends UI.Widget.VBox implements UI.SearchableView.Searchable { #parsedJSON: ParsedJSON; private readonly startCollapsed: boolean; private searchableView: UI.SearchableView.SearchableView|null = null; private objectTree: ObjectUI.ObjectPropertiesSection.ObjectTree|null = null; private readonly search: UI.TreeOutline.TreeSearch<ObjectUI.ObjectPropertiesSection.ObjectTreeNodeBase>; private readonly view: View; constructor(parsedJSON: ParsedJSON, startCollapsed?: boolean, element?: HTMLElement, view: View = DEFAULT_VIEW) { super(element); this.#parsedJSON = parsedJSON; this.startCollapsed = Boolean(startCollapsed); this.search = new UI.TreeOutline.TreeSearch(); this.view = view; } static async createView(content: string): Promise<UI.SearchableView.SearchableView|null> { // 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, null); searchableView.setPlaceholder(i18nString(UIStrings.find)); jsonView.searchableView = searchableView; jsonView.show(searchableView.element); return searchableView; } static createViewSync(obj: Object|null, element?: HTMLElement): UI.SearchableView.SearchableView { const jsonView = new JSONView(new ParsedJSON(obj, '', '')); const searchableView = new UI.SearchableView.SearchableView(jsonView, null, undefined, element); searchableView.setPlaceholder(i18nString(UIStrings.find)); jsonView.searchableView = searchableView; jsonView.show(searchableView.element); jsonView.element.tabIndex = 0; return searchableView; } set parsedJSON(parsedJSON: ParsedJSON) { if (this.objectTree) { this.objectTree.removeEventListener(ObjectUI.ObjectPropertiesSection.ObjectTreeNodeBase.Events.CHILDREN_CHANGED, this.#onChildrenChanged, this); } this.#parsedJSON = parsedJSON; this.objectTree = null; this.onSearchCanceled(); this.requestUpdate(); } setSearchableView(searchableView: UI.SearchableView.SearchableView): void { this.searchableView = searchableView; } private static parseJSON(text: string|null): Promise<ParsedJSON|null> { let returnObj: ParsedJSON<string>|null = null; if (text) { returnObj = JSONView.extractJSON(text); } if (!returnObj) { return Promise.resolve(null); } try { const json = JSON.parse(returnObj.data); if (!json) { return Promise.resolve(null); } returnObj.data = json; } catch { returnObj = null; } return Promise.resolve(returnObj); } private static extractJSON(text: string): ParsedJSON<string>|null { // 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); } private static findBrackets(text: string, open: string, close: string): { start: number, end: number, length: number, } { const start = text.indexOf(open); const end = text.lastIndexOf(close); let length: -1|number = end - start - 1; if (start === -1 || end === -1 || end < start) { length = -1; } return {start, end, length}; } override wasShown(): void { super.wasShown(); this.initialize(); this.requestUpdate(); } private initialize(): void { if (this.objectTree) { return; } const obj = SDK.RemoteObject.RemoteObject.fromLocalObject(this.#parsedJSON.data); this.objectTree = new ObjectUI.ObjectPropertiesSection.ObjectTree(obj, { readOnly: true, propertiesMode: ObjectUI.ObjectPropertiesSection.ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED, search: this.search, }); if (!this.startCollapsed) { this.objectTree.expanded = true; } this.objectTree.addEventListener(ObjectUI.ObjectPropertiesSection.ObjectTreeNodeBase.Events.CHILDREN_CHANGED, this.#onChildrenChanged, this); } #onChildrenChanged(): void { this.requestUpdate(); } override performUpdate(): void { this.initialize(); if (!this.objectTree) { return; } this.view({objectTree: this.objectTree, parsedJSON: this.#parsedJSON}, undefined, this.contentElement); } private jumpToMatch(): void { if (this.searchableView) { this.search.updateSearchableView(this.searchableView); } const currentMatch = this.search.currentMatch(); if (currentMatch) { let current = currentMatch.node.parent; while (current) { current.expanded = true; current = current.parent; } } } onSearchCanceled(): void { this.search.reset(); if (this.searchableView) { this.search.updateSearchableView(this.searchableView); } } performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void { this.initialize(); this.onSearchCanceled(); const searchRegex = searchConfig.toSearchRegex(true).regex; if (!this.objectTree) { return; } this.search.search(this.objectTree, jumpBackwards ?? false, (node, closeTag) => { if (closeTag || !searchRegex) { return []; } return node.match(searchRegex); }); if (shouldJump) { this.jumpToMatch(); } else if (this.searchableView) { this.search.updateSearchableView(this.searchableView); } } jumpToNextSearchResult(): void { this.search.next(); this.jumpToMatch(); } jumpToPreviousSearchResult(): void { this.search.prev(); this.jumpToMatch(); } supportsCaseSensitiveSearch(): boolean { return true; } supportsWholeWordSearch(): boolean { return true; } supportsRegexSearch(): boolean { return true; } } export class ParsedJSON<T extends unknown = unknown> { data: T; prefix: string; suffix: string; constructor(data: T, prefix: string, suffix: string) { this.data = data; this.prefix = prefix; this.suffix = suffix; } } export class SearchableJsonView extends UI.SearchableView.SearchableView { #jsonView: JSONView; constructor(element: HTMLElement) { const jsonView = new JSONView(new ParsedJSON('', '', '')); super(jsonView, null, undefined, element); this.#jsonView = jsonView; this.setPlaceholder(i18nString(UIStrings.find)); jsonView.setSearchableView(this); jsonView.show(this.element); jsonView.element.tabIndex = 0; } set jsonObject(obj: Object|null|undefined) { this.#jsonView.parsedJSON = new ParsedJSON(obj, '', ''); this.searchProvider = this.#jsonView; this.#jsonView.show(this.element); this.requestUpdate(); } }