Merge branch 'master' into feat/math

This commit is contained in:
Dolan
2020-10-08 10:25:45 +01:00
151 changed files with 7913 additions and 3690 deletions

2
.nvmrc
View File

@ -1 +1 @@
v8 v10

11
.nycrc
View File

@ -1,14 +1,15 @@
{ {
"check-coverage": true, "check-coverage": true,
"lines": 92.35, "lines": 96.81,
"functions": 88.28, "functions": 93.80,
"branches": 84.64, "branches": 92.63,
"statements": 92.16, "statements": 96.80,
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"
], ],
"exclude": [ "exclude": [
"src/**/*.spec.ts" "src/**/*.spec.ts",
"src/import-dotx/import-dotx.ts"
], ],
"reporter": [ "reporter": [
"lcov", "lcov",

View File

@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- 9 - 10
install: install:
- npm install - npm install
- npm install -g codecov - npm install -g codecov
@ -10,7 +10,7 @@ script:
- npm run style - npm run style
- npm run build - npm run build
- npm run ts-node -- ./demo/1-basic.ts - npm run ts-node -- ./demo/1-basic.ts
- npm run e2e "My Document.docx" # - npm run e2e "My Document.docx"
- npm run ts-node -- ./demo/2-declaritive-styles.ts - npm run ts-node -- ./demo/2-declaritive-styles.ts
- npm run ts-node -- ./demo/3-numbering-and-bullet-points.ts - npm run ts-node -- ./demo/3-numbering-and-bullet-points.ts
- npm run ts-node -- ./demo/4-basic-table.ts - npm run ts-node -- ./demo/4-basic-table.ts
@ -20,7 +20,7 @@ script:
- npm run ts-node -- ./demo/8-header-footer.ts - npm run ts-node -- ./demo/8-header-footer.ts
- npm run ts-node -- ./demo/9-images-in-header-and-footer.ts - npm run ts-node -- ./demo/9-images-in-header-and-footer.ts
- npm run ts-node -- ./demo/10-my-cv.ts - npm run ts-node -- ./demo/10-my-cv.ts
- npm run e2e "My Document.docx" # - npm run e2e "My Document.docx"
- npm run ts-node -- ./demo/11-declaritive-styles-2.ts - npm run ts-node -- ./demo/11-declaritive-styles-2.ts
- npm run ts-node -- ./demo/12-scaling-images.ts - npm run ts-node -- ./demo/12-scaling-images.ts
- npm run ts-node -- ./demo/13-xml-styles.ts - npm run ts-node -- ./demo/13-xml-styles.ts

View File

@ -18,7 +18,7 @@
[![codecov][codecov-image]][codecov-url] [![codecov][codecov-image]][codecov-url]
<p align="center"> <p align="center">
<img src="https://i.imgur.com/H5FA1Qy.gif" alt="drawing" width="800"/> <img src="https://i.imgur.com/TCH0YzD.png" alt="drawing" width="800"/>
</p> </p>
# Demo # Demo
@ -27,13 +27,22 @@
Here are examples of `docx` being used with basic `HTML/JS` in a browser environment: Here are examples of `docx` being used with basic `HTML/JS` in a browser environment:
* https://codepen.io/anon/pen/dqoVgQ * https://codepen.io/dolanmiu/pen/RwNeObg
* https://jsfiddle.net/3xhezb5w/2 * https://jsfiddle.net/dolanmiu/kqxrj35u/1/
Here is an example of `docx` working in `Angular`: Here is an example of `docx` working in `Angular`:
* https://stackblitz.com/edit/angular-afvxtz * https://stackblitz.com/edit/angular-afvxtz
Here is an example of `docx` working in `React`:
* https://stackblitz.com/edit/react-ts-qq25sp
* https://stackblitz.com/edit/react-ts-qdqu7z (adding images to Word Document)
Here is an example of `docx` working in `Vue.js`:
* https://stackblitz.com/edit/vuejs-docx
## Node ## Node
Press `endpoint` on the `RunKit` website: Press `endpoint` on the `RunKit` website:
@ -50,7 +59,7 @@ Press `endpoint` on the `RunKit` website:
* https://runkit.com/dolanmiu/docx-demo8 - Header and Footer * https://runkit.com/dolanmiu/docx-demo8 - Header and Footer
* https://runkit.com/dolanmiu/docx-demo10 - **My CV generated with docx** * https://runkit.com/dolanmiu/docx-demo10 - **My CV generated with docx**
More [here](https://docx.js.org/#/examples) and [here](https://github.com/dolanmiu/docx/tree/master/demo) More [here](https://github.com/dolanmiu/docx/tree/master/demo)
# How to use & Documentation # How to use & Documentation
@ -58,7 +67,7 @@ Please refer to the [documentation at https://docx.js.org/](https://docx.js.org/
# Examples # Examples
Check the `examples` section in the [documentation](https://docx.js.org/#/examples) and the [demo folder](https://github.com/dolanmiu/docx/tree/master/demo) for examples. Check the [demo folder](https://github.com/dolanmiu/docx/tree/master/demo) for examples.
# Contributing # Contributing
@ -73,6 +82,10 @@ Read the contribution guidelines [here](https://docx.js.org/#/contribution-guide
[<img src="https://i.imgur.com/suiH2zc.png" alt="drawing" height="50"/>](https://www.dabblewriter.com/) [<img src="https://i.imgur.com/suiH2zc.png" alt="drawing" height="50"/>](https://www.dabblewriter.com/)
[<img src="https://i.imgur.com/1LjuK2M.png" alt="drawing" height="50"/>](https://turbopatent.com/) [<img src="https://i.imgur.com/1LjuK2M.png" alt="drawing" height="50"/>](https://turbopatent.com/)
[<img src="https://i.imgur.com/dHMg0wF.gif" alt="drawing" height="50"/>](http://www.madisoncres.com/) [<img src="https://i.imgur.com/dHMg0wF.gif" alt="drawing" height="50"/>](http://www.madisoncres.com/)
[<img src="https://i.imgur.com/QEZXU5b.png" alt="drawing" height="50"/>](https://www.beekast.com/)
[<img src="https://imgur.com/XVU6aoi.png" alt="drawing" height="50"/>](https://herraizsoto.com/)
[<img src="https://i.imgur.com/fn1xccG.png" alt="drawing" height="50"/>](http://www.ativer.com.br/)
...and many more! ...and many more!

View File

@ -16,9 +16,9 @@ doc.addSection({
bold: true, bold: true,
}), }),
new TextRun({ new TextRun({
text: "Github is the best", text: "\tGithub is the best",
bold: true, bold: true,
}).tab(), }),
], ],
}), }),
], ],

View File

@ -238,9 +238,9 @@ class DocumentCreator {
bold: true, bold: true,
}), }),
new TextRun({ new TextRun({
text: dateText, text: `\t${dateText}`,
bold: true, bold: true,
}).tab(), }),
], ],
}); });
} }

View File

@ -1,7 +1,7 @@
// Page numbers // Page numbers
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { AlignmentType, Document, Header, Packer, PageBreak, Paragraph, TextRun } from "../build"; import { AlignmentType, Document, Header, Packer, PageBreak, PageNumber, Paragraph, TextRun } from "../build";
const doc = new Document(); const doc = new Document();
@ -11,7 +11,12 @@ doc.addSection({
children: [ children: [
new Paragraph({ new Paragraph({
alignment: AlignmentType.RIGHT, alignment: AlignmentType.RIGHT,
children: [new TextRun("My Title "), new TextRun("Page ").pageNumber()], children: [
new TextRun("My Title "),
new TextRun({
children: ["Page ", PageNumber.CURRENT],
}),
],
}), }),
], ],
}), }),
@ -19,7 +24,12 @@ doc.addSection({
children: [ children: [
new Paragraph({ new Paragraph({
alignment: AlignmentType.RIGHT, alignment: AlignmentType.RIGHT,
children: [new TextRun("First Page Header "), new TextRun("Page ").pageNumber()], children: [
new TextRun("First Page Header "),
new TextRun({
children: ["Page ", PageNumber.CURRENT],
}),
],
}), }),
], ],
}), }),

View File

@ -1,7 +1,7 @@
// Multiple sections and headers // Multiple sections and headers
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, Footer, Header, Packer, PageNumberFormat, PageOrientation, Paragraph, TextRun } from "../build"; import { Document, Footer, Header, Packer, PageNumber, PageNumberFormat, PageOrientation, Paragraph, TextRun } from "../build";
const doc = new Document(); const doc = new Document();
@ -53,7 +53,11 @@ doc.addSection({
default: new Header({ default: new Header({
children: [ children: [
new Paragraph({ new Paragraph({
children: [new TextRun("Page number: ").pageNumber()], children: [
new TextRun({
children: ["Page number: ", PageNumber.CURRENT],
}),
],
}), }),
], ],
}), }),
@ -69,7 +73,11 @@ doc.addSection({
default: new Header({ default: new Header({
children: [ children: [
new Paragraph({ new Paragraph({
children: [new TextRun("Page number: ").pageNumber()], children: [
new TextRun({
children: ["Page number: ", PageNumber.CURRENT],
}),
],
}), }),
], ],
}), }),
@ -90,7 +98,11 @@ doc.addSection({
default: new Header({ default: new Header({
children: [ children: [
new Paragraph({ new Paragraph({
children: [new TextRun("Page number: ").pageNumber()], children: [
new TextRun({
children: ["Page number: ", PageNumber.CURRENT],
}),
],
}), }),
], ],
}), }),

View File

@ -1,22 +1,54 @@
// Footnotes // Footnotes
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, Packer, Paragraph, TextRun } from "../build"; import { Document, FootnoteReferenceRun, Packer, Paragraph, TextRun } from "../build";
const doc = new Document(); const doc = new Document({
footnotes: [
new Paragraph("Foo"),
new Paragraph("Test"),
new Paragraph("My amazing reference"),
new Paragraph("Foo1"),
new Paragraph("Test1"),
new Paragraph("My amazing reference1"),
],
});
doc.addSection({ doc.addSection({
children: [ children: [
new Paragraph({ new Paragraph({
children: [new TextRun("Hello").referenceFootnote(1), new TextRun(" World!").referenceFootnote(2)], children: [
new TextRun({
children: ["Hello", new FootnoteReferenceRun(1)],
}),
new TextRun({
children: [" World!", new FootnoteReferenceRun(2)],
}),
],
}),
new Paragraph({
children: [new TextRun("Hello World"), new FootnoteReferenceRun(3)],
}), }),
new Paragraph("Hello World").referenceFootnote(3),
], ],
}); });
doc.createFootnote(new Paragraph("Foo")); doc.addSection({
doc.createFootnote(new Paragraph("Test")); children: [
doc.createFootnote(new Paragraph("My amazing reference")); new Paragraph({
children: [
new TextRun({
children: ["Hello", new FootnoteReferenceRun(4)],
}),
new TextRun({
children: [" World!", new FootnoteReferenceRun(5)],
}),
],
}),
new Paragraph({
children: [new TextRun("Hello World"), new FootnoteReferenceRun(6)],
}),
],
});
Packer.toBuffer(doc).then((buffer) => { Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer); fs.writeFileSync("My Document.docx", buffer);

View File

@ -15,9 +15,9 @@ doc.addSection({
bold: true, bold: true,
}), }),
new TextRun({ new TextRun({
text: "Bar", text: "\tBar",
bold: true, bold: true,
}).tab(), }),
], ],
}), }),
], ],

View File

