Skip to content

Commit 3d3c3d7

Browse files
authored
fix: fix code replacement (#1224)
* fix: fix code replacement * chore: fix formatting
1 parent fad7f7c commit 3d3c3d7

File tree

3 files changed

+230
-6
lines changed

3 files changed

+230
-6
lines changed

.changeset/ninety-peaches-roll.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": patch
3+
---
4+
5+
fix code replacement in mdx

packages/cli/src/cli/loaders/mdx2/code-placeholder.spec.ts

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,4 +646,225 @@ describe("MDX Code Placeholder Loader", () => {
646646
expect(pushedResult).toContain("قم بتطبيق");
647647
});
648648
});
649+
650+
describe("raw code outside fences", () => {
651+
it("should handle raw JavaScript code outside fences", async () => {
652+
const loader = createMdxCodePlaceholderLoader();
653+
loader.setDefaultLocale("en");
654+
655+
// Test case matching user's file structure - raw JS between JSX components
656+
const md = dedent`
657+
</Tabs>
658+
659+
// Attach to button click
660+
document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
661+
662+
<Callout type="warning">
663+
Content here
664+
</Callout>
665+
`;
666+
667+
const pulled = await loader.pull("en", md);
668+
const pushed = await loader.push("en", pulled);
669+
670+
// Should round-trip correctly
671+
expect(pushed).toBe(md);
672+
});
673+
674+
it("should handle mixed code blocks and raw code", async () => {
675+
const loader = createMdxCodePlaceholderLoader();
676+
loader.setDefaultLocale("en");
677+
678+
const md = dedent`
679+
Here's a code block:
680+
681+
\`\`\`typescript
682+
const x = 1;
683+
\`\`\`
684+
685+
Now some raw code outside:
686+
// This is outside
687+
const y = 2;
688+
689+
And another block:
690+
691+
\`\`\`javascript
692+
const z = 3;
693+
\`\`\`
694+
`;
695+
696+
const pulled = await loader.pull("en", md);
697+
const pushed = await loader.push("en", pulled);
698+
699+
// Should preserve raw code outside fences
700+
expect(pushed).toContain("// This is outside");
701+
expect(pushed).toContain("const y = 2;");
702+
});
703+
704+
it("should handle code blocks with extra blank lines added by translation", async () => {
705+
const loader = createMdxCodePlaceholderLoader();
706+
loader.setDefaultLocale("en");
707+
708+
// English source - no extra blank lines
709+
const enMd = dedent`
710+
<Tab value="npm">
711+
\`\`\`bash
712+
npm install
713+
\`\`\`
714+
</Tab>
715+
`;
716+
717+
// Pull English to establish placeholders
718+
const enPulled = await loader.pull("en", enMd);
719+
720+
// German translation with extra blank lines (simulating AI translation behavior)
721+
const deMd = dedent`
722+
<Tab value="npm">
723+
724+
\`\`\`bash
725+
npm install
726+
\`\`\`
727+
728+
</Tab>
729+
`;
730+
731+
// Pull German version
732+
const dePulled = await loader.pull("de", deMd);
733+
734+
// Push back - should restore code blocks correctly
735+
const dePushed = await loader.push("de", dePulled);
736+
737+
// The code block should be present and not replaced with placeholder
738+
expect(dePushed).toContain("```bash");
739+
expect(dePushed).toContain("npm install");
740+
expect(dePushed).not.toMatch(/---CODE-PLACEHOLDER-/);
741+
});
742+
743+
it("should preserve double newlines around placeholders for section splitting", async () => {
744+
const loader = createMdxCodePlaceholderLoader();
745+
loader.setDefaultLocale("en");
746+
747+
// Test that placeholders maintain double newlines so section-split works correctly
748+
const md = dedent`
749+
Text before.
750+
751+
\`\`\`typescript
752+
code1
753+
\`\`\`
754+
755+
Text between.
756+
757+
\`\`\`javascript
758+
code2
759+
\`\`\`
760+
761+
Text after.
762+
`;
763+
764+
const pulled = await loader.pull("en", md);
765+
766+
// Verify placeholders are surrounded by double newlines for proper section splitting
767+
const placeholders = pulled.match(/---CODE-PLACEHOLDER-[a-f0-9]+---/g);
768+
expect(placeholders).toHaveLength(2);
769+
770+
// Check that each placeholder has double newlines around it
771+
for (const placeholder of placeholders!) {
772+
// Should have \n\n before (except at start) and \n\n after (except at end)
773+
const placeholderIndex = pulled.indexOf(placeholder);
774+
775+
// Check for double newline after (unless at end)
776+
const afterPlaceholder = pulled.substring(
777+
placeholderIndex + placeholder.length,
778+
placeholderIndex + placeholder.length + 2,
779+
);
780+
if (placeholderIndex + placeholder.length < pulled.length - 2) {
781+
expect(afterPlaceholder).toBe("\n\n");
782+
}
783+
}
784+
785+
// Ensure we can split on \n\n and get separate sections
786+
const sections = pulled.split("\n\n").filter(Boolean);
787+
expect(sections.length).toBeGreaterThanOrEqual(5); // Text + placeholder + text + placeholder + text
788+
});
789+
});
790+
});
791+
792+
describe("adjacent code blocks bug", () => {
793+
it("should handle closing fence followed immediately by opening fence", async () => {
794+
const loader = createMdxCodePlaceholderLoader();
795+
loader.setDefaultLocale("en");
796+
797+
// This reproduces the actual bug from the user's file
798+
const md = dedent`
799+
\`\`\`typescript
800+
function example() {
801+
return true;
802+
}
803+
\`\`\`
804+
805+
\`\`\`typescript
806+
import { Something } from 'somewhere';
807+
\`\`\`
808+
`;
809+
810+
const pulled = await loader.pull("en", md);
811+
812+
console.log("PULLED CONTENT:");
813+
console.log(pulled);
814+
console.log("---");
815+
816+
// The bug: placeholder is concatenated with "typescript" from next block
817+
const bugPattern = /---CODE-PLACEHOLDER-[a-f0-9]+---typescript/;
818+
expect(pulled).not.toMatch(bugPattern);
819+
820+
// Should have proper separation
821+
expect(pulled).toMatch(
822+
/---CODE-PLACEHOLDER-[a-f0-9]+---\n\n---CODE-PLACEHOLDER-[a-f0-9]+---/,
823+
);
824+
});
825+
});
826+
827+
describe("$ special character handling in replacement functions", () => {
828+
it("should preserve $ characters in ensureTrailingFenceNewline", async () => {
829+
const loader = createMdxCodePlaceholderLoader();
830+
loader.setDefaultLocale("en");
831+
832+
// Tests fix for lines 38, 68: replaceAll(match, () => replacement)
833+
// Code block with $ that would trigger special replacement behavior if not using function replacer
834+
const content = dedent`
835+
Some text
836+
\`\`\`js
837+
console.log('Current period cost: $' + amount);
838+
const template = \`Price: $\${price}\`;
839+
\`\`\`
840+
More text
841+
`;
842+
843+
const pulled = await loader.pull("en", content);
844+
const pushed = await loader.push("en", pulled);
845+
846+
// All $ characters should be preserved exactly
847+
expect(pushed).toContain("console.log('Current period cost: $' + amount);");
848+
expect(pushed).toContain("const template = `Price: $");
849+
});
850+
851+
it("should preserve $ characters in ensureSurroundingImageNewlines", async () => {
852+
const loader = createMdxCodePlaceholderLoader();
853+
loader.setDefaultLocale("en");
854+
855+
// Tests fix for line 38: replaceAll(match, () => replacement) in image handling
856+
// Image with $ in URL and alt text that would break with string replacer
857+
const content = dedent`
858+
Here is an image:
859+
![Price: $100](https://api.example.com/chart?price=$500&currency=$USD)
860+
End of text
861+
`;
862+
863+
const pulled = await loader.pull("en", content);
864+
const pushed = await loader.push("en", pulled);
865+
866+
// All $ characters in URL and alt text should be preserved
867+
expect(pushed).toContain("![Price: $100]");
868+
expect(pushed).toContain("price=$500&currency=$USD");
869+
});
649870
});

