Bug 518606 - Improve about:support's "Copy text to clipboard" output. r=unfocused

This commit is contained in:
Drew Willcoxon
2013-05-20 20:05:53 -07:00
parent 58d056100d
commit 06badf1caf
2 changed files with 154 additions and 45 deletions

View File

@@ -327,15 +327,8 @@ function copyContentsToClipboard() {
// 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
@@ -345,44 +338,160 @@ function createTextForElement(elem) {
return text;
}
function generateTextForElement(elem, indent, textFragmentAccumulator) {
if (elem.classList.contains("no-copy"))
return;
// Add a little extra spacing around most elements.
if (elem.tagName != "td")
textFragmentAccumulator.push("\n");
// Generate the text representation for each child node.
let node = elem.firstChild;
while (node) {
if (node.nodeType == Node.TEXT_NODE) {
// Text belonging to this element uses its indentation level.
generateTextForTextNode(node, indent, textFragmentAccumulator);
}
else if (node.nodeType == Node.ELEMENT_NODE) {
// Recurse on the child element with an extra level of indentation.
generateTextForElement(node, indent + " ", textFragmentAccumulator);
}
// Advance!
node = node.nextSibling;
}
function Serializer() {
}
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);
Serializer.prototype = {
// 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);
}
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;
},
_serializeElement: function (elem) {
if (this._ignoreElement(elem))
return;
// table
if (elem.localName == "table") {
this._serializeTable(elem);
return;
}
// all other elements
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);
}
// 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));
}
}
// 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;
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;
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.