Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/patcher/paragraph-split-inject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -243,11 +250,6 @@ describe("paragraph-split-inject", () => {
name: "w:r",
type: "element",
},
right: {
elements: [],
name: "w:r",
type: "element",
},
});
});

Expand Down
14 changes: 12 additions & 2 deletions src/patcher/paragraph-split-inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/patcher/paragraph-token-replacer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
67 changes: 67 additions & 0 deletions src/patcher/replacer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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");
});
});
});