Web Inspector: console should show an icon for console.info() messages
[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
41         this._element = document.createElement("div");
42         this._element.classList.add("console-message");
43
44         // FIXME: <https://webkit.org/b/143545> Web Inspector: LogContentView should use higher level objects
45         this._element.__message = this._message;
46         this._element.__messageView = this;
47
48         switch (this._message.level) {
49         case WebInspector.ConsoleMessage.MessageLevel.Log:
50             this._element.classList.add("console-log-level");
51             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Log: "));
52             break;
53         case WebInspector.ConsoleMessage.MessageLevel.Info:
54             this._element.classList.add("console-info-level");
55             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Info: "));
56             break;
57         case WebInspector.ConsoleMessage.MessageLevel.Debug:
58             this._element.classList.add("console-debug-level");
59             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Debug: "));
60             break;
61         case WebInspector.ConsoleMessage.MessageLevel.Warning:
62             this._element.classList.add("console-warning-level");
63             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Warning: "));
64             break;
65         case WebInspector.ConsoleMessage.MessageLevel.Error:
66             this._element.classList.add("console-error-level");
67             this._element.setAttribute("data-labelprefix", WebInspector.UIString("Error: "));
68             break;
69         }
70
71         if (this._message.type === WebInspector.ConsoleMessage.MessageType.Result) {
72             this._element.classList.add("console-user-command-result");
73             if (!this._element.getAttribute("data-labelprefix"))
74                 this._element.setAttribute("data-labelprefix", WebInspector.UIString("Output: "));
75         }
76
77         if (this._message.type === WebInspector.ConsoleMessage.MessageType.StartGroup || this._message.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed)
78             this._element.classList.add("console-group-title");
79
80         // These are the parameters unused by the messages's optional format string.
81         // Any extra parameters will be displayed as children of this message.
82         this._extraParameters = this._message.parameters;
83
84         // FIXME: The location link should include stack trace information.
85         this._appendLocationLink();
86
87         this._messageTextElement = this._element.appendChild(document.createElement("span"));
88         this._messageTextElement.classList.add("console-top-level-message");
89         this._messageTextElement.classList.add("console-message-text");
90         this._appendMessageTextAndArguments(this._messageTextElement);
91         this._appendSavedResultIndex();
92
93         this._appendExtraParameters();
94         this._appendStackTrace();
95
96         this.repeatCount = this._message.repeatCount;
97     }
98
99     // Public
100
101     get element()
102     {
103         return this._element;
104     }
105
106     get message()
107     {
108         return this._message;
109     }
110
111     get repeatCount()
112     {
113         return this._repeatCount;
114     }
115
116     set repeatCount(count)
117     {
118         console.assert(typeof count === "number");
119
120         if (this._repeatCount === count)
121             return;
122
123         this._repeatCount = count;
124
125         if (count <= 1) {
126             if (this._repeatCountElement) {
127                 this._repeatCountElement.remove();
128                 this._repeatCountElement = null;
129             }
130             return;
131         }
132
133         if (!this._repeatCountElement) {
134             this._repeatCountElement = document.createElement("span");
135             this._repeatCountElement.classList.add("repeat-count");
136             this._element.insertBefore(this._repeatCountElement, this._element.firstChild);
137         }
138
139         this._repeatCountElement.textContent = count;
140     }
141
142     get expandable()
143     {
144         return this._element.classList.contains("expandable");
145     }
146
147     set expandable(x)
148     {
149         if (x === this.expandable)
150             return;
151
152         if (!this._boundClickHandler)
153             this._boundClickHandler = this.toggle.bind(this);
154
155         var becameExpandable = this._element.classList.toggle("expandable", x);
156
157         if (becameExpandable)
158             this._messageTextElement.addEventListener("click", this._boundClickHandler);
159         else
160             this._messageTextElement.removeEventListener("click", this._boundClickHandler);
161     }
162
163     expand()
164     {
165         this._element.classList.add("expanded");
166
167         // Auto-expand an inner object tree if there is a single object.
168         if (this._objectTree && this._extraParameters.length === 1)
169             this._objectTree.expand();
170     }
171
172     collapse()
173     {
174         this._element.classList.remove("expanded");
175     }
176
177     toggle()
178     {
179         if (this._element.classList.contains("expanded"))
180             this.collapse();
181         else
182             this.expand();
183     }
184
185     toClipboardString(isPrefixOptional)
186     {
187         var clipboardString = this._messageTextElement.innerText;
188
189         if (this._message.type === WebInspector.ConsoleMessage.MessageType.Trace)
190             clipboardString = "console.trace()";
191
192         var hasStackTrace = this._shouldShowStackTrace();
193         if (hasStackTrace) {
194             this._message.stackTrace.forEach(function(frame) {
195                 clipboardString += "\n\t" + (frame.functionName || WebInspector.UIString("(anonymous function)"));
196                 if (frame.url)
197                     clipboardString += " (" + WebInspector.displayNameForURL(frame.url) + ", line " + frame.lineNumber + ")";
198             });
199         } else {
200             var repeatString = this.repeatCount > 1 ? "x" + this.repeatCount : "";
201             var urlLine = "";
202             if (this._message.url) {
203                 var components = [WebInspector.displayNameForURL(this._message.url), "line " + this._message.line];
204                 if (repeatString)
205                     components.push(repeatString);
206                 urlLine = " (" + components.join(", ") + ")";
207             } else if (repeatString)
208                 urlLine = " (" + repeatString + ")";
209
210             if (urlLine) {
211                 var lines = clipboardString.split("\n");
212                 lines[0] += urlLine;
213                 clipboardString = lines.join("\n");
214             }
215         }
216
217         if (!isPrefixOptional || this._enforcesClipboardPrefixString())
218             return this._clipboardPrefixString() + clipboardString;
219         return clipboardString;
220     }
221
222     // Private
223
224     _appendMessageTextAndArguments(element)
225     {
226         if (this._message.source === WebInspector.ConsoleMessage.MessageSource.ConsoleAPI) {
227             switch (this._message.type) {
228             case WebInspector.ConsoleMessage.MessageType.Trace:
229                 // FIXME: We should use a better string then console.trace.
230                 element.appendChild(document.createTextNode("console.trace()"));
231                 break;
232
233             case WebInspector.ConsoleMessage.MessageType.Assert:
234                 var args = [WebInspector.UIString("Assertion failed:")];
235                 if (this._message.parameters)
236                     args.concat(this._message.parameters);
237                 this._appendFormattedArguments(element, args);
238                 break;
239
240             case WebInspector.ConsoleMessage.MessageType.Dir:
241                 var obj = this._message.parameters ? this._message.parameters[0] : undefined;
242                 this._appendFormattedArguments(element, ["%O", obj]);
243                 break;
244
245             case WebInspector.ConsoleMessage.MessageType.Table:
246                 // FIXME: Remove messageText?
247                 var args = this._message.parameters || [this._message.messageText];
248                 element.appendChild(this._formatParameterAsTable(args));
249                 break;
250
251             default:
252                 var args = this._message.parameters || [this._message.messageText];
253                 this._appendFormattedArguments(element, args);
254                 break;
255             }
256             return;
257         }
258
259         // FIXME: Better handle WebInspector.ConsoleMessage.MessageSource.Network.
260
261         var args = this._message.parameters || [this._message.messageText];
262         this._appendFormattedArguments(element, args);
263     }
264
265     _appendSavedResultIndex(element)
266     {
267         if (!this._message.savedResultIndex)
268             return;
269
270         console.assert(this._message instanceof WebInspector.ConsoleCommandResultMessage);
271         console.assert(this._message.type === WebInspector.ConsoleMessage.MessageType.Result);
272
273         var savedVariableElement = document.createElement("span");
274         savedVariableElement.classList.add("console-saved-variable");
275         savedVariableElement.textContent = " = $" + this._message.savedResultIndex;
276
277         if (this._objectTree)
278             this._objectTree.appendTitleSuffix(savedVariableElement);
279         else
280             this._messageTextElement.appendChild(savedVariableElement);
281     }
282
283     _appendLocationLink()
284     {
285         // FIXME: Better handle WebInspector.ConsoleMessage.MessageSource.Network.
286         if (this._message.source === WebInspector.ConsoleMessage.MessageSource.Network || this._message.request)
287             return;
288
289         var firstNonNativeCallFrame = this._firstNonNativeCallFrame();
290         if (firstNonNativeCallFrame) {
291             var urlElement = this._linkifyCallFrame(firstNonNativeCallFrame);
292             this._element.appendChild(urlElement);
293         } else if (this._message.url && !this._shouldHideURL(this._message.url)) {
294             var urlElement = this._linkifyLocation(this._message.url, this._message.line, this._message.column);
295             this._element.appendChild(urlElement);
296         }
297     }
298
299     _appendExtraParameters()
300     {
301         if (!this._extraParameters || !this._extraParameters.length)
302             return;
303
304         this.expandable = true;
305
306         // Auto-expand if there are multiple objects.
307         if (this._extraParameters.length > 1)
308             this.expand();
309
310         this._extraElementsList = this._element.appendChild(document.createElement("ol"));
311         this._extraElementsList.classList.add("console-message-extra-parameters-container");
312
313         for (var parameter of this._extraParameters) {
314             var listItemElement = this._extraElementsList.appendChild(document.createElement("li"));
315             var forceObjectExpansion = parameter.type === "object" && (parameter.subtype !== "null" && parameter.subtype !== "regexp" && parameter.subtype !== "node");
316             listItemElement.classList.add("console-message-extra-parameter");
317             listItemElement.appendChild(this._formatParameter(parameter, forceObjectExpansion));
318         }
319     }
320
321     _appendStackTrace()
322     {
323         if (!this._shouldShowStackTrace())
324             return;
325
326         this.expandable = true;
327
328         // Auto-expand for console.trace.
329         if (this._message.type === WebInspector.ConsoleMessage.MessageType.Trace)
330             this.expand();
331
332         this._stackTraceElement = this._element.appendChild(document.createElement("ul"));
333         this._stackTraceElement.classList.add("console-message-stack-trace-container");
334         this._stackTraceElement.classList.add("console-message-text");
335
336         for (var callFrame of this._message.stackTrace) {
337             var callFrameElement = this._stackTraceElement.appendChild(document.createElement("li"));
338             callFrameElement.classList.add("console-message-stack-trace-call-frame");
339             callFrameElement.textContent = callFrame.functionName || WebInspector.UIString("(anonymous function)");
340             if (callFrame.url && !this._shouldHideURL(callFrame.url))
341                 callFrameElement.appendChild(this._linkifyCallFrame(callFrame));
342         }
343     }
344
345     _appendFormattedArguments(element, parameters)
346     {
347         if (!parameters.length)
348             return;
349
350         // FIXME: Only pass RemoteObjects here so we can avoid this work.
351         for (var i = 0; i < parameters.length; ++i) {
352             if (parameters[i] instanceof WebInspector.RemoteObject)
353                 continue;
354
355             if (typeof parameters[i] === "object")
356                 parameters[i] = WebInspector.RemoteObject.fromPayload(parameters[i]);
357             else
358                 parameters[i] = WebInspector.RemoteObject.fromPrimitiveValue(parameters[i]);
359         }
360
361         var builderElement = element.appendChild(document.createElement("span"));
362         var shouldFormatWithStringSubstitution = WebInspector.RemoteObject.type(parameters[0]) === "string" && this._message.type !== WebInspector.ConsoleMessage.MessageType.Result;
363
364         // Single object (e.g. console result or logging a non-string object).
365         if (parameters.length === 1 && !shouldFormatWithStringSubstitution) {
366             this._extraParameters = null;
367             builderElement.appendChild(this._formatParameter(parameters[0], false));
368             return;
369         }
370
371         console.assert(this._message.type !== WebInspector.ConsoleMessage.MessageType.Result);
372
373         // Format string / message / default message.
374         if (shouldFormatWithStringSubstitution) {
375             var result = this._formatWithSubstitutionString(parameters, builderElement);
376             parameters = result.unusedSubstitutions;
377             this._extraParameters = parameters;
378         } else {
379             var defaultMessage = WebInspector.UIString("No message");
380             builderElement.appendChild(document.createTextNode(defaultMessage));
381         }
382
383         // Trailing parameters.
384         if (parameters.length) {
385             if (parameters.length === 1) {
386                 // Single object. Show a preview.
387                 var enclosedElement = builderElement.appendChild(document.createElement("span"));
388                 enclosedElement.classList.add("console-message-preview-divider");
389                 enclosedElement.textContent = " \u2013 ";
390
391                 var previewContainer = builderElement.appendChild(document.createElement("span"));
392                 previewContainer.classList.add("console-message-preview");
393
394                 var parameter = parameters[0];
395                 var preview = WebInspector.FormattedValue.createObjectPreviewOrFormattedValueForRemoteObject(parameter, WebInspector.ObjectPreviewView.Mode.Brief);
396                 var isPreviewView = preview instanceof WebInspector.ObjectPreviewView;
397
398                 if (isPreviewView)
399                     preview.setOriginatingObjectInfo(parameter, null);
400
401                 var previewElement = isPreviewView ? preview.element : preview;
402                 previewContainer.appendChild(previewElement);
403
404                 // If this preview is effectively lossless, we can avoid making this console message expandable.
405                 if ((isPreviewView && preview.lossless) || (!isPreviewView && this._shouldConsiderObjectLossless(parameter)))
406                     this._extraParameters = null;
407             } else {
408                 // Multiple objects. Show an indicator.
409                 builderElement.appendChild(document.createTextNode(" "));
410                 var enclosedElement = builderElement.appendChild(document.createElement("span"));
411                 enclosedElement.classList.add("console-message-enclosed");
412                 enclosedElement.textContent = "(" + parameters.length + ")";
413             }
414         }
415     }
416
417     _shouldConsiderObjectLossless(object)
418     {
419         return object.type !== "object" || object.subtype === "null" || object.subtype === "regexp";
420     }
421
422     _formatParameter(parameter, forceObjectFormat)
423     {
424         var type;
425         if (forceObjectFormat)
426             type = "object";
427         else if (parameter instanceof WebInspector.RemoteObject)
428             type = parameter.subtype || parameter.type;
429         else {
430             console.assert(false, "no longer reachable");
431             type = typeof parameter;
432         }
433
434         var formatters = {
435             "object": this._formatParameterAsObject,
436             "error": this._formatParameterAsObject,
437             "map": this._formatParameterAsObject,
438             "set": this._formatParameterAsObject,
439             "weakmap": this._formatParameterAsObject,
440             "weakset": this._formatParameterAsObject,
441             "iterator": this._formatParameterAsObject,
442             "class": this._formatParameterAsObject,
443             "array": this._formatParameterAsArray,
444             "node": this._formatParameterAsNode,
445             "string": this._formatParameterAsString,
446         };
447
448         var formatter = formatters[type] || this._formatParameterAsValue;
449
450         var span = document.createElement("span");
451         formatter.call(this, parameter, span, forceObjectFormat);
452         return span;
453     }
454
455     _formatParameterAsValue(value, element)
456     {
457         element.appendChild(WebInspector.FormattedValue.createElementForRemoteObject(value));
458     }
459
460     _formatParameterAsString(object, element)
461     {
462         element.appendChild(WebInspector.FormattedValue.createLinkifiedElementString(object.description));
463     }
464
465     _formatParameterAsNode(object, element)
466     {
467         element.appendChild(WebInspector.FormattedValue.createElementForNode(object));
468     }
469
470     _formatParameterAsObject(object, element, forceExpansion)
471     {
472         // FIXME: Should have a better ObjectTreeView mode for classes (static methods and methods).
473         // FIXME: Only need to assign to objectTree if this is a ConsoleMessageResult. We should assert that.
474         this._objectTree = new WebInspector.ObjectTreeView(object, null, this._rootPropertyPathForObject(object), forceExpansion);
475         element.appendChild(this._objectTree.element);
476     }
477
478     _formatParameterAsArray(array, element)
479     {
480         this._objectTree = new WebInspector.ObjectTreeView(array, WebInspector.ObjectTreeView.Mode.Properties, this._rootPropertyPathForObject(array));
481         element.appendChild(this._objectTree.element);
482     }
483
484     _rootPropertyPathForObject(object)
485     {
486         if (!this._message.savedResultIndex)
487             return null;
488
489         return new WebInspector.PropertyPath(object, "$" + this._message.savedResultIndex);
490     }
491
492     _formatWithSubstitutionString(parameters, formattedResult)
493     {
494         function parameterFormatter(force, obj)
495         {
496             return this._formatParameter(obj, force);
497         }
498
499         function stringFormatter(obj)
500         {
501             return obj.description;
502         }
503
504         function floatFormatter(obj)
505         {
506             if (typeof obj.value !== "number")
507                 return parseFloat(obj.description);
508             return obj.value;
509         }
510
511         function integerFormatter(obj)
512         {
513             if (typeof obj.value !== "number")
514                 return parseInt(obj.description);
515             return Math.floor(obj.value);
516         }
517
518         var currentStyle = null;
519         function styleFormatter(obj)
520         {
521             currentStyle = {};
522             var buffer = document.createElement("span");
523             buffer.setAttribute("style", obj.description);
524             for (var i = 0; i < buffer.style.length; i++) {
525                 var property = buffer.style[i];
526                 if (isWhitelistedProperty(property))
527                     currentStyle[property] = buffer.style[property];
528             }
529         }
530
531         function isWhitelistedProperty(property)
532         {
533             for (var prefix of ["background", "border", "color", "font", "line", "margin", "padding", "text"]) {
534                 if (property.startsWith(prefix) || property.startsWith("-webkit-" + prefix))
535                     return true;
536             }
537             return false;
538         }
539
540         // Firebug uses %o for formatting objects.
541         var formatters = {};
542         formatters.o = parameterFormatter.bind(this, false);
543         formatters.s = stringFormatter;
544         formatters.f = floatFormatter;
545
546         // Firebug allows both %i and %d for formatting integers.
547         formatters.i = integerFormatter;
548         formatters.d = integerFormatter;
549
550         // Firebug uses %c for styling the message.
551         formatters.c = styleFormatter;
552
553         // Support %O to force object formatting, instead of the type-based %o formatting.
554         formatters.O = parameterFormatter.bind(this, true);
555
556         function append(a, b)
557         {
558             if (b instanceof Node)
559                 a.appendChild(b);
560             else if (b) {
561                 var toAppend = WebInspector.linkifyStringAsFragment(b.toString());
562                 if (currentStyle) {
563                     var wrapper = document.createElement("span");
564                     for (var key in currentStyle)
565                         wrapper.style[key] = currentStyle[key];
566                     wrapper.appendChild(toAppend);
567                     toAppend = wrapper;
568                 }
569
570                 var span = document.createElement("span");
571                 span.className = "type-string";
572                 span.appendChild(toAppend);
573                 a.appendChild(span);
574             }
575             return a;
576         }
577
578         // String.format does treat formattedResult like a Builder, result is an object.
579         return String.format(parameters[0].description, parameters.slice(1), formatters, formattedResult, append);
580     }
581
582     _shouldShowStackTrace()
583     {
584         if (!this._message.stackTrace || !this._message.stackTrace.length)
585             return false;
586
587         return this._message.source === WebInspector.ConsoleMessage.MessageSource.Network
588             || this._message.level === WebInspector.ConsoleMessage.MessageLevel.Error
589             || this._message.type === WebInspector.ConsoleMessage.MessageType.Trace;
590     }
591
592     _shouldHideURL(url)
593     {
594         return url === "undefined" || url === "[native code]";
595     }
596
597     _firstNonNativeCallFrame()
598     {
599         if (!this._message.stackTrace)
600             return null;
601
602         for (var i = 0; i < this._message.stackTrace.length; i++) {
603             var frame = this._message.stackTrace[i];
604             if (!frame.url || frame.url === "[native code]")
605                 continue;
606             return frame;
607         }
608
609         return null;
610     }
611
612     _linkifyLocation(url, lineNumber, columnNumber)
613     {
614         // ConsoleMessage stack trace line numbers are one-based.
615         lineNumber = lineNumber ? lineNumber - 1 : 0;
616         columnNumber = columnNumber ? columnNumber - 1 : 0;
617         return WebInspector.linkifyLocation(url, lineNumber, columnNumber, "console-message-url");
618     }
619
620     _linkifyCallFrame(callFrame)
621     {
622         return this._linkifyLocation(callFrame.url, callFrame.lineNumber, callFrame.columnNumber);
623     }
624
625     _userProvidedColumnNames(columnNamesArgument)
626     {
627         if (!columnNamesArgument)
628             return null;
629
630         console.assert(columnNamesArgument instanceof WebInspector.RemoteObject);
631
632         // Single primitive argument.
633         if (columnNamesArgument.type === "string" || columnNamesArgument.type === "number")
634             return [String(columnNamesArgument.value)];
635
636         // Ignore everything that is not an array with property previews.
637         if (columnNamesArgument.type !== "object" || columnNamesArgument.subtype !== "array" || !columnNamesArgument.preview || !columnNamesArgument.preview.propertyPreviews)
638             return null;
639
640         // Array. Look into the preview and get string values.
641         var extractedColumnNames = [];
642         for (var propertyPreview of columnNamesArgument.preview.propertyPreviews) {
643             if (propertyPreview.type === "string" || propertyPreview.type === "number")
644                 extractedColumnNames.push(String(propertyPreview.value));
645         }
646
647         return extractedColumnNames.length ? extractedColumnNames : null;
648     }
649
650     _formatParameterAsTable(parameters)
651     {
652         var element = document.createElement("span");
653         var table = parameters[0];
654         if (!table || !table.preview)
655             return element;
656
657         var rows = [];
658         var columnNames = [];
659         var flatValues = [];
660         var preview = table.preview;
661         var userProvidedColumnNames = false;
662
663         // User provided columnNames.
664         var extractedColumnNames = this._userProvidedColumnNames(parameters[1]);
665         if (extractedColumnNames) {
666             userProvidedColumnNames = true;
667             columnNames = extractedColumnNames;
668         }
669
670         // Check first for valuePreviews in the properties meaning this was an array of objects.
671         if (preview.propertyPreviews) {
672             for (var i = 0; i < preview.propertyPreviews.length; ++i) {
673                 var rowProperty = preview.propertyPreviews[i];
674                 var rowPreview = rowProperty.valuePreview;
675                 if (!rowPreview)
676                     continue;
677
678                 var rowValue = {};
679                 var maxColumnsToRender = 10;
680                 for (var j = 0; j < rowPreview.propertyPreviews.length; ++j) {
681                     var cellProperty = rowPreview.propertyPreviews[j];
682                     var columnRendered = columnNames.includes(cellProperty.name);
683                     if (!columnRendered) {
684                         if (userProvidedColumnNames || columnNames.length === maxColumnsToRender)
685                             continue;
686                         columnRendered = true;
687                         columnNames.push(cellProperty.name);
688                     }
689
690                     rowValue[cellProperty.name] = WebInspector.FormattedValue.createElementForPropertyPreview(cellProperty);
691                 }
692                 rows.push([rowProperty.name, rowValue]);
693             }
694         }
695
696         // If there were valuePreviews, convert to a flat list.
697         if (rows.length) {
698             var emDash = "\u2014";
699             columnNames.unshift(WebInspector.UIString("(Index)"));
700             for (var i = 0; i < rows.length; ++i) {
701                 var rowName = rows[i][0];
702                 var rowValue = rows[i][1];
703                 flatValues.push(rowName);
704                 for (var j = 1; j < columnNames.length; ++j) {
705                     var columnName = columnNames[j];
706                     if (!(columnName in rowValue))
707                         flatValues.push(emDash);
708                     else
709                         flatValues.push(rowValue[columnName]);
710                 }
711             }
712         }
713
714         // If there were no value Previews, then check for an array of values.
715         if (!flatValues.length && preview.propertyPreviews) {
716             for (var i = 0; i < preview.propertyPreviews.length; ++i) {
717                 var rowProperty = preview.propertyPreviews[i];
718                 if (!("value" in rowProperty))
719                     continue;
720
721                 if (!columnNames.length) {
722                     columnNames.push(WebInspector.UIString("Index"));
723                     columnNames.push(WebInspector.UIString("Value"));
724                 }
725
726                 flatValues.push(rowProperty.name);
727                 flatValues.push(WebInspector.FormattedValue.createElementForPropertyPreview(rowProperty));
728             }
729         }
730
731         // If lossless or not table data, output the object so full data can be gotten.
732         if (!preview.lossless || !flatValues.length) {
733             element.appendChild(this._formatParameter(table));
734             if (!flatValues.length)
735                 return element;
736         }
737
738         var dataGrid = WebInspector.DataGrid.createSortableDataGrid(columnNames, flatValues);
739         dataGrid.element.classList.add("inline");
740         element.appendChild(dataGrid.element);
741
742         return element;
743     }
744
745     _levelString()
746     {
747         switch (this._message.level) {
748         case WebInspector.ConsoleMessage.MessageLevel.Log:
749             return "Log";
750         case WebInspector.ConsoleMessage.MessageLevel.Info:
751             return "Info";
752         case WebInspector.ConsoleMessage.MessageLevel.Warning:
753             return "Warning";
754         case WebInspector.ConsoleMessage.MessageLevel.Debug:
755             return "Debug";
756         case WebInspector.ConsoleMessage.MessageLevel.Error:
757             return "Error";
758         }
759     }
760
761     _enforcesClipboardPrefixString()
762     {
763         return this._message.type !== WebInspector.ConsoleMessage.MessageType.Result;
764     }
765
766     _clipboardPrefixString()
767     {
768         if (this._message.type === WebInspector.ConsoleMessage.MessageType.Result)
769             return "< ";
770
771         return "[" + this._levelString() + "] ";
772     }
773 };