Web Inspector: Logging a native function to the console, such as `alert`, produces...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ConsoleMessageView.js
1 /*
2  * Copyright (C) 2011 Google Inc.  All rights reserved.
3  * Copyright (C) 2007, 2008, 2013-2015 Apple Inc.  All rights reserved.
4  * Copyright (C) 2009 Joseph Pecoraro
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1.  Redistributions of source code must retain the above copyright
11  *     notice, this list of conditions and the following disclaimer.
12  * 2.  Redistributions in binary form must reproduce the above copyright
13  *     notice, this list of conditions and the following disclaimer in the
14  *     documentation and/or other materials provided with the distribution.
15  * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
16  *     its contributors may be used to endorse or promote products derived
17  *     from this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
32 {
33     constructor(message)
34     {
35         super();
36
37         console.assert(message instanceof WI.ConsoleMessage);
38
39         this._message = message;
40         this._expandable = false;
41         this._repeatCount = message._repeatCount || 0;
42
43         // These are the parameters unused by the messages's optional format string.
44         // Any extra parameters will be displayed as children of this message.
45         this._extraParameters = message.parameters;
46     }
47
48     // Public
49
50     render()
51     {
52         this._element = document.createElement("div");
53         this._element.classList.add("console-message");
54
55         // FIXME: <https://webkit.org/b/143545> Web Inspector: LogContentView should use higher level objects
56         this._element.__message = this._message;
57         this._element.__messageView = this;
58
59         if (this._message.type === WI.ConsoleMessage.MessageType.Result) {
60             this._element.classList.add("console-user-command-result");
61             this._element.setAttribute("data-labelprefix", WI.UIString("Output: "));
62         } else if (this._message.type === WI.ConsoleMessage.MessageType.StartGroup || this._message.type === WI.ConsoleMessage.MessageType.StartGroupCollapsed)
63             this._element.classList.add("console-group-title");
64
65         switch (this._message.level) {
66         case WI.ConsoleMessage.MessageLevel.Log:
67             this._element.classList.add("console-log-level");
68             this._element.setAttribute("data-labelprefix", WI.UIString("Log: "));
69             break;
70         case WI.ConsoleMessage.MessageLevel.Info:
71             this._element.classList.add("console-info-level");
72             this._element.setAttribute("data-labelprefix", WI.UIString("Info: "));
73             break;
74         case WI.ConsoleMessage.MessageLevel.Debug:
75             this._element.classList.add("console-debug-level");
76             this._element.setAttribute("data-labelprefix", WI.UIString("Debug: "));
77             break;
78         case WI.ConsoleMessage.MessageLevel.Warning:
79             this._element.classList.add("console-warning-level");
80             this._element.setAttribute("data-labelprefix", WI.UIString("Warning: "));
81             break;
82         case WI.ConsoleMessage.MessageLevel.Error:
83             this._element.classList.add("console-error-level");
84             this._element.setAttribute("data-labelprefix", WI.UIString("Error: "));
85             break;
86         }
87
88         // FIXME: The location link should include stack trace information.
89         this._appendLocationLink();
90
91         this._messageTextElement = this._element.appendChild(document.createElement("span"));
92         this._messageTextElement.classList.add("console-top-level-message");
93         this._messageTextElement.classList.add("console-message-text");
94         this._appendMessageTextAndArguments(this._messageTextElement);
95         this._appendSavedResultIndex();
96
97         this._appendExtraParameters();
98         this._appendStackTrace();
99
100         this._renderRepeatCount();
101     }
102
103     get element()
104     {
105         return this._element;
106     }
107
108     get message()
109     {
110         return this._message;
111     }
112
113     get repeatCount()
114     {
115         return this._repeatCount;
116     }
117
118     set repeatCount(count)
119     {
120         console.assert(typeof count === "number");
121
122         if (this._repeatCount === count)
123             return;
124
125         this._repeatCount = count;
126
127         if (this._element)
128             this._renderRepeatCount();
129     }
130
131     _renderRepeatCount()
132     {
133         let count = this._repeatCount;
134
135         if (count <= 1) {
136             if (this._repeatCountElement) {
137                 this._repeatCountElement.remove();
138                 this._repeatCountElement = null;
139             }
140             return;
141         }
142
143         if (!this._repeatCountElement) {
144             this._repeatCountElement = document.createElement("span");
145             this._repeatCountElement.classList.add("repeat-count");
146             this._element.insertBefore(this._repeatCountElement, this._element.firstChild);
147         }
148
149         this._repeatCountElement.textContent = Number.abbreviate(count);
150     }
151
152     get expandable()
153     {
154         // There are extra arguments or a call stack that can be shown.
155         if (this._expandable)
156             return true;
157
158         // There is an object tree that could be expanded.
159         if (this._objectTree)
160             return true;
161
162         return false;
163     }
164
165     expand()
166     {
167         if (this._expandable)
168             this._element.classList.add("expanded");
169
170         // Auto-expand an inner object tree if there is a single object.
171         // For Trace messages we are auto-expanding for the call stack, don't also auto-expand an object as well.
172         if (this._objectTree && this._message.type !== WI.ConsoleMessage.MessageType.Trace) {
173             if (!this._extraParameters || this._extraParameters.length <= 1)
174                 this._objectTree.expand();
175         }
176     }
177
178     collapse()
179     {
180         if (this._expandable)
181             this._element.classList.remove("expanded");
182
183         // Collapse the object tree just in cases where it was autoexpanded.
184         if (this._objectTree) {
185             if (!this._extraParameters || this._extraParameters.length <= 1)
186                 this._objectTree.collapse();
187         }
188     }
189
190     toggle()
191     {
192         if (this._element.classList.contains("expanded"))
193             this.collapse();
194         else
195             this.expand();
196     }
197
198     toClipboardString(isPrefixOptional)
199     {
200         let clipboardString = this._messageTextElement.innerText.removeWordBreakCharacters();
201         if (this._message.savedResultIndex)
202             clipboardString = clipboardString.replace(/\s*=\s*(\$\d+)$/, "");
203
204         let hasStackTrace = this._shouldShowStackTrace();
205         if (!hasStackTrace) {
206             let repeatString = this.repeatCount > 1 ? "x" + this.repeatCount : "";
207             let urlLine = "";
208             if (this._message.url) {
209                 let components = [WI.displayNameForURL(this._message.url), "line " + this._message.line];
210                 if (repeatString)
211                     components.push(repeatString);
212                 urlLine = " (" + components.join(", ") + ")";
213             } else if (repeatString)
214                 urlLine = " (" + repeatString + ")";
215
216             if (urlLine) {
217                 let lines = clipboardString.split("\n");
218                 lines[0] += urlLine;
219                 clipboardString = lines.join("\n");
220             }
221         }
222
223         if (this._extraElementsList)
224             clipboardString += "\n" + this._extraElementsList.innerText.removeWordBreakCharacters().trim();
225
226         if (hasStackTrace) {
227             this._message.stackTrace.callFrames.forEach(function(frame) {
228                 clipboardString += "\n\t" + (frame.functionName || WI.UIString("(anonymous function)"));
229                 if (frame.sourceCodeLocation)
230                     clipboardString += " (" + frame.sourceCodeLocation.originalLocationString() + ")";
231             });
232         }
233
234         if (!isPrefixOptional || this._enforcesClipboardPrefixString())
235             return this._clipboardPrefixString() + clipboardString;
236         return clipboardString;
237     }
238
239     // Private
240
241     _appendMessageTextAndArguments(element)
242     {
243         if (this._message.source === WI.ConsoleMessage.MessageSource.ConsoleAPI) {
244             switch (this._message.type) {
245             case WI.ConsoleMessage.MessageType.Trace:
246                 var args = [WI.UIString("Trace")];
247                 if (this._message.parameters) {
248                     if (this._message.parameters[0].type === "string") {
249                         var prefixedFormatString = WI.UIString("Trace: %s").format(this._message.parameters[0].description);
250                         args = [prefixedFormatString].concat(this._message.parameters.slice(1));
251                     } else
252                         args = args.concat(this._message.parameters);
253                 }
254                 this._appendFormattedArguments(element, args);
255                 break;
256
257             case WI.ConsoleMessage.MessageType.Assert:
258                 var args = [WI.UIString("Assertion Failed")];
259                 if (this._message.parameters) {
260                     if (this._message.parameters[0].type === "string") {
261                         var prefixedFormatString = WI.UIString("Assertion Failed: %s").format(this._message.parameters[0].description);
262                         args = [prefixedFormatString].concat(this._message.parameters.slice(1));
263                     } else
264                         args = args.concat(this._message.parameters);
265                 }
266                 this._appendFormattedArguments(element, args);
267                 break;
268
269             case WI.ConsoleMessage.MessageType.Dir:
270                 var obj = this._message.parameters ? this._message.parameters[0] : undefined;
271                 this._appendFormattedArguments(element, ["%O", obj]);
272                 break;
273
274             case WI.ConsoleMessage.MessageType.Table:
275                 var args = this._message.parameters;
276                 element.appendChild(this._formatParameterAsTable(args));
277                 this._extraParameters = null;
278                 break;
279
280             case WI.ConsoleMessage.MessageType.StartGroup:
281             case WI.ConsoleMessage.MessageType.StartGroupCollapsed:
282                 var args = this._message.parameters || [this._message.messageText || WI.UIString("Group")];
283                 this._formatWithSubstitutionString(args, element);
284                 this._extraParameters = null;
285                 break;
286
287             default:
288                 var args = this._message.parameters || [this._message.messageText];
289                 this._appendFormattedArguments(element, args);
290                 break;
291             }
292             return;
293         }
294
295         // FIXME: Better handle WI.ConsoleMessage.MessageSource.Network once it has request info.
296
297         var args = this._message.parameters || [this._message.messageText];
298         this._appendFormattedArguments(element, args);
299     }
300
301     _appendSavedResultIndex(element)
302     {
303         if (!this._message.savedResultIndex)
304             return;
305
306         console.assert(this._message instanceof WI.ConsoleCommandResultMessage);
307         console.assert(this._message.type === WI.ConsoleMessage.MessageType.Result);
308
309         var savedVariableElement = document.createElement("span");
310         savedVariableElement.classList.add("console-saved-variable");
311         savedVariableElement.textContent = " = $" + this._message.savedResultIndex;
312
313         if (this._objectTree)
314             this._objectTree.appendTitleSuffix(savedVariableElement);
315         else
316             this._messageTextElement.appendChild(savedVariableElement);
317     }
318
319     _appendLocationLink()
320     {
321         if (this._message.source === WI.ConsoleMessage.MessageSource.Network) {
322             if (this._message.url) {
323                 var anchor = WI.linkifyURLAsNode(this._message.url, this._message.url, "console-message-url");
324                 anchor.classList.add("console-message-location");
325                 this._element.appendChild(anchor);
326             }
327             return;
328         }
329
330         var firstNonNativeNonAnonymousCallFrame = this._message.stackTrace.firstNonNativeNonAnonymousCallFrame;
331
332         var callFrame;
333         if (firstNonNativeNonAnonymousCallFrame) {
334             // JavaScript errors and console.* methods.
335             callFrame = firstNonNativeNonAnonymousCallFrame;
336         } else if (this._message.url && !this._shouldHideURL(this._message.url)) {
337             // CSS warnings have no stack traces.
338             callFrame = WI.CallFrame.fromPayload(this._message.target, {
339                 functionName: "",
340                 url: this._message.url,
341                 lineNumber: this._message.line,
342                 columnNumber: this._message.column
343             });
344         }
345
346         if (callFrame && (!callFrame.isConsoleEvaluation || WI.isDebugUIEnabled())) {
347             const showFunctionName = !!callFrame.functionName;
348             var locationElement = new WI.CallFrameView(callFrame, showFunctionName);
349             locationElement.classList.add("console-message-location");
350             this._element.appendChild(locationElement);
351             return;
352         }
353
354         if (this._message.parameters && this._message.parameters.length === 1) {
355             var parameter = this._createRemoteObjectIfNeeded(this._message.parameters[0]);
356
357             parameter.findFunctionSourceCodeLocation().then((result) => {
358                 if (result === WI.RemoteObject.SourceCodeLocationPromise.NoSourceFound || result === WI.RemoteObject.SourceCodeLocationPromise.MissingObjectId)
359                     return;
360
361                 var link = this._linkifyLocation(result.sourceCode.sourceURL || result.sourceCode.url, result.lineNumber, result.columnNumber);
362                 link.classList.add("console-message-location");
363
364                 if (this._element.hasChildNodes())
365                     this._element.insertBefore(link, this._element.firstChild);
366                 else
367                     this._element.appendChild(link);
368             });
369         }
370     }
371
372     _appendExtraParameters()
373     {
374         if (!this._extraParameters || !this._extraParameters.length)
375             return;
376
377         this._makeExpandable();
378
379         // Auto-expand if there are multiple objects or if there were simple parameters.
380         if (this._extraParameters.length > 1)
381             this.expand();
382
383         this._extraElementsList = this._element.appendChild(document.createElement("ol"));
384         this._extraElementsList.classList.add("console-message-extra-parameters-container");
385
386         for (var parameter of this._extraParameters) {
387             var listItemElement = this._extraElementsList.appendChild(document.createElement("li"));
388             const forceObjectFormat = parameter.type === "object" && (parameter.subtype !== "null" && parameter.subtype !== "regexp" && parameter.subtype !== "node" && parameter.subtype !== "error");
389             listItemElement.classList.add("console-message-extra-parameter");
390             listItemElement.appendChild(this._formatParameter(parameter, forceObjectFormat));
391         }
392     }
393
394     _appendStackTrace()
395     {
396         if (!this._shouldShowStackTrace())
397             return;
398
399         this._makeExpandable();
400
401         // Auto-expand for console.trace.
402         if (this._message.type === WI.ConsoleMessage.MessageType.Trace)
403             this.expand();
404
405         this._stackTraceElement = this._element.appendChild(document.createElement("div"));
406         this._stackTraceElement.classList.add("console-message-text", "console-message-stack-trace-container");
407
408         var callFramesElement = new WI.StackTraceView(this._message.stackTrace).element;
409         this._stackTraceElement.appendChild(callFramesElement);
410     }
411
412     _createRemoteObjectIfNeeded(parameter)
413     {
414         // FIXME: Only pass RemoteObjects here so we can avoid this work.
415         if (parameter instanceof WI.RemoteObject)
416             return parameter;
417
418         if (typeof parameter === "object")
419             return WI.RemoteObject.fromPayload(parameter, this._message.target);
420
421         return WI.RemoteObject.fromPrimitiveValue(parameter);
422     }
423
424     _appendFormattedArguments(element, parameters)
425     {
426         if (!parameters.length)
427             return;
428
429         for (let i = 0; i < parameters.length; ++i)
430             parameters[i] = this._createRemoteObjectIfNeeded(parameters[i]);
431
432         let builderElement = element.appendChild(document.createElement("span"));
433         let shouldFormatWithStringSubstitution = parameters[0].type === "string" && this._message.type !== WI.ConsoleMessage.MessageType.Result;
434
435         // Single object (e.g. console result or logging a non-string object).
436         if (parameters.length === 1 && !shouldFormatWithStringSubstitution) {
437             this._extraParameters = null;
438             builderElement.appendChild(this._formatParameter(parameters[0], false));
439             return;
440         }
441
442         console.assert(this._message.type !== WI.ConsoleMessage.MessageType.Result);
443
444         if (shouldFormatWithStringSubstitution && this._isStackTrace(parameters[0]))
445             shouldFormatWithStringSubstitution = false;
446
447         let needsDivider = false;
448         function appendDividerIfNeeded() {
449             if (!needsDivider)
450                 return null;
451             let element = builderElement.appendChild(document.createElement("span"));
452             element.classList.add("console-message-preview-divider");
453             element.textContent = ` ${enDash} `;
454             return element;
455         }
456
457         // Format string.
458         if (shouldFormatWithStringSubstitution) {
459             let result = this._formatWithSubstitutionString(parameters, builderElement);
460             parameters = result.unusedSubstitutions;
461             this._extraParameters = parameters;
462             needsDivider = true;
463         }
464
465         // Trailing inline parameters.
466         if (parameters.length) {
467             let simpleParametersCount = 0;
468             for (let parameter of parameters) {
469                 if (!this._hasSimpleDisplay(parameter))
470                     break;
471                 simpleParametersCount++;
472             }
473
474             // Show one or more simple parameters inline on the message line.
475             if (simpleParametersCount) {
476                 let simpleParameters = parameters.splice(0, simpleParametersCount);
477                 this._extraParameters = parameters;
478
479                 for (let parameter of simpleParameters) {
480                     let dividerElement = appendDividerIfNeeded();
481                     if (dividerElement)
482                         dividerElement.classList.add("inline-lossless");
483
484                     let previewContainer = builderElement.appendChild(document.createElement("span"));
485                     previewContainer.classList.add("inline-lossless");
486
487                     let preview = WI.FormattedValue.createObjectPreviewOrFormattedValueForRemoteObject(parameter, WI.ObjectPreviewView.Mode.Brief);
488                     let isPreviewView = preview instanceof WI.ObjectPreviewView;
489
490                     if (isPreviewView)
491                         preview.setOriginatingObjectInfo(parameter, null);
492
493                     let previewElement = isPreviewView ? preview.element : preview;
494                     previewContainer.appendChild(previewElement);
495
496                     needsDivider = true;
497
498                     // Simple displayable parameters should pretty much always be lossless.
499                     // An exception might be a truncated string.
500                     console.assert((isPreviewView && preview.lossless) || (!isPreviewView && this._shouldConsiderObjectLossless(parameter)));
501                 }
502             }
503
504             // If there is a single non-simple parameter after simple paramters, show it inline.
505             if (parameters.length === 1 && !this._isStackTrace(parameters[0])) {
506                 let parameter = parameters[0];
507
508                 let dividerElement = appendDividerIfNeeded();
509
510                 let previewContainer = builderElement.appendChild(document.createElement("span"));
511                 previewContainer.classList.add("console-message-preview");
512
513                 let preview = WI.FormattedValue.createObjectPreviewOrFormattedValueForRemoteObject(parameter, WI.ObjectPreviewView.Mode.Brief);
514                 let isPreviewView = preview instanceof WI.ObjectPreviewView;
515
516                 if (isPreviewView)
517                     preview.setOriginatingObjectInfo(parameter, null);
518
519                 let previewElement = isPreviewView ? preview.element : preview;
520                 previewContainer.appendChild(previewElement);
521
522                 needsDivider = true;
523
524                 // If this preview is effectively lossless, we can avoid making this console message expandable.
525                 if ((isPreviewView && preview.lossless) || (!isPreviewView && this._shouldConsiderObjectLossless(parameter))) {
526                     this._extraParameters = null;
527                     if (dividerElement)
528                         dividerElement.classList.add("inline-lossless");
529                     previewContainer.classList.add("inline-lossless");
530                 }
531             } else if (parameters.length) {
532                 // Multiple remaining objects. Show an indicator and they will be appended as extra parameters.
533                 let enclosedElement = document.createElement("span");
534                 builderElement.append(" ", enclosedElement);
535                 enclosedElement.classList.add("console-message-enclosed");
536                 enclosedElement.textContent = "(" + parameters.length + ")";
537             }
538         }
539     }
540
541     _hasSimpleDisplay(parameter)
542     {
543         console.assert(parameter instanceof WI.RemoteObject);
544
545         return WI.FormattedValue.hasSimpleDisplay(parameter) && !this._isStackTrace(parameter);
546     }
547
548     _isStackTrace(parameter)
549     {
550         console.assert(parameter instanceof WI.RemoteObject);
551
552         return parameter.type === "string" && WI.StackTrace.isLikelyStackTrace(parameter.description);
553     }
554
555     _shouldConsiderObjectLossless(object)
556     {
557         if (object.type === "string") {
558             const description = object.description;
559             const maxLength = WI.FormattedValue.MAX_PREVIEW_STRING_LENGTH;
560             const longOrMultiLineString = description.length > maxLength || description.slice(0, maxLength).includes("\n");
561             return !longOrMultiLineString;
562         }
563
564         return object.type !== "object" || object.subtype === "null" || object.subtype === "regexp";
565     }
566
567     _formatParameter(parameter, forceObjectFormat)
568     {
569         var type;
570         if (forceObjectFormat)
571             type = "object";
572         else if (parameter instanceof WI.RemoteObject)
573             type = parameter.subtype || parameter.type;
574         else {
575             console.assert(false, "no longer reachable");
576             type = typeof parameter;
577         }
578
579         var formatters = {
580             "object": this._formatParameterAsObject,
581             "error": this._formatParameterAsError,
582             "map": this._formatParameterAsObject,
583             "set": this._formatParameterAsObject,
584             "weakmap": this._formatParameterAsObject,
585             "weakset": this._formatParameterAsObject,
586             "iterator": this._formatParameterAsObject,
587             "class": this._formatParameterAsObject,
588             "proxy": this._formatParameterAsObject,
589             "array": this._formatParameterAsArray,
590             "node": this._formatParameterAsNode,
591             "string": this._formatParameterAsString,
592         };
593
594         var formatter = formatters[type] || this._formatParameterAsValue;
595
596         const fragment = document.createDocumentFragment();
597         formatter.call(this, parameter, fragment, forceObjectFormat);
598         return fragment;
599     }
600
601     _formatParameterAsValue(value, fragment)
602     {
603         fragment.appendChild(WI.FormattedValue.createElementForRemoteObject(value));
604     }
605
606     _formatParameterAsString(object, fragment)
607     {
608         if (this._isStackTrace(object)) {
609             let stackTrace = WI.StackTrace.fromString(this._message.target, object.description);
610             if (stackTrace.callFrames.length) {
611                 let stackView = new WI.StackTraceView(stackTrace);
612                 fragment.appendChild(stackView.element);
613                 return;
614             }
615         }
616
617         fragment.appendChild(WI.FormattedValue.createLinkifiedElementString(object.description));
618     }
619
620     _formatParameterAsNode(object, fragment)
621     {
622         fragment.appendChild(WI.FormattedValue.createElementForNode(object));
623     }
624
625     _formatParameterAsObject(object, fragment, forceExpansion)
626     {
627         // FIXME: Should have a better ObjectTreeView mode for classes (static methods and methods).
628         this._objectTree = new WI.ObjectTreeView(object, null, this._rootPropertyPathForObject(object), forceExpansion);
629         fragment.appendChild(this._objectTree.element);
630     }
631
632     _formatParameterAsError(object, fragment)
633     {
634         this._objectTree = new WI.ErrorObjectView(object);
635         fragment.appendChild(this._objectTree.element);
636     }
637
638     _formatParameterAsArray(array, fragment)
639     {
640         this._objectTree = new WI.ObjectTreeView(array, WI.ObjectTreeView.Mode.Properties, this._rootPropertyPathForObject(array));
641         fragment.appendChild(this._objectTree.element);
642     }
643
644     _rootPropertyPathForObject(object)
645     {
646         if (!this._message.savedResultIndex)
647             return null;
648
649         return new WI.PropertyPath(object, "$" + this._message.savedResultIndex);
650     }
651
652     _formatWithSubstitutionString(parameters, formattedResult)
653     {
654         function parameterFormatter(force, obj)
655         {
656             return this._formatParameter(obj, force);
657         }
658
659         function stringFormatter(obj)
660         {
661             return obj.description;
662         }
663
664         function floatFormatter(obj, token)
665         {
666             let value = typeof obj.value === "number" ? obj.value : obj.description;
667             return String.standardFormatters.f(value, token);
668         }
669
670         function integerFormatter(obj)
671         {
672             let value = typeof obj.value === "number" ? obj.value : obj.description;
673             return String.standardFormatters.d(value);
674         }
675
676         var currentStyle = null;
677         function styleFormatter(obj)
678         {
679             currentStyle = {};
680             var buffer = document.createElement("span");
681             buffer.setAttribute("style", obj.description);
682             for (var i = 0; i < buffer.style.length; i++) {
683                 var property = buffer.style[i];
684                 if (isWhitelistedProperty(property))
685                     currentStyle[property] = buffer.style[property];
686             }
687         }
688
689         function isWhitelistedProperty(property)
690         {
691             for (var prefix of ["background", "border", "color", "font", "line", "margin", "padding", "text"]) {
692                 if (property.startsWith(prefix) || property.startsWith("-webkit-" + prefix))
693                     return true;
694             }
695             return false;
696         }
697
698         // Firebug uses %o for formatting objects.
699         var formatters = {};
700         formatters.o = parameterFormatter.bind(this, false);
701         formatters.s = stringFormatter;
702         formatters.f = floatFormatter;
703
704         // Firebug allows both %i and %d for formatting integers.
705         formatters.i = integerFormatter;
706         formatters.d = integerFormatter;
707
708         // Firebug uses %c for styling the message.
709         formatters.c = styleFormatter;
710
711         // Support %O to force object formatting, instead of the type-based %o formatting.
712         formatters.O = parameterFormatter.bind(this, true);
713
714         function append(a, b)
715         {
716             if (b instanceof Node)
717                 a.appendChild(b);
718             else if (b !== undefined) {
719                 var toAppend = WI.linkifyStringAsFragment(b.toString());
720                 if (currentStyle) {
721                     var wrapper = document.createElement("span");
722                     for (var key in currentStyle)
723                         wrapper.style[key] = currentStyle[key];
724                     wrapper.appendChild(toAppend);
725                     toAppend = wrapper;
726                 }
727
728                 a.appendChild(toAppend);
729             }
730             return a;
731         }
732
733         // String.format does treat formattedResult like a Builder, result is an object.
734         return String.format(parameters[0].description, parameters.slice(1), formatters, formattedResult, append);
735     }
736
737     _shouldShowStackTrace()
738     {
739         if (!this._message.stackTrace.callFrames.length)
740             return false;
741
742         return this._message.source === WI.ConsoleMessage.MessageSource.Network
743             || this._message.level === WI.ConsoleMessage.MessageLevel.Error
744             || this._message.type === WI.ConsoleMessage.MessageType.Trace;
745     }
746
747     _shouldHideURL(url)
748     {
749         return url === "undefined" || url === "[native code]";
750     }
751
752     _linkifyLocation(url, lineNumber, columnNumber)
753     {
754         const options = {
755             className: "console-message-url",
756             ignoreNetworkTab: true,
757             ignoreSearchTab: true,
758         };
759         return WI.linkifyLocation(url, new WI.SourceCodePosition(lineNumber, columnNumber), options);
760     }
761
762     _userProvidedColumnNames(columnNamesArgument)
763     {
764         if (!columnNamesArgument)
765             return null;
766
767         console.assert(columnNamesArgument instanceof WI.RemoteObject);
768
769         // Single primitive argument.
770         if (columnNamesArgument.type === "string" || columnNamesArgument.type === "number")
771             return [String(columnNamesArgument.value)];
772
773         // Ignore everything that is not an array with property previews.
774         if (columnNamesArgument.type !== "object" || columnNamesArgument.subtype !== "array" || !columnNamesArgument.preview || !columnNamesArgument.preview.propertyPreviews)
775             return null;
776
777         // Array. Look into the preview and get string values.
778         var extractedColumnNames = [];
779         for (var propertyPreview of columnNamesArgument.preview.propertyPreviews) {
780             if (propertyPreview.type === "string" || propertyPreview.type === "number")
781                 extractedColumnNames.push(String(propertyPreview.value));
782         }
783
784         return extractedColumnNames.length ? extractedColumnNames : null;
785     }
786
787     _formatParameterAsTable(parameters)
788     {
789         var element = document.createElement("span");
790         var table = parameters[0];
791         if (!table || !table.preview)
792             return element;
793
794         var rows = [];
795         var columnNames = [];
796         var flatValues = [];
797         var preview = table.preview;
798         var userProvidedColumnNames = false;
799
800         // User provided columnNames.
801         var extractedColumnNames = this._userProvidedColumnNames(parameters[1]);
802         if (extractedColumnNames) {
803             userProvidedColumnNames = true;
804             columnNames = extractedColumnNames;
805         }
806
807         // Check first for valuePreviews in the properties meaning this was an array of objects.
808         if (preview.propertyPreviews) {
809             for (var i = 0; i < preview.propertyPreviews.length; ++i) {
810                 var rowProperty = preview.propertyPreviews[i];
811                 var rowPreview = rowProperty.valuePreview;
812                 if (!rowPreview || !rowPreview.propertyPreviews)
813                     continue;
814
815                 var rowValue = {};
816                 var maxColumnsToRender = 15;
817                 for (var j = 0; j < rowPreview.propertyPreviews.length; ++j) {
818                     var cellProperty = rowPreview.propertyPreviews[j];
819                     var columnRendered = columnNames.includes(cellProperty.name);
820                     if (!columnRendered) {
821                         if (userProvidedColumnNames || columnNames.length === maxColumnsToRender)
822                             continue;
823                         columnRendered = true;
824                         columnNames.push(cellProperty.name);
825                     }
826
827                     rowValue[cellProperty.name] = WI.FormattedValue.createElementForPropertyPreview(cellProperty);
828                 }
829                 rows.push([rowProperty.name, rowValue]);
830             }
831         }
832
833         // If there were valuePreviews, convert to a flat list.
834         if (rows.length) {
835             columnNames.unshift(WI.UIString("(Index)"));
836             for (var i = 0; i < rows.length; ++i) {
837                 var rowName = rows[i][0];
838                 var rowValue = rows[i][1];
839                 flatValues.push(rowName);
840                 for (var j = 1; j < columnNames.length; ++j) {
841                     var columnName = columnNames[j];
842                     if (!(columnName in rowValue))
843                         flatValues.push(emDash);
844                     else
845                         flatValues.push(rowValue[columnName]);
846                 }
847             }
848         }
849
850         // If there were no value Previews, then check for an array of values.
851         if (!flatValues.length && preview.propertyPreviews) {
852             for (var i = 0; i < preview.propertyPreviews.length; ++i) {
853                 var rowProperty = preview.propertyPreviews[i];
854                 if (!("value" in rowProperty))
855                     continue;
856
857                 if (!columnNames.length) {
858                     columnNames.push(WI.UIString("Index"));
859                     columnNames.push(WI.UIString("Value"));
860                 }
861
862                 flatValues.push(rowProperty.name);
863                 flatValues.push(WI.FormattedValue.createElementForPropertyPreview(rowProperty));
864             }
865         }
866
867         // If no table data show nothing.
868         if (!flatValues.length)
869             return element;
870
871         // FIXME: Should we output something extra if the preview is lossless?
872
873         var dataGrid = WI.DataGrid.createSortableDataGrid(columnNames, flatValues);
874         dataGrid.inline = true;
875         dataGrid.variableHeightRows = true;
876
877         element.appendChild(dataGrid.element);
878
879         dataGrid.updateLayoutIfNeeded();
880
881         return element;
882     }
883
884     _levelString()
885     {
886         switch (this._message.level) {
887         case WI.ConsoleMessage.MessageLevel.Log:
888             return "Log";
889         case WI.ConsoleMessage.MessageLevel.Info:
890             return "Info";
891         case WI.ConsoleMessage.MessageLevel.Warning:
892             return "Warning";
893         case WI.ConsoleMessage.MessageLevel.Debug:
894             return "Debug";
895         case WI.ConsoleMessage.MessageLevel.Error:
896             return "Error";
897         }
898     }
899
900     _enforcesClipboardPrefixString()
901     {
902         return this._message.type !== WI.ConsoleMessage.MessageType.Result;
903     }
904
905     _clipboardPrefixString()
906     {
907         if (this._message.type === WI.ConsoleMessage.MessageType.Result)
908             return "< ";
909
910         return "[" + this._levelString() + "] ";
911     }
912
913     _makeExpandable()
914     {
915         if (this._expandable)
916             return;
917
918         this._expandable = true;
919
920         this._element.classList.add("expandable");
921
922         this._boundClickHandler = this.toggle.bind(this);
923         this._messageTextElement.addEventListener("click", this._boundClickHandler);
924     }
925 };