/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Ci, Cu} = require("chrome"); loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); loader.lazyImporter(this, "escapeHTML", "resource://devtools/client/shared/widgets/VariablesView.jsm"); loader.lazyRequireGetter(this, "promise"); loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); loader.lazyRequireGetter(this, "TableWidget", "devtools/client/shared/widgets/TableWidget", true); loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); const { extend } = require("sdk/core/heritage"); const XHTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const STRINGS_URI = "devtools/client/locales/webconsole.properties"; const WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; const { getSourceNames } = require("devtools/client/shared/source-utils"); const {Task} = require("devtools/shared/task"); const l10n = new WebConsoleUtils.L10n(STRINGS_URI); const nodeConstants = require("devtools/shared/dom-node-constants"); const {PluralForm} = require("devtools/shared/plural-form"); const MAX_STRING_GRIP_LENGTH = 36; const {ELLIPSIS} = require("devtools/shared/l10n"); const validProtocols = /^(http|https|ftp|resource|chrome):/i; // Constants for compatibility with the Web Console output implementation before // bug 778766. // TODO: remove these once bug 778766 is fixed. const COMPAT = { // The various categories of messages. CATEGORIES: { NETWORK: 0, CSS: 1, JS: 2, WEBDEV: 3, INPUT: 4, OUTPUT: 5, SECURITY: 6, SERVER: 7, }, // The possible message severities. SEVERITIES: { ERROR: 0, WARNING: 1, INFO: 2, LOG: 3, }, // The preference keys to use for each category/severity combination, indexed // first by category (rows) and then by severity (columns). // // Most of these rather idiosyncratic names are historical and predate the // division of message type into "category" and "severity". /* eslint-disable no-multi-spaces */ /* eslint-disable max-len */ /* eslint-disable no-inline-comments */ PREFERENCE_KEYS: [ // Error Warning Info Log [ "network", "netwarn", null, "networkinfo", ], // Network [ "csserror", "cssparser", null, null, ], // CSS [ "exception", "jswarn", null, "jslog", ], // JS [ "error", "warn", "info", "log", ], // Web Developer [ null, null, null, null, ], // Input [ null, null, null, null, ], // Output [ "secerror", "secwarn", null, null, ], // Security [ "servererror", "serverwarn", "serverinfo", "serverlog", ], // Server Logging ], /* eslint-enable no-inline-comments */ /* eslint-enable max-len */ /* eslint-enable no-multi-spaces */ // The fragment of a CSS class name that identifies each category. CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console", "input", "output", "security", "server" ], // The fragment of a CSS class name that identifies each severity. SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ], // The indent of a console group in pixels. GROUP_INDENT: 12, }; // A map from the console API call levels to the Web Console severities. const CONSOLE_API_LEVELS_TO_SEVERITIES = { error: "error", exception: "error", assert: "error", warn: "warning", info: "info", log: "log", clear: "log", trace: "log", table: "log", debug: "log", dir: "log", dirxml: "log", group: "log", groupCollapsed: "log", groupEnd: "log", time: "log", timeEnd: "log", count: "log" }; // Array of known message source URLs we need to hide from output. const IGNORED_SOURCE_URLS = ["debugger eval code"]; // The maximum length of strings to be displayed by the Web Console. const MAX_LONG_STRING_LENGTH = 200000; // Regular expression that matches the allowed CSS property names when using // the `window.console` API. 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)/; // Regular expressions to search and replace with 'notallowed' in the styles // given to the `window.console` API methods. const RE_CLEANUP_STYLES = [ // url(), -moz-element() /\b(?:url|(?:-moz-)?element)[\s('"]+/gi, // various URL protocols /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi, ]; // Maximum number of rows to display in console.table(). const TABLE_ROW_MAX_ITEMS = 1000; // Maximum number of columns to display in console.table(). const TABLE_COLUMN_MAX_ITEMS = 10; /** * The ConsoleOutput object is used to manage output of messages in the Web * Console. * * @constructor * @param object owner * The console output owner. This usually the WebConsoleFrame instance. * Any other object can be used, as long as it has the following * properties and methods: * - window * - document * - outputMessage(category, methodOrNode[, methodArguments]) * TODO: this is needed temporarily, until bug 778766 is fixed. */ function ConsoleOutput(owner) { this.owner = owner; this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this); } ConsoleOutput.prototype = { _dummyElement: null, /** * The output container. * @type DOMElement */ get element() { return this.owner.outputNode; }, /** * The document that holds the output. * @type DOMDocument */ get document() { return this.owner ? this.owner.document : null; }, /** * The DOM window that holds the output. * @type Window */ get window() { return this.owner.window; }, /** * Getter for the debugger WebConsoleClient. * @type object */ get webConsoleClient() { return this.owner.webConsoleClient; }, /** * Getter for the current toolbox debuggee target. * @type Target */ get toolboxTarget() { return this.owner.owner.target; }, /** * Release an actor. * * @private * @param string actorId * The actor ID you want to release. */ _releaseObject: function (actorId) { this.owner._releaseObject(actorId); }, /** * Add a message to output. * * @param object ...args * Any number of Message objects. * @return this */ addMessage: function (...args) { for (let msg of args) { msg.init(this); this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage, [msg]); } return this; }, /** * Message renderer used for compatibility with the current Web Console output * implementation. This method is invoked for every message object that is * flushed to output. The message object is initialized and rendered, then it * is displayed. * * TODO: remove this method once bug 778766 is fixed. * * @private * @param object message * The message object to render. * @return DOMElement * The message DOM element that can be added to the console output. */ _onFlushOutputMessage: function (message) { return message.render().element; }, /** * Get an array of selected messages. This list is based on the text selection * start and end points. * * @param number [limit] * Optional limit of selected messages you want. If no value is given, * all of the selected messages are returned. * @return array * Array of DOM elements for each message that is currently selected. */ getSelectedMessages: function (limit) { let selection = this.window.getSelection(); if (selection.isCollapsed) { return []; } if (selection.containsNode(this.element, true)) { return Array.slice(this.element.children); } let anchor = this.getMessageForElement(selection.anchorNode); let focus = this.getMessageForElement(selection.focusNode); if (!anchor || !focus) { return []; } let start, end; if (anchor.timestamp > focus.timestamp) { start = focus; end = anchor; } else { start = anchor; end = focus; } let result = []; let current = start; while (current) { result.push(current); if (current == end || (limit && result.length == limit)) { break; } current = current.nextSibling; } return result; }, /** * Find the DOM element of a message for any given descendant. * * @param DOMElement elem * The element to start the search from. * @return DOMElement|null * The DOM element of the message, if any. */ getMessageForElement: function (elem) { while (elem && elem.parentNode) { if (elem.classList && elem.classList.contains("message")) { return elem; } elem = elem.parentNode; } return null; }, /** * Select all messages. */ selectAllMessages: function () { let selection = this.window.getSelection(); selection.removeAllRanges(); let range = this.document.createRange(); range.selectNodeContents(this.element); selection.addRange(range); }, /** * Add a message to the selection. * * @param DOMElement elem * The message element to select. */ selectMessage: function (elem) { let selection = this.window.getSelection(); selection.removeAllRanges(); let range = this.document.createRange(); range.selectNodeContents(elem); selection.addRange(range); }, /** * Open an URL in a new tab. * @see WebConsole.openLink() in hudservice.js */ openLink: function () { this.owner.owner.openLink.apply(this.owner.owner, arguments); }, openLocationInDebugger: function ({url, line}) { return this.owner.owner.viewSourceInDebugger(url, line); }, /** * Open the variables view to inspect an object actor. * @see JSTerm.openVariablesView() in webconsole.js */ openVariablesView: function () { this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments); }, /** * Destroy this ConsoleOutput instance. */ destroy: function () { this._dummyElement = null; this.owner = null; }, }; // ConsoleOutput.prototype /** * Message objects container. * @type object */ var Messages = {}; /** * The BaseMessage object is used for all types of messages. Every kind of * message should use this object as its base. * * @constructor */ Messages.BaseMessage = function () { this.widgets = new Set(); this._onClickAnchor = this._onClickAnchor.bind(this); this._repeatID = { uid: gSequenceId() }; this.textContent = ""; }; Messages.BaseMessage.prototype = { /** * Reference to the ConsoleOutput owner. * * @type object|null * This is |null| if the message is not yet initialized. */ output: null, /** * Reference to the parent message object, if this message is in a group or if * it is otherwise owned by another message. * * @type object|null */ parent: null, /** * Message DOM element. * * @type DOMElement|null * This is |null| if the message is not yet rendered. */ element: null, /** * Tells if this message is visible or not. * @type boolean */ get visible() { return this.element && this.element.parentNode; }, /** * The owner DOM document. * @type DOMElement */ get document() { return this.output.document; }, /** * Holds the text-only representation of the message. * @type string */ textContent: null, /** * Set of widgets included in this message. * @type Set */ widgets: null, // Properties that allow compatibility with the current Web Console output // implementation. _categoryCompat: null, _severityCompat: null, _categoryNameCompat: null, _severityNameCompat: null, _filterKeyCompat: null, /** * Object that is JSON-ified and used as a non-unique ID for tracking * duplicate messages. * @private * @type object */ _repeatID: null, /** * Initialize the message. * * @param object output * The ConsoleOutput owner. * @param object [parent=null] * Optional: a different message object that owns this instance. * @return this */ init: function (output, parent = null) { this.output = output; this.parent = parent; return this; }, /** * Non-unique ID for this message object used for tracking duplicate messages. * Different message kinds can identify themselves based their own criteria. * * @return string */ getRepeatID: function () { return JSON.stringify(this._repeatID); }, /** * Render the message. After this method is invoked the |element| property * will point to the DOM element of this message. * @return this */ render: function () { if (!this.element) { this.element = this._renderCompat(); } return this; }, /** * Prepare the message container for the Web Console, such that it is * compatible with the current implementation. * TODO: remove this once bug 778766 is fixed. * * @private * @return Element * The DOM element that wraps the message. */ _renderCompat: function () { let doc = this.output.document; let container = doc.createElementNS(XHTML_NS, "div"); container.id = "console-msg-" + gSequenceId(); container.className = "message"; if (this.category == "input") { // Assistive technology tools shouldn't echo input to the user, // as the user knows what they've just typed. container.setAttribute("aria-live", "off"); } container.category = this._categoryCompat; container.severity = this._severityCompat; container.setAttribute("category", this._categoryNameCompat); container.setAttribute("severity", this._severityNameCompat); container.setAttribute("filter", this._filterKeyCompat); container.clipboardText = this.textContent; container.timestamp = this.timestamp; container._messageObject = this; return container; }, /** * Add a click callback to a given DOM element. * * @private * @param Element element * The DOM element to which you want to add a click event handler. * @param function [callback=this._onClickAnchor] * Optional click event handler. The default event handler is * |this._onClickAnchor|. */ _addLinkCallback: function (element, callback = this._onClickAnchor) { // This is going into the WebConsoleFrame object instance that owns // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole // object instance from hudservice.js. // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766 // is fixed. this.output.owner._addMessageLinkCallback(element, callback); }, /** * The default |click| event handler for links in the output. This function * opens the anchor's link in a new tab. * * @private * @param Event event * The DOM event that invoked this function. */ _onClickAnchor: function (event) { this.output.openLink(event.target.href); }, destroy: function () { // Destroy all widgets that have registered themselves in this.widgets for (let widget of this.widgets) { widget.destroy(); } this.widgets.clear(); } }; /** * The NavigationMarker is used to show a page load event. * * @constructor * @extends Messages.BaseMessage * @param object response * The response received from the back end. * @param number timestamp * The message date and time, milliseconds elapsed since 1 January 1970 * 00:00:00 UTC. */ Messages.NavigationMarker = function (response, timestamp) { Messages.BaseMessage.call(this); // Store the response packet received from the server. It might // be useful for extensions customizing the console output. this.response = response; this._url = response.url; this.textContent = "------ " + this._url; this.timestamp = timestamp; }; Messages.NavigationMarker.prototype = extend(Messages.BaseMessage.prototype, { /** * The address of the loading page. * @private * @type string */ _url: null, /** * Message timestamp. * * @type number * Milliseconds elapsed since 1 January 1970 00:00:00 UTC. */ timestamp: 0, _categoryCompat: COMPAT.CATEGORIES.NETWORK, _severityCompat: COMPAT.SEVERITIES.LOG, _categoryNameCompat: "network", _severityNameCompat: "info", _filterKeyCompat: "networkinfo", /** * Prepare the DOM element for this message. * @return this */ render: function () { if (this.element) { return this; } let url = this._url; let pos = url.indexOf("?"); if (pos > -1) { url = url.substr(0, pos); } let doc = this.output.document; let urlnode = doc.createElementNS(XHTML_NS, "a"); urlnode.className = "url"; urlnode.textContent = url; urlnode.title = this._url; urlnode.href = this._url; urlnode.draggable = false; this._addLinkCallback(urlnode); let render = Messages.BaseMessage.prototype.render.bind(this); render().element.appendChild(urlnode); this.element.classList.add("navigation-marker"); this.element.url = this._url; this.element.appendChild(doc.createTextNode("\n")); return this; }, }); /** * The Simple message is used to show any basic message in the Web Console. * * @constructor * @extends Messages.BaseMessage * @param string|Node|function message * The message to display. * @param object [options] * Options for this message: * - category: (string) category that this message belongs to. Defaults * to no category. * - severity: (string) severity of the message. Defaults to no severity. * - timestamp: (number) date and time when the message was recorded. * Defaults to |Date.now()|. * - link: (string) if provided, the message will be wrapped in an anchor * pointing to the given URL here. * - linkCallback: (function) if provided, the message will be wrapped in * an anchor. The |linkCallback| function will be added as click event * handler. * - location: object that tells the message source: url, line, column * and lineText. * - stack: array that tells the message source stack. * - className: (string) additional element class names for styling * purposes. * - private: (boolean) mark this as a private message. * - filterDuplicates: (boolean) true if you do want this message to be * filtered as a potential duplicate message, false otherwise. */ Messages.Simple = function (message, options = {}) { Messages.BaseMessage.call(this); this.category = options.category; this.severity = options.severity; this.location = options.location; this.stack = options.stack; this.timestamp = options.timestamp || Date.now(); this.prefix = options.prefix; this.private = !!options.private; this._message = message; this._className = options.className; this._link = options.link; this._linkCallback = options.linkCallback; this._filterDuplicates = options.filterDuplicates; this._onClickCollapsible = this._onClickCollapsible.bind(this); }; Messages.Simple.prototype = extend(Messages.BaseMessage.prototype, { /** * Message category. * @type string */ category: null, /** * Message severity. * @type string */ severity: null, /** * Message source location. Properties: url, line, column, lineText. * @type object */ location: null, /** * Holds the stackframes received from the server. * * @private * @type array */ stack: null, /** * Message prefix * @type string|null */ prefix: null, /** * Tells if this message comes from a private browsing context. * @type boolean */ private: false, /** * Custom class name for the DOM element of the message. * @private * @type string */ _className: null, /** * Message link - if this message is clicked then this URL opens in a new tab. * @private * @type string */ _link: null, /** * Message click event handler. * @private * @type function */ _linkCallback: null, /** * Tells if this message should be checked if it is a duplicate of another * message or not. */ _filterDuplicates: false, /** * The raw message displayed by this Message object. This can be a function, * DOM node or a string. * * @private * @type mixed */ _message: null, /** * The message's "attachment" element to be displayed under the message. * Used for things like stack traces or tables in console.table(). * * @private * @type DOMElement|null */ _attachment: null, _objectActors: null, _groupDepthCompat: 0, /** * Message timestamp. * * @type number * Milliseconds elapsed since 1 January 1970 00:00:00 UTC. */ timestamp: 0, get _categoryCompat() { return this.category ? COMPAT.CATEGORIES[this.category.toUpperCase()] : null; }, get _severityCompat() { return this.severity ? COMPAT.SEVERITIES[this.severity.toUpperCase()] : null; }, get _categoryNameCompat() { return this.category ? COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null; }, get _severityNameCompat() { return this.severity ? COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null; }, get _filterKeyCompat() { return this._categoryCompat !== null && this._severityCompat !== null ? COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] : null; }, init: function () { Messages.BaseMessage.prototype.init.apply(this, arguments); this._groupDepthCompat = this.output.owner.groupDepth; this._initRepeatID(); return this; }, /** * Tells if the message can be expanded/collapsed. * @type boolean */ collapsible: false, /** * Getter that tells if this message is collapsed - no details are shown. * @type boolean */ get collapsed() { return this.collapsible && this.element && !this.element.hasAttribute("open"); }, _initRepeatID: function () { if (!this._filterDuplicates) { return; } // Add the properties we care about for identifying duplicate messages. let rid = this._repeatID; delete rid.uid; rid.category = this.category; rid.severity = this.severity; rid.prefix = this.prefix; rid.private = this.private; rid.location = this.location; rid.link = this._link; rid.linkCallback = this._linkCallback + ""; rid.className = this._className; rid.groupDepth = this._groupDepthCompat; rid.textContent = ""; }, getRepeatID: function () { // No point in returning a string that includes other properties when there // is a unique ID. if (this._repeatID.uid) { return JSON.stringify({ uid: this._repeatID.uid }); } return JSON.stringify(this._repeatID); }, render: function () { if (this.element) { return this; } let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render(); let icon = this.document.createElementNS(XHTML_NS, "span"); icon.className = "icon"; icon.title = l10n.getStr("severity." + this._severityNameCompat); if (this.stack) { icon.addEventListener("click", this._onClickCollapsible); } let prefixNode; if (this.prefix) { prefixNode = this.document.createElementNS(XHTML_NS, "span"); prefixNode.className = "prefix devtools-monospace"; prefixNode.textContent = this.prefix + ":"; } // Apply the current group by indenting appropriately. // TODO: remove this once bug 778766 is fixed. let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT; let indentNode = this.document.createElementNS(XHTML_NS, "span"); indentNode.className = "indent"; indentNode.style.width = indent + "px"; let body = this._renderBody(); Messages.BaseMessage.prototype.render.call(this); if (this._className) { this.element.className += " " + this._className; } this.element.appendChild(timestamp.element); this.element.appendChild(indentNode); this.element.appendChild(icon); if (prefixNode) { this.element.appendChild(prefixNode); } if (this.stack) { let twisty = this.document.createElementNS(XHTML_NS, "a"); twisty.className = "theme-twisty"; twisty.href = "#"; twisty.title = l10n.getStr("messageToggleDetails"); twisty.addEventListener("click", this._onClickCollapsible); this.element.appendChild(twisty); this.collapsible = true; this.element.setAttribute("collapsible", true); } this.element.appendChild(body); this.element.clipboardText = this.element.textContent; if (this.private) { this.element.setAttribute("private", true); } // TODO: handle object releasing in a more elegant way once all console // messages use the new API - bug 778766. this.element._objectActors = this._objectActors; this._objectActors = null; return this; }, /** * Render the message body DOM element. * @private * @return Element */ _renderBody: function () { let bodyWrapper = this.document.createElementNS(XHTML_NS, "span"); bodyWrapper.className = "message-body-wrapper"; let bodyFlex = this.document.createElementNS(XHTML_NS, "span"); bodyFlex.className = "message-flex-body"; bodyWrapper.appendChild(bodyFlex); let body = this.document.createElementNS(XHTML_NS, "span"); body.className = "message-body devtools-monospace"; bodyFlex.appendChild(body); let anchor, container = body; if (this._link || this._linkCallback) { container = anchor = this.document.createElementNS(XHTML_NS, "a"); anchor.href = this._link || "#"; anchor.draggable = false; this._addLinkCallback(anchor, this._linkCallback); body.appendChild(anchor); } if (typeof this._message == "function") { container.appendChild(this._message(this)); } else if (this._message instanceof Ci.nsIDOMNode) { container.appendChild(this._message); } else { container.textContent = this._message; } // do this before repeatNode is rendered - it has no effect afterwards this._repeatID.textContent += "|" + container.textContent; let repeatNode = this._renderRepeatNode(); let location = this._renderLocation(); if (repeatNode) { bodyFlex.appendChild(this.document.createTextNode(" ")); bodyFlex.appendChild(repeatNode); } if (location) { bodyFlex.appendChild(this.document.createTextNode(" ")); bodyFlex.appendChild(location); } bodyFlex.appendChild(this.document.createTextNode("\n")); if (this.stack) { this._attachment = new Widgets.Stacktrace(this, this.stack).render().element; } if (this._attachment) { bodyWrapper.appendChild(this._attachment); } return bodyWrapper; }, /** * Render the repeat bubble DOM element part of the message. * @private * @return Element */ _renderRepeatNode: function () { if (!this._filterDuplicates) { return null; } let repeatNode = this.document.createElementNS(XHTML_NS, "span"); repeatNode.setAttribute("value", "1"); repeatNode.className = "message-repeats"; repeatNode.textContent = 1; repeatNode._uid = this.getRepeatID(); return repeatNode; }, /** * Render the message source location DOM element. * @private * @return Element */ _renderLocation: function () { if (!this.location) { return null; } let {url, line, column} = this.location; if (IGNORED_SOURCE_URLS.indexOf(url) != -1) { return null; } // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js. // TODO: move createLocationNode() into this file when bug 778766 is fixed. return this.output.owner.createLocationNode({url, line, column }); }, /** * The click event handler for the message expander arrow element. This method * toggles the display of message details. * * @private * @param nsIDOMEvent ev * The DOM event object. * @see this.toggleDetails() */ _onClickCollapsible: function (ev) { ev.preventDefault(); this.toggleDetails(); }, /** * Expand/collapse message details. */ toggleDetails: function () { let twisty = this.element.querySelector(".theme-twisty"); if (this.element.hasAttribute("open")) { this.element.removeAttribute("open"); twisty.removeAttribute("open"); } else { this.element.setAttribute("open", true); twisty.setAttribute("open", true); } }, }); // Messages.Simple.prototype /** * The Extended message. * * @constructor * @extends Messages.Simple * @param array messagePieces * The message to display given as an array of elements. Each array * element can be a DOM node, function, ObjectActor, LongString or * a string. * @param object [options] * Options for rendering this message: * - quoteStrings: boolean that tells if you want strings to be wrapped * in quotes or not. */ Messages.Extended = function (messagePieces, options = {}) { Messages.Simple.call(this, null, options); this._messagePieces = messagePieces; if ("quoteStrings" in options) { this._quoteStrings = options.quoteStrings; } this._repeatID.quoteStrings = this._quoteStrings; this._repeatID.messagePieces = JSON.stringify(messagePieces); this._repeatID.actors = new Set(); // using a set to avoid duplicates }; Messages.Extended.prototype = extend(Messages.Simple.prototype, { /** * The message pieces displayed by this message instance. * @private * @type array */ _messagePieces: null, /** * Boolean that tells if the strings displayed in this message are wrapped. * @private * @type boolean */ _quoteStrings: true, getRepeatID: function () { if (this._repeatID.uid) { return JSON.stringify({ uid: this._repeatID.uid }); } // Sets are not stringified correctly. Temporarily switching to an array. let actors = this._repeatID.actors; this._repeatID.actors = [...actors]; let result = JSON.stringify(this._repeatID); this._repeatID.actors = actors; return result; }, render: function () { let result = this.document.createDocumentFragment(); for (let i = 0; i < this._messagePieces.length; i++) { let separator = i > 0 ? this._renderBodyPieceSeparator() : null; if (separator) { result.appendChild(separator); } let piece = this._messagePieces[i]; result.appendChild(this._renderBodyPiece(piece)); } this._message = result; this._messagePieces = null; return Messages.Simple.prototype.render.call(this); }, /** * Render the separator between the pieces of the message. * * @private * @return Element */ _renderBodyPieceSeparator: function () { return null; }, /** * Render one piece/element of the message array. * * @private * @param mixed piece * Message element to display - this can be a LongString, ObjectActor, * DOM node or a function to invoke. * @return Element */ _renderBodyPiece: function (piece, options = {}) { if (piece instanceof Ci.nsIDOMNode) { return piece; } if (typeof piece == "function") { return piece(this); } return this._renderValueGrip(piece, options); }, /** * Render a grip that represents a value received from the server. This method * picks the appropriate widget to render the value with. * * @private * @param object grip * The value grip received from the server. * @param object options * Options for displaying the value. Available options: * - noStringQuotes - boolean that tells the renderer to not use quotes * around strings. * - concise - boolean that tells the renderer to compactly display the * grip. This is typically set to true when the object needs to be * displayed in an array preview, or as a property value in object * previews, etc. * - shorten - boolean that tells the renderer to display a truncated * grip. * @return DOMElement * The DOM element that displays the given grip. */ _renderValueGrip: function (grip, options = {}) { let isPrimitive = VariablesView.isPrimitive({ value: grip }); let isActorGrip = WebConsoleUtils.isActorGrip(grip); let noStringQuotes = !this._quoteStrings; if ("noStringQuotes" in options) { noStringQuotes = options.noStringQuotes; } if (isActorGrip) { this._repeatID.actors.add(grip.actor); if (!isPrimitive) { return this._renderObjectActor(grip, options); } if (grip.type == "longString") { let widget = new Widgets.LongString(this, grip, options).render(); return widget.element; } } let unshortenedGrip = grip; if (options.shorten) { grip = this.shortenValueGrip(grip); } let result = this.document.createElementNS(XHTML_NS, "span"); if (isPrimitive) { if (Widgets.URLString.prototype.containsURL.call(Widgets.URLString.prototype, grip)) { let widget = new Widgets.URLString(this, grip, unshortenedGrip).render(); return widget.element; } let className = this.getClassNameForValueGrip(grip); if (className) { result.className = className; } result.textContent = VariablesView.getString(grip, { noStringQuotes: noStringQuotes, concise: options.concise, }); } else { result.textContent = grip; } return result; }, /** * Shorten grips of the type string, leaves other grips unmodified. * * @param object grip * Value grip from the server. * @return object * Possible values of object: * - A shortened string, if original grip was of string type. * - The unmodified input grip, if it wasn't of string type. */ shortenValueGrip: function (grip) { let shortVal = grip; if (typeof (grip) == "string") { shortVal = grip.replace(/(\r\n|\n|\r)/gm, " "); if (shortVal.length > MAX_STRING_GRIP_LENGTH) { shortVal = shortVal.substring(0, MAX_STRING_GRIP_LENGTH - 1) + ELLIPSIS; } } return shortVal; }, /** * Get a CodeMirror-compatible class name for a given value grip. * * @param object grip * Value grip from the server. * @return string * The class name for the grip. */ getClassNameForValueGrip: function (grip) { let map = { "number": "cm-number", "longstring": "console-string", "string": "console-string", "regexp": "cm-string-2", "boolean": "cm-atom", "-infinity": "cm-atom", "infinity": "cm-atom", "null": "cm-atom", "undefined": "cm-comment", "symbol": "cm-atom" }; let className = map[typeof grip]; if (!className && grip && grip.type) { className = map[grip.type.toLowerCase()]; } if (!className && grip && grip.class) { className = map[grip.class.toLowerCase()]; } return className; }, /** * Display an object actor with the appropriate renderer. * * @private * @param object objectActor * The ObjectActor to display. * @param object options * Options to use for displaying the ObjectActor. * @see this._renderValueGrip for the available options. * @return DOMElement * The DOM element that displays the object actor. */ _renderObjectActor: function (objectActor, options = {}) { let widget = Widgets.ObjectRenderers.byClass[objectActor.class]; let { preview } = objectActor; if ((!widget || (widget.canRender && !widget.canRender(objectActor))) && preview && preview.kind) { widget = Widgets.ObjectRenderers.byKind[preview.kind]; } if (!widget || (widget.canRender && !widget.canRender(objectActor))) { widget = Widgets.JSObject; } let instance = new widget(this, objectActor, options).render(); return instance.element; }, }); // Messages.Extended.prototype /** * The JavaScriptEvalOutput message. * * @constructor * @extends Messages.Extended * @param object evalResponse * The evaluation response packet received from the server. * @param string [errorMessage] * Optional error message to display. * @param string [errorDocLink] * Optional error doc URL to link to. */ Messages.JavaScriptEvalOutput = function (evalResponse, errorMessage, errorDocLink) { let severity = "log", msg, quoteStrings = true; // Store also the response packet from the back end. It might // be useful to extensions customizing the console output. this.response = evalResponse; if (typeof (errorMessage) !== "undefined") { severity = "error"; msg = errorMessage; quoteStrings = false; } else { msg = evalResponse.result; } let options = { className: "cm-s-mozilla", timestamp: evalResponse.timestamp, category: "output", severity: severity, quoteStrings: quoteStrings, }; let messages = [msg]; if (errorDocLink) { messages.push(errorDocLink); } Messages.Extended.call(this, messages, options); }; Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype; /** * The ConsoleGeneric message is used for console API calls. * * @constructor * @extends Messages.Extended * @param object packet * The Console API call packet received from the server. */ Messages.ConsoleGeneric = function (packet) { let options = { className: "cm-s-mozilla", timestamp: packet.timeStamp, category: packet.category || "webdev", severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], prefix: packet.prefix, private: packet.private, filterDuplicates: true, location: { url: packet.filename, line: packet.lineNumber, column: packet.columnNumber }, }; switch (packet.level) { case "count": { let counter = packet.counter, label = counter.label; if (!label) { label = l10n.getStr("noCounterLabel"); } Messages.Extended.call(this, [label + ": " + counter.count], options); break; } default: Messages.Extended.call(this, packet.arguments, options); break; } this._repeatID.consoleApiLevel = packet.level; this._repeatID.styles = packet.styles; this.stack = this._repeatID.stacktrace = packet.stacktrace; this._styles = packet.styles || []; }; Messages.ConsoleGeneric.prototype = extend(Messages.Extended.prototype, { _styles: null, _renderBodyPieceSeparator: function () { return this.document.createTextNode(" "); }, render: function () { let result = this.document.createDocumentFragment(); this._renderBodyPieces(result); this._message = result; this._stacktrace = null; Messages.Simple.prototype.render.call(this); return this; }, _renderBodyPieces: function (container) { let lastStyle = null; let stylePieces = this._styles.length > 0 ? this._styles.length : 1; for (let i = 0; i < this._messagePieces.length; i++) { // Pieces with an associated style definition come from "%c" formatting. // For body pieces beyond that, add a separator before each one. if (i >= stylePieces) { container.appendChild(this._renderBodyPieceSeparator()); } let piece = this._messagePieces[i]; let style = this._styles[i]; // No long string support. lastStyle = (style && typeof style == "string") ? this.cleanupStyle(style) : null; container.appendChild(this._renderBodyPiece(piece, lastStyle)); } this._messagePieces = null; this._styles = null; }, _renderBodyPiece: function (piece, style) { // Skip quotes for top-level strings. let options = { noStringQuotes: true }; let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece, options); let result = elem; if (style) { if (elem.nodeType == nodeConstants.ELEMENT_NODE) { elem.style = style; } else { let span = this.document.createElementNS(XHTML_NS, "span"); span.style = style; span.appendChild(elem); result = span; } } return result; }, /** * Given a style attribute value, return a cleaned up version of the string * such that: * * - no external URL is allowed to load. See RE_CLEANUP_STYLES. * - only some of the properties are allowed, based on a whitelist. See * RE_ALLOWED_STYLES. * * @param string style * The style string to cleanup. * @return string * The style value after cleanup. */ cleanupStyle: function (style) { for (let r of RE_CLEANUP_STYLES) { style = style.replace(r, "notallowed"); } let dummy = this.output._dummyElement; if (!dummy) { dummy = this.output._dummyElement = this.document.createElementNS(XHTML_NS, "div"); } dummy.style = style; let toRemove = []; for (let i = 0; i < dummy.style.length; i++) { let prop = dummy.style[i]; if (!RE_ALLOWED_STYLES.test(prop)) { toRemove.push(prop); } } for (let prop of toRemove) { dummy.style.removeProperty(prop); } style = dummy.style.cssText; dummy.style = ""; return style; }, }); // Messages.ConsoleGeneric.prototype /** * The ConsoleTrace message is used for console.trace() calls. * * @constructor * @extends Messages.Simple * @param object packet * The Console API call packet received from the server. */ Messages.ConsoleTrace = function (packet) { let options = { className: "cm-s-mozilla", timestamp: packet.timeStamp, category: packet.category || "webdev", severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], private: packet.private, filterDuplicates: true, location: { url: packet.filename, line: packet.lineNumber, }, }; Messages.Simple.call(this, null, options); this._repeatID.consoleApiLevel = packet.level; this._stacktrace = this._repeatID.stacktrace = packet.stacktrace; this._arguments = packet.arguments; }; Messages.ConsoleTrace.prototype = extend(Messages.Simple.prototype, { /** * Holds the stackframes received from the server. * * @private * @type array */ _stacktrace: null, /** * Holds the arguments the content script passed to the console.trace() * method. This array is cleared when the message is initialized, and * associated actors are released. * * @private * @type array */ _arguments: null, init: function () { let result = Messages.Simple.prototype.init.apply(this, arguments); // We ignore console.trace() arguments. Release object actors. if (Array.isArray(this._arguments)) { for (let arg of this._arguments) { if (WebConsoleUtils.isActorGrip(arg)) { this.output._releaseObject(arg.actor); } } } this._arguments = null; return result; }, render: function () { this._message = this._renderMessage(); this._attachment = this._renderStack(); Messages.Simple.prototype.render.apply(this, arguments); this.element.setAttribute("open", true); return this; }, /** * Render the console messageNode */ _renderMessage: function () { let cmvar = this.document.createElementNS(XHTML_NS, "span"); cmvar.className = "cm-variable"; cmvar.textContent = "console"; let cmprop = this.document.createElementNS(XHTML_NS, "span"); cmprop.className = "cm-property"; cmprop.textContent = "trace"; let frag = this.document.createDocumentFragment(); frag.appendChild(cmvar); frag.appendChild(this.document.createTextNode(".")); frag.appendChild(cmprop); frag.appendChild(this.document.createTextNode("():")); return frag; }, /** * Render the stack frames. * * @private * @return DOMElement */ _renderStack: function () { return new Widgets.Stacktrace(this, this._stacktrace).render().element; }, }); // Messages.ConsoleTrace.prototype /** * The ConsoleTable message is used for console.table() calls. * * @constructor * @extends Messages.Extended * @param object packet * The Console API call packet received from the server. */ Messages.ConsoleTable = function (packet) { let options = { className: "cm-s-mozilla", timestamp: packet.timeStamp, category: packet.category || "webdev", severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], private: packet.private, filterDuplicates: false, location: { url: packet.filename, line: packet.lineNumber, }, }; this._populateTableData = this._populateTableData.bind(this); this._renderMessage = this._renderMessage.bind(this); Messages.Extended.call(this, [this._renderMessage], options); this._repeatID.consoleApiLevel = packet.level; this._arguments = packet.arguments; }; Messages.ConsoleTable.prototype = extend(Messages.Extended.prototype, { /** * Holds the arguments the content script passed to the console.table() * method. * * @private * @type array */ _arguments: null, /** * Array of objects that holds the data to log in the table. * * @private * @type array */ _data: null, /** * Key value pair of the id and display name for the columns in the table. * Refer to the TableWidget API. * * @private * @type object */ _columns: null, /** * A promise that resolves when the table data is ready or null if invalid * arguments are provided. * * @private * @type promise|null */ _populatePromise: null, init: function () { let result = Messages.Extended.prototype.init.apply(this, arguments); this._data = []; this._columns = {}; this._populatePromise = this._populateTableData(); return result; }, /** * Sets the key value pair of the id and display name for the columns in the * table. * * @private * @param array|string columns * Either a string or array containing the names for the columns in * the output table. */ _setColumns: function (columns) { if (columns.class == "Array") { let items = columns.preview.items; for (let item of items) { if (typeof item == "string") { this._columns[item] = item; } } } else if (typeof columns == "string" && columns) { this._columns[columns] = columns; } }, /** * Retrieves the table data and columns from the arguments received from the * server. * * @return Promise|null * Returns a promise that resolves when the table data is ready or * null if the arguments are invalid. */ _populateTableData: function () { let deferred = promise.defer(); if (this._arguments.length <= 0) { return; } let data = this._arguments[0]; if (data.class != "Array" && data.class != "Object" && data.class != "Map" && data.class != "Set" && data.class != "WeakMap" && data.class != "WeakSet") { return; } let hasColumnsArg = false; if (this._arguments.length > 1) { if (data.class == "Object" || data.class == "Array") { this._columns["_index"] = l10n.getStr("table.index"); } else { this._columns["_index"] = l10n.getStr("table.iterationIndex"); } this._setColumns(this._arguments[1]); hasColumnsArg = true; } if (data.class == "Object" || data.class == "Array") { // Get the object properties, and parse the key and value properties into // the table data and columns. this.client = new ObjectClient(this.output.owner.jsterm.hud.proxy.client, data); this.client.getPrototypeAndProperties(aResponse => { let {ownProperties} = aResponse; let rowCount = 0; let columnCount = 0; for (let index of Object.keys(ownProperties || {})) { // Avoid outputting the length property if the data argument provided // is an array if (data.class == "Array" && index == "length") { continue; } if (!hasColumnsArg) { this._columns["_index"] = l10n.getStr("table.index"); } if (data.class == "Array") { if (index == parseInt(index)) { index = parseInt(index); } } let property = ownProperties[index].value; let item = { _index: index }; if (property.class == "Object" || property.class == "Array") { let {preview} = property; let entries = property.class == "Object" ? preview.ownProperties : preview.items; for (let key of Object.keys(entries)) { let value = property.class == "Object" ? preview.ownProperties[key].value : preview.items[key]; item[key] = this._renderValueGrip(value, { concise: true }); if (!hasColumnsArg && !(key in this._columns) && (++columnCount <= TABLE_COLUMN_MAX_ITEMS)) { this._columns[key] = key; } } } else { // Display the value for any non-object data input. item["_value"] = this._renderValueGrip(property, { concise: true }); if (!hasColumnsArg && !("_value" in this._columns)) { this._columns["_value"] = l10n.getStr("table.value"); } } this._data.push(item); if (++rowCount == TABLE_ROW_MAX_ITEMS) { break; } } deferred.resolve(); }); } else if (data.class == "Map" || data.class == "WeakMap") { let entries = data.preview.entries; if (!hasColumnsArg) { this._columns["_index"] = l10n.getStr("table.iterationIndex"); this._columns["_key"] = l10n.getStr("table.key"); this._columns["_value"] = l10n.getStr("table.value"); } let rowCount = 0; for (let [key, value] of entries) { let item = { _index: rowCount, _key: this._renderValueGrip(key, { concise: true }), _value: this._renderValueGrip(value, { concise: true }) }; this._data.push(item); if (++rowCount == TABLE_ROW_MAX_ITEMS) { break; } } deferred.resolve(); } else if (data.class == "Set" || data.class == "WeakSet") { let entries = data.preview.items; if (!hasColumnsArg) { this._columns["_index"] = l10n.getStr("table.iterationIndex"); this._columns["_value"] = l10n.getStr("table.value"); } let rowCount = 0; for (let entry of entries) { let item = { _index : rowCount, _value: this._renderValueGrip(entry, { concise: true }) }; this._data.push(item); if (++rowCount == TABLE_ROW_MAX_ITEMS) { break; } } deferred.resolve(); } return deferred.promise; }, render: function () { this._attachment = this._renderTable(); Messages.Extended.prototype.render.apply(this, arguments); this.element.setAttribute("open", true); return this; }, _renderMessage: function () { let cmvar = this.document.createElementNS(XHTML_NS, "span"); cmvar.className = "cm-variable"; cmvar.textContent = "console"; let cmprop = this.document.createElementNS(XHTML_NS, "span"); cmprop.className = "cm-property"; cmprop.textContent = "table"; let frag = this.document.createDocumentFragment(); frag.appendChild(cmvar); frag.appendChild(this.document.createTextNode(".")); frag.appendChild(cmprop); frag.appendChild(this.document.createTextNode("():")); return frag; }, /** * Render the table. * * @private * @return DOMElement */ _renderTable: function () { let result = this.document.createElementNS(XHTML_NS, "div"); if (this._populatePromise) { this._populatePromise.then(() => { if (this._data.length > 0) { let widget = new Widgets.Table(this, this._data, this._columns).render(); result.appendChild(widget.element); } result.scrollIntoView(); this.output.owner.emit("messages-table-rendered"); // Release object actors if (Array.isArray(this._arguments)) { for (let arg of this._arguments) { if (WebConsoleUtils.isActorGrip(arg)) { this.output._releaseObject(arg.actor); } } } this._arguments = null; }); } return result; }, }); // Messages.ConsoleTable.prototype var Widgets = {}; /** * The base widget class. * * @constructor * @param object message * The owning message. */ Widgets.BaseWidget = function (message) { this.message = message; }; Widgets.BaseWidget.prototype = { /** * The owning message object. * @type object */ message: null, /** * The DOM element of the rendered widget. * @type Element */ element: null, /** * Getter for the DOM document that holds the output. * @type Document */ get document() { return this.message.document; }, /** * The ConsoleOutput instance that owns this widget instance. */ get output() { return this.message.output; }, /** * Render the widget DOM element. * @return this */ render: function () { }, /** * Destroy this widget instance. */ destroy: function () { }, /** * Helper for creating DOM elements for widgets. * * Usage: * this.el("tag#id.class.names"); // create element "tag" with ID "id" and * two class names, .class and .names. * * this.el("span", { attr1: "value1", ... }) // second argument can be an * object that holds element attributes and values for the new DOM element. * * this.el("p", { attr1: "value1", ... }, "text content"); // the third * argument can include the default .textContent of the new DOM element. * * this.el("p", "text content"); // if the second argument is not an object, * it will be used as .textContent for the new DOM element. * * @param string tagNameIdAndClasses * Tag name for the new element, optionally followed by an ID and/or * class names. Examples: "span", "div#fooId", "div.class.names", * "p#id.class". * @param string|object [attributesOrTextContent] * If this argument is an object it will be used to set the attributes * of the new DOM element. Otherwise, the value becomes the * .textContent of the new DOM element. * @param string [textContent] * If this argument is provided the value is used as the textContent of * the new DOM element. * @return DOMElement * The new DOM element. */ el: function (tagNameIdAndClasses) { let attrs, text; if (typeof arguments[1] == "object") { attrs = arguments[1]; text = arguments[2]; } else { text = arguments[1]; } let tagName = tagNameIdAndClasses.split(/#|\./)[0]; let elem = this.document.createElementNS(XHTML_NS, tagName); for (let name of Object.keys(attrs || {})) { elem.setAttribute(name, attrs[name]); } if (text !== undefined && text !== null) { elem.textContent = text; } let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g); for (let idOrClass of (idAndClasses || [])) { if (idOrClass.charAt(0) == "#") { elem.id = idOrClass.substr(1); } else { elem.classList.add(idOrClass.substr(1)); } } return elem; }, }; /** * The timestamp widget. * * @constructor * @param object message * The owning message. * @param number timestamp * The UNIX timestamp to display. */ Widgets.MessageTimestamp = function (message, timestamp) { Widgets.BaseWidget.call(this, message); this.timestamp = timestamp; }; Widgets.MessageTimestamp.prototype = extend(Widgets.BaseWidget.prototype, { /** * The UNIX timestamp. * @type number */ timestamp: 0, render: function () { if (this.element) { return this; } this.element = this.document.createElementNS(XHTML_NS, "span"); this.element.className = "timestamp devtools-monospace"; this.element.textContent = l10n.timestampString(this.timestamp) + " "; return this; }, }); // Widgets.MessageTimestamp.prototype /** * The URLString widget, for rendering strings where at least one token is a * URL. * * @constructor * @param object message * The owning message. * @param string str * The string, which contains at least one valid URL. * @param string unshortenedStr * The unshortened form of the string, if it was shortened. */ Widgets.URLString = function (message, str, unshortenedStr) { Widgets.BaseWidget.call(this, message); this.str = str; this.unshortenedStr = unshortenedStr; }; Widgets.URLString.prototype = extend(Widgets.BaseWidget.prototype, { /** * The string to format, which contains at least one valid URL. * @type string */ str: "", render: function () { if (this.element) { return this; } // The rendered URLString will be a containing a number of text // for non-URL tokens and 's for URL tokens. this.element = this.el("span", { class: "console-string" }); this.element.appendChild(this._renderText("\"")); // As we walk through the tokens of the source string, we make sure to preserve // the original whitespace that separated the tokens. let tokens = this.str.split(/\s+/); let textStart = 0; let tokenStart; for (let i = 0; i < tokens.length; i++) { let token = tokens[i]; let unshortenedToken; tokenStart = this.str.indexOf(token, textStart); if (this._isURL(token)) { // The last URL in the string might be shortened. If so, get the // real URL so the rendered link can point to it. if (i === tokens.length - 1 && this.unshortenedStr) { unshortenedToken = this.unshortenedStr.slice(tokenStart).split(/\s+/, 1)[0]; } this.element.appendChild(this._renderText(this.str.slice(textStart, tokenStart))); textStart = tokenStart + token.length; this.element.appendChild(this._renderURL(token, unshortenedToken)); } } // Clean up any non-URL text at the end of the source string. this.element.appendChild(this._renderText(this.str.slice(textStart, this.str.length))); this.element.appendChild(this._renderText("\"")); return this; }, /** * Determines whether a grip is a string containing a URL. * * @param string grip * The grip, which may contain a URL. * @return boolean * Whether the grip is a string containing a URL. */ containsURL: function (grip) { if (typeof grip != "string") { return false; } let tokens = grip.split(/\s+/); return tokens.some(this._isURL); }, /** * Determines whether a string token is a valid URL. * * @param string token * The token. * @return boolean * Whenther the token is a URL. */ _isURL: function (token) { try { if (!validProtocols.test(token)) { return false; } new URL(token); return true; } catch (e) { return false; } }, /** * Renders a string as a URL. * * @param string url * The string to be rendered as a url. * @param string fullUrl * The unshortened form of the URL, if it was shortened. * @return DOMElement * An element containing the rendered string. */ _renderURL: function (url, fullUrl) { let unshortened = fullUrl || url; let result = this.el("a", { class: "url", title: unshortened, href: unshortened, draggable: false }, url); this.message._addLinkCallback(result); return result; }, _renderText: function (text) { return this.el("span", text); }, }); // Widgets.URLString.prototype /** * Widget used for displaying ObjectActors that have no specialised renderers. * * @constructor * @param object message * The owning message. * @param object objectActor * The ObjectActor to display. * @param object [options] * Options for displaying the given ObjectActor. See * Messages.Extended.prototype._renderValueGrip for the available * options. */ Widgets.JSObject = function (message, objectActor, options = {}) { Widgets.BaseWidget.call(this, message); this.objectActor = objectActor; this.options = options; this._onClick = this._onClick.bind(this); }; Widgets.JSObject.prototype = extend(Widgets.BaseWidget.prototype, { /** * The ObjectActor displayed by the widget. * @type object */ objectActor: null, render: function () { if (!this.element) { this._render(); } return this; }, _render: function () { let str = VariablesView.getString(this.objectActor, this.options); let className = this.message.getClassNameForValueGrip(this.objectActor); if (!className && this.objectActor.class == "Object") { className = "cm-variable"; } this.element = this._anchor(str, { className: className }); }, /** * Render a concise representation of an object. */ _renderConciseObject: function () { this.element = this._anchor(this.objectActor.class, { className: "cm-variable" }); }, /** * Render the ` { ` prefix of an object. */ _renderObjectPrefix: function () { let { kind } = this.objectActor.preview; this.element = this.el("span.kind-" + kind); this._anchor(this.objectActor.class, { className: "cm-variable" }); this._text(" { "); }, /** * Render the ` }` suffix of an object. */ _renderObjectSuffix: function () { this._text(" }"); }, /** * Render an object property. * * @param String key * The property name. * @param Object value * The property value, as an RDP grip. * @param nsIDOMNode container * The container node to render to. * @param Boolean needsComma * True if there was another property before this one and we need to * separate them with a comma. * @param Boolean valueIsText * Add the value as is, don't treat it as a grip and pass it to * `_renderValueGrip`. */ _renderObjectProperty: function (key, value, container, needsComma, valueIsText = false) { if (needsComma) { this._text(", "); } container.appendChild(this.el("span.cm-property", key)); this._text(": "); if (valueIsText) { this._text(value); } else { let valueElem = this.message._renderValueGrip(value, { concise: true, shorten: true }); container.appendChild(valueElem); } }, /** * Render this object's properties. * * @param nsIDOMNode container * The container node to render to. * @param Boolean needsComma * True if there was another property before this one and we need to * separate them with a comma. */ _renderObjectProperties: function (container, needsComma) { let { preview } = this.objectActor; let { ownProperties, safeGetterValues } = preview; let shown = 0; let getValue = desc => { if (desc.get) { return "Getter"; } else if (desc.set) { return "Setter"; } else { return desc.value; } }; for (let key of Object.keys(ownProperties || {})) { this._renderObjectProperty(key, getValue(ownProperties[key]), container, shown > 0 || needsComma, ownProperties[key].get || ownProperties[key].set); shown++; } let ownPropertiesShown = shown; for (let key of Object.keys(safeGetterValues || {})) { this._renderObjectProperty(key, safeGetterValues[key].getterValue, container, shown > 0 || needsComma); shown++; } if (typeof preview.ownPropertiesLength == "number" && ownPropertiesShown < preview.ownPropertiesLength) { this._text(", "); let n = preview.ownPropertiesLength - ownPropertiesShown; let str = VariablesView.stringifiers._getNMoreString(n); this._anchor(str); } }, /** * Render an anchor with a given text content and link. * * @private * @param string text * Text to show in the anchor. * @param object [options] * Available options: * - onClick (function): "click" event handler.By default a click on * the anchor opens the variables view for the current object actor * (this.objectActor). * - href (string): if given the string is used as a link, and clicks * on the anchor open the link in a new tab. * - appendTo (DOMElement): append the element to the given DOM * element. If not provided, the anchor is appended to |this.element| * if it is available. If |appendTo| is provided and if it is a falsy * value, the anchor is not appended to any element. * @return DOMElement * The DOM element of the new anchor. */ _anchor: function (text, options = {}) { if (!options.onClick) { // If the anchor has an URL, open it in a new tab. If not, show the // current object actor. options.onClick = options.href ? this._onClickAnchor : this._onClick; } options.onContextMenu = options.onContextMenu || this._onContextMenu; let anchor = this.el("a", { class: options.className, draggable: false, href: options.href || "#", }, text); this.message._addLinkCallback(anchor, options.onClick); anchor.addEventListener("contextmenu", options.onContextMenu.bind(this)); if (options.appendTo) { options.appendTo.appendChild(anchor); } else if (!("appendTo" in options) && this.element) { this.element.appendChild(anchor); } return anchor; }, openObjectInVariablesView: function () { this.output.openVariablesView({ label: VariablesView.getString(this.objectActor, { concise: true }), objectActor: this.objectActor, autofocus: true, }); }, storeObjectInWindow: function () { let evalString = `{ let i = 0; while (this.hasOwnProperty("temp" + i) && i < 1000) { i++; } this["temp" + i] = _self; "temp" + i; }`; let options = { selectedObjectActor: this.objectActor.actor, }; this.output.owner.jsterm.requestEvaluation(evalString, options).then((res) => { this.output.owner.jsterm.focus(); this.output.owner.jsterm.setInputValue(res.result); }); }, /** * The click event handler for objects shown inline. * @private */ _onClick: function () { this.openObjectInVariablesView(); }, _onContextMenu: function (ev) { // TODO offer a nice API for the context menu. // Probably worth to take a look at Firebug's way // https://github.com/firebug/firebug/blob/master/extension/content/firebug/chrome/menu.js let doc = ev.target.ownerDocument; let cmPopup = doc.getElementById("output-contextmenu"); let openInVarViewCmd = doc.getElementById("menu_openInVarView"); let openVarView = this.openObjectInVariablesView.bind(this); openInVarViewCmd.addEventListener("command", openVarView); openInVarViewCmd.removeAttribute("disabled"); cmPopup.addEventListener("popuphiding", function onPopupHiding() { cmPopup.removeEventListener("popuphiding", onPopupHiding); openInVarViewCmd.removeEventListener("command", openVarView); openInVarViewCmd.setAttribute("disabled", "true"); }); // 'Store as global variable' command isn't supported on pre-44 servers, // so remove it from the menu in that case. let storeInGlobalCmd = doc.getElementById("menu_storeAsGlobal"); if (!this.output.webConsoleClient.traits.selectedObjectActor) { storeInGlobalCmd.remove(); } else if (storeInGlobalCmd) { let storeObjectInWindow = this.storeObjectInWindow.bind(this); storeInGlobalCmd.addEventListener("command", storeObjectInWindow); storeInGlobalCmd.removeAttribute("disabled"); cmPopup.addEventListener("popuphiding", function onPopupHiding() { cmPopup.removeEventListener("popuphiding", onPopupHiding); storeInGlobalCmd.removeEventListener("command", storeObjectInWindow); storeInGlobalCmd.setAttribute("disabled", "true"); }); } }, /** * Add a string to the message. * * @private * @param string str * String to add. * @param DOMElement [target = this.element] * Optional DOM element to append the string to. The default is * this.element. */ _text: function (str, target = this.element) { target.appendChild(this.document.createTextNode(str)); }, }); // Widgets.JSObject.prototype Widgets.ObjectRenderers = {}; Widgets.ObjectRenderers.byKind = {}; Widgets.ObjectRenderers.byClass = {}; /** * Add an object renderer. * * @param object obj * An object that represents the renderer. Properties: * - byClass (string, optional): this renderer will be used for the given * object class. * - byKind (string, optional): this renderer will be used for the given * object kind. * One of byClass or byKind must be provided. * - extends (object, optional): the renderer object extends the given * object. Default: Widgets.JSObject. * - canRender (function, optional): this method is invoked when * a candidate object needs to be displayed. The method is invoked as * a static method, as such, none of the properties of the renderer * object will be available. You get one argument: the object actor grip * received from the server. If the method returns true, then this * renderer is used for displaying the object, otherwise not. * - initialize (function, optional): the constructor of the renderer * widget. This function is invoked with the following arguments: the * owner message object instance, the object actor grip to display, and * an options object. See Messages.Extended.prototype._renderValueGrip() * for details about the options object. * - render (function, required): the method that displays the given * object actor. */ Widgets.ObjectRenderers.add = function (obj) { let extendObj = obj.extends || Widgets.JSObject; let constructor = function () { if (obj.initialize) { obj.initialize.apply(this, arguments); } else { extendObj.apply(this, arguments); } }; let proto = WebConsoleUtils.cloneObject(obj, false, function (key) { if (key == "initialize" || key == "canRender" || (key == "render" && extendObj === Widgets.JSObject)) { return false; } return true; }); if (extendObj === Widgets.JSObject) { proto._render = obj.render; } constructor.canRender = obj.canRender; constructor.prototype = extend(extendObj.prototype, proto); if (obj.byClass) { Widgets.ObjectRenderers.byClass[obj.byClass] = constructor; } else if (obj.byKind) { Widgets.ObjectRenderers.byKind[obj.byKind] = constructor; } else { throw new Error("You are adding an object renderer without any byClass or " + "byKind property."); } }; /** * The widget used for displaying Date objects. */ Widgets.ObjectRenderers.add({ byClass: "Date", render: function () { let {preview} = this.objectActor; this.element = this.el("span.class-" + this.objectActor.class); let anchorText = this.objectActor.class; let anchorClass = "cm-variable"; if (preview && "timestamp" in preview && typeof preview.timestamp != "number") { anchorText = new Date(preview.timestamp).toString(); // invalid date anchorClass = ""; } this._anchor(anchorText, { className: anchorClass }); if (!preview || !("timestamp" in preview) || typeof preview.timestamp != "number") { return; } this._text(" "); let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString()); this.element.appendChild(elem); }, }); /** * The widget used for displaying Function objects. */ Widgets.ObjectRenderers.add({ byClass: "Function", render: function () { let grip = this.objectActor; this.element = this.el("span.class-" + this.objectActor.class); // TODO: Bug 948484 - support arrow functions and ES6 generators let name = grip.userDisplayName || grip.displayName || grip.name || ""; name = VariablesView.getString(name, { noStringQuotes: true }); let str = this.options.concise ? name || "function " : "function " + name; if (this.options.concise) { this._anchor(name || "function", { className: name ? "cm-variable" : "cm-keyword", }); if (!name) { this._text(" "); } } else if (name) { this.element.appendChild(this.el("span.cm-keyword", "function")); this._text(" "); this._anchor(name, { className: "cm-variable" }); } else { this._anchor("function", { className: "cm-keyword" }); this._text(" "); } this._text("("); // TODO: Bug 948489 - Support functions with destructured parameters and // rest parameters let params = grip.parameterNames || []; let shown = 0; for (let param of params) { if (shown > 0) { this._text(", "); } this.element.appendChild(this.el("span.cm-def", param)); shown++; } this._text(")"); }, _onClick: function () { let location = this.objectActor.location; if (location && IGNORED_SOURCE_URLS.indexOf(location.url) === -1) { this.output.openLocationInDebugger(location); } else { this.openObjectInVariablesView(); } } }); // Widgets.ObjectRenderers.byClass.Function /** * The widget used for displaying ArrayLike objects. */ Widgets.ObjectRenderers.add({ byKind: "ArrayLike", render: function () { let {preview} = this.objectActor; let {items} = preview; this.element = this.el("span.kind-" + preview.kind); this._anchor(this.objectActor.class, { className: "cm-variable" }); if (!items || this.options.concise) { this._text("["); this.element.appendChild(this.el("span.cm-number", preview.length)); this._text("]"); return this; } this._text(" [ "); let isFirst = true; let emptySlots = 0; // A helper that renders a comma between items if isFirst == false. let renderSeparator = () => !isFirst && this._text(", "); for (let item of items) { if (item === null) { emptySlots++; } else { renderSeparator(); isFirst = false; if (emptySlots) { this._renderEmptySlots(emptySlots); emptySlots = 0; } let elem = this.message._renderValueGrip(item, { concise: true, shorten: true }); this.element.appendChild(elem); } } if (emptySlots) { renderSeparator(); this._renderEmptySlots(emptySlots, false); } let shown = items.length; if (shown < preview.length) { this._text(", "); let n = preview.length - shown; let str = VariablesView.stringifiers._getNMoreString(n); this._anchor(str); } this._text(" ]"); }, _renderEmptySlots: function (aNumSlots, aAppendComma = true) { let slotLabel = l10n.getStr("emptySlotLabel"); let slotText = PluralForm.get(aNumSlots, slotLabel); this._text("<" + slotText.replace("#1", aNumSlots) + ">"); if (aAppendComma) { this._text(", "); } }, }); // Widgets.ObjectRenderers.byKind.ArrayLike /** * The widget used for displaying MapLike objects. */ Widgets.ObjectRenderers.add({ byKind: "MapLike", render: function () { let {preview} = this.objectActor; let {entries} = preview; let container = this.element = this.el("span.kind-" + preview.kind); this._anchor(this.objectActor.class, { className: "cm-variable" }); if (!entries || this.options.concise) { if (typeof preview.size == "number") { this._text("["); container.appendChild(this.el("span.cm-number", preview.size)); this._text("]"); } return; } this._text(" { "); let shown = 0; for (let [key, value] of entries) { if (shown > 0) { this._text(", "); } let keyElem = this.message._renderValueGrip(key, { concise: true, noStringQuotes: true, }); // Strings are property names. if (keyElem.classList && keyElem.classList.contains("console-string")) { keyElem.classList.remove("console-string"); keyElem.classList.add("cm-property"); } container.appendChild(keyElem); this._text(": "); let valueElem = this.message._renderValueGrip(value, { concise: true }); container.appendChild(valueElem); shown++; } if (typeof preview.size == "number" && shown < preview.size) { this._text(", "); let n = preview.size - shown; let str = VariablesView.stringifiers._getNMoreString(n); this._anchor(str); } this._text(" }"); }, }); // Widgets.ObjectRenderers.byKind.MapLike /** * The widget used for displaying objects with a URL. */ Widgets.ObjectRenderers.add({ byKind: "ObjectWithURL", render: function () { this.element = this._renderElement(this.objectActor, this.objectActor.preview.url); }, _renderElement: function (objectActor, url) { let container = this.el("span.kind-" + objectActor.preview.kind); this._anchor(objectActor.class, { className: "cm-variable", appendTo: container, }); if (!VariablesView.isFalsy({ value: url })) { this._text(" \u2192 ", container); let shortUrl = getSourceNames(url)[this.options.concise ? "short" : "long"]; this._anchor(shortUrl, { href: url, appendTo: container }); } return container; }, }); // Widgets.ObjectRenderers.byKind.ObjectWithURL /** * The widget used for displaying objects with a string next to them. */ Widgets.ObjectRenderers.add({ byKind: "ObjectWithText", render: function () { let {preview} = this.objectActor; this.element = this.el("span.kind-" + preview.kind); this._anchor(this.objectActor.class, { className: "cm-variable" }); if (!this.options.concise) { this._text(" "); this.element.appendChild(this.el("span.theme-fg-color6", VariablesView.getString(preview.text))); } }, }); /** * The widget used for displaying DOM event previews. */ Widgets.ObjectRenderers.add({ byKind: "DOMEvent", render: function () { let {preview} = this.objectActor; let container = this.element = this.el("span.kind-" + preview.kind); this._anchor(preview.type || this.objectActor.class, { className: "cm-variable" }); if (this.options.concise) { return; } if (preview.eventKind == "key" && preview.modifiers && preview.modifiers.length) { this._text(" "); let mods = 0; for (let mod of preview.modifiers) { if (mods > 0) { this._text("-"); } container.appendChild(this.el("span.cm-keyword", mod)); mods++; } } this._text(" { "); let shown = 0; if (preview.target) { container.appendChild(this.el("span.cm-property", "target")); this._text(": "); let target = this.message._renderValueGrip(preview.target, { concise: true }); container.appendChild(target); shown++; } for (let key of Object.keys(preview.properties || {})) { if (shown > 0) { this._text(", "); } container.appendChild(this.el("span.cm-property", key)); this._text(": "); let value = preview.properties[key]; let valueElem = this.message._renderValueGrip(value, { concise: true }); container.appendChild(valueElem); shown++; } this._text(" }"); }, }); // Widgets.ObjectRenderers.byKind.DOMEvent /** * The widget used for displaying DOM node previews. */ Widgets.ObjectRenderers.add({ byKind: "DOMNode", canRender: function (objectActor) { let {preview} = objectActor; if (!preview) { return false; } switch (preview.nodeType) { case nodeConstants.DOCUMENT_NODE: case nodeConstants.ATTRIBUTE_NODE: case nodeConstants.TEXT_NODE: case nodeConstants.COMMENT_NODE: case nodeConstants.DOCUMENT_FRAGMENT_NODE: case nodeConstants.ELEMENT_NODE: return true; default: return false; } }, render: function () { switch (this.objectActor.preview.nodeType) { case nodeConstants.DOCUMENT_NODE: this._renderDocumentNode(); break; case nodeConstants.ATTRIBUTE_NODE: { let {preview} = this.objectActor; this.element = this.el("span.attributeNode.kind-" + preview.kind); let attr = this._renderAttributeNode(preview.nodeName, preview.value, true); this.element.appendChild(attr); break; } case nodeConstants.TEXT_NODE: this._renderTextNode(); break; case nodeConstants.COMMENT_NODE: this._renderCommentNode(); break; case nodeConstants.DOCUMENT_FRAGMENT_NODE: this._renderDocumentFragmentNode(); break; case nodeConstants.ELEMENT_NODE: this._renderElementNode(); break; default: throw new Error("Unsupported nodeType: " + preview.nodeType); } }, _renderDocumentNode: function () { let fn = Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement; this.element = fn.call(this, this.objectActor, this.objectActor.preview.location); this.element.classList.add("documentNode"); }, _renderAttributeNode: function (nodeName, nodeValue, addLink) { let value = VariablesView.getString(nodeValue, { noStringQuotes: true }); let fragment = this.document.createDocumentFragment(); if (addLink) { this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment }); } else { fragment.appendChild(this.el("span.cm-attribute", nodeName)); } this._text("=\"", fragment); fragment.appendChild(this.el("span.theme-fg-color6", escapeHTML(value))); this._text("\"", fragment); return fragment; }, _renderTextNode: function () { let {preview} = this.objectActor; this.element = this.el("span.textNode.kind-" + preview.kind); this._anchor(preview.nodeName, { className: "cm-variable" }); this._text(" "); let text = VariablesView.getString(preview.textContent); this.element.appendChild(this.el("span.console-string", text)); }, _renderCommentNode: function () { let {preview} = this.objectActor; let comment = ""; this.element = this._anchor(comment, { className: "kind-" + preview.kind + " commentNode cm-comment", }); }, _renderDocumentFragmentNode: function () { let {preview} = this.objectActor; let {childNodes} = preview; let container = this.element = this.el("span.documentFragmentNode.kind-" + preview.kind); this._anchor(this.objectActor.class, { className: "cm-variable" }); if (!childNodes || this.options.concise) { this._text("["); container.appendChild(this.el("span.cm-number", preview.childNodesLength)); this._text("]"); return; } this._text(" [ "); let shown = 0; for (let item of childNodes) { if (shown > 0) { this._text(", "); } let elem = this.message._renderValueGrip(item, { concise: true }); container.appendChild(elem); shown++; } if (shown < preview.childNodesLength) { this._text(", "); let n = preview.childNodesLength - shown; let str = VariablesView.stringifiers._getNMoreString(n); this._anchor(str); } this._text(" ]"); }, _renderElementNode: function () { let doc = this.document; let {attributes, nodeName} = this.objectActor.preview; this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode"); this._text("<"); let openTag = this.el("span.cm-tag"); this.element.appendChild(openTag); let tagName = this._anchor(nodeName, { className: "cm-tag", appendTo: openTag }); if (this.options.concise) { if (attributes.id) { tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id)); } if (attributes.class) { tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join("."))); } } else { for (let name of Object.keys(attributes)) { let attr = this._renderAttributeNode(" " + name, attributes[name]); this.element.appendChild(attr); } } this._text(">"); // Register this widget in the owner message so that it gets destroyed when // the message is destroyed. this.message.widgets.add(this); this.linkToInspector().then(null, e => console.error(e)); }, /** * If the DOMNode being rendered can be highlit in the page, this function * will attach mouseover/out event listeners to do so, and the inspector icon * to open the node in the inspector. * @return a promise that resolves when the node has been linked to the * inspector, or rejects if it wasn't (either if no toolbox could be found to * access the inspector, or if the node isn't present in the inspector, i.e. * if the node is in a DocumentFragment or not part of the tree, or not of * type nodeConstants.ELEMENT_NODE). */ linkToInspector: Task.async(function* () { if (this._linkedToInspector) { return; } // Checking the node type if (this.objectActor.preview.nodeType !== nodeConstants.ELEMENT_NODE) { throw new Error("The object cannot be linked to the inspector as it " + "isn't an element node"); } // Checking the presence of a toolbox let target = this.message.output.toolboxTarget; this.toolbox = gDevTools.getToolbox(target); if (!this.toolbox) { // In cases like the browser console, there is no toolbox. return; } // Checking that the inspector supports the node yield this.toolbox.initInspector(); this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor); if (!this._nodeFront) { throw new Error("The object cannot be linked to the inspector, the " + "corresponding nodeFront could not be found"); } // At this stage, the message may have been cleared already if (!this.document) { throw new Error("The object cannot be linked to the inspector, the " + "message was got cleared away"); } // Check it again as this method is async! if (this._linkedToInspector) { return; } this._linkedToInspector = true; this.highlightDomNode = this.highlightDomNode.bind(this); this.element.addEventListener("mouseover", this.highlightDomNode, false); this.unhighlightDomNode = this.unhighlightDomNode.bind(this); this.element.addEventListener("mouseout", this.unhighlightDomNode, false); this._openInspectorNode = this._anchor("", { className: "open-inspector", onClick: this.openNodeInInspector.bind(this) }); this._openInspectorNode.title = l10n.getStr("openNodeInInspector"); }), /** * Highlight the DOMNode corresponding to the ObjectActor in the page. * @return a promise that resolves when the node has been highlighted, or * rejects if the node cannot be highlighted (detached from the DOM) */ highlightDomNode: Task.async(function* () { yield this.linkToInspector(); let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront); if (isAttached) { yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront); } else { throw null; } }), /** * Unhighlight a previously highlit node * @see highlightDomNode * @return a promise that resolves when the highlighter has been hidden */ unhighlightDomNode: function () { return this.linkToInspector().then(() => { return this.toolbox.highlighterUtils.unhighlight(); }).then(null, e => console.error(e)); }, /** * Open the DOMNode corresponding to the ObjectActor in the inspector panel * @return a promise that resolves when the inspector has been switched to * and the node has been selected, or rejects if the node cannot be selected * (detached from the DOM). Note that in any case, the inspector panel will * be switched to. */ openNodeInInspector: Task.async(function* () { yield this.linkToInspector(); yield this.toolbox.selectTool("inspector"); let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront); if (isAttached) { let onReady = promise.defer(); this.toolbox.inspector.once("inspector-updated", onReady.resolve); yield this.toolbox.selection.setNodeFront(this._nodeFront, "console"); yield onReady.promise; } else { throw null; } }), destroy: function () { if (this.toolbox && this._nodeFront) { this.element.removeEventListener("mouseover", this.highlightDomNode, false); this.element.removeEventListener("mouseout", this.unhighlightDomNode, false); this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true); if (this._linkedToInspector) { this.unhighlightDomNode().then(() => { this.toolbox = null; this._nodeFront = null; }); } else { this.toolbox = null; this._nodeFront = null; } } }, }); // Widgets.ObjectRenderers.byKind.DOMNode /** * The widget user for displaying Promise objects. */ Widgets.ObjectRenderers.add({ byClass: "Promise", render: function () { let { ownProperties, safeGetterValues } = this.objectActor.preview || {}; if ((!ownProperties && !safeGetterValues) || this.options.concise) { this._renderConciseObject(); return; } this._renderObjectPrefix(); let container = this.element; let addedPromiseInternalProps = false; if (this.objectActor.promiseState) { const { state, value, reason } = this.objectActor.promiseState; this._renderObjectProperty("", state, container, false); addedPromiseInternalProps = true; if (state == "fulfilled") { this._renderObjectProperty("", value, container, true); } else if (state == "rejected") { this._renderObjectProperty("", reason, container, true); } } this._renderObjectProperties(container, addedPromiseInternalProps); this._renderObjectSuffix(); } }); // Widgets.ObjectRenderers.byClass.Promise /* * A renderer used for wrapped primitive objects. */ function WrappedPrimitiveRenderer() { let { ownProperties, safeGetterValues } = this.objectActor.preview || {}; if ((!ownProperties && !safeGetterValues) || this.options.concise) { this._renderConciseObject(); return; } this._renderObjectPrefix(); let elem = this.message._renderValueGrip(this.objectActor.preview.wrappedValue); this.element.appendChild(elem); this._renderObjectProperties(this.element, true); this._renderObjectSuffix(); } /** * The widget used for displaying Boolean previews. */ Widgets.ObjectRenderers.add({ byClass: "Boolean", render: WrappedPrimitiveRenderer, }); /** * The widget used for displaying Number previews. */ Widgets.ObjectRenderers.add({ byClass: "Number", render: WrappedPrimitiveRenderer, }); /** * The widget used for displaying String previews. */ Widgets.ObjectRenderers.add({ byClass: "String", render: WrappedPrimitiveRenderer, }); /** * The widget used for displaying generic JS object previews. */ Widgets.ObjectRenderers.add({ byKind: "Object", render: function () { let { ownProperties, safeGetterValues } = this.objectActor.preview || {}; if ((!ownProperties && !safeGetterValues) || this.options.concise) { this._renderConciseObject(); return; } this._renderObjectPrefix(); this._renderObjectProperties(this.element, false); this._renderObjectSuffix(); }, }); // Widgets.ObjectRenderers.byKind.Object /** * The long string widget. * * @constructor * @param object message * The owning message. * @param object longStringActor * The LongStringActor to display. * @param object options * Options, such as noStringQuotes */ Widgets.LongString = function (message, longStringActor, options) { Widgets.BaseWidget.call(this, message); this.longStringActor = longStringActor; this.noStringQuotes = (options && "noStringQuotes" in options) ? options.noStringQuotes : !this.message._quoteStrings; this._onClick = this._onClick.bind(this); this._onSubstring = this._onSubstring.bind(this); }; Widgets.LongString.prototype = extend(Widgets.BaseWidget.prototype, { /** * The LongStringActor displayed by the widget. * @type object */ longStringActor: null, render: function () { if (this.element) { return this; } let result = this.element = this.document.createElementNS(XHTML_NS, "span"); result.className = "longString console-string"; this._renderString(this.longStringActor.initial); result.appendChild(this._renderEllipsis()); return this; }, /** * Render the long string in the widget element. * @private * @param string str * The string to display. */ _renderString: function (str) { this.element.textContent = VariablesView.getString(str, { noStringQuotes: this.noStringQuotes, noEllipsis: true, }); }, /** * Render the anchor ellipsis that allows the user to expand the long string. * * @private * @return Element */ _renderEllipsis: function () { let ellipsis = this.document.createElementNS(XHTML_NS, "a"); ellipsis.className = "longStringEllipsis"; ellipsis.textContent = l10n.getStr("longStringEllipsis"); ellipsis.href = "#"; ellipsis.draggable = false; this.message._addLinkCallback(ellipsis, this._onClick); return ellipsis; }, /** * The click event handler for the ellipsis shown after the short string. This * function expands the element to show the full string. * @private */ _onClick: function () { let longString = this.output.webConsoleClient.longString(this.longStringActor); let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH); longString.substring(longString.initial.length, toIndex, this._onSubstring); }, /** * The longString substring response callback. * * @private * @param object response * Response packet. */ _onSubstring: function (response) { if (response.error) { console.error("LongString substring failure: " + response.error); return; } this.element.lastChild.remove(); this.element.classList.remove("longString"); this._renderString(this.longStringActor.initial + response.substring); this.output.owner.emit("new-messages", new Set([{ update: true, node: this.message.element, response: response, }])); let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH); if (toIndex != this.longStringActor.length) { this._logWarningAboutStringTooLong(); } }, /** * Inform user that the string he tries to view is too long. * @private */ _logWarningAboutStringTooLong: function () { let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), { category: "output", severity: "warning", }); this.output.addMessage(msg); }, }); // Widgets.LongString.prototype /** * The stacktrace widget. * * @constructor * @extends Widgets.BaseWidget * @param object message * The owning message. * @param array stacktrace * The stacktrace to display, array of frames as supplied by the server, * over the remote protocol. */ Widgets.Stacktrace = function (message, stacktrace) { Widgets.BaseWidget.call(this, message); this.stacktrace = stacktrace; }; Widgets.Stacktrace.prototype = extend(Widgets.BaseWidget.prototype, { /** * The stackframes received from the server. * @type array */ stacktrace: null, render() { if (this.element) { return this; } let result = this.element = this.document.createElementNS(XHTML_NS, "div"); result.className = "stacktrace devtools-monospace"; if (this.stacktrace) { this.output.owner.ReactDOM.render(this.output.owner.StackTraceView({ stacktrace: this.stacktrace, onViewSourceInDebugger: frame => this.output.openLocationInDebugger(frame) }), result); } return this; } }); /** * The table widget. * * @constructor * @extends Widgets.BaseWidget * @param object message * The owning message. * @param array data * Array of objects that holds the data to log in the table. * @param object columns * Object containing the key value pair of the id and display name for * the columns in the table. */ Widgets.Table = function (message, data, columns) { Widgets.BaseWidget.call(this, message); this.data = data; this.columns = columns; }; Widgets.Table.prototype = extend(Widgets.BaseWidget.prototype, { /** * Array of objects that holds the data to output in the table. * @type array */ data: null, /** * Object containing the key value pair of the id and display name for * the columns in the table. * @type object */ columns: null, render: function () { if (this.element) { return this; } let result = this.element = this.document.createElementNS(XHTML_NS, "div"); result.className = "consoletable devtools-monospace"; this.table = new TableWidget(result, { wrapTextInElements: true, initialColumns: this.columns, uniqueId: "_index", firstColumn: "_index" }); for (let row of this.data) { this.table.push(row); } return this; } }); // Widgets.Table.prototype function gSequenceId() { return gSequenceId.n++; } gSequenceId.n = 0; exports.ConsoleOutput = ConsoleOutput; exports.Messages = Messages; exports.Widgets = Widgets;