/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {Cc, Ci, Cu} = require("chrome");

loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
loader.lazyImporter(this, "escapeHTML", "resource:///modules/devtools/VariablesView.jsm");
loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm");
loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm");

loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "TableWidget", "devtools/shared/widgets/TableWidget", true);

const Heritage = require("sdk/core/heritage");
const URI = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";

const WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
const l10n = new WebConsoleUtils.l10n(STRINGS_URI);

const MAX_STRING_GRIP_LENGTH = 36;
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;

// Constants for compatibility with the Web Console output implementation before
// bug 778766.
// TODO: remove these once bug 778766 is fixed.
const COMPAT = {
  // The various categories of messages.
  CATEGORIES: {
    NETWORK: 0,
    CSS: 1,
    JS: 2,
    WEBDEV: 3,
    INPUT: 4,
    OUTPUT: 5,
    SECURITY: 6,
  },

  // The possible message severities.
  SEVERITIES: {
    ERROR: 0,
    WARNING: 1,
    INFO: 2,
    LOG: 3,
  },

  // The preference keys to use for each category/severity combination, indexed
  // first by category (rows) and then by severity (columns).
  //
  // Most of these rather idiosyncratic names are historical and predate the
  // division of message type into "category" and "severity".
  PREFERENCE_KEYS: [
    // Error        Warning       Info    Log
    [ "network",    "netwarn",    null,   "networkinfo", ],  // Network
    [ "csserror",   "cssparser",  null,   null,          ],  // CSS
    [ "exception",  "jswarn",     null,   "jslog",       ],  // JS
    [ "error",      "warn",       "info", "log",         ],  // Web Developer
    [ null,         null,         null,   null,          ],  // Input
    [ null,         null,         null,   null,          ],  // Output
    [ "secerror",   "secwarn",    null,   null,          ],  // Security
  ],

  // The fragment of a CSS class name that identifies each category.
  CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
                              "input", "output", "security" ],

  // The fragment of a CSS class name that identifies each severity.
  SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],

  // The indent of a console group in pixels.
  GROUP_INDENT: 12,
};

// A map from the console API call levels to the Web Console severities.
const CONSOLE_API_LEVELS_TO_SEVERITIES = {
  error: "error",
  exception: "error",
  assert: "error",
  warn: "warning",
  info: "info",
  log: "log",
  trace: "log",
  table: "log",
  debug: "log",
  dir: "log",
  group: "log",
  groupCollapsed: "log",
  groupEnd: "log",
  time: "log",
  timeEnd: "log",
  count: "log"
};

// Array of known message source URLs we need to hide from output.
const IGNORED_SOURCE_URLS = ["debugger eval code"];

// The maximum length of strings to be displayed by the Web Console.
const MAX_LONG_STRING_LENGTH = 200000;

// Regular expression that matches the allowed CSS property names when using
// the `window.console` API.
const RE_ALLOWED_STYLES = /^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|margin|padding|text|transition|outline|white-space|word|writing|(?:min-|max-)?width|(?:min-|max-)?height)/;

// Regular expressions to search and replace with 'notallowed' in the styles
// given to the `window.console` API methods.
const RE_CLEANUP_STYLES = [
  // url(), -moz-element()
  /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,

  // various URL protocols
  /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
];

// Maximum number of rows to display in console.table().
const TABLE_ROW_MAX_ITEMS = 1000;

// Maximum number of columns to display in console.table().
const TABLE_COLUMN_MAX_ITEMS = 10;

/**
 * The ConsoleOutput object is used to manage output of messages in the Web
 * Console.
 *
 * @constructor
 * @param object owner
 *        The console output owner. This usually the WebConsoleFrame instance.
 *        Any other object can be used, as long as it has the following
 *        properties and methods:
 *          - window
 *          - document
 *          - outputMessage(category, methodOrNode[, methodArguments])
 *            TODO: this is needed temporarily, until bug 778766 is fixed.
 */
function ConsoleOutput(owner)
{
  this.owner = owner;
  this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this);
}

ConsoleOutput.prototype = {
  _dummyElement: null,

  /**
   * The output container.
   * @type DOMElement
   */
  get element() {
    return this.owner.outputNode;
  },

  /**
   * The document that holds the output.
   * @type DOMDocument
   */
  get document() {
    return this.owner ? this.owner.document : null;
  },

  /**
   * The DOM window that holds the output.
   * @type Window
   */
  get window() {
    return this.owner.window;
  },

  /**
   * Getter for the debugger WebConsoleClient.
   * @type object
   */
  get webConsoleClient() {
    return this.owner.webConsoleClient;
  },

  /**
   * Getter for the current toolbox debuggee target.
   * @type Target
   */
  get toolboxTarget() {
    return this.owner.owner.target;
  },

  /**
   * Release an actor.
   *
   * @private
   * @param string actorId
   *        The actor ID you want to release.
   */
  _releaseObject: function(actorId)
  {
    this.owner._releaseObject(actorId);
  },

  /**
   * Add a message to output.
   *
   * @param object ...args
   *        Any number of Message objects.
   * @return this
   */
  addMessage: function(...args)
  {
    for (let msg of args) {
      msg.init(this);
      this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage,
                               [msg]);
    }
    return this;
  },

  /**
   * Message renderer used for compatibility with the current Web Console output
   * implementation. This method is invoked for every message object that is
   * flushed to output. The message object is initialized and rendered, then it
   * is displayed.
   *
   * TODO: remove this method once bug 778766 is fixed.
   *
   * @private
   * @param object message
   *        The message object to render.
   * @return DOMElement
   *         The message DOM element that can be added to the console output.
   */
  _onFlushOutputMessage: function(message)
  {
    return message.render().element;
  },

  /**
   * Get an array of selected messages. This list is based on the text selection
   * start and end points.
   *
   * @param number [limit]
   *        Optional limit of selected messages you want. If no value is given,
   *        all of the selected messages are returned.
   * @return array
   *         Array of DOM elements for each message that is currently selected.
   */
  getSelectedMessages: function(limit)
  {
    let selection = this.window.getSelection();
    if (selection.isCollapsed) {
      return [];
    }

    if (selection.containsNode(this.element, true)) {
      return Array.slice(this.element.children);
    }

    let anchor = this.getMessageForElement(selection.anchorNode);
    let focus = this.getMessageForElement(selection.focusNode);
    if (!anchor || !focus) {
      return [];
    }

    let start, end;
    if (anchor.timestamp > focus.timestamp) {
      start = focus;
      end = anchor;
    } else {
      start = anchor;
      end = focus;
    }

    let result = [];
    let current = start;
    while (current) {
      result.push(current);
      if (current == end || (limit && result.length == limit)) {
        break;
      }
      current = current.nextSibling;
    }
    return result;
  },

  /**
   * Find the DOM element of a message for any given descendant.
   *
   * @param DOMElement elem
   *        The element to start the search from.
   * @return DOMElement|null
   *         The DOM element of the message, if any.
   */
  getMessageForElement: function(elem)
  {
    while (elem && elem.parentNode) {
      if (elem.classList && elem.classList.contains("message")) {
        return elem;
      }
      elem = elem.parentNode;
    }
    return null;
  },

  /**
   * Select all messages.
   */
  selectAllMessages: function()
  {
    let selection = this.window.getSelection();
    selection.removeAllRanges();
    let range = this.document.createRange();
    range.selectNodeContents(this.element);
    selection.addRange(range);
  },

  /**
   * Add a message to the selection.
   *
   * @param DOMElement elem
   *        The message element to select.
   */
  selectMessage: function(elem)
  {
    let selection = this.window.getSelection();
    selection.removeAllRanges();
    let range = this.document.createRange();
    range.selectNodeContents(elem);
    selection.addRange(range);
  },

  /**
   * Open an URL in a new tab.
   * @see WebConsole.openLink() in hudservice.js
   */
  openLink: function()
  {
    this.owner.owner.openLink.apply(this.owner.owner, arguments);
  },

  /**
   * Open the variables view to inspect an object actor.
   * @see JSTerm.openVariablesView() in webconsole.js
   */
  openVariablesView: function()
  {
    this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments);
  },

  /**
   * Destroy this ConsoleOutput instance.
   */
  destroy: function()
  {
    this._dummyElement = null;
    this.owner = null;
  },
}; // ConsoleOutput.prototype

/**
 * Message objects container.
 * @type object
 */
let Messages = {};

/**
 * The BaseMessage object is used for all types of messages. Every kind of
 * message should use this object as its base.
 *
 * @constructor
 */
Messages.BaseMessage = function()
{
  this.widgets = new Set();
  this._onClickAnchor = this._onClickAnchor.bind(this);
  this._repeatID = { uid: gSequenceId() };
  this.textContent = "";
};

Messages.BaseMessage.prototype = {
  /**
   * Reference to the ConsoleOutput owner.
   *
   * @type object|null
   *       This is |null| if the message is not yet initialized.
   */
  output: null,

  /**
   * Reference to the parent message object, if this message is in a group or if
   * it is otherwise owned by another message.
   *
   * @type object|null
   */
  parent: null,

  /**
   * Message DOM element.
   *
   * @type DOMElement|null
   *       This is |null| if the message is not yet rendered.
   */
  element: null,

  /**
   * Tells if this message is visible or not.
   * @type boolean
   */
  get visible() {
    return this.element && this.element.parentNode;
  },

  /**
   * The owner DOM document.
   * @type DOMElement
   */
  get document() {
    return this.output.document;
  },

  /**
   * Holds the text-only representation of the message.
   * @type string
   */
  textContent: null,

  /**
   * Set of widgets included in this message.
   * @type Set
   */
  widgets: null,

  // Properties that allow compatibility with the current Web Console output
  // implementation.
  _categoryCompat: null,
  _severityCompat: null,
  _categoryNameCompat: null,
  _severityNameCompat: null,
  _filterKeyCompat: null,

  /**
   * Object that is JSON-ified and used as a non-unique ID for tracking
   * duplicate messages.
   * @private
   * @type object
   */
  _repeatID: null,

  /**
   * Initialize the message.
   *
   * @param object output
   *        The ConsoleOutput owner.
   * @param object [parent=null]
   *        Optional: a different message object that owns this instance.
   * @return this
   */
  init: function(output, parent=null)
  {
    this.output = output;
    this.parent = parent;
    return this;
  },

  /**
   * Non-unique ID for this message object used for tracking duplicate messages.
   * Different message kinds can identify themselves based their own criteria.
   *
   * @return string
   */
  getRepeatID: function()
  {
    return JSON.stringify(this._repeatID);
  },

  /**
   * Render the message. After this method is invoked the |element| property
   * will point to the DOM element of this message.
   * @return this
   */
  render: function()
  {
    if (!this.element) {
      this.element = this._renderCompat();
    }
    return this;
  },

  /**
   * Prepare the message container for the Web Console, such that it is
   * compatible with the current implementation.
   * TODO: remove this once bug 778766 is fixed.
   *
   * @private
   * @return Element
   *         The DOM element that wraps the message.
   */
  _renderCompat: function()
  {
    let doc = this.output.document;
    let container = doc.createElementNS(XHTML_NS, "div");
    container.id = "console-msg-" + gSequenceId();
    container.className = "message";
    if (this.category == "input") {
      // Assistive technology tools shouldn't echo input to the user,
      // as the user knows what they've just typed.
      container.setAttribute("aria-live", "off");
    }
    container.category = this._categoryCompat;
    container.severity = this._severityCompat;
    container.setAttribute("category", this._categoryNameCompat);
    container.setAttribute("severity", this._severityNameCompat);
    container.setAttribute("filter", this._filterKeyCompat);
    container.clipboardText = this.textContent;
    container.timestamp = this.timestamp;
    container._messageObject = this;

    return container;
  },

  /**
   * Add a click callback to a given DOM element.
   *
   * @private
   * @param Element element
   *        The DOM element to which you want to add a click event handler.
   * @param function [callback=this._onClickAnchor]
   *        Optional click event handler. The default event handler is
   *        |this._onClickAnchor|.
   */
  _addLinkCallback: function(element, callback = this._onClickAnchor)
  {
    // This is going into the WebConsoleFrame object instance that owns
    // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole
    // object instance from hudservice.js.
    // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766
    // is fixed.
    this.output.owner._addMessageLinkCallback(element, callback);
  },

  /**
   * The default |click| event handler for links in the output. This function
   * opens the anchor's link in a new tab.
   *
   * @private
   * @param Event event
   *        The DOM event that invoked this function.
   */
  _onClickAnchor: function(event)
  {
    this.output.openLink(event.target.href);
  },

  destroy: function()
  {
    // Destroy all widgets that have registered themselves in this.widgets
    for (let widget of this.widgets) {
      widget.destroy();
    }
    this.widgets.clear();
  }
}; // Messages.BaseMessage.prototype


