diff options
| author | Michael Williamson <mike@zwobble.org> | 2015-10-13 23:29:41 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2015-10-13 23:49:31 +0100 |
| commit | 7b7ce0d563ddc07172b0f241a5c3baf344f256e8 (patch) | |
| tree | 43f5fddf920d47e281a30139bde99da5a689d667 | |
| parent | e04d054a1b414e843fa6d6f5ee1ae75c0df533b1 (diff) | |
Collapse similar HTML elements
| -rw-r--r-- | lib/document-to-html.js | 283 | ||||
| -rw-r--r-- | lib/html/ast.js | 31 | ||||
| -rw-r--r-- | lib/html/index.js | 38 | ||||
| -rw-r--r-- | lib/html/simplify.js | 43 | ||||
| -rw-r--r-- | lib/images.js | 10 | ||||
| -rw-r--r-- | lib/underline.js | 3 | ||||
| -rw-r--r-- | tests/document-to-html.tests.js | 27 | ||||
| -rw-r--r-- | tests/html/simplify.tests.js | 16 | ||||
| -rw-r--r-- | tests/mammoth.tests.js | 6 |
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>'); }); }); |
