diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 0b509ecaee3..38ab563156c 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -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, diff --git a/demo/87-template-document.ts b/demo/87-template-document.ts index 0dd2c1761db..d91b93e5992 100644 --- a/demo/87-template-document.ts +++ b/demo/87-template-document.ts @@ -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, diff --git a/demo/88-template-document.ts b/demo/88-template-document.ts index 705a5c6f304..b14e076d17e 100644 --- a/demo/88-template-document.ts +++ b/demo/88-template-document.ts @@ -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); diff --git a/demo/89-template-document.ts b/demo/89-template-document.ts index 3b958c78647..ffd1e954c7d 100644 --- a/demo/89-template-document.ts +++ b/demo/89-template-document.ts @@ -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); }); diff --git a/demo/assets/simple-template-3.docx b/demo/assets/simple-template-3.docx index 8d8fa9aacfe..6f520f3367a 100644 Binary files a/demo/assets/simple-template-3.docx and b/demo/assets/simple-template-3.docx differ diff --git a/src/patcher/from-docx.spec.ts b/src/patcher/from-docx.spec.ts index 1b939bad174..cb8032fac6c 100644 --- a/src/patcher/from-docx.spec.ts +++ b/src/patcher/from-docx.spec.ts @@ -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: { diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 1098bf05eab..97c5400b4dd 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -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 = { + 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 => { +export const patchDocument = async ({ + outputType, + data, + patches, + keepOriginalStyles, +}: PatchDocumentOptions): Promise => { const zipContent = await JSZip.loadAsync(data); const contexts = new Map(); 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", }); diff --git a/src/patcher/paragraph-token-replacer.spec.ts b/src/patcher/paragraph-token-replacer.spec.ts index 62ce1b76224..b6561094b7c 100644 --- a/src/patcher/paragraph-token-replacer.spec.ts +++ b/src/patcher/paragraph-token-replacer.spec.ts @@ -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", diff --git a/src/patcher/replacer.spec.ts b/src/patcher/replacer.spec.ts index 3dd0947f494..903ef0bd2b2 100644 --- a/src/patcher/replacer.spec.ts +++ b/src/patcher/replacer.spec.ts @@ -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(); - }); }); }); diff --git a/src/patcher/replacer.ts b/src/patcher/replacer.ts index 2ca7f9f3483..ab069457586 100644 --- a/src/patcher/replacer.ts +++ b/src/patcher/replacer.ts @@ -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]; } diff --git a/src/patcher/run-renderer.spec.ts b/src/patcher/run-renderer.spec.ts index 5771dc02337..7d17d6c7ca4 100644 --- a/src/patcher/run-renderer.spec.ts +++ b/src/patcher/run-renderer.spec.ts @@ -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, diff --git a/src/patcher/run-renderer.ts b/src/patcher/run-renderer.ts index 262d6f265aa..005ad71fe79 100644 --- a/src/patcher/run-renderer.ts +++ b/src/patcher/run-renderer.ts @@ -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), }; }; diff --git a/src/patcher/traverser.spec.ts b/src/patcher/traverser.spec.ts index 0d68a0d00da..a2704e7b09c 100644 --- a/src/patcher/traverser.spec.ts +++ b/src/patcher/traverser.spec.ts @@ -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], + }, + ]); + }); }); });