/**
 * The NavigationMarker is used to show a page load event.
 *
 * @constructor
 * @extends Messages.BaseMessage
 * @param object response
 *        The response received from the back end.
 * @param number timestamp
 *        The message date and time, milliseconds elapsed since 1 January 1970
 *        00:00:00 UTC.
 */
Messages.NavigationMarker = function(response, timestamp)
{
  Messages.BaseMessage.call(this);

  // Store the response packet received from the server. It might
  // be useful for extensions customizing the console output.
  this.response = response;
  this._url = response.url;
  this.textContent = "------ " + this._url;
  this.timestamp = timestamp;
};

Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype,
{
  /**
   * The address of the loading page.
   * @private
   * @type string
   */
  _url: null,

  /**
   * Message timestamp.
   *
   * @type number
   *       Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
   */
  timestamp: 0,

  _categoryCompat: COMPAT.CATEGORIES.NETWORK,
  _severityCompat: COMPAT.SEVERITIES.LOG,
  _categoryNameCompat: "network",
  _severityNameCompat: "info",
  _filterKeyCompat: "networkinfo",

  /**
   * Prepare the DOM element for this message.
   * @return this
   */
  render: function()
  {
    if (this.element) {
      return this;
    }

    let url = this._url;
    let pos = url.indexOf("?");
    if (pos > -1) {
      url = url.substr(0, pos);
    }

    let doc = this.output.document;
    let urlnode = doc.createElementNS(XHTML_NS, "a");
    urlnode.className = "url";
    urlnode.textContent = url;
    urlnode.title = this._url;
    urlnode.href = this._url;
    urlnode.draggable = false;
    this._addLinkCallback(urlnode);

    let render = Messages.BaseMessage.prototype.render.bind(this);
    render().element.appendChild(urlnode);
    this.element.classList.add("navigation-marker");
    this.element.url = this._url;
    this.element.appendChild(doc.createTextNode("\n"));

    return this;
  },
}); // Messages.NavigationMarker.prototype


/**
 * The Simple message is used to show any basic message in the Web Console.
 *
 * @constructor
 * @extends Messages.BaseMessage
 * @param string|Node|function message
 *        The message to display.
 * @param object [options]
 *        Options for this message:
 *        - category: (string) category that this message belongs to. Defaults
 *        to no category.
 *        - severity: (string) severity of the message. Defaults to no severity.
 *        - timestamp: (number) date and time when the message was recorded.
 *        Defaults to |Date.now()|.
 *        - link: (string) if provided, the message will be wrapped in an anchor
 *        pointing to the given URL here.
 *        - linkCallback: (function) if provided, the message will be wrapped in
 *        an anchor. The |linkCallback| function will be added as click event
 *        handler.
 *        - location: object that tells the message source: url, line, column
 *        and lineText.
 *        - className: (string) additional element class names for styling
 *        purposes.
 *        - private: (boolean) mark this as a private message.
 *        - filterDuplicates: (boolean) true if you do want this message to be
 *        filtered as a potential duplicate message, false otherwise.
 */
Messages.Simple = function(message, options = {})
{
  Messages.BaseMessage.call(this);

  this.category = options.category;
  this.severity = options.severity;
  this.location = options.location;
  this.timestamp = options.timestamp || Date.now();
  this.private = !!options.private;

  this._message = message;
  this._className = options.className;
  this._link = options.link;
  this._linkCallback = options.linkCallback;
  this._filterDuplicates = options.filterDuplicates;
};

Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
{
  /**
   * Message category.
   * @type string
   */
  category: null,

  /**
   * Message severity.
   * @type string
   */
  severity: null,

  /**
   * Message source location. Properties: url, line, column, lineText.
   * @type object
   */
  location: null,

  /**
   * Tells if this message comes from a private browsing context.
   * @type boolean
   */
  private: false,

  /**
   * Custom class name for the DOM element of the message.
   * @private
   * @type string
   */
  _className: null,

  /**
   * Message link - if this message is clicked then this URL opens in a new tab.
   * @private
   * @type string
   */
  _link: null,

  /**
   * Message click event handler.
   * @private
   * @type function
   */
  _linkCallback: null,

  /**
   * Tells if this message should be checked if it is a duplicate of another
   * message or not.
   */
  _filterDuplicates: false,

  /**
   * The raw message displayed by this Message object. This can be a function,
   * DOM node or a string.
   *
   * @private
   * @type mixed
   */
  _message: null,

  _afterMessage: null,
  _objectActors: null,
  _groupDepthCompat: 0,

  /**
   * Message timestamp.
   *
   * @type number
   *       Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
   */
  timestamp: 0,

  get _categoryCompat() {
    return this.category ?
           COMPAT.CATEGORIES[this.category.toUpperCase()] : null;
  },
  get _severityCompat() {
    return this.severity ?
           COMPAT.SEVERITIES[this.severity.toUpperCase()] : null;
  },
  get _categoryNameCompat() {
    return this.category ?
           COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null;
  },
  get _severityNameCompat() {
    return this.severity ?
           COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null;
  },

  get _filterKeyCompat() {
    return this._categoryCompat !== null && this._severityCompat !== null ?
           COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] :
           null;
  },

  init: function()
  {
    Messages.BaseMessage.prototype.init.apply(this, arguments);
    this._groupDepthCompat = this.output.owner.groupDepth;
    this._initRepeatID();
    return this;
  },

  _initRepeatID: function()
  {
    if (!this._filterDuplicates) {
      return;
    }

    // Add the properties we care about for identifying duplicate messages.
    let rid = this._repeatID;
    delete rid.uid;

    rid.category = this.category;
    rid.severity = this.severity;
    rid.private = this.private;
    rid.location = this.location;
    rid.link = this._link;
    rid.linkCallback = this._linkCallback + "";
    rid.className = this._className;
    rid.groupDepth = this._groupDepthCompat;
    rid.textContent = "";
  },

  getRepeatID: function()
  {
    // No point in returning a string that includes other properties when there
    // is a unique ID.
    if (this._repeatID.uid) {
      return JSON.stringify({ uid: this._repeatID.uid });
    }

    return JSON.stringify(this._repeatID);
  },

  render: function()
  {
    if (this.element) {
      return this;
    }

    let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render();

    let icon = this.document.createElementNS(XHTML_NS, "span");
    icon.className = "icon";
    icon.title = l10n.getStr("severity." + this._severityNameCompat);

    // Apply the current group by indenting appropriately.
    // TODO: remove this once bug 778766 is fixed.
    let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT;
    let indentNode = this.document.createElementNS(XHTML_NS, "span");
    indentNode.className = "indent";
    indentNode.style.width = indent + "px";

    let body = this._renderBody();
    this._repeatID.textContent += "|" + body.textContent;

    let repeatNode = this._renderRepeatNode();
    let location = this._renderLocation();

    Messages.BaseMessage.prototype.render.call(this);
    if (this._className) {
      this.element.className += " " + this._className;
    }

    this.element.appendChild(timestamp.element);
    this.element.appendChild(indentNode);
    this.element.appendChild(icon);
    this.element.appendChild(body);
    if (repeatNode) {
      this.element.appendChild(repeatNode);
    }
    if (location) {
      this.element.appendChild(location);
    }
    this.element.appendChild(this.document.createTextNode("\n"));

    this.element.clipboardText = this.element.textContent;

    if (this.private) {
      this.element.setAttribute("private", true);
    }

    if (this._afterMessage) {
      this.element._outputAfterNode = this._afterMessage.element;
      this._afterMessage = null;
    }

    // TODO: handle object releasing in a more elegant way once all console
    // messages use the new API - bug 778766.
    this.element._objectActors = this._objectActors;
    this._objectActors = null;

    return this;
  },

  /**
   * Render the message body DOM element.
   * @private
   * @return Element
   */
  _renderBody: function()
  {
    let body = this.document.createElementNS(XHTML_NS, "span");
    body.className = "message-body-wrapper message-body devtools-monospace";

    let bodyInner = this.document.createElementNS(XHTML_NS, "span");
    body.appendChild(bodyInner);

    let anchor, container = bodyInner;
    if (this._link || this._linkCallback) {
      container = anchor = this.document.createElementNS(XHTML_NS, "a");
      anchor.href = this._link || "#";
      anchor.draggable = false;
      this._addLinkCallback(anchor, this._linkCallback);
      bodyInner.appendChild(anchor);
    }

    if (typeof this._message == "function") {
      container.appendChild(this._message(this));
    } else if (this._message instanceof Ci.nsIDOMNode) {
      container.appendChild(this._message);
    } else {
      container.textContent = this._message;
    }

    return body;
  },

  /**
   * Render the repeat bubble DOM element part of the message.
   * @private
   * @return Element
   */
  _renderRepeatNode: function()
  {
    if (!this._filterDuplicates) {
      return null;
    }

    let repeatNode = this.document.createElementNS(XHTML_NS, "span");
    repeatNode.setAttribute("value", "1");
    repeatNode.className = "message-repeats";
    repeatNode.textContent = 1;
    repeatNode._uid = this.getRepeatID();
    return repeatNode;
  },

  /**
   * Render the message source location DOM element.
   * @private
   * @return Element
   */
  _renderLocation: function()
  {
    if (!this.location) {
      return null;
    }

    let {url, line, column} = this.location;
    if (IGNORED_SOURCE_URLS.indexOf(url) != -1) {
      return null;
    }

    // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js.
    // TODO: move createLocationNode() into this file when bug 778766 is fixed.
    return this.output.owner.createLocationNode({url: url,
                                                 line: line,
                                                 column: column});
  },
}); // Messages.Simple.prototype


