remote/shared/Format.jsm
author Norisz Fay <nfay@mozilla.com>
Fri, 27 May 2022 12:25:02 +0300
changeset 619043 914ead848de431dcd0f42c2fda013018704b29b9
parent 586759 ecafeeb0aa817650430322cd33828e3b0320ab19
permissions -rw-r--r--
Merge autoland to mozilla-central a=merge

/* 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";

var EXPORTED_SYMBOLS = ["pprint", "truncate"];

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);

XPCOMUtils.defineLazyModuleGetters(this, {
  Services: "resource://gre/modules/Services.jsm",

  Log: "chrome://remote/content/shared/Log.jsm",
});

XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());

const ELEMENT_NODE = 1;
const MAX_STRING_LENGTH = 250;

const PREF_TRUNCATE = "remote.log.truncate";

/**
 * Pretty-print values passed to template strings.
 *
 * Usage::
 *
 *     let bool = {value: true};
 *     pprint`Expected boolean, got ${bool}`;
 *     => 'Expected boolean, got [object Object] {"value": true}'
 *
 *     let htmlElement = document.querySelector("input#foo");
 *     pprint`Expected element ${htmlElement}`;
 *     => 'Expected element <input id="foo" class="bar baz" type="input">'
 *
 *     pprint`Current window: ${window}`;
 *     => '[object Window https://www.mozilla.org/]'
 */
function pprint(ss, ...values) {
  function pretty(val) {
    let proto = Object.prototype.toString.call(val);
    if (
      typeof val == "object" &&
      val !== null &&
      "nodeType" in val &&
      val.nodeType === ELEMENT_NODE
    ) {
      return prettyElement(val);
    } else if (["[object Window]", "[object ChromeWindow]"].includes(proto)) {
      return prettyWindowGlobal(val);
    } else if (proto == "[object Attr]") {
      return prettyAttr(val);
    }
    return prettyObject(val);
  }

  function prettyElement(el) {
    let attrs = ["id", "class", "href", "name", "src", "type"];

    let idents = "";
    for (let attr of attrs) {
      if (el.hasAttribute(attr)) {
        idents += ` ${attr}="${el.getAttribute(attr)}"`;
      }
    }

    return `<${el.localName}${idents}>`;
  }

  function prettyWindowGlobal(win) {
    let proto = Object.prototype.toString.call(win);
    return `[${proto.substring(1, proto.length - 1)} ${win.location}]`;
  }

  function prettyAttr(obj) {
    return `[object Attr ${obj.name}="${obj.value}"]`;
  }

  function prettyObject(obj) {
    let proto = Object.prototype.toString.call(obj);
    let s = "";
    try {
      s = JSON.stringify(obj);
    } catch (e) {
      if (e instanceof TypeError) {
        s = `<${e.message}>`;
      } else {
        throw e;
      }
    }
    return `${proto} ${s}`;
  }

  let res = [];
  for (let i = 0; i < ss.length; i++) {
    res.push(ss[i]);
    if (i < values.length) {
      let s;
      try {
        s = pretty(values[i]);
      } catch (e) {
        logger.warn("Problem pretty printing:", e);
        s = typeof values[i];
      }
      res.push(s);
    }
  }
  return res.join("");
}

/**
 * Template literal that truncates string values in arbitrary objects.
 *
 * Given any object, the template will walk the object and truncate
 * any strings it comes across to a reasonable limit.  This is suitable
 * when you have arbitrary data and data integrity is not important.
 *
 * The strings are truncated in the middle so that the beginning and
 * the end is preserved.  This will make a long, truncated string look
 * like "X <...> Y", where X and Y are half the number of characters
 * of the maximum string length from either side of the string.
 *
 *
 * Usage::
 *
 *     truncate`Hello ${"x".repeat(260)}!`;
 *     // Hello xxx ... xxx!
 *
 * Functions named `toJSON` or `toString` on objects will be called.
 */
function truncate(strings, ...values) {
  const truncateLog = Services.prefs.getBoolPref(PREF_TRUNCATE, false);
  function walk(obj) {
    const typ = Object.prototype.toString.call(obj);

    switch (typ) {
      case "[object Undefined]":
      case "[object Null]":
      case "[object Boolean]":
      case "[object Number]":
        return obj;

      case "[object String]":
        if (truncateLog && obj.length > MAX_STRING_LENGTH) {
          let s1 = obj.substring(0, MAX_STRING_LENGTH / 2);
          let s2 = obj.substring(obj.length - MAX_STRING_LENGTH / 2);
          return `${s1} ... ${s2}`;
        }
        return obj;

      case "[object Array]":
        return obj.map(walk);

      // arbitrary object
      default:
        if (
          Object.getOwnPropertyNames(obj).includes("toString") &&
          typeof obj.toString == "function"
        ) {
          return walk(obj.toString());
        }

        let rv = {};
        for (let prop in obj) {
          rv[prop] = walk(obj[prop]);
        }
        return rv;
    }
  }

  let res = [];
  for (let i = 0; i < strings.length; ++i) {
    res.push(strings[i]);
    if (i < values.length) {
      let obj = walk(values[i]);
      let t = Object.prototype.toString.call(obj);
      if (t == "[object Array]" || t == "[object Object]") {
        res.push(JSON.stringify(obj));
      } else {
        res.push(obj);
      }
    }
  }
  return res.join("");
}