gecko/browser/devtools/webconsole/console-output.js

1609 lines
40 KiB
JavaScript

/* vim: set 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 {Cc, Ci, Cu} = require("chrome");
loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
const Heritage = 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 = "chrome://browser/locale/devtools/webconsole.properties";
const WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
const l10n = new WebConsoleUtils.l10n(STRINGS_URI);
// 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,
},
// 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".
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
],
// The fragment of a CSS class name that identifies each category.
CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
"input", "output", "security" ],
// 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,
// The default indent in pixels, applied even without any groups.
GROUP_INDENT_DEFAULT: 6,
};
// 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",
trace: "log",
debug: "log",
dir: "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", "self-hosted"];
// The maximum length of strings to be displayed by the Web Console.
const MAX_LONG_STRING_LENGTH = 200000;
/**
* 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 = {
/**
* The output container.
* @type DOMElement
*/
get element() {
return this.owner.outputNode;
},
/**
* The document that holds the output.
* @type DOMDocument
*/
get document() {
return this.owner.document;
},
/**
* 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;
},
/**
* 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);
},
/**
* 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.owner = null;
},
}; // ConsoleOutput.prototype
/**
* Message objects container.
* @type object
*/
let 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";
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);
},
}; // Messages.BaseMessage.prototype
/**
* The NavigationMarker is used to show a page load event.
*
* @constructor
* @extends Messages.BaseMessage
* @param string url
* The URL to display.
* @param number timestamp
* The message date and time, milliseconds elapsed since 1 January 1970
* 00:00:00 UTC.
*/
Messages.NavigationMarker = function(url, timestamp)
{
Messages.BaseMessage.call(this);
this._url = url;
this.textContent = "------ " + url;
this.timestamp = timestamp;
};
Messages.NavigationMarker.prototype = Heritage.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;
},
}); // Messages.NavigationMarker.prototype
/**
* 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.
* - 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.timestamp = options.timestamp || Date.now();
this.private = !!options.private;
this._message = message;
this._className = options.className;
this._link = options.link;
this._linkCallback = options.linkCallback;
this._filterDuplicates = options.filterDuplicates;
};
Messages.Simple.prototype = Heritage.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,
/**
* 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,
_afterMessage: 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;
},
_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.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";
// Apply the current group by indenting appropriately.
// TODO: remove this once bug 778766 is fixed.
let iconMarginLeft = this._groupDepthCompat * COMPAT.GROUP_INDENT +
COMPAT.GROUP_INDENT_DEFAULT;
icon.style.marginLeft = iconMarginLeft + "px";
let body = this._renderBody();
this._repeatID.textContent += "|" + body.textContent;
let repeatNode = this._renderRepeatNode();
let location = this._renderLocation();
Messages.BaseMessage.prototype.render.call(this);
if (this._className) {
this.element.className += " " + this._className;
}
this.element.appendChild(timestamp.element);
this.element.appendChild(icon);
this.element.appendChild(body);
if (repeatNode) {
this.element.appendChild(repeatNode);
}
if (location) {
this.element.appendChild(location);
}
this.element.appendChild(this.document.createTextNode("\n"));
this.element.clipboardText = this.element.textContent;
if (this.private) {
this.element.setAttribute("private", true);
}
if (this._afterMessage) {
this.element._outputAfterNode = this._afterMessage.element;
this._afterMessage = null;
}
// 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 body = this.document.createElementNS(XHTML_NS, "span");
body.className = "body devtools-monospace";
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;
}
return body;
},
/**
* 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} = 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);
},
}); // 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 = messagePieces + "";
this._repeatID.actors = new Set(); // using a set to avoid duplicates
};
Messages.Extended.prototype = Heritage.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)
{
if (piece instanceof Ci.nsIDOMNode) {
return piece;
}
if (typeof piece == "function") {
return piece(this);
}
let isPrimitive = VariablesView.isPrimitive({ value: piece });
let isActorGrip = WebConsoleUtils.isActorGrip(piece);
if (isActorGrip) {
this._repeatID.actors.add(piece.actor);
if (!isPrimitive) {
let widget = new Widgets.JSObject(this, piece).render();
return widget.element;
}
if (piece.type == "longString") {
let widget = new Widgets.LongString(this, piece).render();
return widget.element;
}
}
let result = this.document.createDocumentFragment();
if (isPrimitive) {
result.textContent = VariablesView.getString(piece, {
noStringQuotes: !this._quoteStrings,
});
} else {
result.textContent = piece;
}
return result;
},
}); // 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.
*/
Messages.JavaScriptEvalOutput = function(evalResponse, errorMessage)
{
let severity = "log", msg, quoteStrings = true;
if (errorMessage) {
severity = "error";
msg = errorMessage;
quoteStrings = false;
} else {
msg = evalResponse.result;
}
let options = {
timestamp: evalResponse.timestamp,
category: "output",
severity: severity,
quoteStrings: quoteStrings,
};
Messages.Extended.call(this, [msg], 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 = {
timestamp: packet.timeStamp,
category: "webdev",
severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
private: packet.private,
filterDuplicates: true,
location: {
url: packet.filename,
line: packet.lineNumber,
},
};
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;
};
Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
{
_renderBodyPieceSeparator: function()
{
return this.document.createTextNode(" ");
},
}); // 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: "consoleTrace cm-s-mozilla",
timestamp: packet.timeStamp,
category: "webdev",
severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
private: packet.private,
filterDuplicates: true,
location: {
url: packet.filename,
line: packet.lineNumber,
},
};
this._renderStack = this._renderStack.bind(this);
Messages.Simple.call(this, this._renderStack, options);
this._repeatID.consoleApiLevel = packet.level;
this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
this._arguments = packet.arguments;
};
Messages.ConsoleTrace.prototype = Heritage.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 the stack frames.
*
* @private
* @return DOMElement
*/
_renderStack: 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 title = this.document.createElementNS(XHTML_NS, "span");
title.className = "title devtools-monospace";
title.appendChild(cmvar);
title.appendChild(this.document.createTextNode("."));
title.appendChild(cmprop);
title.appendChild(this.document.createTextNode("():"));
let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
let location = Messages.Simple.prototype._renderLocation.call(this);
if (location) {
location.target = "jsdebugger";
}
let widget = new Widgets.Stacktrace(this, this._stacktrace).render();
let body = this.document.createElementNS(XHTML_NS, "div");
body.appendChild(title);
if (repeatNode) {
body.appendChild(repeatNode);
}
if (location) {
body.appendChild(location);
}
body.appendChild(this.document.createTextNode("\n"));
let frag = this.document.createDocumentFragment();
frag.appendChild(body);
frag.appendChild(widget.element);
return frag;
},
_renderBody: function()
{
let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
body.classList.remove("devtools-monospace");
return body;
},
// no-op for the message location and .repeats elements.
// |this._renderStack| handles customized message output.
_renderLocation: function() { },
_renderRepeatNode: function() { },
}); // Messages.ConsoleTrace.prototype
let 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() { },
};
/**
* 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 = Heritage.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 JavaScript object widget.
*
* @constructor
* @param object message
* The owning message.
* @param object objectActor
* The ObjectActor to display.
*/
Widgets.JSObject = function(message, objectActor)
{
Widgets.BaseWidget.call(this, message);
this.objectActor = objectActor;
this._onClick = this._onClick.bind(this);
};
Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
{
/**
* The ObjectActor displayed by the widget.
* @type object
*/
objectActor: null,
render: function()
{
if (this.element) {
return this;
}
let anchor = this.element = this.document.createElementNS(XHTML_NS, "a");
anchor.href = "#";
anchor.draggable = false;
anchor.textContent = VariablesView.getString(this.objectActor);
this.message._addLinkCallback(anchor, this._onClick);
return this;
},
/**
* The click event handler for objects shown inline.
* @private
*/
_onClick: function()
{
this.output.openVariablesView({
label: VariablesView.getString(this.objectActor, { concise: true }),
objectActor: this.objectActor,
autofocus: true,
});
},
}); // Widgets.JSObject.prototype
/**
* The long string widget.
*
* @constructor
* @param object message
* The owning message.
* @param object longStringActor
* The LongStringActor to display.
*/
Widgets.LongString = function(message, longStringActor)
{
Widgets.BaseWidget.call(this, message);
this.longStringActor = longStringActor;
this._onClick = this._onClick.bind(this);
this._onSubstring = this._onSubstring.bind(this);
};
Widgets.LongString.prototype = Heritage.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";
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.message._quoteStrings,
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) {
Cu.reportError("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("messages-updated", new Set([this.message.element]));
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 = Heritage.extend(Widgets.BaseWidget.prototype,
{
/**
* The stackframes received from the server.
* @type array
*/
stacktrace: null,
render: function()
{
if (this.element) {
return this;
}
let result = this.element = this.document.createElementNS(XHTML_NS, "ul");
result.className = "stacktrace devtools-monospace";
for (let frame of this.stacktrace) {
result.appendChild(this._renderFrame(frame));
}
return this;
},
/**
* Render a frame object received from the server.
*
* @param object frame
* The stack frame to display. This object should have the following
* properties: functionName, filename and lineNumber.
* @return DOMElement
* The DOM element to display for the given frame.
*/
_renderFrame: function(frame)
{
let fn = this.document.createElementNS(XHTML_NS, "span");
fn.className = "function";
if (frame.functionName) {
let span = this.document.createElementNS(XHTML_NS, "span");
span.className = "cm-variable";
span.textContent = frame.functionName;
fn.appendChild(span);
fn.appendChild(this.document.createTextNode("()"));
} else {
fn.classList.add("cm-comment");
fn.textContent = l10n.getStr("stacktrace.anonymousFunction");
}
let location = this.output.owner.createLocationNode(frame.filename,
frame.lineNumber,
"jsdebugger");
// .devtools-monospace sets font-size to 80%, however .body already has
// .devtools-monospace. If we keep it here, the location would be rendered
// smaller.
location.classList.remove("devtools-monospace");
let elem = this.document.createElementNS(XHTML_NS, "li");
elem.appendChild(fn);
elem.appendChild(location);
elem.appendChild(this.document.createTextNode("\n"));
return elem;
},
}); // Widgets.Stacktrace.prototype
function gSequenceId()
{
return gSequenceId.n++;
}
gSequenceId.n = 0;
exports.ConsoleOutput = ConsoleOutput;
exports.Messages = Messages;
exports.Widgets = Widgets;