@ -1,7 +1,7 @@
// Example on how to customise the look at feel using Styles // Example on how to customise the look at feel using Styles
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, HeadingLevel, Packer, Paragraph, TextRun, UnderlineType } from "../build"; import { AlignmentType, Document, HeadingLevel, Packer, Paragraph, TextRun, UnderlineType } from "../build";
const doc = new Document({ const doc = new Document({
creator: "Clippy", creator: "Clippy",
@ -83,15 +83,23 @@ const doc = new Document({
}, },
], ],
}, },
numbering: {
config: [
{
reference: "my-crazy-numbering",
levels: [
{
level: 0,
format: "lowerLetter",
text: "%1)",
alignment: AlignmentType.LEFT,
},
],
},
],
},
}); });
const numberedAbstract = doc.Numbering.createAbstractNumbering();
numberedAbstract.createLevel(0, "lowerLetter", "%1)", "left");
const letterNumbering = doc.Numbering.createConcreteNumbering(numberedAbstract);
const letterNumbering5 = doc.Numbering.createConcreteNumbering(numberedAbstract);
letterNumbering5.overrideLevel(0, 5);
doc.addSection({ doc.addSection({
children: [ children: [
new Paragraph({ new Paragraph({
@ -106,21 +114,21 @@ doc.addSection({
new Paragraph({ new Paragraph({
text: "Option1", text: "Option1",
numbering: { numbering: {
num: letterNumbering, reference: "my-crazy-numbering",
level: 0, level: 0,
}, },
}), }),
new Paragraph({ new Paragraph({
text: "Option5 -- override 2 to 5", text: "Option5 -- override 2 to 5",
numbering: { numbering: {
num: letterNumbering, reference: "my-crazy-numbering",
level: 0, level: 0,
}, },
}), }),
new Paragraph({ new Paragraph({
text: "Option3", text: "Option3",
numbering: { numbering: {
num: letterNumbering, reference: "my-crazy-numbering",
level: 0, level: 0,
}, },
}), }),
@ -153,6 +161,10 @@ doc.addSection({
text: "and then underlined ", text: "and then underlined ",
underline: {}, underline: {},
}), }),
new TextRun({
text: "and then emphasis-mark ",
emphasisMark: {},
}),
new TextRun({ new TextRun({
text: "and back to normal.", text: "and back to normal.",
}), }),

View File

@ -1,7 +1,7 @@
// This demo shows how to create bookmarks then link to them with internal hyperlinks // This demo shows how to create bookmarks then link to them with internal hyperlinks
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, HeadingLevel, Packer, PageBreak, Paragraph } from "../build"; import { Bookmark, Document, HeadingLevel, HyperlinkRef, HyperlinkType, Packer, PageBreak, Paragraph } from "../build";
const LOREM_IPSUM = const LOREM_IPSUM =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mi velit, convallis convallis scelerisque nec, faucibus nec leo. Phasellus at posuere mauris, tempus dignissim velit. Integer et tortor dolor. Duis auctor efficitur mattis. Vivamus ut metus accumsan tellus auctor sollicitudin venenatis et nibh. Cras quis massa ac metus fringilla venenatis. Proin rutrum mauris purus, ut suscipit magna consectetur id. Integer consectetur sollicitudin ante, vitae faucibus neque efficitur in. Praesent ultricies nibh lectus. Mauris pharetra id odio eget iaculis. Duis dictum, risus id pellentesque rutrum, lorem quam malesuada massa, quis ullamcorper turpis urna a diam. Cras vulputate metus vel massa porta ullamcorper. Etiam porta condimentum nulla nec tristique. Sed nulla urna, pharetra non tortor sed, sollicitudin molestie diam. Maecenas enim leo, feugiat eget vehicula id, sollicitudin vitae ante."; "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mi velit, convallis convallis scelerisque nec, faucibus nec leo. Phasellus at posuere mauris, tempus dignissim velit. Integer et tortor dolor. Duis auctor efficitur mattis. Vivamus ut metus accumsan tellus auctor sollicitudin venenatis et nibh. Cras quis massa ac metus fringilla venenatis. Proin rutrum mauris purus, ut suscipit magna consectetur id. Integer consectetur sollicitudin ante, vitae faucibus neque efficitur in. Praesent ultricies nibh lectus. Mauris pharetra id odio eget iaculis. Duis dictum, risus id pellentesque rutrum, lorem quam malesuada massa, quis ullamcorper turpis urna a diam. Cras vulputate metus vel massa porta ullamcorper. Etiam porta condimentum nulla nec tristique. Sed nulla urna, pharetra non tortor sed, sollicitudin molestie diam. Maecenas enim leo, feugiat eget vehicula id, sollicitudin vitae ante.";
@ -10,19 +10,19 @@ const doc = new Document({
creator: "Clippy", creator: "Clippy",
title: "Sample Document", title: "Sample Document",
description: "A brief example of using docx with bookmarks and internal hyperlinks", description: "A brief example of using docx with bookmarks and internal hyperlinks",
hyperlinks: {
myAnchorId: {
text: "Hyperlink",
type: HyperlinkType.INTERNAL,
},
},
}); });
const anchorId = "anchorID";
// First create the bookmark
const bookmark = doc.createBookmark(anchorId, "Lorem Ipsum");
const hyperlink = doc.createInternalHyperLink(anchorId, `Click me!`);
doc.addSection({ doc.addSection({
children: [ children: [
new Paragraph({ new Paragraph({
heading: HeadingLevel.HEADING_1, heading: HeadingLevel.HEADING_1,
children: [bookmark], children: [new Bookmark("myAnchorId", "Lorem Ipsum")],
}), }),
new Paragraph("\n"), new Paragraph("\n"),
new Paragraph(LOREM_IPSUM), new Paragraph(LOREM_IPSUM),
@ -30,7 +30,7 @@ doc.addSection({
children: [new PageBreak()], children: [new PageBreak()],
}), }),
new Paragraph({ new Paragraph({
children: [hyperlink], children: [new HyperlinkRef("myAnchorId")],
}), }),
], ],
}); });

View File

@ -1,23 +1,53 @@
// Numbered lists // Numbered lists
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, Numbering, Packer, Paragraph } from "../build"; import { AlignmentType, Document, Packer, Paragraph } from "../build";
const doc = new Document(); const doc = new Document({
numbering: {
const numbering = new Numbering(); config: [
{
const abstractNum = numbering.createAbstractNumbering(); levels: [
abstractNum.createLevel(0, "upperRoman", "%1", "start").indent({ left: 720, hanging: 260 }); {
level: 0,
const concrete = numbering.createConcreteNumbering(abstractNum); format: "upperRoman",
text: "%1",
alignment: AlignmentType.START,
style: {
paragraph: {
indent: { left: 720, hanging: 260 },
},
},
},
],
reference: "my-crazy-reference",
},
{
levels: [
{
level: 0,
format: "decimal",
text: "%1",
alignment: AlignmentType.START,
style: {
paragraph: {
indent: { left: 720, hanging: 260 },
},
},
},
],
reference: "my-number-numbering-reference",
},
],
},
});
doc.addSection({ doc.addSection({
children: [ children: [
new Paragraph({ new Paragraph({
text: "line with contextual spacing", text: "line with contextual spacing",
numbering: { numbering: {
num: concrete, reference: "my-crazy-reference",
level: 0, level: 0,
}, },
contextualSpacing: true, contextualSpacing: true,
@ -28,7 +58,7 @@ doc.addSection({
new Paragraph({ new Paragraph({
text: "line with contextual spacing", text: "line with contextual spacing",
numbering: { numbering: {
num: concrete, reference: "my-crazy-reference",
level: 0, level: 0,
}, },
contextualSpacing: true, contextualSpacing: true,
@ -39,7 +69,7 @@ doc.addSection({
new Paragraph({ new Paragraph({
text: "line without contextual spacing", text: "line without contextual spacing",
numbering: { numbering: {
num: concrete, reference: "my-crazy-reference",
level: 0, level: 0,
}, },
contextualSpacing: false, contextualSpacing: false,
@ -50,7 +80,7 @@ doc.addSection({
new Paragraph({ new Paragraph({
text: "line without contextual spacing", text: "line without contextual spacing",
numbering: { numbering: {
num: concrete, reference: "my-crazy-reference",
level: 0, level: 0,
}, },
contextualSpacing: false, contextualSpacing: false,
@ -58,6 +88,27 @@ doc.addSection({
before: 200, before: 200,
}, },
}), }),
new Paragraph({
text: "Step 1 - Add sugar",
numbering: {
reference: "my-number-numbering-reference",
level: 0,
},
}),
new Paragraph({
text: "Step 2 - Add wheat",
numbering: {
reference: "my-number-numbering-reference",
level: 0,
},
}),
new Paragraph({
text: "Step 3 - Put in oven",
numbering: {
reference: "my-number-numbering-reference",
level: 0,
},
}),
], ],
}); });

View File

@ -1,46 +1,91 @@
// Numbering and bullet points example // Numbering and bullet points example
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, Numbering, Packer, Paragraph } from "../build"; import { AlignmentType, Document, Packer, Paragraph } from "../build";
const doc = new Document(); const doc = new Document({
numbering: {
const numbering = new Numbering(); config: [
{
const abstractNum = numbering.createAbstractNumbering(); reference: "my-crazy-numbering",
abstractNum.createLevel(0, "upperRoman", "%1", "start").indent({ left: 720, hanging: 260 }); levels: [
abstractNum.createLevel(1, "decimal", "%2.", "start").indent({ left: 1440, hanging: 980 }); {
abstractNum.createLevel(2, "lowerLetter", "%3)", "start").indent({ left: 2160, hanging: 1700 }); level: 0,
format: "upperRoman",
const concrete = numbering.createConcreteNumbering(abstractNum); text: "%1",
alignment: AlignmentType.START,
style: {
paragraph: {
indent: { left: 720, hanging: 260 },
},
},
},
{
level: 1,
format: "decimal",
text: "%2.",
alignment: AlignmentType.START,
style: {
paragraph: {
indent: { left: 1440, hanging: 980 },
},
},
},
{
level: 2,
format: "lowerLetter",
text: "%3)",
alignment: AlignmentType.START,
style: {
paragraph: {
indent: { left: 2160, hanging: 1700 },
},
},
},
{
level: 3,
format: "upperLetter",
text: "%4)",
alignment: AlignmentType.START,
style: {
paragraph: {
indent: { left: 2880, hanging: 2420 },
},
},
},
],
},
],
},
});
doc.addSection({ doc.addSection({
children: [ children: [
new Paragraph({ new Paragraph({
text: "Hey you", text: "Hey you",
numbering: { numbering: {
num: concrete, reference: "my-crazy-numbering",
level: 0, level: 0,
}, },
}), }),
new Paragraph({ new Paragraph({
text: "What's up fam", text: "What's up fam",
numbering: { numbering: {
num: concrete, reference: "my-crazy-numbering",
level: 1, level: 1,
}, },
}), }),
new Paragraph({ new Paragraph({
text: "Hello World 2", text: "Hello World 2",
numbering: { numbering: {
num: concrete, reference: "my-crazy-numbering",
level: 1, level: 1,
}, },
}), }),
new Paragraph({ new Paragraph({
text: "Yeah boi", text: "Yeah boi",
numbering: { numbering: {
num: concrete, reference: "my-crazy-numbering",
level: 2, level: 2,
}, },
}), }),
@ -68,6 +113,27 @@ doc.addSection({
level: 3, level: 3,
}, },
}), }),
new Paragraph({
text: "101 MSXFM",
numbering: {
reference: "my-crazy-numbering",
level: 3,
},
}),
new Paragraph({
text: "back to level 1",
numbering: {
reference: "my-crazy-numbering",
level: 1,
},
}),
new Paragraph({
text: "back to level 0",
numbering: {
reference: "my-crazy-numbering",
level: 0,
},
}),
], ],
}); });

View File

@ -1,7 +1,7 @@
// Example of how you would create a table and add data to it // Example of how you would create a table and add data to it
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, HeadingLevel, Packer, Paragraph, Table, TableCell, TableRow, VerticalAlign } from "../build"; import { Document, HeadingLevel, Packer, Paragraph, Table, TableCell, TableRow, VerticalAlign, TextDirection } from "../build";
const doc = new Document(); const doc = new Document();
@ -17,6 +17,14 @@ const table = new Table({
children: [new Paragraph({}), new Paragraph({})], children: [new Paragraph({}), new Paragraph({})],
verticalAlign: VerticalAlign.CENTER, verticalAlign: VerticalAlign.CENTER,
}), }),
new TableCell({
children: [new Paragraph({ text: "bottom to top" }), new Paragraph({})],
textDirection: TextDirection.BOTTOM_TO_TOP_LEFT_TO_RIGHT,
}),
new TableCell({
children: [new Paragraph({ text: "top to bottom" }), new Paragraph({})],
textDirection: TextDirection.TOP_TO_BOTTOM_RIGHT_TO_LEFT,
}),
], ],
}), }),
new TableRow({ new TableRow({
@ -38,6 +46,22 @@ const table = new Table({
], ],
verticalAlign: VerticalAlign.CENTER, verticalAlign: VerticalAlign.CENTER,
}), }),
new TableCell({
children: [
new Paragraph({
text: "Text above should be vertical from bottom to top",
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [
new Paragraph({
text: "Text above should be vertical from top to bottom",
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
], ],
}), }),
], ],

View File

@ -1,7 +1,8 @@
// Example of how you would merge cells together (Rows and Columns) and apply shading // Example of how you would merge cells together (Rows and Columns) and apply shading
// Also includes an example on how to center tables
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, HeadingLevel, Packer, Paragraph, ShadingType, Table, TableCell, TableRow, WidthType } from "../build"; import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, ShadingType, Table, TableCell, TableRow, WidthType } from "../build";
const doc = new Document(); const doc = new Document();
@ -29,6 +30,7 @@ const table = new Table({
}); });
const table2 = new Table({ const table2 = new Table({
alignment: AlignmentType.CENTER,
rows: [ rows: [
new TableRow({ new TableRow({
children: [ children: [
@ -66,6 +68,7 @@ const table2 = new Table({
}); });
const table3 = new Table({ const table3 = new Table({
alignment: AlignmentType.CENTER,
rows: [ rows: [
new TableRow({ new TableRow({
children: [ children: [
@ -181,7 +184,7 @@ const table5 = new Table({
new TableRow({ new TableRow({
children: [ children: [
new TableCell({ new TableCell({
children: [], children: [new Paragraph("1,0")],
}), }),
new TableCell({ new TableCell({
children: [new Paragraph("1,2")], children: [new Paragraph("1,2")],
@ -192,10 +195,10 @@ const table5 = new Table({
new TableRow({ new TableRow({
children: [ children: [
new TableCell({ new TableCell({
children: [], children: [new Paragraph("2,0")],
}), }),
new TableCell({ new TableCell({
children: [], children: [new Paragraph("2,1")],
}), }),
], ],
}), }),
@ -206,6 +209,163 @@ const table5 = new Table({
}, },
}); });
const borders = {
top: {
style: BorderStyle.DASH_SMALL_GAP,
size: 1,
color: "red",
},
bottom: {
style: BorderStyle.DASH_SMALL_GAP,
size: 1,
color: "red",
},
left: {
style: BorderStyle.DASH_SMALL_GAP,
size: 1,
color: "red",
},
right: {
style: BorderStyle.DASH_SMALL_GAP,
size: 1,
color: "red",
},
};
const table6 = new Table({
rows: [
new TableRow({
children: [
new TableCell({
borders,
children: [new Paragraph("0,0")],
rowSpan: 2,
}),
new TableCell({
borders,
children: [new Paragraph("0,1")],
}),
],
}),
new TableRow({
children: [
new TableCell({
borders,
children: [new Paragraph("1,1")],
rowSpan: 2,
}),
],
}),
new TableRow({
children: [
new TableCell({
borders,
children: [new Paragraph("2,0")],
}),
],
}),
],
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
});
const table7 = new Table({
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("0,0")],
}),
new TableCell({
children: [new Paragraph("0,1")],
}),
new TableCell({
children: [new Paragraph("0,2")],
rowSpan: 2,
}),
new TableCell({
children: [new Paragraph("0,3")],
rowSpan: 3,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("1,0")],
columnSpan: 2,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("2,0")],
columnSpan: 2,
}),
new TableCell({
children: [new Paragraph("2,2")],
rowSpan: 2,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("3,0")],
}),
new TableCell({
children: [new Paragraph("3,1")],
}),
new TableCell({
children: [new Paragraph("3,3")],
}),
],
}),
],
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
});
const table8 = new Table({
rows: [
new TableRow({
children: [
new TableCell({ children: [new Paragraph("1,1")] }),
new TableCell({ children: [new Paragraph("1,2")] }),
new TableCell({ children: [new Paragraph("1,3")] }),
new TableCell({ children: [new Paragraph("1,4")], rowSpan: 4, borders }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph("2,1")] }),
new TableCell({ children: [new Paragraph("2,2")] }),
new TableCell({ children: [new Paragraph("2,3")], rowSpan: 3 }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph("3,1")] }),
new TableCell({ children: [new Paragraph("3,2")], rowSpan: 2 }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph("4,1")] }),
],
}),
],
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
});
doc.addSection({ doc.addSection({
children: [ children: [
table, table,
@ -219,10 +379,16 @@ doc.addSection({
heading: HeadingLevel.HEADING_2, heading: HeadingLevel.HEADING_2,
}), }),
table3, table3,
new Paragraph("Merging columns"), new Paragraph("Merging columns 1"),
table4, table4,
new Paragraph("More Merging columns"), new Paragraph("Merging columns 2"),
table5, table5,
new Paragraph("Merging columns 3"),
table6,
new Paragraph("Merging columns 4"),
table7,
new Paragraph("Merging columns 5"),
table8,
], ],
}); });

View File

@ -3,6 +3,7 @@
import * as fs from "fs"; import * as fs from "fs";
import { import {
Document, Document,
OverlapType,
Packer, Packer,
Paragraph, Paragraph,
RelativeHorizontalPosition, RelativeHorizontalPosition,
@ -43,6 +44,7 @@ const table = new Table({
verticalAnchor: TableAnchorType.MARGIN, verticalAnchor: TableAnchorType.MARGIN,
relativeHorizontalPosition: RelativeHorizontalPosition.RIGHT, relativeHorizontalPosition: RelativeHorizontalPosition.RIGHT,
relativeVerticalPosition: RelativeVerticalPosition.BOTTOM, relativeVerticalPosition: RelativeVerticalPosition.BOTTOM,
overlap: OverlapType.NEVER,
}, },
width: { width: {
size: 4535, size: 4535,

View File

@ -1,15 +1,32 @@
// Example on how to add hyperlinks to websites // Example on how to add hyperlinks to websites
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Document, Packer, Paragraph } from "../build"; import { Document, HyperlinkRef, HyperlinkType, Packer, Paragraph, Media } from "../build";
const doc = new Document(); const doc = new Document({
const link = doc.createHyperlink("http://www.example.com", "Hyperlink"); hyperlinks: {
myCoolLink: {
link: "http://www.example.com",
text: "Hyperlink",
type: HyperlinkType.EXTERNAL,
},
myOtherLink: {
link: "http://www.google.com",
text: "Google Link",
type: HyperlinkType.EXTERNAL,
},
},
});
const image1 = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg"));
doc.addSection({ doc.addSection({
children: [ children: [
new Paragraph({ new Paragraph({
children: [link], children: [new HyperlinkRef("myCoolLink")],
}),
new Paragraph({
children: [image1, new HyperlinkRef("myOtherLink")],
}), }),
], ],
}); });

View File

@ -1,7 +1,7 @@
// Example how to display page numbers // Example how to display page numbers
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { AlignmentType, Document, Footer, Header, Packer, PageNumberFormat, Paragraph, TextRun } from "../build"; import { AlignmentType, Document, Footer, Header, Packer, PageBreak, PageNumber, PageNumberFormat, Paragraph, TextRun } from "../build";
const doc = new Document({}); const doc = new Document({});
@ -9,9 +9,17 @@ doc.addSection({
headers: { headers: {
default: new Header({ default: new Header({
children: [ children: [
new Paragraph("Foo Bar corp. ") new Paragraph({
.addRun(new TextRun("Page Number ").pageNumber()) children: [
.addRun(new TextRun(" to ").numberOfTotalPages()), new TextRun("Foo Bar corp. "),
new TextRun({
children: ["Page Number ", PageNumber.CURRENT],
}),
new TextRun({
children: [" to ", PageNumber.TOTAL_PAGES],
}),
],
}),
], ],
}), }),
}, },
@ -19,11 +27,17 @@ doc.addSection({
default: new Footer({ default: new Footer({
children: [ children: [
new Paragraph({ new Paragraph({
text: "Foo Bar corp. ",
alignment: AlignmentType.CENTER, alignment: AlignmentType.CENTER,
}) children: [
.addRun(new TextRun("Page Number: ").pageNumber()) new TextRun("Foo Bar corp. "),
.addRun(new TextRun(" to ").numberOfTotalPages()), new TextRun({
children: ["Page Number: ", PageNumber.CURRENT],
}),
new TextRun({
children: [" to ", PageNumber.TOTAL_PAGES],
}),
],
}),
], ],
}), }),
}, },
@ -32,11 +46,21 @@ doc.addSection({
pageNumberFormatType: PageNumberFormat.DECIMAL, pageNumberFormatType: PageNumberFormat.DECIMAL,
}, },
children: [ children: [
new Paragraph("Hello World 1").pageBreak(), new Paragraph({
new Paragraph("Hello World 2").pageBreak(), children: [new TextRun("Hello World 1"), new PageBreak()],
new Paragraph("Hello World 3").pageBreak(), }),
new Paragraph("Hello World 4").pageBreak(), new Paragraph({
new Paragraph("Hello World 5").pageBreak(), children: [new TextRun("Hello World 2"), new PageBreak()],
}),
new Paragraph({
children: [new TextRun("Hello World 3"), new PageBreak()],
}),
new Paragraph({
children: [new TextRun("Hello World 4"), new PageBreak()],
}),
new Paragraph({
children: [new TextRun("Hello World 5"), new PageBreak()],
}),
], ],
}); });

View File

@ -1,7 +1,7 @@
// Multiple sections with total number of pages in each section // Multiple sections with total number of pages in each section
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { AlignmentType, Document, Packer, PageNumberFormat, TextRun, Header, Paragraph, Footer, PageBreak } from "../build"; import { AlignmentType, Document, Footer, Header, Packer, PageBreak, PageNumber, PageNumberFormat, Paragraph, TextRun } from "../build";
const doc = new Document(); const doc = new Document();
@ -10,8 +10,12 @@ const header = new Header({
new Paragraph({ new Paragraph({
children: [ children: [
new TextRun("Header on another page"), new TextRun("Header on another page"),
new TextRun("Page Number: ").pageNumber(), new TextRun({
new TextRun(" to ").numberOfTotalPagesSection(), children: ["Page number: ", PageNumber.CURRENT],
}),
new TextRun({
children: [" to ", PageNumber.TOTAL_PAGES_IN_SECTION],
}),
], ],
alignment: AlignmentType.CENTER, alignment: AlignmentType.CENTER,
}), }),

31
demo/48-vertical-align.ts Normal file
View File

@ -0,0 +1,31 @@
// Example of making content of section vertically aligned
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, Packer, Paragraph, SectionVerticalAlignValue, TextRun } from "../build";
const doc = new Document();
doc.addSection({
properties: {
verticalAlign: SectionVerticalAlignValue.CENTER,
},
children: [
new Paragraph({
children: [
new TextRun("Hello World"),
new TextRun({
text: "Foo Bar",
bold: true,
}),
new TextRun({
text: "\tGithub is the best",
bold: true,
}),
],
}),
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

37
demo/49-table-borders.ts Normal file
View File

@ -0,0 +1,37 @@
// Add custom borders to the table itself
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { BorderStyle, Document, Packer, Paragraph, Table, TableCell, TableRow } from "../build";
const doc = new Document();
const table = new Table({
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("Hello")],
}),
new TableCell({
children: [],
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [],
}),
new TableCell({
children: [new Paragraph("World")],
}),
],
}),
],
});
doc.addSection({ children: [table] });
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

61
demo/50-readme-demo.ts Normal file
View File

@ -0,0 +1,61 @@
// 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, HeadingLevel, Media, Packer, Paragraph, Table, TableCell, TableRow, VerticalAlign } from "../build";
const doc = new Document();
const image1 = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg"));
const image2 = Media.addImage(doc, fs.readFileSync("./demo/images/pizza.gif"));
const table = new Table({
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph(image1)],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [
new Paragraph({
text: "Hello",
heading: HeadingLevel.HEADING_1,
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [
new Paragraph({
text: "World",
heading: HeadingLevel.HEADING_1,
}),
],
}),
new TableCell({
children: [new Paragraph(image1)],
}),
],
}),
],
});
doc.addSection({
children: [
new Paragraph({
text: "Hello World",
heading: HeadingLevel.HEADING_1,
}),
table,
new Paragraph(image2),
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

View File

@ -0,0 +1,37 @@
// Custom character styles using JavaScript configuration
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, Packer, Paragraph, TextRun } from "../build";
const doc = new Document({
styles: {
characterStyles: [
{
id: "myRedStyle",
name: "My Wonky Style",
basedOn: "Normal",
run: {
color: "990000",
italics: true,
},
},
],
},
});
doc.addSection({
children: [
new Paragraph({
children: [
new TextRun({
text: "Foo bar",
style: "myRedStyle",
}),
],
}),
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

37
demo/52-japanese.ts Normal file
View File

@ -0,0 +1,37 @@
// Japanese text - Need to use a Japanese font
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, HeadingLevel, Packer, Paragraph } from "../build";
const doc = new Document({
styles: {
paragraphStyles: [
{
id: "Normal",
name: "Normal",
basedOn: "Normal",
next: "Normal",
quickFormat: true,
run: {
font: "MS Gothic",
},
},
],
},
});
doc.addSection({
children: [
new Paragraph({
text: "KFCを食べるのが好き",
heading: HeadingLevel.HEADING_1,
}),
new Paragraph({
text: "こんにちは",
}),
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

55
demo/53-chinese.ts Normal file
View File

@ -0,0 +1,55 @@
// Chinese text - Chinese text need to use a Chinese font. And ascii text need to use a ascii font.
// Different from the `52-japanese.ts`.
// `52-japanese.ts` will set all characters to use Japanese font.
// `53-chinese.ts` will set Chinese characters to use Chinese font, and set ascii characters to use ascii font.
// Note that if the OS have not install `KaiTi` font, this demo doesn't work.
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, HeadingLevel, Packer, Paragraph, TextRun } from "../build";
const doc = new Document({
styles: {
paragraphStyles: [
{
id: "Normal",
name: "Normal",
basedOn: "Normal",
next: "Normal",
quickFormat: true,
run: {
font: {
ascii: "Times",
eastAsia: "KaiTi",
},
},
},
],
},
});
doc.addSection({
children: [
new Paragraph({
text: "中文和英文 Chinese and English",
heading: HeadingLevel.HEADING_1,
}),
new Paragraph({
text: "中文和英文 Chinese and English",
}),
new Paragraph({
children: [
new TextRun({
text: "中文和英文 Chinese and English",
font: { eastAsia: "SimSun" }, // set eastAsia to "SimSun".
// The ascii characters will use the default font ("Times") specified in paragraphStyles
}),
],
}),
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

132
demo/54-track-revisions.ts Normal file
View File

@ -0,0 +1,132 @@
// Track Revisions aka. "Track Changes"
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, Packer, Paragraph, TextRun, ShadingType, DeletedTextRun, InsertedTextRun, Footer, PageNumber, AlignmentType, FootnoteReferenceRun } from "../build";
/*
For reference, see
- https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.insertedrun
- https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.deletedrun
The method `addTrackRevisions()` adds an element `<w:trackRevisions />` to the `settings.xml` file. This specifies that the application shall track *new* revisions made to the existing document.
See also https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.trackrevisions
Note that this setting enables to track *new changes* after teh file is generated, so this example will still show inserted and deleted text runs when you remove it.
*/
const doc = new Document({
footnotes: [
new Paragraph({
children:[
new TextRun("This is a footnote"),
new DeletedTextRun({
text: " with some extra text which was deleted",
id: 0,
author: "Firstname Lastname",
date: "2020-10-06T09:05:00Z",
}),
new InsertedTextRun({
text: " and new content",
id: 1,
author: "Firstname Lastname",
date: "2020-10-06T09:05:00Z",
})
]
}),
],
});
doc.Settings.addTrackRevisions()
const paragraph = new Paragraph({
children: [
new TextRun("This is a simple demo "),
new TextRun({
text: "on how to "
}),
new InsertedTextRun({
text: "mark a text as an insertion ",
id: 0,
author: "Firstname Lastname",
date: "2020-10-06T09:00:00Z",
}),
new DeletedTextRun({
text: "or a deletion.",
id: 1,
author: "Firstname Lastname",
date: "2020-10-06T09:00:00Z",
})
],
});
doc.addSection({
properties: {},
children: [
paragraph,
new Paragraph({
children: [
new TextRun("This is a demo "),
new DeletedTextRun({
text: "in order",
color: "red",
bold: true,
size: 24,
font: {
name: "Garamond",
},
shading: {
type: ShadingType.REVERSE_DIAGONAL_STRIPE,
color: "00FFFF",
fill: "FF0000",
},
id: 2,
author: "Firstname Lastname",
date: "2020-10-06T09:00:00Z",
}).break(),
new InsertedTextRun({
text: "to show how to ",
bold: false,
id: 3,
author: "Firstname Lastname",
date: "2020-10-06T09:05:00Z",
}),
new TextRun({
bold: true,
children: [ "\tuse Inserted and Deleted TextRuns.", new FootnoteReferenceRun(1) ],
}),
],
}),
],
footers: {
default: new Footer({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun("Awesome LLC"),
new TextRun({
children: ["Page Number: ", PageNumber.CURRENT],
}),
new DeletedTextRun({
children: [" to ", PageNumber.TOTAL_PAGES],
id: 4,
author: "Firstname Lastname",
date: "2020-10-06T09:05:00Z",
}),
new InsertedTextRun({
children: [" from ", PageNumber.TOTAL_PAGES],
bold: true,
id: 5,
author: "Firstname Lastname",
date: "2020-10-06T09:05:00Z",
}),
],
}),
],
}),
},
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

View File

@ -21,9 +21,9 @@ doc.addSection({
bold: true, bold: true,
}), }),
new TextRun({ new TextRun({
text: "Github is the best", text: "\tGithub is the best",
bold: true, bold: true,
}).tab(), }),
], ],
}), }),
new Paragraph({ new Paragraph({

View File

@ -24,9 +24,9 @@
bold: true, bold: true,
}), }),
new docx.TextRun({ new docx.TextRun({
text: "Github is the best", text: "\tGithub is the best",
bold: true, bold: true,
}).tab(), }),
], ],
}), }),
], ],

View File

@ -1,13 +1,3 @@
<p align="center">
<img alt="clippy the assistant" src="https://i.imgur.com/37uBGhO.gif">
</p>
<p align="center">
Easily generate .docx files with JS/TS. Works for Node and on the Browser. :100:
</p>
---
# Welcome # Welcome
## Installation ## Installation
@ -50,9 +40,9 @@ doc.addSection({
bold: true, bold: true,
}), }),
new TextRun({ new TextRun({
text: "Github is the best", text: "\tGithub is the best",
bold: true, bold: true,
}).tab(), }),
], ],
}), }),
], ],
@ -63,17 +53,11 @@ Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer); fs.writeFileSync("My Document.docx", buffer);
}); });
// Done! A file called 'My First Document.docx' will be in your file system. // Done! A file called 'My Document.docx' will be in your file system.
``` ```
## Honoured Mentions
[@felipeochoa](https://github.com/felipeochoa)
[@h4buli](https://github.com/h4buli)
<p align="center"> <p align="center">
<img alt="clippy the assistant" src="http://i60.tinypic.com/339pvtt.png"> <img alt="clippy the assistant" src="./clippy.png">
</p> </p>
--- ---

10
docs/_coverpage.md Normal file
View File

@ -0,0 +1,10 @@
<img src="https://i.imgur.com/37uBGhO.gif" alt="drawing" style="width:200px;"/>
> Easily generate .docx files with JS/TS. Works for Node and on the Browser. :100:
- Simple, declarative API
- 50+ usage examples
- Battle tested, mature, 95%+ coverage
[GitHub](https://github.com/dolanmiu/docx)
[Get Started](#Welcome)

View File

@ -1,6 +1,6 @@
* [Getting Started](/) * [Getting Started](/)
* [Examples](examples.md) * [Examples](https://github.com/dolanmiu/docx/tree/master/demo)
* API * API
@ -20,6 +20,7 @@
* [Tab Stops](usage/tab-stops.md) * [Tab Stops](usage/tab-stops.md)
* [Table of Contents](usage/table-of-contents.md) * [Table of Contents](usage/table-of-contents.md)
* [Page Numbers](usage/page-numbers.md) * [Page Numbers](usage/page-numbers.md)
* [Change Tracking](usage/change-tracking.md)
* Styling * Styling
* [Styling with JS](usage/styling-with-js.md) * [Styling with JS](usage/styling-with-js.md)
* [Styling with XML](usage/styling-with-xml.md) * [Styling with XML](usage/styling-with-xml.md)
@ -28,4 +29,3 @@
* [Packers](usage/packers.md) * [Packers](usage/packers.md)
* [Contribution Guidelines](contribution-guidelines.md) * [Contribution Guidelines](contribution-guidelines.md)

BIN
docs/clippy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/clippy.psd Normal file

Binary file not shown.

View File

@ -1,25 +1,23 @@
# Contribution Guidelines # Contribution Guidelines
* Include documentation reference(s) at the top of each file: - Include documentation reference(s) at the top of each file:
```ts ```ts
// http://officeopenxml.com/WPdocument.php // http://officeopenxml.com/WPdocument.php
``` ```
* Follow Prettier standards, and consider using the [Prettier VSCode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) plugin. - Follow Prettier standards, and consider using the [Prettier VSCode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) plugin.
* Follow the `TSLint` rules - Follow the `TSLint` rules
## Always think about the user ## Always think about the user
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. Put yourself in their position, and imagine how they would feel about your feature you wrote.
1. Is it easy to use? 1. Is it easy to use?
2. Has it been documented well? 2. Has it been documented well?
3. Is it intuitive? 3. Is it intuitive?
4. Is it consistent with the rest of the API? 4. Is it declarative?
5. Is it fun to use? 5. Is it fun to use?
## Good Commit Names ## Good Commit Names
@ -27,6 +25,7 @@ Put yourself in their position, and imagine how they would feel about your featu
Please write good commit messages when making a commit: https://chris.beams.io/posts/git-commit/ Please write good commit messages when making a commit: https://chris.beams.io/posts/git-commit/
**Do not:** **Do not:**
``` ```
c // What? c // What?
rtl // Adding acryonyms without explaining anything else is not helpful rtl // Adding acryonyms without explaining anything else is not helpful
@ -35,34 +34,6 @@ demo updated // Getting better, but capitalize the first letter
Unesesary coment removed // Make sure to use correct spelling 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.
Try to make method parameters of the outside API accept primitives, 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 for the end user, no need for:
```ts
import { ChildComponent } from "./my-feature/sub-component/deeper/.../my-deep.component";
```
2. This is what I consider "leakage". The code is aware of the underlying implementation of the component.
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 `new` up the object, and inject it in like so:
```ts
public float(tableFloatProperties: TableFloatProperties): Table
```
```ts
table.float(new TableFloatProperties(...));
```
**Do** **Do**
`ITableFloatOptions` is an interface for a JSON of primitives. The end user would need to pass in a json object and not need to worry about the internals: `ITableFloatOptions` is an interface for a JSON of primitives. The end user would need to pass in a json object and not need to worry about the internals:
@ -71,31 +42,29 @@ This is so that:
public float(tableFloatOptions: ITableFloatOptions): Table public float(tableFloatOptions: ITableFloatOptions): Table
``` ```
## Delcariative API
Make sure the API is declarative, so no _method calling_ or _mutation_. This is a design decision, consistent with the rest of the project. There are benefits to delcariative code over other styles of code, explained here: https://dzone.com/articles/why-declarative-coding-makes-you-a-better-programm
**Do not:**
```ts ```ts
table.float({...}); const paragraph = doc.createParagraph();
const text = paragraph.createText();
text.contents = "Hello World";
``` ```
## Add vs Create **Do**
This is just a guideline, and the rules can sometimes be broken. ```ts
doc.addSection({
* Use `create` if the method `new`'s up an element inside: children: [
new Paragraph({
```ts children: [new TextRun("Hello World")],
public createParagraph() { }),
const paragraph = new Paragraph(); ],
this.root.push(paragraph); });
} ```
```
* 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:
```ts
public add(paragraph: Paragraph) {
this.root.push(paragraph);
}
```
## Getters and Setters ## Getters and Setters
@ -107,7 +76,7 @@ public get Level() {
} }
``` ```
There is no performance advantage by doing this. It means we don't need to prefix all private variables with the ugly `_`: This is the convention of this project. There is no performance advantage by doing this. It means we don't need to prefix all private variables with `_`:
**Do not:** **Do not:**
@ -121,30 +90,6 @@ private get _level: string;
private get level: string; private get level: string;
``` ```
## Temporal Methods
Some methods are `non-temporal`, which means regardless of when you call the method, it will have the same affect on the document. For example, setting the width of a table at the end of the document will have the same effect as setting the width at the start:
```ts
table.setWidth(1000); // now removed as of version 5.0.0
```
Whereas some methods are `temporal`, which means depending on the time-frame they are called, it would produce a difference result. For example, moving `createParagraph()` around your code will physically alter the document.
```ts
doc.createParagraph("hello");
```
If a method is `non-temporal`, put it in the objects `constructor`. For example:
```ts
const table = new Table(width: number);
```
`Non-temporal` methods are usually methods which can only be used one time and one time only. For example, `.float()`. It does not make sense to call `.float()` again if its already floating.
I am not sure what the real term is, but this will do.
## Interfaces over type alias ## Interfaces over type alias
Do not use `type`, but rather use `Interfaces`. `type` cannot be extended, and a class cannot implement it. Do not use `type`, but rather use `Interfaces`. `type` cannot be extended, and a class cannot implement it.
@ -152,14 +97,14 @@ Do not use `type`, but rather use `Interfaces`. `type` cannot be extended, and a
> "In general, use what you want ( type alias / interface ) just be consistent" > "In general, use what you want ( type alias / interface ) just be consistent"
> "always use interface for public API's definition when authoring a library or 3rd party ambient type definitions" > "always use interface for public API's definition when authoring a library or 3rd party ambient type definitions"
> >
> * https://medium.com/@martin_hotell/interface-vs-type-alias-in-typescript-2-7-2a8f1777af4c > - https://medium.com/@martin_hotell/interface-vs-type-alias-in-typescript-2-7-2a8f1777af4c
`Interface` is generally preferred over `type`: https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types `Interface` is generally preferred over `type`: https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types
**Do not:** **Do not:**
```ts ```ts
type RelationshipFileInfo = { id: number, target: string }; type RelationshipFileInfo = { id: number; target: string };
``` ```
**Do:** **Do:**
@ -193,26 +138,26 @@ enum WeaponType = {
## Spell correctly, in full and in American English ## Spell correctly, in full and in American English
I am not sure where these habits in software development come from, but I do not believe it is beneficial:
**Do not:** **Do not:**
```ts ```ts
readdy // misspelling readdy; // misspelling
perm // abbreviation perm; // abbreviation
conf // abbreviation conf; // abbreviation
cnty // abbreviation cnty; // abbreviation
relationFile // abbreviation relationFile; // abbreviation
colour // U.K. English colour; // U.K. English
``` ```
**Do:** **Do:**
```ts ```ts
ready ready;
permission permission;
config config;
country country;
relationshipFile relationshipFile;
color color;
``` ```
## Keep files small (within reason) ## Keep files small (within reason)

View File

@ -1,219 +0,0 @@
# Examples
> All examples can run independently and can be found in the `/demo` folder of the project
All the examples below can be ran locally, to do so, run the following command:
```sh
npm run demo
```
This command will run the `demo selector app` in the `/demo` folder. It will prompt you to select a demo number, which will run a demo from that folder.
## Simple
A simple hello world of the `docx` library:
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo1.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo1.ts_
## Styles
### Styling with JS
This example shows how to customise the look and feel of a document using JS configuration
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo2.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo2.ts_
### Styling with XML
This example shows how to customise the look and feel of a document using XML configuration
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo13.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo13.ts_
## Numbering
This example shows many levels of numbering
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo3.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo3.ts_
## Table
Example of simple table
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo4.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo4.ts_
### Styling table borders
Styling the borders of a table
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo20.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo20.ts_
## Images
### Add image to the document
Importing Images from file system path
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo5.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo5.ts_
### Add images to header and footer
Example showing how to add image to headers and footers
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo9.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo9.ts_
### Scaling images
Example showing how to scale images
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo12.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo12.ts_
### Add Image to media before adding to document
This is the best way to add an image to a document because you can add the same image in two locations without increasing document size by re-using the same image
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo23.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo23.ts_
### Add image to table
As before, to add an image to a table, you would need to add it to the `Media` object first
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo24.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo24.ts_
### Images using Base64 URI
If you want to use a Base64 image instead
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo18.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo18.ts_
## Margins
Example showing how to set custom margins
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo6.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo6.ts_
## Orientation
Example showing how to set the document to `landscape` or `portrait`
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo7.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo7.ts_
## Headers & Footers
Example showing how to add headers and footers
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo8.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo8.ts_
## Multiple headers and footers
Check out `Sections` for this feature
## Page Breaks
### Normal page breaks
Example showing how to page break
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo14.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo14.ts_
### Page break before
Example showing how to page break before like in Word
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo15.ts ':include')
_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.
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')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo16.ts_
## Footnotes
Example of how to add footnotes. Good for references
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo17.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo17.ts_
## Packers
## Buffer output
Example showing how to use the Buffer packer and then write that buffer to the file system
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo19.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo19.ts_
## Bookmarks
Example showing how to make bookmarks to make internal hyperlinks within the document
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo21.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo21.ts_
## Bidirectional text
Example showing how to use bidirectional text for certain languages such as Hebrew
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo22.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo22.ts_
## Showcase
### My CV
Example showing how to add headers and footers
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo10.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo10.ts_
### Style and Images
This example shows how to customise the look and feel of a document and add images
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo11.ts ':include')
_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo11.ts_