/**
 * The Extended message.
 *
 * @constructor
 * @extends Messages.Simple
 * @param array messagePieces
 *        The message to display given as an array of elements. Each array
 *        element can be a DOM node, function, ObjectActor, LongString or
 *        a string.
 * @param object [options]
 *        Options for rendering this message:
 *        - quoteStrings: boolean that tells if you want strings to be wrapped
 *        in quotes or not.
 */
Messages.Extended = function(messagePieces, options = {})
{
  Messages.Simple.call(this, null, options);

  this._messagePieces = messagePieces;

  if ("quoteStrings" in options) {
    this._quoteStrings = options.quoteStrings;
  }

  this._repeatID.quoteStrings = this._quoteStrings;
  this._repeatID.messagePieces = messagePieces + "";
  this._repeatID.actors = new Set(); // using a set to avoid duplicates
};

Messages.Extended.prototype = Heritage.extend(Messages.Simple.prototype,
{
  /**
   * The message pieces displayed by this message instance.
   * @private
   * @type array
   */
  _messagePieces: null,

  /**
   * Boolean that tells if the strings displayed in this message are wrapped.
   * @private
   * @type boolean
   */
  _quoteStrings: true,

  getRepeatID: function()
  {
    if (this._repeatID.uid) {
      return JSON.stringify({ uid: this._repeatID.uid });
    }

    // Sets are not stringified correctly. Temporarily switching to an array.
    let actors = this._repeatID.actors;
    this._repeatID.actors = [...actors];
    let result = JSON.stringify(this._repeatID);
    this._repeatID.actors = actors;
    return result;
  },

  render: function()
  {
    let result = this.document.createDocumentFragment();

    for (let i = 0; i < this._messagePieces.length; i++) {
      let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
      if (separator) {
        result.appendChild(separator);
      }

      let piece = this._messagePieces[i];
      result.appendChild(this._renderBodyPiece(piece));
    }

    this._message = result;
    this._messagePieces = null;
    return Messages.Simple.prototype.render.call(this);
  },

  /**
   * Render the separator between the pieces of the message.
   *
   * @private
   * @return Element
   */
  _renderBodyPieceSeparator: function() { return null; },

  /**
   * Render one piece/element of the message array.
   *
   * @private
   * @param mixed piece
   *        Message element to display - this can be a LongString, ObjectActor,
   *        DOM node or a function to invoke.
   * @return Element
   */
  _renderBodyPiece: function(piece)
  {
    if (piece instanceof Ci.nsIDOMNode) {
      return piece;
    }
    if (typeof piece == "function") {
      return piece(this);
    }

    return this._renderValueGrip(piece);
  },

  /**
   * Render a grip that represents a value received from the server. This method
   * picks the appropriate widget to render the value with.
   *
   * @private
   * @param object grip
   *        The value grip received from the server.
   * @param object options
   *        Options for displaying the value. Available options:
   *        - noStringQuotes - boolean that tells the renderer to not use quotes
   *        around strings.
   *        - concise - boolean that tells the renderer to compactly display the
   *        grip. This is typically set to true when the object needs to be
   *        displayed in an array preview, or as a property value in object
   *        previews, etc.
   * @return DOMElement
   *         The DOM element that displays the given grip.
   */
  _renderValueGrip: function(grip, options = {})
  {
    let isPrimitive = VariablesView.isPrimitive({ value: grip });
    let isActorGrip = WebConsoleUtils.isActorGrip(grip);
    let noStringQuotes = !this._quoteStrings;
    if ("noStringQuotes" in options) {
      noStringQuotes = options.noStringQuotes;
    }

    if (isActorGrip) {
      this._repeatID.actors.add(grip.actor);

      if (!isPrimitive) {
        return this._renderObjectActor(grip, options);
      }
      if (grip.type == "longString") {
        let widget = new Widgets.LongString(this, grip, options).render();
        return widget.element;
      }
    }

    let result = this.document.createElementNS(XHTML_NS, "span");
    if (isPrimitive) {
      if (Widgets.URLString.prototype.containsURL.call(Widgets.URLString.prototype, grip)) {
        let widget = new Widgets.URLString(this, grip, options).render();
        return widget.element;
      }

      let className = this.getClassNameForValueGrip(grip);
      if (className) {
        result.className = className;
      }

      result.textContent = VariablesView.getString(grip, {
        noStringQuotes: noStringQuotes,
        concise: options.concise,
      });
    } else {
      result.textContent = grip;
    }

    return result;
  },

  /**
   * Shorten grips of the type string, leaves other grips unmodified.
   *
   * @param object grip
   *        Value grip from the server.
   * @return object
   *        Possible values of object:
   *        - A shortened string, if original grip was of string type.
   *        - The unmodified input grip, if it wasn't of string type.
   */
  shortenValueGrip: function(grip)
  {
    let shortVal = grip;
    if (typeof(grip)=="string") {
      shortVal = grip.replace(/(\r\n|\n|\r)/gm," ");
      if (shortVal.length > MAX_STRING_GRIP_LENGTH) {
        shortVal = shortVal.substring(0,MAX_STRING_GRIP_LENGTH - 1) + ELLIPSIS;
      }
    }

    return shortVal;
  },

  /**
   * Get a CodeMirror-compatible class name for a given value grip.
   *
   * @param object grip
   *        Value grip from the server.
   * @return string
   *         The class name for the grip.
   */
  getClassNameForValueGrip: function(grip)
  {
    let map = {
      "number": "cm-number",
      "longstring": "console-string",
      "string": "console-string",
      "regexp": "cm-string-2",
      "boolean": "cm-atom",
      "-infinity": "cm-atom",
      "infinity": "cm-atom",
      "null": "cm-atom",
      "undefined": "cm-comment",
      "symbol": "cm-atom"
    };

    let className = map[typeof grip];
    if (!className && grip && grip.type) {
      className = map[grip.type.toLowerCase()];
    }
    if (!className && grip && grip.class) {
      className = map[grip.class.toLowerCase()];
    }

    return className;
  },

  /**
   * Display an object actor with the appropriate renderer.
   *
   * @private
   * @param object objectActor
   *        The ObjectActor to display.
   * @param object options
   *        Options to use for displaying the ObjectActor.
   * @see this._renderValueGrip for the available options.
   * @return DOMElement
   *         The DOM element that displays the object actor.
   */
  _renderObjectActor: function(objectActor, options = {})
  {
    let widget = Widgets.ObjectRenderers.byClass[objectActor.class];

    let { preview } = objectActor;
    if ((!widget || (widget.canRender && !widget.canRender(objectActor)))
        && preview
        && preview.kind) {
      widget = Widgets.ObjectRenderers.byKind[preview.kind];
    }

    if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
      widget = Widgets.JSObject;
    }

    let instance = new widget(this, objectActor, options).render();
    return instance.element;
  },
}); // Messages.Extended.prototype



/**
 * The JavaScriptEvalOutput message.
 *
 * @constructor
 * @extends Messages.Extended
 * @param object evalResponse
 *        The evaluation response packet received from the server.
 * @param string [errorMessage]
 *        Optional error message to display.
 */
Messages.JavaScriptEvalOutput = function(evalResponse, errorMessage)
{
  let severity = "log", msg, quoteStrings = true;

  // Store also the response packet from the back end. It might
  // be useful to extensions customizing the console output.
  this.response = evalResponse;

  if (errorMessage) {
    severity = "error";
    msg = errorMessage;
    quoteStrings = false;
  } else {
    msg = evalResponse.result;
  }

  let options = {
    className: "cm-s-mozilla",
    timestamp: evalResponse.timestamp,
    category: "output",
    severity: severity,
    quoteStrings: quoteStrings,
  };
  Messages.Extended.call(this, [msg], options);
};

Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype;

/**
 * The ConsoleGeneric message is used for console API calls.
 *
 * @constructor
 * @extends Messages.Extended
 * @param object packet
 *        The Console API call packet received from the server.
 */
