browser/devtools/webconsole/console-output.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* vim: set ts=2 et sw=2 tw=80: */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 "use strict";
     8 const {Cc, Ci, Cu} = require("chrome");
    10 loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
    11 loader.lazyImporter(this, "escapeHTML", "resource:///modules/devtools/VariablesView.jsm");
    12 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
    13 loader.lazyImporter(this, "Task","resource://gre/modules/Task.jsm");
    15 const Heritage = require("sdk/core/heritage");
    16 const XHTML_NS = "http://www.w3.org/1999/xhtml";
    17 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    18 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
    20 const WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
    21 const l10n = new WebConsoleUtils.l10n(STRINGS_URI);
    23 // Constants for compatibility with the Web Console output implementation before
    24 // bug 778766.
    25 // TODO: remove these once bug 778766 is fixed.
    26 const COMPAT = {
    27   // The various categories of messages.
    28   CATEGORIES: {
    29     NETWORK: 0,
    30     CSS: 1,
    31     JS: 2,
    32     WEBDEV: 3,
    33     INPUT: 4,
    34     OUTPUT: 5,
    35     SECURITY: 6,
    36   },
    38   // The possible message severities.
    39   SEVERITIES: {
    40     ERROR: 0,
    41     WARNING: 1,
    42     INFO: 2,
    43     LOG: 3,
    44   },
    46   // The preference keys to use for each category/severity combination, indexed
    47   // first by category (rows) and then by severity (columns).
    48   //
    49   // Most of these rather idiosyncratic names are historical and predate the
    50   // division of message type into "category" and "severity".
    51   PREFERENCE_KEYS: [
    52     // Error        Warning       Info    Log
    53     [ "network",    "netwarn",    null,   "networkinfo", ],  // Network
    54     [ "csserror",   "cssparser",  null,   null,          ],  // CSS
    55     [ "exception",  "jswarn",     null,   "jslog",       ],  // JS
    56     [ "error",      "warn",       "info", "log",         ],  // Web Developer
    57     [ null,         null,         null,   null,          ],  // Input
    58     [ null,         null,         null,   null,          ],  // Output
    59     [ "secerror",   "secwarn",    null,   null,          ],  // Security
    60   ],
    62   // The fragment of a CSS class name that identifies each category.
    63   CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
    64                               "input", "output", "security" ],
    66   // The fragment of a CSS class name that identifies each severity.
    67   SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],
    69   // The indent of a console group in pixels.
    70   GROUP_INDENT: 12,
    71 };
    73 // A map from the console API call levels to the Web Console severities.
    74 const CONSOLE_API_LEVELS_TO_SEVERITIES = {
    75   error: "error",
    76   exception: "error",
    77   assert: "error",
    78   warn: "warning",
    79   info: "info",
    80   log: "log",
    81   trace: "log",
    82   debug: "log",
    83   dir: "log",
    84   group: "log",
    85   groupCollapsed: "log",
    86   groupEnd: "log",
    87   time: "log",
    88   timeEnd: "log",
    89   count: "log"
    90 };
    92 // Array of known message source URLs we need to hide from output.
    93 const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"];
    95 // The maximum length of strings to be displayed by the Web Console.
    96 const MAX_LONG_STRING_LENGTH = 200000;
    98 // Regular expression that matches the allowed CSS property names when using
    99 // the `window.console` API.
   100 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)/;
   102 // Regular expressions to search and replace with 'notallowed' in the styles
   103 // given to the `window.console` API methods.
   104 const RE_CLEANUP_STYLES = [
   105   // url(), -moz-element()
   106   /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
   108   // various URL protocols
   109   /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
   110 ];
   112 /**
   113  * The ConsoleOutput object is used to manage output of messages in the Web
   114  * Console.
   115  *
   116  * @constructor
   117  * @param object owner
   118  *        The console output owner. This usually the WebConsoleFrame instance.
   119  *        Any other object can be used, as long as it has the following
   120  *        properties and methods:
   121  *          - window
   122  *          - document
   123  *          - outputMessage(category, methodOrNode[, methodArguments])
   124  *            TODO: this is needed temporarily, until bug 778766 is fixed.
   125  */
   126 function ConsoleOutput(owner)
   127 {
   128   this.owner = owner;
   129   this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this);
   130 }
   132 ConsoleOutput.prototype = {
   133   _dummyElement: null,
   135   /**
   136    * The output container.
   137    * @type DOMElement
   138    */
   139   get element() {
   140     return this.owner.outputNode;
   141   },
   143   /**
   144    * The document that holds the output.
   145    * @type DOMDocument
   146    */
   147   get document() {
   148     return this.owner ? this.owner.document : null;
   149   },
   151   /**
   152    * The DOM window that holds the output.
   153    * @type Window
   154    */
   155   get window() {
   156     return this.owner.window;
   157   },
   159   /**
   160    * Getter for the debugger WebConsoleClient.
   161    * @type object
   162    */
   163   get webConsoleClient() {
   164     return this.owner.webConsoleClient;
   165   },
   167   /**
   168    * Getter for the current toolbox debuggee target.
   169    * @type Target
   170    */
   171   get toolboxTarget() {
   172     return this.owner.owner.target;
   173   },
   175   /**
   176    * Release an actor.
   177    *
   178    * @private
   179    * @param string actorId
   180    *        The actor ID you want to release.
   181    */
   182   _releaseObject: function(actorId)
   183   {
   184     this.owner._releaseObject(actorId);
   185   },
   187   /**
   188    * Add a message to output.
   189    *
   190    * @param object ...args
   191    *        Any number of Message objects.
   192    * @return this
   193    */
   194   addMessage: function(...args)
   195   {
   196     for (let msg of args) {
   197       msg.init(this);
   198       this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage,
   199                                [msg]);
   200     }
   201     return this;
   202   },
   204   /**
   205    * Message renderer used for compatibility with the current Web Console output
   206    * implementation. This method is invoked for every message object that is
   207    * flushed to output. The message object is initialized and rendered, then it
   208    * is displayed.
   209    *
   210    * TODO: remove this method once bug 778766 is fixed.
   211    *
   212    * @private
   213    * @param object message
   214    *        The message object to render.
   215    * @return DOMElement
   216    *         The message DOM element that can be added to the console output.
   217    */
   218   _onFlushOutputMessage: function(message)
   219   {
   220     return message.render().element;
   221   },
   223   /**
   224    * Get an array of selected messages. This list is based on the text selection
   225    * start and end points.
   226    *
   227    * @param number [limit]
   228    *        Optional limit of selected messages you want. If no value is given,
   229    *        all of the selected messages are returned.
   230    * @return array
   231    *         Array of DOM elements for each message that is currently selected.
   232    */
   233   getSelectedMessages: function(limit)
   234   {
   235     let selection = this.window.getSelection();
   236     if (selection.isCollapsed) {
   237       return [];
   238     }
   240     if (selection.containsNode(this.element, true)) {
   241       return Array.slice(this.element.children);
   242     }
   244     let anchor = this.getMessageForElement(selection.anchorNode);
   245     let focus = this.getMessageForElement(selection.focusNode);
   246     if (!anchor || !focus) {
   247       return [];
   248     }
   250     let start, end;
   251     if (anchor.timestamp > focus.timestamp) {
   252       start = focus;
   253       end = anchor;
   254     } else {
   255       start = anchor;
   256       end = focus;
   257     }
   259     let result = [];
   260     let current = start;
   261     while (current) {
   262       result.push(current);
   263       if (current == end || (limit && result.length == limit)) {
   264         break;
   265       }
   266       current = current.nextSibling;
   267     }
   268     return result;
   269   },
   271   /**
   272    * Find the DOM element of a message for any given descendant.
   273    *
   274    * @param DOMElement elem
   275    *        The element to start the search from.
   276    * @return DOMElement|null
   277    *         The DOM element of the message, if any.
   278    */
   279   getMessageForElement: function(elem)
   280   {
   281     while (elem && elem.parentNode) {
   282       if (elem.classList && elem.classList.contains("message")) {
   283         return elem;
   284       }
   285       elem = elem.parentNode;
   286     }
   287     return null;
   288   },
   290   /**
   291    * Select all messages.
   292    */
   293   selectAllMessages: function()
   294   {
   295     let selection = this.window.getSelection();
   296     selection.removeAllRanges();
   297     let range = this.document.createRange();
   298     range.selectNodeContents(this.element);
   299     selection.addRange(range);
   300   },
   302   /**
   303    * Add a message to the selection.
   304    *
   305    * @param DOMElement elem
   306    *        The message element to select.
   307    */
   308   selectMessage: function(elem)
   309   {
   310     let selection = this.window.getSelection();
   311     selection.removeAllRanges();
   312     let range = this.document.createRange();
   313     range.selectNodeContents(elem);
   314     selection.addRange(range);
   315   },
   317   /**
   318    * Open an URL in a new tab.
   319    * @see WebConsole.openLink() in hudservice.js
   320    */
   321   openLink: function()
   322   {
   323     this.owner.owner.openLink.apply(this.owner.owner, arguments);
   324   },
   326   /**
   327    * Open the variables view to inspect an object actor.
   328    * @see JSTerm.openVariablesView() in webconsole.js
   329    */
   330   openVariablesView: function()
   331   {
   332     this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments);
   333   },
   335   /**
   336    * Destroy this ConsoleOutput instance.
   337    */
   338   destroy: function()
   339   {
   340     this._dummyElement = null;
   341     this.owner = null;
   342   },
   343 }; // ConsoleOutput.prototype
   345 /**
   346  * Message objects container.
   347  * @type object
   348  */
   349 let Messages = {};
   351 /**
   352  * The BaseMessage object is used for all types of messages. Every kind of
   353  * message should use this object as its base.
   354  *
   355  * @constructor
   356  */
   357 Messages.BaseMessage = function()
   358 {
   359   this.widgets = new Set();
   360   this._onClickAnchor = this._onClickAnchor.bind(this);
   361   this._repeatID = { uid: gSequenceId() };
   362   this.textContent = "";
   363 };
   365 Messages.BaseMessage.prototype = {
   366   /**
   367    * Reference to the ConsoleOutput owner.
   368    *
   369    * @type object|null
   370    *       This is |null| if the message is not yet initialized.
   371    */
   372   output: null,
   374   /**
   375    * Reference to the parent message object, if this message is in a group or if
   376    * it is otherwise owned by another message.
   377    *
   378    * @type object|null
   379    */
   380   parent: null,
   382   /**
   383    * Message DOM element.
   384    *
   385    * @type DOMElement|null
   386    *       This is |null| if the message is not yet rendered.
   387    */
   388   element: null,
   390   /**
   391    * Tells if this message is visible or not.
   392    * @type boolean
   393    */
   394   get visible() {
   395     return this.element && this.element.parentNode;
   396   },
   398   /**
   399    * The owner DOM document.
   400    * @type DOMElement
   401    */
   402   get document() {
   403     return this.output.document;
   404   },
   406   /**
   407    * Holds the text-only representation of the message.
   408    * @type string
   409    */
   410   textContent: null,
   412   /**
   413    * Set of widgets included in this message.
   414    * @type Set
   415    */
   416   widgets: null,
   418   // Properties that allow compatibility with the current Web Console output
   419   // implementation.
   420   _categoryCompat: null,
   421   _severityCompat: null,
   422   _categoryNameCompat: null,
   423   _severityNameCompat: null,
   424   _filterKeyCompat: null,
   426   /**
   427    * Object that is JSON-ified and used as a non-unique ID for tracking
   428    * duplicate messages.
   429    * @private
   430    * @type object
   431    */
   432   _repeatID: null,
   434   /**
   435    * Initialize the message.
   436    *
   437    * @param object output
   438    *        The ConsoleOutput owner.
   439    * @param object [parent=null]
   440    *        Optional: a different message object that owns this instance.
   441    * @return this
   442    */
   443   init: function(output, parent=null)
   444   {
   445     this.output = output;
   446     this.parent = parent;
   447     return this;
   448   },
   450   /**
   451    * Non-unique ID for this message object used for tracking duplicate messages.
   452    * Different message kinds can identify themselves based their own criteria.
   453    *
   454    * @return string
   455    */
   456   getRepeatID: function()
   457   {
   458     return JSON.stringify(this._repeatID);
   459   },
   461   /**
   462    * Render the message. After this method is invoked the |element| property
   463    * will point to the DOM element of this message.
   464    * @return this
   465    */
   466   render: function()
   467   {
   468     if (!this.element) {
   469       this.element = this._renderCompat();
   470     }
   471     return this;
   472   },
   474   /**
   475    * Prepare the message container for the Web Console, such that it is
   476    * compatible with the current implementation.
   477    * TODO: remove this once bug 778766 is fixed.
   478    *
   479    * @private
   480    * @return Element
   481    *         The DOM element that wraps the message.
   482    */
   483   _renderCompat: function()
   484   {
   485     let doc = this.output.document;
   486     let container = doc.createElementNS(XHTML_NS, "div");
   487     container.id = "console-msg-" + gSequenceId();
   488     container.className = "message";
   489     container.category = this._categoryCompat;
   490     container.severity = this._severityCompat;
   491     container.setAttribute("category", this._categoryNameCompat);
   492     container.setAttribute("severity", this._severityNameCompat);
   493     container.setAttribute("filter", this._filterKeyCompat);
   494     container.clipboardText = this.textContent;
   495     container.timestamp = this.timestamp;
   496     container._messageObject = this;
   498     return container;
   499   },
   501   /**
   502    * Add a click callback to a given DOM element.
   503    *
   504    * @private
   505    * @param Element element
   506    *        The DOM element to which you want to add a click event handler.
   507    * @param function [callback=this._onClickAnchor]
   508    *        Optional click event handler. The default event handler is
   509    *        |this._onClickAnchor|.
   510    */
   511   _addLinkCallback: function(element, callback = this._onClickAnchor)
   512   {
   513     // This is going into the WebConsoleFrame object instance that owns
   514     // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole
   515     // object instance from hudservice.js.
   516     // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766
   517     // is fixed.
   518     this.output.owner._addMessageLinkCallback(element, callback);
   519   },
   521   /**
   522    * The default |click| event handler for links in the output. This function
   523    * opens the anchor's link in a new tab.
   524    *
   525    * @private
   526    * @param Event event
   527    *        The DOM event that invoked this function.
   528    */
   529   _onClickAnchor: function(event)
   530   {
   531     this.output.openLink(event.target.href);
   532   },
   534   destroy: function()
   535   {
   536     // Destroy all widgets that have registered themselves in this.widgets
   537     for (let widget of this.widgets) {
   538       widget.destroy();
   539     }
   540     this.widgets.clear();
   541   }
   542 }; // Messages.BaseMessage.prototype
   545 /**
   546  * The NavigationMarker is used to show a page load event.
   547  *
   548  * @constructor
   549  * @extends Messages.BaseMessage
   550  * @param string url
   551  *        The URL to display.
   552  * @param number timestamp
   553  *        The message date and time, milliseconds elapsed since 1 January 1970
   554  *        00:00:00 UTC.
   555  */
   556 Messages.NavigationMarker = function(url, timestamp)
   557 {
   558   Messages.BaseMessage.call(this);
   559   this._url = url;
   560   this.textContent = "------ " + url;
   561   this.timestamp = timestamp;
   562 };
   564 Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype,
   565 {
   566   /**
   567    * The address of the loading page.
   568    * @private
   569    * @type string
   570    */
   571   _url: null,
   573   /**
   574    * Message timestamp.
   575    *
   576    * @type number
   577    *       Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
   578    */
   579   timestamp: 0,
   581   _categoryCompat: COMPAT.CATEGORIES.NETWORK,
   582   _severityCompat: COMPAT.SEVERITIES.LOG,
   583   _categoryNameCompat: "network",
   584   _severityNameCompat: "info",
   585   _filterKeyCompat: "networkinfo",
   587   /**
   588    * Prepare the DOM element for this message.
   589    * @return this
   590    */
   591   render: function()
   592   {
   593     if (this.element) {
   594       return this;
   595     }
   597     let url = this._url;
   598     let pos = url.indexOf("?");
   599     if (pos > -1) {
   600       url = url.substr(0, pos);
   601     }
   603     let doc = this.output.document;
   604     let urlnode = doc.createElementNS(XHTML_NS, "a");
   605     urlnode.className = "url";
   606     urlnode.textContent = url;
   607     urlnode.title = this._url;
   608     urlnode.href = this._url;
   609     urlnode.draggable = false;
   610     this._addLinkCallback(urlnode);
   612     let render = Messages.BaseMessage.prototype.render.bind(this);
   613     render().element.appendChild(urlnode);
   614     this.element.classList.add("navigation-marker");
   615     this.element.url = this._url;
   616     this.element.appendChild(doc.createTextNode("\n"));
   618     return this;
   619   },
   620 }); // Messages.NavigationMarker.prototype
   623 /**
   624  * The Simple message is used to show any basic message in the Web Console.
   625  *
   626  * @constructor
   627  * @extends Messages.BaseMessage
   628  * @param string|Node|function message
   629  *        The message to display.
   630  * @param object [options]
   631  *        Options for this message:
   632  *        - category: (string) category that this message belongs to. Defaults
   633  *        to no category.
   634  *        - severity: (string) severity of the message. Defaults to no severity.
   635  *        - timestamp: (number) date and time when the message was recorded.
   636  *        Defaults to |Date.now()|.
   637  *        - link: (string) if provided, the message will be wrapped in an anchor
   638  *        pointing to the given URL here.
   639  *        - linkCallback: (function) if provided, the message will be wrapped in
   640  *        an anchor. The |linkCallback| function will be added as click event
   641  *        handler.
   642  *        - location: object that tells the message source: url, line, column
   643  *        and lineText.
   644  *        - className: (string) additional element class names for styling
   645  *        purposes.
   646  *        - private: (boolean) mark this as a private message.
   647  *        - filterDuplicates: (boolean) true if you do want this message to be
   648  *        filtered as a potential duplicate message, false otherwise.
   649  */
   650 Messages.Simple = function(message, options = {})
   651 {
   652   Messages.BaseMessage.call(this);
   654   this.category = options.category;
   655   this.severity = options.severity;
   656   this.location = options.location;
   657   this.timestamp = options.timestamp || Date.now();
   658   this.private = !!options.private;
   660   this._message = message;
   661   this._className = options.className;
   662   this._link = options.link;
   663   this._linkCallback = options.linkCallback;
   664   this._filterDuplicates = options.filterDuplicates;
   665 };
   667 Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
   668 {
   669   /**
   670    * Message category.
   671    * @type string
   672    */
   673   category: null,
   675   /**
   676    * Message severity.
   677    * @type string
   678    */
   679   severity: null,
   681   /**
   682    * Message source location. Properties: url, line, column, lineText.
   683    * @type object
   684    */
   685   location: null,
   687   /**
   688    * Tells if this message comes from a private browsing context.
   689    * @type boolean
   690    */
   691   private: false,
   693   /**
   694    * Custom class name for the DOM element of the message.
   695    * @private
   696    * @type string
   697    */
   698   _className: null,
   700   /**
   701    * Message link - if this message is clicked then this URL opens in a new tab.
   702    * @private
   703    * @type string
   704    */
   705   _link: null,
   707   /**
   708    * Message click event handler.
   709    * @private
   710    * @type function
   711    */
   712   _linkCallback: null,
   714   /**
   715    * Tells if this message should be checked if it is a duplicate of another
   716    * message or not.
   717    */
   718   _filterDuplicates: false,
   720   /**
   721    * The raw message displayed by this Message object. This can be a function,
   722    * DOM node or a string.
   723    *
   724    * @private
   725    * @type mixed
   726    */
   727   _message: null,
   729   _afterMessage: null,
   730   _objectActors: null,
   731   _groupDepthCompat: 0,
   733   /**
   734    * Message timestamp.
   735    *
   736    * @type number
   737    *       Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
   738    */
   739   timestamp: 0,
   741   get _categoryCompat() {
   742     return this.category ?
   743            COMPAT.CATEGORIES[this.category.toUpperCase()] : null;
   744   },
   745   get _severityCompat() {
   746     return this.severity ?
   747            COMPAT.SEVERITIES[this.severity.toUpperCase()] : null;
   748   },
   749   get _categoryNameCompat() {
   750     return this.category ?
   751            COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null;
   752   },
   753   get _severityNameCompat() {
   754     return this.severity ?
   755            COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null;
   756   },
   758   get _filterKeyCompat() {
   759     return this._categoryCompat !== null && this._severityCompat !== null ?
   760            COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] :
   761            null;
   762   },
   764   init: function()
   765   {
   766     Messages.BaseMessage.prototype.init.apply(this, arguments);
   767     this._groupDepthCompat = this.output.owner.groupDepth;
   768     this._initRepeatID();
   769     return this;
   770   },
   772   _initRepeatID: function()
   773   {
   774     if (!this._filterDuplicates) {
   775       return;
   776     }
   778     // Add the properties we care about for identifying duplicate messages.
   779     let rid = this._repeatID;
   780     delete rid.uid;
   782     rid.category = this.category;
   783     rid.severity = this.severity;
   784     rid.private = this.private;
   785     rid.location = this.location;
   786     rid.link = this._link;
   787     rid.linkCallback = this._linkCallback + "";
   788     rid.className = this._className;
   789     rid.groupDepth = this._groupDepthCompat;
   790     rid.textContent = "";
   791   },
   793   getRepeatID: function()
   794   {
   795     // No point in returning a string that includes other properties when there
   796     // is a unique ID.
   797     if (this._repeatID.uid) {
   798       return JSON.stringify({ uid: this._repeatID.uid });
   799     }
   801     return JSON.stringify(this._repeatID);
   802   },
   804   render: function()
   805   {
   806     if (this.element) {
   807       return this;
   808     }
   810     let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render();
   812     let icon = this.document.createElementNS(XHTML_NS, "span");
   813     icon.className = "icon";
   815     // Apply the current group by indenting appropriately.
   816     // TODO: remove this once bug 778766 is fixed.
   817     let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT;
   818     let indentNode = this.document.createElementNS(XHTML_NS, "span");
   819     indentNode.className = "indent";
   820     indentNode.style.width = indent + "px";
   822     let body = this._renderBody();
   823     this._repeatID.textContent += "|" + body.textContent;
   825     let repeatNode = this._renderRepeatNode();
   826     let location = this._renderLocation();
   828     Messages.BaseMessage.prototype.render.call(this);
   829     if (this._className) {
   830       this.element.className += " " + this._className;
   831     }
   833     this.element.appendChild(timestamp.element);
   834     this.element.appendChild(indentNode);
   835     this.element.appendChild(icon);
   836     this.element.appendChild(body);
   837     if (repeatNode) {
   838       this.element.appendChild(repeatNode);
   839     }
   840     if (location) {
   841       this.element.appendChild(location);
   842     }
   843     this.element.appendChild(this.document.createTextNode("\n"));
   845     this.element.clipboardText = this.element.textContent;
   847     if (this.private) {
   848       this.element.setAttribute("private", true);
   849     }
   851     if (this._afterMessage) {
   852       this.element._outputAfterNode = this._afterMessage.element;
   853       this._afterMessage = null;
   854     }
   856     // TODO: handle object releasing in a more elegant way once all console
   857     // messages use the new API - bug 778766.
   858     this.element._objectActors = this._objectActors;
   859     this._objectActors = null;
   861     return this;
   862   },
   864   /**
   865    * Render the message body DOM element.
   866    * @private
   867    * @return Element
   868    */
   869   _renderBody: function()
   870   {
   871     let body = this.document.createElementNS(XHTML_NS, "span");
   872     body.className = "message-body-wrapper message-body devtools-monospace";
   874     let anchor, container = body;
   875     if (this._link || this._linkCallback) {
   876       container = anchor = this.document.createElementNS(XHTML_NS, "a");
   877       anchor.href = this._link || "#";
   878       anchor.draggable = false;
   879       this._addLinkCallback(anchor, this._linkCallback);
   880       body.appendChild(anchor);
   881     }
   883     if (typeof this._message == "function") {
   884       container.appendChild(this._message(this));
   885     } else if (this._message instanceof Ci.nsIDOMNode) {
   886       container.appendChild(this._message);
   887     } else {
   888       container.textContent = this._message;
   889     }
   891     return body;
   892   },
   894   /**
   895    * Render the repeat bubble DOM element part of the message.
   896    * @private
   897    * @return Element
   898    */
   899   _renderRepeatNode: function()
   900   {
   901     if (!this._filterDuplicates) {
   902       return null;
   903     }
   905     let repeatNode = this.document.createElementNS(XHTML_NS, "span");
   906     repeatNode.setAttribute("value", "1");
   907     repeatNode.className = "message-repeats";
   908     repeatNode.textContent = 1;
   909     repeatNode._uid = this.getRepeatID();
   910     return repeatNode;
   911   },
   913   /**
   914    * Render the message source location DOM element.
   915    * @private
   916    * @return Element
   917    */
   918   _renderLocation: function()
   919   {
   920     if (!this.location) {
   921       return null;
   922     }
   924     let {url, line} = this.location;
   925     if (IGNORED_SOURCE_URLS.indexOf(url) != -1) {
   926       return null;
   927     }
   929     // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js.
   930     // TODO: move createLocationNode() into this file when bug 778766 is fixed.
   931     return this.output.owner.createLocationNode(url, line);
   932   },
   933 }); // Messages.Simple.prototype
   936 /**
   937  * The Extended message.
   938  *
   939  * @constructor
   940  * @extends Messages.Simple
   941  * @param array messagePieces
   942  *        The message to display given as an array of elements. Each array
   943  *        element can be a DOM node, function, ObjectActor, LongString or
   944  *        a string.
   945  * @param object [options]
   946  *        Options for rendering this message:
   947  *        - quoteStrings: boolean that tells if you want strings to be wrapped
   948  *        in quotes or not.
   949  */
   950 Messages.Extended = function(messagePieces, options = {})
   951 {
   952   Messages.Simple.call(this, null, options);
   954   this._messagePieces = messagePieces;
   956   if ("quoteStrings" in options) {
   957     this._quoteStrings = options.quoteStrings;
   958   }
   960   this._repeatID.quoteStrings = this._quoteStrings;
   961   this._repeatID.messagePieces = messagePieces + "";
   962   this._repeatID.actors = new Set(); // using a set to avoid duplicates
   963 };
   965 Messages.Extended.prototype = Heritage.extend(Messages.Simple.prototype,
   966 {
   967   /**
   968    * The message pieces displayed by this message instance.
   969    * @private
   970    * @type array
   971    */
   972   _messagePieces: null,
   974   /**
   975    * Boolean that tells if the strings displayed in this message are wrapped.
   976    * @private
   977    * @type boolean
   978    */
   979   _quoteStrings: true,
   981   getRepeatID: function()
   982   {
   983     if (this._repeatID.uid) {
   984       return JSON.stringify({ uid: this._repeatID.uid });
   985     }
   987     // Sets are not stringified correctly. Temporarily switching to an array.
   988     let actors = this._repeatID.actors;
   989     this._repeatID.actors = [...actors];
   990     let result = JSON.stringify(this._repeatID);
   991     this._repeatID.actors = actors;
   992     return result;
   993   },
   995   render: function()
   996   {
   997     let result = this.document.createDocumentFragment();
   999     for (let i = 0; i < this._messagePieces.length; i++) {
  1000       let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
  1001       if (separator) {
  1002         result.appendChild(separator);
  1005       let piece = this._messagePieces[i];
  1006       result.appendChild(this._renderBodyPiece(piece));
  1009     this._message = result;
  1010     this._messagePieces = null;
  1011     return Messages.Simple.prototype.render.call(this);
  1012   },
  1014   /**
  1015    * Render the separator between the pieces of the message.
  1017    * @private
  1018    * @return Element
  1019    */
  1020   _renderBodyPieceSeparator: function() { return null; },
  1022   /**
  1023    * Render one piece/element of the message array.
  1025    * @private
  1026    * @param mixed piece
  1027    *        Message element to display - this can be a LongString, ObjectActor,
  1028    *        DOM node or a function to invoke.
  1029    * @return Element
  1030    */
  1031   _renderBodyPiece: function(piece)
  1033     if (piece instanceof Ci.nsIDOMNode) {
  1034       return piece;
  1036     if (typeof piece == "function") {
  1037       return piece(this);
  1040     return this._renderValueGrip(piece);
  1041   },
  1043   /**
  1044    * Render a grip that represents a value received from the server. This method
  1045    * picks the appropriate widget to render the value with.
  1047    * @private
  1048    * @param object grip
  1049    *        The value grip received from the server.
  1050    * @param object options
  1051    *        Options for displaying the value. Available options:
  1052    *        - noStringQuotes - boolean that tells the renderer to not use quotes
  1053    *        around strings.
  1054    *        - concise - boolean that tells the renderer to compactly display the
  1055    *        grip. This is typically set to true when the object needs to be
  1056    *        displayed in an array preview, or as a property value in object
  1057    *        previews, etc.
  1058    * @return DOMElement
  1059    *         The DOM element that displays the given grip.
  1060    */
  1061   _renderValueGrip: function(grip, options = {})
  1063     let isPrimitive = VariablesView.isPrimitive({ value: grip });
  1064     let isActorGrip = WebConsoleUtils.isActorGrip(grip);
  1065     let noStringQuotes = !this._quoteStrings;
  1066     if ("noStringQuotes" in options) {
  1067       noStringQuotes = options.noStringQuotes;
  1070     if (isActorGrip) {
  1071       this._repeatID.actors.add(grip.actor);
  1073       if (!isPrimitive) {
  1074         return this._renderObjectActor(grip, options);
  1076       if (grip.type == "longString") {
  1077         let widget = new Widgets.LongString(this, grip, options).render();
  1078         return widget.element;
  1082     let result = this.document.createElementNS(XHTML_NS, "span");
  1083     if (isPrimitive) {
  1084       let className = this.getClassNameForValueGrip(grip);
  1085       if (className) {
  1086         result.className = className;
  1089       result.textContent = VariablesView.getString(grip, {
  1090         noStringQuotes: noStringQuotes,
  1091         concise: options.concise,
  1092       });
  1093     } else {
  1094       result.textContent = grip;
  1097     return result;
  1098   },
  1100   /**
  1101    * Get a CodeMirror-compatible class name for a given value grip.
  1103    * @param object grip
  1104    *        Value grip from the server.
  1105    * @return string
  1106    *         The class name for the grip.
  1107    */
  1108   getClassNameForValueGrip: function(grip)
  1110     let map = {
  1111       "number": "cm-number",
  1112       "longstring": "console-string",
  1113       "string": "console-string",
  1114       "regexp": "cm-string-2",
  1115       "boolean": "cm-atom",
  1116       "-infinity": "cm-atom",
  1117       "infinity": "cm-atom",
  1118       "null": "cm-atom",
  1119       "undefined": "cm-comment",
  1120     };
  1122     let className = map[typeof grip];
  1123     if (!className && grip && grip.type) {
  1124       className = map[grip.type.toLowerCase()];
  1126     if (!className && grip && grip.class) {
  1127       className = map[grip.class.toLowerCase()];
  1130     return className;
  1131   },
  1133   /**
  1134    * Display an object actor with the appropriate renderer.
  1136    * @private
  1137    * @param object objectActor
  1138    *        The ObjectActor to display.
  1139    * @param object options
  1140    *        Options to use for displaying the ObjectActor.
  1141    * @see this._renderValueGrip for the available options.
  1142    * @return DOMElement
  1143    *         The DOM element that displays the object actor.
  1144    */
  1145   _renderObjectActor: function(objectActor, options = {})
  1147     let widget = null;
  1148     let {preview} = objectActor;
  1150     if (preview && preview.kind) {
  1151       widget = Widgets.ObjectRenderers.byKind[preview.kind];
  1154     if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
  1155       widget = Widgets.ObjectRenderers.byClass[objectActor.class];
  1158     if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
  1159       widget = Widgets.JSObject;
  1162     let instance = new widget(this, objectActor, options).render();
  1163     return instance.element;
  1164   },
  1165 }); // Messages.Extended.prototype
  1169 /**
  1170  * The JavaScriptEvalOutput message.
  1172  * @constructor
  1173  * @extends Messages.Extended
  1174  * @param object evalResponse
  1175  *        The evaluation response packet received from the server.
  1176  * @param string [errorMessage]
  1177  *        Optional error message to display.
  1178  */
  1179 Messages.JavaScriptEvalOutput = function(evalResponse, errorMessage)
  1181   let severity = "log", msg, quoteStrings = true;
  1183   if (errorMessage) {
  1184     severity = "error";
  1185     msg = errorMessage;
  1186     quoteStrings = false;
  1187   } else {
  1188     msg = evalResponse.result;
  1191   let options = {
  1192     className: "cm-s-mozilla",
  1193     timestamp: evalResponse.timestamp,
  1194     category: "output",
  1195     severity: severity,
  1196     quoteStrings: quoteStrings,
  1197   };
  1198   Messages.Extended.call(this, [msg], options);
  1199 };
  1201 Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype;
  1203 /**
  1204  * The ConsoleGeneric message is used for console API calls.
  1206  * @constructor
  1207  * @extends Messages.Extended
  1208  * @param object packet
  1209  *        The Console API call packet received from the server.
  1210  */
  1211 Messages.ConsoleGeneric = function(packet)
  1213   let options = {
  1214     className: "cm-s-mozilla",
  1215     timestamp: packet.timeStamp,
  1216     category: "webdev",
  1217     severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
  1218     private: packet.private,
  1219     filterDuplicates: true,
  1220     location: {
  1221       url: packet.filename,
  1222       line: packet.lineNumber,
  1223     },
  1224   };
  1226   switch (packet.level) {
  1227     case "count": {
  1228       let counter = packet.counter, label = counter.label;
  1229       if (!label) {
  1230         label = l10n.getStr("noCounterLabel");
  1232       Messages.Extended.call(this, [label+ ": " + counter.count], options);
  1233       break;
  1235     default:
  1236       Messages.Extended.call(this, packet.arguments, options);
  1237       break;
  1240   this._repeatID.consoleApiLevel = packet.level;
  1241   this._repeatID.styles = packet.styles;
  1242   this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
  1243   this._styles = packet.styles || [];
  1245   this._onClickCollapsible = this._onClickCollapsible.bind(this);
  1246 };
  1248 Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
  1250   _styles: null,
  1251   _stacktrace: null,
  1253   /**
  1254    * Tells if the message can be expanded/collapsed.
  1255    * @type boolean
  1256    */
  1257   collapsible: false,
  1259   /**
  1260    * Getter that tells if this message is collapsed - no details are shown.
  1261    * @type boolean
  1262    */
  1263   get collapsed() {
  1264     return this.collapsible && this.element && !this.element.hasAttribute("open");
  1265   },
  1267   _renderBodyPieceSeparator: function()
  1269     return this.document.createTextNode(" ");
  1270   },
  1272   render: function()
  1274     let msg = this.document.createElementNS(XHTML_NS, "span");
  1275     msg.className = "message-body devtools-monospace";
  1277     this._renderBodyPieces(msg);
  1279     let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
  1280     let location = Messages.Simple.prototype._renderLocation.call(this);
  1281     if (location) {
  1282       location.target = "jsdebugger";
  1285     let stack = null;
  1286     let twisty = null;
  1287     if (this._stacktrace && this._stacktrace.length > 0) {
  1288       stack = new Widgets.Stacktrace(this, this._stacktrace).render().element;
  1290       twisty = this.document.createElementNS(XHTML_NS, "a");
  1291       twisty.className = "theme-twisty";
  1292       twisty.href = "#";
  1293       twisty.title = l10n.getStr("messageToggleDetails");
  1294       twisty.addEventListener("click", this._onClickCollapsible);
  1297     let flex = this.document.createElementNS(XHTML_NS, "span");
  1298     flex.className = "message-flex-body";
  1300     if (twisty) {
  1301       flex.appendChild(twisty);
  1304     flex.appendChild(msg);
  1306     if (repeatNode) {
  1307       flex.appendChild(repeatNode);
  1309     if (location) {
  1310       flex.appendChild(location);
  1313     let result = this.document.createDocumentFragment();
  1314     result.appendChild(flex);
  1316     if (stack) {
  1317       result.appendChild(this.document.createTextNode("\n"));
  1318       result.appendChild(stack);
  1321     this._message = result;
  1322     this._stacktrace = null;
  1324     Messages.Simple.prototype.render.call(this);
  1326     if (stack) {
  1327       this.collapsible = true;
  1328       this.element.setAttribute("collapsible", true);
  1330       let icon = this.element.querySelector(".icon");
  1331       icon.addEventListener("click", this._onClickCollapsible);
  1334     return this;
  1335   },
  1337   _renderBody: function()
  1339     let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
  1340     body.classList.remove("devtools-monospace", "message-body");
  1341     return body;
  1342   },
  1344   _renderBodyPieces: function(container)
  1346     let lastStyle = null;
  1348     for (let i = 0; i < this._messagePieces.length; i++) {
  1349       let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
  1350       if (separator) {
  1351         container.appendChild(separator);
  1354       let piece = this._messagePieces[i];
  1355       let style = this._styles[i];
  1357       // No long string support.
  1358       if (style && typeof style == "string" ) {
  1359         lastStyle = this.cleanupStyle(style);
  1362       container.appendChild(this._renderBodyPiece(piece, lastStyle));
  1365     this._messagePieces = null;
  1366     this._styles = null;
  1367   },
  1369   _renderBodyPiece: function(piece, style)
  1371     let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece);
  1372     let result = elem;
  1374     if (style) {
  1375       if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
  1376         elem.style = style;
  1377       } else {
  1378         let span = this.document.createElementNS(XHTML_NS, "span");
  1379         span.style = style;
  1380         span.appendChild(elem);
  1381         result = span;
  1385     return result;
  1386   },
  1388   // no-op for the message location and .repeats elements.
  1389   // |this.render()| handles customized message output.
  1390   _renderLocation: function() { },
  1391   _renderRepeatNode: function() { },
  1393   /**
  1394    * Expand/collapse message details.
  1395    */
  1396   toggleDetails: function()
  1398     let twisty = this.element.querySelector(".theme-twisty");
  1399     if (this.element.hasAttribute("open")) {
  1400       this.element.removeAttribute("open");
  1401       twisty.removeAttribute("open");
  1402     } else {
  1403       this.element.setAttribute("open", true);
  1404       twisty.setAttribute("open", true);
  1406   },
  1408   /**
  1409    * The click event handler for the message expander arrow element. This method
  1410    * toggles the display of message details.
  1412    * @private
  1413    * @param nsIDOMEvent ev
  1414    *        The DOM event object.
  1415    * @see this.toggleDetails()
  1416    */
  1417   _onClickCollapsible: function(ev)
  1419     ev.preventDefault();
  1420     this.toggleDetails();
  1421   },
  1423   /**
  1424    * Given a style attribute value, return a cleaned up version of the string
  1425    * such that:
  1427    * - no external URL is allowed to load. See RE_CLEANUP_STYLES.
  1428    * - only some of the properties are allowed, based on a whitelist. See
  1429    *   RE_ALLOWED_STYLES.
  1431    * @param string style
  1432    *        The style string to cleanup.
  1433    * @return string
  1434    *         The style value after cleanup.
  1435    */
  1436   cleanupStyle: function(style)
  1438     for (let r of RE_CLEANUP_STYLES) {
  1439       style = style.replace(r, "notallowed");
  1442     let dummy = this.output._dummyElement;
  1443     if (!dummy) {
  1444       dummy = this.output._dummyElement =
  1445         this.document.createElementNS(XHTML_NS, "div");
  1447     dummy.style = style;
  1449     let toRemove = [];
  1450     for (let i = 0; i < dummy.style.length; i++) {
  1451       let prop = dummy.style[i];
  1452       if (!RE_ALLOWED_STYLES.test(prop)) {
  1453         toRemove.push(prop);
  1457     for (let prop of toRemove) {
  1458       dummy.style.removeProperty(prop);
  1461     style = dummy.style.cssText;
  1463     dummy.style = "";
  1465     return style;
  1466   },
  1467 }); // Messages.ConsoleGeneric.prototype
  1469 /**
  1470  * The ConsoleTrace message is used for console.trace() calls.
  1472  * @constructor
  1473  * @extends Messages.Simple
  1474  * @param object packet
  1475  *        The Console API call packet received from the server.
  1476  */
  1477 Messages.ConsoleTrace = function(packet)
  1479   let options = {
  1480     className: "cm-s-mozilla",
  1481     timestamp: packet.timeStamp,
  1482     category: "webdev",
  1483     severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
  1484     private: packet.private,
  1485     filterDuplicates: true,
  1486     location: {
  1487       url: packet.filename,
  1488       line: packet.lineNumber,
  1489     },
  1490   };
  1492   this._renderStack = this._renderStack.bind(this);
  1493   Messages.Simple.call(this, this._renderStack, options);
  1495   this._repeatID.consoleApiLevel = packet.level;
  1496   this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
  1497   this._arguments = packet.arguments;
  1498 };
  1500 Messages.ConsoleTrace.prototype = Heritage.extend(Messages.Simple.prototype,
  1502   /**
  1503    * Holds the stackframes received from the server.
  1505    * @private
  1506    * @type array
  1507    */
  1508   _stacktrace: null,
  1510   /**
  1511    * Holds the arguments the content script passed to the console.trace()
  1512    * method. This array is cleared when the message is initialized, and
  1513    * associated actors are released.
  1515    * @private
  1516    * @type array
  1517    */
  1518   _arguments: null,
  1520   init: function()
  1522     let result = Messages.Simple.prototype.init.apply(this, arguments);
  1524     // We ignore console.trace() arguments. Release object actors.
  1525     if (Array.isArray(this._arguments)) {
  1526       for (let arg of this._arguments) {
  1527         if (WebConsoleUtils.isActorGrip(arg)) {
  1528           this.output._releaseObject(arg.actor);
  1532     this._arguments = null;
  1534     return result;
  1535   },
  1537   render: function()
  1539     Messages.Simple.prototype.render.apply(this, arguments);
  1540     this.element.setAttribute("open", true);
  1541     return this;
  1542   },
  1544   /**
  1545    * Render the stack frames.
  1547    * @private
  1548    * @return DOMElement
  1549    */
  1550   _renderStack: function()
  1552     let cmvar = this.document.createElementNS(XHTML_NS, "span");
  1553     cmvar.className = "cm-variable";
  1554     cmvar.textContent = "console";
  1556     let cmprop = this.document.createElementNS(XHTML_NS, "span");
  1557     cmprop.className = "cm-property";
  1558     cmprop.textContent = "trace";
  1560     let title = this.document.createElementNS(XHTML_NS, "span");
  1561     title.className = "message-body devtools-monospace";
  1562     title.appendChild(cmvar);
  1563     title.appendChild(this.document.createTextNode("."));
  1564     title.appendChild(cmprop);
  1565     title.appendChild(this.document.createTextNode("():"));
  1567     let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
  1568     let location = Messages.Simple.prototype._renderLocation.call(this);
  1569     if (location) {
  1570       location.target = "jsdebugger";
  1573     let widget = new Widgets.Stacktrace(this, this._stacktrace).render();
  1575     let body = this.document.createElementNS(XHTML_NS, "span");
  1576     body.className = "message-flex-body";
  1577     body.appendChild(title);
  1578     if (repeatNode) {
  1579       body.appendChild(repeatNode);
  1581     if (location) {
  1582       body.appendChild(location);
  1584     body.appendChild(this.document.createTextNode("\n"));
  1586     let frag = this.document.createDocumentFragment();
  1587     frag.appendChild(body);
  1588     frag.appendChild(widget.element);
  1590     return frag;
  1591   },
  1593   _renderBody: function()
  1595     let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
  1596     body.classList.remove("devtools-monospace", "message-body");
  1597     return body;
  1598   },
  1600   // no-op for the message location and .repeats elements.
  1601   // |this._renderStack| handles customized message output.
  1602   _renderLocation: function() { },
  1603   _renderRepeatNode: function() { },
  1604 }); // Messages.ConsoleTrace.prototype
  1606 let Widgets = {};
  1608 /**
  1609  * The base widget class.
  1611  * @constructor
  1612  * @param object message
  1613  *        The owning message.
  1614  */
  1615 Widgets.BaseWidget = function(message)
  1617   this.message = message;
  1618 };
  1620 Widgets.BaseWidget.prototype = {
  1621   /**
  1622    * The owning message object.
  1623    * @type object
  1624    */
  1625   message: null,
  1627   /**
  1628    * The DOM element of the rendered widget.
  1629    * @type Element
  1630    */
  1631   element: null,
  1633   /**
  1634    * Getter for the DOM document that holds the output.
  1635    * @type Document
  1636    */
  1637   get document() {
  1638     return this.message.document;
  1639   },
  1641   /**
  1642    * The ConsoleOutput instance that owns this widget instance.
  1643    */
  1644   get output() {
  1645     return this.message.output;
  1646   },
  1648   /**
  1649    * Render the widget DOM element.
  1650    * @return this
  1651    */
  1652   render: function() { },
  1654   /**
  1655    * Destroy this widget instance.
  1656    */
  1657   destroy: function() { },
  1659   /**
  1660    * Helper for creating DOM elements for widgets.
  1662    * Usage:
  1663    *   this.el("tag#id.class.names"); // create element "tag" with ID "id" and
  1664    *   two class names, .class and .names.
  1666    *   this.el("span", { attr1: "value1", ... }) // second argument can be an
  1667    *   object that holds element attributes and values for the new DOM element.
  1669    *   this.el("p", { attr1: "value1", ... }, "text content"); // the third
  1670    *   argument can include the default .textContent of the new DOM element.
  1672    *   this.el("p", "text content"); // if the second argument is not an object,
  1673    *   it will be used as .textContent for the new DOM element.
  1675    * @param string tagNameIdAndClasses
  1676    *        Tag name for the new element, optionally followed by an ID and/or
  1677    *        class names. Examples: "span", "div#fooId", "div.class.names",
  1678    *        "p#id.class".
  1679    * @param string|object [attributesOrTextContent]
  1680    *        If this argument is an object it will be used to set the attributes
  1681    *        of the new DOM element. Otherwise, the value becomes the
  1682    *        .textContent of the new DOM element.
  1683    * @param string [textContent]
  1684    *        If this argument is provided the value is used as the textContent of
  1685    *        the new DOM element.
  1686    * @return DOMElement
  1687    *         The new DOM element.
  1688    */
  1689   el: function(tagNameIdAndClasses)
  1691     let attrs, text;
  1692     if (typeof arguments[1] == "object") {
  1693       attrs = arguments[1];
  1694       text = arguments[2];
  1695     } else {
  1696       text = arguments[1];
  1699     let tagName = tagNameIdAndClasses.split(/#|\./)[0];
  1701     let elem = this.document.createElementNS(XHTML_NS, tagName);
  1702     for (let name of Object.keys(attrs || {})) {
  1703       elem.setAttribute(name, attrs[name]);
  1705     if (text !== undefined && text !== null) {
  1706       elem.textContent = text;
  1709     let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g);
  1710     for (let idOrClass of (idAndClasses || [])) {
  1711       if (idOrClass.charAt(0) == "#") {
  1712         elem.id = idOrClass.substr(1);
  1713       } else {
  1714         elem.classList.add(idOrClass.substr(1));
  1718     return elem;
  1719   },
  1720 };
  1722 /**
  1723  * The timestamp widget.
  1725  * @constructor
  1726  * @param object message
  1727  *        The owning message.
  1728  * @param number timestamp
  1729  *        The UNIX timestamp to display.
  1730  */
  1731 Widgets.MessageTimestamp = function(message, timestamp)
  1733   Widgets.BaseWidget.call(this, message);
  1734   this.timestamp = timestamp;
  1735 };
  1737 Widgets.MessageTimestamp.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
  1739   /**
  1740    * The UNIX timestamp.
  1741    * @type number
  1742    */
  1743   timestamp: 0,
  1745   render: function()
  1747     if (this.element) {
  1748       return this;
  1751     this.element = this.document.createElementNS(XHTML_NS, "span");
  1752     this.element.className = "timestamp devtools-monospace";
  1753     this.element.textContent = l10n.timestampString(this.timestamp) + " ";
  1755     return this;
  1756   },
  1757 }); // Widgets.MessageTimestamp.prototype
  1760 /**
  1761  * Widget used for displaying ObjectActors that have no specialised renderers.
  1763  * @constructor
  1764  * @param object message
  1765  *        The owning message.
  1766  * @param object objectActor
  1767  *        The ObjectActor to display.
  1768  * @param object [options]
  1769  *        Options for displaying the given ObjectActor. See
  1770  *        Messages.Extended.prototype._renderValueGrip for the available
  1771  *        options.
  1772  */
  1773 Widgets.JSObject = function(message, objectActor, options = {})
  1775   Widgets.BaseWidget.call(this, message);
  1776   this.objectActor = objectActor;
  1777   this.options = options;
  1778   this._onClick = this._onClick.bind(this);
  1779 };
  1781 Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
  1783   /**
  1784    * The ObjectActor displayed by the widget.
  1785    * @type object
  1786    */
  1787   objectActor: null,
  1789   render: function()
  1791     if (!this.element) {
  1792       this._render();
  1795     return this;
  1796   },
  1798   _render: function()
  1800     let str = VariablesView.getString(this.objectActor, this.options);
  1801     let className = this.message.getClassNameForValueGrip(this.objectActor);
  1802     if (!className && this.objectActor.class == "Object") {
  1803       className = "cm-variable";
  1806     this.element = this._anchor(str, { className: className });
  1807   },
  1809   /**
  1810    * Render an anchor with a given text content and link.
  1812    * @private
  1813    * @param string text
  1814    *        Text to show in the anchor.
  1815    * @param object [options]
  1816    *        Available options:
  1817    *        - onClick (function): "click" event handler.By default a click on
  1818    *        the anchor opens the variables view for the current object actor
  1819    *        (this.objectActor).
  1820    *        - href (string): if given the string is used as a link, and clicks
  1821    *        on the anchor open the link in a new tab.
  1822    *        - appendTo (DOMElement): append the element to the given DOM
  1823    *        element. If not provided, the anchor is appended to |this.element|
  1824    *        if it is available. If |appendTo| is provided and if it is a falsy
  1825    *        value, the anchor is not appended to any element.
  1826    * @return DOMElement
  1827    *         The DOM element of the new anchor.
  1828    */
  1829   _anchor: function(text, options = {})
  1831     if (!options.onClick && !options.href) {
  1832       options.onClick = this._onClick;
  1835     let anchor = this.el("a", {
  1836       class: options.className,
  1837       draggable: false,
  1838       href: options.href || "#",
  1839     }, text);
  1841     this.message._addLinkCallback(anchor, !options.href ? options.onClick : null);
  1843     if (options.appendTo) {
  1844       options.appendTo.appendChild(anchor);
  1845     } else if (!("appendTo" in options) && this.element) {
  1846       this.element.appendChild(anchor);
  1849     return anchor;
  1850   },
  1852   /**
  1853    * The click event handler for objects shown inline.
  1854    * @private
  1855    */
  1856   _onClick: function()
  1858     this.output.openVariablesView({
  1859       label: VariablesView.getString(this.objectActor, { concise: true }),
  1860       objectActor: this.objectActor,
  1861       autofocus: true,
  1862     });
  1863   },
  1865   /**
  1866    * Add a string to the message.
  1868    * @private
  1869    * @param string str
  1870    *        String to add.
  1871    * @param DOMElement [target = this.element]
  1872    *        Optional DOM element to append the string to. The default is
  1873    *        this.element.
  1874    */
  1875   _text: function(str, target = this.element)
  1877     target.appendChild(this.document.createTextNode(str));
  1878   },
  1879 }); // Widgets.JSObject.prototype
  1881 Widgets.ObjectRenderers = {};
  1882 Widgets.ObjectRenderers.byKind = {};
  1883 Widgets.ObjectRenderers.byClass = {};
  1885 /**
  1886  * Add an object renderer.
  1888  * @param object obj
  1889  *        An object that represents the renderer. Properties:
  1890  *        - byClass (string, optional): this renderer will be used for the given
  1891  *        object class.
  1892  *        - byKind (string, optional): this renderer will be used for the given
  1893  *        object kind.
  1894  *        One of byClass or byKind must be provided.
  1895  *        - extends (object, optional): the renderer object extends the given
  1896  *        object. Default: Widgets.JSObject.
  1897  *        - canRender (function, optional): this method is invoked when
  1898  *        a candidate object needs to be displayed. The method is invoked as
  1899  *        a static method, as such, none of the properties of the renderer
  1900  *        object will be available. You get one argument: the object actor grip
  1901  *        received from the server. If the method returns true, then this
  1902  *        renderer is used for displaying the object, otherwise not.
  1903  *        - initialize (function, optional): the constructor of the renderer
  1904  *        widget. This function is invoked with the following arguments: the
  1905  *        owner message object instance, the object actor grip to display, and
  1906  *        an options object. See Messages.Extended.prototype._renderValueGrip()
  1907  *        for details about the options object.
  1908  *        - render (function, required): the method that displays the given
  1909  *        object actor.
  1910  */
  1911 Widgets.ObjectRenderers.add = function(obj)
  1913   let extendObj = obj.extends || Widgets.JSObject;
  1915   let constructor = function() {
  1916     if (obj.initialize) {
  1917       obj.initialize.apply(this, arguments);
  1918     } else {
  1919       extendObj.apply(this, arguments);
  1921   };
  1923   let proto = WebConsoleUtils.cloneObject(obj, false, function(key) {
  1924     if (key == "initialize" || key == "canRender" ||
  1925         (key == "render" && extendObj === Widgets.JSObject)) {
  1926       return false;
  1928     return true;
  1929   });
  1931   if (extendObj === Widgets.JSObject) {
  1932     proto._render = obj.render;
  1935   constructor.canRender = obj.canRender;
  1936   constructor.prototype = Heritage.extend(extendObj.prototype, proto);
  1938   if (obj.byClass) {
  1939     Widgets.ObjectRenderers.byClass[obj.byClass] = constructor;
  1940   } else if (obj.byKind) {
  1941     Widgets.ObjectRenderers.byKind[obj.byKind] = constructor;
  1942   } else {
  1943     throw new Error("You are adding an object renderer without any byClass or " +
  1944                     "byKind property.");
  1946 };
  1949 /**
  1950  * The widget used for displaying Date objects.
  1951  */
  1952 Widgets.ObjectRenderers.add({
  1953   byClass: "Date",
  1955   render: function()
  1957     let {preview} = this.objectActor;
  1958     this.element = this.el("span.class-" + this.objectActor.class);
  1960     let anchorText = this.objectActor.class;
  1961     let anchorClass = "cm-variable";
  1962     if ("timestamp" in preview && typeof preview.timestamp != "number") {
  1963       anchorText = new Date(preview.timestamp).toString(); // invalid date
  1964       anchorClass = "";
  1967     this._anchor(anchorText, { className: anchorClass });
  1969     if (!("timestamp" in preview) || typeof preview.timestamp != "number") {
  1970       return;
  1973     this._text(" ");
  1975     let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString());
  1976     this.element.appendChild(elem);
  1977   },
  1978 });
  1980 /**
  1981  * The widget used for displaying Function objects.
  1982  */
  1983 Widgets.ObjectRenderers.add({
  1984   byClass: "Function",
  1986   render: function()
  1988     let grip = this.objectActor;
  1989     this.element = this.el("span.class-" + this.objectActor.class);
  1991     // TODO: Bug 948484 - support arrow functions and ES6 generators
  1992     let name = grip.userDisplayName || grip.displayName || grip.name || "";
  1993     name = VariablesView.getString(name, { noStringQuotes: true });
  1995     let str = this.options.concise ? name || "function " : "function " + name;
  1997     if (this.options.concise) {
  1998       this._anchor(name || "function", {
  1999         className: name ? "cm-variable" : "cm-keyword",
  2000       });
  2001       if (!name) {
  2002         this._text(" ");
  2004     } else if (name) {
  2005       this.element.appendChild(this.el("span.cm-keyword", "function"));
  2006       this._text(" ");
  2007       this._anchor(name, { className: "cm-variable" });
  2008     } else {
  2009       this._anchor("function", { className: "cm-keyword" });
  2010       this._text(" ");
  2013     this._text("(");
  2015     // TODO: Bug 948489 - Support functions with destructured parameters and
  2016     // rest parameters
  2017     let params = grip.parameterNames || [];
  2018     let shown = 0;
  2019     for (let param of params) {
  2020       if (shown > 0) {
  2021         this._text(", ");
  2023       this.element.appendChild(this.el("span.cm-def", param));
  2024       shown++;
  2027     this._text(")");
  2028   },
  2029 }); // Widgets.ObjectRenderers.byClass.Function
  2031 /**
  2032  * The widget used for displaying ArrayLike objects.
  2033  */
  2034 Widgets.ObjectRenderers.add({
  2035   byKind: "ArrayLike",
  2037   render: function()
  2039     let {preview} = this.objectActor;
  2040     let {items} = preview;
  2041     this.element = this.el("span.kind-" + preview.kind);
  2043     this._anchor(this.objectActor.class, { className: "cm-variable" });
  2045     if (!items || this.options.concise) {
  2046       this._text("[");
  2047       this.element.appendChild(this.el("span.cm-number", preview.length));
  2048       this._text("]");
  2049       return this;
  2052     this._text(" [ ");
  2054     let shown = 0;
  2055     for (let item of items) {
  2056       if (shown > 0) {
  2057         this._text(", ");
  2060       if (item !== null) {
  2061         let elem = this.message._renderValueGrip(item, { concise: true });
  2062         this.element.appendChild(elem);
  2063       } else if (shown == (items.length - 1)) {
  2064         this._text(", ");
  2067       shown++;
  2070     if (shown < preview.length) {
  2071       this._text(", ");
  2073       let n = preview.length - shown;
  2074       let str = VariablesView.stringifiers._getNMoreString(n);
  2075       this._anchor(str);
  2078     this._text(" ]");
  2079   },
  2080 }); // Widgets.ObjectRenderers.byKind.ArrayLike
  2082 /**
  2083  * The widget used for displaying MapLike objects.
  2084  */
  2085 Widgets.ObjectRenderers.add({
  2086   byKind: "MapLike",
  2088   render: function()
  2090     let {preview} = this.objectActor;
  2091     let {entries} = preview;
  2093     let container = this.element = this.el("span.kind-" + preview.kind);
  2094     this._anchor(this.objectActor.class, { className: "cm-variable" });
  2096     if (!entries || this.options.concise) {
  2097       if (typeof preview.size == "number") {
  2098         this._text("[");
  2099         container.appendChild(this.el("span.cm-number", preview.size));
  2100         this._text("]");
  2102       return;
  2105     this._text(" { ");
  2107     let shown = 0;
  2108     for (let [key, value] of entries) {
  2109       if (shown > 0) {
  2110         this._text(", ");
  2113       let keyElem = this.message._renderValueGrip(key, {
  2114         concise: true,
  2115         noStringQuotes: true,
  2116       });
  2118       // Strings are property names.
  2119       if (keyElem.classList && keyElem.classList.contains("console-string")) {
  2120         keyElem.classList.remove("console-string");
  2121         keyElem.classList.add("cm-property");
  2124       container.appendChild(keyElem);
  2126       this._text(": ");
  2128       let valueElem = this.message._renderValueGrip(value, { concise: true });
  2129       container.appendChild(valueElem);
  2131       shown++;
  2134     if (typeof preview.size == "number" && shown < preview.size) {
  2135       this._text(", ");
  2137       let n = preview.size - shown;
  2138       let str = VariablesView.stringifiers._getNMoreString(n);
  2139       this._anchor(str);
  2142     this._text(" }");
  2143   },
  2144 }); // Widgets.ObjectRenderers.byKind.MapLike
  2146 /**
  2147  * The widget used for displaying objects with a URL.
  2148  */
  2149 Widgets.ObjectRenderers.add({
  2150   byKind: "ObjectWithURL",
  2152   render: function()
  2154     this.element = this._renderElement(this.objectActor,
  2155                                        this.objectActor.preview.url);
  2156   },
  2158   _renderElement: function(objectActor, url)
  2160     let container = this.el("span.kind-" + objectActor.preview.kind);
  2162     this._anchor(objectActor.class, {
  2163       className: "cm-variable",
  2164       appendTo: container,
  2165     });
  2167     if (!VariablesView.isFalsy({ value: url })) {
  2168       this._text(" \u2192 ", container);
  2169       let shortUrl = WebConsoleUtils.abbreviateSourceURL(url, {
  2170         onlyCropQuery: !this.options.concise
  2171       });
  2172       this._anchor(shortUrl, { href: url, appendTo: container });
  2175     return container;
  2176   },
  2177 }); // Widgets.ObjectRenderers.byKind.ObjectWithURL
  2179 /**
  2180  * The widget used for displaying objects with a string next to them.
  2181  */
  2182 Widgets.ObjectRenderers.add({
  2183   byKind: "ObjectWithText",
  2185   render: function()
  2187     let {preview} = this.objectActor;
  2188     this.element = this.el("span.kind-" + preview.kind);
  2190     this._anchor(this.objectActor.class, { className: "cm-variable" });
  2192     if (!this.options.concise) {
  2193       this._text(" ");
  2194       this.element.appendChild(this.el("span.console-string",
  2195                                        VariablesView.getString(preview.text)));
  2197   },
  2198 });
  2200 /**
  2201  * The widget used for displaying DOM event previews.
  2202  */
  2203 Widgets.ObjectRenderers.add({
  2204   byKind: "DOMEvent",
  2206   render: function()
  2208     let {preview} = this.objectActor;
  2210     let container = this.element = this.el("span.kind-" + preview.kind);
  2212     this._anchor(preview.type || this.objectActor.class,
  2213                  { className: "cm-variable" });
  2215     if (this.options.concise) {
  2216       return;
  2219     if (preview.eventKind == "key" && preview.modifiers &&
  2220         preview.modifiers.length) {
  2221       this._text(" ");
  2223       let mods = 0;
  2224       for (let mod of preview.modifiers) {
  2225         if (mods > 0) {
  2226           this._text("-");
  2228         container.appendChild(this.el("span.cm-keyword", mod));
  2229         mods++;
  2233     this._text(" { ");
  2235     let shown = 0;
  2236     if (preview.target) {
  2237       container.appendChild(this.el("span.cm-property", "target"));
  2238       this._text(": ");
  2239       let target = this.message._renderValueGrip(preview.target, { concise: true });
  2240       container.appendChild(target);
  2241       shown++;
  2244     for (let key of Object.keys(preview.properties || {})) {
  2245       if (shown > 0) {
  2246         this._text(", ");
  2249       container.appendChild(this.el("span.cm-property", key));
  2250       this._text(": ");
  2252       let value = preview.properties[key];
  2253       let valueElem = this.message._renderValueGrip(value, { concise: true });
  2254       container.appendChild(valueElem);
  2256       shown++;
  2259     this._text(" }");
  2260   },
  2261 }); // Widgets.ObjectRenderers.byKind.DOMEvent
  2263 /**
  2264  * The widget used for displaying DOM node previews.
  2265  */
  2266 Widgets.ObjectRenderers.add({
  2267   byKind: "DOMNode",
  2269   canRender: function(objectActor) {
  2270     let {preview} = objectActor;
  2271     if (!preview) {
  2272       return false;
  2275     switch (preview.nodeType) {
  2276       case Ci.nsIDOMNode.DOCUMENT_NODE:
  2277       case Ci.nsIDOMNode.ATTRIBUTE_NODE:
  2278       case Ci.nsIDOMNode.TEXT_NODE:
  2279       case Ci.nsIDOMNode.COMMENT_NODE:
  2280       case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
  2281       case Ci.nsIDOMNode.ELEMENT_NODE:
  2282         return true;
  2283       default:
  2284         return false;
  2286   },
  2288   render: function()
  2290     switch (this.objectActor.preview.nodeType) {
  2291       case Ci.nsIDOMNode.DOCUMENT_NODE:
  2292         this._renderDocumentNode();
  2293         break;
  2294       case Ci.nsIDOMNode.ATTRIBUTE_NODE: {
  2295         let {preview} = this.objectActor;
  2296         this.element = this.el("span.attributeNode.kind-" + preview.kind);
  2297         let attr = this._renderAttributeNode(preview.nodeName, preview.value, true);
  2298         this.element.appendChild(attr);
  2299         break;
  2301       case Ci.nsIDOMNode.TEXT_NODE:
  2302         this._renderTextNode();
  2303         break;
  2304       case Ci.nsIDOMNode.COMMENT_NODE:
  2305         this._renderCommentNode();
  2306         break;
  2307       case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
  2308         this._renderDocumentFragmentNode();
  2309         break;
  2310       case Ci.nsIDOMNode.ELEMENT_NODE:
  2311         this._renderElementNode();
  2312         break;
  2313       default:
  2314         throw new Error("Unsupported nodeType: " + preview.nodeType);
  2316   },
  2318   _renderDocumentNode: function()
  2320     let fn = Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement;
  2321     this.element = fn.call(this, this.objectActor,
  2322                            this.objectActor.preview.location);
  2323     this.element.classList.add("documentNode");
  2324   },
  2326   _renderAttributeNode: function(nodeName, nodeValue, addLink)
  2328     let value = VariablesView.getString(nodeValue, { noStringQuotes: true });
  2330     let fragment = this.document.createDocumentFragment();
  2331     if (addLink) {
  2332       this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment });
  2333     } else {
  2334       fragment.appendChild(this.el("span.cm-attribute", nodeName));
  2337     this._text("=", fragment);
  2338     fragment.appendChild(this.el("span.console-string",
  2339                                  '"' + escapeHTML(value) + '"'));
  2341     return fragment;
  2342   },
  2344   _renderTextNode: function()
  2346     let {preview} = this.objectActor;
  2347     this.element = this.el("span.textNode.kind-" + preview.kind);
  2349     this._anchor(preview.nodeName, { className: "cm-variable" });
  2350     this._text(" ");
  2352     let text = VariablesView.getString(preview.textContent);
  2353     this.element.appendChild(this.el("span.console-string", text));
  2354   },
  2356   _renderCommentNode: function()
  2358     let {preview} = this.objectActor;
  2359     let comment = "<!-- " + VariablesView.getString(preview.textContent, {
  2360       noStringQuotes: true,
  2361     }) + " -->";
  2363     this.element = this._anchor(comment, {
  2364       className: "kind-" + preview.kind + " commentNode cm-comment",
  2365     });
  2366   },
  2368   _renderDocumentFragmentNode: function()
  2370     let {preview} = this.objectActor;
  2371     let {childNodes} = preview;
  2372     let container = this.element = this.el("span.documentFragmentNode.kind-" +
  2373                                            preview.kind);
  2375     this._anchor(this.objectActor.class, { className: "cm-variable" });
  2377     if (!childNodes || this.options.concise) {
  2378       this._text("[");
  2379       container.appendChild(this.el("span.cm-number", preview.childNodesLength));
  2380       this._text("]");
  2381       return;
  2384     this._text(" [ ");
  2386     let shown = 0;
  2387     for (let item of childNodes) {
  2388       if (shown > 0) {
  2389         this._text(", ");
  2392       let elem = this.message._renderValueGrip(item, { concise: true });
  2393       container.appendChild(elem);
  2394       shown++;
  2397     if (shown < preview.childNodesLength) {
  2398       this._text(", ");
  2400       let n = preview.childNodesLength - shown;
  2401       let str = VariablesView.stringifiers._getNMoreString(n);
  2402       this._anchor(str);
  2405     this._text(" ]");
  2406   },
  2408   _renderElementNode: function()
  2410     let doc = this.document;
  2411     let {attributes, nodeName} = this.objectActor.preview;
  2413     this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode");
  2415     let openTag = this.el("span.cm-tag");
  2416     openTag.textContent = "<";
  2417     this.element.appendChild(openTag);
  2419     let tagName = this._anchor(nodeName, {
  2420       className: "cm-tag",
  2421       appendTo: openTag
  2422     });
  2424     if (this.options.concise) {
  2425       if (attributes.id) {
  2426         tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id));
  2428       if (attributes.class) {
  2429         tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join(".")));
  2431     } else {
  2432       for (let name of Object.keys(attributes)) {
  2433         let attr = this._renderAttributeNode(" " + name, attributes[name]);
  2434         this.element.appendChild(attr);
  2438     let closeTag = this.el("span.cm-tag");
  2439     closeTag.textContent = ">";
  2440     this.element.appendChild(closeTag);
  2442     // Register this widget in the owner message so that it gets destroyed when
  2443     // the message is destroyed.
  2444     this.message.widgets.add(this);
  2446     this.linkToInspector();
  2447   },
  2449   /**
  2450    * If the DOMNode being rendered can be highlit in the page, this function
  2451    * will attach mouseover/out event listeners to do so, and the inspector icon
  2452    * to open the node in the inspector.
  2453    * @return a promise (always the same) that resolves when the node has been
  2454    * linked to the inspector, or rejects if it wasn't (either if no toolbox
  2455    * could be found to access the inspector, or if the node isn't present in the
  2456    * inspector, i.e. if the node is in a DocumentFragment or not part of the
  2457    * tree, or not of type Ci.nsIDOMNode.ELEMENT_NODE).
  2458    */
  2459   linkToInspector: function()
  2461     if (this._linkedToInspector) {
  2462       return this._linkedToInspector;
  2465     this._linkedToInspector = Task.spawn(function*() {
  2466       // Checking the node type
  2467       if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
  2468         throw null;
  2471       // Checking the presence of a toolbox
  2472       let target = this.message.output.toolboxTarget;
  2473       this.toolbox = gDevTools.getToolbox(target);
  2474       if (!this.toolbox) {
  2475         throw null;
  2478       // Checking that the inspector supports the node
  2479       yield this.toolbox.initInspector();
  2480       this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor);
  2481       if (!this._nodeFront) {
  2482         throw null;
  2485       // At this stage, the message may have been cleared already
  2486       if (!this.document) {
  2487         throw null;
  2490       this.highlightDomNode = this.highlightDomNode.bind(this);
  2491       this.element.addEventListener("mouseover", this.highlightDomNode, false);
  2492       this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
  2493       this.element.addEventListener("mouseout", this.unhighlightDomNode, false);
  2495       this._openInspectorNode = this._anchor("", {
  2496         className: "open-inspector",
  2497         onClick: this.openNodeInInspector.bind(this)
  2498       });
  2499       this._openInspectorNode.title = l10n.getStr("openNodeInInspector");
  2500     }.bind(this));
  2502     return this._linkedToInspector;
  2503   },
  2505   /**
  2506    * Highlight the DOMNode corresponding to the ObjectActor in the page.
  2507    * @return a promise that resolves when the node has been highlighted, or
  2508    * rejects if the node cannot be highlighted (detached from the DOM)
  2509    */
  2510   highlightDomNode: function()
  2512     return Task.spawn(function*() {
  2513       yield this.linkToInspector();
  2514       let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
  2515       if (isAttached) {
  2516         yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
  2517       } else {
  2518         throw null;
  2520     }.bind(this));
  2521   },
  2523   /**
  2524    * Unhighlight a previously highlit node
  2525    * @see highlightDomNode
  2526    * @return a promise that resolves when the highlighter has been hidden
  2527    */
  2528   unhighlightDomNode: function()
  2530     return this.linkToInspector().then(() => {
  2531       return this.toolbox.highlighterUtils.unhighlight();
  2532     });
  2533   },
  2535   /**
  2536    * Open the DOMNode corresponding to the ObjectActor in the inspector panel
  2537    * @return a promise that resolves when the inspector has been switched to
  2538    * and the node has been selected, or rejects if the node cannot be selected
  2539    * (detached from the DOM). Note that in any case, the inspector panel will
  2540    * be switched to.
  2541    */
  2542   openNodeInInspector: function()
  2544     return Task.spawn(function*() {
  2545       yield this.linkToInspector();
  2546       yield this.toolbox.selectTool("inspector");
  2548       let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
  2549       if (isAttached) {
  2550         let onReady = this.toolbox.inspector.once("inspector-updated");
  2551         yield this.toolbox.selection.setNodeFront(this._nodeFront, "console");
  2552         yield onReady;
  2553       } else {
  2554         throw null;
  2556     }.bind(this));
  2557   },
  2559   destroy: function()
  2561     if (this.toolbox && this._nodeFront) {
  2562       this.element.removeEventListener("mouseover", this.highlightDomNode, false);
  2563       this.element.removeEventListener("mouseout", this.unhighlightDomNode, false);
  2564       this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true);
  2565       this.toolbox = null;
  2566       this._nodeFront = null;
  2568   },
  2569 }); // Widgets.ObjectRenderers.byKind.DOMNode
  2571 /**
  2572  * The widget used for displaying generic JS object previews.
  2573  */
  2574 Widgets.ObjectRenderers.add({
  2575   byKind: "Object",
  2577   render: function()
  2579     let {preview} = this.objectActor;
  2580     let {ownProperties, safeGetterValues} = preview;
  2582     if ((!ownProperties && !safeGetterValues) || this.options.concise) {
  2583       this.element = this._anchor(this.objectActor.class,
  2584                                   { className: "cm-variable" });
  2585       return;
  2588     let container = this.element = this.el("span.kind-" + preview.kind);
  2589     this._anchor(this.objectActor.class, { className: "cm-variable" });
  2590     this._text(" { ");
  2592     let addProperty = (str) => {
  2593       container.appendChild(this.el("span.cm-property", str));
  2594     };
  2596     let shown = 0;
  2597     for (let key of Object.keys(ownProperties || {})) {
  2598       if (shown > 0) {
  2599         this._text(", ");
  2602       let value = ownProperties[key];
  2604       addProperty(key);
  2605       this._text(": ");
  2607       if (value.get) {
  2608         addProperty("Getter");
  2609       } else if (value.set) {
  2610         addProperty("Setter");
  2611       } else {
  2612         let valueElem = this.message._renderValueGrip(value.value, { concise: true });
  2613         container.appendChild(valueElem);
  2616       shown++;
  2619     let ownPropertiesShown = shown;
  2621     for (let key of Object.keys(safeGetterValues || {})) {
  2622       if (shown > 0) {
  2623         this._text(", ");
  2626       addProperty(key);
  2627       this._text(": ");
  2629       let value = safeGetterValues[key].getterValue;
  2630       let valueElem = this.message._renderValueGrip(value, { concise: true });
  2631       container.appendChild(valueElem);
  2633       shown++;
  2636     if (typeof preview.ownPropertiesLength == "number" &&
  2637         ownPropertiesShown < preview.ownPropertiesLength) {
  2638       this._text(", ");
  2640       let n = preview.ownPropertiesLength - ownPropertiesShown;
  2641       let str = VariablesView.stringifiers._getNMoreString(n);
  2642       this._anchor(str);
  2645     this._text(" }");
  2646   },
  2647 }); // Widgets.ObjectRenderers.byKind.Object
  2649 /**
  2650  * The long string widget.
  2652  * @constructor
  2653  * @param object message
  2654  *        The owning message.
  2655  * @param object longStringActor
  2656  *        The LongStringActor to display.
  2657  */
  2658 Widgets.LongString = function(message, longStringActor)
  2660   Widgets.BaseWidget.call(this, message);
  2661   this.longStringActor = longStringActor;
  2662   this._onClick = this._onClick.bind(this);
  2663   this._onSubstring = this._onSubstring.bind(this);
  2664 };
  2666 Widgets.LongString.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
  2668   /**
  2669    * The LongStringActor displayed by the widget.
  2670    * @type object
  2671    */
  2672   longStringActor: null,
  2674   render: function()
  2676     if (this.element) {
  2677       return this;
  2680     let result = this.element = this.document.createElementNS(XHTML_NS, "span");
  2681     result.className = "longString console-string";
  2682     this._renderString(this.longStringActor.initial);
  2683     result.appendChild(this._renderEllipsis());
  2685     return this;
  2686   },
  2688   /**
  2689    * Render the long string in the widget element.
  2690    * @private
  2691    * @param string str
  2692    *        The string to display.
  2693    */
  2694   _renderString: function(str)
  2696     this.element.textContent = VariablesView.getString(str, {
  2697       noStringQuotes: !this.message._quoteStrings,
  2698       noEllipsis: true,
  2699     });
  2700   },
  2702   /**
  2703    * Render the anchor ellipsis that allows the user to expand the long string.
  2705    * @private
  2706    * @return Element
  2707    */
  2708   _renderEllipsis: function()
  2710     let ellipsis = this.document.createElementNS(XHTML_NS, "a");
  2711     ellipsis.className = "longStringEllipsis";
  2712     ellipsis.textContent = l10n.getStr("longStringEllipsis");
  2713     ellipsis.href = "#";
  2714     ellipsis.draggable = false;
  2715     this.message._addLinkCallback(ellipsis, this._onClick);
  2717     return ellipsis;
  2718   },
  2720   /**
  2721    * The click event handler for the ellipsis shown after the short string. This
  2722    * function expands the element to show the full string.
  2723    * @private
  2724    */
  2725   _onClick: function()
  2727     let longString = this.output.webConsoleClient.longString(this.longStringActor);
  2728     let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH);
  2730     longString.substring(longString.initial.length, toIndex, this._onSubstring);
  2731   },
  2733   /**
  2734    * The longString substring response callback.
  2736    * @private
  2737    * @param object response
  2738    *        Response packet.
  2739    */
  2740   _onSubstring: function(response)
  2742     if (response.error) {
  2743       Cu.reportError("LongString substring failure: " + response.error);
  2744       return;
  2747     this.element.lastChild.remove();
  2748     this.element.classList.remove("longString");
  2750     this._renderString(this.longStringActor.initial + response.substring);
  2752     this.output.owner.emit("messages-updated", new Set([this.message.element]));
  2754     let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH);
  2755     if (toIndex != this.longStringActor.length) {
  2756       this._logWarningAboutStringTooLong();
  2758   },
  2760   /**
  2761    * Inform user that the string he tries to view is too long.
  2762    * @private
  2763    */
  2764   _logWarningAboutStringTooLong: function()
  2766     let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), {
  2767       category: "output",
  2768       severity: "warning",
  2769     });
  2770     this.output.addMessage(msg);
  2771   },
  2772 }); // Widgets.LongString.prototype
  2775 /**
  2776  * The stacktrace widget.
  2778  * @constructor
  2779  * @extends Widgets.BaseWidget
  2780  * @param object message
  2781  *        The owning message.
  2782  * @param array stacktrace
  2783  *        The stacktrace to display, array of frames as supplied by the server,
  2784  *        over the remote protocol.
  2785  */
  2786 Widgets.Stacktrace = function(message, stacktrace)
  2788   Widgets.BaseWidget.call(this, message);
  2789   this.stacktrace = stacktrace;
  2790 };
  2792 Widgets.Stacktrace.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
  2794   /**
  2795    * The stackframes received from the server.
  2796    * @type array
  2797    */
  2798   stacktrace: null,
  2800   render: function()
  2802     if (this.element) {
  2803       return this;
  2806     let result = this.element = this.document.createElementNS(XHTML_NS, "ul");
  2807     result.className = "stacktrace devtools-monospace";
  2809     for (let frame of this.stacktrace) {
  2810       result.appendChild(this._renderFrame(frame));
  2813     return this;
  2814   },
  2816   /**
  2817    * Render a frame object received from the server.
  2819    * @param object frame
  2820    *        The stack frame to display. This object should have the following
  2821    *        properties: functionName, filename and lineNumber.
  2822    * @return DOMElement
  2823    *         The DOM element to display for the given frame.
  2824    */
  2825   _renderFrame: function(frame)
  2827     let fn = this.document.createElementNS(XHTML_NS, "span");
  2828     fn.className = "function";
  2829     if (frame.functionName) {
  2830       let span = this.document.createElementNS(XHTML_NS, "span");
  2831       span.className = "cm-variable";
  2832       span.textContent = frame.functionName;
  2833       fn.appendChild(span);
  2834       fn.appendChild(this.document.createTextNode("()"));
  2835     } else {
  2836       fn.classList.add("cm-comment");
  2837       fn.textContent = l10n.getStr("stacktrace.anonymousFunction");
  2840     let location = this.output.owner.createLocationNode(frame.filename,
  2841                                                         frame.lineNumber,
  2842                                                         "jsdebugger");
  2844     // .devtools-monospace sets font-size to 80%, however .body already has
  2845     // .devtools-monospace. If we keep it here, the location would be rendered
  2846     // smaller.
  2847     location.classList.remove("devtools-monospace");
  2849     let elem = this.document.createElementNS(XHTML_NS, "li");
  2850     elem.appendChild(fn);
  2851     elem.appendChild(location);
  2852     elem.appendChild(this.document.createTextNode("\n"));
  2854     return elem;
  2855   },
  2856 }); // Widgets.Stacktrace.prototype
  2859 function gSequenceId()
  2861   return gSequenceId.n++;
  2863 gSequenceId.n = 0;
  2865 exports.ConsoleOutput = ConsoleOutput;
  2866 exports.Messages = Messages;
  2867 exports.Widgets = Widgets;

mercurial