View File

@ -1,31 +1,36 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head> <meta charset="UTF-8" />
<meta charset="UTF-8">
<title>docx - Generate .docx documents with JavaScript</title> <title>docx - Generate .docx documents with JavaScript</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Generate .docx documents with JavaScript"> <meta name="description" content="Generate .docx documents with JavaScript" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css"> <link
</head> rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify-darklight-theme@latest/dist/style.min.css"
title="docsify-darklight-theme"
type="text/css"
/>
</head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script> <script>
window.$docsify = { window.$docsify = {
name: 'docx', name: "docx",
repo: 'https://github.com/dolanmiu/docx', repo: "https://github.com/dolanmiu/docx",
loadSidebar: true, loadSidebar: true,
subMaxLevel: 2, subMaxLevel: 2,
search: 'auto', search: "auto",
} coverpage: true,
};
</script> </script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script> <script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/emoji.min.js"></script> <script src="//unpkg.com/docsify/lib/plugins/emoji.min.js"></script>
<script src="https://unpkg.com/docsify-copy-code@2"></script> <script src="https://unpkg.com/docsify-copy-code@2"></script>
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script> <script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-typescript.min.js"></script> <script src="//unpkg.com/prismjs/components/prism-typescript.min.js"></script>
</body> <script src="//cdn.jsdelivr.net/npm/docsify-darklight-theme@latest/dist/index.min.js" type="text/javascript"></script>
</body>
</html> </html>

View File

@ -0,0 +1,58 @@
# Change Tracking
> Instead of adding a `TextRun` into a `Paragraph`, you can also add an `InsertedTextRun` or `DeletedTextRun` where you need to supply an `id`, `author` and `date` for the change.
```ts
import { Paragraph, TextRun, InsertedTextRun, DeletedTextRun } from "docx";
const paragraph = new Paragraph({
children: [
new TextRun("This is a simple demo "),
new TextRun({
text: "on how to "
}),
new InsertedTextRun({
text: "mark a text as an insertion ",
id: 0,
author: "Firstname Lastname",
date: "2020-10-06T09:00:00Z",
}),
new DeletedTextRun({
text: "or a deletion.",
id: 1,
author: "Firstname Lastname",
date: "2020-10-06T09:00:00Z",
})
],
});
```
Note that for a `InsertedTextRun` and `DeletedTextRun`, it is not possible to simply call it with only a text as in `new TextRun("some text")`, since the additonal fields for change tracking need to be provided. Similar to a normal `TextRun` you can add additional text properties.
```ts
import { Paragraph, TextRun, InsertedTextRun, DeletedTextRun } from "docx";
const paragraph = new Paragraph({
children: [
new TextRun("This is a simple demo"),
new DeletedTextRun({
text: "with a deletion.",
color: "red",
bold: true,
size: 24,
id: 0,
author: "Firstname Lastname",
date: "2020-10-06T09:00:00Z",
})
],
});
```
In addtion to marking text as inserted or deleted, change tracking can also be added via the document settings. This will enable new changes to be tracked as well.
```ts
import { Document } from "docx";
const doc = new Document({});
doc.Settings.addTrackRevisions()
```

View File

