diff --git a/.gitignore b/.gitignore index 53f1416525..4f8777b466 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,10 @@ node_modules build build-tests +# Documentation +docs/api/ +docs/.nojekyll + # VSCode .vscode/* !.vscode/settings.json diff --git a/.travis.yml b/.travis.yml index fb685d8fc6..986233743d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "stable" + - 9 install: - npm install script: @@ -24,7 +24,8 @@ after_failure: - "cat /home/travis/builds/dolanmiu/docx/npm-debug.log" after_success: - npm run typedoc - - echo "janchi.co.uk" > docs/.nojekyll + - echo "docx.js.org" > docs/.nojekyll + - echo "docx.js.org" > docs/CNAME deploy: provider: pages skip-cleanup: true diff --git a/README.md b/README.md index 1be9336a61..84b4a75e0b 100644 --- a/README.md +++ b/README.md @@ -3,106 +3,59 @@

- Generate .docx files with JS/TS very easily, written in TS. + Easily generate .docx files with JS/TS.

--- [![NPM version][npm-image]][npm-url] +[![Downloads per month][downloads-image]][downloads-url] [![Build Status][travis-image]][travis-url] -[![Dependency Status][gemnasium-image]][gemnasium-url] +[![Dependency Status][daviddm-image]][daviddm-url] [![Known Vulnerabilities][snky-image]][snky-url] [![Chat on Gitter][gitter-image]][gitter-url] -[![code style: prettier][prettier-image]][prettier-url] [![PRs Welcome][pr-image]][pr-url] -[![NPM](https://nodei.co/npm/docx.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/docx/) +

+ drawing +

-# docx - -## Install - -```sh -$ npm install --save docx -``` - -## Demo +# Demo Press `endpoint` on the `RunKit` website: ![RunKit Instructions](https://user-images.githubusercontent.com/2917613/38582539-f84311b6-3d07-11e8-90db-5885ae02c3c4.png) -* https://runkit.com/dolanmiu/docx-demo1 - Simple paragraph and text -* https://runkit.com/dolanmiu/docx-demo2 - Advanced Paragraphs and text -* https://runkit.com/dolanmiu/docx-demo3 - Bullet points -* https://runkit.com/dolanmiu/docx-demo4 - Simple table -* https://runkit.com/dolanmiu/docx-demo5 - Images -* https://runkit.com/dolanmiu/docx-demo6 - Margins -* https://runkit.com/dolanmiu/docx-demo7 - Landscape -* https://runkit.com/dolanmiu/docx-demo8/1.0.1 - Header and Footer -* https://runkit.com/dolanmiu/docx-demo10 - **My CV generated with docx** +* https://runkit.com/dolanmiu/docx-demo1 - Simple paragraph and text +* https://runkit.com/dolanmiu/docx-demo2 - Advanced Paragraphs and text +* https://runkit.com/dolanmiu/docx-demo3 - Bullet points +* https://runkit.com/dolanmiu/docx-demo4 - Simple table +* https://runkit.com/dolanmiu/docx-demo5 - Images +* https://runkit.com/dolanmiu/docx-demo6 - Margins +* https://runkit.com/dolanmiu/docx-demo7 - Landscape +* https://runkit.com/dolanmiu/docx-demo8/1.0.1 - Header and Footer +* https://runkit.com/dolanmiu/docx-demo10 - **My CV generated with docx** -#### Run demos locally: +# How to use & Documentation -```sh -$ npm run demo -``` +Please refer to the [documentation at https://docx.js.org/](https://docx.js.org/) for details on how to use this library, examples and much more! -This command will run the demo selector app in the `demo` folder. It will prompt you to select a demo number, which will run a demo from that folder. +# Examples -## Guide - -Please refer to [the Wiki](https://github.com/dolanmiu/docx/wiki) for details on how to use this library, examples and much more! - -Full documentation can be found here: [http://dolanmiu.github.io/docx/index.html](http://dolanmiu.github.io/docx/index.html) - -## Simple Usage - -```js -// Used to create docx files -var docx = require("docx"); - -// Create document -var doc = new docx.Document(); - -// Add some content in the document -var paragraph = new docx.Paragraph("Some cool text here."); -// Add more text into the paragraph if you wish -paragraph.addRun(new docx.TextRun("Lorem Ipsum Foo Bar")); -doc.addParagraph(paragraph); - -// Used to export the file into a .docx file -var exporter = new docx.LocalPacker(doc); - -// Or use the express packer to make the file downloadable. -// res is express' Response object -var exporter = new docx.ExpressPacker(doc, res); - -exporter.pack("My First Document"); -// If you want to export it as a .pdf file instead -exporter.packPdf("My First Document"); - -// done! A file called 'My First Document.docx' -// will be in your file system if you used LocalPacker -// Or it will start downloading if you are using Express -``` - -## Examples - -Check [the Wiki](https://github.com/dolanmiu/docx/wiki/Examples) for examples. +Check the `examples` section in the [documentation](https://docx.js.org/#/examples) and the [demo folder](https://github.com/dolanmiu/docx/tree/master/demo) for examples. # Contributing -Read the contribution guidelines [here](https://github.com/dolanmiu/docx/wiki/Contributing-Guidelines). +Read the contribution guidelines [here](https://docx.js.org/#/contribution-guidelines). --- Made with 💖 -Huge thanks to [@felipeochoa](https://github.com/felipeochoa) for awesome contributions to this project - [npm-image]: https://badge.fury.io/js/docx.svg [npm-url]: https://npmjs.org/package/docx +[downloads-image]: https://img.shields.io/npm/dm/docx.svg +[downloads-url]: https://npmjs.org/package/docx [travis-image]: https://travis-ci.org/dolanmiu/docx.svg?branch=master [travis-url]: https://travis-ci.org/dolanmiu/docx [daviddm-image]: https://david-dm.org/dolanmiu/docx.svg?theme=shields.io @@ -111,9 +64,5 @@ Huge thanks to [@felipeochoa](https://github.com/felipeochoa) for awesome contri [snky-url]: https://snyk.io/test/github/dolanmiu/docx [gitter-image]: https://badges.gitter.im/dolanmiu/docx.svg [gitter-url]: https://gitter.im/docx-lib/Lobby -[gemnasium-image]: https://gemnasium.com/badges/github.com/dolanmiu/docx.svg -[gemnasium-url]: https://gemnasium.com/github.com/dolanmiu/docx -[prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg -[prettier-url]: https://github.com/prettier/prettier [pr-image]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg [pr-url]: http://makeapullrequest.com diff --git a/demo/assets/custom-styles.xml b/demo/assets/custom-styles.xml new file mode 100644 index 0000000000..76159f2985 --- /dev/null +++ b/demo/assets/custom-styles.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/demo/demo13.js b/demo/demo13.js index 6ce1ddd04e..f141547d7a 100644 --- a/demo/demo13.js +++ b/demo/demo13.js @@ -1,17 +1,26 @@ +// This example shows 3 styles +const fs = require('fs'); const docx = require('../build'); -var doc = new docx.Document(); +const styles = fs.readFileSync('./demo/assets/custom-styles.xml', 'utf-8'); +const doc = new docx.Document({ + title: 'Title', + externalStyles: styles +}); -var paragraph = new docx.Paragraph("Hello World"); -var institutionText = new docx.TextRun("University College London").bold(); -var dateText = new docx.TextRun("5th Dec 2015").tab().bold(); -paragraph.addRun(institutionText); -paragraph.addRun(dateText); +doc.createParagraph('Cool Heading Text').heading1(); +let paragraph = new docx.Paragraph('This is a custom named style from the template "MyFancyStyle"'); +paragraph.style('MyFancyStyle'); +doc.addParagraph(paragraph); + +doc.createParagraph('Some normal text') + +doc.createParagraph('MyFancyStyle again').style('MyFancyStyle'); +paragraph.style('MyFancyStyle'); doc.addParagraph(paragraph); var exporter = new docx.LocalPacker(doc); -exporter.packPdf('My Document').then(() => { - console.log('Document created successfully at project root!'); -}); +exporter.pack('My Document'); +console.log('Document created successfully at project root!'); diff --git a/demo/demo14.js b/demo/demo14.js new file mode 100644 index 0000000000..b2262f106a --- /dev/null +++ b/demo/demo14.js @@ -0,0 +1,24 @@ +const docx = require('../build'); + +var doc = new docx.Document(); + +doc.createParagraph("First Page").pageBreak() +doc.createParagraph("Second Page"); + +var pageNumber = new docx.TextRun().pageNumber() + +var pageoneheader = new docx.Paragraph("First Page Header ").right(); + +pageoneheader.addRun(pageNumber); +var firstPageHeader = doc.createFirstPageHeader(); +firstPageHeader.addParagraph(pageoneheader); + +var pagetwoheader = new docx.Paragraph("My Title ").right(); + +pagetwoheader.addRun(pageNumber) +doc.Header.addParagraph(pagetwoheader) + +var exporter = new docx.LocalPacker(doc); +exporter.pack('My Document'); + +console.log('Document created successfully at project root!'); diff --git a/demo/demo15.js b/demo/demo15.js new file mode 100644 index 0000000000..61d9351817 --- /dev/null +++ b/demo/demo15.js @@ -0,0 +1,14 @@ +const docx = require('../build'); + +var doc = new docx.Document(); + +var paragraph = new docx.Paragraph("Hello World"); +var paragraph2 = new docx.Paragraph("Hello World on another page").pageBreakBefore(); + +doc.addParagraph(paragraph); +doc.addParagraph(paragraph2); + +var exporter = new docx.LocalPacker(doc); +exporter.pack('My Document'); + +console.log('Document created successfully at project root!'); diff --git a/demo/demo16.js b/demo/demo16.js new file mode 100644 index 0000000000..f850bc9b4b --- /dev/null +++ b/demo/demo16.js @@ -0,0 +1,36 @@ +const docx = require("../build"); + +var doc = new docx.Document(); + +var paragraph = new docx.Paragraph("Hello World").pageBreak(); + +doc.addParagraph(paragraph); + +var header = doc.createHeader(); +header.createParagraph("Header on another page"); +var footer = doc.createFooter(); +footer.createParagraph("Footer on another page"); + +doc.addSection({ + headerId: header.Header.ReferenceId, + footerId: footer.Footer.ReferenceId, + pageNumberStart: 1, + pageNumberFormatType: docx.PageNumberFormat.DECIMAL, +}); + +doc.createParagraph("hello"); + +doc.addSection({ + headerId: header.Header.ReferenceId, + footerId: footer.Footer.ReferenceId, + pageNumberStart: 1, + pageNumberFormatType: docx.PageNumberFormat.DECIMAL, + orientation: docx.PageOrientation.LANDSCAPE, +}); + +doc.createParagraph("hello in landscape"); + +var exporter = new docx.LocalPacker(doc); +exporter.pack("My Document"); + +console.log("Document created successfully at project root!"); diff --git a/demo/demo17.js b/demo/demo17.js new file mode 100644 index 0000000000..266e556b7d --- /dev/null +++ b/demo/demo17.js @@ -0,0 +1,17 @@ +const docx = require('../build'); + +var doc = new docx.Document(); + +var paragraph = new docx.Paragraph("Hello World").referenceFootnote(1); +var paragraph2 = new docx.Paragraph("Hello World").referenceFootnote(2); + +doc.addParagraph(paragraph); +doc.addParagraph(paragraph2); + +doc.createFootnote(new docx.Paragraph("Test")); +doc.createFootnote(new docx.Paragraph("My amazing reference")); + +var exporter = new docx.LocalPacker(doc); +exporter.pack('My Document'); + +console.log('Document created successfully at project root!'); diff --git a/demo/demo18.js b/demo/demo18.js new file mode 100644 index 0000000000..9035d19e7a --- /dev/null +++ b/demo/demo18.js @@ -0,0 +1,15 @@ +// Insert image from a buffer +const docx = require('../build'); + +var doc = new docx.Document(); + + +const imageBase64Data = `iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAACzVBMVEUAAAAAAAAAAAAAAAA/AD8zMzMqKiokJCQfHx8cHBwZGRkuFxcqFSonJyckJCQiIiIfHx8eHh4cHBwoGhomGSYkJCQhISEfHx8eHh4nHR0lHBwkGyQjIyMiIiIgICAfHx8mHh4lHh4kHR0jHCMiGyIhISEgICAfHx8lHx8kHh4jHR0hHCEhISEgICAlHx8kHx8jHh4jHh4iHSIhHCEhISElICAkHx8jHx8jHh4iHh4iHSIhHSElICAkICAjHx8jHx8iHh4iHh4hHiEhHSEkICAjHx8iHx8iHx8hHh4hHiEkHSEjHSAjHx8iHx8iHx8hHh4kHiEkHiEjHSAiHx8hHx8hHh4kHiEjHiAjHSAiHx8iHx8hHx8kHh4jHiEjHiAjHiAiICAiHx8kHx8jHh4jHiEjHiAiHiAiHSAiHx8jHx8jHx8jHiAiHiAiHiAiHSAiHx8jHx8jHx8iHiAiHiAiHiAjHx8jHx8jHx8jHx8iHiAiHiAiHiAjHx8jHx8jHx8iHx8iHSAiHiAjHiAjHx8jHx8hHx8iHx8iHyAiHiAjHiAjHiAjHh4hHx8iHx8iHx8iHyAjHSAjHiAjHiAjHh4hHx8iHx8iHx8jHyAjHiAhHh4iHx8iHx8jHyAjHSAjHSAhHiAhHh4iHx8iHx8jHx8jHyAjHSAjHSAiHh4iHh4jHx8jHx8jHyAjHyAhHSAhHSAiHh4iHh4jHx8jHx8jHyAhHyAhHSAiHSAiHh4jHh4jHx8jHx8jHyAhHyAhHSAiHSAjHR4jHh4jHx8jHx8hHyAhHyAiHSAjHSAjHR4jHh4jHx8hHx8hHyAhHyAiHyAjHSAjHR4jHR4hHh4hHx8hHyAiHyAjHyAjHSAjHR4jHR4hHh4hHx8hHyAjHyAjHyAjHSAjHR4hHR4hHR4hHx8iHyAjHyAjHyAjHSAhHR4hHR4hHR4hHx8jHyAjHyAjHyAjHyC9S2xeAAAA7nRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFxgZGhscHR4fICEiIyQlJicoKSorLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZISUpLTE1OUFFSU1RVVllaW1xdXmBhYmNkZWZnaGprbG1ub3Byc3R1dnd4eXp8fn+AgYKDhIWGiImKi4yNj5CRkpOUlZaXmJmam5ydnp+goaKjpKaoqqusra6vsLGys7S1tri5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+fkZpVQAABcBJREFUGBntwftjlQMcBvDnnLNL22qzJjWlKLHFVogyty3SiFq6EZliqZGyhnSxsLlMRahYoZKRFcul5dKFCatYqWZaNKvWtrPz/A2+7/b27qRzec/lPfvl/XxgMplMJpPJZDKZAtA9HJ3ppnIez0KnSdtC0RCNznHdJrbrh85wdSlVVRaEXuoGamYi5K5430HNiTiEWHKJg05eRWgNfKeV7RxbqUhGKPV/207VupQ8is0IoX5vtFC18SqEHaK4GyHTZ2kzVR8PBTCO4oANIZL4ShNVZcOhKKeYg9DoWdhI1ec3os2VFI0JCIUez5+i6st0qJZRrEAIJCw+QdW223BG/EmKwTBc/IJ/qfp2FDrkUnwFo8U9dZyqnaPhxLqfYjyM1S3vb6p+GGOBszsojoTDSDFz6qj66R4LzvYJxVMwUNRjf1H1ywQr/megg2RzLximy8waqvbda8M5iijegVEiHjlM1W/3h+FcXesphsMY4dMOUnUgOxyuPEzxPQwRNvV3qg5Nj4BreyimwADWe/dRVTMjEm6MoGLzGwtystL6RyOY3qSqdlYU3FpLZw1VW0sK5943MvUCKwJ1noNtjs6Ohge76Zq9ZkfpigU5WWkDYuCfbs1U5HWFR8/Qq4a9W0uK5k4ZmdrTCl8spGIePLPlbqqsc1Afe83O0hULc8alDYiBd7ZyitYMeBfR55rR2fOKP6ioPk2dGvZ+UVI0d8rtqT2tcCexlqK2F3wRn5Q+YVbBqrLKOupkr9lZujAOrmS0UpTb4JeIPkNHZ+cXr6uoPk2vyuBSPhWLEKj45PQJuQWryyqP0Z14uGLdROHIRNBEXDR09EP5r62rOHCazhrD4VKPwxTH+sIA3ZPTJ+YuWV22n+IruHFDC8X2CBjnPoolcGc2FYUwzmsUWXDHsoGKLBhmN0VvuBVfTVE/AAbpaid5CB4MbaLY1QXGuIViLTyZQcVyGGMuxWPwaA0Vk2GI9RRp8Ci2iuLkIBjhT5LNUfAspZFiTwyC72KK7+DNg1SsRvCNp3gZXq2k4iEEXSHFJHgVXUlxejCCbTvFAHiXdIJiXxyCK7KJ5FHoMZGK9xBcwyg2QpdlVMxEUM2iyIMuXXZQNF+HswxMsSAAJRQjoE//eoqDCXBSTO6f1xd+O0iyNRY6jaWi1ALNYCocZROj4JdEikroVkjFk9DcStXxpdfCD2MoXodu4RUU9ptxxmXssOfxnvDVcxRTod9FxyhqLoAqis5aPhwTDp9spRgEH2Q6KLbYoKqlaKTm6Isp0C/sJMnjFvhiERXPQvUNRe9p29lhR04CdBpC8Sl8YiuncIxEuzUUg4Dkgj+paVozygY9plPMh28SaymO9kabAopREGF3vt9MzeFFl8G7lRSZ8FFGK8XX4VA8QjEd7XrM3M0OXz8YCy+qKBLgq3wqnofiTorF0Ax56Rg1J1elW+BBAsVe+My6iYq7IK6keBdOIseV2qn5Pb8f3MqkWAXf9ThM8c8lAOIotuFsF875lRrH5klRcG0+xcPwQ1oLxfeRAP4heQTnGL78X2rqlw2DK59SXAV/zKaiGMAuko5InCt68mcOan5+ohf+z1pP8lQY/GHZQMV4YD3FpXDp4qerqbF/lBWBswyi+AL+ia+maLgcRRQj4IYlY/UpauqKBsPJAxQF8NM1TRQ/RudSPAD34rK3scOuR8/HGcspxsJfOVS8NZbiGXiUtPgINU3v3WFDmx8pEuG3EiqKKVbCC1vm2iZqap5LAtCtleQf8F9sFYWDohzeJczYyQ4V2bEZFGsQgJRGqqqhS2phHTWn9lDkIhBTqWqxQZ+IsRvtdHY9AvI2VX2hW68nfqGmuQsCEl3JdjfCF8OW1bPdtwhQ0gm2mQzfRE3a7KCYj0BNZJs8+Kxf/r6WtTEI2FIqlsMfFgRB5A6KUnSe/vUkX0AnuvUIt8SjM1m6wWQymUwmk8lkMgXRf5vi8rLQxtUhAAAAAElFTkSuQmCC` + +// doc.createImageFromBuffer(Buffer.from(imageBase64Data, 'base64')); +doc.createImageFromBuffer(Buffer.from(imageBase64Data, 'base64'), 100, 100); + +var exporter = new docx.LocalPacker(doc); +exporter.pack('My Document'); + +console.log('Document created successfully at project root!'); diff --git a/demo/demo19.js b/demo/demo19.js new file mode 100644 index 0000000000..3aafebc142 --- /dev/null +++ b/demo/demo19.js @@ -0,0 +1,20 @@ +const fs = require("fs"); +const docx = require("../build"); + +var doc = new docx.Document(); + +var paragraph = new docx.Paragraph("Hello World"); +var institutionText = new docx.TextRun("Foo").bold(); +var dateText = new docx.TextRun("Bar").tab().bold(); +paragraph.addRun(institutionText); +paragraph.addRun(dateText); + +doc.addParagraph(paragraph); + +var exporter = new docx.BufferPacker(doc); +exporter.pack("My Document").then((buffer) => { + // At this point, you can do anything with the buffer, including casting it to a string etc. + console.log(buffer); + fs.writeFileSync('My Document.docx', buffer); + console.log("Document created successfully at project root!"); +}); diff --git a/demo/demo20.js b/demo/demo20.js new file mode 100644 index 0000000000..eef332c1ae --- /dev/null +++ b/demo/demo20.js @@ -0,0 +1,17 @@ +const docx = require("../build"); + +var doc = new docx.Document(); + +const table = doc.createTable(4, 4); +table + .getCell(2, 2) + .addContent(new docx.Paragraph("Hello")) + .CellProperties.Borders.addTopBorder(docx.BorderStyle.DASH_DOT_STROKED, 3, "red") + .addBottomBorder(docx.BorderStyle.DOUBLE, 3, "blue") + .addStartBorder(docx.BorderStyle.DOT_DOT_DASH, 3, "green") + .addEndBorder(docx.BorderStyle.DOT_DOT_DASH, 3, "#ff8000"); + +var exporter = new docx.LocalPacker(doc); +exporter.pack("My Document"); + +console.log("Document created successfully at project root!"); diff --git a/demo/demo21.js b/demo/demo21.js new file mode 100644 index 0000000000..fd74e61b31 --- /dev/null +++ b/demo/demo21.js @@ -0,0 +1,31 @@ +/** This demo shows how to create bookmarks then link to them with internal hyperlinks */ + +const docx = require("../build"); + +const loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mi velit, convallis convallis scelerisque nec, faucibus nec leo. Phasellus at posuere mauris, tempus dignissim velit. Integer et tortor dolor. Duis auctor efficitur mattis. Vivamus ut metus accumsan tellus auctor sollicitudin venenatis et nibh. Cras quis massa ac metus fringilla venenatis. Proin rutrum mauris purus, ut suscipit magna consectetur id. Integer consectetur sollicitudin ante, vitae faucibus neque efficitur in. Praesent ultricies nibh lectus. Mauris pharetra id odio eget iaculis. Duis dictum, risus id pellentesque rutrum, lorem quam malesuada massa, quis ullamcorper turpis urna a diam. Cras vulputate metus vel massa porta ullamcorper. Etiam porta condimentum nulla nec tristique. Sed nulla urna, pharetra non tortor sed, sollicitudin molestie diam. Maecenas enim leo, feugiat eget vehicula id, sollicitudin vitae ante."; + +const doc = new docx.Document({ + creator: 'Clippy', + title: 'Sample Document', + description: 'A brief example of using docx with bookmarks and internal hyperlinks', +}); + +const anchorId = "anchorID"; + +// First create the bookmark +const bookmark = doc.createBookmark(anchorId, "Lorem Ipsum"); +// That has header styling +doc.createParagraph().addBookmark(bookmark).heading1(); +doc.createParagraph("\n"); + +doc.createParagraph(loremIpsum); +doc.createParagraph().pageBreak(); + +// Now the link back up to the bookmark +const hyperlink = doc.createInternalHyperLink(anchorId, `Click me!`); +doc.createParagraph().addHyperLink(hyperlink); + +var exporter = new docx.LocalPacker(doc); +exporter.pack("My Document"); + +console.log("Document created successfully at project root!"); diff --git a/demo/demo22.js b/demo/demo22.js new file mode 100644 index 0000000000..9bc7e9ac54 --- /dev/null +++ b/demo/demo22.js @@ -0,0 +1,26 @@ +const docx = require('../build'); + +var doc = new docx.Document(); + + + +var paragraph1 = new docx.Paragraph().bidirectional(); +var textRun1 = new docx.TextRun("שלום עולם").rightToLeft(); +paragraph1.addRun(textRun1); +doc.addParagraph(paragraph1); + +var paragraph2 = new docx.Paragraph().bidirectional(); +var textRun2 = new docx.TextRun("שלום עולם").bold().rightToLeft(); +paragraph2.addRun(textRun2); +doc.addParagraph(paragraph2); + +var paragraph3 = new docx.Paragraph().bidirectional(); +var textRun3 = new docx.TextRun("שלום עולם").italic().rightToLeft(); +paragraph3.addRun(textRun3); +doc.addParagraph(paragraph3); + + +var exporter = new docx.LocalPacker(doc); +exporter.pack('My Document'); + +console.log('Document created successfully at project root!'); diff --git a/demo/demo23.js b/demo/demo23.js new file mode 100644 index 0000000000..7fd5ebb6fb --- /dev/null +++ b/demo/demo23.js @@ -0,0 +1,31 @@ +// This demo adds an image to the Media cache, and then insert to the document afterwards +const docx = require("../build"); + +var doc = new docx.Document(); + +var paragraph = new docx.Paragraph("Hello World"); +doc.addParagraph(paragraph); + +const image = docx.Media.addImage(doc, "./demo/images/image1.jpeg"); +const image2 = docx.Media.addImage(doc, "./demo/images/dog.png"); +const image3 = docx.Media.addImage(doc, "./demo/images/cat.jpg"); +const image4 = docx.Media.addImage(doc, "./demo/images/parrots.bmp"); +const image5 = docx.Media.addImage(doc, "./demo/images/pizza.gif"); + +const imageBase64Data = `iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAACzVBMVEUAAAAAAAAAAAAAAAA/AD8zMzMqKiokJCQfHx8cHBwZGRkuFxcqFSonJyckJCQiIiIfHx8eHh4cHBwoGhomGSYkJCQhISEfHx8eHh4nHR0lHBwkGyQjIyMiIiIgICAfHx8mHh4lHh4kHR0jHCMiGyIhISEgICAfHx8lHx8kHh4jHR0hHCEhISEgICAlHx8kHx8jHh4jHh4iHSIhHCEhISElICAkHx8jHx8jHh4iHh4iHSIhHSElICAkICAjHx8jHx8iHh4iHh4hHiEhHSEkICAjHx8iHx8iHx8hHh4hHiEkHSEjHSAjHx8iHx8iHx8hHh4kHiEkHiEjHSAiHx8hHx8hHh4kHiEjHiAjHSAiHx8iHx8hHx8kHh4jHiEjHiAjHiAiICAiHx8kHx8jHh4jHiEjHiAiHiAiHSAiHx8jHx8jHx8jHiAiHiAiHiAiHSAiHx8jHx8jHx8iHiAiHiAiHiAjHx8jHx8jHx8jHx8iHiAiHiAiHiAjHx8jHx8jHx8iHx8iHSAiHiAjHiAjHx8jHx8hHx8iHx8iHyAiHiAjHiAjHiAjHh4hHx8iHx8iHx8iHyAjHSAjHiAjHiAjHh4hHx8iHx8iHx8jHyAjHiAhHh4iHx8iHx8jHyAjHSAjHSAhHiAhHh4iHx8iHx8jHx8jHyAjHSAjHSAiHh4iHh4jHx8jHx8jHyAjHyAhHSAhHSAiHh4iHh4jHx8jHx8jHyAhHyAhHSAiHSAiHh4jHh4jHx8jHx8jHyAhHyAhHSAiHSAjHR4jHh4jHx8jHx8hHyAhHyAiHSAjHSAjHR4jHh4jHx8hHx8hHyAhHyAiHyAjHSAjHR4jHR4hHh4hHx8hHyAiHyAjHyAjHSAjHR4jHR4hHh4hHx8hHyAjHyAjHyAjHSAjHR4hHR4hHR4hHx8iHyAjHyAjHyAjHSAhHR4hHR4hHR4hHx8jHyAjHyAjHyAjHyC9S2xeAAAA7nRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFxgZGhscHR4fICEiIyQlJicoKSorLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZISUpLTE1OUFFSU1RVVllaW1xdXmBhYmNkZWZnaGprbG1ub3Byc3R1dnd4eXp8fn+AgYKDhIWGiImKi4yNj5CRkpOUlZaXmJmam5ydnp+goaKjpKaoqqusra6vsLGys7S1tri5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+fkZpVQAABcBJREFUGBntwftjlQMcBvDnnLNL22qzJjWlKLHFVogyty3SiFq6EZliqZGyhnSxsLlMRahYoZKRFcul5dKFCatYqWZaNKvWtrPz/A2+7/b27qRzec/lPfvl/XxgMplMJpPJZDKZAtA9HJ3ppnIez0KnSdtC0RCNznHdJrbrh85wdSlVVRaEXuoGamYi5K5430HNiTiEWHKJg05eRWgNfKeV7RxbqUhGKPV/207VupQ8is0IoX5vtFC18SqEHaK4GyHTZ2kzVR8PBTCO4oANIZL4ShNVZcOhKKeYg9DoWdhI1ec3os2VFI0JCIUez5+i6st0qJZRrEAIJCw+QdW223BG/EmKwTBc/IJ/qfp2FDrkUnwFo8U9dZyqnaPhxLqfYjyM1S3vb6p+GGOBszsojoTDSDFz6qj66R4LzvYJxVMwUNRjf1H1ywQr/megg2RzLximy8waqvbda8M5iijegVEiHjlM1W/3h+FcXesphsMY4dMOUnUgOxyuPEzxPQwRNvV3qg5Nj4BreyimwADWe/dRVTMjEm6MoGLzGwtystL6RyOY3qSqdlYU3FpLZw1VW0sK5943MvUCKwJ1noNtjs6Ohge76Zq9ZkfpigU5WWkDYuCfbs1U5HWFR8/Qq4a9W0uK5k4ZmdrTCl8spGIePLPlbqqsc1Afe83O0hULc8alDYiBd7ZyitYMeBfR55rR2fOKP6ioPk2dGvZ+UVI0d8rtqT2tcCexlqK2F3wRn5Q+YVbBqrLKOupkr9lZujAOrmS0UpTb4JeIPkNHZ+cXr6uoPk2vyuBSPhWLEKj45PQJuQWryyqP0Z14uGLdROHIRNBEXDR09EP5r62rOHCazhrD4VKPwxTH+sIA3ZPTJ+YuWV22n+IruHFDC8X2CBjnPoolcGc2FYUwzmsUWXDHsoGKLBhmN0VvuBVfTVE/AAbpaid5CB4MbaLY1QXGuIViLTyZQcVyGGMuxWPwaA0Vk2GI9RRp8Ci2iuLkIBjhT5LNUfAspZFiTwyC72KK7+DNg1SsRvCNp3gZXq2k4iEEXSHFJHgVXUlxejCCbTvFAHiXdIJiXxyCK7KJ5FHoMZGK9xBcwyg2QpdlVMxEUM2iyIMuXXZQNF+HswxMsSAAJRQjoE//eoqDCXBSTO6f1xd+O0iyNRY6jaWi1ALNYCocZROj4JdEikroVkjFk9DcStXxpdfCD2MoXodu4RUU9ptxxmXssOfxnvDVcxRTod9FxyhqLoAqis5aPhwTDp9spRgEH2Q6KLbYoKqlaKTm6Isp0C/sJMnjFvhiERXPQvUNRe9p29lhR04CdBpC8Sl8YiuncIxEuzUUg4Dkgj+paVozygY9plPMh28SaymO9kabAopREGF3vt9MzeFFl8G7lRSZ8FFGK8XX4VA8QjEd7XrM3M0OXz8YCy+qKBLgq3wqnofiTorF0Ax56Rg1J1elW+BBAsVe+My6iYq7IK6keBdOIseV2qn5Pb8f3MqkWAXf9ThM8c8lAOIotuFsF875lRrH5klRcG0+xcPwQ1oLxfeRAP4heQTnGL78X2rqlw2DK59SXAV/zKaiGMAuko5InCt68mcOan5+ohf+z1pP8lQY/GHZQMV4YD3FpXDp4qerqbF/lBWBswyi+AL+ia+maLgcRRQj4IYlY/UpauqKBsPJAxQF8NM1TRQ/RudSPAD34rK3scOuR8/HGcspxsJfOVS8NZbiGXiUtPgINU3v3WFDmx8pEuG3EiqKKVbCC1vm2iZqap5LAtCtleQf8F9sFYWDohzeJczYyQ4V2bEZFGsQgJRGqqqhS2phHTWn9lDkIhBTqWqxQZ+IsRvtdHY9AvI2VX2hW68nfqGmuQsCEl3JdjfCF8OW1bPdtwhQ0gm2mQzfRE3a7KCYj0BNZJs8+Kxf/r6WtTEI2FIqlsMfFgRB5A6KUnSe/vUkX0AnuvUIt8SjM1m6wWQymUwmk8lkMgXRf5vi8rLQxtUhAAAAAElFTkSuQmCC` +const image6 = docx.Media.addImageFromBuffer(doc, Buffer.from(imageBase64Data, 'base64'), 100, 100); + +// I am adding an image to the paragraph rather than the document to make the image inline +paragraph.addImage(image5); + +doc.addImage(image); +doc.addImage(image2); +doc.addImage(image3); +doc.addImage(image4); +doc.addImage(image5); +doc.addImage(image6); + +var exporter = new docx.LocalPacker(doc); +exporter.pack("My Document"); + +console.log("Document created successfully at project root!"); diff --git a/demo/demo24.js b/demo/demo24.js new file mode 100644 index 0000000000..a95189a6ed --- /dev/null +++ b/demo/demo24.js @@ -0,0 +1,15 @@ +// Add image to table cell +const docx = require('../build'); + +var doc = new docx.Document(); + +const table = doc.createTable(4, 4); +table.getCell(2, 2).addContent(new docx.Paragraph('Hello')); + +const image = docx.Media.addImage(doc, "./demo/images/image1.jpeg"); +table.getCell(1, 1).addContent(image); + +var exporter = new docx.LocalPacker(doc); +exporter.pack('My Document'); + +console.log('Document created successfully at project root!'); diff --git a/demo/demo25.js b/demo/demo25.js new file mode 100644 index 0000000000..85dd29febe --- /dev/null +++ b/demo/demo25.js @@ -0,0 +1,15 @@ +const fs = require("fs"); +const docx = require("../build"); + +var doc = new docx.Document(); + +var paragraph = new docx.Paragraph("Hello World"); +var institutionText = new docx.TextRun("Foo").bold(); +var dateText = new docx.TextRun("Bar").tab().bold(); +paragraph.addRun(institutionText); +paragraph.addRun(dateText); + +doc.addParagraph(paragraph); + +var exporter = new docx.LocalPacker(doc); +exporter.packPdf("My Document"); diff --git a/demo/demo5.js b/demo/demo5.js index da073f6ca8..89638ad49b 100644 --- a/demo/demo5.js +++ b/demo/demo5.js @@ -1,17 +1,17 @@ -const docx = require('../build'); +const docx = require("../build"); var doc = new docx.Document(); var paragraph = new docx.Paragraph("Hello World"); doc.addParagraph(paragraph); -const image = doc.createImage("./demo/images/image1.jpeg"); -const image2 = doc.createImage("./demo/images/dog.png"); -const image3 = doc.createImage("./demo/images/cat.jpg"); -const image4 = doc.createImage("./demo/images/parrots.bmp"); -const image5 = doc.createImage("./demo/images/pizza.gif"); +doc.createImage("./demo/images/image1.jpeg"); +doc.createImage("./demo/images/dog.png"); +doc.createImage("./demo/images/cat.jpg"); +doc.createImage("./demo/images/parrots.bmp"); +doc.createImage("./demo/images/pizza.gif"); var exporter = new docx.LocalPacker(doc); -exporter.pack('My Document'); +exporter.pack("My Document"); -console.log('Document created successfully at project root!'); +console.log("Document created successfully at project root!"); diff --git a/demo/demo8.js b/demo/demo8.js index e849ae3f11..03ba944473 100644 --- a/demo/demo8.js +++ b/demo/demo8.js @@ -10,4 +10,4 @@ doc.Footer.createParagraph("Footer text"); var exporter = new docx.LocalPacker(doc); exporter.pack('My Document'); -console.log('Document created successfully at project root!'); +console.log('Document created successfully at project root!'); \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..557bcbad65 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,59 @@ +

+ clippy the assistant +

+ +

+ Easily generate .docx files with JS/TS. :100: +

+ +--- + +# Welcome + +## Installation + +```sh +npm install --save docx +``` + +Then you can `require` or `import` as usual: + +```js +let docx = require("docx"); +``` + +```js +import * as docx from "docx"; +``` + +## Basic Usage + +```js +var docx = require("docx"); + +// Create document +var doc = new docx.Document(); + +// Add some content in the document +var paragraph = new docx.Paragraph("Some cool text here."); +// Add more text into the paragraph if you wish +paragraph.addRun(new docx.TextRun("Lorem Ipsum Foo Bar")); +doc.addParagraph(paragraph); + +// Used to export the file into a .docx file +var exporter = new docx.LocalPacker(doc); + +exporter.pack("My First Document"); + +// Done! A file called 'My First Document.docx' will be in your file system if you used LocalPacker +``` + +## Honoured Mentions + +[@felipeochoa](https://github.com/felipeochoa) + +[@h4buli](https://github.com/h4buli) + +--- + +Made with 💖 diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 0000000000..0b159a95a8 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,27 @@ +* [Getting Started](/) + +* [Examples](examples.md) + +* API + + * [Documentation](https://docx.js.org/api/) + +* Usage + + * [Document](usage/document.md) + * [Paragraph](usage/paragraph.md) + * [Text](usage/text.md) + * [Image](usage/images.md) + * [Headers & Footers](usage/headers-and-footers.md) + * [Bullet Points](usage/bullet-points.md) + * [Numbering](usage/numbering.md) + * [Tab Stops](usage/tab-stops.md) + * Styling + * [Styling with JS](usage/styling-with-js.md) + * [Styling with XML](usage/styling-with-xml.md) +* Exporting + + * [Packers](usage/packers.md) + +* [Contribution Guidelines](contribution-guidelines.md) + diff --git a/docs/contribution-guidelines.md b/docs/contribution-guidelines.md new file mode 100644 index 0000000000..5d1442fe0c --- /dev/null +++ b/docs/contribution-guidelines.md @@ -0,0 +1,80 @@ +# Contribution Guidelines + +## Writing Code + +* Include documentation reference(s) at the top of each file: + + ```js + // http://officeopenxml.com/WPdocument.php + ``` + +* Follow Prettier standards, and consider using the [Prettier VSCode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) plugin. + +* Follow the `TSLint` rules + +## Add vs Create + +This is just a guideline, and the rules can sometimes be broken. + +* Use `create` if the method `new`'s up an element inside: + + ```js + public createParagraph() { + const paragraph = new Paragraph(); + this.root.push(paragraph); + } + ``` + +* Use `add` if you add the element into the method as a parameter: + + ```js + public addParagraph(paragraph: Paragraph) { + this.root.push(paragraph); + } + ``` + +## Getters and Setters + +Getters and Setters are done with a capital letter like so: + +```js +public get Level() { + +} +``` + +There is no performance advantage by doing this. It means we don't need to prefix all private variables with the ugly `_`: + +**Do not:** + +```js +private get _level: string; +``` + +**Do** + +```js +private get level: string; +``` + +## Testing + +Please write a test of every file you make and suffix it with `.spec.ts`. + +Here is a template of a test: + +```js +import { assert } from "chai"; + +describe("ClassName", () => { + beforeEach(() => { + // TODO + }); + + describe("#methodName()", () => { + it("should ", () => { + // TODO + }); + }); +}); +``` diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000000..cd811ded43 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,225 @@ +# Examples + +> All examples can run independently and can be found in the `/demo` folder of the project + +All the examples below can be ran locally, to do so, run the following command: + +```sh +npm run demo +``` + +This command will run the `demo selector app` in the `/demo` folder. It will prompt you to select a demo number, which will run a demo from that folder. + +## Simple + +A simple hello world of the `docx` library: + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo1.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo1.js_ + +## Styles + +### Styling with JS + +This example shows how to customise the look and feel of a document using JS configuration + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo2.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo2.js_ + +### Styling with XML + +This example shows how to customise the look and feel of a document using XML configuration + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo13.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo13.js_ + +## Numbering + +This example shows many levels of numbering + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo3.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo3.js_ + +## Table + +Example of simple table + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo4.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo4.js_ + +### Styling table borders + +Styling the borders of a table + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo20.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo20.js_ + +## Images + +### Add image to the document + +Importing Images from file system path + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo5.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo5.js_ + +### Add images to header and footer + +Example showing how to add image to headers and footers + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo9.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo9.js_ + +### Scaling images + +Example showing how to scale images + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo12.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo12.js_ + +### Add Image to media before adding to document + +This is the best way to add an image to a document because you can add the same image in two locations without increasing document size by re-using the same image + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo23.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo23.js_ + +### Add image to table + +As before, to add an image to a table, you would need to add it to the `Media` object first + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo24.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo24.js_ + +### Images using Base64 URI + +If you want to use a Base64 image instead + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo18.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo18.js_ + +## Margins + +Example showing how to set custom margains + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo6.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo6.js_ + +## Orientation + +Example showing how to set the document to `landscape` or `portrait` + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo7.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo7.js_ + +## Headers & Footers + +Example showing how to add headers and footers + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo8.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo8.js_ + +## Multiple headers and footers + +Check out `Sections` for this feature + +## Page Breaks + +### Normal page breaks + +Example showing how to page break + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo14.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo14.js_ + +### Page break before + +Example showing how to page break before like in Word + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo15.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo15.js_ + +## Sections + +Example of how sections work. Sections allow multiple headers and footers, and `landscape`/`portrait` inside the same document + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo16.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo16.js_ + +## Footnotes + +Example of how to add footnotes. Good for references + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo17.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo17.js_ + +## Packers + +## Buffer Packer + +Example showing how to use the Buffer packer and then write that buffer to the file system + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo19.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo19.js_ + +## PDF Packing + +Example of how to use the `LocalPacker` to create a PDF document + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo25.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo25.js_ + +## Bookmarks + +Example showing how to make bookmarks to make internal hyperlinks within the document + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo21.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo21.js_ + +## Bidirectional text + +Example showing how to use bidirectional text for certain languages such as Hebrew + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo22.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo22.js_ + +## Showcase + +### My CV + +Example showing how to add headers and footers + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo10.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo10.js_ + +### Style and Images + +This example shows how to customise the look and feel of a document and add images + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo11.js ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo11.js_ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000000..4b35f622af --- /dev/null +++ b/docs/index.html @@ -0,0 +1,30 @@ + + + + + + docx - Generate .docx documents with JavaScript + + + + + + + +
+ + + + + + + + diff --git a/docs/usage/bullet-points.md b/docs/usage/bullet-points.md new file mode 100644 index 0000000000..c2fb2987d0 --- /dev/null +++ b/docs/usage/bullet-points.md @@ -0,0 +1,21 @@ +# Bullet Points + +## Example + +To make a bullet point, simply make a paragraph into a bullet point: + +```js +var text = new docx.TextRun("Bullet points"); +var paragraph = new docx.Paragraph(text).bullet(); + +var text2 = new docx.TextRun("Are awesome"); +var paragraph2 = new docx.Paragraph(text2).bullet(); + +doc.addParagraph(paragraph); +doc.addParagraph(paragraph2); +``` + +### This will produce: + +* Bullet points +* Are awesome diff --git a/docs/usage/document.md b/docs/usage/document.md new file mode 100644 index 0000000000..f874502d3c --- /dev/null +++ b/docs/usage/document.md @@ -0,0 +1,35 @@ +# Document + +> The `Document` object is the starting point of your `.docx` journey, this is the literal Word Document. You add all your content such as `Paragraphs` to this `Document`, and at the end export it however you like. + +To create a new document, it is very easy: + +```js +var doc = new docx.Document(); +``` + +## Document properties + +You can add properties to the Word document by specifying options, for example: + +```js +var doc = new docx.Document({ + creator: "Dolan Miu", + description: "My extremely interesting document", + title: "My Document", +}); +``` + +### Full list of options: + +``` +creator +description +title +subject +keywords +lastModifiedBy +revision +``` + +You can mix and match whatever properties you want, or provide no properties. diff --git a/docs/usage/headers-and-footers.md b/docs/usage/headers-and-footers.md new file mode 100644 index 0000000000..bfe626a04c --- /dev/null +++ b/docs/usage/headers-and-footers.md @@ -0,0 +1,47 @@ +# Headers and Footers + +## Example + +Creating Headers and footers is simple. Access the `Header` and `Footer` by doing so like this: + +```js +doc.Header; +doc.Footer; +``` + +You can call the same methods as you would with a `File`: + +```js +doc.Header.createParagraph("Header text"); +doc.Footer.createParagraph("Footer text"); +``` + +Even add images: + +```js +doc.Header.createImage([PATH_TO_YOUR_IMAGE]); +doc.Footer.createImage([PATH_TO_YOUR_IMAGE]); +``` + +Refer to `demo8.js` for more information + +## Multiple Headers and Footers + +Also all the supported section properties are implemented according to: http://officeopenxml.com/WPsection.php + +### Example + +```js + const header = this.document.createHeader(); + const footer = this.document.createFooter(); + + // Add new section with another header and footer + doc.addSection({ + headerId: header.Header.ReferenceId, + footerId: footer.Footer.ReferenceId, + pageNumberStart: 1, + pageNumberFormatType: docx.PageNumberFormat.DECIMAL, + }); +``` + + diff --git a/docs/usage/images.md b/docs/usage/images.md new file mode 100644 index 0000000000..2cc0339507 --- /dev/null +++ b/docs/usage/images.md @@ -0,0 +1,156 @@ +# Images + +## Intro + +Adding images is very simple + +Simply call the `createImage` method: + +```js +const image = doc.createImage([PATH_TO_YOUR_IMAGE]); +``` + +`docx` supports `jpeg`, `jpg`, `bmp`, `gif` and `png` + +Check `demo5.js` for an example + +## Positioning + +Images can be: + +* floating position of images +* Wrapped around the text +* Inline + +By default, picture are exported as `INLINE` elements. + +In Word this is found in: + +![Word Image Positiong](https://user-images.githubusercontent.com/34742290/41765548-b0946302-7604-11e8-96f9-166a9f0b8f39.png) + +### Usage + +The `PictureRun` element support various options to define the positioning of the element in the document. + +```js +interface DrawingOptions { + position?: PlacementPosition; + textWrapping?: TextWrapping; + floating?: Floating; +} +``` + +can be passed when creating `PictureRun()` for example: + +```js +const imageData = document.createImageData(filename, buffer, 903, 1149); + +new docx.PictureRun(imageData, { + position: docx.PlacementPosition.FLOATING, + floating: { + horizontalPosition: { + relative: HorizontalPositionRelativeFrom.PAGE, + align: HorizontalPositionAlign.LEFT, + }, + verticalPosition: { + relative: VerticalPositionRelativeFrom.PAGE, + align: VerticalPositionAlign.TOP, + }, + }, +}); +``` + +So, the first thing is to define the placement position: `INLINE` or `FLOATING`. Inline is the default one so there is no need to pass drawing options for inline. + +When placement position is FLOATING then we can use two options: + +### Wrap text + +for `drawingOptions.textWrapping` we can define various options. `textWrapping` has the following properties: + +```js +interface TextWrapping { + textWrapStyle: TextWrapStyle; + wrapTextOption?: WrapTextOption; + distanceFromText?: Distance; +} + +enum TextWrapStyle { + NONE, + SQUARE, + TIGHT, + TOP_AND_BOTTOM, +} + +enum WrapTextOption { + BOTH_SIDES = "bothSides", + LEFT = "left", + RIGHT = "right", + LARGEST = "largest", +} +``` + +### Floating position + +When we want to position the image relative or absolute then we need to use option `drawingOptions.floating`: + +```js +export interface Floating { + horizontalPosition: HorizontalPositionOptions; + verticalPosition: VerticalPositionOptions; + allowOverlap?: boolean; + lockAnchor?: boolean; + behindDocument?: boolean; + layoutInCell?: boolean; +} + +export interface HorizontalPositionOptions { + relative: HorizontalPositionRelativeFrom; + align?: HorizontalPositionAlign; + offset?: number; +} + +export interface VerticalPositionOptions { + relative: VerticalPositionRelativeFrom; + align?: VerticalPositionAlign; + offset?: number; +} + +export enum HorizontalPositionRelativeFrom { + CHARACTER = "character", + COLUMN = "column", + INSIDE_MARGIN = "insideMargin", + LEFT_MARGIN = "leftMargin", + MARGIN = "margin", + OUTSIDE_MARGIN = "outsideMargin", + PAGE = "page", + RIGHT_MARGIN = "rightMargin", +} + +export enum VerticalPositionRelativeFrom { + BOTTOM_MARGIN = "bottomMargin", + INSIDE_MARGIN = "insideMargin", + LINE = "line", + MARGIN = "margin", + OUTSIDE_MARGIN = "outsideMargin", + PAGE = "page", + PARAGRAPH = "paragraph", + TOP_MARGIN = "topMargin", +} + +export enum HorizontalPositionAlign { + CENTER = "center", + INSIDE = "inside", + LEFT = "left", + OUTSIDE = "outside", + RIGHT = "right", +} + +export enum VerticalPositionAlign { + BOTTOM = "bottom", + CENTER = "center", + INSIDE = "inside", + OUTSIDE = "outside", + TOP = "top", +} +``` diff --git a/docs/usage/numbering.md b/docs/usage/numbering.md new file mode 100644 index 0000000000..69a501b736 --- /dev/null +++ b/docs/usage/numbering.md @@ -0,0 +1,96 @@ +# Bullets and Numbering + +`docx` is quite flexible in its bullets and numbering system, allowing +the user great freedom in how bullets and numbers are to be styled and +displayed. E.g., numbers can be shown using Arabic numerals, roman +numerals, or even ordinal words ("one", "two", "three", ...). The +format also supports re-using bullets/numbering styles throughout the +document, so that different lists using the same style need not +redefine them. + +Because of this flexibility, bullets and numbering in DOCX involves a +couple of moving pieces: + +1. Document-level bullets/numbering definitions (abstract) +2. Document-level bullets/numbering definitions (concrete) +3. Paragraph-level bullets/numbering selection + +## Document-level bullets/numbering definitions (abstract) + +Every document contains a set of abstract bullets/numbering +definitions which define the formatting and layout of paragraphs using +those bullets/numbering. An abstract numbering system defines how +bullets/numbers are to be shown for lists, including any sublists that +may be used. Thus each abstract definition includes a series of +_levels_ which form a sequence starting at 0 indicating the top-level +list look and increasing from there to descibe the sublists, then +sub-sublists, etc. Each level includes the following properties: + +* **level**: This its 0-based index in the defintion stack +* **numberFormat**: This indicates how the bullet or number should be + generated. Options include `bullet` (meaning don't count), `decimal` + (arabic numerals), `upperRoman`, `lowerRoman`, `hex`, and many + more. +* **levelText**: This is a format string using the output of the + `numberFormat` function and generating a string to insert before + every item in the list. You may use `%1`, `%2`, ... to reference the + numbers from each numbering level before this one. Thus a level + text of `%d)` with a number format of `lowerLetter` would result in + the sequence "a)", "b)", ... +* and a few others, which you can see in the OXML spec section 17.9.6 + +## Document-level bullets/numbering defintions (concrete) + +Concrete definitions are sort of like concrete subclasses of the +abstract defintions. They indicate their parent and are allowed to +override certain level definitions. Thus two lists that differ only in +how sub-sub-lists are to be displayed can share the same abstract +numbering definition and have slightly different concrete definitions. + +## Paragraph-level bullets/numbering selection + +In order to use a bullets/numbering definition (which must be +concrete), paragraphs need to select it, similar to applying a CSS +class to an element, using both the concrete numbering definition ID +and the level number that the paragraph should be at. Additionally, MS +Word and LibreOffice typically apply a "ListParagraph" style to +paragraphs that are being numbered. + +## Using bullets/numbering in `docx` + +`docx` includes a pre-defined bullet style which you can add to your +paragraphs using `para.bullets()`. If you require different bullet +styles or numbering of any kind, you'll have to use the +`docx.Numbering` class. + +First you need to create a new numbering container class and use it to +create your abstract numbering style, define your levels, and creat +your concreate numbering style: + +```js +const numbering = new docx.Numbering(); + +const abstractNum = numbering.createAbstractNumbering(); +abstractNum.createLevel(0, "upperRoman", "%1", "start").addParagraphProperty(new Indent(720, 260)); +abstractNum.createLevel(1, "decimal", "%2.", "start").addParagraphProperty(new Indent(1440, 980)); +abstractNum.createLevel(2, "lowerLetter", "%3)", "start").addParagraphProperty(new Indent(2160, 1700)); + +const concrete = numbering.createConcreteNumbering(numberedAbstract); +``` + +You can then apply your concrete style to paragraphs using their +`#setNumbering` method: + +```js +topLevelP.setNumbering(concrete, 0); +subP.setNumbering(concrete, 1); +subSubP.setNumbering(concrete, 2); +``` + +Finally, you need to let your exporter know about your numbering +styles when you're ready to render the document: + +```js +const packer = new Packer(doc, undefined, undefined, numbering); +packer.pack(myOutput); +``` diff --git a/docs/usage/packers.md b/docs/usage/packers.md new file mode 100644 index 0000000000..db837702de --- /dev/null +++ b/docs/usage/packers.md @@ -0,0 +1,80 @@ +# Packers + +> Packers are the way in which `docx` turns your code into `.docx` format. It is completely decoupled from the `docx.Document`. + +## File System Packer + +```js +const docx = require("docx"); + +const doc = new docx.Document(); +const exporter = new docx.LocalPacker(doc); +exporter.pack("My Document"); +// Word Document is in file system +``` + +## Buffer Packer + +```js +const docx = require("docx"); + +const doc = new docx.Document(); +const exporter = new docx.BufferPacker(doc); +const buffer = exporter.pack(); +``` + +## Stream Packer + +Creates a `node` `Readable` stream + +```js +const docx = require("docx"); + +const doc = new docx.Document(); +const exporter = new docx.StreamPacker(doc); +const stream = exporter.pack(); +``` + +## Express Packer + +The old express packer is now deprecated and may disappear soon, so you should upgrade. + +The reason for this is because it means this project needs to know about and use `express`, which for a Word document generator, does not sound right. Seperation of concerns. + +It will still be usable (for now), but it is ill advised. + +I used the express exporter in my [website](http://www.dolan.bio). + +The recommended way is to use the `StreamPacker` and handle the `express` magic outside of the library: + +```js +const docx = require("docx"); + +const doc = new docx.Document(); +const exporter = new docx.StreamPacker(doc); + +const stream = exporter.pack(); + +// Express' response object +res.attachment('yourfile.xlsx'); +stream.pipe(res); +``` + +where `res` is the response object obtained through the Express router. It is that simple. The file will begin downloading in the browser. + +## PDF Exporting + +You can export your word document as a PDF file like so: + +```js +const exporter = new docx.LocalPacker(doc); +exporter.packPdf("My Document"); + +// Express +const exporter = new docx.ExpressPacker(doc, res); +exporter.packPdf("My Document"); +``` + +## Browser based docx exporting + +It is on the bucket list. It has been requested by a few, and work is already on it diff --git a/docs/usage/paragraph.md b/docs/usage/paragraph.md new file mode 100644 index 0000000000..6f3ee39ac5 --- /dev/null +++ b/docs/usage/paragraph.md @@ -0,0 +1,113 @@ +# Paragraph + +> Everything (text, images, graphs etc) in OpenXML is organised in paragraphs. + +## Example + +You can add more text to the paragraph by doing this: + +```js +var paragraph = new docx.Paragraph(), +``` + +```js +var text = new docx.TextRun("Lorem Ipsum Foo Bar"); +var paragraph = new docx.Paragraph(); +paragraph.addRun(text); +``` + +```js +var paragraph = new docx.Paragraph("Short hand notation for adding text."); +``` + +After you create the paragraph, you must add the paragraph into the `document`: + +```js +doc.addParagraph(paragraph); +``` + +## Styles + +To create styles, please refer to the styling Wiki: https://github.com/dolanmiu/docx/wiki/Styling + +![Word 2013 Styles menu](http://content.gcflearnfree.org/topics/233/style_apply_choose.png "Word 2013 Styles menu") + +### Heading1 - Heading5 + +```js +paragraph.heading1(); +paragraph.heading2(); +paragraph.heading3(); +paragraph.heading4(); +paragraph.heading5(); +``` + +### Title + +```js +paragraph.title(); +``` + +## Text Alignment + +To change the text alignment of a paragraph, for center, left, right or justified: + +```js +paragraph.center(); +``` + +```js +paragraph.left(); +``` + +```js +paragraph.right(); +``` + +```js +paragraph.justified(); +``` + +### Example + +```js +paragraph.heading1().center(); +``` + +The above will create a `heading 1` which is `centered`. + +## Thematic Break + +To add a break in the page, simply add `.thematicBreak()` on a paragraph: + +```js +var paragraph = new docx.Paragraph("Amazing Heading").heading1().thematicBreak(); +``` + +The above example will create a heading with a page break directly under it. + +## Page Break + +To move to a new page (insert a page break), simply add `.pageBreak()` on a paragraph: + +```js +var paragraph = new docx.Paragraph("Amazing Heading").heading1().pageBreak(); +``` + +The above example will create a heading and start a new page immediately afterwards. + +### Page break before: + +This option (available in word) will make sure that the paragraph will start on a new page (if it's not already on a new page). + +```js +var paragraph = new docx.Paragraph("Hello World on another page").pageBreakBefore(); +``` + +![Page Break Before in Word](https://user-images.githubusercontent.com/34742290/40176503-df3a8398-59db-11e8-8b9c-d719f13aa8b4.png) + +Example: https://github.com/dolanmiu/docx/blob/master/demo/demo15.js + +## Page break control + +Paragraphs have `.keepLines()` and `.keepNext()` methods that allow restricting page breaks within and between paragraphs. See [this Microsoft article](https://support.office.com/en-us/article/Keep-lines-and-paragraphs-together-d72af534-926f-4c4b-830a-abfc2daa3bfa) for more details) diff --git a/docs/usage/styling-with-js.md b/docs/usage/styling-with-js.md new file mode 100644 index 0000000000..3e482bca14 --- /dev/null +++ b/docs/usage/styling-with-js.md @@ -0,0 +1,162 @@ +# Styling with JS + +## Example + +```js +const para = new Paragraph("To whom it may concern:").heading2().center(); + +const name = new TextRun("Name:") + .bold() + .font("Calibri") + .allCaps(); +``` + +## Available methods + +* For run formatting: + * `.bold()`, `.italic()`, `.smallCaps()`, `.allCaps()`, `.strike()`, `.doubleStrike()`, `.subScript()`, `.superScript()`: Set the formatting property to true + * `.underline(style="single", color=null)`: Set the underline style and color + * `.color(color)`: Set the text color, using 6 hex characters for RRGGBB (no leading `#`) + * `.size(halfPts)`: Set the font size, measured in half-points + * `.font(name)`: Set the run's font + * `.style(name)`: Apply a named run style +* For paragraph formatting: + * `.heading1()`, `.heading2()`, `.heading3()`, `.heading4()`, `.heading5()`, `.title()`: apply the appropriate style to the paragraph + * `.left()`, `.center()`, `.right()`, `.justified()`: set the paragraph's alignment + * `.thematicBreak()`, `.pageBreak()`: Insert a thick rule or a page break beneath the paragraph + * `.leftTabStop(position)`: Add a left tab stop (measured in TWIPs from the left) + * `.maxRightTabStop()`: Add a right tab stop at the far right + * `.bullet()`: Use the default bullet style + * `.setNumbering(numbering, indentLevel)`: Use a custom numbering format for the paragraph + * `.style(name)`: Apply a named paragraph style + * `.indent(start, hanging=0)`: Set the paragraph's indent level (in TWIPs) + * `.spacing({before=0, after=0, line=0})`: Set the line and before/after on the paragraph. Before/after is measured in TWIPs, line is measured in 240ths of a line + +Paragraph styles have all the run formatting methods, except `style()`, and `.left()`, `.center()`, `.right()`, `.justified()`, `.thematicBreak()`, `.leftTabStop(position)`, `.maxRightTabStop()`, `.indent(start, hanging=0)`, and `.spacing({before=0, after=0, line=0})` methods. + +## Detailed guide + +There are 4 items in DOCX that can be styled: + +* Characters: Attributes that can change within a paragraph. e.g., bold, italics, etc. +* Paragraphs: Attributes like indent, text alignment, line spacing, etc. +* Tables: Border styles, table formats, etc. +* List items: These are the numbers and bullets that are automatically inserted + +There are a few different ways of styling this content in DOCX, which somewhat resemble the HTML/CSS approach. In order of greatest to lowest priority: + +1. Direct formatting (AKA inline formatting) +2. Centrally defined styles (similar to external CSS) +3. Document defaults (similar to a `*` rule in CSS) + +Unlike CSS, less specific rules don't _necessarily_ override parent rules. The rules are a bit wonky, but if you're interested, see the [advanced formatting section](#Advanced formatting). + +### Direct formatting (AKA inline formatting) + +This is the type of formatting that your uncle uses when he types out documents: _N ... a ... m ... e ... :_ Then he grabs the mouse, highlights _Name:_ and moves over to the **B** for bold. This manner of formatting results in markup that is similar to writing `Name:` if you were typing out HTML. DOCX (the format) allows you to specify this for any of the four types of items. `docx` (the library) only supports this type of formatting for paragraphs and characters, using a _fluent_ api. Thus you could do: + +```js +const name = new TextRun("Name:") + .bold() + .font("Calibri") + .allCaps(); +``` + +Or for paragraph formatting: + +```js +const para = new Paragraph("To whom it may concern:").heading2().center(); +``` + +### Centrally defined styles (similar to external CSS) + +DOCX files contain a styles section separate from the main content, much like how HTML includes CSS files. Unlike CSS, DOCX distinguishes between styles meant for tables (which show up in the table formatting toolbar), styles for lists (which show up under bullets and numbering), and styles for runs and paragraphs, which show up as dropdowns offering standard styles, like "Heading 1", "Caption", or any custom styles defined in that document. . `docx` allows you to define these styles using a fluent interface as well. + +There are three parts to using custom styles with `docx`: + +1. Create a container object for the style definitions: + ```js + const myStyles = new docx.Styles(); + ``` +2. Define your custom styles, similar to the way you would format a paragraph or run + + ```js + // The first argument is an ID you use to apply the style to paragraphs + // The second argument is a human-friendly name to show in the UI + myStyles + .createParagraphStyle("myWonkyStyle", "My Wonky Style") + .basedOn("Normal") + .next("Normal") + .color("999999") + .italics() + .indent(720) // 720 TWIP === 720 / 20 pt === .5 in + .spacing({ line: 276 }); // 276 / 240 = 1.15x line spacing + + myStyles + .createParagraphStyle("Heading2", "Heading 2") + .basedOn("Normal") + .next("Normal") + .quickFormat() + .size(26) // 26 half-points === 13pt font + .bold() + .underline("double", "FF0000") + .spacing({ before: 240, after: 120 }); // TWIP for both + ``` + +3. When you generate your document, make sure to pass the `styles` container to the `Packer`: + + ```js + const packer = new Packer(doc, myStyles); + packer.pack(myOutStream); + ``` + +**Note**: If you are using the `.headingX` or `.title` methods of paragraphs, you must make sure to define `HeadingX` or `Title` styles for these. Otherwise they'll show up unstyled :(. If you are using the `.bullet` or `.setNumbering` methods, you need to define a `ListParagraph` style or the numbers may not show up. + +### Document defaults + +Setting document defaults acts like a `*` rule in CSS: it applies to every paragraph and run in the document, but at a low priority level. Other styles affecting this property will override these defaults. + +## Advanced formatting + +### Style inheritance + +Styles may define a `basedOn` attribute that references another style of the same type. In this case, any unspecified properties are inherited from the parent style. + +### Interactions between the 4 items + +In addition to the 3-layer hierarchy spelled above, there is some interaction between the 4 items that you can style. +For instance numbering styles may also specify some styling for paragraphs (typically indentation and tab stops); paragraphs may specify character formatting (e.g., heading font sizes); etc. + +The elements that each style may affect are summarized in the table below. So, e.g., table styles may specify table formatting, paragraph formatting, and character formatting. + +| Style type | Table | Paragraph | List item | Characters | +| :---------------- | :---: | :-------: | :-------: | :--------: | +| Document defaults | | X | | X | +| Table | X | X | | X | +| Paragraph | | X | X | X | +| Numbering | | X | X | | +| Character | | | | X | +| Direct formatting | X | X | X | X | + +To determine the value of a styling property, you must first identify whether it's a table, paragraph, list item, or character property. E.g., numbering definition is a list item property. Then you need to find the last row in the table for which that property has an "X" and the document has formatting of that type. So if a particular run was in a paragraph whose style specified color as `FF0000`, but it also had a character style specifying color as `00DD00`, then the character style (lower down on the table) would trump, and the character would have color `00DD00`. + +### Toggle properties + +The following properties are treated in a special manner; they're called toggle properties: + +* Bold +* All caps +* Small caps +* Italics +* Single strike-through +* Hidden +* Imprint +* Emboss +* Character outline +* Character shadow + +For these properties, the rules state the following conflict resolution in case the property is specified at multiple points for the same item: + +* Direct formatting trumps all if specified (either true or false) +* Otherwise, if the property is true in document defaults, the property is set to true +* Otherwise, the property's value is an XOR of its effective table, paragraph, and character values. (So specifying bold `true` on a table style and a paragraph style would result in non-bold text if a paragraph inside the table had that style) diff --git a/docs/usage/styling-with-xml.md b/docs/usage/styling-with-xml.md new file mode 100644 index 0000000000..ac3a757d36 --- /dev/null +++ b/docs/usage/styling-with-xml.md @@ -0,0 +1,47 @@ +# Styling with XML + +## Setup + +1. Create a new word document in Microsoft Word +2. Customise the styles on the Ribbon Bar. + For example, modify the `Normal`, `Heading 1`, `Heading 2` like so: + + ![image](https://user-images.githubusercontent.com/2917613/41195113-65edebfa-6c1f-11e8-97b4-77de2d60044a.png) + ![image](https://user-images.githubusercontent.com/2917613/41195126-ca99c36c-6c1f-11e8-9e58-19e5f69b3b87.png) + +3. You can even create a totally new `Style`: + + ![image](https://user-images.githubusercontent.com/2917613/41195135-f0f7862a-6c1f-11e8-8be4-dd6d8fe5be03.png) + ![image](https://user-images.githubusercontent.com/2917613/41195139-0ec52130-6c20-11e8-8fae-f6b44b43fdf8.png) + +4. Save +5. Re-name the saved `.docx` file to `.zip` and un-zip +6. Find `styles.xml` + + ![image](https://user-images.githubusercontent.com/2917613/41195178-bb9ba9c4-6c20-11e8-850e-a7a6ada9a2f6.png) + +## Usage + +Read the styles using `fs`, and put it into the `Document` object in the constructor: + +```js +const styles = fs.readFileSync("./styles.xml", "utf-8"); +const doc = new docx.Document({ + title: "Title", + externalStyles: styles, +}); +``` + +You can use paragraphs, `heading1()`, `heading2()` etc and it will be styled according to your `styles.xml` created earlier. You can even use your new style you made by calling the `style` method: + +```js +doc.createParagraph("Cool Heading Text").heading1(); + +let paragraph = new docx.Paragraph('This is a custom named style from the template "Cool New Style"'); +paragraph.style("Cool New Style"); +doc.addParagraph(paragraph); + +doc.createParagraph("Some normal text"); +``` + +Example: https://github.com/dolanmiu/docx/blob/master/demo/demo13.js diff --git a/docs/usage/tab-stops.md b/docs/usage/tab-stops.md new file mode 100644 index 0000000000..8447d03998 --- /dev/null +++ b/docs/usage/tab-stops.md @@ -0,0 +1,54 @@ +# Tab Stops + +> Tab stops are useful, if you are unclear of what they are, [here is a link explaining](https://en.wikipedia.org/wiki/Tab_stop). It enables side by side text which is nicely laid out without the need for tables, or constantly pressing space bar. + +**Note**: At the moment, the unit of measurement for a tab stop is counter intuitive for a human. It is using OpenXMLs own measuring system. For example, 2268 roughly translates to 3cm. Therefore in the future, I may consider changing it to percentages or even cm. + +![Word 2013 Tabs](http://www.teachucomp.com/wp-content/uploads/blog-4-22-2015-UsingTabStopsInWord-1024x577.png "Word 2013 Tab Stops") + +Simply call the relevant methods on the paragraph listed below. Then just add a `tab()` method call to a text object. Adding multiple `tabStops` will mean you would have to chain `tab()` until the desired `tabStop` is selected. Example is shown below. + +## Example + +```js +var paragraph = new docx.Paragraph().maxRightTabStop(); +var leftText = new docx.TextRun("Hey everyone").bold(); +var rightText = new docx.TextRun("11th November 2015").tab(); +paragraph.addRun(leftText); +paragraph.addRun(rightText); +``` +The example above will create a left aligned text, and a right aligned text on the same line. The laymans approach to this problem would be to either use text boxes or tables. YUK! + +```js +var paragraph = new docx.Paragraph(); +paragraph.maxRightTabStop(); +paragraph.leftTabStop(1000); +var text = new docx.TextRun("Second tab stop here I come!").tab().tab(); +paragraph.addRun(text); +``` + +The above shows the use of two tab stops, and how to select/use it. + +## Left Tab Stop +```js +paragraph.leftTabStop(2268); +``` +2268 is the distance from the left side. + +## Center Tab Stop +```js +paragraph.centerTabStop(2268); +``` +2268 is the distance from the left side. + +## Right Tab Stop +```js +paragraph.rightTabStop(2268); +``` +2268 is the distance from the left side. + +## Max Right Tab Stop +```js +paragraph.maxRightTabStop(); +``` +This will create a tab stop on the very edge of the right hand side. Handy for right aligning and left aligning text on the same line. diff --git a/docs/usage/text.md b/docs/usage/text.md new file mode 100644 index 0000000000..3ca98804bb --- /dev/null +++ b/docs/usage/text.md @@ -0,0 +1,84 @@ +# Text + +Paragraphs need `text run` objects. To create text: + +```js +var text = new docx.TextRun("My awesome text here for my university dissertation"); +paragraph.addRun(text); +``` + +Text objects have methods inside which changes the way the text is displayed. + +## Typographical Emphasis + +More info [here](https://english.stackexchange.com/questions/97081/what-is-the-typography-term-which-refers-to-the-usage-of-bold-italics-and-unde) + +### Bold + +```js +text.bold(); +``` + +### Italics + +```js +text.italic(); +``` + +### Underline + +```js +text.underline(); +``` + +### Strike through + +```js +text.strike(); +``` + +### Double strike through + +```js +text.doubleStrike(); +``` + +### Superscript + +```js +text.superScript(); +``` + +### Subscript + +```js +text.subScript(); +``` + +### All Capitals + +```js +text.allCaps(); +``` + +### Small Capitals + +```js +text.smallCaps(); +``` + +## Break + +Sometimes you would want to put text underneath another line of text but inside the same paragraph. + +```js +text.break(); +``` + +## Chaining + +What if you want to create a paragraph which is **_bold_** and **_italic_**? + +```js +paragraph.bold().italic(); +``` diff --git a/package.json b/package.json index cc5394462b..5d4bd2e7a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx", - "version": "3.4.0", + "version": "3.6.0", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { @@ -14,10 +14,15 @@ "webpack": "rimraf ./build && webpack", "build.web": "webpack --config webpack.web.config.js", "demo": "npm run build && node ./demo", - "typedoc": "typedoc --out docs/ src/ --module commonjs --target ES6 --disableOutputCheck --excludePrivate --externalPattern \"**/*.spec.ts\"", + "typedoc": "typedoc src/index.ts", "style": "prettier -l \"src/**/*.ts\"", + "style.fix": "prettier \"src/**/*.ts\" --write", "fix-types": "node types-absolute-fixer.js" }, + "pre-commit": [ + "style", + "lint" + ], "files": [ "src", "build", @@ -42,11 +47,13 @@ "types": "./build/index.d.ts", "dependencies": { "@types/archiver": "^2.1.0", + "@types/bluebird": "3.5.20", "@types/express": "^4.0.35", "@types/image-size": "0.0.29", "@types/jszip": "^3.1.3", - "@types/request-promise": "^4.1.41", + "@types/request-promise": "^4.1.42", "archiver": "^2.1.1", + "fast-xml-parser": "^3.3.6", "image-size": "^0.6.2", "jszip": "^3.1.5", "request": "^2.83.0", @@ -62,19 +69,23 @@ "devDependencies": { "@types/chai": "^3.4.35", "@types/mocha": "^2.2.39", + "@types/sinon": "^4.3.1", "awesome-typescript-loader": "^3.4.1", "chai": "^3.5.0", "glob": "^7.1.2", + "jszip": "^3.1.5", "mocha": "^3.2.0", "mocha-webpack": "^1.0.1", - "prettier": "^1.10.2", + "pre-commit": "^1.2.2", + "prettier": "^1.12.1", "prompt": "^1.0.0", "replace-in-file": "^3.1.0", "rimraf": "^2.5.2", "shelljs": "^0.7.7", - "tslint": "^5.1.0", - "typedoc": "^0.9.0", - "typescript": "2.6.2", + "sinon": "^5.0.7", + "tslint": "^5.11.0", + "typedoc": "^0.11.1", + "typescript": "2.9.2", "webpack": "^3.10.0" } } diff --git a/src/export/index.ts b/src/export/index.ts index a68c57de55..f6a47e4826 100644 --- a/src/export/index.ts +++ b/src/export/index.ts @@ -2,3 +2,4 @@ export * from "./packer/local"; export * from "./packer/express"; export * from "./packer/packer"; export * from "./packer/stream"; +export * from "./packer/buffer"; diff --git a/src/export/packer/buffer-stream.ts b/src/export/packer/buffer-stream.ts new file mode 100644 index 0000000000..e0d2f8ed73 --- /dev/null +++ b/src/export/packer/buffer-stream.ts @@ -0,0 +1,28 @@ +import { Writable } from "stream"; + +export class BufferStream extends Writable { + private readonly data: Buffer[]; + + constructor() { + super(); + + this.data = []; + } + + // tslint:disable-next-line:no-any + public _write(chunk: any, _: string, next: (err?: Error) => void): void { + this.data.push(Buffer.from(chunk)); + next(); + } + + // tslint:disable-next-line:ban-types + public end(cb?: Function): void { + super.end(cb); + + this.emit("close"); + } + + public get Buffer(): Buffer { + return Buffer.concat(this.data); + } +} diff --git a/src/export/packer/buffer.spec.ts b/src/export/packer/buffer.spec.ts new file mode 100644 index 0000000000..95eccc5ecc --- /dev/null +++ b/src/export/packer/buffer.spec.ts @@ -0,0 +1,44 @@ +/* tslint:disable:typedef space-before-function-paren */ +import { assert } from "chai"; +import { stub } from "sinon"; + +import { BufferPacker } from "../../export/packer/buffer"; +import { File, Paragraph } from "../../file"; + +describe("BufferPacker", () => { + let packer: BufferPacker; + + beforeEach(() => { + const file = new File({ + creator: "Dolan Miu", + revision: "1", + lastModifiedBy: "Dolan Miu", + }); + const paragraph = new Paragraph("test text"); + const heading = new Paragraph("Hello world").heading1(); + file.addParagraph(new Paragraph("title").title()); + file.addParagraph(heading); + file.addParagraph(new Paragraph("heading 2").heading2()); + file.addParagraph(paragraph); + + packer = new BufferPacker(file); + }); + + describe("#pack()", () => { + it("should create a standard docx file", async function() { + this.timeout(99999999); + const buffer = await packer.pack(); + assert.isDefined(buffer); + assert.isTrue(buffer.byteLength > 0); + }); + + it("should handle exception if it throws any", () => { + // tslint:disable-next-line:no-any + const compiler = stub((packer as any).packer, "compile"); + compiler.throwsException(); + return packer.pack().catch((error) => { + assert.isDefined(error); + }); + }); + }); +}); diff --git a/src/export/packer/buffer.ts b/src/export/packer/buffer.ts new file mode 100644 index 0000000000..9c46c4e166 --- /dev/null +++ b/src/export/packer/buffer.ts @@ -0,0 +1,20 @@ +import { File } from "../../file"; +import { BufferStream } from "./buffer-stream"; +import { Compiler } from "./compiler"; +import { IPacker } from "./packer"; + +export class BufferPacker implements IPacker { + private readonly packer: Compiler; + + constructor(file: File) { + this.packer = new Compiler(file); + } + + public async pack(): Promise { + const stream = new BufferStream(); + + await this.packer.compile(stream); + + return stream.Buffer; + } +} diff --git a/src/export/packer/compiler.spec.ts b/src/export/packer/compiler.spec.ts new file mode 100644 index 0000000000..78d2339b11 --- /dev/null +++ b/src/export/packer/compiler.spec.ts @@ -0,0 +1,76 @@ +/* tslint:disable:typedef space-before-function-paren */ +import * as fs from "fs"; +import * as JSZip from "jszip"; + +import { expect } from "chai"; +import { File } from "../../file"; +import { Compiler } from "./compiler"; + +describe("Compiler", () => { + let compiler: Compiler; + let file: File; + + beforeEach(() => { + file = new File(); + compiler = new Compiler(file); + }); + + describe("#compile()", () => { + it("should pack all the content", async function() { + this.timeout(99999999); + const fileName = "build/tests/test.docx"; + await compiler.compile(fs.createWriteStream(fileName)); + + const docxFile = fs.readFileSync(fileName); + const zipFile: JSZip = await JSZip.loadAsync(docxFile); + const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); + + expect(fileNames).is.an.instanceof(Array); + expect(fileNames).has.length(13); + expect(fileNames).to.include("word/document.xml"); + expect(fileNames).to.include("word/styles.xml"); + expect(fileNames).to.include("docProps/core.xml"); + expect(fileNames).to.include("docProps/app.xml"); + expect(fileNames).to.include("word/numbering.xml"); + expect(fileNames).to.include("word/header1.xml"); + expect(fileNames).to.include("word/_rels/header1.xml.rels"); + expect(fileNames).to.include("word/footer1.xml"); + expect(fileNames).to.include("word/footnotes.xml"); + expect(fileNames).to.include("word/_rels/footer1.xml.rels"); + expect(fileNames).to.include("word/_rels/document.xml.rels"); + expect(fileNames).to.include("[Content_Types].xml"); + expect(fileNames).to.include("_rels/.rels"); + }); + + it("should pack all additional headers and footers", async function() { + file.createFooter(); + file.createFooter(); + file.createHeader(); + file.createHeader(); + + this.timeout(99999999); + const fileName = "build/tests/test2.docx"; + await compiler.compile(fs.createWriteStream(fileName)); + + const docxFile = fs.readFileSync(fileName); + const zipFile: JSZip = await JSZip.loadAsync(docxFile); + const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); + + expect(fileNames).is.an.instanceof(Array); + expect(fileNames).has.length(21); + + expect(fileNames).to.include("word/header1.xml"); + expect(fileNames).to.include("word/_rels/header1.xml.rels"); + expect(fileNames).to.include("word/header2.xml"); + expect(fileNames).to.include("word/_rels/header2.xml.rels"); + expect(fileNames).to.include("word/header3.xml"); + expect(fileNames).to.include("word/_rels/header3.xml.rels"); + expect(fileNames).to.include("word/footer1.xml"); + expect(fileNames).to.include("word/_rels/footer1.xml.rels"); + expect(fileNames).to.include("word/footer2.xml"); + expect(fileNames).to.include("word/_rels/footer2.xml.rels"); + expect(fileNames).to.include("word/footer3.xml"); + expect(fileNames).to.include("word/_rels/footer3.xml.rels"); + }); + }); +}); diff --git a/src/export/packer/compiler.ts b/src/export/packer/compiler.ts index 410ecc669b..a845833a34 100644 --- a/src/export/packer/compiler.ts +++ b/src/export/packer/compiler.ts @@ -1,6 +1,5 @@ import * as archiver from "archiver"; import * as express from "express"; -import * as fs from "fs"; import { Writable } from "stream"; import * as xml from "xml"; @@ -9,9 +8,9 @@ import { Formatter } from "../formatter"; export class Compiler { protected archive: archiver.Archiver; - private formatter: Formatter; + private readonly formatter: Formatter; - constructor(private file: File) { + constructor(private readonly file: File) { this.formatter = new Formatter(); this.archive = archiver.create("zip", {}); @@ -23,7 +22,7 @@ export class Compiler { public async compile(output: Writable | express.Response): Promise { this.archive.pipe(output); - const xmlDocument = xml(this.formatter.format(this.file.Document), true); + const xmlDocument = xml(this.formatter.format(this.file.Document)); const xmlStyles = xml(this.formatter.format(this.file.Styles)); const xmlProperties = xml(this.formatter.format(this.file.CoreProperties), { declaration: { @@ -34,12 +33,9 @@ export class Compiler { const xmlNumbering = xml(this.formatter.format(this.file.Numbering)); const xmlRelationships = xml(this.formatter.format(this.file.DocumentRelationships)); const xmlFileRelationships = xml(this.formatter.format(this.file.FileRelationships)); - const xmlHeader = xml(this.formatter.format(this.file.Header.Header)); - const xmlFooter = xml(this.formatter.format(this.file.Footer.Footer)); - const xmlHeaderRelationships = xml(this.formatter.format(this.file.Header.Relationships)); - const xmlFooterRelationships = xml(this.formatter.format(this.file.Footer.Relationships)); const xmlContentTypes = xml(this.formatter.format(this.file.ContentTypes)); const xmlAppProperties = xml(this.formatter.format(this.file.AppProperties)); + const xmlFootnotes = xml(this.formatter.format(this.file.FootNotes)); this.archive.append(xmlDocument, { name: "word/document.xml", @@ -61,26 +57,38 @@ export class Compiler { name: "word/numbering.xml", }); - this.archive.append(xmlHeader, { - name: "word/header1.xml", - }); + // headers + for (let i = 0; i < this.file.Headers.length; i++) { + const element = this.file.Headers[i]; + this.archive.append(xml(this.formatter.format(element.Header)), { + name: `word/header${i + 1}.xml`, + }); - this.archive.append(xmlFooter, { - name: "word/footer1.xml", + this.archive.append(xml(this.formatter.format(element.Relationships)), { + name: `word/_rels/header${i + 1}.xml.rels`, + }); + } + + // footers + for (let i = 0; i < this.file.Footers.length; i++) { + const element = this.file.Footers[i]; + this.archive.append(xml(this.formatter.format(element.Footer)), { + name: `word/footer${i + 1}.xml`, + }); + + this.archive.append(xml(this.formatter.format(element.Relationships)), { + name: `word/_rels/footer${i + 1}.xml.rels`, + }); + } + + this.archive.append(xmlFootnotes, { + name: "word/footnotes.xml", }); this.archive.append(xmlRelationships, { name: "word/_rels/document.xml.rels", }); - this.archive.append(xmlHeaderRelationships, { - name: "word/_rels/header1.xml.rels", - }); - - this.archive.append(xmlFooterRelationships, { - name: "word/_rels/footer1.xml.rels", - }); - this.archive.append(xmlContentTypes, { name: "[Content_Types].xml", }); @@ -89,8 +97,8 @@ export class Compiler { name: "_rels/.rels", }); - for (const data of this.file.Media.array) { - this.archive.append(fs.createReadStream(data.path), { + for (const data of this.file.Media.Array) { + this.archive.append(data.stream, { name: `word/media/${data.fileName}`, }); } diff --git a/src/export/packer/express.spec.ts b/src/export/packer/express.spec.ts new file mode 100644 index 0000000000..416d206815 --- /dev/null +++ b/src/export/packer/express.spec.ts @@ -0,0 +1,43 @@ +// tslint:disable:typedef space-before-function-paren +// tslint:disable:no-empty +// tslint:disable:no-any +import { assert } from "chai"; +import { stub } from "sinon"; + +import { ExpressPacker } from "../../export/packer/express"; +import { File, Paragraph } from "../../file"; + +describe("LocalPacker", () => { + let packer: ExpressPacker; + + beforeEach(() => { + const file = new File({ + creator: "Dolan Miu", + revision: "1", + lastModifiedBy: "Dolan Miu", + }); + const paragraph = new Paragraph("test text"); + const heading = new Paragraph("Hello world").heading1(); + file.addParagraph(new Paragraph("title").title()); + file.addParagraph(heading); + file.addParagraph(new Paragraph("heading 2").heading2()); + file.addParagraph(paragraph); + + const expressResMock = { + on: () => {}, + attachment: () => {}, + }; + + packer = new ExpressPacker(file, expressResMock as any); + }); + + describe("#pack()", () => { + it("should handle exception if it throws any", () => { + const compiler = stub((packer as any).packer, "compile"); + compiler.throwsException(); + return packer.pack("build/tests/test").catch((error) => { + assert.isDefined(error); + }); + }); + }); +}); diff --git a/src/export/packer/express.ts b/src/export/packer/express.ts index 4f99298424..3a4fa4b3ef 100644 --- a/src/export/packer/express.ts +++ b/src/export/packer/express.ts @@ -4,6 +4,9 @@ import { File } from "file"; import { Compiler } from "./compiler"; import { IPacker } from "./packer"; +/** + * @deprecated ExpressPacker is now deprecated. Please use the StreamPacker instead and pipe that to `express`' `res` object + */ export class ExpressPacker implements IPacker { private readonly packer: Compiler; diff --git a/src/export/packer/local.spec.ts b/src/export/packer/local.spec.ts index 16fb99705b..e942fb70a9 100644 --- a/src/export/packer/local.spec.ts +++ b/src/export/packer/local.spec.ts @@ -1,5 +1,7 @@ /* tslint:disable:typedef space-before-function-paren */ +import { assert } from "chai"; import * as fs from "fs"; +import { stub } from "sinon"; import { LocalPacker } from "../../export/packer/local"; import { File, Paragraph } from "../../file"; @@ -29,14 +31,36 @@ describe("LocalPacker", () => { await packer.pack("build/tests/test"); fs.statSync("build/tests/test.docx"); }); + + it("should handle exception if it throws any", () => { + // tslint:disable-next-line:no-any + const compiler = stub((packer as any).packer, "compile"); + compiler.throwsException(); + return packer.pack("build/tests/test").catch((error) => { + assert.isDefined(error); + }); + }); }); describe("#packPdf", () => { it("should create a standard PDF file", async function() { this.timeout(99999999); + // tslint:disable-next-line:no-any + const pdfConverterConvert = stub((packer as any).pdfConverter, "convert"); + pdfConverterConvert.returns("Test PDF Contents"); + await packer.packPdf("build/tests/pdf-test"); fs.statSync("build/tests/pdf-test.pdf"); }); + + it("should handle exception if it throws any", () => { + // tslint:disable-next-line:no-any + const compiler = stub((packer as any).packer, "compile"); + compiler.throwsException(); + return packer.packPdf("build/tests/pdf-test").catch((error) => { + assert.isDefined(error); + }); + }); }); }); diff --git a/src/export/packer/local.ts b/src/export/packer/local.ts index 27bba0ae69..892a3422f1 100644 --- a/src/export/packer/local.ts +++ b/src/export/packer/local.ts @@ -20,7 +20,7 @@ export class LocalPacker implements IPacker { filePath = filePath.replace(/.docx$/, ""); const zip = await this.packer.compile(); - const zipData = await zip.generateAsync({ type: "base64" }) as string; + const zipData = (await zip.generateAsync({ type: "base64" })) as string; await this.writeToFile(`${filePath}.docx`, zipData); } @@ -32,7 +32,7 @@ export class LocalPacker implements IPacker { const tempPath = path.join(os.tmpdir(), `${fileName}.docx`); const zip = await this.packer.compile(); - const zipData = await zip.generateAsync({ type: "base64" }) as string; + const zipData = (await zip.generateAsync({ type: "base64" })) as string; await this.writeToFile(tempPath, zipData); const text = await this.pdfConverter.convert(tempPath); diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index b8ddaf2b82..4d26fbd5ea 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -1,4 +1,3 @@ -import * as fs from "fs"; import * as JSZip from "jszip"; import * as xml from "xml"; @@ -26,9 +25,9 @@ interface IXmlifyedFileMapping { } export class Compiler { - private formatter: Formatter; + private readonly formatter: Formatter; - constructor(private file: File) { + constructor(private readonly file: File) { this.formatter = new Formatter(); } @@ -47,8 +46,8 @@ export class Compiler { zip.file(xmlifiedFile.path, xmlifiedFile.data); } - for (const data of this.file.Media.array) { - const mediaData = await this.readFile(data.path); + for (const data of this.file.Media.Array) { + const mediaData = data.stream; zip.file(`word/media/${data.fileName}`, mediaData); } @@ -112,17 +111,4 @@ export class Compiler { }, }; } - - private readFile(path: string): Promise { - return new Promise((resolve, reject) => { - fs.readFile(path, (err, data) => { - if (err) { - reject(); - return; - } - - resolve(data); - }); - }); - } } diff --git a/src/export/packer/packer.ts b/src/export/packer/packer.ts index 64fa05a9aa..848924f457 100644 --- a/src/export/packer/packer.ts +++ b/src/export/packer/packer.ts @@ -3,4 +3,7 @@ export interface IPacker { } // Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432 +/** + * @ignore + */ export const WORKAROUND = ""; diff --git a/src/export/packer/pdf-convert-wrapper.ts b/src/export/packer/pdf-convert-wrapper.ts index 5067082d12..e1229cf768 100644 --- a/src/export/packer/pdf-convert-wrapper.ts +++ b/src/export/packer/pdf-convert-wrapper.ts @@ -1,5 +1,3 @@ -/* tslint:disable:object-literal-key-quotes */ -// This tslint disable is needed, or it simply won't work import * as fs from "fs"; import * as request from "request-promise"; @@ -11,6 +9,7 @@ export class PdfConvertWrapper { public convert(filePath: string): request.RequestPromise { return request.post({ url: "http://mirror1.convertonlinefree.com", + // tslint:disable-next-line:no-null-keyword encoding: null, headers: { "User-Agent": diff --git a/src/file/content-types/content-types.spec.ts b/src/file/content-types/content-types.spec.ts new file mode 100644 index 0000000000..14665df4ee --- /dev/null +++ b/src/file/content-types/content-types.spec.ts @@ -0,0 +1,139 @@ +import { expect } from "chai"; +import { Formatter } from "../../export/formatter"; +import { ContentTypes } from "./content-types"; +describe("ContentTypes", () => { + let contentTypes: ContentTypes; + + beforeEach(() => { + contentTypes = new ContentTypes(); + }); + + describe("#constructor()", () => { + it("should create default content types", () => { + const tree = new Formatter().format(contentTypes); + + expect(tree["Types"]).to.be.an.instanceof(Array); + + expect(tree["Types"][0]).to.deep.equal({ _attr: { xmlns: "http://schemas.openxmlformats.org/package/2006/content-types" } }); + expect(tree["Types"][1]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/png", Extension: "png" } }] }); + expect(tree["Types"][2]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/jpeg", Extension: "jpeg" } }] }); + expect(tree["Types"][3]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/jpeg", Extension: "jpg" } }] }); + expect(tree["Types"][4]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/bmp", Extension: "bmp" } }] }); + expect(tree["Types"][5]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/gif", Extension: "gif" } }] }); + expect(tree["Types"][6]).to.deep.equal({ + Default: [{ _attr: { ContentType: "application/vnd.openxmlformats-package.relationships+xml", Extension: "rels" } }], + }); + expect(tree["Types"][7]).to.deep.equal({ Default: [{ _attr: { ContentType: "application/xml", Extension: "xml" } }] }); + expect(tree["Types"][8]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", + PartName: "/word/document.xml", + }, + }, + ], + }); + expect(tree["Types"][9]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", + PartName: "/word/styles.xml", + }, + }, + ], + }); + expect(tree["Types"][10]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-package.core-properties+xml", + PartName: "/docProps/core.xml", + }, + }, + ], + }); + expect(tree["Types"][11]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.extended-properties+xml", + PartName: "/docProps/app.xml", + }, + }, + ], + }); + expect(tree["Types"][12]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", + PartName: "/word/numbering.xml", + }, + }, + ], + }); + }); + }); + + describe("#addFooter()", () => { + it("should add footer", () => { + contentTypes.addFooter(101); + contentTypes.addFooter(102); + const tree = new Formatter().format(contentTypes); + + expect(tree["Types"][14]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", + PartName: "/word/footer101.xml", + }, + }, + ], + }); + + expect(tree["Types"][15]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", + PartName: "/word/footer102.xml", + }, + }, + ], + }); + }); + }); + + describe("#addHeader()", () => { + it("should add header", () => { + contentTypes.addHeader(201); + contentTypes.addHeader(202); + const tree = new Formatter().format(contentTypes); + + expect(tree["Types"][14]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", + PartName: "/word/header201.xml", + }, + }, + ], + }); + + expect(tree["Types"][15]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", + PartName: "/word/header202.xml", + }, + }, + ], + }); + }); + }); +}); diff --git a/src/file/content-types/content-types.ts b/src/file/content-types/content-types.ts index 4ce020a918..8735d3d9e2 100644 --- a/src/file/content-types/content-types.ts +++ b/src/file/content-types/content-types.ts @@ -24,11 +24,23 @@ export class ContentTypes extends XmlComponent { this.root.push( new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "/word/document.xml"), ); - this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", "/word/header1.xml")); - this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", "/word/footer1.xml")); + this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", "/word/styles.xml")); this.root.push(new Override("application/vnd.openxmlformats-package.core-properties+xml", "/docProps/core.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.extended-properties+xml", "/docProps/app.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", "/word/numbering.xml")); + this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", "/word/footnotes.xml")); + } + + public addFooter(index: number): void { + this.root.push( + new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", `/word/footer${index}.xml`), + ); + } + + public addHeader(index: number): void { + this.root.push( + new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", `/word/header${index}.xml`), + ); } } diff --git a/src/file/core-properties/properties.ts b/src/file/core-properties/properties.ts index cd2395c27b..aa9d3aec51 100644 --- a/src/file/core-properties/properties.ts +++ b/src/file/core-properties/properties.ts @@ -10,6 +10,7 @@ export interface IPropertiesOptions { description?: string; lastModifiedBy?: string; revision?: string; + externalStyles?: string; } export class CoreProperties extends XmlComponent { diff --git a/src/file/document/body/body.spec.ts b/src/file/document/body/body.spec.ts index c5498bc99a..31c1e49e32 100644 --- a/src/file/document/body/body.spec.ts +++ b/src/file/document/body/body.spec.ts @@ -1,39 +1,42 @@ -// import { assert } from "chai"; +import { expect } from "chai"; -// import { Utility } from "../../../tests/utility"; -// import { Body } from "./"; +import { Formatter } from "../../../export/formatter"; +import { Body } from "./body"; describe("Body", () => { - // let body: Body; + let body: Body; beforeEach(() => { - // body = new Body(); + body = new Body(); }); - // describe("#constructor()", () => { - // it("should create the Section Properties", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[0].rootKey, "w:sectPr"); - // }); + describe("#constructor()", () => { + it("should create default section", () => { + const formatted = new Formatter().format(body)["w:body"][0]; + expect(formatted) + .to.have.property("w:sectPr") + .and.to.be.an.instanceof(Array); + expect(formatted["w:sectPr"]).to.have.length(7); + }); + }); - // it("should create the Page Size", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[1].rootKey, "w:pgSz"); - // }); + describe("addSection", () => { + it("should add section with options", () => { + body.addSection({ + width: 10000, + height: 10000, + }); - // it("should create the Page Margin", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[2].rootKey, "w:pgMar"); - // }); + const formatted = new Formatter().format(body)["w:body"]; + expect(formatted).to.be.an.instanceof(Array); + const defaultSectionPr = formatted[0]["w:p"][1]["w:pPr"][0]["w:sectPr"]; - // it("should create the Columns", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[3].rootKey, "w:cols"); - // }); + // check that this is the default section and added first in paragraph + expect(defaultSectionPr[0]).to.deep.equal({ "w:pgSz": [{ _attr: { "w:h": 16838, "w:w": 11906, "w:orient": "portrait" } }] }); - // it("should create the Document Grid", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[4].rootKey, "w:docGrid"); - // }); - // }); + // check for new section (since it's the last one, it's direct child of body) + const newSection = formatted[1]["w:sectPr"]; + expect(newSection[0]).to.deep.equal({ "w:pgSz": [{ _attr: { "w:h": 10000, "w:w": 10000, "w:orient": "portrait" } }] }); + }); + }); }); diff --git a/src/file/document/body/body.ts b/src/file/document/body/body.ts index fa30099f75..4ec5156bfc 100644 --- a/src/file/document/body/body.ts +++ b/src/file/document/body/body.ts @@ -1,14 +1,63 @@ -import { XmlComponent } from "file/xml-components"; -import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties"; +import { IXmlableObject, XmlComponent } from "file/xml-components"; +import { Paragraph, ParagraphProperties } from "../.."; +import { SectionProperties, SectionPropertiesOptions } from "./section-properties"; export class Body extends XmlComponent { + private readonly defaultSection: SectionProperties; + + private readonly sections: SectionProperties[] = []; + constructor(sectionPropertiesOptions?: SectionPropertiesOptions) { super("w:body"); - this.root.push(new SectionProperties(sectionPropertiesOptions)); + this.defaultSection = new SectionProperties(sectionPropertiesOptions); + this.sections.push(this.defaultSection); + } + + /** + * Adds new section properties. + * Note: Previous section is created in paragraph after the current element, and then new section will be added. + * The spec says: + * - section element should be in the last paragraph of the section + * - last section should be direct child of body + * @param section new section + */ + public addSection(section: SectionPropertiesOptions | SectionProperties): void { + const currentSection = this.sections.pop() as SectionProperties; + this.root.push(this.createSectionParagraph(currentSection)); + if (section instanceof SectionProperties) { + this.sections.push(section); + } else { + const params = { + ...this.defaultSection.Options, + ...section, + }; + this.sections.push(new SectionProperties(params)); + } + } + public prepForXml(): IXmlableObject { + if (this.sections.length === 1) { + this.root.push(this.sections[0]); + } else if (this.sections.length > 1) { + throw new Error("Invalid usage of sections. At the end of the body element there must be ONE section."); + } + + return super.prepForXml(); } public push(component: XmlComponent): void { this.root.push(component); } + + public get DefaultSection(): SectionProperties { + return this.defaultSection; + } + + private createSectionParagraph(section: SectionProperties): Paragraph { + const paragraph = new Paragraph(); + const properties = new ParagraphProperties(); + properties.addChildElement(section); + paragraph.addChildElement(properties); + return paragraph; + } } diff --git a/src/file/document/body/index.ts b/src/file/document/body/index.ts index 93f4529388..83678ef8ea 100644 --- a/src/file/document/body/index.ts +++ b/src/file/document/body/index.ts @@ -1 +1,2 @@ export * from "./body"; +export * from "./section-properties"; diff --git a/src/file/document/body/section-properties/footer-reference/footer-reference-attributes.ts b/src/file/document/body/section-properties/footer-reference/footer-reference-attributes.ts index 0097de0dbb..763053e36a 100644 --- a/src/file/document/body/section-properties/footer-reference/footer-reference-attributes.ts +++ b/src/file/document/body/section-properties/footer-reference/footer-reference-attributes.ts @@ -1,5 +1,11 @@ import { XmlAttributeComponent } from "file/xml-components"; +export enum FooterReferenceType { + DEFAULT = "default", + FIRST = "first", + EVEN = "even", +} + export interface IFooterReferenceAttributes { type: string; id: string; diff --git a/src/file/document/body/section-properties/footer-reference/footer-reference.ts b/src/file/document/body/section-properties/footer-reference/footer-reference.ts index 6b7012b6d3..6c2786432d 100644 --- a/src/file/document/body/section-properties/footer-reference/footer-reference.ts +++ b/src/file/document/body/section-properties/footer-reference/footer-reference.ts @@ -1,13 +1,19 @@ import { XmlComponent } from "file/xml-components"; -import { FooterReferenceAttributes } from "./footer-reference-attributes"; +import { FooterReferenceAttributes, FooterReferenceType } from "./footer-reference-attributes"; + +export interface IFooterOptions { + footerType?: FooterReferenceType; + footerId?: number; +} export class FooterReference extends XmlComponent { - constructor() { + constructor(options: IFooterOptions) { super("w:footerReference"); + this.root.push( new FooterReferenceAttributes({ - type: "default", - id: `rId${4}`, + type: options.footerType || FooterReferenceType.DEFAULT, + id: `rId${options.footerId}`, }), ); } diff --git a/src/file/document/body/section-properties/footer-reference/index.ts b/src/file/document/body/section-properties/footer-reference/index.ts new file mode 100644 index 0000000000..9673319fba --- /dev/null +++ b/src/file/document/body/section-properties/footer-reference/index.ts @@ -0,0 +1,2 @@ +export * from "./footer-reference"; +export * from "./footer-reference-attributes"; diff --git a/src/file/document/body/section-properties/header-reference/header-reference-attributes.ts b/src/file/document/body/section-properties/header-reference/header-reference-attributes.ts index b0407c4a0a..5569fa76b9 100644 --- a/src/file/document/body/section-properties/header-reference/header-reference-attributes.ts +++ b/src/file/document/body/section-properties/header-reference/header-reference-attributes.ts @@ -1,5 +1,11 @@ import { XmlAttributeComponent } from "file/xml-components"; +export enum HeaderReferenceType { + DEFAULT = "default", + FIRST = "first", + EVEN = "even", +} + export interface IHeaderReferenceAttributes { type: string; id: string; diff --git a/src/file/document/body/section-properties/header-reference/header-reference.ts b/src/file/document/body/section-properties/header-reference/header-reference.ts index 3809047bef..8464a33a92 100644 --- a/src/file/document/body/section-properties/header-reference/header-reference.ts +++ b/src/file/document/body/section-properties/header-reference/header-reference.ts @@ -1,13 +1,18 @@ import { XmlComponent } from "file/xml-components"; -import { HeaderReferenceAttributes } from "./header-reference-attributes"; +import { HeaderReferenceAttributes, HeaderReferenceType } from "./header-reference-attributes"; + +export interface IHeaderOptions { + headerType?: HeaderReferenceType; + headerId?: number; +} export class HeaderReference extends XmlComponent { - constructor() { + constructor(options: IHeaderOptions) { super("w:headerReference"); this.root.push( new HeaderReferenceAttributes({ - type: "default", - id: `rId${3}`, + type: options.headerType || HeaderReferenceType.DEFAULT, + id: `rId${options.headerId}`, }), ); } diff --git a/src/file/document/body/section-properties/header-reference/index.ts b/src/file/document/body/section-properties/header-reference/index.ts new file mode 100644 index 0000000000..80239ad98e --- /dev/null +++ b/src/file/document/body/section-properties/header-reference/index.ts @@ -0,0 +1,2 @@ +export * from "./header-reference"; +export * from "./header-reference-attributes"; diff --git a/src/file/document/body/section-properties/index.ts b/src/file/document/body/section-properties/index.ts new file mode 100644 index 0000000000..f1b5eabb84 --- /dev/null +++ b/src/file/document/body/section-properties/index.ts @@ -0,0 +1,5 @@ +export * from "./section-properties"; +export * from "./footer-reference"; +export * from "./header-reference"; +export * from "./page-size"; +export * from "./page-number"; diff --git a/src/file/document/body/section-properties/page-number/index.ts b/src/file/document/body/section-properties/page-number/index.ts new file mode 100644 index 0000000000..57e81d8724 --- /dev/null +++ b/src/file/document/body/section-properties/page-number/index.ts @@ -0,0 +1 @@ +export * from "./page-number"; diff --git a/src/file/document/body/section-properties/page-number/page-number.ts b/src/file/document/body/section-properties/page-number/page-number.ts new file mode 100644 index 0000000000..d6239d3be4 --- /dev/null +++ b/src/file/document/body/section-properties/page-number/page-number.ts @@ -0,0 +1,41 @@ +// http://officeopenxml.com/WPSectionPgNumType.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +export enum PageNumberFormat { + CARDINAL_TEXT = "cardinalText", + DECIMAL = "decimal", + DECIMAL_ENCLOSED_CIRCLE = "decimalEnclosedCircle", + DECIMAL_ENCLOSED_FULL_STOP = "decimalEnclosedFullstop", + DECIMAL_ENCLOSED_PAREN = "decimalEnclosedParen", + DECIMAL_ZERO = "decimalZero", + LOWER_LETTER = "lowerLetter", + LOWER_ROMAN = "lowerRoman", + NONE = "none", + ORDINAL_TEXT = "ordinalText", + UPPER_LETTER = "upperLetter", + UPPER_ROMAN = "upperRoman", +} + +export interface IPageNumberTypeAttributes { + pageNumberStart?: number; + pageNumberFormatType?: PageNumberFormat; +} + +export class PageNumberTypeAttributes extends XmlAttributeComponent { + protected xmlKeys = { + pageNumberStart: "w:start", + pageNumberFormatType: "w:fmt", + }; +} + +export class PageNumberType extends XmlComponent { + constructor(start?: number, numberFormat?: PageNumberFormat) { + super("w:pgNumType"); + this.root.push( + new PageNumberTypeAttributes({ + pageNumberStart: start, + pageNumberFormatType: numberFormat, + }), + ); + } +} diff --git a/src/file/document/body/section-properties/page-size/index.ts b/src/file/document/body/section-properties/page-size/index.ts new file mode 100644 index 0000000000..567f7c2d58 --- /dev/null +++ b/src/file/document/body/section-properties/page-size/index.ts @@ -0,0 +1,2 @@ +export * from "./page-size"; +export * from "./page-size-attributes"; diff --git a/src/file/document/body/section-properties/page-size/page-size-attributes.ts b/src/file/document/body/section-properties/page-size/page-size-attributes.ts index 5a3cd90907..4af206ac2d 100644 --- a/src/file/document/body/section-properties/page-size/page-size-attributes.ts +++ b/src/file/document/body/section-properties/page-size/page-size-attributes.ts @@ -1,9 +1,14 @@ import { XmlAttributeComponent } from "file/xml-components"; +export enum PageOrientation { + PORTRAIT = "portrait", + LANDSCAPE = "landscape", +} + export interface IPageSizeAttributes { width?: number; height?: number; - orientation?: string; + orientation?: PageOrientation; } export class PageSizeAttributes extends XmlAttributeComponent { diff --git a/src/file/document/body/section-properties/page-size/page-size.spec.ts b/src/file/document/body/section-properties/page-size/page-size.spec.ts index 4ebcf989c0..bd5b17aaa5 100644 --- a/src/file/document/body/section-properties/page-size/page-size.spec.ts +++ b/src/file/document/body/section-properties/page-size/page-size.spec.ts @@ -2,11 +2,12 @@ import { expect } from "chai"; import { Formatter } from "../../../../../export/formatter"; import { PageSize } from "./page-size"; +import { PageOrientation } from "./page-size-attributes"; describe("PageSize", () => { describe("#constructor()", () => { it("should create page size with portrait", () => { - const properties = new PageSize(100, 200, "portrait"); + const properties = new PageSize(100, 200, PageOrientation.PORTRAIT); const tree = new Formatter().format(properties); expect(Object.keys(tree)).to.deep.equal(["w:pgSz"]); @@ -15,7 +16,7 @@ describe("PageSize", () => { }); it("should create page size with horizontal and invert the lengths", () => { - const properties = new PageSize(100, 200, "landscape"); + const properties = new PageSize(100, 200, PageOrientation.LANDSCAPE); const tree = new Formatter().format(properties); expect(Object.keys(tree)).to.deep.equal(["w:pgSz"]); diff --git a/src/file/document/body/section-properties/page-size/page-size.ts b/src/file/document/body/section-properties/page-size/page-size.ts index ea93e1ebc0..6aa400bea2 100644 --- a/src/file/document/body/section-properties/page-size/page-size.ts +++ b/src/file/document/body/section-properties/page-size/page-size.ts @@ -1,11 +1,11 @@ import { XmlComponent } from "file/xml-components"; -import { PageSizeAttributes } from "./page-size-attributes"; +import { PageOrientation, PageSizeAttributes } from "./page-size-attributes"; export class PageSize extends XmlComponent { - constructor(width: number, height: number, orientation: string) { + constructor(width: number, height: number, orientation: PageOrientation) { super("w:pgSz"); - const flip = orientation === "landscape"; + const flip = orientation === PageOrientation.LANDSCAPE; this.root.push( new PageSizeAttributes({ diff --git a/src/file/document/body/section-properties/section-properties.spec.ts b/src/file/document/body/section-properties/section-properties.spec.ts index 994db13891..05a49d09d9 100644 --- a/src/file/document/body/section-properties/section-properties.spec.ts +++ b/src/file/document/body/section-properties/section-properties.spec.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import { Formatter } from "../../../../export/formatter"; import { SectionProperties } from "./section-properties"; +import { FooterReferenceType, PageNumberFormat } from "."; describe("SectionProperties", () => { describe("#constructor()", () => { @@ -18,6 +19,11 @@ describe("SectionProperties", () => { gutter: 0, space: 708, linePitch: 360, + headerId: 100, + footerId: 200, + footerType: FooterReferenceType.EVEN, + pageNumberStart: 10, + pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT, }); const tree = new Formatter().format(properties); expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]); @@ -38,6 +44,12 @@ describe("SectionProperties", () => { }, ], }); + + expect(tree["w:sectPr"][2]).to.deep.equal({ "w:cols": [{ _attr: { "w:space": 708 } }] }); + expect(tree["w:sectPr"][3]).to.deep.equal({ "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }); + expect(tree["w:sectPr"][4]).to.deep.equal({ "w:headerReference": [{ _attr: { "r:id": "rId100", "w:type": "default" } }] }); + expect(tree["w:sectPr"][5]).to.deep.equal({ "w:footerReference": [{ _attr: { "r:id": "rId200", "w:type": "even" } }] }); + expect(tree["w:sectPr"][6]).to.deep.equal({ "w:pgNumType": [{ _attr: { "w:fmt": "cardinalText", "w:start": 10 } }] }); }); it("should create section properties with no options", () => { @@ -61,6 +73,11 @@ describe("SectionProperties", () => { }, ], }); + expect(tree["w:sectPr"][2]).to.deep.equal({ "w:cols": [{ _attr: { "w:space": 708 } }] }); + expect(tree["w:sectPr"][3]).to.deep.equal({ "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }); + expect(tree["w:sectPr"][4]).to.deep.equal({ "w:headerReference": [{ _attr: { "r:id": "rId0", "w:type": "default" } }] }); + expect(tree["w:sectPr"][5]).to.deep.equal({ "w:footerReference": [{ _attr: { "r:id": "rId0", "w:type": "default" } }] }); + expect(tree["w:sectPr"][6]).to.deep.equal({ "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }); }); it("should create section properties with changed options", () => { diff --git a/src/file/document/body/section-properties/section-properties.ts b/src/file/document/body/section-properties/section-properties.ts index 27d3f0bd99..4cb6d12b99 100644 --- a/src/file/document/body/section-properties/section-properties.ts +++ b/src/file/document/body/section-properties/section-properties.ts @@ -1,19 +1,30 @@ // http://officeopenxml.com/WPsection.php import { XmlComponent } from "file/xml-components"; +import { FooterReferenceType, IPageNumberTypeAttributes, PageNumberFormat, PageNumberType } from "./"; import { Columns } from "./columns/columns"; import { IColumnsAttributes } from "./columns/columns-attributes"; import { DocumentGrid } from "./doc-grid/doc-grid"; import { IDocGridAttributesProperties } from "./doc-grid/doc-grid-attributes"; -import { FooterReference } from "./footer-reference/footer-reference"; -import { HeaderReference } from "./header-reference/header-reference"; +import { FooterReference, IFooterOptions } from "./footer-reference/footer-reference"; +import { HeaderReference, IHeaderOptions } from "./header-reference/header-reference"; +import { HeaderReferenceType } from "./header-reference/header-reference-attributes"; import { PageMargin } from "./page-margin/page-margin"; import { IPageMarginAttributes } from "./page-margin/page-margin-attributes"; import { PageSize } from "./page-size/page-size"; -import { IPageSizeAttributes } from "./page-size/page-size-attributes"; +import { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes"; +// import { TitlePage } from "./title-page/title-page"; -export type SectionPropertiesOptions = IPageSizeAttributes & IPageMarginAttributes & IColumnsAttributes & IDocGridAttributesProperties; +export type SectionPropertiesOptions = IPageSizeAttributes & + IPageMarginAttributes & + IColumnsAttributes & + IDocGridAttributesProperties & + IHeaderOptions & + IFooterOptions & + IPageNumberTypeAttributes; export class SectionProperties extends XmlComponent { + private readonly options: SectionPropertiesOptions; + constructor(options?: SectionPropertiesOptions) { super("w:sectPr"); @@ -29,7 +40,13 @@ export class SectionProperties extends XmlComponent { gutter: 0, space: 708, linePitch: 360, - orientation: "portrait", + orientation: PageOrientation.PORTRAIT, + headerType: HeaderReferenceType.DEFAULT, + headerId: 0, + footerType: FooterReferenceType.DEFAULT, + footerId: 0, + pageNumberStart: undefined, + pageNumberFormatType: PageNumberFormat.DECIMAL, }; const mergedOptions = { @@ -51,7 +68,26 @@ export class SectionProperties extends XmlComponent { ); this.root.push(new Columns(mergedOptions.space)); this.root.push(new DocumentGrid(mergedOptions.linePitch)); - this.root.push(new HeaderReference()); - this.root.push(new FooterReference()); + + this.root.push( + new HeaderReference({ + headerType: mergedOptions.headerType, + headerId: mergedOptions.headerId, + }), + ); + this.root.push( + new FooterReference({ + footerType: mergedOptions.footerType, + footerId: mergedOptions.footerId, + }), + ); + + this.root.push(new PageNumberType(mergedOptions.pageNumberStart, mergedOptions.pageNumberFormatType)); + + this.options = mergedOptions; + } + + public get Options(): SectionPropertiesOptions { + return this.options; } } diff --git a/src/file/document/body/section-properties/title-page/title-page-attributes.ts b/src/file/document/body/section-properties/title-page/title-page-attributes.ts new file mode 100644 index 0000000000..9022cccf3d --- /dev/null +++ b/src/file/document/body/section-properties/title-page/title-page-attributes.ts @@ -0,0 +1,11 @@ +import { XmlAttributeComponent } from "file/xml-components"; + +export interface IHeaderReferenceAttributes { + value: string; +} + +export class TitlePageAttributes extends XmlAttributeComponent { + protected xmlKeys = { + value: "w:val", + }; +} diff --git a/src/file/document/body/section-properties/title-page/title-page.spec.ts b/src/file/document/body/section-properties/title-page/title-page.spec.ts new file mode 100644 index 0000000000..75374a22db --- /dev/null +++ b/src/file/document/body/section-properties/title-page/title-page.spec.ts @@ -0,0 +1,17 @@ +import { expect } from "chai"; + +import { Formatter } from "../../../../../export/formatter"; +import { TitlePage } from "./title-page"; + +describe("PageSize", () => { + describe("#constructor()", () => { + it("should create title page property for different first page header", () => { + const properties = new TitlePage(); + const tree = new Formatter().format(properties); + + expect(Object.keys(tree)).to.deep.equal(["w:titlePg"]); + expect(tree["w:titlePg"]).to.be.an.instanceof(Array); + expect(tree["w:titlePg"][0]).to.deep.equal({ _attr: { "w:val": "1" } }); + }); + }); +}); diff --git a/src/file/document/body/section-properties/title-page/title-page.ts b/src/file/document/body/section-properties/title-page/title-page.ts new file mode 100644 index 0000000000..5b11d77581 --- /dev/null +++ b/src/file/document/body/section-properties/title-page/title-page.ts @@ -0,0 +1,13 @@ +import { XmlComponent } from "file/xml-components"; +import { TitlePageAttributes } from "./title-page-attributes"; + +export class TitlePage extends XmlComponent { + constructor() { + super("w:titlePg"); + this.root.push( + new TitlePageAttributes({ + value: "1", + }), + ); + } +} diff --git a/src/file/document/document.spec.ts b/src/file/document/document.spec.ts index 218ff55a98..e2e8781d7a 100644 --- a/src/file/document/document.spec.ts +++ b/src/file/document/document.spec.ts @@ -23,6 +23,11 @@ describe("Document", () => { } assert.isTrue(true); }); + + it("should create default section", () => { + const body = new Formatter().format(document)["w:document"][1]["w:body"]; + expect(body[0]).to.have.property("w:sectPr"); + }); }); describe("#createParagraph", () => { @@ -33,7 +38,7 @@ describe("Document", () => { expect(body) .to.be.an("array") .which.has.length.at.least(1); - expect(body[1]).to.have.property("w:p"); + expect(body[0]).to.have.property("w:p"); }); it("should use the text given to create a run in the paragraph", () => { @@ -43,7 +48,7 @@ describe("Document", () => { expect(body) .to.be.an("array") .which.has.length.at.least(1); - expect(body[1]) + expect(body[0]) .to.have.property("w:p") .which.includes({ "w:r": [{ "w:rPr": [] }, { "w:t": [{ _attr: { "xml:space": "preserve" } }, "sample paragraph text"] }], @@ -59,7 +64,7 @@ describe("Document", () => { expect(body) .to.be.an("array") .which.has.length.at.least(1); - expect(body[1]).to.have.property("w:tbl"); + expect(body[0]).to.have.property("w:tbl"); }); it("should create a table with the correct dimensions", () => { @@ -68,7 +73,7 @@ describe("Document", () => { expect(body) .to.be.an("array") .which.has.length.at.least(1); - expect(body[1]) + expect(body[0]) .to.have.property("w:tbl") .which.includes({ "w:tblGrid": [ @@ -77,7 +82,7 @@ describe("Document", () => { { "w:gridCol": [{ _attr: { "w:w": 1 } }] }, ], }); - expect(body[1]["w:tbl"].filter((x) => x["w:tr"])).to.have.length(2); + expect(body[0]["w:tbl"].filter((x) => x["w:tr"])).to.have.length(2); }); }); }); diff --git a/src/file/document/document.ts b/src/file/document/document.ts index fcf52ec27d..ad4a77bac5 100644 --- a/src/file/document/document.ts +++ b/src/file/document/document.ts @@ -1,7 +1,6 @@ // http://officeopenxml.com/WPdocument.php -import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; -import { Paragraph, PictureRun } from "../paragraph"; +import { Paragraph } from "../paragraph"; import { Table } from "../table"; import { Body } from "./body"; import { SectionPropertiesOptions } from "./body/section-properties/section-properties"; @@ -37,8 +36,9 @@ export class Document extends XmlComponent { this.root.push(this.body); } - public addParagraph(paragraph: Paragraph): void { + public addParagraph(paragraph: Paragraph): Document { this.body.push(paragraph); + return this; } public createParagraph(text?: string): Paragraph { @@ -57,17 +57,7 @@ export class Document extends XmlComponent { return table; } - public addDrawing(pictureParagraph: Paragraph): void { - this.body.push(pictureParagraph); - } - - public createDrawing(imageData: IMediaData): PictureRun { - const paragraph = new Paragraph(); - const run = new PictureRun(imageData); - - paragraph.addRun(run); - this.addDrawing(paragraph); - - return run; + public get Body(): Body { + return this.body; } } diff --git a/src/file/document/index.ts b/src/file/document/index.ts index fe6d89c0eb..6b128299f6 100644 --- a/src/file/document/index.ts +++ b/src/file/document/index.ts @@ -1 +1,2 @@ export * from "./document"; +export * from "./body"; diff --git a/src/file/drawing/anchor/anchor-attributes.ts b/src/file/drawing/anchor/anchor-attributes.ts new file mode 100644 index 0000000000..cfd8ad3144 --- /dev/null +++ b/src/file/drawing/anchor/anchor-attributes.ts @@ -0,0 +1,26 @@ +import { XmlAttributeComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +export interface IAnchorAttributes extends IDistance { + allowOverlap?: "0" | "1"; + behindDoc?: "0" | "1"; + layoutInCell?: "0" | "1"; + locked?: "0" | "1"; + relativeHeight?: number; + simplePos?: "0" | "1"; +} + +export class AnchorAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + distL: "distL", + distR: "distR", + allowOverlap: "allowOverlap", + behindDoc: "behindDoc", + layoutInCell: "layoutInCell", + locked: "locked", + relativeHeight: "relativeHeight", + simplePos: "simplePos", + }; +} diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts new file mode 100644 index 0000000000..58adefd174 --- /dev/null +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -0,0 +1,118 @@ +import { assert } from "chai"; + +import { Utility } from "../../../tests/utility"; +import { IDrawingOptions, TextWrapStyle } from ".././"; +import { Anchor } from "./"; + +function createDrawing(drawingOptions: IDrawingOptions): Anchor { + return new Anchor( + 1, + { + pixels: { + x: 100, + y: 100, + }, + emus: { + x: 100 * 9525, + y: 100 * 9525, + }, + }, + drawingOptions, + ); +} + +describe("Anchor", () => { + let anchor: Anchor; + + describe("#constructor()", () => { + it("should create a Drawing with correct root key", () => { + anchor = createDrawing({}); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.rootKey, "wp:anchor"); + assert.equal(newJson.root.length, 10); + }); + + it("should create a Drawing with all default options", () => { + anchor = createDrawing({}); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + const anchorAttributes = newJson.root[0].root; + assert.include(anchorAttributes, { + distT: 0, + distB: 0, + distL: 0, + distR: 0, + simplePos: "0", + allowOverlap: "1", + behindDoc: "0", + locked: "0", + layoutInCell: "1", + relativeHeight: 952500, + }); + + // 1: simple pos + assert.equal(newJson.root[1].rootKey, "wp:simplePos"); + + // 2: horizontal position + const horizontalPosition = newJson.root[2]; + assert.equal(horizontalPosition.rootKey, "wp:positionH"); + assert.include(horizontalPosition.root[0].root, { + relativeFrom: "column", + }); + assert.equal(horizontalPosition.root[1].rootKey, "wp:posOffset"); + assert.include(horizontalPosition.root[1].root[0], 0); + + // 3: vertical position + const verticalPosition = newJson.root[3]; + assert.equal(verticalPosition.rootKey, "wp:positionV"); + assert.include(verticalPosition.root[0].root, { + relativeFrom: "paragraph", + }); + assert.equal(verticalPosition.root[1].rootKey, "wp:posOffset"); + assert.include(verticalPosition.root[1].root[0], 0); + + // 4: extent + const extent = newJson.root[4]; + assert.equal(extent.rootKey, "wp:extent"); + assert.include(extent.root[0].root, { + cx: 952500, + cy: 952500, + }); + + // 5: effect extent + const effectExtent = newJson.root[5]; + assert.equal(effectExtent.rootKey, "wp:effectExtent"); + + // 6 text wrap: none + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapNone"); + + // 7: doc properties + const docProperties = newJson.root[7]; + assert.equal(docProperties.rootKey, "wp:docPr"); + + // 8: graphic frame properties + const graphicFrame = newJson.root[8]; + assert.equal(graphicFrame.rootKey, "wp:cNvGraphicFramePr"); + + // 9: graphic + const graphic = newJson.root[9]; + assert.equal(graphic.rootKey, "a:graphic"); + }); + + it("should create a Drawing with text wrapping", () => { + anchor = createDrawing({ + textWrapping: { + textWrapStyle: TextWrapStyle.SQUARE, + }, + }); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + // 6 text wrap: square + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapSquare"); + }); + }); +}); diff --git a/src/file/drawing/anchor/anchor.ts b/src/file/drawing/anchor/anchor.ts new file mode 100644 index 0000000000..d76a11402c --- /dev/null +++ b/src/file/drawing/anchor/anchor.ts @@ -0,0 +1,88 @@ +// http://officeopenxml.com/drwPicFloating.php +import { IMediaDataDimensions } from "file/media"; +import { XmlComponent } from "file/xml-components"; +import { IDrawingOptions } from "../drawing"; +import { + HorizontalPosition, + HorizontalPositionRelativeFrom, + IFloating, + SimplePos, + VerticalPosition, + VerticalPositionRelativeFrom, +} from "../floating"; +import { Graphic } from "../inline/graphic"; +import { TextWrapStyle, WrapNone, WrapSquare, WrapTight, WrapTopAndBottom } from "../text-wrap"; +import { DocProperties } from "./../doc-properties/doc-properties"; +import { EffectExtent } from "./../effect-extent/effect-extent"; +import { Extent } from "./../extent/extent"; +import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties"; +import { AnchorAttributes } from "./anchor-attributes"; + +const defaultOptions: IFloating = { + allowOverlap: true, + behindDocument: false, + lockAnchor: false, + layoutInCell: true, + verticalPosition: { + relative: VerticalPositionRelativeFrom.PARAGRAPH, + offset: 0, + }, + horizontalPosition: { + relative: HorizontalPositionRelativeFrom.COLUMN, + offset: 0, + }, +}; + +export class Anchor extends XmlComponent { + constructor(referenceId: number, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) { + super("wp:anchor"); + + const floating = { + ...defaultOptions, + ...drawingOptions.floating, + }; + this.root.push( + new AnchorAttributes({ + distT: 0, + distB: 0, + distL: 0, + distR: 0, + simplePos: "0", // note: word doesn't fully support - so we use 0 + allowOverlap: floating.allowOverlap === true ? "1" : "0", + behindDoc: floating.behindDocument === true ? "1" : "0", + locked: floating.lockAnchor === true ? "1" : "0", + layoutInCell: floating.layoutInCell === true ? "1" : "0", + relativeHeight: dimensions.emus.y, + }), + ); + + this.root.push(new SimplePos()); + this.root.push(new HorizontalPosition(floating.horizontalPosition)); + this.root.push(new VerticalPosition(floating.verticalPosition)); + this.root.push(new Extent(dimensions.emus.x, dimensions.emus.y)); + this.root.push(new EffectExtent()); + + if (drawingOptions.textWrapping !== undefined) { + switch (drawingOptions.textWrapping.textWrapStyle) { + case TextWrapStyle.SQUARE: + this.root.push(new WrapSquare(drawingOptions.textWrapping)); + break; + case TextWrapStyle.TIGHT: + this.root.push(new WrapTight(drawingOptions.textWrapping.distanceFromText)); + break; + case TextWrapStyle.TOP_AND_BOTTOM: + this.root.push(new WrapTopAndBottom(drawingOptions.textWrapping.distanceFromText)); + break; + case TextWrapStyle.NONE: + default: + this.root.push(new WrapNone()); + } + } else { + this.root.push(new WrapNone()); + } + + this.root.push(new DocProperties()); + this.root.push(new GraphicFrameProperties()); + this.root.push(new Graphic(referenceId, dimensions.emus.x, dimensions.emus.y)); + } +} diff --git a/src/file/drawing/anchor/index.ts b/src/file/drawing/anchor/index.ts new file mode 100644 index 0000000000..57faf47fc0 --- /dev/null +++ b/src/file/drawing/anchor/index.ts @@ -0,0 +1,2 @@ +export * from "./anchor"; +export * from "./anchor-attributes"; diff --git a/src/file/drawing/inline/doc-properties/doc-properties-attributes.ts b/src/file/drawing/doc-properties/doc-properties-attributes.ts similarity index 100% rename from src/file/drawing/inline/doc-properties/doc-properties-attributes.ts rename to src/file/drawing/doc-properties/doc-properties-attributes.ts diff --git a/src/file/drawing/inline/doc-properties/doc-properties.ts b/src/file/drawing/doc-properties/doc-properties.ts similarity index 100% rename from src/file/drawing/inline/doc-properties/doc-properties.ts rename to src/file/drawing/doc-properties/doc-properties.ts diff --git a/src/file/drawing/drawing.spec.ts b/src/file/drawing/drawing.spec.ts index 6ca09faa4d..70d36f2110 100644 --- a/src/file/drawing/drawing.spec.ts +++ b/src/file/drawing/drawing.spec.ts @@ -2,14 +2,12 @@ import { assert } from "chai"; import * as fs from "fs"; import { Utility } from "../../tests/utility"; -import { Drawing } from "./"; +import { Drawing, IDrawingOptions, PlacementPosition } from "./"; -describe("Drawing", () => { - let currentBreak: Drawing; - - beforeEach(() => { - const path = "./demo/images/image1.jpeg"; - currentBreak = new Drawing({ +function createDrawing(drawingOptions?: IDrawingOptions): Drawing { + const path = "./demo/images/image1.jpeg"; + return new Drawing( + { fileName: "test.jpg", referenceId: 1, path: path, @@ -23,14 +21,33 @@ describe("Drawing", () => { y: 100 * 9525, }, }, - }); - }); + }, + drawingOptions, + ); +} + +describe("Drawing", () => { + let currentBreak: Drawing; describe("#constructor()", () => { it("should create a Drawing with correct root key", () => { + currentBreak = createDrawing(); const newJson = Utility.jsonify(currentBreak); assert.equal(newJson.rootKey, "w:drawing"); - // console.log(JSON.stringify(newJson, null, 2)); + }); + + it("should create a drawing with inline element when there are no options passed", () => { + currentBreak = createDrawing(); + const newJson = Utility.jsonify(currentBreak); + assert.equal(newJson.root[0].rootKey, "wp:inline"); + }); + + it("should create a drawing with anchor element when there options are passed", () => { + currentBreak = createDrawing({ + position: PlacementPosition.FLOATING, + }); + const newJson = Utility.jsonify(currentBreak); + assert.equal(newJson.root[0].rootKey, "wp:anchor"); }); }); }); diff --git a/src/file/drawing/drawing.ts b/src/file/drawing/drawing.ts index 61c93592df..d45a6ad3bb 100644 --- a/src/file/drawing/drawing.ts +++ b/src/file/drawing/drawing.ts @@ -1,20 +1,53 @@ import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; +import { Anchor } from "./anchor"; +import { IFloating } from "./floating"; import { Inline } from "./inline"; +import { ITextWrapping } from "./text-wrap"; + +export enum PlacementPosition { + INLINE, + FLOATING, +} + +export interface IDistance { + distT?: number; + distB?: number; + distL?: number; + distR?: number; +} + +export interface IDrawingOptions { + position?: PlacementPosition; + textWrapping?: ITextWrapping; + floating?: IFloating; +} + +const defaultDrawingOptions: IDrawingOptions = { + position: PlacementPosition.INLINE, +}; export class Drawing extends XmlComponent { - private inline: Inline; + private readonly inline: Inline; - constructor(imageData: IMediaData) { + constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { super("w:drawing"); if (imageData === undefined) { throw new Error("imageData cannot be undefined"); } - this.inline = new Inline(imageData.referenceId, imageData.dimensions); + const mergedOptions = { + ...defaultDrawingOptions, + ...drawingOptions, + }; - this.root.push(this.inline); + if (mergedOptions.position === PlacementPosition.INLINE) { + this.inline = new Inline(imageData.referenceId, imageData.dimensions); + this.root.push(this.inline); + } else if (mergedOptions.position === PlacementPosition.FLOATING) { + this.root.push(new Anchor(imageData.referenceId, imageData.dimensions, mergedOptions)); + } } public scale(factorX: number, factorY: number): void { diff --git a/src/file/drawing/inline/effect-extent/effect-extent-attributes.ts b/src/file/drawing/effect-extent/effect-extent-attributes.ts similarity index 100% rename from src/file/drawing/inline/effect-extent/effect-extent-attributes.ts rename to src/file/drawing/effect-extent/effect-extent-attributes.ts diff --git a/src/file/drawing/inline/effect-extent/effect-extent.ts b/src/file/drawing/effect-extent/effect-extent.ts similarity index 100% rename from src/file/drawing/inline/effect-extent/effect-extent.ts rename to src/file/drawing/effect-extent/effect-extent.ts diff --git a/src/file/drawing/inline/extent/extent-attributes.ts b/src/file/drawing/extent/extent-attributes.ts similarity index 100% rename from src/file/drawing/inline/extent/extent-attributes.ts rename to src/file/drawing/extent/extent-attributes.ts diff --git a/src/file/drawing/inline/extent/extent.ts b/src/file/drawing/extent/extent.ts similarity index 90% rename from src/file/drawing/inline/extent/extent.ts rename to src/file/drawing/extent/extent.ts index 0e22348cf1..270a85a8a8 100644 --- a/src/file/drawing/inline/extent/extent.ts +++ b/src/file/drawing/extent/extent.ts @@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components"; import { ExtentAttributes } from "./extent-attributes"; export class Extent extends XmlComponent { - private attributes: ExtentAttributes; + private readonly attributes: ExtentAttributes; constructor(x: number, y: number) { super("wp:extent"); diff --git a/src/file/drawing/floating/align.spec.ts b/src/file/drawing/floating/align.spec.ts new file mode 100644 index 0000000000..5ec77d6fd0 --- /dev/null +++ b/src/file/drawing/floating/align.spec.ts @@ -0,0 +1,15 @@ +import { assert } from "chai"; + +import { VerticalPositionAlign } from "."; +import { Utility } from "../../../tests/utility"; +import { Align } from "./align"; + +describe("Align", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new Align(VerticalPositionAlign.CENTER)); + assert.equal(newJson.rootKey, "wp:align"); + assert.include(newJson.root[0], VerticalPositionAlign.CENTER); + }); + }); +}); diff --git a/src/file/drawing/floating/align.ts b/src/file/drawing/floating/align.ts new file mode 100644 index 0000000000..2ffa4ac52b --- /dev/null +++ b/src/file/drawing/floating/align.ts @@ -0,0 +1,10 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlComponent } from "file/xml-components"; +import { HorizontalPositionAlign, VerticalPositionAlign } from "./floating-position"; + +export class Align extends XmlComponent { + constructor(value: HorizontalPositionAlign | VerticalPositionAlign) { + super("wp:align"); + this.root.push(value); + } +} diff --git a/src/file/drawing/floating/floating-position.ts b/src/file/drawing/floating/floating-position.ts new file mode 100644 index 0000000000..7039846bc7 --- /dev/null +++ b/src/file/drawing/floating/floating-position.ts @@ -0,0 +1,60 @@ +// http://officeopenxml.com/drwPicFloating-position.php + +export enum HorizontalPositionRelativeFrom { + CHARACTER = "character", + COLUMN = "column", + INSIDE_MARGIN = "insideMargin", + LEFT_MARGIN = "leftMargin", + MARGIN = "margin", + OUTSIDE_MARGIN = "outsideMargin", + PAGE = "page", + RIGHT_MARGIN = "rightMargin", +} + +export enum VerticalPositionRelativeFrom { + BOTTOM_MARGIN = "bottomMargin", + INSIDE_MARGIN = "insideMargin", + LINE = "line", + MARGIN = "margin", + OUTSIDE_MARGIN = "outsideMargin", + PAGE = "page", + PARAGRAPH = "paragraph", + TOP_MARGIN = "topMargin", +} + +export enum HorizontalPositionAlign { + CENTER = "center", + INSIDE = "inside", + LEFT = "left", + OUTSIDE = "outside", + RIGHT = "right", +} + +export enum VerticalPositionAlign { + BOTTOM = "bottom", + CENTER = "center", + INSIDE = "inside", + OUTSIDE = "outside", + TOP = "top", +} + +export interface IHorizontalPositionOptions { + relative: HorizontalPositionRelativeFrom; + align?: HorizontalPositionAlign; + offset?: number; +} + +export interface IVerticalPositionOptions { + relative: VerticalPositionRelativeFrom; + align?: VerticalPositionAlign; + offset?: number; +} + +export interface IFloating { + horizontalPosition: IHorizontalPositionOptions; + verticalPosition: IVerticalPositionOptions; + allowOverlap?: boolean; + lockAnchor?: boolean; + behindDocument?: boolean; + layoutInCell?: boolean; +} diff --git a/src/file/drawing/floating/horizontal-position.spec.ts b/src/file/drawing/floating/horizontal-position.spec.ts new file mode 100644 index 0000000000..1b139b47be --- /dev/null +++ b/src/file/drawing/floating/horizontal-position.spec.ts @@ -0,0 +1,41 @@ +import { assert } from "chai"; + +import { HorizontalPositionAlign, HorizontalPositionRelativeFrom } from "."; +import { Utility } from "../../../tests/utility"; +import { HorizontalPosition } from "./horizontal-position"; + +describe("HorizontalPosition", () => { + describe("#constructor()", () => { + it("should create a element with position align", () => { + const newJson = Utility.jsonify( + new HorizontalPosition({ + relative: HorizontalPositionRelativeFrom.MARGIN, + align: HorizontalPositionAlign.CENTER, + }), + ); + assert.equal(newJson.rootKey, "wp:positionH"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:align"); + assert.include(newJson.root[1].root, "center"); + }); + + it("should create a element with offset", () => { + const newJson = Utility.jsonify( + new HorizontalPosition({ + relative: HorizontalPositionRelativeFrom.MARGIN, + offset: 40, + }), + ); + assert.equal(newJson.rootKey, "wp:positionH"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:posOffset"); + assert.include(newJson.root[1].root[0], 40); + }); + }); +}); diff --git a/src/file/drawing/floating/horizontal-position.ts b/src/file/drawing/floating/horizontal-position.ts new file mode 100644 index 0000000000..f0725aa857 --- /dev/null +++ b/src/file/drawing/floating/horizontal-position.ts @@ -0,0 +1,35 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { Align } from "./align"; +import { HorizontalPositionRelativeFrom, IHorizontalPositionOptions } from "./floating-position"; +import { PositionOffset } from "./position-offset"; + +interface IHorizontalPositionAttributes { + relativeFrom: HorizontalPositionRelativeFrom; +} + +class HorizontalPositionAttributes extends XmlAttributeComponent { + protected xmlKeys = { + relativeFrom: "relativeFrom", + }; +} + +export class HorizontalPosition extends XmlComponent { + constructor(horizontalPosition: IHorizontalPositionOptions) { + super("wp:positionH"); + + this.root.push( + new HorizontalPositionAttributes({ + relativeFrom: horizontalPosition.relative, + }), + ); + + if (horizontalPosition.align) { + this.root.push(new Align(horizontalPosition.align)); + } else if (horizontalPosition.offset !== undefined) { + this.root.push(new PositionOffset(horizontalPosition.offset)); + } else { + throw new Error("There is no configuration provided for floating position (Align or offset)"); + } + } +} diff --git a/src/file/drawing/floating/index.ts b/src/file/drawing/floating/index.ts new file mode 100644 index 0000000000..80061d16e1 --- /dev/null +++ b/src/file/drawing/floating/index.ts @@ -0,0 +1,4 @@ +export * from "./floating-position"; +export * from "./simple-pos"; +export * from "./horizontal-position"; +export * from "./vertical-position"; diff --git a/src/file/drawing/floating/position-offset.spec.ts b/src/file/drawing/floating/position-offset.spec.ts new file mode 100644 index 0000000000..74aebaebc2 --- /dev/null +++ b/src/file/drawing/floating/position-offset.spec.ts @@ -0,0 +1,14 @@ +import { assert } from "chai"; + +import { Utility } from "../../../tests/utility"; +import { PositionOffset } from "./position-offset"; + +describe("PositionOffset", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new PositionOffset(50)); + assert.equal(newJson.rootKey, "wp:posOffset"); + assert.equal(newJson.root[0], 50); + }); + }); +}); diff --git a/src/file/drawing/floating/position-offset.ts b/src/file/drawing/floating/position-offset.ts new file mode 100644 index 0000000000..4d3aa96b07 --- /dev/null +++ b/src/file/drawing/floating/position-offset.ts @@ -0,0 +1,9 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlComponent } from "file/xml-components"; + +export class PositionOffset extends XmlComponent { + constructor(offsetValue: number) { + super("wp:posOffset"); + this.root.push(offsetValue.toString()); + } +} diff --git a/src/file/drawing/floating/simple-pos.spec.ts b/src/file/drawing/floating/simple-pos.spec.ts new file mode 100644 index 0000000000..a86739b7b0 --- /dev/null +++ b/src/file/drawing/floating/simple-pos.spec.ts @@ -0,0 +1,17 @@ +import { assert } from "chai"; + +import { SimplePos } from "./simple-pos"; +import { Utility } from "../../../tests/utility"; + +describe("SimplePos", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new SimplePos()); + assert.equal(newJson.rootKey, "wp:simplePos"); + assert.include(newJson.root[0].root, { + x: 0, + y: 0, + }); + }); + }); +}); diff --git a/src/file/drawing/floating/simple-pos.ts b/src/file/drawing/floating/simple-pos.ts new file mode 100644 index 0000000000..6330f6660a --- /dev/null +++ b/src/file/drawing/floating/simple-pos.ts @@ -0,0 +1,28 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +interface ISimplePosAttributes { + x: number; + y: number; +} + +class SimplePosAttributes extends XmlAttributeComponent { + protected xmlKeys = { + x: "x", + y: "y", + }; +} + +export class SimplePos extends XmlComponent { + constructor() { + super("wp:simplePos"); + + // NOTE: It's not fully supported in Microsoft Word, but this element is needed anyway + this.root.push( + new SimplePosAttributes({ + x: 0, + y: 0, + }), + ); + } +} diff --git a/src/file/drawing/floating/vertical-position.spec.ts b/src/file/drawing/floating/vertical-position.spec.ts new file mode 100644 index 0000000000..a9d7ed65f8 --- /dev/null +++ b/src/file/drawing/floating/vertical-position.spec.ts @@ -0,0 +1,41 @@ +import { assert } from "chai"; + +import { VerticalPositionAlign, VerticalPositionRelativeFrom } from "."; +import { Utility } from "../../../tests/utility"; +import { VerticalPosition } from "./vertical-position"; + +describe("VerticalPosition", () => { + describe("#constructor()", () => { + it("should create a element with position align", () => { + const newJson = Utility.jsonify( + new VerticalPosition({ + relative: VerticalPositionRelativeFrom.MARGIN, + align: VerticalPositionAlign.INSIDE, + }), + ); + assert.equal(newJson.rootKey, "wp:positionV"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:align"); + assert.include(newJson.root[1].root, "inside"); + }); + + it("should create a element with offset", () => { + const newJson = Utility.jsonify( + new VerticalPosition({ + relative: VerticalPositionRelativeFrom.MARGIN, + offset: 40, + }), + ); + assert.equal(newJson.rootKey, "wp:positionV"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:posOffset"); + assert.include(newJson.root[1].root[0], 40); + }); + }); +}); diff --git a/src/file/drawing/floating/vertical-position.ts b/src/file/drawing/floating/vertical-position.ts new file mode 100644 index 0000000000..10b6d6028f --- /dev/null +++ b/src/file/drawing/floating/vertical-position.ts @@ -0,0 +1,35 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { Align } from "./align"; +import { IVerticalPositionOptions, VerticalPositionRelativeFrom } from "./floating-position"; +import { PositionOffset } from "./position-offset"; + +interface IVerticalPositionAttributes { + relativeFrom: VerticalPositionRelativeFrom; +} + +class VerticalPositionAttributes extends XmlAttributeComponent { + protected xmlKeys = { + relativeFrom: "relativeFrom", + }; +} + +export class VerticalPosition extends XmlComponent { + constructor(verticalPosition: IVerticalPositionOptions) { + super("wp:positionV"); + + this.root.push( + new VerticalPositionAttributes({ + relativeFrom: verticalPosition.relative, + }), + ); + + if (verticalPosition.align) { + this.root.push(new Align(verticalPosition.align)); + } else if (verticalPosition.offset !== undefined) { + this.root.push(new PositionOffset(verticalPosition.offset)); + } else { + throw new Error("There is no configuration provided for floating position (Align or offset)"); + } + } +} diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts b/src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts rename to src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts b/src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts rename to src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-properties.ts b/src/file/drawing/graphic-frame/graphic-frame-properties.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-properties.ts rename to src/file/drawing/graphic-frame/graphic-frame-properties.ts diff --git a/src/file/drawing/index.ts b/src/file/drawing/index.ts index ba96e11de9..8a1a62a201 100644 --- a/src/file/drawing/index.ts +++ b/src/file/drawing/index.ts @@ -1 +1,3 @@ -export { Drawing } from "./drawing"; +export * from "./drawing"; +export * from "./text-wrap"; +export * from "./floating"; diff --git a/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts b/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts index ca3d4600be..78606fd399 100644 --- a/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts +++ b/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts @@ -3,7 +3,7 @@ import { GraphicDataAttributes } from "./graphic-data-attribute"; import { Pic } from "./pic"; export class GraphicData extends XmlComponent { - private pic: Pic; + private readonly pic: Pic; constructor(referenceId: number, x: number, y: number) { super("a:graphicData"); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts b/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts index f8b555af00..14ccf63f02 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts @@ -6,7 +6,7 @@ import { PicAttributes } from "./pic-attributes"; import { ShapeProperties } from "./shape-properties/shape-properties"; export class Pic extends XmlComponent { - private shapeProperties: ShapeProperties; + private readonly shapeProperties: ShapeProperties; constructor(referenceId: number, x: number, y: number) { super("pic:pic"); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/form/extents/extents.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/form/extents/extents.ts index 74aea18a48..55bf8bae0b 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/form/extents/extents.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/form/extents/extents.ts @@ -3,7 +3,7 @@ import { XmlComponent } from "file/xml-components"; import { ExtentsAttributes } from "./extents-attributes"; export class Extents extends XmlComponent { - private attributes: ExtentsAttributes; + private readonly attributes: ExtentsAttributes; constructor(x: number, y: number) { super("a:ext"); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/form/form.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/form/form.ts index 06523e0965..48c069f644 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/form/form.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/form/form.ts @@ -4,7 +4,7 @@ import { Extents } from "./extents/extents"; import { Offset } from "./offset/off"; export class Form extends XmlComponent { - private extents: Extents; + private readonly extents: Extents; constructor(x: number, y: number) { super("a:xfrm"); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/shape-properties.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/shape-properties.ts index 9c910f62e2..9dacae18d6 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/shape-properties.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/shape-properties.ts @@ -7,7 +7,7 @@ import { PresetGeometry } from "./preset-geometry/preset-geometry"; import { ShapePropertiesAttributes } from "./shape-properties-attributes"; export class ShapeProperties extends XmlComponent { - private form: Form; + private readonly form: Form; constructor(x: number, y: number) { super("pic:spPr"); diff --git a/src/file/drawing/inline/graphic/graphic.ts b/src/file/drawing/inline/graphic/graphic.ts index ce61893e05..dba653d2ca 100644 --- a/src/file/drawing/inline/graphic/graphic.ts +++ b/src/file/drawing/inline/graphic/graphic.ts @@ -12,7 +12,7 @@ class GraphicAttributes extends XmlAttributeComponent { } export class Graphic extends XmlComponent { - private data: GraphicData; + private readonly data: GraphicData; constructor(referenceId: number, x: number, y: number) { super("a:graphic"); diff --git a/src/file/drawing/inline/inline-attributes.ts b/src/file/drawing/inline/inline-attributes.ts index 1a4ef74e3c..5f7489188c 100644 --- a/src/file/drawing/inline/inline-attributes.ts +++ b/src/file/drawing/inline/inline-attributes.ts @@ -1,11 +1,8 @@ import { XmlAttributeComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; -export interface IInlineAttributes { - distT?: number; - distB?: number; - distL?: number; - distR?: number; -} +// tslint:disable-next-line:no-empty-interface +export interface IInlineAttributes extends IDistance {} export class InlineAttributes extends XmlAttributeComponent { protected xmlKeys = { diff --git a/src/file/drawing/inline/inline.ts b/src/file/drawing/inline/inline.ts index 0205eb3090..f36dd19cf3 100644 --- a/src/file/drawing/inline/inline.ts +++ b/src/file/drawing/inline/inline.ts @@ -1,18 +1,18 @@ // http://officeopenxml.com/drwPicInline.php import { IMediaDataDimensions } from "file/media"; import { XmlComponent } from "file/xml-components"; -import { DocProperties } from "./doc-properties/doc-properties"; -import { EffectExtent } from "./effect-extent/effect-extent"; -import { Extent } from "./extent/extent"; -import { Graphic } from "./graphic"; -import { GraphicFrameProperties } from "./graphic-frame/graphic-frame-properties"; +import { DocProperties } from "./../doc-properties/doc-properties"; +import { EffectExtent } from "./../effect-extent/effect-extent"; +import { Extent } from "./../extent/extent"; +import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties"; +import { Graphic } from "./../inline/graphic"; import { InlineAttributes } from "./inline-attributes"; export class Inline extends XmlComponent { - private extent: Extent; - private graphic: Graphic; + private readonly extent: Extent; + private readonly graphic: Graphic; - constructor(referenceId: number, private dimensions: IMediaDataDimensions) { + constructor(referenceId: number, private readonly dimensions: IMediaDataDimensions) { super("wp:inline"); this.root.push( diff --git a/src/file/drawing/text-wrap/index.ts b/src/file/drawing/text-wrap/index.ts new file mode 100644 index 0000000000..ce8c0bbd13 --- /dev/null +++ b/src/file/drawing/text-wrap/index.ts @@ -0,0 +1,5 @@ +export * from "./text-wrapping"; +export * from "./wrap-none"; +export * from "./wrap-square"; +export * from "./wrap-tight"; +export * from "./wrap-top-and-bottom"; diff --git a/src/file/drawing/text-wrap/text-wrapping.ts b/src/file/drawing/text-wrap/text-wrapping.ts new file mode 100644 index 0000000000..7fc14a52fd --- /dev/null +++ b/src/file/drawing/text-wrap/text-wrapping.ts @@ -0,0 +1,22 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { IDistance } from "../drawing"; + +export enum TextWrapStyle { + NONE, + SQUARE, + TIGHT, + TOP_AND_BOTTOM, +} + +export enum WrapTextOption { + BOTH_SIDES = "bothSides", + LEFT = "left", + RIGHT = "right", + LARGEST = "largest", +} + +export interface ITextWrapping { + textWrapStyle: TextWrapStyle; + wrapTextOption?: WrapTextOption; + distanceFromText?: IDistance; +} diff --git a/src/file/drawing/text-wrap/wrap-none.ts b/src/file/drawing/text-wrap/wrap-none.ts new file mode 100644 index 0000000000..0ac4c632f0 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-none.ts @@ -0,0 +1,8 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlComponent } from "file/xml-components"; + +export class WrapNone extends XmlComponent { + constructor() { + super("wp:wrapNone"); + } +} diff --git a/src/file/drawing/text-wrap/wrap-square.ts b/src/file/drawing/text-wrap/wrap-square.ts new file mode 100644 index 0000000000..08ed108209 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-square.ts @@ -0,0 +1,31 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { ITextWrapping, WrapTextOption } from "."; +import { IDistance } from "../drawing"; + +interface IWrapSquareAttributes extends IDistance { + wrapText?: WrapTextOption; +} + +class WrapSquareAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + distL: "distL", + distR: "distR", + wrapText: "wrapText", + }; +} + +export class WrapSquare extends XmlComponent { + constructor(textWrapping: ITextWrapping) { + super("wp:wrapSquare"); + + this.root.push( + new WrapSquareAttributes({ + wrapText: textWrapping.wrapTextOption || WrapTextOption.BOTH_SIDES, + ...textWrapping.distanceFromText, + }), + ); + } +} diff --git a/src/file/drawing/text-wrap/wrap-tight.ts b/src/file/drawing/text-wrap/wrap-tight.ts new file mode 100644 index 0000000000..cda9a20194 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-tight.ts @@ -0,0 +1,33 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +interface IWrapTightAttributes { + distT?: number; + distB?: number; +} + +class WrapTightAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + }; +} + +export class WrapTight extends XmlComponent { + constructor(distanceFromText?: IDistance) { + super("wp:wrapTight"); + + distanceFromText = distanceFromText || { + distT: 0, + distB: 0, + }; + + this.root.push( + new WrapTightAttributes({ + distT: distanceFromText.distT, + distB: distanceFromText.distB, + }), + ); + } +} diff --git a/src/file/drawing/text-wrap/wrap-top-and-bottom.ts b/src/file/drawing/text-wrap/wrap-top-and-bottom.ts new file mode 100644 index 0000000000..bf6a5c3cae --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-top-and-bottom.ts @@ -0,0 +1,33 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +interface IWrapTopAndBottomAttributes { + distT?: number; + distB?: number; +} + +class WrapTopAndBottomAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + }; +} + +export class WrapTopAndBottom extends XmlComponent { + constructor(distanceFromText?: IDistance) { + super("wp:wrapTopAndBottom"); + + distanceFromText = distanceFromText || { + distT: 0, + distB: 0, + }; + + this.root.push( + new WrapTopAndBottomAttributes({ + distT: distanceFromText.distT, + distB: distanceFromText.distB, + }), + ); + } +} diff --git a/src/file/file.ts b/src/file/file.ts index 67393a8c6f..d7ef02b520 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -2,14 +2,17 @@ import { AppProperties } from "./app-properties/app-properties"; import { ContentTypes } from "./content-types/content-types"; import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { Document } from "./document"; +import { FooterReferenceType, HeaderReference, HeaderReferenceType } from "./document/body/section-properties"; import { SectionPropertiesOptions } from "./document/body/section-properties/section-properties"; import { FooterWrapper } from "./footer-wrapper"; +import { FootNotes } from "./footnotes"; import { HeaderWrapper } from "./header-wrapper"; -import { Media } from "./media"; +import { Image, Media } from "./media"; import { Numbering } from "./numbering"; -import { Paragraph, PictureRun } from "./paragraph"; +import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Styles } from "./styles"; +import { ExternalStylesFactory } from "./styles/external-styles-factory"; import { DefaultStylesFactory } from "./styles/factory"; import { Table } from "./table"; @@ -21,16 +24,16 @@ export class File { private readonly media: Media; private readonly docRelationships: Relationships; private readonly fileRelationships: Relationships; - private readonly headerWrapper: HeaderWrapper; - private readonly footerWrapper: FooterWrapper; + private readonly headerWrapper: HeaderWrapper[] = []; + private readonly footerWrapper: FooterWrapper[] = []; + private readonly footNotes: FootNotes; + private readonly contentTypes: ContentTypes; private readonly appProperties: AppProperties; - constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { - this.document = new Document(sectionPropertiesOptions); - const stylesFactory = new DefaultStylesFactory(); - this.styles = stylesFactory.newInstance(); + private currentRelationshipId: number = 1; + constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { if (!options) { options = { creator: "Un-named", @@ -39,33 +42,39 @@ export class File { }; } + if (options.externalStyles) { + const stylesFactory = new ExternalStylesFactory(); + this.styles = stylesFactory.newInstance(options.externalStyles); + } else { + const stylesFactory = new DefaultStylesFactory(); + this.styles = stylesFactory.newInstance(); + } + this.coreProperties = new CoreProperties(options); this.numbering = new Numbering(); this.docRelationships = new Relationships(); this.docRelationships.createRelationship( - 1, + this.currentRelationshipId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", "styles.xml", ); this.docRelationships.createRelationship( - 2, + this.currentRelationshipId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", "numbering.xml", ); + this.contentTypes = new ContentTypes(); + this.docRelationships.createRelationship( - 3, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", - "header1.xml", - ); - this.docRelationships.createRelationship( - 4, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", - "footer1.xml", + this.currentRelationshipId++, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", + "footnotes.xml", ); this.media = new Media(); - this.headerWrapper = new HeaderWrapper(this.media); - this.footerWrapper = new FooterWrapper(this.media); - this.contentTypes = new ContentTypes(); + + const header = this.createHeader(); + const footer = this.createFooter(); + this.fileRelationships = new Relationships(); this.fileRelationships.createRelationship( 1, @@ -83,6 +92,20 @@ export class File { "docProps/app.xml", ); this.appProperties = new AppProperties(); + + this.footNotes = new FootNotes(); + if (!sectionPropertiesOptions) { + sectionPropertiesOptions = { + footerType: FooterReferenceType.DEFAULT, + headerType: HeaderReferenceType.DEFAULT, + headerId: header.Header.ReferenceId, + footerId: footer.Footer.ReferenceId, + }; + } else { + sectionPropertiesOptions.headerId = header.Header.ReferenceId; + sectionPropertiesOptions.footerId = footer.Footer.ReferenceId; + } + this.document = new Document(sectionPropertiesOptions); } public addParagraph(paragraph: Paragraph): void { @@ -101,14 +124,94 @@ export class File { return this.document.createTable(rows, cols); } - public createImage(image: string): PictureRun { - const mediaData = this.media.addMedia(image, this.docRelationships.RelationshipCount); + public createImage(filePath: string): Image { + const image = Media.addImage(this, filePath); + this.document.addParagraph(image.Paragraph); + + return image; + } + + public addImage(image: Image): File { + this.document.addParagraph(image.Paragraph); + return this; + } + + public createImageFromBuffer(buffer: Buffer, width?: number, height?: number): Image { + const image = Media.addImageFromBuffer(this, buffer, width, height); + this.document.addParagraph(image.Paragraph); + + return image; + } + + public createHyperlink(link: string, text?: string): Hyperlink { + text = text === undefined ? link : text; + const hyperlink = new Hyperlink(text, this.docRelationships.RelationshipCount); this.docRelationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, + hyperlink.linkId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + link, + "External", ); - return this.document.createDrawing(mediaData); + return hyperlink; + } + + public createInternalHyperLink(anchor: string, text?: string): Hyperlink { + text = text === undefined ? anchor : text; + const hyperlink = new Hyperlink(text, this.docRelationships.RelationshipCount, anchor); + // NOTE: unlike File#createHyperlink(), since the link is to an internal bookmark + // we don't need to create a new relationship. + return hyperlink; + } + + public createBookmark(name: string, text?: string): Bookmark { + text = text === undefined ? name : text; + const bookmark = new Bookmark(name, text, this.docRelationships.RelationshipCount); + return bookmark; + } + + public addSection(sectionPropertiesOptions: SectionPropertiesOptions): void { + this.document.Body.addSection(sectionPropertiesOptions); + } + + public createFootnote(paragraph: Paragraph): void { + this.footNotes.createFootNote(paragraph); + } + + public createHeader(): HeaderWrapper { + const header = new HeaderWrapper(this.media, this.currentRelationshipId++); + this.headerWrapper.push(header); + this.docRelationships.createRelationship( + header.Header.ReferenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", + `header${this.headerWrapper.length}.xml`, + ); + this.contentTypes.addHeader(this.headerWrapper.length); + return header; + } + + public createFooter(): FooterWrapper { + const footer = new FooterWrapper(this.media, this.currentRelationshipId++); + this.footerWrapper.push(footer); + this.docRelationships.createRelationship( + footer.Footer.ReferenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", + `footer${this.footerWrapper.length}.xml`, + ); + this.contentTypes.addFooter(this.footerWrapper.length); + return footer; + } + + public createFirstPageHeader(): HeaderWrapper { + const headerWrapper = this.createHeader(); + + this.document.Body.DefaultSection.addChildElement( + new HeaderReference({ + headerType: HeaderReferenceType.FIRST, + headerId: headerWrapper.Header.ReferenceId, + }), + ); + + return headerWrapper; } public get Document(): Document { @@ -140,13 +243,37 @@ export class File { } public get Header(): HeaderWrapper { + return this.headerWrapper[0]; + } + + public get Headers(): HeaderWrapper[] { return this.headerWrapper; } + public HeaderByRefNumber(refId: number): HeaderWrapper { + const entry = this.headerWrapper.find((h) => h.Header.ReferenceId === refId); + if (entry) { + return entry; + } + throw new Error(`There is no header with given reference id ${refId}`); + } + public get Footer(): FooterWrapper { + return this.footerWrapper[0]; + } + + public get Footers(): FooterWrapper[] { return this.footerWrapper; } + public FooterByRefNumber(refId: number): FooterWrapper { + const entry = this.footerWrapper.find((h) => h.Footer.ReferenceId === refId); + if (entry) { + return entry; + } + throw new Error(`There is no footer with given reference id ${refId}`); + } + public get ContentTypes(): ContentTypes { return this.contentTypes; } @@ -154,4 +281,8 @@ export class File { public get AppProperties(): AppProperties { return this.appProperties; } + + public get FootNotes(): FootNotes { + return this.footNotes; + } } diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts index bb6efbe25a..c1080bbd4e 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -1,6 +1,7 @@ +import { XmlComponent } from "file/xml-components"; import { Footer } from "./footer/footer"; -import { IMediaData, Media } from "./media"; -import { Paragraph } from "./paragraph"; +import { Image, Media } from "./media"; +import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; @@ -8,8 +9,8 @@ export class FooterWrapper { private readonly footer: Footer; private readonly relationships: Relationships; - constructor(private readonly media: Media) { - this.footer = new Footer(); + constructor(private readonly media: Media, referenceId: number) { + this.footer = new Footer(referenceId); this.relationships = new Relationships(); } @@ -31,8 +32,8 @@ export class FooterWrapper { return this.footer.createTable(rows, cols); } - public addDrawing(imageData: IMediaData): void { - this.footer.addDrawing(imageData); + public addChildElement(childElement: XmlComponent | string): void { + this.footer.addChildElement(childElement); } public createImage(image: string): void { @@ -42,7 +43,12 @@ export class FooterWrapper { "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", `media/${mediaData.fileName}`, ); - this.addDrawing(mediaData); + this.addImage(new Image(new ImageParagraph(mediaData))); + } + + public addImage(image: Image): FooterWrapper { + this.footer.addParagraph(image.Paragraph); + return this; } public get Footer(): Footer { diff --git a/src/file/footer/footer.ts b/src/file/footer/footer.ts index 9a64909caa..532e662c25 100644 --- a/src/file/footer/footer.ts +++ b/src/file/footer/footer.ts @@ -1,13 +1,15 @@ // http://officeopenxml.com/WPfooters.php -import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; -import { Paragraph, PictureRun } from "../paragraph"; +import { Paragraph } from "../paragraph"; import { Table } from "../table"; import { FooterAttributes } from "./footer-attributes"; export class Footer extends XmlComponent { - constructor() { + private readonly refId: number; + + constructor(referenceNumber: number) { super("w:ftr"); + this.refId = referenceNumber; this.root.push( new FooterAttributes({ wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", @@ -30,8 +32,14 @@ export class Footer extends XmlComponent { ); } - public addParagraph(paragraph: Paragraph): void { + public get ReferenceId(): number { + return this.refId; + } + + public addParagraph(paragraph: Paragraph): Footer { this.root.push(paragraph); + + return this; } public createParagraph(text?: string): Paragraph { @@ -49,12 +57,4 @@ export class Footer extends XmlComponent { this.addTable(table); return table; } - - public addDrawing(imageData: IMediaData): void { - const paragraph = new Paragraph(); - const run = new PictureRun(imageData); - paragraph.addRun(run); - - this.root.push(paragraph); - } } diff --git a/src/file/footnotes/footnote/footnote-attributes.ts b/src/file/footnotes/footnote/footnote-attributes.ts new file mode 100644 index 0000000000..b6d96f89d8 --- /dev/null +++ b/src/file/footnotes/footnote/footnote-attributes.ts @@ -0,0 +1,13 @@ +import { XmlAttributeComponent } from "file/xml-components"; + +export interface IFootnoteAttributesProperties { + type?: string; + id: number; +} + +export class FootnoteAttributes extends XmlAttributeComponent { + protected xmlKeys = { + type: "w:type", + id: "w:id", + }; +} diff --git a/src/file/footnotes/footnote/footnote.spec.ts b/src/file/footnotes/footnote/footnote.spec.ts new file mode 100644 index 0000000000..caeb8cc694 --- /dev/null +++ b/src/file/footnotes/footnote/footnote.spec.ts @@ -0,0 +1,25 @@ +import { expect } from "chai"; +import { Formatter } from "../../../export/formatter"; +import { Footnote, FootnoteType } from "./footnote"; + +describe("Footnote", () => { + describe("#constructor", () => { + it("should create a footnote with a footnote type", () => { + const footnote = new Footnote(1, FootnoteType.SEPERATOR); + const tree = new Formatter().format(footnote); + + expect(Object.keys(tree)).to.deep.equal(["w:footnote"]); + expect(tree["w:footnote"]).to.be.an.instanceof(Array); + expect(tree["w:footnote"][0]).to.deep.equal({ _attr: { "w:type": "separator", "w:id": 1 } }); + }); + + it("should create a footnote without a footnote type", () => { + const footnote = new Footnote(1); + const tree = new Formatter().format(footnote); + + expect(Object.keys(tree)).to.deep.equal(["w:footnote"]); + expect(tree["w:footnote"]).to.be.an.instanceof(Array); + expect(tree["w:footnote"][0]).to.deep.equal({ _attr: { "w:id": 1 } }); + }); + }); +}); diff --git a/src/file/footnotes/footnote/footnote.ts b/src/file/footnotes/footnote/footnote.ts new file mode 100644 index 0000000000..6760e9bb03 --- /dev/null +++ b/src/file/footnotes/footnote/footnote.ts @@ -0,0 +1,26 @@ +import { XmlComponent } from "file/xml-components"; +import { Paragraph } from "../../paragraph"; +import { FootnoteAttributes } from "./footnote-attributes"; +import { FootnoteRefRun } from "./run/footnote-ref-run"; + +export enum FootnoteType { + SEPERATOR = "separator", + CONTINUATION_SEPERATOR = "continuationSeparator", +} + +export class Footnote extends XmlComponent { + constructor(id: number, type?: FootnoteType) { + super("w:footnote"); + this.root.push( + new FootnoteAttributes({ + type: type, + id: id, + }), + ); + } + + public addParagraph(paragraph: Paragraph): void { + paragraph.addRunToFront(new FootnoteRefRun()); + this.root.push(paragraph); + } +} diff --git a/src/file/footnotes/footnote/run/continuation-seperator-run.ts b/src/file/footnotes/footnote/run/continuation-seperator-run.ts new file mode 100644 index 0000000000..4cf3ad2d21 --- /dev/null +++ b/src/file/footnotes/footnote/run/continuation-seperator-run.ts @@ -0,0 +1,10 @@ +import { Run } from "file/paragraph"; +import { ContinuationSeperator } from "./continuation-seperator"; + +export class ContinuationSeperatorRun extends Run { + constructor() { + super(); + + this.root.push(new ContinuationSeperator()); + } +} diff --git a/src/file/footnotes/footnote/run/continuation-seperator.ts b/src/file/footnotes/footnote/run/continuation-seperator.ts new file mode 100644 index 0000000000..6e9cc87c77 --- /dev/null +++ b/src/file/footnotes/footnote/run/continuation-seperator.ts @@ -0,0 +1,7 @@ +import { XmlComponent } from "file/xml-components"; + +export class ContinuationSeperator extends XmlComponent { + constructor() { + super("w:continuationSeparator"); + } +} diff --git a/src/file/footnotes/footnote/run/footnote-ref-run.ts b/src/file/footnotes/footnote/run/footnote-ref-run.ts new file mode 100644 index 0000000000..65ef0b697a --- /dev/null +++ b/src/file/footnotes/footnote/run/footnote-ref-run.ts @@ -0,0 +1,11 @@ +import { Run } from "file/paragraph"; +import { FootnoteRef } from "./footnote-ref"; + +export class FootnoteRefRun extends Run { + constructor() { + super(); + + this.style("FootnoteReference"); + this.root.push(new FootnoteRef()); + } +} diff --git a/src/file/footnotes/footnote/run/footnote-ref.ts b/src/file/footnotes/footnote/run/footnote-ref.ts new file mode 100644 index 0000000000..4a7d11fd7f --- /dev/null +++ b/src/file/footnotes/footnote/run/footnote-ref.ts @@ -0,0 +1,7 @@ +import { XmlComponent } from "file/xml-components"; + +export class FootnoteRef extends XmlComponent { + constructor() { + super("w:footnoteRef"); + } +} diff --git a/src/file/footnotes/footnote/run/reference-run.ts b/src/file/footnotes/footnote/run/reference-run.ts new file mode 100644 index 0000000000..ef40d37608 --- /dev/null +++ b/src/file/footnotes/footnote/run/reference-run.ts @@ -0,0 +1,35 @@ +import { Run } from "file/paragraph/run"; +import { Style } from "file/paragraph/run/style"; +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +export interface IFootNoteReferenceRunAttributesProperties { + id: number; +} + +export class FootNoteReferenceRunAttributes extends XmlAttributeComponent { + protected xmlKeys = { + id: "w:id", + }; +} + +export class FootnoteReference extends XmlComponent { + constructor(id: number) { + super("w:footnoteReference"); + + this.root.push( + new FootNoteReferenceRunAttributes({ + id: id, + }), + ); + } +} + +export class FootnoteReferenceRun extends Run { + constructor(id: number) { + super(); + + this.properties.push(new Style("FootnoteReference")); + + this.root.push(new FootnoteReference(id)); + } +} diff --git a/src/file/footnotes/footnote/run/seperator-run.ts b/src/file/footnotes/footnote/run/seperator-run.ts new file mode 100644 index 0000000000..17cc69d8ec --- /dev/null +++ b/src/file/footnotes/footnote/run/seperator-run.ts @@ -0,0 +1,10 @@ +import { Run } from "file/paragraph"; +import { Seperator } from "./seperator"; + +export class SeperatorRun extends Run { + constructor() { + super(); + + this.root.push(new Seperator()); + } +} diff --git a/src/file/footnotes/footnote/run/seperator.ts b/src/file/footnotes/footnote/run/seperator.ts new file mode 100644 index 0000000000..e6038e33ad --- /dev/null +++ b/src/file/footnotes/footnote/run/seperator.ts @@ -0,0 +1,7 @@ +import { XmlComponent } from "file/xml-components"; + +export class Seperator extends XmlComponent { + constructor() { + super("w:separator"); + } +} diff --git a/src/file/footnotes/footnotes-attributes.ts b/src/file/footnotes/footnotes-attributes.ts new file mode 100644 index 0000000000..f09216c9ac --- /dev/null +++ b/src/file/footnotes/footnotes-attributes.ts @@ -0,0 +1,43 @@ +import { XmlAttributeComponent } from "file/xml-components"; + +export interface IFootnotesAttributesProperties { + wpc?: string; + mc?: string; + o?: string; + r?: string; + m?: string; + v?: string; + wp14?: string; + wp?: string; + w10?: string; + w?: string; + w14?: string; + w15?: string; + wpg?: string; + wpi?: string; + wne?: string; + wps?: string; + Ignorable?: string; +} + +export class FootnotesAttributes extends XmlAttributeComponent { + protected xmlKeys = { + wpc: "xmlns:wpc", + mc: "xmlns:mc", + o: "xmlns:o", + r: "xmlns:r", + m: "xmlns:m", + v: "xmlns:v", + wp14: "xmlns:wp14", + wp: "xmlns:wp", + w10: "xmlns:w10", + w: "xmlns:w", + w14: "xmlns:w14", + w15: "xmlns:w15", + wpg: "xmlns:wpg", + wpi: "xmlns:wpi", + wne: "xmlns:wne", + wps: "xmlns:wps", + Ignorable: "mc:Ignorable", + }; +} diff --git a/src/file/footnotes/footnotes.ts b/src/file/footnotes/footnotes.ts new file mode 100644 index 0000000000..bb6ac388c5 --- /dev/null +++ b/src/file/footnotes/footnotes.ts @@ -0,0 +1,70 @@ +import { XmlComponent } from "file/xml-components"; +import { Paragraph } from "../paragraph"; +import { Footnote, FootnoteType } from "./footnote/footnote"; +import { ContinuationSeperatorRun } from "./footnote/run/continuation-seperator-run"; +import { SeperatorRun } from "./footnote/run/seperator-run"; +import { FootnotesAttributes } from "./footnotes-attributes"; + +export class FootNotes extends XmlComponent { + private counter: number; + + constructor() { + super("w:footnotes"); + + this.counter = 1; + + this.root.push( + new FootnotesAttributes({ + wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", + mc: "http://schemas.openxmlformats.org/markup-compatibility/2006", + o: "urn:schemas-microsoft-com:office:office", + r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + m: "http://schemas.openxmlformats.org/officeDocument/2006/math", + v: "urn:schemas-microsoft-com:vml", + wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing", + wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + w10: "urn:schemas-microsoft-com:office:word", + w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + w14: "http://schemas.microsoft.com/office/word/2010/wordml", + w15: "http://schemas.microsoft.com/office/word/2012/wordml", + wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup", + wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", + wne: "http://schemas.microsoft.com/office/word/2006/wordml", + wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", + Ignorable: "w14 w15 wp14", + }), + ); + + const begin = new Footnote(-1, FootnoteType.SEPERATOR); + begin.addParagraph( + new Paragraph() + .spacing({ + after: 0, + line: 240, + lineRule: "auto", + }) + .addRun(new SeperatorRun()), + ); + this.root.push(begin); + + const spacing = new Footnote(0, FootnoteType.CONTINUATION_SEPERATOR); + spacing.addParagraph( + new Paragraph() + .spacing({ + after: 0, + line: 240, + lineRule: "auto", + }) + .addRun(new ContinuationSeperatorRun()), + ); + this.root.push(spacing); + } + + public createFootNote(paragraph: Paragraph): void { + const footnote = new Footnote(this.counter); + footnote.addParagraph(paragraph); + this.root.push(footnote); + + this.counter++; + } +} diff --git a/src/file/footnotes/index.ts b/src/file/footnotes/index.ts new file mode 100644 index 0000000000..91f3a9948b --- /dev/null +++ b/src/file/footnotes/index.ts @@ -0,0 +1 @@ +export * from "./footnotes"; diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts index df15871bef..1053bc2096 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -1,6 +1,7 @@ +import { XmlComponent } from "file/xml-components"; import { Header } from "./header/header"; -import { IMediaData, Media } from "./media"; -import { Paragraph } from "./paragraph"; +import { Image, Media } from "./media"; +import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; @@ -8,8 +9,8 @@ export class HeaderWrapper { private readonly header: Header; private readonly relationships: Relationships; - constructor(private readonly media: Media) { - this.header = new Header(); + constructor(private readonly media: Media, referenceId: number) { + this.header = new Header(referenceId); this.relationships = new Relationships(); } @@ -31,8 +32,8 @@ export class HeaderWrapper { return this.header.createTable(rows, cols); } - public addDrawing(imageData: IMediaData): void { - this.header.addDrawing(imageData); + public addChildElement(childElement: XmlComponent | string): void { + this.header.addChildElement(childElement); } public createImage(image: string): void { @@ -42,7 +43,12 @@ export class HeaderWrapper { "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", `media/${mediaData.fileName}`, ); - this.addDrawing(mediaData); + this.addImage(new Image(new ImageParagraph(mediaData))); + } + + public addImage(image: Image): HeaderWrapper { + this.header.addParagraph(image.Paragraph); + return this; } public get Header(): Header { diff --git a/src/file/header/header.ts b/src/file/header/header.ts index b4dd0918ef..be1708658f 100644 --- a/src/file/header/header.ts +++ b/src/file/header/header.ts @@ -1,13 +1,16 @@ // http://officeopenxml.com/WPheaders.php -import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; -import { Paragraph, PictureRun } from "../paragraph"; +import { Paragraph } from "../paragraph"; import { Table } from "../table"; import { HeaderAttributes } from "./header-attributes"; export class Header extends XmlComponent { - constructor() { + private readonly refId: number; + + constructor(referenceNumber: number) { super("w:hdr"); + + this.refId = referenceNumber; this.root.push( new HeaderAttributes({ wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", @@ -30,6 +33,10 @@ export class Header extends XmlComponent { ); } + public get ReferenceId(): number { + return this.refId; + } + public addParagraph(paragraph: Paragraph): void { this.root.push(paragraph); } @@ -49,12 +56,4 @@ export class Header extends XmlComponent { this.addTable(table); return table; } - - public addDrawing(imageData: IMediaData): void { - const paragraph = new Paragraph(); - const run = new PictureRun(imageData); - paragraph.addRun(run); - - this.root.push(paragraph); - } } diff --git a/src/file/index.ts b/src/file/index.ts index aae389e50a..d1e04ffcf7 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -4,4 +4,6 @@ export * from "./file"; export * from "./numbering"; export * from "./media"; export * from "./drawing"; +export * from "./document"; export * from "./styles"; +export * from "./xml-components"; diff --git a/src/file/media/data.ts b/src/file/media/data.ts index b4a349a967..dfc7a664a0 100644 --- a/src/file/media/data.ts +++ b/src/file/media/data.ts @@ -1,3 +1,5 @@ +import * as fs from "fs"; + export interface IMediaDataDimensions { pixels: { x: number; @@ -11,10 +13,14 @@ export interface IMediaDataDimensions { export interface IMediaData { referenceId: number; - path: string; + stream: fs.ReadStream | Buffer; + path?: string; fileName: string; dimensions: IMediaDataDimensions; } // Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432 +/** + * @ignore + */ export const WORKAROUND2 = ""; diff --git a/src/file/media/image.ts b/src/file/media/image.ts new file mode 100644 index 0000000000..b596f77d9d --- /dev/null +++ b/src/file/media/image.ts @@ -0,0 +1,17 @@ +import { ImageParagraph, PictureRun } from "../paragraph"; + +export class Image { + constructor(private readonly paragraph: ImageParagraph) {} + + public get Paragraph(): ImageParagraph { + return this.paragraph; + } + + public get Run(): PictureRun { + return this.paragraph.Run; + } + + public scale(factorX: number, factorY?: number): void { + this.paragraph.Run.scale(factorX, factorY); + } +} diff --git a/src/file/media/index.ts b/src/file/media/index.ts index 3575274e26..2ccc436e68 100644 --- a/src/file/media/index.ts +++ b/src/file/media/index.ts @@ -1,2 +1,3 @@ export * from "./media"; export * from "./data"; +export * from "./image"; diff --git a/src/file/media/media.ts b/src/file/media/media.ts index 3f23e50b38..b0fb4019d8 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -1,9 +1,60 @@ +import * as fs from "fs"; import * as sizeOf from "image-size"; import * as path from "path"; +import { File } from "../file"; +import { ImageParagraph } from "../paragraph"; import { IMediaData } from "./data"; +import { Image } from "./image"; + +interface IHackedFile { + currentRelationshipId: number; +} export class Media { + public static addImage(file: File, filePath: string): Image { + // Workaround to expose id without exposing to API + const exposedFile = (file as {}) as IHackedFile; + const mediaData = file.Media.addMedia(filePath, exposedFile.currentRelationshipId++); + file.DocumentRelationships.createRelationship( + mediaData.referenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + return new Image(new ImageParagraph(mediaData)); + } + + public static addImageFromBuffer(file: File, buffer: Buffer, width?: number, height?: number): Image { + // Workaround to expose id without exposing to API + const exposedFile = (file as {}) as IHackedFile; + const mediaData = file.Media.addMediaFromBuffer( + `${Media.generateId()}.png`, + buffer, + exposedFile.currentRelationshipId++, + width, + height, + ); + file.DocumentRelationships.createRelationship( + mediaData.referenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + + return new Image(new ImageParagraph(mediaData)); + } + + private static generateId(): string { + // https://gist.github.com/6174/6062387 + return ( + Math.random() + .toString(36) + .substring(2, 15) + + Math.random() + .toString(36) + .substring(2, 15) + ); + } + private readonly map: Map; constructor() { @@ -20,12 +71,37 @@ export class Media { return data; } - public addMedia(filePath: string, relationshipsCount: number): IMediaData { + public addMedia(filePath: string, referenceId: number): IMediaData { const key = path.basename(filePath); const dimensions = sizeOf(filePath); + return this.createMedia(key, referenceId, dimensions, fs.createReadStream(filePath), filePath); + } + public addMediaFromBuffer(fileName: string, buffer: Buffer, referenceId: number, width?: number, height?: number): IMediaData { + const key = fileName; + let dimensions; + if (width && height) { + dimensions = { + width: width, + height: height, + }; + } else { + dimensions = sizeOf(buffer); + } + + return this.createMedia(key, referenceId, dimensions, buffer); + } + + private createMedia( + key: string, + relationshipsCount: number, + dimensions: { width: number; height: number }, + data: fs.ReadStream | Buffer, + filePath?: string, + ): IMediaData { const imageData = { referenceId: this.map.size + relationshipsCount + 1, + stream: data, path: filePath, fileName: key, dimensions: { @@ -39,12 +115,13 @@ export class Media { }, }, }; + this.map.set(key, imageData); return imageData; } - public get array(): IMediaData[] { + public get Array(): IMediaData[] { const array = new Array(); this.map.forEach((data) => { diff --git a/src/file/numbering/index.ts b/src/file/numbering/index.ts index 33832de65b..a861d336ee 100644 --- a/src/file/numbering/index.ts +++ b/src/file/numbering/index.ts @@ -1 +1,2 @@ export * from "./numbering"; +export * from "./abstract-numbering"; diff --git a/src/file/numbering/num.ts b/src/file/numbering/num.ts index cd3034d920..e4600339cf 100644 --- a/src/file/numbering/num.ts +++ b/src/file/numbering/num.ts @@ -56,7 +56,7 @@ export class LevelOverride extends XmlComponent { } } - get level(): LevelForOverride { + public get Level(): LevelForOverride { let lvl: LevelForOverride; if (!this.lvl) { lvl = new LevelForOverride(this.levelNum); diff --git a/src/file/numbering/numbering.spec.ts b/src/file/numbering/numbering.spec.ts index b695d5d1d4..05dc4a391a 100644 --- a/src/file/numbering/numbering.spec.ts +++ b/src/file/numbering/numbering.spec.ts @@ -34,7 +34,7 @@ describe("Numbering", () => { ]); // Once chai 4.0.0 lands and #644 is resolved, we can add the following to the test: // {"w:lvlText": [{"_attr": {"w:val": "•"}}]}, - // {"w:rPr": [{"w:rFonts": [{"_attr": {"w:ascii": "Symbol", "w:hAnsi": "Symbol", "w:hint": "default"}}]}]}, + // {"w:rPr": [{"w:rFonts": [{"_attr": {"w:ascii": "Symbol", "w:cs": "Symbol", "w:eastAsia": "Symbol", "w:hAnsi": "Symbol", "w:hint": "default"}}]}]}, // {"w:pPr": [{"_attr": {}}, // {"w:ind": [{"_attr": {"w:left": 720, "w:hanging": 360}}]}]}, }); @@ -297,7 +297,9 @@ describe("AbstractNumbering", () => { const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").font("Times"); const tree = new Formatter().format(level); expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:hAnsi": "Times" } }] }], + "w:rPr": [ + { "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } }] }, + ], }); }); @@ -385,9 +387,9 @@ describe("concrete numbering", () => { }); }); - it("sets the lvl element if overrideLevel.level is accessed", () => { + it("sets the lvl element if overrideLevel.Level is accessed", () => { const ol = concreteNumbering.overrideLevel(1); - expect(ol.level).to.be.instanceof(LevelForOverride); + expect(ol.Level).to.be.instanceof(LevelForOverride); const tree = new Formatter().format(concreteNumbering); expect(tree["w:num"]).to.include({ "w:lvlOverride": [ diff --git a/src/file/numbering/numbering.ts b/src/file/numbering/numbering.ts index 427fcec05a..a256500c45 100644 --- a/src/file/numbering/numbering.ts +++ b/src/file/numbering/numbering.ts @@ -1,12 +1,15 @@ -import { XmlComponent } from "file/xml-components"; +import { Indent } from "file/paragraph"; +import { IXmlableObject, XmlComponent } from "file/xml-components"; import { DocumentAttributes } from "../document/document-attributes"; -import { Indent } from "../paragraph/formatting"; import { AbstractNumbering } from "./abstract-numbering"; import { Num } from "./num"; export class Numbering extends XmlComponent { private nextId: number; + private readonly abstractNumbering: XmlComponent[] = []; + private readonly concreteNumbering: XmlComponent[] = []; + constructor() { super("w:numbering"); this.root.push( @@ -58,13 +61,19 @@ export class Numbering extends XmlComponent { public createAbstractNumbering(): AbstractNumbering { const num = new AbstractNumbering(this.nextId++); - this.root.push(num); + this.abstractNumbering.push(num); return num; } public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num { const num = new Num(this.nextId++, abstractNumbering.id); - this.root.push(num); + this.concreteNumbering.push(num); return num; } + + public prepForXml(): IXmlableObject { + this.abstractNumbering.forEach((x) => this.root.push(x)); + this.concreteNumbering.forEach((x) => this.root.push(x)); + return super.prepForXml(); + } } diff --git a/src/file/paragraph/formatting/alignment.ts b/src/file/paragraph/formatting/alignment.ts index fbf0fdcdae..b1fa0ca2af 100644 --- a/src/file/paragraph/formatting/alignment.ts +++ b/src/file/paragraph/formatting/alignment.ts @@ -1,7 +1,7 @@ // http://officeopenxml.com/WPalignment.php import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; -export type AlignmentOptions = "left" | "center" | "right" | "both"; +export type AlignmentOptions = "start" | "end" | "center" | "both" | "distribute" | "left" | "right"; export class AlignmentAttributes extends XmlAttributeComponent<{ val: AlignmentOptions }> { protected xmlKeys = { val: "w:val" }; diff --git a/src/file/paragraph/formatting/bidirectional.ts b/src/file/paragraph/formatting/bidirectional.ts new file mode 100644 index 0000000000..4083247e78 --- /dev/null +++ b/src/file/paragraph/formatting/bidirectional.ts @@ -0,0 +1,7 @@ +import { XmlComponent } from "file/xml-components"; + +export class Bidirectional extends XmlComponent { + constructor() { + super("w:bidi"); + } +} diff --git a/src/file/paragraph/formatting/page-break.spec.ts b/src/file/paragraph/formatting/page-break.spec.ts index 675ac5d112..6ba877311f 100644 --- a/src/file/paragraph/formatting/page-break.spec.ts +++ b/src/file/paragraph/formatting/page-break.spec.ts @@ -1,7 +1,7 @@ import { assert } from "chai"; import { Utility } from "../../../tests/utility"; -import { PageBreak } from "./page-break"; +import { PageBreak, PageBreakBefore } from "./page-break"; describe("PageBreak", () => { let pageBreak: PageBreak; @@ -30,3 +30,11 @@ describe("PageBreak", () => { }); }); }); + +describe("PageBreakBefore", () => { + it("should create page break before", () => { + const pageBreakBefore = new PageBreakBefore(); + const newJson = Utility.jsonify(pageBreakBefore); + assert.equal(newJson.rootKey, "w:pageBreakBefore"); + }); +}); diff --git a/src/file/paragraph/formatting/page-break.ts b/src/file/paragraph/formatting/page-break.ts index 1b4e6642b5..13556d12e2 100644 --- a/src/file/paragraph/formatting/page-break.ts +++ b/src/file/paragraph/formatting/page-break.ts @@ -19,3 +19,12 @@ export class PageBreak extends Run { this.root.push(new Break()); } } + +/** + * Add page break before the paragraph if there is no one added before. + */ +export class PageBreakBefore extends XmlComponent { + constructor() { + super("w:pageBreakBefore"); + } +} diff --git a/src/file/paragraph/formatting/spacing.ts b/src/file/paragraph/formatting/spacing.ts index fceab8af8c..292864bf53 100644 --- a/src/file/paragraph/formatting/spacing.ts +++ b/src/file/paragraph/formatting/spacing.ts @@ -5,6 +5,7 @@ export interface ISpacingProperties { after?: number; before?: number; line?: number; + lineRule?: string; } class SpacingAttributes extends XmlAttributeComponent { @@ -12,6 +13,7 @@ class SpacingAttributes extends XmlAttributeComponent { after: "w:after", before: "w:before", line: "w:line", + lineRule: "w:lineRule", }; } diff --git a/src/file/paragraph/image.spec.ts b/src/file/paragraph/image.spec.ts new file mode 100644 index 0000000000..45c06b0628 --- /dev/null +++ b/src/file/paragraph/image.spec.ts @@ -0,0 +1,251 @@ +// tslint:disable:object-literal-key-quotes +import { assert, expect } from "chai"; + +import { Formatter } from "../../export/formatter"; +import { ImageParagraph } from "./image"; + +describe("Image", () => { + let image: ImageParagraph; + + beforeEach(() => { + image = new ImageParagraph({ + referenceId: 0, + stream: new Buffer(""), + path: "", + fileName: "", + dimensions: { + pixels: { + x: 10, + y: 10, + }, + emus: { + x: 10, + y: 10, + }, + }, + }); + }); + + describe("#constructor()", () => { + it("should create valid JSON", () => { + const stringifiedJson = JSON.stringify(image); + + try { + JSON.parse(stringifiedJson); + } catch (e) { + assert.isTrue(false); + } + assert.isTrue(true); + }); + }); + + describe("#scale()", () => { + it("should set the scale of the object properly", () => { + image.scale(2); + const tree = new Formatter().format(image); + expect(tree).to.deep.equal({ + "w:p": [ + { + "w:pPr": [], + }, + { + "w:r": [ + { + "w:rPr": [], + }, + { + "w:drawing": [ + { + "wp:inline": [ + { + _attr: { + distB: 0, + distL: 0, + distR: 0, + distT: 0, + }, + }, + { + "wp:extent": [ + { + _attr: { + cx: 20, + cy: 20, + }, + }, + ], + }, + { + "wp:effectExtent": [ + { + _attr: { + b: 0, + l: 0, + r: 0, + t: 0, + }, + }, + ], + }, + { + "wp:docPr": [ + { + _attr: { + descr: "", + id: 0, + name: "", + }, + }, + ], + }, + { + "wp:cNvGraphicFramePr": [ + { + "a:graphicFrameLocks": [ + { + _attr: { + noChangeAspect: 1, + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + ], + }, + ], + }, + { + "a:graphic": [ + { + _attr: { + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + { + "a:graphicData": [ + { + _attr: { + uri: "http://schemas.openxmlformats.org/drawingml/2006/picture", + }, + }, + { + "pic:pic": [ + { + _attr: { + "xmlns:pic": + "http://schemas.openxmlformats.org/drawingml/2006/picture", + }, + }, + { + "pic:nvPicPr": [ + { + "pic:cNvPr": [ + { + _attr: { + desc: "", + id: 0, + name: "", + }, + }, + ], + }, + { + "pic:cNvPicPr": [ + { + "a:picLocks": [ + { + _attr: { + noChangeArrowheads: 1, + noChangeAspect: 1, + }, + }, + ], + }, + ], + }, + ], + }, + { + "pic:blipFill": [ + { + "a:blip": [ + { + _attr: { + cstate: "none", + "r:embed": "rId0", + }, + }, + ], + }, + { + "a:srcRect": [], + }, + { + "a:stretch": [ + { + "a:fillRect": [], + }, + ], + }, + ], + }, + { + "pic:spPr": [ + { + _attr: { + bwMode: "auto", + }, + }, + { + "a:xfrm": [ + { + "a:ext": [ + { + _attr: { + cx: 10, + cy: 10, + }, + }, + ], + }, + { + "a:off": [ + { + _attr: { + x: 0, + y: 0, + }, + }, + ], + }, + ], + }, + { + "a:prstGeom": [ + { + _attr: { + prst: "rect", + }, + }, + { + "a:avLst": [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }); + }); + }); +}); diff --git a/src/file/paragraph/image.ts b/src/file/paragraph/image.ts new file mode 100644 index 0000000000..634f76e517 --- /dev/null +++ b/src/file/paragraph/image.ts @@ -0,0 +1,22 @@ +import { IDrawingOptions } from "../drawing"; +import { IMediaData } from "../media"; +import { Paragraph } from "./paragraph"; +import { PictureRun } from "./run"; + +export class ImageParagraph extends Paragraph { + private readonly pictureRun: PictureRun; + + constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { + super(); + this.pictureRun = new PictureRun(imageData, drawingOptions); + this.root.push(this.pictureRun); + } + + public scale(factorX: number, factorY?: number): void { + this.pictureRun.scale(factorX, factorY); + } + + public get Run(): PictureRun { + return this.pictureRun; + } +} diff --git a/src/file/paragraph/index.ts b/src/file/paragraph/index.ts index 08d4e487cf..222cb1bf4f 100644 --- a/src/file/paragraph/index.ts +++ b/src/file/paragraph/index.ts @@ -2,3 +2,5 @@ export * from "./formatting"; export * from "./paragraph"; export * from "./properties"; export * from "./run"; +export * from "./links"; +export * from "./image"; diff --git a/src/file/paragraph/links/bookmark-attributes.ts b/src/file/paragraph/links/bookmark-attributes.ts new file mode 100644 index 0000000000..670c462306 --- /dev/null +++ b/src/file/paragraph/links/bookmark-attributes.ts @@ -0,0 +1,23 @@ +import { XmlAttributeComponent } from "file/xml-components"; + +export interface IBookmarkStartAttributesProperties { + id: string; + name: string; +} + +export class BookmarkStartAttributes extends XmlAttributeComponent { + protected xmlKeys = { + id: "w:id", + name: "w:name", + }; +} + +export interface IBookmarkEndAttributesProperties { + id: string; +} + +export class BookmarkEndAttributes extends XmlAttributeComponent { + protected xmlKeys = { + id: "w:id", + }; +} diff --git a/src/file/paragraph/links/bookmark.spec.ts b/src/file/paragraph/links/bookmark.spec.ts new file mode 100644 index 0000000000..08d0aec91c --- /dev/null +++ b/src/file/paragraph/links/bookmark.spec.ts @@ -0,0 +1,42 @@ +import { assert } from "chai"; + +import { Utility } from "../../../tests/utility"; +import { Bookmark } from "./"; + +describe("Bookmark", () => { + let bookmark: Bookmark; + + beforeEach(() => { + bookmark = new Bookmark("anchor", "Internal Link", 0); + }); + + it("should create a bookmark with three root elements", () => { + const newJson = Utility.jsonify(bookmark); + assert.equal(newJson.rootKey, undefined); + assert.equal(newJson.start.rootKey, "w:bookmarkStart"); + assert.equal(newJson.text.rootKey, "w:r"); + assert.equal(newJson.end.rootKey, "w:bookmarkEnd"); + }); + + it("should create a bookmark with the correct attributes on the bookmark start element", () => { + const newJson = Utility.jsonify(bookmark); + const attributes = { + name: "anchor", + id: "1", + }; + assert.equal(JSON.stringify(newJson.start.root[0].root), JSON.stringify(attributes)); + }); + + it("should create a bookmark with the correct attributes on the text element", () => { + const newJson = Utility.jsonify(bookmark); + assert.equal(JSON.stringify(newJson.text.root[1].root[1]), JSON.stringify("Internal Link")); + }); + + it("should create a bookmark with the correct attributes on the bookmark end element", () => { + const newJson = Utility.jsonify(bookmark); + const attributes = { + id: "1", + }; + assert.equal(JSON.stringify(newJson.end.root[0].root), JSON.stringify(attributes)); + }); +}); diff --git a/src/file/paragraph/links/bookmark.ts b/src/file/paragraph/links/bookmark.ts new file mode 100644 index 0000000000..de4fabdaa2 --- /dev/null +++ b/src/file/paragraph/links/bookmark.ts @@ -0,0 +1,54 @@ +// http://officeopenxml.com/WPbookmark.php + +import { XmlComponent } from "file/xml-components"; +import { TextRun } from "../run"; +import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes"; + +export class Bookmark { + public linkId: number; + + public readonly start: BookmarkStart; + + public readonly text: TextRun; + + public readonly end: BookmarkEnd; + + constructor(name: string, text: string, relationshipsCount: number) { + this.linkId = relationshipsCount + 1; + + this.start = new BookmarkStart(name, this.linkId); + this.text = new TextRun(text); + this.end = new BookmarkEnd(this.linkId); + } +} + +export class BookmarkStart extends XmlComponent { + public linkId: number; + + constructor(name: string, relationshipsCount: number) { + super("w:bookmarkStart"); + + this.linkId = relationshipsCount; + const id = `${this.linkId}`; + const attributes = new BookmarkStartAttributes({ + name, + id, + }); + this.root.push(attributes); + } +} + +export class BookmarkEnd extends XmlComponent { + public linkId: number; + + constructor(relationshipsCount: number) { + super("w:bookmarkEnd"); + + this.linkId = relationshipsCount; + const id = `${this.linkId}`; + const attributes = new BookmarkEndAttributes({ + id, + }); + this.root.push(attributes); + } +} diff --git a/src/file/paragraph/links/hyperlink-attributes.ts b/src/file/paragraph/links/hyperlink-attributes.ts new file mode 100644 index 0000000000..6d68e265b4 --- /dev/null +++ b/src/file/paragraph/links/hyperlink-attributes.ts @@ -0,0 +1,15 @@ +import { XmlAttributeComponent } from "file/xml-components"; + +export interface IHyperlinkAttributesProperties { + id?: string; + anchor?: string; + history: number; +} + +export class HyperlinkAttributes extends XmlAttributeComponent { + protected xmlKeys = { + id: "r:id", + history: "w:history", + anchor: "w:anchor", + }; +} diff --git a/src/file/paragraph/links/hyperlink.spec.ts b/src/file/paragraph/links/hyperlink.spec.ts new file mode 100644 index 0000000000..d6432b432a --- /dev/null +++ b/src/file/paragraph/links/hyperlink.spec.ts @@ -0,0 +1,55 @@ +import { assert, expect } from "chai"; + +import { Formatter } from "../../../export/formatter"; +import { Utility } from "../../../tests/utility"; +import { Hyperlink } from "./"; + +describe("Hyperlink", () => { + let hyperlink: Hyperlink; + + beforeEach(() => { + hyperlink = new Hyperlink("https://example.com", 0); + }); + + describe("#constructor()", () => { + it("should create a hyperlink with correct root key", () => { + const newJson = Utility.jsonify(hyperlink); + assert.equal(newJson.rootKey, "w:hyperlink"); + }); + + it("should create a hyperlink with right attributes", () => { + const newJson = Utility.jsonify(hyperlink); + const attributes = { + history: 1, + id: "rId1", + }; + assert.equal(JSON.stringify(newJson.root[0].root), JSON.stringify(attributes)); + }); + + it("should create a hyperlink with a run component", () => { + const tree = new Formatter().format(hyperlink); + const runJson = { + "w:r": [ + { "w:rPr": [{ "w:rStyle": [{ _attr: { "w:val": "Hyperlink" } }] }] }, + { "w:t": [{ _attr: { "xml:space": "preserve" } }, "https://example.com"] }, + ], + }; + expect(tree["w:hyperlink"][1]).to.deep.equal(runJson); + }); + + describe("with optional anchor parameter", () => { + beforeEach(() => { + hyperlink = new Hyperlink("Anchor Text", 0, "anchor"); + }); + + it("should create an internal link with anchor tag", () => { + const newJson = Utility.jsonify(hyperlink); + const attributes = { + history: 1, + anchor: "anchor", + }; + assert.equal(JSON.stringify(newJson.root[0].root), JSON.stringify(attributes)); + }); + }); + }); +}); diff --git a/src/file/paragraph/links/hyperlink.ts b/src/file/paragraph/links/hyperlink.ts new file mode 100644 index 0000000000..b6fe1b221b --- /dev/null +++ b/src/file/paragraph/links/hyperlink.ts @@ -0,0 +1,29 @@ +// http://officeopenxml.com/WPhyperlink.php + +import { XmlComponent } from "file/xml-components"; +import { TextRun } from "../run"; +import { HyperlinkAttributes, IHyperlinkAttributesProperties } from "./hyperlink-attributes"; + +export class Hyperlink extends XmlComponent { + public linkId: number; + + constructor(text: string, relationshipsCount: number, anchor?: string) { + super("w:hyperlink"); + + this.linkId = relationshipsCount + 1; + + const props: IHyperlinkAttributesProperties = { + history: 1, + }; + + if (anchor) { + props.anchor = anchor; + } else { + props.id = `rId${this.linkId}`; + } + + const attributes = new HyperlinkAttributes(props); + this.root.push(attributes); + this.root.push(new TextRun(text).style("Hyperlink")); + } +} diff --git a/src/file/paragraph/links/index.ts b/src/file/paragraph/links/index.ts new file mode 100644 index 0000000000..09084aa8c7 --- /dev/null +++ b/src/file/paragraph/links/index.ts @@ -0,0 +1,2 @@ +export * from "./hyperlink"; +export * from "./bookmark"; diff --git a/src/file/paragraph/paragraph.spec.ts b/src/file/paragraph/paragraph.spec.ts index bd36211295..e00fa6167c 100644 --- a/src/file/paragraph/paragraph.spec.ts +++ b/src/file/paragraph/paragraph.spec.ts @@ -161,6 +161,24 @@ describe("Paragraph", () => { }); }); + describe("#pageBreakBefore()", () => { + it("should add page break before to JSON", () => { + paragraph.pageBreakBefore(); + const tree = new Formatter().format(paragraph); + expect(tree).to.deep.equal({ + "w:p": [ + { + "w:pPr": [ + { + "w:pageBreakBefore": [], + }, + ], + }, + ], + }); + }); + }); + describe("#bullet()", () => { it("should default to 0 indent level if no bullet was specified", () => { paragraph.bullet(); @@ -320,4 +338,14 @@ describe("Paragraph", () => { }); }); }); + + describe("#bidirectional", () => { + it("set paragraph right to left layout", () => { + paragraph.bidirectional(); + const tree = new Formatter().format(paragraph); + expect(tree).to.deep.equal({ + "w:p": [{ "w:pPr": [{ "w:bidi": [] }] }], + }); + }); + }); }); diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index 2c5e007a9b..b21e2c3ac0 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -1,22 +1,25 @@ // http://officeopenxml.com/WPparagraph.php -import { IMediaData } from "file/media"; +import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run"; +import { Image } from "file/media"; import { Num } from "file/numbering/num"; import { XmlComponent } from "file/xml-components"; -import { PictureRun, Run, TextRun } from "./run"; import { Alignment } from "./formatting/alignment"; +import { Bidirectional } from "./formatting/bidirectional"; import { ThematicBreak } from "./formatting/border"; import { Indent } from "./formatting/indent"; import { KeepLines, KeepNext } from "./formatting/keep"; -import { PageBreak } from "./formatting/page-break"; +import { PageBreak, PageBreakBefore } from "./formatting/page-break"; import { ISpacingProperties, Spacing } from "./formatting/spacing"; import { Style } from "./formatting/style"; import { CenterTabStop, LeftTabStop, MaxRightTabStop, RightTabStop } from "./formatting/tab-stop"; import { NumberProperties } from "./formatting/unordered-list"; +import { Bookmark, Hyperlink } from "./links"; import { ParagraphProperties } from "./properties"; +import { PictureRun, Run, TextRun } from "./run"; export class Paragraph extends XmlComponent { - private properties: ParagraphProperties; + private readonly properties: ParagraphProperties; constructor(text?: string) { super("w:p"); @@ -32,15 +35,29 @@ export class Paragraph extends XmlComponent { return this; } + public addHyperLink(hyperlink: Hyperlink): Paragraph { + this.root.push(hyperlink); + return this; + } + + public addBookmark(bookmark: Bookmark): Paragraph { + // Bookmarks by spec have three components, a start, text, and end + this.root.push(bookmark.start); + this.root.push(bookmark.text); + this.root.push(bookmark.end); + return this; + } + public createTextRun(text: string): TextRun { const run = new TextRun(text); this.addRun(run); return run; } - public createPictureRun(imageData: IMediaData): PictureRun { - const run = new PictureRun(imageData); + public addImage(image: Image): PictureRun { + const run = image.Run; this.addRun(run); + return run; } @@ -94,6 +111,21 @@ export class Paragraph extends XmlComponent { return this; } + public start(): Paragraph { + this.properties.push(new Alignment("start")); + return this; + } + + public end(): Paragraph { + this.properties.push(new Alignment("end")); + return this; + } + + public distribute(): Paragraph { + this.properties.push(new Alignment("distribute")); + return this; + } + public justified(): Paragraph { this.properties.push(new Alignment("both")); return this; @@ -109,6 +141,11 @@ export class Paragraph extends XmlComponent { return this; } + public pageBreakBefore(): Paragraph { + this.properties.push(new PageBreakBefore()); + return this; + } + public maxRightTabStop(): Paragraph { this.properties.push(new MaxRightTabStop()); return this; @@ -141,6 +178,11 @@ export class Paragraph extends XmlComponent { return this; } + public setCustomNumbering(numberId: number, indentLevel: number): Paragraph { + this.properties.push(new NumberProperties(numberId, indentLevel)); + return this; + } + public style(styleId: string): Paragraph { this.properties.push(new Style(styleId)); return this; @@ -165,4 +207,19 @@ export class Paragraph extends XmlComponent { this.properties.push(new KeepLines()); return this; } + + public referenceFootnote(id: number): Paragraph { + this.root.push(new FootnoteReferenceRun(id)); + return this; + } + + public addRunToFront(run: Run): Paragraph { + this.root.splice(1, 0, run); + return this; + } + + public bidirectional(): Paragraph { + this.properties.push(new Bidirectional()); + return this; + } } diff --git a/src/file/paragraph/run/formatting.ts b/src/file/paragraph/run/formatting.ts index 16fb744e5d..8b1be933ec 100644 --- a/src/file/paragraph/run/formatting.ts +++ b/src/file/paragraph/run/formatting.ts @@ -14,6 +14,17 @@ export class Bold extends XmlComponent { } } +export class BoldComplexScript extends XmlComponent { + constructor() { + super("w:bCs"); + this.root.push( + new Attributes({ + val: true, + }), + ); + } +} + export class Italics extends XmlComponent { constructor() { super("w:i"); @@ -25,6 +36,17 @@ export class Italics extends XmlComponent { } } +export class ItalicsComplexScript extends XmlComponent { + constructor() { + super("w:iCs"); + this.root.push( + new Attributes({ + val: true, + }), + ); + } +} + export class Caps extends XmlComponent { constructor() { super("w:caps"); @@ -123,3 +145,25 @@ export class Size extends XmlComponent { ); } } + +export class SizeComplexScript extends XmlComponent { + constructor(size: number) { + super("w:szCs"); + this.root.push( + new Attributes({ + val: size, + }), + ); + } +} + +export class RightToLeft extends XmlComponent { + constructor() { + super("w:rtl"); + this.root.push( + new Attributes({ + val: true, + }), + ); + } +} diff --git a/src/file/paragraph/run/page-number.ts b/src/file/paragraph/run/page-number.ts new file mode 100644 index 0000000000..4048c4f79a --- /dev/null +++ b/src/file/paragraph/run/page-number.ts @@ -0,0 +1,38 @@ +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +class FidCharAttrs extends XmlAttributeComponent<{ type: "begin" | "end" | "separate" }> { + protected xmlKeys = { type: "w:fldCharType" }; +} + +class TextAttributes extends XmlAttributeComponent<{ space: "default" | "preserve" }> { + protected xmlKeys = { space: "xml:space" }; +} + +export class Begin extends XmlComponent { + constructor() { + super("w:fldChar"); + this.root.push(new FidCharAttrs({ type: "begin" })); + } +} + +export class Page extends XmlComponent { + constructor() { + super("w:instrText"); + this.root.push(new TextAttributes({ space: "preserve" })); + this.root.push("PAGE"); + } +} + +export class Separate extends XmlComponent { + constructor() { + super("w:fldChar"); + this.root.push(new FidCharAttrs({ type: "separate" })); + } +} + +export class End extends XmlComponent { + constructor() { + super("w:fldChar"); + this.root.push(new FidCharAttrs({ type: "end" })); + } +} diff --git a/src/file/paragraph/run/picture-run.ts b/src/file/paragraph/run/picture-run.ts index 1b256ea20d..d51d0fc8f9 100644 --- a/src/file/paragraph/run/picture-run.ts +++ b/src/file/paragraph/run/picture-run.ts @@ -1,18 +1,19 @@ import { Drawing } from "../../drawing"; +import { IDrawingOptions } from "../../drawing/drawing"; import { IMediaData } from "../../media/data"; import { Run } from "../run"; export class PictureRun extends Run { - private drawing: Drawing; + private readonly drawing: Drawing; - constructor(imageData: IMediaData) { + constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { super(); if (imageData === undefined) { throw new Error("imageData cannot be undefined"); } - this.drawing = new Drawing(imageData); + this.drawing = new Drawing(imageData, drawingOptions); this.root.push(this.drawing); } diff --git a/src/file/paragraph/run/run-fonts.spec.ts b/src/file/paragraph/run/run-fonts.spec.ts index 54a84f2c9e..366c0e16d5 100644 --- a/src/file/paragraph/run/run-fonts.spec.ts +++ b/src/file/paragraph/run/run-fonts.spec.ts @@ -8,14 +8,16 @@ describe("RunFonts", () => { it("uses the font name for both ascii and hAnsi", () => { const tree = new Formatter().format(new RunFonts("Times")); expect(tree).to.deep.equal({ - "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:hAnsi": "Times" } }], + "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } }], }); }); it("uses hint if given", () => { const tree = new Formatter().format(new RunFonts("Times", "default")); expect(tree).to.deep.equal({ - "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:hAnsi": "Times", "w:hint": "default" } }], + "w:rFonts": [ + { _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times", "w:hint": "default" } }, + ], }); }); }); diff --git a/src/file/paragraph/run/run-fonts.ts b/src/file/paragraph/run/run-fonts.ts index 0b6b8f9806..bf667f6b19 100644 --- a/src/file/paragraph/run/run-fonts.ts +++ b/src/file/paragraph/run/run-fonts.ts @@ -2,6 +2,8 @@ import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; interface IRunFontAttributesProperties { ascii: string; + cs: string; + eastAsia: string; hAnsi: string; hint?: string; } @@ -9,6 +11,8 @@ interface IRunFontAttributesProperties { class RunFontAttributes extends XmlAttributeComponent { protected xmlKeys = { ascii: "w:ascii", + cs: "w:cs", + eastAsia: "w:eastAsia", hAnsi: "w:hAnsi", hint: "w:hint", }; @@ -20,6 +24,8 @@ export class RunFonts extends XmlComponent { this.root.push( new RunFontAttributes({ ascii: ascii, + cs: ascii, + eastAsia: ascii, hAnsi: ascii, hint: hint, }), diff --git a/src/file/paragraph/run/run.spec.ts b/src/file/paragraph/run/run.spec.ts index 62a5fa425f..cdd925d9d5 100644 --- a/src/file/paragraph/run/run.spec.ts +++ b/src/file/paragraph/run/run.spec.ts @@ -16,6 +16,7 @@ describe("Run", () => { run.bold(); const newJson = Utility.jsonify(run); assert.equal(newJson.root[0].root[0].rootKey, "w:b"); + assert.equal(newJson.root[0].root[1].rootKey, "w:bCs"); }); }); @@ -24,6 +25,7 @@ describe("Run", () => { run.italic(); const newJson = Utility.jsonify(run); assert.equal(newJson.root[0].root[0].rootKey, "w:i"); + assert.equal(newJson.root[0].root[1].rootKey, "w:iCs"); }); }); @@ -108,7 +110,13 @@ describe("Run", () => { run.font("Times"); const tree = new Formatter().format(run); expect(tree).to.deep.equal({ - "w:r": [{ "w:rPr": [{ "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:hAnsi": "Times" } }] }] }], + "w:r": [ + { + "w:rPr": [ + { "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } }] }, + ], + }, + ], }); }); }); @@ -128,7 +136,21 @@ describe("Run", () => { run.size(24); const tree = new Formatter().format(run); expect(tree).to.deep.equal({ - "w:r": [{ "w:rPr": [{ "w:sz": [{ _attr: { "w:val": 24 } }] }] }], + "w:r": [ + { + "w:rPr": [{ "w:sz": [{ _attr: { "w:val": 24 } }] }, { "w:szCs": [{ _attr: { "w:val": 24 } }] }], + }, + ], + }); + }); + }); + + describe("#rtl", () => { + it("should set the run to the RTL mode", () => { + run.rightToLeft(); + const tree = new Formatter().format(run); + expect(tree).to.deep.equal({ + "w:r": [{ "w:rPr": [{ "w:rtl": [{ _attr: { "w:val": true } }] }] }], }); }); }); diff --git a/src/file/paragraph/run/run.ts b/src/file/paragraph/run/run.ts index 243cea68d3..70b344842f 100644 --- a/src/file/paragraph/run/run.ts +++ b/src/file/paragraph/run/run.ts @@ -1,7 +1,19 @@ // http://officeopenxml.com/WPtext.php import { Break } from "./break"; import { Caps, SmallCaps } from "./caps"; -import { Bold, Color, DoubleStrike, Italics, Size, Strike } from "./formatting"; +import { + Bold, + BoldComplexScript, + Color, + DoubleStrike, + Italics, + ItalicsComplexScript, + RightToLeft, + Size, + SizeComplexScript, + Strike, +} from "./formatting"; +import { Begin, End, Page, Separate } from "./page-number"; import { RunProperties } from "./properties"; import { RunFonts } from "./run-fonts"; import { SubScript, SuperScript } from "./script"; @@ -12,7 +24,7 @@ import { Underline } from "./underline"; import { XmlComponent } from "file/xml-components"; export class Run extends XmlComponent { - private properties: RunProperties; + protected properties: RunProperties; constructor() { super("w:r"); @@ -22,11 +34,13 @@ export class Run extends XmlComponent { public bold(): Run { this.properties.push(new Bold()); + this.properties.push(new BoldComplexScript()); return this; } public italic(): Run { this.properties.push(new Italics()); + this.properties.push(new ItalicsComplexScript()); return this; } @@ -42,6 +56,12 @@ export class Run extends XmlComponent { public size(size: number): Run { this.properties.push(new Size(size)); + this.properties.push(new SizeComplexScript(size)); + return this; + } + + public rightToLeft(): Run { + this.properties.push(new RightToLeft()); return this; } @@ -55,6 +75,14 @@ export class Run extends XmlComponent { return this; } + public pageNumber(): Run { + this.root.push(new Begin()); + this.root.push(new Page()); + this.root.push(new Separate()); + this.root.push(new End()); + return this; + } + public smallCaps(): Run { this.properties.push(new SmallCaps()); return this; @@ -85,8 +113,8 @@ export class Run extends XmlComponent { return this; } - public font(fontName: string): Run { - this.properties.push(new RunFonts(fontName)); + public font(fontName: string, hint?: string | undefined): Run { + this.properties.push(new RunFonts(fontName, hint)); return this; } diff --git a/src/file/relationships/relationship/relationship-attributes.ts b/src/file/relationships/relationship/relationship-attributes.ts index 1b23e26977..1cb3e116c0 100644 --- a/src/file/relationships/relationship/relationship-attributes.ts +++ b/src/file/relationships/relationship/relationship-attributes.ts @@ -4,6 +4,7 @@ export interface IRelationshipAttributesProperties { id: string; type: string; target: string; + targetMode?: string; } export class RelationshipAttributes extends XmlAttributeComponent { @@ -11,5 +12,6 @@ export class RelationshipAttributes extends XmlAttributeComponent { + let externalStyles; + + beforeEach(() => { + externalStyles = ` + + + + + + + + + + + + + + + + + + + + + + + + + + `; + }); + + describe("#parse", () => { + it("should parse w:styles attributes", () => { + const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any; + + expect(importedStyle.rootKey).to.equal("w:styles"); + expect(importedStyle.root[0]._attr).to.eql({ + "xmlns:mc": "first", + "xmlns:r": "second", + }); + }); + + it("should parse other child elements of w:styles", () => { + const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any; + + expect(importedStyle.root.length).to.equal(5); + expect(importedStyle.root[1]).to.eql({ + deleted: false, + root: [], + rootKey: "w:docDefaults", + }); + expect(importedStyle.root[2]).to.eql({ + _attr: { + "w:defLockedState": "1", + "w:defUIPriority": "99", + }, + deleted: false, + root: [], + rootKey: "w:latentStyles", + }); + }); + + it("should parse styles elements", () => { + const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any; + + expect(importedStyle.root.length).to.equal(5); + expect(importedStyle.root[3]).to.eql({ + _attr: { + "w:default": "1", + "w:styleId": "Normal", + "w:type": "paragraph", + }, + deleted: false, + root: [ + { + _attr: { + "w:val": "Normal", + }, + deleted: false, + root: [], + rootKey: "w:name", + }, + { + deleted: false, + root: [], + rootKey: "w:qFormat", + }, + ], + rootKey: "w:style", + }); + + expect(importedStyle.root[4]).to.eql({ + _attr: { + "w:styleId": "Heading1", + "w:type": "paragraph", + }, + deleted: false, + root: [ + { + _attr: { + "w:val": "heading 1", + }, + deleted: false, + root: [], + rootKey: "w:name", + }, + { + _attr: { + "w:val": "Normal", + }, + deleted: false, + root: [], + rootKey: "w:basedOn", + }, + { + deleted: false, + root: [ + { + deleted: false, + root: [], + rootKey: "w:keepNext", + }, + { + deleted: false, + root: [], + rootKey: "w:keepLines", + }, + { + deleted: false, + root: [ + { + _attr: { + "w:color": "auto", + "w:space": "1", + "w:sz": "4", + "w:val": "single", + }, + deleted: false, + root: [], + rootKey: "w:bottom", + }, + ], + rootKey: "w:pBdr", + }, + ], + rootKey: "w:pPr", + }, + ], + rootKey: "w:style", + }); + }); + }); +}); diff --git a/src/file/styles/external-styles-factory.ts b/src/file/styles/external-styles-factory.ts new file mode 100644 index 0000000000..2ebd6323bd --- /dev/null +++ b/src/file/styles/external-styles-factory.ts @@ -0,0 +1,44 @@ +import * as fastXmlParser from "fast-xml-parser"; +import { convertToXmlComponent, ImportedRootElementAttributes, ImportedXmlComponent, parseOptions } from "file/xml-components"; +import { Styles } from "./"; + +export class ExternalStylesFactory { + /** + * Creates new Style based on the given styles. + * Parses the styles and convert them to XmlComponent. + * Example content from styles.xml: + * + * + * + * + * + * ..... + * + * + * + * + * ..... + * + * + * Or any other element will be parsed to + * + * + * @param externalStyles context from styles.xml + */ + public newInstance(externalStyles: string): Styles { + const xmlStyles = fastXmlParser.parse(externalStyles, parseOptions)["w:styles"]; + // create styles with attributes from the parsed xml + const importedStyle = new Styles(new ImportedRootElementAttributes(xmlStyles._attr)); + + // convert other elements (not styles definitions, but default styles and so on ...) + Object.keys(xmlStyles) + .filter((element) => element !== "_attr" && element !== "w:style") + .forEach((element) => { + importedStyle.push(new ImportedXmlComponent(element, xmlStyles[element]._attr)); + }); + + // convert the styles one by one + xmlStyles["w:style"].map((style) => convertToXmlComponent("w:style", style)).forEach(importedStyle.push.bind(importedStyle)); + return importedStyle; + } +} diff --git a/src/file/styles/factory.ts b/src/file/styles/factory.ts index 0b5faf048d..800d2ca9ce 100644 --- a/src/file/styles/factory.ts +++ b/src/file/styles/factory.ts @@ -1,21 +1,33 @@ +import { DocumentAttributes } from "../document/document-attributes"; import { Color, Italics, Size } from "../paragraph/run/formatting"; - import { Styles } from "./"; -// import { DocumentDefaults } from "./defaults"; + import { + FootnoteReferenceStyle, + FootnoteText, + FootnoteTextChar, Heading1Style, Heading2Style, Heading3Style, Heading4Style, Heading5Style, Heading6Style, + HyperlinkStyle, ListParagraph, TitleStyle, } from "./style"; export class DefaultStylesFactory { public newInstance(): Styles { - const styles = new Styles(); + const documentAttributes = new DocumentAttributes({ + mc: "http://schemas.openxmlformats.org/markup-compatibility/2006", + r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + w14: "http://schemas.microsoft.com/office/word/2010/wordml", + w15: "http://schemas.microsoft.com/office/word/2012/wordml", + Ignorable: "w14 w15", + }); + const styles = new Styles(documentAttributes); styles.createDocumentDefaults(); const titleStyle = new TitleStyle(); @@ -54,6 +66,18 @@ export class DefaultStylesFactory { // listParagraph.addParagraphProperty(); styles.push(listParagraph); + const hyperLinkStyle = new HyperlinkStyle(); + styles.push(hyperLinkStyle); + + const footnoteReferenceStyle = new FootnoteReferenceStyle(); + styles.push(footnoteReferenceStyle); + + const footnoteTextStyle = new FootnoteText(); + styles.push(footnoteTextStyle); + + const footnoteTextCharStyle = new FootnoteTextChar(); + styles.push(footnoteTextCharStyle); + return styles; } } diff --git a/src/file/styles/index.ts b/src/file/styles/index.ts index 5d8e89ee10..30f81c0012 100644 --- a/src/file/styles/index.ts +++ b/src/file/styles/index.ts @@ -1,21 +1,13 @@ -import { XmlComponent } from "file/xml-components"; -import { DocumentAttributes } from "../document/document-attributes"; +import { BaseXmlComponent, XmlComponent } from "file/xml-components"; import { DocumentDefaults } from "./defaults"; import { ParagraphStyle } from "./style"; export class Styles extends XmlComponent { - constructor() { + constructor(initialStyles?: BaseXmlComponent) { super("w:styles"); - this.root.push( - new DocumentAttributes({ - mc: "http://schemas.openxmlformats.org/markup-compatibility/2006", - r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships", - w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main", - w14: "http://schemas.microsoft.com/office/word/2010/wordml", - w15: "http://schemas.microsoft.com/office/word/2012/wordml", - Ignorable: "w14 w15", - }), - ); + if (initialStyles) { + this.root.push(initialStyles); + } } public push(style: XmlComponent): Styles { diff --git a/src/file/styles/style/components.ts b/src/file/styles/style/components.ts index 2a50ef5d57..5535036ac9 100644 --- a/src/file/styles/style/components.ts +++ b/src/file/styles/style/components.ts @@ -44,7 +44,11 @@ export class UiPriority extends XmlComponent { } } -export class UnhideWhenUsed extends XmlComponent {} +export class UnhideWhenUsed extends XmlComponent { + constructor() { + super("w:unhideWhenUsed"); + } +} export class QuickFormat extends XmlComponent { constructor() { @@ -56,4 +60,8 @@ export class TableProperties extends XmlComponent {} export class RsId extends XmlComponent {} -export class SemiHidden extends XmlComponent {} +export class SemiHidden extends XmlComponent { + constructor() { + super("w:semiHidden"); + } +} diff --git a/src/file/styles/style/index.ts b/src/file/styles/style/index.ts index 7436fbb22a..655cc28d2a 100644 --- a/src/file/styles/style/index.ts +++ b/src/file/styles/style/index.ts @@ -3,7 +3,7 @@ import * as paragraph from "../../paragraph"; import * as formatting from "../../paragraph/run/formatting"; import { RunProperties } from "../../paragraph/run/properties"; -import { BasedOn, Name, Next, QuickFormat } from "./components"; +import { BasedOn, Link, Name, Next, QuickFormat, SemiHidden, UiPriority, UnhideWhenUsed } from "./components"; export interface IStyleAttributes { type?: string; @@ -74,6 +74,7 @@ export class ParagraphStyle extends Style { public size(twips: number): ParagraphStyle { this.addRunProperty(new formatting.Size(twips)); + this.addRunProperty(new formatting.SizeComplexScript(twips)); return this; } @@ -249,3 +250,95 @@ export class ListParagraph extends ParagraphStyle { this.root.push(new QuickFormat()); } } + +export class CharacterStyle extends Style { + private readonly runProperties: RunProperties; + + constructor(styleId: string, name?: string) { + super({ type: "character", styleId: styleId }, name); + this.runProperties = new RunProperties(); + this.root.push(this.runProperties); + this.root.push(new UiPriority("99")); + this.root.push(new UnhideWhenUsed()); + } + + public basedOn(parentId: string): CharacterStyle { + this.root.push(new BasedOn(parentId)); + return this; + } + + public addRunProperty(property: XmlComponent): void { + this.runProperties.push(property); + } + + public color(color: string): CharacterStyle { + this.addRunProperty(new formatting.Color(color)); + return this; + } + + public underline(underlineType?: string, color?: string): CharacterStyle { + this.addRunProperty(new formatting.Underline(underlineType, color)); + return this; + } + + public size(twips: number): CharacterStyle { + this.addRunProperty(new formatting.Size(twips)); + this.addRunProperty(new formatting.SizeComplexScript(twips)); + return this; + } +} + +export class HyperlinkStyle extends CharacterStyle { + constructor() { + super("Hyperlink", "Hyperlink"); + this.basedOn("DefaultParagraphFont") + .color("0563C1") + .underline("single"); + } +} + +export class FootnoteReferenceStyle extends Style { + private readonly runProperties: RunProperties; + + constructor() { + super({ type: "character", styleId: "FootnoteReference" }); + this.root.push(new Name("footnote reference")); + this.root.push(new BasedOn("DefaultParagraphFont")); + this.root.push(new UiPriority("99")); + this.root.push(new SemiHidden()); + this.root.push(new UnhideWhenUsed()); + + this.runProperties = new RunProperties(); + this.runProperties.addChildElement(new formatting.SuperScript()); + this.root.push(this.runProperties); + } +} + +export class FootnoteText extends ParagraphStyle { + constructor() { + super("FootnoteText"); + this.root.push(new Name("footnote text")); + this.root.push(new BasedOn("Normal")); + this.root.push(new Link("FootnoteTextChar")); + this.root.push(new UiPriority("99")); + this.root.push(new SemiHidden()); + this.root.push(new UnhideWhenUsed()); + this.spacing({ + after: 0, + line: 240, + lineRule: "auto", + }); + this.size(20); + } +} + +export class FootnoteTextChar extends CharacterStyle { + constructor() { + super("FootnoteTextChar", "Footnote Text Char"); + this.basedOn("DefaultParagraphFont"); + this.root.push(new Link("FootnoteText")); + this.root.push(new UiPriority("99")); + this.root.push(new SemiHidden()); + this.size(20); + } +} diff --git a/src/file/styles/styles.spec.ts b/src/file/styles/styles.spec.ts index e8c7bc6f92..1c828337b2 100644 --- a/src/file/styles/styles.spec.ts +++ b/src/file/styles/styles.spec.ts @@ -362,7 +362,7 @@ describe("ParagraphStyle", () => { { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:pPr": [] }, { - "w:rPr": [{ "w:sz": [{ _attr: { "w:val": 24 } }] }], + "w:rPr": [{ "w:sz": [{ _attr: { "w:val": 24 } }] }, { "w:szCs": [{ _attr: { "w:val": 24 } }] }], }, ], }); @@ -459,7 +459,11 @@ describe("ParagraphStyle", () => { "w:style": [ { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:pPr": [] }, - { "w:rPr": [{ "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:hAnsi": "Times" } }] }] }, + { + "w:rPr": [ + { "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } }] }, + ], + }, ], }); }); diff --git a/src/file/table/index.ts b/src/file/table/index.ts index 0e948df9e8..dfba175857 100644 --- a/src/file/table/index.ts +++ b/src/file/table/index.ts @@ -1 +1,2 @@ export * from "./table"; +export * from "./table-cell"; diff --git a/src/file/table/properties.spec.ts b/src/file/table/properties.spec.ts index b6336c1411..c10c18f4c2 100644 --- a/src/file/table/properties.spec.ts +++ b/src/file/table/properties.spec.ts @@ -22,9 +22,9 @@ describe("TableProperties", () => { }); }); - describe("#fixedWidthLayout", () => { + describe("#setFixedWidthLayout", () => { it("sets the table to fixed width layout", () => { - const tp = new TableProperties().fixedWidthLayout(); + const tp = new TableProperties().setFixedWidthLayout(); const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [{ "w:tblLayout": [{ _attr: { "w:type": "fixed" } }] }], diff --git a/src/file/table/properties.ts b/src/file/table/properties.ts index 1478399dae..64b0ed53ad 100644 --- a/src/file/table/properties.ts +++ b/src/file/table/properties.ts @@ -12,7 +12,7 @@ export class TableProperties extends XmlComponent { return this; } - public fixedWidthLayout(): TableProperties { + public setFixedWidthLayout(): TableProperties { this.root.push(new TableLayout("fixed")); return this; } diff --git a/src/file/table/table-cell.spec.ts b/src/file/table/table-cell.spec.ts new file mode 100644 index 0000000000..01c81848bb --- /dev/null +++ b/src/file/table/table-cell.spec.ts @@ -0,0 +1,181 @@ +import { expect } from "chai"; + +import { TableCellBorders, BorderStyle, TableCellWidth, WidthType } from "./table-cell"; +import { Formatter } from "../../export/formatter"; + +describe("TableCellBorders", () => { + describe("#prepForXml", () => { + it("should not add empty borders element if there are no borders defined", () => { + const tb = new TableCellBorders(); + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal(""); + }); + }); + + describe("#addingBorders", () => { + it("should add top border", () => { + const tb = new TableCellBorders(); + tb.addTopBorder(BorderStyle.DOTTED, 1, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:top": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 1, + "w:val": "dotted", + }, + }, + ], + }, + ], + }); + }); + + it("should add start(left) border", () => { + const tb = new TableCellBorders(); + tb.addStartBorder(BorderStyle.SINGLE, 2, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:start": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 2, + "w:val": "single", + }, + }, + ], + }, + ], + }); + }); + + it("should add bottom border", () => { + const tb = new TableCellBorders(); + tb.addBottomBorder(BorderStyle.DOUBLE, 1, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:bottom": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 1, + "w:val": "double", + }, + }, + ], + }, + ], + }); + }); + + it("should add end(right) border", () => { + const tb = new TableCellBorders(); + tb.addEndBorder(BorderStyle.THICK, 3, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:end": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 3, + "w:val": "thick", + }, + }, + ], + }, + ], + }); + }); + + it("should add multiple borders", () => { + const tb = new TableCellBorders(); + tb.addTopBorder(BorderStyle.DOTTED, 1, "FF00FF"); + tb.addEndBorder(BorderStyle.THICK, 3, "FF00FF"); + tb.addBottomBorder(BorderStyle.DOUBLE, 1, "FF00FF"); + tb.addStartBorder(BorderStyle.SINGLE, 2, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:top": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 1, + "w:val": "dotted", + }, + }, + ], + }, + { + "w:end": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 3, + "w:val": "thick", + }, + }, + ], + }, + { + "w:bottom": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 1, + "w:val": "double", + }, + }, + ], + }, + { + "w:start": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 2, + "w:val": "single", + }, + }, + ], + }, + ], + }); + }); + }); +}); + +describe("TableCellWidth", () => { + describe("#constructor", () => { + it("should create object", () => { + const tcWidth = new TableCellWidth(100, WidthType.DXA); + const tree = new Formatter().format(tcWidth); + expect(tree).to.deep.equal({ + "w:tcW": [ + { + _attr: { + "w:type": "dxa", + "w:w": 100, + }, + }, + ], + }); + }); + }); +}); diff --git a/src/file/table/table-cell.ts b/src/file/table/table-cell.ts new file mode 100644 index 0000000000..954786aa12 --- /dev/null +++ b/src/file/table/table-cell.ts @@ -0,0 +1,231 @@ +import { IXmlableObject, XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +export enum BorderStyle { + SINGLE = "single", + DASH_DOT_STROKED = "dashDotStroked", + DASHED = "dashed", + DASH_SMALL_GAP = "dashSmallGap", + DOT_DASH = "dotDash", + DOT_DOT_DASH = "dotDotDash", + DOTTED = "dotted", + DOUBLE = "double", + DOUBLE_WAVE = "doubleWave", + INSET = "inset", + NIL = "nil", + NONE = "none", + OUTSET = "outset", + THICK = "thick", + THICK_THIN_LARGE_GAP = "thickThinLargeGap", + THICK_THIN_MEDIUM_GAP = "thickThinMediumGap", + THICK_THIN_SMALL_GAP = "thickThinSmallGap", + THIN_THICK_LARGE_GAP = "thinThickLargeGap", + THIN_THICK_MEDIUM_GAP = "thinThickMediumGap", + THIN_THICK_SMALL_GAP = "thinThickSmallGap", + THIN_THICK_THIN_LARGE_GAP = "thinThickThinLargeGap", + THIN_THICK_THIN_MEDIUM_GAP = "thinThickThinMediumGap", + THIN_THICK_THIN_SMALL_GAP = "thinThickThinSmallGap", + THREE_D_EMBOSS = "threeDEmboss", + THREE_D_ENGRAVE = "threeDEngrave", + TRIPLE = "triple", + WAVE = "wave", +} + +interface ICellBorder { + style: BorderStyle; + size: number; + color: string; +} + +class CellBorderAttributes extends XmlAttributeComponent { + protected xmlKeys = { style: "w:val", size: "w:sz", color: "w:color" }; +} + +class BaseTableCellBorder extends XmlComponent { + public setProperties(style: BorderStyle, size: number, color: string): BaseTableCellBorder { + const attrs = new CellBorderAttributes({ + style: style, + size: size, + color: color, + }); + this.root.push(attrs); + + return this; + } +} + +export class TableCellBorders extends XmlComponent { + constructor() { + super("w:tcBorders"); + } + + public prepForXml(): IXmlableObject { + return this.root.length > 0 ? super.prepForXml() : ""; + } + + public addTopBorder(style: BorderStyle, size: number, color: string): TableCellBorders { + const top = new BaseTableCellBorder("w:top"); + top.setProperties(style, size, color); + this.root.push(top); + + return this; + } + + public addStartBorder(style: BorderStyle, size: number, color: string): TableCellBorders { + const start = new BaseTableCellBorder("w:start"); + start.setProperties(style, size, color); + this.root.push(start); + + return this; + } + + public addBottomBorder(style: BorderStyle, size: number, color: string): TableCellBorders { + const bottom = new BaseTableCellBorder("w:bottom"); + bottom.setProperties(style, size, color); + this.root.push(bottom); + + return this; + } + + public addEndBorder(style: BorderStyle, size: number, color: string): TableCellBorders { + const end = new BaseTableCellBorder("w:end"); + end.setProperties(style, size, color); + this.root.push(end); + + return this; + } +} + +/** + * Attributes fot the GridSpan element. + */ +class GridSpanAttributes extends XmlAttributeComponent<{ val: number }> { + protected xmlKeys = { val: "w:val" }; +} + +/** + * GridSpan element. Should be used in a table cell. Pass the number of columns that this cell need to span. + */ +export class GridSpan extends XmlComponent { + constructor(value: number) { + super("w:gridSpan"); + + this.root.push( + new GridSpanAttributes({ + val: value, + }), + ); + } +} + +/** + * Vertical merge types. + */ +export enum VMergeType { + /** + * Cell that is merged with upper one. + */ + CONTINUE = "continue", + /** + * Cell that is starting the vertical merge. + */ + RESTART = "restart", +} + +class VMergeAttributes extends XmlAttributeComponent<{ val: VMergeType }> { + protected xmlKeys = { val: "w:val" }; +} + +/** + * Vertical merge element. Should be used in a table cell. + */ +export class VMerge extends XmlComponent { + constructor(value: VMergeType) { + super("w:vMerge"); + + this.root.push( + new VMergeAttributes({ + val: value, + }), + ); + } +} + +export enum VerticalAlign { + BOTTOM = "bottom", + CENTER = "center", + TOP = "top", +} + +class VAlignAttributes extends XmlAttributeComponent<{ val: VerticalAlign }> { + protected xmlKeys = { val: "w:val" }; +} + +/** + * Vertical align element. + */ +export class VAlign extends XmlComponent { + constructor(value: VerticalAlign) { + super("w:vAlign"); + + this.root.push( + new VAlignAttributes({ + val: value, + }), + ); + } +} + +export enum WidthType { + /** Auto. */ + AUTO = "auto", + /** Value is in twentieths of a point */ + DXA = "dxa", + /** No (empty) value. */ + NIL = "nil", + /** Value is in percentage. */ + PERCENTAGE = "pct", +} + +class TableCellWidthAttributes extends XmlAttributeComponent<{ type: WidthType; width: string | number }> { + protected xmlKeys = { width: "w:w", type: "w:type" }; +} + +/** + * Table cell width element. + */ +export class TableCellWidth extends XmlComponent { + constructor(value: string | number, type: WidthType) { + super("w:tcW"); + + this.root.push( + new TableCellWidthAttributes({ + width: value, + type: type, + }), + ); + } +} + +interface ITableCellShadingAttributesProperties { + fill?: string; + color?: string; + val?: string; +} + +class TableCellShadingAttributes extends XmlAttributeComponent { + protected xmlKeys = { + fill: "w:fill", + color: "w:color", + val: "w:val", + }; +} + +/** + * Table cell shading element. + */ +export class TableCellShading extends XmlComponent { + constructor(attrs: object) { + super("w:shd"); + this.root.push(new TableCellShadingAttributes(attrs)); + } +} diff --git a/src/file/table/table.spec.ts b/src/file/table/table.spec.ts index 84cbe99590..21ee3e63d4 100644 --- a/src/file/table/table.spec.ts +++ b/src/file/table/table.spec.ts @@ -186,9 +186,9 @@ describe("Table", () => { }); }); - describe("#fixedWidthLayout", () => { + describe("#setFixedWidthLayout", () => { it("sets the table to fixed width layout", () => { - const table = new Table(2, 2).fixedWidthLayout(); + const table = new Table(2, 2).setFixedWidthLayout(); const tree = new Formatter().format(table); expect(tree) .to.have.property("w:tbl") diff --git a/src/file/table/table.ts b/src/file/table/table.ts index 8fdec3d37c..d82da34ff2 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -1,3 +1,14 @@ +import { + GridSpan, + TableCellBorders, + TableCellShading, + TableCellWidth, + VAlign, + VerticalAlign, + VMerge, + VMergeType, + WidthType, +} from "file/table/table-cell"; import { IXmlableObject, XmlComponent } from "file/xml-components"; import { Paragraph } from "../paragraph"; import { TableGrid } from "./grid"; @@ -8,27 +19,32 @@ export class Table extends XmlComponent { private readonly rows: TableRow[]; private readonly grid: TableGrid; - constructor(rows: number, cols: number) { + constructor(rows: number, cols: number, colSizes?: number[]) { super("w:tbl"); this.properties = new TableProperties(); this.root.push(this.properties); this.properties.setBorder(); - const gridCols: number[] = []; - for (let i = 0; i < cols; i++) { - /* - 0-width columns don't get rendered correctly, so we need - to give them some value. A reasonable default would be - ~6in / numCols, but if we do that it becomes very hard - to resize the table using setWidth, unless the layout - algorithm is set to 'fixed'. Instead, the approach here - means even in 'auto' layout, setting a width on the - table will make it look reasonable, as the layout - algorithm will expand columns to fit its content - */ - gridCols.push(1); + if (colSizes && colSizes.length > 0) { + this.grid = new TableGrid(colSizes); + } else { + const gridCols: number[] = []; + for (let i = 0; i < cols; i++) { + /* + 0-width columns don't get rendered correctly, so we need + to give them some value. A reasonable default would be + ~6in / numCols, but if we do that it becomes very hard + to resize the table using setWidth, unless the layout + algorithm is set to 'fixed'. Instead, the approach here + means even in 'auto' layout, setting a width on the + table will make it look reasonable, as the layout + algorithm will expand columns to fit its content + */ + gridCols.push(1); + } + this.grid = new TableGrid(gridCols); } - this.grid = new TableGrid(gridCols); + this.root.push(this.grid); this.rows = []; @@ -56,8 +72,8 @@ export class Table extends XmlComponent { return this; } - public fixedWidthLayout(): Table { - this.properties.fixedWidthLayout(); + public setFixedWidthLayout(): Table { + this.properties.setFixedWidthLayout(); return this; } } @@ -75,6 +91,15 @@ export class TableRow extends XmlComponent { public getCell(ix: number): TableCell { return this.cells[ix]; } + + public addGridSpan(ix: number, cellSpan: number): TableCell { + const remainCell = this.cells[ix]; + remainCell.CellProperties.addGridSpan(cellSpan); + this.cells.splice(ix + 1, cellSpan - 1); + this.root.splice(ix + 2, cellSpan - 1); + + return remainCell; + } } export class TableRowProperties extends XmlComponent { @@ -112,10 +137,52 @@ export class TableCell extends XmlComponent { this.addContent(para); return para; } + + public get CellProperties(): TableCellProperties { + return this.properties; + } } export class TableCellProperties extends XmlComponent { + private readonly cellBorder: TableCellBorders; + constructor() { super("w:tcPr"); + this.cellBorder = new TableCellBorders(); + this.root.push(this.cellBorder); + } + + public get Borders(): TableCellBorders { + return this.cellBorder; + } + + public addGridSpan(cellSpan: number): TableCellProperties { + this.root.push(new GridSpan(cellSpan)); + + return this; + } + + public addVerticalMerge(type: VMergeType): TableCellProperties { + this.root.push(new VMerge(type)); + + return this; + } + + public setVerticalAlign(type: VerticalAlign): TableCellProperties { + this.root.push(new VAlign(type)); + + return this; + } + + public setWidth(width: string | number, type: WidthType): TableCellProperties { + this.root.push(new TableCellWidth(width, type)); + + return this; + } + + public setShading(attrs: object): TableCellProperties { + this.root.push(new TableCellShading(attrs)); + + return this; } } diff --git a/src/file/xml-components/base.ts b/src/file/xml-components/base.ts index d634a418a9..a5f24f3824 100644 --- a/src/file/xml-components/base.ts +++ b/src/file/xml-components/base.ts @@ -2,10 +2,15 @@ import { IXmlableObject } from "./xmlable-object"; export abstract class BaseXmlComponent { protected rootKey: string; + protected deleted: boolean = false; constructor(rootKey: string) { this.rootKey = rootKey; } public abstract prepForXml(): IXmlableObject; + + public get IsDeleted(): boolean { + return this.deleted; + } } diff --git a/src/file/xml-components/imported-xml-component.spec.ts b/src/file/xml-components/imported-xml-component.spec.ts new file mode 100644 index 0000000000..e941d3f0ec --- /dev/null +++ b/src/file/xml-components/imported-xml-component.spec.ts @@ -0,0 +1,93 @@ +import { expect } from "chai"; +import { ImportedXmlComponent, convertToXmlComponent } from "./"; + +const xmlString = ` + + + some value + + + Text 1 + + + Text 2 + + + `; + +const importedXmlElement = { + "w:p": { + _attr: { "w:one": "value 1", "w:two": "value 2" }, + "w:rPr": { "w:noProof": "some value" }, + "w:r": [{ _attr: { active: "true" }, "w:t": "Text 1" }, { _attr: { active: "true" }, "w:t": "Text 2" }], + }, +}; + +const convertedXmlElement = { + deleted: false, + rootKey: "w:p", + root: [ + { + deleted: false, + rootKey: "w:rPr", + root: [{ deleted: false, rootKey: "w:noProof", root: ["some value"] }], + }, + { + deleted: false, + rootKey: "w:r", + root: [{ deleted: false, rootKey: "w:t", root: ["Text 1"] }], + _attr: { active: "true" }, + }, + { + deleted: false, + rootKey: "w:r", + root: [{ deleted: false, rootKey: "w:t", root: ["Text 2"] }], + _attr: { active: "true" }, + }, + ], + _attr: { "w:one": "value 1", "w:two": "value 2" }, +}; + +describe("ImportedXmlComponent", () => { + let importedXmlComponent: ImportedXmlComponent; + + beforeEach(() => { + const attributes = { + someAttr: "1", + otherAttr: "2", + }; + importedXmlComponent = new ImportedXmlComponent("w:test", attributes); + importedXmlComponent.push(new ImportedXmlComponent("w:child")); + }); + + describe("#prepForXml()", () => { + it("should transform for xml", () => { + const converted = importedXmlComponent.prepForXml(); + expect(converted).to.eql({ + "w:test": [ + { + _attr: { + someAttr: "1", + otherAttr: "2", + }, + }, + { + "w:child": [], + }, + ], + }); + }); + }); + + it("should create XmlComponent from xml string", () => { + const converted = ImportedXmlComponent.fromXmlString(xmlString); + expect(converted).to.eql(convertedXmlElement); + }); + + describe("convertToXmlComponent", () => { + it("should convert to xml component", () => { + const converted = convertToXmlComponent("w:p", importedXmlElement["w:p"]); + expect(converted).to.eql(convertedXmlElement); + }); + }); +}); diff --git a/src/file/xml-components/imported-xml-component.ts b/src/file/xml-components/imported-xml-component.ts new file mode 100644 index 0000000000..23c4137900 --- /dev/null +++ b/src/file/xml-components/imported-xml-component.ts @@ -0,0 +1,138 @@ +/* tslint:disable */ +import { XmlComponent, IXmlableObject } from "."; +import * as fastXmlParser from "fast-xml-parser"; +import { flatMap } from "lodash"; + +export const parseOptions = { + ignoreAttributes: false, + attributeNamePrefix: "", + attrNodeName: "_attr", +}; + +/** + * Converts the given xml element (in json format) into XmlComponent. + * Note: If element is array, them it will return ImportedXmlComponent[]. Example for given: + * element = [ + * { w:t: "val 1"}, + * { w:t: "val 2"} + * ] + * will return + * [ + * ImportedXmlComponent { rootKey: "w:t", root: [ "val 1" ]}, + * ImportedXmlComponent { rootKey: "w:t", root: [ "val 2" ]} + * ] + * + * @param elementName name (rootKey) of the XmlComponent + * @param element the xml element in json presentation + */ +export function convertToXmlComponent(elementName: string, element: any): ImportedXmlComponent | ImportedXmlComponent[] { + const xmlElement = new ImportedXmlComponent(elementName, element._attr); + if (Array.isArray(element)) { + const out: any[] = []; + element.forEach((itemInArray) => { + out.push(convertToXmlComponent(elementName, itemInArray)); + }); + return flatMap(out); + } else if (typeof element === "object") { + Object.keys(element) + .filter((key) => key !== "_attr") + .map((item) => convertToXmlComponent(item, element[item])) + .forEach((converted) => { + if (Array.isArray(converted)) { + converted.forEach(xmlElement.push.bind(xmlElement)); + } else { + xmlElement.push(converted); + } + }); + } else if (element !== "") { + xmlElement.push(element); + } + return xmlElement; +} + +/** + * Represents imported xml component from xml file. + */ +export class ImportedXmlComponent extends XmlComponent { + private _attr: any; + + constructor(rootKey: string, _attr?: any) { + super(rootKey); + if (_attr) { + this._attr = _attr; + } + } + + /** + * Transforms the object so it can be converted to xml. Example: + * + * + * + * + * { + * 'w:someKey': [ + * { + * _attr: { + * someAttr: "1", + * otherAttr: "11" + * } + * }, + * { + * 'w:child': [ + * { + * _attr: { + * childAttr: "2" + * } + * } + * ] + * } + * ] + * } + */ + prepForXml(): IXmlableObject { + const result = super.prepForXml(); + if (!!this._attr) { + if (!Array.isArray(result[this.rootKey])) { + result[this.rootKey] = [result[this.rootKey]]; + } + result[this.rootKey].unshift({ _attr: this._attr }); + } + return result; + } + + push(xmlComponent: XmlComponent) { + this.root.push(xmlComponent); + } + + /** + * Converts the xml string to a XmlComponent tree. + * + * @param importedContent xml content of the imported component + */ + static fromXmlString(importedContent: string): ImportedXmlComponent { + const imported = fastXmlParser.parse(importedContent, parseOptions); + const elementName = Object.keys(imported)[0]; + + const converted = convertToXmlComponent(elementName, imported[elementName]); + + if (Array.isArray(converted) && converted.length > 1) { + throw new Error("Invalid conversion, input must be one element."); + } + return Array.isArray(converted) ? converted[0] : converted; + } +} + +/** + * Used for the attributes of root element that is being imported. + */ +export class ImportedRootElementAttributes extends XmlComponent { + constructor(private _attr: any) { + super(""); + } + + public prepForXml(): IXmlableObject { + return { + _attr: this._attr, + }; + } +} diff --git a/src/file/xml-components/index.ts b/src/file/xml-components/index.ts index 5d20da53d2..917933869e 100644 --- a/src/file/xml-components/index.ts +++ b/src/file/xml-components/index.ts @@ -1,4 +1,5 @@ export * from "./xml-component"; export * from "./attributes"; export * from "./default-attributes"; +export * from "./imported-xml-component"; export * from "./xmlable-object"; diff --git a/src/file/xml-components/xml-component.spec.ts b/src/file/xml-components/xml-component.spec.ts index 17d3d4d1cb..8b4f983388 100644 --- a/src/file/xml-components/xml-component.spec.ts +++ b/src/file/xml-components/xml-component.spec.ts @@ -18,4 +18,15 @@ describe("XmlComponent", () => { assert.equal(newJson.rootKey, "w:test"); }); }); + + describe("#prepForXml()", () => { + it("should skip deleted elements", () => { + const child = new TestComponent("w:test1"); + child.delete(); + xmlComponent.addChildElement(child); + + const xml = xmlComponent.prepForXml(); + assert.equal(xml["w:test"].length, 0); + }); + }); }); diff --git a/src/file/xml-components/xml-component.ts b/src/file/xml-components/xml-component.ts index d3705478cb..0ed604e9ee 100644 --- a/src/file/xml-components/xml-component.ts +++ b/src/file/xml-components/xml-component.ts @@ -12,6 +12,12 @@ export abstract class XmlComponent extends BaseXmlComponent { public prepForXml(): IXmlableObject { const children = this.root + .filter((c) => { + if (c instanceof BaseXmlComponent) { + return !c.IsDeleted; + } + return true; + }) .map((comp) => { if (comp instanceof BaseXmlComponent) { return comp.prepForXml(); @@ -23,4 +29,15 @@ export abstract class XmlComponent extends BaseXmlComponent { [this.rootKey]: children, }; } + + // TODO: Unused method + public addChildElement(child: XmlComponent | string): XmlComponent { + this.root.push(child); + + return this; + } + + public delete(): void { + this.deleted = true; + } } diff --git a/src/file/xml-components/xmlable-object.ts b/src/file/xml-components/xmlable-object.ts index 9fa0d5b600..255df14fc2 100644 --- a/src/file/xml-components/xmlable-object.ts +++ b/src/file/xml-components/xmlable-object.ts @@ -3,4 +3,7 @@ export interface IXmlableObject extends Object { } // Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432 -export const WORKAROUND3 = "workaround"; +/** + * @ignore + */ +export const WORKAROUND3 = ""; diff --git a/tsconfig.json b/tsconfig.json index 2d46f20143..4ef656d049 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "module": "commonjs", "declaration": true, "noUnusedLocals": true, + "noUnusedParameters": true, "baseUrl": "./src", "paths" : { "/*": [ @@ -22,5 +23,23 @@ "tests", "**/*.spec.ts", "**/_*" - ] + ], + "typedocOptions": { + "mode": "file", + "out": "docs/api", + "exclude": "test", + "theme": "default", + "ignoreCompilerErrors": true, + "excludePrivate": true, + "excludeProtected": true, + "excludeNotExported": true, + "excludeExternals": false, + "target": "ES6", + "moduleResolution": "node", + "preserveConstEnums": true, + "stripInternal": true, + "suppressExcessPropertyErrors": true, + "suppressImplicitAnyIndexErrors": true, + "module": "commonjs" + } } diff --git a/tslint.json b/tslint.json index 66e44398fc..497035eccc 100644 --- a/tslint.json +++ b/tslint.json @@ -11,7 +11,8 @@ ], "no-require-imports": true, "member-access": [ - true + true, + "check-accessor" ], "indent": [ true, @@ -31,10 +32,10 @@ "max-classes-per-file": [ false ], - "no-unused-variable": [ - true - ], "no-implicit-dependencies": false, - "no-submodule-imports": false + "no-submodule-imports": false, + "no-null-keyword": true, + "return-undefined": true, + "prefer-readonly": true } -} \ No newline at end of file +}