packages/cli/src/cli/loaders/mdx2/code-placeholder.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function ensureSurroundingImageNewlines(_content: string) {
3535
? match
3636
: `\n\n${match}\n\n`;
3737

38-
content = content.replaceAll(match, replacement);
38+
content = content.replaceAll(match, () => replacement);
3939
workingContent = workingContent.replaceAll(match, "");
4040
found = true;
4141
}
@@ -65,7 +65,7 @@ function ensureTrailingFenceNewline(_content: string) {
6565
const replacement = match.trim().startsWith(">")
6666
? match
6767
: `\n\n${match}\n\n`;
68-
content = content.replaceAll(match, replacement);
68+
content = content.replaceAll(match, () => replacement);
6969
workingContent = workingContent.replaceAll(match, "");
7070
found = true;
7171
}
@@ -106,19 +106,17 @@ function extractCodePlaceholders(content: string): {
106106
const replacement = codeBlock.trim().startsWith(">")
107107
? `> ${placeholder}`
108108
: `${placeholder}`;
109-
finalContent = finalContent.replace(codeBlock, replacement);
109+
finalContent = finalContent.replace(codeBlock, () => replacement);
110110
}
111111

112112
const inlineCodeMatches = finalContent.matchAll(inlineCodeRegex);
113113
for (const match of inlineCodeMatches) {
114114
const inlineCode = match[0];
115115
const inlineCodeHash = md5(inlineCode);
116116
const placeholder = `---INLINE-CODE-PLACEHOLDER-${inlineCodeHash}---`;
117-
118117
codePlaceholders[placeholder] = inlineCode;
119-
120118
const replacement = placeholder;
121-
finalContent = finalContent.replace(inlineCode, replacement);
119+
finalContent = finalContent.replace(inlineCode, () => replacement);
122120
}
123121

124122
return {

0 commit comments

Comments
 (0)