@ -2,11 +2,7 @@
> Packers are the way in which `docx` turns your code into `.docx` format. It is completely decoupled from the `docx.Document`. > Packers are the way in which `docx` turns your code into `.docx` format. It is completely decoupled from the `docx.Document`.
Packers in `version 4` and above are now one single `Packer`. It works in both a node and browser environment (Angular etc). Now, the packer returns a `Buffer`, `Blob` or `base64 string`. It is up to you to take that and persist it with node's `fs`, send it down as a downloadable file, or anything else you wish. As of version 4, this library will not have options to export to PDF. Packers works in both a node and browser environment (Angular etc). Now, the packer returns a `Buffer`, `Blob` or `base64 string`. It is up to you to take that and persist it with node's `fs`, send it down as a downloadable file, or anything else you wish. As of `version 4+`, this library will not have options to export to PDF.
## Version 5
Packers in `version 5` and above are now static methods on `Packer`.
### Export as Buffer ### Export as Buffer
@ -36,117 +32,3 @@ Packer.toBlob(doc).then((blob) => {
saveAs(blob, "example.docx"); saveAs(blob, "example.docx");
}); });
``` ```
## Version 4
The `Packer` in `version 4` requires an instance of `Packer`, so be sure to `new` it.
### Export as Buffer
This will return a NodeJS `Buffer`. If this is used in the browser, it will return a `UInt8Array` instead.
```ts
const packer = new docx.Packer();
packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});
```
### Export as a `base64` string
```ts
const packer = new docx.Packer();
packer.toBase64String(doc).then((string) => {
console.log(string);
});
```
### Export as Blob
This is useful if you want to send it as an downloadable in a browser environment.
```ts
const packer = new docx.Packer();
packer.toBlob(doc).then((blob) => {
// saveAs from FileSaver will download the file
saveAs(blob, "example.docx");
});
```
## Version 3 and below
### File System Packer
```ts
const docx = require("docx");
const doc = new docx.Document();
const exporter = new docx.LocalPacker(doc);
exporter.pack("My Document");
// Word Document is in file system
```
### Buffer Packer
```ts
const docx = require("docx");
const doc = new docx.Document();
const exporter = new docx.BufferPacker(doc);
const buffer = exporter.pack();
```
### Stream Packer
Creates a `node` `Readable` stream
```ts
const docx = require("docx");
const doc = new docx.Document();
const exporter = new docx.StreamPacker(doc);
const stream = exporter.pack();
```
### Express Packer
The old express packer is now deprecated and may disappear soon, so you should upgrade.
The reason for this is because it means this project needs to know about and use `express`, which for a Word document generator, does not sound right. Seperation of concerns.
It will still be usable (for now), but it is ill advised.
I used the express exporter in my [website](http://www.dolan.bio).
The recommended way is to use the `StreamPacker` and handle the `express` magic outside of the library:
```ts
const docx = require("docx");
const doc = new docx.Document();
const exporter = new docx.StreamPacker(doc);
const stream = exporter.pack();
// Express' response object
res.attachment("yourfile.xlsx");
stream.pipe(res);
```
where `res` is the response object obtained through the Express router. It is that simple. The file will begin downloading in the browser.
### PDF Exporting
You can export your word document as a PDF file like so:
```ts
const exporter = new docx.LocalPacker(doc);
exporter.packPdf("My Document");
// Express
const exporter = new docx.ExpressPacker(doc, res);
exporter.packPdf("My Document");
```

View File

@ -20,11 +20,7 @@ This method is useful for adding different [text](text.md) with different styles
```ts ```ts
const paragraph = new Paragraph({ const paragraph = new Paragraph({
children: [ children: [new TextRun("Lorem Ipsum Foo Bar"), new TextRun("Hello World"), new SymbolRun("F071")],
new TextRun("Lorem Ipsum Foo Bar"),
new TextRun("Hello World"),
new SymbolRun("F071"),
],
}); });
``` ```
@ -61,7 +57,7 @@ doc.addSection({
This is the list of options for a paragraph. A detailed explanation is below: This is the list of options for a paragraph. A detailed explanation is below:
| Property | Type | Mandatory? | Possible Values | | Property | Type | Mandatory? | Possible Values |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
| [text](#text) | `string` | Optional | | | [text](#text) | `string` | Optional | |
| [heading](#heading) | `HeadingLevel` | Optional | `HEADING_1`, `HEADING_2`, `HEADING_3`, `HEADING_4`, `HEADING_5`, `HEADING_6`, `TITLE` | | [heading](#heading) | `HeadingLevel` | Optional | `HEADING_1`, `HEADING_2`, `HEADING_3`, `HEADING_4`, `HEADING_5`, `HEADING_6`, `TITLE` |
| [border](#border) | `IBorderOptions` | Optional | `top`, `bottom`, `left`, `right`. Each of these are of type IBorderPropertyOptions. Click here for Example | | [border](#border) | `IBorderOptions` | Optional | `top`, `bottom`, `left`, `right`. Each of these are of type IBorderPropertyOptions. Click here for Example |
@ -80,7 +76,7 @@ This is the list of options for a paragraph. A detailed explanation is below:
| style | `string` | Optional | | | style | `string` | Optional | |
| tabStop | `{ left?: ITabStopOptions; right?: ITabStopOptions; maxRight?: { leader: LeaderType; }; center?: ITabStopOptions }` | Optional | | | tabStop | `{ left?: ITabStopOptions; right?: ITabStopOptions; maxRight?: { leader: LeaderType; }; center?: ITabStopOptions }` | Optional | |
| bullet | `{ level: number }` | Optional | | | bullet | `{ level: number }` | Optional | |
| numbering | `{ num: Num; level: number; custom?: boolean }` | Optional | | | numbering | `{ num: ConcreteNumbering; level: number; custom?: boolean }` | Optional | |
## Text ## Text
@ -252,10 +248,7 @@ To move to a new page (insert a page break):
```ts ```ts
const paragraph = new docx.Paragraph({ const paragraph = new docx.Paragraph({
children: [ children: [new TextRun("Amazing Heading"), new PageBreak()],
new TextRun("Amazing Heading"),
new PageBreak(),
]
}); });
``` ```

View File

@ -22,10 +22,11 @@ const name = new TextRun({
### Run formatting ### Run formatting
- `bold`, `italics`, `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 - `underline({type="single", color=null})`: Set the underline style and color
- `emphasisMark({type="dot"})`: Set the emphasis mark style
- `color(color)`: Set the text color, using 6 hex characters for RRGGBB (no leading `#`) - `color(color)`: Set the text color, using 6 hex characters for RRGGBB (no leading `#`)
- `size(halfPts)`: Set the font size, measured in half-points - `size(halfPts)`: Set the font size, measured in half-points
- `font(name)`: Set the run's font - `font(name)` or `font({ascii, cs, eastAsia, hAnsi, hint})`: Set the run's font
- `style(name)`: Apply a named run style - `style(name)`: Apply a named run style
- `characterSpacing(value)`: Set the character spacing adjustment (in TWIPs) - `characterSpacing(value)`: Set the character spacing adjustment (in TWIPs)

View File

@ -12,7 +12,7 @@ Simply call the relevant methods on the paragraph listed below. Then just add a
```ts ```ts
const paragraph = new Paragraph({ const paragraph = new Paragraph({
children: [new TextRun("Hey everyone").bold(), new TextRun("11th November 1999").tab()], children: [new TextRun("Hey everyone").bold(), new TextRun(\t"11th November 1999")],
tabStops: [ tabStops: [
{ {
type: TabStopType.RIGHT, type: TabStopType.RIGHT,
@ -26,7 +26,7 @@ The example above will create a left aligned text, and a right aligned text on t
```ts ```ts
const paragraph = new Paragraph({ const paragraph = new Paragraph({
children: [new TextRun("Second tab stop here I come!").tab().tab()], children: [new TextRun("Second tab stop here I come!")],
tabStops: [ tabStops: [
{ {
type: TabStopType.RIGHT, type: TabStopType.RIGHT,
@ -46,7 +46,7 @@ You can add multiple tab stops of the same `type` too.
```ts ```ts
const paragraph = new Paragraph({ const paragraph = new Paragraph({
children: [new TextRun("Multiple tab stops!").tab().tab()], children: [new TextRun("Multiple tab stops!")],
tabStops: [ tabStops: [
{ {
type: TabStopType.RIGHT, type: TabStopType.RIGHT,

View File

@ -103,7 +103,7 @@ Here is a list of options you can add to the `table row`:
| children | `Array<TableCell>` | Required | | children | `Array<TableCell>` | Required |
| cantSplit | `boolean` | Optional | | cantSplit | `boolean` | Optional |
| tableHeader | `boolean` | Optional | | tableHeader | `boolean` | Optional |
| height | `{ value: number, rule: HeightRule }` | Optional | | height | `{ height: number, rule: HeightRule }` | Optional |
### Repeat row ### Repeat row

View File

@ -68,6 +68,15 @@ const text = new TextRun({
}); });
``` ```
### Emphasis Mark
```ts
const text = new TextRun({
text: "and then emphasis mark",
emphasisMark: {},
});
```
### Strike through ### Strike through
```ts ```ts

3265
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "docx", "name": "docx",
"version": "5.0.0-rc7", "version": "5.3.0",
"description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.",
"main": "build/index.js", "main": "build/index.js",
"scripts": { "scripts": {
"pretest": "rimraf ./build", "pretest": "rimraf ./build",
@ -50,7 +50,9 @@
"types": "./build/index.d.ts", "types": "./build/index.d.ts",
"dependencies": { "dependencies": {
"@types/jszip": "^3.1.4", "@types/jszip": "^3.1.4",
"@types/node": "^14.0.5",
"jszip": "^3.1.5", "jszip": "^3.1.5",
"shortid": "^2.2.15",
"xml": "^1.0.1", "xml": "^1.0.1",
"xml-js": "^1.6.8" "xml-js": "^1.6.8"
}, },
@ -62,9 +64,10 @@
"homepage": "https://github.com/dolanmiu/docx#readme", "homepage": "https://github.com/dolanmiu/docx#readme",
"devDependencies": { "devDependencies": {
"@types/chai": "^3.4.35", "@types/chai": "^3.4.35",
"@types/mocha": "^2.2.39", "@types/mocha": "^8.0.0",
"@types/request-promise": "^4.1.42", "@types/request-promise": "^4.1.42",
"@types/sinon": "^4.3.1", "@types/shortid": "0.0.29",
"@types/sinon": "^9.0.4",
"@types/webpack": "^4.4.24", "@types/webpack": "^4.4.24",
"awesome-typescript-loader": "^3.4.1", "awesome-typescript-loader": "^3.4.1",
"chai": "^3.5.0", "chai": "^3.5.0",
@ -74,24 +77,24 @@
"jszip": "^3.1.5", "jszip": "^3.1.5",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"mocha-webpack": "^1.0.1", "mocha-webpack": "^1.0.1",
"nyc": "^14.1.1", "nyc": "^15.1.0",
"pre-commit": "^1.2.2", "pre-commit": "^1.2.2",
"prettier": "^1.15.2", "prettier": "^2.0.5",
"prompt": "^1.0.0", "prompt": "^1.0.0",
"replace-in-file": "^3.1.0", "replace-in-file": "^3.1.0",
"request": "^2.88.0", "request": "^2.88.0",
"request-promise": "^4.2.2", "request-promise": "^4.2.2",
"rimraf": "^2.5.2", "rimraf": "^3.0.2",
"shelljs": "^0.7.7", "shelljs": "^0.8.4",
"sinon": "^5.0.7", "sinon": "^9.0.2",
"ts-node": "^7.0.1", "ts-node": "^9.0.0",
"tslint": "^5.11.0", "tslint": "^6.1.3",
"tslint-immutable": "^4.9.0", "tslint-immutable": "^4.9.0",
"typedoc": "^0.11.1", "typedoc": "^0.16.11",
"typescript": "2.9.2", "typescript": "2.9.2",
"webpack": "^3.10.0" "webpack": "^3.10.0"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=10"
} }
} }

View File

@ -1,8 +1,9 @@
import { BaseXmlComponent, IXmlableObject } from "file/xml-components"; import { BaseXmlComponent, IXmlableObject } from "file/xml-components";
import { File } from "../file";
export class Formatter { export class Formatter {
public format(input: BaseXmlComponent): IXmlableObject { public format(input: BaseXmlComponent, file?: File): IXmlableObject {
const output = input.prepForXml(); const output = input.prepForXml(file);
if (output) { if (output) {
return output; return output;

View File

@ -16,7 +16,7 @@ describe("Compiler", () => {
}); });
describe("#compile()", () => { describe("#compile()", () => {
it("should pack all the content", async function() { it("should pack all the content", async function () {
this.timeout(99999999); this.timeout(99999999);
const zipFile = compiler.compile(file); const zipFile = compiler.compile(file);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
@ -35,7 +35,7 @@ describe("Compiler", () => {
expect(fileNames).to.include("_rels/.rels"); expect(fileNames).to.include("_rels/.rels");
}); });
it("should pack all additional headers and footers", async function() { it("should pack all additional headers and footers", async function () {
file.addSection({ file.addSection({
headers: { headers: {
default: new Header(), default: new Header(),

View File

@ -4,6 +4,7 @@ import * as xml from "xml";
import { File } from "file"; import { File } from "file";
import { Formatter } from "../formatter"; import { Formatter } from "../formatter";
import { ImageReplacer } from "./image-replacer"; import { ImageReplacer } from "./image-replacer";
import { NumberingReplacer } from "./numbering-replacer";
interface IXmlifyedFile { interface IXmlifyedFile {
readonly data: string; readonly data: string;
@ -30,10 +31,12 @@ interface IXmlifyedFileMapping {
export class Compiler { export class Compiler {
private readonly formatter: Formatter; private readonly formatter: Formatter;
private readonly imageReplacer: ImageReplacer; private readonly imageReplacer: ImageReplacer;
private readonly numberingReplacer: NumberingReplacer;
constructor() { constructor() {
this.formatter = new Formatter(); this.formatter = new Formatter();
this.imageReplacer = new ImageReplacer(); this.imageReplacer = new ImageReplacer();
this.numberingReplacer = new NumberingReplacer();
} }
public compile(file: File, prettifyXml?: boolean): JSZip { public compile(file: File, prettifyXml?: boolean): JSZip {
@ -68,7 +71,7 @@ export class Compiler {
file.verifyUpdateFields(); file.verifyUpdateFields();
const documentRelationshipCount = file.DocumentRelationships.RelationshipCount + 1; const documentRelationshipCount = file.DocumentRelationships.RelationshipCount + 1;
const documentXmlData = xml(this.formatter.format(file.Document), prettify); const documentXmlData = xml(this.formatter.format(file.Document, file), prettify);
const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media); const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media);
return { return {
@ -82,24 +85,25 @@ export class Compiler {
); );
}); });
return xml(this.formatter.format(file.DocumentRelationships), prettify); return xml(this.formatter.format(file.DocumentRelationships, file), prettify);
})(), })(),
path: "word/_rels/document.xml.rels", path: "word/_rels/document.xml.rels",
}, },
Document: { Document: {
data: (() => { data: (() => {
const xmlData = this.imageReplacer.replace(documentXmlData, documentMediaDatas, documentRelationshipCount); const xmlData = this.imageReplacer.replace(documentXmlData, documentMediaDatas, documentRelationshipCount);
const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering);
return xmlData; return referenedXmlData;
})(), })(),
path: "word/document.xml", path: "word/document.xml",
}, },
Styles: { Styles: {
data: xml(this.formatter.format(file.Styles), prettify), data: xml(this.formatter.format(file.Styles, file), prettify),
path: "word/styles.xml", path: "word/styles.xml",
}, },
Properties: { Properties: {
data: xml(this.formatter.format(file.CoreProperties), { data: xml(this.formatter.format(file.CoreProperties, file), {
declaration: { declaration: {
standalone: "yes", standalone: "yes",
encoding: "UTF-8", encoding: "UTF-8",
@ -108,15 +112,15 @@ export class Compiler {
path: "docProps/core.xml", path: "docProps/core.xml",
}, },
Numbering: { Numbering: {
data: xml(this.formatter.format(file.Numbering), prettify), data: xml(this.formatter.format(file.Numbering, file), prettify),
path: "word/numbering.xml", path: "word/numbering.xml",
}, },
FileRelationships: { FileRelationships: {
data: xml(this.formatter.format(file.FileRelationships), prettify), data: xml(this.formatter.format(file.FileRelationships, file), prettify),
path: "_rels/.rels", path: "_rels/.rels",
}, },
HeaderRelationships: file.Headers.map((headerWrapper, index) => { HeaderRelationships: file.Headers.map((headerWrapper, index) => {
const xmlData = xml(this.formatter.format(headerWrapper.Header), prettify); const xmlData = xml(this.formatter.format(headerWrapper.Header, file), prettify);
const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media);
mediaDatas.forEach((mediaData, i) => { mediaDatas.forEach((mediaData, i) => {
@ -128,12 +132,12 @@ export class Compiler {
}); });
return { return {
data: xml(this.formatter.format(headerWrapper.Relationships), prettify), data: xml(this.formatter.format(headerWrapper.Relationships, file), prettify),
path: `word/_rels/header${index + 1}.xml.rels`, path: `word/_rels/header${index + 1}.xml.rels`,
}; };
}), }),
FooterRelationships: file.Footers.map((footerWrapper, index) => { FooterRelationships: file.Footers.map((footerWrapper, index) => {
const xmlData = xml(this.formatter.format(footerWrapper.Footer), prettify); const xmlData = xml(this.formatter.format(footerWrapper.Footer, file), prettify);
const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media);
mediaDatas.forEach((mediaData, i) => { mediaDatas.forEach((mediaData, i) => {
@ -145,12 +149,12 @@ export class Compiler {
}); });
return { return {
data: xml(this.formatter.format(footerWrapper.Relationships), prettify), data: xml(this.formatter.format(footerWrapper.Relationships, file), prettify),
path: `word/_rels/footer${index + 1}.xml.rels`, path: `word/_rels/footer${index + 1}.xml.rels`,
}; };
}), }),
Headers: file.Headers.map((headerWrapper, index) => { Headers: file.Headers.map((headerWrapper, index) => {
const tempXmlData = xml(this.formatter.format(headerWrapper.Header), prettify); const tempXmlData = xml(this.formatter.format(headerWrapper.Header, file), prettify);
const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media);
// TODO: 0 needs to be changed when headers get relationships of their own // TODO: 0 needs to be changed when headers get relationships of their own
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0); const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0);
@ -161,7 +165,7 @@ export class Compiler {
}; };
}), }),
Footers: file.Footers.map((footerWrapper, index) => { Footers: file.Footers.map((footerWrapper, index) => {
const tempXmlData = xml(this.formatter.format(footerWrapper.Footer), prettify); const tempXmlData = xml(this.formatter.format(footerWrapper.Footer, file), prettify);
const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media);
// TODO: 0 needs to be changed when headers get relationships of their own // TODO: 0 needs to be changed when headers get relationships of their own
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0); const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0);
@ -172,19 +176,19 @@ export class Compiler {
}; };
}), }),
ContentTypes: { ContentTypes: {
data: xml(this.formatter.format(file.ContentTypes), prettify), data: xml(this.formatter.format(file.ContentTypes, file), prettify),
path: "[Content_Types].xml", path: "[Content_Types].xml",
}, },
AppProperties: { AppProperties: {
data: xml(this.formatter.format(file.AppProperties), prettify), data: xml(this.formatter.format(file.AppProperties, file), prettify),
path: "docProps/app.xml", path: "docProps/app.xml",
}, },
FootNotes: { FootNotes: {
data: xml(this.formatter.format(file.FootNotes), prettify), data: xml(this.formatter.format(file.FootNotes, file), prettify),
path: "word/footnotes.xml", path: "word/footnotes.xml",
}, },
Settings: { Settings: {
data: xml(this.formatter.format(file.Settings), prettify), data: xml(this.formatter.format(file.Settings, file), prettify),
path: "word/settings.xml", path: "word/settings.xml",
}, },
}; };

View File

@ -0,0 +1,17 @@
import { ConcreteNumbering } from "file";
export class NumberingReplacer {
public replace(xmlData: string, concreteNumberings: ConcreteNumbering[]): string {
let currentXmlData = xmlData;
for (const concreteNumbering of concreteNumberings) {
if (!concreteNumbering.reference) {
continue;
}
currentXmlData = currentXmlData.replace(new RegExp(`{${concreteNumbering.reference}}`, "g"), concreteNumbering.id.toString());
}
return currentXmlData;
}
}

View File

@ -36,7 +36,7 @@ describe("Packer", () => {
}); });
describe("#toBuffer()", () => { describe("#toBuffer()", () => {
it("should create a standard docx file", async function() { it("should create a standard docx file", async function () {
this.timeout(99999999); this.timeout(99999999);
const buffer = await Packer.toBuffer(file); const buffer = await Packer.toBuffer(file);
@ -61,7 +61,7 @@ describe("Packer", () => {
}); });
describe("#toBase64String()", () => { describe("#toBase64String()", () => {
it("should create a standard docx file", async function() { it("should create a standard docx file", async function () {
this.timeout(99999999); this.timeout(99999999);
const str = await Packer.toBase64String(file); const str = await Packer.toBase64String(file);

View File

@ -1,9 +1,22 @@
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes"; import { DocumentAttributes } from "../document/document-attributes";
import { INumberingOptions } from "../numbering";
import { HyperlinkType, Paragraph } from "../paragraph";
import { IStylesOptions } from "../styles"; import { IStylesOptions } from "../styles";
import { Created, Creator, Description, Keywords, LastModifiedBy, Modified, Revision, Subject, Title } from "./components"; import { Created, Creator, Description, Keywords, LastModifiedBy, Modified, Revision, Subject, Title } from "./components";
export interface IInternalHyperlinkDefinition {
readonly text: string;
readonly type: HyperlinkType.INTERNAL;
}
export interface IExternalHyperlinkDefinition {
readonly link: string;
readonly text: string;
readonly type: HyperlinkType.EXTERNAL;
}
export interface IPropertiesOptions { export interface IPropertiesOptions {
readonly title?: string; readonly title?: string;
readonly subject?: string; readonly subject?: string;
@ -14,6 +27,11 @@ export interface IPropertiesOptions {
readonly revision?: string; readonly revision?: string;
readonly externalStyles?: string; readonly externalStyles?: string;
readonly styles?: IStylesOptions; readonly styles?: IStylesOptions;
readonly numbering?: INumberingOptions;
readonly footnotes?: Paragraph[];
readonly hyperlinks?: {
readonly [key: string]: IInternalHyperlinkDefinition | IExternalHyperlinkDefinition;
};
} }
export class CoreProperties extends XmlComponent { export class CoreProperties extends XmlComponent {

View File

@ -22,9 +22,6 @@ describe("Body", () => {
expect(tree).to.deep.equal({ expect(tree).to.deep.equal({
"w:body": [ "w:body": [
{
"w:p": {},
},
{ {
"w:sectPr": [ "w:sectPr": [
{ "w:pgSz": { _attr: { "w:w": 10000, "w:h": 10000, "w:orient": "portrait" } } }, { "w:pgSz": { _attr: { "w:w": 10000, "w:h": 10000, "w:orient": "portrait" } } },

View File

@ -1,5 +1,6 @@
import { IXmlableObject, XmlComponent } from "file/xml-components"; import { IXmlableObject, XmlComponent } from "file/xml-components";
import { Paragraph, ParagraphProperties, TableOfContents } from "../.."; import { Paragraph, ParagraphProperties, TableOfContents } from "../..";
import { File } from "../../../file";
import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties"; import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties";
export class Body extends XmlComponent { export class Body extends XmlComponent {
@ -24,12 +25,13 @@ export class Body extends XmlComponent {
this.sections.push(new SectionProperties(options)); this.sections.push(new SectionProperties(options));
} }
public prepForXml(): IXmlableObject | undefined { public prepForXml(file?: File): IXmlableObject | undefined {
if (this.sections.length === 1) { if (this.sections.length === 1) {
this.root.splice(0, 1);
this.root.push(this.sections.pop() as SectionProperties); this.root.push(this.sections.pop() as SectionProperties);
} }
return super.prepForXml(); return super.prepForXml(file);
} }
public push(component: XmlComponent): void { public push(component: XmlComponent): void {
@ -43,7 +45,7 @@ export class Body extends XmlComponent {
private createSectionParagraph(section: SectionProperties): Paragraph { private createSectionParagraph(section: SectionProperties): Paragraph {
const paragraph = new Paragraph({}); const paragraph = new Paragraph({});
const properties = new ParagraphProperties({}); const properties = new ParagraphProperties({});
properties.addChildElement(section); properties.push(section);
paragraph.addChildElement(properties); paragraph.addChildElement(properties);
return paragraph; return paragraph;
} }

View File

@ -5,3 +5,4 @@ export * from "./page-size";
export * from "./page-number"; export * from "./page-number";
export * from "./page-border"; export * from "./page-border";
export * from "./line-number"; export * from "./line-number";
export * from "./vertical-align";

View File

@ -14,6 +14,7 @@ export enum PageNumberFormat {
ORDINAL_TEXT = "ordinalText", ORDINAL_TEXT = "ordinalText",
UPPER_LETTER = "upperLetter", UPPER_LETTER = "upperLetter",
UPPER_ROMAN = "upperRoman", UPPER_ROMAN = "upperRoman",
DECIMAL_FULL_WIDTH = "decimalFullWidth",
} }
export interface IPageNumberTypeAttributes { export interface IPageNumberTypeAttributes {

View File

@ -8,6 +8,7 @@ import { Media } from "file/media";
import { PageBorderOffsetFrom } from "./page-border"; import { PageBorderOffsetFrom } from "./page-border";
import { PageNumberFormat } from "./page-number"; import { PageNumberFormat } from "./page-number";
import { SectionProperties } from "./section-properties"; import { SectionProperties } from "./section-properties";
import { SectionVerticalAlignValue } from "./vertical-align";
describe("SectionProperties", () => { describe("SectionProperties", () => {
describe("#constructor()", () => { describe("#constructor()", () => {
@ -39,6 +40,7 @@ describe("SectionProperties", () => {
pageNumberStart: 10, pageNumberStart: 10,
pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT, pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT,
titlePage: true, titlePage: true,
verticalAlign: SectionVerticalAlignValue.TOP,
}); });
const tree = new Formatter().format(properties); const tree = new Formatter().format(properties);
expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]); expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]);

View File

@ -18,6 +18,7 @@ import { IPageNumberTypeAttributes, PageNumberType } from "./page-number";
import { PageSize } from "./page-size/page-size"; import { PageSize } from "./page-size/page-size";
import { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes"; import { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes";
import { TitlePage } from "./title-page/title-page"; import { TitlePage } from "./title-page/title-page";
import { ISectionVerticalAlignAttributes, SectionVerticalAlign } from "./vertical-align";
export interface IHeaderFooterGroup<T> { export interface IHeaderFooterGroup<T> {
readonly default?: T; readonly default?: T;
@ -45,7 +46,8 @@ export type SectionPropertiesOptions = IPageSizeAttributes &
IPageNumberTypeAttributes & IPageNumberTypeAttributes &
ILineNumberAttributes & ILineNumberAttributes &
IPageBordersOptions & IPageBordersOptions &
ITitlePageOptions & { ITitlePageOptions &
ISectionVerticalAlignAttributes & {
readonly column?: { readonly column?: {
readonly space?: number; readonly space?: number;
readonly count?: number; readonly count?: number;
@ -87,6 +89,7 @@ export class SectionProperties extends XmlComponent {
pageBorderBottom, pageBorderBottom,
pageBorderLeft, pageBorderLeft,
titlePage = false, titlePage = false,
verticalAlign,
} = options; } = options;
this.options = options; this.options = options;
@ -121,6 +124,10 @@ export class SectionProperties extends XmlComponent {
if (titlePage) { if (titlePage) {
this.root.push(new TitlePage()); this.root.push(new TitlePage());
} }
if (verticalAlign) {
this.root.push(new SectionVerticalAlign(verticalAlign));
}
} }
private addHeaders(headers?: IHeaderFooterGroup<HeaderWrapper>): void { private addHeaders(headers?: IHeaderFooterGroup<HeaderWrapper>): void {

View File

@ -0,0 +1,2 @@
export * from "./vertical-align";
export * from "./vertical-align-attributes";

View File

@ -0,0 +1,12 @@
import { XmlAttributeComponent } from "file/xml-components";
import { SectionVerticalAlignValue } from "./vertical-align";
export interface ISectionVerticalAlignAttributes {
readonly verticalAlign?: SectionVerticalAlignValue;
}
export class SectionVerticalAlignAttributes extends XmlAttributeComponent<ISectionVerticalAlignAttributes> {
protected readonly xmlKeys = {
verticalAlign: "w:val",
};
}

View File

@ -0,0 +1,18 @@
// http://officeopenxml.com/WPsection.php
import { XmlComponent } from "file/xml-components";
import { SectionVerticalAlignAttributes } from "./vertical-align-attributes";
export enum SectionVerticalAlignValue {
BOTH = "both",
BOTTOM = "bottom",
CENTER = "center",
TOP = "top",
}
export class SectionVerticalAlign extends XmlComponent {
constructor(value: SectionVerticalAlignValue) {
super("w:vAlign");
this.root.push(new SectionVerticalAlignAttributes({ verticalAlign: value }));
}
}

View File

@ -1,6 +1,6 @@
// http://officeopenxml.com/WPdocument.php // http://officeopenxml.com/WPdocument.php
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { Paragraph } from "../paragraph"; import { Hyperlink, Paragraph } from "../paragraph";
import { Table } from "../table"; import { Table } from "../table";
import { TableOfContents } from "../table-of-contents"; import { TableOfContents } from "../table-of-contents";
import { Body } from "./body"; import { Body } from "./body";
@ -36,7 +36,7 @@ export class Document extends XmlComponent {
this.root.push(this.body); this.root.push(this.body);
} }
public add(item: Paragraph | Table | TableOfContents): Document { public add(item: Paragraph | Table | TableOfContents | Hyperlink): Document {
this.body.push(item); this.body.push(item);
return this; return this;
} }

View File

@ -5,7 +5,7 @@ import { Formatter } from "export/formatter";
import { File } from "./file"; import { File } from "./file";
import { Footer, Header } from "./header"; import { Footer, Header } from "./header";
import { Paragraph } from "./paragraph"; import { HyperlinkRef, Paragraph } from "./paragraph";
import { Table, TableCell, TableRow } from "./table"; import { Table, TableCell, TableRow } from "./table";
import { TableOfContents } from "./table-of-contents"; import { TableOfContents } from "./table-of-contents";
@ -20,8 +20,8 @@ describe("File", () => {
const tree = new Formatter().format(doc.Document.Body); const tree = new Formatter().format(doc.Document.Body);
expect(tree["w:body"][1]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default");
expect(tree["w:body"][1]["w:sectPr"][5]["w:footerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][5]["w:footerReference"]._attr["w:type"]).to.equal("default");
}); });
it("should create with correct headers and footers", () => { it("should create with correct headers and footers", () => {
@ -39,8 +39,8 @@ describe("File", () => {
const tree = new Formatter().format(doc.Document.Body); const tree = new Formatter().format(doc.Document.Body);
expect(tree["w:body"][1]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default");
expect(tree["w:body"][1]["w:sectPr"][5]["w:footerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][5]["w:footerReference"]._attr["w:type"]).to.equal("default");
}); });
it("should create with first headers and footers", () => { it("should create with first headers and footers", () => {
@ -58,8 +58,8 @@ describe("File", () => {
const tree = new Formatter().format(doc.Document.Body); const tree = new Formatter().format(doc.Document.Body);
expect(tree["w:body"][1]["w:sectPr"][5]["w:headerReference"]._attr["w:type"]).to.equal("first"); expect(tree["w:body"][0]["w:sectPr"][5]["w:headerReference"]._attr["w:type"]).to.equal("first");
expect(tree["w:body"][1]["w:sectPr"][7]["w:footerReference"]._attr["w:type"]).to.equal("first"); expect(tree["w:body"][0]["w:sectPr"][7]["w:footerReference"]._attr["w:type"]).to.equal("first");
}); });
it("should create with correct headers", () => { it("should create with correct headers", () => {
@ -81,13 +81,98 @@ describe("File", () => {
const tree = new Formatter().format(doc.Document.Body); const tree = new Formatter().format(doc.Document.Body);
expect(tree["w:body"][1]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default");
expect(tree["w:body"][1]["w:sectPr"][5]["w:headerReference"]._attr["w:type"]).to.equal("first"); expect(tree["w:body"][0]["w:sectPr"][5]["w:headerReference"]._attr["w:type"]).to.equal("first");
expect(tree["w:body"][1]["w:sectPr"][6]["w:headerReference"]._attr["w:type"]).to.equal("even"); expect(tree["w:body"][0]["w:sectPr"][6]["w:headerReference"]._attr["w:type"]).to.equal("even");
expect(tree["w:body"][1]["w:sectPr"][7]["w:footerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][7]["w:footerReference"]._attr["w:type"]).to.equal("default");
expect(tree["w:body"][1]["w:sectPr"][8]["w:footerReference"]._attr["w:type"]).to.equal("first"); expect(tree["w:body"][0]["w:sectPr"][8]["w:footerReference"]._attr["w:type"]).to.equal("first");
expect(tree["w:body"][1]["w:sectPr"][9]["w:footerReference"]._attr["w:type"]).to.equal("even"); expect(tree["w:body"][0]["w:sectPr"][9]["w:footerReference"]._attr["w:type"]).to.equal("even");
});
it("should add child", () => {
const doc = new File(undefined, undefined, [
{
children: [new Paragraph("test")],
},
]);
const tree = new Formatter().format(doc.Document.Body);
expect(tree).to.deep.equal({
"w:body": [
{
"w:p": [
{
"w:r": [
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"test",
],
},
],
},
],
},
{
"w:sectPr": [
{
"w:pgSz": {
_attr: {
"w:h": 16838,
"w:orient": "portrait",
"w:w": 11906,
},
},
},
{
"w:pgMar": {
_attr: {
"w:bottom": 1440,
"w:footer": 708,
"w:gutter": 0,
"w:header": 708,
"w:left": 1440,
"w:mirrorMargins": false,
"w:right": 1440,
"w:top": 1440,
},
},
},
{
"w:cols": {
_attr: {
"w:num": 1,
"w:space": 708,
},
},
},
{
"w:docGrid": {
_attr: {
"w:linePitch": 360,
},
},
},
],
},
],
});
});
it("should add hyperlink child", () => {
const doc = new File(undefined, undefined, [
{
children: [new HyperlinkRef("test")],
},
]);
expect(doc.HyperlinkCache).to.deep.equal({});
}); });
}); });
@ -102,6 +187,16 @@ describe("File", () => {
expect(spy.called).to.equal(true); expect(spy.called).to.equal(true);
}); });
it("should add hyperlink child", () => {
const doc = new File();
doc.addSection({
children: [new HyperlinkRef("test")],
});
expect(doc.HyperlinkCache).to.deep.equal({});
});
it("should call the underlying document's add when adding a Table", () => { it("should call the underlying document's add when adding a Table", () => {
const file = new File(); const file = new File();
const spy = sinon.spy(file.Document, "add"); const spy = sinon.spy(file.Document, "add");
@ -148,13 +243,196 @@ describe("File", () => {
}); });
}); });
describe("#createFootnote", () => { describe("#HyperlinkCache", () => {
it("should call the underlying document's createFootnote", () => { it("should initially have empty hyperlink cache", () => {
const wrapper = new File(); const file = new File();
const spy = sinon.spy(wrapper.FootNotes, "createFootNote");
wrapper.createFootnote(new Paragraph(""));
expect(spy.called).to.equal(true); expect(file.HyperlinkCache).to.deep.equal({});
});
});
describe("#createFootnote", () => {
it("should create footnote", () => {
const wrapper = new File({
footnotes: [new Paragraph("hello")],
});
const tree = new Formatter().format(wrapper.FootNotes);
expect(tree).to.deep.equal({
"w:footnotes": [
{
_attr: {
"mc:Ignorable": "w14 w15 wp14",
"xmlns:m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
"xmlns:o": "urn:schemas-microsoft-com:office:office",
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"xmlns:v": "urn:schemas-microsoft-com:vml",
"xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"xmlns:w10": "urn:schemas-microsoft-com:office:word",
"xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml",
"xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml",
"xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml",
"xmlns:wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"xmlns:wp14": "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
"xmlns:wpc": "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
"xmlns:wpg": "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
"xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
"xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
},
},
{
"w:footnote": [
{
_attr: {
"w:id": -1,
"w:type": "separator",
},
},
{
"w:p": [
{
"w:pPr": [
{
"w:spacing": {
_attr: {
"w:after": 0,
"w:line": 240,
"w:lineRule": "auto",
},
},
},
],
},
{
"w:r": [
{
"w:rPr": [
{
"w:rStyle": {
_attr: {
"w:val": "FootnoteReference",
},
},
},
],
},
{
"w:footnoteRef": {},
},
],
},
{
"w:r": [
{
"w:separator": {},
},
],
},
],
},
],
},
{
"w:footnote": [
{
_attr: {
"w:id": 0,
"w:type": "continuationSeparator",
},
},
{
"w:p": [
{
"w:pPr": [
{
"w:spacing": {
_attr: {
"w:after": 0,
"w:line": 240,
"w:lineRule": "auto",
},
},
},
],
},
{
"w:r": [
{
"w:rPr": [
{
"w:rStyle": {
_attr: {
"w:val": "FootnoteReference",
},
},
},
],
},
{
"w:footnoteRef": {},
},
],
},
{
"w:r": [
{
"w:continuationSeparator": {},
},
],
},
],
},
],
},
{
"w:footnote": [
{
_attr: {
"w:id": 1,
},
},
{
"w:p": [
{
"w:r": [
{
"w:rPr": [
{
"w:rStyle": {
_attr: {
"w:val": "FootnoteReference",
},
},
},
],
},
{
"w:footnoteRef": {},
},
],
},
{
"w:r": [
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"hello",
],
},
],
},
],
},
],
},
],
});
}); });
}); });
}); });

View File

@ -1,3 +1,4 @@
import * as shortid from "shortid";
import { AppProperties } from "./app-properties/app-properties"; import { AppProperties } from "./app-properties/app-properties";
import { ContentTypes } from "./content-types/content-types"; import { ContentTypes } from "./content-types/content-types";
import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { CoreProperties, IPropertiesOptions } from "./core-properties";
@ -16,7 +17,7 @@ import { Footer, Header } from "./header";
import { HeaderWrapper, IDocumentHeader } from "./header-wrapper"; import { HeaderWrapper, IDocumentHeader } from "./header-wrapper";
import { Media } from "./media"; import { Media } from "./media";
import { Numbering } from "./numbering"; import { Numbering } from "./numbering";
import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; import { Hyperlink, HyperlinkRef, HyperlinkType, Paragraph } from "./paragraph";
import { Relationships } from "./relationships"; import { Relationships } from "./relationships";
import { TargetModeType } from "./relationships/relationship/relationship"; import { TargetModeType } from "./relationships/relationship/relationship";
import { Settings } from "./settings"; import { Settings } from "./settings";
@ -40,7 +41,7 @@ export interface ISectionOptions {
readonly size?: IPageSizeAttributes; readonly size?: IPageSizeAttributes;
readonly margins?: IPageMarginAttributes; readonly margins?: IPageMarginAttributes;
readonly properties?: SectionPropertiesOptions; readonly properties?: SectionPropertiesOptions;
readonly children: Array<Paragraph | Table | TableOfContents>; readonly children: (Paragraph | Table | TableOfContents | HyperlinkRef)[];
} }
export class File { export class File {
@ -60,6 +61,7 @@ export class File {
private readonly contentTypes: ContentTypes; private readonly contentTypes: ContentTypes;
private readonly appProperties: AppProperties; private readonly appProperties: AppProperties;
private readonly styles: Styles; private readonly styles: Styles;
private readonly hyperlinkCache: { readonly [key: string]: Hyperlink } = {};
constructor( constructor(
options: IPropertiesOptions = { options: IPropertiesOptions = {
@ -71,7 +73,13 @@ export class File {
sections: ISectionOptions[] = [], sections: ISectionOptions[] = [],
) { ) {
this.coreProperties = new CoreProperties(options); this.coreProperties = new CoreProperties(options);
this.numbering = new Numbering(); this.numbering = new Numbering(
options.numbering
? options.numbering
: {
config: [],
},
);
this.docRelationships = new Relationships(); this.docRelationships = new Relationships();
this.fileRelationships = new Relationships(); this.fileRelationships = new Relationships();
this.appProperties = new AppProperties(); this.appProperties = new AppProperties();
@ -126,33 +134,42 @@ export class File {
this.document.Body.addSection(section.properties ? section.properties : {}); this.document.Body.addSection(section.properties ? section.properties : {});
for (const child of section.children) { for (const child of section.children) {
if (child instanceof HyperlinkRef) {
const hyperlink = this.hyperlinkCache[child.id];
this.document.add(hyperlink);
continue;
}
this.document.add(child); this.document.add(child);
} }
} }
if (options.footnotes) {
for (const paragraph of options.footnotes) {
this.footNotes.createFootNote(paragraph);
}
} }
public createHyperlink(link: string, text?: string): Hyperlink { if (options.hyperlinks) {
const newText = text === undefined ? link : text; const cache = {};
const hyperlink = new Hyperlink(newText, this.docRelationships.RelationshipCount);
this.docRelationships.createRelationship( for (const key in options.hyperlinks) {
hyperlink.linkId, if (!options.hyperlinks[key]) {
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", continue;
link,
TargetModeType.EXTERNAL,
);
return hyperlink;
} }
public createInternalHyperLink(anchor: string, text?: string): Hyperlink { const hyperlinkRef = options.hyperlinks[key];
const newText = text === undefined ? anchor : text;
const hyperlink = new Hyperlink(newText, this.docRelationships.RelationshipCount, anchor); const hyperlink =
// NOTE: unlike File#createHyperlink(), since the link is to an internal bookmark hyperlinkRef.type === HyperlinkType.EXTERNAL
// we don't need to create a new relationship. ? this.createHyperlink(hyperlinkRef.link, hyperlinkRef.text)
return hyperlink; : this.createInternalHyperLink(key, hyperlinkRef.text);
cache[key] = hyperlink;
} }
public createBookmark(name: string, text: string = name): Bookmark { this.hyperlinkCache = cache;
return new Bookmark(name, text, this.docRelationships.RelationshipCount); }
} }
public addSection({ public addSection({
@ -180,12 +197,14 @@ export class File {
}); });
for (const child of children) { for (const child of children) {
this.document.add(child); if (child instanceof HyperlinkRef) {
} const hyperlink = this.hyperlinkCache[child.id];
this.document.add(hyperlink);
continue;
} }
public createFootnote(paragraph: Paragraph): void { this.document.add(child);
this.footNotes.createFootNote(paragraph); }
} }
public verifyUpdateFields(): void { public verifyUpdateFields(): void {
@ -194,6 +213,24 @@ export class File {
} }
} }
private createHyperlink(link: string, text: string = link): Hyperlink {
const hyperlink = new Hyperlink(text, shortid.generate().toLowerCase());
this.docRelationships.createRelationship(
hyperlink.linkId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
link,
TargetModeType.EXTERNAL,
);
return hyperlink;
}
private createInternalHyperLink(anchor: string, text: string = anchor): Hyperlink {
const hyperlink = new Hyperlink(text, shortid.generate().toLowerCase(), anchor);
// NOTE: unlike File#createHyperlink(), since the link is to an internal bookmark
// we don't need to create a new relationship.
return hyperlink;
}
private createHeader(header: Header): HeaderWrapper { private createHeader(header: Header): HeaderWrapper {
const wrapper = new HeaderWrapper(this.media, this.currentRelationshipId++); const wrapper = new HeaderWrapper(this.media, this.currentRelationshipId++);
@ -326,4 +363,8 @@ export class File {
public get Settings(): Settings { public get Settings(): Settings {
return this.settings; return this.settings;
} }
public get HyperlinkCache(): { readonly [key: string]: Hyperlink } {
return this.hyperlinkCache;
}
} }

View File

@ -0,0 +1 @@
export * from "./run";

View File

@ -0,0 +1 @@
export * from "./reference-run";

View File

@ -1 +1,2 @@
export * from "./footnotes"; export * from "./footnotes";
export * from "./footnote";

View File

@ -2,7 +2,7 @@ import { Paragraph } from "./paragraph";
import { Table } from "./table"; import { Table } from "./table";
export interface IHeaderOptions { export interface IHeaderOptions {
readonly children: Array<Paragraph | Table>; readonly children: (Paragraph | Table)[];
} }
export class Header { export class Header {

View File

@ -12,3 +12,5 @@ export * from "./xml-components";
export * from "./header-wrapper"; export * from "./header-wrapper";
export * from "./footer-wrapper"; export * from "./footer-wrapper";
export * from "./header"; export * from "./header";
export * from "./footnotes";
export * from "./track-revision";

View File

@ -21,14 +21,7 @@ export class Media {
private static generateId(): string { private static generateId(): string {
// https://gist.github.com/6174/6062387 // https://gist.github.com/6174/6062387
return ( return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
Math.random()
.toString(36)
.substring(2, 15) +
Math.random()
.toString(36)
.substring(2, 15)
);
} }
private readonly map: Map<string, IMediaData>; private readonly map: Map<string, IMediaData>;

View File

@ -0,0 +1,823 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { EMPTY_OBJECT } from "file/xml-components";
import { AlignmentType, EmphasisMarkType, TabStopPosition } from "../paragraph";
import { UnderlineType } from "../paragraph/run/underline";
import { ShadingType } from "../table";
import { AbstractNumbering } from "./abstract-numbering";
import { LevelSuffix } from "./level";
describe("AbstractNumbering", () => {
it("stores its ID at its .id property", () => {
const abstractNumbering = new AbstractNumbering(5, []);
expect(abstractNumbering.id).to.equal(5);
});
describe("#createLevel", () => {
it("creates a level with the given characteristics", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 3,
format: "lowerLetter",
text: "%1)",
alignment: AlignmentType.END,
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ _attr: { "w:ilvl": 3, "w15:tentative": 1 } });
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:start": { _attr: { "w:val": 1 } } });
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:lvlJc": { _attr: { "w:val": "end" } } });
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:numFmt": { _attr: { "w:val": "lowerLetter" } } });
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:lvlText": { _attr: { "w:val": "%1)" } } });
});
it("uses 'start' as the default alignment", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 3,
format: "lowerLetter",
text: "%1)",
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ _attr: { "w:ilvl": 3, "w15:tentative": 1 } });
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:start": { _attr: { "w:val": 1 } } });
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:lvlJc": { _attr: { "w:val": "start" } } });
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:numFmt": { _attr: { "w:val": "lowerLetter" } } });
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:lvlText": { _attr: { "w:val": "%1)" } } });
});
it("has suffix", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 3,
format: "lowerLetter",
text: "%1)",
alignment: AlignmentType.END,
suffix: LevelSuffix.SPACE,
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:suff": { _attr: { "w:val": "space" } } });
});
describe("formatting methods: paragraph properties", () => {
it("#indent", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
indent: { left: 720 },
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [{ "w:ind": { _attr: { "w:left": 720 } } }],
});
});
it("#spacing", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
spacing: { before: 50, after: 150 },
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [{ "w:spacing": { _attr: { "w:before": 50, "w:after": 150 } } }],
});
});
it("#center", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
alignment: AlignmentType.CENTER,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [{ "w:jc": { _attr: { "w:val": "center" } } }],
});
});
it("#left", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
alignment: AlignmentType.LEFT,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [{ "w:jc": { _attr: { "w:val": "left" } } }],
});
});
it("#right", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
alignment: AlignmentType.RIGHT,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [{ "w:jc": { _attr: { "w:val": "right" } } }],
});
});
it("#justified", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
alignment: AlignmentType.JUSTIFIED,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [{ "w:jc": { _attr: { "w:val": "both" } } }],
});
});
it("#thematicBreak", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
thematicBreak: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [
{
"w:pBdr": [
{
"w:bottom": {
_attr: {
"w:color": "auto",
"w:space": 1,
"w:val": "single",
"w:sz": 6,
},
},
},
],
},
],
});
});
it("#leftTabStop", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
leftTabStop: 1200,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [
{
"w:tabs": [{ "w:tab": { _attr: { "w:val": "left", "w:pos": 1200 } } }],
},
],
});
});
it("#maxRightTabStop", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
rightTabStop: TabStopPosition.MAX,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [
{
"w:tabs": [{ "w:tab": { _attr: { "w:val": "right", "w:pos": 9026 } } }],
},
],
});
});
it("#keepLines", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
keepLines: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [{ "w:keepLines": EMPTY_OBJECT }],
});
});
it("#keepNext", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
paragraph: {
keepNext: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:pPr": [{ "w:keepNext": EMPTY_OBJECT }],
});
});
});
describe("formatting methods: run properties", () => {
const sizeTests = [
{
size: 24,
expected: [{ "w:sz": { _attr: { "w:val": 24 } } }, { "w:szCs": { _attr: { "w:val": 24 } } }],
},
{
size: 24,
sizeComplexScript: true,
expected: [{ "w:sz": { _attr: { "w:val": 24 } } }, { "w:szCs": { _attr: { "w:val": 24 } } }],
},
{
size: 24,
sizeComplexScript: false,
expected: [{ "w:sz": { _attr: { "w:val": 24 } } }],
},
{
size: 24,
sizeComplexScript: 26,
expected: [{ "w:sz": { _attr: { "w:val": 24 } } }, { "w:szCs": { _attr: { "w:val": 26 } } }],
},
];
sizeTests.forEach(({ size, sizeComplexScript, expected }) => {
it(`#size ${size} cs ${sizeComplexScript}`, () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: { size, sizeComplexScript },
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:rPr": expected });
});
});
it("#smallCaps", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
smallCaps: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:smallCaps": { _attr: { "w:val": true } } }],
});
});
it("#allCaps", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
allCaps: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:caps": { _attr: { "w:val": true } } }],
});
});
it("#strike", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
strike: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:strike": { _attr: { "w:val": true } } }],
});
});
it("#doubleStrike", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
doubleStrike: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:dstrike": { _attr: { "w:val": true } } }],
});
});
it("#subScript", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
subScript: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "subscript" } } }],
});
});
it("#superScript", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
superScript: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "superscript" } } }],
});
});
it("#font by name", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
font: "Times",
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [
{
"w:rFonts": {
_attr: {
"w:ascii": "Times",
"w:cs": "Times",
"w:eastAsia": "Times",
"w:hAnsi": "Times",
},
},
},
],
});
});
it("#font for ascii and eastAsia", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
font: {
ascii: "Times",
eastAsia: "KaiTi",
},
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [
{
"w:rFonts": {
_attr: {
"w:ascii": "Times",
"w:eastAsia": "KaiTi",
},
},
},
],
});
});
const boldTests = [
{
bold: true,
expected: [{ "w:b": { _attr: { "w:val": true } } }, { "w:bCs": { _attr: { "w:val": true } } }],
},
{
bold: true,
boldComplexScript: true,
expected: [{ "w:b": { _attr: { "w:val": true } } }, { "w:bCs": { _attr: { "w:val": true } } }],
},
{
bold: true,
boldComplexScript: false,
expected: [{ "w:b": { _attr: { "w:val": true } } }],
},
];
boldTests.forEach(({ bold, boldComplexScript, expected }) => {
it(`#bold ${bold} cs ${boldComplexScript}`, () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: { bold, boldComplexScript },
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:rPr": expected });
});
});
const italicsTests = [
{
italics: true,
expected: [{ "w:i": { _attr: { "w:val": true } } }, { "w:iCs": { _attr: { "w:val": true } } }],
},
{
italics: true,
italicsComplexScript: true,
expected: [{ "w:i": { _attr: { "w:val": true } } }, { "w:iCs": { _attr: { "w:val": true } } }],
},
{
italics: true,
italicsComplexScript: false,
expected: [{ "w:i": { _attr: { "w:val": true } } }],
},
];
italicsTests.forEach(({ italics, italicsComplexScript, expected }) => {
it(`#italics ${italics} cs ${italicsComplexScript}`, () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: { italics, italicsComplexScript },
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:rPr": expected });
});
});
const highlightTests = [
{
highlight: "005599",
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "005599" } } }],
},
{
highlight: "005599",
highlightComplexScript: true,
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "005599" } } }],
},
{
highlight: "005599",
highlightComplexScript: false,
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }],
},
{
highlight: "005599",
highlightComplexScript: "550099",
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "550099" } } }],
},
];
highlightTests.forEach(({ highlight, highlightComplexScript, expected }) => {
it(`#highlight ${highlight} cs ${highlightComplexScript}`, () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: { highlight, highlightComplexScript },
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:rPr": expected });
});
});
const shadingTests = [
{
shadow: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "FF0000",
},
expected: [
{ "w:shd": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } },
{ "w:shdCs": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } },
],
},
{
shading: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "FF0000",
},
expected: [
{ "w:shd": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } },
{ "w:shdCs": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } },
],
},
{
shading: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "FF0000",
},
shadingComplexScript: true,
expected: [
{ "w:shd": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } },
{ "w:shdCs": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } },
],
},
{
shading: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "FF0000",
},
shadingComplexScript: false,
expected: [{ "w:shd": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } }],
},
{
shading: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "FF0000",
},
shadingComplexScript: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "00FF00",
},
expected: [
{ "w:shd": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } },
{ "w:shdCs": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "00FF00" } } },
],
},
];
shadingTests.forEach(({ shadow, shading, shadingComplexScript, expected }) => {
it("#shadow correctly", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: { shadow, shading, shadingComplexScript },
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:rPr": expected });
});
});
describe("#underline", () => {
it("should set underline to 'single' if no arguments are given", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
underline: {},
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:u": { _attr: { "w:val": "single" } } }],
});
});
it("should set the style if given", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
underline: {
type: UnderlineType.DOUBLE,
},
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:u": { _attr: { "w:val": "double" } } }],
});
});
it("should set the style and color if given", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
underline: {
type: UnderlineType.DOUBLE,
color: "005599",
},
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:u": { _attr: { "w:val": "double", "w:color": "005599" } } }],
});
});
});
describe("#emphasisMark", () => {
it("should set emphasisMark to 'dot' if no arguments are given", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
emphasisMark: {},
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:em": { _attr: { "w:val": "dot" } } }],
});
});
it("should set the style if given", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
emphasisMark: {
type: EmphasisMarkType.DOT,
},
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:em": { _attr: { "w:val": "dot" } } }],
});
});
});
it("#color", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
color: "123456",
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:color": { _attr: { "w:val": "123456" } } }],
});
});
});
});
});

