| // 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. | |
| /** | |
| * @unrestricted | |
| */ | |
| export default class RemoteObjectPreviewFormatter { | |
| /** | |
| * @param {!Protocol.Runtime.PropertyPreview} a | |
| * @param {!Protocol.Runtime.PropertyPreview} b | |
| * @return {number} | |
| */ | |
| static _objectPropertyComparator(a, b) { | |
| return sortValue(a) - sortValue(b); | |
| /** | |
| * @param {!Protocol.Runtime.PropertyPreview} property | |
| * @return {number} | |
| */ | |
| function sortValue(property) { | |
| // TODO(einbinder) expose whether preview properties are actually internal. | |
| const internalName = _internalName; | |
| if (property.name === internalName.PromiseStatus) { | |
| return 1; | |
| } else if (property.name === internalName.PromiseValue) { | |
| return 2; | |
| } else if (property.name === internalName.GeneratorStatus || property.name === internalName.PrimitiveValue) { | |
| return 3; | |
| } | |
| // TODO(einbinder) expose whether preview properties are actually private. | |
| else if (property.type !== 'function' && !property.name.startsWith('#')) { | |
| return 4; | |
| } | |
| return 5; | |
| } | |
| } | |
| /** | |
| * @param {!DocumentFragment|!Element} parentElement | |
| * @param {!Protocol.Runtime.ObjectPreview} preview | |
| * @param {boolean} isEntry | |
| */ | |
| appendObjectPreview(parentElement, preview, isEntry) { | |
| const description = preview.description; | |
| const subTypesWithoutValuePreview = new Set(['null', 'regexp', 'error', 'internal#entry']); | |
| if (preview.type !== 'object' || subTypesWithoutValuePreview.has(preview.subtype) || isEntry) { | |
| parentElement.appendChild(this.renderPropertyPreview(preview.type, preview.subtype, description)); | |
| return; | |
| } | |
| const isArrayOrTypedArray = preview.subtype === 'array' || preview.subtype === 'typedarray'; | |
| if (description) { | |
| let text; | |
| if (isArrayOrTypedArray) { | |
| const arrayLength = SDK.RemoteObject.arrayLength(preview); | |
| const arrayLengthText = arrayLength > 1 ? ('(' + arrayLength + ')') : ''; | |
| const arrayName = SDK.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'); | |
| propertiesElement.createTextChild(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.length > 1 ? ',\xA0\u2026' : '\u2026'; | |
| propertiesElement.createChild('span').textContent = ellipsisText; | |
| } | |
| propertiesElement.createTextChild(isArrayOrTypedArray ? ']' : '}'); | |
| } | |
| /** | |
| * @param {string} description | |
| * @return {string} | |
| */ | |
| _abbreviateFullQualifiedClassName(description) { | |
| const abbreviatedDescription = description.split('.'); | |
| for (let i = 0; i < abbreviatedDescription.length - 1; ++i) { | |
| abbreviatedDescription[i] = abbreviatedDescription[i].trimMiddle(3); | |
| } | |
| return abbreviatedDescription.join('.'); | |
| } | |
| /** | |
| * @param {!Element} parentElement | |
| * @param {!Protocol.Runtime.ObjectPreview} preview | |
| */ | |
| _appendObjectPropertiesPreview(parentElement, preview) { | |
| const internalName = _internalName; | |
| const properties = preview.properties.filter(p => p.type !== 'accessor') | |
| .sort(RemoteObjectPreviewFormatter._objectPropertyComparator); | |
| for (let i = 0; i < properties.length; ++i) { | |
| if (i > 0) { | |
| parentElement.createTextChild(', '); | |
| } | |
| const property = properties[i]; | |
| const name = property.name; | |
| // Internal properties are given special formatting, e.g. Promises `<rejected>: 123`. | |
| if (preview.subtype === 'promise' && name === internalName.PromiseStatus) { | |
| parentElement.appendChild(this._renderDisplayName('<' + property.value + '>')); | |
| const nextProperty = i + 1 < properties.length ? properties[i + 1] : null; | |
| if (nextProperty && nextProperty.name === internalName.PromiseValue) { | |
| if (property.value !== 'pending') { | |
| parentElement.createTextChild(': '); | |
| parentElement.appendChild(this._renderPropertyPreviewOrAccessor([nextProperty])); | |
| } | |
| i++; | |
| } | |
| } else if (preview.subtype === 'generator' && name === internalName.GeneratorStatus) { | |
| parentElement.appendChild(this._renderDisplayName('<' + property.value + '>')); | |
| } else if (name === internalName.PrimitiveValue) { | |
| parentElement.appendChild(this._renderPropertyPreviewOrAccessor([property])); | |
| } else { | |
| parentElement.appendChild(this._renderDisplayName(name)); | |
| parentElement.createTextChild(': '); | |
| parentElement.appendChild(this._renderPropertyPreviewOrAccessor([property])); | |
| } | |
| } | |
| } | |
| /** | |
| * @param {!Element} parentElement | |
| * @param {!Protocol.Runtime.ObjectPreview} preview | |
| */ | |
| _appendArrayPropertiesPreview(parentElement, preview) { | |
| const arrayLength = SDK.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); | |
| /** | |
| * @param {!Protocol.Runtime.PropertyPreview} a | |
| * @param {!Protocol.Runtime.PropertyPreview} b | |
| * @return {number} | |
| */ | |
| function arrayEntryComparator(a, b) { | |
| return toArrayIndex(a.name) - toArrayIndex(b.name); | |
| } | |
| /** | |
| * @param {string} name | |
| * @return {number} | |
| */ | |
| function toArrayIndex(name) { | |
| const index = 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) { | |
| parentElement.createTextChild(', '); | |
| } | |
| const property = indexProperties[i]; | |
| const index = toArrayIndex(property.name); | |
| if (canShowGaps && index - lastNonEmptyArrayIndex > 1) { | |
| appendUndefined(index); | |
| parentElement.createTextChild(', '); | |
| } | |
| if (!canShowGaps && i !== index) { | |
| parentElement.appendChild(this._renderDisplayName(property.name)); | |
| parentElement.createTextChild(': '); | |
| } | |
| parentElement.appendChild(this._renderPropertyPreviewOrAccessor([property])); | |
| lastNonEmptyArrayIndex = index; | |
| elementsAdded = true; | |
| } | |
| if (canShowGaps && arrayLength - lastNonEmptyArrayIndex > 1) { | |
| if (elementsAdded) { | |
| parentElement.createTextChild(', '); | |
| } | |
| appendUndefined(arrayLength); | |
| } | |
| for (let i = 0; i < otherProperties.length; ++i) { | |
| if (elementsAdded) { | |
| parentElement.createTextChild(', '); | |
| } | |
| const property = otherProperties[i]; | |
| parentElement.appendChild(this._renderDisplayName(property.name)); | |
| parentElement.createTextChild(': '); | |
| parentElement.appendChild(this._renderPropertyPreviewOrAccessor([property])); | |
| elementsAdded = true; | |
| } | |
| /** | |
| * @param {number} index | |
| */ | |
| function appendUndefined(index) { | |
| const span = parentElement.createChild('span', 'object-value-undefined'); | |
| const count = index - lastNonEmptyArrayIndex - 1; | |
| span.textContent = count !== 1 ? Common.UIString('empty × %d', count) : Common.UIString('empty'); | |
| elementsAdded = true; | |
| } | |
| } | |
| /** | |
| * @param {!Element} parentElement | |
| * @param {!Protocol.Runtime.ObjectPreview} preview | |
| */ | |
| _appendEntriesPreview(parentElement, preview) { | |
| for (let i = 0; i < preview.entries.length; ++i) { | |
| if (i > 0) { | |
| parentElement.createTextChild(', '); | |
| } | |
| const entry = preview.entries[i]; | |
| if (entry.key) { | |
| this.appendObjectPreview(parentElement, entry.key, true /* isEntry */); | |
| parentElement.createTextChild(' => '); | |
| } | |
| this.appendObjectPreview(parentElement, entry.value, true /* isEntry */); | |
| } | |
| } | |
| /** | |
| * @param {string} name | |
| * @return {!Element} | |
| */ | |
| _renderDisplayName(name) { | |
| const result = createElementWithClass('span', 'name'); | |
| const needsQuotes = /^\s|\s$|^$|\n/.test(name); | |
| result.textContent = needsQuotes ? '"' + name.replace(/\n/g, '\u21B5') + '"' : name; | |
| return result; | |
| } | |
| /** | |
| * @param {!Array.<!Protocol.Runtime.PropertyPreview>} propertyPath | |
| * @return {!Element} | |
| */ | |
| _renderPropertyPreviewOrAccessor(propertyPath) { | |
| const property = propertyPath.peekLast(); | |
| return this.renderPropertyPreview(property.type, /** @type {string} */ (property.subtype), property.value); | |
| } | |
| /** | |
| * @param {string} type | |
| * @param {string=} subtype | |
| * @param {string=} description | |
| * @return {!Element} | |
| */ | |
| renderPropertyPreview(type, subtype, description) { | |
| const span = createElementWithClass('span', 'object-value-' + (subtype || type)); | |
| description = description || ''; | |
| if (type === 'accessor') { | |
| span.textContent = '(...)'; | |
| span.title = Common.UIString('The property is computed with a getter'); | |
| return span; | |
| } | |
| if (type === 'function') { | |
| span.textContent = '\u0192'; | |
| return span; | |
| } | |
| if (type === 'object' && subtype === 'node' && description) { | |
| createSpansForNodeTitle(span, description); | |
| return span; | |
| } | |
| if (type === 'string') { | |
| span.createTextChildren('"', description.replace(/\n/g, '\u21B5'), '"'); | |
| return span; | |
| } | |
| if (type === 'object' && !subtype) { | |
| let preview = this._abbreviateFullQualifiedClassName(description); | |
| if (preview === 'Object') { | |
| preview = '{\u2026}'; | |
| } | |
| span.textContent = preview; | |
| span.title = description; | |
| return span; | |
| } | |
| span.textContent = description; | |
| return span; | |
| } | |
| } | |
| /** @enum {string} */ | |
| const _internalName = { | |
| GeneratorStatus: '[[GeneratorStatus]]', | |
| PrimitiveValue: '[[PrimitiveValue]]', | |
| PromiseStatus: '[[PromiseStatus]]', | |
| PromiseValue: '[[PromiseValue]]' | |
| }; | |
| /** | |
| * @param {!Element} container | |
| * @param {string} nodeTitle | |
| */ | |
| export const createSpansForNodeTitle = function(container, nodeTitle) { | |
| const match = nodeTitle.match(/([^#.]+)(#[^.]+)?(\..*)?/); | |
| 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]; | |
| } | |
| }; | |
| /* Legacy exported object */ | |
| self.ObjectUI = self.ObjectUI || {}; | |
| /* Legacy exported object */ | |
| ObjectUI = ObjectUI || {}; | |
| /** @constructor */ | |
| ObjectUI.RemoteObjectPreviewFormatter = RemoteObjectPreviewFormatter; | |
| /** | |
| * @param {!Element} container | |
| * @param {string} nodeTitle | |
| */ | |
| ObjectUI.RemoteObjectPreviewFormatter.createSpansForNodeTitle = createSpansForNodeTitle; |