Messages.ConsoleGeneric = function(packet)
{
  let options = {
    className: "cm-s-mozilla",
    timestamp: packet.timeStamp,
    category: "webdev",
    severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
    private: packet.private,
    filterDuplicates: true,
    location: {
      url: packet.filename,
      line: packet.lineNumber,
      column: packet.columnNumber
    },
  };

  switch (packet.level) {
    case "count": {
      let counter = packet.counter, label = counter.label;
      if (!label) {
        label = l10n.getStr("noCounterLabel");
      }
      Messages.Extended.call(this, [label+ ": " + counter.count], options);
      break;
    }
    default:
      Messages.Extended.call(this, packet.arguments, options);
      break;
  }

  this._repeatID.consoleApiLevel = packet.level;
  this._repeatID.styles = packet.styles;
  this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
  this._styles = packet.styles || [];

  this._onClickCollapsible = this._onClickCollapsible.bind(this);
};

Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
{
  _styles: null,
  _stacktrace: null,

  /**
   * Tells if the message can be expanded/collapsed.
   * @type boolean
   */
  collapsible: false,

  /**
   * Getter that tells if this message is collapsed - no details are shown.
   * @type boolean
   */
  get collapsed() {
    return this.collapsible && this.element && !this.element.hasAttribute("open");
  },

  _renderBodyPieceSeparator: function()
  {
    return this.document.createTextNode(" ");
  },

  render: function()
  {
    let msg = this.document.createElementNS(XHTML_NS, "span");
    msg.className = "message-body devtools-monospace";

    this._renderBodyPieces(msg);

    let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
    let location = Messages.Simple.prototype._renderLocation.call(this);
    if (location) {
      location.target = "jsdebugger";
    }

    let stack = null;
    let twisty = null;
    if (this._stacktrace && this._stacktrace.length > 0) {
      stack = new Widgets.Stacktrace(this, this._stacktrace).render().element;

      twisty = this.document.createElementNS(XHTML_NS, "a");
      twisty.className = "theme-twisty";
      twisty.href = "#";
      twisty.title = l10n.getStr("messageToggleDetails");
      twisty.addEventListener("click", this._onClickCollapsible);
    }

    let flex = this.document.createElementNS(XHTML_NS, "span");
    flex.className = "message-flex-body";

    if (twisty) {
      flex.appendChild(twisty);
    }

    flex.appendChild(msg);

    if (repeatNode) {
      flex.appendChild(repeatNode);
    }
    if (location) {
      flex.appendChild(location);
    }

    let result = this.document.createDocumentFragment();
    result.appendChild(flex);

    if (stack) {
      result.appendChild(this.document.createTextNode("\n"));
      result.appendChild(stack);
    }

    this._message = result;
    this._stacktrace = null;

    Messages.Simple.prototype.render.call(this);

    if (stack) {
      this.collapsible = true;
      this.element.setAttribute("collapsible", true);

      let icon = this.element.querySelector(".icon");
      icon.addEventListener("click", this._onClickCollapsible);
    }

    return this;
  },

  _renderBody: function()
  {
    let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
    body.classList.remove("devtools-monospace", "message-body");
    return body;
  },

  _renderBodyPieces: function(container)
  {
    let lastStyle = null;

    for (let i = 0; i < this._messagePieces.length; i++) {
      let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
      if (separator) {
        container.appendChild(separator);
      }

      let piece = this._messagePieces[i];
      let style = this._styles[i];

      // No long string support.
      if (style && typeof style == "string" ) {
        lastStyle = this.cleanupStyle(style);
      }

      container.appendChild(this._renderBodyPiece(piece, lastStyle));
    }

    this._messagePieces = null;
    this._styles = null;
  },

  _renderBodyPiece: function(piece, style)
  {
    let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece);
    let result = elem;

    if (style) {
      if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
        elem.style = style;
      } else {
        let span = this.document.createElementNS(XHTML_NS, "span");
        span.style = style;
        span.appendChild(elem);
        result = span;
      }
    }

    return result;
  },

  // no-op for the message location and .repeats elements.
  // |this.render()| handles customized message output.
  _renderLocation: function() { },
  _renderRepeatNode: function() { },

  /**
   * Expand/collapse message details.
   */
  toggleDetails: function()
  {
    let twisty = this.element.querySelector(".theme-twisty");
    if (this.element.hasAttribute("open")) {
      this.element.removeAttribute("open");
      twisty.removeAttribute("open");
    } else {
      this.element.setAttribute("open", true);
      twisty.setAttribute("open", true);
    }
  },

  /**
   * The click event handler for the message expander arrow element. This method
   * toggles the display of message details.
   *
   * @private
   * @param nsIDOMEvent ev
   *        The DOM event object.
   * @see this.toggleDetails()
   */
  _onClickCollapsible: function(ev)
  {
    ev.preventDefault();
    this.toggleDetails();
  },

  /**
   * Given a style attribute value, return a cleaned up version of the string
   * such that:
   *
   * - no external URL is allowed to load. See RE_CLEANUP_STYLES.
   * - only some of the properties are allowed, based on a whitelist. See
   *   RE_ALLOWED_STYLES.
   *
   * @param string style
   *        The style string to cleanup.
   * @return string
   *         The style value after cleanup.
   */
  cleanupStyle: function(style)
  {
    for (let r of RE_CLEANUP_STYLES) {
      style = style.replace(r, "notallowed");
    }

    let dummy = this.output._dummyElement;
    if (!dummy) {
      dummy = this.output._dummyElement =
        this.document.createElementNS(XHTML_NS, "div");
    }
    dummy.style = style;

    let toRemove = [];
    for (let i = 0; i < dummy.style.length; i++) {
      let prop = dummy.style[i];
      if (!RE_ALLOWED_STYLES.test(prop)) {
        toRemove.push(prop);
      }
    }

    for (let prop of toRemove) {
      dummy.style.removeProperty(prop);
    }

    style = dummy.style.cssText;

    dummy.style = "";

    return style;
  },
}); // Messages.ConsoleGeneric.prototype

/**
 * The ConsoleTrace message is used for console.trace() calls.
 *
 * @constructor
 * @extends Messages.Simple
 * @param object packet
 *        The Console API call packet received from the server.
 */
Messages.ConsoleTrace = function(packet)
{
  let options = {
    className: "cm-s-mozilla",
    timestamp: packet.timeStamp,
    category: "webdev",
    severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
    private: packet.private,
    filterDuplicates: true,
    location: {
      url: packet.filename,
      line: packet.lineNumber,
    },
  };

  this._renderStack = this._renderStack.bind(this);
  Messages.Simple.call(this, this._renderStack, options);

  this._repeatID.consoleApiLevel = packet.level;
  this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
  this._arguments = packet.arguments;
};

Messages.ConsoleTrace.prototype = Heritage.extend(Messages.Simple.prototype,
{
  /**
   * Holds the stackframes received from the server.
   *
   * @private
   * @type array
   */
  _stacktrace: null,

  /**
   * Holds the arguments the content script passed to the console.trace()
   * method. This array is cleared when the message is initialized, and
   * associated actors are released.
   *
   * @private
   * @type array
   */
  _arguments: null,

  init: function()
  {
    let result = Messages.Simple.prototype.init.apply(this, arguments);

    // We ignore console.trace() arguments. Release object actors.
    if (Array.isArray(this._arguments)) {
      for (let arg of this._arguments) {
        if (WebConsoleUtils.isActorGrip(arg)) {
          this.output._releaseObject(arg.actor);
        }
      }
    }
    this._arguments = null;

    return result;
  },

  render: function()
  {
    Messages.Simple.prototype.render.apply(this, arguments);
    this.element.setAttribute("open", true);
    return this;
  },

  /**
   * Render the stack frames.
   *
   * @private
   * @return DOMElement
   */
  _renderStack: function()
  {
    let cmvar = this.document.createElementNS(XHTML_NS, "span");
    cmvar.className = "cm-variable";
    cmvar.textContent = "console";

    let cmprop = this.document.createElementNS(XHTML_NS, "span");
    cmprop.className = "cm-property";
    cmprop.textContent = "trace";

    let title = this.document.createElementNS(XHTML_NS, "span");
    title.className = "message-body devtools-monospace";
    title.appendChild(cmvar);
    title.appendChild(this.document.createTextNode("."));
    title.appendChild(cmprop);
    title.appendChild(this.document.createTextNode("():"));

    let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
    let location = Messages.Simple.prototype._renderLocation.call(this);
    if (location) {
      location.target = "jsdebugger";
    }

    let widget = new Widgets.Stacktrace(this, this._stacktrace).render();

    let body = this.document.createElementNS(XHTML_NS, "span");
    body.className = "message-flex-body";
    body.appendChild(title);
    if (repeatNode) {
      body.appendChild(repeatNode);
    }
    if (location) {
      body.appendChild(location);
    }
    body.appendChild(this.document.createTextNode("\n"));

    let frag = this.document.createDocumentFragment();
    frag.appendChild(body);
    frag.appendChild(widget.element);

    return frag;
  },

  _renderBody: function()
  {
    let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
    body.classList.remove("devtools-monospace", "message-body");
    return body;
  },

  // no-op for the message location and .repeats elements.
  // |this._renderStack| handles customized message output.
  _renderLocation: function() { },
  _renderRepeatNode: function() { },
}); // Messages.ConsoleTrace.prototype

/**
 * The ConsoleTable message is used for console.table() calls.
 *
 * @constructor
 * @extends Messages.Extended
 * @param object packet
 *        The Console API call packet received from the server.
 */
Messages.ConsoleTable = function(packet)
{
  let options = {
    className: "cm-s-mozilla",
    timestamp: packet.timeStamp,
    category: "webdev",
    severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
    private: packet.private,
    filterDuplicates: false,
    location: {
      url: packet.filename,
      line: packet.lineNumber,
    },
  };

  this._populateTableData = this._populateTableData.bind(this);
  this._renderTable = this._renderTable.bind(this);
  Messages.Extended.call(this, [this._renderTable], options);

  this._repeatID.consoleApiLevel = packet.level;
  this._arguments = packet.arguments;
};

