chrome-devtools-frontend
Version:
Chrome DevTools UI
341 lines (308 loc) • 14.5 kB
text/typescript
// Copyright (c) 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-imperative-dom-api */
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 * as UI from '../../legacy.js';
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_);
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;
}
}
appendObjectPreview(
parentElement: DocumentFragment|Element, preview: Protocol.Runtime.ObjectPreview, isEntry: boolean): void {
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)) || isEntry) {
parentElement.appendChild(this.renderPropertyPreview(preview.type, preview.subtype, undefined, description));
return;
}
const isArrayOrTypedArray = preview.subtype === Protocol.Runtime.ObjectPreviewSubtype.Array ||
preview.subtype === Protocol.Runtime.ObjectPreviewSubtype.Typedarray;
if (description) {
let text;
if (isArrayOrTypedArray) {
const arrayLength = SDK.RemoteObject.RemoteObject.arrayLength(preview);
const arrayLengthText = arrayLength > 1 ? ('(' + arrayLength + ')') : '';
const arrayName = SDK.RemoteObject.RemoteObject.arrayNameFromDescription(description);
text = arrayName === 'Array' ? arrayLengthText : (arrayName + arrayLengthText);
} else {
const hideDescription = description === 'Object';
text = hideDescription ? '' : description;
}
if (text.length > 0) {
parentElement.createChild('span', 'object-description').textContent = text + '\xA0';
}
}
const propertiesElement = parentElement.createChild('span', 'object-properties-preview');
UI.UIUtils.createTextChild(propertiesElement, isArrayOrTypedArray ? '[' : '{');
if (preview.entries) {
this.appendEntriesPreview(propertiesElement, preview);
} else if (isArrayOrTypedArray) {
this.appendArrayPropertiesPreview(propertiesElement, preview);
} else {
this.appendObjectPropertiesPreview(propertiesElement, preview);
}
if (preview.overflow) {
const ellipsisText = propertiesElement.textContent && propertiesElement.textContent.length > 1 ? ',\xA0…' : '…';
propertiesElement.createChild('span').textContent = ellipsisText;
}
UI.UIUtils.createTextChild(propertiesElement, isArrayOrTypedArray ? ']' : '}');
}
private 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.join('.');
}
private appendObjectPropertiesPreview(parentElement: Element, preview: Protocol.Runtime.ObjectPreview): void {
const properties = preview.properties.filter(p => p.type !== 'accessor')
.sort(RemoteObjectPreviewFormatter.objectPropertyComparator);
for (let i = 0; i < properties.length; ++i) {
if (i > 0) {
UI.UIUtils.createTextChild(parentElement, ', ');
}
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) {
parentElement.appendChild(this.renderDisplayName('<' + property.value + '>'));
const nextProperty = i + 1 < properties.length ? properties[i + 1] : null;
if (nextProperty && nextProperty.name === InternalName.PROMISE_RESULT) {
if (property.value !== 'pending') {
UI.UIUtils.createTextChild(parentElement, ': ');
parentElement.appendChild(this.renderPropertyPreviewOrAccessor([nextProperty]));
}
i++;
}
} else if (preview.subtype === 'generator' && name === InternalName.GENERATOR_STATE) {
parentElement.appendChild(this.renderDisplayName('<' + property.value + '>'));
} else if (name === InternalName.PRIMITIVE_VALUE) {
parentElement.appendChild(this.renderPropertyPreviewOrAccessor([property]));
} else if (name === InternalName.WEAK_REF_TARGET) {
if (property.type === Protocol.Runtime.PropertyPreviewType.Undefined) {
parentElement.appendChild(this.renderDisplayName('<cleared>'));
} else {
parentElement.appendChild(this.renderPropertyPreviewOrAccessor([property]));
}
} else {
parentElement.appendChild(this.renderDisplayName(name));
UI.UIUtils.createTextChild(parentElement, ': ');
parentElement.appendChild(this.renderPropertyPreviewOrAccessor([property]));
}
}
}
private appendArrayPropertiesPreview(parentElement: Element, preview: Protocol.Runtime.ObjectPreview): void {
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;
let lastNonEmptyArrayIndex = -1;
let elementsAdded = false;
for (let i = 0; i < indexProperties.length; ++i) {
if (elementsAdded) {
UI.UIUtils.createTextChild(parentElement, ', ');
}
const property = indexProperties[i];
const index = toArrayIndex(property.name);
if (canShowGaps && index - lastNonEmptyArrayIndex > 1) {
appendUndefined(index);
UI.UIUtils.createTextChild(parentElement, ', ');
}
if (!canShowGaps && i !== index) {
parentElement.appendChild(this.renderDisplayName(property.name));
UI.UIUtils.createTextChild(parentElement, ': ');
}
parentElement.appendChild(this.renderPropertyPreviewOrAccessor([property]));
lastNonEmptyArrayIndex = index;
elementsAdded = true;
}
if (canShowGaps && arrayLength - lastNonEmptyArrayIndex > 1) {
if (elementsAdded) {
UI.UIUtils.createTextChild(parentElement, ', ');
}
appendUndefined(arrayLength);
}
for (let i = 0; i < otherProperties.length; ++i) {
if (elementsAdded) {
UI.UIUtils.createTextChild(parentElement, ', ');
}
const property = otherProperties[i];
parentElement.appendChild(this.renderDisplayName(property.name));
UI.UIUtils.createTextChild(parentElement, ': ');
parentElement.appendChild(this.renderPropertyPreviewOrAccessor([property]));
elementsAdded = true;
}
function appendUndefined(index: number): void {
const span = parentElement.createChild('span', 'object-value-undefined');
const count = index - lastNonEmptyArrayIndex - 1;
// TODO(l10n): Plurals. Tricky because of a bug in the presubmit check for plurals.
span.textContent = count !== 1 ? i18nString(UIStrings.emptyD, {PH1: count}) : i18nString(UIStrings.empty);
elementsAdded = true;
}
}
private appendEntriesPreview(parentElement: Element, preview: Protocol.Runtime.ObjectPreview): void {
if (!preview.entries) {
return;
}
for (let i = 0; i < preview.entries.length; ++i) {
if (i > 0) {
UI.UIUtils.createTextChild(parentElement, ', ');
}
const entry = preview.entries[i];
if (entry.key) {
this.appendObjectPreview(parentElement, entry.key, true /* isEntry */);
UI.UIUtils.createTextChild(parentElement, ' => ');
}
this.appendObjectPreview(parentElement, entry.value, true /* isEntry */);
}
}
private renderDisplayName(name: string): Element {
const result = document.createElement('span');
result.classList.add('name');
const needsQuotes = /^\s|\s$|^$|\n/.test(name);
result.textContent = needsQuotes ? '"' + name.replace(/\n/g, '\u21B5') + '"' : name;
return result;
}
private renderPropertyPreviewOrAccessor(propertyPath: Protocol.Runtime.PropertyPreview[]): Element {
const property = propertyPath[propertyPath.length - 1];
if (!property) {
throw new Error('Could not find property');
}
return this.renderPropertyPreview(property.type, (property.subtype as string), property.name, property.value);
}
renderPropertyPreview(type: string, subtype?: string, className?: string|null, description?: string): HTMLElement {
const span = document.createElement('span');
span.classList.add('object-value-' + (subtype || type));
description = description || '';
if (type === 'accessor') {
span.textContent = '(...)';
UI.Tooltip.Tooltip.install(span, i18nString(UIStrings.thePropertyIsComputedWithAGetter));
return span;
}
if (type === 'function') {
span.textContent = '\u0192';
return span;
}
if (type === 'object' && subtype === 'trustedtype' && className) {
createSpanForTrustedType(span, description, className);
return span;
}
if (type === 'object' && subtype === 'node' && description) {
createSpansForNodeTitle(span, description);
return span;
}
if (type === 'string') {
UI.UIUtils.createTextChildren(span, Platform.StringUtilities.formatAsJSLiteral(description));
return span;
}
if (type === 'object' && !subtype) {
let preview = this.abbreviateFullQualifiedClassName(description);
if (preview === 'Object') {
preview = '{…}';
}
span.textContent = preview;
UI.Tooltip.Tooltip.install(span, description);
return span;
}
span.textContent = description;
return span;
}
}
const enum InternalName {
GENERATOR_STATE = '[[GeneratorState]]',
PRIMITIVE_VALUE = '[[PrimitiveValue]]',
PROMISE_STATE = '[[PromiseState]]',
PROMISE_RESULT = '[[PromiseResult]]',
WEAK_REF_TARGET = '[[WeakRefTarget]]',
}
export const createSpansForNodeTitle = function(container: Element, nodeTitle: string): void {
const match = nodeTitle.match(/([^#.]+)(#[^.]+)?(\..*)?/);
if (!match) {
return;
}
container.createChild('span', 'webkit-html-tag-name').textContent = match[1];
if (match[2]) {
container.createChild('span', 'webkit-html-attribute-value').textContent = match[2];
}
if (match[3]) {
container.createChild('span', 'webkit-html-attribute-name').textContent = match[3];
}
};
export const createSpanForTrustedType = function(span: Element, description: string, className: string): void {
UI.UIUtils.createTextChildren(span, `${className} `);
const trustedContentSpan = document.createElement('span');
trustedContentSpan.classList.add('object-value-string');
UI.UIUtils.createTextChildren(trustedContentSpan, '"', description.replace(/\n/g, '\u21B5'), '"');
span.appendChild(trustedContentSpan);
};