michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: michael@0: loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm"); michael@0: loader.lazyImporter(this, "escapeHTML", "resource:///modules/devtools/VariablesView.jsm"); michael@0: loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); michael@0: loader.lazyImporter(this, "Task","resource://gre/modules/Task.jsm"); michael@0: michael@0: const Heritage = require("sdk/core/heritage"); michael@0: const XHTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; michael@0: const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; michael@0: michael@0: const WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; michael@0: const l10n = new WebConsoleUtils.l10n(STRINGS_URI); michael@0: michael@0: // Constants for compatibility with the Web Console output implementation before michael@0: // bug 778766. michael@0: // TODO: remove these once bug 778766 is fixed. michael@0: const COMPAT = { michael@0: // The various categories of messages. michael@0: CATEGORIES: { michael@0: NETWORK: 0, michael@0: CSS: 1, michael@0: JS: 2, michael@0: WEBDEV: 3, michael@0: INPUT: 4, michael@0: OUTPUT: 5, michael@0: SECURITY: 6, michael@0: }, michael@0: michael@0: // The possible message severities. michael@0: SEVERITIES: { michael@0: ERROR: 0, michael@0: WARNING: 1, michael@0: INFO: 2, michael@0: LOG: 3, michael@0: }, michael@0: michael@0: // The preference keys to use for each category/severity combination, indexed michael@0: // first by category (rows) and then by severity (columns). michael@0: // michael@0: // Most of these rather idiosyncratic names are historical and predate the michael@0: // division of message type into "category" and "severity". michael@0: PREFERENCE_KEYS: [ michael@0: // Error Warning Info Log michael@0: [ "network", "netwarn", null, "networkinfo", ], // Network michael@0: [ "csserror", "cssparser", null, null, ], // CSS michael@0: [ "exception", "jswarn", null, "jslog", ], // JS michael@0: [ "error", "warn", "info", "log", ], // Web Developer michael@0: [ null, null, null, null, ], // Input michael@0: [ null, null, null, null, ], // Output michael@0: [ "secerror", "secwarn", null, null, ], // Security michael@0: ], michael@0: michael@0: // The fragment of a CSS class name that identifies each category. michael@0: CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console", michael@0: "input", "output", "security" ], michael@0: michael@0: // The fragment of a CSS class name that identifies each severity. michael@0: SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ], michael@0: michael@0: // The indent of a console group in pixels. michael@0: GROUP_INDENT: 12, michael@0: }; michael@0: michael@0: // A map from the console API call levels to the Web Console severities. michael@0: const CONSOLE_API_LEVELS_TO_SEVERITIES = { michael@0: error: "error", michael@0: exception: "error", michael@0: assert: "error", michael@0: warn: "warning", michael@0: info: "info", michael@0: log: "log", michael@0: trace: "log", michael@0: debug: "log", michael@0: dir: "log", michael@0: group: "log", michael@0: groupCollapsed: "log", michael@0: groupEnd: "log", michael@0: time: "log", michael@0: timeEnd: "log", michael@0: count: "log" michael@0: }; michael@0: michael@0: // Array of known message source URLs we need to hide from output. michael@0: const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"]; michael@0: michael@0: // The maximum length of strings to be displayed by the Web Console. michael@0: const MAX_LONG_STRING_LENGTH = 200000; michael@0: michael@0: // Regular expression that matches the allowed CSS property names when using michael@0: // the `window.console` API. michael@0: const RE_ALLOWED_STYLES = /^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|margin|padding|text|transition|outline|white-space|word|writing|(?:min-|max-)?width|(?:min-|max-)?height)/; michael@0: michael@0: // Regular expressions to search and replace with 'notallowed' in the styles michael@0: // given to the `window.console` API methods. michael@0: const RE_CLEANUP_STYLES = [ michael@0: // url(), -moz-element() michael@0: /\b(?:url|(?:-moz-)?element)[\s('"]+/gi, michael@0: michael@0: // various URL protocols michael@0: /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi, michael@0: ]; michael@0: michael@0: /** michael@0: * The ConsoleOutput object is used to manage output of messages in the Web michael@0: * Console. michael@0: * michael@0: * @constructor michael@0: * @param object owner michael@0: * The console output owner. This usually the WebConsoleFrame instance. michael@0: * Any other object can be used, as long as it has the following michael@0: * properties and methods: michael@0: * - window michael@0: * - document michael@0: * - outputMessage(category, methodOrNode[, methodArguments]) michael@0: * TODO: this is needed temporarily, until bug 778766 is fixed. michael@0: */ michael@0: function ConsoleOutput(owner) michael@0: { michael@0: this.owner = owner; michael@0: this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this); michael@0: } michael@0: michael@0: ConsoleOutput.prototype = { michael@0: _dummyElement: null, michael@0: michael@0: /** michael@0: * The output container. michael@0: * @type DOMElement michael@0: */ michael@0: get element() { michael@0: return this.owner.outputNode; michael@0: }, michael@0: michael@0: /** michael@0: * The document that holds the output. michael@0: * @type DOMDocument michael@0: */ michael@0: get document() { michael@0: return this.owner ? this.owner.document : null; michael@0: }, michael@0: michael@0: /** michael@0: * The DOM window that holds the output. michael@0: * @type Window michael@0: */ michael@0: get window() { michael@0: return this.owner.window; michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the debugger WebConsoleClient. michael@0: * @type object michael@0: */ michael@0: get webConsoleClient() { michael@0: return this.owner.webConsoleClient; michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the current toolbox debuggee target. michael@0: * @type Target michael@0: */ michael@0: get toolboxTarget() { michael@0: return this.owner.owner.target; michael@0: }, michael@0: michael@0: /** michael@0: * Release an actor. michael@0: * michael@0: * @private michael@0: * @param string actorId michael@0: * The actor ID you want to release. michael@0: */ michael@0: _releaseObject: function(actorId) michael@0: { michael@0: this.owner._releaseObject(actorId); michael@0: }, michael@0: michael@0: /** michael@0: * Add a message to output. michael@0: * michael@0: * @param object ...args michael@0: * Any number of Message objects. michael@0: * @return this michael@0: */ michael@0: addMessage: function(...args) michael@0: { michael@0: for (let msg of args) { michael@0: msg.init(this); michael@0: this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage, michael@0: [msg]); michael@0: } michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Message renderer used for compatibility with the current Web Console output michael@0: * implementation. This method is invoked for every message object that is michael@0: * flushed to output. The message object is initialized and rendered, then it michael@0: * is displayed. michael@0: * michael@0: * TODO: remove this method once bug 778766 is fixed. michael@0: * michael@0: * @private michael@0: * @param object message michael@0: * The message object to render. michael@0: * @return DOMElement michael@0: * The message DOM element that can be added to the console output. michael@0: */ michael@0: _onFlushOutputMessage: function(message) michael@0: { michael@0: return message.render().element; michael@0: }, michael@0: michael@0: /** michael@0: * Get an array of selected messages. This list is based on the text selection michael@0: * start and end points. michael@0: * michael@0: * @param number [limit] michael@0: * Optional limit of selected messages you want. If no value is given, michael@0: * all of the selected messages are returned. michael@0: * @return array michael@0: * Array of DOM elements for each message that is currently selected. michael@0: */ michael@0: getSelectedMessages: function(limit) michael@0: { michael@0: let selection = this.window.getSelection(); michael@0: if (selection.isCollapsed) { michael@0: return []; michael@0: } michael@0: michael@0: if (selection.containsNode(this.element, true)) { michael@0: return Array.slice(this.element.children); michael@0: } michael@0: michael@0: let anchor = this.getMessageForElement(selection.anchorNode); michael@0: let focus = this.getMessageForElement(selection.focusNode); michael@0: if (!anchor || !focus) { michael@0: return []; michael@0: } michael@0: michael@0: let start, end; michael@0: if (anchor.timestamp > focus.timestamp) { michael@0: start = focus; michael@0: end = anchor; michael@0: } else { michael@0: start = anchor; michael@0: end = focus; michael@0: } michael@0: michael@0: let result = []; michael@0: let current = start; michael@0: while (current) { michael@0: result.push(current); michael@0: if (current == end || (limit && result.length == limit)) { michael@0: break; michael@0: } michael@0: current = current.nextSibling; michael@0: } michael@0: return result; michael@0: }, michael@0: michael@0: /** michael@0: * Find the DOM element of a message for any given descendant. michael@0: * michael@0: * @param DOMElement elem michael@0: * The element to start the search from. michael@0: * @return DOMElement|null michael@0: * The DOM element of the message, if any. michael@0: */ michael@0: getMessageForElement: function(elem) michael@0: { michael@0: while (elem && elem.parentNode) { michael@0: if (elem.classList && elem.classList.contains("message")) { michael@0: return elem; michael@0: } michael@0: elem = elem.parentNode; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Select all messages. michael@0: */ michael@0: selectAllMessages: function() michael@0: { michael@0: let selection = this.window.getSelection(); michael@0: selection.removeAllRanges(); michael@0: let range = this.document.createRange(); michael@0: range.selectNodeContents(this.element); michael@0: selection.addRange(range); michael@0: }, michael@0: michael@0: /** michael@0: * Add a message to the selection. michael@0: * michael@0: * @param DOMElement elem michael@0: * The message element to select. michael@0: */ michael@0: selectMessage: function(elem) michael@0: { michael@0: let selection = this.window.getSelection(); michael@0: selection.removeAllRanges(); michael@0: let range = this.document.createRange(); michael@0: range.selectNodeContents(elem); michael@0: selection.addRange(range); michael@0: }, michael@0: michael@0: /** michael@0: * Open an URL in a new tab. michael@0: * @see WebConsole.openLink() in hudservice.js michael@0: */ michael@0: openLink: function() michael@0: { michael@0: this.owner.owner.openLink.apply(this.owner.owner, arguments); michael@0: }, michael@0: michael@0: /** michael@0: * Open the variables view to inspect an object actor. michael@0: * @see JSTerm.openVariablesView() in webconsole.js michael@0: */ michael@0: openVariablesView: function() michael@0: { michael@0: this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy this ConsoleOutput instance. michael@0: */ michael@0: destroy: function() michael@0: { michael@0: this._dummyElement = null; michael@0: this.owner = null; michael@0: }, michael@0: }; // ConsoleOutput.prototype michael@0: michael@0: /** michael@0: * Message objects container. michael@0: * @type object michael@0: */ michael@0: let Messages = {}; michael@0: michael@0: /** michael@0: * The BaseMessage object is used for all types of messages. Every kind of michael@0: * message should use this object as its base. michael@0: * michael@0: * @constructor michael@0: */ michael@0: Messages.BaseMessage = function() michael@0: { michael@0: this.widgets = new Set(); michael@0: this._onClickAnchor = this._onClickAnchor.bind(this); michael@0: this._repeatID = { uid: gSequenceId() }; michael@0: this.textContent = ""; michael@0: }; michael@0: michael@0: Messages.BaseMessage.prototype = { michael@0: /** michael@0: * Reference to the ConsoleOutput owner. michael@0: * michael@0: * @type object|null michael@0: * This is |null| if the message is not yet initialized. michael@0: */ michael@0: output: null, michael@0: michael@0: /** michael@0: * Reference to the parent message object, if this message is in a group or if michael@0: * it is otherwise owned by another message. michael@0: * michael@0: * @type object|null michael@0: */ michael@0: parent: null, michael@0: michael@0: /** michael@0: * Message DOM element. michael@0: * michael@0: * @type DOMElement|null michael@0: * This is |null| if the message is not yet rendered. michael@0: */ michael@0: element: null, michael@0: michael@0: /** michael@0: * Tells if this message is visible or not. michael@0: * @type boolean michael@0: */ michael@0: get visible() { michael@0: return this.element && this.element.parentNode; michael@0: }, michael@0: michael@0: /** michael@0: * The owner DOM document. michael@0: * @type DOMElement michael@0: */ michael@0: get document() { michael@0: return this.output.document; michael@0: }, michael@0: michael@0: /** michael@0: * Holds the text-only representation of the message. michael@0: * @type string michael@0: */ michael@0: textContent: null, michael@0: michael@0: /** michael@0: * Set of widgets included in this message. michael@0: * @type Set michael@0: */ michael@0: widgets: null, michael@0: michael@0: // Properties that allow compatibility with the current Web Console output michael@0: // implementation. michael@0: _categoryCompat: null, michael@0: _severityCompat: null, michael@0: _categoryNameCompat: null, michael@0: _severityNameCompat: null, michael@0: _filterKeyCompat: null, michael@0: michael@0: /** michael@0: * Object that is JSON-ified and used as a non-unique ID for tracking michael@0: * duplicate messages. michael@0: * @private michael@0: * @type object michael@0: */ michael@0: _repeatID: null, michael@0: michael@0: /** michael@0: * Initialize the message. michael@0: * michael@0: * @param object output michael@0: * The ConsoleOutput owner. michael@0: * @param object [parent=null] michael@0: * Optional: a different message object that owns this instance. michael@0: * @return this michael@0: */ michael@0: init: function(output, parent=null) michael@0: { michael@0: this.output = output; michael@0: this.parent = parent; michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Non-unique ID for this message object used for tracking duplicate messages. michael@0: * Different message kinds can identify themselves based their own criteria. michael@0: * michael@0: * @return string michael@0: */ michael@0: getRepeatID: function() michael@0: { michael@0: return JSON.stringify(this._repeatID); michael@0: }, michael@0: michael@0: /** michael@0: * Render the message. After this method is invoked the |element| property michael@0: * will point to the DOM element of this message. michael@0: * @return this michael@0: */ michael@0: render: function() michael@0: { michael@0: if (!this.element) { michael@0: this.element = this._renderCompat(); michael@0: } michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Prepare the message container for the Web Console, such that it is michael@0: * compatible with the current implementation. michael@0: * TODO: remove this once bug 778766 is fixed. michael@0: * michael@0: * @private michael@0: * @return Element michael@0: * The DOM element that wraps the message. michael@0: */ michael@0: _renderCompat: function() michael@0: { michael@0: let doc = this.output.document; michael@0: let container = doc.createElementNS(XHTML_NS, "div"); michael@0: container.id = "console-msg-" + gSequenceId(); michael@0: container.className = "message"; michael@0: container.category = this._categoryCompat; michael@0: container.severity = this._severityCompat; michael@0: container.setAttribute("category", this._categoryNameCompat); michael@0: container.setAttribute("severity", this._severityNameCompat); michael@0: container.setAttribute("filter", this._filterKeyCompat); michael@0: container.clipboardText = this.textContent; michael@0: container.timestamp = this.timestamp; michael@0: container._messageObject = this; michael@0: michael@0: return container; michael@0: }, michael@0: michael@0: /** michael@0: * Add a click callback to a given DOM element. michael@0: * michael@0: * @private michael@0: * @param Element element michael@0: * The DOM element to which you want to add a click event handler. michael@0: * @param function [callback=this._onClickAnchor] michael@0: * Optional click event handler. The default event handler is michael@0: * |this._onClickAnchor|. michael@0: */ michael@0: _addLinkCallback: function(element, callback = this._onClickAnchor) michael@0: { michael@0: // This is going into the WebConsoleFrame object instance that owns michael@0: // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole michael@0: // object instance from hudservice.js. michael@0: // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766 michael@0: // is fixed. michael@0: this.output.owner._addMessageLinkCallback(element, callback); michael@0: }, michael@0: michael@0: /** michael@0: * The default |click| event handler for links in the output. This function michael@0: * opens the anchor's link in a new tab. michael@0: * michael@0: * @private michael@0: * @param Event event michael@0: * The DOM event that invoked this function. michael@0: */ michael@0: _onClickAnchor: function(event) michael@0: { michael@0: this.output.openLink(event.target.href); michael@0: }, michael@0: michael@0: destroy: function() michael@0: { michael@0: // Destroy all widgets that have registered themselves in this.widgets michael@0: for (let widget of this.widgets) { michael@0: widget.destroy(); michael@0: } michael@0: this.widgets.clear(); michael@0: } michael@0: }; // Messages.BaseMessage.prototype michael@0: michael@0: michael@0: /** michael@0: * The NavigationMarker is used to show a page load event. michael@0: * michael@0: * @constructor michael@0: * @extends Messages.BaseMessage michael@0: * @param string url michael@0: * The URL to display. michael@0: * @param number timestamp michael@0: * The message date and time, milliseconds elapsed since 1 January 1970 michael@0: * 00:00:00 UTC. michael@0: */ michael@0: Messages.NavigationMarker = function(url, timestamp) michael@0: { michael@0: Messages.BaseMessage.call(this); michael@0: this._url = url; michael@0: this.textContent = "------ " + url; michael@0: this.timestamp = timestamp; michael@0: }; michael@0: michael@0: Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype, michael@0: { michael@0: /** michael@0: * The address of the loading page. michael@0: * @private michael@0: * @type string michael@0: */ michael@0: _url: null, michael@0: michael@0: /** michael@0: * Message timestamp. michael@0: * michael@0: * @type number michael@0: * Milliseconds elapsed since 1 January 1970 00:00:00 UTC. michael@0: */ michael@0: timestamp: 0, michael@0: michael@0: _categoryCompat: COMPAT.CATEGORIES.NETWORK, michael@0: _severityCompat: COMPAT.SEVERITIES.LOG, michael@0: _categoryNameCompat: "network", michael@0: _severityNameCompat: "info", michael@0: _filterKeyCompat: "networkinfo", michael@0: michael@0: /** michael@0: * Prepare the DOM element for this message. michael@0: * @return this michael@0: */ michael@0: render: function() michael@0: { michael@0: if (this.element) { michael@0: return this; michael@0: } michael@0: michael@0: let url = this._url; michael@0: let pos = url.indexOf("?"); michael@0: if (pos > -1) { michael@0: url = url.substr(0, pos); michael@0: } michael@0: michael@0: let doc = this.output.document; michael@0: let urlnode = doc.createElementNS(XHTML_NS, "a"); michael@0: urlnode.className = "url"; michael@0: urlnode.textContent = url; michael@0: urlnode.title = this._url; michael@0: urlnode.href = this._url; michael@0: urlnode.draggable = false; michael@0: this._addLinkCallback(urlnode); michael@0: michael@0: let render = Messages.BaseMessage.prototype.render.bind(this); michael@0: render().element.appendChild(urlnode); michael@0: this.element.classList.add("navigation-marker"); michael@0: this.element.url = this._url; michael@0: this.element.appendChild(doc.createTextNode("\n")); michael@0: michael@0: return this; michael@0: }, michael@0: }); // Messages.NavigationMarker.prototype michael@0: michael@0: michael@0: /** michael@0: * The Simple message is used to show any basic message in the Web Console. michael@0: * michael@0: * @constructor michael@0: * @extends Messages.BaseMessage michael@0: * @param string|Node|function message michael@0: * The message to display. michael@0: * @param object [options] michael@0: * Options for this message: michael@0: * - category: (string) category that this message belongs to. Defaults michael@0: * to no category. michael@0: * - severity: (string) severity of the message. Defaults to no severity. michael@0: * - timestamp: (number) date and time when the message was recorded. michael@0: * Defaults to |Date.now()|. michael@0: * - link: (string) if provided, the message will be wrapped in an anchor michael@0: * pointing to the given URL here. michael@0: * - linkCallback: (function) if provided, the message will be wrapped in michael@0: * an anchor. The |linkCallback| function will be added as click event michael@0: * handler. michael@0: * - location: object that tells the message source: url, line, column michael@0: * and lineText. michael@0: * - className: (string) additional element class names for styling michael@0: * purposes. michael@0: * - private: (boolean) mark this as a private message. michael@0: * - filterDuplicates: (boolean) true if you do want this message to be michael@0: * filtered as a potential duplicate message, false otherwise. michael@0: */ michael@0: Messages.Simple = function(message, options = {}) michael@0: { michael@0: Messages.BaseMessage.call(this); michael@0: michael@0: this.category = options.category; michael@0: this.severity = options.severity; michael@0: this.location = options.location; michael@0: this.timestamp = options.timestamp || Date.now(); michael@0: this.private = !!options.private; michael@0: michael@0: this._message = message; michael@0: this._className = options.className; michael@0: this._link = options.link; michael@0: this._linkCallback = options.linkCallback; michael@0: this._filterDuplicates = options.filterDuplicates; michael@0: }; michael@0: michael@0: Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype, michael@0: { michael@0: /** michael@0: * Message category. michael@0: * @type string michael@0: */ michael@0: category: null, michael@0: michael@0: /** michael@0: * Message severity. michael@0: * @type string michael@0: */ michael@0: severity: null, michael@0: michael@0: /** michael@0: * Message source location. Properties: url, line, column, lineText. michael@0: * @type object michael@0: */ michael@0: location: null, michael@0: michael@0: /** michael@0: * Tells if this message comes from a private browsing context. michael@0: * @type boolean michael@0: */ michael@0: private: false, michael@0: michael@0: /** michael@0: * Custom class name for the DOM element of the message. michael@0: * @private michael@0: * @type string michael@0: */ michael@0: _className: null, michael@0: michael@0: /** michael@0: * Message link - if this message is clicked then this URL opens in a new tab. michael@0: * @private michael@0: * @type string michael@0: */ michael@0: _link: null, michael@0: michael@0: /** michael@0: * Message click event handler. michael@0: * @private michael@0: * @type function michael@0: */ michael@0: _linkCallback: null, michael@0: michael@0: /** michael@0: * Tells if this message should be checked if it is a duplicate of another michael@0: * message or not. michael@0: */ michael@0: _filterDuplicates: false, michael@0: michael@0: /** michael@0: * The raw message displayed by this Message object. This can be a function, michael@0: * DOM node or a string. michael@0: * michael@0: * @private michael@0: * @type mixed michael@0: */ michael@0: _message: null, michael@0: michael@0: _afterMessage: null, michael@0: _objectActors: null, michael@0: _groupDepthCompat: 0, michael@0: michael@0: /** michael@0: * Message timestamp. michael@0: * michael@0: * @type number michael@0: * Milliseconds elapsed since 1 January 1970 00:00:00 UTC. michael@0: */ michael@0: timestamp: 0, michael@0: michael@0: get _categoryCompat() { michael@0: return this.category ? michael@0: COMPAT.CATEGORIES[this.category.toUpperCase()] : null; michael@0: }, michael@0: get _severityCompat() { michael@0: return this.severity ? michael@0: COMPAT.SEVERITIES[this.severity.toUpperCase()] : null; michael@0: }, michael@0: get _categoryNameCompat() { michael@0: return this.category ? michael@0: COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null; michael@0: }, michael@0: get _severityNameCompat() { michael@0: return this.severity ? michael@0: COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null; michael@0: }, michael@0: michael@0: get _filterKeyCompat() { michael@0: return this._categoryCompat !== null && this._severityCompat !== null ? michael@0: COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] : michael@0: null; michael@0: }, michael@0: michael@0: init: function() michael@0: { michael@0: Messages.BaseMessage.prototype.init.apply(this, arguments); michael@0: this._groupDepthCompat = this.output.owner.groupDepth; michael@0: this._initRepeatID(); michael@0: return this; michael@0: }, michael@0: michael@0: _initRepeatID: function() michael@0: { michael@0: if (!this._filterDuplicates) { michael@0: return; michael@0: } michael@0: michael@0: // Add the properties we care about for identifying duplicate messages. michael@0: let rid = this._repeatID; michael@0: delete rid.uid; michael@0: michael@0: rid.category = this.category; michael@0: rid.severity = this.severity; michael@0: rid.private = this.private; michael@0: rid.location = this.location; michael@0: rid.link = this._link; michael@0: rid.linkCallback = this._linkCallback + ""; michael@0: rid.className = this._className; michael@0: rid.groupDepth = this._groupDepthCompat; michael@0: rid.textContent = ""; michael@0: }, michael@0: michael@0: getRepeatID: function() michael@0: { michael@0: // No point in returning a string that includes other properties when there michael@0: // is a unique ID. michael@0: if (this._repeatID.uid) { michael@0: return JSON.stringify({ uid: this._repeatID.uid }); michael@0: } michael@0: michael@0: return JSON.stringify(this._repeatID); michael@0: }, michael@0: michael@0: render: function() michael@0: { michael@0: if (this.element) { michael@0: return this; michael@0: } michael@0: michael@0: let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render(); michael@0: michael@0: let icon = this.document.createElementNS(XHTML_NS, "span"); michael@0: icon.className = "icon"; michael@0: michael@0: // Apply the current group by indenting appropriately. michael@0: // TODO: remove this once bug 778766 is fixed. michael@0: let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT; michael@0: let indentNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: indentNode.className = "indent"; michael@0: indentNode.style.width = indent + "px"; michael@0: michael@0: let body = this._renderBody(); michael@0: this._repeatID.textContent += "|" + body.textContent; michael@0: michael@0: let repeatNode = this._renderRepeatNode(); michael@0: let location = this._renderLocation(); michael@0: michael@0: Messages.BaseMessage.prototype.render.call(this); michael@0: if (this._className) { michael@0: this.element.className += " " + this._className; michael@0: } michael@0: michael@0: this.element.appendChild(timestamp.element); michael@0: this.element.appendChild(indentNode); michael@0: this.element.appendChild(icon); michael@0: this.element.appendChild(body); michael@0: if (repeatNode) { michael@0: this.element.appendChild(repeatNode); michael@0: } michael@0: if (location) { michael@0: this.element.appendChild(location); michael@0: } michael@0: this.element.appendChild(this.document.createTextNode("\n")); michael@0: michael@0: this.element.clipboardText = this.element.textContent; michael@0: michael@0: if (this.private) { michael@0: this.element.setAttribute("private", true); michael@0: } michael@0: michael@0: if (this._afterMessage) { michael@0: this.element._outputAfterNode = this._afterMessage.element; michael@0: this._afterMessage = null; michael@0: } michael@0: michael@0: // TODO: handle object releasing in a more elegant way once all console michael@0: // messages use the new API - bug 778766. michael@0: this.element._objectActors = this._objectActors; michael@0: this._objectActors = null; michael@0: michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Render the message body DOM element. michael@0: * @private michael@0: * @return Element michael@0: */ michael@0: _renderBody: function() michael@0: { michael@0: let body = this.document.createElementNS(XHTML_NS, "span"); michael@0: body.className = "message-body-wrapper message-body devtools-monospace"; michael@0: michael@0: let anchor, container = body; michael@0: if (this._link || this._linkCallback) { michael@0: container = anchor = this.document.createElementNS(XHTML_NS, "a"); michael@0: anchor.href = this._link || "#"; michael@0: anchor.draggable = false; michael@0: this._addLinkCallback(anchor, this._linkCallback); michael@0: body.appendChild(anchor); michael@0: } michael@0: michael@0: if (typeof this._message == "function") { michael@0: container.appendChild(this._message(this)); michael@0: } else if (this._message instanceof Ci.nsIDOMNode) { michael@0: container.appendChild(this._message); michael@0: } else { michael@0: container.textContent = this._message; michael@0: } michael@0: michael@0: return body; michael@0: }, michael@0: michael@0: /** michael@0: * Render the repeat bubble DOM element part of the message. michael@0: * @private michael@0: * @return Element michael@0: */ michael@0: _renderRepeatNode: function() michael@0: { michael@0: if (!this._filterDuplicates) { michael@0: return null; michael@0: } michael@0: michael@0: let repeatNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: repeatNode.setAttribute("value", "1"); michael@0: repeatNode.className = "message-repeats"; michael@0: repeatNode.textContent = 1; michael@0: repeatNode._uid = this.getRepeatID(); michael@0: return repeatNode; michael@0: }, michael@0: michael@0: /** michael@0: * Render the message source location DOM element. michael@0: * @private michael@0: * @return Element michael@0: */ michael@0: _renderLocation: function() michael@0: { michael@0: if (!this.location) { michael@0: return null; michael@0: } michael@0: michael@0: let {url, line} = this.location; michael@0: if (IGNORED_SOURCE_URLS.indexOf(url) != -1) { michael@0: return null; michael@0: } michael@0: michael@0: // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js. michael@0: // TODO: move createLocationNode() into this file when bug 778766 is fixed. michael@0: return this.output.owner.createLocationNode(url, line); michael@0: }, michael@0: }); // Messages.Simple.prototype michael@0: michael@0: michael@0: /** michael@0: * The Extended message. michael@0: * michael@0: * @constructor michael@0: * @extends Messages.Simple michael@0: * @param array messagePieces michael@0: * The message to display given as an array of elements. Each array michael@0: * element can be a DOM node, function, ObjectActor, LongString or michael@0: * a string. michael@0: * @param object [options] michael@0: * Options for rendering this message: michael@0: * - quoteStrings: boolean that tells if you want strings to be wrapped michael@0: * in quotes or not. michael@0: */ michael@0: Messages.Extended = function(messagePieces, options = {}) michael@0: { michael@0: Messages.Simple.call(this, null, options); michael@0: michael@0: this._messagePieces = messagePieces; michael@0: michael@0: if ("quoteStrings" in options) { michael@0: this._quoteStrings = options.quoteStrings; michael@0: } michael@0: michael@0: this._repeatID.quoteStrings = this._quoteStrings; michael@0: this._repeatID.messagePieces = messagePieces + ""; michael@0: this._repeatID.actors = new Set(); // using a set to avoid duplicates michael@0: }; michael@0: michael@0: Messages.Extended.prototype = Heritage.extend(Messages.Simple.prototype, michael@0: { michael@0: /** michael@0: * The message pieces displayed by this message instance. michael@0: * @private michael@0: * @type array michael@0: */ michael@0: _messagePieces: null, michael@0: michael@0: /** michael@0: * Boolean that tells if the strings displayed in this message are wrapped. michael@0: * @private michael@0: * @type boolean michael@0: */ michael@0: _quoteStrings: true, michael@0: michael@0: getRepeatID: function() michael@0: { michael@0: if (this._repeatID.uid) { michael@0: return JSON.stringify({ uid: this._repeatID.uid }); michael@0: } michael@0: michael@0: // Sets are not stringified correctly. Temporarily switching to an array. michael@0: let actors = this._repeatID.actors; michael@0: this._repeatID.actors = [...actors]; michael@0: let result = JSON.stringify(this._repeatID); michael@0: this._repeatID.actors = actors; michael@0: return result; michael@0: }, michael@0: michael@0: render: function() michael@0: { michael@0: let result = this.document.createDocumentFragment(); michael@0: michael@0: for (let i = 0; i < this._messagePieces.length; i++) { michael@0: let separator = i > 0 ? this._renderBodyPieceSeparator() : null; michael@0: if (separator) { michael@0: result.appendChild(separator); michael@0: } michael@0: michael@0: let piece = this._messagePieces[i]; michael@0: result.appendChild(this._renderBodyPiece(piece)); michael@0: } michael@0: michael@0: this._message = result; michael@0: this._messagePieces = null; michael@0: return Messages.Simple.prototype.render.call(this); michael@0: }, michael@0: michael@0: /** michael@0: * Render the separator between the pieces of the message. michael@0: * michael@0: * @private michael@0: * @return Element michael@0: */ michael@0: _renderBodyPieceSeparator: function() { return null; }, michael@0: michael@0: /** michael@0: * Render one piece/element of the message array. michael@0: * michael@0: * @private michael@0: * @param mixed piece michael@0: * Message element to display - this can be a LongString, ObjectActor, michael@0: * DOM node or a function to invoke. michael@0: * @return Element michael@0: */ michael@0: _renderBodyPiece: function(piece) michael@0: { michael@0: if (piece instanceof Ci.nsIDOMNode) { michael@0: return piece; michael@0: } michael@0: if (typeof piece == "function") { michael@0: return piece(this); michael@0: } michael@0: michael@0: return this._renderValueGrip(piece); michael@0: }, michael@0: michael@0: /** michael@0: * Render a grip that represents a value received from the server. This method michael@0: * picks the appropriate widget to render the value with. michael@0: * michael@0: * @private michael@0: * @param object grip michael@0: * The value grip received from the server. michael@0: * @param object options michael@0: * Options for displaying the value. Available options: michael@0: * - noStringQuotes - boolean that tells the renderer to not use quotes michael@0: * around strings. michael@0: * - concise - boolean that tells the renderer to compactly display the michael@0: * grip. This is typically set to true when the object needs to be michael@0: * displayed in an array preview, or as a property value in object michael@0: * previews, etc. michael@0: * @return DOMElement michael@0: * The DOM element that displays the given grip. michael@0: */ michael@0: _renderValueGrip: function(grip, options = {}) michael@0: { michael@0: let isPrimitive = VariablesView.isPrimitive({ value: grip }); michael@0: let isActorGrip = WebConsoleUtils.isActorGrip(grip); michael@0: let noStringQuotes = !this._quoteStrings; michael@0: if ("noStringQuotes" in options) { michael@0: noStringQuotes = options.noStringQuotes; michael@0: } michael@0: michael@0: if (isActorGrip) { michael@0: this._repeatID.actors.add(grip.actor); michael@0: michael@0: if (!isPrimitive) { michael@0: return this._renderObjectActor(grip, options); michael@0: } michael@0: if (grip.type == "longString") { michael@0: let widget = new Widgets.LongString(this, grip, options).render(); michael@0: return widget.element; michael@0: } michael@0: } michael@0: michael@0: let result = this.document.createElementNS(XHTML_NS, "span"); michael@0: if (isPrimitive) { michael@0: let className = this.getClassNameForValueGrip(grip); michael@0: if (className) { michael@0: result.className = className; michael@0: } michael@0: michael@0: result.textContent = VariablesView.getString(grip, { michael@0: noStringQuotes: noStringQuotes, michael@0: concise: options.concise, michael@0: }); michael@0: } else { michael@0: result.textContent = grip; michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: /** michael@0: * Get a CodeMirror-compatible class name for a given value grip. michael@0: * michael@0: * @param object grip michael@0: * Value grip from the server. michael@0: * @return string michael@0: * The class name for the grip. michael@0: */ michael@0: getClassNameForValueGrip: function(grip) michael@0: { michael@0: let map = { michael@0: "number": "cm-number", michael@0: "longstring": "console-string", michael@0: "string": "console-string", michael@0: "regexp": "cm-string-2", michael@0: "boolean": "cm-atom", michael@0: "-infinity": "cm-atom", michael@0: "infinity": "cm-atom", michael@0: "null": "cm-atom", michael@0: "undefined": "cm-comment", michael@0: }; michael@0: michael@0: let className = map[typeof grip]; michael@0: if (!className && grip && grip.type) { michael@0: className = map[grip.type.toLowerCase()]; michael@0: } michael@0: if (!className && grip && grip.class) { michael@0: className = map[grip.class.toLowerCase()]; michael@0: } michael@0: michael@0: return className; michael@0: }, michael@0: michael@0: /** michael@0: * Display an object actor with the appropriate renderer. michael@0: * michael@0: * @private michael@0: * @param object objectActor michael@0: * The ObjectActor to display. michael@0: * @param object options michael@0: * Options to use for displaying the ObjectActor. michael@0: * @see this._renderValueGrip for the available options. michael@0: * @return DOMElement michael@0: * The DOM element that displays the object actor. michael@0: */ michael@0: _renderObjectActor: function(objectActor, options = {}) michael@0: { michael@0: let widget = null; michael@0: let {preview} = objectActor; michael@0: michael@0: if (preview && preview.kind) { michael@0: widget = Widgets.ObjectRenderers.byKind[preview.kind]; michael@0: } michael@0: michael@0: if (!widget || (widget.canRender && !widget.canRender(objectActor))) { michael@0: widget = Widgets.ObjectRenderers.byClass[objectActor.class]; michael@0: } michael@0: michael@0: if (!widget || (widget.canRender && !widget.canRender(objectActor))) { michael@0: widget = Widgets.JSObject; michael@0: } michael@0: michael@0: let instance = new widget(this, objectActor, options).render(); michael@0: return instance.element; michael@0: }, michael@0: }); // Messages.Extended.prototype michael@0: michael@0: michael@0: michael@0: /** michael@0: * The JavaScriptEvalOutput message. michael@0: * michael@0: * @constructor michael@0: * @extends Messages.Extended michael@0: * @param object evalResponse michael@0: * The evaluation response packet received from the server. michael@0: * @param string [errorMessage] michael@0: * Optional error message to display. michael@0: */ michael@0: Messages.JavaScriptEvalOutput = function(evalResponse, errorMessage) michael@0: { michael@0: let severity = "log", msg, quoteStrings = true; michael@0: michael@0: if (errorMessage) { michael@0: severity = "error"; michael@0: msg = errorMessage; michael@0: quoteStrings = false; michael@0: } else { michael@0: msg = evalResponse.result; michael@0: } michael@0: michael@0: let options = { michael@0: className: "cm-s-mozilla", michael@0: timestamp: evalResponse.timestamp, michael@0: category: "output", michael@0: severity: severity, michael@0: quoteStrings: quoteStrings, michael@0: }; michael@0: Messages.Extended.call(this, [msg], options); michael@0: }; michael@0: michael@0: Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype; michael@0: michael@0: /** michael@0: * The ConsoleGeneric message is used for console API calls. michael@0: * michael@0: * @constructor michael@0: * @extends Messages.Extended michael@0: * @param object packet michael@0: * The Console API call packet received from the server. michael@0: */ michael@0: Messages.ConsoleGeneric = function(packet) michael@0: { michael@0: let options = { michael@0: className: "cm-s-mozilla", michael@0: timestamp: packet.timeStamp, michael@0: category: "webdev", michael@0: severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], michael@0: private: packet.private, michael@0: filterDuplicates: true, michael@0: location: { michael@0: url: packet.filename, michael@0: line: packet.lineNumber, michael@0: }, michael@0: }; michael@0: michael@0: switch (packet.level) { michael@0: case "count": { michael@0: let counter = packet.counter, label = counter.label; michael@0: if (!label) { michael@0: label = l10n.getStr("noCounterLabel"); michael@0: } michael@0: Messages.Extended.call(this, [label+ ": " + counter.count], options); michael@0: break; michael@0: } michael@0: default: michael@0: Messages.Extended.call(this, packet.arguments, options); michael@0: break; michael@0: } michael@0: michael@0: this._repeatID.consoleApiLevel = packet.level; michael@0: this._repeatID.styles = packet.styles; michael@0: this._stacktrace = this._repeatID.stacktrace = packet.stacktrace; michael@0: this._styles = packet.styles || []; michael@0: michael@0: this._onClickCollapsible = this._onClickCollapsible.bind(this); michael@0: }; michael@0: michael@0: Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype, michael@0: { michael@0: _styles: null, michael@0: _stacktrace: null, michael@0: michael@0: /** michael@0: * Tells if the message can be expanded/collapsed. michael@0: * @type boolean michael@0: */ michael@0: collapsible: false, michael@0: michael@0: /** michael@0: * Getter that tells if this message is collapsed - no details are shown. michael@0: * @type boolean michael@0: */ michael@0: get collapsed() { michael@0: return this.collapsible && this.element && !this.element.hasAttribute("open"); michael@0: }, michael@0: michael@0: _renderBodyPieceSeparator: function() michael@0: { michael@0: return this.document.createTextNode(" "); michael@0: }, michael@0: michael@0: render: function() michael@0: { michael@0: let msg = this.document.createElementNS(XHTML_NS, "span"); michael@0: msg.className = "message-body devtools-monospace"; michael@0: michael@0: this._renderBodyPieces(msg); michael@0: michael@0: let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this); michael@0: let location = Messages.Simple.prototype._renderLocation.call(this); michael@0: if (location) { michael@0: location.target = "jsdebugger"; michael@0: } michael@0: michael@0: let stack = null; michael@0: let twisty = null; michael@0: if (this._stacktrace && this._stacktrace.length > 0) { michael@0: stack = new Widgets.Stacktrace(this, this._stacktrace).render().element; michael@0: michael@0: twisty = this.document.createElementNS(XHTML_NS, "a"); michael@0: twisty.className = "theme-twisty"; michael@0: twisty.href = "#"; michael@0: twisty.title = l10n.getStr("messageToggleDetails"); michael@0: twisty.addEventListener("click", this._onClickCollapsible); michael@0: } michael@0: michael@0: let flex = this.document.createElementNS(XHTML_NS, "span"); michael@0: flex.className = "message-flex-body"; michael@0: michael@0: if (twisty) { michael@0: flex.appendChild(twisty); michael@0: } michael@0: michael@0: flex.appendChild(msg); michael@0: michael@0: if (repeatNode) { michael@0: flex.appendChild(repeatNode); michael@0: } michael@0: if (location) { michael@0: flex.appendChild(location); michael@0: } michael@0: michael@0: let result = this.document.createDocumentFragment(); michael@0: result.appendChild(flex); michael@0: michael@0: if (stack) { michael@0: result.appendChild(this.document.createTextNode("\n")); michael@0: result.appendChild(stack); michael@0: } michael@0: michael@0: this._message = result; michael@0: this._stacktrace = null; michael@0: michael@0: Messages.Simple.prototype.render.call(this); michael@0: michael@0: if (stack) { michael@0: this.collapsible = true; michael@0: this.element.setAttribute("collapsible", true); michael@0: michael@0: let icon = this.element.querySelector(".icon"); michael@0: icon.addEventListener("click", this._onClickCollapsible); michael@0: } michael@0: michael@0: return this; michael@0: }, michael@0: michael@0: _renderBody: function() michael@0: { michael@0: let body = Messages.Simple.prototype._renderBody.apply(this, arguments); michael@0: body.classList.remove("devtools-monospace", "message-body"); michael@0: return body; michael@0: }, michael@0: michael@0: _renderBodyPieces: function(container) michael@0: { michael@0: let lastStyle = null; michael@0: michael@0: for (let i = 0; i < this._messagePieces.length; i++) { michael@0: let separator = i > 0 ? this._renderBodyPieceSeparator() : null; michael@0: if (separator) { michael@0: container.appendChild(separator); michael@0: } michael@0: michael@0: let piece = this._messagePieces[i]; michael@0: let style = this._styles[i]; michael@0: michael@0: // No long string support. michael@0: if (style && typeof style == "string" ) { michael@0: lastStyle = this.cleanupStyle(style); michael@0: } michael@0: michael@0: container.appendChild(this._renderBodyPiece(piece, lastStyle)); michael@0: } michael@0: michael@0: this._messagePieces = null; michael@0: this._styles = null; michael@0: }, michael@0: michael@0: _renderBodyPiece: function(piece, style) michael@0: { michael@0: let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece); michael@0: let result = elem; michael@0: michael@0: if (style) { michael@0: if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) { michael@0: elem.style = style; michael@0: } else { michael@0: let span = this.document.createElementNS(XHTML_NS, "span"); michael@0: span.style = style; michael@0: span.appendChild(elem); michael@0: result = span; michael@0: } michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: // no-op for the message location and .repeats elements. michael@0: // |this.render()| handles customized message output. michael@0: _renderLocation: function() { }, michael@0: _renderRepeatNode: function() { }, michael@0: michael@0: /** michael@0: * Expand/collapse message details. michael@0: */ michael@0: toggleDetails: function() michael@0: { michael@0: let twisty = this.element.querySelector(".theme-twisty"); michael@0: if (this.element.hasAttribute("open")) { michael@0: this.element.removeAttribute("open"); michael@0: twisty.removeAttribute("open"); michael@0: } else { michael@0: this.element.setAttribute("open", true); michael@0: twisty.setAttribute("open", true); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The click event handler for the message expander arrow element. This method michael@0: * toggles the display of message details. michael@0: * michael@0: * @private michael@0: * @param nsIDOMEvent ev michael@0: * The DOM event object. michael@0: * @see this.toggleDetails() michael@0: */ michael@0: _onClickCollapsible: function(ev) michael@0: { michael@0: ev.preventDefault(); michael@0: this.toggleDetails(); michael@0: }, michael@0: michael@0: /** michael@0: * Given a style attribute value, return a cleaned up version of the string michael@0: * such that: michael@0: * michael@0: * - no external URL is allowed to load. See RE_CLEANUP_STYLES. michael@0: * - only some of the properties are allowed, based on a whitelist. See michael@0: * RE_ALLOWED_STYLES. michael@0: * michael@0: * @param string style michael@0: * The style string to cleanup. michael@0: * @return string michael@0: * The style value after cleanup. michael@0: */ michael@0: cleanupStyle: function(style) michael@0: { michael@0: for (let r of RE_CLEANUP_STYLES) { michael@0: style = style.replace(r, "notallowed"); michael@0: } michael@0: michael@0: let dummy = this.output._dummyElement; michael@0: if (!dummy) { michael@0: dummy = this.output._dummyElement = michael@0: this.document.createElementNS(XHTML_NS, "div"); michael@0: } michael@0: dummy.style = style; michael@0: michael@0: let toRemove = []; michael@0: for (let i = 0; i < dummy.style.length; i++) { michael@0: let prop = dummy.style[i]; michael@0: if (!RE_ALLOWED_STYLES.test(prop)) { michael@0: toRemove.push(prop); michael@0: } michael@0: } michael@0: michael@0: for (let prop of toRemove) { michael@0: dummy.style.removeProperty(prop); michael@0: } michael@0: michael@0: style = dummy.style.cssText; michael@0: michael@0: dummy.style = ""; michael@0: michael@0: return style; michael@0: }, michael@0: }); // Messages.ConsoleGeneric.prototype michael@0: michael@0: /** michael@0: * The ConsoleTrace message is used for console.trace() calls. michael@0: * michael@0: * @constructor michael@0: * @extends Messages.Simple michael@0: * @param object packet michael@0: * The Console API call packet received from the server. michael@0: */ michael@0: Messages.ConsoleTrace = function(packet) michael@0: { michael@0: let options = { michael@0: className: "cm-s-mozilla", michael@0: timestamp: packet.timeStamp, michael@0: category: "webdev", michael@0: severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], michael@0: private: packet.private, michael@0: filterDuplicates: true, michael@0: location: { michael@0: url: packet.filename, michael@0: line: packet.lineNumber, michael@0: }, michael@0: }; michael@0: michael@0: this._renderStack = this._renderStack.bind(this); michael@0: Messages.Simple.call(this, this._renderStack, options); michael@0: michael@0: this._repeatID.consoleApiLevel = packet.level; michael@0: this._stacktrace = this._repeatID.stacktrace = packet.stacktrace; michael@0: this._arguments = packet.arguments; michael@0: }; michael@0: michael@0: Messages.ConsoleTrace.prototype = Heritage.extend(Messages.Simple.prototype, michael@0: { michael@0: /** michael@0: * Holds the stackframes received from the server. michael@0: * michael@0: * @private michael@0: * @type array michael@0: */ michael@0: _stacktrace: null, michael@0: michael@0: /** michael@0: * Holds the arguments the content script passed to the console.trace() michael@0: * method. This array is cleared when the message is initialized, and michael@0: * associated actors are released. michael@0: * michael@0: * @private michael@0: * @type array michael@0: */ michael@0: _arguments: null, michael@0: michael@0: init: function() michael@0: { michael@0: let result = Messages.Simple.prototype.init.apply(this, arguments); michael@0: michael@0: // We ignore console.trace() arguments. Release object actors. michael@0: if (Array.isArray(this._arguments)) { michael@0: for (let arg of this._arguments) { michael@0: if (WebConsoleUtils.isActorGrip(arg)) { michael@0: this.output._releaseObject(arg.actor); michael@0: } michael@0: } michael@0: } michael@0: this._arguments = null; michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: render: function() michael@0: { michael@0: Messages.Simple.prototype.render.apply(this, arguments); michael@0: this.element.setAttribute("open", true); michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Render the stack frames. michael@0: * michael@0: * @private michael@0: * @return DOMElement michael@0: */ michael@0: _renderStack: function() michael@0: { michael@0: let cmvar = this.document.createElementNS(XHTML_NS, "span"); michael@0: cmvar.className = "cm-variable"; michael@0: cmvar.textContent = "console"; michael@0: michael@0: let cmprop = this.document.createElementNS(XHTML_NS, "span"); michael@0: cmprop.className = "cm-property"; michael@0: cmprop.textContent = "trace"; michael@0: michael@0: let title = this.document.createElementNS(XHTML_NS, "span"); michael@0: title.className = "message-body devtools-monospace"; michael@0: title.appendChild(cmvar); michael@0: title.appendChild(this.document.createTextNode(".")); michael@0: title.appendChild(cmprop); michael@0: title.appendChild(this.document.createTextNode("():")); michael@0: michael@0: let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this); michael@0: let location = Messages.Simple.prototype._renderLocation.call(this); michael@0: if (location) { michael@0: location.target = "jsdebugger"; michael@0: } michael@0: michael@0: let widget = new Widgets.Stacktrace(this, this._stacktrace).render(); michael@0: michael@0: let body = this.document.createElementNS(XHTML_NS, "span"); michael@0: body.className = "message-flex-body"; michael@0: body.appendChild(title); michael@0: if (repeatNode) { michael@0: body.appendChild(repeatNode); michael@0: } michael@0: if (location) { michael@0: body.appendChild(location); michael@0: } michael@0: body.appendChild(this.document.createTextNode("\n")); michael@0: michael@0: let frag = this.document.createDocumentFragment(); michael@0: frag.appendChild(body); michael@0: frag.appendChild(widget.element); michael@0: michael@0: return frag; michael@0: }, michael@0: michael@0: _renderBody: function() michael@0: { michael@0: let body = Messages.Simple.prototype._renderBody.apply(this, arguments); michael@0: body.classList.remove("devtools-monospace", "message-body"); michael@0: return body; michael@0: }, michael@0: michael@0: // no-op for the message location and .repeats elements. michael@0: // |this._renderStack| handles customized message output. michael@0: _renderLocation: function() { }, michael@0: _renderRepeatNode: function() { }, michael@0: }); // Messages.ConsoleTrace.prototype michael@0: michael@0: let Widgets = {}; michael@0: michael@0: /** michael@0: * The base widget class. michael@0: * michael@0: * @constructor michael@0: * @param object message michael@0: * The owning message. michael@0: */ michael@0: Widgets.BaseWidget = function(message) michael@0: { michael@0: this.message = message; michael@0: }; michael@0: michael@0: Widgets.BaseWidget.prototype = { michael@0: /** michael@0: * The owning message object. michael@0: * @type object michael@0: */ michael@0: message: null, michael@0: michael@0: /** michael@0: * The DOM element of the rendered widget. michael@0: * @type Element michael@0: */ michael@0: element: null, michael@0: michael@0: /** michael@0: * Getter for the DOM document that holds the output. michael@0: * @type Document michael@0: */ michael@0: get document() { michael@0: return this.message.document; michael@0: }, michael@0: michael@0: /** michael@0: * The ConsoleOutput instance that owns this widget instance. michael@0: */ michael@0: get output() { michael@0: return this.message.output; michael@0: }, michael@0: michael@0: /** michael@0: * Render the widget DOM element. michael@0: * @return this michael@0: */ michael@0: render: function() { }, michael@0: michael@0: /** michael@0: * Destroy this widget instance. michael@0: */ michael@0: destroy: function() { }, michael@0: michael@0: /** michael@0: * Helper for creating DOM elements for widgets. michael@0: * michael@0: * Usage: michael@0: * this.el("tag#id.class.names"); // create element "tag" with ID "id" and michael@0: * two class names, .class and .names. michael@0: * michael@0: * this.el("span", { attr1: "value1", ... }) // second argument can be an michael@0: * object that holds element attributes and values for the new DOM element. michael@0: * michael@0: * this.el("p", { attr1: "value1", ... }, "text content"); // the third michael@0: * argument can include the default .textContent of the new DOM element. michael@0: * michael@0: * this.el("p", "text content"); // if the second argument is not an object, michael@0: * it will be used as .textContent for the new DOM element. michael@0: * michael@0: * @param string tagNameIdAndClasses michael@0: * Tag name for the new element, optionally followed by an ID and/or michael@0: * class names. Examples: "span", "div#fooId", "div.class.names", michael@0: * "p#id.class". michael@0: * @param string|object [attributesOrTextContent] michael@0: * If this argument is an object it will be used to set the attributes michael@0: * of the new DOM element. Otherwise, the value becomes the michael@0: * .textContent of the new DOM element. michael@0: * @param string [textContent] michael@0: * If this argument is provided the value is used as the textContent of michael@0: * the new DOM element. michael@0: * @return DOMElement michael@0: * The new DOM element. michael@0: */ michael@0: el: function(tagNameIdAndClasses) michael@0: { michael@0: let attrs, text; michael@0: if (typeof arguments[1] == "object") { michael@0: attrs = arguments[1]; michael@0: text = arguments[2]; michael@0: } else { michael@0: text = arguments[1]; michael@0: } michael@0: michael@0: let tagName = tagNameIdAndClasses.split(/#|\./)[0]; michael@0: michael@0: let elem = this.document.createElementNS(XHTML_NS, tagName); michael@0: for (let name of Object.keys(attrs || {})) { michael@0: elem.setAttribute(name, attrs[name]); michael@0: } michael@0: if (text !== undefined && text !== null) { michael@0: elem.textContent = text; michael@0: } michael@0: michael@0: let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g); michael@0: for (let idOrClass of (idAndClasses || [])) { michael@0: if (idOrClass.charAt(0) == "#") { michael@0: elem.id = idOrClass.substr(1); michael@0: } else { michael@0: elem.classList.add(idOrClass.substr(1)); michael@0: } michael@0: } michael@0: michael@0: return elem; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * The timestamp widget. michael@0: * michael@0: * @constructor michael@0: * @param object message michael@0: * The owning message. michael@0: * @param number timestamp michael@0: * The UNIX timestamp to display. michael@0: */ michael@0: Widgets.MessageTimestamp = function(message, timestamp) michael@0: { michael@0: Widgets.BaseWidget.call(this, message); michael@0: this.timestamp = timestamp; michael@0: }; michael@0: michael@0: Widgets.MessageTimestamp.prototype = Heritage.extend(Widgets.BaseWidget.prototype, michael@0: { michael@0: /** michael@0: * The UNIX timestamp. michael@0: * @type number michael@0: */ michael@0: timestamp: 0, michael@0: michael@0: render: function() michael@0: { michael@0: if (this.element) { michael@0: return this; michael@0: } michael@0: michael@0: this.element = this.document.createElementNS(XHTML_NS, "span"); michael@0: this.element.className = "timestamp devtools-monospace"; michael@0: this.element.textContent = l10n.timestampString(this.timestamp) + " "; michael@0: michael@0: return this; michael@0: }, michael@0: }); // Widgets.MessageTimestamp.prototype michael@0: michael@0: michael@0: /** michael@0: * Widget used for displaying ObjectActors that have no specialised renderers. michael@0: * michael@0: * @constructor michael@0: * @param object message michael@0: * The owning message. michael@0: * @param object objectActor michael@0: * The ObjectActor to display. michael@0: * @param object [options] michael@0: * Options for displaying the given ObjectActor. See michael@0: * Messages.Extended.prototype._renderValueGrip for the available michael@0: * options. michael@0: */ michael@0: Widgets.JSObject = function(message, objectActor, options = {}) michael@0: { michael@0: Widgets.BaseWidget.call(this, message); michael@0: this.objectActor = objectActor; michael@0: this.options = options; michael@0: this._onClick = this._onClick.bind(this); michael@0: }; michael@0: michael@0: Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype, michael@0: { michael@0: /** michael@0: * The ObjectActor displayed by the widget. michael@0: * @type object michael@0: */ michael@0: objectActor: null, michael@0: michael@0: render: function() michael@0: { michael@0: if (!this.element) { michael@0: this._render(); michael@0: } michael@0: michael@0: return this; michael@0: }, michael@0: michael@0: _render: function() michael@0: { michael@0: let str = VariablesView.getString(this.objectActor, this.options); michael@0: let className = this.message.getClassNameForValueGrip(this.objectActor); michael@0: if (!className && this.objectActor.class == "Object") { michael@0: className = "cm-variable"; michael@0: } michael@0: michael@0: this.element = this._anchor(str, { className: className }); michael@0: }, michael@0: michael@0: /** michael@0: * Render an anchor with a given text content and link. michael@0: * michael@0: * @private michael@0: * @param string text michael@0: * Text to show in the anchor. michael@0: * @param object [options] michael@0: * Available options: michael@0: * - onClick (function): "click" event handler.By default a click on michael@0: * the anchor opens the variables view for the current object actor michael@0: * (this.objectActor). michael@0: * - href (string): if given the string is used as a link, and clicks michael@0: * on the anchor open the link in a new tab. michael@0: * - appendTo (DOMElement): append the element to the given DOM michael@0: * element. If not provided, the anchor is appended to |this.element| michael@0: * if it is available. If |appendTo| is provided and if it is a falsy michael@0: * value, the anchor is not appended to any element. michael@0: * @return DOMElement michael@0: * The DOM element of the new anchor. michael@0: */ michael@0: _anchor: function(text, options = {}) michael@0: { michael@0: if (!options.onClick && !options.href) { michael@0: options.onClick = this._onClick; michael@0: } michael@0: michael@0: let anchor = this.el("a", { michael@0: class: options.className, michael@0: draggable: false, michael@0: href: options.href || "#", michael@0: }, text); michael@0: michael@0: this.message._addLinkCallback(anchor, !options.href ? options.onClick : null); michael@0: michael@0: if (options.appendTo) { michael@0: options.appendTo.appendChild(anchor); michael@0: } else if (!("appendTo" in options) && this.element) { michael@0: this.element.appendChild(anchor); michael@0: } michael@0: michael@0: return anchor; michael@0: }, michael@0: michael@0: /** michael@0: * The click event handler for objects shown inline. michael@0: * @private michael@0: */ michael@0: _onClick: function() michael@0: { michael@0: this.output.openVariablesView({ michael@0: label: VariablesView.getString(this.objectActor, { concise: true }), michael@0: objectActor: this.objectActor, michael@0: autofocus: true, michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Add a string to the message. michael@0: * michael@0: * @private michael@0: * @param string str michael@0: * String to add. michael@0: * @param DOMElement [target = this.element] michael@0: * Optional DOM element to append the string to. The default is michael@0: * this.element. michael@0: */ michael@0: _text: function(str, target = this.element) michael@0: { michael@0: target.appendChild(this.document.createTextNode(str)); michael@0: }, michael@0: }); // Widgets.JSObject.prototype michael@0: michael@0: Widgets.ObjectRenderers = {}; michael@0: Widgets.ObjectRenderers.byKind = {}; michael@0: Widgets.ObjectRenderers.byClass = {}; michael@0: michael@0: /** michael@0: * Add an object renderer. michael@0: * michael@0: * @param object obj michael@0: * An object that represents the renderer. Properties: michael@0: * - byClass (string, optional): this renderer will be used for the given michael@0: * object class. michael@0: * - byKind (string, optional): this renderer will be used for the given michael@0: * object kind. michael@0: * One of byClass or byKind must be provided. michael@0: * - extends (object, optional): the renderer object extends the given michael@0: * object. Default: Widgets.JSObject. michael@0: * - canRender (function, optional): this method is invoked when michael@0: * a candidate object needs to be displayed. The method is invoked as michael@0: * a static method, as such, none of the properties of the renderer michael@0: * object will be available. You get one argument: the object actor grip michael@0: * received from the server. If the method returns true, then this michael@0: * renderer is used for displaying the object, otherwise not. michael@0: * - initialize (function, optional): the constructor of the renderer michael@0: * widget. This function is invoked with the following arguments: the michael@0: * owner message object instance, the object actor grip to display, and michael@0: * an options object. See Messages.Extended.prototype._renderValueGrip() michael@0: * for details about the options object. michael@0: * - render (function, required): the method that displays the given michael@0: * object actor. michael@0: */ michael@0: Widgets.ObjectRenderers.add = function(obj) michael@0: { michael@0: let extendObj = obj.extends || Widgets.JSObject; michael@0: michael@0: let constructor = function() { michael@0: if (obj.initialize) { michael@0: obj.initialize.apply(this, arguments); michael@0: } else { michael@0: extendObj.apply(this, arguments); michael@0: } michael@0: }; michael@0: michael@0: let proto = WebConsoleUtils.cloneObject(obj, false, function(key) { michael@0: if (key == "initialize" || key == "canRender" || michael@0: (key == "render" && extendObj === Widgets.JSObject)) { michael@0: return false; michael@0: } michael@0: return true; michael@0: }); michael@0: michael@0: if (extendObj === Widgets.JSObject) { michael@0: proto._render = obj.render; michael@0: } michael@0: michael@0: constructor.canRender = obj.canRender; michael@0: constructor.prototype = Heritage.extend(extendObj.prototype, proto); michael@0: michael@0: if (obj.byClass) { michael@0: Widgets.ObjectRenderers.byClass[obj.byClass] = constructor; michael@0: } else if (obj.byKind) { michael@0: Widgets.ObjectRenderers.byKind[obj.byKind] = constructor; michael@0: } else { michael@0: throw new Error("You are adding an object renderer without any byClass or " + michael@0: "byKind property."); michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * The widget used for displaying Date objects. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byClass: "Date", michael@0: michael@0: render: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: this.element = this.el("span.class-" + this.objectActor.class); michael@0: michael@0: let anchorText = this.objectActor.class; michael@0: let anchorClass = "cm-variable"; michael@0: if ("timestamp" in preview && typeof preview.timestamp != "number") { michael@0: anchorText = new Date(preview.timestamp).toString(); // invalid date michael@0: anchorClass = ""; michael@0: } michael@0: michael@0: this._anchor(anchorText, { className: anchorClass }); michael@0: michael@0: if (!("timestamp" in preview) || typeof preview.timestamp != "number") { michael@0: return; michael@0: } michael@0: michael@0: this._text(" "); michael@0: michael@0: let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString()); michael@0: this.element.appendChild(elem); michael@0: }, michael@0: }); michael@0: michael@0: /** michael@0: * The widget used for displaying Function objects. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byClass: "Function", michael@0: michael@0: render: function() michael@0: { michael@0: let grip = this.objectActor; michael@0: this.element = this.el("span.class-" + this.objectActor.class); michael@0: michael@0: // TODO: Bug 948484 - support arrow functions and ES6 generators michael@0: let name = grip.userDisplayName || grip.displayName || grip.name || ""; michael@0: name = VariablesView.getString(name, { noStringQuotes: true }); michael@0: michael@0: let str = this.options.concise ? name || "function " : "function " + name; michael@0: michael@0: if (this.options.concise) { michael@0: this._anchor(name || "function", { michael@0: className: name ? "cm-variable" : "cm-keyword", michael@0: }); michael@0: if (!name) { michael@0: this._text(" "); michael@0: } michael@0: } else if (name) { michael@0: this.element.appendChild(this.el("span.cm-keyword", "function")); michael@0: this._text(" "); michael@0: this._anchor(name, { className: "cm-variable" }); michael@0: } else { michael@0: this._anchor("function", { className: "cm-keyword" }); michael@0: this._text(" "); michael@0: } michael@0: michael@0: this._text("("); michael@0: michael@0: // TODO: Bug 948489 - Support functions with destructured parameters and michael@0: // rest parameters michael@0: let params = grip.parameterNames || []; michael@0: let shown = 0; michael@0: for (let param of params) { michael@0: if (shown > 0) { michael@0: this._text(", "); michael@0: } michael@0: this.element.appendChild(this.el("span.cm-def", param)); michael@0: shown++; michael@0: } michael@0: michael@0: this._text(")"); michael@0: }, michael@0: }); // Widgets.ObjectRenderers.byClass.Function michael@0: michael@0: /** michael@0: * The widget used for displaying ArrayLike objects. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byKind: "ArrayLike", michael@0: michael@0: render: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: let {items} = preview; michael@0: this.element = this.el("span.kind-" + preview.kind); michael@0: michael@0: this._anchor(this.objectActor.class, { className: "cm-variable" }); michael@0: michael@0: if (!items || this.options.concise) { michael@0: this._text("["); michael@0: this.element.appendChild(this.el("span.cm-number", preview.length)); michael@0: this._text("]"); michael@0: return this; michael@0: } michael@0: michael@0: this._text(" [ "); michael@0: michael@0: let shown = 0; michael@0: for (let item of items) { michael@0: if (shown > 0) { michael@0: this._text(", "); michael@0: } michael@0: michael@0: if (item !== null) { michael@0: let elem = this.message._renderValueGrip(item, { concise: true }); michael@0: this.element.appendChild(elem); michael@0: } else if (shown == (items.length - 1)) { michael@0: this._text(", "); michael@0: } michael@0: michael@0: shown++; michael@0: } michael@0: michael@0: if (shown < preview.length) { michael@0: this._text(", "); michael@0: michael@0: let n = preview.length - shown; michael@0: let str = VariablesView.stringifiers._getNMoreString(n); michael@0: this._anchor(str); michael@0: } michael@0: michael@0: this._text(" ]"); michael@0: }, michael@0: }); // Widgets.ObjectRenderers.byKind.ArrayLike michael@0: michael@0: /** michael@0: * The widget used for displaying MapLike objects. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byKind: "MapLike", michael@0: michael@0: render: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: let {entries} = preview; michael@0: michael@0: let container = this.element = this.el("span.kind-" + preview.kind); michael@0: this._anchor(this.objectActor.class, { className: "cm-variable" }); michael@0: michael@0: if (!entries || this.options.concise) { michael@0: if (typeof preview.size == "number") { michael@0: this._text("["); michael@0: container.appendChild(this.el("span.cm-number", preview.size)); michael@0: this._text("]"); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: this._text(" { "); michael@0: michael@0: let shown = 0; michael@0: for (let [key, value] of entries) { michael@0: if (shown > 0) { michael@0: this._text(", "); michael@0: } michael@0: michael@0: let keyElem = this.message._renderValueGrip(key, { michael@0: concise: true, michael@0: noStringQuotes: true, michael@0: }); michael@0: michael@0: // Strings are property names. michael@0: if (keyElem.classList && keyElem.classList.contains("console-string")) { michael@0: keyElem.classList.remove("console-string"); michael@0: keyElem.classList.add("cm-property"); michael@0: } michael@0: michael@0: container.appendChild(keyElem); michael@0: michael@0: this._text(": "); michael@0: michael@0: let valueElem = this.message._renderValueGrip(value, { concise: true }); michael@0: container.appendChild(valueElem); michael@0: michael@0: shown++; michael@0: } michael@0: michael@0: if (typeof preview.size == "number" && shown < preview.size) { michael@0: this._text(", "); michael@0: michael@0: let n = preview.size - shown; michael@0: let str = VariablesView.stringifiers._getNMoreString(n); michael@0: this._anchor(str); michael@0: } michael@0: michael@0: this._text(" }"); michael@0: }, michael@0: }); // Widgets.ObjectRenderers.byKind.MapLike michael@0: michael@0: /** michael@0: * The widget used for displaying objects with a URL. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byKind: "ObjectWithURL", michael@0: michael@0: render: function() michael@0: { michael@0: this.element = this._renderElement(this.objectActor, michael@0: this.objectActor.preview.url); michael@0: }, michael@0: michael@0: _renderElement: function(objectActor, url) michael@0: { michael@0: let container = this.el("span.kind-" + objectActor.preview.kind); michael@0: michael@0: this._anchor(objectActor.class, { michael@0: className: "cm-variable", michael@0: appendTo: container, michael@0: }); michael@0: michael@0: if (!VariablesView.isFalsy({ value: url })) { michael@0: this._text(" \u2192 ", container); michael@0: let shortUrl = WebConsoleUtils.abbreviateSourceURL(url, { michael@0: onlyCropQuery: !this.options.concise michael@0: }); michael@0: this._anchor(shortUrl, { href: url, appendTo: container }); michael@0: } michael@0: michael@0: return container; michael@0: }, michael@0: }); // Widgets.ObjectRenderers.byKind.ObjectWithURL michael@0: michael@0: /** michael@0: * The widget used for displaying objects with a string next to them. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byKind: "ObjectWithText", michael@0: michael@0: render: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: this.element = this.el("span.kind-" + preview.kind); michael@0: michael@0: this._anchor(this.objectActor.class, { className: "cm-variable" }); michael@0: michael@0: if (!this.options.concise) { michael@0: this._text(" "); michael@0: this.element.appendChild(this.el("span.console-string", michael@0: VariablesView.getString(preview.text))); michael@0: } michael@0: }, michael@0: }); michael@0: michael@0: /** michael@0: * The widget used for displaying DOM event previews. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byKind: "DOMEvent", michael@0: michael@0: render: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: michael@0: let container = this.element = this.el("span.kind-" + preview.kind); michael@0: michael@0: this._anchor(preview.type || this.objectActor.class, michael@0: { className: "cm-variable" }); michael@0: michael@0: if (this.options.concise) { michael@0: return; michael@0: } michael@0: michael@0: if (preview.eventKind == "key" && preview.modifiers && michael@0: preview.modifiers.length) { michael@0: this._text(" "); michael@0: michael@0: let mods = 0; michael@0: for (let mod of preview.modifiers) { michael@0: if (mods > 0) { michael@0: this._text("-"); michael@0: } michael@0: container.appendChild(this.el("span.cm-keyword", mod)); michael@0: mods++; michael@0: } michael@0: } michael@0: michael@0: this._text(" { "); michael@0: michael@0: let shown = 0; michael@0: if (preview.target) { michael@0: container.appendChild(this.el("span.cm-property", "target")); michael@0: this._text(": "); michael@0: let target = this.message._renderValueGrip(preview.target, { concise: true }); michael@0: container.appendChild(target); michael@0: shown++; michael@0: } michael@0: michael@0: for (let key of Object.keys(preview.properties || {})) { michael@0: if (shown > 0) { michael@0: this._text(", "); michael@0: } michael@0: michael@0: container.appendChild(this.el("span.cm-property", key)); michael@0: this._text(": "); michael@0: michael@0: let value = preview.properties[key]; michael@0: let valueElem = this.message._renderValueGrip(value, { concise: true }); michael@0: container.appendChild(valueElem); michael@0: michael@0: shown++; michael@0: } michael@0: michael@0: this._text(" }"); michael@0: }, michael@0: }); // Widgets.ObjectRenderers.byKind.DOMEvent michael@0: michael@0: /** michael@0: * The widget used for displaying DOM node previews. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byKind: "DOMNode", michael@0: michael@0: canRender: function(objectActor) { michael@0: let {preview} = objectActor; michael@0: if (!preview) { michael@0: return false; michael@0: } michael@0: michael@0: switch (preview.nodeType) { michael@0: case Ci.nsIDOMNode.DOCUMENT_NODE: michael@0: case Ci.nsIDOMNode.ATTRIBUTE_NODE: michael@0: case Ci.nsIDOMNode.TEXT_NODE: michael@0: case Ci.nsIDOMNode.COMMENT_NODE: michael@0: case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: michael@0: case Ci.nsIDOMNode.ELEMENT_NODE: michael@0: return true; michael@0: default: michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: render: function() michael@0: { michael@0: switch (this.objectActor.preview.nodeType) { michael@0: case Ci.nsIDOMNode.DOCUMENT_NODE: michael@0: this._renderDocumentNode(); michael@0: break; michael@0: case Ci.nsIDOMNode.ATTRIBUTE_NODE: { michael@0: let {preview} = this.objectActor; michael@0: this.element = this.el("span.attributeNode.kind-" + preview.kind); michael@0: let attr = this._renderAttributeNode(preview.nodeName, preview.value, true); michael@0: this.element.appendChild(attr); michael@0: break; michael@0: } michael@0: case Ci.nsIDOMNode.TEXT_NODE: michael@0: this._renderTextNode(); michael@0: break; michael@0: case Ci.nsIDOMNode.COMMENT_NODE: michael@0: this._renderCommentNode(); michael@0: break; michael@0: case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: michael@0: this._renderDocumentFragmentNode(); michael@0: break; michael@0: case Ci.nsIDOMNode.ELEMENT_NODE: michael@0: this._renderElementNode(); michael@0: break; michael@0: default: michael@0: throw new Error("Unsupported nodeType: " + preview.nodeType); michael@0: } michael@0: }, michael@0: michael@0: _renderDocumentNode: function() michael@0: { michael@0: let fn = Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement; michael@0: this.element = fn.call(this, this.objectActor, michael@0: this.objectActor.preview.location); michael@0: this.element.classList.add("documentNode"); michael@0: }, michael@0: michael@0: _renderAttributeNode: function(nodeName, nodeValue, addLink) michael@0: { michael@0: let value = VariablesView.getString(nodeValue, { noStringQuotes: true }); michael@0: michael@0: let fragment = this.document.createDocumentFragment(); michael@0: if (addLink) { michael@0: this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment }); michael@0: } else { michael@0: fragment.appendChild(this.el("span.cm-attribute", nodeName)); michael@0: } michael@0: michael@0: this._text("=", fragment); michael@0: fragment.appendChild(this.el("span.console-string", michael@0: '"' + escapeHTML(value) + '"')); michael@0: michael@0: return fragment; michael@0: }, michael@0: michael@0: _renderTextNode: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: this.element = this.el("span.textNode.kind-" + preview.kind); michael@0: michael@0: this._anchor(preview.nodeName, { className: "cm-variable" }); michael@0: this._text(" "); michael@0: michael@0: let text = VariablesView.getString(preview.textContent); michael@0: this.element.appendChild(this.el("span.console-string", text)); michael@0: }, michael@0: michael@0: _renderCommentNode: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: let comment = ""; michael@0: michael@0: this.element = this._anchor(comment, { michael@0: className: "kind-" + preview.kind + " commentNode cm-comment", michael@0: }); michael@0: }, michael@0: michael@0: _renderDocumentFragmentNode: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: let {childNodes} = preview; michael@0: let container = this.element = this.el("span.documentFragmentNode.kind-" + michael@0: preview.kind); michael@0: michael@0: this._anchor(this.objectActor.class, { className: "cm-variable" }); michael@0: michael@0: if (!childNodes || this.options.concise) { michael@0: this._text("["); michael@0: container.appendChild(this.el("span.cm-number", preview.childNodesLength)); michael@0: this._text("]"); michael@0: return; michael@0: } michael@0: michael@0: this._text(" [ "); michael@0: michael@0: let shown = 0; michael@0: for (let item of childNodes) { michael@0: if (shown > 0) { michael@0: this._text(", "); michael@0: } michael@0: michael@0: let elem = this.message._renderValueGrip(item, { concise: true }); michael@0: container.appendChild(elem); michael@0: shown++; michael@0: } michael@0: michael@0: if (shown < preview.childNodesLength) { michael@0: this._text(", "); michael@0: michael@0: let n = preview.childNodesLength - shown; michael@0: let str = VariablesView.stringifiers._getNMoreString(n); michael@0: this._anchor(str); michael@0: } michael@0: michael@0: this._text(" ]"); michael@0: }, michael@0: michael@0: _renderElementNode: function() michael@0: { michael@0: let doc = this.document; michael@0: let {attributes, nodeName} = this.objectActor.preview; michael@0: michael@0: this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode"); michael@0: michael@0: let openTag = this.el("span.cm-tag"); michael@0: openTag.textContent = "<"; michael@0: this.element.appendChild(openTag); michael@0: michael@0: let tagName = this._anchor(nodeName, { michael@0: className: "cm-tag", michael@0: appendTo: openTag michael@0: }); michael@0: michael@0: if (this.options.concise) { michael@0: if (attributes.id) { michael@0: tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id)); michael@0: } michael@0: if (attributes.class) { michael@0: tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join("."))); michael@0: } michael@0: } else { michael@0: for (let name of Object.keys(attributes)) { michael@0: let attr = this._renderAttributeNode(" " + name, attributes[name]); michael@0: this.element.appendChild(attr); michael@0: } michael@0: } michael@0: michael@0: let closeTag = this.el("span.cm-tag"); michael@0: closeTag.textContent = ">"; michael@0: this.element.appendChild(closeTag); michael@0: michael@0: // Register this widget in the owner message so that it gets destroyed when michael@0: // the message is destroyed. michael@0: this.message.widgets.add(this); michael@0: michael@0: this.linkToInspector(); michael@0: }, michael@0: michael@0: /** michael@0: * If the DOMNode being rendered can be highlit in the page, this function michael@0: * will attach mouseover/out event listeners to do so, and the inspector icon michael@0: * to open the node in the inspector. michael@0: * @return a promise (always the same) that resolves when the node has been michael@0: * linked to the inspector, or rejects if it wasn't (either if no toolbox michael@0: * could be found to access the inspector, or if the node isn't present in the michael@0: * inspector, i.e. if the node is in a DocumentFragment or not part of the michael@0: * tree, or not of type Ci.nsIDOMNode.ELEMENT_NODE). michael@0: */ michael@0: linkToInspector: function() michael@0: { michael@0: if (this._linkedToInspector) { michael@0: return this._linkedToInspector; michael@0: } michael@0: michael@0: this._linkedToInspector = Task.spawn(function*() { michael@0: // Checking the node type michael@0: if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { michael@0: throw null; michael@0: } michael@0: michael@0: // Checking the presence of a toolbox michael@0: let target = this.message.output.toolboxTarget; michael@0: this.toolbox = gDevTools.getToolbox(target); michael@0: if (!this.toolbox) { michael@0: throw null; michael@0: } michael@0: michael@0: // Checking that the inspector supports the node michael@0: yield this.toolbox.initInspector(); michael@0: this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor); michael@0: if (!this._nodeFront) { michael@0: throw null; michael@0: } michael@0: michael@0: // At this stage, the message may have been cleared already michael@0: if (!this.document) { michael@0: throw null; michael@0: } michael@0: michael@0: this.highlightDomNode = this.highlightDomNode.bind(this); michael@0: this.element.addEventListener("mouseover", this.highlightDomNode, false); michael@0: this.unhighlightDomNode = this.unhighlightDomNode.bind(this); michael@0: this.element.addEventListener("mouseout", this.unhighlightDomNode, false); michael@0: michael@0: this._openInspectorNode = this._anchor("", { michael@0: className: "open-inspector", michael@0: onClick: this.openNodeInInspector.bind(this) michael@0: }); michael@0: this._openInspectorNode.title = l10n.getStr("openNodeInInspector"); michael@0: }.bind(this)); michael@0: michael@0: return this._linkedToInspector; michael@0: }, michael@0: michael@0: /** michael@0: * Highlight the DOMNode corresponding to the ObjectActor in the page. michael@0: * @return a promise that resolves when the node has been highlighted, or michael@0: * rejects if the node cannot be highlighted (detached from the DOM) michael@0: */ michael@0: highlightDomNode: function() michael@0: { michael@0: return Task.spawn(function*() { michael@0: yield this.linkToInspector(); michael@0: let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront); michael@0: if (isAttached) { michael@0: yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront); michael@0: } else { michael@0: throw null; michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Unhighlight a previously highlit node michael@0: * @see highlightDomNode michael@0: * @return a promise that resolves when the highlighter has been hidden michael@0: */ michael@0: unhighlightDomNode: function() michael@0: { michael@0: return this.linkToInspector().then(() => { michael@0: return this.toolbox.highlighterUtils.unhighlight(); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Open the DOMNode corresponding to the ObjectActor in the inspector panel michael@0: * @return a promise that resolves when the inspector has been switched to michael@0: * and the node has been selected, or rejects if the node cannot be selected michael@0: * (detached from the DOM). Note that in any case, the inspector panel will michael@0: * be switched to. michael@0: */ michael@0: openNodeInInspector: function() michael@0: { michael@0: return Task.spawn(function*() { michael@0: yield this.linkToInspector(); michael@0: yield this.toolbox.selectTool("inspector"); michael@0: michael@0: let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront); michael@0: if (isAttached) { michael@0: let onReady = this.toolbox.inspector.once("inspector-updated"); michael@0: yield this.toolbox.selection.setNodeFront(this._nodeFront, "console"); michael@0: yield onReady; michael@0: } else { michael@0: throw null; michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: destroy: function() michael@0: { michael@0: if (this.toolbox && this._nodeFront) { michael@0: this.element.removeEventListener("mouseover", this.highlightDomNode, false); michael@0: this.element.removeEventListener("mouseout", this.unhighlightDomNode, false); michael@0: this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true); michael@0: this.toolbox = null; michael@0: this._nodeFront = null; michael@0: } michael@0: }, michael@0: }); // Widgets.ObjectRenderers.byKind.DOMNode michael@0: michael@0: /** michael@0: * The widget used for displaying generic JS object previews. michael@0: */ michael@0: Widgets.ObjectRenderers.add({ michael@0: byKind: "Object", michael@0: michael@0: render: function() michael@0: { michael@0: let {preview} = this.objectActor; michael@0: let {ownProperties, safeGetterValues} = preview; michael@0: michael@0: if ((!ownProperties && !safeGetterValues) || this.options.concise) { michael@0: this.element = this._anchor(this.objectActor.class, michael@0: { className: "cm-variable" }); michael@0: return; michael@0: } michael@0: michael@0: let container = this.element = this.el("span.kind-" + preview.kind); michael@0: this._anchor(this.objectActor.class, { className: "cm-variable" }); michael@0: this._text(" { "); michael@0: michael@0: let addProperty = (str) => { michael@0: container.appendChild(this.el("span.cm-property", str)); michael@0: }; michael@0: michael@0: let shown = 0; michael@0: for (let key of Object.keys(ownProperties || {})) { michael@0: if (shown > 0) { michael@0: this._text(", "); michael@0: } michael@0: michael@0: let value = ownProperties[key]; michael@0: michael@0: addProperty(key); michael@0: this._text(": "); michael@0: michael@0: if (value.get) { michael@0: addProperty("Getter"); michael@0: } else if (value.set) { michael@0: addProperty("Setter"); michael@0: } else { michael@0: let valueElem = this.message._renderValueGrip(value.value, { concise: true }); michael@0: container.appendChild(valueElem); michael@0: } michael@0: michael@0: shown++; michael@0: } michael@0: michael@0: let ownPropertiesShown = shown; michael@0: michael@0: for (let key of Object.keys(safeGetterValues || {})) { michael@0: if (shown > 0) { michael@0: this._text(", "); michael@0: } michael@0: michael@0: addProperty(key); michael@0: this._text(": "); michael@0: michael@0: let value = safeGetterValues[key].getterValue; michael@0: let valueElem = this.message._renderValueGrip(value, { concise: true }); michael@0: container.appendChild(valueElem); michael@0: michael@0: shown++; michael@0: } michael@0: michael@0: if (typeof preview.ownPropertiesLength == "number" && michael@0: ownPropertiesShown < preview.ownPropertiesLength) { michael@0: this._text(", "); michael@0: michael@0: let n = preview.ownPropertiesLength - ownPropertiesShown; michael@0: let str = VariablesView.stringifiers._getNMoreString(n); michael@0: this._anchor(str); michael@0: } michael@0: michael@0: this._text(" }"); michael@0: }, michael@0: }); // Widgets.ObjectRenderers.byKind.Object michael@0: michael@0: /** michael@0: * The long string widget. michael@0: * michael@0: * @constructor michael@0: * @param object message michael@0: * The owning message. michael@0: * @param object longStringActor michael@0: * The LongStringActor to display. michael@0: */ michael@0: Widgets.LongString = function(message, longStringActor) michael@0: { michael@0: Widgets.BaseWidget.call(this, message); michael@0: this.longStringActor = longStringActor; michael@0: this._onClick = this._onClick.bind(this); michael@0: this._onSubstring = this._onSubstring.bind(this); michael@0: }; michael@0: michael@0: Widgets.LongString.prototype = Heritage.extend(Widgets.BaseWidget.prototype, michael@0: { michael@0: /** michael@0: * The LongStringActor displayed by the widget. michael@0: * @type object michael@0: */ michael@0: longStringActor: null, michael@0: michael@0: render: function() michael@0: { michael@0: if (this.element) { michael@0: return this; michael@0: } michael@0: michael@0: let result = this.element = this.document.createElementNS(XHTML_NS, "span"); michael@0: result.className = "longString console-string"; michael@0: this._renderString(this.longStringActor.initial); michael@0: result.appendChild(this._renderEllipsis()); michael@0: michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Render the long string in the widget element. michael@0: * @private michael@0: * @param string str michael@0: * The string to display. michael@0: */ michael@0: _renderString: function(str) michael@0: { michael@0: this.element.textContent = VariablesView.getString(str, { michael@0: noStringQuotes: !this.message._quoteStrings, michael@0: noEllipsis: true, michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Render the anchor ellipsis that allows the user to expand the long string. michael@0: * michael@0: * @private michael@0: * @return Element michael@0: */ michael@0: _renderEllipsis: function() michael@0: { michael@0: let ellipsis = this.document.createElementNS(XHTML_NS, "a"); michael@0: ellipsis.className = "longStringEllipsis"; michael@0: ellipsis.textContent = l10n.getStr("longStringEllipsis"); michael@0: ellipsis.href = "#"; michael@0: ellipsis.draggable = false; michael@0: this.message._addLinkCallback(ellipsis, this._onClick); michael@0: michael@0: return ellipsis; michael@0: }, michael@0: michael@0: /** michael@0: * The click event handler for the ellipsis shown after the short string. This michael@0: * function expands the element to show the full string. michael@0: * @private michael@0: */ michael@0: _onClick: function() michael@0: { michael@0: let longString = this.output.webConsoleClient.longString(this.longStringActor); michael@0: let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH); michael@0: michael@0: longString.substring(longString.initial.length, toIndex, this._onSubstring); michael@0: }, michael@0: michael@0: /** michael@0: * The longString substring response callback. michael@0: * michael@0: * @private michael@0: * @param object response michael@0: * Response packet. michael@0: */ michael@0: _onSubstring: function(response) michael@0: { michael@0: if (response.error) { michael@0: Cu.reportError("LongString substring failure: " + response.error); michael@0: return; michael@0: } michael@0: michael@0: this.element.lastChild.remove(); michael@0: this.element.classList.remove("longString"); michael@0: michael@0: this._renderString(this.longStringActor.initial + response.substring); michael@0: michael@0: this.output.owner.emit("messages-updated", new Set([this.message.element])); michael@0: michael@0: let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH); michael@0: if (toIndex != this.longStringActor.length) { michael@0: this._logWarningAboutStringTooLong(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Inform user that the string he tries to view is too long. michael@0: * @private michael@0: */ michael@0: _logWarningAboutStringTooLong: function() michael@0: { michael@0: let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), { michael@0: category: "output", michael@0: severity: "warning", michael@0: }); michael@0: this.output.addMessage(msg); michael@0: }, michael@0: }); // Widgets.LongString.prototype michael@0: michael@0: michael@0: /** michael@0: * The stacktrace widget. michael@0: * michael@0: * @constructor michael@0: * @extends Widgets.BaseWidget michael@0: * @param object message michael@0: * The owning message. michael@0: * @param array stacktrace michael@0: * The stacktrace to display, array of frames as supplied by the server, michael@0: * over the remote protocol. michael@0: */ michael@0: Widgets.Stacktrace = function(message, stacktrace) michael@0: { michael@0: Widgets.BaseWidget.call(this, message); michael@0: this.stacktrace = stacktrace; michael@0: }; michael@0: michael@0: Widgets.Stacktrace.prototype = Heritage.extend(Widgets.BaseWidget.prototype, michael@0: { michael@0: /** michael@0: * The stackframes received from the server. michael@0: * @type array michael@0: */ michael@0: stacktrace: null, michael@0: michael@0: render: function() michael@0: { michael@0: if (this.element) { michael@0: return this; michael@0: } michael@0: michael@0: let result = this.element = this.document.createElementNS(XHTML_NS, "ul"); michael@0: result.className = "stacktrace devtools-monospace"; michael@0: michael@0: for (let frame of this.stacktrace) { michael@0: result.appendChild(this._renderFrame(frame)); michael@0: } michael@0: michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Render a frame object received from the server. michael@0: * michael@0: * @param object frame michael@0: * The stack frame to display. This object should have the following michael@0: * properties: functionName, filename and lineNumber. michael@0: * @return DOMElement michael@0: * The DOM element to display for the given frame. michael@0: */ michael@0: _renderFrame: function(frame) michael@0: { michael@0: let fn = this.document.createElementNS(XHTML_NS, "span"); michael@0: fn.className = "function"; michael@0: if (frame.functionName) { michael@0: let span = this.document.createElementNS(XHTML_NS, "span"); michael@0: span.className = "cm-variable"; michael@0: span.textContent = frame.functionName; michael@0: fn.appendChild(span); michael@0: fn.appendChild(this.document.createTextNode("()")); michael@0: } else { michael@0: fn.classList.add("cm-comment"); michael@0: fn.textContent = l10n.getStr("stacktrace.anonymousFunction"); michael@0: } michael@0: michael@0: let location = this.output.owner.createLocationNode(frame.filename, michael@0: frame.lineNumber, michael@0: "jsdebugger"); michael@0: michael@0: // .devtools-monospace sets font-size to 80%, however .body already has michael@0: // .devtools-monospace. If we keep it here, the location would be rendered michael@0: // smaller. michael@0: location.classList.remove("devtools-monospace"); michael@0: michael@0: let elem = this.document.createElementNS(XHTML_NS, "li"); michael@0: elem.appendChild(fn); michael@0: elem.appendChild(location); michael@0: elem.appendChild(this.document.createTextNode("\n")); michael@0: michael@0: return elem; michael@0: }, michael@0: }); // Widgets.Stacktrace.prototype michael@0: michael@0: michael@0: function gSequenceId() michael@0: { michael@0: return gSequenceId.n++; michael@0: } michael@0: gSequenceId.n = 0; michael@0: michael@0: exports.ConsoleOutput = ConsoleOutput; michael@0: exports.Messages = Messages; michael@0: exports.Widgets = Widgets;