Messages.ConsoleTable.prototype = Heritage.extend(Messages.Extended.prototype,
{
  /**
   * Holds the arguments the content script passed to the console.table()
   * method.
   *
   * @private
   * @type array
   */
  _arguments: null,

  /**
   * Array of objects that holds the data to log in the table.
   *
   * @private
   * @type array
   */
  _data: null,

  /**
   * Key value pair of the id and display name for the columns in the table.
   * Refer to the TableWidget API.
   *
   * @private
   * @type object
   */
  _columns: null,

  /**
   * A promise that resolves when the table data is ready or null if invalid
   * arguments are provided.
   *
   * @private
   * @type promise|null
   */
  _populatePromise: null,

  init: function()
  {
    let result = Messages.Extended.prototype.init.apply(this, arguments);
    this._data = [];
    this._columns = {};

    this._populatePromise = this._populateTableData();

    return result;
  },

  /**
   * Sets the key value pair of the id and display name for the columns in the
   * table.
   *
   * @private
   * @param array|string columns
   *        Either a string or array containing the names for the columns in
   *        the output table.
   */
  _setColumns: function(columns)
  {
    if (columns.class == "Array") {
      let items = columns.preview.items;

      for (let item of items) {
        if (typeof item == "string") {
          this._columns[item] = item;
        }
      }
    } else if (typeof columns == "string" && columns) {
      this._columns[columns] = columns;
    }
  },

  /**
   * Retrieves the table data and columns from the arguments received from the
   * server.
   *
   * @return Promise|null
   *         Returns a promise that resolves when the table data is ready or
   *         null if the arguments are invalid.
   */
  _populateTableData: function()
  {
    let deferred = promise.defer();

    if (this._arguments.length <= 0) {
      return;
    }

    let data = this._arguments[0];
    if (data.class != "Array" && data.class != "Object" &&
        data.class != "Map" && data.class != "Set") {
      return;
    }

    let hasColumnsArg = false;
    if (this._arguments.length > 1) {
      if (data.class == "Object" || data.class == "Array") {
        this._columns["_index"] = l10n.getStr("table.index");
      } else {
        this._columns["_index"] = l10n.getStr("table.iterationIndex");
      }

      this._setColumns(this._arguments[1]);
      hasColumnsArg = true;
    }

    if (data.class == "Object" || data.class == "Array") {
      // Get the object properties, and parse the key and value properties into
      // the table data and columns.
      this.client = new ObjectClient(this.output.owner.jsterm.hud.proxy.client,
          data);
      this.client.getPrototypeAndProperties(aResponse => {
        let {ownProperties} = aResponse;
        let rowCount = 0;
        let columnCount = 0;

        for (let index of Object.keys(ownProperties || {})) {
          // Avoid outputting the length property if the data argument provided
          // is an array
          if (data.class == "Array" && index == "length") {
            continue;
          }

          if (!hasColumnsArg) {
            this._columns["_index"] = l10n.getStr("table.index");
          }

          if (data.class == "Array") {
            if (index == parseInt(index)) {
              index = parseInt(index);
            }
          }

          let property = ownProperties[index].value;
          let item = { _index: index };

          if (property.class == "Object" || property.class == "Array") {
            let {preview} = property;
            let entries = property.class == "Object" ?
                preview.ownProperties : preview.items;

            for (let key of Object.keys(entries)) {
              let value = property.class == "Object" ?
                  preview.ownProperties[key].value : preview.items[key];

              item[key] = this._renderValueGrip(value, { concise: true });

              if (!hasColumnsArg && !(key in this._columns) &&
                  (++columnCount <= TABLE_COLUMN_MAX_ITEMS)) {
                this._columns[key] = key;
              }
            }
          } else {
            // Display the value for any non-object data input.
            item["_value"] = this._renderValueGrip(property, { concise: true });

            if (!hasColumnsArg && !("_value" in this._columns)) {
              this._columns["_value"] = l10n.getStr("table.value");
            }
          }

          this._data.push(item);

          if (++rowCount == TABLE_ROW_MAX_ITEMS) {
            break;
          }
        }

        deferred.resolve();
      });
    } else if (data.class == "Map") {
      let entries = data.preview.entries;

      if (!hasColumnsArg) {
        this._columns["_index"] = l10n.getStr("table.iterationIndex");
        this._columns["_key"] = l10n.getStr("table.key");
        this._columns["_value"] = l10n.getStr("table.value");
      }

      let rowCount = 0;
      for (let [key, value] of entries) {
        let item = {
          _index: rowCount,
          _key: this._renderValueGrip(key, { concise: true }),
          _value: this._renderValueGrip(value, { concise: true })
        };

        this._data.push(item);

        if (++rowCount == TABLE_ROW_MAX_ITEMS) {
          break;
        }
      }

      deferred.resolve();
    } else if (data.class == "Set") {
      let entries = data.preview.items;

      if (!hasColumnsArg) {
        this._columns["_index"] = l10n.getStr("table.iterationIndex");
        this._columns["_value"] = l10n.getStr("table.value");
      }

      let rowCount = 0;
      for (let entry of entries) {
        let item = {
          _index : rowCount,
          _value: this._renderValueGrip(entry, { concise: true })
        };

        this._data.push(item);

        if (++rowCount == TABLE_ROW_MAX_ITEMS) {
          break;
        }
      }

      deferred.resolve();
    }

    return deferred.promise;
  },

  render: function()
  {
    Messages.Extended.prototype.render.apply(this, arguments);
    this.element.setAttribute("open", true);
    return this;
  },

  /**
   * Render the table.
   *
   * @private
   * @return DOMElement
   */
  _renderTable: function()
  {
    let cmvar = this.document.createElementNS(XHTML_NS, "span");
    cmvar.className = "cm-variable";
    cmvar.textContent = "console";

    let cmprop = this.document.createElementNS(XHTML_NS, "span");
    cmprop.className = "cm-property";
    cmprop.textContent = "table";

    let title = this.document.createElementNS(XHTML_NS, "span");
    title.className = "message-body devtools-monospace";
    title.appendChild(cmvar);
    title.appendChild(this.document.createTextNode("."));
    title.appendChild(cmprop);
    title.appendChild(this.document.createTextNode("():"));

    let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
    let location = Messages.Simple.prototype._renderLocation.call(this);
    if (location) {
      location.target = "jsdebugger";
    }

    let body = this.document.createElementNS(XHTML_NS, "span");
    body.className = "message-flex-body";
    body.appendChild(title);
    if (repeatNode) {
      body.appendChild(repeatNode);
    }
    if (location) {
      body.appendChild(location);
    }
    body.appendChild(this.document.createTextNode("\n"));

    let result = this.document.createElementNS(XHTML_NS, "div");
    result.appendChild(body);

    if (this._populatePromise) {
      this._populatePromise.then(() => {
        if (this._data.length > 0) {
          let widget = new Widgets.Table(this, this._data, this._columns).render();
          result.appendChild(widget.element);
        }

        result.scrollIntoView();
        this.output.owner.emit("messages-table-rendered");

        // Release object actors
        if (Array.isArray(this._arguments)) {
          for (let arg of this._arguments) {
            if (WebConsoleUtils.isActorGrip(arg)) {
              this.output._releaseObject(arg.actor);
            }
          }
        }
        this._arguments = null;
      });
    }

    return result;
  },

  _renderBody: function()
  {
    let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
    body.classList.remove("devtools-monospace", "message-body");
    return body;
  },

  // no-op for the message location and .repeats elements.
  // |this._renderTable| handles customized message output.
  _renderLocation: function() { },
  _renderRepeatNode: function() { },
}); // Messages.ConsoleTable.prototype

let Widgets = {};

/**
 * The base widget class.
 *
 * @constructor
 * @param object message
 *        The owning message.
 */
Widgets.BaseWidget = function(message)
{
  this.message = message;
};

Widgets.BaseWidget.prototype = {
  /**
   * The owning message object.
   * @type object
   */
  message: null,

  /**
   * The DOM element of the rendered widget.
   * @type Element
   */
  element: null,

  /**
   * Getter for the DOM document that holds the output.
   * @type Document
   */
  get document() {
    return this.message.document;
  },

  /**
   * The ConsoleOutput instance that owns this widget instance.
   */
  get output() {
    return this.message.output;
  },

  /**
   * Render the widget DOM element.
   * @return this
   */
  render: function() { },

  /**
   * Destroy this widget instance.
   */
  destroy: function() { },

  /**
   * Helper for creating DOM elements for widgets.
   *
   * Usage:
   *   this.el("tag#id.class.names"); // create element "tag" with ID "id" and
   *   two class names, .class and .names.
   *
   *   this.el("span", { attr1: "value1", ... }) // second argument can be an
   *   object that holds element attributes and values for the new DOM element.
   *
   *   this.el("p", { attr1: "value1", ... }, "text content"); // the third
   *   argument can include the default .textContent of the new DOM element.
   *
   *   this.el("p", "text content"); // if the second argument is not an object,
   *   it will be used as .textContent for the new DOM element.
   *
   * @param string tagNameIdAndClasses
   *        Tag name for the new element, optionally followed by an ID and/or
   *        class names. Examples: "span", "div#fooId", "div.class.names",
   *        "p#id.class".
   * @param string|object [attributesOrTextContent]
   *        If this argument is an object it will be used to set the attributes
   *        of the new DOM element. Otherwise, the value becomes the
   *        .textContent of the new DOM element.
   * @param string [textContent]
   *        If this argument is provided the value is used as the textContent of
   *        the new DOM element.
   * @return DOMElement
   *         The new DOM element.
   */
  el: function(tagNameIdAndClasses)
  {
    let attrs, text;
    if (typeof arguments[1] == "object") {
      attrs = arguments[1];
      text = arguments[2];
    } else {
      text = arguments[1];
    }

    let tagName = tagNameIdAndClasses.split(/#|\./)[0];

    let elem = this.document.createElementNS(XHTML_NS, tagName);
    for (let name of Object.keys(attrs || {})) {
      elem.setAttribute(name, attrs[name]);
    }
    if (text !== undefined && text !== null) {
      elem.textContent = text;
    }

    let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g);
    for (let idOrClass of (idAndClasses || [])) {
      if (idOrClass.charAt(0) == "#") {
        elem.id = idOrClass.substr(1);
      } else {
        elem.classList.add(idOrClass.substr(1));
      }
    }

    return elem;
  },
};

/**
 * The timestamp widget.
 *
 * @constructor
 * @param object message
 *        The owning message.
 * @param number timestamp
 *        The UNIX timestamp to display.
 */
Widgets.MessageTimestamp = function(message, timestamp)
{
  Widgets.BaseWidget.call(this, message);
  this.timestamp = timestamp;
};

Widgets.MessageTimestamp.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
{
  /**
   * The UNIX timestamp.
   * @type number
   */
  timestamp: 0,

  render: function()
  {
    if (this.element) {
      return this;
    }

    this.element = this.document.createElementNS(XHTML_NS, "span");
    this.element.className = "timestamp devtools-monospace";
    this.element.textContent = l10n.timestampString(this.timestamp) + " ";

    return this;
  },
}); // Widgets.MessageTimestamp.prototype


/**
 * The URLString widget, for rendering strings where at least one token is a
 * URL.
 *
 * @constructor
 * @param object message
 *        The owning message.
 * @param string str
 *        The string, which contains at least one valid URL.
 */
Widgets.URLString = function(message, str)
{
  Widgets.BaseWidget.call(this, message);
  this.str = str;
};

Widgets.URLString.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
{
  /**
   * The string to format, which contains at least one valid URL.
   * @type string
   */
  str: "",

  render: function()
  {
    if (this.element) {
      return this;
    }

    // The rendered URLString will be a <span> containing a number of text
    // <spans> for non-URL tokens and <a>'s for URL tokens.
    this.element = this.el("span", {
      class: "console-string"
    });
    this.element.appendChild(this._renderText("\""));

    // As we walk through the tokens of the source string, we make sure to preserve
    // the original whitespace that seperated the tokens.
    let tokens = this.str.split(/\s+/);
    let textStart = 0;
    let tokenStart;
    for (let token of tokens) {
      tokenStart = this.str.indexOf(token, textStart);
      if (this._isURL(token)) {
        this.element.appendChild(this._renderText(this.str.slice(textStart, tokenStart)));
        textStart = tokenStart + token.length;
        this.element.appendChild(this._renderURL(token));
      }
    }

    // Clean up any non-URL text at the end of the source string.
    this.element.appendChild(this._renderText(this.str.slice(textStart, this.str.length)));
    this.element.appendChild(this._renderText("\""));

    return this;
  },

  /**
   * Determines whether a grip is a string containing a URL.
   *
   * @param string grip
   *        The grip, which may contain a URL.
   * @return boolean
   *         Whether the grip is a string containing a URL.
   */
  containsURL: function(grip)
  {
    if (typeof grip != "string") {
      return false;
    }

    let tokens = grip.split(/\s+/);
    return tokens.some(this._isURL);
  },

  /**
   * Determines whether a string token is a valid URL.
   *
   * @param string token
   *        The token.
   * @return boolean
   *         Whenther the token is a URL.
   */
  _isURL: function(token) {
    try {
      let uri = URI.newURI(token, null, null);
      let url = uri.QueryInterface(Ci.nsIURL);
      return true;
    } catch (e) {
      return false;
    }
  },

  /**
   * Renders a string as a URL.
   *
   * @param string url
   *        The string to be rendered as a url.
   * @return DOMElement
   *         An element containing the rendered string.
   */
  _renderURL: function(url)
  {
    let result = this.el("a", {
      class: "url",
      title: url,
      href: url,
      draggable: false
    }, url);
    this.message._addLinkCallback(result);
    return result;
  },

  _renderText: function(text) {
    return this.el("span", text);
  },
}); // Widgets.URLString.prototype

/**
 * Widget used for displaying ObjectActors that have no specialised renderers.
 *
 * @constructor
 * @param object message
 *        The owning message.
 * @param object objectActor
 *        The ObjectActor to display.
 * @param object [options]
 *        Options for displaying the given ObjectActor. See
 *        Messages.Extended.prototype._renderValueGrip for the available
 *        options.
 */
