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,
|
||||
"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({
|
||||
|
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",
|
||||
"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.",
|
||||
"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",
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,5 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"typedocOptions": {
|
||||
"out": "docs/api",
|
||||
"exclude": "test",
|
||||
|
Reference in New Issue
Block a user