Compare commits

..

74 Commits

Author SHA1 Message Date
e9579d75c4 Merge pull request #483 from dolanmiu/feat/right-indent
Version 5.0.0
2019-12-31 15:54:07 +00:00
97824f1bb5 Version 5.0.0 2019-12-31 15:52:01 +00:00
884c134b25 Merge pull request #482 from dolanmiu/feat/right-indent
Add new image for README
2019-12-31 15:51:12 +00:00
4f3cb49076 Add new image for README 2019-12-31 15:49:34 +00:00
152285ed5a Merge pull request #481 from dolanmiu/feat/right-indent
Bump up coverage stats
2019-12-27 01:54:09 +00:00
83450e6277 Bump up coverage stats 2019-12-27 01:44:20 +00:00
6d6155c742 Merge pull request #478 from dolanmiu/feat/right-indent
Declarative bookmarks
2019-12-27 01:43:48 +00:00
f8da2c311b Merge branch 'master' of github.com:dolanmiu/docx into feat/right-indent 2019-12-27 01:37:37 +00:00
f6bcaef5b5 Fix test 2019-12-27 01:29:15 +00:00
1a9e71bfa1 Add children and section tests for File 2019-12-27 01:21:19 +00:00
3591e11637 Add hyperlink cache test 2019-12-24 16:31:35 +00:00
47533cf4e2 Bookmark tests 2019-12-24 03:39:55 +00:00
de03f19b46 Remove unused code and improve coverage 2019-12-24 00:44:15 +00:00
d1472368f6 Merge pull request #476 from mustapelto/empty-first-paragraph-fix
Fix for empty first paragraph
2019-12-21 04:21:50 +00:00
b83d2c388f Make Bookmark declarative 2019-12-21 03:59:40 +00:00
ee5425bef7 Add shortId type 2019-12-21 03:58:58 +00:00
3fdbca939e Make internal hyperlink declarative 2019-12-21 03:31:09 +00:00
c68dc8c52a Make hyperlinks declarative 2019-12-18 21:11:15 +00:00
96471ecb45 Fix for empty first paragraph
If there is only one section, remove the extraneous empty paragraph during XML creation.
2019-12-15 22:56:24 +02:00
a13f898ad3 Merge pull request #474 from kalda341/allow-contextual-spacing-paragraph-style
Allow contextual spacing to be specified as an argument to paragraph style
2019-12-13 23:22:39 +00:00
b4cce534a5 Test contextual spacing 2019-12-12 11:44:00 +13:00
e0698554ad Fix style linting error 2019-12-11 15:21:50 +13:00
1649d2a0fa Allow contextual spacing to be specified as an argument to paragraph style 2019-12-11 15:10:46 +13:00
9683e89159 Merge pull request #466 from dolanmiu/feat/right-indent
Make create footnote declarative
2019-12-04 01:41:01 +00:00
2bece0bb61 Make create footnote declarative 2019-12-03 23:04:48 +00:00
deb6c42d86 Merge pull request #465 from dolanmiu/feat/right-indent
Add right indent
2019-12-02 23:57:54 +00:00
2b0953bb19 Add right indent 2019-12-02 23:13:26 +00:00
d25d22508c Merge pull request #460 from dolanmiu/feat/table-alignment
Overlap option for tables
2019-11-24 03:35:26 +00:00
6db37eb4fb Overlap tables 2019-11-24 03:22:50 +00:00
af461f8418 Merge pull request #459 from dolanmiu/feat/table-alignment
Alignment of tables
2019-11-24 02:47:02 +00:00
8bdbd1de39 Alignment of tables 2019-11-24 01:22:17 +00:00
1bdc93ef59 Update numbering demo to add more numbering levels 2019-11-23 23:22:02 +00:00
c12101fbc6 Add decimal to numbering demo 2019-11-23 21:06:15 +00:00
d5ba6578b3 Merge pull request #458 from dolanmiu/feat/remove-tab-method
Make tab and page numbers declarative
2019-11-23 19:22:43 +00:00
b37aefdc8d Correctly exporting methods 2019-11-23 02:26:59 +00:00
0be7c26798 Add companies 2019-11-23 01:21:25 +00:00
bf1d10e893 Fix demos 2019-11-21 01:12:32 +00:00
7dfb016faa Make tab and page numbers declarative 2019-11-21 01:02:46 +00:00
b2aeb2d83c Merge pull request #454 from dolanmiu/feat/table-borders
Add table borders
2019-11-19 22:46:46 +00:00
8f4c78e2a8 Update package lock 2019-11-19 22:08:01 +00:00
bd1f5898a8 Add tests 2019-11-19 22:06:10 +00:00
05a4ef1702 Update package.json 2019-11-18 11:29:32 +00:00
da9e6d6664 Add table borders 2019-11-18 01:04:31 +00:00
e9d3853d93 Merge pull request #448 from Sraleik/master
fix hyperlink id
2019-11-12 00:48:21 +00:00
5a9d6120a5 Merge pull request #447 from dolanmiu/feat/declaritive-numbering
Declarative numbering
2019-11-08 10:53:25 +00:00
e8410ff692 fix lint 2019-11-08 11:22:27 +01:00
3427d220c7 fix hyperlink id 2019-11-08 11:01:26 +01:00
643e3c2f84 Finish making numbering declarative 2019-11-08 03:11:19 +00:00
9b40b5e55e Add work for custom level id 2019-11-06 20:54:39 +00:00
a622c210ef Progress on declaritive numbering 2019-11-04 20:19:09 +00:00
9495f30e2d Merge pull request #446 from dolanmiu/dependabot/npm_and_yarn/lodash-4.17.15
Bump lodash from 4.17.11 to 4.17.15
2019-11-04 01:48:59 +00:00
617af87065 Bump lodash from 4.17.11 to 4.17.15
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-02 15:07:36 +00:00
c1dd421b27 Merge pull request #443 from alex-bogomolov/master
Add ability to vertically align content of section
2019-11-01 02:43:12 +00:00
8eff6bd0cf Declarative numbering work 2019-11-01 01:57:01 +00:00
8566c64eab improve coverage 2019-10-31 17:01:17 +02:00
afd468277e refactor code 2019-10-31 13:54:53 +02:00
ca9c992237 Merge pull request #444 from mloar/master
Allow image reuse
2019-10-31 02:10:55 +00:00
c93b74661b Fix linting error 2019-10-30 08:59:29 -05:00
08ff092638 Allow image reuse 2019-10-29 21:44:04 -05:00
2276572902 add ability to vertically align content of section 2019-10-29 12:55:15 +02:00
43ddeec6e1 Create FUNDING.yml 2019-10-29 01:04:43 +00:00
9eaf04f4c7 Merge pull request #436 from zaunermax/master
Support footnotes on TextRun elements
2019-10-19 14:46:23 +01:00
c3c29bb119 fix(test): added unit test for TextRun 2019-10-18 14:09:33 +02:00
ddb34e6a46 feat(textRun): implemented footnote references on the text run element 2019-10-18 13:33:47 +02:00
ef3055430b Merge pull request #428 from dolanmiu/feat/webpack-export
Put docx in a seperate namespace in the browser version
2019-10-12 22:37:02 +01:00
75cdae1473 Put docx in a seperate namespace in the browser version rather than the global namespace 2019-10-12 22:16:36 +01:00
860afe8f28 Merge pull request #427 from dolanmiu/feat/package-lock
Add lock file
2019-10-12 21:53:07 +01:00
f685dbe0d1 Add lock file 2019-10-12 21:46:18 +01:00
50911fff57 Add color to example 2019-10-12 21:42:10 +01:00
a7064f4728 Merge pull request #424 from seiya-git/master
fix set table cell width
2019-10-12 20:11:30 +01:00
564e9600ea Merge pull request #426 from jamesmontalvo3/nyc14
Bump nyc from 13.1.0 to 14.1.1
2019-10-12 18:50:01 +01:00
fcc4202598 Bump nyc from 13.1.0 to 14.1.1 2019-10-12 00:42:54 -05:00
bfbe59cb84 table cell width test 2019-10-12 03:14:25 +03:00
06abde2425 fix set table cell width 2019-10-12 02:52:30 +03:00
95 changed files with 11503 additions and 1037 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: dolanmiu
patreon: dolanmiu
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

1
.gitignore vendored
View File

@ -52,7 +52,6 @@ docs/.nojekyll
.idea
# Lock files
package-lock.json
yarn.lock
# Documents

8
.nycrc
View File

