UNPKG

mammoth-style

Version:

Convert Word documents (.docx files) to HTML (with style)

418 lines (368 loc) 14.1 kB
var _ = require("underscore"); var promises = require("./promises"); var documents = require("./documents"); var htmlPaths = require("./html-paths"); var results = require("./results"); var images = require("./images"); var Html = require("./html"); var writers = require("./writers"); var clone = require('clone'); exports.DocumentConverter = DocumentConverter; function DocumentConverter(options) { return { convertToHtml: function(element) { var comments = _.indexBy( element.type === documents.types.document ? element.comments : [], "commentId" ); var conversion = new DocumentConversion(options, comments); return conversion.convertToHtml(element); } }; } function DocumentConversion(options, comments) { var noteNumber = 1; var noteReferences = []; var referencedComments = []; options = _.extend({ignoreEmptyParagraphs: true}, options); var idPrefix = options.idPrefix === undefined ? "" : options.idPrefix; var defaultParagraphStyle = htmlPaths.topLevelElement("p"); var styleMap = options.styleMap || []; function convertToHtml(document) { var messages = []; var html = elementToHtml(document, messages); var deferredNodes = []; walkHtml(html, function(node) { if (node.type === "deferred") { deferredNodes.push(node); } }); var deferredValues = {}; return promises.mapSeries(deferredNodes, function(deferred) { return deferred.value().then(function(value) { deferredValues[deferred.id] = value; }); }).then(function() { function replaceDeferred(nodes) { return flatMap(nodes, function(node) { if (node.type === "deferred") { return deferredValues[node.id]; } else if (node.children) { return [ _.extend({}, node, { children: replaceDeferred(node.children) }) ]; } else { return [node]; } }); } var writer = writers.writer({ prettyPrint: options.prettyPrint, outputFormat: options.outputFormat }); Html.write(writer, Html.simplify(replaceDeferred(html))); return new results.Result(writer.asString(), messages); }); } function convertElements(elements, messages) { return flatMap(elements, function(element) { return elementToHtml(element, messages); }); } function elementToHtml(element, messages) { var handler = elementConverters[element.type]; if (handler) { return handler(element, messages); } else { return []; } } function convertParagraph(element, messages) { return htmlPathForParagraph(element, messages).wrap(function() { var content = convertElements(element.children, messages); if (options.ignoreEmptyParagraphs) { return content; } else { return [Html.forceWrite].concat(content); } }); } function htmlPathForParagraph(element, messages) { var style = findStyle(element); var listTypes = { "decimal" : "1", "upperRoman" : "I", "lowerRoman" : "i", "upperLetter" : "A", "lowerLetter" : "a" } if (style) { var style_clone = clone(style); for (var i in style_clone.to._elements) { if (style_clone.to._elements[i].tagName == 'ol') { if (element.numbering && element.numbering.format) { style_clone.to._elements[i].attributes['type'] = listTypes[element.numbering.format]; } } } return style_clone.to; } else { if (element.styleId) { messages.push(unrecognisedStyleWarning("paragraph", element)); } return defaultParagraphStyle; } } function convertRun(run, messages) { var nodes = function() { return convertElements(run.children, messages); }; var paths = []; if (run.isStrikethrough) { paths.push(findHtmlPathForRunProperty("strikethrough", "s")); } if (run.isUnderline) { paths.push(findHtmlPathForRunProperty("underline")); } if (run.verticalAlignment === documents.verticalAlignment.subscript) { paths.push(htmlPaths.element("sub", {}, {fresh: false})); } if (run.verticalAlignment === documents.verticalAlignment.superscript) { paths.push(htmlPaths.element("sup", {}, {fresh: false})); } if (run.isItalic) { paths.push(findHtmlPathForRunProperty("italic", "em")); } if (run.isBold) { paths.push(findHtmlPathForRunProperty("bold", "strong")); } if (run.color) { paths.push(htmlPaths.element('font', {color : run.color}, {fresh: false})); } var stylePath = htmlPaths.empty; if (run.styleId) { var style = findStyle(run); if (style) { stylePath = style.to; } else { messages.push(unrecognisedStyleWarning("run", run)); } } paths.push(stylePath); paths.forEach(function(path) { nodes = path.wrap.bind(path, nodes); }); return nodes(); } function findHtmlPathForRunProperty(elementType, defaultTagName) { var path = findHtmlPath({type: elementType}); if (path) { return path; } else if (defaultTagName) { return htmlPaths.element(defaultTagName, {}, {fresh: false}); } else { return htmlPaths.empty; } } function findHtmlPath(element, defaultPath) { var style = findStyle(element); return style ? style.to : defaultPath; } function findStyle(element) { for (var i = 0; i < styleMap.length; i++) { if (styleMap[i].from.matches(element)) { return styleMap[i]; } } } var defaultConvertImage = images.imgElement(function(element) { return element.read("base64").then(function(imageBuffer) { return { src: "data:" + element.contentType + ";base64," + imageBuffer }; }); }); function recoveringConvertImage(convertImage) { return function(image, messages) { return convertImage(image, messages).caught(function(error) { messages.push(results.warning(error.message)); return []; }); }; } function noteHtmlId(note) { return referentHtmlId(note.noteType, note.noteId); } function noteRefHtmlId(note) { return referenceHtmlId(note.noteType, note.noteId); } function referentHtmlId(referenceType, referenceId) { return htmlId(referenceType + "-" + referenceId); } function referenceHtmlId(referenceType, referenceId) { return htmlId(referenceType + "-ref-" + referenceId); } function htmlId(suffix) { return idPrefix + suffix; } function convertTable(element, messages) { return wrapChildrenInFreshElement(element, "table", messages); } function convertTableRow(element, messages) { return wrapChildrenInFreshElement(element, "tr", messages); } function convertTableCell(element, messages) { var children = convertElements(element.children, messages); var attributes = {}; if (element.colSpan !== 1) { attributes.colspan = element.colSpan.toString(); } if (element.rowSpan !== 1) { attributes.rowspan = element.rowSpan.toString(); } return [ Html.freshElement("td", attributes, [Html.forceWrite].concat(children)) ]; } function convertCommentReference(reference, messages) { return findHtmlPath(reference, htmlPaths.ignore).wrap(function() { var comment = comments[reference.commentId]; var count = referencedComments.length + 1; var label = "[" + commentAuthorLabel(comment) + count + "]"; referencedComments.push({label: label, comment: comment}); // TODO: remove duplication with note references return [ Html.freshElement("a", { href: "#" + referentHtmlId("comment", reference.commentId), id: referenceHtmlId("comment", reference.commentId) }, [Html.text(label)]) ]; }); } function convertComment(referencedComment, messages) { // TODO: remove duplication with note references var label = referencedComment.label; var comment = referencedComment.comment; var body = convertElements(comment.body).concat([ Html.nonFreshElement("p", {}, [ Html.text(" "), Html.freshElement("a", {"href": "#" + referenceHtmlId("comment", comment.commentId)}, [ Html.text("↑") ]) ]) ]); return [ Html.freshElement( "dt", {"id": referentHtmlId("comment", comment.commentId)}, [Html.text("Comment " + label)] ), Html.freshElement("dd", {}, body) ]; } function wrapChildrenInFreshElement(element, wrapElementName, messages) { var children = convertElements(element.children, messages); return [ Html.freshElement(wrapElementName, {}, [Html.forceWrite].concat(children)) ]; } var elementConverters = { "document": function(document, messages) { var children = convertElements(document.children, messages); var notes = noteReferences.map(function(noteReference) { return document.notes.resolve(noteReference); }); var notesNodes = convertElements(notes, messages); return children.concat([ Html.freshElement("ol", {}, notesNodes), Html.freshElement("dl", {}, flatMap(referencedComments, function(referencedComment) { return convertComment(referencedComment, messages); })) ]); }, "paragraph": convertParagraph, "run": convertRun, "text": function(element, messages) { return [Html.text(element.value)]; }, "tab": function(element, messages) { return [Html.text("\t")]; }, "hyperlink": function(element, messages) { var href = element.anchor ? "#" + htmlId(element.anchor) : element.href; var children = convertElements(element.children, messages); return [Html.freshElement("a", {href: href}, children)]; }, "bookmarkStart": function(element, messages) { var anchor = Html.freshElement("a", { id: htmlId(element.name) }, [Html.forceWrite]); return [anchor]; }, "noteReference": function(element, messages) { noteReferences.push(element); var anchor = Html.freshElement("a", { href: "#" + noteHtmlId(element), id: noteRefHtmlId(element) }, [Html.text("[" + (noteNumber++) + "]")]); return [Html.freshElement("sup", {}, [anchor])]; }, "note": function(element, messages) { var children = convertElements(element.body, messages); var backLink = Html.elementWithTag(htmlPaths.element("p", {}, {fresh: false}), [ Html.text(" "), Html.freshElement("a", {href: "#" + noteRefHtmlId(element)}, [Html.text("↑")]) ]); var body = children.concat([backLink]); return Html.freshElement("li", {id: noteHtmlId(element)}, body); }, "commentReference": convertCommentReference, "comment": convertComment, "image": deferredConversion(recoveringConvertImage(options.convertImage || defaultConvertImage)), "table": convertTable, "tableRow": convertTableRow, "tableCell": convertTableCell, "lineBreak": function(element, messages) { return [Html.selfClosingElement("br")]; } }; return { convertToHtml: convertToHtml }; } var deferredId = 1; function deferredConversion(func) { return function(element, messages) { return [ { type: "deferred", id: deferredId++, value: function() { return func(element, messages); } } ]; }; } function unrecognisedStyleWarning(type, element) { return results.warning( "Unrecognised " + type + " style: '" + element.styleName + "'" + " (Style ID: " + element.styleId + ")" ); } function flatMap(values, func) { return _.flatten(values.map(func), true); } function walkHtml(nodes, callback) { nodes.forEach(function(node) { callback(node); if (node.children) { walkHtml(node.children, callback); } }); } var commentAuthorLabel = exports.commentAuthorLabel = function commentAuthorLabel(comment) { return comment.authorInitials || ""; };