dca90be030ce7bbabfbf74a1de37641a46b508d5
[WebKit-https.git] / WebCore / inspector / front-end / Console.js
1 /*
2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 WebInspector.Console = function()
30 {
31     this.messages = [];
32
33     WebInspector.View.call(this, document.getElementById("console"));
34
35     this.messagesElement = document.getElementById("console-messages");
36     this.messagesElement.addEventListener("selectstart", this._messagesSelectStart.bind(this), false);
37     this.messagesElement.addEventListener("click", this._messagesClicked.bind(this), true);
38
39     this.promptElement = document.getElementById("console-prompt");
40     this.promptElement.handleKeyEvent = this._promptKeyDown.bind(this);
41     this.prompt = new WebInspector.TextPrompt(this.promptElement, this.completions.bind(this), " .=:[({;");
42
43     this.toggleButton = document.getElementById("console-status-bar-item");
44     this.toggleButton.title = WebInspector.UIString("Show console.");
45     this.toggleButton.addEventListener("click", this._toggleButtonClicked.bind(this), false);
46
47     this.clearButton = document.getElementById("clear-console-status-bar-item");
48     this.clearButton.title = WebInspector.UIString("Clear console log.");
49     this.clearButton.addEventListener("click", this._clearButtonClicked.bind(this), false);
50
51     this.topGroup = new WebInspector.ConsoleGroup(null, 0);
52     this.messagesElement.insertBefore(this.topGroup.element, this.promptElement);
53     this.groupLevel = 0;
54     this.currentGroup = this.topGroup;
55
56     document.getElementById("main-status-bar").addEventListener("mousedown", this._startStatusBarDragging.bind(this), true);
57 }
58
59 WebInspector.Console.prototype = {
60     show: function()
61     {
62         if (this._animating || this.visible)
63             return;
64
65         WebInspector.View.prototype.show.call(this);
66
67         this._animating = true;
68
69         this.toggleButton.addStyleClass("toggled-on");
70         this.toggleButton.title = WebInspector.UIString("Hide console.");
71
72         document.body.addStyleClass("console-visible");
73
74         var anchoredItems = document.getElementById("anchored-status-bar-items");
75
76         var animations = [
77             {element: document.getElementById("main"), end: {bottom: this.element.offsetHeight}},
78             {element: document.getElementById("main-status-bar"), start: {"padding-left": anchoredItems.offsetWidth - 1}, end: {"padding-left": 0}},
79             {element: document.getElementById("other-console-status-bar-items"), start: {opacity: 0}, end: {opacity: 1}}
80         ];
81
82         var consoleStatusBar = document.getElementById("console-status-bar");
83         consoleStatusBar.insertBefore(anchoredItems, consoleStatusBar.firstChild);
84
85         function animationFinished()
86         {
87             if ("updateStatusBarItems" in WebInspector.currentPanel)
88                 WebInspector.currentPanel.updateStatusBarItems();
89             WebInspector.currentFocusElement = this.promptElement;
90             delete this._animating;
91         }
92
93         WebInspector.animateStyle(animations, window.event && window.event.shiftKey ? 2000 : 250, animationFinished.bind(this));
94
95         if (!this.prompt.isCaretInsidePrompt())
96             this.prompt.moveCaretToEndOfPrompt();
97     },
98
99     hide: function()
100     {
101         if (this._animating || !this.visible)
102             return;
103
104         WebInspector.View.prototype.hide.call(this);
105
106         this._animating = true;
107
108         this.toggleButton.removeStyleClass("toggled-on");
109         this.toggleButton.title = WebInspector.UIString("Show console.");
110
111         if (this.element === WebInspector.currentFocusElement || this.element.isAncestor(WebInspector.currentFocusElement))
112             WebInspector.currentFocusElement = WebInspector.previousFocusElement;
113
114         var anchoredItems = document.getElementById("anchored-status-bar-items");
115
116         // Temporally set properties and classes to mimic the post-animation values so panels
117         // like Elements in their updateStatusBarItems call will size things to fit the final location.
118         document.getElementById("main-status-bar").style.setProperty("padding-left", (anchoredItems.offsetWidth - 1) + "px");
119         document.body.removeStyleClass("console-visible");
120         if ("updateStatusBarItems" in WebInspector.currentPanel)
121             WebInspector.currentPanel.updateStatusBarItems();
122         document.body.addStyleClass("console-visible");
123
124         var animations = [
125             {element: document.getElementById("main"), end: {bottom: 0}},
126             {element: document.getElementById("main-status-bar"), start: {"padding-left": 0}, end: {"padding-left": anchoredItems.offsetWidth - 1}},
127             {element: document.getElementById("other-console-status-bar-items"), start: {opacity: 1}, end: {opacity: 0}}
128         ];
129
130         function animationFinished()
131         {
132             var mainStatusBar = document.getElementById("main-status-bar");
133             mainStatusBar.insertBefore(anchoredItems, mainStatusBar.firstChild);
134             mainStatusBar.style.removeProperty("padding-left");
135             document.body.removeStyleClass("console-visible");
136             delete this._animating;
137         }
138
139         WebInspector.animateStyle(animations, window.event && window.event.shiftKey ? 2000 : 250, animationFinished.bind(this));
140     },
141
142     addMessage: function(msg)
143     {
144         if (msg instanceof WebInspector.ConsoleMessage) {
145             msg.totalRepeatCount = msg.repeatCount;
146             msg.repeatDelta = msg.repeatCount;
147
148             var messageRepeated = false;
149
150             if (msg.isEqual && msg.isEqual(this.previousMessage)) {
151                 // Because sometimes we get a large number of repeated messages and sometimes
152                 // we get them one at a time, we need to know the difference between how many
153                 // repeats we used to have and how many we have now.
154                 msg.repeatDelta -= this.previousMessage.totalRepeatCount;
155
156                 if (!isNaN(this.repeatCountBeforeCommand))
157                     msg.repeatCount -= this.repeatCountBeforeCommand;
158
159                 if (!this.commandSincePreviousMessage) {
160                     // Recreate the previous message element to reset the repeat count.
161                     var messagesElement = this.currentGroup.messagesElement;
162                     messagesElement.removeChild(messagesElement.lastChild);
163                     messagesElement.appendChild(msg.toMessageElement());
164
165                     messageRepeated = true;
166                 }
167             } else
168                 delete this.repeatCountBeforeCommand;
169
170             // Increment the error or warning count
171             switch (msg.level) {
172             case WebInspector.ConsoleMessage.MessageLevel.Warning:
173                 WebInspector.warnings += msg.repeatDelta;
174                 break;
175             case WebInspector.ConsoleMessage.MessageLevel.Error:
176                 WebInspector.errors += msg.repeatDelta;
177                 break;
178             }
179
180             // Add message to the resource panel
181             if (msg.url in WebInspector.resourceURLMap) {
182                 msg.resource = WebInspector.resourceURLMap[msg.url];
183                 if (WebInspector.panels.resources)
184                     WebInspector.panels.resources.addMessageToResource(msg.resource, msg);
185             }
186
187             this.commandSincePreviousMessage = false;
188             this.previousMessage = msg;
189
190             if (messageRepeated)
191                 return;
192         } else if (msg instanceof WebInspector.ConsoleCommand) {
193             if (this.previousMessage) {
194                 this.commandSincePreviousMessage = true;
195                 this.repeatCountBeforeCommand = this.previousMessage.totalRepeatCount;
196             }
197         }
198
199         this.messages.push(msg);
200
201         if (msg.level === WebInspector.ConsoleMessage.MessageLevel.EndGroup) {
202             if (this.groupLevel < 1)
203                 return;
204
205             this.groupLevel--;
206
207             this.currentGroup = this.currentGroup.parentGroup;
208         } else {
209             if (msg.level === WebInspector.ConsoleMessage.MessageLevel.StartGroup) {
210                 this.groupLevel++;
211
212                 var group = new WebInspector.ConsoleGroup(this.currentGroup, this.groupLevel);
213                 this.currentGroup.messagesElement.appendChild(group.element);
214                 this.currentGroup = group;
215             }
216
217             this.currentGroup.addMessage(msg);
218         }
219
220         this.promptElement.scrollIntoView(false);
221     },
222
223     clearMessages: function(clearInspectorController)
224     {
225         if (clearInspectorController)
226             InspectorController.clearMessages();
227         if (WebInspector.panels.resources)
228             WebInspector.panels.resources.clearMessages();
229
230         this.messages = [];
231
232         this.groupLevel = 0;
233         this.currentGroup = this.topGroup;
234         this.topGroup.messagesElement.removeChildren();
235
236         WebInspector.errors = 0;
237         WebInspector.warnings = 0;
238
239         delete this.commandSincePreviousMessage;
240         delete this.repeatCountBeforeCommand;
241         delete this.previousMessage;
242     },
243
244     completions: function(wordRange, bestMatchOnly)
245     {
246         // Pass less stop characters to rangeOfWord so the range will be a more complete expression.
247         const expressionStopCharacters = " =:{;";
248         var expressionRange = wordRange.startContainer.rangeOfWord(wordRange.startOffset, expressionStopCharacters, this.promptElement, "backward");
249         var expressionString = expressionRange.toString();
250         var lastIndex = expressionString.length - 1;
251
252         var dotNotation = (expressionString[lastIndex] === ".");
253         var bracketNotation = (expressionString[lastIndex] === "[");
254
255         if (dotNotation || bracketNotation)
256             expressionString = expressionString.substr(0, lastIndex);
257
258         var prefix = wordRange.toString();
259         if (!expressionString && !prefix)
260             return;
261
262         var result;
263         if (expressionString) {
264             try {
265                 result = this._evalInInspectedWindow(expressionString);
266             } catch(e) {
267                 // Do nothing, the prefix will be considered a window property.
268             }
269         } else {
270             // There is no expressionString, so the completion should happen against global properties.
271             // Or if the debugger is paused, against properties in scope of the selected call frame.
272             if (WebInspector.panels.scripts && WebInspector.panels.scripts.paused)
273                 result = WebInspector.panels.scripts.variablesInScopeForSelectedCallFrame();
274             else
275                 result = InspectorController.inspectedWindow();
276         }
277
278         if (bracketNotation) {
279             if (prefix.length && prefix[0] === "'")
280                 var quoteUsed = "'";
281             else
282                 var quoteUsed = "\"";
283         }
284
285         var results = [];
286         var properties = Object.sortedProperties(result);
287         for (var i = 0; i < properties.length; ++i) {
288             var property = properties[i];
289
290             if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property))
291                 continue;
292
293             if (bracketNotation) {
294                 if (!/^[0-9]+$/.test(property))
295                     property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + quoteUsed;
296                 property += "]";
297             }
298
299             if (property.length < prefix.length)
300                 continue;
301             if (property.indexOf(prefix) !== 0)
302                 continue;
303
304             results.push(property);
305             if (bestMatchOnly)
306                 break;
307         }
308
309         return results;
310     },
311
312     _toggleButtonClicked: function()
313     {
314         this.visible = !this.visible;
315     },
316
317     _clearButtonClicked: function()
318     {
319         this.clearMessages(true);
320     },
321
322     _messagesSelectStart: function(event)
323     {
324         if (this._selectionTimeout)
325             clearTimeout(this._selectionTimeout);
326
327         this.prompt.clearAutoComplete();
328
329         function moveBackIfOutside()
330         {
331             delete this._selectionTimeout;
332             if (!this.prompt.isCaretInsidePrompt() && window.getSelection().isCollapsed)
333                 this.prompt.moveCaretToEndOfPrompt();
334             this.prompt.autoCompleteSoon();
335         }
336
337         this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
338     },
339
340     _messagesClicked: function(event)
341     {
342         var link = event.target.enclosingNodeOrSelfWithNodeName("a");
343         if (!link || !link.representedNode)
344             return;
345
346         WebInspector.updateFocusedNode(link.representedNode);
347         event.stopPropagation();
348         event.preventDefault();
349     },
350
351     _promptKeyDown: function(event)
352     {
353         switch (event.keyIdentifier) {
354             case "Enter":
355                 this._enterKeyPressed(event);
356                 return;
357         }
358
359         this.prompt.handleKeyEvent(event);
360     },
361
362     _startStatusBarDragging: function(event)
363     {
364         if (!this.visible || event.target !== document.getElementById("main-status-bar"))
365             return;
366
367         WebInspector.elementDragStart(document.getElementById("main-status-bar"), this._statusBarDragging.bind(this), this._endStatusBarDragging.bind(this), event, "row-resize");
368
369         this._statusBarDragOffset = event.pageY - this.element.totalOffsetTop;
370
371         event.stopPropagation();
372     },
373
374     _statusBarDragging: function(event)
375     {
376         var mainElement = document.getElementById("main");
377
378         var height = window.innerHeight - event.pageY + this._statusBarDragOffset;
379         height = Number.constrain(height, Preferences.minConsoleHeight, window.innerHeight - mainElement.totalOffsetTop - Preferences.minConsoleHeight);
380
381         mainElement.style.bottom = height + "px";
382         this.element.style.height = height + "px";
383
384         event.preventDefault();
385         event.stopPropagation();
386     },
387
388     _endStatusBarDragging: function(event)
389     {
390         WebInspector.elementDragEnd(event);
391
392         delete this._statusBarDragOffset;
393
394         event.stopPropagation();
395     },
396
397     _evalInInspectedWindow: function(expression)
398     {
399         if (WebInspector.panels.scripts && WebInspector.panels.scripts.paused)
400             return WebInspector.panels.scripts.evaluateInSelectedCallFrame(expression);
401
402         var inspectedWindow = InspectorController.inspectedWindow();
403         if (!inspectedWindow._inspectorCommandLineAPI) {
404             inspectedWindow.eval("window._inspectorCommandLineAPI = { \
405                 $: function() { return document.getElementById.apply(document, arguments) }, \
406                 $$: function() { return document.querySelectorAll.apply(document, arguments) }, \
407                 $x: function(xpath, context) { \
408                     var nodes = []; \
409                     try { \
410                         var doc = context || document; \
411                         var results = doc.evaluate(xpath, doc, null, XPathResult.ANY_TYPE, null); \
412                         var node; \
413                         while (node = results.iterateNext()) nodes.push(node); \
414                     } catch (e) {} \
415                     return nodes; \
416                 }, \
417                 dir: function() { return console.dir.apply(console, arguments) }, \
418                 dirxml: function() { return console.dirxml.apply(console, arguments) }, \
419                 keys: function(o) { var a = []; for (k in o) a.push(k); return a; }, \
420                 values: function(o) { var a = []; for (k in o) a.push(o[k]); return a; }, \
421                 profile: function() { return console.profile.apply(console, arguments) }, \
422                 profileEnd: function() { return console.profileEnd.apply(console, arguments) } \
423             };");
424
425             inspectedWindow._inspectorCommandLineAPI.clear = InspectorController.wrapCallback(this.clearMessages.bind(this));
426         }
427
428         // Surround the expression in with statements to inject our command line API so that
429         // the window object properties still take more precedent than our API functions.
430         expression = "with (window._inspectorCommandLineAPI) { with (window) { " + expression + " } }";
431
432         return inspectedWindow.eval(expression);
433     },
434
435     _enterKeyPressed: function(event)
436     {
437         if (event.altKey)
438             return;
439
440         event.preventDefault();
441         event.stopPropagation();
442
443         this.prompt.clearAutoComplete(true);
444
445         var str = this.prompt.text;
446         if (!str.length)
447             return;
448
449         var commandMessage = new WebInspector.ConsoleCommand(str);
450         this.addMessage(commandMessage);
451
452         var result;
453         var exception = false;
454         try {
455             result = this._evalInInspectedWindow(str);
456         } catch(e) {
457             result = e;
458             exception = true;
459         }
460
461         this.prompt.history.push(str);
462         this.prompt.historyOffset = 0;
463         this.prompt.text = "";
464
465         this.addMessage(new WebInspector.ConsoleCommandResult(result, exception, commandMessage));
466     },
467
468     _mouseOverNode: function(event)
469     {
470         var anchorElement = event.target.enclosingNodeOrSelfWithNodeName("a");
471         WebInspector.hoveredDOMNode = (anchorElement ? anchorElement.representedNode : null);
472     },
473
474     _mouseOutOfNode: function(event)
475     {
476         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
477         var anchorElement = nodeUnderMouse.enclosingNodeOrSelfWithNodeName("a");
478         if (!anchorElement || !anchorElement.representedNode)
479             WebInspector.hoveredDOMNode = null;
480     },
481
482     _format: function(output, inline)
483     {
484         var type = Object.type(output, InspectorController.inspectedWindow());
485         if (type === "object") {
486             if (output instanceof InspectorController.inspectedWindow().Node)
487                 type = "node";
488         }
489
490         // We don't perform any special formatting on these types, so we just
491         // pass them through the simple _formatvalue function.
492         var undecoratedTypes = {
493             "undefined": 1,
494             "null": 1,
495             "boolean": 1,
496             "number": 1,
497             "date": 1,
498             "function": 1,
499         };
500
501         var formatter;
502         if (type in undecoratedTypes)
503             formatter = "_formatvalue";
504         else {
505             formatter = "_format" + type;
506             if (!(formatter in this)) {
507                 formatter = "_formatobject";
508                 type = "object";
509             }
510         }
511
512         var span = document.createElement("span");
513         span.addStyleClass("console-formatted-" + type);
514         this[formatter](output, span, inline);
515         return span;
516     },
517
518     _formatvalue: function(val, elem, inline)
519     {
520         elem.appendChild(document.createTextNode(val));
521     },
522
523     _formatstring: function(str, elem, inline)
524     {
525         elem.appendChild(document.createTextNode("\"" + str + "\""));
526     },
527
528     _formatregexp: function(re, elem, inline)
529     {
530         var formatted = String(re).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1);
531         elem.appendChild(document.createTextNode(formatted));
532     },
533
534     _formatarray: function(arr, elem, inline)
535     {
536         elem.appendChild(document.createTextNode("["));
537         for (var i = 0; i < arr.length; ++i) {
538             elem.appendChild(this._format(arr[i], true));
539             if (i < arr.length - 1)
540                 elem.appendChild(document.createTextNode(", "));
541         }
542         elem.appendChild(document.createTextNode("]"));
543     },
544
545     _formatnode: function(node, elem, inline)
546     {
547         var anchor = document.createElement("a");
548         anchor.className = "inspectable-node";
549         anchor.innerHTML = nodeTitleInfo.call(node).title;
550         anchor.representedNode = node;
551         anchor.addEventListener("mouseover", this._mouseOverNode.bind(this), false);
552         anchor.addEventListener("mouseout", this._mouseOutOfNode.bind(this), false);
553
554         if (inline)
555             elem.appendChild(anchor);
556         else
557             elem.appendChild(new WebInspector.ObjectPropertiesSection(node, anchor, null, null, true).element);
558     },
559
560     _formatobject: function(obj, elem, inline)
561     {
562         if (inline)
563             elem.appendChild(document.createTextNode(Object.describe(obj)));
564         else
565             elem.appendChild(new WebInspector.ObjectPropertiesSection(obj, null, null, null, true).element);
566     },
567
568     _formaterror: function(obj, elem, inline)
569     {
570         var messageElement = document.createElement("span");
571         messageElement.className = "error-message";
572         messageElement.textContent = obj.name + ": " + obj.message;
573         elem.appendChild(messageElement);
574
575         if (obj.sourceURL) {
576             var urlElement = document.createElement("a");
577             urlElement.className = "webkit-html-resource-link";
578             urlElement.href = obj.sourceURL;
579             urlElement.lineNumber = obj.line;
580             urlElement.preferredPanel = "scripts";
581
582             if (obj.line > 0)
583                 urlElement.textContent = WebInspector.displayNameForURL(obj.sourceURL) + ":" + obj.line;
584             else
585                 urlElement.textContent = WebInspector.displayNameForURL(obj.sourceURL);
586
587             elem.appendChild(document.createTextNode(" ("));
588             elem.appendChild(urlElement);
589             elem.appendChild(document.createTextNode(")"));
590         }
591     },
592 }
593
594 WebInspector.Console.prototype.__proto__ = WebInspector.View.prototype;
595
596 WebInspector.ConsoleMessage = function(source, level, line, url, groupLevel, repeatCount)
597 {
598     this.source = source;
599     this.level = level;
600     this.line = line;
601     this.url = url;
602     this.groupLevel = groupLevel;
603     this.repeatCount = repeatCount;
604
605     switch (this.level) {
606         case WebInspector.ConsoleMessage.MessageLevel.Object:
607             var propertiesSection = new WebInspector.ObjectPropertiesSection(arguments[6], null, null, null, true);
608             propertiesSection.element.addStyleClass("console-message");
609             this.propertiesSection = propertiesSection;
610             break;
611         case WebInspector.ConsoleMessage.MessageLevel.Node:
612             var node = arguments[6];
613             if (!(node instanceof InspectorController.inspectedWindow().Node))
614                 return;
615             this.elementsTreeOutline = new WebInspector.ElementsTreeOutline();
616             this.elementsTreeOutline.rootDOMNode = node;
617             break;
618         case WebInspector.ConsoleMessage.MessageLevel.Trace:
619             var span = document.createElement("span");
620             span.addStyleClass("console-formatted-trace");
621             var stack = Array.prototype.slice.call(arguments, 6);
622             var funcNames = stack.map(function(f) {
623                 return f || WebInspector.UIString("(anonymous function)");
624             });
625             span.appendChild(document.createTextNode(funcNames.join("\n")));
626             this.formattedMessage = span;
627             break;
628         default:
629             // The formatedMessage property is used for the rich and interactive console.
630             this.formattedMessage = this._format(Array.prototype.slice.call(arguments, 6));
631
632             // This is used for inline message bubbles in SourceFrames, or other plain-text representations.
633             this.message = this.formattedMessage.textContent;
634             break;
635     }
636 }
637
638 WebInspector.ConsoleMessage.prototype = {
639     isErrorOrWarning: function()
640     {
641         return (this.level === WebInspector.ConsoleMessage.MessageLevel.Warning || this.level === WebInspector.ConsoleMessage.MessageLevel.Error);
642     },
643
644     _format: function(parameters)
645     {
646         var formattedResult = document.createElement("span");
647
648         if (!parameters.length)
649             return formattedResult;
650
651         function formatForConsole(obj)
652         {
653             return WebInspector.console._format(obj, true);
654         }
655
656         if (Object.type(parameters[0], InspectorController.inspectedWindow()) === "string") {
657             var formatters = {}
658             for (var i in String.standardFormatters)
659                 formatters[i] = String.standardFormatters[i];
660
661             // Firebug uses %o for formatting objects.
662             formatters.o = formatForConsole;
663             // Firebug allows both %i and %d for formatting integers.
664             formatters.i = formatters.d;
665
666             function append(a, b)
667             {
668                 if (!(b instanceof Node))
669                     a.appendChild(WebInspector.linkifyStringAsFragment(b.toString()));
670                 else
671                     a.appendChild(b);
672                 return a;
673             }
674
675             var result = String.format(parameters[0], parameters.slice(1), formatters, formattedResult, append);
676             formattedResult = result.formattedResult;
677             parameters = result.unusedSubstitutions;
678             if (parameters.length)
679                 formattedResult.appendChild(document.createTextNode(" "));
680         }
681
682         for (var i = 0; i < parameters.length; ++i) {
683             if (typeof parameters[i] === "string")
684                 formattedResult.appendChild(WebInspector.linkifyStringAsFragment(parameters[i]));
685             else if (parameters.length === 1)
686                 formattedResult.appendChild(WebInspector.console._format(parameters[0]));
687             else
688                 formattedResult.appendChild(formatForConsole(parameters[i]));
689             if (i < parameters.length - 1)
690                 formattedResult.appendChild(document.createTextNode(" "));
691         }
692
693         return formattedResult;
694     },
695
696     toMessageElement: function()
697     {
698         if (this.propertiesSection)
699             return this.propertiesSection.element;
700
701         var element = document.createElement("div");
702         element.message = this;
703         element.className = "console-message";
704
705         switch (this.source) {
706             case WebInspector.ConsoleMessage.MessageSource.HTML:
707                 element.addStyleClass("console-html-source");
708                 break;
709             case WebInspector.ConsoleMessage.MessageSource.WML:
710                 element.addStyleClass("console-wml-source");
711                 break;
712             case WebInspector.ConsoleMessage.MessageSource.XML:
713                 element.addStyleClass("console-xml-source");
714                 break;
715             case WebInspector.ConsoleMessage.MessageSource.JS:
716                 element.addStyleClass("console-js-source");
717                 break;
718             case WebInspector.ConsoleMessage.MessageSource.CSS:
719                 element.addStyleClass("console-css-source");
720                 break;
721             case WebInspector.ConsoleMessage.MessageSource.Other:
722                 element.addStyleClass("console-other-source");
723                 break;
724         }
725
726         switch (this.level) {
727             case WebInspector.ConsoleMessage.MessageLevel.Tip:
728                 element.addStyleClass("console-tip-level");
729                 break;
730             case WebInspector.ConsoleMessage.MessageLevel.Log:
731                 element.addStyleClass("console-log-level");
732                 break;
733             case WebInspector.ConsoleMessage.MessageLevel.Warning:
734                 element.addStyleClass("console-warning-level");
735                 break;
736             case WebInspector.ConsoleMessage.MessageLevel.Error:
737                 element.addStyleClass("console-error-level");
738                 break;
739             case WebInspector.ConsoleMessage.MessageLevel.StartGroup:
740                 element.addStyleClass("console-group-title-level");
741         }
742
743         if (this.elementsTreeOutline) {
744             element.addStyleClass("outline-disclosure");
745             element.appendChild(this.elementsTreeOutline.element);
746             return element;
747         }
748
749         if (this.repeatCount > 1) {
750             var messageRepeatCountElement = document.createElement("span");
751             messageRepeatCountElement.className = "bubble";
752             messageRepeatCountElement.textContent = this.repeatCount;
753
754             element.appendChild(messageRepeatCountElement);
755             element.addStyleClass("repeated-message");
756         }
757
758         if (this.url && this.url !== "undefined") {
759             var urlElement = document.createElement("a");
760             urlElement.className = "console-message-url webkit-html-resource-link";
761             urlElement.href = this.url;
762             urlElement.lineNumber = this.line;
763
764             if (this.source === WebInspector.ConsoleMessage.MessageSource.JS)
765                 urlElement.preferredPanel = "scripts";
766
767             if (this.line > 0)
768                 urlElement.textContent = WebInspector.displayNameForURL(this.url) + ":" + this.line;
769             else
770                 urlElement.textContent = WebInspector.displayNameForURL(this.url);
771
772             element.appendChild(urlElement);
773         }
774
775         var messageTextElement = document.createElement("span");
776         messageTextElement.className = "console-message-text";
777         messageTextElement.appendChild(this.formattedMessage);
778         element.appendChild(messageTextElement);
779
780         return element;
781     },
782
783     toString: function()
784     {
785         var sourceString;
786         switch (this.source) {
787             case WebInspector.ConsoleMessage.MessageSource.HTML:
788                 sourceString = "HTML";
789                 break;
790             case WebInspector.ConsoleMessage.MessageSource.WML:
791                 sourceString = "WML";
792                 break;
793             case WebInspector.ConsoleMessage.MessageSource.XML:
794                 sourceString = "XML";
795                 break;
796             case WebInspector.ConsoleMessage.MessageSource.JS:
797                 sourceString = "JS";
798                 break;
799             case WebInspector.ConsoleMessage.MessageSource.CSS:
800                 sourceString = "CSS";
801                 break;
802             case WebInspector.ConsoleMessage.MessageSource.Other:
803                 sourceString = "Other";
804                 break;
805         }
806
807         var levelString;
808         switch (this.level) {
809             case WebInspector.ConsoleMessage.MessageLevel.Tip:
810                 levelString = "Tip";
811                 break;
812             case WebInspector.ConsoleMessage.MessageLevel.Log:
813                 levelString = "Log";
814                 break;
815             case WebInspector.ConsoleMessage.MessageLevel.Warning:
816                 levelString = "Warning";
817                 break;
818             case WebInspector.ConsoleMessage.MessageLevel.Error:
819                 levelString = "Error";
820                 break;
821             case WebInspector.ConsoleMessage.MessageLevel.Object:
822                 levelString = "Object";
823                 break;
824             case WebInspector.ConsoleMessage.MessageLevel.GroupTitle:
825                 levelString = "GroupTitle";
826                 break;
827         }
828
829         return sourceString + " " + levelString + ": " + this.formattedMessage.textContent + "\n" + this.url + " line " + this.line;
830     },
831     
832     isEqual: function(msg, disreguardGroup)
833     {
834         if (!msg)
835             return false;
836
837         var ret = (this.source == msg.source)
838             && (this.level == msg.level)
839             && (this.line == msg.line)
840             && (this.url == msg.url)
841             && (this.message == msg.message);
842
843         return (disreguardGroup ? ret : (ret && (this.groupLevel == msg.groupLevel)));
844     }
845 }
846
847 // Note: Keep these constants in sync with the ones in Console.h
848 WebInspector.ConsoleMessage.MessageSource = {
849     HTML: 0,
850     WML: 1,
851     XML: 2,
852     JS: 3,
853     CSS: 4,
854     Other: 5
855 }
856
857 WebInspector.ConsoleMessage.MessageLevel = {
858     Tip: 0,
859     Log: 1,
860     Warning: 2,
861     Error: 3,
862     Object: 4,
863     Node: 5,
864     Trace: 6,
865     StartGroup: 7,
866     EndGroup: 8
867 }
868
869 WebInspector.ConsoleCommand = function(command)
870 {
871     this.command = command;
872 }
873
874 WebInspector.ConsoleCommand.prototype = {
875     toMessageElement: function()
876     {
877         var element = document.createElement("div");
878         element.command = this;
879         element.className = "console-user-command";
880
881         var commandTextElement = document.createElement("span");
882         commandTextElement.className = "console-message-text";
883         commandTextElement.textContent = this.command;
884         element.appendChild(commandTextElement);
885
886         return element;
887     }
888 }
889
890 WebInspector.ConsoleCommandResult = function(result, exception, originatingCommand)
891 {
892     var level = (exception ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log);
893     var message = (exception ? String(result) : result);
894     var line = (exception ? result.line : -1);
895     var url = (exception ? result.sourceURL : null);
896
897     WebInspector.ConsoleMessage.call(this, WebInspector.ConsoleMessage.MessageSource.JS, level, line, url, null, 1, message);
898
899     this.originatingCommand = originatingCommand;
900 }
901
902 WebInspector.ConsoleCommandResult.prototype = {
903     toMessageElement: function()
904     {
905         var element = WebInspector.ConsoleMessage.prototype.toMessageElement.call(this);
906         element.addStyleClass("console-user-command-result");
907         return element;
908     }
909 }
910
911 WebInspector.ConsoleCommandResult.prototype.__proto__ = WebInspector.ConsoleMessage.prototype;
912
913 WebInspector.ConsoleGroup = function(parentGroup, level)
914 {
915     this.parentGroup = parentGroup;
916     this.level = level;
917
918     var element = document.createElement("div");
919     element.className = "console-group";
920     element.group = this;
921     this.element = element;
922
923     var messagesElement = document.createElement("div");
924     messagesElement.className = "console-group-messages";
925     element.appendChild(messagesElement);
926     this.messagesElement = messagesElement;
927 }
928
929 WebInspector.ConsoleGroup.prototype = {
930     addMessage: function(msg)
931     {
932         var element = msg.toMessageElement();
933         
934         if (msg.level === WebInspector.ConsoleMessage.MessageLevel.StartGroup) {
935             this.messagesElement.parentNode.insertBefore(element, this.messagesElement);
936             element.addEventListener("click", this._titleClicked.bind(this), true);
937         } else
938             this.messagesElement.appendChild(element);
939
940         if (element.previousSibling && msg.originatingCommand && element.previousSibling.command === msg.originatingCommand)
941             element.previousSibling.addStyleClass("console-adjacent-user-command-result");
942     },
943
944     _titleClicked: function(event)
945     {
946         var groupTitleElement = event.target.enclosingNodeOrSelfWithClass("console-group-title-level");
947         if (groupTitleElement) {
948             var groupElement = groupTitleElement.enclosingNodeOrSelfWithClass("console-group");
949             if (groupElement)
950                 if (groupElement.hasStyleClass("collapsed"))
951                     groupElement.removeStyleClass("collapsed");
952                 else
953                     groupElement.addStyleClass("collapsed");
954             groupTitleElement.scrollIntoViewIfNeeded(true);
955         }
956
957         event.stopPropagation();
958         event.preventDefault();
959     }
960 }