Widgets.JSObject = function(message, objectActor, options = {})
{
  Widgets.BaseWidget.call(this, message);
  this.objectActor = objectActor;
  this.options = options;
  this._onClick = this._onClick.bind(this);
};

Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
{
  /**
   * The ObjectActor displayed by the widget.
   * @type object
   */
  objectActor: null,

  render: function()
  {
    if (!this.element) {
      this._render();
    }

    return this;
  },

  _render: function()
  {
    let str = VariablesView.getString(this.objectActor, this.options);
    let className = this.message.getClassNameForValueGrip(this.objectActor);
    if (!className && this.objectActor.class == "Object") {
      className = "cm-variable";
    }

    this.element = this._anchor(str, { className: className });
  },

  /**
   * Render a concise representation of an object.
   */
  _renderConciseObject: function()
  {
    this.element = this._anchor(this.objectActor.class,
                                { className: "cm-variable" });
  },

  /**
   * Render the `<class> { ` prefix of an object.
   */
  _renderObjectPrefix: function()
  {
    let { kind } = this.objectActor.preview;
    this.element = this.el("span.kind-" + kind);
    this._anchor(this.objectActor.class, { className: "cm-variable" });
    this._text(" { ");
  },

  /**
   * Render the ` }` suffix of an object.
   */
  _renderObjectSuffix: function()
  {
    this._text(" }");
  },

  /**
   * Render an object property.
   *
   * @param String key
   *        The property name.
   * @param Object value
   *        The property value, as an RDP grip.
   * @param nsIDOMNode container
   *        The container node to render to.
   * @param Boolean needsComma
   *        True if there was another property before this one and we need to
   *        separate them with a comma.
   * @param Boolean valueIsText
   *        Add the value as is, don't treat it as a grip and pass it to
   *        `_renderValueGrip`.
   */
  _renderObjectProperty: function(key, value, container, needsComma, valueIsText = false)
  {
    if (needsComma) {
      this._text(", ");
    }

    container.appendChild(this.el("span.cm-property", key));
    this._text(": ");

    if (valueIsText) {
      this._text(value);
    } else {
      let shortVal = this.message.shortenValueGrip(value);
      let valueElem = this.message._renderValueGrip(shortVal, { concise: true });
      container.appendChild(valueElem);
    }
  },

  /**
   * Render this object's properties.
   *
   * @param nsIDOMNode container
   *        The container node to render to.
   * @param Boolean needsComma
   *        True if there was another property before this one and we need to
   *        separate them with a comma.
   */
  _renderObjectProperties: function(container, needsComma)
  {
    let { preview } = this.objectActor;
    let { ownProperties, safeGetterValues } = preview;

    let shown = 0;

    let getValue = desc => {
      if (desc.get) {
        return "Getter";
      } else if (desc.set) {
        return "Setter";
      } else {
        return desc.value;
      }
    };

    for (let key of Object.keys(ownProperties || {})) {
      this._renderObjectProperty(key, getValue(ownProperties[key]), container,
                                 shown > 0 || needsComma,
                                 ownProperties[key].get || ownProperties[key].set);
      shown++;
    }

    let ownPropertiesShown = shown;

    for (let key of Object.keys(safeGetterValues || {})) {
      this._renderObjectProperty(key, safeGetterValues[key].getterValue,
                                 container, shown > 0 || needsComma);
      shown++;
    }

    if (typeof preview.ownPropertiesLength == "number" &&
        ownPropertiesShown < preview.ownPropertiesLength) {
      this._text(", ");

      let n = preview.ownPropertiesLength - ownPropertiesShown;
      let str = VariablesView.stringifiers._getNMoreString(n);
      this._anchor(str);
    }
  },

  /**
   * Render an anchor with a given text content and link.
   *
   * @private
   * @param string text
   *        Text to show in the anchor.
   * @param object [options]
   *        Available options:
   *        - onClick (function): "click" event handler.By default a click on
   *        the anchor opens the variables view for the current object actor
   *        (this.objectActor).
   *        - href (string): if given the string is used as a link, and clicks
   *        on the anchor open the link in a new tab.
   *        - appendTo (DOMElement): append the element to the given DOM
   *        element. If not provided, the anchor is appended to |this.element|
   *        if it is available. If |appendTo| is provided and if it is a falsy
   *        value, the anchor is not appended to any element.
   * @return DOMElement
   *         The DOM element of the new anchor.
   */
  _anchor: function(text, options = {})
  {
    if (!options.onClick) {
      // If the anchor has an URL, open it in a new tab. If not, show the
      // current object actor.
      options.onClick = options.href ? this._onClickAnchor : this._onClick;
    }

    let anchor = this.el("a", {
      class: options.className,
      draggable: false,
      href: options.href || "#",
    }, text);

    this.message._addLinkCallback(anchor, options.onClick);

    if (options.appendTo) {
      options.appendTo.appendChild(anchor);
    } else if (!("appendTo" in options) && this.element) {
      this.element.appendChild(anchor);
    }

    return anchor;
  },

  /**
   * The click event handler for objects shown inline.
   * @private
   */
  _onClick: function()
  {
    this.output.openVariablesView({
      label: VariablesView.getString(this.objectActor, { concise: true }),
      objectActor: this.objectActor,
      autofocus: true,
    });
  },

  /**
   * Add a string to the message.
   *
   * @private
   * @param string str
   *        String to add.
   * @param DOMElement [target = this.element]
   *        Optional DOM element to append the string to. The default is
   *        this.element.
   */
  _text: function(str, target = this.element)
  {
    target.appendChild(this.document.createTextNode(str));
  },
}); // Widgets.JSObject.prototype

Widgets.ObjectRenderers = {};
Widgets.ObjectRenderers.byKind = {};
Widgets.ObjectRenderers.byClass = {};

/**
 * Add an object renderer.
 *
 * @param object obj
 *        An object that represents the renderer. Properties:
 *        - byClass (string, optional): this renderer will be used for the given
 *        object class.
 *        - byKind (string, optional): this renderer will be used for the given
 *        object kind.
 *        One of byClass or byKind must be provided.
 *        - extends (object, optional): the renderer object extends the given
 *        object. Default: Widgets.JSObject.
 *        - canRender (function, optional): this method is invoked when
 *        a candidate object needs to be displayed. The method is invoked as
 *        a static method, as such, none of the properties of the renderer
 *        object will be available. You get one argument: the object actor grip
 *        received from the server. If the method returns true, then this
 *        renderer is used for displaying the object, otherwise not.
 *        - initialize (function, optional): the constructor of the renderer
 *        widget. This function is invoked with the following arguments: the
 *        owner message object instance, the object actor grip to display, and
 *        an options object. See Messages.Extended.prototype._renderValueGrip()
 *        for details about the options object.
 *        - render (function, required): the method that displays the given
 *        object actor.
 */
Widgets.ObjectRenderers.add = function(obj)
{
  let extendObj = obj.extends || Widgets.JSObject;

  let constructor = function() {
    if (obj.initialize) {
      obj.initialize.apply(this, arguments);
    } else {
      extendObj.apply(this, arguments);
    }
  };

  let proto = WebConsoleUtils.cloneObject(obj, false, function(key) {
    if (key == "initialize" || key == "canRender" ||
        (key == "render" && extendObj === Widgets.JSObject)) {
      return false;
    }
    return true;
  });

  if (extendObj === Widgets.JSObject) {
    proto._render = obj.render;
  }

  constructor.canRender = obj.canRender;
  constructor.prototype = Heritage.extend(extendObj.prototype, proto);

  if (obj.byClass) {
    Widgets.ObjectRenderers.byClass[obj.byClass] = constructor;
  } else if (obj.byKind) {
    Widgets.ObjectRenderers.byKind[obj.byKind] = constructor;
  } else {
    throw new Error("You are adding an object renderer without any byClass or " +
                    "byKind property.");
  }
};


/**
 * The widget used for displaying Date objects.
 */
Widgets.ObjectRenderers.add({
  byClass: "Date",

  render: function()
  {
    let {preview} = this.objectActor;
    this.element = this.el("span.class-" + this.objectActor.class);

    let anchorText = this.objectActor.class;
    let anchorClass = "cm-variable";
    if (preview && "timestamp" in preview && typeof preview.timestamp != "number") {
      anchorText = new Date(preview.timestamp).toString(); // invalid date
      anchorClass = "";
    }

    this._anchor(anchorText, { className: anchorClass });

    if (!preview || !("timestamp" in preview) || typeof preview.timestamp != "number") {
      return;
    }

    this._text(" ");

    let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString());
    this.element.appendChild(elem);
  },
});

/**
 * The widget used for displaying Function objects.
 */
Widgets.ObjectRenderers.add({
  byClass: "Function",

  render: function()
  {
    let grip = this.objectActor;
    this.element = this.el("span.class-" + this.objectActor.class);

    // TODO: Bug 948484 - support arrow functions and ES6 generators
    let name = grip.userDisplayName || grip.displayName || grip.name || "";
    name = VariablesView.getString(name, { noStringQuotes: true });

    let str = this.options.concise ? name || "function " : "function " + name;

    if (this.options.concise) {
      this._anchor(name || "function", {
        className: name ? "cm-variable" : "cm-keyword",
      });
      if (!name) {
        this._text(" ");
      }
    } else if (name) {
      this.element.appendChild(this.el("span.cm-keyword", "function"));
      this._text(" ");
      this._anchor(name, { className: "cm-variable" });
    } else {
      this._anchor("function", { className: "cm-keyword" });
      this._text(" ");
    }

    this._text("(");

    // TODO: Bug 948489 - Support functions with destructured parameters and
    // rest parameters
    let params = grip.parameterNames || [];
    let shown = 0;
    for (let param of params) {
      if (shown > 0) {
        this._text(", ");
      }
      this.element.appendChild(this.el("span.cm-def", param));
      shown++;
    }

    this._text(")");
  },
}); // Widgets.ObjectRenderers.byClass.Function

/**
 * The widget used for displaying ArrayLike objects.
 */
