Compare commits
76 Commits
Author | SHA1 | Date | |
---|---|---|---|
b59fa6bb16 | |||
c0c62001fe | |||
e77a7dfdcd | |||
a0437381e7 | |||
bb686bbcbe | |||
c898d0a3c3 | |||
704c678333 | |||
c59c5350fd | |||
accb1d44d0 | |||
b61f798cd9 | |||
be2ec9d4cf | |||
514fbb1c09 | |||
8ce397f61e | |||
3b0dfb8489 | |||
f4933deaf2 | |||
add2273551 | |||
91b00466a0 | |||
d0612e115e | |||
e4a6cbe887 | |||
0386810714 | |||
d4585f60b3 | |||
18f015a959 | |||
10c866bdea | |||
680c938dcf | |||
c3a617fefd | |||
6362b498a6 | |||
2e30ab29be | |||
44568023cd | |||
c57aaa856d | |||
0f4f24c3f3 | |||
2fe7a2e3d3 | |||
20b62f5768 | |||
ab063eee08 | |||
66c7624b40 | |||
bfc302023f | |||
3505912f74 | |||
ec379f2e4d | |||
2d17954104 | |||
01c834b16b | |||
2aeb2c2e88 | |||
be88086434 | |||
28f0cc681d | |||
def29c535b | |||
168e5e6e11 | |||
ce71f19826 | |||
4fd3f3a6b8 | |||
39763d3869 | |||
36f90edc67 | |||
b0cbd6f609 | |||
d23d4a4557 | |||
07c58ee43c | |||
1037e8f1b9 | |||
f0ae8396f7 | |||
1ae5995f9c | |||
751ad9d304 | |||
0bd1514826 | |||
16bfc78d8d | |||
528008406a | |||
9e3010ac6a | |||
7c1fa6f4bf | |||
f58d31cc3d | |||
286c742ece | |||
3edc51d5a0 | |||
05d4c9688e | |||
d4401d1597 | |||
90358e73f6 | |||
a77ca02ea9 | |||
7785f0df02 | |||
0e6ed6e5ea | |||
70e84c8d38 | |||
11c3886659 | |||
6104e5bc35 | |||
c748b9a7fc | |||
714dbc0179 | |||
7f16cfc359 | |||
71953cf45a |
61
.cspell.json
61
.cspell.json
@ -4,34 +4,40 @@
|
||||
"version": "0.2",
|
||||
// language - current active spelling language
|
||||
"language": "en_US",
|
||||
"dictionaries": ["en_US", "typescript", "softwareTerms", "fonts", "npm"],
|
||||
"dictionaries": [
|
||||
"en_US",
|
||||
"typescript",
|
||||
"softwareTerms",
|
||||
"fonts",
|
||||
"npm"
|
||||
],
|
||||
// words - list of words to be always considered correct
|
||||
"words": [
|
||||
"Xmlable",
|
||||
"Abjad",
|
||||
"aiueo",
|
||||
"ATLEAST",
|
||||
"chosung",
|
||||
"clippy",
|
||||
"datas",
|
||||
"docsify",
|
||||
"dolan",
|
||||
"falsey",
|
||||
"Initializable",
|
||||
"iroha",
|
||||
"jsonify",
|
||||
"jszip",
|
||||
"NUMPAGES",
|
||||
"odttf",
|
||||
"ooxml",
|
||||
"panose",
|
||||
"rels",
|
||||
"rsid",
|
||||
"twip",
|
||||
"twips",
|
||||
"jsonify",
|
||||
"falsey",
|
||||
"aiueo",
|
||||
"iroha",
|
||||
"aiueo",
|
||||
"iroha",
|
||||
"chosung",
|
||||
"Abjad",
|
||||
"Initializable",
|
||||
"rels",
|
||||
"dolan",
|
||||
"xmlify",
|
||||
"Xmlifyed",
|
||||
"Xmlable",
|
||||
"xmlified",
|
||||
"datas",
|
||||
"jszip",
|
||||
"rsid",
|
||||
"NUMPAGES",
|
||||
"ATLEAST",
|
||||
"ooxml",
|
||||
"clippy",
|
||||
"docsify"
|
||||
"xmlify",
|
||||
"Xmlifyed"
|
||||
],
|
||||
"ignoreRegExpList": [
|
||||
"/\"w:.+\"/",
|
||||
@ -51,10 +57,15 @@
|
||||
"/<element name=\"[a-z]+\"/gi",
|
||||
"/<attribute name=\"[a-z]+\"/gi"
|
||||
],
|
||||
"ignorePaths": ["package.json", "docs/api"],
|
||||
"ignorePaths": [
|
||||
"package.json",
|
||||
"docs/api"
|
||||
],
|
||||
"allowCompoundWords": true,
|
||||
// flagWords - list of words to be always considered incorrect
|
||||
// This is useful for offensive words and common spelling errors.
|
||||
// For example "hte" should be "the"
|
||||
"flagWords": ["hte"]
|
||||
"flagWords": [
|
||||
"hte"
|
||||
]
|
||||
}
|
||||
|
2
.nycrc
2
.nycrc
@ -1,7 +1,7 @@
|
||||
{
|
||||
"check-coverage": true,
|
||||
"statements": 99.87,
|
||||
"branches": 98.29,
|
||||
"branches": 98.21,
|
||||
"functions": 100,
|
||||
"lines": 99.86,
|
||||
"include": [
|
||||
|
@ -25,6 +25,17 @@ const doc = new Document({
|
||||
},
|
||||
},
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "",
|
||||
border: {
|
||||
top: {
|
||||
color: "auto",
|
||||
space: 1,
|
||||
style: BorderStyle.SINGLE,
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
|
@ -32,6 +32,55 @@ const doc = new Document({
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
author: "Bob Ross",
|
||||
date: new Date(),
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "Some initial text content",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "comment text content",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: "John Doe",
|
||||
date: new Date(),
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "Hello World",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: "Beatriz",
|
||||
date: new Date(),
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "Another reply",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [
|
||||
@ -53,6 +102,32 @@ const doc = new Document({
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new CommentRangeStart(1),
|
||||
new CommentRangeStart(2),
|
||||
new CommentRangeStart(3),
|
||||
new TextRun({
|
||||
text: "Some text which need commenting",
|
||||
bold: true,
|
||||
}),
|
||||
new CommentRangeEnd(1),
|
||||
new TextRun({
|
||||
children: [new CommentReference(1)],
|
||||
bold: true,
|
||||
}),
|
||||
new CommentRangeEnd(2),
|
||||
new TextRun({
|
||||
children: [new CommentReference(2)],
|
||||
bold: true,
|
||||
}),
|
||||
new CommentRangeEnd(3),
|
||||
new TextRun({
|
||||
children: [new CommentReference(3)],
|
||||
bold: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
15
demo/87-template-document.ts
Normal file
15
demo/87-template-document.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Patch a document with patches
|
||||
// Import from 'docx' rather than '../build' if you install from npm
|
||||
import * as fs from "fs";
|
||||
import { patchDocument, PatchType, TextRun } from "../build";
|
||||
|
||||
patchDocument(fs.readFileSync("demo/assets/simple-template-2.docx"), {
|
||||
patches: {
|
||||
name: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [new TextRun("Max")],
|
||||
},
|
||||
},
|
||||
}).then((doc) => {
|
||||
fs.writeFileSync("My Document.docx", doc);
|
||||
});
|
31
demo/88-template-document.ts
Normal file
31
demo/88-template-document.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// Patch a document with patches
|
||||
// Import from 'docx' rather than '../build' if you install from npm
|
||||
import * as fs from "fs";
|
||||
import { IPatch, patchDocument, PatchType, TextRun } from "../build";
|
||||
|
||||
export const font = "Trebuchet MS";
|
||||
export const getPatches = (fields: { [key: string]: string }) => {
|
||||
const patches: { [key: string]: IPatch } = {};
|
||||
|
||||
for (const field in fields) {
|
||||
patches[field] = {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [new TextRun({ text: fields[field], font })],
|
||||
};
|
||||
}
|
||||
|
||||
return patches;
|
||||
};
|
||||
|
||||
const patches = getPatches({
|
||||
name: "Mr",
|
||||
table_heading_1: "John",
|
||||
item_1: "Doe",
|
||||
paragraph_replace: "Lorem ipsum paragraph",
|
||||
});
|
||||
|
||||
patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
|
||||
patches,
|
||||
}).then((doc) => {
|
||||
fs.writeFileSync("My Document.docx", doc);
|
||||
});
|
29
demo/89-template-document.ts
Normal file
29
demo/89-template-document.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Patch a document with patches
|
||||
// Import from 'docx' rather than '../build' if you install from npm
|
||||
import * as fs from "fs";
|
||||
import { IPatch, patchDocument, PatchType, TextRun } from "../build";
|
||||
|
||||
export const font = "Trebuchet MS";
|
||||
export const getPatches = (fields: { [key: string]: string }) => {
|
||||
const patches: { [key: string]: IPatch } = {};
|
||||
|
||||
for (const field in fields) {
|
||||
patches[field] = {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [new TextRun({ text: fields[field], font })],
|
||||
};
|
||||
}
|
||||
|
||||
return patches;
|
||||
};
|
||||
|
||||
const patches = getPatches({
|
||||
salutation: "Mr.",
|
||||
"first-name": "John",
|
||||
});
|
||||
|
||||
patchDocument(fs.readFileSync("demo/assets/simple-template-3.docx"), {
|
||||
patches,
|
||||
}).then((doc) => {
|
||||
fs.writeFileSync("My Document.docx", doc);
|
||||
});
|
BIN
demo/assets/simple-template-2.docx
Normal file
BIN
demo/assets/simple-template-2.docx
Normal file
Binary file not shown.
BIN
demo/assets/simple-template-3.docx
Normal file
BIN
demo/assets/simple-template-3.docx
Normal file
Binary file not shown.
@ -38,6 +38,7 @@
|
||||
<script src="https://unpkg.com/docsify-copy-code@2"></script>
|
||||
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
|
||||
<script src="//unpkg.com/prismjs/components/prism-typescript.min.js"></script>
|
||||
<script src="https://unpkg.com/docsify-sign-off-sheet@1.0.0/dist/index.iife.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-darklight-theme@latest/dist/index.min.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -305,4 +305,12 @@ Example: https://github.com/dolanmiu/docx/blob/master/demo/15-page-break-before.
|
||||
|
||||
## Page break control
|
||||
|
||||
Paragraphs have `.keepLines()` and `.keepNext()` methods that allow restricting page breaks within and between paragraphs. See [this Microsoft article](https://support.office.com/en-us/article/Keep-lines-and-paragraphs-together-d72af534-926f-4c4b-830a-abfc2daa3bfa) for more details)
|
||||
Paragraphs have `keepLines` and `keepNext` properties that allow restricting page breaks within and between paragraphs. See [this Microsoft article](https://support.office.com/en-us/article/Keep-lines-and-paragraphs-together-d72af534-926f-4c4b-830a-abfc2daa3bfa) for more details.
|
||||
|
||||
```ts
|
||||
const paragraph = new Paragraph({
|
||||
text: "Stay on the same page",
|
||||
keepLines: true,
|
||||
keepNext: true,
|
||||
});
|
||||
```
|
||||
|
935
package-lock.json
generated
935
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docx",
|
||||
"version": "8.0.0",
|
||||
"version": "8.0.4",
|
||||
"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": {
|
||||
@ -28,9 +28,7 @@
|
||||
"lint"
|
||||
],
|
||||
"files": [
|
||||
"src",
|
||||
"build",
|
||||
"template"
|
||||
"build"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -61,15 +59,14 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/dolanmiu/docx/issues"
|
||||
},
|
||||
"homepage": "https://github.com/dolanmiu/docx#readme",
|
||||
"homepage": "https://docx.js.org",
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.15",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/glob": "^8.0.0",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/prompt": "^1.1.1",
|
||||
"@types/request-promise": "^4.1.42",
|
||||
"@types/shelljs": "^0.8.9",
|
||||
"@types/shelljs": "^0.8.11",
|
||||
"@types/sinon": "^10.0.0",
|
||||
"@types/unzipper": "^0.10.4",
|
||||
"@types/webpack": "^5.0.0",
|
||||
@ -107,7 +104,7 @@
|
||||
"tsconfig-paths": "^4.0.0",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typedoc": "^0.23.2",
|
||||
"typescript": "5.0.2",
|
||||
"typescript": "5.0.3",
|
||||
"unzipper": "^0.10.11",
|
||||
"webpack": "^5.28.0",
|
||||
"webpack-cli": "^5.0.0"
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { assert, expect } from "chai";
|
||||
import { SinonStub, stub } from "sinon";
|
||||
|
||||
import { Formatter } from "@export/formatter";
|
||||
import * as convenienceFunctions from "@util/convenience-functions";
|
||||
|
||||
import { Utility } from "tests/utility";
|
||||
|
||||
@ -40,14 +38,6 @@ const createAnchor = (drawingOptions: IDrawingOptions): Anchor =>
|
||||
);
|
||||
|
||||
describe("Anchor", () => {
|
||||
before(() => {
|
||||
stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(convenienceFunctions.uniqueNumericId as SinonStub).restore();
|
||||
});
|
||||
|
||||
let anchor: Anchor;
|
||||
|
||||
describe("#constructor()", () => {
|
||||
@ -466,7 +456,7 @@ describe("Anchor", () => {
|
||||
"wp:docPr": {
|
||||
_attr: {
|
||||
descr: "test",
|
||||
id: 0,
|
||||
id: 1,
|
||||
name: "test",
|
||||
title: "test",
|
||||
},
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { IContext, IXmlableObject, NextAttributeComponent, XmlComponent } from "@file/xml-components";
|
||||
import { ConcreteHyperlink } from "@file/paragraph";
|
||||
|
||||
import { uniqueNumericId } from "@util/convenience-functions";
|
||||
import { docPropertiesUniqueNumericIdGen } from "@util/convenience-functions";
|
||||
|
||||
import { createHyperlinkClick } from "./doc-properties-children";
|
||||
|
||||
@ -25,6 +25,8 @@ export interface DocPropertiesOptions {
|
||||
}
|
||||
|
||||
export class DocProperties extends XmlComponent {
|
||||
private readonly docPropertiesUniqueNumericId = docPropertiesUniqueNumericIdGen();
|
||||
|
||||
public constructor({ name, description, title }: DocPropertiesOptions = { name: "", description: "", title: "" }) {
|
||||
super("wp:docPr");
|
||||
|
||||
@ -32,7 +34,7 @@ export class DocProperties extends XmlComponent {
|
||||
new NextAttributeComponent({
|
||||
id: {
|
||||
key: "id",
|
||||
value: uniqueNumericId(),
|
||||
value: this.docPropertiesUniqueNumericId(),
|
||||
},
|
||||
name: {
|
||||
key: "name",
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { expect } from "chai";
|
||||
import { SinonStub, stub } from "sinon";
|
||||
|
||||
import { IContext } from "@file/xml-components";
|
||||
import { Formatter } from "@export/formatter";
|
||||
import * as convenienceFunctions from "@util/convenience-functions";
|
||||
|
||||
import { ConcreteHyperlink, TextRun } from "../";
|
||||
import { Drawing, IDrawingOptions } from "./drawing";
|
||||
@ -30,14 +28,6 @@ const createDrawing = (drawingOptions?: IDrawingOptions): Drawing =>
|
||||
);
|
||||
|
||||
describe("Drawing", () => {
|
||||
before(() => {
|
||||
stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(convenienceFunctions.uniqueNumericId as SinonStub).restore();
|
||||
});
|
||||
|
||||
let currentBreak: Drawing;
|
||||
|
||||
describe("#constructor()", () => {
|
||||
@ -78,7 +68,7 @@ describe("Drawing", () => {
|
||||
"wp:docPr": {
|
||||
_attr: {
|
||||
descr: "",
|
||||
id: 0,
|
||||
id: 1,
|
||||
name: "",
|
||||
title: "",
|
||||
},
|
||||
@ -309,7 +299,7 @@ describe("Drawing", () => {
|
||||
"wp:docPr": {
|
||||
_attr: {
|
||||
descr: "",
|
||||
id: 0,
|
||||
id: 1,
|
||||
name: "",
|
||||
title: "",
|
||||
},
|
||||
@ -543,7 +533,7 @@ describe("Drawing", () => {
|
||||
{
|
||||
_attr: {
|
||||
descr: "",
|
||||
id: 0,
|
||||
id: 1,
|
||||
name: "",
|
||||
title: "",
|
||||
},
|
||||
|
@ -1,20 +1,10 @@
|
||||
import { expect } from "chai";
|
||||
import { SinonStub, stub } from "sinon";
|
||||
|
||||
import { Formatter } from "@export/formatter";
|
||||
import * as convenienceFunctions from "@util/convenience-functions";
|
||||
|
||||
import { Numbering } from "./numbering";
|
||||
|
||||
describe("Numbering", () => {
|
||||
before(() => {
|
||||
stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(convenienceFunctions.uniqueNumericId as SinonStub).restore();
|
||||
});
|
||||
|
||||
describe("#constructor", () => {
|
||||
it("creates a default numbering with one abstract and one concrete instance", () => {
|
||||
const numbering = new Numbering({
|
||||
@ -26,7 +16,7 @@ describe("Numbering", () => {
|
||||
const abstractNums = tree["w:numbering"].filter((el) => el["w:abstractNum"]);
|
||||
expect(abstractNums).to.have.lengthOf(1);
|
||||
expect(abstractNums[0]["w:abstractNum"]).to.deep.include.members([
|
||||
{ _attr: { "w:abstractNumId": 0, "w15:restartNumberingAfterBreak": 0 } },
|
||||
{ _attr: { "w:abstractNumId": 1, "w15:restartNumberingAfterBreak": 0 } },
|
||||
{ "w:multiLevelType": { _attr: { "w:val": "hybridMultilevel" } } },
|
||||
]);
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
// https://stackoverflow.com/questions/58622437/purpose-of-abstractnum-and-numberinginstance
|
||||
import { AlignmentType } from "@file/paragraph";
|
||||
import { IContext, IXmlableObject, XmlComponent } from "@file/xml-components";
|
||||
import { convertInchesToTwip, uniqueNumericId } from "@util/convenience-functions";
|
||||
import { abstractNumUniqueNumericIdGen, concreteNumUniqueNumericIdGen, convertInchesToTwip } from "@util/convenience-functions";
|
||||
|
||||
import { DocumentAttributes } from "../document/document-attributes";
|
||||
import { AbstractNumbering } from "./abstract-numbering";
|
||||
@ -30,6 +30,8 @@ export class Numbering extends XmlComponent {
|
||||
private readonly abstractNumberingMap = new Map<string, AbstractNumbering>();
|
||||
private readonly concreteNumberingMap = new Map<string, ConcreteNumbering>();
|
||||
private readonly referenceConfigMap = new Map<string, object>();
|
||||
private readonly abstractNumUniqueNumericId = abstractNumUniqueNumericIdGen();
|
||||
private readonly concreteNumUniqueNumericId = concreteNumUniqueNumericIdGen();
|
||||
|
||||
public constructor(options: INumberingOptions) {
|
||||
super("w:numbering");
|
||||
@ -55,7 +57,7 @@ export class Numbering extends XmlComponent {
|
||||
}),
|
||||
);
|
||||
|
||||
const abstractNumbering = new AbstractNumbering(uniqueNumericId(), [
|
||||
const abstractNumbering = new AbstractNumbering(this.abstractNumUniqueNumericId(), [
|
||||
{
|
||||
level: 0,
|
||||
format: LevelFormat.BULLET,
|
||||
@ -176,7 +178,7 @@ export class Numbering extends XmlComponent {
|
||||
this.abstractNumberingMap.set("default-bullet-numbering", abstractNumbering);
|
||||
|
||||
for (const con of options.config) {
|
||||
this.abstractNumberingMap.set(con.reference, new AbstractNumbering(uniqueNumericId(), con.levels));
|
||||
this.abstractNumberingMap.set(con.reference, new AbstractNumbering(this.abstractNumUniqueNumericId(), con.levels));
|
||||
this.referenceConfigMap.set(con.reference, con.levels);
|
||||
}
|
||||
}
|
||||
@ -209,7 +211,7 @@ export class Numbering extends XmlComponent {
|
||||
const firstLevelStartNumber = referenceConfigLevels && referenceConfigLevels[0].start;
|
||||
|
||||
const concreteNumberingSettings = {
|
||||
numId: uniqueNumericId(),
|
||||
numId: this.concreteNumUniqueNumericId(),
|
||||
abstractNumId: abstractNumbering.id,
|
||||
reference,
|
||||
instance,
|
||||
|
@ -1,17 +1,19 @@
|
||||
// http://officeopenxml.com/WPbookmark.php
|
||||
import { XmlComponent } from "@file/xml-components";
|
||||
import { uniqueNumericId } from "@util/convenience-functions";
|
||||
import { bookmarkUniqueNumericIdGen } from "@util/convenience-functions";
|
||||
|
||||
import { ParagraphChild } from "../paragraph";
|
||||
import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes";
|
||||
|
||||
export class Bookmark {
|
||||
private readonly bookmarkUniqueNumericId = bookmarkUniqueNumericIdGen();
|
||||
|
||||
public readonly start: BookmarkStart;
|
||||
public readonly children: readonly ParagraphChild[];
|
||||
public readonly end: BookmarkEnd;
|
||||
|
||||
public constructor(options: { readonly id: string; readonly children: readonly ParagraphChild[] }) {
|
||||
const linkId = uniqueNumericId();
|
||||
const linkId = this.bookmarkUniqueNumericId();
|
||||
|
||||
this.start = new BookmarkStart(options.id, linkId);
|
||||
this.children = options.children;
|
||||
|
@ -20,12 +20,12 @@ import { TextRun } from "./run";
|
||||
describe("Paragraph", () => {
|
||||
before(() => {
|
||||
stub(convenienceFunctions, "uniqueId").callsFake(() => "test-unique-id");
|
||||
stub(convenienceFunctions, "uniqueNumericId").callsFake(() => -101);
|
||||
stub(convenienceFunctions, "bookmarkUniqueNumericIdGen").callsFake(() => () => -101);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(convenienceFunctions.uniqueId as SinonStub).restore();
|
||||
(convenienceFunctions.uniqueNumericId as SinonStub).restore();
|
||||
(convenienceFunctions.bookmarkUniqueNumericIdGen as SinonStub).restore();
|
||||
});
|
||||
|
||||
describe("#constructor()", () => {
|
||||
|
@ -23,7 +23,13 @@ export interface ILevelParagraphStylePropertiesOptions {
|
||||
readonly leftTabStop?: number;
|
||||
readonly indent?: IIndentAttributesProperties;
|
||||
readonly spacing?: ISpacingProperties;
|
||||
/**
|
||||
* Specifies that the paragraph (or at least part of it) should be rendered on the same page as the next paragraph when possible. If multiple paragraphs are to be kept together but they exceed a page, then the set of paragraphs begin on a new page and page breaks are used thereafter as needed.
|
||||
*/
|
||||
readonly keepNext?: boolean;
|
||||
/**
|
||||
* Specifies that all lines of the paragraph are to be kept on a single page when possible.
|
||||
*/
|
||||
readonly keepLines?: boolean;
|
||||
readonly outlineLevel?: number;
|
||||
}
|
||||
|
@ -11,12 +11,10 @@ import { ImageRun } from "./image-run";
|
||||
describe("ImageRun", () => {
|
||||
before(() => {
|
||||
stub(convenienceFunctions, "uniqueId").callsFake(() => "test-unique-id");
|
||||
stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(convenienceFunctions.uniqueId as SinonStub).restore();
|
||||
(convenienceFunctions.uniqueNumericId as SinonStub).restore();
|
||||
});
|
||||
|
||||
describe("#constructor()", () => {
|
||||
@ -126,7 +124,7 @@ describe("ImageRun", () => {
|
||||
"wp:docPr": {
|
||||
_attr: {
|
||||
descr: "",
|
||||
id: 0,
|
||||
id: 1,
|
||||
name: "",
|
||||
title: "",
|
||||
},
|
||||
@ -378,7 +376,7 @@ describe("ImageRun", () => {
|
||||
"wp:docPr": {
|
||||
_attr: {
|
||||
descr: "",
|
||||
id: 0,
|
||||
id: 1,
|
||||
name: "",
|
||||
title: "",
|
||||
},
|
||||
@ -634,7 +632,7 @@ describe("ImageRun", () => {
|
||||
"wp:docPr": {
|
||||
_attr: {
|
||||
descr: "",
|
||||
id: 0,
|
||||
id: 1,
|
||||
name: "",
|
||||
title: "",
|
||||
},
|
||||
@ -893,7 +891,7 @@ describe("ImageRun", () => {
|
||||
"wp:docPr": {
|
||||
_attr: {
|
||||
descr: "",
|
||||
id: 0,
|
||||
id: 1,
|
||||
name: "",
|
||||
title: "",
|
||||
},
|
||||
|
@ -8,7 +8,6 @@ export interface IBaseCharacterStyleOptions extends IStyleOptions {
|
||||
|
||||
export interface ICharacterStyleOptions extends IBaseCharacterStyleOptions {
|
||||
readonly id: string;
|
||||
readonly name?: string;
|
||||
}
|
||||
|
||||
export class StyleForCharacter extends Style {
|
||||
|
@ -9,7 +9,6 @@ export interface IBaseParagraphStyleOptions extends IStyleOptions {
|
||||
|
||||
export interface IParagraphStyleOptions extends IBaseParagraphStyleOptions {
|
||||
readonly id: string;
|
||||
readonly name?: string;
|
||||
}
|
||||
|
||||
export class StyleForParagraph extends Style {
|
||||
|
@ -41,6 +41,13 @@ export interface IStyleAttributes {
|
||||
|
||||
export interface IStyleOptions {
|
||||
readonly name?: string;
|
||||
/**
|
||||
* Specifies the style upon which the current style is based-that is, the style from which the current style inherits. It is the mechanism for implementing style inheritance.
|
||||
* Note that if the type of the current style must match the type of the style upon which it is based or the basedOn element will be ignored.
|
||||
* However, if the current style is a numbering style, then the `basedOn` element is ignored.
|
||||
*
|
||||
* **WARNING**: You cannot set `basedOn` to be the same as `name`. This is akin to inheriting from itself. This creates a cyclic dependency and cause undesirable behavior.
|
||||
*/
|
||||
readonly basedOn?: string;
|
||||
readonly next?: string;
|
||||
readonly link?: string;
|
||||
|
@ -206,8 +206,7 @@ const MOCK_XML = `
|
||||
describe("from-docx", () => {
|
||||
describe("patchDocument", () => {
|
||||
describe("document.xml and [Content_Types].xml", () => {
|
||||
before(() => {
|
||||
sinon.createStubInstance(JSZip, {});
|
||||
beforeEach(() => {
|
||||
sinon.stub(JSZip, "loadAsync").callsFake(
|
||||
() =>
|
||||
new Promise<JSZip>((resolve) => {
|
||||
@ -220,7 +219,7 @@ describe("from-docx", () => {
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterEach(() => {
|
||||
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
||||
});
|
||||
|
||||
@ -292,8 +291,7 @@ describe("from-docx", () => {
|
||||
});
|
||||
|
||||
describe("document.xml and [Content_Types].xml with relationships", () => {
|
||||
before(() => {
|
||||
sinon.createStubInstance(JSZip, {});
|
||||
beforeEach(() => {
|
||||
sinon.stub(JSZip, "loadAsync").callsFake(
|
||||
() =>
|
||||
new Promise<JSZip>((resolve) => {
|
||||
@ -307,7 +305,7 @@ describe("from-docx", () => {
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterEach(() => {
|
||||
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
||||
});
|
||||
|
||||
@ -322,6 +320,14 @@ describe("from-docx", () => {
|
||||
data: Buffer.from(""),
|
||||
transformation: { width: 100, height: 100 },
|
||||
}),
|
||||
new ExternalHyperlink({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "Google Link",
|
||||
}),
|
||||
],
|
||||
link: "https://www.google.co.uk",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -331,8 +337,7 @@ describe("from-docx", () => {
|
||||
});
|
||||
|
||||
describe("document.xml", () => {
|
||||
before(() => {
|
||||
sinon.createStubInstance(JSZip, {});
|
||||
beforeEach(() => {
|
||||
sinon.stub(JSZip, "loadAsync").callsFake(
|
||||
() =>
|
||||
new Promise<JSZip>((resolve) => {
|
||||
@ -344,7 +349,45 @@ describe("from-docx", () => {
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterEach(() => {
|
||||
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
||||
});
|
||||
|
||||
it("should throw an error if the content types is not found", () =>
|
||||
expect(
|
||||
patchDocument(Buffer.from(""), {
|
||||
patches: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
image_test: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: Buffer.from(""),
|
||||
transformation: { width: 100, height: 100 },
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.eventually.be.rejected);
|
||||
});
|
||||
|
||||
describe("Images", () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(JSZip, "loadAsync").callsFake(
|
||||
() =>
|
||||
new Promise<JSZip>((resolve) => {
|
||||
const zip = new JSZip();
|
||||
|
||||
zip.file("word/document.xml", MOCK_XML);
|
||||
zip.file("word/document.bmp", "");
|
||||
|
||||
resolve(zip);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
||||
});
|
||||
|
||||
|
@ -68,7 +68,14 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
||||
const hyperlinkRelationshipAdditions: IHyperlinkRelationshipAddition[] = [];
|
||||
let hasMedia = false;
|
||||
|
||||
const binaryContentMap = new Map<string, Buffer>();
|
||||
|
||||
for (const [key, value] of Object.entries(zipContent.files)) {
|
||||
if (!key.endsWith(".xml") && !key.endsWith(".rels")) {
|
||||
binaryContentMap.set(key, await value.async("nodebuffer"));
|
||||
continue;
|
||||
}
|
||||
|
||||
const json = toJson(await value.async("text"));
|
||||
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
|
||||
const context: IContext = {
|
||||
@ -196,6 +203,10 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
||||
zip.file(key, output);
|
||||
}
|
||||
|
||||
for (const [key, value] of binaryContentMap) {
|
||||
zip.file(key, value);
|
||||
}
|
||||
|
||||
for (const { stream, fileName } of file.Media.Array) {
|
||||
zip.file(`word/media/${fileName}`, stream);
|
||||
}
|
||||
|
@ -86,6 +86,59 @@ describe("paragraph-split-inject", () => {
|
||||
),
|
||||
).to.throw();
|
||||
});
|
||||
|
||||
it("should continue if text run doesn't have text", () => {
|
||||
expect(() =>
|
||||
findRunElementIndexWithToken(
|
||||
{
|
||||
name: "w:p",
|
||||
type: "element",
|
||||
elements: [
|
||||
{
|
||||
name: "w:r",
|
||||
type: "element",
|
||||
elements: [
|
||||
{
|
||||
name: "w:t",
|
||||
type: "element",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"hello",
|
||||
),
|
||||
).to.throw();
|
||||
});
|
||||
|
||||
it("should continue if text run doesn't have text", () => {
|
||||
expect(() =>
|
||||
findRunElementIndexWithToken(
|
||||
{
|
||||
name: "w:p",
|
||||
type: "element",
|
||||
elements: [
|
||||
{
|
||||
name: "w:r",
|
||||
type: "element",
|
||||
elements: [
|
||||
{
|
||||
name: "w:t",
|
||||
type: "element",
|
||||
elements: [
|
||||
{
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"hello",
|
||||
),
|
||||
).to.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitRunElement", () => {
|
||||
|
@ -8,7 +8,11 @@ export const findRunElementIndexWithToken = (paragraphElement: Element, token: s
|
||||
const textElement = (element.elements ?? []).filter((e) => e.type === "element" && e.name === "w:t");
|
||||
|
||||
for (const text of textElement) {
|
||||
if ((text.elements?.[0].text as string)?.includes(token)) {
|
||||
if (!text.elements?.[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((text.elements[0].text as string)?.includes(token)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,156 @@ describe("paragraph-token-replacer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle case where it cannot find any text to replace", () => {
|
||||
const output = replaceTokenInParagraphElement({
|
||||
paragraphElement: {
|
||||
name: "w:p",
|
||||
attributes: {
|
||||
"w14:paraId": "2499FE9F",
|
||||
"w14:textId": "27B4FBC2",
|
||||
"w:rsidR": "00B51233",
|
||||
"w:rsidRDefault": "007B52ED",
|
||||
"w:rsidP": "007B52ED",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:pPr",
|
||||
elements: [{ type: "element", name: "w:pStyle", attributes: { "w:val": "Title" } }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
attributes: { "xml:space": "preserve" },
|
||||
elements: [{ type: "text", text: "Hello " }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
attributes: { "w:rsidR": "007F116B" },
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
attributes: { "xml:space": "preserve" },
|
||||
elements: [{ type: "text", text: "{{name}} " }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "World" }] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
renderedParagraph: {
|
||||
text: "Hello {{name}} World",
|
||||
runs: [
|
||||
{ text: "Hello ", parts: [{ text: "Hello ", index: 0, start: 0, end: 5 }], index: 1, start: 0, end: 5 },
|
||||
{ text: "{{name}} ", parts: [{ text: "{{name}} ", index: 0, start: 6, end: 14 }], index: 2, start: 6, end: 14 },
|
||||
{ text: "World", parts: [{ text: "World", index: 0, start: 15, end: 19 }], index: 3, start: 15, end: 19 },
|
||||
],
|
||||
index: 0,
|
||||
path: [0, 1, 0, 0],
|
||||
},
|
||||
originalText: "{{name}}",
|
||||
replacementText: "John",
|
||||
});
|
||||
|
||||
expect(output).to.deep.equal({
|
||||
attributes: {
|
||||
"w14:paraId": "2499FE9F",
|
||||
"w14:textId": "27B4FBC2",
|
||||
"w:rsidP": "007B52ED",
|
||||
"w:rsidR": "00B51233",
|
||||
"w:rsidRDefault": "007B52ED",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
elements: [
|
||||
{
|
||||
attributes: {
|
||||
"w:val": "Title",
|
||||
},
|
||||
name: "w:pStyle",
|
||||
type: "element",
|
||||
},
|
||||
],
|
||||
name: "w:pPr",
|
||||
type: "element",
|
||||
},
|
||||
{
|
||||
elements: [
|
||||
{
|
||||
attributes: {
|
||||
"xml:space": "preserve",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
text: "Hello ",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
name: "w:t",
|
||||
type: "element",
|
||||
},
|
||||
],
|
||||
name: "w:r",
|
||||
type: "element",
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
"w:rsidR": "007F116B",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
attributes: {
|
||||
"xml:space": "preserve",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
text: "John ",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
name: "w:t",
|
||||
type: "element",
|
||||
},
|
||||
],
|
||||
name: "w:r",
|
||||
type: "element",
|
||||
},
|
||||
{
|
||||
elements: [
|
||||
{
|
||||
attributes: {
|
||||
"xml:space": "preserve",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
text: "World",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
name: "w:t",
|
||||
type: "element",
|
||||
},
|
||||
],
|
||||
name: "w:r",
|
||||
type: "element",
|
||||
},
|
||||
],
|
||||
name: "w:p",
|
||||
});
|
||||
});
|
||||
|
||||
// Try to fill rest of test coverage
|
||||
// it("should replace token in paragraph", () => {
|
||||
// const output = replaceTokenInParagraphElement({
|
||||
|
@ -30,9 +30,15 @@ export const replaceTokenInParagraphElement = ({
|
||||
switch (replaceMode) {
|
||||
case ReplaceMode.START:
|
||||
if (startIndex >= start) {
|
||||
const partToReplace = run.text.substring(Math.max(startIndex, start), Math.min(endIndex, end) + 1);
|
||||
const offsetStartIndex = startIndex - start;
|
||||
const offsetEndIndex = Math.min(endIndex, end) - start;
|
||||
const partToReplace = run.text.substring(offsetStartIndex, offsetEndIndex + 1);
|
||||
// We use a token to split the text if the replacement is within the same run
|
||||
// If not, we just add text to the middle of the run later
|
||||
if (partToReplace === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstPart = text.replace(partToReplace, replacementText);
|
||||
patchTextElement(paragraphElement.elements![run.index].elements![index], firstPart);
|
||||
replaceMode = ReplaceMode.MIDDLE;
|
||||
|
@ -7,7 +7,7 @@ import { Text } from "@file/paragraph/run/run-components/text";
|
||||
const formatter = new Formatter();
|
||||
|
||||
export const toJson = (xmlData: string): Element => {
|
||||
const xmlObj = xml2js(xmlData, { compact: false }) as Element;
|
||||
const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as Element;
|
||||
return xmlObj;
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { expect } from "chai";
|
||||
|
||||
import { convertInchesToTwip, convertMillimetersToTwip, uniqueId, uniqueNumericId } from "./convenience-functions";
|
||||
import { convertInchesToTwip, convertMillimetersToTwip, uniqueId, uniqueNumericIdCreator } from "./convenience-functions";
|
||||
|
||||
describe("Utility", () => {
|
||||
describe("#convertMillimetersToTwip", () => {
|
||||
@ -17,8 +17,9 @@ describe("Utility", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#uniqueNumericId", () => {
|
||||
describe("#uniqueNumericIdCreator", () => {
|
||||
it("should generate a unique incrementing ID", () => {
|
||||
const uniqueNumericId = uniqueNumericIdCreator();
|
||||
expect(uniqueNumericId()).to.not.be.undefined;
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,25 @@
|
||||
import { nanoid } from "nanoid/non-secure";
|
||||
|
||||
let currentCount = 0;
|
||||
|
||||
// Twip - twentieths of a point
|
||||
export const convertMillimetersToTwip = (millimeters: number): number => Math.floor((millimeters / 25.4) * 72 * 20);
|
||||
|
||||
export const convertInchesToTwip = (inches: number): number => Math.floor(inches * 72 * 20);
|
||||
|
||||
export const uniqueNumericId = (): number => ++currentCount;
|
||||
export type UniqueNumericIdCreator = () => number;
|
||||
|
||||
export const uniqueNumericIdCreator = (initial = 0): UniqueNumericIdCreator => {
|
||||
let currentCount = initial;
|
||||
|
||||
return () => ++currentCount;
|
||||
};
|
||||
|
||||
export const abstractNumUniqueNumericIdGen = (): UniqueNumericIdCreator => uniqueNumericIdCreator();
|
||||
|
||||
// Setting initial to 1 as we have numId = 1 for "default-bullet-numbering"
|
||||
export const concreteNumUniqueNumericIdGen = (): UniqueNumericIdCreator => uniqueNumericIdCreator(1);
|
||||
|
||||
export const docPropertiesUniqueNumericIdGen = (): UniqueNumericIdCreator => uniqueNumericIdCreator();
|
||||
|
||||
export const bookmarkUniqueNumericIdGen = (): UniqueNumericIdCreator => uniqueNumericIdCreator();
|
||||
|
||||
export const uniqueId = (): string => nanoid().toLowerCase();
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"typedocOptions": {
|
||||
"out": "docs/api",
|
||||
"exclude": "test",
|
||||
|
Reference in New Issue
Block a user