View File

@ -1,5 +1,6 @@
import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { Level } from "./level";
import { ILevelsOptions, Level } from "./level";
import { MultiLevelType } from "./multi-level-type"; import { MultiLevelType } from "./multi-level-type";
interface IAbstractNumberingAttributesProperties { interface IAbstractNumberingAttributesProperties {
@ -17,7 +18,7 @@ class AbstractNumberingAttributes extends XmlAttributeComponent<IAbstractNumberi
export class AbstractNumbering extends XmlComponent { export class AbstractNumbering extends XmlComponent {
public readonly id: number; public readonly id: number;
constructor(id: number) { constructor(id: number, levelOptions: ILevelsOptions[]) {
super("w:abstractNum"); super("w:abstractNum");
this.root.push( this.root.push(
new AbstractNumberingAttributes({ new AbstractNumberingAttributes({
@ -27,15 +28,9 @@ export class AbstractNumbering extends XmlComponent {
); );
this.root.push(new MultiLevelType("hybridMultilevel")); this.root.push(new MultiLevelType("hybridMultilevel"));
this.id = id; this.id = id;
}
public addLevel(level: Level): void { for (const option of levelOptions) {
this.root.push(level); this.root.push(new Level(option));
} }
public createLevel(num: number, format: string, text: string, align: string = "start"): Level {
const level = new Level(num, format, text, align);
this.addLevel(level);
return level;
} }
} }

View File

@ -0,0 +1,79 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { LevelForOverride } from "./level";
import { ConcreteNumbering } from "./num";
describe("ConcreteNumbering", () => {
describe("#overrideLevel", () => {
let concreteNumbering: ConcreteNumbering;
beforeEach(() => {
concreteNumbering = new ConcreteNumbering(0, 1);
});
it("sets a new override level for the given level number", () => {
concreteNumbering.overrideLevel(3);
const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [
{ _attr: { "w:ilvl": 3 } },
{
"w:lvl": [
{ _attr: { "w:ilvl": 3, "w15:tentative": 1 } },
{ "w:start": { _attr: { "w:val": 1 } } },
{ "w:lvlJc": { _attr: { "w:val": "start" } } },
],
},
],
});
});
it("sets the startOverride element if start is given", () => {
concreteNumbering.overrideLevel(1, 9);
const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [
{
_attr: {
"w:ilvl": 1,
},
},
{
"w:startOverride": {
_attr: {
"w:val": 9,
},
},
},
{
"w:lvl": [
{ _attr: { "w:ilvl": 1, "w15:tentative": 1 } },
{ "w:start": { _attr: { "w:val": 1 } } },
{ "w:lvlJc": { _attr: { "w:val": "start" } } },
],
},
],
});
});
it("sets the lvl element if overrideLevel.Level is accessed", () => {
const ol = concreteNumbering.overrideLevel(1);
expect(ol.Level).to.be.instanceof(LevelForOverride);
const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [
{ _attr: { "w:ilvl": 1 } },
{
"w:lvl": [
{ _attr: { "w:ilvl": 1, "w15:tentative": 1 } },
{ "w:start": { _attr: { "w:val": 1 } } },
{ "w:lvlJc": { _attr: { "w:val": "start" } } },
],
},
],
});
});
});
});

View File

@ -1,21 +1,7 @@
import { Attributes, XmlAttributeComponent, XmlComponent } from "file/xml-components"; import { Attributes, XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { import { AlignmentType } from "../paragraph/formatting";
Alignment, import { IParagraphStylePropertiesOptions, ParagraphProperties } from "../paragraph/properties";
AlignmentType, import { IRunStylePropertiesOptions, RunProperties } from "../paragraph/run/properties";
IIndentAttributesProperties,
Indent,
ISpacingProperties,
KeepLines,
KeepNext,
Spacing,
TabStop,
TabStopType,
ThematicBreak,
} from "../paragraph/formatting";
import { ParagraphProperties } from "../paragraph/properties";
import * as formatting from "../paragraph/run/formatting";
import { RunProperties } from "../paragraph/run/properties";
import { UnderlineType } from "../paragraph/run/underline";
interface ILevelAttributesProperties { interface ILevelAttributesProperties {
readonly ilvl?: number; readonly ilvl?: number;
@ -63,7 +49,7 @@ class LevelText extends XmlComponent {
} }
class LevelJc extends XmlComponent { class LevelJc extends XmlComponent {
constructor(value: string) { constructor(value: AlignmentType) {
super("w:lvlJc"); super("w:lvlJc");
this.root.push( this.root.push(
new Attributes({ new Attributes({
@ -79,6 +65,19 @@ export enum LevelSuffix {
TAB = "tab", TAB = "tab",
} }
export interface ILevelsOptions {
readonly level: number;
readonly format?: string;
readonly text?: string;
readonly alignment?: AlignmentType;
readonly start?: number;
readonly suffix?: LevelSuffix;
readonly style?: {
readonly run?: IRunStylePropertiesOptions;
readonly paragraph?: IParagraphStylePropertiesOptions;
};
}
class Suffix extends XmlComponent { class Suffix extends XmlComponent {
constructor(value: LevelSuffix) { constructor(value: LevelSuffix) {
super("w:suff"); super("w:suff");
@ -94,7 +93,7 @@ export class LevelBase extends XmlComponent {
private readonly paragraphProperties: ParagraphProperties; private readonly paragraphProperties: ParagraphProperties;
private readonly runProperties: RunProperties; private readonly runProperties: RunProperties;
constructor(level: number, start?: number, numberFormat?: string, levelText?: string, lvlJc?: string) { constructor({ level, format, text, alignment = AlignmentType.START, start = 1, style, suffix }: ILevelsOptions) {
super("w:lvl"); super("w:lvl");
this.root.push( this.root.push(
new LevelAttributes({ new LevelAttributes({
@ -103,174 +102,34 @@ export class LevelBase extends XmlComponent {
}), }),
); );
if (start !== undefined) {
this.root.push(new Start(start)); this.root.push(new Start(start));
} this.root.push(new LevelJc(alignment));
if (numberFormat !== undefined) {
this.root.push(new NumberFormat(numberFormat)); if (format) {
} this.root.push(new NumberFormat(format));
if (levelText !== undefined) {
this.root.push(new LevelText(levelText));
}
if (lvlJc !== undefined) {
this.root.push(new LevelJc(lvlJc));
} }
this.paragraphProperties = new ParagraphProperties({}); if (text) {
this.runProperties = new RunProperties(); this.root.push(new LevelText(text));
}
this.paragraphProperties = new ParagraphProperties(style && style.paragraph);
this.runProperties = new RunProperties(style && style.run);
this.root.push(this.paragraphProperties); this.root.push(this.paragraphProperties);
this.root.push(this.runProperties); this.root.push(this.runProperties);
if (suffix) {
this.root.push(new Suffix(suffix));
} }
public setSuffix(value: LevelSuffix): LevelBase {
this.root.push(new Suffix(value));
return this;
}
public addParagraphProperty(property: XmlComponent): Level {
this.paragraphProperties.push(property);
return this;
}
public addRunProperty(property: XmlComponent): Level {
this.runProperties.push(property);
return this;
}
// ---------- Run formatting ---------------------- //
public size(twips: number): Level {
this.addRunProperty(new formatting.Size(twips));
return this;
}
public bold(): Level {
this.addRunProperty(new formatting.Bold());
return this;
}
public italics(): Level {
this.addRunProperty(new formatting.Italics());
return this;
}
public smallCaps(): Level {
this.addRunProperty(new formatting.SmallCaps());
return this;
}
public allCaps(): Level {
this.addRunProperty(new formatting.Caps());
return this;
}
public strike(): Level {
this.addRunProperty(new formatting.Strike());
return this;
}
public doubleStrike(): Level {
this.addRunProperty(new formatting.DoubleStrike());
return this;
}
public subScript(): Level {
this.addRunProperty(new formatting.SubScript());
return this;
}
public superScript(): Level {
this.addRunProperty(new formatting.SuperScript());
return this;
}
public underline(underlineType?: UnderlineType, color?: string): Level {
this.addRunProperty(new formatting.Underline(underlineType, color));
return this;
}
public color(color: string): Level {
this.addRunProperty(new formatting.Color(color));
return this;
}
public font(fontName: string): Level {
this.addRunProperty(new formatting.RunFonts(fontName));
return this;
}
public highlight(color: string): Level {
this.addRunProperty(new formatting.Highlight(color));
return this;
}
public shadow(value: string, fill: string, color: string): Level {
this.addRunProperty(new formatting.Shading(value, fill, color));
return this;
}
// --------------------- Paragraph formatting ------------------------ //
public center(): Level {
this.addParagraphProperty(new Alignment(AlignmentType.CENTER));
return this;
}
public left(): Level {
this.addParagraphProperty(new Alignment(AlignmentType.LEFT));
return this;
}
public right(): Level {
this.addParagraphProperty(new Alignment(AlignmentType.RIGHT));
return this;
}
public justified(): Level {
this.addParagraphProperty(new Alignment(AlignmentType.BOTH));
return this;
}
public thematicBreak(): Level {
this.addParagraphProperty(new ThematicBreak());
return this;
}
public rightTabStop(position: number): Level {
return this.addParagraphProperty(new TabStop(TabStopType.RIGHT, position));
}
public leftTabStop(position: number): Level {
this.addParagraphProperty(new TabStop(TabStopType.LEFT, position));
return this;
}
public indent(attrs: IIndentAttributesProperties): Level {
this.addParagraphProperty(new Indent(attrs));
return this;
}
public spacing(params: ISpacingProperties): Level {
this.addParagraphProperty(new Spacing(params));
return this;
}
public keepNext(): Level {
this.addParagraphProperty(new KeepNext());
return this;
}
public keepLines(): Level {
this.addParagraphProperty(new KeepLines());
return this;
} }
} }
export class Level extends LevelBase { export class Level extends LevelBase {
// This is the level that sits under abstractNum. We make a // This is the level that sits under abstractNum. We make a
// handful of properties required // handful of properties required
constructor(level: number, numberFormat: string, levelText: string, lvlJc: string) { constructor(options: ILevelsOptions) {
super(level, 1, numberFormat, levelText, lvlJc); super(options);
} }
} }

View File

@ -20,10 +20,10 @@ class NumAttributes extends XmlAttributeComponent<INumAttributesProperties> {
protected readonly xmlKeys = { numId: "w:numId" }; protected readonly xmlKeys = { numId: "w:numId" };
} }
export class Num extends XmlComponent { export class ConcreteNumbering extends XmlComponent {
public readonly id: number; public readonly id: number;
constructor(numId: number, abstractNumId: number) { constructor(numId: number, abstractNumId: number, public readonly reference?: string) {
super("w:num"); super("w:num");
this.root.push( this.root.push(
new NumAttributes({ new NumAttributes({
@ -55,7 +55,9 @@ export class LevelOverride extends XmlComponent {
this.root.push(new StartOverride(start)); this.root.push(new StartOverride(start));
} }
this.lvl = new LevelForOverride(this.levelNum); this.lvl = new LevelForOverride({
level: this.levelNum,
});
this.root.push(this.lvl); this.root.push(this.lvl);
} }

View File

@ -2,24 +2,15 @@ import { expect } from "chai";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { AbstractNumbering } from "./abstract-numbering";
import { LevelForOverride } from "./level";
import { Num } from "./num";
import { Numbering } from "./numbering"; import { Numbering } from "./numbering";
import { EMPTY_OBJECT } from "file/xml-components";
import { TabStopPosition } from "../paragraph";
import { UnderlineType } from "../paragraph/run/underline";
describe("Numbering", () => { describe("Numbering", () => {
let numbering: Numbering;
beforeEach(() => {
numbering = new Numbering();
});
describe("#constructor", () => { describe("#constructor", () => {
it("creates a default numbering with one abstract and one concrete instance", () => { it("creates a default numbering with one abstract and one concrete instance", () => {
const numbering = new Numbering({
config: [],
});
const tree = new Formatter().format(numbering); const tree = new Formatter().format(numbering);
expect(Object.keys(tree)).to.deep.equal(["w:numbering"]); expect(Object.keys(tree)).to.deep.equal(["w:numbering"]);
const abstractNums = tree["w:numbering"].filter((el) => el["w:abstractNum"]); const abstractNums = tree["w:numbering"].filter((el) => el["w:abstractNum"]);
@ -48,418 +39,4 @@ describe("Numbering", () => {
}); });
}); });
}); });
describe("#createAbstractNumbering", () => {
it("returns a new AbstractNumbering instance", () => {
const a2 = numbering.createAbstractNumbering();
expect(a2).to.be.instanceof(AbstractNumbering);
});
it("assigns a unique ID to each abstract numbering it creates", () => {
const a2 = numbering.createAbstractNumbering();
const a3 = numbering.createAbstractNumbering();
expect(a2.id).not.to.equal(a3.id);
});
});
describe("#createConcreteNumbering", () => {
it("returns a new Num instance with its abstract ID set to the AbstractNumbering's ID", () => {
const a2 = numbering.createAbstractNumbering();
const n = numbering.createConcreteNumbering(a2);
expect(n).to.be.instanceof(Num);
const tree = new Formatter().format(numbering);
const serializedN = tree["w:numbering"].find((obj) => obj["w:num"] && obj["w:num"][0]._attr["w:numId"] === n.id);
expect(serializedN["w:num"][1]["w:abstractNumId"]._attr["w:val"]).to.equal(a2.id);
});
it("assigns a unique ID to each concrete numbering it creates", () => {
const a2 = numbering.createAbstractNumbering();
const n = numbering.createConcreteNumbering(a2);
const n2 = numbering.createConcreteNumbering(a2);
expect(n.id).not.to.equal(n2.id);
});
});
});
describe("AbstractNumbering", () => {
it("stores its ID at its .id property", () => {
const abstractNumbering = new AbstractNumbering(5);
expect(abstractNumbering.id).to.equal(5);
});
describe("#createLevel", () => {
it("creates a level with the given characteristics", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(3, "lowerLetter", "%1)", "end");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({ _attr: { "w:ilvl": 3, "w15:tentative": 1 } });
expect(tree["w:lvl"]).to.include({ "w:start": { _attr: { "w:val": 1 } } });
expect(tree["w:lvl"]).to.include({ "w:lvlJc": { _attr: { "w:val": "end" } } });
expect(tree["w:lvl"]).to.include({ "w:numFmt": { _attr: { "w:val": "lowerLetter" } } });
expect(tree["w:lvl"]).to.include({ "w:lvlText": { _attr: { "w:val": "%1)" } } });
});
it("uses 'start' as the default alignment", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(3, "lowerLetter", "%1)");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({ _attr: { "w:ilvl": 3, "w15:tentative": 1 } });
expect(tree["w:lvl"]).to.include({ "w:start": { _attr: { "w:val": 1 } } });
expect(tree["w:lvl"]).to.include({ "w:lvlJc": { _attr: { "w:val": "start" } } });
expect(tree["w:lvl"]).to.include({ "w:numFmt": { _attr: { "w:val": "lowerLetter" } } });
expect(tree["w:lvl"]).to.include({ "w:lvlText": { _attr: { "w:val": "%1)" } } });
});
describe("formatting methods: paragraph properties", () => {
it("#indent", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerLetter", "%0.").indent({ left: 720 });
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [{ "w:ind": { _attr: { "w:left": 720 } } }],
});
});
it("#spacing", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerLetter", "%0.").spacing({ before: 50, after: 150 });
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [{ "w:spacing": { _attr: { "w:before": 50, "w:after": 150 } } }],
});
});
it("#center", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerLetter", "%0.").center();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [{ "w:jc": { _attr: { "w:val": "center" } } }],
});
});
it("#left", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.", "left").left();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [{ "w:jc": { _attr: { "w:val": "left" } } }],
});
});
it("#right", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").right();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [{ "w:jc": { _attr: { "w:val": "right" } } }],
});
});
it("#justified", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").justified();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [{ "w:jc": { _attr: { "w:val": "both" } } }],
});
});
it("#thematicBreak", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").thematicBreak();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [
{
"w:pBdr": [
{
"w:bottom": {
_attr: {
"w:color": "auto",
"w:space": 1,
"w:val": "single",
"w:sz": 6,
},
},
},
],
},
],
});
});
it("#leftTabStop", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").leftTabStop(1200);
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [
{
"w:tabs": [{ "w:tab": { _attr: { "w:val": "left", "w:pos": 1200 } } }],
},
],
});
});
it("#maxRightTabStop", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").rightTabStop(TabStopPosition.MAX);
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [
{
"w:tabs": [{ "w:tab": { _attr: { "w:val": "right", "w:pos": 9026 } } }],
},
],
});
});
it("#keepLines", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").keepLines();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [{ "w:keepLines": EMPTY_OBJECT }],
});
});
it("#keepNext", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").keepNext();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [{ "w:keepNext": EMPTY_OBJECT }],
});
});
});
describe("formatting methods: run properties", () => {
it("#size", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").size(24);
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:sz": { _attr: { "w:val": 24 } } }],
});
});
it("#smallCaps", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").smallCaps();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:smallCaps": { _attr: { "w:val": true } } }],
});
});
it("#allCaps", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").allCaps();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:caps": { _attr: { "w:val": true } } }],
});
});
it("#strike", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").strike();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:strike": { _attr: { "w:val": true } } }],
});
});
it("#doubleStrike", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").doubleStrike();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:dstrike": { _attr: { "w:val": true } } }],
});
});
it("#subScript", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").subScript();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "subscript" } } }],
});
});
it("#superScript", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").superScript();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "superscript" } } }],
});
});
it("#font", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").font("Times");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [
{ "w:rFonts": { _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } } },
],
});
});
it("#bold", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").bold();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:b": { _attr: { "w:val": true } } }],
});
});
it("#italics", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").italics();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:i": { _attr: { "w:val": true } } }],
});
});
it("#highlight", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").highlight("005599");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:highlight": { _attr: { "w:val": "005599" } } }],
});
});
it("#shadow", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").shadow("pct10", "00FFFF", "FF0000");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:shd": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } }],
});
});
describe("#underline", () => {
it("should set underline to 'single' if no arguments are given", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline();
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:u": { _attr: { "w:val": "single" } } }],
});
});
it("should set the style if given", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline(UnderlineType.DOUBLE);
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:u": { _attr: { "w:val": "double" } } }],
});
});
it("should set the style and color if given", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline(UnderlineType.DOUBLE, "005599");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:u": { _attr: { "w:val": "double", "w:color": "005599" } } }],
});
});
});
it("#color", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").color("123456");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:color": { _attr: { "w:val": "123456" } } }],
});
});
});
});
});
describe("concrete numbering", () => {
describe("#overrideLevel", () => {
let numbering;
let abstractNumbering;
let concreteNumbering;
beforeEach(() => {
numbering = new Numbering();
abstractNumbering = numbering.createAbstractNumbering();
concreteNumbering = numbering.createConcreteNumbering(abstractNumbering);
});
it("sets a new override level for the given level number", () => {
concreteNumbering.overrideLevel(3);
const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [
{
_attr: {
"w:ilvl": 3,
},
},
{
"w:lvl": {
_attr: {
"w:ilvl": 3,
"w15:tentative": 1,
},
},
},
],
});
});
it("sets the startOverride element if start is given", () => {
concreteNumbering.overrideLevel(1, 9);
const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [
{
_attr: {
"w:ilvl": 1,
},
},
{
"w:startOverride": {
_attr: {
"w:val": 9,
},
},
},
{
"w:lvl": {
_attr: {
"w:ilvl": 1,
"w15:tentative": 1,
},
},
},
],
});
});
it("sets the lvl element if overrideLevel.Level is accessed", () => {
const ol = concreteNumbering.overrideLevel(1);
expect(ol.Level).to.be.instanceof(LevelForOverride);
const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [
{ _attr: { "w:ilvl": 1 } },
{
"w:lvl": { _attr: { "w15:tentative": 1, "w:ilvl": 1 } },
},
],
});
});
});
}); });

