UNPKG

chrome-devtools-frontend

Version:
301 lines (267 loc) 14.2 kB
// Copyright 2015 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as i18n from '../../../../core/i18n/i18n.js'; import * as Platform from '../../../../core/platform/platform.js'; import * as SDK from '../../../../core/sdk/sdk.js'; import * as Protocol from '../../../../generated/protocol.js'; import {Directives, html, type LitTemplate, nothing, render} from '../../../lit/lit.js'; const {ifDefined, repeat} = Directives; const UIStrings = { /** * @description Text shown in the console object preview. Shown when the user is inspecting a * JavaScript object and there are multiple empty properties on the object (x = * 'times'/'multiply'). * @example {3} PH1 */ emptyD: 'empty × {PH1}', /** * @description Shown when the user is inspecting a JavaScript object in the console and there is * an empty property on the object.. */ empty: 'empty', /** * @description Text shown when the user is inspecting a JavaScript object, but of the properties * is not immediately available because it is a JavaScript 'getter' function, which means we have * to run some code first in order to compute this property. */ thePropertyIsComputedWithAGetter: 'The property is computed with a getter', } as const; const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/object_ui/RemoteObjectPreviewFormatter.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); interface PropertyPreviewValue { name?: string; entry?: Protocol.Runtime.EntryPreview; value?: Protocol.Runtime.PropertyPreview; placeholder?: string; } export class RemoteObjectPreviewFormatter { private static objectPropertyComparator(a: Protocol.Runtime.PropertyPreview, b: Protocol.Runtime.PropertyPreview): number { return sortValue(a) - sortValue(b); function sortValue(property: Protocol.Runtime.PropertyPreview): number { // TODO(einbinder) expose whether preview properties are actually internal. if (property.name === InternalName.PROMISE_STATE) { return 1; } if (property.name === InternalName.PROMISE_RESULT) { return 2; } if (property.name === InternalName.GENERATOR_STATE || property.name === InternalName.PRIMITIVE_VALUE || property.name === InternalName.WEAK_REF_TARGET) { return 3; } if (property.type !== Protocol.Runtime.PropertyPreviewType.Function && !property.name.startsWith('#')) { return 4; } return 5; } } renderObjectPreview(preview: Protocol.Runtime.ObjectPreview): LitTemplate { const description = preview.description; const subTypesWithoutValuePreview = new Set<Protocol.Runtime.ObjectPreviewSubtype|'internal#entry'|'trustedtype'>([ Protocol.Runtime.ObjectPreviewSubtype.Arraybuffer, Protocol.Runtime.ObjectPreviewSubtype.Dataview, Protocol.Runtime.ObjectPreviewSubtype.Error, Protocol.Runtime.ObjectPreviewSubtype.Null, Protocol.Runtime.ObjectPreviewSubtype.Regexp, Protocol.Runtime.ObjectPreviewSubtype.Webassemblymemory, 'internal#entry', 'trustedtype', ]); if (preview.type !== Protocol.Runtime.ObjectPreviewType.Object || (preview.subtype && subTypesWithoutValuePreview.has(preview.subtype))) { return this.renderPropertyPreview(preview.type, preview.subtype, undefined, description); } const isArrayOrTypedArray = preview.subtype === Protocol.Runtime.ObjectPreviewSubtype.Array || preview.subtype === Protocol.Runtime.ObjectPreviewSubtype.Typedarray; let objectDescription = ''; if (description) { if (isArrayOrTypedArray) { const arrayLength = SDK.RemoteObject.RemoteObject.arrayLength(preview); const arrayLengthText = arrayLength > 1 ? ('(' + arrayLength + ')') : ''; const arrayName = SDK.RemoteObject.RemoteObject.arrayNameFromDescription(description); objectDescription = arrayName === 'Array' ? arrayLengthText : (arrayName + arrayLengthText); } else { const hideDescription = description === 'Object'; objectDescription = hideDescription ? '' : description; } } const items = Array.from( preview.entries ? this.renderEntries(preview) : isArrayOrTypedArray ? this.renderArrayProperties(preview) : this.renderObjectProperties(preview)); // clang-format off const renderName = (name: string): LitTemplate => html`<span class=name>${ /^\s|\s$|^$|\n/.test(name)? '"' + name.replace(/\n/g, '\u21B5') + '"' : name}</span>`; const renderPlaceholder = (placeholder: string): LitTemplate => html`<span class=object-value-undefined>${placeholder}</span>`; const renderValue = (value: Protocol.Runtime.PropertyPreview): LitTemplate=> this.renderPropertyPreview(value.type, value.subtype, value.name, value.value); const renderEntry = (entry: Protocol.Runtime.EntryPreview): LitTemplate=> html`${entry.key && html`${this.renderPropertyPreview(entry.key.type, entry.key.subtype, undefined, entry.key.description)} => `} ${this.renderPropertyPreview(entry.value.type, entry.value.subtype, undefined, entry.value.description)}`; const renderItem = ({name, entry, value, placeholder}: PropertyPreviewValue, index: number): LitTemplate => html`${ index > 0 ? ', ' : ''}${ placeholder !== undefined ? renderPlaceholder(placeholder) : nothing}${ name !== undefined ? renderName(name) : nothing}${ name !== undefined && value ? ': ' : ''}${ value ? renderValue(value) : nothing}${ entry ? renderEntry(entry) : nothing}`; // clang-format on return html`${ objectDescription.length > 0 ? html`<span class=object-description>${objectDescription + '\xA0'}</span>` : nothing}<span class=object-properties-preview>${isArrayOrTypedArray ? '[' : '{'}${ repeat(items, renderItem)}${preview.overflow ? html`<span>${items.length > 0 ? ',\xA0…' : '…'}</span>` : ''} ${isArrayOrTypedArray ? ']' : '}'}</span>`; } private * renderObjectProperties(preview: Protocol.Runtime.ObjectPreview): Generator<PropertyPreviewValue> { const properties = preview.properties.filter(p => p.type !== 'accessor') .sort(RemoteObjectPreviewFormatter.objectPropertyComparator); for (let i = 0; i < properties.length; ++i) { const property = properties[i]; const name = property.name; // Internal properties are given special formatting, e.g. Promises `<rejected>: 123`. if (preview.subtype === Protocol.Runtime.ObjectPreviewSubtype.Promise && name === InternalName.PROMISE_STATE) { const promiseResult = properties.at(i + 1)?.name === InternalName.PROMISE_RESULT ? properties.at(i + 1) : undefined; if (promiseResult) { i++; } yield {name: '<' + property.value + '>', value: property.value !== 'pending' ? promiseResult : undefined}; } else if (preview.subtype === 'generator' && name === InternalName.GENERATOR_STATE) { yield {name: '<' + property.value + '>'}; } else if (name === InternalName.PRIMITIVE_VALUE) { yield {value: property}; } else if (name === InternalName.WEAK_REF_TARGET) { if (property.type === Protocol.Runtime.PropertyPreviewType.Undefined) { yield {name: '<cleared>'}; } else { yield {value: property}; } } else { yield {name, value: property}; } } } private * renderArrayProperties(preview: Protocol.Runtime.ObjectPreview): Generator<PropertyPreviewValue> { const arrayLength = SDK.RemoteObject.RemoteObject.arrayLength(preview); const indexProperties = preview.properties.filter(p => toArrayIndex(p.name) !== -1).sort(arrayEntryComparator); const otherProperties = preview.properties.filter(p => toArrayIndex(p.name) === -1) .sort(RemoteObjectPreviewFormatter.objectPropertyComparator); function arrayEntryComparator(a: Protocol.Runtime.PropertyPreview, b: Protocol.Runtime.PropertyPreview): number { return toArrayIndex(a.name) - toArrayIndex(b.name); } function toArrayIndex(name: string): number { // We need to differentiate between property accesses and array index accesses // Therefore, we need to make sure we are always dealing with an i32, in the event // that a particular property also exists, but as the literal string. For example // for {["1.5"]: true}, we don't want to return `true` if we provide `1.5` as the // value, but only want to do that if we provide `"1.5"`. const index = Number(name) >>> 0; if (String(index) === name && index < arrayLength) { return index; } return -1; } // Gaps can be shown when all properties are guaranteed to be in the preview. const canShowGaps = !preview.overflow; const indexedProperties: Array<{property: Protocol.Runtime.PropertyPreview, index: number, gap: number, hasGaps: boolean}> = []; for (const property of indexProperties) { const index = toArrayIndex(property.name); const gap = index - (indexedProperties.at(-1)?.index ?? -1) - 1; const hasGaps = index !== indexedProperties.length; indexedProperties.push({property, index, gap, hasGaps}); } const trailingGap = arrayLength - (indexedProperties.at(-1)?.index ?? -1) - 1; // TODO(l10n): Plurals. Tricky because of a bug in the presubmit check for plurals. const renderGap = (count: number): {placeholder: string} => ({placeholder: count !== 1 ? i18nString(UIStrings.emptyD, {PH1: count}) : i18nString(UIStrings.empty)}); for (const {property, gap, hasGaps} of indexedProperties) { if (canShowGaps && gap > 0) { yield renderGap(gap); } yield {name: !canShowGaps && hasGaps ? property.name : undefined, value: property}; } if (canShowGaps && trailingGap > 0) { yield renderGap(trailingGap); } for (const property of otherProperties) { yield {name: property.name, value: property}; } } private * renderEntries(preview: Protocol.Runtime.ObjectPreview): Generator<PropertyPreviewValue> { for (const entry of preview.entries ?? []) { yield {entry}; } } renderPropertyPreview(type: string, subtype?: string, className?: string|null, description?: string): LitTemplate { const title = type === 'accessor' ? i18nString(UIStrings.thePropertyIsComputedWithAGetter) : (type === 'object' && !subtype) ? description : undefined; const abbreviateFullQualifiedClassName = (description: string): string => { const abbreviatedDescription = description.split('.'); for (let i = 0; i < abbreviatedDescription.length - 1; ++i) { abbreviatedDescription[i] = Platform.StringUtilities.trimMiddle(abbreviatedDescription[i], 3); } return abbreviatedDescription.length === 1 && abbreviatedDescription[0] === 'Object' ? '{…}' : abbreviatedDescription.join('.'); }; const preview = (): string|LitTemplate|undefined|null => type === 'accessor' ? '(...)' : type === 'function' ? '\u0192' : type === 'object' && subtype === 'trustedtype' && className ? renderTrustedType(description ?? '', className) : type === 'object' && subtype === 'node' && description ? renderNodeTitle(description) : type === 'string' ? Platform.StringUtilities.formatAsJSLiteral(description ?? '') : type === 'object' && !subtype ? abbreviateFullQualifiedClassName(description ?? '') : description; return html`<span class='object-value-${(subtype || type)}' title=${ifDefined(title)}>${preview()}</span>`; } renderEvaluationResultPreview(result: SDK.RuntimeModel.EvaluationResult, allowErrors?: boolean): LitTemplate { if ('error' in result) { return nothing; } if (result.exceptionDetails?.exception?.description) { const exception = result.exceptionDetails.exception.description; if (exception.startsWith('TypeError: ') || allowErrors) { return html`<span>${result.exceptionDetails.text} ${exception}</span>`; } return nothing; } const {preview, type, subtype, className, description} = result.object; if (preview && type === 'object' && subtype !== 'node' && subtype !== 'trustedtype') { return this.renderObjectPreview(preview); } return this.renderPropertyPreview( type, subtype, className, Platform.StringUtilities.trimEndWithMaxLength(description || '', 400)); } /** @deprecated (crbug.com/457388389) Use lit version instead */ renderEvaluationResultPreviewFragment(result: SDK.RuntimeModel.EvaluationResult, allowErrors?: boolean): DocumentFragment { const fragment = document.createDocumentFragment(); /* eslint-disable-next-line @devtools/no-lit-render-outside-of-view */ render(this.renderEvaluationResultPreview(result, allowErrors), fragment); return fragment; } } const enum InternalName { GENERATOR_STATE = '[[GeneratorState]]', PRIMITIVE_VALUE = '[[PrimitiveValue]]', PROMISE_STATE = '[[PromiseState]]', PROMISE_RESULT = '[[PromiseResult]]', WEAK_REF_TARGET = '[[WeakRefTarget]]', } export function renderNodeTitle(nodeTitle: string): LitTemplate|null { const match = nodeTitle.match(/([^#.]+)(#[^.]+)?(\..*)?/); if (!match) { return null; } return html`<span class=webkit-html-tag-name>${match[1]}</span>${ match[2] && html`<span class=webkit-html-attribute-value>${match[2]}</span>`}${ match[3] && html`<span class=webkit-html-attribute-name>${match[3]}</span>`}`; } export function renderTrustedType(description: string, className: string): LitTemplate { return html`${className} <span class=object-value-string>"${description.replace(/\n/g, '\u21B5')}"</span>`; }