summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2015-10-13 23:29:41 +0100
committerMichael Williamson <mike@zwobble.org>2015-10-13 23:49:31 +0100
commit7b7ce0d563ddc07172b0f241a5c3baf344f256e8 (patch)
tree43f5fddf920d47e281a30139bde99da5a689d667
parente04d054a1b414e843fa6d6f5ee1ae75c0df533b1 (diff)
Collapse similar HTML elements
-rw-r--r--lib/document-to-html.js283
-rw-r--r--lib/html/ast.js31
-rw-r--r--lib/html/index.js38
-rw-r--r--lib/html/simplify.js43
-rw-r--r--lib/images.js10
-rw-r--r--lib/underline.js3
-rw-r--r--tests/document-to-html.tests.js27
-rw-r--r--tests/html/simplify.tests.js16
-rw-r--r--tests/mammoth.tests.js6
9 files changed, 254 insertions, 203 deletions
diff --git a/lib/document-to-html.js b/lib/document-to-html.js
index 8a7cec0..eef0f8e 100644
--- a/lib/document-to-html.js
+++ b/lib/document-to-html.js
@@ -4,9 +4,10 @@ var _ = require("underscore");
var promises = require("./promises");
var documents = require("./documents");
var htmlPaths = require("./html-paths");
-var HtmlGenerator = require("./html-generator").HtmlGenerator;
var results = require("./results");
var images = require("./images");
+var Html = require("./html");
+var writers = require("./writers");
exports.DocumentConverter = DocumentConverter;
@@ -42,48 +43,44 @@ function DocumentConversion(options) {
var styleMap = options.styleMap || [];
- function createHtmlGenerator() {
- return new HtmlGenerator({
- prettyPrint: options.prettyPrint,
- outputFormat: options.outputFormat
- });
- }
-
function convertToHtml(document) {
var messages = [];
- var html = createHtmlGenerator();
- return promises.nfcall(elementToHtml, document, html, messages).then(function() {
- html.closeAll();
-
- return new results.Result(html.asString(), messages);
+ return promises.nfcall(elementToHtml, document, messages).then(function(html) {
+ var writer = writers.writer({
+ prettyPrint: options.prettyPrint,
+ outputFormat: options.outputFormat
+ });
+ Html.write(writer, Html.simplify(html));
+ return new results.Result(writer.asString(), messages);
});
}
- function convertElements(elements, html, messages, callback) {
- async.eachSeries(elements, function(element, callback) {
- return elementToHtml(element, html, messages, callback);
+ function convertElements(elements, messages, callback) {
+ async.mapSeries(elements, function(element, callback) {
+ return elementToHtml(element, messages, callback);
}, callback);
}
- function elementToHtml(element, html, messages, callback) {
+ function elementToHtml(element, messages, callback) {
var handler = elementConverters[element.type];
if (handler) {
- handler(element, html, messages, callback);
+ handler(element, messages, callback);
} else {
callback();
}
}
- function convertParagraph(element, html, messages, callback) {
- html.satisfyPath(styleForParagraph(element, messages));
-
- if (!options.ignoreEmptyParagraphs) {
- html.writeAll();
- }
+ function convertParagraph(element, messages, callback) {
+ var style = styleForParagraph(element, messages);
setImmediate(function() {
- convertElements(element.children, html, messages, callback);
+ convertElements(element.children, messages, function(error, children) {
+ if (!options.ignoreEmptyParagraphs) {
+ children = [Html.forceWrite, Html.fragment(children)];
+ }
+ callback(null, Html.pathToNode(style, children));
+ });
});
}
@@ -100,62 +97,63 @@ function DocumentConversion(options) {
}
}
- function convertRun(run, html, messages, callback) {
- var runHtml = createHtmlGenerator();
+ function convertRun(run, messages, callback) {
if (run.styleId) {
var style = findStyle(run);
- if (style) {
- runHtml.satisfyPath(style.to);
- } else {
+ if (!style) {
messages.push(unrecognisedStyleWarning("run", run));
}
}
- if (run.isBold) {
- runHtml.open(htmlPaths.element("strong"));
- }
- if (run.isItalic) {
- runHtml.open(htmlPaths.element("em"));
- }
- if (run.verticalAlignment === documents.verticalAlignment.superscript) {
- runHtml.open(htmlPaths.element("sup"));
- }
- if (run.verticalAlignment === documents.verticalAlignment.subscript) {
- runHtml.open(htmlPaths.element("sub"));
- }
- if (run.isUnderline) {
- convertUnderline(runHtml);
- }
- if (run.isStrikethrough) {
- convertStrikethrough(runHtml);
- }
- convertElements(run.children, runHtml, messages, function(err) {
- if (err) {
- callback(err);
- } else {
- runHtml.closeAll();
- html.append(runHtml);
- callback();
+ convertElements(run.children, messages, function(error, children) {
+ var result = Html.fragment(children);
+ if (run.isStrikethrough) {
+ result = convertStrikethrough(result);
+ }
+ if (run.isUnderline) {
+ result = convertUnderline(result);
+ }
+ if (run.verticalAlignment === documents.verticalAlignment.subscript) {
+ result = wrap(result, "sub");
+ }
+ if (run.verticalAlignment === documents.verticalAlignment.superscript) {
+ result = wrap(result, "sup");
+ }
+ if (run.isItalic) {
+ result = wrap(result, "em");
}
+ if (run.isBold) {
+ result = wrap(result, "strong");
+ }
+ if (style) {
+ result = Html.pathToNode(style.to, [result]);
+ }
+ callback(null, result);
});
}
+ function wrap(node, tagName) {
+ return Html.elementWithTag(htmlPaths.element(tagName, {}, {fresh: false}), [node]);
+ }
+
function convertUnderline(runHtml) {
- (options.convertUnderline || defaultConvertUnderline)(runHtml);
+ return (options.convertUnderline || defaultConvertUnderline)(runHtml);
}
- function convertStrikethrough(runHtml) {
+ function convertStrikethrough(result) {
var style = findStyle({type: "strikethrough"});
if (style) {
- runHtml.appendPath(style.to);
+ return Html.pathToNode(style.to, [result]);
} else {
- runHtml.open(htmlPaths.element("s"));
+ return wrap(result, "s");
}
}
function defaultConvertUnderline(runHtml) {
var style = findStyle({type: "underline"});
if (style) {
- runHtml.appendPath(style.to);
+ return Html.pathToNode(style.to, [runHtml]);
+ } else {
+ return runHtml;
}
}
@@ -176,12 +174,14 @@ function DocumentConversion(options) {
});
function recoveringConvertImage(convertImage) {
- return function(image, html, messages, callback) {
- convertImage(image, html, messages, function(error) {
+ return function(image, messages, callback) {
+ convertImage(image, messages, function(error, result) {
if (error) {
messages.push(results.warning(error.message));
+ callback(null, Html.fragment());
+ } else {
+ callback(null, result);
}
- callback();
});
};
}
@@ -198,141 +198,88 @@ function DocumentConversion(options) {
return idPrefix + "-" + suffix;
}
- function convertTable(element, html, messages, callback) {
- html.closeAll();
- wrapChildren(element, "table", html, messages, callback);
+ function convertTable(element, messages, callback) {
+ wrapChildren(element, "table", messages, callback);
}
- function convertTableRow(element, html, messages, callback) {
- wrapChildren(element, "tr", html, messages, callback);
+ function convertTableRow(element, messages, callback) {
+ wrapChildren(element, "tr", messages, callback);
}
- function convertTableCell(element, html, messages, callback) {
- html.open(htmlPaths.element("td"), {alwaysWrite: true});
-
- async.map(element.children, function(child, callback) {
- var cellHtml = createHtmlGenerator();
- elementToHtml(child, cellHtml, messages, function(error) {
- if (error) {
- callback(error);
- } else {
- cellHtml.closeAll();
- callback(null, cellHtml);
- }
- });
- }, function(error, childrenHtml) {
- if (error) {
- callback(error);
- } else {
- childrenHtml.forEach(function(childHtml) {
- html.append(childHtml);
- });
- html.close();
- callback();
- }
- });
+ function convertTableCell(element, messages, callback) {
+ wrapChildren(element, "td", messages, callback);
}
- function wrapChildren(element, wrapElementName, html, messages, callback) {
- html.open(htmlPaths.element(wrapElementName));
- convertElements(element.children, html, messages, function(err) {
- if (err) {
- callback(err);
- } else {
- html.close();
- callback();
- }
+ function wrapChildren(element, wrapElementName, messages, callback) {
+ convertElements(element.children, messages, function(error, children) {
+ callback(null, Html.element(wrapElementName, {}, [
+ Html.forceWrite,
+ Html.fragment(children)]));
});
}
var elementConverters = {
- "document": function(document, html, messages, callback) {
- convertElements(document.children, html, messages, function(error) {
- if (error) {
- callback(error);
- } else {
- html.closeAll();
- html.open(htmlPaths.element("ol"));
- var notes = noteReferences.map(function(noteReference) {
- return document.notes.resolve(noteReference);
- });
- convertElements(notes, html, messages, function(error) {
- if (error) {
- callback(error);
- } else {
- html.close();
- callback();
- }
- });
- }
+ "document": function(document, messages, callback) {
+ convertElements(document.children, messages, function(error, children) {
+ var notes = noteReferences.map(function(noteReference) {
+ return document.notes.resolve(noteReference);
+ });
+ convertElements(notes, messages, function(error, notesNodes) {
+ callback(null, Html.fragment([
+ Html.fragment(children),
+ Html.element("ol", {}, notesNodes)]));
+ });
});
},
"paragraph": convertParagraph,
"run": convertRun,
- "text": function(element, html, messages, callback) {
- html.text(element.value);
- callback();
+ "text": function(element, messages, callback) {
+ callback(null, Html.text(element.value));
},
- "tab": function(element, html, messages, callback) {
- html.text("\t");
- callback();
+ "tab": function(element, messages, callback) {
+ callback(null, Html.text("\t"));
},
- "hyperlink": function(element, html, messages, callback) {
+ "hyperlink": function(element, messages, callback) {
var href = element.anchor ? "#" + htmlId(element.anchor) : element.href;
- html.open(htmlPaths.element("a", {href: href}));
- convertElements(element.children, html, messages, function(err) {
- if (err) {
- callback(err);
- } else {
- html.close();
- callback();
- }
+ convertElements(element.children, messages, function(error, children) {
+ callback(null, Html.element("a", {href: href}, children));
});
},
- "bookmarkStart": function(element, html, messages, callback) {
- html.open(htmlPaths.element("a", {
+ "bookmarkStart": function(element, messages, callback) {
+ callback(null, Html.element("a", {
id: htmlId(element.name)
- }), {alwaysWrite: true});
- html.close();
- callback();
+ }, [Html.forceWrite]));
},
- "noteReference": function(element, html, messages, callback) {
- html.open(htmlPaths.element("sup"));
- html.open(htmlPaths.element("a", {
- href: "#" + noteHtmlId(element),
- id: noteRefHtmlId(element)
- }));
+ "noteReference": function(element, messages, callback) {
noteReferences.push(element);
- html.text("[" + (noteNumber++) + "]");
- html.close();
- html.close();
- callback();
+
+ callback(null, Html.element("sup", {}, [
+ Html.element("a", {
+ href: "#" + noteHtmlId(element),
+ id: noteRefHtmlId(element)
+ }, [Html.text("[" + (noteNumber++) + "]")])
+ ]));
},
- "note": function(element, html, messages, callback) {
- html.open(htmlPaths.element("li", {id: noteHtmlId(element)}));
- var footnoteHtml = createHtmlGenerator();
- convertElements(element.body, footnoteHtml, messages, function(err) {
- if (err) {
- callback(err);
- } else {
- footnoteHtml.text(" ");
- footnoteHtml.open(htmlPaths.element("a", {href: "#" + noteRefHtmlId(element)}));
- footnoteHtml.text("↑");
- footnoteHtml.closeAll();
- html.append(footnoteHtml);
- html.close();
- callback();
- }
+ "note": function(element, messages, callback) {
+ convertElements(element.body, messages, function(error, children) {
+ callback(null, Html.element("li", {id: noteHtmlId(element)}, [
+ Html.fragment(children),
+
+ Html.elementWithTag(htmlPaths.element("p", {}, {fresh: false}), [
+ Html.text(" "),
+ Html.element("a", {href: "#" + noteRefHtmlId(element)}, [Html.text("↑")]),
+
+ ])
+ ]));
});
},
"image": recoveringConvertImage(options.convertImage || defaultConvertImage),
"table": convertTable,
"tableRow": convertTableRow,
"tableCell": convertTableCell,
- "lineBreak": function(element, html, messages, callback) {
- html.selfClosing(htmlPaths.element("br"));
- callback();
+ "lineBreak": function(element, messages, callback) {
+ callback(null, Html.selfClosingElement("br"));
}
};
return {
diff --git a/lib/html/ast.js b/lib/html/ast.js
index afecfce..96aa6c6 100644
--- a/lib/html/ast.js
+++ b/lib/html/ast.js
@@ -20,24 +20,49 @@ function elementWithTag(tag, children) {
return {
type: "element",
tag: tag,
- children: collapseFragments(children)
+ children: collapseFragments(children),
+ isEmpty: _.all(children, function(child) {
+ return child.isEmpty;
+ }),
+ appendChild: appendChild
+ };
+}
+
+function appendChild(child) {
+ this.children.push(child);
+ this.isEmpty = this.isEmpty && child.isEmpty;
+}
+
+function selfClosingElement(tagName, attributes) {
+ return {
+ type: "selfClosingElement",
+ tagName: tagName,
+ attributes: attributes
};
}
function text(value) {
return {
type: "text",
- value: value
+ value: value,
+ isEmpty: !value
};
}
+var forceWrite = {
+ type: "forceWrite",
+ isEmpty: false
+};
+
function collapseFragments(elements) {
return _.flatten((elements || []).map(function(element) {
return element.type === "fragment" ? element.children : [element];
- }), true)
+ }), true);
}
exports.fragment = fragment;
exports.element = element;
exports.elementWithTag = elementWithTag;
+exports.selfClosingElement = selfClosingElement;
exports.text = text;
+exports.forceWrite = forceWrite;
diff --git a/lib/html/index.js b/lib/html/index.js
index 41d348f..f74a98f 100644
--- a/lib/html/index.js
+++ b/lib/html/index.js
@@ -3,7 +3,9 @@ var ast = require("./ast");
exports.fragment = ast.fragment;
exports.element = ast.element;
exports.elementWithTag = ast.elementWithTag;
+exports.selfClosingElement = ast.selfClosingElement;
exports.text = ast.text;
+exports.forceWrite = ast.forceWrite;
function pathToNode(path, children) {
var result = ast.fragment(children);
@@ -15,3 +17,39 @@ function pathToNode(path, children) {
exports.pathToNode = pathToNode;
exports.simplify = require("./simplify");
+
+function write(writer, node) {
+ toStrings[node.type](writer, node);
+}
+
+var toStrings = {
+ fragment: generateFragmentString,
+ element: generateElementString,
+ selfClosingElement: generateSelfClosingElementString,
+ text: generateTextString,
+ forceWrite: function() { }
+};
+
+function generateFragmentString(writer, node) {
+ node.children.forEach(function(child) {
+ write(writer, child);
+ });
+}
+
+function generateElementString(writer, node) {
+ writer.open(node.tag.tagName, node.tag.attributes);
+ node.children.forEach(function(child) {
+ write(writer, child);
+ });
+ writer.close(node.tag.tagName);
+}
+
+function generateSelfClosingElementString(writer, node) {
+ writer.selfClosing(node.tagName, node.attributes);
+}
+
+function generateTextString(writer, node) {
+ writer.text(node.value);
+}
+
+exports.write = write;
diff --git a/lib/html/simplify.js b/lib/html/simplify.js
index 291e5d4..8ab4a1f 100644
--- a/lib/html/simplify.js
+++ b/lib/html/simplify.js
@@ -1,15 +1,15 @@
-var _ = require("underscore");
-
var ast = require("./ast");
function simplify(node) {
- return simplifiers[node.type](node);
-};
+ return removeEmpty(simplifiers[node.type](node));
+}
var simplifiers = {
fragment: simplifyFragment,
element: simplifyElement,
- text: simplifyText
+ selfClosingElement: identity,
+ text: identity,
+ forceWrite: identity
};
function simplifyFragment(node) {
@@ -20,8 +20,8 @@ function simplifyElement(node) {
return ast.elementWithTag(node.tag, simplifyNodes(node.children));
}
-function simplifyText(node) {
- return node;
+function identity(value) {
+ return value;
}
function simplifyNodes(nodes) {
@@ -29,10 +29,10 @@ function simplifyNodes(nodes) {
nodes.map(simplify).forEach(function(child) {
var lastChild = children[children.length - 1];
- if (child.type === "element" && !child.tag.fresh && lastChild && child.tag.matchesElement(lastChild.tag)) {
+ if (child.type === "element" && !child.tag.fresh && lastChild && lastChild.type === "element" && child.tag.matchesElement(lastChild.tag)) {
child.children.forEach(function(grandChild) {
// Mutation is fine since simplifying elements create a copy of the children.
- lastChild.children.push(grandChild);
+ lastChild.appendChild(grandChild);
});
} else {
children.push(child);
@@ -41,4 +41,29 @@ function simplifyNodes(nodes) {
return children;
}
+function removeEmpty(node) {
+ return emptiers[node.type](node);
+}
+
+var emptiers = {
+ fragment: fragmentEmptier,
+ element: elementEmptier,
+ selfClosingElement: identity,
+ text: identity,
+ forceWrite: identity
+};
+
+function fragmentEmptier(fragment) {
+ return ast.fragment(fragment.children.filter(function(child) {
+ return !child.isEmpty;
+ }));
+}
+
+function elementEmptier(element) {
+ return ast.elementWithTag(element.tag, element.children.filter(function(child) {
+ return !child.isEmpty;
+ }));
+}
+
+
module.exports = simplify;
diff --git a/lib/images.js b/lib/images.js
index 613a264..d7c2ebc 100644
--- a/lib/images.js
+++ b/lib/images.js
@@ -1,14 +1,16 @@
var promises = require("./promises");
-var htmlPaths = require("./html-paths");
+var Html = require("./html");
exports.inline = function(func) {
- return function(element, html, messages, callback) {
+ return function(element, messages, callback) {
return promises.when(func(element)).then(function(result) {
var attributes = {src: result.src};
if (element.altText) {
attributes.alt = element.altText;
}
- html.selfClosing(htmlPaths.element("img", attributes));
- }).then(callback, callback);
+ return Html.selfClosingElement("img", attributes);
+ }).then(function(value) {
+ callback(null, value);
+ }, callback);
};
};
diff --git a/lib/underline.js b/lib/underline.js
index 17448e8..fd5dc0f 100644
--- a/lib/underline.js
+++ b/lib/underline.js
@@ -1,10 +1,11 @@
var htmlPaths = require("./html-paths");
+var Html = require("./html");
exports.element = element;
function element(name) {
return function(html) {
- html.open(htmlPaths.element(name));
+ return Html.elementWithTag(htmlPaths.element(name), [html]);
};
}
diff --git a/tests/document-to-html.tests.js b/tests/document-to-html.tests.js
index 323efb6..7180191 100644
--- a/tests/document-to-html.tests.js
+++ b/tests/document-to-html.tests.js
@@ -9,6 +9,7 @@ var htmlPaths = require("../lib/html-paths");
var xml = require("../lib/xml");
var results = require("../lib/results");
var documentMatchers = require("../lib/document-matchers");
+var Html = require("../lib/html");
describe('DocumentConverter', function() {
@@ -157,17 +158,16 @@ describe('DocumentConverter', function() {
});
});
- // TODO
- //~ test('consecutive bold runs are wrapped in a single <strong> element', function() {
- //~ var paragraph = new documents.Paragraph([
- //~ runOfText("Hello", {isBold: true}),
- //~ runOfText(".", {isBold: true})
- //~ ]);
- //~ var converter = new DocumentConverter();
- //~ return converter.convertToHtml(paragraph).then(function(result) {
- //~ assert.equal(result.value, "<p><strong>Hello.</strong></p>");
- //~ });
- //~ });
+ test('consecutive bold runs are wrapped in a single <strong> element', function() {
+ var paragraph = new documents.Paragraph([
+ runOfText("Hello", {isBold: true}),
+ runOfText(".", {isBold: true})
+ ]);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(paragraph).then(function(result) {
+ assert.equal(result.value, "<p><strong>Hello.</strong></p>");
+ });
+ });
test('underline runs are ignored by default', function() {
var run = runOfText("Hello.", {isUnderline: true});
@@ -476,10 +476,9 @@ describe('DocumentConverter', function() {
contentType: "image/png"
});
var converter = new DocumentConverter({
- convertImage: function(element, html, messages, callback) {
+ convertImage: function(element, messages, callback) {
element.read("utf8").then(function(altText) {
- html.selfClosing(htmlPaths.element("img", {alt: altText}));
- callback();
+ callback(null, Html.selfClosingElement("img", {alt: altText}));
});
}
});
diff --git a/tests/html/simplify.tests.js b/tests/html/simplify.tests.js
index 7e17903..e23c379 100644
--- a/tests/html/simplify.tests.js
+++ b/tests/html/simplify.tests.js
@@ -6,11 +6,25 @@ var htmlPaths = require("../../lib/html-paths");
var fragment = html.fragment;
-var element = html.element;
var text = html.text;
var pathToNode = html.pathToNode;
describe("simplify", function() {
+ test("empty text nodes are removed", function() {
+ assert.deepEqual(
+ html.simplify(fragment([text("")])),
+ fragment([]));
+ });
+
+ test("empty elements are removed", function() {
+ var path = htmlPaths.elements([
+ htmlPaths.element("p", {}, {fresh: false})]);
+ assert.deepEqual(
+ html.simplify(fragment([
+ pathToNode(path, [text("")])])),
+ fragment([]));
+ });
+
test("successive fresh elements are not collapsed", function() {
var path = htmlPaths.elements([
htmlPaths.element("p", {}, {fresh: true})]);
diff --git a/tests/mammoth.tests.js b/tests/mammoth.tests.js
index 10b95ef..34adb09 100644
--- a/tests/mammoth.tests.js
+++ b/tests/mammoth.tests.js
@@ -280,21 +280,21 @@ describe('mammoth', function() {
test('underline is ignored by default', function() {
var docxPath = path.join(__dirname, "test-data/underline.docx");
return mammoth.convertToHtml({path: docxPath}).then(function(result) {
- assert.equal(result.value, '<p><strong>The </strong><strong>Sunset</strong><strong> Tree</strong></p>');
+ assert.equal(result.value, '<p><strong>The Sunset Tree</strong></p>');
});
});
test('underline can be configured with convertUnderline option', function() {
var docxPath = path.join(__dirname, "test-data/underline.docx");
return mammoth.convertToHtml({path: docxPath}, {convertUnderline: mammoth.underline.element("em")}).then(function(result) {
- assert.equal(result.value, '<p><strong>The </strong><strong><em>Sunset</em></strong><strong> Tree</strong></p>');
+ assert.equal(result.value, '<p><strong>The <em>Sunset</em> Tree</strong></p>');
});
});
test('underline can be configured with style mapping', function() {
var docxPath = path.join(__dirname, "test-data/underline.docx");
return mammoth.convertToHtml({path: docxPath}, {styleMap: "u => em"}).then(function(result) {
- assert.equal(result.value, '<p><strong>The </strong><strong><em>Sunset</em></strong><strong> Tree</strong></p>');
+ assert.equal(result.value, '<p><strong>The <em>Sunset</em> Tree</strong></p>');
});
});