var assert = require("assert");
var promises = require("../lib/promises");
var mammoth = require("../lib");
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, "
Hello.
");
});
});
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, "1 < 2
");
});
});
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, "Hello.
Goodbye.
");
});
});
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, "Hello.
");
});
});
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, "Hello.
");
});
});
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, "Hello.
");
});
});
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, "Hello.
");
});
});
test('bold runs are wrapped in tags', function() {
var run = runOfText("Hello.", {isBold: true});
var converter = new DocumentConverter();
return converter.convertToHtml(run).then(function(result) {
assert.equal(result.value, "Hello.");
});
});
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, "Hello.
");
});
});
test('consecutive bold runs are wrapped in a single 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, "Hello.
");
});
});
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 wrapped in tags using convertUnderline option', function() {
var run = runOfText("Hello.", {isUnderline: true});
var converter = new DocumentConverter({
convertUnderline: mammoth.underline.element("u")
});
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, "Hello.");
});
});
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, "Hello.");
});
});
test('strikethrough runs are wrapped in 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, "Hello.");
});
});
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, "Hello.");
});
});
test('italic runs are wrapped in tags', function() {
var run = runOfText("Hello.", {isItalic: true});
var converter = new DocumentConverter();
return converter.convertToHtml(run).then(function(result) {
assert.equal(result.value, "Hello.");
});
});
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, "Hello.");
});
});
test('superscript runs are wrapped in 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, "Hello.");
});
});
test('subscript runs are wrapped in 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, "Hello.");
});
});
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, "Hello.");
});
});
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 ', 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, 'Hello.');
});
});
test('docx hyperlink with anchor is converted to ', 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, 'Hello.');
});
});
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, '');
});
});
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 = "" +
"Top left | Top right |
" +
"Bottom left | Bottom right |
" +
"
";
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 = "";
assert.equal(result.value, expectedHtml);
});
});
test('line break is converted to
', function() {
var lineBreak = new documents.LineBreak();
var converter = new DocumentConverter();
return converter.convertToHtml(lineBreak).then(function(result) {
assert.equal(result.value, "
");
});
});
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, '');
});
});
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 = 'Knock knock
' +
'
';
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, '
');
});
});
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, callback) {
element.read("utf8").then(function(altText) {
callback(null, Html.selfClosingElement("img", {alt: altText}));
});
}
});
return converter.convertToHtml(image).then(function(result) {
assert.equal(result.value, '
');
});
});
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("Hello.
"), 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);
}