Widgets.ObjectRenderers.add({
  byKind: "ArrayLike",

  render: function()
  {
    let {preview} = this.objectActor;
    let {items} = preview;
    this.element = this.el("span.kind-" + preview.kind);

    this._anchor(this.objectActor.class, { className: "cm-variable" });

    if (!items || this.options.concise) {
      this._text("[");
      this.element.appendChild(this.el("span.cm-number", preview.length));
      this._text("]");
      return this;
    }

    this._text(" [ ");

    let isFirst = true;
    let emptySlots = 0;
    // A helper that renders a comma between items if isFirst == false.
    let renderSeparator = () => !isFirst && this._text(", ");

    for (let item of items) {
      if (item === null) {
        emptySlots++;
      }
      else {
        renderSeparator();
        isFirst = false;

        if (emptySlots) {
          this._renderEmptySlots(emptySlots);
          emptySlots = 0;
        }

        let shortVal = this.message.shortenValueGrip(item);
        let elem = this.message._renderValueGrip(shortVal, { concise: true });
        this.element.appendChild(elem);
      }
    }

    if (emptySlots) {
      renderSeparator();
      this._renderEmptySlots(emptySlots, false);
    }

    let shown = items.length;
    if (shown < preview.length) {
      this._text(", ");

      let n = preview.length - shown;
      let str = VariablesView.stringifiers._getNMoreString(n);
      this._anchor(str);
    }

    this._text(" ]");
  },

  _renderEmptySlots: function(aNumSlots, aAppendComma=true) {
    let slotLabel = l10n.getStr("emptySlotLabel");
    let slotText = PluralForm.get(aNumSlots, slotLabel);
    this._text("<" + slotText.replace("#1", aNumSlots) + ">");
    if (aAppendComma) {
      this._text(", ");
    }
  },

}); // Widgets.ObjectRenderers.byKind.ArrayLike

/**
 * The widget used for displaying MapLike objects.
 */
Widgets.ObjectRenderers.add({
  byKind: "MapLike",

  render: function()
  {
    let {preview} = this.objectActor;
    let {entries} = preview;

    let container = this.element = this.el("span.kind-" + preview.kind);
    this._anchor(this.objectActor.class, { className: "cm-variable" });

    if (!entries || this.options.concise) {
      if (typeof preview.size == "number") {
        this._text("[");
        container.appendChild(this.el("span.cm-number", preview.size));
        this._text("]");
      }
      return;
    }

    this._text(" { ");

    let shown = 0;
    for (let [key, value] of entries) {
      if (shown > 0) {
        this._text(", ");
      }

      let keyElem = this.message._renderValueGrip(key, {
        concise: true,
        noStringQuotes: true,
      });

      // Strings are property names.
      if (keyElem.classList && keyElem.classList.contains("console-string")) {
        keyElem.classList.remove("console-string");
        keyElem.classList.add("cm-property");
      }

      container.appendChild(keyElem);

      this._text(": ");

      let valueElem = this.message._renderValueGrip(value, { concise: true });
      container.appendChild(valueElem);

      shown++;
    }

    if (typeof preview.size == "number" && shown < preview.size) {
      this._text(", ");

      let n = preview.size - shown;
      let str = VariablesView.stringifiers._getNMoreString(n);
      this._anchor(str);
    }

    this._text(" }");
  },
}); // Widgets.ObjectRenderers.byKind.MapLike

/**
 * The widget used for displaying objects with a URL.
 */
Widgets.ObjectRenderers.add({
  byKind: "ObjectWithURL",

  render: function()
  {
    this.element = this._renderElement(this.objectActor,
                                       this.objectActor.preview.url);
  },

  _renderElement: function(objectActor, url)
  {
    let container = this.el("span.kind-" + objectActor.preview.kind);

    this._anchor(objectActor.class, {
      className: "cm-variable",
      appendTo: container,
    });

    if (!VariablesView.isFalsy({ value: url })) {
      this._text(" \u2192 ", container);
      let shortUrl = WebConsoleUtils.abbreviateSourceURL(url, {
        onlyCropQuery: !this.options.concise
      });
      this._anchor(shortUrl, { href: url, appendTo: container });
    }

    return container;
  },
}); // Widgets.ObjectRenderers.byKind.ObjectWithURL

/**
 * The widget used for displaying objects with a string next to them.
 */
Widgets.ObjectRenderers.add({
  byKind: "ObjectWithText",

  render: function()
  {
    let {preview} = this.objectActor;
    this.element = this.el("span.kind-" + preview.kind);

    this._anchor(this.objectActor.class, { className: "cm-variable" });

    if (!this.options.concise) {
      this._text(" ");
      this.element.appendChild(this.el("span.console-string",
                                       VariablesView.getString(preview.text)));
    }
  },
});

/**
 * The widget used for displaying DOM event previews.
 */
Widgets.ObjectRenderers.add({
  byKind: "DOMEvent",

  render: function()
  {
    let {preview} = this.objectActor;

    let container = this.element = this.el("span.kind-" + preview.kind);

    this._anchor(preview.type || this.objectActor.class,
                 { className: "cm-variable" });

    if (this.options.concise) {
      return;
    }

    if (preview.eventKind == "key" && preview.modifiers &&
        preview.modifiers.length) {
      this._text(" ");

      let mods = 0;
      for (let mod of preview.modifiers) {
        if (mods > 0) {
          this._text("-");
        }
        container.appendChild(this.el("span.cm-keyword", mod));
        mods++;
      }
    }

    this._text(" { ");

    let shown = 0;
    if (preview.target) {
      container.appendChild(this.el("span.cm-property", "target"));
      this._text(": ");
      let target = this.message._renderValueGrip(preview.target, { concise: true });
      container.appendChild(target);
      shown++;
    }

    for (let key of Object.keys(preview.properties || {})) {
      if (shown > 0) {
        this._text(", ");
      }

      container.appendChild(this.el("span.cm-property", key));
      this._text(": ");

      let value = preview.properties[key];
      let valueElem = this.message._renderValueGrip(value, { concise: true });
      container.appendChild(valueElem);

      shown++;
    }

    this._text(" }");
  },
}); // Widgets.ObjectRenderers.byKind.DOMEvent

/**
 * The widget used for displaying DOM node previews.
 */
Widgets.ObjectRenderers.add({
  byKind: "DOMNode",

  canRender: function(objectActor) {
    let {preview} = objectActor;
    if (!preview) {
      return false;
    }

    switch (preview.nodeType) {
      case Ci.nsIDOMNode.DOCUMENT_NODE:
      case Ci.nsIDOMNode.ATTRIBUTE_NODE:
      case Ci.nsIDOMNode.TEXT_NODE:
      case Ci.nsIDOMNode.COMMENT_NODE:
      case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
      case Ci.nsIDOMNode.ELEMENT_NODE:
        return true;
      default:
        return false;
    }
  },

  render: function()
  {
    switch (this.objectActor.preview.nodeType) {
      case Ci.nsIDOMNode.DOCUMENT_NODE:
        this._renderDocumentNode();
        break;
      case Ci.nsIDOMNode.ATTRIBUTE_NODE: {
        let {preview} = this.objectActor;
        this.element = this.el("span.attributeNode.kind-" + preview.kind);
        let attr = this._renderAttributeNode(preview.nodeName, preview.value, true);
        this.element.appendChild(attr);
        break;
      }
      case Ci.nsIDOMNode.TEXT_NODE:
        this._renderTextNode();
        break;
      case Ci.nsIDOMNode.COMMENT_NODE:
        this._renderCommentNode();
        break;
      case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
        this._renderDocumentFragmentNode();
        break;
      case Ci.nsIDOMNode.ELEMENT_NODE:
        this._renderElementNode();
        break;
      default:
        throw new Error("Unsupported nodeType: " + preview.nodeType);
    }
  },

  _renderDocumentNode: function()
  {
    let fn = Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement;
    this.element = fn.call(this, this.objectActor,
                           this.objectActor.preview.location);
    this.element.classList.add("documentNode");
  },

  _renderAttributeNode: function(nodeName, nodeValue, addLink)
  {
    let value = VariablesView.getString(nodeValue, { noStringQuotes: true });

    let fragment = this.document.createDocumentFragment();
    if (addLink) {
      this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment });
    } else {
      fragment.appendChild(this.el("span.cm-attribute", nodeName));
    }

    this._text("=", fragment);
    fragment.appendChild(this.el("span.console-string",
                                 '"' + escapeHTML(value) + '"'));

    return fragment;
  },

  _renderTextNode: function()
  {
    let {preview} = this.objectActor;
    this.element = this.el("span.textNode.kind-" + preview.kind);

    this._anchor(preview.nodeName, { className: "cm-variable" });
    this._text(" ");

    let text = VariablesView.getString(preview.textContent);
    this.element.appendChild(this.el("span.console-string", text));
  },

  _renderCommentNode: function()
  {
    let {preview} = this.objectActor;
    let comment = "<!-- " + VariablesView.getString(preview.textContent, {
      noStringQuotes: true,
    }) + " -->";

    this.element = this._anchor(comment, {
      className: "kind-" + preview.kind + " commentNode cm-comment",
    });
  },

  _renderDocumentFragmentNode: function()
  {
    let {preview} = this.objectActor;
    let {childNodes} = preview;
    let container = this.element = this.el("span.documentFragmentNode.kind-" +
                                           preview.kind);

    this._anchor(this.objectActor.class, { className: "cm-variable" });

    if (!childNodes || this.options.concise) {
      this._text("[");
      container.appendChild(this.el("span.cm-number", preview.childNodesLength));
      this._text("]");
      return;
    }

    this._text(" [ ");

    let shown = 0;
    for (let item of childNodes) {
      if (shown > 0) {
        this._text(", ");
      }

      let elem = this.message._renderValueGrip(item, { concise: true });
      container.appendChild(elem);
      shown++;
    }

    if (shown < preview.childNodesLength) {
      this._text(", ");

      let n = preview.childNodesLength - shown;
      let str = VariablesView.stringifiers._getNMoreString(n);
      this._anchor(str);
    }

    this._text(" ]");
  },

  _renderElementNode: function()
  {
    let doc = this.document;
    let {attributes, nodeName} = this.objectActor.preview;

    this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode");

    let openTag = this.el("span.cm-tag");
    openTag.textContent = "<";
    this.element.appendChild(openTag);

    let tagName = this._anchor(nodeName, {
      className: "cm-tag",
      appendTo: openTag
    });

    if (this.options.concise) {
      if (attributes.id) {
        tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id));
      }
      if (attributes.class) {
        tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join(".")));
      }
    } else {
      for (let name of Object.keys(attributes)) {
        let attr = this._renderAttributeNode(" " + name, attributes[name]);
        this.element.appendChild(attr);
      }
    }

    let closeTag = this.el("span.cm-tag");
    closeTag.textContent = ">";
    this.element.appendChild(closeTag);

    // Register this widget in the owner message so that it gets destroyed when
    // the message is destroyed.
    this.message.widgets.add(this);

    this.linkToInspector().then(null, Cu.reportError);
  },

  /**
   * If the DOMNode being rendered can be highlit in the page, this function
   * will attach mouseover/out event listeners to do so, and the inspector icon
   * to open the node in the inspector.
   * @return a promise that resolves when the node has been linked to the
   * inspector, or rejects if it wasn't (either if no toolbox could be found to
   * access the inspector, or if the node isn't present in the inspector, i.e.
   * if the node is in a DocumentFragment or not part of the tree, or not of
   * type Ci.nsIDOMNode.ELEMENT_NODE).
   */
  linkToInspector: Task.async(function*()
  {
    if (this._linkedToInspector) {
      return;
    }

    // Checking the node type
    if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
      throw new Error("The object cannot be linked to the inspector as it " +
        "isn't an element node");
    }

    // Checking the presence of a toolbox
    let target = this.message.output.toolboxTarget;
    this.toolbox = gDevTools.getToolbox(target);
    if (!this.toolbox) {
      throw new Error("The object cannot be linked to the inspector without a " +
        "toolbox");
    }

    // Checking that the inspector supports the node
    yield this.toolbox.initInspector();
    this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor);
    if (!this._nodeFront) {
      throw new Error("The object cannot be linked to the inspector, the " +
        "corresponding nodeFront could not be found");
    }

    // At this stage, the message may have been cleared already
    if (!this.document) {
      throw new Error("The object cannot be linked to the inspector, the " +
        "message was got cleared away");
    }

    this.highlightDomNode = this.highlightDomNode.bind(this);
    this.element.addEventListener("mouseover", this.highlightDomNode, false);
    this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
    this.element.addEventListener("mouseout", this.unhighlightDomNode, false);

    this._openInspectorNode = this._anchor("", {
      className: "open-inspector",
      onClick: this.openNodeInInspector.bind(this)
    });
    this._openInspectorNode.title = l10n.getStr("openNodeInInspector");

    this._linkedToInspector = true;
  }),

  /**
   * Highlight the DOMNode corresponding to the ObjectActor in the page.
   * @return a promise that resolves when the node has been highlighted, or
   * rejects if the node cannot be highlighted (detached from the DOM)
   */
  highlightDomNode: Task.async(function*()
  {
    yield this.linkToInspector();
    let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
    if (isAttached) {
      yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
    } else {
      throw null;
    }
  }),

  /**
   * Unhighlight a previously highlit node
   * @see highlightDomNode
   * @return a promise that resolves when the highlighter has been hidden
   */
  unhighlightDomNode: function()
  {
    return this.linkToInspector().then(() => {
      return this.toolbox.highlighterUtils.unhighlight();
    }).then(null, Cu.reportError);
  },

  /**
   * Open the DOMNode corresponding to the ObjectActor in the inspector panel
   * @return a promise that resolves when the inspector has been switched to
   * and the node has been selected, or rejects if the node cannot be selected
   * (detached from the DOM). Note that in any case, the inspector panel will
   * be switched to.
   */
  openNodeInInspector: Task.async(function*()
  {
    yield this.linkToInspector();
    yield this.toolbox.selectTool("inspector");

    let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
    if (isAttached) {
      let onReady = this.toolbox.inspector.once("inspector-updated");
      yield this.toolbox.selection.setNodeFront(this._nodeFront, "console");
      yield onReady;
    } else {
      throw null;
    }
  }),

  destroy: function()
  {
    if (this.toolbox && this._nodeFront) {
      this.element.removeEventListener("mouseover", this.highlightDomNode, false);
      this.element.removeEventListener("mouseout", this.unhighlightDomNode, false);
      this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true);
      this.toolbox = null;
      this._nodeFront = null;
    }
  },
}); // Widgets.ObjectRenderers.byKind.DOMNode

