document-to-html.tests.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. var assert = require("assert");
  2. var promises = require("../lib/promises");
  3. var documents = require("../lib/documents");
  4. var documentToHtml = require("../lib/document-to-html");
  5. var DocumentConverter = documentToHtml.DocumentConverter;
  6. var commentAuthorLabel = documentToHtml.commentAuthorLabel;
  7. var test = require("./test")(module);
  8. var htmlPaths = require("../lib/styles/html-paths");
  9. var xml = require("../lib/xml");
  10. var results = require("../lib/results");
  11. var documentMatchers = require("../lib/styles/document-matchers");
  12. var Html = require("../lib/html");
  13. test('should empty document to empty string', function() {
  14. var document = new documents.Document([]);
  15. var converter = new DocumentConverter();
  16. return converter.convertToHtml(document).then(function(result) {
  17. assert.equal(result.value, "");
  18. });
  19. });
  20. test('should convert document containing one paragraph to single p element', function() {
  21. var document = new documents.Document([
  22. paragraphOfText("Hello.")
  23. ]);
  24. var converter = new DocumentConverter();
  25. return converter.convertToHtml(document).then(function(result) {
  26. assert.equal(result.value, "<p>Hello.</p>");
  27. });
  28. });
  29. test('ignores empty paragraphs', function() {
  30. var document = new documents.Document([
  31. paragraphOfText("")
  32. ]);
  33. var converter = new DocumentConverter();
  34. return converter.convertToHtml(document).then(function(result) {
  35. assert.equal(result.value, "");
  36. });
  37. });
  38. test('text is HTML-escaped', function() {
  39. var document = new documents.Document([
  40. paragraphOfText("1 < 2")
  41. ]);
  42. var converter = new DocumentConverter();
  43. return converter.convertToHtml(document).then(function(result) {
  44. assert.equal(result.value, "<p>1 &lt; 2</p>");
  45. });
  46. });
  47. test('should convert document containing multiple paragraphs to multiple p elements', function() {
  48. var document = new documents.Document([
  49. paragraphOfText("Hello."),
  50. paragraphOfText("Goodbye.")
  51. ]);
  52. var converter = new DocumentConverter();
  53. return converter.convertToHtml(document).then(function(result) {
  54. assert.equal(result.value, "<p>Hello.</p><p>Goodbye.</p>");
  55. });
  56. });
  57. test('uses style mappings to pick HTML element for docx paragraph', function() {
  58. var document = new documents.Document([
  59. paragraphOfText("Hello.", "Heading1", "Heading 1")
  60. ]);
  61. var converter = new DocumentConverter({
  62. styleMap: [
  63. {
  64. from: documentMatchers.paragraph({styleName: documentMatchers.equalTo("Heading 1")}),
  65. to: htmlPaths.topLevelElement("h1")
  66. }
  67. ]
  68. });
  69. return converter.convertToHtml(document).then(function(result) {
  70. assert.equal(result.value, "<h1>Hello.</h1>");
  71. });
  72. });
  73. test('mappings for style names are case insensitive', function() {
  74. var document = new documents.Document([
  75. paragraphOfText("Hello.", "Heading1", "heading 1")
  76. ]);
  77. var converter = new DocumentConverter({
  78. styleMap: [
  79. {
  80. from: documentMatchers.paragraph({styleName: documentMatchers.equalTo("Heading 1")}),
  81. to: htmlPaths.topLevelElement("h1")
  82. }
  83. ]
  84. });
  85. return converter.convertToHtml(document).then(function(result) {
  86. assert.equal(result.value, "<h1>Hello.</h1>");
  87. });
  88. });
  89. test('can use non-default HTML element for unstyled paragraphs', function() {
  90. var document = new documents.Document([
  91. paragraphOfText("Hello.")
  92. ]);
  93. var converter = new DocumentConverter({
  94. styleMap: [
  95. {
  96. from: documentMatchers.paragraph(),
  97. to: htmlPaths.topLevelElement("h1")
  98. }
  99. ]
  100. });
  101. return converter.convertToHtml(document).then(function(result) {
  102. assert.equal(result.value, "<h1>Hello.</h1>");
  103. });
  104. });
  105. test('warning is emitted if paragraph style is unrecognised', function() {
  106. var document = new documents.Document([
  107. paragraphOfText("Hello.", "Heading1", "Heading 1")
  108. ]);
  109. var converter = new DocumentConverter();
  110. return converter.convertToHtml(document).then(function(result) {
  111. assert.deepEqual(result.messages, [results.warning("Unrecognised paragraph style: 'Heading 1' (Style ID: Heading1)")]);
  112. });
  113. });
  114. test('can use stacked styles to generate nested HTML elements', function() {
  115. var document = new documents.Document([
  116. paragraphOfText("Hello.")
  117. ]);
  118. var converter = new DocumentConverter({
  119. styleMap: [
  120. {
  121. from: documentMatchers.paragraph(),
  122. to: htmlPaths.elements(["h1", "span"])
  123. }
  124. ]
  125. });
  126. return converter.convertToHtml(document).then(function(result) {
  127. assert.equal(result.value, "<h1><span>Hello.</span></h1>");
  128. });
  129. });
  130. test('bold runs are wrapped in <strong> tags by default', function() {
  131. var run = runOfText("Hello.", {isBold: true});
  132. var converter = new DocumentConverter();
  133. return converter.convertToHtml(run).then(function(result) {
  134. assert.equal(result.value, "<strong>Hello.</strong>");
  135. });
  136. });
  137. test('bold runs can be configured with style mapping', function() {
  138. var run = runOfText("Hello.", {isBold: true});
  139. var converter = new DocumentConverter({
  140. styleMap: [
  141. {
  142. from: documentMatchers.bold,
  143. to: htmlPaths.elements([htmlPaths.element("em")])
  144. }
  145. ]
  146. });
  147. return converter.convertToHtml(run).then(function(result) {
  148. assert.equal(result.value, "<em>Hello.</em>");
  149. });
  150. });
  151. test('bold runs can exist inside other tags', function() {
  152. var run = new documents.Paragraph([
  153. runOfText("Hello.", {isBold: true})
  154. ]);
  155. var converter = new DocumentConverter();
  156. return converter.convertToHtml(run).then(function(result) {
  157. assert.equal(result.value, "<p><strong>Hello.</strong></p>");
  158. });
  159. });
  160. test('consecutive bold runs are wrapped in a single <strong> element', function() {
  161. var paragraph = new documents.Paragraph([
  162. runOfText("Hello", {isBold: true}),
  163. runOfText(".", {isBold: true})
  164. ]);
  165. var converter = new DocumentConverter();
  166. return converter.convertToHtml(paragraph).then(function(result) {
  167. assert.equal(result.value, "<p><strong>Hello.</strong></p>");
  168. });
  169. });
  170. test('underline runs are ignored by default', function() {
  171. var run = runOfText("Hello.", {isUnderline: true});
  172. var converter = new DocumentConverter();
  173. return converter.convertToHtml(run).then(function(result) {
  174. assert.equal(result.value, "Hello.");
  175. });
  176. });
  177. test('underline runs can be mapped using style mapping', function() {
  178. var run = runOfText("Hello.", {isUnderline: true});
  179. var converter = new DocumentConverter({
  180. styleMap: [
  181. {
  182. from: documentMatchers.underline,
  183. to: htmlPaths.elements([htmlPaths.element("u")])
  184. }
  185. ]
  186. });
  187. return converter.convertToHtml(run).then(function(result) {
  188. assert.equal(result.value, "<u>Hello.</u>");
  189. });
  190. });
  191. test('style mapping for underline runs does not close parent elements', function() {
  192. var run = runOfText("Hello.", {isUnderline: true, isBold: true});
  193. var converter = new DocumentConverter({
  194. styleMap: [
  195. {
  196. from: documentMatchers.underline,
  197. to: htmlPaths.elements([htmlPaths.element("u")])
  198. }
  199. ]
  200. });
  201. return converter.convertToHtml(run).then(function(result) {
  202. assert.equal(result.value, "<strong><u>Hello.</u></strong>");
  203. });
  204. });
  205. test('strikethrough runs are wrapped in <s> tags by default', function() {
  206. var run = runOfText("Hello.", {isStrikethrough: true});
  207. var converter = new DocumentConverter();
  208. return converter.convertToHtml(run).then(function(result) {
  209. assert.equal(result.value, "<s>Hello.</s>");
  210. });
  211. });
  212. test('strikethrough runs can be configured with style mapping', function() {
  213. var run = runOfText("Hello.", {isStrikethrough: true});
  214. var converter = new DocumentConverter({
  215. styleMap: [
  216. {
  217. from: documentMatchers.strikethrough,
  218. to: htmlPaths.elements([htmlPaths.element("del")])
  219. }
  220. ]
  221. });
  222. return converter.convertToHtml(run).then(function(result) {
  223. assert.equal(result.value, "<del>Hello.</del>");
  224. });
  225. });
  226. test('italic runs are wrapped in <em> tags', function() {
  227. var run = runOfText("Hello.", {isItalic: true});
  228. var converter = new DocumentConverter();
  229. return converter.convertToHtml(run).then(function(result) {
  230. assert.equal(result.value, "<em>Hello.</em>");
  231. });
  232. });
  233. test('italic runs can be configured with style mapping', function() {
  234. var run = runOfText("Hello.", {isItalic: true});
  235. var converter = new DocumentConverter({
  236. styleMap: [
  237. {
  238. from: documentMatchers.italic,
  239. to: htmlPaths.elements([htmlPaths.element("strong")])
  240. }
  241. ]
  242. });
  243. return converter.convertToHtml(run).then(function(result) {
  244. assert.equal(result.value, "<strong>Hello.</strong>");
  245. });
  246. });
  247. test('run can be both bold and italic', function() {
  248. var run = runOfText("Hello.", {isBold: true, isItalic: true});
  249. var converter = new DocumentConverter();
  250. return converter.convertToHtml(run).then(function(result) {
  251. assert.equal(result.value, "<strong><em>Hello.</em></strong>");
  252. });
  253. });
  254. test('superscript runs are wrapped in <sup> tags', function() {
  255. var run = runOfText("Hello.", {
  256. verticalAlignment: documents.verticalAlignment.superscript
  257. });
  258. var converter = new DocumentConverter();
  259. return converter.convertToHtml(run).then(function(result) {
  260. assert.equal(result.value, "<sup>Hello.</sup>");
  261. });
  262. });
  263. test('subscript runs are wrapped in <sub> tags', function() {
  264. var run = runOfText("Hello.", {
  265. verticalAlignment: documents.verticalAlignment.subscript
  266. });
  267. var converter = new DocumentConverter();
  268. return converter.convertToHtml(run).then(function(result) {
  269. assert.equal(result.value, "<sub>Hello.</sub>");
  270. });
  271. });
  272. test('all caps runs are ignored by default', function() {
  273. var run = runOfText("Hello.", {isAllCaps: true});
  274. var converter = new DocumentConverter();
  275. return converter.convertToHtml(run).then(function(result) {
  276. assert.equal(result.value, "Hello.");
  277. });
  278. });
  279. test('all caps runs can be configured with style mapping', function() {
  280. var run = runOfText("Hello.", {isAllCaps: true});
  281. var converter = new DocumentConverter({
  282. styleMap: [
  283. {
  284. from: documentMatchers.allCaps,
  285. to: htmlPaths.elements([htmlPaths.element("span")])
  286. }
  287. ]
  288. });
  289. return converter.convertToHtml(run).then(function(result) {
  290. assert.equal(result.value, "<span>Hello.</span>");
  291. });
  292. });
  293. test('small caps runs are ignored by default', function() {
  294. var run = runOfText("Hello.", {isSmallCaps: true});
  295. var converter = new DocumentConverter();
  296. return converter.convertToHtml(run).then(function(result) {
  297. assert.equal(result.value, "Hello.");
  298. });
  299. });
  300. test('small caps runs can be configured with style mapping', function() {
  301. var run = runOfText("Hello.", {isSmallCaps: true});
  302. var converter = new DocumentConverter({
  303. styleMap: [
  304. {
  305. from: documentMatchers.smallCaps,
  306. to: htmlPaths.elements([htmlPaths.element("span")])
  307. }
  308. ]
  309. });
  310. return converter.convertToHtml(run).then(function(result) {
  311. assert.equal(result.value, "<span>Hello.</span>");
  312. });
  313. });
  314. test('highlighted runs are ignored by default', function() {
  315. var run = runOfText("Hello.", {highlight: "yellow"});
  316. var converter = new DocumentConverter();
  317. return converter.convertToHtml(run).then(function(result) {
  318. assert.equal(result.value, "Hello.");
  319. });
  320. });
  321. test('highlighted runs can be configured with style mapping for all highlights', function() {
  322. var run = runOfText("Hello.", {highlight: "yellow"});
  323. var converter = new DocumentConverter({
  324. styleMap: [
  325. {
  326. from: documentMatchers.highlight(null),
  327. to: htmlPaths.elements([htmlPaths.element("mark")])
  328. }
  329. ]
  330. });
  331. return converter.convertToHtml(run).then(function(result) {
  332. assert.equal(result.value, "<mark>Hello.</mark>");
  333. });
  334. });
  335. test('highlighted runs can be configured with style mapping for specific highlight color', function() {
  336. var paragraph = new documents.Paragraph([
  337. runOfText("Yellow", {highlight: "yellow"}),
  338. runOfText("Red", {highlight: "red"})
  339. ]);
  340. var converter = new DocumentConverter({
  341. styleMap: [
  342. {
  343. from: documentMatchers.highlight({color: "yellow"}),
  344. to: htmlPaths.elements([htmlPaths.element("mark", {"class": "yellow"})])
  345. },
  346. {
  347. from: documentMatchers.highlight({color: undefined}),
  348. to: htmlPaths.elements([htmlPaths.element("mark")])
  349. }
  350. ]
  351. });
  352. return converter.convertToHtml(paragraph).then(function(result) {
  353. assert.equal(result.value, '<p><mark class="yellow">Yellow</mark><mark>Red</mark></p>');
  354. });
  355. });
  356. test('run styles are converted to HTML if mapping exists', function() {
  357. var run = runOfText("Hello.", {styleId: "Heading1Char", styleName: "Heading 1 Char"});
  358. var converter = new DocumentConverter({
  359. styleMap: [
  360. {
  361. from: documentMatchers.run({styleName: documentMatchers.equalTo("Heading 1 Char")}),
  362. to: htmlPaths.elements(["strong"])
  363. }
  364. ]
  365. });
  366. return converter.convertToHtml(run).then(function(result) {
  367. assert.equal(result.value, "<strong>Hello.</strong>");
  368. });
  369. });
  370. test('warning is emitted if run style is unrecognised', function() {
  371. var run = runOfText("Hello.", {styleId: "Heading1Char", styleName: "Heading 1 Char"});
  372. var converter = new DocumentConverter();
  373. return converter.convertToHtml(run).then(function(result) {
  374. assert.deepEqual(result.messages, [results.warning("Unrecognised run style: 'Heading 1 Char' (Style ID: Heading1Char)")]);
  375. });
  376. });
  377. test('docx hyperlink is converted to <a>', function() {
  378. var hyperlink = new documents.Hyperlink(
  379. [runOfText("Hello.")],
  380. {href: "http://www.example.com"}
  381. );
  382. var converter = new DocumentConverter();
  383. return converter.convertToHtml(hyperlink).then(function(result) {
  384. assert.equal(result.value, '<a href="http://www.example.com">Hello.</a>');
  385. });
  386. });
  387. test('docx hyperlink can be collapsed', function() {
  388. var hyperlink = new documents.Document([
  389. new documents.Hyperlink(
  390. [runOfText("Hello ")],
  391. {href: "http://www.example.com"}
  392. ),
  393. new documents.Hyperlink(
  394. [runOfText("world")],
  395. {href: "http://www.example.com"}
  396. )
  397. ]);
  398. var converter = new DocumentConverter();
  399. return converter.convertToHtml(hyperlink).then(function(result) {
  400. assert.equal(result.value, '<a href="http://www.example.com">Hello world</a>');
  401. });
  402. });
  403. test('docx hyperlink with anchor is converted to <a>', function() {
  404. var hyperlink = new documents.Hyperlink(
  405. [runOfText("Hello.")],
  406. {anchor: "_Peter"}
  407. );
  408. var converter = new DocumentConverter({
  409. idPrefix: "doc-42-"
  410. });
  411. return converter.convertToHtml(hyperlink).then(function(result) {
  412. assert.equal(result.value, '<a href="#doc-42-_Peter">Hello.</a>');
  413. });
  414. });
  415. test('hyperlink target frame is used as anchor target', function() {
  416. var hyperlink = new documents.Hyperlink(
  417. [runOfText("Hello.")],
  418. {anchor: "start", targetFrame: "_blank"}
  419. );
  420. var converter = new DocumentConverter();
  421. return converter.convertToHtml(hyperlink).then(function(result) {
  422. assert.equal(result.value, '<a href="#start" target="_blank">Hello.</a>');
  423. });
  424. });
  425. test('bookmarks are converted to anchors', function() {
  426. var bookmarkStart = new documents.BookmarkStart({name: "_Peter"});
  427. var converter = new DocumentConverter({
  428. idPrefix: "doc-42-"
  429. });
  430. var document = new documents.Document([bookmarkStart]);
  431. return converter.convertToHtml(document).then(function(result) {
  432. assert.equal(result.value, '<a id="doc-42-_Peter"></a>');
  433. });
  434. });
  435. test('docx tab is converted to tab in HTML', function() {
  436. var tab = new documents.Tab();
  437. var converter = new DocumentConverter();
  438. return converter.convertToHtml(tab).then(function(result) {
  439. assert.equal(result.value, "\t");
  440. });
  441. });
  442. test('docx table is converted to table in HTML', function() {
  443. var table = new documents.Table([
  444. new documents.TableRow([
  445. new documents.TableCell([paragraphOfText("Top left")]),
  446. new documents.TableCell([paragraphOfText("Top right")])
  447. ]),
  448. new documents.TableRow([
  449. new documents.TableCell([paragraphOfText("Bottom left")]),
  450. new documents.TableCell([paragraphOfText("Bottom right")])
  451. ])
  452. ]);
  453. var converter = new DocumentConverter();
  454. return converter.convertToHtml(table).then(function(result) {
  455. var expectedHtml = "<table>" +
  456. "<tr><td><p>Top left</p></td><td><p>Top right</p></td></tr>" +
  457. "<tr><td><p>Bottom left</p></td><td><p>Bottom right</p></td></tr>" +
  458. "</table>";
  459. assert.equal(result.value, expectedHtml);
  460. });
  461. });
  462. test('table style mappings can be used to map tables', function() {
  463. var table = new documents.Table([], {styleName: "Normal Table"});
  464. var converter = new DocumentConverter({
  465. styleMap: [
  466. {
  467. from: documentMatchers.table({styleName: documentMatchers.equalTo("Normal Table")}),
  468. to: htmlPaths.topLevelElement("table", {"class": "normal-table"})
  469. }
  470. ]
  471. });
  472. return converter.convertToHtml(table).then(function(result) {
  473. var expectedHtml = '<table class="normal-table"></table>';
  474. assert.equal(result.value, expectedHtml);
  475. });
  476. });
  477. test('header rows are wrapped in thead', function() {
  478. var table = new documents.Table([
  479. new documents.TableRow([new documents.TableCell([])], {isHeader: true}),
  480. new documents.TableRow([new documents.TableCell([])], {isHeader: true}),
  481. new documents.TableRow([new documents.TableCell([])], {isHeader: false})
  482. ]);
  483. var converter = new DocumentConverter();
  484. return converter.convertToHtml(table).then(function(result) {
  485. var expectedHtml = "<table>" +
  486. "<thead><tr><th></th></tr><tr><th></th></tr></thead>" +
  487. "<tbody><tr><td></td></tr></tbody>" +
  488. "</table>";
  489. assert.equal(result.value, expectedHtml);
  490. });
  491. });
  492. test('tbody is omitted if all rows are headers', function() {
  493. var table = new documents.Table([
  494. new documents.TableRow([new documents.TableCell([])], {isHeader: true})
  495. ]);
  496. var converter = new DocumentConverter();
  497. return converter.convertToHtml(table).then(function(result) {
  498. var expectedHtml = "<table>" +
  499. "<thead><tr><th></th></tr></thead>" +
  500. "</table>";
  501. assert.equal(result.value, expectedHtml);
  502. });
  503. });
  504. test('unexpected table children do not cause error', function() {
  505. var table = new documents.Table([
  506. new documents.tab()
  507. ]);
  508. var converter = new DocumentConverter();
  509. return converter.convertToHtml(table).then(function(result) {
  510. var expectedHtml = "<table>\t</table>";
  511. assert.equal(result.value, expectedHtml);
  512. });
  513. });
  514. test('empty cells are preserved in table', function() {
  515. var table = new documents.Table([
  516. new documents.TableRow([
  517. new documents.TableCell([paragraphOfText("")]),
  518. new documents.TableCell([paragraphOfText("Top right")])
  519. ])
  520. ]);
  521. var converter = new DocumentConverter();
  522. return converter.convertToHtml(table).then(function(result) {
  523. var expectedHtml = "<table>" +
  524. "<tr><td></td><td><p>Top right</p></td></tr>" +
  525. "</table>";
  526. assert.equal(result.value, expectedHtml);
  527. });
  528. });
  529. test('empty rows are preserved in table', function() {
  530. var table = new documents.Table([
  531. new documents.TableRow([
  532. new documents.TableCell([paragraphOfText("Row 1")])
  533. ]),
  534. new documents.TableRow([])
  535. ]);
  536. var converter = new DocumentConverter();
  537. return converter.convertToHtml(table).then(function(result) {
  538. var expectedHtml = "<table>" +
  539. "<tr><td><p>Row 1</p></td></tr><tr></tr>" +
  540. "</table>";
  541. assert.equal(result.value, expectedHtml);
  542. });
  543. });
  544. test('table cells are written with colSpan if not equal to one', function() {
  545. var table = new documents.Table([
  546. new documents.TableRow([
  547. new documents.TableCell([paragraphOfText("Top left")], {colSpan: 2}),
  548. new documents.TableCell([paragraphOfText("Top right")])
  549. ])
  550. ]);
  551. var converter = new DocumentConverter();
  552. return converter.convertToHtml(table).then(function(result) {
  553. var expectedHtml = "<table>" +
  554. "<tr><td colspan=\"2\"><p>Top left</p></td><td><p>Top right</p></td></tr>" +
  555. "</table>";
  556. assert.equal(result.value, expectedHtml);
  557. });
  558. });
  559. test('table cells are written with rowSpan if not equal to one', function() {
  560. var table = new documents.Table([
  561. new documents.TableRow([
  562. new documents.TableCell([], {rowSpan: 2})
  563. ])
  564. ]);
  565. var converter = new DocumentConverter();
  566. return converter.convertToHtml(table).then(function(result) {
  567. var expectedHtml = "<table>" +
  568. "<tr><td rowspan=\"2\"></td></tr>" +
  569. "</table>";
  570. assert.equal(result.value, expectedHtml);
  571. });
  572. });
  573. test('line break is converted to <br>', function() {
  574. var converter = new DocumentConverter();
  575. return converter.convertToHtml(documents.lineBreak).then(function(result) {
  576. assert.equal(result.value, "<br />");
  577. });
  578. });
  579. test('breaks that are not line breaks are ignored', function() {
  580. var converter = new DocumentConverter();
  581. return converter.convertToHtml(documents.pageBreak).then(function(result) {
  582. assert.equal(result.value, "");
  583. });
  584. });
  585. test('breaks can be mapped using style mappings', function() {
  586. var converter = new DocumentConverter({
  587. styleMap: [
  588. {
  589. from: documentMatchers.pageBreak,
  590. to: htmlPaths.topLevelElement("hr")
  591. },
  592. {
  593. from: documentMatchers.lineBreak,
  594. to: htmlPaths.topLevelElement("br", {class: "line-break"})
  595. }
  596. ]
  597. });
  598. var run = documents.run([documents.pageBreak, documents.lineBreak]);
  599. return converter.convertToHtml(run).then(function(result) {
  600. assert.equal(result.value, '<hr /><br class="line-break" />');
  601. });
  602. });
  603. test('footnote reference is converted to superscript intra-page link', function() {
  604. var footnoteReference = new documents.NoteReference({
  605. noteType: "footnote",
  606. noteId: "4"
  607. });
  608. var converter = new DocumentConverter({
  609. idPrefix: "doc-42-"
  610. });
  611. return converter.convertToHtml(footnoteReference).then(function(result) {
  612. assert.equal(result.value, '<sup><a href="#doc-42-footnote-4" id="doc-42-footnote-ref-4">[1]</a></sup>');
  613. });
  614. });
  615. test('footnotes are included after the main body', function() {
  616. var footnoteReference = new documents.NoteReference({
  617. noteType: "footnote",
  618. noteId: "4"
  619. });
  620. var document = new documents.Document(
  621. [new documents.Paragraph([
  622. runOfText("Knock knock"),
  623. new documents.Run([footnoteReference])
  624. ])],
  625. {
  626. notes: new documents.Notes({
  627. 4: new documents.Note({
  628. noteType: "footnote",
  629. noteId: "4",
  630. body: [paragraphOfText("Who's there?")]
  631. })
  632. })
  633. }
  634. );
  635. var converter = new DocumentConverter({
  636. idPrefix: "doc-42-"
  637. });
  638. return converter.convertToHtml(document).then(function(result) {
  639. var expectedOutput = '<p>Knock knock<sup><a href="#doc-42-footnote-4" id="doc-42-footnote-ref-4">[1]</a></sup></p>' +
  640. '<ol><li id="doc-42-footnote-4"><p>Who\'s there? <a href="#doc-42-footnote-ref-4">↑</a></p></li></ol>';
  641. assert.equal(result.value, expectedOutput);
  642. });
  643. });
  644. test('comments are ignored by default', function() {
  645. var reference = documents.commentReference({commentId: "4"});
  646. var comment = documents.comment({
  647. commentId: "4",
  648. body: [paragraphOfText("Who's there?")]
  649. });
  650. var document = documents.document([
  651. documents.paragraph([
  652. runOfText("Knock knock"),
  653. documents.run([reference])
  654. ])
  655. ], {comments: [comment]});
  656. var converter = new DocumentConverter({});
  657. return converter.convertToHtml(document).then(function(result) {
  658. assert.equal(result.value, '<p>Knock knock</p>');
  659. assert.deepEqual(result.messages, []);
  660. });
  661. });
  662. test('comment references are linked to comment after main body', function() {
  663. var reference = documents.commentReference({commentId: "4"});
  664. var comment = documents.comment({
  665. commentId: "4",
  666. body: [paragraphOfText("Who's there?")],
  667. authorName: "The Piemaker",
  668. authorInitials: "TP"
  669. });
  670. var document = documents.document([
  671. documents.paragraph([
  672. runOfText("Knock knock"),
  673. documents.run([reference])
  674. ])
  675. ], {comments: [comment]});
  676. var converter = new DocumentConverter({
  677. idPrefix: "doc-42-",
  678. styleMap: [
  679. {from: documentMatchers.commentReference, to: htmlPaths.element("sup")}
  680. ]
  681. });
  682. return converter.convertToHtml(document).then(function(result) {
  683. var expectedHtml = (
  684. '<p>Knock knock<sup><a href="#doc-42-comment-4" id="doc-42-comment-ref-4">[TP1]</a></sup></p>' +
  685. '<dl><dt id="doc-42-comment-4">Comment [TP1]</dt><dd><p>Who\'s there? <a href="#doc-42-comment-ref-4">↑</a></p></dd></dl>'
  686. );
  687. assert.equal(result.value, expectedHtml);
  688. assert.deepEqual(result.messages, []);
  689. });
  690. });
  691. test('images are written with data URIs', function() {
  692. var imageBuffer = new Buffer("Not an image at all!");
  693. var image = new documents.Image({
  694. readImage: function(encoding) {
  695. return promises.when(imageBuffer.toString(encoding));
  696. },
  697. contentType: "image/png"
  698. });
  699. var converter = new DocumentConverter();
  700. return converter.convertToHtml(image).then(function(result) {
  701. assert.equal(result.value, '<img src="data:image/png;base64,' + imageBuffer.toString("base64") + '" />');
  702. });
  703. });
  704. test('images have alt attribute if available', function() {
  705. var imageBuffer = new Buffer("Not an image at all!");
  706. var image = new documents.Image({
  707. readImage: function() {
  708. return promises.when(imageBuffer);
  709. },
  710. altText: "It's a hat"
  711. });
  712. var converter = new DocumentConverter();
  713. return converter.convertToHtml(image)
  714. .then(function(result) {
  715. return xml.readString(result.value);
  716. })
  717. .then(function(htmlImageElement) {
  718. assert.equal(htmlImageElement.attributes.alt, "It's a hat");
  719. });
  720. });
  721. test('can add custom handler for images', function() {
  722. var imageBuffer = new Buffer("Not an image at all!");
  723. var image = new documents.Image({
  724. readImage: function(encoding) {
  725. return promises.when(imageBuffer.toString(encoding));
  726. },
  727. contentType: "image/png"
  728. });
  729. var converter = new DocumentConverter({
  730. convertImage: function(element, messages) {
  731. return element.read("utf8").then(function(altText) {
  732. return [Html.freshElement("img", {alt: altText})];
  733. });
  734. }
  735. });
  736. return converter.convertToHtml(image).then(function(result) {
  737. assert.equal(result.value, '<img alt="Not an image at all!" />');
  738. });
  739. });
  740. test('when custom image handler throws error then error is stored in error message', function() {
  741. var error = new Error("Failed to convert image");
  742. var image = new documents.Image({
  743. readImage: function(encoding) {
  744. return promises.when(new Buffer().toString(encoding));
  745. },
  746. contentType: "image/png"
  747. });
  748. var converter = new DocumentConverter({
  749. convertImage: function(element, messages) {
  750. throw error;
  751. }
  752. });
  753. return converter.convertToHtml(image).then(function(result) {
  754. assert.equal(result.value, '');
  755. assert.equal(result.messages.length, 1);
  756. var message = result.messages[0];
  757. assert.equal("error", message.type);
  758. assert.equal("Failed to convert image", message.message);
  759. assert.equal(error, message.error);
  760. });
  761. });
  762. test('long documents do not cause stack overflow', function() {
  763. var paragraphs = [];
  764. for (var i = 0; i < 1000; i++) {
  765. paragraphs.push(paragraphOfText("Hello."));
  766. }
  767. var document = new documents.Document(paragraphs);
  768. var converter = new DocumentConverter();
  769. return converter.convertToHtml(document).then(function(result) {
  770. assert.equal(result.value.indexOf("<p>Hello.</p>"), 0);
  771. });
  772. });
  773. function paragraphOfText(text, styleId, styleName) {
  774. var run = runOfText(text);
  775. return new documents.Paragraph([run], {
  776. styleId: styleId,
  777. styleName: styleName
  778. });
  779. }
  780. function runOfText(text, properties) {
  781. var textElement = new documents.Text(text);
  782. return new documents.Run([textElement], properties);
  783. }
  784. test('when initials are not blank then comment author label is initials', function() {
  785. assert.equal(commentAuthorLabel({authorInitials: "TP"}), "TP");
  786. });
  787. test('when initials are blank then comment author label is blank', function() {
  788. assert.equal(commentAuthorLabel({authorInitials: ""}), "");
  789. assert.equal(commentAuthorLabel({authorInitials: undefined}), "");
  790. assert.equal(commentAuthorLabel({authorInitials: null}), "");
  791. });