diff --git a/README.md b/README.md index f9120883e0..cd876aa0a5 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ Check the `examples` section in the [documentation](https://docx.js.org/#/exampl Read the contribution guidelines [here](https://docx.js.org/#/contribution-guidelines). +# Used by + +[drawing](https://hfour.com/) +[drawing](https://fuzzproductions.com/) +[drawing](https://www.mettzer.com/) + --- Made with 💖 diff --git a/demo/demo10.ts b/demo/demo10.ts index 84713e777f..5d207d5783 100644 --- a/demo/demo10.ts +++ b/demo/demo10.ts @@ -233,7 +233,7 @@ class DocumentCreator { public createRoleText(roleText: string): Paragraph { const paragraph = new Paragraph(); - const role = new TextRun(roleText).italic(); + const role = new TextRun(roleText).italics(); paragraph.addRun(role); diff --git a/demo/demo11.ts b/demo/demo11.ts index 76ec915060..0253adb751 100644 --- a/demo/demo11.ts +++ b/demo/demo11.ts @@ -84,8 +84,7 @@ doc.Styles.createParagraphStyle("ListParagraph", "List Paragraph") .basedOn("Normal"); doc.createImage(fs.readFileSync("./demo/images/pizza.gif")); -doc - .createParagraph("HEADING") +doc.createParagraph("HEADING") .heading1() .center(); @@ -111,8 +110,8 @@ const table = new Table(4, 4); table .getRow(0) .getCell(0) - .addContent(new Paragraph("Pole No.")); -// table.Properties.width = 10000; + .addParagraph(new Paragraph("Pole No.")); + doc.addTable(table); const arrboth = [ @@ -129,8 +128,6 @@ const arrboth = [ arrboth.forEach((item) => { const para = doc.createParagraph(); para.addImage(doc.createImage(fs.readFileSync(item.image))); - // para.Properties.width = 60; - // para.Properties.height = 90; doc.createParagraph(item.comment).style("normalPara2"); }); diff --git a/demo/demo16.ts b/demo/demo16.ts index 6c75218e53..9113a368b8 100644 --- a/demo/demo16.ts +++ b/demo/demo16.ts @@ -1,7 +1,7 @@ // Multiple sections and headers // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { Document, Packer, PageNumberFormat, PageOrientation, Paragraph } from "../build"; +import { Document, Packer, PageNumberFormat, PageOrientation, Paragraph, TextRun } from "../build"; const doc = new Document(); @@ -41,6 +41,40 @@ doc.addSection({ doc.createParagraph("hello in landscape"); +const header2 = doc.createHeader(); +const pageNumber = new TextRun("Page number: ").pageNumber(); +header2.createParagraph().addRun(pageNumber); + +doc.addSection({ + headers: { + default: header2, + }, + orientation: PageOrientation.PORTRAIT, +}); + +doc.createParagraph("Page number in the header must be 2, because it continues from the previous section."); + +doc.addSection({ + headers: { + default: header2, + }, + pageNumberFormatType: PageNumberFormat.UPPER_ROMAN, + orientation: PageOrientation.PORTRAIT, +}); + +doc.createParagraph("Page number in the header must be III, because it continues from the previous section, but is defined as upper roman."); + +doc.addSection({ + headers: { + default: header2, + }, + pageNumberFormatType: PageNumberFormat.DECIMAL, + pageNumberStart: 25, + orientation: PageOrientation.PORTRAIT, +}); + +doc.createParagraph("Page number in the header must be 25, because it is defined to start at 25 and to be decimal in this section."); + const packer = new Packer(); packer.toBuffer(doc).then((buffer) => { diff --git a/demo/demo20.ts b/demo/demo20.ts index f3f43edb26..c0158d8dce 100644 --- a/demo/demo20.ts +++ b/demo/demo20.ts @@ -8,8 +8,8 @@ const doc = new Document(); const table = doc.createTable(4, 4); table .getCell(2, 2) - .addContent(new Paragraph("Hello")) - .CellProperties.Borders.addTopBorder(BorderStyle.DASH_DOT_STROKED, 3, "red") + .addParagraph(new Paragraph("Hello")) + .Borders.addTopBorder(BorderStyle.DASH_DOT_STROKED, 3, "red") .addBottomBorder(BorderStyle.DOUBLE, 3, "blue") .addStartBorder(BorderStyle.DOT_DOT_DASH, 3, "green") .addEndBorder(BorderStyle.DOT_DOT_DASH, 3, "#ff8000"); diff --git a/demo/demo22.ts b/demo/demo22.ts index c5051ab019..6f5baa970c 100644 --- a/demo/demo22.ts +++ b/demo/demo22.ts @@ -16,7 +16,7 @@ paragraph2.addRun(textRun2); doc.addParagraph(paragraph2); const paragraph3 = new Paragraph().bidirectional(); -const textRun3 = new TextRun("שלום עולם").italic().rightToLeft(); +const textRun3 = new TextRun("שלום עולם").italics().rightToLeft(); paragraph3.addRun(textRun3); doc.addParagraph(paragraph3); diff --git a/demo/demo24.ts b/demo/demo24.ts index 4901477ced..62d80592a4 100644 --- a/demo/demo24.ts +++ b/demo/demo24.ts @@ -6,10 +6,10 @@ import { Document, Media, Packer, Paragraph } from "../build"; const doc = new Document(); const table = doc.createTable(4, 4); -table.getCell(2, 2).addContent(new Paragraph("Hello")); +table.getCell(2, 2).addParagraph(new Paragraph("Hello")); const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); -table.getCell(1, 1).addContent(image.Paragraph); +table.getCell(1, 1).addParagraph(image.Paragraph); const packer = new Packer(); diff --git a/demo/demo31.ts b/demo/demo31.ts index 366227102a..349dd7e393 100644 --- a/demo/demo31.ts +++ b/demo/demo31.ts @@ -8,12 +8,12 @@ const doc = new Document(); const table = doc.createTable(2, 2); table .getCell(1, 1) - .addContent(new Paragraph("This text should be in the middle of the cell")) - .CellProperties.setVerticalAlign(VerticalAlign.CENTER); + .addParagraph(new Paragraph("This text should be in the middle of the cell")) + .setVerticalAlign(VerticalAlign.CENTER); table .getCell(1, 0) - .addContent( + .addParagraph( new Paragraph( "Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah", ).heading1(), diff --git a/demo/demo32.ts b/demo/demo32.ts index 373ca64266..67c9e8a335 100644 --- a/demo/demo32.ts +++ b/demo/demo32.ts @@ -1,4 +1,4 @@ -// Example of how you would create a table and add data to it +// Example of how you would merge cells together // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; import { Document, Packer, Paragraph } from "../build"; @@ -7,24 +7,24 @@ const doc = new Document(); let table = doc.createTable(2, 2); -table.getCell(0, 0).addContent(new Paragraph("Hello")); +table.getCell(0, 0).addParagraph(new Paragraph("Hello")); table.getRow(0).mergeCells(0, 1); doc.createParagraph("Another table").heading2(); table = doc.createTable(2, 3); -table.getCell(0, 0).addContent(new Paragraph("World")); +table.getCell(0, 0).addParagraph(new Paragraph("World")); table.getRow(0).mergeCells(0, 2); doc.createParagraph("Another table").heading2(); table = doc.createTable(2, 4); -table.getCell(0, 0).addContent(new Paragraph("Foo")); +table.getCell(0, 0).addParagraph(new Paragraph("Foo")); -table.getCell(1, 0).addContent(new Paragraph("Bar1")); -table.getCell(1, 1).addContent(new Paragraph("Bar2")); -table.getCell(1, 2).addContent(new Paragraph("Bar3")); -table.getCell(1, 3).addContent(new Paragraph("Bar4")); +table.getCell(1, 0).addParagraph(new Paragraph("Bar1")); +table.getCell(1, 1).addParagraph(new Paragraph("Bar2")); +table.getCell(1, 2).addParagraph(new Paragraph("Bar3")); +table.getCell(1, 3).addParagraph(new Paragraph("Bar4")); table.getRow(0).mergeCells(0, 3); diff --git a/demo/demo34.ts b/demo/demo34.ts index 098e1c2065..2a46308394 100644 --- a/demo/demo34.ts +++ b/demo/demo34.ts @@ -20,9 +20,9 @@ const table = doc.createTable(2, 2).float({ relativeVerticalPosition: RelativeVerticalPosition.BOTTOM, }); table.setFixedWidthLayout(); -table.setWidth(WidthType.DXA, 4535); +table.setWidth(4535, WidthType.DXA); -table.getCell(0, 0).addContent(new Paragraph("Hello")); +table.getCell(0, 0).addParagraph(new Paragraph("Hello")); table.getRow(0).mergeCells(0, 1); const packer = new Packer(); diff --git a/demo/demo35.ts b/demo/demo35.ts new file mode 100644 index 0000000000..a91ad2347d --- /dev/null +++ b/demo/demo35.ts @@ -0,0 +1,17 @@ +// Simple example to add text to a document +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, Packer, Paragraph, TextRun } from "../build"; + +var doc = new Document(); +var paragraph = new Paragraph(); +var link = doc.createHyperlink('http://www.example.com', 'Hyperlink'); +link.bold(); +paragraph.addHyperLink(link); +doc.addParagraph(paragraph); + +const packer = new Packer(); + +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/demo/demo36.ts b/demo/demo36.ts new file mode 100644 index 0000000000..e9db47f25c --- /dev/null +++ b/demo/demo36.ts @@ -0,0 +1,23 @@ +// Add images to header and footer +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, Media, Packer, Table } from "../build"; + +const doc = new Document(); +const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); + +const table = new Table(2, 2); +table.getCell(1, 1).addContent(image.Paragraph); + +// doc.createParagraph("Hello World"); +doc.addTable(table); + +// doc.Header.createImage(fs.readFileSync("./demo/images/pizza.gif")); +doc.Header.addTable(table); +// doc.Footer.createImage(fs.readFileSync("./demo/images/pizza.gif")); + +const packer = new Packer(); + +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/demo/demo37.ts b/demo/demo37.ts new file mode 100644 index 0000000000..0aaf380d51 --- /dev/null +++ b/demo/demo37.ts @@ -0,0 +1,18 @@ +// Add images to header and footer +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, Media, Packer } from "../build"; + +const doc = new Document(); +const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); +doc.createParagraph("Hello World"); + +doc.Header.addImage(image); +doc.Header.createImage(fs.readFileSync("./demo/images/pizza.gif")); +doc.Header.createImage(fs.readFileSync("./demo/images/image1.jpeg")); + +const packer = new Packer(); + +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/demo/demo4.ts b/demo/demo4.ts index eb6f5b8c09..87c3f418e6 100644 --- a/demo/demo4.ts +++ b/demo/demo4.ts @@ -6,7 +6,7 @@ import { Document, Packer, Paragraph } from "../build"; const doc = new Document(); const table = doc.createTable(4, 4); -table.getCell(2, 2).addContent(new Paragraph("Hello")); +table.getCell(2, 2).addParagraph(new Paragraph("Hello")); const packer = new Packer(); diff --git a/docs/README.md b/docs/README.md index 4fc5757477..fd5ad9ecd9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,7 @@ import * as docx from "docx"; ## Basic Usage ```js +var fs = require("fs"); var docx = require("docx"); // Create document @@ -41,11 +42,12 @@ 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); +var packer = new docx.Packer(); +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My First Document.docx", buffer); +}); -exporter.pack("My First Document"); - -// Done! A file called 'My First Document.docx' will be in your file system if you used LocalPacker +// Done! A file called 'My First Document.docx' will be in your file system. ``` ## Honoured Mentions diff --git a/docs/contribution-guidelines.md b/docs/contribution-guidelines.md index 9bc1d4c90d..89f2f2b900 100644 --- a/docs/contribution-guidelines.md +++ b/docs/contribution-guidelines.md @@ -12,7 +12,7 @@ ## Always think about the user -The number one pillar for contribution is to **ALWAYS** think about how the user will use the library. +The number one pillar for contribution to `docx` is to **ALWAYS** think about how the user will use `docx`. Put yourself in their position, and imagine how they would feel about your feature you wrote. @@ -37,13 +37,13 @@ Unesesary coment removed // Make sure to use correct spelling ## No leaky components in API interface -This mainly applies to the API the end user will consume. +> This mainly applies to the API the end user will consume. -Try to make method parameters accept primatives, or `json` objects, so that child components are created **inside** the component, rather than being **injected** in. +Try to make method parameters of the outside API accept primatives, or `json` objects, so that child components are created **inside** the component, rather than being **injected** in. This is so that: -1. Imports are much cleaner, no need for: +1. Imports are much cleaner for the end user, no need for: ```js import { ChildComponent } from "./my-feature/sub-component/deeper/.../my-deep.component"; ``` @@ -52,13 +52,17 @@ This is so that: 3. It means the end user does not need to import and create the child component to be injected. **Do not** -`TableFloatProperties` is a class. The outside world would have to construct the object, and inject it in + +`TableFloatProperties` is a class. The outside world would have to `new` up the object, and inject it in like so: + ```js public float(tableFloatProperties: TableFloatProperties): Table ``` **Do** -`ITableFloatOptions` is an interface for a JSON of primatives. + +`ITableFloatOptions` is an interface for a JSON of primatives. The end user would need to pass in a json object and not need to worry about the internals: + ```js public float(tableFloatOptions: ITableFloatOptions): Table ``` @@ -76,7 +80,8 @@ This is just a guideline, and the rules can sometimes be broken. } ``` -* Use `add` if you add the element into the method as a parameter: +* Use `add` if you add the element into the method as a parameter. + *Note:* This may look like its breaking the previous guideline, but it has semantically different meanings. The previous one is using data to construct an object, whereas this one is simply adding elements into the document: ```js public addParagraph(paragraph: Paragraph) { diff --git a/docs/examples.md b/docs/examples.md index 05dbfb2de5..5964aecafe 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -158,7 +158,8 @@ _Source: https://github.com/dolanmiu/docx/blob/master/demo/demo15.ts_ ## Sections -Example of how sections work. Sections allow multiple headers and footers, and `landscape`/`portrait` inside the same document +Example of how sections work. Sections allow multiple headers and footers, and `landscape`/`portrait` inside the same document. +Also you can have different page number formats and starts for different sections. [Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo16.ts ":include") diff --git a/docs/index.html b/docs/index.html index 4b35f622af..9af955e10e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -25,6 +25,7 @@ + diff --git a/docs/usage/document.md b/docs/usage/document.md index f874502d3c..175e4ecad1 100644 --- a/docs/usage/document.md +++ b/docs/usage/document.md @@ -5,7 +5,7 @@ To create a new document, it is very easy: ```js -var doc = new docx.Document(); +const doc = new docx.Document(); ``` ## Document properties @@ -13,7 +13,7 @@ var doc = new docx.Document(); You can add properties to the Word document by specifying options, for example: ```js -var doc = new docx.Document({ +const doc = new docx.Document({ creator: "Dolan Miu", description: "My extremely interesting document", title: "My Document", @@ -22,14 +22,18 @@ var doc = new docx.Document({ ### Full list of options: -``` -creator -description -title -subject -keywords -lastModifiedBy -revision -``` + +* creator +* description +* title +* subject +* keywords +* lastModifiedBy +* revision You can mix and match whatever properties you want, or provide no properties. + +### units for positioning + +Various parts of the API require positioning arguments. The units are "20ths of a point" from the [OOXML](http://officeopenxml.com/index.php) specification. +See [Lars Corneliussen's blog post](https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/) for more information and how to convert units. \ No newline at end of file diff --git a/docs/usage/headers-and-footers.md b/docs/usage/headers-and-footers.md index 1489576dda..f1d351dee3 100644 --- a/docs/usage/headers-and-footers.md +++ b/docs/usage/headers-and-footers.md @@ -37,8 +37,12 @@ Also all the supported section properties are implemented according to: http://o // Add new section with another header and footer doc.addSection({ - headerId: header.Header.ReferenceId, - footerId: footer.Footer.ReferenceId, + headers: { + default: header + }, + footers: { + default: footer + }, pageNumberStart: 1, pageNumberFormatType: docx.PageNumberFormat.DECIMAL, }); diff --git a/docs/usage/styling-with-js.md b/docs/usage/styling-with-js.md index 4f362192af..f1f043cb1b 100644 --- a/docs/usage/styling-with-js.md +++ b/docs/usage/styling-with-js.md @@ -14,7 +14,7 @@ const name = new TextRun("Name:") ## Available methods * For run formatting: - * `.bold()`, `.italic()`, `.smallCaps()`, `.allCaps()`, `.strike()`, `.doubleStrike()`, `.subScript()`, `.superScript()`: Set the formatting property to true + * `.bold()`, `.italics()`, `.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 diff --git a/docs/usage/tab-stops.md b/docs/usage/tab-stops.md index 8447d03998..aa15ea75a9 100644 --- a/docs/usage/tab-stops.md +++ b/docs/usage/tab-stops.md @@ -2,7 +2,7 @@ > 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. +!> **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") diff --git a/docs/usage/tables.md b/docs/usage/tables.md new file mode 100644 index 0000000000..a129023217 --- /dev/null +++ b/docs/usage/tables.md @@ -0,0 +1,154 @@ +# Tables + +You can create tables with `docx`. More information can be found [here](http://officeopenxml.com/WPtable.php). + +## Create Table + +To create a table, simply use the `createTable()` method on a `document`. + +```ts +const table = doc.createTable([NUMBER OF ROWS], [NUMBER OF COLUMNS]); +``` + +Alternatively, you can create a table object directly, and then add it in the `document` + +```ts +const table = new Table(4, 4); +doc.addTable(table); +``` + +The snippet below creates a table of 2 rows and 4 columns. + +```ts +const table = doc.createTable(2, 4); + +// Or + +const table = new Table(2, 4); +doc.addTable(table); +``` + +## Cells + +The above section created a table with cells. To access the cell, use the `getCell` method. + +```ts +const cell = table.getCell([ROW INDEX], [COLUMN INDEX]); +``` + +For example: + +```ts +const cell = table.getCell(0, 2); +``` + +### Add paragraph to a cell + +Once you have got the cell, you can add data to it with the `addParagraph` method. + +```ts +cell.addParagraph(new Paragraph("Hello")); +``` + +## Borders + +BorderStyle can be imported from `docx`. Size determines the thickness. HTML color can be a hex code or alias such as `red`. + +```ts +cell.Borders.addTopBorder([BorderStyle], [SIZE], [HTML COLOR]); +``` + +```ts +cell.Borders.addBottomBorder([BorderStyle], [SIZE], [HTML COLOR]); +``` + +```ts +cell.Borders.addStartBorder([[BorderStyle]], [SIZE], [HTML COLOR]); +``` + +```ts +cell.Borders.addEndBorder([BorderStyle], [SIZE], [HTML COLOR]); +``` + +### Example + +```ts +import { BorderStyle } from "docx"; + +cell.Borders.addStartBorder(BorderStyle.DOT_DOT_DASH, 3, "green"); +cell.Borders.addStartBorder(BorderStyle.DOT_DOT_DASH, 3, "#ff8000"); +``` + +## Set Width + +```ts +import { WidthType } from "docx"; + +table.setWidth([WIDTH], [OPTIONAL WidthType. Defaults to DXA]); +``` + +For example: + +```ts +table.setWidth(4535, WidthType.DXA); +``` + +## Vertical Align + +Sets the vertical alignment of the contents of the cell + +```ts +import { VerticalAlign } from "docx"; + +cell.setVerticalAlign([VerticalAlign TYPE]); +``` + +For example, to center align a cell: + +```ts +cell.setVerticalAlign(VerticalAlign.CENTER); +``` + +## Rows + +To get a row, use the `getRow` method on a `table`. There are a handful of methods which you can apply to a row which will be explained below. + +```ts +table.getRow([ROW INDEX]); +``` + +## Merge cells together + +### Merging on a row + +First obtain the row, and call `mergeCells()`. The first argument is where the merge should start. The second argument is where the merge should end. + +```ts +table.getRow(0).mergeCells([FROM INDEX], [TO INDEX]); +``` + +#### Example + +This will merge 3 cells together starting from index `0`: + +```ts +table.getRow(0).mergeCells(0, 2); +``` + +### Merging on a column + +It has not been implemented yet, but it will follow a similar structure as merging a row. + +## Nested Tables + +To have a table within a table + +```ts +cell.addTable(new Table(1, 1)); +``` + +## Examples + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo4.ts ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo4.ts_ diff --git a/docs/usage/text.md b/docs/usage/text.md index 3ca98804bb..faa2bdf2d5 100644 --- a/docs/usage/text.md +++ b/docs/usage/text.md @@ -22,7 +22,7 @@ text.bold(); ### Italics ```js -text.italic(); +text.italics(); ``` ### Underline @@ -80,5 +80,5 @@ text.break(); What if you want to create a paragraph which is **_bold_** and **_italic_**? ```js -paragraph.bold().italic(); +paragraph.bold().italics(); ``` diff --git a/package.json b/package.json index 0ede47b9e4..5cbf873c6c 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "mocha-webpack": "^1.0.1", "nyc": "^13.1.0", "pre-commit": "^1.2.2", - "prettier": "^1.12.1", + "prettier": "^1.15.2", "prompt": "^1.0.0", "replace-in-file": "^3.1.0", "rimraf": "^2.5.2", diff --git a/src/export/packer/image-replacer.ts b/src/export/packer/image-replacer.ts new file mode 100644 index 0000000000..9cb3f950d5 --- /dev/null +++ b/src/export/packer/image-replacer.ts @@ -0,0 +1,17 @@ +import { IMediaData, Media } from "file/media"; + +export class ImageReplacer { + public replace(xmlData: string, mediaData: IMediaData[], offset: number): string { + let currentXmlData = xmlData; + + mediaData.forEach((image, i) => { + currentXmlData = currentXmlData.replace(`{${image.fileName}}`, (offset + i).toString()); + }); + + return currentXmlData; + } + + public getMediaData(xmlData: string, media: Media): IMediaData[] { + return media.Array.filter((image) => xmlData.search(`{${image.fileName}}`) > 0); + } +} diff --git a/src/export/packer/next-compiler.spec.ts b/src/export/packer/next-compiler.spec.ts index 2ac32dda33..dec853ba68 100644 --- a/src/export/packer/next-compiler.spec.ts +++ b/src/export/packer/next-compiler.spec.ts @@ -17,7 +17,7 @@ describe("Compiler", () => { describe("#compile()", () => { it("should pack all the content", async function() { this.timeout(99999999); - const zipFile = await compiler.compile(file); + const zipFile = compiler.compile(file); const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); expect(fileNames).is.an.instanceof(Array); @@ -46,7 +46,7 @@ describe("Compiler", () => { this.timeout(99999999); - const zipFile = await compiler.compile(file); + const zipFile = compiler.compile(file); const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); expect(fileNames).is.an.instanceof(Array); diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index d311cf310c..e97a155b01 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -3,6 +3,7 @@ import * as xml from "xml"; import { File } from "file"; import { Formatter } from "../formatter"; +import { ImageReplacer } from "./image-replacer"; interface IXmlifyedFile { readonly data: string; @@ -28,14 +29,15 @@ interface IXmlifyedFileMapping { export class Compiler { private readonly formatter: Formatter; + private readonly imageReplacer: ImageReplacer; constructor() { this.formatter = new Formatter(); + this.imageReplacer = new ImageReplacer(); } - public async compile(file: File): Promise { + public compile(file: File): JSZip { const zip = new JSZip(); - const xmlifiedFileMapping = this.xmlifyFile(file); for (const key in xmlifiedFileMapping) { @@ -59,26 +61,39 @@ export class Compiler { zip.file(`word/media/${data.fileName}`, mediaData); } - for (const header of file.Headers) { - for (const data of header.Media.Array) { - zip.file(`word/media/${data.fileName}`, data.stream); - } - } - - for (const footer of file.Footers) { - for (const data of footer.Media.Array) { - zip.file(`word/media/${data.fileName}`, data.stream); - } - } - return zip; } private xmlifyFile(file: File): IXmlifyedFileMapping { file.verifyUpdateFields(); + const documentRelationshipCount = file.DocumentRelationships.RelationshipCount + 1; + return { + Relationships: { + data: (() => { + const xmlData = xml(this.formatter.format(file.Document)); + const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); + + mediaDatas.forEach((mediaData, i) => { + file.DocumentRelationships.createRelationship( + documentRelationshipCount + i, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + }); + + return xml(this.formatter.format(file.DocumentRelationships)); + })(), + path: "word/_rels/document.xml.rels", + }, Document: { - data: xml(this.formatter.format(file.Document), true), + data: (() => { + const tempXmlData = xml(this.formatter.format(file.Document), true); + const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); + const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, documentRelationshipCount); + + return xmlData; + })(), path: "word/document.xml", }, Styles: { @@ -98,30 +113,66 @@ export class Compiler { data: xml(this.formatter.format(file.Numbering)), path: "word/numbering.xml", }, - Relationships: { - data: xml(this.formatter.format(file.DocumentRelationships)), - path: "word/_rels/document.xml.rels", - }, FileRelationships: { data: xml(this.formatter.format(file.FileRelationships)), path: "_rels/.rels", }, - Headers: file.Headers.map((headerWrapper, index) => ({ - data: xml(this.formatter.format(headerWrapper.Header)), - path: `word/header${index + 1}.xml`, - })), - Footers: file.Footers.map((footerWrapper, index) => ({ - data: xml(this.formatter.format(footerWrapper.Footer)), - path: `word/footer${index + 1}.xml`, - })), - HeaderRelationships: file.Headers.map((headerWrapper, index) => ({ - data: xml(this.formatter.format(headerWrapper.Relationships)), - path: `word/_rels/header${index + 1}.xml.rels`, - })), - FooterRelationships: file.Footers.map((footerWrapper, index) => ({ - data: xml(this.formatter.format(footerWrapper.Relationships)), - path: `word/_rels/footer${index + 1}.xml.rels`, - })), + HeaderRelationships: file.Headers.map((headerWrapper, index) => { + const xmlData = xml(this.formatter.format(headerWrapper.Header)); + const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); + + mediaDatas.forEach((mediaData, i) => { + headerWrapper.Relationships.createRelationship( + i, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + }); + + return { + data: xml(this.formatter.format(headerWrapper.Relationships)), + path: `word/_rels/header${index + 1}.xml.rels`, + }; + }), + FooterRelationships: file.Footers.map((footerWrapper, index) => { + const xmlData = xml(this.formatter.format(footerWrapper.Footer)); + const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); + + mediaDatas.forEach((mediaData, i) => { + footerWrapper.Relationships.createRelationship( + i, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + }); + + return { + data: xml(this.formatter.format(footerWrapper.Relationships)), + path: `word/_rels/footer${index + 1}.xml.rels`, + }; + }), + Headers: file.Headers.map((headerWrapper, index) => { + const tempXmlData = xml(this.formatter.format(headerWrapper.Header)); + const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); + // TODO: 0 needs to be changed when headers get relationships of their own + const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0); + + return { + data: xmlData, + path: `word/header${index + 1}.xml`, + }; + }), + Footers: file.Footers.map((footerWrapper, index) => { + const tempXmlData = xml(this.formatter.format(footerWrapper.Footer)); + const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); + // TODO: 0 needs to be changed when headers get relationships of their own + const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0); + + return { + data: xmlData, + path: `word/footer${index + 1}.xml`, + }; + }), ContentTypes: { data: xml(this.formatter.format(file.ContentTypes)), path: "[Content_Types].xml", diff --git a/src/export/packer/packer.spec.ts b/src/export/packer/packer.spec.ts index 110089da01..b61165ab46 100644 --- a/src/export/packer/packer.spec.ts +++ b/src/export/packer/packer.spec.ts @@ -46,4 +46,24 @@ describe("Packer", () => { }); }); }); + + describe("#toBase64String()", () => { + it("should create a standard docx file", async function() { + this.timeout(99999999); + const str = await packer.toBase64String(file); + + assert.isDefined(str); + assert.isTrue(str.length > 0); + }); + + it("should handle exception if it throws any", () => { + // tslint:disable-next-line:no-any + const compiler = stub((packer as any).compiler, "compile"); + + compiler.throwsException(); + return packer.toBase64String(file).catch((error) => { + assert.isDefined(error); + }); + }); + }); }); diff --git a/src/export/packer/packer.ts b/src/export/packer/packer.ts index 856f74b59e..5f4c12c66f 100644 --- a/src/export/packer/packer.ts +++ b/src/export/packer/packer.ts @@ -9,7 +9,7 @@ export class Packer { } public async toBuffer(file: File): Promise { - const zip = await this.compiler.compile(file); + const zip = this.compiler.compile(file); const zipData = (await zip.generateAsync({ type: "nodebuffer", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -19,7 +19,7 @@ export class Packer { } public async toBase64String(file: File): Promise { - const zip = await this.compiler.compile(file); + const zip = this.compiler.compile(file); const zipData = (await zip.generateAsync({ type: "base64", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -29,7 +29,7 @@ export class Packer { } public async toBlob(file: File): Promise { - const zip = await this.compiler.compile(file); + const zip = this.compiler.compile(file); const zipData = (await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", diff --git a/src/file/document/body/body.spec.ts b/src/file/document/body/body.spec.ts index 137ef298ba..b82944ff78 100644 --- a/src/file/document/body/body.spec.ts +++ b/src/file/document/body/body.spec.ts @@ -17,7 +17,7 @@ describe("Body", () => { expect(formatted) .to.have.property("w:sectPr") .and.to.be.an.instanceof(Array); - expect(formatted["w:sectPr"]).to.have.length(5); + expect(formatted["w:sectPr"]).to.have.length(4); }); }); @@ -76,7 +76,6 @@ describe("Body", () => { }, { "w:cols": [{ _attr: { "w:space": 708 } }] }, { "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }, - { "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }, ], }, ], @@ -104,7 +103,6 @@ describe("Body", () => { }, { "w:cols": [{ _attr: { "w:space": 708 } }] }, { "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }, - { "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }, ], }, ], 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 d767744e34..81135daba6 100644 --- a/src/file/document/body/section-properties/section-properties.spec.ts +++ b/src/file/document/body/section-properties/section-properties.spec.ts @@ -88,7 +88,6 @@ 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:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }); }); it("should create section properties with changed options", () => { @@ -183,5 +182,25 @@ describe("SectionProperties", () => { "w:pgBorders": [{ _attr: { "w:offsetFrom": "page" } }], }); }); + + it("should create section properties with page number type, but without start attribute", () => { + const properties = new SectionProperties({ + pageNumberFormatType: PageNumberFormat.UPPER_ROMAN, + }); + const tree = new Formatter().format(properties); + expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]); + const pgNumType = tree["w:sectPr"].find((item) => item["w:pgNumType"] !== undefined); + expect(pgNumType).to.deep.equal({ + "w:pgNumType": [{ _attr: { "w:fmt": "upperRoman" } }], + }); + }); + + it("should create section properties without page number type", () => { + const properties = new SectionProperties({}); + const tree = new Formatter().format(properties); + expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]); + const pgNumType = tree["w:sectPr"].find((item) => item["w:pgNumType"] !== undefined); + expect(pgNumType).to.equal(undefined); + }); }); }); diff --git a/src/file/document/body/section-properties/section-properties.ts b/src/file/document/body/section-properties/section-properties.ts index 4056570de2..9ae9e2bfb8 100644 --- a/src/file/document/body/section-properties/section-properties.ts +++ b/src/file/document/body/section-properties/section-properties.ts @@ -14,7 +14,7 @@ import { HeaderReference } from "./header-reference/header-reference"; import { IPageBordersOptions, PageBorders } from "./page-border"; import { PageMargin } from "./page-margin/page-margin"; import { IPageMarginAttributes } from "./page-margin/page-margin-attributes"; -import { IPageNumberTypeAttributes, PageNumberFormat, PageNumberType } from "./page-number"; +import { IPageNumberTypeAttributes, PageNumberType } from "./page-number"; import { PageSize } from "./page-size/page-size"; import { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes"; import { TitlePage } from "./title-page/title-page"; @@ -69,7 +69,7 @@ export class SectionProperties extends XmlComponent { orientation = PageOrientation.PORTRAIT, headers, footers, - pageNumberFormatType = PageNumberFormat.DECIMAL, + pageNumberFormatType, pageNumberStart, pageBorders, pageBorderTop, @@ -88,7 +88,9 @@ export class SectionProperties extends XmlComponent { this.addHeaders(headers); this.addFooters(footers); - this.root.push(new PageNumberType(pageNumberStart, pageNumberFormatType)); + if (pageNumberStart || pageNumberFormatType) { + this.root.push(new PageNumberType(pageNumberStart, pageNumberFormatType)); + } if (pageBorders || pageBorderTop || pageBorderRight || pageBorderBottom || pageBorderLeft) { this.root.push( diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts index 02eddb4037..733d60c769 100644 --- a/src/file/drawing/anchor/anchor.spec.ts +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -8,7 +8,20 @@ import { Anchor } from "./anchor"; function createAnchor(drawingOptions: IDrawingOptions): Anchor { return new Anchor( - 1, + { + fileName: "test.png", + stream: new Buffer(""), + dimensions: { + pixels: { + x: 0, + y: 0, + }, + emus: { + x: 0, + y: 0, + }, + }, + }, { pixels: { x: 100, diff --git a/src/file/drawing/anchor/anchor.ts b/src/file/drawing/anchor/anchor.ts index ee405ef5c0..b3b88ad1f6 100644 --- a/src/file/drawing/anchor/anchor.ts +++ b/src/file/drawing/anchor/anchor.ts @@ -1,5 +1,5 @@ // http://officeopenxml.com/drwPicFloating.php -import { IMediaDataDimensions } from "file/media"; +import { IMediaData, IMediaDataDimensions } from "file/media"; import { XmlComponent } from "file/xml-components"; import { IDrawingOptions } from "../drawing"; import { HorizontalPosition, IFloating, SimplePos, VerticalPosition } from "../floating"; @@ -21,7 +21,7 @@ const defaultOptions: IFloating = { }; export class Anchor extends XmlComponent { - constructor(referenceId: number, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) { + constructor(mediaData: IMediaData, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) { super("wp:anchor"); const floating = { @@ -70,6 +70,6 @@ export class Anchor extends XmlComponent { this.root.push(new DocProperties()); this.root.push(new GraphicFrameProperties()); - this.root.push(new Graphic(referenceId, dimensions.emus.x, dimensions.emus.y)); + this.root.push(new Graphic(mediaData, dimensions.emus.x, dimensions.emus.y)); } } diff --git a/src/file/drawing/drawing.spec.ts b/src/file/drawing/drawing.spec.ts index e44104959c..5ab77f28d0 100644 --- a/src/file/drawing/drawing.spec.ts +++ b/src/file/drawing/drawing.spec.ts @@ -11,7 +11,6 @@ function createDrawing(drawingOptions?: IDrawingOptions): Drawing { return new Drawing( { fileName: "test.jpg", - referenceId: 1, stream: Buffer.from(imageBase64Data, "base64"), path: path, dimensions: { diff --git a/src/file/drawing/drawing.ts b/src/file/drawing/drawing.ts index 1113ceab4b..cb8cc53684 100644 --- a/src/file/drawing/drawing.ts +++ b/src/file/drawing/drawing.ts @@ -28,10 +28,10 @@ export class Drawing extends XmlComponent { } if (!drawingOptions.floating) { - this.inline = new Inline(imageData.referenceId, imageData.dimensions); + this.inline = new Inline(imageData, imageData.dimensions); this.root.push(this.inline); } else { - this.root.push(new Anchor(imageData.referenceId, imageData.dimensions, drawingOptions)); + this.root.push(new Anchor(imageData, imageData.dimensions, drawingOptions)); } } 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 78606fd399..ce04f68179 100644 --- a/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts +++ b/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts @@ -1,11 +1,13 @@ +import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; + import { GraphicDataAttributes } from "./graphic-data-attribute"; import { Pic } from "./pic"; export class GraphicData extends XmlComponent { private readonly pic: Pic; - constructor(referenceId: number, x: number, y: number) { + constructor(mediaData: IMediaData, x: number, y: number) { super("a:graphicData"); this.root.push( @@ -14,7 +16,7 @@ export class GraphicData extends XmlComponent { }), ); - this.pic = new Pic(referenceId, x, y); + this.pic = new Pic(mediaData, x, y); this.root.push(this.pic); } diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-fill.ts b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-fill.ts index c4630ddee3..ea0ee27023 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-fill.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-fill.ts @@ -1,12 +1,15 @@ +import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; + import { Blip } from "./blip"; import { SourceRectangle } from "./source-rectangle"; import { Stretch } from "./stretch"; export class BlipFill extends XmlComponent { - constructor(referenceId: number) { + constructor(mediaData: IMediaData) { super("pic:blipFill"); - this.root.push(new Blip(referenceId)); + + this.root.push(new Blip(mediaData)); this.root.push(new SourceRectangle()); this.root.push(new Stretch()); } diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip.ts b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip.ts index bd279b2fae..f45b4d8316 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip.ts @@ -1,3 +1,4 @@ +import { IMediaData } from "file/media"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; interface IBlipProperties { @@ -13,11 +14,11 @@ class BlipAttributes extends XmlAttributeComponent { } export class Blip extends XmlComponent { - constructor(referenceId: number) { + constructor(mediaData: IMediaData) { super("a:blip"); this.root.push( new BlipAttributes({ - embed: `rId${referenceId}`, + embed: `rId{${mediaData.fileName}}`, cstate: "none", }), ); 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 14ccf63f02..cce8116677 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts @@ -1,5 +1,7 @@ // http://officeopenxml.com/drwPic.php +import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; + import { BlipFill } from "./blip/blip-fill"; import { NonVisualPicProperties } from "./non-visual-pic-properties/non-visual-pic-properties"; import { PicAttributes } from "./pic-attributes"; @@ -8,7 +10,7 @@ import { ShapeProperties } from "./shape-properties/shape-properties"; export class Pic extends XmlComponent { private readonly shapeProperties: ShapeProperties; - constructor(referenceId: number, x: number, y: number) { + constructor(mediaData: IMediaData, x: number, y: number) { super("pic:pic"); this.root.push( @@ -20,7 +22,7 @@ export class Pic extends XmlComponent { this.shapeProperties = new ShapeProperties(x, y); this.root.push(new NonVisualPicProperties()); - this.root.push(new BlipFill(referenceId)); + this.root.push(new BlipFill(mediaData)); this.root.push(new ShapeProperties(x, y)); } diff --git a/src/file/drawing/inline/graphic/graphic.ts b/src/file/drawing/inline/graphic/graphic.ts index 0228b78dfb..52d11d8e42 100644 --- a/src/file/drawing/inline/graphic/graphic.ts +++ b/src/file/drawing/inline/graphic/graphic.ts @@ -1,4 +1,6 @@ +import { IMediaData } from "file/media"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + import { GraphicData } from "./graphic-data"; interface IGraphicProperties { @@ -14,7 +16,7 @@ class GraphicAttributes extends XmlAttributeComponent { export class Graphic extends XmlComponent { private readonly data: GraphicData; - constructor(referenceId: number, x: number, y: number) { + constructor(mediaData: IMediaData, x: number, y: number) { super("a:graphic"); this.root.push( new GraphicAttributes({ @@ -22,7 +24,7 @@ export class Graphic extends XmlComponent { }), ); - this.data = new GraphicData(referenceId, x, y); + this.data = new GraphicData(mediaData, x, y); this.root.push(this.data); } diff --git a/src/file/drawing/inline/inline.ts b/src/file/drawing/inline/inline.ts index f36dd19cf3..7c40e7c3b3 100644 --- a/src/file/drawing/inline/inline.ts +++ b/src/file/drawing/inline/inline.ts @@ -1,5 +1,5 @@ // http://officeopenxml.com/drwPicInline.php -import { IMediaDataDimensions } from "file/media"; +import { IMediaData, IMediaDataDimensions } from "file/media"; import { XmlComponent } from "file/xml-components"; import { DocProperties } from "./../doc-properties/doc-properties"; import { EffectExtent } from "./../effect-extent/effect-extent"; @@ -12,7 +12,7 @@ export class Inline extends XmlComponent { private readonly extent: Extent; private readonly graphic: Graphic; - constructor(referenceId: number, private readonly dimensions: IMediaDataDimensions) { + constructor(readonly mediaData: IMediaData, private readonly dimensions: IMediaDataDimensions) { super("wp:inline"); this.root.push( @@ -25,7 +25,7 @@ export class Inline extends XmlComponent { ); this.extent = new Extent(dimensions.emus.x, dimensions.emus.y); - this.graphic = new Graphic(referenceId, dimensions.emus.x, dimensions.emus.y); + this.graphic = new Graphic(mediaData, dimensions.emus.x, dimensions.emus.y); this.root.push(this.extent); this.root.push(new EffectExtent()); diff --git a/src/file/file.spec.ts b/src/file/file.spec.ts index 47a1a15d27..f8e2ca9077 100644 --- a/src/file/file.spec.ts +++ b/src/file/file.spec.ts @@ -1,12 +1,15 @@ import { expect } from "chai"; +import * as sinon from "sinon"; import { Formatter } from "export/formatter"; import { File } from "./file"; +import { Paragraph } from "./paragraph"; +import { Table } from "./table"; describe("File", () => { describe("#constructor", () => { - it("should create with correct headers", () => { + it("should create with correct headers and footers", () => { const doc = new File(); const header = doc.createHeader(); const footer = doc.createFooter(); @@ -26,6 +29,26 @@ describe("File", () => { expect(tree["w:body"][1]["w:sectPr"][5]["w:footerReference"][0]._attr["w:type"]).to.equal("default"); }); + it("should create with first headers and footers", () => { + const doc = new File(); + const header = doc.createHeader(); + const footer = doc.createFooter(); + + doc.addSection({ + headers: { + first: header, + }, + footers: { + first: footer, + }, + }); + + const tree = new Formatter().format(doc.Document.Body); + + expect(tree["w:body"][1]["w:sectPr"][4]["w:headerReference"][0]._attr["w:type"]).to.equal("first"); + expect(tree["w:body"][1]["w:sectPr"][5]["w:footerReference"][0]._attr["w:type"]).to.equal("first"); + }); + it("should create with correct headers", () => { const doc = new File(); const header = doc.createHeader(); @@ -55,4 +78,88 @@ describe("File", () => { expect(tree["w:body"][1]["w:sectPr"][9]["w:footerReference"][0]._attr["w:type"]).to.equal("even"); }); }); + + describe("#addParagraph", () => { + it("should call the underlying document's addParagraph", () => { + const file = new File(); + const spy = sinon.spy(file.Document, "addParagraph"); + file.addParagraph(new Paragraph()); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addTable", () => { + it("should call the underlying document's addTable", () => { + const wrapper = new File(); + const spy = sinon.spy(wrapper.Document, "addTable"); + wrapper.addTable(new Table(1, 1)); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createTable", () => { + it("should call the underlying document's createTable", () => { + const wrapper = new File(); + const spy = sinon.spy(wrapper.Document, "createTable"); + wrapper.createTable(1, 1); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addTableOfContents", () => { + it("should call the underlying document's addTableOfContents", () => { + const wrapper = new File(); + const spy = sinon.spy(wrapper.Document, "addTableOfContents"); + // tslint:disable-next-line:no-any + wrapper.addTableOfContents({} as any); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createParagraph", () => { + it("should call the underlying document's createParagraph", () => { + const wrapper = new File(); + const spy = sinon.spy(wrapper.Document, "createParagraph"); + wrapper.createParagraph("test"); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addImage", () => { + it("should call the underlying document's addImage", () => { + const wrapper = new File(); + const spy = sinon.spy(wrapper.Document, "addParagraph"); + // tslint:disable-next-line:no-any + wrapper.addImage({} as any); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createImage", () => { + it("should call the underlying document's createImage", () => { + const wrapper = new File(); + const spy = sinon.spy(wrapper.Media, "addMedia"); + const wrapperSpy = sinon.spy(wrapper.Document, "addParagraph"); + wrapper.createImage(""); + + expect(spy.called).to.equal(true); + expect(wrapperSpy.called).to.equal(true); + }); + }); + + describe("#createFootnote", () => { + it("should call the underlying document's createFootnote", () => { + const wrapper = new File(); + const spy = sinon.spy(wrapper.FootNotes, "createFootNote"); + wrapper.createFootnote(new Paragraph("")); + + expect(spy.called).to.equal(true); + }); + }); }); diff --git a/src/file/file.ts b/src/file/file.ts index 13015a812f..a74a2c284f 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -18,6 +18,7 @@ import { Image, Media } from "./media"; import { Numbering } from "./numbering"; import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; +import { TargetModeType } from "./relationships/relationship/relationship"; import { Settings } from "./settings"; import { Styles } from "./styles"; import { ExternalStylesFactory } from "./styles/external-styles-factory"; @@ -153,7 +154,7 @@ export class File { hyperlink.linkId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", link, - "External", + TargetModeType.EXTERNAL, ); return hyperlink; } @@ -299,12 +300,6 @@ export class File { for (const header of headers) { switch (header.type) { - case HeaderReferenceType.DEFAULT: - newGroup = { - ...newGroup, - default: header.header, - }; - break; case HeaderReferenceType.FIRST: newGroup = { ...newGroup, @@ -317,6 +312,7 @@ export class File { even: header.header, }; break; + case HeaderReferenceType.DEFAULT: default: newGroup = { ...newGroup, @@ -334,12 +330,6 @@ export class File { for (const footer of footers) { switch (footer.type) { - case FooterReferenceType.DEFAULT: - newGroup = { - ...newGroup, - default: footer.footer, - }; - break; case FooterReferenceType.FIRST: newGroup = { ...newGroup, @@ -352,6 +342,7 @@ export class File { even: footer.footer, }; break; + case FooterReferenceType.DEFAULT: default: newGroup = { ...newGroup, diff --git a/src/file/footer-wrapper.spec.ts b/src/file/footer-wrapper.spec.ts new file mode 100644 index 0000000000..e1942bc7a4 --- /dev/null +++ b/src/file/footer-wrapper.spec.ts @@ -0,0 +1,83 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { FooterWrapper } from "./footer-wrapper"; +import { Media } from "./media"; +import { Paragraph } from "./paragraph"; +import { Table } from "./table"; + +describe("FooterWrapper", () => { + describe("#addParagraph", () => { + it("should call the underlying footer's addParagraph", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.Footer, "addParagraph"); + file.addParagraph(new Paragraph()); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addTable", () => { + it("should call the underlying footer's addParagraph", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.Footer, "addTable"); + file.addTable(new Table(1, 1)); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createTable", () => { + it("should call the underlying footer's createTable", () => { + const wrapper = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(wrapper.Footer, "createTable"); + wrapper.createTable(1, 1); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createParagraph", () => { + it("should call the underlying footer's createParagraph", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.Footer, "addParagraph"); + file.createParagraph(); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addImage", () => { + it("should call the underlying footer's addImage", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.Footer, "addParagraph"); + // tslint:disable-next-line:no-any + file.addImage({} as any); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createImage", () => { + it("should call the underlying footer's createImage", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.Media, "addMedia"); + const fileSpy = sinon.spy(file, "addImage"); + file.createImage(""); + + expect(spy.called).to.equal(true); + expect(fileSpy.called).to.equal(true); + }); + }); + + describe("#addChildElement", () => { + it("should call the underlying footer's addChildElement", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.Footer, "addChildElement"); + // tslint:disable-next-line:no-any + file.addChildElement({} as any); + + expect(spy.called).to.equal(true); + }); + }); +}); diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts index f9fecf6a50..6b55730f9f 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components"; import { FooterReferenceType } from "./document"; import { Footer } from "./footer/footer"; -import { Image, IMediaData, Media } from "./media"; +import { Image, Media } from "./media"; import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; @@ -43,29 +43,8 @@ export class FooterWrapper { this.footer.addChildElement(childElement); } - public addImageRelationship(image: Buffer, refId: number, width?: number, height?: number): IMediaData { - const mediaData = this.media.addMedia(image, refId, width, height); - this.relationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - return mediaData; - } - - public addHyperlinkRelationship(target: string, refId: number, targetMode?: "External" | undefined): void { - this.relationships.createRelationship( - refId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", - target, - targetMode, - ); - } - public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void { - // TODO - // tslint:disable-next-line:no-any - const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height); + const mediaData = this.media.addMedia(image, width, height); this.addImage(new Image(new ImageParagraph(mediaData))); } diff --git a/src/file/header-wrapper.spec.ts b/src/file/header-wrapper.spec.ts index 4985887e33..f7d73c39cc 100644 --- a/src/file/header-wrapper.spec.ts +++ b/src/file/header-wrapper.spec.ts @@ -9,19 +9,73 @@ import { Table } from "./table"; describe("HeaderWrapper", () => { describe("#addParagraph", () => { it("should call the underlying header's addParagraph", () => { - const file = new HeaderWrapper(new Media(), 1); - const spy = sinon.spy(file.Header, "addParagraph"); - file.addParagraph(new Paragraph()); + const wrapper = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(wrapper.Header, "addParagraph"); + wrapper.addParagraph(new Paragraph()); expect(spy.called).to.equal(true); }); }); describe("#addTable", () => { - it("should call the underlying header's addParagraph", () => { + it("should call the underlying header's addTable", () => { + const wrapper = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(wrapper.Header, "addTable"); + wrapper.addTable(new Table(1, 1)); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createTable", () => { + it("should call the underlying header's createTable", () => { + const wrapper = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(wrapper.Header, "createTable"); + wrapper.createTable(1, 1); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createParagraph", () => { + it("should call the underlying header's createParagraph", () => { const file = new HeaderWrapper(new Media(), 1); - const spy = sinon.spy(file.Header, "addTable"); - file.addTable(new Table(1, 1)); + const spy = sinon.spy(file.Header, "addParagraph"); + file.createParagraph(); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addImage", () => { + it("should call the underlying header's addImage", () => { + const file = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(file.Header, "addParagraph"); + // tslint:disable-next-line:no-any + file.addImage({} as any); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#createImage", () => { + it("should call the underlying header's createImage", () => { + const file = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(file.Media, "addMedia"); + const fileSpy = sinon.spy(file, "addImage"); + file.createImage(""); + + expect(spy.called).to.equal(true); + expect(fileSpy.called).to.equal(true); + }); + }); + + describe("#addChildElement", () => { + it("should call the underlying header's addChildElement", () => { + const file = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(file.Header, "addChildElement"); + // tslint:disable-next-line:no-any + file.addChildElement({} as any); expect(spy.called).to.equal(true); }); diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts index 5730b5eaa6..878d23bc9e 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components"; import { HeaderReferenceType } from "./document"; import { Header } from "./header/header"; -import { Image, IMediaData, Media } from "./media"; +import { Image, Media } from "./media"; import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; @@ -43,29 +43,8 @@ export class HeaderWrapper { this.header.addChildElement(childElement); } - public addImageRelationship(image: Buffer, refId: number, width?: number, height?: number): IMediaData { - const mediaData = this.media.addMedia(image, refId, width, height); - this.relationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - return mediaData; - } - - public addHyperlinkRelationship(target: string, refId: number, targetMode?: "External" | undefined): void { - this.relationships.createRelationship( - refId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", - target, - targetMode, - ); - } - public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void { - // TODO - // tslint:disable-next-line:no-any - const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height); + const mediaData = this.media.addMedia(image, width, height); this.addImage(new Image(new ImageParagraph(mediaData))); } diff --git a/src/file/media/data.ts b/src/file/media/data.ts index ce8af83919..c49457b803 100644 --- a/src/file/media/data.ts +++ b/src/file/media/data.ts @@ -10,7 +10,6 @@ export interface IMediaDataDimensions { } export interface IMediaData { - readonly referenceId: number; readonly stream: Buffer | Uint8Array | ArrayBuffer; readonly path?: string; readonly fileName: string; diff --git a/src/file/media/media.spec.ts b/src/file/media/media.spec.ts index 3e833bc17d..04630ff5ed 100644 --- a/src/file/media/media.spec.ts +++ b/src/file/media/media.spec.ts @@ -1,5 +1,6 @@ // tslint:disable:object-literal-key-quotes import { expect } from "chai"; +import { stub } from "sinon"; import { Formatter } from "export/formatter"; @@ -20,16 +21,18 @@ describe("Media", () => { }); it("should ensure the correct relationship id is used when adding image", () => { + // tslint:disable-next-line:no-any + stub(Media as any, "generateId").callsFake(() => "testId"); + const file = new File(); const image1 = Media.addImage(file, "test"); - const tree = new Formatter().format(image1.Paragraph); const inlineElements = tree["w:p"][1]["w:r"][1]["w:drawing"][0]["wp:inline"]; const graphicData = inlineElements.find((x) => x["a:graphic"]); expect(graphicData["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"][0]).to.deep.equal({ _attr: { - "r:embed": `rId${file.DocumentRelationships.RelationshipCount}`, + "r:embed": `rId{testId.png}`, cstate: "none", }, }); @@ -41,7 +44,7 @@ describe("Media", () => { expect(graphicData2["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"][0]).to.deep.equal({ _attr: { - "r:embed": `rId${file.DocumentRelationships.RelationshipCount}`, + "r:embed": `rId{testId.png}`, cstate: "none", }, }); @@ -53,9 +56,8 @@ describe("Media", () => { // tslint:disable-next-line:no-any (Media as any).generateId = () => "test"; - const image = new Media().addMedia("", 1); + const image = new Media().addMedia(""); expect(image.fileName).to.equal("test.png"); - expect(image.referenceId).to.equal(1); expect(image.dimensions).to.deep.equal({ pixels: { x: 100, @@ -74,7 +76,7 @@ describe("Media", () => { // tslint:disable-next-line:no-any (Media as any).generateId = () => "test"; - const image = new Media().addMedia("", 1); + const image = new Media().addMedia(""); expect(image.stream).to.be.an.instanceof(Uint8Array); }); }); @@ -85,12 +87,11 @@ describe("Media", () => { (Media as any).generateId = () => "test"; const media = new Media(); - media.addMedia("", 1); + media.addMedia(""); const image = media.getMedia("test.png"); expect(image.fileName).to.equal("test.png"); - expect(image.referenceId).to.equal(1); expect(image.dimensions).to.deep.equal({ pixels: { x: 100, @@ -116,7 +117,7 @@ describe("Media", () => { (Media as any).generateId = () => "test"; const media = new Media(); - media.addMedia("", 1); + media.addMedia(""); const array = media.Array; expect(array).to.be.an.instanceof(Array); @@ -124,7 +125,6 @@ describe("Media", () => { const image = array[0]; expect(image.fileName).to.equal("test.png"); - expect(image.referenceId).to.equal(1); expect(image.dimensions).to.deep.equal({ pixels: { x: 100, diff --git a/src/file/media/media.ts b/src/file/media/media.ts index c1f67b3023..8a160291ca 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -4,11 +4,6 @@ import { ImageParagraph } from "../paragraph"; import { IMediaData } from "./data"; import { Image } from "./image"; -interface IHackedFile { - // tslint:disable-next-line:readonly-keyword - currentRelationshipId: number; -} - export class Media { public static addImage( file: File, @@ -18,14 +13,7 @@ export class Media { drawingOptions?: IDrawingOptions, ): Image { // Workaround to expose id without exposing to API - const exposedFile = (file as {}) as IHackedFile; - const mediaData = file.Media.addMedia(buffer, exposedFile.currentRelationshipId++, width, height); - file.DocumentRelationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - + const mediaData = file.Media.addMedia(buffer, width, height); return new Image(new ImageParagraph(mediaData, drawingOptions)); } @@ -57,17 +45,11 @@ export class Media { return data; } - public addMedia( - buffer: Buffer | string | Uint8Array | ArrayBuffer, - referenceId: number, - width: number = 100, - height: number = 100, - ): IMediaData { + public addMedia(buffer: Buffer | string | Uint8Array | ArrayBuffer, width: number = 100, height: number = 100): IMediaData { const key = `${Media.generateId()}.png`; return this.createMedia( key, - referenceId, { width: width, height: height, @@ -78,7 +60,6 @@ export class Media { private createMedia( key: string, - relationshipsCount: number, dimensions: { readonly width: number; readonly height: number }, data: Buffer | string | Uint8Array | ArrayBuffer, filePath?: string, @@ -86,7 +67,6 @@ export class Media { const newData = typeof data === "string" ? this.convertDataURIToBinary(data) : data; const imageData: IMediaData = { - referenceId: relationshipsCount, stream: newData, path: filePath, fileName: key, diff --git a/src/file/numbering/index.ts b/src/file/numbering/index.ts index d83cffc61c..9356d6ffa8 100644 --- a/src/file/numbering/index.ts +++ b/src/file/numbering/index.ts @@ -1,3 +1,4 @@ export * from "./numbering"; export * from "./abstract-numbering"; export * from "./level"; +export * from "./num"; diff --git a/src/file/paragraph/image.spec.ts b/src/file/paragraph/image.spec.ts index c8289325e5..7dcb6ae148 100644 --- a/src/file/paragraph/image.spec.ts +++ b/src/file/paragraph/image.spec.ts @@ -10,10 +10,9 @@ describe("Image", () => { beforeEach(() => { image = new ImageParagraph({ - referenceId: 0, stream: new Buffer(""), path: "", - fileName: "", + fileName: "test.png", dimensions: { pixels: { x: 10, @@ -171,7 +170,7 @@ describe("Image", () => { { _attr: { cstate: "none", - "r:embed": "rId0", + "r:embed": "rId{test.png}", }, }, ], diff --git a/src/file/paragraph/run/run.spec.ts b/src/file/paragraph/run/run.spec.ts index c4ff3e8b70..bd9f3bf778 100644 --- a/src/file/paragraph/run/run.spec.ts +++ b/src/file/paragraph/run/run.spec.ts @@ -21,9 +21,9 @@ describe("Run", () => { }); }); - describe("#italic()", () => { + describe("#italics()", () => { it("it should add italics to the properties", () => { - run.italic(); + run.italics(); 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"); diff --git a/src/file/paragraph/run/run.ts b/src/file/paragraph/run/run.ts index 74e011b402..c03ed8d78e 100644 --- a/src/file/paragraph/run/run.ts +++ b/src/file/paragraph/run/run.ts @@ -39,7 +39,7 @@ export class Run extends XmlComponent { return this; } - public italic(): Run { + public italics(): Run { this.properties.push(new Italics()); this.properties.push(new ItalicsComplexScript()); return this; diff --git a/src/file/relationships/relationship/relationship.ts b/src/file/relationships/relationship/relationship.ts index 92553a38f3..82672644c9 100644 --- a/src/file/relationships/relationship/relationship.ts +++ b/src/file/relationships/relationship/relationship.ts @@ -17,7 +17,9 @@ export type RelationshipType = | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"; -export type TargetModeType = "External"; +export enum TargetModeType { + EXTERNAL = "External", +} export class Relationship extends XmlComponent { constructor(id: string, type: RelationshipType, target: string, targetMode?: TargetModeType) { diff --git a/src/file/styles/style/character-style.spec.ts b/src/file/styles/style/character-style.spec.ts new file mode 100644 index 0000000000..1556aae109 --- /dev/null +++ b/src/file/styles/style/character-style.spec.ts @@ -0,0 +1,296 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; + +import { CharacterStyle } from "./character-style"; + +describe("CharacterStyle", () => { + describe("#constructor", () => { + it("should set the style type to character and use the given style id", () => { + const style = new CharacterStyle("myStyleId"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { "w:rPr": [] }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + + it("should set the name of the style, if given", () => { + const style = new CharacterStyle("myStyleId", "Style Name"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { "w:name": [{ _attr: { "w:val": "Style Name" } }] }, + { "w:rPr": [] }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + }); + + describe("formatting methods: style attributes", () => { + it("#basedOn", () => { + const style = new CharacterStyle("myStyleId").basedOn("otherId"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { "w:rPr": [] }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + { "w:basedOn": [{ _attr: { "w:val": "otherId" } }] }, + ], + }); + }); + }); + + describe("formatting methods: run properties", () => { + it("#size", () => { + const style = new CharacterStyle("myStyleId").size(24); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { + "w:rPr": [{ "w:sz": [{ _attr: { "w:val": 24 } }] }, { "w:szCs": [{ _attr: { "w:val": 24 } }] }], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + + describe("#underline", () => { + it("should set underline to 'single' if no arguments are given", () => { + const style = new CharacterStyle("myStyleId").underline(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { + "w:rPr": [{ "w:u": [{ _attr: { "w:val": "single" } }] }], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + + it("should set the style if given", () => { + const style = new CharacterStyle("myStyleId").underline("double"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { + "w:rPr": [{ "w:u": [{ _attr: { "w:val": "double" } }] }], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + + it("should set the style and color if given", () => { + const style = new CharacterStyle("myStyleId").underline("double", "005599"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { + "w:rPr": [{ "w:u": [{ _attr: { "w:val": "double", "w:color": "005599" } }] }], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + }); + + it("#superScript", () => { + const style = new CharacterStyle("myStyleId").superScript(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { + "w:rPr": [ + { + "w:vertAlign": [ + { + _attr: { + "w:val": "superscript", + }, + }, + ], + }, + ], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + + it("#color", () => { + const style = new CharacterStyle("myStyleId").color("123456"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { + "w:rPr": [{ "w:color": [{ _attr: { "w:val": "123456" } }] }], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + + it("#link", () => { + const style = new CharacterStyle("myStyleId").link("MyLink"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { + "w:rPr": [], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + { "w:link": [{ _attr: { "w:val": "MyLink" } }] }, + ], + }); + }); + + it("#semiHidden", () => { + const style = new CharacterStyle("myStyleId").semiHidden(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "myStyleId" } }, + { + "w:rPr": [], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { "w:unhideWhenUsed": [] }, + { "w:semiHidden": [] }, + ], + }); + }); + }); +}); diff --git a/src/file/styles/style/character-style.ts b/src/file/styles/style/character-style.ts new file mode 100644 index 0000000000..f7354ae788 --- /dev/null +++ b/src/file/styles/style/character-style.ts @@ -0,0 +1,53 @@ +import * as formatting from "file/paragraph/run/formatting"; +import { RunProperties } from "file/paragraph/run/properties"; +import { XmlComponent } from "file/xml-components"; +import { BasedOn, Link, SemiHidden, UiPriority, UnhideWhenUsed } from "./components"; +import { Style } from "./style"; + +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): CharacterStyle { + this.runProperties.push(property); + return this; + } + + public color(color: string): CharacterStyle { + return this.addRunProperty(new formatting.Color(color)); + } + + public underline(underlineType?: string, color?: string): CharacterStyle { + return this.addRunProperty(new formatting.Underline(underlineType, color)); + } + + public superScript(): CharacterStyle { + return this.addRunProperty(new formatting.SuperScript()); + } + + public size(twips: number): CharacterStyle { + return this.addRunProperty(new formatting.Size(twips)).addRunProperty(new formatting.SizeComplexScript(twips)); + } + + public link(link: string): CharacterStyle { + this.root.push(new Link(link)); + return this; + } + + public semiHidden(): CharacterStyle { + this.root.push(new SemiHidden()); + return this; + } +} diff --git a/src/file/styles/style/components.spec.ts b/src/file/styles/style/components.spec.ts new file mode 100644 index 0000000000..4ac3ca998f --- /dev/null +++ b/src/file/styles/style/components.spec.ts @@ -0,0 +1,53 @@ +import { expect } from "chai"; +import { Formatter } from "export/formatter"; +import * as components from "./components"; + +describe("Style components", () => { + it("Name#constructor", () => { + const style = new components.Name("Style Name"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ "w:name": [{ _attr: { "w:val": "Style Name" } }] }); + }); + + it("BasedOn#constructor", () => { + const style = new components.BasedOn("otherId"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ "w:basedOn": [{ _attr: { "w:val": "otherId" } }] }); + }); + + it("Next#constructor", () => { + const style = new components.Next("otherId"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ "w:next": [{ _attr: { "w:val": "otherId" } }] }); + }); + + it("Link#constructor", () => { + const style = new components.Link("otherId"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ "w:link": [{ _attr: { "w:val": "otherId" } }] }); + }); + + it("UiPriority#constructor", () => { + const style = new components.UiPriority("123"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ "w:uiPriority": [{ _attr: { "w:val": "123" } }] }); + }); + + it("UnhideWhenUsed#constructor", () => { + const style = new components.UnhideWhenUsed(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ "w:unhideWhenUsed": [] }); + }); + + it("QuickFormat#constructor", () => { + const style = new components.QuickFormat(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ "w:qFormat": [] }); + }); + + it("SemiHidden#constructor", () => { + const style = new components.SemiHidden(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ "w:semiHidden": [] }); + }); +}); diff --git a/src/file/styles/style/default-styles.spec.ts b/src/file/styles/style/default-styles.spec.ts new file mode 100644 index 0000000000..a3144f186b --- /dev/null +++ b/src/file/styles/style/default-styles.spec.ts @@ -0,0 +1,331 @@ +import { expect } from "chai"; +import { Formatter } from "export/formatter"; +import * as defaultStyels from "./default-styles"; + +describe("Default Styles", () => { + it("HeadingStyle#constructor", () => { + const style = new defaultStyels.HeadingStyle("Heading1", "Heading 1"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "Heading1" } }, + { "w:name": [{ _attr: { "w:val": "Heading 1" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:next": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("TitleStyle#constructor", () => { + const style = new defaultStyels.TitleStyle(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "Title" } }, + { "w:name": [{ _attr: { "w:val": "Title" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:next": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("Heading1Style#constructor", () => { + const style = new defaultStyels.Heading1Style(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "Heading1" } }, + { "w:name": [{ _attr: { "w:val": "Heading 1" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:next": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("Heading2Style#constructor", () => { + const style = new defaultStyels.Heading2Style(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "Heading2" } }, + { "w:name": [{ _attr: { "w:val": "Heading 2" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:next": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("Heading3Style#constructor", () => { + const style = new defaultStyels.Heading3Style(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "Heading3" } }, + { "w:name": [{ _attr: { "w:val": "Heading 3" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:next": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("Heading4Style#constructor", () => { + const style = new defaultStyels.Heading4Style(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "Heading4" } }, + { "w:name": [{ _attr: { "w:val": "Heading 4" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:next": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("Heading5Style#constructor", () => { + const style = new defaultStyels.Heading5Style(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "Heading5" } }, + { "w:name": [{ _attr: { "w:val": "Heading 5" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:next": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("Heading6Style#constructor", () => { + const style = new defaultStyels.Heading6Style(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "Heading6" } }, + { "w:name": [{ _attr: { "w:val": "Heading 6" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:next": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("ListParagraph#constructor", () => { + const style = new defaultStyels.ListParagraph(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "ListParagraph" } }, + { "w:name": [{ _attr: { "w:val": "List Paragraph" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("FootnoteText#constructor", () => { + const style = new defaultStyels.FootnoteText(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "FootnoteText" } }, + { "w:name": [{ _attr: { "w:val": "footnote text" } }] }, + { + "w:pPr": [ + { + "w:spacing": [ + { + _attr: { + "w:after": 0, + "w:line": 240, + "w:lineRule": "auto", + }, + }, + ], + }, + ], + }, + { + "w:rPr": [ + { + "w:sz": [ + { + _attr: { + "w:val": 20, + }, + }, + ], + }, + { + "w:szCs": [ + { + _attr: { + "w:val": 20, + }, + }, + ], + }, + ], + }, + { "w:basedOn": [{ _attr: { "w:val": "Normal" } }] }, + { "w:link": [{ _attr: { "w:val": "FootnoteTextChar" } }] }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:semiHidden": [], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }); + }); + + it("FootnoteReferenceStyle#constructor", () => { + const style = new defaultStyels.FootnoteReferenceStyle(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "FootnoteReference" } }, + { "w:name": [{ _attr: { "w:val": "footnote reference" } }] }, + { + "w:rPr": [ + { + "w:vertAlign": [ + { + _attr: { + "w:val": "superscript", + }, + }, + ], + }, + ], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + { "w:basedOn": [{ _attr: { "w:val": "DefaultParagraphFont" } }] }, + + { + "w:semiHidden": [], + }, + ], + }); + }); + + it("FootnoteTextChar#constructor", () => { + const style = new defaultStyels.FootnoteTextChar(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "FootnoteTextChar" } }, + { "w:name": [{ _attr: { "w:val": "Footnote Text Char" } }] }, + { + "w:rPr": [ + { + "w:sz": [ + { + _attr: { + "w:val": 20, + }, + }, + ], + }, + { + "w:szCs": [ + { + _attr: { + "w:val": 20, + }, + }, + ], + }, + ], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + { "w:basedOn": [{ _attr: { "w:val": "DefaultParagraphFont" } }] }, + { "w:link": [{ _attr: { "w:val": "FootnoteText" } }] }, + { + "w:semiHidden": [], + }, + ], + }); + }); + + it("HyperlinkStyle#constructor", () => { + const style = new defaultStyels.HyperlinkStyle(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "Hyperlink" } }, + { "w:name": [{ _attr: { "w:val": "Hyperlink" } }] }, + { + "w:rPr": [{ "w:color": [{ _attr: { "w:val": "0563C1" } }] }, { "w:u": [{ _attr: { "w:val": "single" } }] }], + }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + { "w:basedOn": [{ _attr: { "w:val": "DefaultParagraphFont" } }] }, + ], + }); + }); +}); diff --git a/src/file/styles/style/default-styles.ts b/src/file/styles/style/default-styles.ts new file mode 100644 index 0000000000..d9572499c2 --- /dev/null +++ b/src/file/styles/style/default-styles.ts @@ -0,0 +1,106 @@ +import { CharacterStyle } from "./character-style"; +import { ParagraphStyle } from "./paragraph-style"; + +export class HeadingStyle extends ParagraphStyle { + constructor(styleId: string, name: string) { + super(styleId, name); + this.basedOn("Normal"); + this.next("Normal"); + this.quickFormat(); + } +} + +export class TitleStyle extends HeadingStyle { + constructor() { + super("Title", "Title"); + } +} + +export class Heading1Style extends HeadingStyle { + constructor() { + super("Heading1", "Heading 1"); + } +} + +export class Heading2Style extends HeadingStyle { + constructor() { + super("Heading2", "Heading 2"); + } +} + +export class Heading3Style extends HeadingStyle { + constructor() { + super("Heading3", "Heading 3"); + } +} + +export class Heading4Style extends HeadingStyle { + constructor() { + super("Heading4", "Heading 4"); + } +} + +export class Heading5Style extends HeadingStyle { + constructor() { + super("Heading5", "Heading 5"); + } +} + +export class Heading6Style extends HeadingStyle { + constructor() { + super("Heading6", "Heading 6"); + } +} + +export class ListParagraph extends ParagraphStyle { + constructor() { + super("ListParagraph", "List Paragraph"); + this.basedOn("Normal"); + this.quickFormat(); + } +} + +export class FootnoteText extends ParagraphStyle { + constructor() { + super("FootnoteText", "footnote text"); + this.basedOn("Normal") + .link("FootnoteTextChar") + .uiPriority("99") + .semiHidden() + .unhideWhenUsed() + .spacing({ + after: 0, + line: 240, + lineRule: "auto", + }) + .size(20); + } +} + +export class FootnoteReferenceStyle extends CharacterStyle { + constructor() { + super("FootnoteReference", "footnote reference"); + this.basedOn("DefaultParagraphFont") + .semiHidden() + .superScript(); + } +} + +export class FootnoteTextChar extends CharacterStyle { + constructor() { + super("FootnoteTextChar", "Footnote Text Char"); + this.basedOn("DefaultParagraphFont") + .link("FootnoteText") + .semiHidden() + .size(20); + } +} + +export class HyperlinkStyle extends CharacterStyle { + constructor() { + super("Hyperlink", "Hyperlink"); + this.basedOn("DefaultParagraphFont") + .color("0563C1") + .underline("single"); + } +} diff --git a/src/file/styles/style/index.ts b/src/file/styles/style/index.ts index cf14ae6c5e..abe53aa61e 100644 --- a/src/file/styles/style/index.ts +++ b/src/file/styles/style/index.ts @@ -1,335 +1,4 @@ -import { - Alignment, - AlignmentOptions, - Indent, - ISpacingProperties, - KeepLines, - KeepNext, - LeftTabStop, - MaxRightTabStop, - ParagraphProperties, - Spacing, - ThematicBreak, -} from "file/paragraph"; -import * as formatting from "file/paragraph/run/formatting"; -import { RunProperties } from "file/paragraph/run/properties"; -import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; - -import { BasedOn, Link, Name, Next, QuickFormat, SemiHidden, UiPriority, UnhideWhenUsed } from "./components"; - -export interface IStyleAttributes { - readonly type?: string; - readonly styleId?: string; - readonly default?: boolean; - readonly customStyle?: string; -} - -class StyleAttributes extends XmlAttributeComponent { - protected readonly xmlKeys = { - type: "w:type", - styleId: "w:styleId", - default: "w:default", - customStyle: "w:customStyle", - }; -} - -export class Style extends XmlComponent { - constructor(attributes: IStyleAttributes, name?: string) { - super("w:style"); - this.root.push(new StyleAttributes(attributes)); - if (name) { - this.root.push(new Name(name)); - } - } - - public push(styleSegment: XmlComponent): void { - this.root.push(styleSegment); - } -} - -export class ParagraphStyle extends Style { - private readonly paragraphProperties: ParagraphProperties; - private readonly runProperties: RunProperties; - - constructor(styleId: string, name?: string) { - super({ type: "paragraph", styleId: styleId }, name); - this.paragraphProperties = new ParagraphProperties(); - this.runProperties = new RunProperties(); - this.root.push(this.paragraphProperties); - this.root.push(this.runProperties); - } - - public addParagraphProperty(property: XmlComponent): ParagraphStyle { - this.paragraphProperties.push(property); - return this; - } - - public addRunProperty(property: XmlComponent): ParagraphStyle { - this.runProperties.push(property); - return this; - } - - public basedOn(parentId: string): ParagraphStyle { - this.root.push(new BasedOn(parentId)); - return this; - } - - public quickFormat(): ParagraphStyle { - this.root.push(new QuickFormat()); - return this; - } - - public next(nextId: string): ParagraphStyle { - this.root.push(new Next(nextId)); - return this; - } - - // ---------- Run formatting ---------------------- // - - public size(twips: number): ParagraphStyle { - return this.addRunProperty(new formatting.Size(twips)).addRunProperty(new formatting.SizeComplexScript(twips)); - } - - public bold(): ParagraphStyle { - return this.addRunProperty(new formatting.Bold()); - } - - public italics(): ParagraphStyle { - return this.addRunProperty(new formatting.Italics()); - } - - public smallCaps(): ParagraphStyle { - return this.addRunProperty(new formatting.SmallCaps()); - } - - public allCaps(): ParagraphStyle { - return this.addRunProperty(new formatting.Caps()); - } - - public strike(): ParagraphStyle { - return this.addRunProperty(new formatting.Strike()); - } - - public doubleStrike(): ParagraphStyle { - return this.addRunProperty(new formatting.DoubleStrike()); - } - - public subScript(): ParagraphStyle { - return this.addRunProperty(new formatting.SubScript()); - } - - public superScript(): ParagraphStyle { - return this.addRunProperty(new formatting.SuperScript()); - } - - public underline(underlineType?: string, color?: string): ParagraphStyle { - return this.addRunProperty(new formatting.Underline(underlineType, color)); - } - - public color(color: string): ParagraphStyle { - return this.addRunProperty(new formatting.Color(color)); - } - - public font(fontName: string): ParagraphStyle { - return this.addRunProperty(new formatting.RunFonts(fontName)); - } - - public characterSpacing(value: number): ParagraphStyle { - return this.addRunProperty(new formatting.CharacterSpacing(value)); - } - - // --------------------- Paragraph formatting ------------------------ // - - public center(): ParagraphStyle { - return this.addParagraphProperty(new Alignment(AlignmentOptions.CENTER)); - } - - public left(): ParagraphStyle { - return this.addParagraphProperty(new Alignment(AlignmentOptions.LEFT)); - } - - public right(): ParagraphStyle { - return this.addParagraphProperty(new Alignment(AlignmentOptions.RIGHT)); - } - - public justified(): ParagraphStyle { - return this.addParagraphProperty(new Alignment(AlignmentOptions.BOTH)); - } - - public thematicBreak(): ParagraphStyle { - return this.addParagraphProperty(new ThematicBreak()); - } - - public maxRightTabStop(): ParagraphStyle { - return this.addParagraphProperty(new MaxRightTabStop()); - } - - public leftTabStop(position: number): ParagraphStyle { - return this.addParagraphProperty(new LeftTabStop(position)); - } - - public indent(attrs: object): ParagraphStyle { - return this.addParagraphProperty(new Indent(attrs)); - } - - public spacing(params: ISpacingProperties): ParagraphStyle { - return this.addParagraphProperty(new Spacing(params)); - } - - public keepNext(): ParagraphStyle { - return this.addParagraphProperty(new KeepNext()); - } - - public keepLines(): ParagraphStyle { - return this.addParagraphProperty(new KeepLines()); - } -} - -export class HeadingStyle extends ParagraphStyle { - constructor(styleId: string, name: string) { - super(styleId, name); - this.basedOn("Normal"); - this.next("Normal"); - this.quickFormat(); - } -} - -export class TitleStyle extends HeadingStyle { - constructor() { - super("Title", "Title"); - } -} - -export class Heading1Style extends HeadingStyle { - constructor() { - super("Heading1", "Heading 1"); - } -} - -export class Heading2Style extends HeadingStyle { - constructor() { - super("Heading2", "Heading 2"); - } -} - -export class Heading3Style extends HeadingStyle { - constructor() { - super("Heading3", "Heading 3"); - } -} - -export class Heading4Style extends HeadingStyle { - constructor() { - super("Heading4", "Heading 4"); - } -} - -export class Heading5Style extends HeadingStyle { - constructor() { - super("Heading5", "Heading 5"); - } -} - -export class Heading6Style extends HeadingStyle { - constructor() { - super("Heading6", "Heading 6"); - } -} - -export class ListParagraph extends ParagraphStyle { - constructor() { - super("ListParagraph"); - this.root.push(new Name("List Paragraph")); - this.root.push(new BasedOn("Normal")); - 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): CharacterStyle { - this.runProperties.push(property); - return this; - } - - public color(color: string): CharacterStyle { - return this.addRunProperty(new formatting.Color(color)); - } - - public underline(underlineType?: string, color?: string): CharacterStyle { - return this.addRunProperty(new formatting.Underline(underlineType, color)); - } - - public size(twips: number): CharacterStyle { - return this.addRunProperty(new formatting.Size(twips)).addRunProperty(new formatting.SizeComplexScript(twips)); - } -} - -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); - } -} +export * from "./style"; +export * from "./paragraph-style"; +export * from "./character-style"; +export * from "./default-styles"; diff --git a/src/file/styles/style/paragraph-style.spec.ts b/src/file/styles/style/paragraph-style.spec.ts new file mode 100644 index 0000000000..0fb854ce22 --- /dev/null +++ b/src/file/styles/style/paragraph-style.spec.ts @@ -0,0 +1,524 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; + +import { ParagraphStyle } from "./paragraph-style"; + +describe("ParagraphStyle", () => { + describe("#constructor", () => { + it("should set the style type to paragraph and use the given style id", () => { + const style = new ParagraphStyle("myStyleId"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:pPr": [] }, { "w:rPr": [] }], + }); + }); + + it("should set the name of the style, if given", () => { + const style = new ParagraphStyle("myStyleId", "Style Name"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:name": [{ _attr: { "w:val": "Style Name" } }] }, + { "w:pPr": [] }, + { "w:rPr": [] }, + ], + }); + }); + }); + + describe("formatting methods: style attributes", () => { + it("#basedOn", () => { + const style = new ParagraphStyle("myStyleId").basedOn("otherId"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:basedOn": [{ _attr: { "w:val": "otherId" } }] }, + ], + }); + }); + + it("#quickFormat", () => { + const style = new ParagraphStyle("myStyleId").quickFormat(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:qFormat": [] }, + ], + }); + }); + + it("#next", () => { + const style = new ParagraphStyle("myStyleId").next("otherId"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:next": [{ _attr: { "w:val": "otherId" } }] }, + ], + }); + }); + }); + + describe("formatting methods: paragraph properties", () => { + it("#indent", () => { + const style = new ParagraphStyle("myStyleId").indent({ left: 720 }); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [{ "w:ind": [{ _attr: { "w:left": 720 } }] }], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#spacing", () => { + const style = new ParagraphStyle("myStyleId").spacing({ before: 50, after: 150 }); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [{ "w:spacing": [{ _attr: { "w:before": 50, "w:after": 150 } }] }], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#center", () => { + const style = new ParagraphStyle("myStyleId").center(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [{ "w:jc": [{ _attr: { "w:val": "center" } }] }], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#character spacing", () => { + const style = new ParagraphStyle("myStyleId").characterSpacing(24); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:spacing": [{ _attr: { "w:val": 24 } }] }], + }, + ], + }); + }); + + it("#left", () => { + const style = new ParagraphStyle("myStyleId").left(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [{ "w:jc": [{ _attr: { "w:val": "left" } }] }], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#right", () => { + const style = new ParagraphStyle("myStyleId").right(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [{ "w:jc": [{ _attr: { "w:val": "right" } }] }], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#justified", () => { + const style = new ParagraphStyle("myStyleId").justified(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [{ "w:jc": [{ _attr: { "w:val": "both" } }] }], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#thematicBreak", () => { + const style = new ParagraphStyle("myStyleId").thematicBreak(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [ + { + "w:pBdr": [ + { + "w:bottom": [ + { + _attr: { + "w:color": "auto", + "w:space": "1", + "w:val": "single", + "w:sz": "6", + }, + }, + ], + }, + ], + }, + ], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#leftTabStop", () => { + const style = new ParagraphStyle("myStyleId").leftTabStop(1200); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [ + { + "w:tabs": [{ "w:tab": [{ _attr: { "w:val": "left", "w:pos": 1200 } }] }], + }, + ], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#maxRightTabStop", () => { + const style = new ParagraphStyle("myStyleId").maxRightTabStop(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { + "w:pPr": [ + { + "w:tabs": [{ "w:tab": [{ _attr: { "w:val": "right", "w:pos": 9026 } }] }], + }, + ], + }, + { "w:rPr": [] }, + ], + }); + }); + + it("#keepLines", () => { + const style = new ParagraphStyle("myStyleId").keepLines(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [{ "w:keepLines": [] }] }, + { "w:rPr": [] }, + ], + }); + }); + + it("#keepNext", () => { + const style = new ParagraphStyle("myStyleId").keepNext(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [{ "w:keepNext": [] }] }, + { "w:rPr": [] }, + ], + }); + }); + }); + + describe("formatting methods: run properties", () => { + it("#size", () => { + const style = new ParagraphStyle("myStyleId").size(24); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:sz": [{ _attr: { "w:val": 24 } }] }, { "w:szCs": [{ _attr: { "w:val": 24 } }] }], + }, + ], + }); + }); + + it("#smallCaps", () => { + const style = new ParagraphStyle("myStyleId").smallCaps(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:smallCaps": [{ _attr: { "w:val": true } }] }], + }, + ], + }); + }); + + it("#allCaps", () => { + const style = new ParagraphStyle("myStyleId").allCaps(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:caps": [{ _attr: { "w:val": true } }] }], + }, + ], + }); + }); + + it("#strike", () => { + const style = new ParagraphStyle("myStyleId").strike(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:strike": [{ _attr: { "w:val": true } }] }], + }, + ], + }); + }); + + it("#doubleStrike", () => { + const style = new ParagraphStyle("myStyleId").doubleStrike(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:dstrike": [{ _attr: { "w:val": true } }] }], + }, + ], + }); + }); + + it("#subScript", () => { + const style = new ParagraphStyle("myStyleId").subScript(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:vertAlign": [{ _attr: { "w:val": "subscript" } }] }], + }, + ], + }); + }); + + it("#superScript", () => { + const style = new ParagraphStyle("myStyleId").superScript(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:vertAlign": [{ _attr: { "w:val": "superscript" } }] }], + }, + ], + }); + }); + + it("#font", () => { + const style = new ParagraphStyle("myStyleId").font("Times"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [ + { "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } }] }, + ], + }, + ], + }); + }); + + it("#bold", () => { + const style = new ParagraphStyle("myStyleId").bold(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:b": [{ _attr: { "w:val": true } }] }], + }, + ], + }); + }); + + it("#italics", () => { + const style = new ParagraphStyle("myStyleId").italics(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:i": [{ _attr: { "w:val": true } }] }], + }, + ], + }); + }); + + describe("#underline", () => { + it("should set underline to 'single' if no arguments are given", () => { + const style = new ParagraphStyle("myStyleId").underline(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:u": [{ _attr: { "w:val": "single" } }] }], + }, + ], + }); + }); + + it("should set the style if given", () => { + const style = new ParagraphStyle("myStyleId").underline("double"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:u": [{ _attr: { "w:val": "double" } }] }], + }, + ], + }); + }); + + it("should set the style and color if given", () => { + const style = new ParagraphStyle("myStyleId").underline("double", "005599"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:u": [{ _attr: { "w:val": "double", "w:color": "005599" } }] }], + }, + ], + }); + }); + }); + + it("#color", () => { + const style = new ParagraphStyle("myStyleId").color("123456"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:color": [{ _attr: { "w:val": "123456" } }] }], + }, + ], + }); + }); + + it("#link", () => { + const style = new ParagraphStyle("myStyleId").link("MyLink"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:link": [{ _attr: { "w:val": "MyLink" } }] }, + ], + }); + }); + + it("#semiHidden", () => { + const style = new ParagraphStyle("myStyleId").semiHidden(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:semiHidden": [] }, + ], + }); + }); + + it("#uiPriority", () => { + const style = new ParagraphStyle("myStyleId").uiPriority("99"); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + ], + }); + }); + + it("#unhideWhenUsed", () => { + const style = new ParagraphStyle("myStyleId").unhideWhenUsed(); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { "w:rPr": [] }, + { "w:unhideWhenUsed": [] }, + ], + }); + }); + }); +}); diff --git a/src/file/styles/style/paragraph-style.ts b/src/file/styles/style/paragraph-style.ts new file mode 100644 index 0000000000..60f0035529 --- /dev/null +++ b/src/file/styles/style/paragraph-style.ts @@ -0,0 +1,178 @@ +import { + Alignment, + AlignmentOptions, + Indent, + ISpacingProperties, + KeepLines, + KeepNext, + LeftTabStop, + MaxRightTabStop, + ParagraphProperties, + Spacing, + ThematicBreak, +} from "file/paragraph"; +import * as formatting from "file/paragraph/run/formatting"; +import { RunProperties } from "file/paragraph/run/properties"; +import { XmlComponent } from "file/xml-components"; +import { BasedOn, Link, Next, QuickFormat, SemiHidden, UiPriority, UnhideWhenUsed } from "./components"; +import { Style } from "./style"; + +export class ParagraphStyle extends Style { + private readonly paragraphProperties: ParagraphProperties; + private readonly runProperties: RunProperties; + + constructor(styleId: string, name?: string) { + super({ type: "paragraph", styleId: styleId }, name); + this.paragraphProperties = new ParagraphProperties(); + this.runProperties = new RunProperties(); + this.root.push(this.paragraphProperties); + this.root.push(this.runProperties); + } + + public addParagraphProperty(property: XmlComponent): ParagraphStyle { + this.paragraphProperties.push(property); + return this; + } + + public addRunProperty(property: XmlComponent): ParagraphStyle { + this.runProperties.push(property); + return this; + } + + public basedOn(parentId: string): ParagraphStyle { + this.root.push(new BasedOn(parentId)); + return this; + } + + public quickFormat(): ParagraphStyle { + this.root.push(new QuickFormat()); + return this; + } + + public next(nextId: string): ParagraphStyle { + this.root.push(new Next(nextId)); + return this; + } + + // ---------- Run formatting ---------------------- // + + public size(twips: number): ParagraphStyle { + return this.addRunProperty(new formatting.Size(twips)).addRunProperty(new formatting.SizeComplexScript(twips)); + } + + public bold(): ParagraphStyle { + return this.addRunProperty(new formatting.Bold()); + } + + public italics(): ParagraphStyle { + return this.addRunProperty(new formatting.Italics()); + } + + public smallCaps(): ParagraphStyle { + return this.addRunProperty(new formatting.SmallCaps()); + } + + public allCaps(): ParagraphStyle { + return this.addRunProperty(new formatting.Caps()); + } + + public strike(): ParagraphStyle { + return this.addRunProperty(new formatting.Strike()); + } + + public doubleStrike(): ParagraphStyle { + return this.addRunProperty(new formatting.DoubleStrike()); + } + + public subScript(): ParagraphStyle { + return this.addRunProperty(new formatting.SubScript()); + } + + public superScript(): ParagraphStyle { + return this.addRunProperty(new formatting.SuperScript()); + } + + public underline(underlineType?: string, color?: string): ParagraphStyle { + return this.addRunProperty(new formatting.Underline(underlineType, color)); + } + + public color(color: string): ParagraphStyle { + return this.addRunProperty(new formatting.Color(color)); + } + + public font(fontName: string): ParagraphStyle { + return this.addRunProperty(new formatting.RunFonts(fontName)); + } + + public characterSpacing(value: number): ParagraphStyle { + return this.addRunProperty(new formatting.CharacterSpacing(value)); + } + + // --------------------- Paragraph formatting ------------------------ // + + public center(): ParagraphStyle { + return this.addParagraphProperty(new Alignment(AlignmentOptions.CENTER)); + } + + public left(): ParagraphStyle { + return this.addParagraphProperty(new Alignment(AlignmentOptions.LEFT)); + } + + public right(): ParagraphStyle { + return this.addParagraphProperty(new Alignment(AlignmentOptions.RIGHT)); + } + + public justified(): ParagraphStyle { + return this.addParagraphProperty(new Alignment(AlignmentOptions.BOTH)); + } + + public thematicBreak(): ParagraphStyle { + return this.addParagraphProperty(new ThematicBreak()); + } + + public maxRightTabStop(): ParagraphStyle { + return this.addParagraphProperty(new MaxRightTabStop()); + } + + public leftTabStop(position: number): ParagraphStyle { + return this.addParagraphProperty(new LeftTabStop(position)); + } + + public indent(attrs: object): ParagraphStyle { + return this.addParagraphProperty(new Indent(attrs)); + } + + public spacing(params: ISpacingProperties): ParagraphStyle { + return this.addParagraphProperty(new Spacing(params)); + } + + public keepNext(): ParagraphStyle { + return this.addParagraphProperty(new KeepNext()); + } + + public keepLines(): ParagraphStyle { + return this.addParagraphProperty(new KeepLines()); + } + + /*-------------- Style Properties -----------------*/ + + public link(link: string): ParagraphStyle { + this.root.push(new Link(link)); + return this; + } + + public semiHidden(): ParagraphStyle { + this.root.push(new SemiHidden()); + return this; + } + + public uiPriority(priority: string): ParagraphStyle { + this.root.push(new UiPriority(priority)); + return this; + } + + public unhideWhenUsed(): ParagraphStyle { + this.root.push(new UnhideWhenUsed()); + return this; + } +} diff --git a/src/file/styles/style/style.spec.ts b/src/file/styles/style/style.spec.ts new file mode 100644 index 0000000000..13b12ca021 --- /dev/null +++ b/src/file/styles/style/style.spec.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { Formatter } from "export/formatter"; +import { Style } from "./style"; + +describe("Style", () => { + describe("#constructor()", () => { + it("should set the given properties", () => { + const style = new Style({ + type: "paragraph", + styleId: "myStyleId", + default: true, + }); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId", "w:default": true } }], + }); + }); + + it("should set the name of the style, if given", () => { + const style = new Style( + { + type: "paragraph", + styleId: "myStyleId", + }, + "Style Name", + ); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:name": [{ _attr: { "w:val": "Style Name" } }] }, + ], + }); + }); + }); +}); diff --git a/src/file/styles/style/style.ts b/src/file/styles/style/style.ts new file mode 100644 index 0000000000..92df610e0d --- /dev/null +++ b/src/file/styles/style/style.ts @@ -0,0 +1,32 @@ +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { Name } from "./components"; + +export interface IStyleAttributes { + readonly type?: string; + readonly styleId?: string; + readonly default?: boolean; + readonly customStyle?: string; +} + +class StyleAttributes extends XmlAttributeComponent { + protected readonly xmlKeys = { + type: "w:type", + styleId: "w:styleId", + default: "w:default", + customStyle: "w:customStyle", + }; +} + +export class Style extends XmlComponent { + constructor(attributes: IStyleAttributes, name?: string) { + super("w:style"); + this.root.push(new StyleAttributes(attributes)); + if (name) { + this.root.push(new Name(name)); + } + } + + public push(styleSegment: XmlComponent): void { + this.root.push(styleSegment); + } +} diff --git a/src/file/styles/styles.spec.ts b/src/file/styles/styles.spec.ts index 8b7a8f793f..5476c0742d 100644 --- a/src/file/styles/styles.spec.ts +++ b/src/file/styles/styles.spec.ts @@ -2,8 +2,8 @@ import { assert, expect } from "chai"; import { Formatter } from "export/formatter"; -import { ParagraphStyle, Style } from "./style"; -import * as components from "./style/components"; +import { CharacterStyle, ParagraphStyle } from "./style"; + import { Styles } from "./styles"; describe("Styles", () => { @@ -22,7 +22,8 @@ describe("Styles", () => { describe("#createParagraphStyle", () => { it("should create a new paragraph style and push it onto this collection", () => { - styles.createParagraphStyle("pStyleId"); + const pStyle = styles.createParagraphStyle("pStyleId"); + expect(pStyle).to.instanceOf(ParagraphStyle); const tree = new Formatter().format(styles)["w:styles"].filter((x) => !x._attr); expect(tree).to.deep.equal([ { @@ -32,7 +33,8 @@ describe("Styles", () => { }); it("should set the paragraph name if given", () => { - styles.createParagraphStyle("pStyleId", "Paragraph Style"); + const pStyle = styles.createParagraphStyle("pStyleId", "Paragraph Style"); + expect(pStyle).to.instanceOf(ParagraphStyle); const tree = new Formatter().format(styles)["w:styles"].filter((x) => !x._attr); expect(tree).to.deep.equal([ { @@ -46,528 +48,59 @@ describe("Styles", () => { ]); }); }); -}); -describe("Style", () => { - describe("#constructor()", () => { - it("should set the given properties", () => { - const style = new Style({ - type: "paragraph", - styleId: "myStyleId", - default: true, - }); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId", "w:default": true } }], - }); - }); - - it("should set the name of the style, if given", () => { - const style = new Style( + describe("#createCharacterStyle", () => { + it("should create a new character style and push it onto this collection", () => { + const cStyle = styles.createCharacterStyle("pStyleId"); + expect(cStyle).to.instanceOf(CharacterStyle); + const tree = new Formatter().format(styles)["w:styles"].filter((x) => !x._attr); + expect(tree).to.deep.equal([ { - type: "paragraph", - styleId: "myStyleId", - }, - "Style Name", - ); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:name": [{ _attr: { "w:val": "Style Name" } }] }, - ], - }); - }); - }); -}); - -describe("Style components", () => { - it("Name#constructor", () => { - const style = new components.Name("Style Name"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ "w:name": [{ _attr: { "w:val": "Style Name" } }] }); - }); - - it("BasedOn#constructor", () => { - const style = new components.BasedOn("otherId"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ "w:basedOn": [{ _attr: { "w:val": "otherId" } }] }); - }); - - it("Next#constructor", () => { - const style = new components.Next("otherId"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ "w:next": [{ _attr: { "w:val": "otherId" } }] }); - }); - - it("Link#constructor", () => { - const style = new components.Link("otherId"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ "w:link": [{ _attr: { "w:val": "otherId" } }] }); - }); - - it("UiPriority#constructor", () => { - const style = new components.UiPriority("123"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ "w:uiPriority": [{ _attr: { "w:val": "123" } }] }); - }); -}); - -describe("ParagraphStyle", () => { - describe("#constructor", () => { - it("should set the style type to paragraph and use the given style id", () => { - const style = new ParagraphStyle("myStyleId"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:pPr": [] }, { "w:rPr": [] }], - }); - }); - - it("should set the name of the style, if given", () => { - const style = new ParagraphStyle("myStyleId", "Style Name"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:name": [{ _attr: { "w:val": "Style Name" } }] }, - { "w:pPr": [] }, - { "w:rPr": [] }, - ], - }); - }); - }); - - describe("formatting methods: style attributes", () => { - it("#basedOn", () => { - const style = new ParagraphStyle("myStyleId").basedOn("otherId"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { "w:rPr": [] }, - { "w:basedOn": [{ _attr: { "w:val": "otherId" } }] }, - ], - }); - }); - - it("#quickFormat", () => { - const style = new ParagraphStyle("myStyleId").quickFormat(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { "w:rPr": [] }, - { "w:qFormat": [] }, - ], - }); - }); - - it("#next", () => { - const style = new ParagraphStyle("myStyleId").next("otherId"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { "w:rPr": [] }, - { "w:next": [{ _attr: { "w:val": "otherId" } }] }, - ], - }); - }); - }); - - describe("formatting methods: paragraph properties", () => { - it("#indent", () => { - const style = new ParagraphStyle("myStyleId").indent({ left: 720 }); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [{ "w:ind": [{ _attr: { "w:left": 720 } }] }], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#spacing", () => { - const style = new ParagraphStyle("myStyleId").spacing({ before: 50, after: 150 }); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [{ "w:spacing": [{ _attr: { "w:before": 50, "w:after": 150 } }] }], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#center", () => { - const style = new ParagraphStyle("myStyleId").center(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [{ "w:jc": [{ _attr: { "w:val": "center" } }] }], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#character spacing", () => { - const style = new ParagraphStyle("myStyleId").characterSpacing(24); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:spacing": [{ _attr: { "w:val": 24 } }] }], - }, - ], - }); - }); - - it("#left", () => { - const style = new ParagraphStyle("myStyleId").left(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [{ "w:jc": [{ _attr: { "w:val": "left" } }] }], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#right", () => { - const style = new ParagraphStyle("myStyleId").right(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [{ "w:jc": [{ _attr: { "w:val": "right" } }] }], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#justified", () => { - const style = new ParagraphStyle("myStyleId").justified(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [{ "w:jc": [{ _attr: { "w:val": "both" } }] }], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#thematicBreak", () => { - const style = new ParagraphStyle("myStyleId").thematicBreak(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [ - { - "w:pBdr": [ - { - "w:bottom": [ - { - _attr: { - "w:color": "auto", - "w:space": "1", - "w:val": "single", - "w:sz": "6", - }, - }, - ], + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "pStyleId" } }, + { "w:rPr": [] }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", }, - ], - }, - ], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#leftTabStop", () => { - const style = new ParagraphStyle("myStyleId").leftTabStop(1200); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [ - { - "w:tabs": [{ "w:tab": [{ _attr: { "w:val": "left", "w:pos": 1200 } }] }], - }, - ], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#maxRightTabStop", () => { - const style = new ParagraphStyle("myStyleId").maxRightTabStop(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { - "w:pPr": [ - { - "w:tabs": [{ "w:tab": [{ _attr: { "w:val": "right", "w:pos": 9026 } }] }], - }, - ], - }, - { "w:rPr": [] }, - ], - }); - }); - - it("#keepLines", () => { - const style = new ParagraphStyle("myStyleId").keepLines(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [{ "w:keepLines": [] }] }, - { "w:rPr": [] }, - ], - }); - }); - - it("#keepNext", () => { - const style = new ParagraphStyle("myStyleId").keepNext(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [{ "w:keepNext": [] }] }, - { "w:rPr": [] }, - ], - }); - }); - }); - - describe("formatting methods: run properties", () => { - it("#size", () => { - const style = new ParagraphStyle("myStyleId").size(24); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:sz": [{ _attr: { "w:val": 24 } }] }, { "w:szCs": [{ _attr: { "w:val": 24 } }] }], - }, - ], - }); - }); - - it("#smallCaps", () => { - const style = new ParagraphStyle("myStyleId").smallCaps(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:smallCaps": [{ _attr: { "w:val": true } }] }], - }, - ], - }); - }); - - it("#allCaps", () => { - const style = new ParagraphStyle("myStyleId").allCaps(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:caps": [{ _attr: { "w:val": true } }] }], - }, - ], - }); - }); - - it("#strike", () => { - const style = new ParagraphStyle("myStyleId").strike(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:strike": [{ _attr: { "w:val": true } }] }], - }, - ], - }); - }); - - it("#doubleStrike", () => { - const style = new ParagraphStyle("myStyleId").doubleStrike(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:dstrike": [{ _attr: { "w:val": true } }] }], - }, - ], - }); - }); - - it("#subScript", () => { - const style = new ParagraphStyle("myStyleId").subScript(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:vertAlign": [{ _attr: { "w:val": "subscript" } }] }], - }, - ], - }); - }); - - it("#superScript", () => { - const style = new ParagraphStyle("myStyleId").superScript(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:vertAlign": [{ _attr: { "w:val": "superscript" } }] }], - }, - ], - }); - }); - - it("#font", () => { - const style = new ParagraphStyle("myStyleId").font("Times"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [ - { "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } }] }, - ], - }, - ], - }); - }); - - it("#bold", () => { - const style = new ParagraphStyle("myStyleId").bold(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:b": [{ _attr: { "w:val": true } }] }], - }, - ], - }); - }); - - it("#italics", () => { - const style = new ParagraphStyle("myStyleId").italics(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:i": [{ _attr: { "w:val": true } }] }], - }, - ], - }); - }); - - describe("#underline", () => { - it("should set underline to 'single' if no arguments are given", () => { - const style = new ParagraphStyle("myStyleId").underline(); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, + }, + ], + }, { - "w:rPr": [{ "w:u": [{ _attr: { "w:val": "single" } }] }], + "w:unhideWhenUsed": [], }, ], - }); - }); - - it("should set the style if given", () => { - const style = new ParagraphStyle("myStyleId").underline("double"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:u": [{ _attr: { "w:val": "double" } }] }], - }, - ], - }); - }); - - it("should set the style and color if given", () => { - const style = new ParagraphStyle("myStyleId").underline("double", "005599"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:u": [{ _attr: { "w:val": "double", "w:color": "005599" } }] }], - }, - ], - }); - }); + }, + ]); }); - it("#color", () => { - const style = new ParagraphStyle("myStyleId").color("123456"); - const tree = new Formatter().format(style); - expect(tree).to.deep.equal({ - "w:style": [ - { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, - { "w:pPr": [] }, - { - "w:rPr": [{ "w:color": [{ _attr: { "w:val": "123456" } }] }], - }, - ], - }); + it("should set the character name if given", () => { + const cStyle = styles.createCharacterStyle("pStyleId", "Character Style"); + expect(cStyle).to.instanceOf(CharacterStyle); + const tree = new Formatter().format(styles)["w:styles"].filter((x) => !x._attr); + expect(tree).to.deep.equal([ + { + "w:style": [ + { _attr: { "w:type": "character", "w:styleId": "pStyleId" } }, + { "w:name": [{ _attr: { "w:val": "Character Style" } }] }, + { "w:rPr": [] }, + { + "w:uiPriority": [ + { + _attr: { + "w:val": "99", + }, + }, + ], + }, + { + "w:unhideWhenUsed": [], + }, + ], + }, + ]); }); }); }); diff --git a/src/file/styles/styles.ts b/src/file/styles/styles.ts index d6cdbfc6f9..ff38f5ec6e 100644 --- a/src/file/styles/styles.ts +++ b/src/file/styles/styles.ts @@ -1,6 +1,6 @@ import { BaseXmlComponent, XmlComponent } from "file/xml-components"; import { DocumentDefaults } from "./defaults"; -import { ParagraphStyle } from "./style"; +import { CharacterStyle, ParagraphStyle } from "./style"; export * from "./border"; export class Styles extends XmlComponent { @@ -23,8 +23,14 @@ export class Styles extends XmlComponent { } public createParagraphStyle(styleId: string, name?: string): ParagraphStyle { - const para = new ParagraphStyle(styleId, name); - this.push(para); - return para; + const paragraphStyle = new ParagraphStyle(styleId, name); + this.push(paragraphStyle); + return paragraphStyle; + } + + public createCharacterStyle(styleId: string, name?: string): CharacterStyle { + const characterStyle = new CharacterStyle(styleId, name); + this.push(characterStyle); + return characterStyle; } } diff --git a/src/file/table/table-cell/table-cell-properties.spec.ts b/src/file/table/table-cell/table-cell-properties.spec.ts index 0884d28da7..504a0549d0 100644 --- a/src/file/table/table-cell/table-cell-properties.spec.ts +++ b/src/file/table/table-cell/table-cell-properties.spec.ts @@ -1,6 +1,7 @@ import { expect } from "chai"; import { Formatter } from "export/formatter"; +import { BorderStyle } from "file/styles"; import { VerticalAlign, VMergeType, WidthType } from "./table-cell-components"; import { TableCellProperties } from "./table-cell-properties"; @@ -42,12 +43,19 @@ describe("TableCellProperties", () => { }); describe("#setWidth", () => { - it("sets width", () => { + it("should set width", () => { const cellMargain = new TableCellProperties(); cellMargain.setWidth(1, WidthType.DXA); const tree = new Formatter().format(cellMargain); expect(tree).to.deep.equal({ "w:tcPr": [{ "w:tcW": [{ _attr: { "w:type": "dxa", "w:w": 1 } }] }] }); }); + + it("should set width using default of AUTO", () => { + const cellMargain = new TableCellProperties(); + cellMargain.setWidth(1); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tcPr": [{ "w:tcW": [{ _attr: { "w:type": "auto", "w:w": 1 } }] }] }); + }); }); describe("#setShading", () => { @@ -61,4 +69,18 @@ describe("TableCellProperties", () => { expect(tree).to.deep.equal({ "w:tcPr": [{ "w:shd": [{ _attr: { "w:fill": "test", "w:color": "000" } }] }] }); }); }); + + describe("#Borders", () => { + it("should return the TableCellBorders if Border has borders", () => { + const cellMargain = new TableCellProperties(); + cellMargain.Borders.addTopBorder(BorderStyle.DASH_DOT_STROKED, 3, "red"); + const borders = cellMargain.Borders; + + const tree = new Formatter().format(borders); + + expect(tree).to.deep.equal({ + "w:tcBorders": [{ "w:top": [{ _attr: { "w:val": "dashDotStroked", "w:sz": 3, "w:color": "red" } }] }], + }); + }); + }); }); diff --git a/src/file/table/table-cell/table-cell-properties.ts b/src/file/table/table-cell/table-cell-properties.ts index 1f653b53bb..2d8d45ed2d 100644 --- a/src/file/table/table-cell/table-cell-properties.ts +++ b/src/file/table/table-cell/table-cell-properties.ts @@ -44,7 +44,7 @@ export class TableCellProperties extends XmlComponent { return this; } - public setWidth(width: string | number, type: WidthType): TableCellProperties { + public setWidth(width: string | number, type: WidthType = WidthType.AUTO): TableCellProperties { this.root.push(new TableCellWidth(width, type)); return this; diff --git a/src/file/table/table-cell/table-cell.ts b/src/file/table/table-cell/table-cell.ts index 60673ec87a..3526e788bd 100644 --- a/src/file/table/table-cell/table-cell.ts +++ b/src/file/table/table-cell/table-cell.ts @@ -3,6 +3,7 @@ import { Paragraph } from "file/paragraph"; import { IXmlableObject, XmlComponent } from "file/xml-components"; import { Table } from "../table"; +import { TableCellBorders, VerticalAlign } from "./table-cell-components"; import { TableCellProperties } from "./table-cell-properties"; export class TableCell extends XmlComponent { @@ -14,7 +15,12 @@ export class TableCell extends XmlComponent { this.root.push(this.properties); } - public addContent(content: Paragraph | Table): TableCell { + public addParagraph(content: Paragraph): TableCell { + this.root.push(content); + return this; + } + + public addTable(content: Table): TableCell { this.root.push(content); return this; } @@ -35,11 +41,24 @@ export class TableCell extends XmlComponent { public createParagraph(text?: string): Paragraph { const para = new Paragraph(text); - this.addContent(para); + this.addParagraph(para); + return para; } - public get CellProperties(): TableCellProperties { - return this.properties; + public setVerticalAlign(type: VerticalAlign): TableCell { + this.properties.setVerticalAlign(type); + + return this; + } + + public addGridSpan(cellSpan: number): TableCell { + this.properties.addGridSpan(cellSpan); + + return this; + } + + public get Borders(): TableCellBorders { + return this.properties.Borders; } } diff --git a/src/file/table/table-properties/table-properties.spec.ts b/src/file/table/table-properties/table-properties.spec.ts index 8b9656d643..dbd80004eb 100644 --- a/src/file/table/table-properties/table-properties.spec.ts +++ b/src/file/table/table-properties/table-properties.spec.ts @@ -15,13 +15,21 @@ describe("TableProperties", () => { }); describe("#setWidth", () => { - it("adds a table width property", () => { - const tp = new TableProperties().setWidth(WidthType.DXA, 1234); + it("should add a table width property", () => { + const tp = new TableProperties().setWidth(1234, WidthType.DXA); const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [{ "w:tblW": [{ _attr: { "w:type": "dxa", "w:w": 1234 } }] }], }); }); + + it("should add a table width property with default of AUTO", () => { + const tp = new TableProperties().setWidth(1234); + const tree = new Formatter().format(tp); + expect(tree).to.deep.equal({ + "w:tblPr": [{ "w:tblW": [{ _attr: { "w:type": "auto", "w:w": 1234 } }] }], + }); + }); }); describe("#setFixedWidthLayout", () => { diff --git a/src/file/table/table-properties/table-properties.ts b/src/file/table/table-properties/table-properties.ts index f8198eea59..c6184d70c1 100644 --- a/src/file/table/table-properties/table-properties.ts +++ b/src/file/table/table-properties/table-properties.ts @@ -17,8 +17,8 @@ export class TableProperties extends XmlComponent { this.root.push(this.cellMargin); } - public setWidth(type: WidthType, w: number | string): TableProperties { - this.root.push(new PreferredTableWidth(type, w)); + public setWidth(width: number | string, type: WidthType = WidthType.AUTO): TableProperties { + this.root.push(new PreferredTableWidth(type, width)); return this; } diff --git a/src/file/table/table-row/table-row.ts b/src/file/table/table-row/table-row.ts index d1ced91c4a..3a08197cf7 100644 --- a/src/file/table/table-row/table-row.ts +++ b/src/file/table/table-row/table-row.ts @@ -25,7 +25,7 @@ export class TableRow extends XmlComponent { public addGridSpan(index: number, cellSpan: number): TableCell { const remainCell = this.cells[index]; - remainCell.CellProperties.addGridSpan(cellSpan); + remainCell.addGridSpan(cellSpan); this.cells.splice(index + 1, cellSpan - 1); this.root.splice(index + 2, cellSpan - 1); diff --git a/src/file/table/table.spec.ts b/src/file/table/table.spec.ts index f1ddfe0ed6..2d5388725c 100644 --- a/src/file/table/table.spec.ts +++ b/src/file/table/table.spec.ts @@ -111,19 +111,19 @@ describe("Table", () => { table .getRow(0) .getCell(0) - .addContent(new Paragraph("A1")); + .addParagraph(new Paragraph("A1")); table .getRow(0) .getCell(1) - .addContent(new Paragraph("B1")); + .addParagraph(new Paragraph("B1")); table .getRow(1) .getCell(0) - .addContent(new Paragraph("A2")); + .addParagraph(new Paragraph("A2")); table .getRow(1) .getCell(1) - .addContent(new Paragraph("B2")); + .addParagraph(new Paragraph("B2")); const tree = new Formatter().format(table); const cell = (c) => ({ "w:tc": [ @@ -149,10 +149,10 @@ describe("Table", () => { describe("#getCell", () => { it("returns the correct cell", () => { const table = new Table(2, 2); - table.getCell(0, 0).addContent(new Paragraph("A1")); - table.getCell(0, 1).addContent(new Paragraph("B1")); - table.getCell(1, 0).addContent(new Paragraph("A2")); - table.getCell(1, 1).addContent(new Paragraph("B2")); + table.getCell(0, 0).addParagraph(new Paragraph("A1")); + table.getCell(0, 1).addParagraph(new Paragraph("B1")); + table.getCell(1, 0).addParagraph(new Paragraph("A2")); + table.getCell(1, 1).addParagraph(new Paragraph("B2")); const tree = new Formatter().format(table); const cell = (c) => ({ "w:tc": [ @@ -176,8 +176,8 @@ describe("Table", () => { }); describe("#setWidth", () => { - it("sets the preferred width on the table", () => { - const table = new Table(2, 2).setWidth(WidthType.PERCENTAGE, 1000); + it("should set the preferred width on the table", () => { + const table = new Table(2, 2).setWidth(1000, WidthType.PERCENTAGE); const tree = new Formatter().format(table); expect(tree) .to.have.property("w:tbl") @@ -187,6 +187,15 @@ describe("Table", () => { "w:tblPr": [DEFAULT_TABLE_PROPERTIES, { "w:tblW": [{ _attr: { "w:type": "pct", "w:w": 1000 } }] }], }); }); + + it("sets the preferred width on the table with a default of AUTO", () => { + const table = new Table(2, 2).setWidth(1000); + const tree = new Formatter().format(table); + + expect(tree["w:tbl"][0]).to.deep.equal({ + "w:tblPr": [DEFAULT_TABLE_PROPERTIES, { "w:tblW": [{ _attr: { "w:type": "auto", "w:w": 1000 } }] }], + }); + }); }); describe("#setFixedWidthLayout", () => { @@ -223,7 +232,7 @@ describe("Table", () => { it("inserts a paragraph at the end of the cell even if it has a child table", () => { const parentTable = new Table(1, 1); - parentTable.getCell(0, 0).addContent(new Table(1, 1)); + parentTable.getCell(0, 0).addTable(new Table(1, 1)); const tree = new Formatter().format(parentTable); expect(tree) .to.have.property("w:tbl") @@ -242,7 +251,7 @@ describe("Table", () => { it("does not insert a paragraph if it already ends with one", () => { const parentTable = new Table(1, 1); - parentTable.getCell(0, 0).addContent(new Paragraph("Hello")); + parentTable.getCell(0, 0).addParagraph(new Paragraph("Hello")); const tree = new Formatter().format(parentTable); expect(tree) .to.have.property("w:tbl") diff --git a/src/file/table/table.ts b/src/file/table/table.ts index 8311b5ecfd..6b81d3991f 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -65,8 +65,8 @@ export class Table extends XmlComponent { return this.getRow(row).getCell(col); } - public setWidth(type: WidthType, width: number | string): Table { - this.properties.setWidth(type, width); + public setWidth(width: number | string, type: WidthType = WidthType.AUTO): Table { + this.properties.setWidth(width, type); return this; } diff --git a/src/import-dotx/import-dotx.ts b/src/import-dotx/import-dotx.ts index 9ece4db3f1..fdfedc7304 100644 --- a/src/import-dotx/import-dotx.ts +++ b/src/import-dotx/import-dotx.ts @@ -5,11 +5,11 @@ import { FooterReferenceType } from "file/document/body/section-properties/foote import { HeaderReferenceType } from "file/document/body/section-properties/header-reference"; import { FooterWrapper, IDocumentFooter } from "file/footer-wrapper"; import { HeaderWrapper, IDocumentHeader } from "file/header-wrapper"; -import { convertToXmlComponent, ImportedXmlComponent } from "file/xml-components"; - import { Media } from "file/media"; +import { TargetModeType } from "file/relationships/relationship/relationship"; import { Styles } from "file/styles"; import { ExternalStylesFactory } from "file/styles/external-styles-factory"; +import { convertToXmlComponent, ImportedXmlComponent } from "file/xml-components"; const schemeToType = { "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header": "header", @@ -23,10 +23,17 @@ interface IDocumentRefs { readonly footers: Array<{ readonly id: number; readonly type: FooterReferenceType }>; } +enum RelationshipType { + HEADER = "header", + FOOTER = "footer", + IMAGE = "image", + HYPERLINK = "hyperlink", +} + interface IRelationshipFileInfo { readonly id: number; readonly target: string; - readonly type: "header" | "footer" | "image" | "hyperlink"; + readonly type: RelationshipType; } // Document Template @@ -51,19 +58,69 @@ export class ImportDotx { const zipContent = await JSZip.loadAsync(data); const stylesContent = await zipContent.files["word/styles.xml"].async("text"); + const documentContent = await zipContent.files["word/document.xml"].async("text"); + const relationshipContent = await zipContent.files["word/_rels/document.xml.rels"].async("text"); + const stylesFactory = new ExternalStylesFactory(); - const styles = stylesFactory.newInstance(stylesContent); - - const documentContent = zipContent.files["word/document.xml"]; - const documentRefs: IDocumentRefs = this.extractDocumentRefs(await documentContent.async("text")); - const titlePageIsDefined = this.titlePageIsDefined(await documentContent.async("text")); - - const relationshipContent = zipContent.files["word/_rels/document.xml.rels"]; - const documentRelationships: IRelationshipFileInfo[] = this.findReferenceFiles(await relationshipContent.async("text")); + const documentRefs = this.extractDocumentRefs(documentContent); + const documentRelationships = this.findReferenceFiles(relationshipContent); const media = new Media(); + const templateDocument: IDocumentTemplate = { + headers: await this.createHeaders(zipContent, documentRefs, documentRelationships, media), + footers: await this.createFooters(zipContent, documentRefs, documentRelationships, media), + currentRelationshipId: this.currentRelationshipId, + styles: stylesFactory.newInstance(stylesContent), + titlePageIsDefined: this.checkIfTitlePageIsDefined(documentContent), + }; + + return templateDocument; + } + + private async createFooters( + zipContent: JSZip, + documentRefs: IDocumentRefs, + documentRelationships: IRelationshipFileInfo[], + media: Media, + ): Promise { + const footers: IDocumentFooter[] = []; + + for (const footerRef of documentRefs.footers) { + const relationFileInfo = documentRelationships.find((rel) => rel.id === footerRef.id); + + if (relationFileInfo === null || !relationFileInfo) { + throw new Error(`Can not find target file for id ${footerRef.id}`); + } + + const xmlData = await zipContent.files[`word/${relationFileInfo.target}`].async("text"); + const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement; + let footerXmlElement: XMLElement | undefined; + for (const xmlElm of xmlObj.elements || []) { + if (xmlElm.name === "w:ftr") { + footerXmlElement = xmlElm; + } + } + if (footerXmlElement === undefined) { + continue; + } + const importedComp = convertToXmlComponent(footerXmlElement) as ImportedXmlComponent; + const footer = new FooterWrapper(media, this.currentRelationshipId++, importedComp); + await this.addRelationshipToWrapper(relationFileInfo, zipContent, footer, media); + footers.push({ type: footerRef.type, footer }); + } + + return footers; + } + + private async createHeaders( + zipContent: JSZip, + documentRefs: IDocumentRefs, + documentRelationships: IRelationshipFileInfo[], + media: Media, + ): Promise { const headers: IDocumentHeader[] = []; + for (const headerRef of documentRefs.headers) { const relationFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id); if (relationFileInfo === null || !relationFileInfo) { @@ -83,66 +140,52 @@ export class ImportDotx { } const importedComp = convertToXmlComponent(headerXmlElement) as ImportedXmlComponent; const header = new HeaderWrapper(media, this.currentRelationshipId++, importedComp); - await this.addRelationToWrapper(relationFileInfo, zipContent, header); + // await this.addMedia(zipContent, media, documentRefs, documentRelationships); + await this.addRelationshipToWrapper(relationFileInfo, zipContent, header, media); headers.push({ type: headerRef.type, header }); } - const footers: IDocumentFooter[] = []; - for (const footerRef of documentRefs.footers) { - const relationFileInfo = documentRelationships.find((rel) => rel.id === footerRef.id); - if (relationFileInfo === null || !relationFileInfo) { - throw new Error(`Can not find target file for id ${footerRef.id}`); - } - const xmlData = await zipContent.files[`word/${relationFileInfo.target}`].async("text"); - const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement; - let footerXmlElement: XMLElement | undefined; - for (const xmlElm of xmlObj.elements || []) { - if (xmlElm.name === "w:ftr") { - footerXmlElement = xmlElm; - } - } - if (footerXmlElement === undefined) { - continue; - } - const importedComp = convertToXmlComponent(footerXmlElement) as ImportedXmlComponent; - const footer = new FooterWrapper(media, this.currentRelationshipId++, importedComp); - await this.addRelationToWrapper(relationFileInfo, zipContent, footer); - footers.push({ type: footerRef.type, footer }); - } - - const templateDocument: IDocumentTemplate = { - headers, - footers, - currentRelationshipId: this.currentRelationshipId, - styles, - titlePageIsDefined, - }; - return templateDocument; + return headers; } - public async addRelationToWrapper( + private async addRelationshipToWrapper( relationhipFile: IRelationshipFileInfo, zipContent: JSZip, wrapper: HeaderWrapper | FooterWrapper, + media: Media, ): Promise { - let wrapperImagesReferences: IRelationshipFileInfo[] = []; - let hyperLinkReferences: IRelationshipFileInfo[] = []; const refFile = zipContent.files[`word/_rels/${relationhipFile.target}.rels`]; - if (refFile) { - const xmlRef = await refFile.async("text"); - wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "image"); - hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "hyperlink"); + + if (!refFile) { + return; } + + const xmlRef = await refFile.async("text"); + const wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === RelationshipType.IMAGE); + const hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === RelationshipType.HYPERLINK); + for (const r of wrapperImagesReferences) { const buffer = await zipContent.files[`word/${r.target}`].async("nodebuffer"); - wrapper.addImageRelationship(buffer, r.id); + const mediaData = media.addMedia(buffer); + + wrapper.Relationships.createRelationship( + r.id, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); } + for (const r of hyperLinkReferences) { - wrapper.addHyperlinkRelationship(r.target, r.id, "External"); + wrapper.Relationships.createRelationship( + r.id, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + r.target, + TargetModeType.EXTERNAL, + ); } } - public findReferenceFiles(xmlData: string): IRelationshipFileInfo[] { + private findReferenceFiles(xmlData: string): IRelationshipFileInfo[] { const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const relationXmlArray = Array.isArray(xmlObj.Relationships.Relationship) ? xmlObj.Relationships.Relationship @@ -162,7 +205,7 @@ export class ImportDotx { return relationships; } - public extractDocumentRefs(xmlData: string): IDocumentRefs { + private extractDocumentRefs(xmlData: string): IDocumentRefs { const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; @@ -208,13 +251,14 @@ export class ImportDotx { return { headers, footers }; } - public titlePageIsDefined(xmlData: string): boolean { + private checkIfTitlePageIsDefined(xmlData: string): boolean { const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; + return sectionProp["w:titlePg"] !== undefined; } - public parseRefId(str: string): number { + private parseRefId(str: string): number { const match = /^rId(\d+)$/.exec(str); if (match === null) { throw new Error("Invalid ref id");