diff --git a/src/patcher/paragraph-split-inject.spec.ts b/src/patcher/paragraph-split-inject.spec.ts index 7b30dd36ade..b2eda0ddd2b 100644 --- a/src/patcher/paragraph-split-inject.spec.ts +++ b/src/patcher/paragraph-split-inject.spec.ts @@ -228,8 +228,15 @@ describe("paragraph-split-inject", () => { "*", ); + // When the token is not found in the text, splitIndex remains -1 + // so left gets nothing and right gets all elements expect(output).to.deep.equal({ left: { + elements: [], + name: "w:r", + type: "element", + }, + right: { elements: [ { attributes: { @@ -243,11 +250,6 @@ describe("paragraph-split-inject", () => { name: "w:r", type: "element", }, - right: { - elements: [], - name: "w:r", - type: "element", - }, }); }); diff --git a/src/patcher/paragraph-split-inject.ts b/src/patcher/paragraph-split-inject.ts index c3033732cdc..56c1bae85ec 100644 --- a/src/patcher/paragraph-split-inject.ts +++ b/src/patcher/paragraph-split-inject.ts @@ -24,11 +24,16 @@ export const findRunElementIndexWithToken = (paragraphElement: Element, token: s }; export const splitRunElement = (runElement: Element, token: string): { readonly left: Element; readonly right: Element } => { - let splitIndex = 0; + let splitIndex = -1; const splitElements = runElement.elements ?.map((e, i) => { + // Only take the first split we see, don't mutate the rest + if (splitIndex !== -1) { + return e; + } + if (e.type === "element" && e.name === "w:t") { const text = (e.elements?.[0]?.text as string) ?? ""; const splitText = text.split(token); @@ -37,7 +42,12 @@ export const splitRunElement = (runElement: Element, token: string): { readonly ...patchSpaceAttribute(e), elements: createTextElementContents(t), })); - splitIndex = i; + + // Only set splitIndex if this element actually contains the token + if (splitText.length > 1) { + splitIndex = i; + } + return newElements; } else { return e; diff --git a/src/patcher/paragraph-token-replacer.ts b/src/patcher/paragraph-token-replacer.ts index 32eec3bbcc4..1fd45c307fa 100644 --- a/src/patcher/paragraph-token-replacer.ts +++ b/src/patcher/paragraph-token-replacer.ts @@ -29,7 +29,7 @@ export const replaceTokenInParagraphElement = ({ for (const { text, index, start, end } of run.parts) { switch (replaceMode) { case ReplaceMode.START: - if (startIndex >= start) { + if (startIndex >= start && startIndex <= end) { const offsetStartIndex = startIndex - start; const offsetEndIndex = Math.min(endIndex, end) - start; const partToReplace = run.text.substring(offsetStartIndex, offsetEndIndex + 1); diff --git a/src/patcher/replacer.spec.ts b/src/patcher/replacer.spec.ts index 762c9403225..be3fb64e022 100644 --- a/src/patcher/replacer.spec.ts +++ b/src/patcher/replacer.spec.ts @@ -6,6 +6,7 @@ import { Paragraph, TextRun } from "@file/paragraph"; import { PatchType } from "./from-docx"; import { replacer } from "./replacer"; +import { traverse } from "./traverser"; export const MOCK_JSON = { elements: [ @@ -724,5 +725,71 @@ describe("replacer", () => { expect(JSON.stringify(element)).not.to.contain("{{empty}}"); expect(didFindOccurrence).toBe(true); }); + + it("should handle multiple replacements in a single run with multiple text elements", () => { + // Minimal reproduction of bug where: + // 1. A w:r (run) contains multiple w:t (text) elements + // 2. First replacement splits the run, creating additional w:t elements + // 3. Second replacement must correctly: + // - Find the token in the remaining w:t elements (not get confused by earlier parts) + // - Split at the correct position in the flattened element array + const json = { + elements: [ + { + type: "element", + name: "w:p", + elements: [ + { + type: "element", + name: "w:r", + elements: [ + { + type: "element", + name: "w:t", + elements: [{ type: "text", text: "A{{token1}}B" }], + }, + { type: "element", name: "w:tab" }, + { + type: "element", + name: "w:t", + elements: [{ type: "text", text: "C{{token2}}D" }], + }, + ], + }, + ], + }, + ], + }; + + // First replacement + replacer({ + json, + patch: { type: PatchType.PARAGRAPH, children: [new TextRun("X")] }, + patchText: "{{token1}}", + context: { + file: {} as unknown as File, + viewWrapper: { Relationships: {} } as unknown as IViewWrapper, + stack: [], + }, + }); + + // Second replacement - this is where the bug occurred + const { didFindOccurrence } = replacer({ + json, + patch: { type: PatchType.PARAGRAPH, children: [new TextRun("Y")] }, + patchText: "{{token2}}", + context: { + file: {} as unknown as File, + viewWrapper: { Relationships: {} } as unknown as IViewWrapper, + stack: [], + }, + }); + + expect(didFindOccurrence).toBe(true); + + // Verify the rendered text is correct + const paragraphs = traverse(json); + expect(paragraphs[0].text).to.equal("AXBCYD"); + }); }); });