@ -1,9 +1,9 @@
{
"check-coverage": true,
"lines": 92.35,
"functions": 88.28,
"branches": 84.64,
"statements": 92.16,
"lines": 93.53,
"functions": 89.63,
"branches": 88.57,
"statements": 93.34,
"include": [
"src/**/*.ts"
],

View File

@ -18,7 +18,7 @@
[![codecov][codecov-image]][codecov-url]
<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>
# Demo
@ -73,6 +73,8 @@ 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/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/QEZXU5b.png" alt="drawing" height="50"/>](https://www.beekast.com/)
[<img src="https://imgur.com/XVU6aoi.png" alt="drawing" height="50"/>](https://herraizsoto.com/)
...and many more!

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// Page numbers
// Import from 'docx' rather than '../build' if you install from npm
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();
@ -11,7 +11,12 @@ doc.addSection({
children: [
new Paragraph({
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: [
new Paragraph({
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
// Import from 'docx' rather than '../build' if you install from npm
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();
@ -53,7 +53,11 @@ doc.addSection({
default: new Header({
children: [
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({
children: [
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({
children: [
new Paragraph({
children: [new TextRun("Page number: ").pageNumber()],
children: [
new TextRun({
children: ["Page number: ", PageNumber.CURRENT],
}),
],
}),
],
}),

View File

@ -1,16 +1,54 @@
// Footnotes
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, Packer, Paragraph } from "../build";
import { Document, FootnoteReferenceRun, Packer, Paragraph, TextRun } from "../build";
const doc = new Document();
doc.addSection({
children: [new Paragraph("Hello World").referenceFootnote(1), new Paragraph("Hello World").referenceFootnote(2)],
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.createFootnote(new Paragraph("Test"));
doc.createFootnote(new Paragraph("My amazing reference"));
doc.addSection({
children: [
new Paragraph({
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)],
}),
],
});
doc.addSection({
children: [
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) => {
fs.writeFileSync("My Document.docx", buffer);

View File

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

View File

@ -1,7 +1,7 @@
// Example on how to customise the look at feel using Styles
// Import from 'docx' rather than '../build' if you install from npm
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({
creator: "Clippy",
@ -19,6 +19,7 @@ const doc = new Document({
size: 28,
bold: true,
italics: true,
color: "red",
},
paragraph: {
spacing: {
@ -82,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({
children: [
new Paragraph({
@ -105,21 +114,21 @@ doc.addSection({
new Paragraph({
text: "Option1",
numbering: {
num: letterNumbering,
reference: "my-crazy-numbering",
level: 0,
},
}),
new Paragraph({
text: "Option5 -- override 2 to 5",
numbering: {
num: letterNumbering,
reference: "my-crazy-numbering",
level: 0,
},
}),
new Paragraph({
text: "Option3",
numbering: {
num: letterNumbering,
reference: "my-crazy-numbering",
level: 0,
},
}),

View File

@ -1,7 +1,7 @@
// 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 * 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 =
"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",
title: "Sample Document",
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({
children: [
new Paragraph({
heading: HeadingLevel.HEADING_1,
children: [bookmark],
children: [new Bookmark("myAnchorId", "Lorem Ipsum")],
}),
new Paragraph("\n"),
new Paragraph(LOREM_IPSUM),
@ -30,7 +30,7 @@ doc.addSection({
children: [new PageBreak()],
}),
new Paragraph({
children: [hyperlink],
children: [new HyperlinkRef("myAnchorId")],
}),
],
});

View File

@ -1,23 +1,53 @@
// Numbered lists
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, Numbering, Packer, Paragraph } from "../build";
import { AlignmentType, Document, Packer, Paragraph } from "../build";
const doc = new Document();
const numbering = new Numbering();
const abstractNum = numbering.createAbstractNumbering();
abstractNum.createLevel(0, "upperRoman", "%1", "start").indent({ left: 720, hanging: 260 });
const concrete = numbering.createConcreteNumbering(abstractNum);
const doc = new Document({
numbering: {
config: [
{
levels: [
{
level: 0,
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({
children: [
new Paragraph({
text: "line with contextual spacing",
numbering: {
num: concrete,
reference: "my-crazy-reference",
level: 0,
},
contextualSpacing: true,
@ -28,7 +58,7 @@ doc.addSection({
new Paragraph({
text: "line with contextual spacing",
numbering: {
num: concrete,
reference: "my-crazy-reference",
level: 0,
},
contextualSpacing: true,
@ -39,7 +69,7 @@ doc.addSection({
new Paragraph({
text: "line without contextual spacing",
numbering: {
num: concrete,
reference: "my-crazy-reference",
level: 0,
},
contextualSpacing: false,
@ -50,7 +80,7 @@ doc.addSection({
new Paragraph({
text: "line without contextual spacing",
numbering: {
num: concrete,
reference: "my-crazy-reference",
level: 0,
},
contextualSpacing: false,
@ -58,6 +88,27 @@ doc.addSection({
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
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, Numbering, Packer, Paragraph } from "../build";
import { AlignmentType, Document, Packer, Paragraph } from "../build";
const doc = new Document();
const numbering = new Numbering();
const abstractNum = numbering.createAbstractNumbering();
abstractNum.createLevel(0, "upperRoman", "%1", "start").indent({ left: 720, hanging: 260 });
abstractNum.createLevel(1, "decimal", "%2.", "start").indent({ left: 1440, hanging: 980 });
abstractNum.createLevel(2, "lowerLetter", "%3)", "start").indent({ left: 2160, hanging: 1700 });
const concrete = numbering.createConcreteNumbering(abstractNum);
const doc = new Document({
numbering: {
config: [
{
reference: "my-crazy-numbering",
levels: [
{
level: 0,
format: "upperRoman",
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({
children: [
new Paragraph({
text: "Hey you",
numbering: {
num: concrete,
reference: "my-crazy-numbering",
level: 0,
},
}),
new Paragraph({
text: "What's up fam",
numbering: {
num: concrete,
reference: "my-crazy-numbering",
level: 1,
},
}),
new Paragraph({
text: "Hello World 2",
numbering: {
num: concrete,
reference: "my-crazy-numbering",
level: 1,
},
}),
new Paragraph({
text: "Yeah boi",
numbering: {
num: concrete,
reference: "my-crazy-numbering",
level: 2,
},
}),
@ -68,6 +113,27 @@ doc.addSection({
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,8 @@
// 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 * as fs from "fs";
import { Document, HeadingLevel, Packer, Paragraph, ShadingType, Table, TableCell, TableRow, WidthType } from "../build";
import { AlignmentType, Document, HeadingLevel, Packer, Paragraph, ShadingType, Table, TableCell, TableRow, WidthType } from "../build";
const doc = new Document();
@ -29,6 +30,7 @@ const table = new Table({
});
const table2 = new Table({
alignment: AlignmentType.CENTER,
rows: [
new TableRow({
children: [
@ -66,6 +68,7 @@ const table2 = new Table({
});
const table3 = new Table({
alignment: AlignmentType.CENTER,
rows: [
new TableRow({
children: [

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// Example how to display page numbers
// Import from 'docx' rather than '../build' if you install from npm
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({});
@ -9,9 +9,17 @@ doc.addSection({
headers: {
default: new Header({
children: [
new Paragraph("Foo Bar corp. ")
.addRun(new TextRun("Page Number ").pageNumber())
.addRun(new TextRun(" to ").numberOfTotalPages()),
new Paragraph({
children: [
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({
children: [
new Paragraph({
text: "Foo Bar corp. ",
alignment: AlignmentType.CENTER,
})
.addRun(new TextRun("Page Number: ").pageNumber())
.addRun(new TextRun(" to ").numberOfTotalPages()),
children: [
new TextRun("Foo Bar corp. "),
new TextRun({
children: ["Page Number: ", PageNumber.CURRENT],
}),
new TextRun({
children: [" to ", PageNumber.TOTAL_PAGES],
}),
],
}),
],
}),
},
@ -32,11 +46,21 @@ doc.addSection({
pageNumberFormatType: PageNumberFormat.DECIMAL,
},
children: [
new Paragraph("Hello World 1").pageBreak(),
new Paragraph("Hello World 2").pageBreak(),
new Paragraph("Hello World 3").pageBreak(),
new Paragraph("Hello World 4").pageBreak(),
new Paragraph("Hello World 5").pageBreak(),
new Paragraph({
children: [new TextRun("Hello World 1"), new PageBreak()],
}),
new Paragraph({
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
// Import from 'docx' rather than '../build' if you install from npm
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();
@ -10,8 +10,12 @@ const header = new Header({
new Paragraph({
children: [
new TextRun("Header on another page"),
new TextRun("Page Number: ").pageNumber(),
new TextRun(" to ").numberOfTotalPagesSection(),
new TextRun({
children: ["Page number: ", PageNumber.CURRENT],
}),
new TextRun({
children: [" to ", PageNumber.TOTAL_PAGES_IN_SECTION],
}),
],
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

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

View File

@ -12,21 +12,21 @@
<script>
function generate() {
const doc = new Document();
const doc = new docx.Document();
doc.addSection({
children: [
new Paragraph({
new docx.Paragraph({
children: [
new TextRun("Hello World"),
new TextRun({
new docx.TextRun("Hello World"),
new docx.TextRun({
text: "Foo Bar",
bold: true,
}),
new TextRun({
text: "Github is the best",
new docx.TextRun({
text: "\tGithub is the best",
bold: true,
}).tab(),
}),
],
}),
],
@ -34,7 +34,7 @@
Packer.toBlob(doc).then((blob) => {
docx.Packer.toBlob(doc).then((blob) => {
console.log(blob);
saveAs(blob, "example.docx");
console.log("Document created successfully");

View File

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

View File

@ -20,11 +20,7 @@ This method is useful for adding different [text](text.md) with different styles
```ts
const paragraph = new Paragraph({
children: [
new TextRun("Lorem Ipsum Foo Bar"),
new TextRun("Hello World"),
new SymbolRun("F071"),
],
children: [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:
| Property | Type | Mandatory? | Possible Values |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
| [text](#text) | `string` | Optional | |
| [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 |
@ -80,7 +76,7 @@ This is the list of options for a paragraph. A detailed explanation is below:
| style | `string` | Optional | |
| tabStop | `{ left?: ITabStopOptions; right?: ITabStopOptions; maxRight?: { leader: LeaderType; }; center?: ITabStopOptions }` | Optional | |
| bullet | `{ level: number }` | Optional | |
| numbering | `{ num: Num; level: number; custom?: boolean }` | Optional | |
| numbering | `{ num: ConcreteNumbering; level: number; custom?: boolean }` | Optional | |
## Text
@ -252,10 +248,7 @@ To move to a new page (insert a page break):
```ts
const paragraph = new docx.Paragraph({
children: [
new TextRun("Amazing Heading"),
new PageBreak(),
]
children: [new TextRun("Amazing Heading"), new PageBreak()],
});
```

View File

@ -12,7 +12,7 @@ Simply call the relevant methods on the paragraph listed below. Then just add a
```ts
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: [
{
type: TabStopType.RIGHT,
@ -26,7 +26,7 @@ The example above will create a left aligned text, and a right aligned text on t
```ts
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: [
{
type: TabStopType.RIGHT,
@ -46,7 +46,7 @@ You can add multiple tab stops of the same `type` too.
```ts
const paragraph = new Paragraph({
children: [new TextRun("Multiple tab stops!").tab().tab()],
children: [new TextRun("Multiple tab stops!")],
tabStops: [
{
type: TabStopType.RIGHT,

8453
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "docx",
"version": "5.0.0-rc7",
"description": "Generate .docx documents with JavaScript (formerly Office-Clippy)",
"version": "5.0.0",
"description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.",
"main": "build/index.js",
"scripts": {
"pretest": "rimraf ./build",
@ -51,6 +51,7 @@
"dependencies": {
"@types/jszip": "^3.1.4",
"jszip": "^3.1.5",
"shortid": "^2.2.15",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
@ -64,6 +65,7 @@
"@types/chai": "^3.4.35",
"@types/mocha": "^2.2.39",
"@types/request-promise": "^4.1.42",
"@types/shortid": "0.0.29",
"@types/sinon": "^4.3.1",
"@types/webpack": "^4.4.24",
"awesome-typescript-loader": "^3.4.1",
@ -74,7 +76,7 @@
"jszip": "^3.1.5",
"mocha": "^5.2.0",
"mocha-webpack": "^1.0.1",
"nyc": "^13.1.0",
"nyc": "^14.1.1",
"pre-commit": "^1.2.2",
"prettier": "^1.15.2",
"prompt": "^1.0.0",

View File

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

View File

@ -5,7 +5,7 @@ export class ImageReplacer {
let currentXmlData = xmlData;
mediaData.forEach((image, i) => {
currentXmlData = currentXmlData.replace(`{${image.fileName}}`, (offset + i).toString());
currentXmlData = currentXmlData.replace(new RegExp(`{${image.fileName}}`, "g"), (offset + i).toString());
});
return currentXmlData;

View File

@ -4,6 +4,7 @@ import * as xml from "xml";
import { File } from "file";
import { Formatter } from "../formatter";
import { ImageReplacer } from "./image-replacer";
import { NumberingReplacer } from "./numbering-replacer";
interface IXmlifyedFile {
readonly data: string;
@ -30,10 +31,12 @@ interface IXmlifyedFileMapping {
export class Compiler {
private readonly formatter: Formatter;
private readonly imageReplacer: ImageReplacer;
private readonly numberingReplacer: NumberingReplacer;
constructor() {
this.formatter = new Formatter();
this.imageReplacer = new ImageReplacer();
this.numberingReplacer = new NumberingReplacer();
}
public compile(file: File, prettifyXml?: boolean): JSZip {
@ -68,7 +71,7 @@ export class Compiler {
file.verifyUpdateFields();
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);
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",
},
Document: {
data: (() => {
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",
},
Styles: {
data: xml(this.formatter.format(file.Styles), prettify),
data: xml(this.formatter.format(file.Styles, file), prettify),
path: "word/styles.xml",
},
Properties: {
data: xml(this.formatter.format(file.CoreProperties), {
data: xml(this.formatter.format(file.CoreProperties, file), {
declaration: {
standalone: "yes",
encoding: "UTF-8",
@ -108,15 +112,15 @@ export class Compiler {
path: "docProps/core.xml",
},
Numbering: {
data: xml(this.formatter.format(file.Numbering), prettify),
data: xml(this.formatter.format(file.Numbering, file), prettify),
path: "word/numbering.xml",
},
FileRelationships: {
data: xml(this.formatter.format(file.FileRelationships), prettify),
data: xml(this.formatter.format(file.FileRelationships, file), prettify),
path: "_rels/.rels",
},
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);
mediaDatas.forEach((mediaData, i) => {
@ -128,12 +132,12 @@ export class Compiler {
});
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`,
};
}),
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);
mediaDatas.forEach((mediaData, i) => {
@ -145,12 +149,12 @@ export class Compiler {
});
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`,
};
}),
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);
// TODO: 0 needs to be changed when headers get relationships of their own
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0);
@ -161,7 +165,7 @@ export class Compiler {
};
}),
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);
// TODO: 0 needs to be changed when headers get relationships of their own
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0);
@ -172,19 +176,19 @@ export class Compiler {
};
}),
ContentTypes: {
data: xml(this.formatter.format(file.ContentTypes), prettify),
data: xml(this.formatter.format(file.ContentTypes, file), prettify),
path: "[Content_Types].xml",
},
AppProperties: {
data: xml(this.formatter.format(file.AppProperties), prettify),
data: xml(this.formatter.format(file.AppProperties, file), prettify),
path: "docProps/app.xml",
},
FootNotes: {
data: xml(this.formatter.format(file.FootNotes), prettify),
data: xml(this.formatter.format(file.FootNotes, file), prettify),
path: "word/footnotes.xml",
},
Settings: {
data: xml(this.formatter.format(file.Settings), prettify),
data: xml(this.formatter.format(file.Settings, file), prettify),
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

@ -1,9 +1,22 @@
import { XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes";
import { INumberingOptions } from "../numbering";
import { HyperlinkType, Paragraph } from "../paragraph";
import { IStylesOptions } from "../styles";
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 {
readonly title?: string;
readonly subject?: string;
@ -14,6 +27,11 @@ export interface IPropertiesOptions {
readonly revision?: string;
readonly externalStyles?: string;
readonly styles?: IStylesOptions;
readonly numbering?: INumberingOptions;
readonly footnotes?: Paragraph[];
readonly hyperlinks?: {
readonly [key: string]: IInternalHyperlinkDefinition | IExternalHyperlinkDefinition;
};
}
export class CoreProperties extends XmlComponent {

View File

@ -22,9 +22,6 @@ describe("Body", () => {
expect(tree).to.deep.equal({
"w:body": [
{
"w:p": {},
},
{
"w:sectPr": [
{ "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 { Paragraph, ParagraphProperties, TableOfContents } from "../..";
import { File } from "../../../file";
import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties";
export class Body extends XmlComponent {
@ -24,12 +25,13 @@ export class Body extends XmlComponent {
this.sections.push(new SectionProperties(options));
}
public prepForXml(): IXmlableObject | undefined {
public prepForXml(file?: File): IXmlableObject | undefined {
if (this.sections.length === 1) {
this.root.splice(0, 1);
this.root.push(this.sections.pop() as SectionProperties);
}
return super.prepForXml();
return super.prepForXml(file);
}
public push(component: XmlComponent): void {

View File

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

View File

@ -8,6 +8,7 @@ import { Media } from "file/media";
import { PageBorderOffsetFrom } from "./page-border";
import { PageNumberFormat } from "./page-number";
import { SectionProperties } from "./section-properties";
import { SectionVerticalAlignValue } from "./vertical-align";
describe("SectionProperties", () => {
describe("#constructor()", () => {
@ -39,6 +40,7 @@ describe("SectionProperties", () => {
pageNumberStart: 10,
pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT,
titlePage: true,
verticalAlign: SectionVerticalAlignValue.TOP,
});
const tree = new Formatter().format(properties);
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 { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes";
import { TitlePage } from "./title-page/title-page";
import { ISectionVerticalAlignAttributes, SectionVerticalAlign } from "./vertical-align";
export interface IHeaderFooterGroup<T> {
readonly default?: T;
@ -45,7 +46,8 @@ export type SectionPropertiesOptions = IPageSizeAttributes &
IPageNumberTypeAttributes &
ILineNumberAttributes &
IPageBordersOptions &
ITitlePageOptions & {
ITitlePageOptions &
ISectionVerticalAlignAttributes & {
readonly column?: {
readonly space?: number;
readonly count?: number;
@ -87,6 +89,7 @@ export class SectionProperties extends XmlComponent {
pageBorderBottom,
pageBorderLeft,
titlePage = false,
verticalAlign,
} = options;
this.options = options;
@ -121,6 +124,10 @@ export class SectionProperties extends XmlComponent {
if (titlePage) {
this.root.push(new TitlePage());
}
if (verticalAlign) {
this.root.push(new SectionVerticalAlign(verticalAlign));
}
}
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
import { XmlComponent } from "file/xml-components";
import { Paragraph } from "../paragraph";
import { Hyperlink, Paragraph } from "../paragraph";
import { Table } from "../table";
import { TableOfContents } from "../table-of-contents";
import { Body } from "./body";
@ -36,7 +36,7 @@ export class Document extends XmlComponent {
this.root.push(this.body);
}
public add(item: Paragraph | Table | TableOfContents): Document {
public add(item: Paragraph | Table | TableOfContents | Hyperlink): Document {
this.body.push(item);
return this;
}

View File

@ -5,7 +5,7 @@ import { Formatter } from "export/formatter";
import { File } from "./file";
import { Footer, Header } from "./header";
import { Paragraph } from "./paragraph";
import { HyperlinkRef, Paragraph } from "./paragraph";
import { Table, TableCell, TableRow } from "./table";
import { TableOfContents } from "./table-of-contents";
@ -20,8 +20,8 @@ describe("File", () => {
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"][1]["w:sectPr"][5]["w:footerReference"]._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"][0]["w:sectPr"][5]["w:footerReference"]._attr["w:type"]).to.equal("default");
});
it("should create with correct headers and footers", () => {
@ -39,8 +39,8 @@ describe("File", () => {
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"][1]["w:sectPr"][5]["w:footerReference"]._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"][0]["w:sectPr"][5]["w:footerReference"]._attr["w:type"]).to.equal("default");
});
it("should create with first headers and footers", () => {
@ -58,8 +58,8 @@ describe("File", () => {
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"][1]["w:sectPr"][7]["w:footerReference"]._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"][0]["w:sectPr"][7]["w:footerReference"]._attr["w:type"]).to.equal("first");
});
it("should create with correct headers", () => {
@ -81,13 +81,98 @@ describe("File", () => {
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"][1]["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"][4]["w:headerReference"]._attr["w:type"]).to.equal("default");
expect(tree["w:body"][0]["w:sectPr"][5]["w:headerReference"]._attr["w:type"]).to.equal("first");
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"][1]["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"][7]["w:footerReference"]._attr["w:type"]).to.equal("default");
expect(tree["w:body"][0]["w:sectPr"][8]["w:footerReference"]._attr["w:type"]).to.equal("first");
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);
});
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", () => {
const file = new File();
const spy = sinon.spy(file.Document, "add");
@ -148,13 +243,196 @@ describe("File", () => {
});
});
describe("#createFootnote", () => {
it("should call the underlying document's createFootnote", () => {
const wrapper = new File();
const spy = sinon.spy(wrapper.FootNotes, "createFootNote");
wrapper.createFootnote(new Paragraph(""));
describe("#HyperlinkCache", () => {
it("should initially have empty hyperlink cache", () => {
const file = new File();
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 { ContentTypes } from "./content-types/content-types";
import { CoreProperties, IPropertiesOptions } from "./core-properties";
@ -16,7 +17,7 @@ import { Footer, Header } from "./header";
import { HeaderWrapper, IDocumentHeader } from "./header-wrapper";
import { Media } from "./media";
import { Numbering } from "./numbering";
import { Bookmark, Hyperlink, Paragraph } from "./paragraph";
import { Hyperlink, HyperlinkRef, HyperlinkType, Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { TargetModeType } from "./relationships/relationship/relationship";
import { Settings } from "./settings";
@ -40,7 +41,7 @@ export interface ISectionOptions {
readonly size?: IPageSizeAttributes;
readonly margins?: IPageMarginAttributes;
readonly properties?: SectionPropertiesOptions;
readonly children: Array<Paragraph | Table | TableOfContents>;
readonly children: Array<Paragraph | Table | TableOfContents | HyperlinkRef>;
}
export class File {
@ -60,6 +61,7 @@ export class File {
private readonly contentTypes: ContentTypes;
private readonly appProperties: AppProperties;
private readonly styles: Styles;
private readonly hyperlinkCache: { readonly [key: string]: Hyperlink } = {};
constructor(
options: IPropertiesOptions = {
@ -71,7 +73,13 @@ export class File {
sections: ISectionOptions[] = [],
) {
this.coreProperties = new CoreProperties(options);
this.numbering = new Numbering();
this.numbering = new Numbering(
options.numbering
? options.numbering
: {
config: [],
},
);
this.docRelationships = new Relationships();
this.fileRelationships = new Relationships();
this.appProperties = new AppProperties();
@ -126,33 +134,42 @@ export class File {
this.document.Body.addSection(section.properties ? section.properties : {});
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);
}
}
if (options.footnotes) {
for (const paragraph of options.footnotes) {
this.footNotes.createFootNote(paragraph);
}
}
public createHyperlink(link: string, text?: string): Hyperlink {
const newText = text === undefined ? link : text;
const hyperlink = new Hyperlink(newText, this.docRelationships.RelationshipCount);
this.docRelationships.createRelationship(
hyperlink.linkId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
link,
TargetModeType.EXTERNAL,
);
return hyperlink;
if (options.hyperlinks) {
const cache = {};
for (const key in options.hyperlinks) {
if (!options.hyperlinks[key]) {
continue;
}
public createInternalHyperLink(anchor: string, text?: string): Hyperlink {
const newText = text === undefined ? anchor : text;
const hyperlink = new Hyperlink(newText, this.docRelationships.RelationshipCount, anchor);
// NOTE: unlike File#createHyperlink(), since the link is to an internal bookmark
// we don't need to create a new relationship.
return hyperlink;
const hyperlinkRef = options.hyperlinks[key];
const hyperlink =
hyperlinkRef.type === HyperlinkType.EXTERNAL
? this.createHyperlink(hyperlinkRef.link, hyperlinkRef.text)
: this.createInternalHyperLink(key, hyperlinkRef.text);
cache[key] = hyperlink;
}
public createBookmark(name: string, text: string = name): Bookmark {
return new Bookmark(name, text, this.docRelationships.RelationshipCount);
this.hyperlinkCache = cache;
}
}
public addSection({
@ -180,12 +197,14 @@ export class File {
});
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.footNotes.createFootNote(paragraph);
this.document.add(child);
}
}
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 {
const wrapper = new HeaderWrapper(this.media, this.currentRelationshipId++);
@ -326,4 +363,8 @@ export class File {
public get Settings(): 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 "./footnote";

View File

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

View File

@ -0,0 +1,605 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { EMPTY_OBJECT } from "file/xml-components";
import { AlignmentType, TabStopPosition } from "../paragraph";
import { UnderlineType } from "../paragraph/run/underline";
import { ShadingType } from "../table";
import { AbstractNumbering } from "./abstract-numbering";
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)" } } });
});
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", () => {
it("#size", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
size: 24,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:sz": { _attr: { "w:val": 24 } } }],
});
});
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", () => {
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("#bold", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
bold: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:b": { _attr: { "w:val": true } } }],
});
});
it("#italics", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
italics: true,
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:i": { _attr: { "w:val": true } } }],
});
});
it("#highlight", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
highlight: "005599",
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({
"w:rPr": [{ "w:highlight": { _attr: { "w:val": "005599" } } }],
});
});
it("#shadow", () => {
const abstractNumbering = new AbstractNumbering(1, [
{
level: 0,
format: "lowerRoman",
text: "%0.",
style: {
run: {
shadow: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "FF0000",
},
},
},
},
]);
const tree = new Formatter().format(abstractNumbering);
expect(tree["w:abstractNum"][2]["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, [
{
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" } } }],
});
});
});
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 { Level } from "./level";
import { ILevelsOptions, Level } from "./level";
import { MultiLevelType } from "./multi-level-type";
interface IAbstractNumberingAttributesProperties {
@ -17,7 +18,7 @@ class AbstractNumberingAttributes extends XmlAttributeComponent<IAbstractNumberi
export class AbstractNumbering extends XmlComponent {
public readonly id: number;
constructor(id: number) {
constructor(id: number, levelOptions: ILevelsOptions[]) {
super("w:abstractNum");
this.root.push(
new AbstractNumberingAttributes({
@ -27,15 +28,9 @@ export class AbstractNumbering extends XmlComponent {
);
this.root.push(new MultiLevelType("hybridMultilevel"));
this.id = id;
}
public addLevel(level: Level): void {
this.root.push(level);
for (const option of levelOptions) {
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

@ -2,9 +2,7 @@ import { Attributes, XmlAttributeComponent, XmlComponent } from "file/xml-compon
import {
Alignment,
AlignmentType,
IIndentAttributesProperties,
Indent,
ISpacingProperties,
KeepLines,
KeepNext,
Spacing,
@ -15,7 +13,7 @@ import {
import { ParagraphProperties } from "../paragraph/properties";
import * as formatting from "../paragraph/run/formatting";
import { RunProperties } from "../paragraph/run/properties";
import { UnderlineType } from "../paragraph/run/underline";
import { IParagraphStyleOptions2, IRunStyleOptions } from "../styles/style-options";
interface ILevelAttributesProperties {
readonly ilvl?: number;
@ -63,7 +61,7 @@ class LevelText extends XmlComponent {
}
class LevelJc extends XmlComponent {
constructor(value: string) {
constructor(value: AlignmentType) {
super("w:lvlJc");
this.root.push(
new Attributes({
@ -79,6 +77,19 @@ export enum LevelSuffix {
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?: IRunStyleOptions;
readonly paragraph?: IParagraphStyleOptions2;
};
}
class Suffix extends XmlComponent {
constructor(value: LevelSuffix) {
super("w:suff");
@ -94,7 +105,7 @@ export class LevelBase extends XmlComponent {
private readonly paragraphProperties: ParagraphProperties;
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");
this.root.push(
new LevelAttributes({
@ -103,17 +114,15 @@ export class LevelBase extends XmlComponent {
}),
);
if (start !== undefined) {
this.root.push(new Start(start));
this.root.push(new LevelJc(alignment));
if (format) {
this.root.push(new NumberFormat(format));
}
if (numberFormat !== undefined) {
this.root.push(new NumberFormat(numberFormat));
}
if (levelText !== undefined) {
this.root.push(new LevelText(levelText));
}
if (lvlJc !== undefined) {
this.root.push(new LevelJc(lvlJc));
if (text) {
this.root.push(new LevelText(text));
}
this.paragraphProperties = new ParagraphProperties({});
@ -121,156 +130,112 @@ export class LevelBase extends XmlComponent {
this.root.push(this.paragraphProperties);
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;
if (style) {
if (style.run) {
if (style.run.size) {
this.runProperties.push(new formatting.Size(style.run.size));
}
public addParagraphProperty(property: XmlComponent): Level {
this.paragraphProperties.push(property);
return this;
if (style.run.bold) {
this.runProperties.push(new formatting.Bold());
}
public addRunProperty(property: XmlComponent): Level {
this.runProperties.push(property);
return this;
if (style.run.italics) {
this.runProperties.push(new formatting.Italics());
}
// ---------- Run formatting ---------------------- //
public size(twips: number): Level {
this.addRunProperty(new formatting.Size(twips));
return this;
if (style.run.smallCaps) {
this.runProperties.push(new formatting.SmallCaps());
}
public bold(): Level {
this.addRunProperty(new formatting.Bold());
return this;
if (style.run.allCaps) {
this.runProperties.push(new formatting.Caps());
}
public italics(): Level {
this.addRunProperty(new formatting.Italics());
return this;
if (style.run.strike) {
this.runProperties.push(new formatting.Strike());
}
public smallCaps(): Level {
this.addRunProperty(new formatting.SmallCaps());
return this;
if (style.run.doubleStrike) {
this.runProperties.push(new formatting.DoubleStrike());
}
public allCaps(): Level {
this.addRunProperty(new formatting.Caps());
return this;
if (style.run.subScript) {
this.runProperties.push(new formatting.SubScript());
}
public strike(): Level {
this.addRunProperty(new formatting.Strike());
return this;
if (style.run.superScript) {
this.runProperties.push(new formatting.SuperScript());
}
public doubleStrike(): Level {
this.addRunProperty(new formatting.DoubleStrike());
return this;
if (style.run.underline) {
this.runProperties.push(new formatting.Underline(style.run.underline.type, style.run.underline.color));
}
public subScript(): Level {
this.addRunProperty(new formatting.SubScript());
return this;
if (style.run.color) {
this.runProperties.push(new formatting.Color(style.run.color));
}
public superScript(): Level {
this.addRunProperty(new formatting.SuperScript());
return this;
if (style.run.font) {
this.runProperties.push(new formatting.RunFonts(style.run.font));
}
public underline(underlineType?: UnderlineType, color?: string): Level {
this.addRunProperty(new formatting.Underline(underlineType, color));
return this;
if (style.run.highlight) {
this.runProperties.push(new formatting.Highlight(style.run.highlight));
}
public color(color: string): Level {
this.addRunProperty(new formatting.Color(color));
return this;
if (style.run.shadow) {
this.runProperties.push(new formatting.Shading(style.run.shadow.type, style.run.shadow.fill, style.run.shadow.color));
}
}
public font(fontName: string): Level {
this.addRunProperty(new formatting.RunFonts(fontName));
return this;
if (style.paragraph) {
if (style.paragraph.alignment) {
this.paragraphProperties.push(new Alignment(style.paragraph.alignment));
}
public highlight(color: string): Level {
this.addRunProperty(new formatting.Highlight(color));
return this;
if (style.paragraph.thematicBreak) {
this.paragraphProperties.push(new ThematicBreak());
}
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;
if (style.paragraph.rightTabStop) {
this.paragraphProperties.push(new TabStop(TabStopType.RIGHT, style.paragraph.rightTabStop));
}
public left(): Level {
this.addParagraphProperty(new Alignment(AlignmentType.LEFT));
return this;
if (style.paragraph.leftTabStop) {
this.paragraphProperties.push(new TabStop(TabStopType.LEFT, style.paragraph.leftTabStop));
}
public right(): Level {
this.addParagraphProperty(new Alignment(AlignmentType.RIGHT));
return this;
if (style.paragraph.indent) {
this.paragraphProperties.push(new Indent(style.paragraph.indent));
}
public justified(): Level {
this.addParagraphProperty(new Alignment(AlignmentType.BOTH));
return this;
if (style.paragraph.spacing) {
this.paragraphProperties.push(new Spacing(style.paragraph.spacing));
}
public thematicBreak(): Level {
this.addParagraphProperty(new ThematicBreak());
return this;
if (style.paragraph.keepNext) {
this.paragraphProperties.push(new KeepNext());
}
public rightTabStop(position: number): Level {
return this.addParagraphProperty(new TabStop(TabStopType.RIGHT, position));
if (style.paragraph.keepLines) {
this.paragraphProperties.push(new KeepLines());
}
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 {
// This is the level that sits under abstractNum. We make a
// handful of properties required
constructor(level: number, numberFormat: string, levelText: string, lvlJc: string) {
super(level, 1, numberFormat, levelText, lvlJc);
constructor(options: ILevelsOptions) {
super(options);
}
}

View File

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

View File

@ -2,24 +2,15 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { AbstractNumbering } from "./abstract-numbering";
import { LevelForOverride } from "./level";
import { Num } from "./num";
import { Numbering } from "./numbering";
import { EMPTY_OBJECT } from "file/xml-components";
import { TabStopPosition } from "../paragraph";
import { UnderlineType } from "../paragraph/run/underline";
describe("Numbering", () => {
let numbering: Numbering;
beforeEach(() => {
numbering = new Numbering();
});
describe("#constructor", () => {
it("creates a default numbering with one abstract and one concrete instance", () => {
const numbering = new Numbering({
config: [],
});
const tree = new Formatter().format(numbering);
expect(Object.keys(tree)).to.deep.equal(["w:numbering"]);
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 { DocumentAttributes } from "../document/document-attributes";
import { AbstractNumbering } from "./abstract-numbering";
import { Num } from "./num";
import { ILevelsOptions } from "./level";
import { ConcreteNumbering } from "./num";
export interface INumberingOptions {
readonly config: Array<{
readonly levels: ILevelsOptions[];
readonly reference: string;
}>;
}
export class Numbering extends XmlComponent {
// tslint:disable-next-line:readonly-keyword
private nextId: number;
private readonly abstractNumbering: XmlComponent[] = [];
private readonly concreteNumbering: XmlComponent[] = [];
private readonly abstractNumbering: AbstractNumbering[] = [];
private readonly concreteNumbering: ConcreteNumbering[] = [];
constructor() {
constructor(options: INumberingOptions) {
super("w:numbering");
this.root.push(
new DocumentAttributes({
@ -37,39 +47,114 @@ export class Numbering extends XmlComponent {
this.nextId = 0;
const abstractNumbering = this.createAbstractNumbering();
abstractNumbering.createLevel(0, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 720, hanging: 360 }));
abstractNumbering.createLevel(1, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 1440, hanging: 360 }));
abstractNumbering.createLevel(2, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 2160, hanging: 360 }));
abstractNumbering.createLevel(3, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 2880, 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 }));
abstractNumbering.createLevel(6, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 5040, hanging: 360 }));
abstractNumbering.createLevel(7, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 5760, hanging: 360 }));
abstractNumbering.createLevel(8, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 6480, hanging: 360 }));
const abstractNumbering = this.createAbstractNumbering([
{
level: 0,
format: "bullet",
text: "\u25CF",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 720, hanging: 360 },
},
},
},
{
level: 1,
format: "bullet",
text: "\u25CB",
alignment: AlignmentType.LEFT,
style: {
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);
}
public createAbstractNumbering(): AbstractNumbering {
const num = new AbstractNumbering(this.nextId++);
this.abstractNumbering.push(num);
return num;
for (const con of options.config) {
const currentAbstractNumbering = this.createAbstractNumbering(con.levels);
this.createConcreteNumbering(currentAbstractNumbering, con.reference);
}
public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num {
const num = new Num(this.nextId++, abstractNumbering.id);
this.concreteNumbering.push(num);
return num;
}
public prepForXml(): IXmlableObject | undefined {
@ -77,4 +162,20 @@ export class Numbering extends XmlComponent {
this.concreteNumbering.forEach((x) => this.root.push(x));
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/WPtableAlignment.php
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export enum AlignmentType {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { Attributes, XmlComponent } from "file/xml-components";
export class NumberProperties extends XmlComponent {
constructor(numberId: number, indentLevel: number) {
constructor(numberId: number | string, indentLevel: number) {
super("w:numPr");
this.root.push(new IndentLevel(indentLevel));
this.root.push(new NumberId(numberId));
@ -20,11 +20,11 @@ class IndentLevel extends XmlComponent {
}
class NumberId extends XmlComponent {
constructor(id: number) {
constructor(id: number | string) {
super("w:numId");
this.root.push(
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";
@ -8,7 +8,7 @@ describe("Bookmark", () => {
let bookmark: Bookmark;
beforeEach(() => {
bookmark = new Bookmark("anchor", "Internal Link", 0);
bookmark = new Bookmark("anchor", "Internal Link");
});
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", () => {
const newJson = Utility.jsonify(bookmark);
const attributes = {
name: "anchor",
id: "1",
};
assert.equal(JSON.stringify(newJson.start.root[0].root), JSON.stringify(attributes));
assert.equal(newJson.start.root[0].root.name, "anchor");
});
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", () => {
const newJson = Utility.jsonify(bookmark);
const attributes = {
id: "1",
};
assert.equal(JSON.stringify(newJson.end.root[0].root), JSON.stringify(attributes));
expect(newJson.end.root[0].root.id).to.be.a("string");
});
});

View File

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

View File

@ -3,12 +3,13 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { Hyperlink } from "./";
import { HyperlinkRef } from "./hyperlink";
describe("Hyperlink", () => {
let hyperlink: Hyperlink;
beforeEach(() => {
hyperlink = new Hyperlink("https://example.com", 0);
hyperlink = new Hyperlink("https://example.com", "superid");
});
describe("#constructor()", () => {
@ -19,7 +20,7 @@ describe("Hyperlink", () => {
{
_attr: {
"w:history": 1,
"r:id": "rId1",
"r:id": "rIdsuperid",
},
},
{
@ -34,7 +35,7 @@ describe("Hyperlink", () => {
describe("with optional anchor parameter", () => {
beforeEach(() => {
hyperlink = new Hyperlink("Anchor Text", 0, "anchor");
hyperlink = new Hyperlink("Anchor Text", "superid2", "anchor");
});
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 { 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 {
public readonly linkId: number;
public readonly linkId: string;
private readonly textRun: TextRun;
constructor(text: string, relationshipsCount: number, anchor?: string) {
constructor(text: string, relationshipId: string, anchor?: string) {
super("w:hyperlink");
this.linkId = relationshipsCount + 1;
this.linkId = relationshipId;
const props: IHyperlinkAttributesProperties = {
history: 1,

View File

@ -1,10 +1,12 @@
import { assert, expect } from "chai";
import * as shortid from "shortid";
import { stub } from "sinon";
import { Formatter } from "export/formatter";
import { EMPTY_OBJECT } from "file/xml-components";
import { Numbering } from "../numbering";
import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting";
import { Bookmark } from "./links";
import { Paragraph } from "./paragraph";
describe("Paragraph", () => {
@ -596,14 +598,9 @@ describe("Paragraph", () => {
describe("#setNumbering", () => {
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({
numbering: {
num: letterNumbering,
reference: "test id",
level: 0,
},
});
@ -622,14 +619,9 @@ describe("Paragraph", () => {
});
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({
numbering: {
num: letterNumbering,
reference: "test id",
level: 0,
},
});
@ -640,10 +632,7 @@ describe("Paragraph", () => {
"w:pPr": [
{ "w:pStyle": { _attr: { "w:val": "ListParagraph" } } },
{
"w:numPr": [
{ "w:ilvl": { _attr: { "w:val": 0 } } },
{ "w:numId": { _attr: { "w:val": letterNumbering.id } } },
],
"w:numPr": [{ "w:ilvl": { _attr: { "w:val": 0 } } }, { "w:numId": { _attr: { "w:val": "{test id}" } } }],
},
],
},
@ -652,6 +641,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", () => {
it("should set the paragraph style to the given styleId", () => {
const paragraph = new Paragraph({

View File

@ -1,8 +1,8 @@
// http://officeopenxml.com/WPparagraph.php
import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { Num } from "file/numbering/num";
import { XmlComponent } from "file/xml-components";
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { File } from "../file";
import { Alignment, AlignmentType } from "./formatting/alignment";
import { Bidirectional } from "./formatting/bidirectional";
import { IBorderOptions, ThematicBreak } from "./formatting/border";
@ -13,7 +13,7 @@ import { ContextualSpacing, ISpacingProperties, Spacing } from "./formatting/spa
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 { Bookmark, HyperlinkRef, OutlineLevel } from "./links";
import { ParagraphProperties } from "./properties";
import { PictureRun, Run, SequentialIdentifier, SymbolRun, TextRun } from "./run";
@ -41,11 +41,13 @@ export interface IParagraphOptions {
readonly level: number;
};
readonly numbering?: {
readonly num: Num;
readonly reference: string;
readonly level: number;
readonly custom?: boolean;
};
readonly children?: Array<TextRun | PictureRun | Hyperlink | SymbolRun | Bookmark | PageBreak | SequentialIdentifier>;
readonly children?: Array<
TextRun | PictureRun | SymbolRun | Bookmark | PageBreak | SequentialIdentifier | FootnoteReferenceRun | HyperlinkRef
>;
}
export class Paragraph extends XmlComponent {
@ -141,7 +143,7 @@ export class Paragraph extends XmlComponent {
if (!options.numbering.custom) {
this.properties.push(new Style("ListParagraph"));
}
this.properties.push(new NumberProperties(options.numbering.num.id, options.numbering.level));
this.properties.push(new NumberProperties(options.numbering.reference, options.numbering.level));
}
if (options.children) {
@ -158,9 +160,15 @@ export class Paragraph extends XmlComponent {
}
}
public referenceFootnote(id: number): Paragraph {
this.root.push(new FootnoteReferenceRun(id));
return this;
public prepForXml(file: File): IXmlableObject | undefined {
for (const element of this.root) {
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 {

View File

@ -5,3 +5,4 @@ export * from "./picture-run";
export * from "./run-fonts";
export * from "./sequential-identifier";
export * from "./underline";
export * from "./tab";

View File

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

View File

@ -6,12 +6,6 @@ import { Text } from "./text";
describe("Text", () => {
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", () => {
const t = new Text(" this is\n text");
const f = new Formatter().format(t);

View File

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

View File

@ -1,9 +1,11 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
// import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { ShadingType } from "file/table";
import { Run } from "./";
import { PageNumber } from "./run";
import { UnderlineType } from "./underline";
describe("Run", () => {
@ -130,6 +132,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()", () => {
it("it should add highlight to the properties", () => {
const run = new Run({
@ -197,17 +223,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()", () => {
it("should set the font as named", () => {
const run = new Run({
@ -270,8 +285,10 @@ describe("Run", () => {
describe("#numberOfTotalPages", () => {
it("should set the run to the RTL mode", () => {
const run = new Run({});
run.numberOfTotalPages();
const run = new Run({
children: [PageNumber.TOTAL_PAGES],
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [
@ -286,8 +303,10 @@ describe("Run", () => {
describe("#numberOfTotalPagesSection", () => {
it("should set the run to the RTL mode", () => {
const run = new Run({});
run.numberOfTotalPagesSection();
const run = new Run({
children: [PageNumber.TOTAL_PAGES_IN_SECTION],
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [
@ -302,8 +321,9 @@ describe("Run", () => {
describe("#pageNumber", () => {
it("should set the run to the RTL mode", () => {
const run = new Run({});
run.pageNumber();
const run = new Run({
children: [PageNumber.CURRENT],
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [

View File

@ -2,6 +2,7 @@
import { ShadingType } from "file/table";
import { XmlComponent } from "file/xml-components";
import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { FieldInstruction } from "file/table-of-contents/field-instruction";
import { Break } from "./break";
import { Caps, SmallCaps } from "./caps";
@ -24,10 +25,10 @@ import {
} from "./formatting";
import { NumberOfPages, NumberOfPagesSection, Page } from "./page-number";
import { RunProperties } from "./properties";
import { Text } from "./run-components/text";
import { RunFonts } from "./run-fonts";
import { SubScript, SuperScript } from "./script";
import { Style } from "./style";
import { Tab } from "./tab";
import { Underline, UnderlineType } from "./underline";
export interface IRunOptions {
@ -57,7 +58,14 @@ export interface IRunOptions {
readonly fill: string;
readonly color: string;
};
readonly children?: Array<Begin | FieldInstruction | Separate | End>;
readonly children?: Array<Begin | FieldInstruction | Separate | End | PageNumber | FootnoteReferenceRun | string>;
readonly text?: string;
}
export enum PageNumber {
CURRENT = "CURRENT",
TOTAL_PAGES = "TOTAL_PAGES",
TOTAL_PAGES_IN_SECTION = "TOTAL_PAGES_IN_SECTION",
}
export class Run extends XmlComponent {
@ -139,8 +147,37 @@ export class Run extends XmlComponent {
if (options.children) {
for (const child of options.children) {
if (typeof child === "string") {
switch (child) {
case PageNumber.CURRENT:
this.root.push(new Begin());
this.root.push(new Page());
this.root.push(new Separate());
this.root.push(new End());
break;
case PageNumber.TOTAL_PAGES:
this.root.push(new Begin());
this.root.push(new NumberOfPages());
this.root.push(new Separate());
this.root.push(new End());
break;
case PageNumber.TOTAL_PAGES_IN_SECTION:
this.root.push(new Begin());
this.root.push(new NumberOfPagesSection());
this.root.push(new Separate());
this.root.push(new End());
break;
default:
this.root.push(new Text(child));
break;
}
continue;
}
this.root.push(child);
}
} else if (options.text) {
this.root.push(new Text(options.text));
}
}
@ -148,33 +185,4 @@ export class Run extends XmlComponent {
this.root.splice(1, 0, new Break());
return this;
}
public tab(): Run {
this.root.splice(1, 0, new Tab());
return this;
}
public pageNumber(): Run {
this.root.push(new Begin());
this.root.push(new Page());
this.root.push(new Separate());
this.root.push(new End());
return this;
}
public numberOfTotalPages(): Run {
this.root.push(new Begin());
this.root.push(new NumberOfPages());
this.root.push(new Separate());
this.root.push(new End());
return this;
}
public numberOfTotalPagesSection(): Run {
this.root.push(new Begin());
this.root.push(new NumberOfPagesSection());
this.root.push(new Separate());
this.root.push(new End());
return this;
}
}

View File

@ -1,6 +1,7 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { TextRun } from "./text-run";
@ -16,4 +17,25 @@ describe("TextRun", () => {
});
});
});
describe("#referenceFootnote()", () => {
it("should add a valid footnote reference", () => {
run = new TextRun({
children: ["test", new FootnoteReferenceRun(1)],
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [
{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test"] },
{
"w:r": [
{ "w:rPr": [{ "w:rStyle": { _attr: { "w:val": "FootnoteReference" } } }] },
{ "w:footnoteReference": { _attr: { "w:id": 1 } } },
],
},
],
});
});
});
});

View File

@ -1,12 +1,8 @@
import { IRunOptions, Run } from "./run";
import { Text } from "./run-components/text";
export interface ITextRunOptions extends IRunOptions {
readonly text: string;
}
export class TextRun extends Run {
constructor(options: ITextRunOptions | string) {
constructor(options: IRunOptions | string) {
if (typeof options === "string") {
super({});
this.root.push(new Text(options));
@ -14,6 +10,5 @@ export class TextRun extends Run {
}
super(options);
this.root.push(new Text(options.text));
}
}

View File

@ -16,7 +16,7 @@ export class Relationships extends XmlComponent {
this.root.push(relationship);
}
public createRelationship(id: number, type: RelationshipType, target: string, targetMode?: TargetModeType): Relationship {
public createRelationship(id: number | string, type: RelationshipType, target: string, targetMode?: TargetModeType): Relationship {
const relationship = new Relationship(`rId${id}`, type, target, targetMode);
this.addRelationship(relationship);

View File

@ -0,0 +1,40 @@
import { AlignmentType, IIndentAttributesProperties, ISpacingProperties, UnderlineType } from "../paragraph";
import { ShadingType } from "../table";
export interface IRunStyleOptions {
readonly size?: number;
readonly bold?: boolean;
readonly italics?: boolean;
readonly smallCaps?: boolean;
readonly allCaps?: boolean;
readonly strike?: boolean;
readonly doubleStrike?: boolean;
readonly subScript?: boolean;
readonly superScript?: boolean;
readonly underline?: {
readonly type?: UnderlineType;
readonly color?: string;
};
readonly color?: string;
readonly font?: string;
readonly characterSpacing?: number;
readonly highlight?: string;
readonly shadow?: {
readonly type: ShadingType;
readonly fill: string;
readonly color: string;
};
}
export interface IParagraphStyleOptions2 {
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;
}

View File

@ -220,6 +220,32 @@ describe("ParagraphStyle", () => {
});
});
it("#contextualSpacing", () => {
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
contextualSpacing: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } },
{
"w:pPr": [
{
"w:contextualSpacing": {
_attr: {
"w:val": 1,
},
},
},
],
},
],
});
});
it("#leftTabStop", () => {
const style = new ParagraphStyle({
id: "myStyleId",

View File

@ -1,8 +1,7 @@
import {
Alignment,
AlignmentType,
ContextualSpacing,
Indent,
ISpacingProperties,
KeepLines,
KeepNext,
OutlineLevel,
@ -10,12 +9,11 @@ import {
Spacing,
ThematicBreak,
} from "file/paragraph";
import { IIndentAttributesProperties, TabStop, TabStopType } from "file/paragraph/formatting";
import { TabStop, TabStopType } from "file/paragraph/formatting";
import * as formatting from "file/paragraph/run/formatting";
import { RunProperties } from "file/paragraph/run/properties";
import { UnderlineType } from "file/paragraph/run/underline";
import { ShadingType } from "file/table";
import { IParagraphStyleOptions2, IRunStyleOptions } from "../style-options";
import { BasedOn, Link, Next, QuickFormat, SemiHidden, UiPriority, UnhideWhenUsed } from "./components";
import { Style } from "./style";
@ -27,41 +25,8 @@ export interface IBaseParagraphStyleOptions {
readonly semiHidden?: boolean;
readonly uiPriority?: number;
readonly unhideWhenUsed?: boolean;
readonly run?: {
readonly size?: number;
readonly bold?: boolean;
readonly italics?: boolean;
readonly smallCaps?: boolean;
readonly allCaps?: boolean;
readonly strike?: boolean;
readonly doubleStrike?: boolean;
readonly subScript?: boolean;
readonly superScript?: boolean;
readonly underline?: {
readonly type?: UnderlineType;
readonly color?: string;
};
readonly color?: string;
readonly font?: string;
readonly characterSpacing?: number;
readonly highlight?: string;
readonly shadow?: {
readonly type: ShadingType;
readonly fill: string;
readonly color: string;
};
};
readonly paragraph?: {
readonly alignment?: AlignmentType;
readonly thematicBreak?: boolean;
readonly rightTabStop?: number;
readonly leftTabStop?: number;
readonly indent?: IIndentAttributesProperties;
readonly spacing?: ISpacingProperties;
readonly keepNext?: boolean;
readonly keepLines?: boolean;
readonly outlineLevel?: number;
};
readonly run?: IRunStyleOptions;
readonly paragraph?: IParagraphStyleOptions2;
}
export interface IParagraphStyleOptions extends IBaseParagraphStyleOptions {
@ -179,6 +144,10 @@ export class ParagraphStyle extends Style {
this.paragraphProperties.push(new ThematicBreak());
}
if (options.paragraph.contextualSpacing) {
this.paragraphProperties.push(new ContextualSpacing(options.paragraph.contextualSpacing));
}
if (options.paragraph.rightTabStop) {
this.paragraphProperties.push(new TabStop(TabStopType.RIGHT, options.paragraph.rightTabStop));
}

View File

@ -395,6 +395,33 @@ describe("TableCell", () => {
});
});
it("should create with width", () => {
const cell = new TableCell({
children: [],
width: { size: 100, type: WidthType.DXA },
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:tcW": {
_attr: {
"w:type": "dxa",
"w:w": 100,
},
},
},
],
},
{
"w:p": {},
},
],
});
});
it("should create with column span", () => {
const cell = new TableCell({
children: [],

View File

@ -3,10 +3,11 @@ import { Paragraph } from "file/paragraph";
import { BorderStyle } from "file/styles";
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { File } from "../../file";
import { ITableShadingAttributesProperties } from "../shading";
import { Table } from "../table";
import { ITableCellMarginOptions } from "./cell-margin/table-cell-margins";
import { VerticalAlign, VerticalMergeType } from "./table-cell-components";
import { VerticalAlign, VerticalMergeType, WidthType } from "./table-cell-components";
import { TableCellProperties } from "./table-cell-properties";
export interface ITableCellOptions {
@ -14,6 +15,10 @@ export interface ITableCellOptions {
readonly margins?: ITableCellMarginOptions;
readonly verticalAlign?: VerticalAlign;
readonly verticalMerge?: VerticalMergeType;
readonly width?: {
readonly size: number | string;
readonly type?: WidthType;
};
readonly columnSpan?: number;
readonly rowSpan?: number;
readonly borders?: {
@ -78,6 +83,10 @@ export class TableCell extends XmlComponent {
this.properties.addVerticalMerge(VerticalMergeType.RESTART);
}
if (options.width) {
this.properties.setWidth(options.width.size, options.width.type);
}
if (options.borders) {
if (options.borders.top) {
this.properties.Borders.addTopBorder(options.borders.top.style, options.borders.top.size, options.borders.top.color);
@ -102,11 +111,11 @@ export class TableCell extends XmlComponent {
}
}
public prepForXml(): IXmlableObject | undefined {
public prepForXml(file?: File): IXmlableObject | undefined {
// Cells must end with a paragraph
if (!(this.root[this.root.length - 1] instanceof Paragraph)) {
this.root.push(new Paragraph({}));
}
return super.prepForXml();
return super.prepForXml(file);
}
}

View File

@ -1,3 +1,5 @@
export * from "./table-properties";
export * from "./table-float-properties";
export * from "./table-layout";
export * from "./table-borders";
export * from "./table-overlap";

View File

@ -0,0 +1,550 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { BorderStyle } from "file/styles";
import { TableBorders } from "./table-borders";
describe("TableBorders", () => {
describe("#constructor", () => {
describe("default borders", () => {
it("should add a table cell top border using default width type", () => {
const tableBorders = new TableBorders({});
const tree = new Formatter().format(tableBorders);
expect(tree).to.deep.equal({
"w:tblBorders": [
{
"w:top": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:left": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:right": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideH": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideV": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
],
});
});
});
describe("top border", () => {
it("should add a table cell top border", () => {
const tableBorders = new TableBorders({
top: {
style: BorderStyle.DOUBLE,
size: 1,
color: "red",
},
});
const tree = new Formatter().format(tableBorders);
expect(tree).to.deep.equal({
"w:tblBorders": [
{
"w:top": {
_attr: {
"w:color": "red",
"w:space": 0,
"w:sz": 1,
"w:val": "double",
},
},
},
{
"w:left": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:right": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideH": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideV": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
],
});
});
});
describe("left border", () => {
it("should add a table cell left border", () => {
const tableBorders = new TableBorders({
left: {
style: BorderStyle.DOUBLE,
size: 1,
color: "red",
},
});
const tree = new Formatter().format(tableBorders);
expect(tree).to.deep.equal({
"w:tblBorders": [
{
"w:top": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:left": {
_attr: {
"w:color": "red",
"w:space": 0,
"w:sz": 1,
"w:val": "double",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:right": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideH": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideV": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
],
});
});
});
describe("bottom border", () => {
it("should add a table cell bottom border", () => {
const tableBorders = new TableBorders({
bottom: {
style: BorderStyle.DOUBLE,
size: 1,
color: "red",
},
});
const tree = new Formatter().format(tableBorders);
expect(tree).to.deep.equal({
"w:tblBorders": [
{
"w:top": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:left": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "red",
"w:space": 0,
"w:sz": 1,
"w:val": "double",
},
},
},
{
"w:right": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideH": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideV": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
],
});
});
});
describe("right border", () => {
it("should add a table cell right border", () => {
const tableBorders = new TableBorders({
right: {
style: BorderStyle.DOUBLE,
size: 1,
color: "red",
},
});
const tree = new Formatter().format(tableBorders);
expect(tree).to.deep.equal({
"w:tblBorders": [
{
"w:top": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:left": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:right": {
_attr: {
"w:color": "red",
"w:space": 0,
"w:sz": 1,
"w:val": "double",
},
},
},
{
"w:insideH": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideV": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
],
});
});
});
describe("inside horizontal border", () => {
it("should add a table cell inside horizontal border", () => {
const tableBorders = new TableBorders({
insideHorizontal: {
style: BorderStyle.DOUBLE,
size: 1,
color: "red",
},
});
const tree = new Formatter().format(tableBorders);
expect(tree).to.deep.equal({
"w:tblBorders": [
{
"w:top": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:left": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:right": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideH": {
_attr: {
"w:color": "red",
"w:space": 0,
"w:sz": 1,
"w:val": "double",
},
},
},
{
"w:insideV": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
],
});
});
});
describe("inside vertical border", () => {
it("should add a table cell inside horizontal border", () => {
const tableBorders = new TableBorders({
insideVertical: {
style: BorderStyle.DOUBLE,
size: 1,
color: "red",
},
});
const tree = new Formatter().format(tableBorders);
expect(tree).to.deep.equal({
"w:tblBorders": [
{
"w:top": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:left": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:right": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideH": {
_attr: {
"w:color": "auto",
"w:space": 0,
"w:sz": 4,
"w:val": "single",
},
},
},
{
"w:insideV": {
_attr: {
"w:color": "red",
"w:space": 0,
"w:sz": 1,
"w:val": "double",
},
},
},
],
});
});
});
});
});

View File

@ -1,14 +1,95 @@
// http://officeopenxml.com/WPtableBorders.php
import { BorderStyle } from "file/styles";
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export interface ITableBordersOptions {
readonly top?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
readonly bottom?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
readonly left?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
readonly right?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
readonly insideHorizontal?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
readonly insideVertical?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
}
export class TableBorders extends XmlComponent {
constructor() {
constructor(options: ITableBordersOptions) {
super("w:tblBorders");
this.root.push(new TableBordersElement("w:top", "single", 4, 0, "auto"));
this.root.push(new TableBordersElement("w:left", "single", 4, 0, "auto"));
this.root.push(new TableBordersElement("w:bottom", "single", 4, 0, "auto"));
this.root.push(new TableBordersElement("w:right", "single", 4, 0, "auto"));
this.root.push(new TableBordersElement("w:insideH", "single", 4, 0, "auto"));
this.root.push(new TableBordersElement("w:insideV", "single", 4, 0, "auto"));
if (options.top) {
this.root.push(new TableBordersElement("w:top", options.top.style, options.top.size, 0, options.top.color));
} else {
this.root.push(new TableBordersElement("w:top", BorderStyle.SINGLE, 4, 0, "auto"));
}
if (options.left) {
this.root.push(new TableBordersElement("w:left", options.left.style, options.left.size, 0, options.left.color));
} else {
this.root.push(new TableBordersElement("w:left", BorderStyle.SINGLE, 4, 0, "auto"));
}
if (options.bottom) {
this.root.push(new TableBordersElement("w:bottom", options.bottom.style, options.bottom.size, 0, options.bottom.color));
} else {
this.root.push(new TableBordersElement("w:bottom", BorderStyle.SINGLE, 4, 0, "auto"));
}
if (options.right) {
this.root.push(new TableBordersElement("w:right", options.right.style, options.right.size, 0, options.right.color));
} else {
this.root.push(new TableBordersElement("w:right", BorderStyle.SINGLE, 4, 0, "auto"));
}
if (options.insideHorizontal) {
this.root.push(
new TableBordersElement(
"w:insideH",
options.insideHorizontal.style,
options.insideHorizontal.size,
0,
options.insideHorizontal.color,
),
);
} else {
this.root.push(new TableBordersElement("w:insideH", BorderStyle.SINGLE, 4, 0, "auto"));
}
if (options.insideVertical) {
this.root.push(
new TableBordersElement(
"w:insideV",
options.insideVertical.style,
options.insideVertical.size,
0,
options.insideVertical.color,
),
);
} else {
this.root.push(new TableBordersElement("w:insideV", BorderStyle.SINGLE, 4, 0, "auto"));
}
}
}

View File

@ -3,11 +3,12 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { RelativeHorizontalPosition, RelativeVerticalPosition, TableAnchorType, TableFloatProperties } from "./table-float-properties";
import { OverlapType } from "./table-overlap";
describe("Table Float Properties", () => {
describe("#constructor", () => {
it("should construct a TableFloatProperties with all options", () => {
const tfp = new TableFloatProperties({
const properties = new TableFloatProperties({
horizontalAnchor: TableAnchorType.MARGIN,
verticalAnchor: TableAnchorType.PAGE,
absoluteHorizontalPosition: 10,
@ -19,8 +20,32 @@ describe("Table Float Properties", () => {
leftFromText: 50,
rightFromText: 60,
});
const tree = new Formatter().format(tfp);
expect(tree).to.be.deep.equal(DEFAULT_TFP);
const tree = new Formatter().format(properties);
expect(tree).to.deep.equal(DEFAULT_TFP);
});
it("should add overlap", () => {
const properties = new TableFloatProperties({
overlap: OverlapType.NEVER,
});
const tree = new Formatter().format(properties);
expect(tree).to.deep.equal({
"w:tblpPr": [
{
_attr: {
overlap: "never",
},
},
{
"w:tblOverlap": {
_attr: {
"w:val": "never",
},
},
},
],
});
});
});
});

View File

@ -1,5 +1,7 @@
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { OverlapType, TableOverlap } from "./table-overlap";
export enum TableAnchorType {
MARGIN = "margin",
PAGE = "page",
@ -109,6 +111,7 @@ export interface ITableFloatOptions {
* to the right of the table. The value is in twentieths of a point. If omitted, the value is assumed to be zero.
*/
readonly rightFromText?: number;
readonly overlap?: OverlapType;
}
export class TableFloatOptionsAttributes extends XmlAttributeComponent<ITableFloatOptions> {
@ -130,5 +133,9 @@ export class TableFloatProperties extends XmlComponent {
constructor(options: ITableFloatOptions) {
super("w:tblpPr");
this.root.push(new TableFloatOptionsAttributes(options));
if (options.overlap) {
this.root.push(new TableOverlap(options.overlap));
}
}
}

View File

@ -0,0 +1,22 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { OverlapType, TableOverlap } from "./table-overlap";
describe("TableOverlap", () => {
describe("#constructor", () => {
it("sets the width attribute to the value given", () => {
const tableOverlap = new TableOverlap(OverlapType.OVERLAP);
const tree = new Formatter().format(tableOverlap);
expect(tree).to.deep.equal({
"w:tblOverlap": {
_attr: {
"w:val": "overlap",
},
},
});
});
});
});

View File

@ -0,0 +1,17 @@
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export enum OverlapType {
NEVER = "never",
OVERLAP = "overlap",
}
class TableOverlapAttributes extends XmlAttributeComponent<{ readonly val: OverlapType }> {
protected readonly xmlKeys = { val: "w:val" };
}
export class TableOverlap extends XmlComponent {
constructor(type: OverlapType) {
super("w:tblOverlap");
this.root.push(new TableOverlapAttributes({ val: type }));
}
}

View File

@ -2,6 +2,7 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { AlignmentType } from "../../paragraph";
import { ShadingType } from "../shading";
import { WidthType } from "../table-cell";
import { TableLayoutType } from "./table-layout";
@ -92,4 +93,23 @@ describe("TableProperties", () => {
});
});
});
describe("#setAlignment", () => {
it("sets the shading of the table", () => {
const tp = new TableProperties();
tp.setAlignment(AlignmentType.CENTER);
const tree = new Formatter().format(tp);
expect(tree).to.deep.equal({
"w:tblPr": [
{
"w:jc": {
_attr: {
"w:val": "center",
},
},
},
],
});
});
});
});

View File

@ -1,8 +1,10 @@
// http://officeopenxml.com/WPtableProperties.php
import { IgnoreIfEmptyXmlComponent } from "file/xml-components";
import { Alignment, AlignmentType } from "../../paragraph";
import { ITableShadingAttributesProperties, TableShading } from "../shading";
import { WidthType } from "../table-cell";
import { TableBorders } from "./table-borders";
import { ITableBordersOptions, TableBorders } from "./table-borders";
import { TableCellMargin } from "./table-cell-margin";
import { ITableFloatOptions, TableFloatProperties } from "./table-float-properties";
import { TableLayout, TableLayoutType } from "./table-layout";
@ -27,8 +29,8 @@ export class TableProperties extends IgnoreIfEmptyXmlComponent {
this.root.push(new TableLayout(type));
}
public setBorder(): TableProperties {
this.root.push(new TableBorders());
public setBorder(borderOptions: ITableBordersOptions): TableProperties {
this.root.push(new TableBorders(borderOptions));
return this;
}
@ -46,4 +48,8 @@ export class TableProperties extends IgnoreIfEmptyXmlComponent {
return this;
}
public setAlignment(type: AlignmentType): void {
this.root.push(new Alignment(type));
}
}

View File

@ -3,7 +3,7 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { Paragraph } from "../paragraph";
import { AlignmentType, Paragraph } from "../paragraph";
import { Table } from "./table";
// import { WidthType } from "./table-cell";
import { RelativeHorizontalPosition, RelativeVerticalPosition, TableAnchorType } from "./table-properties";
@ -211,6 +211,29 @@ describe("Table", () => {
});
});
it("should center the table", () => {
const table = new Table({
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
alignment: AlignmentType.CENTER,
});
const tree = new Formatter().format(table);
expect(tree)
.to.have.property("w:tbl")
.which.is.an("array")
.with.has.length.at.least(1);
expect(tree["w:tbl"][0]).to.deep.equal({
"w:tblPr": [DEFAULT_TABLE_PROPERTIES, BORDERS, WIDTHS, { "w:jc": { _attr: { "w:val": "center" } } }],
});
});
it("should set the table to provided width", () => {
const table = new Table({
rows: [

View File

@ -1,8 +1,10 @@
// http://officeopenxml.com/WPtableGrid.php
import { XmlComponent } from "file/xml-components";
import { AlignmentType } from "../paragraph";
import { TableGrid } from "./grid";
import { TableCell, VerticalMergeType, WidthType } from "./table-cell";
import { ITableFloatOptions, TableProperties } from "./table-properties";
import { ITableBordersOptions, ITableFloatOptions, TableProperties } from "./table-properties";
import { TableLayoutType } from "./table-properties/table-layout";
import { TableRow } from "./table-row";
@ -32,6 +34,8 @@ export interface ITableOptions {
};
readonly float?: ITableFloatOptions;
readonly layout?: TableLayoutType;
readonly borders?: ITableBordersOptions;
readonly alignment?: AlignmentType;
}
export class Table extends XmlComponent {
@ -44,11 +48,18 @@ export class Table extends XmlComponent {
margins: { marginUnitType, top, bottom, right, left } = { marginUnitType: WidthType.AUTO, top: 0, bottom: 0, right: 0, left: 0 },
float,
layout,
borders,
alignment,
}: ITableOptions) {
super("w:tbl");
this.properties = new TableProperties();
this.root.push(this.properties);
this.properties.setBorder();
if (borders) {
this.properties.setBorder(borders);
} else {
this.properties.setBorder({});
}
if (width) {
this.properties.setWidth(width.size, width.type);
@ -96,5 +107,9 @@ export class Table extends XmlComponent {
if (layout) {
this.properties.setLayout(layout);
}
if (alignment) {
this.properties.setAlignment(alignment);
}
}
}

View File

@ -1,3 +1,4 @@
import { File } from "../file";
import { IXmlableObject } from "./xmlable-object";
export abstract class BaseXmlComponent {
@ -9,7 +10,7 @@ export abstract class BaseXmlComponent {
this.rootKey = rootKey;
}
public abstract prepForXml(): IXmlableObject | undefined;
public abstract prepForXml(file?: File): IXmlableObject | undefined;
public get IsDeleted(): boolean {
return this.deleted;

View File

@ -4,3 +4,4 @@ export * from "./default-attributes";
export * from "./imported-xml-component";
export * from "./xmlable-object";
export * from "./initializable-xml-component";
export * from "./base";

View File

@ -1,19 +1,19 @@
import { File } from "../file";
import { BaseXmlComponent } from "./base";
import { IXmlableObject } from "./xmlable-object";
export { BaseXmlComponent };
export const EMPTY_OBJECT = Object.seal({});
export abstract class XmlComponent extends BaseXmlComponent {
// tslint:disable-next-line:readonly-keyword
protected root: Array<BaseXmlComponent | string>;
// tslint:disable-next-line:readonly-keyword no-any
protected root: Array<BaseXmlComponent | string | any>;
constructor(rootKey: string) {
super(rootKey);
this.root = new Array<BaseXmlComponent | string>();
}
public prepForXml(): IXmlableObject | undefined {
public prepForXml(file?: File): IXmlableObject | undefined {
const children = this.root
.filter((c) => {
if (c instanceof BaseXmlComponent) {
@ -23,7 +23,7 @@ export abstract class XmlComponent extends BaseXmlComponent {
})
.map((comp) => {
if (comp instanceof BaseXmlComponent) {
return comp.prepForXml();
return comp.prepForXml(file);
}
return comp;
})

View File

@ -9,6 +9,7 @@ module.exports = {
path: path.resolve("build"),
filename: "index.js",
libraryTarget: "umd",
library: "docx",
},
resolve: {
@ -24,15 +25,16 @@ module.exports = {
},
// For coverage testing
...(process.env.NODE_ENV !== "production"
? [{
? [
{
test: /\.(ts)/,
include: path.resolve("src"),
loader: "istanbul-instrumenter-loader",
enforce: "post",
exclude: [/node_modules/],
}]
: []
)
},
]
: []),
],
},