| // 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, includeNullOrUndefined = true): 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, includeNullOrUndefined)); |
| |
| // 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, includeNullOrUndefined: boolean): |
| 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; |
| if (!includeNullOrUndefined && |
| (property.type === 'undefined' || (property.type === 'object' && property.subtype === 'null'))) { |
| continue; |
| } |
| |
| // 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>`; |
| } |