Bug 518606 - Improve about:support's "Copy text to clipboard" output. r=unfocused
authorDrew Willcoxon <adw@mozilla.com>
Mon, 20 May 2013 20:05:53 -0700
changeset 139601 7b4e9af7376413cd5a17503c2f811c986eb98525
parent 139600 1cd3b5a8c5a8155f1ccf04fe3949353e60f7f332
child 139602 7080277aa570a9e7bbef4cf4cfb2ce4b17e713df
push id3911
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 20:17:26 +0000
treeherdermozilla-aurora@7e26ca8db92b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersunfocused
bugs518606
milestone24.0a1
Bug 518606 - Improve about:support's "Copy text to clipboard" output. r=unfocused
toolkit/content/aboutSupport.js
toolkit/content/aboutSupport.xhtml
--- a/toolkit/content/aboutSupport.js
+++ b/toolkit/content/aboutSupport.js
@@ -322,72 +322,181 @@ function copyContentsToClipboard() {
     getService(Ci.nsIAndroidBridge).
     handleGeckoMessage(JSON.stringify(message));
 #endif
 }
 
 // Return the plain text representation of an element.  Do a little bit
 // of pretty-printing to make it human-readable.
 function createTextForElement(elem) {
-  // Generate the initial text.
-  let textFragmentAccumulator = [];
-  generateTextForElement(elem, "", textFragmentAccumulator);
-  let text = textFragmentAccumulator.join("");
-
-  // Trim extraneous whitespace before newlines, then squash extraneous
-  // blank lines.
-  text = text.replace(/[ \t]+\n/g, "\n");
-  text = text.replace(/\n\n\n+/g, "\n\n");
+  let serializer = new Serializer();
+  let text = serializer.serialize(elem);
 
   // Actual CR/LF pairs are needed for some Windows text editors.
 #ifdef XP_WIN
   text = text.replace(/\n/g, "\r\n");
 #endif
 
   return text;
 }
 