/**
 * The widget user for displaying Promise objects.
 */
Widgets.ObjectRenderers.add({
  byClass: "Promise",

  render: function()
  {
    let { ownProperties, safeGetterValues } = this.objectActor.preview;
    if ((!ownProperties && !safeGetterValues) || this.options.concise) {
      this._renderConciseObject();
      return;
    }

    this._renderObjectPrefix();
    let container = this.element;
    let addedPromiseInternalProps = false;

    if (this.objectActor.promiseState) {
      const { state, value, reason } = this.objectActor.promiseState;

      this._renderObjectProperty("<state>", state, container, false);
      addedPromiseInternalProps = true;

      if (state == "fulfilled") {
        this._renderObjectProperty("<value>", value, container, true);
      } else if (state == "rejected") {
        this._renderObjectProperty("<reason>", reason, container, true);
      }
    }

    this._renderObjectProperties(container, addedPromiseInternalProps);
    this._renderObjectSuffix();
  }
}); // Widgets.ObjectRenderers.byClass.Promise

/**
 * The widget used for displaying generic JS object previews.
 */
Widgets.ObjectRenderers.add({
  byKind: "Object",

  render: function()
  {
    let { ownProperties, safeGetterValues } = this.objectActor.preview;
    if ((!ownProperties && !safeGetterValues) || this.options.concise) {
      this._renderConciseObject();
      return;
    }

    this._renderObjectPrefix();
    this._renderObjectProperties(this.element, false);
    this._renderObjectSuffix();
  },
}); // Widgets.ObjectRenderers.byKind.Object

/**
 * The long string widget.
 *
 * @constructor
 * @param object message
 *        The owning message.
 * @param object longStringActor
 *        The LongStringActor to display.
 */
Widgets.LongString = function(message, longStringActor)
{
  Widgets.BaseWidget.call(this, message);
  this.longStringActor = longStringActor;
  this._onClick = this._onClick.bind(this);
  this._onSubstring = this._onSubstring.bind(this);
};

Widgets.LongString.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
{
  /**
   * The LongStringActor displayed by the widget.
   * @type object
   */
  longStringActor: null,

  render: function()
  {
    if (this.element) {
      return this;
    }

    let result = this.element = this.document.createElementNS(XHTML_NS, "span");
    result.className = "longString console-string";
    this._renderString(this.longStringActor.initial);
    result.appendChild(this._renderEllipsis());

    return this;
  },

  /**
   * Render the long string in the widget element.
   * @private
   * @param string str
   *        The string to display.
   */
  _renderString: function(str)
  {
    this.element.textContent = VariablesView.getString(str, {
      noStringQuotes: !this.message._quoteStrings,
      noEllipsis: true,
    });
  },

  /**
   * Render the anchor ellipsis that allows the user to expand the long string.
   *
   * @private
   * @return Element
   */
  _renderEllipsis: function()
  {
    let ellipsis = this.document.createElementNS(XHTML_NS, "a");
    ellipsis.className = "longStringEllipsis";
    ellipsis.textContent = l10n.getStr("longStringEllipsis");
    ellipsis.href = "#";
    ellipsis.draggable = false;
    this.message._addLinkCallback(ellipsis, this._onClick);

    return ellipsis;
  },

  /**
   * The click event handler for the ellipsis shown after the short string. This
   * function expands the element to show the full string.
   * @private
   */
  _onClick: function()
  {
    let longString = this.output.webConsoleClient.longString(this.longStringActor);
    let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH);

    longString.substring(longString.initial.length, toIndex, this._onSubstring);
  },

  /**
   * The longString substring response callback.
   *
   * @private
   * @param object response
   *        Response packet.
   */
  _onSubstring: function(response)
  {
    if (response.error) {
      Cu.reportError("LongString substring failure: " + response.error);
      return;
    }

    this.element.lastChild.remove();
    this.element.classList.remove("longString");

    this._renderString(this.longStringActor.initial + response.substring);

    this.output.owner.emit("new-messages", new Set([{
      update: true,
      node: this.message.element,
      response: response,
    }]));

    let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH);
    if (toIndex != this.longStringActor.length) {
      this._logWarningAboutStringTooLong();
    }
  },

  /**
   * Inform user that the string he tries to view is too long.
   * @private
   */
  _logWarningAboutStringTooLong: function()
  {
    let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), {
      category: "output",
      severity: "warning",
    });
    this.output.addMessage(msg);
  },
}); // Widgets.LongString.prototype


/**
 * The stacktrace widget.
 *
 * @constructor
 * @extends Widgets.BaseWidget
 * @param object message
 *        The owning message.
 * @param array stacktrace
 *        The stacktrace to display, array of frames as supplied by the server,
 *        over the remote protocol.
 */
Widgets.Stacktrace = function(message, stacktrace)
{
  Widgets.BaseWidget.call(this, message);
  this.stacktrace = stacktrace;
};

Widgets.Stacktrace.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
{
  /**
   * The stackframes received from the server.
   * @type array
   */
  stacktrace: null,

  render: function()
  {
    if (this.element) {
      return this;
    }

    let result = this.element = this.document.createElementNS(XHTML_NS, "ul");
    result.className = "stacktrace devtools-monospace";

    for (let frame of this.stacktrace) {
      result.appendChild(this._renderFrame(frame));
    }

    return this;
  },

  /**
   * Render a frame object received from the server.
   *
   * @param object frame
   *        The stack frame to display. This object should have the following
   *        properties: functionName, filename and lineNumber.
   * @return DOMElement
   *         The DOM element to display for the given frame.
   */
  _renderFrame: function(frame)
  {
    let fn = this.document.createElementNS(XHTML_NS, "span");
    fn.className = "function";
    if (frame.functionName) {
      let span = this.document.createElementNS(XHTML_NS, "span");
      span.className = "cm-variable";
      span.textContent = frame.functionName;
      fn.appendChild(span);
      fn.appendChild(this.document.createTextNode("()"));
    } else {
      fn.classList.add("cm-comment");
      fn.textContent = l10n.getStr("stacktrace.anonymousFunction");
    }

    let location = this.output.owner.createLocationNode({url: frame.filename,
                                                        line: frame.lineNumber},
                                                        "jsdebugger");

    // .devtools-monospace sets font-size to 80%, however .body already has
    // .devtools-monospace. If we keep it here, the location would be rendered
    // smaller.
    location.classList.remove("devtools-monospace");

    let elem = this.document.createElementNS(XHTML_NS, "li");
    elem.appendChild(fn);
    elem.appendChild(location);
    elem.appendChild(this.document.createTextNode("\n"));

    return elem;
  },
}); // Widgets.Stacktrace.prototype


/**
 * The table widget.
 *
 * @constructor
 * @extends Widgets.BaseWidget
 * @param object message
 *        The owning message.
 * @param array data
 *        Array of objects that holds the data to log in the table.
 * @param object columns
 *        Object containing the key value pair of the id and display name for
 *        the columns in the table.
 */
Widgets.Table = function(message, data, columns)
{
  Widgets.BaseWidget.call(this, message);
  this.data = data;
  this.columns = columns;
};

Widgets.Table.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
{
  /**
   * Array of objects that holds the data to output in the table.
   * @type array
   */
  data: null,

  /**
   * Object containing the key value pair of the id and display name for
   * the columns in the table.
   * @type object
   */
  columns: null,

  render: function() {
    if (this.element) {
      return this;
    }

    let result = this.element = this.document.createElementNS(XHTML_NS, "div");
    result.className = "consoletable devtools-monospace";

    this.table = new TableWidget(result, {
      initialColumns: this.columns,
      uniqueId: "_index",
      firstColumn: "_index"
    });

    for (let row of this.data) {
      this.table.push(row);
    }

    return this;
  }
}); // Widgets.Table.prototype

function gSequenceId()
{
  return gSequenceId.n++;
}
gSequenceId.n = 0;

exports.ConsoleOutput = ConsoleOutput;
exports.Messages = Messages;
exports.Widgets = Widgets;
