Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
2
.nycrc
2
.nycrc
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"check-coverage": true,
|
"check-coverage": true,
|
||||||
"statements": 99.87,
|
"statements": 99.87,
|
||||||
"branches": 98.29,
|
"branches": 98.21,
|
||||||
"functions": 100,
|
"functions": 100,
|
||||||
"lines": 99.86,
|
"lines": 99.86,
|
||||||
"include": [
|
"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({
|
new Paragraph({
|
||||||
children: [
|
children: [
|
||||||
new TextRun({
|
new TextRun({
|
||||||
|
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);
|
||||||
|
});
|
BIN
demo/assets/simple-template-2.docx
Normal file
BIN
demo/assets/simple-template-2.docx
Normal file
Binary file not shown.
722
package-lock.json
generated
722
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "docx",
|
"name": "docx",
|
||||||
"version": "8.0.0",
|
"version": "8.0.1",
|
||||||
"description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.",
|
"description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.",
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -28,9 +28,7 @@
|
|||||||
"lint"
|
"lint"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"build"
|
||||||
"build",
|
|
||||||
"template"
|
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -61,15 +59,14 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/dolanmiu/docx/issues"
|
"url": "https://github.com/dolanmiu/docx/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/dolanmiu/docx#readme",
|
"homepage": "https://docx.js.org",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.15",
|
"@types/chai": "^4.2.15",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/glob": "^8.0.0",
|
|
||||||
"@types/mocha": "^10.0.0",
|
"@types/mocha": "^10.0.0",
|
||||||
"@types/prompt": "^1.1.1",
|
"@types/prompt": "^1.1.1",
|
||||||
"@types/request-promise": "^4.1.42",
|
"@types/request-promise": "^4.1.42",
|
||||||
"@types/shelljs": "^0.8.9",
|
"@types/shelljs": "^0.8.11",
|
||||||
"@types/sinon": "^10.0.0",
|
"@types/sinon": "^10.0.0",
|
||||||
"@types/unzipper": "^0.10.4",
|
"@types/unzipper": "^0.10.4",
|
||||||
"@types/webpack": "^5.0.0",
|
"@types/webpack": "^5.0.0",
|
||||||
|
@ -8,7 +8,6 @@ export interface IBaseCharacterStyleOptions extends IStyleOptions {
|
|||||||
|
|
||||||
export interface ICharacterStyleOptions extends IBaseCharacterStyleOptions {
|
export interface ICharacterStyleOptions extends IBaseCharacterStyleOptions {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly name?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StyleForCharacter extends Style {
|
export class StyleForCharacter extends Style {
|
||||||
|
@ -9,7 +9,6 @@ export interface IBaseParagraphStyleOptions extends IStyleOptions {
|
|||||||
|
|
||||||
export interface IParagraphStyleOptions extends IBaseParagraphStyleOptions {
|
export interface IParagraphStyleOptions extends IBaseParagraphStyleOptions {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly name?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StyleForParagraph extends Style {
|
export class StyleForParagraph extends Style {
|
||||||
|
@ -206,8 +206,7 @@ const MOCK_XML = `
|
|||||||
describe("from-docx", () => {
|
describe("from-docx", () => {
|
||||||
describe("patchDocument", () => {
|
describe("patchDocument", () => {
|
||||||
describe("document.xml and [Content_Types].xml", () => {
|
describe("document.xml and [Content_Types].xml", () => {
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
sinon.createStubInstance(JSZip, {});
|
|
||||||
sinon.stub(JSZip, "loadAsync").callsFake(
|
sinon.stub(JSZip, "loadAsync").callsFake(
|
||||||
() =>
|
() =>
|
||||||
new Promise<JSZip>((resolve) => {
|
new Promise<JSZip>((resolve) => {
|
||||||
@ -220,7 +219,7 @@ describe("from-docx", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => {
|
afterEach(() => {
|
||||||
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -292,8 +291,7 @@ describe("from-docx", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("document.xml and [Content_Types].xml with relationships", () => {
|
describe("document.xml and [Content_Types].xml with relationships", () => {
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
sinon.createStubInstance(JSZip, {});
|
|
||||||
sinon.stub(JSZip, "loadAsync").callsFake(
|
sinon.stub(JSZip, "loadAsync").callsFake(
|
||||||
() =>
|
() =>
|
||||||
new Promise<JSZip>((resolve) => {
|
new Promise<JSZip>((resolve) => {
|
||||||
@ -307,7 +305,7 @@ describe("from-docx", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => {
|
afterEach(() => {
|
||||||
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -322,6 +320,14 @@ describe("from-docx", () => {
|
|||||||
data: Buffer.from(""),
|
data: Buffer.from(""),
|
||||||
transformation: { width: 100, height: 100 },
|
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", () => {
|
describe("document.xml", () => {
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
sinon.createStubInstance(JSZip, {});
|
|
||||||
sinon.stub(JSZip, "loadAsync").callsFake(
|
sinon.stub(JSZip, "loadAsync").callsFake(
|
||||||
() =>
|
() =>
|
||||||
new Promise<JSZip>((resolve) => {
|
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();
|
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,7 +68,14 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
|||||||
const hyperlinkRelationshipAdditions: IHyperlinkRelationshipAddition[] = [];
|
const hyperlinkRelationshipAdditions: IHyperlinkRelationshipAddition[] = [];
|
||||||
let hasMedia = false;
|
let hasMedia = false;
|
||||||
|
|
||||||
|
const binaryContentMap = new Map<string, Buffer>();
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(zipContent.files)) {
|
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"));
|
const json = toJson(await value.async("text"));
|
||||||
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
|
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
|
||||||
const context: IContext = {
|
const context: IContext = {
|
||||||
@ -196,6 +203,10 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
|||||||
zip.file(key, output);
|
zip.file(key, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of binaryContentMap) {
|
||||||
|
zip.file(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
for (const { stream, fileName } of file.Media.Array) {
|
for (const { stream, fileName } of file.Media.Array) {
|
||||||
zip.file(`word/media/${fileName}`, stream);
|
zip.file(`word/media/${fileName}`, stream);
|
||||||
}
|
}
|
||||||
|
@ -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
|
// Try to fill rest of test coverage
|
||||||
// it("should replace token in paragraph", () => {
|
// it("should replace token in paragraph", () => {
|
||||||
// const output = replaceTokenInParagraphElement({
|
// const output = replaceTokenInParagraphElement({
|
||||||
|
@ -30,9 +30,15 @@ export const replaceTokenInParagraphElement = ({
|
|||||||
switch (replaceMode) {
|
switch (replaceMode) {
|
||||||
case ReplaceMode.START:
|
case ReplaceMode.START:
|
||||||
if (startIndex >= 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
|
// 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 not, we just add text to the middle of the run later
|
||||||
|
if (partToReplace === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const firstPart = text.replace(partToReplace, replacementText);
|
const firstPart = text.replace(partToReplace, replacementText);
|
||||||
patchTextElement(paragraphElement.elements![run.index].elements![index], firstPart);
|
patchTextElement(paragraphElement.elements![run.index].elements![index], firstPart);
|
||||||
replaceMode = ReplaceMode.MIDDLE;
|
replaceMode = ReplaceMode.MIDDLE;
|
||||||
|
@ -7,7 +7,7 @@ import { Text } from "@file/paragraph/run/run-components/text";
|
|||||||
const formatter = new Formatter();
|
const formatter = new Formatter();
|
||||||
|
|
||||||
export const toJson = (xmlData: string): Element => {
|
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;
|
return xmlObj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
"typedocOptions": {
|
"typedocOptions": {
|
||||||
"out": "docs/api",
|
"out": "docs/api",
|
||||||
"exclude": "test",
|
"exclude": "test",
|
||||||
|
Reference in New Issue
Block a user