Skip to content

Commit 4b5a9e8

Browse files
kennethkalmerclaude
andcommitted
fix: preserve import/export in code blocks during markdown transpilation
Replace regex-based import/export removal with line-by-line parser that only removes import/export statements from the top of MDX files. This preserves code samples containing legitimate import/export statements. The previous regex approach used /gm flags that matched import/export at the start of any line in the file, breaking JavaScript/TypeScript code examples. Changes: - Rewrote removeImportExportStatements() to use state machine approach - Only processes import/export at top of file (after frontmatter) - Tracks multi-line statements with brace depth counting - Added test cases for code sample preservation Fixes #3034 Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
1 parent 14b7498 commit 4b5a9e8

4 files changed

Lines changed: 196 additions & 20 deletions

File tree

data/onPostBuild/__fixtures__/input.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {
1616
} from 'module'
1717

1818
export const foo = 'bar';
19+
export const ArrowFunc = () => {
20+
const x = 1;
21+
return x;
22+
};
1923
export default SomeComponent;
2024

2125
{/* This is a JSX comment */}

data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ This is a test introduction
88
99
1010
11-
12-
1311
## Basic heading
1412
1513
## Heading with anchor

data/onPostBuild/transpileMdxToMarkdown.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,102 @@ Content without intro`;
9595
expect(output).not.toContain('class Foo');
9696
expect(output).toContain('Content here');
9797
});
98+
99+
it('should preserve import/export statements in code blocks', () => {
100+
const input = `import Component from './component'
101+
102+
## Code Example
103+
104+
\`\`\`javascript
105+
import { realtime } from '@ably/realtime';
106+
export const config = { ... };
107+
\`\`\`
108+
`;
109+
const output = removeImportExportStatements(input);
110+
expect(output).toContain('import { realtime }');
111+
expect(output).toContain('export const config');
112+
expect(output).not.toContain('import Component');
113+
});
114+
115+
it('should handle multi-line imports followed by content', () => {
116+
const input = `import {
117+
Foo,
118+
Bar
119+
} from 'module';
120+
121+
## First Heading
122+
123+
Content here`;
124+
const output = removeImportExportStatements(input);
125+
expect(output).toContain('## First Heading');
126+
expect(output).toContain('Content here');
127+
expect(output).not.toContain('import');
128+
expect(output).not.toContain('Foo');
129+
expect(output).not.toContain('Bar');
130+
});
131+
132+
it('should stop removing at first non-import/export line', () => {
133+
const input = `import Foo from 'bar';
134+
135+
{/* JSX comment */}
136+
137+
import { something } from 'somewhere';`;
138+
const output = removeImportExportStatements(input);
139+
expect(output).toContain('{/* JSX comment */}');
140+
expect(output).toContain("import { something } from 'somewhere'");
141+
expect(output).not.toContain('import Foo');
142+
});
143+
144+
it('should handle blank lines between imports', () => {
145+
const input = `import Foo from 'bar';
146+
147+
import Baz from 'qux';
148+
149+
## Content`;
150+
const output = removeImportExportStatements(input);
151+
expect(output).toContain('## Content');
152+
expect(output).not.toContain('import Foo');
153+
expect(output).not.toContain('import Baz');
154+
});
155+
156+
it('should handle export function on one line', () => {
157+
const input = `export function foo() { return 'bar'; }
158+
159+
## Content`;
160+
const output = removeImportExportStatements(input);
161+
expect(output).toContain('## Content');
162+
expect(output).not.toContain('export');
163+
expect(output).not.toContain('function foo');
164+
});
165+
166+
it('should remove multi-line arrow function exports', () => {
167+
const input = `export const MyComponent = () => {
168+
const x = 1;
169+
return x;
170+
};
171+
172+
## Content`;
173+
const output = removeImportExportStatements(input);
174+
expect(output).toContain('## Content');
175+
expect(output).not.toContain('export');
176+
expect(output).not.toContain('MyComponent');
177+
expect(output).not.toContain('const x = 1');
178+
});
179+
180+
it('should remove object exports with nested braces', () => {
181+
const input = `export const config = {
182+
nested: {
183+
value: 'test';
184+
}
185+
};
186+
187+
## Content`;
188+
const output = removeImportExportStatements(input);
189+
expect(output).toContain('## Content');
190+
expect(output).not.toContain('export');
191+
expect(output).not.toContain('config');
192+
expect(output).not.toContain('nested');
193+
});
98194
});
99195