-function generateTextForElement(elem, indent, textFragmentAccumulator) {
-  if (elem.classList.contains("no-copy"))
-    return;
+function Serializer() {
+}
+
+Serializer.prototype = {
 
-  // Add a little extra spacing around most elements.
-  if (elem.tagName != "td")
-    textFragmentAccumulator.push("\n");
+  serialize: function (rootElem) {
+    this._lines = [];
+    this._startNewLine();
+    this._serializeElement(rootElem);
+    this._startNewLine();
+    return this._lines.join("\n").trim() + "\n";
+  },
+
+  // The current line is always the line that writing will start at next.  When
+  // an element is serialized, the current line is updated to be the line at
+  // which the next element should be written.
+  get _currentLine() {
+    return this._lines.length ? this._lines[this._lines.length - 1] : null;
+  },
+
+  set _currentLine(val) {
+    return this._lines[this._lines.length - 1] = val;
+  },
 
-  // Generate the text representation for each child node.
-  let node = elem.firstChild;
-  while (node) {
+  _serializeElement: function (elem) {
+    if (this._ignoreElement(elem))
+      return;
+
+    // table
+    if (elem.localName == "table") {
+      this._serializeTable(elem);
+      return;
+    }
+
+    // all other elements
 
-    if (node.nodeType == Node.TEXT_NODE) {
-      // Text belonging to this element uses its indentation level.
-      generateTextForTextNode(node, indent, textFragmentAccumulator);
+    let hasText = false;
+    for (let child of elem.childNodes) {
+      if (child.nodeType == Node.TEXT_NODE) {
+        let text = this._nodeText(child);
+        this._appendText(text);
+        hasText = hasText || !!text.trim();
+      }
+      else if (child.nodeType == Node.ELEMENT_NODE)
+        this._serializeElement(child);
     }
-    else if (node.nodeType == Node.ELEMENT_NODE) {
-      // Recurse on the child element with an extra level of indentation.
-      generateTextForElement(node, indent + "  ", textFragmentAccumulator);
+
+    // For headings, draw a "line" underneath them so they stand out.
+    if (/^h[0-9]+$/.test(elem.localName)) {
+      let headerText = (this._currentLine || "").trim();
+      if (headerText) {
+        this._startNewLine();
+        this._appendText("-".repeat(headerText.length));
+      }
     }
 
-    // Advance!
-    node = node.nextSibling;
-  }
-}
+    // Add a blank line underneath block elements but only if they contain text.
+    if (hasText) {
+      let display = window.getComputedStyle(elem).getPropertyValue("display");
+      if (display == "block") {
+        this._startNewLine();
+        this._startNewLine();
+      }
+    }
+  },
+
+  _startNewLine: function (lines) {
+    let currLine = this._currentLine;
+    if (currLine) {
+      // The current line is not empty.  Trim it.
+      this._currentLine = currLine.trim();
+      if (!this._currentLine)
+        // The current line became empty.  Discard it.
+        this._lines.pop();
+    }
+    this._lines.push("");
+  },
+
+  _appendText: function (text, lines) {
+    this._currentLine += text;
+  },
+
+  _serializeTable: function (table) {
+    // Collect the table's column headings if in fact there are any.  First
+    // check thead.  If there's no thead, check the first tr.
+    let colHeadings = {};
+    let tableHeadingElem = table.querySelector("thead");
+    if (!tableHeadingElem)
+      tableHeadingElem = table.querySelector("tr");
+    if (tableHeadingElem) {
+      let tableHeadingCols = tableHeadingElem.querySelectorAll("th,td");
+      // If there's a contiguous run of th's in the children starting from the
+      // rightmost child, then consider them to be column headings.
+      for (let i = tableHeadingCols.length - 1; i >= 0; i--) {
+        if (tableHeadingCols[i].localName != "th")
+          break;
+        colHeadings[i] = this._nodeText(tableHeadingCols[i]).trim();
+      }
+    }
+    let hasColHeadings = Object.keys(colHeadings).length > 0;
+    if (!hasColHeadings)
+      tableHeadingElem = null;
 
-function generateTextForTextNode(node, indent, textFragmentAccumulator) {
-  // If the text node is the first of a run of text nodes, then start
-  // a new line and add the initial indentation.
-  let prevNode = node.previousSibling;
-  if (!prevNode || prevNode.nodeType == Node.TEXT_NODE)
-    textFragmentAccumulator.push("\n" + indent);
+    let trs = table.querySelectorAll("table > tr, tbody > tr");
+    let startRow =
+      tableHeadingElem && tableHeadingElem.localName == "tr" ? 1 : 0;
+
+    if (startRow >= trs.length)
+      // The table's empty.
+      return;
 
-  // Trim the text node's text content and add proper indentation after
-  // any internal line breaks.
-  let text = node.textContent.trim().replace("\n", "\n" + indent, "g");
-  textFragmentAccumulator.push(text);
-}
+    if (hasColHeadings && !this._ignoreElement(tableHeadingElem)) {
+      // Use column headings.  Print each tr as a multi-line chunk like:
+      //   Heading 1: Column 1 value
+      //   Heading 2: Column 2 value
+      for (let i = startRow; i < trs.length; i++) {
+        if (this._ignoreElement(trs[i]))
+          continue;
+        let children = trs[i].querySelectorAll("td");
+        for (let j = 0; j < children.length; j++) {
+          let text = "";
+          if (colHeadings[j])
+            text += colHeadings[j] + ": ";
+          text += this._nodeText(children[j]).trim();
+          this._appendText(text);
+          this._startNewLine();
+        }
+        this._startNewLine();
+      }
+      return;
+    }
+
+    // Don't use column headings.  Assume the table has only two columns and
+    // print each tr in a single line like:
+    //   Column 1 value: Column 2 value
+    for (let i = startRow; i < trs.length; i++) {
+      if (this._ignoreElement(trs[i]))
+        continue;
+      let children = trs[i].querySelectorAll("th,td");
+      let rowHeading = this._nodeText(children[0]).trim();
+      this._appendText(rowHeading + ": " + this._nodeText(children[1]).trim());
+      this._startNewLine();
+    }
+    this._startNewLine();
+  },
+
+  _ignoreElement: function (elem) {
+    return elem.classList.contains("no-copy");
+  },
+
+  _nodeText: function (node) {
+    return node.textContent.replace(/\s+/g, " ");
+  },
+};
 
 function openProfileDirectory() {
   // Get the profile directory.
   let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile);
   let profileDir = currProfD.path;
 
   // Show the profile directory.
   let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1",
--- a/toolkit/content/aboutSupport.xhtml
+++ b/toolkit/content/aboutSupport.xhtml
@@ -190,17 +190,17 @@
 
       <!-- - - - - - - - - - - - - - - - - - - - - -->
 
       <h2 class="major-section">
         &aboutSupport.modifiedKeyPrefsTitle;
       </h2>
 
       <table class="prefs-table">
-        <thead>
+        <thead class="no-copy">
           <th class="name">
             &aboutSupport.modifiedPrefsName;
           </th>
 
           <th class="value">
             &aboutSupport.modifiedPrefsValue;
           </th>
         </thead>