Feature/multiple patch document exports (#2497)
* Turn patch document into options object Add outputType to options * Set keep styles to true by default * Simplify method * Rename variable * #2267 Multiple patches of same key * Remove path which won't be visited
This commit is contained in:
@ -16,7 +16,9 @@ import {
|
||||
VerticalAlign,
|
||||
} from "docx";
|
||||
|
||||
patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
|
||||
patchDocument({
|
||||
outputType: "nodebuffer",
|
||||
data: fs.readFileSync("demo/assets/simple-template.docx"),
|
||||
patches: {
|
||||
name: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
@ -56,7 +58,11 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
|
||||
],
|
||||
link: "https://www.google.co.uk",
|
||||
}),
|
||||
new ImageRun({ data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
|
||||
new ImageRun({
|
||||
type: "png",
|
||||
data: fs.readFileSync("./demo/images/dog.png"),
|
||||
transformation: { width: 100, height: 100 },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
@ -82,7 +88,13 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
|
||||
},
|
||||
image_test: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [new ImageRun({ data: fs.readFileSync("./demo/images/image1.jpeg"), transformation: { width: 100, height: 100 } })],
|
||||
children: [
|
||||
new ImageRun({
|
||||
type: "jpg",
|
||||
data: fs.readFileSync("./demo/images/image1.jpeg"),
|
||||
transformation: { width: 100, height: 100 },
|
||||
}),
|
||||
],
|
||||
},
|
||||
table: {
|
||||
type: PatchType.DOCUMENT,
|
||||
|
@ -3,7 +3,9 @@
|
||||
import * as fs from "fs";
|
||||
import { patchDocument, PatchType, TextRun } from "docx";
|
||||
|
||||
patchDocument(fs.readFileSync("demo/assets/simple-template-2.docx"), {
|
||||
patchDocument({
|
||||
outputType: "nodebuffer",
|
||||
data: fs.readFileSync("demo/assets/simple-template-2.docx"),
|
||||
patches: {
|
||||
name: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
|
@ -24,7 +24,9 @@ const patches = getPatches({
|
||||
paragraph_replace: "Lorem ipsum paragraph",
|
||||
});
|
||||
|
||||
patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
|
||||
patchDocument({
|
||||
outputType: "nodebuffer",
|
||||
data: fs.readFileSync("demo/assets/simple-template.docx"),
|
||||
patches,
|
||||
}).then((doc) => {
|
||||
fs.writeFileSync("My Document.docx", doc);
|
||||
|
@ -22,8 +22,11 @@ const patches = getPatches({
|
||||
"first-name": "John",
|
||||
});
|
||||
|
||||
patchDocument(fs.readFileSync("demo/assets/simple-template-3.docx"), {
|
||||
patchDocument({
|
||||
outputType: "nodebuffer",
|
||||
data: fs.readFileSync("demo/assets/simple-template-3.docx"),
|
||||
patches,
|
||||
keepOriginalStyles: true,
|
||||
}).then((doc) => {
|
||||
fs.writeFileSync("My Document.docx", doc);
|
||||
});
|
||||
|
Binary file not shown.
@ -218,7 +218,9 @@ describe("from-docx", () => {
|
||||
});
|
||||
|
||||
it("should patch the document", async () => {
|
||||
const output = await patchDocument(Buffer.from(""), {
|
||||
const output = await patchDocument({
|
||||
outputType: "uint8array",
|
||||
data: Buffer.from(""),
|
||||
patches: {
|
||||
name: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
@ -279,7 +281,9 @@ describe("from-docx", () => {
|
||||
});
|
||||
|
||||
it("should patch the document", async () => {
|
||||
const output = await patchDocument(Buffer.from(""), {
|
||||
const output = await patchDocument({
|
||||
outputType: "uint8array",
|
||||
data: Buffer.from(""),
|
||||
patches: {},
|
||||
});
|
||||
expect(output).to.not.be.undefined;
|
||||
@ -305,7 +309,9 @@ describe("from-docx", () => {
|
||||
});
|
||||
|
||||
it("should use the relationships file rather than create one", async () => {
|
||||
const output = await patchDocument(Buffer.from(""), {
|
||||
const output = await patchDocument({
|
||||
outputType: "uint8array",
|
||||
data: Buffer.from(""),
|
||||
patches: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
image_test: {
|
||||
@ -350,7 +356,9 @@ describe("from-docx", () => {
|
||||
|
||||
it("should throw an error if the content types is not found", () =>
|
||||
expect(
|
||||
patchDocument(Buffer.from(""), {
|
||||
patchDocument({
|
||||
outputType: "uint8array",
|
||||
data: Buffer.from(""),
|
||||
patches: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
image_test: {
|
||||
@ -388,7 +396,9 @@ describe("from-docx", () => {
|
||||
|
||||
it("should throw an error if the content types is not found", () =>
|
||||
expect(
|
||||
patchDocument(Buffer.from(""), {
|
||||
patchDocument({
|
||||
outputType: "uint8array",
|
||||
data: Buffer.from(""),
|
||||
patches: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
image_test: {
|
||||
|
@ -12,7 +12,6 @@ import { TargetModeType } from "@file/relationships/relationship/relationship";
|
||||
import { uniqueId } from "@util/convenience-functions";
|
||||
|
||||
import { replacer } from "./replacer";
|
||||
import { findLocationOfText } from "./traverser";
|
||||
import { toJson } from "./util";
|
||||
import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager";
|
||||
import { appendContentType } from "./content-types-manager";
|
||||
@ -47,14 +46,37 @@ interface IHyperlinkRelationshipAddition {
|
||||
|
||||
export type IPatch = ParagraphPatch | FilePatch;
|
||||
|
||||
export interface PatchDocumentOptions {
|
||||
// From JSZip
|
||||
type OutputByType = {
|
||||
readonly base64: string;
|
||||
// eslint-disable-next-line id-denylist
|
||||
readonly string: string;
|
||||
readonly text: string;
|
||||
readonly binarystring: string;
|
||||
readonly array: readonly number[];
|
||||
readonly uint8array: Uint8Array;
|
||||
readonly arraybuffer: ArrayBuffer;
|
||||
readonly blob: Blob;
|
||||
readonly nodebuffer: Buffer;
|
||||
};
|
||||
|
||||
export type PatchDocumentOutputType = keyof OutputByType;
|
||||
|
||||
export type PatchDocumentOptions<T extends PatchDocumentOutputType = PatchDocumentOutputType> = {
|
||||
readonly outputType: T;
|
||||
readonly data: InputDataType;
|
||||
readonly patches: { readonly [key: string]: IPatch };
|
||||
readonly keepOriginalStyles?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
const imageReplacer = new ImageReplacer();
|
||||
|
||||
export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise<Uint8Array> => {
|
||||
export const patchDocument = async <T extends PatchDocumentOutputType = PatchDocumentOutputType>({
|
||||
outputType,
|
||||
data,
|
||||
patches,
|
||||
keepOriginalStyles,
|
||||
}: PatchDocumentOptions<T>): Promise<OutputByType[T]> => {
|
||||
const zipContent = await JSZip.loadAsync(data);
|
||||
const contexts = new Map<string, IContext>();
|
||||
const file = {
|
||||
@ -104,38 +126,48 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
||||
};
|
||||
contexts.set(key, context);
|
||||
|
||||
for (const [patchKey, patchValue] of Object.entries(options.patches)) {
|
||||
for (const [patchKey, patchValue] of Object.entries(patches)) {
|
||||
const patchText = `{{${patchKey}}}`;
|
||||
const renderedParagraphs = findLocationOfText(json, patchText);
|
||||
// TODO: mutates json. Make it immutable
|
||||
replacer(
|
||||
json,
|
||||
{
|
||||
...patchValue,
|
||||
children: patchValue.children.map((element) => {
|
||||
// We need to replace external hyperlinks with concrete hyperlinks
|
||||
if (element instanceof ExternalHyperlink) {
|
||||
const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId());
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
hyperlinkRelationshipAdditions.push({
|
||||
key,
|
||||
hyperlink: {
|
||||
id: concreteHyperlink.linkId,
|
||||
link: element.options.link,
|
||||
},
|
||||
});
|
||||
return concreteHyperlink;
|
||||
} else {
|
||||
return element;
|
||||
}
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
patchText,
|
||||
renderedParagraphs,
|
||||
context,
|
||||
options.keepOriginalStyles,
|
||||
);
|
||||
// We need to loop through to catch every occurrence of the patch text
|
||||
// It is possible that the patch text is in the same run
|
||||
// This algorithm is limited to one patch per text run
|
||||
// Once it cannot find any more occurrences, it will throw an error, and then we break out of the loop
|
||||
// https://github.com/dolanmiu/docx/issues/2267
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
replacer({
|
||||
json,
|
||||
patch: {
|
||||
...patchValue,
|
||||
children: patchValue.children.map((element) => {
|
||||
// We need to replace external hyperlinks with concrete hyperlinks
|
||||
if (element instanceof ExternalHyperlink) {
|
||||
const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId());
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
hyperlinkRelationshipAdditions.push({
|
||||
key,
|
||||
hyperlink: {
|
||||
id: concreteHyperlink.linkId,
|
||||
link: element.options.link,
|
||||
},
|
||||
});
|
||||
return concreteHyperlink;
|
||||
} else {
|
||||
return element;
|
||||
}
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
patchText,
|
||||
context,
|
||||
keepOriginalStyles,
|
||||
});
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media);
|
||||
@ -201,6 +233,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
||||
appendContentType(contentTypesJson, "image/jpeg", "jpg");
|
||||
appendContentType(contentTypesJson, "image/bmp", "bmp");
|
||||
appendContentType(contentTypesJson, "image/gif", "gif");
|
||||
appendContentType(contentTypesJson, "image/svg+xml", "svg");
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
@ -220,7 +253,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
||||
}
|
||||
|
||||
return zip.generateAsync({
|
||||
type: "uint8array",
|
||||
type: outputType,
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
compression: "DEFLATE",
|
||||
});
|
||||
|
@ -27,7 +27,7 @@ describe("paragraph-token-replacer", () => {
|
||||
},
|
||||
renderedParagraph: {
|
||||
index: 0,
|
||||
path: [0],
|
||||
pathToParagraph: [0],
|
||||
runs: [
|
||||
{
|
||||
end: 4,
|
||||
@ -128,7 +128,7 @@ describe("paragraph-token-replacer", () => {
|
||||
{ text: "World", parts: [{ text: "World", index: 0, start: 15, end: 19 }], index: 3, start: 15, end: 19 },
|
||||
],
|
||||
index: 0,
|
||||
path: [0, 1, 0, 0],
|
||||
pathToParagraph: [0, 1, 0, 0],
|
||||
},
|
||||
originalText: "{{name}}",
|
||||
replacementText: "John",
|
||||
|
@ -8,7 +8,7 @@ import { PatchType } from "./from-docx";
|
||||
|
||||
import { replacer } from "./replacer";
|
||||
|
||||
const MOCK_JSON = {
|
||||
export const MOCK_JSON = {
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
@ -73,103 +73,60 @@ const MOCK_JSON = {
|
||||
|
||||
describe("replacer", () => {
|
||||
describe("replacer", () => {
|
||||
it("should return the same object if nothing is added", () => {
|
||||
const output = replacer(
|
||||
{
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [],
|
||||
},
|
||||
"hello",
|
||||
[],
|
||||
// eslint-disable-next-line functional/prefer-readonly-type
|
||||
vi.fn<[], IContext>()(),
|
||||
);
|
||||
|
||||
expect(output).to.deep.equal({
|
||||
elements: [],
|
||||
});
|
||||
it("should throw an error if nothing is added", () => {
|
||||
expect(() =>
|
||||
replacer({
|
||||
json: {
|
||||
elements: [],
|
||||
},
|
||||
patch: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [],
|
||||
},
|
||||
patchText: "hello",
|
||||
// eslint-disable-next-line functional/prefer-readonly-type
|
||||
context: vi.fn<[], IContext>()(),
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("should replace paragraph type", () => {
|
||||
const output = replacer(
|
||||
MOCK_JSON,
|
||||
{
|
||||
const output = replacer({
|
||||
json: JSON.parse(JSON.stringify(MOCK_JSON)),
|
||||
patch: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [new TextRun("Delightful Header")],
|
||||
},
|
||||
"{{header_adjective}}",
|
||||
[
|
||||
{
|
||||
text: "This is a {{header_adjective}} don’t you think?",
|
||||
runs: [
|
||||
{
|
||||
text: "This is a {{head",
|
||||
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
|
||||
index: 1,
|
||||
start: 0,
|
||||
end: 15,
|
||||
},
|
||||
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
|
||||
{
|
||||
text: "_adjective}} don’t you think?",
|
||||
parts: [{ text: "_adjective}} don’t you think?", index: 0, start: 18, end: 46 }],
|
||||
index: 3,
|
||||
start: 18,
|
||||
end: 46,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
path: [0, 0, 0],
|
||||
},
|
||||
],
|
||||
{
|
||||
patchText: "{{header_adjective}}",
|
||||
context: {
|
||||
file: {} as unknown as File,
|
||||
viewWrapper: {
|
||||
Relationships: {},
|
||||
} as unknown as IViewWrapper,
|
||||
stack: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(JSON.stringify(output)).to.contain("Delightful Header");
|
||||
});
|
||||
|
||||
it("should replace paragraph type keeping original styling if keepOriginalStyles is true", () => {
|
||||
const output = replacer(
|
||||
MOCK_JSON,
|
||||
{
|
||||
const output = replacer({
|
||||
json: JSON.parse(JSON.stringify(MOCK_JSON)),
|
||||
patch: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [new TextRun("sweet")],
|
||||
},
|
||||
"{{bold}}",
|
||||
[
|
||||
{
|
||||
text: "What a {{bold}} text!",
|
||||
runs: [
|
||||
{
|
||||
text: "What a {{bold}} text!",
|
||||
parts: [{ text: "What a {{bold}} text!", index: 1, start: 0, end: 21 }],
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: 21,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
path: [0, 0, 1],
|
||||
},
|
||||
],
|
||||
{
|
||||
patchText: "{{bold}}",
|
||||
context: {
|
||||
file: {} as unknown as File,
|
||||
viewWrapper: {
|
||||
Relationships: {},
|
||||
} as unknown as IViewWrapper,
|
||||
stack: [],
|
||||
},
|
||||
true,
|
||||
);
|
||||
keepOriginalStyles: true,
|
||||
});
|
||||
|
||||
expect(JSON.stringify(output)).to.contain("sweet");
|
||||
expect(output.elements![0].elements![1].elements).toMatchObject([
|
||||
@ -225,91 +182,23 @@ describe("replacer", () => {
|
||||
});
|
||||
|
||||
it("should replace document type", () => {
|
||||
const output = replacer(
|
||||
MOCK_JSON,
|
||||
{
|
||||
const output = replacer({
|
||||
json: JSON.parse(JSON.stringify(MOCK_JSON)),
|
||||
patch: {
|
||||
type: PatchType.DOCUMENT,
|
||||
children: [new Paragraph("Lorem ipsum paragraph")],
|
||||
},
|
||||
"{{header_adjective}}",
|
||||
[
|
||||
{
|
||||
text: "This is a {{header_adjective}} don’t you think?",
|
||||
runs: [
|
||||
{
|
||||
text: "This is a {{head",
|
||||
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
|
||||
index: 1,
|
||||
start: 0,
|
||||
end: 15,
|
||||
},
|
||||
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
|
||||
{
|
||||
text: "_adjective}} don’t you think?",
|
||||
parts: [{ text: "_adjective}} don’t you think?", index: 0, start: 18, end: 46 }],
|
||||
index: 3,
|
||||
start: 18,
|
||||
end: 46,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
path: [0, 0, 0],
|
||||
},
|
||||
],
|
||||
{
|
||||
patchText: "{{header_adjective}}",
|
||||
context: {
|
||||
file: {} as unknown as File,
|
||||
viewWrapper: {
|
||||
Relationships: {},
|
||||
} as unknown as IViewWrapper,
|
||||
stack: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph");
|
||||
});
|
||||
|
||||
it("should throw an error if the type is not supported", () => {
|
||||
expect(() =>
|
||||
replacer(
|
||||
{},
|
||||
{
|
||||
type: PatchType.DOCUMENT,
|
||||
children: [new Paragraph("Lorem ipsum paragraph")],
|
||||
},
|
||||
"{{header_adjective}}",
|
||||
[
|
||||
{
|
||||
text: "This is a {{header_adjective}} don’t you think?",
|
||||
runs: [
|
||||
{
|
||||
text: "This is a {{head",
|
||||
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
|
||||
index: 1,
|
||||
start: 0,
|
||||
end: 15,
|
||||
},
|
||||
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
|
||||
{
|
||||
text: "_adjective}} don’t you think?",
|
||||
parts: [{ text: "_adjective}} don’t you think?", index: 0, start: 18, end: 46 }],
|
||||
index: 3,
|
||||
start: 18,
|
||||
end: 46,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
path: [0, 0, 0],
|
||||
},
|
||||
],
|
||||
{
|
||||
file: {} as unknown as File,
|
||||
viewWrapper: {
|
||||
Relationships: {},
|
||||
} as unknown as IViewWrapper,
|
||||
stack: [],
|
||||
},
|
||||
),
|
||||
).to.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,22 +6,33 @@ import { IContext, XmlComponent } from "@file/xml-components";
|
||||
|
||||
import { IPatch, PatchType } from "./from-docx";
|
||||
import { toJson } from "./util";
|
||||
import { IRenderedParagraphNode } from "./run-renderer";
|
||||
import { replaceTokenInParagraphElement } from "./paragraph-token-replacer";
|
||||
import { findRunElementIndexWithToken, splitRunElement } from "./paragraph-split-inject";
|
||||
import { findLocationOfText } from "./traverser";
|
||||
|
||||
const formatter = new Formatter();
|
||||
|
||||
const SPLIT_TOKEN = "ɵ";
|
||||
|
||||
export const replacer = (
|
||||
json: Element,
|
||||
patch: IPatch,
|
||||
patchText: string,
|
||||
renderedParagraphs: readonly IRenderedParagraphNode[],
|
||||
context: IContext,
|
||||
keepOriginalStyles: boolean = false,
|
||||
): Element => {
|
||||
export const replacer = ({
|
||||
json,
|
||||
patch,
|
||||
patchText,
|
||||
context,
|
||||
keepOriginalStyles = true,
|
||||
}: {
|
||||
readonly json: Element;
|
||||
readonly patch: IPatch;
|
||||
readonly patchText: string;
|
||||
readonly context: IContext;
|
||||
readonly keepOriginalStyles?: boolean;
|
||||
}): Element => {
|
||||
const renderedParagraphs = findLocationOfText(json, patchText);
|
||||
|
||||
if (renderedParagraphs.length === 0) {
|
||||
throw new Error(`Could not find text ${patchText}`);
|
||||
}
|
||||
|
||||
for (const renderedParagraph of renderedParagraphs) {
|
||||
const textJson = patch.children
|
||||
// eslint-disable-next-line no-loop-func
|
||||
@ -30,15 +41,15 @@ export const replacer = (
|
||||
|
||||
switch (patch.type) {
|
||||
case PatchType.DOCUMENT: {
|
||||
const parentElement = goToParentElementFromPath(json, renderedParagraph.path);
|
||||
const elementIndex = getLastElementIndexFromPath(renderedParagraph.path);
|
||||
const parentElement = goToParentElementFromPath(json, renderedParagraph.pathToParagraph);
|
||||
const elementIndex = getLastElementIndexFromPath(renderedParagraph.pathToParagraph);
|
||||
// eslint-disable-next-line functional/immutable-data, prefer-destructuring
|
||||
parentElement.elements!.splice(elementIndex, 1, ...textJson);
|
||||
break;
|
||||
}
|
||||
case PatchType.PARAGRAPH:
|
||||
default: {
|
||||
const paragraphElement = goToElementFromPath(json, renderedParagraph.path);
|
||||
const paragraphElement = goToElementFromPath(json, renderedParagraph.pathToParagraph);
|
||||
replaceTokenInParagraphElement({
|
||||
paragraphElement,
|
||||
renderedParagraph,
|
||||
@ -87,11 +98,7 @@ const goToElementFromPath = (json: Element, path: readonly number[]): Element =>
|
||||
// Which we do not want to double count
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const index = path[i];
|
||||
const nextElements = element.elements;
|
||||
|
||||
if (!nextElements) {
|
||||
throw new Error("Could not find element");
|
||||
}
|
||||
const nextElements = element.elements!;
|
||||
|
||||
element = nextElements[index];
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ describe("run-renderer", () => {
|
||||
const output = renderParagraphNode({ element: { name: "w:p" }, index: 0, parent: undefined });
|
||||
expect(output).to.deep.equal({
|
||||
index: -1,
|
||||
path: [],
|
||||
pathToParagraph: [],
|
||||
runs: [],
|
||||
text: "",
|
||||
});
|
||||
@ -39,7 +39,7 @@ describe("run-renderer", () => {
|
||||
});
|
||||
expect(output).to.deep.equal({
|
||||
index: 0,
|
||||
path: [0],
|
||||
pathToParagraph: [0],
|
||||
runs: [
|
||||
{
|
||||
end: 4,
|
||||
@ -79,7 +79,7 @@ describe("run-renderer", () => {
|
||||
});
|
||||
expect(output).to.deep.equal({
|
||||
index: 0,
|
||||
path: [0],
|
||||
pathToParagraph: [0],
|
||||
runs: [
|
||||
{
|
||||
end: 0,
|
||||
|
@ -6,7 +6,7 @@ export interface IRenderedParagraphNode {
|
||||
readonly text: string;
|
||||
readonly runs: readonly IRenderedRunNode[];
|
||||
readonly index: number;
|
||||
readonly path: readonly number[];
|
||||
readonly pathToParagraph: readonly number[];
|
||||
}
|
||||
|
||||
interface StartAndEnd {
|
||||
@ -35,7 +35,7 @@ export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNod
|
||||
text: "",
|
||||
runs: [],
|
||||
index: -1,
|
||||
path: [],
|
||||
pathToParagraph: [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -50,8 +50,7 @@ export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNod
|
||||
|
||||
return renderedRunNode;
|
||||
})
|
||||
.filter((e) => !!e)
|
||||
.map((e) => e as IRenderedRunNode);
|
||||
.filter((e) => !!e);
|
||||
|
||||
const text = runs.reduce((acc, curr) => acc + curr.text, "");
|
||||
|
||||
@ -59,7 +58,7 @@ export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNod
|
||||
text,
|
||||
runs,
|
||||
index: node.index,
|
||||
path: buildNodePath(node),
|
||||
pathToParagraph: buildNodePath(node),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -139,6 +139,28 @@ const MOCK_JSON = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:p",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:rPr",
|
||||
elements: [{ type: "element", name: "w:b", attributes: { "w:val": "1" } }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: "What a {{bold}} text!" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:p",
|
||||
@ -535,6 +557,45 @@ const MOCK_JSON = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:p",
|
||||
attributes: {
|
||||
"w14:paraId": "3BE1A671",
|
||||
"w14:textId": "74E856C4",
|
||||
"w:rsidR": "000D38A7",
|
||||
"w:rsidRDefault": "000D38A7",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:pPr",
|
||||
elements: [{ type: "element", name: "w:pStyle", attributes: { "w:val": "Header" } }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "This is a {{head" }] }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
attributes: { "w:rsidR": "004A3A99" },
|
||||
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "er" }] }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: "_adjective}} don’t you think?" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:sectPr",
|
||||
@ -574,7 +635,7 @@ describe("traverser", () => {
|
||||
expect(output).to.deep.equal([
|
||||
{
|
||||
index: 1,
|
||||
path: [0, 0, 0, 8, 2, 0, 1],
|
||||
pathToParagraph: [0, 0, 0, 9, 2, 0, 1],
|
||||
runs: [
|
||||
{
|
||||
end: 18,
|
||||
@ -595,5 +656,76 @@ describe("traverser", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should find the location of text", () => {
|
||||
const output = findLocationOfText(MOCK_JSON, "{{bold}}");
|
||||
|
||||
expect(output).to.deep.equal([
|
||||
{
|
||||
text: "What a {{bold}} text!",
|
||||
runs: [
|
||||
{
|
||||
text: "What a {{bold}} text!",
|
||||
parts: [{ text: "What a {{bold}} text!", index: 1, start: 0, end: 20 }],
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: 20,
|
||||
},
|
||||
],
|
||||
index: 5,
|
||||
pathToParagraph: [0, 0, 0, 5],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should find the location of text", () => {
|
||||
const output = findLocationOfText(MOCK_JSON, "{{bold}}");
|
||||
|
||||
expect(output).to.deep.equal([
|
||||
{
|
||||
text: "What a {{bold}} text!",
|
||||
runs: [
|
||||
{
|
||||
text: "What a {{bold}} text!",
|
||||
parts: [{ text: "What a {{bold}} text!", index: 1, start: 0, end: 20 }],
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: 20,
|
||||
},
|
||||
],
|
||||
index: 5,
|
||||
pathToParagraph: [0, 0, 0, 5],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should find the location of text", () => {
|
||||
const output = findLocationOfText(MOCK_JSON, "{{header_adjective}}");
|
||||
|
||||
expect(output).to.deep.equal([
|
||||
{
|
||||
text: "This is a {{header_adjective}} don’t you think?",
|
||||
runs: [
|
||||
{
|
||||
text: "This is a {{head",
|
||||
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
|
||||
index: 1,
|
||||
start: 0,
|
||||
end: 15,
|
||||
},
|
||||
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
|
||||
{
|
||||
text: "_adjective}} don’t you think?",
|
||||
parts: [{ text: "_adjective}} don’t you think?", index: 0, start: 18, end: 46 }],
|
||||
index: 3,
|
||||
start: 18,
|
||||
end: 46,
|
||||
},
|
||||
],
|
||||
index: 14,
|
||||
pathToParagraph: [0, 0, 0, 14],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user