1/*
2 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3 * Copyright (C) 2009 Joseph Pecoraro
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1.  Redistributions of source code must retain the above copyright
10 *     notice, this list of conditions and the following disclaimer.
11 * 2.  Redistributions in binary form must reproduce the above copyright
12 *     notice, this list of conditions and the following disclaimer in the
13 *     documentation and/or other materials provided with the distribution.
14 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 *     its contributors may be used to endorse or promote products derived
16 *     from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30const ExpressionStopCharacters = " =:[({;,!+-*/&|^<>";
31
32WebInspector.ConsoleView = function(drawer)
33{
34    WebInspector.View.call(this, document.getElementById("console-view"));
35
36    this.messages = [];
37    this.drawer = drawer;
38
39    this.clearButton = document.getElementById("clear-console-status-bar-item");
40    this.clearButton.title = WebInspector.UIString("Clear console log.");
41    this.clearButton.addEventListener("click", this._clearButtonClicked.bind(this), false);
42
43    this.messagesElement = document.getElementById("console-messages");
44    this.messagesElement.addEventListener("selectstart", this._messagesSelectStart.bind(this), false);
45    this.messagesElement.addEventListener("click", this._messagesClicked.bind(this), true);
46
47    this.promptElement = document.getElementById("console-prompt");
48    this.promptElement.className = "source-code";
49    this.promptElement.addEventListener("keydown", this._promptKeyDown.bind(this), true);
50    this.prompt = new WebInspector.TextPrompt(this.promptElement, this.completions.bind(this), ExpressionStopCharacters + ".");
51    this.prompt.history = WebInspector.settings.consoleHistory;
52
53    this.topGroup = new WebInspector.ConsoleGroup(null);
54    this.messagesElement.insertBefore(this.topGroup.element, this.promptElement);
55    this.currentGroup = this.topGroup;
56
57    this.toggleConsoleButton = document.getElementById("console-status-bar-item");
58    this.toggleConsoleButton.title = WebInspector.UIString("Show console.");
59    this.toggleConsoleButton.addEventListener("click", this._toggleConsoleButtonClicked.bind(this), false);
60
61    // Will hold the list of filter elements
62    this.filterBarElement = document.getElementById("console-filter");
63
64    function createDividerElement() {
65        var dividerElement = document.createElement("div");
66        dividerElement.addStyleClass("scope-bar-divider");
67        this.filterBarElement.appendChild(dividerElement);
68    }
69
70    var updateFilterHandler = this._updateFilter.bind(this);
71    function createFilterElement(category, label) {
72        var categoryElement = document.createElement("li");
73        categoryElement.category = category;
74        categoryElement.className = category;
75        categoryElement.addEventListener("click", updateFilterHandler, false);
76        categoryElement.textContent = label;
77
78        this.filterBarElement.appendChild(categoryElement);
79
80        return categoryElement;
81    }
82
83    this.allElement = createFilterElement.call(this, "all", WebInspector.UIString("All"));
84    createDividerElement.call(this);
85    this.errorElement = createFilterElement.call(this, "errors", WebInspector.UIString("Errors"));
86    this.warningElement = createFilterElement.call(this, "warnings", WebInspector.UIString("Warnings"));
87    this.logElement = createFilterElement.call(this, "logs", WebInspector.UIString("Logs"));
88
89    this.filter(this.allElement, false);
90    this._registerShortcuts();
91
92    this.messagesElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
93
94    this._customFormatters = {
95        "object": this._formatobject,
96        "array":  this._formatarray,
97        "node":   this._formatnode,
98        "string": this._formatstring
99    };
100
101    this._registerConsoleDomainDispatcher();
102}
103
104WebInspector.ConsoleView.prototype = {
105    _registerConsoleDomainDispatcher: function() {
106        var console = this;
107        var dispatcher = {
108            messageAdded: function(payload)
109            {
110                var consoleMessage = new WebInspector.ConsoleMessage(
111                    payload.source,
112                    payload.type,
113                    payload.level,
114                    payload.line,
115                    payload.url,
116                    payload.repeatCount,
117                    payload.text,
118                    payload.parameters,
119                    payload.stackTrace,
120                    payload.networkIdentifier);
121                console.addMessage(consoleMessage);
122            },
123
124            messageRepeatCountUpdated: function(count)
125            {
126                var msg = console.previousMessage;
127                var prevRepeatCount = msg.totalRepeatCount;
128
129                if (!console.commandSincePreviousMessage) {
130                    msg.repeatDelta = count - prevRepeatCount;
131                    msg.repeatCount = msg.repeatCount + msg.repeatDelta;
132                    msg.totalRepeatCount = count;
133                    msg._updateRepeatCount();
134                    console._incrementErrorWarningCount(msg);
135                } else {
136                    var msgCopy = new WebInspector.ConsoleMessage(msg.source, msg.type, msg.level, msg.line, msg.url, count - prevRepeatCount, msg._messageText, msg._parameters, msg._stackTrace, msg._requestId);
137                    msgCopy.totalRepeatCount = count;
138                    msgCopy._formatMessage();
139                    console.addMessage(msgCopy);
140                }
141            },
142
143            messagesCleared: function()
144            {
145                console.clearMessages();
146            },
147        }
148        InspectorBackend.registerDomainDispatcher("Console", dispatcher);
149    },
150
151    setConsoleMessageExpiredCount: function(count)
152    {
153        if (count) {
154            var message = String.sprintf(WebInspector.UIString("%d console messages are not shown."), count);
155            this.addMessage(WebInspector.ConsoleMessage.createTextMessage(message, WebInspector.ConsoleMessage.MessageLevel.Warning));
156        }
157    },
158
159    _updateFilter: function(e)
160    {
161        var isMac = WebInspector.isMac();
162        var selectMultiple = false;
163        if (isMac && e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey)
164            selectMultiple = true;
165        if (!isMac && e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey)
166            selectMultiple = true;
167
168        this.filter(e.target, selectMultiple);
169    },
170
171    filter: function(target, selectMultiple)
172    {
173        function unselectAll()
174        {
175            this.allElement.removeStyleClass("selected");
176            this.errorElement.removeStyleClass("selected");
177            this.warningElement.removeStyleClass("selected");
178            this.logElement.removeStyleClass("selected");
179
180            this.messagesElement.removeStyleClass("filter-all");
181            this.messagesElement.removeStyleClass("filter-errors");
182            this.messagesElement.removeStyleClass("filter-warnings");
183            this.messagesElement.removeStyleClass("filter-logs");
184        }
185
186        var targetFilterClass = "filter-" + target.category;
187
188        if (target.category === "all") {
189            if (target.hasStyleClass("selected")) {
190                // We can't unselect all, so we break early here
191                return;
192            }
193
194            unselectAll.call(this);
195        } else {
196            // Something other than all is being selected, so we want to unselect all
197            if (this.allElement.hasStyleClass("selected")) {
198                this.allElement.removeStyleClass("selected");
199                this.messagesElement.removeStyleClass("filter-all");
200            }
201        }
202
203        if (!selectMultiple) {
204            // If multiple selection is off, we want to unselect everything else
205            // and just select ourselves.
206            unselectAll.call(this);
207
208            target.addStyleClass("selected");
209            this.messagesElement.addStyleClass(targetFilterClass);
210
211            return;
212        }
213
214        if (target.hasStyleClass("selected")) {
215            // If selectMultiple is turned on, and we were selected, we just
216            // want to unselect ourselves.
217            target.removeStyleClass("selected");
218            this.messagesElement.removeStyleClass(targetFilterClass);
219        } else {
220            // If selectMultiple is turned on, and we weren't selected, we just
221            // want to select ourselves.
222            target.addStyleClass("selected");
223            this.messagesElement.addStyleClass(targetFilterClass);
224        }
225    },
226
227    _toggleConsoleButtonClicked: function()
228    {
229        this.drawer.visibleView = this;
230    },
231
232    attach: function(mainElement, statusBarElement)
233    {
234        mainElement.appendChild(this.element);
235        statusBarElement.appendChild(this.clearButton);
236        statusBarElement.appendChild(this.filterBarElement);
237    },
238
239    show: function()
240    {
241        this.toggleConsoleButton.addStyleClass("toggled-on");
242        this.toggleConsoleButton.title = WebInspector.UIString("Hide console.");
243        if (!this.prompt.isCaretInsidePrompt())
244            this.prompt.moveCaretToEndOfPrompt();
245    },
246
247    afterShow: function()
248    {
249        WebInspector.currentFocusElement = this.promptElement;
250    },
251
252    hide: function()
253    {
254        this.toggleConsoleButton.removeStyleClass("toggled-on");
255        this.toggleConsoleButton.title = WebInspector.UIString("Show console.");
256    },
257
258    _scheduleScrollIntoView: function()
259    {
260        if (this._scrollIntoViewTimer)
261            return;
262
263        function scrollIntoView()
264        {
265            this.promptElement.scrollIntoView(true);
266            delete this._scrollIntoViewTimer;
267        }
268        this._scrollIntoViewTimer = setTimeout(scrollIntoView.bind(this), 20);
269    },
270
271    addMessage: function(msg)
272    {
273        var shouldScrollToLastMessage = this.messagesElement.isScrolledToBottom();
274
275        if (msg instanceof WebInspector.ConsoleMessage && !(msg instanceof WebInspector.ConsoleCommandResult)) {
276            this._incrementErrorWarningCount(msg);
277            WebInspector.resourceTreeModel.addConsoleMessage(msg);
278            WebInspector.panels.scripts.addConsoleMessage(msg);
279            this.commandSincePreviousMessage = false;
280            this.previousMessage = msg;
281        } else if (msg instanceof WebInspector.ConsoleCommand) {
282            if (this.previousMessage) {
283                this.commandSincePreviousMessage = true;
284            }
285        }
286
287        this.messages.push(msg);
288
289        if (msg.type === WebInspector.ConsoleMessage.MessageType.EndGroup) {
290            var parentGroup = this.currentGroup.parentGroup
291            if (parentGroup)
292                this.currentGroup = parentGroup;
293        } else {
294            if (msg.type === WebInspector.ConsoleMessage.MessageType.StartGroup || msg.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed) {
295                var group = new WebInspector.ConsoleGroup(this.currentGroup);
296                this.currentGroup.messagesElement.appendChild(group.element);
297                this.currentGroup = group;
298            }
299
300            this.currentGroup.addMessage(msg);
301        }
302
303        // Always scroll when command result arrives.
304        if (shouldScrollToLastMessage || (msg instanceof WebInspector.ConsoleCommandResult))
305            this._scheduleScrollIntoView();
306    },
307
308    _incrementErrorWarningCount: function(msg)
309    {
310        switch (msg.level) {
311            case WebInspector.ConsoleMessage.MessageLevel.Warning:
312                WebInspector.warnings += msg.repeatDelta;
313                break;
314            case WebInspector.ConsoleMessage.MessageLevel.Error:
315                WebInspector.errors += msg.repeatDelta;
316                break;
317        }
318    },
319
320    requestClearMessages: function()
321    {
322        ConsoleAgent.clearConsoleMessages();
323    },
324
325    clearMessages: function()
326    {
327        WebInspector.resourceTreeModel.clearConsoleMessages();
328        WebInspector.panels.scripts.clearConsoleMessages();
329
330        this.messages = [];
331
332        this.currentGroup = this.topGroup;
333        this.topGroup.messagesElement.removeChildren();
334
335        WebInspector.errors = 0;
336        WebInspector.warnings = 0;
337
338        delete this.commandSincePreviousMessage;
339        delete this.previousMessage;
340    },
341
342    completions: function(wordRange, bestMatchOnly, completionsReadyCallback)
343    {
344        // Pass less stop characters to rangeOfWord so the range will be a more complete expression.
345        var expressionRange = wordRange.startContainer.rangeOfWord(wordRange.startOffset, ExpressionStopCharacters, this.promptElement, "backward");
346        var expressionString = expressionRange.toString();
347        var prefix = wordRange.toString();
348        this._completions(expressionString, prefix, bestMatchOnly, completionsReadyCallback);
349    },
350
351    _completions: function(expressionString, prefix, bestMatchOnly, completionsReadyCallback)
352    {
353        var lastIndex = expressionString.length - 1;
354
355        var dotNotation = (expressionString[lastIndex] === ".");
356        var bracketNotation = (expressionString[lastIndex] === "[");
357
358        if (dotNotation || bracketNotation)
359            expressionString = expressionString.substr(0, lastIndex);
360
361        if (!expressionString && !prefix)
362            return;
363
364        if (!expressionString && WebInspector.panels.scripts.paused)
365            WebInspector.panels.scripts.getSelectedCallFrameVariables(reportCompletions.bind(this));
366        else
367            this.evalInInspectedWindow(expressionString, "completion", true, evaluated.bind(this));
368
369        function evaluated(result)
370        {
371            if (!result)
372                return;
373            result.getAllProperties(evaluatedProperties.bind(this));
374        }
375
376        function evaluatedProperties(properties)
377        {
378            RuntimeAgent.releaseObjectGroup("completion");
379            var propertyNames = {};
380            for (var i = 0; properties && i < properties.length; ++i)
381                propertyNames[properties[i].name] = true;
382            reportCompletions.call(this, propertyNames);
383        }
384
385        function reportCompletions(propertyNames)
386        {
387            var includeCommandLineAPI = (!dotNotation && !bracketNotation);
388            if (includeCommandLineAPI) {
389                const commandLineAPI = ["dir", "dirxml", "keys", "values", "profile", "profileEnd", "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear"];
390                for (var i = 0; i < commandLineAPI.length; ++i)
391                    propertyNames[commandLineAPI[i]] = true;
392            }
393
394            this._reportCompletions(bestMatchOnly, completionsReadyCallback, dotNotation, bracketNotation, prefix, Object.keys(propertyNames));
395        }
396    },
397
398    _reportCompletions: function(bestMatchOnly, completionsReadyCallback, dotNotation, bracketNotation, prefix, properties) {
399        if (bracketNotation) {
400            if (prefix.length && prefix[0] === "'")
401                var quoteUsed = "'";
402            else
403                var quoteUsed = "\"";
404        }
405
406        var results = [];
407        properties.sort();
408
409        for (var i = 0; i < properties.length; ++i) {
410            var property = properties[i];
411
412            if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property))
413                continue;
414
415            if (bracketNotation) {
416                if (!/^[0-9]+$/.test(property))
417                    property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + quoteUsed;
418                property += "]";
419            }
420
421            if (property.length < prefix.length)
422                continue;
423            if (property.indexOf(prefix) !== 0)
424                continue;
425
426            results.push(property);
427            if (bestMatchOnly)
428                break;
429        }
430        completionsReadyCallback(results);
431    },
432
433    _clearButtonClicked: function()
434    {
435        this.requestClearMessages();
436    },
437
438    _handleContextMenuEvent: function(event)
439    {
440        if (!window.getSelection().isCollapsed) {
441            // If there is a selection, we want to show our normal context menu
442            // (with Copy, etc.), and not Clear Console.
443            return;
444        }
445
446        var itemAction = function () {
447            WebInspector.settings.monitoringXHREnabled = !WebInspector.settings.monitoringXHREnabled;
448            ConsoleAgent.setMonitoringXHREnabled(WebInspector.settings.monitoringXHREnabled);
449        }.bind(this);
450        var contextMenu = new WebInspector.ContextMenu();
451        contextMenu.appendCheckboxItem(WebInspector.UIString("XMLHttpRequest logging"), itemAction, WebInspector.settings.monitoringXHREnabled)
452        contextMenu.appendItem(WebInspector.UIString("Clear Console"), this.requestClearMessages.bind(this));
453        contextMenu.show(event);
454    },
455
456    _messagesSelectStart: function(event)
457    {
458        if (this._selectionTimeout)
459            clearTimeout(this._selectionTimeout);
460
461        this.prompt.clearAutoComplete();
462
463        function moveBackIfOutside()
464        {
465            delete this._selectionTimeout;
466            if (!this.prompt.isCaretInsidePrompt() && window.getSelection().isCollapsed)
467                this.prompt.moveCaretToEndOfPrompt();
468            this.prompt.autoCompleteSoon();
469        }
470
471        this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
472    },
473
474    _messagesClicked: function(event)
475    {
476        var link = event.target.enclosingNodeOrSelfWithNodeName("a");
477        if (!link || !link.representedNode)
478            return;
479
480        WebInspector.updateFocusedNode(link.representedNode.id);
481        event.stopPropagation();
482        event.preventDefault();
483    },
484
485    _registerShortcuts: function()
486    {
487        this._shortcuts = {};
488
489        var shortcut = WebInspector.KeyboardShortcut;
490        var shortcutK = shortcut.makeDescriptor("k", WebInspector.KeyboardShortcut.Modifiers.Meta);
491        // This case requires a separate bound function as its isMacOnly property should not be shared among different shortcut handlers.
492        this._shortcuts[shortcutK.key] = this.requestClearMessages.bind(this);
493        this._shortcuts[shortcutK.key].isMacOnly = true;
494
495        var clearConsoleHandler = this.requestClearMessages.bind(this);
496        var shortcutL = shortcut.makeDescriptor("l", WebInspector.KeyboardShortcut.Modifiers.Ctrl);
497        this._shortcuts[shortcutL.key] = clearConsoleHandler;
498
499        var section = WebInspector.shortcutsHelp.section(WebInspector.UIString("Console"));
500        var keys = WebInspector.isMac() ? [ shortcutK.name, shortcutL.name ] : [ shortcutL.name ];
501        section.addAlternateKeys(keys, WebInspector.UIString("Clear Console"));
502
503        keys = [
504            shortcut.shortcutToString(shortcut.Keys.Tab),
505            shortcut.shortcutToString(shortcut.Keys.Tab, shortcut.Modifiers.Shift)
506        ];
507        section.addRelatedKeys(keys, WebInspector.UIString("Next/previous suggestion"));
508        section.addKey(shortcut.shortcutToString(shortcut.Keys.Right), WebInspector.UIString("Accept suggestion"));
509        keys = [
510            shortcut.shortcutToString(shortcut.Keys.Down),
511            shortcut.shortcutToString(shortcut.Keys.Up)
512        ];
513        section.addRelatedKeys(keys, WebInspector.UIString("Next/previous line"));
514        keys = [
515            shortcut.shortcutToString("N", shortcut.Modifiers.Alt),
516            shortcut.shortcutToString("P", shortcut.Modifiers.Alt)
517        ];
518        if (WebInspector.isMac())
519            section.addRelatedKeys(keys, WebInspector.UIString("Next/previous command"));
520        section.addKey(shortcut.shortcutToString(shortcut.Keys.Enter), WebInspector.UIString("Execute command"));
521    },
522
523    _promptKeyDown: function(event)
524    {
525        if (isEnterKey(event)) {
526            this._enterKeyPressed(event);
527            return;
528        }
529
530        var shortcut = WebInspector.KeyboardShortcut.makeKeyFromEvent(event);
531        var handler = this._shortcuts[shortcut];
532        if (handler) {
533            if (!this._shortcuts[shortcut].isMacOnly || WebInspector.isMac()) {
534                handler();
535                event.preventDefault();
536                return;
537            }
538        }
539    },
540
541    evalInInspectedWindow: function(expression, objectGroup, includeCommandLineAPI, callback)
542    {
543        if (WebInspector.panels.scripts && WebInspector.panels.scripts.paused) {
544            WebInspector.panels.scripts.evaluateInSelectedCallFrame(expression, objectGroup, includeCommandLineAPI, callback);
545            return;
546        }
547
548        if (!expression) {
549            // There is no expression, so the completion should happen against global properties.
550            expression = "this";
551        }
552
553        function evalCallback(error, result)
554        {
555            if (!error)
556                callback(WebInspector.RemoteObject.fromPayload(result));
557        }
558        RuntimeAgent.evaluate(expression, objectGroup, includeCommandLineAPI, evalCallback);
559    },
560
561    _enterKeyPressed: function(event)
562    {
563        if (event.altKey || event.ctrlKey || event.shiftKey)
564            return;
565
566        event.preventDefault();
567        event.stopPropagation();
568
569        this.prompt.clearAutoComplete(true);
570
571        var str = this.prompt.text;
572        if (!str.length)
573            return;
574
575        var commandMessage = new WebInspector.ConsoleCommand(str);
576        this.addMessage(commandMessage);
577
578        var self = this;
579        function printResult(result)
580        {
581            self.prompt.history.push(str);
582            self.prompt.historyOffset = 0;
583            self.prompt.text = "";
584
585            WebInspector.settings.consoleHistory = self.prompt.history.slice(-30);
586
587            self.addMessage(new WebInspector.ConsoleCommandResult(result, commandMessage));
588        }
589        this.evalInInspectedWindow(str, "console", true, printResult);
590    },
591
592    _format: function(output, forceObjectFormat)
593    {
594        var isProxy = (output != null && typeof output === "object");
595        var type = (forceObjectFormat ? "object" : WebInspector.RemoteObject.type(output));
596
597        var formatter = this._customFormatters[type];
598        if (!formatter || !isProxy) {
599            formatter = this._formatvalue;
600            output = output.description;
601        }
602
603        var span = document.createElement("span");
604        span.className = "console-formatted-" + type + " source-code";
605        formatter.call(this, output, span);
606        return span;
607    },
608
609    _formatvalue: function(val, elem)
610    {
611        elem.appendChild(document.createTextNode(val));
612    },
613
614    _formatobject: function(obj, elem)
615    {
616        elem.appendChild(new WebInspector.ObjectPropertiesSection(obj, obj.description, null, true).element);
617    },
618
619    _formatnode: function(object, elem)
620    {
621        function printNode(nodeId)
622        {
623            if (!nodeId) {
624                // Sometimes DOM is loaded after the sync message is being formatted, so we get no
625                // nodeId here. So we fall back to object formatting here.
626                this._formatobject(object, elem);
627                return;
628            }
629            var treeOutline = new WebInspector.ElementsTreeOutline();
630            treeOutline.showInElementsPanelEnabled = true;
631            treeOutline.rootDOMNode = WebInspector.domAgent.nodeForId(nodeId);
632            treeOutline.element.addStyleClass("outline-disclosure");
633            if (!treeOutline.children[0].hasChildren)
634                treeOutline.element.addStyleClass("single-node");
635            elem.appendChild(treeOutline.element);
636        }
637        object.pushNodeToFrontend(printNode.bind(this));
638    },
639
640    _formatarray: function(arr, elem)
641    {
642        arr.getOwnProperties(this._printArray.bind(this, elem));
643    },
644
645    _formatstring: function(output, elem)
646    {
647        var span = document.createElement("span");
648        span.className = "console-formatted-string source-code";
649        span.appendChild(WebInspector.linkifyStringAsFragment(output.description));
650
651        // Make black quotes.
652        elem.removeStyleClass("console-formatted-string");
653        elem.appendChild(document.createTextNode("\""));
654        elem.appendChild(span);
655        elem.appendChild(document.createTextNode("\""));
656    },
657
658    _printArray: function(elem, properties)
659    {
660        if (!properties)
661            return;
662
663        var elements = [];
664        for (var i = 0; i < properties.length; ++i) {
665            var name = properties[i].name;
666            if (name == parseInt(name))
667                elements[name] = this._formatAsArrayEntry(properties[i].value);
668        }
669
670        elem.appendChild(document.createTextNode("["));
671        for (var i = 0; i < elements.length; ++i) {
672            var element = elements[i];
673            if (element)
674                elem.appendChild(element);
675            else
676                elem.appendChild(document.createTextNode("undefined"))
677            if (i < elements.length - 1)
678                elem.appendChild(document.createTextNode(", "));
679        }
680        elem.appendChild(document.createTextNode("]"));
681    },
682
683    _formatAsArrayEntry: function(output)
684    {
685        // Prevent infinite expansion of cross-referencing arrays.
686        return this._format(output, WebInspector.RemoteObject.type(output) === "array");
687    }
688}
689
690WebInspector.ConsoleView.prototype.__proto__ = WebInspector.View.prototype;
691
692WebInspector.ConsoleMessage = function(source, type, level, line, url, repeatCount, message, parameters, stackTrace, requestId)
693{
694    this.source = source;
695    this.type = type;
696    this.level = level;
697    this.line = line;
698    this.url = url;
699    this.repeatCount = repeatCount;
700    this.repeatDelta = repeatCount;
701    this.totalRepeatCount = repeatCount;
702    this._messageText = message;
703    this._parameters = parameters;
704    this._stackTrace = stackTrace;
705    this._requestId = requestId;
706
707    if (stackTrace && stackTrace.length) {
708        var topCallFrame = stackTrace[0];
709        if (!this.url)
710            this.url = topCallFrame.url;
711        if (!this.line)
712            this.line = topCallFrame.lineNumber;
713    }
714
715    this._formatMessage();
716}
717
718WebInspector.ConsoleMessage.createTextMessage = function(text, level)
719{
720    level = level || WebInspector.ConsoleMessage.MessageLevel.Log;
721    return new WebInspector.ConsoleMessage(WebInspector.ConsoleMessage.MessageSource.JS, WebInspector.ConsoleMessage.MessageType.Log, level, 0, null, 1, null, [text], null);
722}
723
724WebInspector.ConsoleMessage.prototype = {
725    _formatMessage: function()
726    {
727        var stackTrace = this._stackTrace;
728        var messageText;
729        switch (this.type) {
730            case WebInspector.ConsoleMessage.MessageType.Trace:
731                messageText = document.createTextNode("console.trace()");
732                break;
733            case WebInspector.ConsoleMessage.MessageType.UncaughtException:
734                messageText = document.createTextNode(this._messageText);
735                break;
736            case WebInspector.ConsoleMessage.MessageType.NetworkError:
737                var resource = this._requestId && WebInspector.networkResourceById(this._requestId);
738                if (resource) {
739                    stackTrace = resource.stackTrace;
740
741                    messageText = document.createElement("span");
742                    messageText.appendChild(document.createTextNode(resource.requestMethod + " "));
743                    messageText.appendChild(WebInspector.linkifyURLAsNode(resource.url));
744                    if (resource.failed)
745                        messageText.appendChild(document.createTextNode(" " + resource.localizedFailDescription));
746                    else
747                        messageText.appendChild(document.createTextNode(" " + resource.statusCode + " (" + resource.statusText + ")"));
748                } else
749                    messageText = this._format([this._messageText]);
750                break;
751            case WebInspector.ConsoleMessage.MessageType.Assert:
752                var args = [WebInspector.UIString("Assertion failed:")];
753                if (this._parameters)
754                    args = args.concat(this._parameters);
755                messageText = this._format(args);
756                break;
757            case WebInspector.ConsoleMessage.MessageType.Object:
758                var obj = this._parameters ? this._parameters[0] : undefined;
759                var args = ["%O", obj];
760                messageText = this._format(args);
761                break;
762            default:
763                var args = this._parameters || [this._messageText];
764                messageText = this._format(args);
765                break;
766        }
767
768        this._formattedMessage = document.createElement("span");
769        this._formattedMessage.className = "console-message-text source-code";
770
771        if (this.url && this.url !== "undefined") {
772            var urlElement = WebInspector.linkifyResourceAsNode(this.url, "scripts", this.line, "console-message-url");
773            this._formattedMessage.appendChild(urlElement);
774        }
775
776        this._formattedMessage.appendChild(messageText);
777
778        if (this._stackTrace) {
779            switch (this.type) {
780                case WebInspector.ConsoleMessage.MessageType.Trace:
781                case WebInspector.ConsoleMessage.MessageType.UncaughtException:
782                case WebInspector.ConsoleMessage.MessageType.NetworkError:
783                case WebInspector.ConsoleMessage.MessageType.Assert: {
784                    var ol = document.createElement("ol");
785                    ol.className = "outline-disclosure";
786                    var treeOutline = new TreeOutline(ol);
787
788                    var content = this._formattedMessage;
789                    var root = new TreeElement(content, null, true);
790                    content.treeElementForTest = root;
791                    treeOutline.appendChild(root);
792                    if (this.type === WebInspector.ConsoleMessage.MessageType.Trace)
793                        root.expand();
794
795                    this._populateStackTraceTreeElement(root);
796                    this._formattedMessage = ol;
797                }
798            }
799        }
800
801        // This is used for inline message bubbles in SourceFrames, or other plain-text representations.
802        this.message = this._formattedMessage.textContent;
803    },
804
805    isErrorOrWarning: function()
806    {
807        return (this.level === WebInspector.ConsoleMessage.MessageLevel.Warning || this.level === WebInspector.ConsoleMessage.MessageLevel.Error);
808    },
809
810    _format: function(parameters)
811    {
812        // This node is used like a Builder. Values are continually appended onto it.
813        var formattedResult = document.createElement("span");
814        if (!parameters.length)
815            return formattedResult;
816
817        // Formatting code below assumes that parameters are all wrappers whereas frontend console
818        // API allows passing arbitrary values as messages (strings, numbers, etc.). Wrap them here.
819        for (var i = 0; i < parameters.length; ++i) {
820            if (typeof parameters[i] === "object")
821                parameters[i] = WebInspector.RemoteObject.fromPayload(parameters[i]);
822            else
823                parameters[i] = WebInspector.RemoteObject.fromPrimitiveValue(parameters[i]);
824        }
825
826        // There can be string log and string eval result. We distinguish between them based on message type.
827        var shouldFormatMessage = WebInspector.RemoteObject.type(parameters[0]) === "string" && this.type !== WebInspector.ConsoleMessage.MessageType.Result;
828
829        // Multiple parameters with the first being a format string. Save unused substitutions.
830        if (shouldFormatMessage) {
831            // Multiple parameters with the first being a format string. Save unused substitutions.
832            var result = this._formatWithSubstitutionString(parameters, formattedResult);
833            parameters = result.unusedSubstitutions;
834            if (parameters.length)
835                formattedResult.appendChild(document.createTextNode(" "));
836        }
837
838        // Single parameter, or unused substitutions from above.
839        for (var i = 0; i < parameters.length; ++i) {
840            // Inline strings when formatting.
841            if (shouldFormatMessage && parameters[i].type === "string")
842                formattedResult.appendChild(document.createTextNode(parameters[i].description));
843            else
844                formattedResult.appendChild(WebInspector.console._format(parameters[i]));
845            if (i < parameters.length - 1)
846                formattedResult.appendChild(document.createTextNode(" "));
847        }
848        return formattedResult;
849    },
850
851    _formatWithSubstitutionString: function(parameters, formattedResult)
852    {
853        var formatters = {}
854        for (var i in String.standardFormatters)
855            formatters[i] = String.standardFormatters[i];
856
857        function consoleFormatWrapper(force)
858        {
859            return function(obj) {
860                return WebInspector.console._format(obj, force);
861            };
862        }
863
864        // Firebug uses %o for formatting objects.
865        formatters.o = consoleFormatWrapper();
866        // Firebug allows both %i and %d for formatting integers.
867        formatters.i = formatters.d;
868        // Support %O to force object formatting, instead of the type-based %o formatting.
869        formatters.O = consoleFormatWrapper(true);
870
871        function append(a, b)
872        {
873            if (!(b instanceof Node))
874                a.appendChild(WebInspector.linkifyStringAsFragment(b.toString()));
875            else
876                a.appendChild(b);
877            return a;
878        }
879
880        // String.format does treat formattedResult like a Builder, result is an object.
881        return String.format(parameters[0].description, parameters.slice(1), formatters, formattedResult, append);
882    },
883
884    toMessageElement: function()
885    {
886        if (this._element)
887            return this._element;
888
889        var element = document.createElement("div");
890        element.message = this;
891        element.className = "console-message";
892
893        this._element = element;
894
895        switch (this.level) {
896            case WebInspector.ConsoleMessage.MessageLevel.Tip:
897                element.addStyleClass("console-tip-level");
898                break;
899            case WebInspector.ConsoleMessage.MessageLevel.Log:
900                element.addStyleClass("console-log-level");
901                break;
902            case WebInspector.ConsoleMessage.MessageLevel.Debug:
903                element.addStyleClass("console-debug-level");
904                break;
905            case WebInspector.ConsoleMessage.MessageLevel.Warning:
906                element.addStyleClass("console-warning-level");
907                break;
908            case WebInspector.ConsoleMessage.MessageLevel.Error:
909                element.addStyleClass("console-error-level");
910                break;
911        }
912
913        if (this.type === WebInspector.ConsoleMessage.MessageType.StartGroup || this.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed)
914            element.addStyleClass("console-group-title");
915
916        if (this.elementsTreeOutline) {
917            element.addStyleClass("outline-disclosure");
918            element.appendChild(this.elementsTreeOutline.element);
919            return element;
920        }
921
922        element.appendChild(this._formattedMessage);
923
924        if (this.repeatCount > 1)
925            this._updateRepeatCount();
926
927        return element;
928    },
929
930    _populateStackTraceTreeElement: function(parentTreeElement)
931    {
932        for (var i = 0; i < this._stackTrace.length; i++) {
933            var frame = this._stackTrace[i];
934
935            var content = document.createElement("div");
936            var messageTextElement = document.createElement("span");
937            messageTextElement.className = "console-message-text source-code";
938            var functionName = frame.functionName || WebInspector.UIString("(anonymous function)");
939            messageTextElement.appendChild(document.createTextNode(functionName));
940            content.appendChild(messageTextElement);
941
942            var urlElement = WebInspector.linkifyResourceAsNode(frame.url, "scripts", frame.lineNumber, "console-message-url");
943            content.appendChild(urlElement);
944
945            var treeElement = new TreeElement(content);
946            parentTreeElement.appendChild(treeElement);
947        }
948    },
949
950    _updateRepeatCount: function() {
951        if (!this.repeatCountElement) {
952            this.repeatCountElement = document.createElement("span");
953            this.repeatCountElement.className = "bubble";
954
955            this._element.insertBefore(this.repeatCountElement, this._element.firstChild);
956            this._element.addStyleClass("repeated-message");
957        }
958        this.repeatCountElement.textContent = this.repeatCount;
959    },
960
961    toString: function()
962    {
963        var sourceString;
964        switch (this.source) {
965            case WebInspector.ConsoleMessage.MessageSource.HTML:
966                sourceString = "HTML";
967                break;
968            case WebInspector.ConsoleMessage.MessageSource.WML:
969                sourceString = "WML";
970                break;
971            case WebInspector.ConsoleMessage.MessageSource.XML:
972                sourceString = "XML";
973                break;
974            case WebInspector.ConsoleMessage.MessageSource.JS:
975                sourceString = "JS";
976                break;
977            case WebInspector.ConsoleMessage.MessageSource.CSS:
978                sourceString = "CSS";
979                break;
980            case WebInspector.ConsoleMessage.MessageSource.Other:
981                sourceString = "Other";
982                break;
983        }
984
985        var typeString;
986        switch (this.type) {
987            case WebInspector.ConsoleMessage.MessageType.Log:
988            case WebInspector.ConsoleMessage.MessageType.UncaughtException:
989            case WebInspector.ConsoleMessage.MessageType.NetworkError:
990                typeString = "Log";
991                break;
992            case WebInspector.ConsoleMessage.MessageType.Object:
993                typeString = "Object";
994                break;
995            case WebInspector.ConsoleMessage.MessageType.Trace:
996                typeString = "Trace";
997                break;
998            case WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed:
999            case WebInspector.ConsoleMessage.MessageType.StartGroup:
1000                typeString = "Start Group";
1001                break;
1002            case WebInspector.ConsoleMessage.MessageType.EndGroup:
1003                typeString = "End Group";
1004                break;
1005            case WebInspector.ConsoleMessage.MessageType.Assert:
1006                typeString = "Assert";
1007                break;
1008            case WebInspector.ConsoleMessage.MessageType.Result:
1009                typeString = "Result";
1010                break;
1011        }
1012
1013        var levelString;
1014        switch (this.level) {
1015            case WebInspector.ConsoleMessage.MessageLevel.Tip:
1016                levelString = "Tip";
1017                break;
1018            case WebInspector.ConsoleMessage.MessageLevel.Log:
1019                levelString = "Log";
1020                break;
1021            case WebInspector.ConsoleMessage.MessageLevel.Warning:
1022                levelString = "Warning";
1023                break;
1024            case WebInspector.ConsoleMessage.MessageLevel.Debug:
1025                levelString = "Debug";
1026                break;
1027            case WebInspector.ConsoleMessage.MessageLevel.Error:
1028                levelString = "Error";
1029                break;
1030        }
1031
1032        return sourceString + " " + typeString + " " + levelString + ": " + this._formattedMessage.textContent + "\n" + this.url + " line " + this.line;
1033    },
1034
1035    isEqual: function(msg)
1036    {
1037        if (!msg)
1038            return false;
1039
1040        if (this._stackTrace) {
1041            if (!msg._stackTrace)
1042                return false;
1043            var l = this._stackTrace;
1044            var r = msg._stackTrace;
1045            for (var i = 0; i < l.length; i++) {
1046                if (l[i].url !== r[i].url ||
1047                    l[i].functionName !== r[i].functionName ||
1048                    l[i].lineNumber !== r[i].lineNumber ||
1049                    l[i].columnNumber !== r[i].columnNumber)
1050                    return false;
1051            }
1052        }
1053
1054        return (this.source === msg.source)
1055            && (this.type === msg.type)
1056            && (this.level === msg.level)
1057            && (this.line === msg.line)
1058            && (this.url === msg.url)
1059            && (this.message === msg.message)
1060            && (this._requestId === msg._requestId);
1061    }
1062}
1063
1064// Note: Keep these constants in sync with the ones in Console.h
1065WebInspector.ConsoleMessage.MessageSource = {
1066    HTML: "html",
1067    WML: "wml",
1068    XML: "xml",
1069    JS: "javascript",
1070    CSS: "css",
1071    Other: "other"
1072}
1073
1074WebInspector.ConsoleMessage.MessageType = {
1075    Log: "log",
1076    Object: "other",
1077    Trace: "trace",
1078    StartGroup: "startGroup",
1079    StartGroupCollapsed: "startGroupCollapsed",
1080    EndGroup: "endGroup",
1081    Assert: "assert",
1082    UncaughtException: "uncaughtException",
1083    NetworkError: "networkError",
1084    Result: "result"
1085}
1086
1087WebInspector.ConsoleMessage.MessageLevel = {
1088    Tip: "tip",
1089    Log: "log",
1090    Warning: "warning",
1091    Error: "error",
1092    Debug: "debug"
1093}
1094
1095WebInspector.ConsoleCommand = function(command)
1096{
1097    this.command = command;
1098}
1099
1100WebInspector.ConsoleCommand.prototype = {
1101    toMessageElement: function()
1102    {
1103        var element = document.createElement("div");
1104        element.command = this;
1105        element.className = "console-user-command";
1106
1107        var commandTextElement = document.createElement("span");
1108        commandTextElement.className = "console-message-text source-code";
1109        commandTextElement.textContent = this.command;
1110        element.appendChild(commandTextElement);
1111
1112        return element;
1113    }
1114}
1115
1116WebInspector.ConsoleCommandResult = function(result, originatingCommand)
1117{
1118    var level = (result.isError() ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log);
1119    this.originatingCommand = originatingCommand;
1120    WebInspector.ConsoleMessage.call(this, WebInspector.ConsoleMessage.MessageSource.JS, WebInspector.ConsoleMessage.MessageType.Result, level, -1, null, 1, null, [result]);
1121}
1122
1123WebInspector.ConsoleCommandResult.prototype = {
1124    toMessageElement: function()
1125    {
1126        var element = WebInspector.ConsoleMessage.prototype.toMessageElement.call(this);
1127        element.addStyleClass("console-user-command-result");
1128        return element;
1129    }
1130}
1131
1132WebInspector.ConsoleCommandResult.prototype.__proto__ = WebInspector.ConsoleMessage.prototype;
1133
1134WebInspector.ConsoleGroup = function(parentGroup)
1135{
1136    this.parentGroup = parentGroup;
1137
1138    var element = document.createElement("div");
1139    element.className = "console-group";
1140    element.group = this;
1141    this.element = element;
1142
1143    var messagesElement = document.createElement("div");
1144    messagesElement.className = "console-group-messages";
1145    element.appendChild(messagesElement);
1146    this.messagesElement = messagesElement;
1147}
1148
1149WebInspector.ConsoleGroup.prototype = {
1150    addMessage: function(msg)
1151    {
1152        var element = msg.toMessageElement();
1153
1154        if (msg.type === WebInspector.ConsoleMessage.MessageType.StartGroup || msg.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed) {
1155            this.messagesElement.parentNode.insertBefore(element, this.messagesElement);
1156            element.addEventListener("click", this._titleClicked.bind(this), false);
1157            var groupElement = element.enclosingNodeOrSelfWithClass("console-group");
1158            if (groupElement && msg.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed)
1159                groupElement.addStyleClass("collapsed");
1160        } else
1161            this.messagesElement.appendChild(element);
1162
1163        if (element.previousSibling && msg.originatingCommand && element.previousSibling.command === msg.originatingCommand)
1164            element.previousSibling.addStyleClass("console-adjacent-user-command-result");
1165    },
1166
1167    _titleClicked: function(event)
1168    {
1169        var groupTitleElement = event.target.enclosingNodeOrSelfWithClass("console-group-title");
1170        if (groupTitleElement) {
1171            var groupElement = groupTitleElement.enclosingNodeOrSelfWithClass("console-group");
1172            if (groupElement)
1173                if (groupElement.hasStyleClass("collapsed"))
1174                    groupElement.removeStyleClass("collapsed");
1175                else
1176                    groupElement.addStyleClass("collapsed");
1177            groupTitleElement.scrollIntoViewIfNeeded(true);
1178        }
1179
1180        event.stopPropagation();
1181        event.preventDefault();
1182    }
1183}
1184
1185