e4de88bb898d233ea7000d9b182ae3b497a932dd
[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                 WebInspector.panels.resources.addMessageToResource(msg.resource, msg);
184             }
185
186             this.commandSincePreviousMessage = false;
187             this.previousMessage = msg;
188
189             if (messageRepeated)
190                 return;
191         } else if (msg instanceof WebInspector.ConsoleCommand) {
192             if (this.previousMessage) {
193                 this.commandSincePreviousMessage = true;
194                 this.repeatCountBeforeCommand = this.previousMessage.totalRepeatCount;
195             }
196         }
197
198         this.messages.push(msg);
199
200         if (msg.level === WebInspector.ConsoleMessage.MessageLevel.EndGroup) {
201             if (this.groupLevel < 1)
202                 return;
203
204             this.groupLevel--;
205
206             this.currentGroup = this.currentGroup.parentGroup;
207         } else {
208             if (msg.level === WebInspector.ConsoleMessage.MessageLevel.StartGroup) {
209                 this.groupLevel++;
210
211                 var group = new WebInspector.ConsoleGroup(this.currentGroup, this.groupLevel);
212                 this.currentGroup.messagesElement.appendChild(group.element);
213                 this.currentGroup = group;
214             }
215
216             this.currentGroup.addMessage(msg);
217         }
218
219         this.promptElement.scrollIntoView(false);
220     },
221
222     clearMessages: function(clearInspectorController)
223     {
224         if (clearInspectorController)
225             InspectorController.clearMessages();
226         WebInspector.panels.resources.clearMessages();
227
228         this.messages = [];
229
230         this.groupLevel = 0;
231         this.currentGroup = this.topGroup;
232         this.topGroup.messagesElement.removeChildren();
233
234         WebInspector.errors = 0;
235         WebInspector.warnings = 0;
236
237         delete this.commandSincePreviousMessage;
238         delete this.repeatCountBeforeCommand;
239         delete this.previousMessage;
240     },
241
242     completions: function(wordRange, bestMatchOnly)
243     {
244         // Pass less stop characters to rangeOfWord so the range will be a more complete expression.
245         const expressionStopCharacters = " =:{;";
246         var expressionRange = wordRange.startContainer.rangeOfWord(wordRange.startOffset, expressionStopCharacters, this.promptElement, "backward");
247         var expressionString = expressionRange.toString();
248         var lastIndex = expressionString.length - 1;
249
250         var dotNotation = (expressionString[lastIndex] === ".");
251         var bracketNotation = (expressionString[lastIndex] === "[");
252
253         if (dotNotation || bracketNotation)
254             expressionString = expressionString.substr(0, lastIndex);
255
256         var prefix = wordRange.toString();
257         if (!expressionString && !prefix)
258             return;
259
260         var result;
261         if (expressionString) {
262             try {
263                 result = this._evalInInspectedWindow(expressionString);
264             } catch(e) {
265                 // Do nothing, the prefix will be considered a window property.
266             }
267         } else {
268             // There is no expressionString, so the completion should happen against global properties.
269             // Or if the debugger is paused, against properties in scope of the selected call frame.
270             if (WebInspector.panels.scripts.paused)
271                 result = WebInspector.panels.scripts.variablesInScopeForSelectedCallFrame();
272             else
273                 result = InspectorController.inspectedWindow();
274         }
275
276         if (bracketNotation) {
277             if (prefix.length && prefix[0] === "'")
278                 var quoteUsed = "'";
279             else
280                 var quoteUsed = "\"";
281         }
282
283         var results = [];
284         var properties = Object.sortedProperties(result);
285         for (var i = 0; i < properties.length; ++i) {
286             var property = properties[i];
287
288             if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property))
289                 continue;
290
291             if (bracketNotation) {
292                 if (!/^[0-9]+$/.test(property))
293                     property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + quoteUsed;
294                 property += "]";
295             }
296
297             if (property.length < prefix.length)
298                 continue;
299             if (property.indexOf(prefix) !== 0)
300                 continue;
301
302             results.push(property);
303             if (bestMatchOnly)
304                 break;
305         }
306
307         return results;
308     },
309
310     _toggleButtonClicked: function()
311     {
312         this.visible = !this.visible;
313     },
314
315     _clearButtonClicked: function()
316     {
317         this.clearMessages(true);
318     },
319
320     _messagesSelectStart: function(event)
321     {
322         if (this._selectionTimeout)
323             clearTimeout(this._selectionTimeout);
324
325         this.prompt.clearAutoComplete();
326
327         function moveBackIfOutside()
328         {
329             delete this._selectionTimeout;
330             if (!this.prompt.isCaretInsidePrompt() && window.getSelection().isCollapsed)
331                 this.prompt.moveCaretToEndOfPrompt();
332             this.prompt.autoCompleteSoon();
333         }
334
335         this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
336     },
337
338     _messagesClicked: function(event)
339     {
340         var link = event.target.enclosingNodeOrSelfWithNodeName("a");
341         if (!link || !link.representedNode)
342             return;
343
344         WebInspector.updateFocusedNode(link.representedNode);
345         event.stopPropagation();
346         event.preventDefault();
347     },
348
349     _promptKeyDown: function(event)
350     {
351         switch (event.keyIdentifier) {
352             case "Enter":
353                 this._enterKeyPressed(event);
354                 return;
355         }
356
357         this.prompt.handleKeyEvent(event);
358     },
359
360     _startStatusBarDragging: function(event)
361     {
362         if (!this.visible || event.target !== document.getElementById("main-status-bar"))
363             return;
364
365         WebInspector.elementDragStart(document.getElementById("main-status-bar"), this._statusBarDragging.bind(this), this._endStatusBarDragging.bind(this), event, "row-resize");
366
367         this._statusBarDragOffset = event.pageY - this.element.totalOffsetTop;
368
369         event.stopPropagation();
370     },
371
372     _statusBarDragging: function(event)
373     {
374         var mainElement = document.getElementById("main");
375
376         var height = window.innerHeight - event.pageY + this._statusBarDragOffset;
377         height = Number.constrain(height, Preferences.minConsoleHeight, window.innerHeight - mainElement.totalOffsetTop - Preferences.minConsoleHeight);
378
379         mainElement.style.bottom = height + "px";
380         this.element.style.height = height + "px";
381
382         event.preventDefault();
383         event.stopPropagation();
384     },
385
386     _endStatusBarDragging: function(event)
387     {
388         WebInspector.elementDragEnd(event);
389
390         delete this._statusBarDragOffset;
391
392         event.stopPropagation();
393     },
394
395     _evalInInspectedWindow: function(expression)
396     {
397         if (WebInspector.panels.scripts.paused)
398             return WebInspector.panels.scripts.evaluateInSelectedCallFrame(expression);
399
400         var inspectedWindow = InspectorController.inspectedWindow();
401         if (!inspectedWindow._inspectorCommandLineAPI) {
402             inspectedWindow.eval("window._inspectorCommandLineAPI = { \
403                 $: function() { return document.getElementById.apply(document, arguments) }, \
404                 $$: function() { return document.querySelectorAll.apply(document, arguments) }, \
405                 $x: function(xpath, context) { \
406                     var nodes = []; \
407                     try { \
408                         var doc = context || document; \
409                         var results = doc.evaluate(xpath, doc, null, XPathResult.ANY_TYPE, null); \
410                         var node; \
411                         while (node = results.iterateNext()) nodes.push(node); \
412                     } catch (e) {} \
413                     return nodes; \
414                 }, \
415                 dir: function() { return console.dir.apply(console, arguments) }, \
416                 dirxml: function() { return console.dirxml.apply(console, arguments) }, \
417                 keys: function(o) { var a = []; for (k in o) a.push(k); return a; }, \
418                 values: function(o) { var a = []; for (k in o) a.push(o[k]); return a; }, \
419                 profile: function() { return console.profile.apply(console, arguments) }, \
420                 profileEnd: function() { return console.profileEnd.apply(console, arguments) } \
421             };");
422
423             inspectedWindow._inspectorCommandLineAPI.clear = InspectorController.wrapCallback(this.clearMessages.bind(this));
424         }
425
426         // Surround the expression in with statements to inject our command line API so that
427         // the window object properties still take more precedent than our API functions.
428         expression = "with (window._inspectorCommandLineAPI) { with (window) { " + expression + " } }";
429
430         return inspectedWindow.eval(expression);
431     },
432
433     _enterKeyPressed: function(event)
434     {
435         if (event.altKey)
436             return;
437
438         event.preventDefault();
439         event.stopPropagation();
440
441         this.prompt.clearAutoComplete(true);
442
443         var str = this.prompt.text;
444         if (!str.length)
445             return;
446
447         var result;
448         var exception = false;
449         try {
450             result = this._evalInInspectedWindow(str);
451         } catch(e) {
452             result = e;
453             exception = true;
454         }
455
456         this.prompt.history.push(str);
457         this.prompt.historyOffset = 0;
458         this.prompt.text = "";
459
460         var level = exception ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log;
461         this.addMessage(new WebInspector.ConsoleCommand(str, result, this._format(result), level));
462     },
463
464     _mouseOverNode: function(event)
465     {
466         var anchorElement = event.target.enclosingNodeOrSelfWithNodeName("a");
467         WebInspector.hoveredDOMNode = (anchorElement ? anchorElement.representedNode : null);
468     },
469
470     _mouseOutOfNode: function(event)
471     {
472         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
473         var anchorElement = nodeUnderMouse.enclosingNodeOrSelfWithNodeName("a");
474         if (!anchorElement || !anchorElement.representedNode)
475             WebInspector.hoveredDOMNode = null;
476     },
477
478     _format: function(output)
479     {
480         var type = Object.type(output, InspectorController.inspectedWindow());
481         if (type === "object") {
482             if (output instanceof InspectorController.inspectedWindow().Node)
483                 type = "node";
484         }
485
486         // We don't perform any special formatting on these types, so we just
487         // pass them through the simple _formatvalue function.
488         var undecoratedTypes = {
489             "undefined": 1,
490             "null": 1,
491             "boolean": 1,
492             "number": 1,
493             "date": 1,
494             "function": 1,
495         };
496
497         var formatter;
498         if (type in undecoratedTypes)
499             formatter = "_formatvalue";
500         else {
501             formatter = "_format" + type;
502             if (!(formatter in this)) {
503                 formatter = "_formatobject";
504                 type = "object";
505             }
506         }
507
508         var span = document.createElement("span");
509         span.addStyleClass("console-formatted-" + type);
510         this[formatter](output, span);
511         return span;
512     },
513
514     _formatvalue: function(val, elem)
515     {
516         elem.appendChild(document.createTextNode(val));
517     },
518
519     _formatstring: function(str, elem)
520     {
521         elem.appendChild(document.createTextNode("\"" + str + "\""));
522     },
523
524     _formatregexp: function(re, elem)
525     {
526         var formatted = String(re).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1);
527         elem.appendChild(document.createTextNode(formatted));
528     },
529
530     _formatarray: function(arr, elem)
531     {
532         elem.appendChild(document.createTextNode("["));
533         for (var i = 0; i < arr.length; ++i) {
534             elem.appendChild(this._format(arr[i]));
535             if (i < arr.length - 1)
536                 elem.appendChild(document.createTextNode(", "));
537         }
538         elem.appendChild(document.createTextNode("]"));
539     },
540
541     _formatnode: function(node, elem)
542     {
543         var anchor = document.createElement("a");
544         anchor.className = "inspectible-node";
545         anchor.innerHTML = nodeTitleInfo.call(node).title;
546         anchor.representedNode = node;
547         anchor.addEventListener("mouseover", this._mouseOverNode.bind(this), false);
548         anchor.addEventListener("mouseout", this._mouseOutOfNode.bind(this), false);
549         elem.appendChild(anchor);
550     },
551
552     _formatobject: function(obj, elem)
553     {
554         elem.appendChild(document.createTextNode(Object.describe(obj)));
555     },
556
557     _formaterror: function(obj, elem)
558     {
559         elem.appendChild(document.createTextNode(obj.name + ": " + obj.message + " "));
560
561         if (obj.sourceURL) {
562             var urlElement = document.createElement("a");
563             urlElement.className = "console-message-url webkit-html-resource-link";
564             urlElement.href = obj.sourceURL;
565             urlElement.lineNumber = obj.line;
566             urlElement.preferredPanel = "scripts";
567
568             if (obj.line > 0)
569                 urlElement.textContent = WebInspector.UIString("%s (line %d)", obj.sourceURL, obj.line);
570             else
571                 urlElement.textContent = obj.sourceURL;
572
573             elem.appendChild(urlElement);
574         }
575     },
576 }
577
578 WebInspector.Console.prototype.__proto__ = WebInspector.View.prototype;
579
580 WebInspector.ConsoleMessage = function(source, level, line, url, groupLevel, repeatCount)
581 {
582     this.source = source;
583     this.level = level;
584     this.line = line;
585     this.url = url;
586     this.groupLevel = groupLevel;
587     this.repeatCount = repeatCount;
588
589     switch (this.level) {
590         case WebInspector.ConsoleMessage.MessageLevel.Object:
591             var propertiesSection = new WebInspector.ObjectPropertiesSection(arguments[6], null, null, null, true);
592             propertiesSection.element.addStyleClass("console-message");
593             this.propertiesSection = propertiesSection;
594             break;
595         case WebInspector.ConsoleMessage.MessageLevel.Node:
596             var node = arguments[6];
597             if (!(node instanceof InspectorController.inspectedWindow().Node))
598                 return;
599             this.elementsTreeOutline = new WebInspector.ElementsTreeOutline();
600             this.elementsTreeOutline.rootDOMNode = node;
601             break;
602         case WebInspector.ConsoleMessage.MessageLevel.Trace:
603             var span = document.createElement("span");
604             span.addStyleClass("console-formatted-trace");
605             var stack = Array.prototype.slice.call(arguments, 6);
606             var funcNames = stack.map(function(f) {
607                 return f.name || WebInspector.UIString("(anonymous function)");
608             });
609             span.appendChild(document.createTextNode(funcNames.join("\n")));
610             this.formattedMessage = span;
611             break;
612         default:
613             // The formatedMessage property is used for the rich and interactive console.
614             this.formattedMessage = this._format(Array.prototype.slice.call(arguments, 6));
615
616             // This is used for inline message bubbles in SourceFrames, or other plain-text representations.
617             this.message = this.formattedMessage.textContent;
618             break;
619     }
620 }
621
622 WebInspector.ConsoleMessage.prototype = {
623     isErrorOrWarning: function()
624     {
625         return (this.level === WebInspector.ConsoleMessage.MessageLevel.Warning || this.level === WebInspector.ConsoleMessage.MessageLevel.Error);
626     },
627
628     _format: function(parameters)
629     {
630         var formattedResult = document.createElement("span");
631
632         if (!parameters.length)
633             return formattedResult;
634
635         function formatForConsole(obj)
636         {
637             return WebInspector.console._format(obj);
638         }
639
640         if (Object.type(parameters[0], InspectorController.inspectedWindow()) === "string") {
641             var formatters = {}
642             for (var i in String.standardFormatters)
643                 formatters[i] = String.standardFormatters[i];
644
645             // Firebug uses %o for formatting objects.
646             formatters.o = formatForConsole;
647             // Firebug allows both %i and %d for formatting integers.
648             formatters.i = formatters.d;
649
650             function append(a, b)
651             {
652                 if (!(b instanceof Node))
653                     a.appendChild(WebInspector.linkifyStringAsFragment(b.toString()));
654                 else
655                     a.appendChild(b);
656                 return a;
657             }
658
659             var result = String.format(parameters[0], parameters.slice(1), formatters, formattedResult, append);
660             formattedResult = result.formattedResult;
661             parameters = result.unusedSubstitutions;
662             if (parameters.length)
663                 formattedResult.appendChild(document.createTextNode(" "));
664         }
665
666         for (var i = 0; i < parameters.length; ++i) {
667             if (typeof parameters[i] === "string")
668                 formattedResult.appendChild(WebInspector.linkifyStringAsFragment(parameters[i]));
669             else
670                 formattedResult.appendChild(formatForConsole(parameters[i]));
671             if (i < parameters.length - 1)
672                 formattedResult.appendChild(document.createTextNode(" "));
673         }
674
675         return formattedResult;
676     },
677
678     toMessageElement: function()
679     {
680         if (this.propertiesSection)
681             return this.propertiesSection.element;
682
683         var element = document.createElement("div");
684         element.message = this;
685         element.className = "console-message";
686
687         switch (this.source) {
688             case WebInspector.ConsoleMessage.MessageSource.HTML:
689                 element.addStyleClass("console-html-source");
690                 break;
691             case WebInspector.ConsoleMessage.MessageSource.XML:
692                 element.addStyleClass("console-xml-source");
693                 break;
694             case WebInspector.ConsoleMessage.MessageSource.JS:
695                 element.addStyleClass("console-js-source");
696                 break;
697             case WebInspector.ConsoleMessage.MessageSource.CSS:
698                 element.addStyleClass("console-css-source");
699                 break;
700             case WebInspector.ConsoleMessage.MessageSource.Other:
701                 element.addStyleClass("console-other-source");
702                 break;
703         }
704
705         switch (this.level) {
706             case WebInspector.ConsoleMessage.MessageLevel.Tip:
707                 element.addStyleClass("console-tip-level");
708                 break;
709             case WebInspector.ConsoleMessage.MessageLevel.Log:
710                 element.addStyleClass("console-log-level");
711                 break;
712             case WebInspector.ConsoleMessage.MessageLevel.Warning:
713                 element.addStyleClass("console-warning-level");
714                 break;
715             case WebInspector.ConsoleMessage.MessageLevel.Error:
716                 element.addStyleClass("console-error-level");
717                 break;
718             case WebInspector.ConsoleMessage.MessageLevel.StartGroup:
719                 element.addStyleClass("console-group-title-level");
720         }
721
722         if (this.elementsTreeOutline) {
723             element.addStyleClass("outline-disclosure");
724             element.appendChild(this.elementsTreeOutline.element);
725             return element;
726         }
727
728         if (this.repeatCount > 1) {
729             var messageRepeatCountElement = document.createElement("span");
730             messageRepeatCountElement.className = "bubble";
731             messageRepeatCountElement.textContent = this.repeatCount;
732
733             element.appendChild(messageRepeatCountElement);
734             element.addStyleClass("repeated-message");
735         }
736
737         if (this.url && this.url !== "undefined") {
738             var urlElement = document.createElement("a");
739             urlElement.className = "console-message-url webkit-html-resource-link";
740             urlElement.href = this.url;
741             urlElement.lineNumber = this.line;
742
743             if (this.source === WebInspector.ConsoleMessage.MessageSource.JS)
744                 urlElement.preferredPanel = "scripts";
745
746             if (this.line > 0)
747                 urlElement.textContent = WebInspector.UIString("%s (line %d)", WebInspector.displayNameForURL(this.url), this.line);
748             else
749                 urlElement.textContent = WebInspector.displayNameForURL(this.url);
750
751             element.appendChild(urlElement);
752         }
753
754         var messageTextElement = document.createElement("span");
755         messageTextElement.className = "console-message-text";
756         messageTextElement.appendChild(this.formattedMessage);
757         element.appendChild(messageTextElement);
758
759         return element;
760     },
761
762     toString: function()
763     {
764         var sourceString;
765         switch (this.source) {
766             case WebInspector.ConsoleMessage.MessageSource.HTML:
767                 sourceString = "HTML";
768                 break;
769             case WebInspector.ConsoleMessage.MessageSource.XML:
770                 sourceString = "XML";
771                 break;
772             case WebInspector.ConsoleMessage.MessageSource.JS:
773                 sourceString = "JS";
774                 break;
775             case WebInspector.ConsoleMessage.MessageSource.CSS:
776                 sourceString = "CSS";
777                 break;
778             case WebInspector.ConsoleMessage.MessageSource.Other:
779                 sourceString = "Other";
780                 break;
781         }
782
783         var levelString;
784         switch (this.level) {
785             case WebInspector.ConsoleMessage.MessageLevel.Tip:
786                 levelString = "Tip";
787                 break;
788             case WebInspector.ConsoleMessage.MessageLevel.Log:
789                 levelString = "Log";
790                 break;
791             case WebInspector.ConsoleMessage.MessageLevel.Warning:
792                 levelString = "Warning";
793                 break;
794             case WebInspector.ConsoleMessage.MessageLevel.Error:
795                 levelString = "Error";
796                 break;
797             case WebInspector.ConsoleMessage.MessageLevel.Object:
798                 levelString = "Object";
799                 break;
800             case WebInspector.ConsoleMessage.MessageLevel.GroupTitle:
801                 levelString = "GroupTitle";
802                 break;
803         }
804
805         return sourceString + " " + levelString + ": " + this.formattedMessage.textContent + "\n" + this.url + " line " + this.line;
806     },
807     
808     isEqual: function(msg, disreguardGroup)
809     {
810         if (!msg)
811             return false;
812
813         var ret = (this.source == msg.source)
814             && (this.level == msg.level)
815             && (this.line == msg.line)
816             && (this.url == msg.url)
817             && (this.message == msg.message);
818
819         return (disreguardGroup ? ret : (ret && (this.groupLevel == msg.groupLevel)));
820     }
821 }
822
823 // Note: Keep these constants in sync with the ones in Chrome.h
824 WebInspector.ConsoleMessage.MessageSource = {
825     HTML: 0,
826     XML: 1,
827     JS: 2,
828     CSS: 3,
829     Other: 4,
830 }
831
832 WebInspector.ConsoleMessage.MessageLevel = {
833     Tip: 0,
834     Log: 1,
835     Warning: 2,
836     Error: 3,
837     Object: 4,
838     Node: 5,
839     Trace: 6,
840     StartGroup: 7,
841     EndGroup: 8
842 }
843
844 WebInspector.ConsoleCommand = function(command, result, formattedResultElement, level)
845 {
846     this.command = command;
847     this.formattedResultElement = formattedResultElement;
848     this.level = level;
849 }
850
851 WebInspector.ConsoleCommand.prototype = {
852     toMessageElement: function()
853     {
854         var element = document.createElement("div");
855         element.command = this;
856         element.className = "console-user-command";
857
858         var commandTextElement = document.createElement("span");
859         commandTextElement.className = "console-message-text";
860         commandTextElement.textContent = this.command;
861         element.appendChild(commandTextElement);
862
863         var resultElement = document.createElement("div");
864         resultElement.className = "console-message";
865         element.appendChild(resultElement);
866
867         switch (this.level) {
868             case WebInspector.ConsoleMessage.MessageLevel.Log:
869                 resultElement.addStyleClass("console-log-level");
870                 break;
871             case WebInspector.ConsoleMessage.MessageLevel.Warning:
872                 resultElement.addStyleClass("console-warning-level");
873                 break;
874             case WebInspector.ConsoleMessage.MessageLevel.Error:
875                 resultElement.addStyleClass("console-error-level");
876         }
877
878         var resultTextElement = document.createElement("span");
879         resultTextElement.className = "console-message-text";
880         resultTextElement.appendChild(this.formattedResultElement);
881         resultElement.appendChild(resultTextElement);
882
883         return element;
884     }
885 }
886
887 WebInspector.ConsoleGroup = function(parentGroup, level)
888 {
889     this.parentGroup = parentGroup;
890     this.level = level;
891
892     var element = document.createElement("div");
893     element.className = "console-group";
894     element.group = this;
895     this.element = element;
896
897     var messagesElement = document.createElement("div");
898     messagesElement.className = "console-group-messages";
899     element.appendChild(messagesElement);
900     this.messagesElement = messagesElement;
901 }
902
903 WebInspector.ConsoleGroup.prototype = {
904     addMessage: function(msg)
905     {
906         var element = msg.toMessageElement();
907         
908         if (msg.level === WebInspector.ConsoleMessage.MessageLevel.StartGroup) {
909             this.messagesElement.parentNode.insertBefore(element, this.messagesElement);
910             element.addEventListener("click", this._titleClicked.bind(this), true);
911         } else
912             this.messagesElement.appendChild(element);
913     },
914     
915     _titleClicked: function(event)
916     {
917         var groupTitleElement = event.target.enclosingNodeOrSelfWithClass("console-group-title-level");
918         if (groupTitleElement) {
919             var groupElement = groupTitleElement.enclosingNodeOrSelfWithClass("console-group");
920             if (groupElement)
921                 if (groupElement.hasStyleClass("collapsed"))
922                     groupElement.removeStyleClass("collapsed");
923                 else
924                     groupElement.addStyleClass("collapsed");
925             groupTitleElement.scrollIntoViewIfNeeded(true);
926         }
927
928         event.stopPropagation();
929         event.preventDefault();
930     }
931 }