View File

@ -1,17 +1,27 @@
import { Indent } from "file/paragraph"; // http://officeopenxml.com/WPnumbering.php
import { AlignmentType } from "file/paragraph";
import { IXmlableObject, XmlComponent } from "file/xml-components"; import { IXmlableObject, XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes"; import { DocumentAttributes } from "../document/document-attributes";
import { AbstractNumbering } from "./abstract-numbering"; import { AbstractNumbering } from "./abstract-numbering";
import { Num } from "./num"; import { ILevelsOptions } from "./level";
import { ConcreteNumbering } from "./num";
export interface INumberingOptions {
readonly config: {
readonly levels: ILevelsOptions[];
readonly reference: string;
}[];
}
export class Numbering extends XmlComponent { export class Numbering extends XmlComponent {
// tslint:disable-next-line:readonly-keyword // tslint:disable-next-line:readonly-keyword
private nextId: number; private nextId: number;
private readonly abstractNumbering: XmlComponent[] = []; private readonly abstractNumbering: AbstractNumbering[] = [];
private readonly concreteNumbering: XmlComponent[] = []; private readonly concreteNumbering: ConcreteNumbering[] = [];
constructor() { constructor(options: INumberingOptions) {
super("w:numbering"); super("w:numbering");
this.root.push( this.root.push(
new DocumentAttributes({ new DocumentAttributes({
@ -37,39 +47,114 @@ export class Numbering extends XmlComponent {
this.nextId = 0; this.nextId = 0;
const abstractNumbering = this.createAbstractNumbering(); const abstractNumbering = this.createAbstractNumbering([
{
abstractNumbering.createLevel(0, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 720, hanging: 360 })); level: 0,
format: "bullet",
abstractNumbering.createLevel(1, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 1440, hanging: 360 })); text: "\u25CF",
alignment: AlignmentType.LEFT,
abstractNumbering.createLevel(2, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 2160, hanging: 360 })); style: {
paragraph: {
abstractNumbering.createLevel(3, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 2880, hanging: 360 })); indent: { left: 720, hanging: 360 },
},
abstractNumbering.createLevel(4, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 3600, hanging: 360 })); },
},
abstractNumbering.createLevel(5, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 4320, hanging: 360 })); {
level: 1,
abstractNumbering.createLevel(6, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 5040, hanging: 360 })); format: "bullet",
text: "\u25CB",
abstractNumbering.createLevel(7, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 5760, hanging: 360 })); alignment: AlignmentType.LEFT,
style: {
abstractNumbering.createLevel(8, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 6480, hanging: 360 })); paragraph: {
indent: { left: 1440, hanging: 360 },
},
},
},
{
level: 2,
format: "bullet",
text: "\u25A0",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 2160, hanging: 360 },
},
},
},
{
level: 3,
format: "bullet",
text: "\u25CF",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 2880, hanging: 360 },
},
},
},
{
level: 4,
format: "bullet",
text: "\u25CB",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 3600, hanging: 360 },
},
},
},
{
level: 5,
format: "bullet",
text: "\u25A0",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 4320, hanging: 360 },
},
},
},
{
level: 6,
format: "bullet",
text: "\u25CF",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 5040, hanging: 360 },
},
},
},
{
level: 7,
format: "bullet",
text: "\u25CF",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 5760, hanging: 360 },
},
},
},
{
level: 8,
format: "bullet",
text: "\u25CF",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 6480, hanging: 360 },
},
},
},
]);
this.createConcreteNumbering(abstractNumbering); this.createConcreteNumbering(abstractNumbering);
}
public createAbstractNumbering(): AbstractNumbering { for (const con of options.config) {
const num = new AbstractNumbering(this.nextId++); const currentAbstractNumbering = this.createAbstractNumbering(con.levels);
this.abstractNumbering.push(num); this.createConcreteNumbering(currentAbstractNumbering, con.reference);
return num;
} }
public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num {
const num = new Num(this.nextId++, abstractNumbering.id);
this.concreteNumbering.push(num);
return num;
} }
public prepForXml(): IXmlableObject | undefined { public prepForXml(): IXmlableObject | undefined {
@ -77,4 +162,20 @@ export class Numbering extends XmlComponent {
this.concreteNumbering.forEach((x) => this.root.push(x)); this.concreteNumbering.forEach((x) => this.root.push(x));
return super.prepForXml(); return super.prepForXml();
} }
private createConcreteNumbering(abstractNumbering: AbstractNumbering, reference?: string): ConcreteNumbering {
const num = new ConcreteNumbering(this.nextId++, abstractNumbering.id, reference);
this.concreteNumbering.push(num);
return num;
}
private createAbstractNumbering(options: ILevelsOptions[]): AbstractNumbering {
const num = new AbstractNumbering(this.nextId++, options);
this.abstractNumbering.push(num);
return num;
}
public get ConcreteNumbering(): ConcreteNumbering[] {
return this.concreteNumbering;
}
} }