100196
describe('removeScriptTags', () => {

data/onPostBuild/transpileMdxToMarkdown.ts

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,109 @@ interface FrontMatterAttributes {
3434

3535
/**
3636
* Remove import and export statements from content
37-
* Handles both single-line and multi-line statements
37+
* Uses a line-by-line parser that only removes import/export from the top of the file,
38+
* preserving import/export statements in code blocks later in the file
3839
*/
3940
function removeImportExportStatements(content: string): string {
40-
let result = content;
41+
const lines = content.split('\n');
42+
const result: string[] = [];
43+
let isInTopImportExportSection = true;
44+
let inMultiLineStatement: 'none' | 'import' | 'export' | 'export-function' = 'none';
45+
let braceDepth = 0;
46+
47+
for (const line of lines) {
48+
if (!isInTopImportExportSection) {
49+
// Once we're past the import/export section, keep everything
50+
result.push(line);
51+
continue;
52+
}
53+
54+
const trimmed = line.trim();
55+
56+
// Handle blank lines - skip them while in import/export section
57+
if (trimmed === '') {
58+
continue;
59+
}
4160

42-
// Remove import statements (single and multi-line)
43-
result = result
44-
.replace(/^import\s+[\s\S]*?from\s+['"][^'"]+['"];?\s*$/gm, '')
45-
.replace(/^import\s+['"][^'"]+['"];?\s*$/gm, '');
61+
// Check if we're continuing a multi-line statement
62+
if (inMultiLineStatement !== 'none') {
63+
if (inMultiLineStatement === 'export-function' || inMultiLineStatement === 'export') {
64+
// For any export with braces (functions, classes, arrow functions, etc.), track brace depth
65+
if (braceDepth > 0) {
66+
// Count opening and closing braces
67+
const openBraces = (line.match(/\{/g) || []).length;
68+
const closeBraces = (line.match(/\}/g) || []).length;
69+
braceDepth += openBraces - closeBraces;
70+
71+
// If we've closed all braces, we're done with this statement
72+
if (braceDepth === 0) {
73+
inMultiLineStatement = 'none';
74+
}
75+
} else {
76+
// No braces being tracked, look for semicolon or closing brace to end
77+
if (line.includes(';') || (line.includes('}') && !line.includes('{'))) {
78+
inMultiLineStatement = 'none';
79+
}
80+
}
81+
} else {
82+
// For regular import statements, look for semicolon or closing brace
83+
if (line.includes(';') || (line.includes('}') && !line.includes('{'))) {
84+
inMultiLineStatement = 'none';
85+
}
86+
}
87+
continue;
88+
}
4689

47-
// Remove export statements
48-
// Handle: export { foo, bar }; (single and multi-line)
49-
result = result
50-
.replace(/^export\s+\{[\s\S]*?\}\s*;?\s*$/gm, '')
51-
.replace(/^export\s+\{[\s\S]*?\}\s+from\s+['"][^'"]+['"];?\s*$/gm, '');
90+
// Check if line starts an import statement
91+
if (trimmed.startsWith('import ')) {
92+
// Detect if it's a complete single-line import or incomplete multi-line
93+
// Complete: has semicolon OR (has 'from' and ends with quote)
94+
const hasFrom = trimmed.includes(' from ');
95+
const endsWithQuote = trimmed.match(/['"][;]?\s*$/);
96+
const hasSemicolon = trimmed.includes(';');
97+
98+
if (!hasSemicolon && hasFrom && !endsWithQuote) {
99+
// Incomplete: multi-line import like "import {" without closing
100+
inMultiLineStatement = 'import';
101+
} else if (!hasSemicolon && !hasFrom) {
102+
// Incomplete: just "import" or "import {" at start of multi-line
103+
inMultiLineStatement = 'import';
104+
}
105+
// Otherwise it's complete (has semicolon, or has from+closing quote)
106+
continue;
107+
}
52108

53-
// Handle: export default Component; or export const foo = 'bar';
54-
result = result.replace(/^export\s+(default|const|let|var)\s+.*$/gm, '');
109+
// Check if line starts an export statement
110+
if (trimmed.startsWith('export ')) {
111+
// Detect export function/class (multi-line with braces)
112+
if (trimmed.match(/^export\s+(function|class)\s+/)) {
113+
inMultiLineStatement = 'export-function';
114+
// Count braces on this line
115+
const openBraces = (line.match(/\{/g) || []).length;
116+
const closeBraces = (line.match(/\}/g) || []).length;
117+
braceDepth = openBraces - closeBraces;
118+
// Check if it's all on one line (rare but possible)
119+
if (braceDepth === 0 && line.includes('}')) {
120+
inMultiLineStatement = 'none';
121+
}
122+
} else if (!line.includes(';') && line.includes('{') && !line.includes('}')) {
123+
// Multi-line export with braces (arrow functions, objects, etc.)
124+
inMultiLineStatement = 'export';
125+
// Initialize brace depth tracking
126+
const openBraces = (line.match(/\{/g) || []).length;
127+
const closeBraces = (line.match(/\}/g) || []).length;
128+
braceDepth = openBraces - closeBraces;
129+
}
130+
// Otherwise it's complete (has semicolon, or no braces)
131+
continue;
132+
}
55133

56-
// Handle: export function/class declarations (multi-line)
57-
// Match from 'export function/class' until the closing brace
58-
result = result.replace(/^export\s+(function|class)\s+\w+[\s\S]*?\n\}/gm, '');
134+
// First non-import/export line - we're done with the section
135+
isInTopImportExportSection = false;
136+
result.push(line);
137+
}
59138

60-
// Clean up extra blank lines left behind
61-
return result.replace(/\n\n\n+/g, '\n\n');
139+
return result.join('\n');
62140
}
63141

64142
/**

0 commit comments

Comments
 (0)