| /* |
| * Copyright (C) 2011 Google Inc. All rights reserved. |
| * Copyright (C) 2007, 2008, 2013-2015 Apple Inc. All rights reserved. |
| * Copyright (C) 2009 Joseph Pecoraro |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object |
| { |
| constructor(message) |
| { |
| super(); |
| |
| console.assert(message instanceof WI.ConsoleMessage); |
| |
| this._message = message; |
| this._expandable = false; |
| this._repeatCount = message._repeatCount || 0; |
| |
| // These are the parameters unused by the messages's optional format string. |
| // Any extra parameters will be displayed as children of this message. |
| this._extraParameters = message.parameters; |
| } |
| |
| // Public |
| |
| render() |
| { |
| this._element = document.createElement("div"); |
| this._element.classList.add("console-message"); |
| this._element.dir = "ltr"; |
| |
| // FIXME: <https://webkit.org/b/143545> Web Inspector: LogContentView should use higher level objects |
| this._element.__message = this._message; |
| this._element.__messageView = this; |
| |
| if (this._message.type === WI.ConsoleMessage.MessageType.Result) { |
| this._element.classList.add("console-user-command-result"); |
| this._element.setAttribute("data-labelprefix", WI.UIString("Output: ")); |
| } else if (this._message.type === WI.ConsoleMessage.MessageType.StartGroup || this._message.type === WI.ConsoleMessage.MessageType.StartGroupCollapsed) |
| this._element.classList.add("console-group-title"); |
| |
| switch (this._message.level) { |
| case WI.ConsoleMessage.MessageLevel.Log: |
| this._element.classList.add("console-log-level"); |
| this._element.setAttribute("data-labelprefix", WI.UIString("Log: ")); |
| break; |
| case WI.ConsoleMessage.MessageLevel.Info: |
| this._element.classList.add("console-info-level"); |
| this._element.setAttribute("data-labelprefix", WI.UIString("Info: ")); |
| break; |
| case WI.ConsoleMessage.MessageLevel.Debug: |
| this._element.classList.add("console-debug-level"); |
| this._element.setAttribute("data-labelprefix", WI.UIString("Debug: ")); |
| break; |
| case WI.ConsoleMessage.MessageLevel.Warning: |
| this._element.classList.add("console-warning-level"); |
| this._element.setAttribute("data-labelprefix", WI.UIString("Warning: ")); |
| break; |
| case WI.ConsoleMessage.MessageLevel.Error: |
| this._element.classList.add("console-error-level"); |
| this._element.setAttribute("data-labelprefix", WI.UIString("Error: ")); |
| break; |
| } |
| |
| // FIXME: The location link should include stack trace information. |
| this._appendLocationLink(); |
| |
| this._messageBodyElement = this._element.appendChild(document.createElement("span")); |
| this._messageBodyElement.classList.add("console-top-level-message", "console-message-body"); |
| this._appendMessageTextAndArguments(this._messageBodyElement); |
| this._appendSavedResultIndex(); |
| |
| this._appendExtraParameters(); |
| this._appendStackTrace(); |
| |
| this._renderRepeatCount(); |
| |
| if (this._message.type === WI.ConsoleMessage.MessageType.Dir) |
| this.expand(); |
| else if (this._message.type === WI.ConsoleMessage.MessageType.Image) { |
| this._element.classList.add("console-image"); |
| this._element.addEventListener("contextmenu", this._handleContextMenu.bind(this)); |
| } |
| } |
| |
| get element() |
| { |
| return this._element; |
| } |
| |
| get message() |
| { |
| return this._message; |
| } |
| |
| get repeatCount() |
| { |
| return this._repeatCount; |
| } |
| |
| set repeatCount(count) |
| { |
| console.assert(typeof count === "number"); |
| |
| if (this._repeatCount === count) |
| return; |
| |
| this._repeatCount = count; |
| |
| if (this._element) |
| this._renderRepeatCount(); |
| } |
| |
| _renderRepeatCount() |
| { |
| let count = this._repeatCount; |
| |
| if (count <= 1) { |
| if (this._repeatCountElement) { |
| this._repeatCountElement.remove(); |
| this._repeatCountElement = null; |
| } |
| return; |
| } |
| |
| if (!this._repeatCountElement) { |
| this._repeatCountElement = document.createElement("span"); |
| this._repeatCountElement.classList.add("repeat-count"); |
| this._element.insertBefore(this._repeatCountElement, this._element.firstChild); |
| } |
| |
| this._repeatCountElement.textContent = Number.abbreviate(count); |
| } |
| |
| get expandable() |
| { |
| // There are extra arguments or a call stack that can be shown. |
| if (this._expandable) |
| return true; |
| |
| // There is an object tree that could be expanded. |
| if (this._objectTree) |
| return true; |
| |
| return false; |
| } |
| |
| expand() |
| { |
| if (this._expandable) |
| this._element.classList.add("expanded"); |
| |
| // Auto-expand an inner object tree if there is a single object. |
| // For Trace messages we are auto-expanding for the call stack, don't also auto-expand an object as well. |
| if (this._objectTree && this._message.type !== WI.ConsoleMessage.MessageType.Trace) { |
| if (!this._extraParameters || this._extraParameters.length <= 1) |
| this._objectTree.expand(); |
| } |
| } |
| |
| collapse() |
| { |
| if (this._expandable) |
| this._element.classList.remove("expanded"); |
| |
| // Collapse the object tree just in cases where it was autoexpanded. |
| if (this._objectTree) { |
| if (!this._extraParameters || this._extraParameters.length <= 1) |
| this._objectTree.collapse(); |
| } |
| } |
| |
| toggle() |
| { |
| if (this._element.classList.contains("expanded")) |
| this.collapse(); |
| else |
| this.expand(); |
| } |
| |
| toClipboardString(isPrefixOptional) |
| { |
| let clipboardString = this._messageBodyElement.innerText.removeWordBreakCharacters(); |
| if (this._message.savedResultIndex) |
| clipboardString = clipboardString.replace(new RegExp(`\\s*=\\s*(${WI.RuntimeManager.preferredSavedResultPrefix()}\\d+)$`), ""); |
| |
| let hasStackTrace = this._shouldShowStackTrace(); |
| if (!hasStackTrace) { |
| let repeatString = this.repeatCount > 1 ? "x" + this.repeatCount : ""; |
| let urlLine = ""; |
| if (this._message.url) { |
| let components = [WI.displayNameForURL(this._message.url), "line " + this._message.line]; |
| if (repeatString) |
| components.push(repeatString); |
| urlLine = " (" + components.join(", ") + ")"; |
| } else if (repeatString) |
| urlLine = " (" + repeatString + ")"; |
| |
| if (urlLine) { |
| let lines = clipboardString.split("\n"); |
| lines[0] += urlLine; |
| clipboardString = lines.join("\n"); |
| } |
| } |
| |
| if (this._extraElementsList) |
| clipboardString += "\n" + this._extraElementsList.innerText.removeWordBreakCharacters().trim(); |
| |
| if (hasStackTrace) { |
| this._message.stackTrace.callFrames.forEach(function(frame) { |
| clipboardString += "\n\t" + (frame.functionName || WI.UIString("(anonymous function)")); |
| if (frame.sourceCodeLocation) |
| clipboardString += " (" + frame.sourceCodeLocation.originalLocationString() + ")"; |
| }); |
| } |
| |
| if (!isPrefixOptional || this._enforcesClipboardPrefixString()) |
| return this._clipboardPrefixString() + clipboardString; |
| return clipboardString; |
| } |
| |
| removeEventListeners() |
| { |
| // FIXME: <https://webkit.org/b/196956> Web Inspector: use weak collections for holding event listeners |
| WI.settings.consoleSavedResultAlias.removeEventListener(null, null, this); |
| } |
| |
| // Private |
| |
| _appendMessageTextAndArguments(element) |
| { |
| if (this._message.source === WI.ConsoleMessage.MessageSource.ConsoleAPI) { |
| switch (this._message.type) { |
| case WI.ConsoleMessage.MessageType.Trace: |
| var args = [WI.UIString("Trace")]; |
| if (this._message.parameters) { |
| if (this._message.parameters[0].type === "string") { |
| var prefixedFormatString = WI.UIString("Trace: %s").format(this._message.parameters[0].description); |
| args = [prefixedFormatString].concat(this._message.parameters.slice(1)); |
| } else |
| args.pushAll(this._message.parameters); |
| } |
| this._appendFormattedArguments(element, args); |
| return; |
| |
| case WI.ConsoleMessage.MessageType.Assert: |
| var args = [WI.UIString("Assertion Failed")]; |
| if (this._message.parameters) { |
| if (this._message.parameters[0].type === "string") { |
| var prefixedFormatString = WI.UIString("Assertion Failed: %s").format(this._message.parameters[0].description); |
| args = [prefixedFormatString].concat(this._message.parameters.slice(1)); |
| } else |
| args.pushAll(this._message.parameters); |
| } |
| this._appendFormattedArguments(element, args); |
| return; |
| |
| case WI.ConsoleMessage.MessageType.Dir: |
| var obj = this._message.parameters ? this._message.parameters[0] : undefined; |
| this._appendFormattedArguments(element, ["%O", obj]); |
| return; |
| |
| case WI.ConsoleMessage.MessageType.Table: |
| var args = this._message.parameters; |
| element.appendChild(this._formatParameterAsTable(args)); |
| this._extraParameters = null; |
| return; |
| |
| case WI.ConsoleMessage.MessageType.StartGroup: |
| case WI.ConsoleMessage.MessageType.StartGroupCollapsed: |
| var args = this._message.parameters || [this._message.messageText || WI.UIString("Group")]; |
| this._formatWithSubstitutionString(args, element); |
| this._extraParameters = null; |
| return; |
| |
| case WI.ConsoleMessage.MessageType.Timing: { |
| let args = [this._message.messageText]; |
| if (this._extraParameters) |
| args.pushAll(this._extraParameters); |
| this._appendFormattedArguments(element, args); |
| return; |
| } |
| |
| case WI.ConsoleMessage.MessageType.Image: { |
| if (this._message.level === WI.ConsoleMessage.MessageLevel.Log) { |
| let divider = null; |
| |
| if (this._message.parameters.length > 1) { |
| this._appendFormattedArguments(element, this._message.parameters.slice(1)); |
| |
| divider = element.appendChild(document.createElement("hr")); |
| } |
| |
| let target = this._message.parameters[0]; |
| if (target === "Viewport") |
| target = WI.UIString("Viewport"); |
| this._appendFormattedArguments(element, [target]); |
| |
| if (this._message.messageText) { |
| let img = document.createElement("img"); |
| img.classList.add("show-grid"); |
| img.src = this._message.messageText; |
| img.setAttribute("filename", WI.FileUtilities.screenshotString() + ".png"); |
| img.addEventListener("load", (event) => { |
| if (img.width >= img.height) |
| img.width = img.width / window.devicePixelRatio; |
| else |
| img.height = img.height / window.devicePixelRatio; |
| element.appendChild(img); |
| }); |
| img.addEventListener("error", (event) => { |
| this._element.setAttribute("data-labelprefix", WI.UIString("Error: ")); |
| this._element.classList.add("console-error-level"); |
| this._element.classList.remove("console-log-level"); |
| |
| if (divider) { |
| while (divider.nextSibling) |
| divider.nextSibling.remove(); |
| } else |
| element.removeChildren(); |
| |
| let args = [WI.UIString("Could not capture screenshot"), this._message.messageText]; |
| if (this._extraParameters) |
| args.pushAll(this._extraParameters); |
| this._appendFormattedArguments(element, args); |
| }); |
| } |
| return; |
| } |
| |
| if (this._message.level === WI.ConsoleMessage.MessageLevel.Error) { |
| let args = []; |
| if (this._message.messageText === "Could not capture screenshot") |
| args.push(WI.UIString("Could not capture screenshot")); |
| else |
| args.push(this._message.messageText); |
| if (this._extraParameters) |
| args.pushAll(this._extraParameters); |
| this._appendFormattedArguments(element, args); |
| return; |
| } |
| |
| console.assert(); |
| break; |
| } |
| } |
| } |
| |
| // FIXME: Better handle WI.ConsoleMessage.MessageSource.Network once it has request info. |
| |
| var args = this._message.parameters || [this._message.messageText]; |
| this._appendFormattedArguments(element, args); |
| } |
| |
| _appendSavedResultIndex(element) |
| { |
| let savedResultIndex = this._message.savedResultIndex; |
| if (!savedResultIndex) |
| return; |
| |
| console.assert(this._message instanceof WI.ConsoleCommandResultMessage); |
| console.assert(this._message.type === WI.ConsoleMessage.MessageType.Result); |
| |
| var savedVariableElement = document.createElement("span"); |
| savedVariableElement.classList.add("console-saved-variable"); |
| |
| // FIXME: <https://webkit.org/b/196956> Web Inspector: use weak collections for holding event listeners |
| function updateSavedVariableText() { |
| savedVariableElement.textContent = " = " + WI.RuntimeManager.preferredSavedResultPrefix() + savedResultIndex; |
| } |
| WI.settings.consoleSavedResultAlias.addEventListener(WI.Setting.Event.Changed, updateSavedVariableText, this); |
| updateSavedVariableText(); |
| |
| if (this._objectTree) |
| this._objectTree.appendTitleSuffix(savedVariableElement); |
| else |
| this._messageBodyElement.appendChild(savedVariableElement); |
| } |
| |
| _appendLocationLink() |
| { |
| if (this._message.source === WI.ConsoleMessage.MessageSource.Network) { |
| if (this._message.url) { |
| var anchor = WI.linkifyURLAsNode(this._message.url, this._message.url, "console-message-url"); |
| anchor.classList.add("console-message-location"); |
| this._element.appendChild(anchor); |
| } |
| return; |
| } |
| |
| var firstNonNativeNonAnonymousCallFrame = this._message.stackTrace.firstNonNativeNonAnonymousCallFrame; |
| |
| var callFrame; |
| if (firstNonNativeNonAnonymousCallFrame) { |
| // JavaScript errors and console.* methods. |
| callFrame = firstNonNativeNonAnonymousCallFrame; |
| } else if (this._message.url && !this._shouldHideURL(this._message.url)) { |
| // CSS warnings have no stack traces. |
| callFrame = WI.CallFrame.fromPayload(this._message.target, { |
| functionName: "", |
| url: this._message.url, |
| lineNumber: this._message.line, |
| columnNumber: this._message.column |
| }); |
| } |
| |
| if (callFrame && (!callFrame.isConsoleEvaluation || (WI.isDebugUIEnabled() && WI.settings.debugShowConsoleEvaluations.value))) { |
| const showFunctionName = !!callFrame.functionName; |
| var locationElement = new WI.CallFrameView(callFrame, showFunctionName); |
| locationElement.classList.add("console-message-location"); |
| this._element.appendChild(locationElement); |
| return; |
| } |
| |
| if (this._message.parameters && this._message.parameters.length === 1) { |
| var parameter = this._createRemoteObjectIfNeeded(this._message.parameters[0]); |
| |
| parameter.findFunctionSourceCodeLocation().then((result) => { |
| if (result === WI.RemoteObject.SourceCodeLocationPromise.NoSourceFound || result === WI.RemoteObject.SourceCodeLocationPromise.MissingObjectId) |
| return; |
| |
| let link = WI.linkifySourceCode(result.sourceCode, new WI.SourceCodePosition(result.lineNumber, result.columnNumber), { |
| className: "console-message-url", |
| ignoreNetworkTab: true, |
| ignoreSearchTab: true, |
| }); |
| link.classList.add("console-message-location"); |
| |
| if (this._element.hasChildNodes()) |
| this._element.insertBefore(link, this._element.firstChild); |
| else |
| this._element.appendChild(link); |
| }); |
| } |
| } |
| |
| _appendExtraParameters() |
| { |
| if (!this._extraParameters || !this._extraParameters.length) |
| return; |
| |
| this._makeExpandable(); |
| |
| // Auto-expand if there are multiple objects or if there were simple parameters. |
| if (this._extraParameters.length > 1) |
| this.expand(); |
| |
| this._extraElementsList = this._element.appendChild(document.createElement("ol")); |
| this._extraElementsList.classList.add("console-message-extra-parameters-container"); |
| |
| for (var parameter of this._extraParameters) { |
| var listItemElement = this._extraElementsList.appendChild(document.createElement("li")); |
| const forceObjectFormat = parameter.type === "object" && (parameter.subtype !== "null" && parameter.subtype !== "regexp" && parameter.subtype !== "node" && parameter.subtype !== "error"); |
| listItemElement.classList.add("console-message-extra-parameter"); |
| listItemElement.appendChild(this._formatParameter(parameter, forceObjectFormat)); |
| } |
| } |
| |
| _appendStackTrace() |
| { |
| if (!this._shouldShowStackTrace()) |
| return; |
| |
| this._makeExpandable(); |
| |
| if (this._message.type === WI.ConsoleMessage.MessageType.Trace && WI.settings.consoleAutoExpandTrace.value) |
| this.expand(); |
| |
| this._stackTraceElement = this._element.appendChild(document.createElement("div")); |
| this._stackTraceElement.classList.add("console-message-body", "console-message-stack-trace-container"); |
| |
| var callFramesElement = new WI.StackTraceView(this._message.stackTrace).element; |
| this._stackTraceElement.appendChild(callFramesElement); |
| } |
| |
| _createRemoteObjectIfNeeded(parameter) |
| { |
| // FIXME: Only pass RemoteObjects here so we can avoid this work. |
| if (parameter instanceof WI.RemoteObject) |
| return parameter; |
| |
| if (typeof parameter === "object") |
| return WI.RemoteObject.fromPayload(parameter, this._message.target); |
| |
| return WI.RemoteObject.fromPrimitiveValue(parameter); |
| } |
| |
| _appendFormattedArguments(element, parameters) |
| { |
| if (!parameters.length) |
| return; |
| |
| for (let i = 0; i < parameters.length; ++i) |
| parameters[i] = this._createRemoteObjectIfNeeded(parameters[i]); |
| |
| let builderElement = element.appendChild(document.createElement("span")); |
| let shouldFormatWithStringSubstitution = parameters[0].type === "string" && this._message.type !== WI.ConsoleMessage.MessageType.Result; |
| |
| // Single object (e.g. console result or logging a non-string object). |
| if (parameters.length === 1 && !shouldFormatWithStringSubstitution) { |
| this._extraParameters = null; |
| builderElement.appendChild(this._formatParameter(parameters[0], false)); |
| return; |
| } |
| |
| console.assert(this._message.type !== WI.ConsoleMessage.MessageType.Result); |
| |
| if (shouldFormatWithStringSubstitution && this._isStackTrace(parameters[0])) |
| shouldFormatWithStringSubstitution = false; |
| |
| let needsDivider = false; |
| function appendDividerIfNeeded() { |
| if (!needsDivider) |
| return null; |
| let element = builderElement.appendChild(document.createElement("span")); |
| element.classList.add("console-message-preview-divider"); |
| element.textContent = ` ${enDash} `; |
| return element; |
| } |
| |
| // Format string. |
| if (shouldFormatWithStringSubstitution) { |
| let result = this._formatWithSubstitutionString(parameters, builderElement); |
| parameters = result.unusedSubstitutions; |
| this._extraParameters = parameters; |
| needsDivider = true; |
| } |
| |
| // Trailing inline parameters. |
| if (parameters.length) { |
| let simpleParametersCount = 0; |
| for (let parameter of parameters) { |
| if (!this._hasSimpleDisplay(parameter)) |
| break; |
| simpleParametersCount++; |
| } |
| |
| // Show one or more simple parameters inline on the message line. |
| if (simpleParametersCount) { |
| let simpleParameters = parameters.splice(0, simpleParametersCount); |
| this._extraParameters = parameters; |
| |
| for (let parameter of simpleParameters) { |
| let dividerElement = appendDividerIfNeeded(); |
| if (dividerElement) |
| dividerElement.classList.add("inline-lossless"); |
| |
| let previewContainer = builderElement.appendChild(document.createElement("span")); |
| previewContainer.classList.add("inline-lossless"); |
| |
| let preview = WI.FormattedValue.createObjectPreviewOrFormattedValueForRemoteObject(parameter, WI.ObjectPreviewView.Mode.Brief); |
| let isPreviewView = preview instanceof WI.ObjectPreviewView; |
| |
| if (isPreviewView) |
| preview.setOriginatingObjectInfo(parameter, null); |
| |
| let previewElement = isPreviewView ? preview.element : preview; |
| previewContainer.appendChild(previewElement); |
| |
| needsDivider = true; |
| |
| // Simple displayable parameters should pretty much always be lossless. |
| // An exception might be a truncated string. |
| console.assert((isPreviewView && preview.lossless) || (!isPreviewView && this._shouldConsiderObjectLossless(parameter))); |
| } |
| } |
| |
| // If there is a single non-simple parameter after simple paramters, show it inline. |
| if (parameters.length === 1 && !this._isStackTrace(parameters[0])) { |
| let parameter = parameters[0]; |
| |
| let dividerElement = appendDividerIfNeeded(); |
| |
| let previewContainer = builderElement.appendChild(document.createElement("span")); |
| previewContainer.classList.add("console-message-preview"); |
| |
| let preview = WI.FormattedValue.createObjectPreviewOrFormattedValueForRemoteObject(parameter, WI.ObjectPreviewView.Mode.Brief); |
| let isPreviewView = preview instanceof WI.ObjectPreviewView; |
| |
| if (isPreviewView) |
| preview.setOriginatingObjectInfo(parameter, null); |
| |
| let previewElement = isPreviewView ? preview.element : preview; |
| previewContainer.appendChild(previewElement); |
| |
| needsDivider = true; |
| |
| // If this preview is effectively lossless, we can avoid making this console message expandable. |
| if ((isPreviewView && preview.lossless) || (!isPreviewView && this._shouldConsiderObjectLossless(parameter))) { |
| this._extraParameters = null; |
| if (dividerElement) |
| dividerElement.classList.add("inline-lossless"); |
| previewContainer.classList.add("inline-lossless"); |
| } |
| } else if (parameters.length) { |
| // Multiple remaining objects. Show an indicator and they will be appended as extra parameters. |
| let enclosedElement = document.createElement("span"); |
| builderElement.append(" ", enclosedElement); |
| enclosedElement.classList.add("console-message-enclosed"); |
| enclosedElement.textContent = "(" + parameters.length + ")"; |
| } |
| } |
| } |
| |
| _hasSimpleDisplay(parameter) |
| { |
| console.assert(parameter instanceof WI.RemoteObject); |
| |
| return WI.FormattedValue.hasSimpleDisplay(parameter) && !this._isStackTrace(parameter); |
| } |
| |
| _isStackTrace(parameter) |
| { |
| console.assert(parameter instanceof WI.RemoteObject); |
| |
| return parameter.type === "string" && WI.StackTrace.isLikelyStackTrace(parameter.description); |
| } |
| |
| _shouldConsiderObjectLossless(object) |
| { |
| if (object.type === "string") { |
| const description = object.description; |
| const maxLength = WI.FormattedValue.MAX_PREVIEW_STRING_LENGTH; |
| const longOrMultiLineString = description.length > maxLength || description.slice(0, maxLength).includes("\n"); |
| return !longOrMultiLineString; |
| } |
| |
| return object.type !== "object" || object.subtype === "null" || object.subtype === "regexp"; |
| } |
| |
| _formatParameter(parameter, forceObjectFormat) |
| { |
| var type; |
| if (forceObjectFormat) |
| type = "object"; |
| else if (parameter instanceof WI.RemoteObject) |
| type = parameter.subtype || parameter.type; |
| else { |
| console.assert(false, "no longer reachable"); |
| type = typeof parameter; |
| } |
| |
| var formatters = { |
| "object": this._formatParameterAsObject, |
| "error": this._formatParameterAsError, |
| "map": this._formatParameterAsObject, |
| "set": this._formatParameterAsObject, |
| "weakmap": this._formatParameterAsObject, |
| "weakset": this._formatParameterAsObject, |
| "iterator": this._formatParameterAsObject, |
| "class": this._formatParameterAsObject, |
| "proxy": this._formatParameterAsObject, |
| "array": this._formatParameterAsArray, |
| "node": this._formatParameterAsNode, |
| "string": this._formatParameterAsString, |
| }; |
| |
| var formatter = formatters[type] || this._formatParameterAsValue; |
| |
| const fragment = document.createDocumentFragment(); |
| formatter.call(this, parameter, fragment, forceObjectFormat); |
| return fragment; |
| } |
| |
| _formatParameterAsValue(value, fragment) |
| { |
| fragment.appendChild(WI.FormattedValue.createElementForRemoteObject(value)); |
| } |
| |
| _formatParameterAsString(object, fragment) |
| { |
| if (this._isStackTrace(object)) { |
| let stackTrace = WI.StackTrace.fromString(this._message.target, object.description); |
| if (stackTrace.callFrames.length) { |
| let stackView = new WI.StackTraceView(stackTrace); |
| fragment.appendChild(stackView.element); |
| return; |
| } |
| } |
| |
| fragment.appendChild(WI.FormattedValue.createLinkifiedElementString(object.description)); |
| } |
| |
| _formatParameterAsNode(object, fragment) |
| { |
| fragment.appendChild(WI.FormattedValue.createElementForNode(object)); |
| } |
| |
| _formatParameterAsObject(object, fragment, forceExpansion) |
| { |
| // FIXME: Should have a better ObjectTreeView mode for classes (static methods and methods). |
| this._objectTree = new WI.ObjectTreeView(object, null, this._rootPropertyPathForObject(object), forceExpansion); |
| fragment.appendChild(this._objectTree.element); |
| } |
| |
| _formatParameterAsError(object, fragment) |
| { |
| this._objectTree = new WI.ErrorObjectView(object); |
| fragment.appendChild(this._objectTree.element); |
| } |
| |
| _formatParameterAsArray(array, fragment) |
| { |
| this._objectTree = new WI.ObjectTreeView(array, WI.ObjectTreeView.Mode.Properties, this._rootPropertyPathForObject(array)); |
| fragment.appendChild(this._objectTree.element); |
| } |
| |
| _rootPropertyPathForObject(object) |
| { |
| let savedResultIndex = this._message.savedResultIndex; |
| if (!savedResultIndex) |
| return null; |
| |
| function prefixSavedResultIndex() { |
| return WI.RuntimeManager.preferredSavedResultPrefix() + savedResultIndex; |
| } |
| |
| let propertyPath = new WI.PropertyPath(object, prefixSavedResultIndex()); |
| |
| // FIXME: <https://webkit.org/b/196956> Web Inspector: use weak collections for holding event listeners |
| WI.settings.consoleSavedResultAlias.addEventListener(WI.Setting.Event.Changed, (event) => { |
| propertyPath.pathComponent = prefixSavedResultIndex(); |
| }, this); |
| |
| return propertyPath; |
| } |
| |
| _formatWithSubstitutionString(parameters, formattedResult) |
| { |
| function parameterFormatter(force, obj) |
| { |
| return this._formatParameter(obj, force); |
| } |
| |
| function stringFormatter(obj) |
| { |
| return obj.description; |
| } |
| |
| function floatFormatter(obj, token) |
| { |
| let value = typeof obj.value === "number" ? obj.value : obj.description; |
| return String.standardFormatters.f(value, token); |
| } |
| |
| function integerFormatter(obj) |
| { |
| let value = typeof obj.value === "number" ? obj.value : obj.description; |
| return String.standardFormatters.d(value); |
| } |
| |
| var currentStyle = null; |
| function styleFormatter(obj) |
| { |
| currentStyle = {}; |
| var buffer = document.createElement("span"); |
| buffer.setAttribute("style", obj.description); |
| for (var i = 0; i < buffer.style.length; i++) { |
| var property = buffer.style[i]; |
| if (isWhitelistedProperty(property)) |
| currentStyle[property] = buffer.style[property]; |
| } |
| } |
| |
| function isWhitelistedProperty(property) |
| { |
| for (var prefix of ["background", "border", "color", "font", "line", "margin", "padding", "text"]) { |
| if (property.startsWith(prefix) || property.startsWith("-webkit-" + prefix)) |
| return true; |
| } |
| return false; |
| } |
| |
| // Firebug uses %o for formatting objects. |
| var formatters = {}; |
| formatters.o = parameterFormatter.bind(this, false); |
| formatters.s = stringFormatter; |
| formatters.f = floatFormatter; |
| |
| // Firebug allows both %i and %d for formatting integers. |
| formatters.i = integerFormatter; |
| formatters.d = integerFormatter; |
| |
| // Firebug uses %c for styling the message. |
| formatters.c = styleFormatter; |
| |
| // Support %O to force object formatting, instead of the type-based %o formatting. |
| formatters.O = parameterFormatter.bind(this, true); |
| |
| function append(a, b) |
| { |
| if (b instanceof Node) |
| a.appendChild(b); |
| else if (b !== undefined) { |
| var toAppend = WI.linkifyStringAsFragment(b.toString()); |
| if (currentStyle) { |
| var wrapper = document.createElement("span"); |
| for (var key in currentStyle) |
| wrapper.style[key] = currentStyle[key]; |
| wrapper.appendChild(toAppend); |
| toAppend = wrapper; |
| } |
| |
| a.appendChild(toAppend); |
| } |
| return a; |
| } |
| |
| // String.format does treat formattedResult like a Builder, result is an object. |
| return String.format(parameters[0].description, parameters.slice(1), formatters, formattedResult, append); |
| } |
| |
| _shouldShowStackTrace() |
| { |
| if (!this._message.stackTrace.callFrames.length) |
| return false; |
| |
| return this._message.source === WI.ConsoleMessage.MessageSource.Network |
| || this._message.level === WI.ConsoleMessage.MessageLevel.Error |
| || this._message.type === WI.ConsoleMessage.MessageType.Trace; |
| } |
| |
| _shouldHideURL(url) |
| { |
| return url === "undefined" || url === "[native code]"; |
| } |
| |
| _userProvidedColumnNames(columnNamesArgument) |
| { |
| if (!columnNamesArgument) |
| return null; |
| |
| console.assert(columnNamesArgument instanceof WI.RemoteObject); |
| |
| // Single primitive argument. |
| if (columnNamesArgument.type === "string" || columnNamesArgument.type === "number") |
| return [String(columnNamesArgument.value)]; |
| |
| // Ignore everything that is not an array with property previews. |
| if (columnNamesArgument.type !== "object" || columnNamesArgument.subtype !== "array" || !columnNamesArgument.preview || !columnNamesArgument.preview.propertyPreviews) |
| return null; |
| |
| // Array. Look into the preview and get string values. |
| var extractedColumnNames = []; |
| for (var propertyPreview of columnNamesArgument.preview.propertyPreviews) { |
| if (propertyPreview.type === "string" || propertyPreview.type === "number") |
| extractedColumnNames.push(String(propertyPreview.value)); |
| } |
| |
| return extractedColumnNames.length ? extractedColumnNames : null; |
| } |
| |
| _formatParameterAsTable(parameters) |
| { |
| var element = document.createElement("span"); |
| var table = parameters[0]; |
| if (!table || !table.preview) |
| return element; |
| |
| var rows = []; |
| var columnNames = []; |
| var flatValues = []; |
| var preview = table.preview; |
| var userProvidedColumnNames = false; |
| |
| // User provided columnNames. |
| var extractedColumnNames = this._userProvidedColumnNames(parameters[1]); |
| if (extractedColumnNames) { |
| userProvidedColumnNames = true; |
| columnNames = extractedColumnNames; |
| } |
| |
| // Check first for valuePreviews in the properties meaning this was an array of objects. |
| if (preview.propertyPreviews) { |
| for (var i = 0; i < preview.propertyPreviews.length; ++i) { |
| var rowProperty = preview.propertyPreviews[i]; |
| var rowPreview = rowProperty.valuePreview; |
| if (!rowPreview || !rowPreview.propertyPreviews) |
| continue; |
| |
| var rowValue = {}; |
| var maxColumnsToRender = 15; |
| for (var j = 0; j < rowPreview.propertyPreviews.length; ++j) { |
| var cellProperty = rowPreview.propertyPreviews[j]; |
| var columnRendered = columnNames.includes(cellProperty.name); |
| if (!columnRendered) { |
| if (userProvidedColumnNames || columnNames.length === maxColumnsToRender) |
| continue; |
| columnRendered = true; |
| columnNames.push(cellProperty.name); |
| } |
| |
| rowValue[cellProperty.name] = WI.FormattedValue.createElementForPropertyPreview(cellProperty); |
| } |
| rows.push([rowProperty.name, rowValue]); |
| } |
| } |
| |
| // If there were valuePreviews, convert to a flat list. |
| if (rows.length) { |
| columnNames.unshift(WI.UIString("(Index)")); |
| for (var i = 0; i < rows.length; ++i) { |
| var rowName = rows[i][0]; |
| var rowValue = rows[i][1]; |
| flatValues.push(rowName); |
| for (var j = 1; j < columnNames.length; ++j) { |
| var columnName = columnNames[j]; |
| if (!(columnName in rowValue)) |
| flatValues.push(emDash); |
| else |
| flatValues.push(rowValue[columnName]); |
| } |
| } |
| } |
| |
| // If there were no value Previews, then check for an array of values. |
| if (!flatValues.length && preview.propertyPreviews) { |
| for (var i = 0; i < preview.propertyPreviews.length; ++i) { |
| var rowProperty = preview.propertyPreviews[i]; |
| if (!("value" in rowProperty)) |
| continue; |
| |
| if (!columnNames.length) { |
| columnNames.push(WI.UIString("Index")); |
| columnNames.push(WI.UIString("Value")); |
| } |
| |
| flatValues.push(rowProperty.name); |
| flatValues.push(WI.FormattedValue.createElementForPropertyPreview(rowProperty)); |
| } |
| } |
| |
| // If no table data show nothing. |
| if (!flatValues.length) |
| return element; |
| |
| // FIXME: Should we output something extra if the preview is lossless? |
| |
| var dataGrid = WI.DataGrid.createSortableDataGrid(columnNames, flatValues); |
| dataGrid.inline = true; |
| dataGrid.variableHeightRows = true; |
| |
| element.appendChild(dataGrid.element); |
| |
| dataGrid.updateLayoutIfNeeded(); |
| |
| return element; |
| } |
| |
| _levelString() |
| { |
| switch (this._message.level) { |
| case WI.ConsoleMessage.MessageLevel.Log: |
| return "Log"; |
| case WI.ConsoleMessage.MessageLevel.Info: |
| return "Info"; |
| case WI.ConsoleMessage.MessageLevel.Warning: |
| return "Warning"; |
| case WI.ConsoleMessage.MessageLevel.Debug: |
| return "Debug"; |
| case WI.ConsoleMessage.MessageLevel.Error: |
| return "Error"; |
| } |
| } |
| |
| _enforcesClipboardPrefixString() |
| { |
| return this._message.type !== WI.ConsoleMessage.MessageType.Result; |
| } |
| |
| _clipboardPrefixString() |
| { |
| if (this._message.type === WI.ConsoleMessage.MessageType.Result) |
| return "< "; |
| |
| return "[" + this._levelString() + "] "; |
| } |
| |
| _makeExpandable() |
| { |
| if (this._expandable) |
| return; |
| |
| this._expandable = true; |
| |
| this._element.classList.add("expandable"); |
| |
| this._boundClickHandler = this.toggle.bind(this); |
| this._messageBodyElement.addEventListener("click", this._boundClickHandler); |
| } |
| |
| _handleContextMenu(event) |
| { |
| let image = event.target.closest(".console-image > .console-message-body > img"); |
| if (!image) |
| return; |
| |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| |
| contextMenu.appendItem(WI.UIString("Save Image"), () => { |
| const forceSaveAs = true; |
| WI.FileUtilities.save({ |
| url: WI.FileUtilities.inspectorURLForFilename(image.getAttribute("filename")), |
| content: parseDataURL(this._message.messageText).data, |
| base64Encoded: true, |
| }, forceSaveAs); |
| }); |
| |
| contextMenu.appendSeparator(); |
| } |
| }; |