aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2016-07-03 19:19:21 +0100
committerMichael Williamson <mike@zwobble.org>2016-07-03 19:20:00 +0100
commit0b56dfce42c3944cce47e284608e6806f4c839c3 (patch)
tree5f96f9c2908f1c5b00a7192e71d4c192abd7cf47 /test
parent627c4a755395e313234bdb1914d168392006c483 (diff)
Move tests to test for easier Mocha usage
Diffstat (limited to 'test')
-rw-r--r--test/.eslintrc.json7
-rw-r--r--test/document-matchers.tests.js52
-rw-r--r--test/document-to-html.tests.js566
-rw-r--r--test/docx/body-reader.tests.js724
-rw-r--r--test/docx/content-types-reader.tests.js47
-rw-r--r--test/docx/docx-reader.tests.js42
-rw-r--r--test/docx/files.tests.js94
-rw-r--r--test/docx/notes-reader.tests.js39
-rw-r--r--test/docx/numbering-xml.tests.js32
-rw-r--r--test/docx/office-xml-reader.tests.js26
-rw-r--r--test/docx/style-map.tests.js115
-rw-r--r--test/docx/styles-reader.tests.js92
-rw-r--r--test/html-paths.tests.js22
-rw-r--r--test/html/simplify.tests.js115
-rw-r--r--test/html/write.tests.js44
-rw-r--r--test/images.tests.js13
-rw-r--r--test/main.tests.js91
-rw-r--r--test/mammoth.tests.js353
-rw-r--r--test/options-reader.tests.js48
-rw-r--r--test/results.tests.js17
-rw-r--r--test/style-reader.tests.js159
-rw-r--r--test/test-data/empty.docxbin0 -> 3306 bytes
-rw-r--r--test/test-data/endnotes.docxbin0 -> 4431 bytes
-rw-r--r--test/test-data/external-picture.docxbin0 -> 4120 bytes
-rwxr-xr-xtest/test-data/footnote-hyperlink.docxbin0 -> 16271 bytes
-rwxr-xr-xtest/test-data/footnotes.docxbin0 -> 15936 bytes
-rw-r--r--test/test-data/hello.zipbin0 -> 120 bytes
-rw-r--r--test/test-data/hyperlinks/word/_rels/document.xml.rels10
-rw-r--r--test/test-data/hyperlinks/word/document.xml18
-rwxr-xr-xtest/test-data/simple-list.docxbin0 -> 13887 bytes
-rw-r--r--test/test-data/simple/word/document.xml18
-rwxr-xr-xtest/test-data/single-paragraph.docxbin0 -> 12773 bytes
-rwxr-xr-xtest/test-data/strikethrough.docxbin0 -> 12652 bytes
-rwxr-xr-xtest/test-data/tables.docxbin0 -> 13087 bytes
-rwxr-xr-xtest/test-data/text-box.docxbin0 -> 15850 bytes
-rwxr-xr-xtest/test-data/tiny-picture.docxbin0 -> 13699 bytes
-rw-r--r--test/test-data/tiny-picture.pngbin0 -> 110 bytes
-rwxr-xr-xtest/test-data/underline.docxbin0 -> 12797 bytes
-rw-r--r--test/test-data/utf8-bom.docxbin0 -> 2836 bytes
-rw-r--r--test/testing.js65
-rw-r--r--test/unzip.tests.js40
-rw-r--r--test/writers/html-writer.tests.js117
-rw-r--r--test/writers/markdown-writer.tests.js306
-rw-r--r--test/xml/reader.tests.js88
-rw-r--r--test/xml/writer.tests.js83
-rw-r--r--test/zipfile.tests.js26
46 files changed, 3469 insertions, 0 deletions
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
new file mode 100644
index 0000000..4869f03
--- /dev/null
+++ b/test/.eslintrc.json
@@ -0,0 +1,7 @@
+{
+ "globals": {
+ "__dirname": false,
+ "describe": false,
+ "it": false
+ }
+}
diff --git a/test/document-matchers.tests.js b/test/document-matchers.tests.js
new file mode 100644
index 0000000..58dbf80
--- /dev/null
+++ b/test/document-matchers.tests.js
@@ -0,0 +1,52 @@
+var assert = require("assert");
+
+var test = require("./testing").test;
+var documentMatchers = require("../lib/document-matchers");
+var documents = require("../lib/documents");
+var Paragraph = documents.Paragraph;
+
+describe("document-matchers", function() {
+ test("paragraph with no options matches any paragraph", function() {
+ var matcher = documentMatchers.paragraph();
+ assert.ok(matcher.matches(new Paragraph()));
+ assert.ok(matcher.matches(paragraphWithStyle("Heading1", "Heading 1")));
+ });
+
+ test("paragraph style ID only matches paragraphs with that style ID", function() {
+ var matcher = documentMatchers.paragraph({styleId: "Heading1"});
+ assert.ok(!matcher.matches(new Paragraph()));
+ assert.ok(matcher.matches(paragraphWithStyle("Heading1", "Heading 1")));
+ assert.ok(!matcher.matches(paragraphWithStyle("Heading2", "Heading 2")));
+ });
+
+ test("paragraph style name only matches paragraphs with that style name", function() {
+ var matcher = documentMatchers.paragraph({styleName: "Heading 1"});
+ assert.ok(!matcher.matches(new Paragraph()));
+ assert.ok(matcher.matches(paragraphWithStyle("Heading1", "Heading 1")));
+ assert.ok(!matcher.matches(paragraphWithStyle("Heading2", "Heading 2")));
+ });
+
+ test("ordered-list(index) matches an ordered list with specified level index", function() {
+ var matcher = documentMatchers.paragraph({list: {isOrdered: true, levelIndex: 1}});
+ assert.ok(!matcher.matches(new Paragraph()));
+ assert.ok(matcher.matches(new Paragraph([], {numbering: {level: 1, isOrdered: true}})));
+ assert.ok(!matcher.matches(new Paragraph([], {numbering: {level: 0, isOrdered: true}})));
+ assert.ok(!matcher.matches(new Paragraph([], {numbering: {level: 1, isOrdered: false}})));
+ });
+
+ test("unordered-list(index) matches an unordered list with specified level index", function() {
+ var matcher = documentMatchers.paragraph({list: {isOrdered: false, levelIndex: 1}});
+ assert.ok(!matcher.matches(new Paragraph()));
+ assert.ok(matcher.matches(new Paragraph([], {numbering: {level: 1, isOrdered: false}})));
+ assert.ok(!matcher.matches(new Paragraph([], {numbering: {level: 1, isOrdered: true}})));
+ });
+
+ test("matchers for lists with index 0 do not match elements that are not lists", function() {
+ var matcher = documentMatchers.paragraph({list: {isOrdered: true, levelIndex: 0}});
+ assert.ok(!matcher.matches(new Paragraph()));
+ });
+});
+
+function paragraphWithStyle(styleId, styleName) {
+ return new Paragraph([], {styleId: styleId, styleName: styleName});
+}
diff --git a/test/document-to-html.tests.js b/test/document-to-html.tests.js
new file mode 100644
index 0000000..10ea265
--- /dev/null
+++ b/test/document-to-html.tests.js
@@ -0,0 +1,566 @@
+var assert = require("assert");
+var promises = require("../lib/promises");
+
+var documents = require("../lib/documents");
+var DocumentConverter = require("../lib/document-to-html").DocumentConverter;
+var test = require("./testing").test;
+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() {
+ test('should empty document to empty string', function() {
+ var document = new documents.Document([]);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "");
+ });
+ });
+
+ test('should convert document containing one paragraph to single p element', function() {
+ var document = new documents.Document([
+ paragraphOfText("Hello.")
+ ]);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "<p>Hello.</p>");
+ });
+ });
+
+ test('ignores empty paragraphs', function() {
+ var document = new documents.Document([
+ paragraphOfText("")
+ ]);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "");
+ });
+ });
+
+ test('text is HTML-escaped', function() {
+ var document = new documents.Document([
+ paragraphOfText("1 < 2")
+ ]);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "<p>1 &lt; 2</p>");
+ });
+ });
+
+ test('should convert document containing multiple paragraphs to multiple p elements', function() {
+ var document = new documents.Document([
+ paragraphOfText("Hello."),
+ paragraphOfText("Goodbye.")
+ ]);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "<p>Hello.</p><p>Goodbye.</p>");
+ });
+ });
+
+ test('uses style mappings to pick HTML element for docx paragraph', function() {
+ var document = new documents.Document([
+ paragraphOfText("Hello.", "Heading1", "Heading 1")
+ ]);
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.paragraph({styleName: "Heading 1"}),
+ to: htmlPaths.topLevelElement("h1")
+ }
+ ]
+ });
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "<h1>Hello.</h1>");
+ });
+ });
+
+ test('mappings for style names are case insensitive', function() {
+ var document = new documents.Document([
+ paragraphOfText("Hello.", "Heading1", "heading 1")
+ ]);
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.paragraph({styleName: "Heading 1"}),
+ to: htmlPaths.topLevelElement("h1")
+ }
+ ]
+ });
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "<h1>Hello.</h1>");
+ });
+ });
+
+ test('can use non-default HTML element for unstyled paragraphs', function() {
+ var document = new documents.Document([
+ paragraphOfText("Hello.")
+ ]);
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.paragraph(),
+ to: htmlPaths.topLevelElement("h1")
+ }
+ ]
+ });
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "<h1>Hello.</h1>");
+ });
+ });
+
+ test('warning is emitted if paragraph style is unrecognised', function() {
+ var document = new documents.Document([
+ paragraphOfText("Hello.", "Heading1", "Heading 1")
+ ]);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(document).then(function(result) {
+ assert.deepEqual(result.messages, [results.warning("Unrecognised paragraph style: 'Heading 1' (Style ID: Heading1)")]);
+ });
+ });
+
+ test('can use stacked styles to generate nested HTML elements', function() {
+ var document = new documents.Document([
+ paragraphOfText("Hello.")
+ ]);
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.paragraph(),
+ to: htmlPaths.elements(["h1", "span"])
+ }
+ ]
+ });
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, "<h1><span>Hello.</span></h1>");
+ });
+ });
+
+ test('bold runs are wrapped in <strong> tags by default', function() {
+ var run = runOfText("Hello.", {isBold: true});
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<strong>Hello.</strong>");
+ });
+ });
+
+ test('bold runs can be configured with style mapping', function() {
+ var run = runOfText("Hello.", {isBold: true});
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.bold,
+ to: htmlPaths.elements([htmlPaths.element("em")])
+ }
+ ]
+ });
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<em>Hello.</em>");
+ });
+ });
+
+ test('bold runs can exist inside other tags', function() {
+ var run = new documents.Paragraph([
+ runOfText("Hello.", {isBold: true})
+ ]);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).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});
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "Hello.");
+ });
+ });
+
+ test('underline runs can be mapped using style mapping', function() {
+ var run = runOfText("Hello.", {isUnderline: true});
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.underline,
+ to: htmlPaths.elements([htmlPaths.element("u")])
+ }
+ ]
+ });
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<u>Hello.</u>");
+ });
+ });
+
+ test('style mapping for underline runs does not close parent elements', function() {
+ var run = runOfText("Hello.", {isUnderline: true, isBold: true});
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.underline,
+ to: htmlPaths.elements([htmlPaths.element("u")])
+ }
+ ]
+ });
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<strong><u>Hello.</u></strong>");
+ });
+ });
+
+ test('strikethrough runs are wrapped in <s> tags by default', function() {
+ var run = runOfText("Hello.", {isStrikethrough: true});
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<s>Hello.</s>");
+ });
+ });
+
+ test('strikethrough runs can be configured with style mapping', function() {
+ var run = runOfText("Hello.", {isStrikethrough: true});
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.strikethrough,
+ to: htmlPaths.elements([htmlPaths.element("del")])
+ }
+ ]
+ });
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<del>Hello.</del>");
+ });
+ });
+
+ test('italic runs are wrapped in <em> tags', function() {
+ var run = runOfText("Hello.", {isItalic: true});
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<em>Hello.</em>");
+ });
+ });
+
+ test('italic runs can be configured with style mapping', function() {
+ var run = runOfText("Hello.", {isItalic: true});
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.italic,
+ to: htmlPaths.elements([htmlPaths.element("strong")])
+ }
+ ]
+ });
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<strong>Hello.</strong>");
+ });
+ });
+
+ test('run can be both bold and italic', function() {
+ var run = runOfText("Hello.", {isBold: true, isItalic: true});
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<strong><em>Hello.</em></strong>");
+ });
+ });
+
+ test('superscript runs are wrapped in <sup> tags', function() {
+ var run = runOfText("Hello.", {
+ verticalAlignment: documents.verticalAlignment.superscript
+ });
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<sup>Hello.</sup>");
+ });
+ });
+
+ test('subscript runs are wrapped in <sub> tags', function() {
+ var run = runOfText("Hello.", {
+ verticalAlignment: documents.verticalAlignment.subscript
+ });
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<sub>Hello.</sub>");
+ });
+ });
+
+ test('run styles are converted to HTML if mapping exists', function() {
+ var run = runOfText("Hello.", {styleId: "Heading1Char", styleName: "Heading 1 Char"});
+ var converter = new DocumentConverter({
+ styleMap: [
+ {
+ from: documentMatchers.run({styleName: "Heading 1 Char"}),
+ to: htmlPaths.elements(["strong"])
+ }
+ ]
+ });
+ return converter.convertToHtml(run).then(function(result) {
+ assert.equal(result.value, "<strong>Hello.</strong>");
+ });
+ });
+
+ test('warning is emitted if run style is unrecognised', function() {
+ var run = runOfText("Hello.", {styleId: "Heading1Char", styleName: "Heading 1 Char"});
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(run).then(function(result) {
+ assert.deepEqual(result.messages, [results.warning("Unrecognised run style: 'Heading 1 Char' (Style ID: Heading1Char)")]);
+ });
+ });
+
+ test('docx hyperlink is converted to <a>', function() {
+ var hyperlink = new documents.Hyperlink(
+ [runOfText("Hello.")],
+ {href: "http://www.example.com"}
+ );
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(hyperlink).then(function(result) {
+ assert.equal(result.value, '<a href="http://www.example.com">Hello.</a>');
+ });
+ });
+
+ test('docx hyperlink with anchor is converted to <a>', function() {
+ var hyperlink = new documents.Hyperlink(
+ [runOfText("Hello.")],
+ {anchor: "_Peter"}
+ );
+ var converter = new DocumentConverter({
+ idPrefix: "doc-42-"
+ });
+ return converter.convertToHtml(hyperlink).then(function(result) {
+ assert.equal(result.value, '<a href="#doc-42-_Peter">Hello.</a>');
+ });
+ });
+
+ test('bookmarks are converted to anchors', function() {
+ var bookmarkStart = new documents.BookmarkStart({name: "_Peter"});
+ var converter = new DocumentConverter({
+ idPrefix: "doc-42-"
+ });
+ var document = new documents.Document([bookmarkStart]);
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value, '<a id="doc-42-_Peter"></a>');
+ });
+ });
+
+ test('docx tab is converted to tab in HTML', function() {
+ var tab = new documents.Tab();
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(tab).then(function(result) {
+ assert.equal(result.value, "\t");
+ });
+ });
+
+ test('docx table is converted to table in HTML', function() {
+ var table = new documents.Table([
+ new documents.TableRow([
+ new documents.TableCell([paragraphOfText("Top left")]),
+ new documents.TableCell([paragraphOfText("Top right")])
+ ]),
+ new documents.TableRow([
+ new documents.TableCell([paragraphOfText("Bottom left")]),
+ new documents.TableCell([paragraphOfText("Bottom right")])
+ ])
+ ]);
+ var converter = new DocumentConverter();
+
+ return converter.convertToHtml(table).then(function(result) {
+ var expectedHtml = "<table>" +
+ "<tr><td><p>Top left</p></td><td><p>Top right</p></td></tr>" +
+ "<tr><td><p>Bottom left</p></td><td><p>Bottom right</p></td></tr>" +
+ "</table>";
+ assert.equal(result.value, expectedHtml);
+ });
+ });
+
+ test('empty cells are preserved in table', function() {
+ var table = new documents.Table([
+ new documents.TableRow([
+ new documents.TableCell([paragraphOfText("")]),
+ new documents.TableCell([paragraphOfText("Top right")])
+ ])
+ ]);
+ var converter = new DocumentConverter();
+
+ return converter.convertToHtml(table).then(function(result) {
+ var expectedHtml = "<table>" +
+ "<tr><td></td><td><p>Top right</p></td></tr>" +
+ "</table>";
+ assert.equal(result.value, expectedHtml);
+ });
+ });
+
+ test('table cells are written with colSpan if not equal to one', function() {
+ var table = new documents.Table([
+ new documents.TableRow([
+ new documents.TableCell([paragraphOfText("Top left")], {colSpan: 2}),
+ new documents.TableCell([paragraphOfText("Top right")])
+ ])
+ ]);
+ var converter = new DocumentConverter();
+
+ return converter.convertToHtml(table).then(function(result) {
+ var expectedHtml = "<table>" +
+ "<tr><td colspan=\"2\"><p>Top left</p></td><td><p>Top right</p></td></tr>" +
+ "</table>";
+ assert.equal(result.value, expectedHtml);
+ });
+ });
+
+ test('table cells are written with rowSpan if not equal to one', function() {
+ var table = new documents.Table([
+ new documents.TableRow([
+ new documents.TableCell([], {rowSpan: 2})
+ ])
+ ]);
+ var converter = new DocumentConverter();
+
+ return converter.convertToHtml(table).then(function(result) {
+ var expectedHtml = "<table>" +
+ "<tr><td rowspan=\"2\"></td></tr>" +
+ "</table>";
+ assert.equal(result.value, expectedHtml);
+ });
+ });
+
+ test('line break is converted to <br>', function() {
+ var lineBreak = new documents.LineBreak();
+ var converter = new DocumentConverter();
+
+ return converter.convertToHtml(lineBreak).then(function(result) {
+ assert.equal(result.value, "<br />");
+ });
+ });
+
+ test('footnote reference is converted to superscript intra-page link', function() {
+ var footnoteReference = new documents.NoteReference({
+ noteType: "footnote",
+ noteId: "4"
+ });
+ var converter = new DocumentConverter({
+ idPrefix: "doc-42-"
+ });
+ return converter.convertToHtml(footnoteReference).then(function(result) {
+ assert.equal(result.value, '<sup><a href="#doc-42-footnote-4" id="doc-42-footnote-ref-4">[1]</a></sup>');
+ });
+ });
+
+ test('footnotes are included after the main body', function() {
+ var footnoteReference = new documents.NoteReference({
+ noteType: "footnote",
+ noteId: "4"
+ });
+ var document = new documents.Document(
+ [new documents.Paragraph([
+ runOfText("Knock knock"),
+ new documents.Run([footnoteReference])
+ ])],
+ {
+ notes: new documents.Notes({
+ 4: new documents.Note({
+ noteType: "footnote",
+ noteId: "4",
+ body: [paragraphOfText("Who's there?")]
+ })
+ })
+ }
+ );
+
+ var converter = new DocumentConverter({
+ idPrefix: "doc-42-"
+ });
+ return converter.convertToHtml(document).then(function(result) {
+ var expectedOutput = '<p>Knock knock<sup><a href="#doc-42-footnote-4" id="doc-42-footnote-ref-4">[1]</a></sup></p>' +
+ '<ol><li id="doc-42-footnote-4"><p>Who\'s there? <a href="#doc-42-footnote-ref-4">↑</a></p></li></ol>';
+ assert.equal(result.value, expectedOutput);
+ });
+ });
+
+ test('images are written with data URIs', function() {
+ var imageBuffer = new Buffer("Not an image at all!");
+ var image = new documents.Image({
+ readImage: function(encoding) {
+ return promises.when(imageBuffer.toString(encoding));
+ },
+ contentType: "image/png"
+ });
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(image).then(function(result) {
+ assert.equal(result.value, '<img src="data:image/png;base64,' + imageBuffer.toString("base64") + '" />');
+ });
+ });
+
+ test('images have alt attribute if available', function() {
+ var imageBuffer = new Buffer("Not an image at all!");
+ var image = new documents.Image({
+ readImage: function() {
+ return promises.when(imageBuffer);
+ },
+ altText: "It's a hat"
+ });
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(image)
+ .then(function(result) {
+ return xml.readString(result.value);
+ })
+ .then(function(htmlImageElementDocument) {
+ var htmlImageElement = htmlImageElementDocument.root;
+ assert.equal(htmlImageElement.attributes.alt, "It's a hat");
+ });
+ });
+
+ test('can add custom handler for images', function() {
+ var imageBuffer = new Buffer("Not an image at all!");
+ var image = new documents.Image({
+ readImage: function(encoding) {
+ return promises.when(imageBuffer.toString(encoding));
+ },
+ contentType: "image/png"
+ });
+ var converter = new DocumentConverter({
+ convertImage: function(element, messages) {
+ return element.read("utf8").then(function(altText) {
+ return [Html.selfClosingElement("img", {alt: altText})];
+ });
+ }
+ });
+ return converter.convertToHtml(image).then(function(result) {
+ assert.equal(result.value, '<img alt="Not an image at all!" />');
+ });
+ });
+
+ test('long documents do not cause stack overflow', function() {
+ var paragraphs = [];
+ for (var i = 0; i < 1000; i++) {
+ paragraphs.push(paragraphOfText("Hello."));
+ }
+ var document = new documents.Document(paragraphs);
+ var converter = new DocumentConverter();
+ return converter.convertToHtml(document).then(function(result) {
+ assert.equal(result.value.indexOf("<p>Hello.</p>"), 0);
+ });
+ });
+});
+
+function paragraphOfText(text, styleId, styleName) {
+ var run = runOfText(text);
+ return new documents.Paragraph([run], {
+ styleId: styleId,
+ styleName: styleName
+ });
+}
+
+function runOfText(text, properties) {
+ var textElement = new documents.Text(text);
+ return new documents.Run([textElement], properties);
+}
diff --git a/test/docx/body-reader.tests.js b/test/docx/body-reader.tests.js
new file mode 100644
index 0000000..0a808b0
--- /dev/null
+++ b/test/docx/body-reader.tests.js
@@ -0,0 +1,724 @@
+var assert = require("assert");
+var path = require("path");
+
+var BodyReader = require("../../lib/docx/body-reader").BodyReader;
+var documents = require("../../lib/documents");
+var xml = require("../../lib/xml");
+var XmlElement = xml.Element;
+var Numbering = require("../../lib/docx/numbering-xml").Numbering;
+var Styles = require("../../lib/docx/styles-reader").Styles;
+var warning = require("../../lib/results").warning;
+
+var testing = require("../testing");
+var test = testing.test;
+var createFakeDocxFile = testing.createFakeDocxFile;
+
+function readXmlElement(element, options) {
+ options = Object.create(options || {});
+ options.styles = options.styles || new Styles({}, {});
+ return new BodyReader(options).readXmlElement(element);
+}
+
+function readXmlElementValue(element, options) {
+ var result = readXmlElement(element, options);
+ assert.deepEqual(result.messages, []);
+ return result.value;
+}
+
+var fakeContentTypes = {
+ findContentType: function(filePath) {
+ var extensionTypes = {
+ ".png": "image/png",
+ ".emf": "image/x-emf"
+ };
+ return extensionTypes[path.extname(filePath)];
+ }
+};
+
+describe("readXmlElement: ", function() {
+ test("paragraph has no style if it has no properties", function() {
+ var paragraphXml = new XmlElement("w:p", {}, []);
+ var paragraph = readXmlElementValue(paragraphXml);
+ assert.deepEqual(paragraph.styleId, null);
+ });
+
+ test("paragraph has style ID and name read from paragraph properties if present", function() {
+ var styleXml = new XmlElement("w:pStyle", {"w:val": "Heading1"}, []);
+ var propertiesXml = new XmlElement("w:pPr", {}, [styleXml]);
+ var paragraphXml = new XmlElement("w:p", {}, [propertiesXml]);
+
+ var styles = new Styles({"Heading1": {name: "Heading 1"}}, {});
+
+ var paragraph = readXmlElementValue(paragraphXml, {styles: styles});
+ assert.deepEqual(paragraph.styleId, "Heading1");
+ assert.deepEqual(paragraph.styleName, "Heading 1");
+ });
+
+ test("warning is emitted when paragraph style cannot be found", function() {
+ var styleXml = new XmlElement("w:pStyle", {"w:val": "Heading1"}, []);
+ var propertiesXml = new XmlElement("w:pPr", {}, [styleXml]);
+ var paragraphXml = new XmlElement("w:p", {}, [propertiesXml]);
+
+ var styles = new Styles({}, {});
+
+ var result = readXmlElement(paragraphXml, {styles: styles});
+ var paragraph = result.value;
+ assert.deepEqual(paragraph.styleId, "Heading1");
+ assert.deepEqual(paragraph.styleName, null);
+ assert.deepEqual(result.messages, [warning("Paragraph style with ID Heading1 was referenced but not defined in the document")]);
+ });
+
+ test("paragraph has justification read from paragraph properties if present", function() {
+ var justificationXml = new XmlElement("w:jc", {"w:val": "center"}, []);
+ var propertiesXml = new XmlElement("w:pPr", {}, [justificationXml]);
+ var paragraphXml = new XmlElement("w:p", {}, [propertiesXml]);
+ var paragraph = readXmlElementValue(paragraphXml);
+ assert.deepEqual(paragraph.alignment, "center");
+ });
+
+ test("paragraph has numbering properties from paragraph properties if present", function() {
+ var numberingPropertiesXml = new XmlElement("w:numPr", {}, [
+ new XmlElement("w:ilvl", {"w:val": "1"}),
+ new XmlElement("w:numId", {"w:val": "42"})
+ ]);
+ var propertiesXml = new XmlElement("w:pPr", {}, [numberingPropertiesXml]);
+ var paragraphXml = new XmlElement("w:p", {}, [propertiesXml]);
+
+ var numbering = new Numbering({"42": {"1": {isOrdered: true, level: "1"}}});
+
+ var paragraph = readXmlElementValue(paragraphXml, {numbering: numbering});
+ assert.deepEqual(paragraph.numbering, {level: "1", isOrdered: true});
+ });
+
+ test("numbering properties are converted to numbering at specified level", function() {
+ var numberingPropertiesXml = new XmlElement("w:numPr", {}, [
+ new XmlElement("w:ilvl", {"w:val": "1"}),
+ new XmlElement("w:numId", {"w:val": "42"})
+ ]);
+
+ var numbering = new Numbering({"42": {"1": {isOrdered: true, level: "1"}}});
+
+ var reader = new BodyReader({
+ numbering: numbering
+ });
+ var numberingLevel = reader._readNumberingProperties(numberingPropertiesXml);
+ assert.deepEqual(numberingLevel, {level: "1", isOrdered: true});
+ });
+
+ test("numbering properties are ignored if w:ilvl is missing", function() {
+ var numberingPropertiesXml = new XmlElement("w:numPr", {}, [
+ new XmlElement("w:numId", {"w:val": "42"})
+ ]);
+
+ var numbering = new Numbering({"42": {"1": {isOrdered: true, level: "1"}}});
+
+ var reader = new BodyReader({
+ numbering: numbering
+ });
+ var numberingLevel = reader._readNumberingProperties(numberingPropertiesXml);
+ assert.equal(numberingLevel, null);
+ });
+
+ test("numbering properties are ignored if w:numId is missing", function() {
+ var numberingPropertiesXml = new XmlElement("w:numPr", {}, [
+ new XmlElement("w:ilvl", {"w:val": "1"})
+ ]);
+
+ var numbering = new Numbering({"42": {"1": {isOrdered: true, level: "1"}}});
+
+ var reader = new BodyReader({
+ numbering: numbering
+ });
+ var numberingLevel = reader._readNumberingProperties(numberingPropertiesXml);
+ assert.equal(numberingLevel, null);
+ });
+
+ test("run has no style if it has no properties", function() {
+ var runXml = runWithProperties([]);
+ var run = readXmlElementValue(runXml);
+ assert.deepEqual(run.styleId, null);
+ });
+
+ test("run has style ID and name read from run properties if present", function() {
+ var runStyleXml = new XmlElement("w:rStyle", {"w:val": "Heading1Char"});
+ var runXml = runWithProperties([runStyleXml]);
+
+ var styles = new Styles({}, {"Heading1Char": {name: "Heading 1 Char"}});
+
+ var run = readXmlElementValue(runXml, {styles: styles});
+ assert.deepEqual(run.styleId, "Heading1Char");
+ assert.deepEqual(run.styleName, "Heading 1 Char");
+ });
+
+ test("warning is emitted when run style cannot be found", function() {
+ var runStyleXml = new XmlElement("w:rStyle", {"w:val": "Heading1Char"});
+ var runXml = runWithProperties([runStyleXml]);
+
+ var styles = new Styles({}, {});
+
+ var result = readXmlElement(runXml, {styles: styles});
+ var run = result.value;
+ assert.deepEqual(run.styleId, "Heading1Char");
+ assert.deepEqual(run.styleName, null);
+ assert.deepEqual(result.messages, [warning("Run style with ID Heading1Char was referenced but not defined in the document")]);
+ });
+
+ test("isBold is false if bold element is not present", function() {
+ var runXml = runWithProperties([]);
+ var run = readXmlElementValue(runXml);
+ assert.deepEqual(run.isBold, false);
+ });
+
+ test("isBold is true if bold element is present", function() {
+ var boldXml = new XmlElement("w:b");
+ var runXml = runWithProperties([boldXml]);
+ var run = readXmlElementValue(runXml);
+ assert.equal(run.isBold, true);
+ });
+
+ test("isUnderline is false if underline element is not present", function() {
+ var runXml = runWithProperties([]);
+ var run = readXmlElementValue(runXml);
+ assert.deepEqual(run.isUnderline, false);
+ });
+
+ test("isUnderline is true if underline element is present", function() {
+ var underlineXml = new XmlElement("w:u");
+ var runXml = runWithProperties([underlineXml]);
+ var run = readXmlElementValue(runXml);
+ assert.equal(run.isUnderline, true);
+ });
+
+ test("isStrikethrough is false if strikethrough element is not present", function() {
+ var runXml = runWithProperties([]);
+ var run = readXmlElementValue(runXml);
+ assert.deepEqual(run.isStrikethrough, false);
+ });
+
+ test("isStrikethrough is true if strikethrough element is present", function() {
+ var strikethroughXml = new XmlElement("w:strike");
+ var runXml = runWithProperties([strikethroughXml]);
+ var run = readXmlElementValue(runXml);
+ assert.equal(run.isStrikethrough, true);
+ });
+
+ test("isItalic is false if bold element is not present", function() {
+ var runXml = runWithProperties([]);
+ var run = readXmlElementValue(runXml);
+ assert.deepEqual(run.isItalic, false);
+ });
+
+ test("isItalic is true if bold element is present", function() {
+ var italicXml = new XmlElement("w:i");
+ var runXml = runWithProperties([italicXml]);
+ var run = readXmlElementValue(runXml);
+ assert.equal(run.isItalic, true);
+ });
+
+ test("run has baseline vertical alignment by default", function() {
+ var runXml = runWithProperties([]);
+ var run = readXmlElementValue(runXml);
+ assert.deepEqual(run.verticalAlignment, documents.verticalAlignment.baseline);
+ });
+
+ test("run has vertical alignment read from properties", function() {
+ var verticalAlignmentXml = new XmlElement("w:vertAlign", {"w:val": "superscript"});
+ var runXml = runWithProperties([verticalAlignmentXml]);
+
+ var run = readXmlElementValue(runXml);
+ assert.deepEqual(run.verticalAlignment, documents.verticalAlignment.superscript);
+ });
+
+ test("run properties not included as child of run", function() {
+ var runStyleXml = new XmlElement("w:rStyle");
+ var runPropertiesXml = new XmlElement("w:rPr", {}, [runStyleXml]);
+ var runXml = new XmlElement("w:r", {}, [runPropertiesXml]);
+ var result = readXmlElement(runXml);
+ assert.deepEqual(result.value.children, []);
+ });
+
+ test("w:tab is read as document tab element", function() {
+ var tabXml = new XmlElement("w:tab");
+ var result = readXmlElement(tabXml);
+ assert.deepEqual(result.value, new documents.Tab());
+ });
+
+ test("w:tbl is read as document table element", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ new XmlElement("w:tr", {}, [
+ new XmlElement("w:tc", {}, [
+ new XmlElement("w:p", {}, [])
+ ])
+ ])
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ new documents.TableRow([
+ new documents.TableCell([
+ new documents.Paragraph([])
+ ])
+ ])
+ ]));
+ });
+
+ test("w:gridSpan is read as colSpan for table cell", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ new XmlElement("w:tr", {}, [
+ new XmlElement("w:tc", {}, [
+ new XmlElement("w:tcPr", {}, [
+ new XmlElement("w:gridSpan", {"w:val": "2"})
+ ]),
+ new XmlElement("w:p", {}, [])
+ ])
+ ])
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ new documents.TableRow([
+ new documents.TableCell([
+ new documents.Paragraph([])
+ ], {colSpan: 2})
+ ])
+ ]));
+ });
+
+ test("w:vMerge is read as rowSpan for table cell", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(emptyCell()),
+ row(emptyCell(vMerge("restart"))),
+ row(emptyCell(vMerge("continue"))),
+ row(emptyCell(vMerge("continue"))),
+ row(emptyCell())
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ docRow([docEmptyCell()]),
+ docRow([docEmptyCell({rowSpan: 3})]),
+ docRow([]),
+ docRow([]),
+ docRow([docEmptyCell()])
+ ]));
+ });
+
+ test("w:vMerge without val is treated as continue", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(emptyCell(vMerge("restart"))),
+ row(emptyCell(vMerge()))
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ docRow([docEmptyCell({rowSpan: 2})]),
+ docRow([])
+ ]));
+ });
+
+ test("w:vMerge accounts for cells spanning columns", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(emptyCell(), emptyCell(), emptyCell(vMerge("restart"))),
+ row(emptyCell(gridSpan("2")), emptyCell(vMerge("continue"))),
+ row(emptyCell(), emptyCell(), emptyCell(vMerge("continue"))),
+ row(emptyCell(), emptyCell(), emptyCell())
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ docRow([docEmptyCell(), docEmptyCell(), docEmptyCell({rowSpan: 3})]),
+ docRow([docEmptyCell({colSpan: 2})]),
+ docRow([docEmptyCell(), docEmptyCell()]),
+ docRow([docEmptyCell(), docEmptyCell(), docEmptyCell()])
+ ]));
+ });
+
+ test("no vertical cell merging if merged cells do not line up", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(emptyCell(gridSpan("2"), vMerge("restart"))),
+ row(emptyCell(), emptyCell(vMerge("continue")))
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ docRow([docEmptyCell({colSpan: 2})]),
+ docRow([docEmptyCell(), docEmptyCell()])
+ ]));
+ });
+
+ test("warning if non-row in table", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ new XmlElement("w:p")
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.messages, [warning("unexpected non-row element in table, cell merging may be incorrect")]);
+ });
+
+ test("warning if non-cell in table row", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(new XmlElement("w:p"))
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.messages, [warning("unexpected non-cell element in table row, cell merging may be incorrect")]);
+ });
+
+ function row() {
+ return new XmlElement("w:tr", {}, Array.prototype.slice.call(arguments));
+ }
+
+ function emptyCell() {
+ return new XmlElement("w:tc", {}, [
+ new XmlElement("w:tcPr", {}, Array.prototype.slice.call(arguments))
+ ]);
+ }
+
+ function vMerge(val) {
+ return new XmlElement("w:vMerge", {"w:val": val}, []);
+ }
+
+ function gridSpan(val) {
+ return new XmlElement("w:gridSpan", {"w:val": val});
+ }
+
+ function docRow(children) {
+ return new documents.TableRow(children);
+ }
+
+ function docEmptyCell(properties) {
+ return new documents.TableCell([], properties);
+ }
+
+ test("emits warning on unrecognised element", function() {
+ var unrecognisedElement = new XmlElement("w:not-an-element");
+ var result = readXmlElement(unrecognisedElement);
+ assert.deepEqual(
+ result.messages,
+ [{
+ type: "warning",
+ message: "An unrecognised element was ignored: w:not-an-element"
+ }]
+ );
+ });
+
+ test("w:bookmarkStart is read as a bookmarkStart", function() {
+ var bookmarkStart = new XmlElement("w:bookmarkStart", {"w:name": "_Peter", "w:id": "42"});
+ var result = readXmlElement(bookmarkStart);
+ assert.deepEqual(result.value.name, "_Peter");
+ assert.deepEqual(result.value.type, "bookmarkStart");
+ });
+
+ test('_GoBack bookmark is ignored', function() {
+ var bookmarkStart = new XmlElement("w:bookmarkStart", {"w:name": "_GoBack"});
+ var result = readXmlElement(bookmarkStart);
+ assert.deepEqual(result.value, []);
+ });
+
+ test("can read imagedata elements with r:id attribute", function() {
+ var imagedataElement = new XmlElement("v:imagedata", {"r:id": "rId5", "o:title": "It's a hat"});
+
+ var imageBuffer = new Buffer("Not an image at all!");
+ var reader = new BodyReader({
+ relationships: {
+ "rId5": {target: "media/hat.png"}
+ },
+ contentTypes: fakeContentTypes,
+ docxFile: createFakeDocxFile({
+ "word/media/hat.png": imageBuffer
+ })
+ });
+ var result = reader.readXmlElement(imagedataElement);
+ assert.deepEqual(result.messages, []);
+ var element = result.value;
+ assert.equal("image", element.type);
+ assert.equal(element.altText, "It's a hat");
+ assert.equal(element.contentType, "image/png");
+ return element.read()
+ .then(function(readValue) {
+ assert.equal(readValue, imageBuffer);
+ });
+ });
+
+ test("can read inline pictures", function() {
+ var drawing = createInlineImage({
+ blip: createEmbeddedBlip("rId5"),
+ description: "It's a hat"
+ });
+
+ var imageBuffer = new Buffer("Not an image at all!");
+ var reader = new BodyReader({
+ relationships: {
+ "rId5": {target: "media/hat.png"}
+ },
+ contentTypes: fakeContentTypes,
+ docxFile: createFakeDocxFile({
+ "word/media/hat.png": imageBuffer
+ })
+ });
+ var result = reader.readXmlElement(drawing);
+ assert.deepEqual(result.messages, []);
+ var element = single(result.value);
+ assert.equal("image", element.type);
+ assert.equal(element.altText, "It's a hat");
+ assert.equal(element.contentType, "image/png");
+ return element.read()
+ .then(function(readValue) {
+ assert.equal(readValue, imageBuffer);
+ });
+ });
+
+ test("can read anchored pictures", function() {
+ var drawing = new XmlElement("w:drawing", {}, [
+ new XmlElement("wp:anchor", {}, [
+ new XmlElement("wp:docPr", {descr: "It's a hat"}),
+ new XmlElement("a:graphic", {}, [
+ new XmlElement("a:graphicData", {}, [
+ new XmlElement("pic:pic", {}, [
+ new XmlElement("pic:blipFill", {}, [
+ new XmlElement("a:blip", {"r:embed": "rId5"})
+ ])
+ ])
+ ])
+ ])
+ ])
+ ]);
+
+ var imageBuffer = new Buffer("Not an image at all!");
+ var reader = new BodyReader({
+ relationships: {
+ "rId5": {target: "media/hat.png"}
+ },
+ contentTypes: fakeContentTypes,
+ docxFile: createFakeDocxFile({
+ "word/media/hat.png": imageBuffer
+ })
+ });
+ var result = reader.readXmlElement(drawing);
+ assert.deepEqual(result.messages, []);
+ var element = single(result.value);
+ assert.equal("image", element.type);
+ assert.equal(element.altText, "It's a hat");
+ return element.read()
+ .then(function(readValue) {
+ assert.equal(readValue, imageBuffer);
+ });
+ });
+
+ test("can read linked pictures", function() {
+ var drawing = createInlineImage({
+ blip: createLinkedBlip("rId5"),
+ description: "It's a hat"
+ });
+
+ var imageBuffer = new Buffer("Not an image at all!");
+ var reader = new BodyReader({
+ relationships: {
+ "rId5": {target: "file:///media/hat.png"}
+ },
+ contentTypes: fakeContentTypes,
+ files: testing.createFakeFiles({
+ "file:///media/hat.png": imageBuffer
+ })
+ });
+ var result = reader.readXmlElement(drawing);
+ assert.deepEqual(result.messages, []);
+ var element = single(result.value);
+ assert.equal("image", element.type);
+ assert.equal(element.altText, "It's a hat");
+ assert.equal(element.contentType, "image/png");
+ return element.read()
+ .then(function(readValue) {
+ assert.equal(readValue, imageBuffer);
+ });
+ });
+
+ test("warning if unsupported image type", function() {
+ var drawing = createInlineImage({
+ blip: createEmbeddedBlip("rId5"),
+ description: "It's a hat"
+ });
+
+ var imageBuffer = new Buffer("Not an image at all!");
+ var reader = new BodyReader({
+ relationships: {
+ "rId5": {target: "media/hat.emf"}
+ },
+ contentTypes: fakeContentTypes,
+ docxFile: createFakeDocxFile({
+ "word/media/hat.emf": imageBuffer
+ })
+ });
+ var result = reader.readXmlElement(drawing);
+ assert.deepEqual(result.messages, [warning("Image of type image/x-emf is unlikely to display in web browsers")]);
+ var element = single(result.value);
+ assert.equal(element.contentType, "image/x-emf");
+ });
+
+ test("no elements created if image cannot be found in w:drawing", function() {
+ var drawing = new XmlElement("w:drawing", {}, []);
+
+ var reader = new BodyReader({});
+ var result = reader.readXmlElement(drawing);
+ assert.deepEqual(result.messages, []);
+ assert.deepEqual(result.value, []);
+ });
+
+ test("no elements created if image cannot be found in wp:inline", function() {
+ var drawing = new XmlElement("wp:inline", {}, []);
+
+ var reader = new BodyReader({});
+ var result = reader.readXmlElement(drawing);
+ assert.deepEqual(result.messages, []);
+ assert.deepEqual(result.value, []);
+ });
+
+ test("children of w:ins are converted normally", function() {
+ var runXml = new XmlElement("w:r", {}, []);
+ var insXml = new XmlElement("w:ins", {}, [runXml]);
+ var result = readXmlElement(insXml);
+ assert.deepEqual(result.value[0].type, "run");
+ });
+
+ test("children of w:smartTag are converted normally", function() {
+ var runXml = new XmlElement("w:r", {}, []);
+ var smartTagXml = new XmlElement("w:smartTag", {}, [runXml]);
+ var result = readXmlElement(smartTagXml);
+ assert.deepEqual(result.value[0].type, "run");
+ });
+
+ test("w:hyperlink is read as document hyperlink if it has a relationship ID", function() {
+ var runXml = new XmlElement("w:r", {}, []);
+ var hyperlinkXml = new XmlElement("w:hyperlink", {"r:id": "r42"}, [runXml]);
+ var relationships = {
+ "r42": {target: "http://example.com"}
+ };
+ var result = readXmlElement(hyperlinkXml, {relationships: relationships});
+ assert.deepEqual(result.value.href, "http://example.com");
+ assert.deepEqual(result.value.children[0].type, "run");
+ });
+
+ test("w:hyperlink is read as document hyperlink if it has an anchor", function() {
+ var runXml = new XmlElement("w:r", {}, []);
+ var hyperlinkXml = new XmlElement("w:hyperlink", {"w:anchor": "_Peter"}, [runXml]);
+ var result = readXmlElement(hyperlinkXml);
+ assert.deepEqual(result.value.anchor, "_Peter");
+ assert.deepEqual(result.value.children[0].type, "run");
+ });
+
+ test("w:hyperlink is ignored if it does not have a relationship ID nor anchor", function() {
+ var runXml = new XmlElement("w:r", {}, []);
+ var hyperlinkXml = new XmlElement("w:hyperlink", {}, [runXml]);
+ var result = readXmlElement(hyperlinkXml);
+ assert.deepEqual(result.value[0].type, "run");
+ });
+
+ test("w:br is read as line break", function() {
+ var breakXml = new XmlElement("w:br", {}, []);
+ var result = readXmlElement(breakXml);
+ assert.deepEqual(result.value.type, "lineBreak");
+ assert.deepEqual(result.messages, []);
+ });
+
+ test("w:bookmarkEnd is ignored without warning", function() {
+ var ignoredElement = new XmlElement("w:bookmarkEnd");
+ var result = readXmlElement(ignoredElement);
+ assert.deepEqual(result.messages, []);
+ assert.deepEqual([], result.value);
+ });
+
+ test("warning on breaks that aren't line breaks", function() {
+ var breakXml = new XmlElement("w:br", {"w:type": "page"}, []);
+ var result = readXmlElement(breakXml);
+ assert.deepEqual(result.value, []);
+ assert.deepEqual(result.messages, [warning("Unsupported break type: page")]);
+ });
+
+ test("text boxes have content appended after containing paragraph", function() {
+ var textbox = new XmlElement("w:pict", {}, [
+ new XmlElement("v:shape", {}, [
+ new XmlElement("v:textbox", {}, [
+ new XmlElement("w:txbxContent", {}, [
+ paragraphWithStyleId("textbox-content")
+ ])
+ ])
+ ])
+ ]);
+ var paragraph = new XmlElement("w:p", {}, [
+ new XmlElement("w:r", {}, [textbox])
+ ]);
+ var result = readXmlElement(paragraph);
+ assert.deepEqual(result.value[1].styleId, "textbox-content");
+ });
+
+ test("mc:Fallback is used when mc:AlternateContent is read", function() {
+ var styles = new Styles({"first": {name: "First"}, "second": {name: "Second"}}, {});
+ var textbox = new XmlElement("mc:AlternateContent", {}, [
+ new XmlElement("mc:Choice", {"Requires": "wps"}, [
+ paragraphWithStyleId("first")
+ ]),
+ new XmlElement("mc:Fallback", {}, [
+ paragraphWithStyleId("second")
+ ])
+ ]);
+ var result = readXmlElement(textbox, {styles: styles});
+ assert.deepEqual(result.value[0].styleId, "second");
+ });
+
+ test("w:sdtContent is used when w:sdt is read", function() {
+ var element = xml.element("w:sdt", {}, [
+ xml.element("w:sdtContent", {}, [
+ xml.element("w:t", {}, [xml.text("Blackdown")])
+ ])
+ ]);
+ var result = readXmlElement(element);
+ assert.deepEqual(result.value, [new documents.Text("Blackdown")]);
+ });
+
+ test("text nodes are ignored when reading children", function() {
+ var runXml = new XmlElement("w:r", {}, [xml.text("[text]")]);
+ var run = readXmlElementValue(runXml);
+ assert.deepEqual(run, new documents.Run([]));
+ });
+});
+
+function paragraphWithStyleId(styleId) {
+ return new XmlElement("w:p", {}, [
+ new XmlElement("w:pPr", {}, [
+ new XmlElement("w:pStyle", {"w:val": styleId}, [])
+ ])
+ ]);
+}
+
+function runWithProperties(children) {
+ return new XmlElement("w:r", {}, [createRunPropertiesXml(children)]);
+}
+
+function createRunPropertiesXml(children) {
+ return new XmlElement("w:rPr", {}, children);
+}
+
+function single(array) {
+ if (array.length === 1) {
+ return array[0];
+ } else {
+ throw new Error("Array has " + array.length + " elements");
+ }
+}
+
+function createInlineImage(options) {
+ return new XmlElement("w:drawing", {}, [
+ new XmlElement("wp:inline", {}, [
+ new XmlElement("wp:docPr", {descr: options.description}),
+ new XmlElement("a:graphic", {}, [
+ new XmlElement("a:graphicData", {}, [
+ new XmlElement("pic:pic", {}, [
+ new XmlElement("pic:blipFill", {}, [
+ options.blip
+ ])
+ ])
+ ])
+ ])
+ ])
+ ]);
+}
+
+function createEmbeddedBlip(relationshipId) {
+ return new XmlElement("a:blip", {"r:embed": relationshipId});
+}
+
+function createLinkedBlip(relationshipId) {
+ return new XmlElement("a:blip", {"r:link": relationshipId});
+}
diff --git a/test/docx/content-types-reader.tests.js b/test/docx/content-types-reader.tests.js
new file mode 100644
index 0000000..a223d09
--- /dev/null
+++ b/test/docx/content-types-reader.tests.js
@@ -0,0 +1,47 @@
+var assert = require("assert");
+
+var readContentTypesFromXml = require("../../lib/docx/content-types-reader").readContentTypesFromXml;
+var XmlElement = require("../../lib/xml").Element;
+var test = require("../testing").test;
+
+
+describe('readContentTypesFromXml', function() {
+ test('reads default-per-extension from XML', function() {
+ var contentTypes = readContentTypesFromXml({
+ root: new XmlElement("content-types:Types", {}, [
+ new XmlElement("content-types:Default", {Extension: "png", ContentType: "image/png"})
+ ])
+ });
+ assert.equal(contentTypes.findContentType("word/media/hat.png"), "image/png");
+ });
+
+ test('reads overrides in preference to defaults', function() {
+ var contentTypes = readContentTypesFromXml({
+ root: new XmlElement("content-types:Types", {}, [
+ new XmlElement("content-types:Default", {Extension: "png", ContentType: "image/png"}),
+ new XmlElement("content-types:Override", {PartName: "/word/media/hat.png", ContentType: "image/hat"})
+ ])
+ });
+ assert.equal(contentTypes.findContentType("word/media/hat.png"), "image/hat");
+ });
+
+ test('fallback content types have common image types', function() {
+ var contentTypes = readContentTypesFromXml({
+ root: new XmlElement("content-types:Types", {}, [])
+ });
+ assert.equal(contentTypes.findContentType("word/media/hat.png"), "image/png");
+ assert.equal(contentTypes.findContentType("word/media/hat.gif"), "image/gif");
+ assert.equal(contentTypes.findContentType("word/media/hat.jpg"), "image/jpeg");
+ assert.equal(contentTypes.findContentType("word/media/hat.jpeg"), "image/jpeg");
+ assert.equal(contentTypes.findContentType("word/media/hat.bmp"), "image/bmp");
+ assert.equal(contentTypes.findContentType("word/media/hat.tif"), "image/tiff");
+ assert.equal(contentTypes.findContentType("word/media/hat.tiff"), "image/tiff");
+ });
+
+ test('fallback content types are case insensitive on extension', function() {
+ var contentTypes = readContentTypesFromXml({
+ root: new XmlElement("content-types:Types", {}, [])
+ });
+ assert.equal(contentTypes.findContentType("word/media/hat.PnG"), "image/png");
+ });
+});
diff --git a/test/docx/docx-reader.tests.js b/test/docx/docx-reader.tests.js
new file mode 100644
index 0000000..69ade7e
--- /dev/null
+++ b/test/docx/docx-reader.tests.js
@@ -0,0 +1,42 @@
+var assert = require("assert");
+
+var docxReader = require("../../lib/docx/docx-reader");
+var documents = require("../../lib/documents");
+
+var testing = require("../testing");
+var test = testing.test;
+var testData = testing.testData;
+var createFakeDocxFile = testing.createFakeDocxFile;
+
+
+describe("docx-reader", function() {
+ test("can read document with single paragraph with single run of text", function() {
+ var expectedDocument = documents.Document([
+ documents.Paragraph([
+ documents.Run([
+ documents.Text("Hello.")
+ ])
+ ])
+ ]);
+ var docxFile = createFakeDocxFile({
+ "word/document.xml": testData("simple/word/document.xml")
+ });
+ return docxReader.read(docxFile).then(function(result) {
+ assert.deepEqual(expectedDocument, result.value);
+ });
+ });
+
+ test("hyperlink hrefs are read from relationships file", function() {
+ var docxFile = createFakeDocxFile({
+ "word/document.xml": testData("hyperlinks/word/document.xml"),
+ "word/_rels/document.xml.rels": testData("hyperlinks/word/_rels/document.xml.rels")
+ });
+ return docxReader.read(docxFile).then(function(result) {
+ var paragraph = result.value.children[0];
+ assert.equal(1, paragraph.children.length);
+ var hyperlink = paragraph.children[0];
+ assert.equal(hyperlink.href, "http://www.example.com");
+ assert.equal(hyperlink.children.length, 1);
+ });
+ });
+});
diff --git a/test/docx/files.tests.js b/test/docx/files.tests.js
new file mode 100644
index 0000000..2a0919c
--- /dev/null
+++ b/test/docx/files.tests.js
@@ -0,0 +1,94 @@
+var path = require("path");
+var fs = require("fs");
+var assert = require("assert");
+
+var promises = require("../../lib/promises");
+var Files = require("../../lib/docx/files").Files;
+var uriToPath = require("../../lib/docx/files").uriToPath;
+
+var testing = require("../testing");
+var test = testing.test;
+
+var readFile = promises.promisify(fs.readFile.bind(fs));
+
+
+describe("Files", function() {
+ test("can open files with file URI", function() {
+ var filePath = path.resolve(testing.testPath("tiny-picture.png"));
+ var files = new Files(null);
+ return files.read("file:///" + filePath.replace(/^\//, ""), "base64").then(function(contents) {
+ return readFile(filePath, "base64").then(function(expectedContents) {
+ assert.deepEqual(contents, expectedContents);
+ });
+ });
+ });
+
+ test("can open files with relative URI", function() {
+ var filePath = path.resolve(testing.testPath("tiny-picture.png"));
+ var files = new Files(testing.testPath("."));
+ return files.read("tiny-picture.png", "base64").then(function(contents) {
+ return readFile(filePath, "base64").then(function(expectedContents) {
+ assert.deepEqual(contents, expectedContents);
+ });
+ });
+ });
+
+ test("given base is not set when opening relative uri then error is raised", function() {
+ var files = new Files(null);
+ return assertError(files.read("not-a-real-file.png", "base64"), function(err) {
+ assert.equal(err.message, "could not find external image 'not-a-real-file.png', path of input document is unknown");
+ });
+ });
+
+ test("error if relative uri cannot be opened", function() {
+ var files = new Files("/tmp");
+ return assertError(files.read("not-a-real-file.png", "base64"), function(err) {
+ assertRegex(err.message, /could not open external image: 'not-a-real-file.png' \(document directory: '\/tmp'\)\nENOENT.*\/tmp\/not-a-real-file.png.*/);
+ });
+ });
+});
+
+function assertError(promise, func) {
+ return promise.then(function() {
+ assert(false, "Expected error");
+ }, func);
+}
+
+function assertRegex(actual, expected) {
+ assert.ok(expected.test(actual), "Expected regex: " + expected + "\nbut was: " + actual);
+}
+
+
+describe("uriToPath", function() {
+ test("leading slash is retained on non-Windows file URIs", function() {
+ assert.equal(uriToPath("file:///a/b/c", "linux"), "/a/b/c");
+ assert.equal(uriToPath("file:///a/b/c", "win32"), "/a/b/c");
+ });
+
+ test("URI is unquoted", function() {
+ assert.equal(uriToPath("file:///a%20b"), "/a b");
+ });
+
+ test("when host is set to localhost then path can be found", function() {
+ assert.equal(uriToPath("file://localhost/a/b/c"), "/a/b/c");
+ });
+
+ test("when host is set but not localhost then path cannot be found", function() {
+ assert.throws(function() {
+ uriToPath("file://example/a/b/c");
+ }, /Could not convert URI to path: file:\/\/example\/a\/b\/c/);
+ });
+
+ test("leading slash is not dropped on Windows file URIs when platform is not Windows", function() {
+ assert.equal(uriToPath("file:///c:/a", "linux"), "/c:/a");
+ });
+
+ test("leading slash is dropped on Windows file URIs when platform is Windows", function() {
+ assert.equal(uriToPath("file:///c:/a", "win32"), "c:/a");
+ assert.equal(uriToPath("file:///C:/a", "win32"), "C:/a");
+ });
+
+ test("relative URI is unquoted", function() {
+ assert.equal(uriToPath("a%20b/c"), "a b/c");
+ });
+});
diff --git a/test/docx/notes-reader.tests.js b/test/docx/notes-reader.tests.js
new file mode 100644
index 0000000..79ffed3
--- /dev/null
+++ b/test/docx/notes-reader.tests.js
@@ -0,0 +1,39 @@
+var assert = require("assert");
+
+var createFootnotesReader = require("../../lib/docx/notes-reader").createFootnotesReader;
+var BodyReader = require("../../lib/docx/body-reader").BodyReader;
+var documents = require("../../lib/documents");
+var XmlElement = require("../../lib/xml").Element;
+var test = require("../testing").test;
+
+
+describe('readFootnotesXml', function() {
+ test('ID and body of footnote are read', function() {
+ var bodyReader = new BodyReader({});
+ var footnoteBody = [new XmlElement("w:p", {}, [])];
+ var footnotes = createFootnotesReader(bodyReader)({
+ root: new XmlElement("w:footnotes", {}, [
+ new XmlElement("w:footnote", {"w:id": "1"}, footnoteBody)
+ ])
+ });
+ assert.equal(footnotes.value.length, 1);
+ assert.deepEqual(footnotes.value[0].body, [new documents.Paragraph([])]);
+ assert.deepEqual(footnotes.value[0].noteId, "1");
+ });
+});
+
+footnoteTypeIsIgnored('continuationSeparator');
+footnoteTypeIsIgnored('separator');
+
+function footnoteTypeIsIgnored(type) {
+ describe('footnotes of type ' + type + ' are ignored', function() {
+ test('ID and body of footnote are read', function() {
+ var footnotes = createFootnotesReader()({
+ root: new XmlElement("w:footnotes", {}, [
+ new XmlElement("w:footnote", {"w:id": "1", "w:type": type}, [])
+ ])
+ });
+ assert.equal(footnotes.value.length, 0);
+ });
+ });
+}
diff --git a/test/docx/numbering-xml.tests.js b/test/docx/numbering-xml.tests.js
new file mode 100644
index 0000000..2fea5fb
--- /dev/null
+++ b/test/docx/numbering-xml.tests.js
@@ -0,0 +1,32 @@
+var duck = require("duck");
+
+var readNumberingXml = require("../../lib/docx/numbering-xml").readNumberingXml;
+var XmlElement = require("../../lib/xml").Element;
+var test = require("../testing").test;
+
+
+describe('readNumberingXml', function() {
+ test('w:num element inherits levels from w:abstractNum', function() {
+ var numbering = readNumberingXml({
+ root: new XmlElement("w:numbering", {}, [
+ new XmlElement("w:abstractNum", {"w:abstractNumId": "42"}, [
+ new XmlElement("w:lvl", {"w:ilvl": "0"}, [
+ new XmlElement("w:numFmt", {"w:val": "bullet"})
+ ]),
+ new XmlElement("w:lvl", {"w:ilvl": "1"}, [
+ new XmlElement("w:numFmt", {"w:val": "decimal"})
+ ])
+ ]),
+ new XmlElement("w:num", {"w:numId": "47"}, [
+ new XmlElement("w:abstractNumId", {"w:val": "42"})
+ ])
+ ])
+ });
+ duck.assertThat(numbering.findLevel("47", "0"), duck.hasProperties({
+ isOrdered: false
+ }));
+ duck.assertThat(numbering.findLevel("47", "1"), duck.hasProperties({
+ isOrdered: true
+ }));
+ });
+});
diff --git a/test/docx/office-xml-reader.tests.js b/test/docx/office-xml-reader.tests.js
new file mode 100644
index 0000000..9d202ba
--- /dev/null
+++ b/test/docx/office-xml-reader.tests.js
@@ -0,0 +1,26 @@
+var assert = require("assert");
+
+var xml = require("../../lib/xml");
+var officeXmlReader = require("../../lib/docx/office-xml-reader");
+var test = require("../testing").test;
+
+
+describe("office-xml-reader", function() {
+ test("mc:AlternateContent is replaced by contents of mc:Fallback", function() {
+ var xmlString =
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
+ '<numbering xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">' +
+ '<mc:AlternateContent>' +
+ '<mc:Choice Requires="w14">' +
+ '<choice/>' +
+ '</mc:Choice>' +
+ '<mc:Fallback>' +
+ '<fallback/>' +
+ '</mc:Fallback>' +
+ '</mc:AlternateContent>' +
+ '</numbering>';
+ return officeXmlReader.read(xmlString).then(function(element) {
+ assert.deepEqual(element.root.children, [xml.element("fallback")]);
+ });
+ });
+});
diff --git a/test/docx/style-map.tests.js b/test/docx/style-map.tests.js
new file mode 100644
index 0000000..55171f8
--- /dev/null
+++ b/test/docx/style-map.tests.js
@@ -0,0 +1,115 @@
+var assert = require("assert");
+
+var JSZip = require("jszip");
+
+var zipfile = require("../../lib/zipfile");
+var styleMap = require("../../lib/docx/style-map");
+var test = require("../testing").test;
+
+describe("style-map", function() {
+ test('reading embedded style map on document without embedded style map returns null', function() {
+ var zip = normalDocx();
+
+ return styleMap.readStyleMap(zip).then(function(contents) {
+ assert.equal(contents, null);
+ });
+ });
+
+ test('embedded style map can be read after being written', function() {
+ var zip = normalDocx();
+
+ return styleMap.writeStyleMap(zip, "p => h1").then(function() {
+ return styleMap.readStyleMap(zip).then(function(contents) {
+ assert.equal(contents, "p => h1");
+ });
+ });
+ });
+
+ test('embedded style map is written to separate file', function() {
+ var zip = normalDocx();
+
+ return styleMap.writeStyleMap(zip, "p => h1").then(function() {
+ return zip.read("mammoth/style-map", "utf8").then(function(contents) {
+ assert.equal(contents, "p => h1");
+ });
+ });
+ });
+
+ test('embedded style map is referenced in relationships', function() {
+ var zip = normalDocx();
+
+ return styleMap.writeStyleMap(zip, "p => h1").then(function() {
+ return zip.read("word/_rels/document.xml.rels", "utf8").then(function(contents) {
+ assert.equal(contents, expectedRelationshipsXml);
+ });
+ });
+ });
+
+ test('re-embedding style map replaces original', function() {
+ var zip = normalDocx();
+
+ return styleMap.writeStyleMap(zip, "p => h1").then(function() {
+ return styleMap.writeStyleMap(zip, "p => h2");
+ }).then(function() {
+ return zip.read("word/_rels/document.xml.rels", "utf8").then(function(contents) {
+ assert.equal(contents, expectedRelationshipsXml);
+ });
+ }).then(function() {
+ return styleMap.readStyleMap(zip).then(function(contents) {
+ assert.equal(contents, "p => h2");
+ });
+ });
+ });
+
+ test('embedded style map has override content type in [Content_Types].xml', function() {
+ var zip = normalDocx();
+
+ return styleMap.writeStyleMap(zip, "p => h1").then(function() {
+ return zip.read("[Content_Types].xml", "utf8").then(function(contents) {
+ assert.equal(contents, expectedContentTypesXml);
+ });
+ });
+ });
+
+ test('replacing style map keeps content type', function() {
+ var zip = normalDocx();
+
+ return styleMap.writeStyleMap(zip, "p => h1").then(function() {
+ return styleMap.writeStyleMap(zip, "p => h2");
+ }).then(function() {
+ return zip.read("[Content_Types].xml", "utf8").then(function(contents) {
+ assert.equal(contents, expectedContentTypesXml);
+ });
+ });
+ });
+
+});
+
+var expectedRelationshipsXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
+ '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>' +
+ '<Relationship Id="rMammothStyleMap" Type="http://schemas.zwobble.org/mammoth/style-map" Target="/mammoth/style-map"/>' +
+ '</Relationships>';
+
+var expectedContentTypesXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
+ '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
+ '<Default Extension="png" ContentType="image/png"/>' +
+ '<Override PartName="/mammoth/style-map" ContentType="text/prs.mammoth.style-map"/>' +
+ '</Types>';
+
+function normalDocx() {
+ var zip = new JSZip();
+ var originalRelationshipsXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
+ '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>' +
+ '</Relationships>';
+ var originalContentTypesXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
+ '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
+ '<Default Extension="png" ContentType="image/png"/>' +
+ '</Types>';
+ zip.file("word/_rels/document.xml.rels", originalRelationshipsXml);
+ zip.file("[Content_Types].xml", originalContentTypesXml);
+ var buffer = zip.generate({type: "arraybuffer"});
+ return zipfile.openArrayBuffer(buffer);
+}
+
diff --git a/test/docx/styles-reader.tests.js b/test/docx/styles-reader.tests.js
new file mode 100644
index 0000000..4ff19c7
--- /dev/null
+++ b/test/docx/styles-reader.tests.js
@@ -0,0 +1,92 @@
+var assert = require("assert");
+
+var readStylesXml = require("../../lib/docx/styles-reader").readStylesXml;
+var XmlElement = require("../../lib/xml").Element;
+var test = require("../testing").test;
+
+
+describe('readStylesXml', function() {
+ test('paragraph style is null if no style with that ID exists', function() {
+ var styles = readStylesXml({
+ root: new XmlElement("w:styles", {}, [])
+ });
+ assert.equal(styles.findParagraphStyleById("Heading1"), null);
+ });
+
+ test('paragraph style can be found by ID', function() {
+ var styles = readStylesXml({
+ root: new XmlElement("w:styles", {}, [
+ paragraphStyleElement("Heading1", "Heading 1")
+ ])
+ });
+ assert.equal(styles.findParagraphStyleById("Heading1").styleId, "Heading1");
+ });
+
+ test('character style can be found by ID', function() {
+ var styles = readStylesXml({
+ root: new XmlElement("w:styles", {}, [
+ characterStyleElement("Heading1Char", "Heading 1 Char")
+ ])
+ });
+ assert.equal(styles.findCharacterStyleById("Heading1Char").styleId, "Heading1Char");
+ });
+
+ test('paragraph and character styles are distinct', function() {
+ var styles = readStylesXml({
+ root: new XmlElement("w:styles", {}, [
+ paragraphStyleElement("Heading1", "Heading 1"),
+ characterStyleElement("Heading1Char", "Heading 1 Char")
+ ])
+ });
+ assert.equal(styles.findCharacterStyleById("Heading1"), null);
+ assert.equal(styles.findParagraphStyleById("Heading1Char"), null);
+ });
+
+ test('character and table styles are distinct', function() {
+ var styles = readStylesXml({
+ root: new XmlElement("w:styles", {}, [
+ styleElement("table", "Heading1", "Heading 1")
+ ])
+ });
+ assert.equal(styles.findCharacterStyleById("Heading1"), null);
+ });
+
+ test('styles include names', function() {
+ var styles = readStylesXml({
+ root: new XmlElement("w:styles", {}, [
+ paragraphStyleElement("Heading1", "Heading 1")
+ ])
+ });
+ assert.equal(styles.findParagraphStyleById("Heading1").name, "Heading 1");
+ });
+
+ test('style name is null if w:name element does not exist', function() {
+ var styles = readStylesXml({
+ root: new XmlElement("w:styles", {}, [
+ styleWithoutWNameElement("paragraph", "Heading1"),
+ styleWithoutWNameElement("character", "Heading1Char")
+ ])
+ });
+ assert.equal(styles.findParagraphStyleById("Heading1").name, null);
+ assert.equal(styles.findCharacterStyleById("Heading1Char").name, null);
+ });
+
+});
+
+function paragraphStyleElement(id, name) {
+ return styleElement("paragraph", id, name);
+}
+
+function characterStyleElement(id, name) {
+ return styleElement("character", id, name);
+}
+
+function styleElement(type, id, name) {
+ return new XmlElement("w:style", {"w:type": type, "w:styleId": id}, [
+ new XmlElement("w:name", {"w:val": name}, [])
+ ]);
+}
+
+function styleWithoutWNameElement(type, id) {
+ return new XmlElement("w:style", {"w:type": type, "w:styleId": id}, []);
+}
diff --git a/test/html-paths.tests.js b/test/html-paths.tests.js
new file mode 100644
index 0000000..76a77ef
--- /dev/null
+++ b/test/html-paths.tests.js
@@ -0,0 +1,22 @@
+var assert = require("assert");
+
+var test = require("./testing").test;
+var htmlPaths = require("../lib/html-paths");
+
+
+describe("html-paths", function() {
+ test("element can match multiple tag names", function() {
+ var pathPart = htmlPaths.element(["ul", "ol"]);
+ assert.ok(pathPart.matchesElement({tagName: "ul"}));
+ assert.ok(pathPart.matchesElement({tagName: "ol"}));
+ assert.ok(!pathPart.matchesElement({tagName: "p"}));
+ });
+
+ test("element matches if attributes are the same", function() {
+ var pathPart = htmlPaths.element(["p"], {"class": "tip"});
+ assert.ok(!pathPart.matchesElement({tagName: "p"}));
+ assert.ok(!pathPart.matchesElement({tagName: "p", attributes: {"class": "tip help"}}));
+ assert.ok(pathPart.matchesElement({tagName: "p", attributes: {"class": "tip"}}));
+ });
+});
+
diff --git a/test/html/simplify.tests.js b/test/html/simplify.tests.js
new file mode 100644
index 0000000..ccc5610
--- /dev/null
+++ b/test/html/simplify.tests.js
@@ -0,0 +1,115 @@
+var assert = require("assert");
+
+var _ = require("underscore");
+
+var test = require("../testing").test;
+var html = require("../../lib/html");
+var htmlPaths = require("../../lib/html-paths");
+
+var nonFreshElement = html.nonFreshElement;
+var text = html.text;
+var pathToNodes = html.pathToNodes;
+
+describe("simplify", function() {
+ test("empty text nodes are removed", function() {
+ assert.deepEqual(
+ simplifyNode(text("")),
+ []
+ );
+ });
+
+ test("elements with no children are removed", function() {
+ assert.deepEqual(
+ simplifyNode(nonFreshElement("p", {}, [])),
+ []
+ );
+ });
+
+ test("elements only containing empty nodes are removed", function() {
+ assert.deepEqual(
+ simplifyNode(nonFreshElement("p", {}, [text("")])),
+ []
+ );
+ });
+
+ test("empty children of element are removed", function() {
+ assert.deepEqual(
+ simplifyNode(nonFreshElement("p", {}, [text("Hello"), text("")])),
+ [nonFreshElement("p", {}, [text("Hello")])]
+ );
+ });
+
+ test("successive fresh elements are not collapsed", function() {
+ var path = htmlPaths.elements([
+ htmlPaths.element("p", {}, {fresh: true})
+ ]);
+ var original = concat(
+ pathToNodes(path, [text("Hello")]),
+ pathToNodes(path, [text(" there")])
+ );
+
+ assert.deepEqual(
+ html.simplify(original),
+ original);
+ });
+
+ test("successive plain non-fresh elements are collapsed if they have the same tag name", function() {
+ var path = htmlPaths.elements([
+ htmlPaths.element("p", {}, {fresh: false})
+ ]);
+ assert.deepEqual(
+ html.simplify(concat(
+ pathToNodes(path, [text("Hello")]),
+ pathToNodes(path, [text(" there")])
+ )),
+ pathToNodes(path, [text("Hello"), text(" there")])
+ );
+ });
+
+ test("non-fresh can collapse into preceding fresh element", function() {
+ var freshPath = htmlPaths.elements([
+ htmlPaths.element("p", {}, {fresh: true})]);
+ var nonFreshPath = htmlPaths.elements([
+ htmlPaths.element("p", {}, {fresh: false})]);
+ assert.deepEqual(
+ html.simplify(concat(
+ pathToNodes(freshPath, [text("Hello")]),
+ pathToNodes(nonFreshPath, [text(" there")])
+ )),
+ pathToNodes(freshPath, [text("Hello"), text(" there")])
+ );
+ });
+
+ test("children of collapsed element can collapse with children of another collapsed element", function() {
+ assert.deepEqual(
+ html.simplify([
+ nonFreshElement("blockquote", {}, [nonFreshElement("p", {}, [text("Hello")])]),
+ nonFreshElement("blockquote", {}, [nonFreshElement("p", {}, [text("there")])])
+ ]),
+ [nonFreshElement("blockquote", {}, [nonFreshElement("p", {}, [text("Hello"), text("there")])])]
+ );
+ });
+
+ test("empty elements are removed before collapsing", function() {
+ var freshPath = htmlPaths.elements([
+ htmlPaths.element("p", {}, {fresh: true})]);
+ var nonFreshPath = htmlPaths.elements([
+ htmlPaths.element("p", {}, {fresh: false})]);
+ assert.deepEqual(
+ html.simplify(concat(
+ pathToNodes(nonFreshPath, [text("Hello")]),
+ pathToNodes(freshPath, []),
+ pathToNodes(nonFreshPath, [text(" there")])
+ )),
+ pathToNodes(nonFreshPath, [text("Hello"), text(" there")])
+ );
+ });
+});
+
+function simplifyNode(node) {
+ return html.simplify([node]);
+}
+
+function concat() {
+ return _.flatten(arguments, true);
+}
diff --git a/test/html/write.tests.js b/test/html/write.tests.js
new file mode 100644
index 0000000..531995f
--- /dev/null
+++ b/test/html/write.tests.js
@@ -0,0 +1,44 @@
+var assert = require("assert");
+
+var test = require("../testing").test;
+var html = require("../../lib/html");
+var writers = require("../../lib/writers");
+
+
+describe("HTML write", function() {
+ test("text is HTML escaped", function() {
+ assert.equal(
+ generateString(html.text("<>&")),
+ "&lt;&gt;&amp;");
+ });
+
+ test("double quotes outside of attributes are not escaped", function() {
+ assert.equal(
+ generateString(html.text('"')),
+ '"');
+ });
+
+ test("element attributes are HTML escaped", function() {
+ assert.equal(
+ generateString(html.freshElement("p", {"x": "<"})),
+ '<p x="&lt;"></p>');
+ });
+
+ test("double quotes inside attributes are escaped", function() {
+ assert.equal(
+ generateString(html.freshElement("p", {"x": '"'})),
+ '<p x="&quot;"></p>');
+ });
+
+ test("element children are written", function() {
+ assert.equal(
+ generateString(html.freshElement("p", {}, [html.text("Hello")])),
+ '<p>Hello</p>');
+ });
+});
+
+function generateString(node) {
+ var writer = writers.writer();
+ html.write(writer, [node]);
+ return writer.asString();
+}
diff --git a/test/images.tests.js b/test/images.tests.js
new file mode 100644
index 0000000..7aac5a2
--- /dev/null
+++ b/test/images.tests.js
@@ -0,0 +1,13 @@
+var assert = require("assert");
+
+var mammoth = require("../");
+
+var testing = require("./testing");
+var test = testing.test;
+
+
+describe('images', function() {
+ test('mammoth.images.inline() should be an alias of mammoth.images.imgElement()', function() {
+ assert.ok(mammoth.images.inline === mammoth.images.imgElement);
+ });
+});
diff --git a/test/main.tests.js b/test/main.tests.js
new file mode 100644
index 0000000..d603626
--- /dev/null
+++ b/test/main.tests.js
@@ -0,0 +1,91 @@
+var assert = require("assert");
+var child_process = require("child_process"); // eslint-disable-line camelcase
+var path = require("path");
+var fs = require("fs");
+
+var temp = require('temp').track();
+
+var promises = require("../lib/promises");
+var test = require("./testing").test;
+var testPath = require("./testing").testPath;
+
+describe("CLI", function() {
+ test("HTML is printed to stdout if output file is not set", function() {
+ return runMammoth(testPath("single-paragraph.docx")).then(function(result) {
+ assert.equal(result.stderrOutput, "");
+ assert.equal(result.output, "<p>Walking on imported air</p>");
+ });
+ });
+
+ test("HTML is written to file if output file is set", function() {
+ return createTempDir().then(function(tempDir) {
+ var outputPath = path.join(tempDir, "output.html");
+ return runMammoth(testPath("single-paragraph.docx"), outputPath).then(function(result) {
+ assert.equal(result.stderrOutput, "");
+ assert.equal(result.output, "");
+ assert.equal(fs.readFileSync(outputPath, "utf8"), "<p>Walking on imported air</p>");
+ });
+ });
+ });
+
+ var imageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOvgAADr4B6kKxwAAAABNJREFUKFNj/M+ADzDhlWUYqdIAQSwBE8U+X40AAAAASUVORK5CYII=";
+
+ test("inline images are included in output if writing to single file", function() {
+ return runMammoth(testPath("tiny-picture.docx")).then(function(result) {
+ assert.equal(result.stderrOutput, "");
+ assert.equal(result.output, '<p><img src="data:image/png;base64,' + imageBase64 + '" /></p>');
+ });
+ });
+
+ test("images are written to separate files if output dir is set", function() {
+ return createTempDir().then(function(tempDir) {
+ var outputPath = path.join(tempDir, "tiny-picture.html");
+ var imagePath = path.join(tempDir, "1.png");
+ return runMammoth(testPath("tiny-picture.docx"), "--output-dir", tempDir).then(function(result) {
+ assert.equal(result.stderrOutput, "");
+ assert.equal(result.output, "");
+ assert.equal(fs.readFileSync(outputPath, "utf8"), '<p><img src="1.png" /></p>');
+ assert.equal(fs.readFileSync(imagePath, "base64"), imageBase64);
+ });
+ });
+ });
+
+ test("style map is used if set", function() {
+ return createTempDir().then(function(tempDir) {
+ var styleMapPath = path.join(tempDir, "style-map");
+ fs.writeFileSync(styleMapPath, "p => span:fresh");
+ return runMammoth(testPath("single-paragraph.docx"), "--style-map", styleMapPath).then(function(result) {
+ assert.equal(result.stderrOutput, "");
+ assert.equal(result.output, "<span>Walking on imported air</span>");
+ });
+ });
+ });
+
+ test("--output-format=markdown option generate markdown output", function() {
+ return runMammoth(testPath("single-paragraph.docx"), "--output-format=markdown").then(function(result) {
+ assert.equal(result.stderrOutput, "");
+ assert.equal(result.output, "Walking on imported air\n\n");
+ });
+ });
+});
+
+
+function runMammoth() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ var deferred = promises.defer();
+
+ var processArgs = ["node", "bin/mammoth"].concat(args);
+ // TODO: proper escaping of args
+ var command = processArgs.join(" ");
+ child_process.exec(command, function(error, stdout, stderr) { // eslint-disable-line camelcase
+ console.log(stderr); // eslint-disable-line no-console
+ assert.equal(error, null);
+ deferred.resolve({output: stdout, stderrOutput: stderr});
+ });
+
+ return deferred.promise;
+}
+
+function createTempDir() {
+ return promises.nfcall(temp.mkdir, null);
+}
diff --git a/test/mammoth.tests.js b/test/mammoth.tests.js
new file mode 100644
index 0000000..1a82014
--- /dev/null
+++ b/test/mammoth.tests.js
@@ -0,0 +1,353 @@
+var assert = require("assert");
+var path = require("path");
+var fs = require("fs");
+var _ = require("underscore");
+
+var mammoth = require("../");
+var promises = require("../lib/promises");
+var results = require("../lib/results");
+
+var testing = require("./testing");
+var test = testing.test;
+var testData = testing.testData;
+var createFakeDocxFile = testing.createFakeDocxFile;
+
+
+describe('mammoth', function() {
+ test('should convert docx containing one paragraph to single p element', function() {
+ var docxPath = path.join(__dirname, "test-data/single-paragraph.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ assert.equal(result.value, "<p>Walking on imported air</p>");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('should convert docx represented by a Buffer', function() {
+ var docxPath = path.join(__dirname, "test-data/single-paragraph.docx");
+ return promises.nfcall(fs.readFile, docxPath)
+ .then(function(buffer) {
+ return mammoth.convertToHtml({buffer: buffer});
+ })
+ .then(function(result) {
+ assert.equal(result.value, "<p>Walking on imported air</p>");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('should read docx xml files with unicode byte order mark', function() {
+ var docxPath = path.join(__dirname, "test-data/utf8-bom.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ assert.equal(result.value, "<p>This XML has a byte order mark.</p>");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('empty paragraphs are ignored by default', function() {
+ var docxPath = path.join(__dirname, "test-data/empty.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ assert.equal(result.value, "");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('empty paragraphs are preserved if ignoreEmptyParagraphs is false', function() {
+ var docxPath = path.join(__dirname, "test-data/empty.docx");
+ return mammoth.convertToHtml({path: docxPath}, {ignoreEmptyParagraphs: false}).then(function(result) {
+ assert.equal(result.value, "<p></p>");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('style map can be expressed as string', function() {
+ var docxFile = createFakeDocxFile({
+ "word/document.xml": testData("simple/word/document.xml")
+ });
+ var options = {
+ styleMap: "p => h1"
+ };
+ return mammoth.convertToHtml({file: docxFile}, options).then(function(result) {
+ assert.equal("<h1>Hello.</h1>", result.value);
+ });
+ });
+
+ test('style map can be expressed as array of style mappings', function() {
+ var docxFile = createFakeDocxFile({
+ "word/document.xml": testData("simple/word/document.xml")
+ });
+ var options = {
+ styleMap: ["p => h1"]
+ };
+ return mammoth.convertToHtml({file: docxFile}, options).then(function(result) {
+ assert.equal("<h1>Hello.</h1>", result.value);
+ });
+ });
+
+ test('embedded style map is used if present', function() {
+ var docxPath = path.join(__dirname, "test-data/single-paragraph.docx");
+ return promises.nfcall(fs.readFile, docxPath)
+ .then(function(buffer) {
+ return mammoth.embedStyleMap({buffer: buffer}, "p => h1");
+ })
+ .then(function(docx) {
+ return mammoth.convertToHtml({buffer: docx.toBuffer()});
+ })
+ .then(function(result) {
+ assert.equal(result.value, "<h1>Walking on imported air</h1>");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('embedded style map can be retrieved', function() {
+ var docxPath = path.join(__dirname, "test-data/single-paragraph.docx");
+ return promises.nfcall(fs.readFile, docxPath)
+ .then(function(buffer) {
+ return mammoth.embedStyleMap({buffer: buffer}, "p => h1");
+ })
+ .then(function(docx) {
+ return mammoth.readEmbeddedStyleMap({buffer: docx.toBuffer()});
+ })
+ .then(function(styleMap) {
+ assert.equal(styleMap, "p => h1");
+ });
+ });
+
+ test('warning if style mapping is not understood', function() {
+ var docxPath = path.join(__dirname, "test-data/single-paragraph.docx");
+ var options = {
+ styleMap: "!!!!\np => h1"
+ };
+ return mammoth.convertToHtml({path: docxPath}, options).then(function(result) {
+ assert.equal("<h1>Walking on imported air</h1>", result.value);
+ var warning = "Did not understand this style mapping, so ignored it: !!!!\n" +
+ 'Error was at character number 1: Expected element type but got unrecognisedCharacter "!"';
+ assert.deepEqual(result.messages, [results.warning(warning)]);
+ });
+ });
+
+ test('options are passed to document converter when calling mammoth.convertToHtml', function() {
+ var docxFile = createFakeDocxFile({
+ "word/document.xml": testData("simple/word/document.xml")
+ });
+ var options = {
+ styleMap: "p => h1"
+ };
+ return mammoth.convertToHtml({file: docxFile}, options).then(function(result) {
+ assert.equal("<h1>Hello.</h1>", result.value);
+ });
+ });
+
+ test('options.transformDocument is used to transform document if set', function() {
+ var docxFile = createFakeDocxFile({
+ "word/document.xml": testData("simple/word/document.xml")
+ });
+ var options = {
+ transformDocument: function(document) {
+ document.children[0].styleId = "Heading1";
+ return document;
+ }
+ };
+ return mammoth.convertToHtml({file: docxFile}, options).then(function(result) {
+ assert.equal("<h1>Hello.</h1>", result.value);
+ });
+ });
+
+ test('mammoth.transforms.paragraph only transforms paragraphs', function() {
+ var docxFile = createFakeDocxFile({
+ "word/document.xml": testData("simple/word/document.xml")
+ });
+ var options = {
+ transformDocument: mammoth.transforms.paragraph(function(paragraph) {
+ return _.extend(paragraph, {styleId: "Heading1"});
+ })
+ };
+ return mammoth.convertToHtml({file: docxFile}, options).then(function(result) {
+ assert.equal("<h1>Hello.</h1>", result.value);
+ });
+ });
+
+ test('inline images are included in output', function() {
+ var docxPath = path.join(__dirname, "test-data/tiny-picture.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ assert.equal(result.value, '<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOvgAADr4B6kKxwAAAABNJREFUKFNj/M+ADzDhlWUYqdIAQSwBE8U+X40AAAAASUVORK5CYII=" /></p>');
+ });
+ });
+
+ test('src of inline images can be changed', function() {
+ var docxPath = path.join(__dirname, "test-data/tiny-picture.docx");
+ var convertImage = mammoth.images.imgElement(function(element) {
+ return element.read("base64").then(function(encodedImage) {
+ return {src: encodedImage.substring(0, 2) + "," + element.contentType};
+ });
+ });
+ return mammoth.convertToHtml({path: docxPath}, {convertImage: convertImage}).then(function(result) {
+ assert.equal(result.value, '<p><img src="iV,image/png" /></p>');
+ });
+ });
+
+ test('images stored outside of document are included in output', function() {
+ var docxPath = path.join(__dirname, "test-data/external-picture.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ assert.equal(result.value, '<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOvgAADr4B6kKxwAAAABNJREFUKFNj/M+ADzDhlWUYqdIAQSwBE8U+X40AAAAASUVORK5CYII=" /></p>');
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('warn if images stored outside of document are specified when passing file without path', function() {
+ var docxPath = path.join(__dirname, "test-data/external-picture.docx");
+ var buffer = fs.readFileSync(docxPath);
+ return mammoth.convertToHtml({buffer: buffer}).then(function(result) {
+ assert.equal(result.value, '');
+ assert.deepEqual(result.messages, [results.warning("could not find external image 'tiny-picture.png', path of input document is unknown")]);
+ });
+ });
+
+ test('simple list is converted to list elements', function() {
+ var docxPath = path.join(__dirname, "test-data/simple-list.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ assert.equal(result.value, '<ul><li>Apple</li><li>Banana</li></ul>');
+ });
+ });
+
+ test('word tables are converted to html tables', function() {
+ var docxPath = path.join(__dirname, "test-data/tables.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ var expectedHtml = "<p>Above</p>" +
+ "<table>" +
+ "<tr><td><p>Top left</p></td><td><p>Top right</p></td></tr>" +
+ "<tr><td><p>Bottom left</p></td><td><p>Bottom right</p></td></tr>" +
+ "</table>" +
+ "<p>Below</p>";
+ assert.equal(result.value, expectedHtml);
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('footnotes are appended to text', function() {
+ // TODO: don't duplicate footnotes with multiple references
+ var docxPath = path.join(__dirname, "test-data/footnotes.docx");
+ var options = {
+ idPrefix: "doc-42-"
+ };
+ return mammoth.convertToHtml({path: docxPath}, options).then(function(result) {
+ var expectedOutput = '<p>Ouch' +
+ '<sup><a href="#doc-42-footnote-1" id="doc-42-footnote-ref-1">[1]</a></sup>.' +
+ '<sup><a href="#doc-42-footnote-2" id="doc-42-footnote-ref-2">[2]</a></sup></p>' +
+ '<ol><li id="doc-42-footnote-1"><p> A tachyon walks into a bar. <a href="#doc-42-footnote-ref-1">↑</a></p></li>' +
+ '<li id="doc-42-footnote-2"><p> Fin. <a href="#doc-42-footnote-ref-2">↑</a></p></li></ol>';
+ assert.equal(result.value, expectedOutput);
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('endnotes are appended to text', function() {
+ var docxPath = path.join(__dirname, "test-data/endnotes.docx");
+ var options = {
+ idPrefix: "doc-42-"
+ };
+ return mammoth.convertToHtml({path: docxPath}, options).then(function(result) {
+ var expectedOutput = '<p>Ouch' +
+ '<sup><a href="#doc-42-endnote-2" id="doc-42-endnote-ref-2">[1]</a></sup>.' +
+ '<sup><a href="#doc-42-endnote-3" id="doc-42-endnote-ref-3">[2]</a></sup></p>' +
+ '<ol><li id="doc-42-endnote-2"><p> A tachyon walks into a bar. <a href="#doc-42-endnote-ref-2">↑</a></p></li>' +
+ '<li id="doc-42-endnote-3"><p> Fin. <a href="#doc-42-endnote-ref-3">↑</a></p></li></ol>';
+ assert.equal(result.value, expectedOutput);
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('relationships are handled properly in footnotes', function() {
+ var docxPath = path.join(__dirname, "test-data/footnote-hyperlink.docx");
+ var options = {
+ idPrefix: "doc-42-"
+ };
+ return mammoth.convertToHtml({path: docxPath}, options).then(function(result) {
+ var expectedOutput =
+ '<p><sup><a href="#doc-42-footnote-1" id="doc-42-footnote-ref-1">[1]</a></sup></p>' +
+ '<ol><li id="doc-42-footnote-1"><p> <a href="http://www.example.com">Example</a> <a href="#doc-42-footnote-ref-1">↑</a></p></li></ol>';
+ assert.equal(result.value, expectedOutput);
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('textboxes are read', function() {
+ var docxPath = path.join(__dirname, "test-data/text-box.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ var expectedOutput = '<p>Datum plane</p>';
+ assert.equal(result.value, expectedOutput);
+ });
+ });
+
+ 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 Sunset 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 <em>Sunset</em> Tree</strong></p>');
+ });
+ });
+
+ test('strikethrough is converted to <s> by default', function() {
+ var docxPath = path.join(__dirname, "test-data/strikethrough.docx");
+ return mammoth.convertToHtml({path: docxPath}).then(function(result) {
+ assert.equal(result.value, "<p><s>Today's Special: Salmon</s> Sold out</p>");
+ });
+ });
+
+ test('strikethrough conversion can be configured with style mappings', function() {
+ var docxPath = path.join(__dirname, "test-data/strikethrough.docx");
+ return mammoth.convertToHtml({path: docxPath}, {styleMap: "strike => del"}).then(function(result) {
+ assert.equal(result.value, "<p><del>Today's Special: Salmon</del> Sold out</p>");
+ });
+ });
+
+ test('indentation is used if prettyPrint is true', function() {
+ var docxPath = path.join(__dirname, "test-data/single-paragraph.docx");
+ return mammoth.convertToHtml({path: docxPath}, {prettyPrint: true}).then(function(result) {
+ assert.equal(result.value, "<p>\n Walking on imported air\n</p>");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('using styleMapping throws error', function() {
+ try {
+ mammoth.styleMapping();
+ } catch (error) {
+ assert.equal(
+ error.message,
+ 'Use a raw string instead of mammoth.styleMapping e.g. "p[style-name=\'Title\'] => h1" instead of mammoth.styleMapping("p[style-name=\'Title\'] => h1")'
+ );
+ }
+ });
+
+ test('can convert single paragraph to markdown', function() {
+ var docxPath = path.join(__dirname, "test-data/single-paragraph.docx");
+ return mammoth.convertToMarkdown({path: docxPath}).then(function(result) {
+ assert.equal(result.value, "Walking on imported air\n\n");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+
+ test('extractRawText only retains raw text', function() {
+ var docxPath = path.join(__dirname, "test-data/simple-list.docx");
+ return mammoth.extractRawText({path: docxPath}).then(function(result) {
+ assert.equal(result.value, 'Apple\n\nBanana\n\n');
+ });
+ });
+
+ test('extractRawText can use .docx files represented by a Buffer', function() {
+ var docxPath = path.join(__dirname, "test-data/single-paragraph.docx");
+ return promises.nfcall(fs.readFile, docxPath)
+ .then(function(buffer) {
+ return mammoth.extractRawText({buffer: buffer});
+ })
+ .then(function(result) {
+ assert.equal(result.value, "Walking on imported air\n\n");
+ assert.deepEqual(result.messages, []);
+ });
+ });
+});
diff --git a/test/options-reader.tests.js b/test/options-reader.tests.js
new file mode 100644
index 0000000..94c864c
--- /dev/null
+++ b/test/options-reader.tests.js
@@ -0,0 +1,48 @@
+var assert = require("assert");
+
+var optionsReader = require("../lib/options-reader");
+var standardOptions = optionsReader._standardOptions;
+var readOptions = optionsReader.readOptions;
+
+
+describe('readOptions', function() {
+ it('standard options are used if options is undefined', function() {
+ assert.deepEqual(standardOptions, readOptions(undefined));
+ });
+
+ it('standard options are used if options is empty', function() {
+ assert.deepEqual(standardOptions, readOptions({}));
+ });
+
+ it('custom style map as string is prepended to standard style map', function() {
+ var options = readOptions({
+ styleMap: "p.SectionTitle => h2"
+ });
+ assert.deepEqual("p.SectionTitle => h2", options.styleMap[0]);
+ assert.deepEqual(standardOptions.styleMap, options.styleMap.slice(1));
+ });
+
+ it('custom style map as array is prepended to standard style map', function() {
+ var options = readOptions({
+ styleMap: ["p.SectionTitle => h2"]
+ });
+ assert.deepEqual("p.SectionTitle => h2", options.styleMap[0]);
+ assert.deepEqual(standardOptions.styleMap, options.styleMap.slice(1));
+ });
+
+ it('lines starting with # in custom style map are ignored', function() {
+ var options = readOptions({
+ styleMap: "# p.SectionTitle => h3\np.SectionTitle => h2"
+ });
+ assert.deepEqual("p.SectionTitle => h2", options.styleMap[0]);
+ assert.deepEqual(standardOptions.styleMap, options.styleMap.slice(1));
+ });
+
+ it('default style mappings are ignored if includeDefaultStyleMap is false', function() {
+ var options = readOptions({
+ styleMap: "p.SectionTitle => h2",
+ includeDefaultStyleMap: false
+ });
+ assert.deepEqual(["p.SectionTitle => h2"], options.styleMap);
+ });
+});
diff --git a/test/results.tests.js b/test/results.tests.js
new file mode 100644
index 0000000..3c0df78
--- /dev/null
+++ b/test/results.tests.js
@@ -0,0 +1,17 @@
+var assert = require("assert");
+
+var results = require("../lib/results");
+var Result = results.Result;
+var test = require("./testing").test;
+
+
+describe("Result.combine", function() {
+ test("removes any duplicate messages", function() {
+ var first = new Result(null, [results.warning("Warning...")]);
+ var second = new Result(null, [results.warning("Warning...")]);
+
+ var combined = Result.combine([first, second]);
+
+ assert.deepEqual(combined.messages, [results.warning("Warning...")]);
+ });
+});
diff --git a/test/style-reader.tests.js b/test/style-reader.tests.js
new file mode 100644
index 0000000..0a4dccb
--- /dev/null
+++ b/test/style-reader.tests.js
@@ -0,0 +1,159 @@
+var assert = require("assert");
+var htmlPaths = require("../lib/html-paths");
+var documentMatchers = require("../lib/document-matchers");
+var styleReader = require("../lib/style-reader");
+var results = require("../lib/results");
+var test = require("./testing").test;
+
+
+var readHtmlPath = styleReader.readHtmlPath;
+var readDocumentMatcher = styleReader.readDocumentMatcher;
+var readStyle = styleReader.readStyle;
+
+
+describe('styleReader.readHtmlPath', function() {
+ test('reads empty path', function() {
+ assertHtmlPath("", htmlPaths.empty);
+ });
+
+ test('reads single element', function() {
+ assertHtmlPath("p", htmlPaths.elements(["p"]));
+ });
+
+ test('reads choice of elements', function() {
+ assertHtmlPath(
+ "ul|ol",
+ htmlPaths.elements([
+ htmlPaths.element(["ul", "ol"])
+ ])
+ );
+ });
+
+ test('reads nested elements', function() {
+ assertHtmlPath("ul > li", htmlPaths.elements(["ul", "li"]));
+ });
+
+ test('reads class on element', function() {
+ var expected = htmlPaths.elements([
+ htmlPaths.element("p", {"class": "tip"})
+ ]);
+ assertHtmlPath("p.tip", expected);
+ });
+
+ test('reads multiple classes on element', function() {
+ var expected = htmlPaths.elements([
+ htmlPaths.element("p", {"class": "tip help"})
+ ]);
+ assertHtmlPath("p.tip.help", expected);
+ });
+
+ test('reads when element must be fresh', function() {
+ var expected = htmlPaths.elements([
+ htmlPaths.element("p", {}, {"fresh": true})
+ ]);
+ assertHtmlPath("p:fresh", expected);
+ });
+
+ function assertHtmlPath(input, expected) {
+ assert.deepEqual(readHtmlPath(input), results.success(expected));
+ }
+});
+
+describe("styleReader.readDocumentMatcher", function() {
+ test("reads plain paragraph", function() {
+ assertDocumentMatcher("p", documentMatchers.paragraph());
+ });
+
+ test("reads paragraph with style ID", function() {
+ assertDocumentMatcher(
+ "p.Heading1",
+ documentMatchers.paragraph({styleId: "Heading1"})
+ );
+ });
+
+ test("reads paragraph with style name", function() {
+ assertDocumentMatcher(
+ "p[style-name='Heading 1']",
+ documentMatchers.paragraph({styleName: "Heading 1"})
+ );
+ });
+
+ test("reads p:ordered-list(1) as ordered list with index of 0", function() {
+ assertDocumentMatcher(
+ "p:ordered-list(1)",
+ documentMatchers.paragraph({list: {isOrdered: true, levelIndex: 0}})
+ );
+ });
+
+ test("reads p:unordered-list(1) as unordered list with index of 0", function() {
+ assertDocumentMatcher(
+ "p:unordered-list(1)",
+ documentMatchers.paragraph({list: {isOrdered: false, levelIndex: 0}})
+ );
+ });
+
+ test("reads plain run", function() {
+ assertDocumentMatcher(
+ "r",
+ documentMatchers.run()
+ );
+ });
+
+ test("reads bold", function() {
+ assertDocumentMatcher(
+ "b",
+ documentMatchers.bold
+ );
+ });
+
+ test("reads italic", function() {
+ assertDocumentMatcher(
+ "i",
+ documentMatchers.italic
+ );
+ });
+
+ test("reads underline", function() {
+ assertDocumentMatcher(
+ "u",
+ documentMatchers.underline
+ );
+ });
+
+ test("reads strikethrough", function() {
+ assertDocumentMatcher(
+ "strike",
+ documentMatchers.strikethrough
+ );
+ });
+
+ function assertDocumentMatcher(input, expected) {
+ assert.deepEqual(readDocumentMatcher(input), results.success(expected));
+ }
+});
+
+describe("styleReader.read", function() {
+ test("document matcher is mapped to HTML path using arrow", function() {
+ assertStyleMapping(
+ "p => h1",
+ {
+ from: documentMatchers.paragraph(),
+ to: htmlPaths.elements(["h1"])
+ }
+ );
+ });
+
+ test("reads style mapping with no HTML path", function() {
+ assertStyleMapping(
+ "r =>",
+ {
+ from: documentMatchers.run(),
+ to: htmlPaths.empty
+ }
+ );
+ });
+
+ function assertStyleMapping(input, expected) {
+ assert.deepEqual(readStyle(input), results.success(expected));
+ }
+});
diff --git a/test/test-data/empty.docx b/test/test-data/empty.docx
new file mode 100644
index 0000000..e10b869
--- /dev/null
+++ b/test/test-data/empty.docx
Binary files differ
diff --git a/test/test-data/endnotes.docx b/test/test-data/endnotes.docx
new file mode 100644
index 0000000..1ae4f93
--- /dev/null
+++ b/test/test-data/endnotes.docx
Binary files differ
diff --git a/test/test-data/external-picture.docx b/test/test-data/external-picture.docx
new file mode 100644
index 0000000..3475626
--- /dev/null
+++ b/test/test-data/external-picture.docx
Binary files differ
diff --git a/test/test-data/footnote-hyperlink.docx b/test/test-data/footnote-hyperlink.docx
new file mode 100755
index 0000000..208f3dc
--- /dev/null
+++ b/test/test-data/footnote-hyperlink.docx
Binary files differ
diff --git a/test/test-data/footnotes.docx b/test/test-data/footnotes.docx
new file mode 100755
index 0000000..3228e74
--- /dev/null
+++ b/test/test-data/footnotes.docx
Binary files differ
diff --git a/test/test-data/hello.zip b/test/test-data/hello.zip
new file mode 100644
index 0000000..613eb52
--- /dev/null
+++ b/test/test-data/hello.zip
Binary files differ
diff --git a/test/test-data/hyperlinks/word/_rels/document.xml.rels b/test/test-data/hyperlinks/word/_rels/document.xml.rels
new file mode 100644
index 0000000..baa4102
--- /dev/null
+++ b/test/test-data/hyperlinks/word/_rels/document.xml.rels
@@ -0,0 +1,10 @@
+<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+ <Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml" />
+ <Relationship Id="rId3" Type="http://schemas.microsoft.com/office/2007/relationships/stylesWithEffects" Target="stylesWithEffects.xml" />
+ <Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml" />
+ <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml" />
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml" />
+ <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="http://www.example.com" TargetMode="External" />
+ <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml" />
+ <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml" />
+</Relationships> \ No newline at end of file
diff --git a/test/test-data/hyperlinks/word/document.xml b/test/test-data/hyperlinks/word/document.xml
new file mode 100644
index 0000000..f1ce575
--- /dev/null
+++ b/test/test-data/hyperlinks/word/document.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" ?>
+<w:document mc:Ignorable="w14 wp14" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
+ <w:body>
+ <w:p w:rsidR="00E01ECE" w:rsidRDefault="000636A7">
+ <w:hyperlink r:id="rId6" w:history="1">
+ <w:r>
+ <w:t>coconuts</w:t>
+ </w:r>
+ </w:hyperlink>
+ </w:p>
+ <w:sectPr w:rsidR="00E01ECE">
+ <w:pgSz w:h="16838" w:w="11906"/>
+ <w:pgMar w:bottom="1440" w:footer="708" w:gutter="0" w:header="708" w:left="1440" w:right="1440" w:top="1440"/>
+ <w:cols w:space="708"/>
+ <w:docGrid w:linePitch="360"/>
+ </w:sectPr>
+ </w:body>
+</w:document>
diff --git a/test/test-data/simple-list.docx b/test/test-data/simple-list.docx
new file mode 100755
index 0000000..c2cd5ae
--- /dev/null
+++ b/test/test-data/simple-list.docx
Binary files differ
diff --git a/test/test-data/simple/word/document.xml b/test/test-data/simple/word/document.xml
new file mode 100644
index 0000000..1723398
--- /dev/null
+++ b/test/test-data/simple/word/document.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" ?>
+<w:document mc:Ignorable="w14 wp14" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
+ <w:body>
+ <w:p w:rsidR="00E01ECE" w:rsidRDefault="000636A7">
+ <w:r>
+ <w:t>Hello.</w:t>
+ </w:r>
+ <w:bookmarkStart w:id="0" w:name="_GoBack"/>
+ <w:bookmarkEnd w:id="0"/>
+ </w:p>
+ <w:sectPr w:rsidR="00E01ECE">
+ <w:pgSz w:h="16838" w:w="11906"/>
+ <w:pgMar w:bottom="1440" w:footer="708" w:gutter="0" w:header="708" w:left="1440" w:right="1440" w:top="1440"/>
+ <w:cols w:space="708"/>
+ <w:docGrid w:linePitch="360"/>
+ </w:sectPr>
+ </w:body>
+</w:document>
diff --git a/test/test-data/single-paragraph.docx b/test/test-data/single-paragraph.docx
new file mode 100755
index 0000000..09caf9a
--- /dev/null
+++ b/test/test-data/single-paragraph.docx
Binary files differ
diff --git a/test/test-data/strikethrough.docx b/test/test-data/strikethrough.docx
new file mode 100755
index 0000000..0d2b713
--- /dev/null
+++ b/test/test-data/strikethrough.docx
Binary files differ
diff --git a/test/test-data/tables.docx b/test/test-data/tables.docx
new file mode 100755
index 0000000..86009a7
--- /dev/null
+++ b/test/test-data/tables.docx
Binary files differ
diff --git a/test/test-data/text-box.docx b/test/test-data/text-box.docx
new file mode 100755
index 0000000..2d53703
--- /dev/null
+++ b/test/test-data/text-box.docx
Binary files differ
diff --git a/test/test-data/tiny-picture.docx b/test/test-data/tiny-picture.docx
new file mode 100755
index 0000000..d45bb9e
--- /dev/null
+++ b/test/test-data/tiny-picture.docx
Binary files differ
diff --git a/test/test-data/tiny-picture.png b/test/test-data/tiny-picture.png
new file mode 100644
index 0000000..74cb4ba
--- /dev/null
+++ b/test/test-data/tiny-picture.png
Binary files differ
diff --git a/test/test-data/underline.docx b/test/test-data/underline.docx
new file mode 100755
index 0000000..36604fc
--- /dev/null
+++ b/test/test-data/underline.docx
Binary files differ
diff --git a/test/test-data/utf8-bom.docx b/test/test-data/utf8-bom.docx
new file mode 100644
index 0000000..3881238
--- /dev/null
+++ b/test/test-data/utf8-bom.docx
Binary files differ
diff --git a/test/testing.js b/test/testing.js
new file mode 100644
index 0000000..d70f86e
--- /dev/null
+++ b/test/testing.js
@@ -0,0 +1,65 @@
+var path = require("path");
+var fs = require("fs");
+var promises = require("../lib/promises");
+var _ = require("underscore");
+
+exports.test = test;
+exports.testPath = testPath;
+exports.testData = testData;
+exports.createFakeDocxFile = createFakeDocxFile;
+exports.createFakeFiles = createFakeFiles;
+
+
+function test(name, func) {
+ it(name, function(done) {
+ var result = func();
+ promises.when(result).then(function() {
+ done();
+ }).done();
+ });
+}
+
+function testPath(filename) {
+ return path.join(__dirname, "test-data", filename);
+}
+
+function testData(testDataPath) {
+ var fullPath = testPath(testDataPath);
+ return promises.nfcall(fs.readFile, fullPath, "utf-8");
+}
+
+function createFakeDocxFile(files) {
+ function exists(path) {
+ return !!files[path];
+ }
+
+ return {
+ read: createRead(files),
+ exists: exists
+ };
+}
+
+function createFakeFiles(files) {
+ return {
+ read: createRead(files)
+ };
+}
+
+function createRead(files) {
+ function read(path, encoding) {
+ return promises.when(files[path], function(buffer) {
+ if (_.isString(buffer)) {
+ buffer = new Buffer(buffer);
+ }
+
+ if (!Buffer.isBuffer(buffer)) {
+ return promises.reject(new Error("file was not a buffer"));
+ } else if (encoding) {
+ return promises.when(buffer.toString(encoding));
+ } else {
+ return promises.when(buffer);
+ }
+ });
+ }
+ return read;
+}
diff --git a/test/unzip.tests.js b/test/unzip.tests.js
new file mode 100644
index 0000000..994fb68
--- /dev/null
+++ b/test/unzip.tests.js
@@ -0,0 +1,40 @@
+var fs = require("fs");
+var assert = require("assert");
+var path = require("path");
+
+var test = require("./testing").test;
+var unzip = require("../lib/unzip");
+var promises = require("../lib/promises");
+
+describe("unzip", function() {
+ test("unzip fails if given empty object", function() {
+ return unzip.openZip({}).then(function() {
+ assert.ok(false, "Expected failure");
+ }, function(error) {
+ assert.equal("Could not find file in options", error.message);
+ });
+ });
+
+ test("unzip can open local zip file", function() {
+ var zipPath = path.join(__dirname, "test-data/hello.zip");
+ return unzip.openZip({path: zipPath}).then(function(zipFile) {
+ return zipFile.read("hello", "utf8");
+ }).then(function(contents) {
+ assert.equal(contents, "Hello world\n");
+ });
+ });
+
+ test('unzip can open Buffer', function() {
+ var zipPath = path.join(__dirname, "test-data/hello.zip");
+ return promises.nfcall(fs.readFile, zipPath)
+ .then(function(buffer) {
+ return unzip.openZip({buffer: buffer});
+ })
+ .then(function(zipFile) {
+ return zipFile.read("hello", "utf8");
+ })
+ .then(function(contents) {
+ assert.equal(contents, "Hello world\n");
+ });
+ });
+});
diff --git a/test/writers/html-writer.tests.js b/test/writers/html-writer.tests.js
new file mode 100644
index 0000000..41d8839
--- /dev/null
+++ b/test/writers/html-writer.tests.js
@@ -0,0 +1,117 @@
+var assert = require("assert");
+var test = require("../testing").test;
+
+var htmlWriter = require("../../lib/writers/html-writer");
+
+
+describe('html-writer', function() {
+ test('can generate simple paragraph', function() {
+ var writer = htmlWriter.writer();
+ writer.open("p");
+ writer.text("Hello");
+ writer.close("p");
+ return assert.equal(writer.asString(), "<p>Hello</p>");
+ });
+
+ test('can nest elements', function() {
+ var writer = htmlWriter.writer();
+ writer.open("ul");
+ writer.open("li");
+ writer.text("One");
+ writer.close("li");
+ writer.open("li");
+ writer.text("Two");
+ writer.close("li");
+ writer.close("ul");
+ return assert.equal(writer.asString(), "<ul><li>One</li><li>Two</li></ul>");
+ });
+
+ test('indents text if prettyPrint is true', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("p");
+ writer.text("One");
+ writer.close("p");
+ writer.open("p");
+ writer.text("Two");
+ writer.close("p");
+ return assert.equal(writer.asString(), "<p>\n One\n</p>\n<p>\n Two\n</p>");
+ });
+
+ test('indents element if prettyPrint is true', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("ul");
+ writer.open("li");
+ writer.text("One");
+ writer.close("li");
+ writer.open("li");
+ writer.text("Two");
+ writer.close("li");
+ writer.close("ul");
+ return assert.equal(writer.asString(), "<ul>\n <li>\n One\n </li>\n <li>\n Two\n </li>\n</ul>");
+ });
+
+ test('when prettyPrint is true inner elements do not have new lines', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("p");
+ writer.text("One");
+ writer.open("em");
+ writer.text("Two");
+ writer.close("em");
+ writer.close("p");
+ return assert.equal(writer.asString(), "<p>\n One<em>Two</em>\n</p>");
+ });
+
+ test('indents closing element correctly when nested inside another indented element', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("div");
+ writer.open("div");
+ writer.open("div");
+ writer.text("Hello");
+ writer.close("div");
+ writer.close("div");
+ writer.close("div");
+ return assert.equal(writer.asString(), "<div>\n <div>\n <div>\n Hello\n </div>\n </div>\n</div>");
+ });
+
+ test('newlines in text are indented', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("p");
+ writer.text("One\nTwo");
+ writer.close("p");
+ return assert.equal(writer.asString(), "<p>\n One\n Two\n</p>");
+ });
+
+ test('run of text has only one new line', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("p");
+ writer.text("One");
+ writer.text("Two");
+ writer.close("p");
+ return assert.equal(writer.asString(), "<p>\n OneTwo\n</p>");
+ });
+
+ test('run of html has only one new line', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("p");
+ writer.append("One");
+ writer.append("Two");
+ writer.close("p");
+ return assert.equal(writer.asString(), "<p>\n OneTwo\n</p>");
+ });
+
+ test('self closing elements are indented', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("p");
+ writer.selfClosing("br");
+ writer.close("p");
+ return assert.equal(writer.asString(), "<p>\n <br />\n</p>");
+ });
+
+ test('newlines in appended HTML are indented', function() {
+ var writer = htmlWriter.writer({prettyPrint: true});
+ writer.open("p");
+ writer.append("One\nTwo");
+ writer.close("p");
+ return assert.equal(writer.asString(), "<p>\n One\n Two\n</p>");
+ });
+});
diff --git a/test/writers/markdown-writer.tests.js b/test/writers/markdown-writer.tests.js
new file mode 100644
index 0000000..38d2afe
--- /dev/null
+++ b/test/writers/markdown-writer.tests.js
@@ -0,0 +1,306 @@
+var assert = require("assert");
+var test = require("../testing").test;
+
+var mdWriter = require("../../lib/writers/markdown-writer");
+
+describe('markdown-writer', function() {
+ test('special markdown characters are escaped', function() {
+ var writer = mdWriter.writer();
+ writer.text("\\*");
+ return assert.equal(writer.asString(), "\\\\\\*");
+ });
+
+ test('unrecognised elements are treated as normal text', function() {
+ var writer = mdWriter.writer();
+ writer.open("blah");
+ writer.text("Hello");
+ writer.close("blah");
+ return assert.equal(writer.asString(), "Hello");
+ });
+
+ test('paragraphs are terminated with double new line', function() {
+ var writer = mdWriter.writer();
+ writer.open("p");
+ writer.text("Hello");
+ writer.close("p");
+ return assert.equal(writer.asString(), "Hello\n\n");
+ });
+
+ test('h1 elements are converted to heading with leading hash', function() {
+ var writer = mdWriter.writer();
+ writer.open("h1");
+ writer.text("Hello");
+ writer.close("h1");
+ return assert.equal(writer.asString(), "# Hello\n\n");
+ });
+
+ test('h6 elements are converted to heading with six leading hashes', function() {
+ var writer = mdWriter.writer();
+ writer.open("h6");
+ writer.text("Hello");
+ writer.close("h6");
+ return assert.equal(writer.asString(), "###### Hello\n\n");
+ });
+
+ test('br is written as two spaces followed by new line', function() {
+ var writer = mdWriter.writer();
+ writer.text("Hello World");
+ writer.selfClosing("br");
+ return assert.equal(writer.asString(), "Hello World \n");
+ });
+
+ test('strong text is surrounded by two underscores', function() {
+ var writer = mdWriter.writer();
+ writer.text("Hello ");
+ writer.open("strong");
+ writer.text("World");
+ writer.close("strong");
+ return assert.equal(writer.asString(), "Hello __World__");
+ });
+
+ test('emphasised text is surrounded by one asterix', function() {
+ var writer = mdWriter.writer();
+ writer.text("Hello ");
+ writer.open("em");
+ writer.text("World");
+ writer.close("em");
+ return assert.equal(writer.asString(), "Hello *World*");
+ });
+
+ test('anchor tags are written as hyperlinks', function() {
+ var writer = mdWriter.writer();
+ writer.open("a", {"href": "http://example.com"});
+ writer.text("Hello");
+ writer.close("a");
+ return assert.equal(writer.asString(), "[Hello](http://example.com)");
+ });
+
+ test('anchor tags without href attribute are treated as ordinary text', function() {
+ var writer = mdWriter.writer();
+ writer.open("a");
+ writer.text("Hello");
+ writer.close("a");
+ return assert.equal(writer.asString(), "Hello");
+ });
+
+ test('elements with IDs have anchor tags with IDs appended to start of markdown element', function() {
+ var writer = mdWriter.writer();
+ writer.open("h1", {id: "start"});
+ writer.text("Hello");
+ writer.close("h1");
+ return assert.equal(writer.asString(), '# <a id="start"></a>Hello\n\n');
+ });
+
+ test('links have anchors before opening square bracket', function() {
+ var writer = mdWriter.writer();
+ writer.open("a", {href: "http://example.com", id: "start"});
+ writer.text("Hello");
+ writer.close("a");
+ return assert.equal(writer.asString(), '<a id="start"></a>[Hello](http://example.com)');
+ });
+
+ test('can generate images', function() {
+ var writer = mdWriter.writer();
+ writer.selfClosing("img", {"src": "http://example.com/image.jpg", "alt": "Alt Text"});
+ return assert.equal(writer.asString(), "![Alt Text](http://example.com/image.jpg)");
+ });
+
+ test('can generate images with missing alt attribute', function() {
+ var writer = mdWriter.writer();
+ writer.selfClosing("img", {"src": "http://example.com/image.jpg"});
+ return assert.equal(writer.asString(), "![](http://example.com/image.jpg)");
+ });
+
+ test('can generate images with missing src attribute', function() {
+ var writer = mdWriter.writer();
+ writer.selfClosing("img", {"alt": "Alt Text"});
+ return assert.equal(writer.asString(), "![Alt Text]()");
+ });
+
+ test("doesn't display empty images", function() {
+ var writer = mdWriter.writer();
+ writer.selfClosing("img");
+ return assert.equal(writer.asString(), "");
+ });
+
+ test('list item outside of list is treated as unordered list', function() {
+ var writer = mdWriter.writer();
+ writer.open("li");
+ writer.text("Hello");
+ writer.close("li");
+ return assert.equal(writer.asString(), "- Hello\n");
+ });
+
+ test('can generate an ordered list', function() {
+ var writer = mdWriter.writer();
+ writer.open("ol");
+ writer.open("li");
+ writer.text("Hello");
+ writer.close("li");
+ writer.open("li");
+ writer.text("World");
+ writer.close("li");
+ writer.close("ol");
+ return assert.equal(writer.asString(), "1. Hello\n2. World\n\n");
+ });
+
+ test('can generate an unordered list', function() {
+ var writer = mdWriter.writer();
+ writer.open("ul");
+ writer.open("li");
+ writer.text("Hello");
+ writer.close("li");
+ writer.open("li");
+ writer.text("World");
+ writer.close("li");
+ writer.close("ul");
+ return assert.equal(writer.asString(), "- Hello\n- World\n\n");
+ });
+
+ test('can generate a nested ordered list with correct numbering', function() {
+ var writer = mdWriter.writer();
+ writer.open("ol");
+ writer.open("li");
+ writer.text("Outer One");
+
+ writer.open("ol");
+ writer.open("li");
+ writer.text("Nested One");
+ writer.close("li");
+ writer.open("li");
+ writer.text("Nested Two");
+ writer.close("li");
+ writer.close("ol");
+
+ writer.close("li");
+ writer.open("li");
+ writer.text("Outer Two");
+ writer.close("li");
+ writer.close("ol");
+ return assert.equal(writer.asString(), "1. Outer One\n\t1. Nested One\n\t2. Nested Two\n2. Outer Two\n\n");
+ });
+
+ test('can generate a multi-level nested ordered list', function() {
+ var writer = mdWriter.writer();
+ writer.open("ol");
+ writer.open("li");
+ writer.text("Outer One");
+
+ writer.open("ol");
+ writer.open("li");
+ writer.text("Nested One");
+
+ writer.open("ol");
+ writer.open("li");
+ writer.text("Inner One");
+ writer.close("li");
+ writer.close("ol");
+
+ writer.close("li");
+ writer.close("ol");
+
+ writer.close("li");
+ writer.close("ol");
+ return assert.equal(writer.asString(), "1. Outer One\n\t1. Nested One\n\t\t1. Inner One\n\n");
+ });
+
+ test('new ordered list resets numbering', function() {
+ var writer = mdWriter.writer();
+ writer.open("ol");
+ writer.open("li");
+ writer.text("First");
+ writer.close("li");
+ writer.close("ol");
+
+ writer.open("p");
+ writer.text("Hello");
+ writer.close("p");
+
+ writer.open("ol");
+ writer.open("li");
+ writer.text("Second");
+ writer.close("li");
+ writer.close("ol");
+
+ return assert.equal(writer.asString(), "1. First\n\nHello\n\n1. Second\n\n");
+ });
+
+ test('can generate a nested unordered list', function() {
+ var writer = mdWriter.writer();
+ writer.open("ul");
+ writer.open("li");
+ writer.text("Outer One");
+
+ writer.open("ul");
+ writer.open("li");
+ writer.text("Nested One");
+ writer.close("li");
+ writer.open("li");
+ writer.text("Nested Two");
+ writer.close("li");
+ writer.close("ul");
+
+ writer.close("li");
+ writer.open("li");
+ writer.text("Outer Two");
+ writer.close("li");
+ writer.close("ul");
+ return assert.equal(writer.asString(), "- Outer One\n\t- Nested One\n\t- Nested Two\n- Outer Two\n\n");
+ });
+
+ test('can nest inline elements', function() {
+ var writer = mdWriter.writer();
+ writer.open("p");
+ writer.text("Lorem ");
+ writer.open("strong");
+ writer.text("ipsum ");
+ writer.open("em");
+ writer.text("dolor");
+ writer.close("em");
+ writer.text(" sit");
+ writer.close("strong");
+ writer.text(" amet");
+ writer.close("p");
+ return assert.equal(writer.asString(), "Lorem __ipsum *dolor* sit__ amet\n\n");
+ });
+
+ test('can emphasise list text', function() {
+ var writer = mdWriter.writer();
+ writer.open("ol");
+ writer.open("li");
+ writer.text("Hello ");
+ writer.open("strong");
+ writer.text("Strong");
+ writer.close("strong");
+ writer.text(" World");
+ writer.close("li");
+ writer.open("li");
+ writer.text("Hello ");
+ writer.open("em");
+ writer.text("Emphasis");
+ writer.close("em");
+ writer.text(" World");
+ writer.close("li");
+ writer.close("ol");
+ return assert.equal(writer.asString(), "1. Hello __Strong__ World\n2. Hello *Emphasis* World\n\n");
+ });
+
+ test('generates correct spacing between paragraphs and lists', function() {
+ var writer = mdWriter.writer();
+ writer.open("p");
+ writer.text("Hello World");
+ writer.close("p");
+ writer.open("ul");
+ writer.open("li");
+ writer.text("First Item");
+ writer.close("li");
+ writer.open("li");
+ writer.text("Second Item");
+ writer.close("li");
+ writer.close("ul");
+ writer.open("p");
+ writer.text("Hello World");
+ writer.close("p");
+ return assert.equal(writer.asString(), "Hello World\n\n- First Item\n- Second Item\n\nHello World\n\n");
+ });
+});
diff --git a/test/xml/reader.tests.js b/test/xml/reader.tests.js
new file mode 100644
index 0000000..afc98de
--- /dev/null
+++ b/test/xml/reader.tests.js
@@ -0,0 +1,88 @@
+var assert = require("assert");
+
+var xmlreader = require("../../lib/xml/reader");
+var test = require("../testing").test;
+
+
+describe('xmlreader.readString', function() {
+ test('should read self-closing element', function() {
+ return xmlreader.readString("<body/>").then(function(result) {
+ assert.deepEqual({type: "element", name: "body", attributes: {}, children: []}, result.root);
+ });
+ });
+
+ test('should read empty element with separate closing tag', function() {
+ return xmlreader.readString("<body></body>").then(function(result) {
+ assert.deepEqual({type: "element", name: "body", attributes: {}, children: []}, result.root);
+ });
+ });
+
+ test('should read attributes of tags', function() {
+ return xmlreader.readString('<body name="bob"/>').then(function(result) {
+ assert.deepEqual({name: "bob"}, result.root.attributes);
+ });
+ });
+
+ test('can read text element', function() {
+ return xmlreader.readString('<body>Hello!</body>').then(function(result) {
+ assert.deepEqual({type: "text", value: "Hello!"}, result.root.children[0]);
+ });
+ });
+
+ test('should read element with children', function() {
+ return xmlreader.readString("<body><a/><b/></body>").then(function(result) {
+ var root = result.root;
+ assert.equal(2, root.children.length);
+ assert.equal("a", root.children[0].name);
+ assert.equal("b", root.children[1].name);
+ });
+ });
+
+ test('unmapped namespaces URIs are included in braces as prefix', function() {
+ return xmlreader.readString('<w:body xmlns:w="word"/>').then(function(result) {
+ assert.deepEqual(result.root.name, "{word}body");
+ });
+ });
+
+ test('mapped namespaces URIs are translated using map', function() {
+ var namespaceMap = {
+ "word": "x"
+ };
+
+ return xmlreader.readString('<w:body xmlns:w="word"/>', namespaceMap).then(function(result) {
+ assert.deepEqual(result.root.name, "x:body");
+ });
+ });
+
+ test('namespace of attributes is mapped to prefix', function() {
+ var namespaceMap = {
+ "word": "x"
+ };
+ var xmlString = '<w:body xmlns:w="word" w:val="Hello!"/>';
+ return xmlreader.readString(xmlString, namespaceMap).then(function(result) {
+ assert.deepEqual(result.root.attributes["x:val"], "Hello!");
+ });
+ });
+
+ test('can find first element with name', function() {
+ return xmlreader.readString('<body><a/><b index="1"/><b index="2"/></body>').then(function(result) {
+ var first = result.root.first("b");
+ assert.equal("1", first.attributes.index);
+ });
+ });
+
+ test('whitespace between xml declaration and root tag is ignored', function() {
+ return xmlreader.readString('<?xml version="1.0" ?>\n<body/>').then(function(result) {
+ assert.deepEqual("body", result.root.name);
+ });
+ });
+
+ test('error if XML is badly formed', function() {
+ return xmlreader.readString("<bo").then(function(result) {
+ throw new Error("Expected failure");
+ }, function(error) {
+ assert.ok(error);
+ return 1;
+ });
+ });
+});
diff --git a/test/xml/writer.tests.js b/test/xml/writer.tests.js
new file mode 100644
index 0000000..d41ddc1
--- /dev/null
+++ b/test/xml/writer.tests.js
@@ -0,0 +1,83 @@
+var assert = require("assert");
+
+var xml = require("../../lib/xml");
+var writer = require("../../lib/xml/writer");
+var test = require("../testing").test;
+
+
+describe('xml.writer', function() {
+ test('writing empty root element writes out xml declaration and empty root element', function() {
+ assertXmlString(xml.element("root"), {}, '<root/>');
+ });
+
+ test('can write empty child elements', function() {
+ assertXmlString(xml.element("root", {}, [xml.element("album"), xml.element("single")]), {},
+ '<root><album/><single/></root>');
+ });
+
+ test('can write empty descendant elements', function() {
+ var element = xml.element("root", {}, [
+ xml.element("album", {}, [
+ xml.element("year"),
+ xml.element("song")
+ ])
+ ]);
+ assertXmlString(element, {},
+ '<root><album><year/><song/></album></root>');
+ });
+
+ test('can write element attributes', function() {
+ var element = xml.element("root", {}, [
+ xml.element("album", {"title": "Everything in Transit"})
+ ]);
+ assertXmlString(element, {},
+ '<root><album title="Everything in Transit"/></root>');
+ });
+
+ test('can write text nodes', function() {
+ var element = xml.element("root", {}, [
+ xml.element("album", {}, [
+ xml.text("Everything in Transit")
+ ])
+ ]);
+ assertXmlString(element, {},
+ '<root><album>Everything in Transit</album></root>');
+ });
+
+ test('can write root element with long-form prefix when URI is namespace', function() {
+ var element = xml.element("{music}root");
+ assertXmlString(element, {"m": "music"},
+ '<m:root xmlns:m="music"/>');
+ });
+
+ test('can write child elements with long-form prefix when URI is namespace', function() {
+ var element = xml.element("root", {}, [
+ xml.element("{music}album")
+ ]);
+ assertXmlString(element, {"m": "music"},
+ '<root xmlns:m="music"><m:album/></root>');
+ });
+
+ test('can write child elements with short-form prefix when URI is namespace', function() {
+ var element = xml.element("root", {}, [
+ xml.element("m:album")
+ ]);
+ assertXmlString(element, {"m": "music"},
+ '<root xmlns:m="music"><m:album/></root>');
+ });
+
+ test('default namespace has key of empty string', function() {
+ var element = xml.element("root", {}, [
+ xml.element("{music}album")
+ ]);
+ assertXmlString(element, {"": "music"},
+ '<root xmlns="music"><album/></root>');
+ });
+});
+
+
+function assertXmlString(element, namespaces, expectedString) {
+ assert.equal(writer.writeString(element, namespaces),
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
+ expectedString);
+}
diff --git a/test/zipfile.tests.js b/test/zipfile.tests.js
new file mode 100644
index 0000000..1a0ab99
--- /dev/null
+++ b/test/zipfile.tests.js
@@ -0,0 +1,26 @@
+var assert = require("assert");
+
+var JSZip = require("jszip");
+
+var test = require("./testing").test;
+var zipfile = require("../lib/zipfile");
+
+describe("zipfile", function() {
+ test('file in zip can be read after being written', function() {
+ var zip = emptyZipFile();
+ assert(!zip.exists("song/title"));
+
+ zip.write("song/title", "Dark Blue");
+
+ assert(zip.exists("song/title"));
+ return zip.read("song/title", "utf8").then(function(contents) {
+ assert.equal(contents, "Dark Blue");
+ });
+ });
+});
+
+function emptyZipFile() {
+ var zip = new JSZip();
+ var buffer = zip.generate({type: "arraybuffer"});
+ return zipfile.openArrayBuffer(buffer);
+}