View File

@ -1,4 +1,5 @@
// http://officeopenxml.com/WPalignment.php // http://officeopenxml.com/WPalignment.php
// http://officeopenxml.com/WPtableAlignment.php
import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export enum AlignmentType { export enum AlignmentType {

View File

@ -7,6 +7,7 @@ export interface IIndentAttributesProperties {
readonly firstLine?: number; readonly firstLine?: number;
readonly start?: number; readonly start?: number;
readonly end?: number; readonly end?: number;
readonly right?: number;
} }
class IndentAttributes extends XmlAttributeComponent<IIndentAttributesProperties> { class IndentAttributes extends XmlAttributeComponent<IIndentAttributesProperties> {
@ -16,6 +17,7 @@ class IndentAttributes extends XmlAttributeComponent<IIndentAttributesProperties
firstLine: "w:firstLine", firstLine: "w:firstLine",
start: "w:start", start: "w:start",
end: "w:end", end: "w:end",
right: "w:end", // alias
}; };
} }

View File

@ -11,11 +11,8 @@ export enum HeadingLevel {
} }
export class Style extends XmlComponent { export class Style extends XmlComponent {
public readonly styleId: string;
constructor(styleId: string) { constructor(styleId: string) {
super("w:pStyle"); super("w:pStyle");
this.styleId = styleId;
this.root.push( this.root.push(
new Attributes({ new Attributes({
val: styleId, val: styleId,

View File

@ -1,7 +1,7 @@
import { Attributes, XmlComponent } from "file/xml-components"; import { Attributes, XmlComponent } from "file/xml-components";
export class NumberProperties extends XmlComponent { export class NumberProperties extends XmlComponent {
constructor(numberId: number, indentLevel: number) { constructor(numberId: number | string, indentLevel: number) {
super("w:numPr"); super("w:numPr");
this.root.push(new IndentLevel(indentLevel)); this.root.push(new IndentLevel(indentLevel));
this.root.push(new NumberId(numberId)); this.root.push(new NumberId(numberId));
@ -20,11 +20,11 @@ class IndentLevel extends XmlComponent {
} }
class NumberId extends XmlComponent { class NumberId extends XmlComponent {
constructor(id: number) { constructor(id: number | string) {
super("w:numId"); super("w:numId");
this.root.push( this.root.push(
new Attributes({ new Attributes({
val: id, val: typeof id === "string" ? `{${id}}` : id,
}), }),
); );
} }

View File

@ -1,4 +1,4 @@
import { assert } from "chai"; import { assert, expect } from "chai";
import { Utility } from "tests/utility"; import { Utility } from "tests/utility";
@ -8,7 +8,7 @@ describe("Bookmark", () => {
let bookmark: Bookmark; let bookmark: Bookmark;
beforeEach(() => { beforeEach(() => {
bookmark = new Bookmark("anchor", "Internal Link", 0); bookmark = new Bookmark("anchor", "Internal Link");
}); });
it("should create a bookmark with three root elements", () => { it("should create a bookmark with three root elements", () => {
@ -21,11 +21,8 @@ describe("Bookmark", () => {
it("should create a bookmark with the correct attributes on the bookmark start element", () => { it("should create a bookmark with the correct attributes on the bookmark start element", () => {
const newJson = Utility.jsonify(bookmark); const newJson = Utility.jsonify(bookmark);
const attributes = {
name: "anchor", assert.equal(newJson.start.root[0].root.name, "anchor");
id: "1",
};
assert.equal(JSON.stringify(newJson.start.root[0].root), JSON.stringify(attributes));
}); });
it("should create a bookmark with the correct attributes on the text element", () => { it("should create a bookmark with the correct attributes on the text element", () => {
@ -35,9 +32,6 @@ describe("Bookmark", () => {
it("should create a bookmark with the correct attributes on the bookmark end element", () => { it("should create a bookmark with the correct attributes on the bookmark end element", () => {
const newJson = Utility.jsonify(bookmark); const newJson = Utility.jsonify(bookmark);
const attributes = { expect(newJson.end.root[0].root.id).to.be.a("string");
id: "1",
};
assert.equal(JSON.stringify(newJson.end.root[0].root), JSON.stringify(attributes));
}); });
}); });

View File

@ -1,49 +1,41 @@
// http://officeopenxml.com/WPbookmark.php // http://officeopenxml.com/WPbookmark.php
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import * as shortid from "shortid";
import { TextRun } from "../run"; import { TextRun } from "../run";
import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes"; import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes";
export class Bookmark { export class Bookmark {
public readonly linkId: number;
public readonly start: BookmarkStart; public readonly start: BookmarkStart;
public readonly text: TextRun; public readonly text: TextRun;
public readonly end: BookmarkEnd; public readonly end: BookmarkEnd;
constructor(name: string, text: string, relationshipsCount: number) { constructor(name: string, text: string) {
this.linkId = relationshipsCount + 1; const linkId = shortid.generate().toLowerCase();
this.start = new BookmarkStart(name, this.linkId); this.start = new BookmarkStart(name, linkId);
this.text = new TextRun(text); this.text = new TextRun(text);
this.end = new BookmarkEnd(this.linkId); this.end = new BookmarkEnd(linkId);
} }
} }
export class BookmarkStart extends XmlComponent { export class BookmarkStart extends XmlComponent {
public readonly linkId: number; constructor(name: string, linkId: string) {
constructor(name: string, relationshipsCount: number) {
super("w:bookmarkStart"); super("w:bookmarkStart");
this.linkId = relationshipsCount;
const id = `${this.linkId}`;
const attributes = new BookmarkStartAttributes({ const attributes = new BookmarkStartAttributes({
name, name,
id, id: linkId,
}); });
this.root.push(attributes); this.root.push(attributes);
} }
} }
export class BookmarkEnd extends XmlComponent { export class BookmarkEnd extends XmlComponent {
public readonly linkId: number; constructor(linkId: string) {
constructor(relationshipsCount: number) {
super("w:bookmarkEnd"); super("w:bookmarkEnd");
this.linkId = relationshipsCount;
const id = `${this.linkId}`;
const attributes = new BookmarkEndAttributes({ const attributes = new BookmarkEndAttributes({
id, id: linkId,
}); });
this.root.push(attributes); this.root.push(attributes);
} }

View File

@ -3,12 +3,13 @@ import { expect } from "chai";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { Hyperlink } from "./"; import { Hyperlink } from "./";
import { HyperlinkRef } from "./hyperlink";
describe("Hyperlink", () => { describe("Hyperlink", () => {
let hyperlink: Hyperlink; let hyperlink: Hyperlink;
beforeEach(() => { beforeEach(() => {
hyperlink = new Hyperlink("https://example.com", 0); hyperlink = new Hyperlink("https://example.com", "superid");
}); });
describe("#constructor()", () => { describe("#constructor()", () => {
@ -19,7 +20,7 @@ describe("Hyperlink", () => {
{ {
_attr: { _attr: {
"w:history": 1, "w:history": 1,
"r:id": "rId1", "r:id": "rIdsuperid",
}, },
}, },
{ {
@ -34,7 +35,7 @@ describe("Hyperlink", () => {
describe("with optional anchor parameter", () => { describe("with optional anchor parameter", () => {
beforeEach(() => { beforeEach(() => {
hyperlink = new Hyperlink("Anchor Text", 0, "anchor"); hyperlink = new Hyperlink("Anchor Text", "superid2", "anchor");
}); });
it("should create an internal link with anchor tag", () => { it("should create an internal link with anchor tag", () => {
@ -59,3 +60,11 @@ describe("Hyperlink", () => {
}); });
}); });
}); });
describe("HyperlinkRef", () => {
describe("#constructor()", () => {
const hyperlinkRef = new HyperlinkRef("test-id");
expect(hyperlinkRef.id).to.equal("test-id");
});
});

View File

@ -3,14 +3,23 @@ import { XmlComponent } from "file/xml-components";
import { TextRun } from "../run"; import { TextRun } from "../run";
import { HyperlinkAttributes, IHyperlinkAttributesProperties } from "./hyperlink-attributes"; import { HyperlinkAttributes, IHyperlinkAttributesProperties } from "./hyperlink-attributes";
export enum HyperlinkType {
INTERNAL = "INTERNAL",
EXTERNAL = "EXTERNAL",
}
export class HyperlinkRef {
constructor(public readonly id: string) {}
}
export class Hyperlink extends XmlComponent { export class Hyperlink extends XmlComponent {
public readonly linkId: number; public readonly linkId: string;
private readonly textRun: TextRun; private readonly textRun: TextRun;
constructor(text: string, relationshipsCount: number, anchor?: string) { constructor(text: string, relationshipId: string, anchor?: string) {
super("w:hyperlink"); super("w:hyperlink");
this.linkId = relationshipsCount + 1; this.linkId = relationshipId;
const props: IHyperlinkAttributesProperties = { const props: IHyperlinkAttributesProperties = {
history: 1, history: 1,

View File

@ -1,10 +1,12 @@
import { assert, expect } from "chai"; import { assert, expect } from "chai";
import * as shortid from "shortid";
import { stub } from "sinon";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { EMPTY_OBJECT } from "file/xml-components"; import { EMPTY_OBJECT } from "file/xml-components";
import { Numbering } from "../numbering";
import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting"; import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting";
import { Bookmark } from "./links";
import { Paragraph } from "./paragraph"; import { Paragraph } from "./paragraph";
describe("Paragraph", () => { describe("Paragraph", () => {
@ -540,14 +542,8 @@ describe("Paragraph", () => {
}, },
}); });
const tree = new Formatter().format(paragraph); const tree = new Formatter().format(paragraph);
expect(tree) expect(tree).to.have.property("w:p").which.is.an("array").which.has.length.at.least(1);
.to.have.property("w:p") expect(tree["w:p"][0]).to.have.property("w:pPr").which.is.an("array").which.has.length.at.least(1);
.which.is.an("array")
.which.has.length.at.least(1);
expect(tree["w:p"][0])
.to.have.property("w:pPr")
.which.is.an("array")
.which.has.length.at.least(1);
expect(tree["w:p"][0]["w:pPr"][0]).to.deep.equal({ expect(tree["w:p"][0]["w:pPr"][0]).to.deep.equal({
"w:pStyle": { _attr: { "w:val": "ListParagraph" } }, "w:pStyle": { _attr: { "w:val": "ListParagraph" } },
}); });
@ -560,14 +556,8 @@ describe("Paragraph", () => {
}, },
}); });
const tree = new Formatter().format(paragraph); const tree = new Formatter().format(paragraph);
expect(tree) expect(tree).to.have.property("w:p").which.is.an("array").which.has.length.at.least(1);
.to.have.property("w:p") expect(tree["w:p"][0]).to.have.property("w:pPr").which.is.an("array").which.has.length.at.least(1);
.which.is.an("array")
.which.has.length.at.least(1);
expect(tree["w:p"][0])
.to.have.property("w:pPr")
.which.is.an("array")
.which.has.length.at.least(1);
expect(tree["w:p"][0]["w:pPr"][0]).to.deep.equal({ expect(tree["w:p"][0]["w:pPr"][0]).to.deep.equal({
"w:pStyle": { _attr: { "w:val": "ListParagraph" } }, "w:pStyle": { _attr: { "w:val": "ListParagraph" } },
}); });
@ -580,14 +570,8 @@ describe("Paragraph", () => {
}, },
}); });
const tree = new Formatter().format(paragraph); const tree = new Formatter().format(paragraph);
expect(tree) expect(tree).to.have.property("w:p").which.is.an("array").which.has.length.at.least(1);
.to.have.property("w:p") expect(tree["w:p"][0]).to.have.property("w:pPr").which.is.an("array").which.has.length.at.least(2);
.which.is.an("array")
.which.has.length.at.least(1);
expect(tree["w:p"][0])
.to.have.property("w:pPr")
.which.is.an("array")
.which.has.length.at.least(2);
expect(tree["w:p"][0]["w:pPr"][1]).to.deep.equal({ expect(tree["w:p"][0]["w:pPr"][1]).to.deep.equal({
"w:numPr": [{ "w:ilvl": { _attr: { "w:val": 1 } } }, { "w:numId": { _attr: { "w:val": 1 } } }], "w:numPr": [{ "w:ilvl": { _attr: { "w:val": 1 } } }, { "w:numId": { _attr: { "w:val": 1 } } }],
}); });
@ -596,40 +580,24 @@ describe("Paragraph", () => {
describe("#setNumbering", () => { describe("#setNumbering", () => {
it("should add list paragraph style to JSON", () => { it("should add list paragraph style to JSON", () => {
const numbering = new Numbering();
const numberedAbstract = numbering.createAbstractNumbering();
numberedAbstract.createLevel(0, "lowerLetter", "%1)", "start");
const letterNumbering = numbering.createConcreteNumbering(numberedAbstract);
const paragraph = new Paragraph({ const paragraph = new Paragraph({
numbering: { numbering: {
num: letterNumbering, reference: "test id",
level: 0, level: 0,
}, },
}); });
const tree = new Formatter().format(paragraph); const tree = new Formatter().format(paragraph);
expect(tree) expect(tree).to.have.property("w:p").which.is.an("array").which.has.length.at.least(1);
.to.have.property("w:p") expect(tree["w:p"][0]).to.have.property("w:pPr").which.is.an("array").which.has.length.at.least(1);
.which.is.an("array")
.which.has.length.at.least(1);
expect(tree["w:p"][0])
.to.have.property("w:pPr")
.which.is.an("array")
.which.has.length.at.least(1);
expect(tree["w:p"][0]["w:pPr"][0]).to.deep.equal({ expect(tree["w:p"][0]["w:pPr"][0]).to.deep.equal({
"w:pStyle": { _attr: { "w:val": "ListParagraph" } }, "w:pStyle": { _attr: { "w:val": "ListParagraph" } },
}); });
}); });
it("it should add numbered properties", () => { it("it should add numbered properties", () => {
const numbering = new Numbering();
const numberedAbstract = numbering.createAbstractNumbering();
numberedAbstract.createLevel(0, "lowerLetter", "%1)", "start");
const letterNumbering = numbering.createConcreteNumbering(numberedAbstract);
const paragraph = new Paragraph({ const paragraph = new Paragraph({
numbering: { numbering: {
num: letterNumbering, reference: "test id",
level: 0, level: 0,
}, },
}); });
@ -640,10 +608,7 @@ describe("Paragraph", () => {
"w:pPr": [ "w:pPr": [
{ "w:pStyle": { _attr: { "w:val": "ListParagraph" } } }, { "w:pStyle": { _attr: { "w:val": "ListParagraph" } } },
{ {
"w:numPr": [ "w:numPr": [{ "w:ilvl": { _attr: { "w:val": 0 } } }, { "w:numId": { _attr: { "w:val": "{test id}" } } }],
{ "w:ilvl": { _attr: { "w:val": 0 } } },
{ "w:numId": { _attr: { "w:val": letterNumbering.id } } },
],
}, },
], ],
}, },
@ -652,6 +617,49 @@ describe("Paragraph", () => {
}); });
}); });
it("it should add bookmark", () => {
stub(shortid, "generate").callsFake(() => {
return "test-unique-id";
});
const paragraph = new Paragraph({
children: [new Bookmark("test-id", "test")],
});
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
"w:p": [
{
"w:bookmarkStart": {
_attr: {
"w:id": "test-unique-id",
"w:name": "test-id",
},
},
},
{
"w:r": [
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"test",
],
},
],
},
{
"w:bookmarkEnd": {
_attr: {
"w:id": "test-unique-id",
},
},
},
],
});
});
describe("#style", () => { describe("#style", () => {
it("should set the paragraph style to the given styleId", () => { it("should set the paragraph style to the given styleId", () => {
const paragraph = new Paragraph({ const paragraph = new Paragraph({

View File

@ -1,52 +1,30 @@
// http://officeopenxml.com/WPparagraph.php // http://officeopenxml.com/WPparagraph.php
import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run"; import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { Num } from "file/numbering/num"; import { IXmlableObject, XmlComponent } from "file/xml-components";
import { XmlComponent } from "file/xml-components";
import { Alignment, AlignmentType } from "./formatting/alignment"; import { PageBreak } from "./formatting/page-break";
import { Bidirectional } from "./formatting/bidirectional"; import { Bookmark, HyperlinkRef } from "./links";
import { IBorderOptions, ThematicBreak } from "./formatting/border";
import { IIndentAttributesProperties, Indent } from "./formatting/indent";
import { KeepLines, KeepNext } from "./formatting/keep";
import { PageBreak, PageBreakBefore } from "./formatting/page-break";
import { ContextualSpacing, ISpacingProperties, Spacing } from "./formatting/spacing";
import { HeadingLevel, Style } from "./formatting/style";
import { LeaderType, TabStop, TabStopPosition, TabStopType } from "./formatting/tab-stop";
import { NumberProperties } from "./formatting/unordered-list";
import { Bookmark, Hyperlink, OutlineLevel } from "./links";
import { Math } from "./math"; import { Math } from "./math";
import { ParagraphProperties } from "./properties"; import { File } from "../file";
import { InsertedTextRun, DeletedTextRun } from "../track-revision";
import { IParagraphPropertiesOptions, ParagraphProperties } from "./properties";
import { PictureRun, Run, SequentialIdentifier, SymbolRun, TextRun } from "./run"; import { PictureRun, Run, SequentialIdentifier, SymbolRun, TextRun } from "./run";
export interface IParagraphOptions { export interface IParagraphOptions extends IParagraphPropertiesOptions {
readonly text?: string; readonly text?: string;
readonly border?: IBorderOptions; readonly children?: (
readonly spacing?: ISpacingProperties; | TextRun
readonly outlineLevel?: number; | PictureRun
readonly alignment?: AlignmentType; | SymbolRun
readonly heading?: HeadingLevel; | Bookmark
readonly bidirectional?: boolean; | PageBreak
readonly thematicBreak?: boolean; | SequentialIdentifier
readonly pageBreakBefore?: boolean; | FootnoteReferenceRun
readonly contextualSpacing?: boolean; | HyperlinkRef
readonly indent?: IIndentAttributesProperties; | InsertedTextRun
readonly keepLines?: boolean; | DeletedTextRun
readonly keepNext?: boolean; | Math
readonly tabStops?: Array<{ )[];
readonly position: number | TabStopPosition;
readonly type: TabStopType;
readonly leader?: LeaderType;
}>;
readonly style?: string;
readonly bullet?: {
readonly level: number;
};
readonly numbering?: {
readonly num: Num;
readonly level: number;
readonly custom?: boolean;
};
readonly children?: Array<TextRun | PictureRun | Hyperlink | SymbolRun | Bookmark | PageBreak | SequentialIdentifier | Math>;
} }
export class Paragraph extends XmlComponent { export class Paragraph extends XmlComponent {
@ -69,9 +47,7 @@ export class Paragraph extends XmlComponent {
return; return;
} }
this.properties = new ParagraphProperties({ this.properties = new ParagraphProperties(options);
border: options.border,
});
this.root.push(this.properties); this.root.push(this.properties);
@ -79,72 +55,6 @@ export class Paragraph extends XmlComponent {
this.root.push(new TextRun(options.text)); this.root.push(new TextRun(options.text));
} }
if (options.spacing) {
this.properties.push(new Spacing(options.spacing));
}
if (options.outlineLevel !== undefined) {
this.properties.push(new OutlineLevel(options.outlineLevel));
}
if (options.alignment) {
this.properties.push(new Alignment(options.alignment));
}
if (options.heading) {
this.properties.push(new Style(options.heading));
}
if (options.bidirectional) {
this.properties.push(new Bidirectional());
}
if (options.thematicBreak) {
this.properties.push(new ThematicBreak());
}
if (options.pageBreakBefore) {
this.properties.push(new PageBreakBefore());
}
if (options.contextualSpacing) {
this.properties.push(new ContextualSpacing(options.contextualSpacing));
}
if (options.indent) {
this.properties.push(new Indent(options.indent));
}
if (options.keepLines) {
this.properties.push(new KeepLines());
}
if (options.keepNext) {
this.properties.push(new KeepNext());
}
if (options.tabStops) {
for (const tabStop of options.tabStops) {
this.properties.push(new TabStop(tabStop.type, tabStop.position, tabStop.leader));
}
}
if (options.style) {
this.properties.push(new Style(options.style));
}
if (options.bullet) {
this.properties.push(new Style("ListParagraph"));
this.properties.push(new NumberProperties(1, options.bullet.level));
}
if (options.numbering) {
if (!options.numbering.custom) {
this.properties.push(new Style("ListParagraph"));
}
this.properties.push(new NumberProperties(options.numbering.num.id, options.numbering.level));
}
if (options.children) { if (options.children) {
for (const child of options.children) { for (const child of options.children) {
if (child instanceof Bookmark) { if (child instanceof Bookmark) {
@ -159,9 +69,15 @@ export class Paragraph extends XmlComponent {
} }
} }
public referenceFootnote(id: number): Paragraph { public prepForXml(file: File): IXmlableObject | undefined {
this.root.push(new FootnoteReferenceRun(id)); for (const element of this.root) {
return this; if (element instanceof HyperlinkRef) {
const index = this.root.indexOf(element);
this.root[index] = file.HyperlinkCache[element.id];
}
}
return super.prepForXml();
} }
public addRunToFront(run: Run): Paragraph { public addRunToFront(run: Run): Paragraph {

View File

@ -1,19 +1,136 @@
// http://officeopenxml.com/WPparagraphProperties.php // http://officeopenxml.com/WPparagraphProperties.php
import { IgnoreIfEmptyXmlComponent, XmlComponent } from "file/xml-components"; import { IgnoreIfEmptyXmlComponent, XmlComponent } from "file/xml-components";
import { Alignment, AlignmentType } from "./formatting/alignment";
import { Bidirectional } from "./formatting/bidirectional";
import { Border, IBorderOptions, ThematicBreak } from "./formatting/border";
import { IIndentAttributesProperties, Indent } from "./formatting/indent";
import { KeepLines, KeepNext } from "./formatting/keep";
import { PageBreakBefore } from "./formatting/page-break";
import { ContextualSpacing, ISpacingProperties, Spacing } from "./formatting/spacing";
import { HeadingLevel, Style } from "./formatting/style";
import { LeaderType, TabStop, TabStopPosition, TabStopType } from "./formatting/tab-stop";
import { NumberProperties } from "./formatting/unordered-list";
import { OutlineLevel } from "./links";
import { Border, IBorderOptions } from "./formatting/border"; export interface IParagraphStylePropertiesOptions {
readonly alignment?: AlignmentType;
readonly thematicBreak?: boolean;
readonly contextualSpacing?: boolean;
readonly rightTabStop?: number;
readonly leftTabStop?: number;
readonly indent?: IIndentAttributesProperties;
readonly spacing?: ISpacingProperties;
readonly keepNext?: boolean;
readonly keepLines?: boolean;
readonly outlineLevel?: number;
}
interface IParagraphPropertiesOptions { export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOptions {
readonly border?: IBorderOptions; readonly border?: IBorderOptions;
readonly heading?: HeadingLevel;
readonly bidirectional?: boolean;
readonly pageBreakBefore?: boolean;
readonly tabStops?: {
readonly position: number | TabStopPosition;
readonly type: TabStopType;
readonly leader?: LeaderType;
}[];
readonly style?: string;
readonly bullet?: {
readonly level: number;
};
readonly numbering?: {
readonly reference: string;
readonly level: number;
readonly custom?: boolean;
};
} }
export class ParagraphProperties extends IgnoreIfEmptyXmlComponent { export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
constructor(options: IParagraphPropertiesOptions) { constructor(options?: IParagraphPropertiesOptions) {
super("w:pPr"); super("w:pPr");
if (!options) {
return;
}
if (options.border) { if (options.border) {
this.push(new Border(options.border)); this.push(new Border(options.border));
} }
if (options.spacing) {
this.push(new Spacing(options.spacing));
}
if (options.outlineLevel !== undefined) {
this.push(new OutlineLevel(options.outlineLevel));
}
if (options.alignment) {
this.push(new Alignment(options.alignment));
}
if (options.heading) {
this.push(new Style(options.heading));
}
if (options.bidirectional) {
this.push(new Bidirectional());
}
if (options.thematicBreak) {
this.push(new ThematicBreak());
}
if (options.pageBreakBefore) {
this.push(new PageBreakBefore());
}
if (options.contextualSpacing) {
this.push(new ContextualSpacing(options.contextualSpacing));
}
if (options.indent) {
this.push(new Indent(options.indent));
}
if (options.keepLines) {
this.push(new KeepLines());
}
if (options.keepNext) {
this.push(new KeepNext());
}
if (options.tabStops) {
for (const tabStop of options.tabStops) {
this.push(new TabStop(tabStop.type, tabStop.position, tabStop.leader));
}
}
if (options.style) {
this.push(new Style(options.style));
}
if (options.bullet) {
this.push(new Style("ListParagraph"));
this.push(new NumberProperties(1, options.bullet.level));
}
if (options.numbering) {
if (!options.numbering.custom) {
this.push(new Style("ListParagraph"));
}
this.push(new NumberProperties(options.numbering.reference, options.numbering.level));
}
if (options.rightTabStop) {
this.push(new TabStop(TabStopType.RIGHT, options.rightTabStop));
}
if (options.leftTabStop) {
this.push(new TabStop(TabStopType.LEFT, options.leftTabStop));
}
} }
public push(item: XmlComponent): void { public push(item: XmlComponent): void {

View File

@ -1,13 +0,0 @@
import { XmlComponent } from "file/xml-components";
export class SmallCaps extends XmlComponent {
constructor() {
super("w:smallCaps");
}
}
export class Caps extends XmlComponent {
constructor() {
super("w:caps");
}
}

View File

@ -0,0 +1,29 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import * as em from "./emphasis-mark";
describe("EmphasisMark", () => {
describe("#constructor()", () => {
it("should create a new EmphasisMark object with w:em as the rootKey", () => {
const emphasisMark = new em.EmphasisMark();
const tree = new Formatter().format(emphasisMark);
expect(tree).to.deep.equal({
"w:em": { _attr: { "w:val": "dot" } },
});
});
});
});
describe("DotEmphasisMark", () => {
describe("#constructor()", () => {
it("should put value in attribute", () => {
const emphasisMark = new em.DotEmphasisMark();
const tree = new Formatter().format(emphasisMark);
expect(tree).to.deep.equal({
"w:em": { _attr: { "w:val": "dot" } },
});
});
});
});

View File

@ -0,0 +1,28 @@
import { Attributes, XmlComponent } from "file/xml-components";
export enum EmphasisMarkType {
DOT = "dot",
}
export abstract class BaseEmphasisMark extends XmlComponent {
protected constructor(emphasisMarkType: EmphasisMarkType) {
super("w:em");
this.root.push(
new Attributes({
val: emphasisMarkType,
}),
);
}
}
export class EmphasisMark extends BaseEmphasisMark {
constructor(emphasisMarkType: EmphasisMarkType = EmphasisMarkType.DOT) {
super(emphasisMarkType);
}
}
export class DotEmphasisMark extends BaseEmphasisMark {
constructor() {
super(EmphasisMarkType.DOT);
}
}

View File

@ -1,7 +1,9 @@
import { Attributes, XmlComponent } from "file/xml-components"; import { Attributes, XmlComponent } from "file/xml-components";
export { Underline } from "./underline"; export { Underline } from "./underline";
export { EmphasisMark } from "./emphasis-mark";
export { SubScript, SuperScript } from "./script"; export { SubScript, SuperScript } from "./script";
export { RunFonts } from "./run-fonts"; export { RunFonts, IFontAttributesProperties } from "./run-fonts";
export class Bold extends XmlComponent { export class Bold extends XmlComponent {
constructor() { constructor() {
@ -113,17 +115,6 @@ export class Imprint extends XmlComponent {
} }
} }
/* export class Shadow extends XmlComponent {
constructor() {
super("w:shadow");
this.root.push(
new Attributes({
val: true,
}),
);
}
} */
export class SmallCaps extends XmlComponent { export class SmallCaps extends XmlComponent {
constructor() { constructor() {
super("w:smallCaps"); super("w:smallCaps");

View File

@ -1,7 +1,10 @@
export * from "./run"; export * from "./run";
export * from "./properties";
export * from "./text-run"; export * from "./text-run";
export * from "./symbol-run"; export * from "./symbol-run";
export * from "./picture-run"; export * from "./picture-run";
export * from "./run-fonts"; export * from "./run-fonts";
export * from "./sequential-identifier"; export * from "./sequential-identifier";
export * from "./underline"; export * from "./underline";
export * from "./emphasis-mark";
export * from "./tab";

View File

@ -7,10 +7,6 @@ export class PictureRun extends Run {
constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) {
super({}); super({});
if (imageData === undefined) {
throw new Error("imageData cannot be undefined");
}
const drawing = new Drawing(imageData, drawingOptions); const drawing = new Drawing(imageData, drawingOptions);
this.root.push(drawing); this.root.push(drawing);

View File

@ -1,8 +1,183 @@
import { ShadingType } from "file/table";
import { IgnoreIfEmptyXmlComponent, XmlComponent } from "file/xml-components"; import { IgnoreIfEmptyXmlComponent, XmlComponent } from "file/xml-components";
import { EmphasisMark, EmphasisMarkType } from "./emphasis-mark";
import {
Bold,
BoldComplexScript,
Caps,
CharacterSpacing,
Color,
DoubleStrike,
Highlight,
HighlightComplexScript,
Italics,
ItalicsComplexScript,
RightToLeft,
Shading,
ShadowComplexScript,
Size,
SizeComplexScript,
SmallCaps,
Strike,
} from "./formatting";
import { IFontAttributesProperties, RunFonts } from "./run-fonts";
import { SubScript, SuperScript } from "./script";
import { Style } from "./style";
import { Underline, UnderlineType } from "./underline";
interface IFontOptions {
readonly name: string;
readonly hint?: string;
}
export interface IRunStylePropertiesOptions {
readonly bold?: boolean;
readonly boldComplexScript?: boolean;
readonly italics?: boolean;
readonly italicsComplexScript?: boolean;
readonly underline?: {
readonly color?: string;
readonly type?: UnderlineType;
};
readonly emphasisMark?: {
readonly type?: EmphasisMarkType;
};
readonly color?: string;
readonly size?: number;
readonly sizeComplexScript?: boolean | number;
readonly rightToLeft?: boolean;
readonly smallCaps?: boolean;
readonly allCaps?: boolean;
readonly strike?: boolean;
readonly doubleStrike?: boolean;
readonly subScript?: boolean;
readonly superScript?: boolean;
readonly font?: string | IFontOptions | IFontAttributesProperties;
readonly highlight?: string;
readonly highlightComplexScript?: boolean | string;
readonly characterSpacing?: number;
readonly shading?: {
readonly type: ShadingType;
readonly fill: string;
readonly color: string;
};
readonly shadingComplexScript?: boolean | IRunStylePropertiesOptions["shading"];
readonly shadow?: IRunStylePropertiesOptions["shading"];
}
export interface IRunPropertiesOptions extends IRunStylePropertiesOptions {
readonly style?: string;
}
export class RunProperties extends IgnoreIfEmptyXmlComponent { export class RunProperties extends IgnoreIfEmptyXmlComponent {
constructor() { constructor(options?: IRunPropertiesOptions) {
super("w:rPr"); super("w:rPr");
if (!options) {
return;
}
if (options.bold) {
this.push(new Bold());
}
if ((options.boldComplexScript === undefined && options.bold) || options.boldComplexScript) {
this.push(new BoldComplexScript());
}
if (options.italics) {
this.push(new Italics());
}
if ((options.italicsComplexScript === undefined && options.italics) || options.italicsComplexScript) {
this.push(new ItalicsComplexScript());
}
if (options.underline) {
this.push(new Underline(options.underline.type, options.underline.color));
}
if (options.emphasisMark) {
this.push(new EmphasisMark(options.emphasisMark.type));
}
if (options.color) {
this.push(new Color(options.color));
}
if (options.size) {
this.push(new Size(options.size));
}
const szCs =
options.sizeComplexScript === undefined || options.sizeComplexScript === true ? options.size : options.sizeComplexScript;
if (szCs) {
this.push(new SizeComplexScript(szCs));
}
if (options.rightToLeft) {
this.push(new RightToLeft());
}
if (options.smallCaps) {
this.push(new SmallCaps());
}
if (options.allCaps) {
this.push(new Caps());
}
if (options.strike) {
this.push(new Strike());
}
if (options.doubleStrike) {
this.push(new DoubleStrike());
}
if (options.subScript) {
this.push(new SubScript());
}
if (options.superScript) {
this.push(new SuperScript());
}
if (options.style) {
this.push(new Style(options.style));
}
if (options.font) {
if (typeof options.font === "string") {
this.push(new RunFonts(options.font));
} else if ("name" in options.font) {
this.push(new RunFonts(options.font.name, options.font.hint));
} else {
this.push(new RunFonts(options.font));
}
}
if (options.highlight) {
this.push(new Highlight(options.highlight));
}
const highlightCs =
options.highlightComplexScript === undefined || options.highlightComplexScript === true
? options.highlight
: options.highlightComplexScript;
if (highlightCs) {
this.push(new HighlightComplexScript(highlightCs));
}
if (options.characterSpacing) {
this.push(new CharacterSpacing(options.characterSpacing));
}
const shading = options.shading || options.shadow;
if (shading) {
this.push(new Shading(shading.type, shading.fill, shading.color));
}
const shdCs =
options.shadingComplexScript === undefined || options.shadingComplexScript === true ? shading : options.shadingComplexScript;
if (shdCs) {
this.push(new ShadowComplexScript(shdCs.type, shdCs.fill, shdCs.color));
}
} }
public push(item: XmlComponent): void { public push(item: XmlComponent): void {

View File

@ -6,12 +6,6 @@ import { Text } from "./text";
describe("Text", () => { describe("Text", () => {
describe("#constructor", () => { describe("#constructor", () => {
it("creates an empty text run if no text is given", () => {
const t = new Text("");
const f = new Formatter().format(t);
expect(f).to.deep.equal({ "w:t": { _attr: { "xml:space": "preserve" } } });
});
it("adds the passed in text to the component", () => { it("adds the passed in text to the component", () => {
const t = new Text(" this is\n text"); const t = new Text(" this is\n text");
const f = new Formatter().format(t); const f = new Formatter().format(t);

View File

@ -9,8 +9,7 @@ export class Text extends XmlComponent {
constructor(text: string) { constructor(text: string) {
super("w:t"); super("w:t");
this.root.push(new TextAttributes({ space: SpaceType.PRESERVE })); this.root.push(new TextAttributes({ space: SpaceType.PRESERVE }));
if (text) {
this.root.push(text); this.root.push(text);
} }
}
} }

View File

@ -21,5 +21,12 @@ describe("RunFonts", () => {
}, },
}); });
}); });
it("uses the font attrs for ascii and eastAsia", () => {
const tree = new Formatter().format(new RunFonts({ ascii: "Times", eastAsia: "KaiTi" }));
expect(tree).to.deep.equal({
"w:rFonts": { _attr: { "w:ascii": "Times", "w:eastAsia": "KaiTi" } },
});
});
}); });
}); });

View File

@ -1,14 +1,14 @@
import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
interface IRunFontAttributesProperties { export interface IFontAttributesProperties {
readonly ascii: string; readonly ascii?: string;
readonly cs: string; readonly cs?: string;
readonly eastAsia: string; readonly eastAsia?: string;
readonly hAnsi: string; readonly hAnsi?: string;
readonly hint?: string; readonly hint?: string;
} }
class RunFontAttributes extends XmlAttributeComponent<IRunFontAttributesProperties> { class RunFontAttributes extends XmlAttributeComponent<IFontAttributesProperties> {
protected readonly xmlKeys = { protected readonly xmlKeys = {
ascii: "w:ascii", ascii: "w:ascii",
cs: "w:cs", cs: "w:cs",
@ -19,16 +19,26 @@ class RunFontAttributes extends XmlAttributeComponent<IRunFontAttributesProperti
} }
export class RunFonts extends XmlComponent { export class RunFonts extends XmlComponent {
constructor(ascii: string, hint?: string) { constructor(name: string, hint?: string);
constructor(attrs: string | IFontAttributesProperties);
constructor(nameOrAttrs: string | IFontAttributesProperties, hint?: string) {
super("w:rFonts"); super("w:rFonts");
if (typeof nameOrAttrs === "string") {
// use constructor(name: string, hint?: string);
const name = nameOrAttrs;
this.root.push( this.root.push(
new RunFontAttributes({ new RunFontAttributes({
ascii: ascii, ascii: name,
cs: ascii, cs: name,
eastAsia: ascii, eastAsia: name,
hAnsi: ascii, hAnsi: name,
hint: hint, hint: hint,
}), }),
); );
} else {
// use constructor(attrs: IRunFontAttributesProperties);
const attrs = nameOrAttrs;
this.root.push(new RunFontAttributes(attrs));
}
} }
} }

View File

@ -1,9 +1,12 @@
import { expect } from "chai"; import { expect } from "chai";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
// import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { ShadingType } from "file/table"; import { ShadingType } from "file/table";
import { Run } from "./"; import { Run } from "./";
import { EmphasisMarkType } from "./emphasis-mark";
import { PageNumber } from "./run";
import { UnderlineType } from "./underline"; import { UnderlineType } from "./underline";
describe("Run", () => { describe("Run", () => {
@ -82,6 +85,30 @@ describe("Run", () => {
}); });
}); });
describe("#emphasisMark()", () => {
it("should default to 'dot'", () => {
const run = new Run({
emphasisMark: {},
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [{ "w:rPr": [{ "w:em": { _attr: { "w:val": "dot" } } }] }],
});
});
it("should set the style type if given", () => {
const run = new Run({
emphasisMark: {
type: EmphasisMarkType.DOT,
},
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [{ "w:rPr": [{ "w:em": { _attr: { "w:val": "dot" } } }] }],
});
});
});
describe("#smallCaps()", () => { describe("#smallCaps()", () => {
it("it should add smallCaps to the properties", () => { it("it should add smallCaps to the properties", () => {
const run = new Run({ const run = new Run({
@ -89,7 +116,7 @@ describe("Run", () => {
}); });
const tree = new Formatter().format(run); const tree = new Formatter().format(run);
expect(tree).to.deep.equal({ expect(tree).to.deep.equal({
"w:r": [{ "w:rPr": [{ "w:smallCaps": {} }] }], "w:r": [{ "w:rPr": [{ "w:smallCaps": { _attr: { "w:val": true } } }] }],
}); });
}); });
}); });
@ -101,7 +128,7 @@ describe("Run", () => {
}); });
const tree = new Formatter().format(run); const tree = new Formatter().format(run);
expect(tree).to.deep.equal({ expect(tree).to.deep.equal({
"w:r": [{ "w:rPr": [{ "w:caps": {} }] }], "w:r": [{ "w:rPr": [{ "w:caps": { _attr: { "w:val": true } } }] }],
}); });
}); });
}); });
@ -130,6 +157,30 @@ describe("Run", () => {
}); });
}); });
describe("#subScript()", () => {
it("it should add subScript to the properties", () => {
const run = new Run({
subScript: true,
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [{ "w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "subscript" } } }] }],
});
});
});
describe("#superScript()", () => {
it("it should add superScript to the properties", () => {
const run = new Run({
superScript: true,
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [{ "w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "superscript" } } }] }],
});
});
});
describe("#highlight()", () => { describe("#highlight()", () => {
it("it should add highlight to the properties", () => { it("it should add highlight to the properties", () => {
const run = new Run({ const run = new Run({
@ -197,17 +248,6 @@ describe("Run", () => {
}); });
}); });
describe("#tab()", () => {
it("it should add break to the run", () => {
const run = new Run({});
run.tab();
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [{ "w:tab": {} }],
});
});
});
describe("#font()", () => { describe("#font()", () => {
it("should set the font as named", () => { it("should set the font as named", () => {
const run = new Run({ const run = new Run({
@ -220,7 +260,42 @@ describe("Run", () => {
"w:r": [ "w:r": [
{ {
"w:rPr": [ "w:rPr": [
{ "w:rFonts": { _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } } }, {
"w:rFonts": {
_attr: {
"w:ascii": "Times",
"w:cs": "Times",
"w:eastAsia": "Times",
"w:hAnsi": "Times",
},
},
},
],
},
],
});
});
it("should set the font for ascii and eastAsia", () => {
const run = new Run({
font: {
ascii: "Times",
eastAsia: "KaiTi",
},
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [
{
"w:rPr": [
{
"w:rFonts": {
_attr: {
"w:ascii": "Times",
"w:eastAsia": "KaiTi",
},
},
},
], ],
}, },
], ],
@ -270,8 +345,10 @@ describe("Run", () => {
describe("#numberOfTotalPages", () => { describe("#numberOfTotalPages", () => {
it("should set the run to the RTL mode", () => { it("should set the run to the RTL mode", () => {
const run = new Run({}); const run = new Run({
run.numberOfTotalPages(); children: [PageNumber.TOTAL_PAGES],
});
const tree = new Formatter().format(run); const tree = new Formatter().format(run);
expect(tree).to.deep.equal({ expect(tree).to.deep.equal({
"w:r": [ "w:r": [
@ -286,8 +363,10 @@ describe("Run", () => {
describe("#numberOfTotalPagesSection", () => { describe("#numberOfTotalPagesSection", () => {
it("should set the run to the RTL mode", () => { it("should set the run to the RTL mode", () => {
const run = new Run({}); const run = new Run({
run.numberOfTotalPagesSection(); children: [PageNumber.TOTAL_PAGES_IN_SECTION],
});
const tree = new Formatter().format(run); const tree = new Formatter().format(run);
expect(tree).to.deep.equal({ expect(tree).to.deep.equal({
"w:r": [ "w:r": [
@ -302,8 +381,9 @@ describe("Run", () => {
describe("#pageNumber", () => { describe("#pageNumber", () => {
it("should set the run to the RTL mode", () => { it("should set the run to the RTL mode", () => {
const run = new Run({}); const run = new Run({
run.pageNumber(); children: [PageNumber.CURRENT],
});
const tree = new Formatter().format(run); const tree = new Formatter().format(run);
expect(tree).to.deep.equal({ expect(tree).to.deep.equal({
"w:r": [ "w:r": [

Some files were not shown because too many files have changed in this diff Show More