24a32f327834bd5795208f686aeda41873051001
[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 WebInspector.ConsoleMessageView = class ConsoleMessageView extends WebInspector.Object
32 {
33     constructor(message)
34     {
35         super();
36
37         console.assert(message instanceof WebInspector.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 === WebInspector.ConsoleMessage.MessageType.Result) {
60             this._element.classList.add("console-user-command-result");
61             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Output: "));
62         } else if (this._message.type === WebInspector.ConsoleMessage.MessageType.StartGroup || this._message.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed)
63             this._element.classList.add("console-group-title");
64
65         switch (this._message.level) {
66         case WebInspector.ConsoleMessage.MessageLevel.Log:
67             this._element.classList.add("console-log-level");
68             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Log: "));
69             break;
70         case WebInspector.ConsoleMessage.MessageLevel.Info:
71             this._element.classList.add("console-info-level");
72             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Info: "));
73             break;
74         case WebInspector.ConsoleMessage.MessageLevel.Debug:
75             this._element.classList.add("console-debug-level");
76             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Debug: "));
77             break;
78         case WebInspector.ConsoleMessage.MessageLevel.Warning:
79             this._element.classList.add("console-warning-level");
80             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Warning: "));
81             break;
82         case WebInspector.ConsoleMessage.MessageLevel.Error:
83             this._element.classList.add("console-error-level");
84             this._element.setAttribute("data-labelprefix", WebInspector.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 !== WebInspector.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 = [WebInspector.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 || WebInspector.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 === WebInspector.ConsoleMessage.MessageSource.ConsoleAPI) {
244             switch (this._message.type) {
245             case WebInspector.ConsoleMessage.MessageType.Trace:
246                 var args = [WebInspector.UIString("Trace")];
247                 if (this._message.parameters) {
248                     if (this._message.parameters[0].type === "string") {
249                         var prefixedFormatString = WebInspector.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 WebInspector.ConsoleMessage.MessageType.Assert:
258                 var args = [WebInspector.UIString("Assertion Failed")];
259                 if (this._message.parameters) {
260                     if (this._message.parameters[0].type === "string") {
261                         var prefixedFormatString = WebInspector.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 WebInspector.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 WebInspector.ConsoleMessage.MessageType.Table:
275                 var args = this._message.parameters;
276                 element.appendChild(this._formatParameterAsTable(args));
277                 this._extraParameters = null;
278                 break;
279
280             case WebInspector.ConsoleMessage.MessageType.StartGroup:
281             case WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed:
282                 var args = this._message.parameters || [this._message.messageText || WebInspector.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 WebInspector.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 WebInspector.ConsoleCommandResultMessage);
307         console.assert(this._message.type === WebInspector.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 === WebInspector.ConsoleMessage.MessageSource.Network) {
322             if (this._message.url) {
323                 var anchor = WebInspector.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 = WebInspector.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 || WebInspector.isDebugUIEnabled())) {
347             const showFunctionName = !!callFrame.functionName;
348             var locationElement = new WebInspector.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(function(result) {
358                 if (result === WebInspector.RemoteObject.SourceCodeLocationPromise.NoSourceFound || result === WebInspector.RemoteObject.SourceCodeLocationPromise.MissingObjectId)
359                     return;
360
361                 var link = this._linkifyLocation(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             }.bind(this));
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.
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 === WebInspector.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 WebInspector.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 WebInspector.RemoteObject)
416             return parameter;
417
418         if (typeof parameter === "object")
419             return WebInspector.RemoteObject.fromPayload(parameter, this._message.target);
420
421         return WebInspector.RemoteObject.fromPrimitiveValue(parameter);
422     }
423
424     _appendFormattedArguments(element, parameters)
425     {
426         if (!parameters.length)
427             return;
428
429         for (var i = 0; i < parameters.length; ++i)
430             parameters[i] = this._createRemoteObjectIfNeeded(parameters[i]);
431
432         var builderElement = element.appendChild(document.createElement("span"));
433         var shouldFormatWithStringSubstitution = WebInspector.RemoteObject.type(parameters[0]) === "string" && this._message.type !== WebInspector.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 !== WebInspector.ConsoleMessage.MessageType.Result);
443
444         if (shouldFormatWithStringSubstitution && this._isStackTrace(parameters[0]))
445             shouldFormatWithStringSubstitution = false;
446
447         // Format string / message / default message.
448         if (shouldFormatWithStringSubstitution) {
449             var result = this._formatWithSubstitutionString(parameters, builderElement);
450             parameters = result.unusedSubstitutions;
451             this._extraParameters = parameters;
452         } else {
453             var defaultMessage = WebInspector.UIString("No message");
454             builderElement.append(defaultMessage);
455         }
456
457         // Trailing parameters.
458         if (parameters.length) {
459             let enclosedElement = document.createElement("span");
460
461             if (parameters.length === 1 && !this._isStackTrace(parameters[0])) {
462                 let parameter = parameters[0];
463
464                 // Single object. Show a preview.
465                 builderElement.append(enclosedElement);
466                 enclosedElement.classList.add("console-message-preview-divider");
467                 enclosedElement.textContent = " \u2013 ";
468
469                 var previewContainer = builderElement.appendChild(document.createElement("span"));
470                 previewContainer.classList.add("console-message-preview");
471
472                 var preview = WebInspector.FormattedValue.createObjectPreviewOrFormattedValueForRemoteObject(parameter, WebInspector.ObjectPreviewView.Mode.Brief);
473                 var isPreviewView = preview instanceof WebInspector.ObjectPreviewView;
474
475                 if (isPreviewView)
476                     preview.setOriginatingObjectInfo(parameter, null);
477
478                 var previewElement = isPreviewView ? preview.element : preview;
479                 previewContainer.appendChild(previewElement);
480
481                 // If this preview is effectively lossless, we can avoid making this console message expandable.
482                 if ((isPreviewView && preview.lossless) || (!isPreviewView && this._shouldConsiderObjectLossless(parameter))) {
483                     this._extraParameters = null;
484                     enclosedElement.classList.add("inline-lossless");
485                     previewContainer.classList.add("inline-lossless");
486                 }
487             } else {
488                 // Multiple objects. Show an indicator.
489                 builderElement.append(" ", enclosedElement);
490                 enclosedElement.classList.add("console-message-enclosed");
491                 enclosedElement.textContent = "(" + parameters.length + ")";
492             }
493         }
494     }
495
496     _isStackTrace(parameter)
497     {
498         console.assert(parameter instanceof WebInspector.RemoteObject);
499
500         if (WebInspector.RemoteObject.type(parameter) !== "string")
501             return false;
502
503         return WebInspector.StackTrace.isLikelyStackTrace(parameter.description);
504     }
505
506     _shouldConsiderObjectLossless(object)
507     {
508         if (object.type === "string") {
509             const description = object.description;
510             const maxLength = WebInspector.FormattedValue.MAX_PREVIEW_STRING_LENGTH;
511             const longOrMultiLineString = description.length > maxLength || description.slice(0, maxLength).includes("\n");
512             return !longOrMultiLineString;
513         }
514
515         return object.type !== "object" || object.subtype === "null" || object.subtype === "regexp";
516     }
517
518     _formatParameter(parameter, forceObjectFormat)
519     {
520         var type;
521         if (forceObjectFormat)
522             type = "object";
523         else if (parameter instanceof WebInspector.RemoteObject)
524             type = parameter.subtype || parameter.type;
525         else {
526             console.assert(false, "no longer reachable");
527             type = typeof parameter;
528         }
529
530         var formatters = {
531             "object": this._formatParameterAsObject,
532             "error": this._formatParameterAsError,
533             "map": this._formatParameterAsObject,
534             "set": this._formatParameterAsObject,
535             "weakmap": this._formatParameterAsObject,
536             "weakset": this._formatParameterAsObject,
537             "iterator": this._formatParameterAsObject,
538             "class": this._formatParameterAsObject,
539             "proxy": this._formatParameterAsObject,
540             "array": this._formatParameterAsArray,
541             "node": this._formatParameterAsNode,
542             "string": this._formatParameterAsString,
543         };
544
545         var formatter = formatters[type] || this._formatParameterAsValue;
546
547         const fragment = document.createDocumentFragment();
548         formatter.call(this, parameter, fragment, forceObjectFormat);
549         return fragment;
550     }
551
552     _formatParameterAsValue(value, fragment)
553     {
554         fragment.appendChild(WebInspector.FormattedValue.createElementForRemoteObject(value));
555     }
556
557     _formatParameterAsString(object, fragment)
558     {
559         if (this._isStackTrace(object)) {
560             let stackTrace = WebInspector.StackTrace.fromString(this._message.target, object.description);
561             if (stackTrace.callFrames.length) {
562                 let stackView = new WebInspector.StackTraceView(stackTrace);
563                 fragment.appendChild(stackView.element);
564                 return;
565             }
566         }
567
568         fragment.appendChild(WebInspector.FormattedValue.createLinkifiedElementString(object.description));
569     }
570
571     _formatParameterAsNode(object, fragment)
572     {
573         fragment.appendChild(WebInspector.FormattedValue.createElementForNode(object));
574     }
575
576     _formatParameterAsObject(object, fragment, forceExpansion)
577     {
578         // FIXME: Should have a better ObjectTreeView mode for classes (static methods and methods).
579         this._objectTree = new WebInspector.ObjectTreeView(object, null, this._rootPropertyPathForObject(object), forceExpansion);
580         fragment.appendChild(this._objectTree.element);
581     }
582
583     _formatParameterAsError(object, fragment)
584     {
585         this._objectTree = new WebInspector.ErrorObjectView(object);
586         fragment.appendChild(this._objectTree.element);
587     }
588
589     _formatParameterAsArray(array, fragment)
590     {
591         this._objectTree = new WebInspector.ObjectTreeView(array, WebInspector.ObjectTreeView.Mode.Properties, this._rootPropertyPathForObject(array));
592         fragment.appendChild(this._objectTree.element);
593     }
594
595     _rootPropertyPathForObject(object)
596     {
597         if (!this._message.savedResultIndex)
598             return null;
599
600         return new WebInspector.PropertyPath(object, "$" + this._message.savedResultIndex);
601     }
602
603     _formatWithSubstitutionString(parameters, formattedResult)
604     {
605         function parameterFormatter(force, obj)
606         {
607             return this._formatParameter(obj, force);
608         }
609
610         function stringFormatter(obj)
611         {
612             return obj.description;
613         }
614
615         function floatFormatter(obj, token)
616         {
617             let value = typeof obj.value === "number" ? obj.value : obj.description;
618             return String.standardFormatters.f(value, token);
619         }
620
621         function integerFormatter(obj)
622         {
623             let value = typeof obj.value === "number" ? obj.value : obj.description;
624             return String.standardFormatters.d(value);
625         }
626
627         var currentStyle = null;
628         function styleFormatter(obj)
629         {
630             currentStyle = {};
631             var buffer = document.createElement("span");
632             buffer.setAttribute("style", obj.description);
633             for (var i = 0; i < buffer.style.length; i++) {
634                 var property = buffer.style[i];
635                 if (isWhitelistedProperty(property))
636                     currentStyle[property] = buffer.style[property];
637             }
638         }
639
640         function isWhitelistedProperty(property)
641         {
642             for (var prefix of ["background", "border", "color", "font", "line", "margin", "padding", "text"]) {
643                 if (property.startsWith(prefix) || property.startsWith("-webkit-" + prefix))
644                     return true;
645             }
646             return false;
647         }
648
649         // Firebug uses %o for formatting objects.
650         var formatters = {};
651         formatters.o = parameterFormatter.bind(this, false);
652         formatters.s = stringFormatter;
653         formatters.f = floatFormatter;
654
655         // Firebug allows both %i and %d for formatting integers.
656         formatters.i = integerFormatter;
657         formatters.d = integerFormatter;
658
659         // Firebug uses %c for styling the message.
660         formatters.c = styleFormatter;
661
662         // Support %O to force object formatting, instead of the type-based %o formatting.
663         formatters.O = parameterFormatter.bind(this, true);
664
665         function append(a, b)
666         {
667             if (b instanceof Node)
668                 a.appendChild(b);
669             else if (b !== undefined) {
670                 var toAppend = WebInspector.linkifyStringAsFragment(b.toString());
671                 if (currentStyle) {
672                     var wrapper = document.createElement("span");
673                     for (var key in currentStyle)
674                         wrapper.style[key] = currentStyle[key];
675                     wrapper.appendChild(toAppend);
676                     toAppend = wrapper;
677                 }
678
679                 a.appendChild(toAppend);
680             }
681             return a;
682         }
683
684         // String.format does treat formattedResult like a Builder, result is an object.
685         return String.format(parameters[0].description, parameters.slice(1), formatters, formattedResult, append);
686     }
687
688     _shouldShowStackTrace()
689     {
690         if (!this._message.stackTrace.callFrames.length)
691             return false;
692
693         return this._message.source === WebInspector.ConsoleMessage.MessageSource.Network
694             || this._message.level === WebInspector.ConsoleMessage.MessageLevel.Error
695             || this._message.type === WebInspector.ConsoleMessage.MessageType.Trace;
696     }
697
698     _shouldHideURL(url)
699     {
700         return url === "undefined" || url === "[native code]";
701     }
702
703     _linkifyLocation(url, lineNumber, columnNumber)
704     {
705         const options = {
706             className: "console-message-url",
707             ignoreNetworkTab: true,
708             ignoreSearchTab: true,
709         };
710         return WebInspector.linkifyLocation(url, new WebInspector.SourceCodePosition(lineNumber, columnNumber), options);
711     }
712
713     _userProvidedColumnNames(columnNamesArgument)
714     {
715         if (!columnNamesArgument)
716             return null;
717
718         console.assert(columnNamesArgument instanceof WebInspector.RemoteObject);
719
720         // Single primitive argument.
721         if (columnNamesArgument.type === "string" || columnNamesArgument.type === "number")
722             return [String(columnNamesArgument.value)];
723
724         // Ignore everything that is not an array with property previews.
725         if (columnNamesArgument.type !== "object" || columnNamesArgument.subtype !== "array" || !columnNamesArgument.preview || !columnNamesArgument.preview.propertyPreviews)
726             return null;
727
728         // Array. Look into the preview and get string values.
729         var extractedColumnNames = [];
730         for (var propertyPreview of columnNamesArgument.preview.propertyPreviews) {
731             if (propertyPreview.type === "string" || propertyPreview.type === "number")
732                 extractedColumnNames.push(String(propertyPreview.value));
733         }
734
735         return extractedColumnNames.length ? extractedColumnNames : null;
736     }
737
738     _formatParameterAsTable(parameters)
739     {
740         var element = document.createElement("span");
741         var table = parameters[0];
742         if (!table || !table.preview)
743             return element;
744
745         var rows = [];
746         var columnNames = [];
747         var flatValues = [];
748         var preview = table.preview;
749         var userProvidedColumnNames = false;
750
751         // User provided columnNames.
752         var extractedColumnNames = this._userProvidedColumnNames(parameters[1]);
753         if (extractedColumnNames) {
754             userProvidedColumnNames = true;
755             columnNames = extractedColumnNames;
756         }
757
758         // Check first for valuePreviews in the properties meaning this was an array of objects.
759         if (preview.propertyPreviews) {
760             for (var i = 0; i < preview.propertyPreviews.length; ++i) {
761                 var rowProperty = preview.propertyPreviews[i];
762                 var rowPreview = rowProperty.valuePreview;
763                 if (!rowPreview || !rowPreview.propertyPreviews)
764                     continue;
765
766                 var rowValue = {};
767                 var maxColumnsToRender = 15;
768                 for (var j = 0; j < rowPreview.propertyPreviews.length; ++j) {
769                     var cellProperty = rowPreview.propertyPreviews[j];
770                     var columnRendered = columnNames.includes(cellProperty.name);
771                     if (!columnRendered) {
772                         if (userProvidedColumnNames || columnNames.length === maxColumnsToRender)
773                             continue;
774                         columnRendered = true;
775                         columnNames.push(cellProperty.name);
776                     }
777
778                     rowValue[cellProperty.name] = WebInspector.FormattedValue.createElementForPropertyPreview(cellProperty);
779                 }
780                 rows.push([rowProperty.name, rowValue]);
781             }
782         }
783
784         // If there were valuePreviews, convert to a flat list.
785         if (rows.length) {
786             columnNames.unshift(WebInspector.UIString("(Index)"));
787             for (var i = 0; i < rows.length; ++i) {
788                 var rowName = rows[i][0];
789                 var rowValue = rows[i][1];
790                 flatValues.push(rowName);
791                 for (var j = 1; j < columnNames.length; ++j) {
792                     var columnName = columnNames[j];
793                     if (!(columnName in rowValue))
794                         flatValues.push(emDash);
795                     else
796                         flatValues.push(rowValue[columnName]);
797                 }
798             }
799         }
800
801         // If there were no value Previews, then check for an array of values.
802         if (!flatValues.length && preview.propertyPreviews) {
803             for (var i = 0; i < preview.propertyPreviews.length; ++i) {
804                 var rowProperty = preview.propertyPreviews[i];
805                 if (!("value" in rowProperty))
806                     continue;
807
808                 if (!columnNames.length) {
809                     columnNames.push(WebInspector.UIString("Index"));
810                     columnNames.push(WebInspector.UIString("Value"));
811                 }
812
813                 flatValues.push(rowProperty.name);
814                 flatValues.push(WebInspector.FormattedValue.createElementForPropertyPreview(rowProperty));
815             }
816         }
817
818         // If no table data show nothing.
819         if (!flatValues.length)
820             return element;
821
822         // FIXME: Should we output something extra if the preview is lossless?
823
824         var dataGrid = WebInspector.DataGrid.createSortableDataGrid(columnNames, flatValues);
825         dataGrid.inline = true;
826         dataGrid.variableHeightRows = true;
827
828         element.appendChild(dataGrid.element);
829
830         dataGrid.updateLayoutIfNeeded();
831
832         return element;
833     }
834
835     _levelString()
836     {
837         switch (this._message.level) {
838         case WebInspector.ConsoleMessage.MessageLevel.Log:
839             return "Log";
840         case WebInspector.ConsoleMessage.MessageLevel.Info:
841             return "Info";
842         case WebInspector.ConsoleMessage.MessageLevel.Warning:
843             return "Warning";
844         case WebInspector.ConsoleMessage.MessageLevel.Debug:
845             return "Debug";
846         case WebInspector.ConsoleMessage.MessageLevel.Error:
847             return "Error";
848         }
849     }
850
851     _enforcesClipboardPrefixString()
852     {
853         return this._message.type !== WebInspector.ConsoleMessage.MessageType.Result;
854     }
855
856     _clipboardPrefixString()
857     {
858         if (this._message.type === WebInspector.ConsoleMessage.MessageType.Result)
859             return "< ";
860
861         return "[" + this._levelString() + "] ";
862     }
863
864     _makeExpandable()
865     {
866         if (this._expandable)
867             return;
868
869         this._expandable = true;
870
871         this._element.classList.add("expandable");
872
873         this._boundClickHandler = this.toggle.bind(this);
874         this._messageTextElement.addEventListener("click", this._boundClickHandler);
875     }
876 };