Skip to content

Commit d59c95b

Browse files
committed
Add support for .md URLs
1 parent 83c1456 commit d59c95b

File tree

7 files changed

+884
-115
lines changed

7 files changed

+884
-115
lines changed

docusaurus.config.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import rehypeCodeblockMeta from './src/plugins/rehype-codeblock-meta.ts';
99
import rehypeStaticToDynamic from './src/plugins/rehype-static-to-dynamic.ts';
1010
import rehypeVideoAspectRatio from './src/plugins/rehype-video-aspect-ratio.ts';
1111
import remarkNpm2Yarn from './src/plugins/remark-npm2yarn.ts';
12-
import remarkRawMarkdown from './src/plugins/remark-raw-markdown.ts';
1312
import darkTheme from './src/themes/react-navigation-dark';
1413
import lightTheme from './src/themes/react-navigation-light';
1514

@@ -175,7 +174,7 @@ const config: Config = {
175174
},
176175
breadcrumbs: false,
177176
sidebarCollapsed: false,
178-
remarkPlugins: [remarkRawMarkdown, [remarkNpm2Yarn, { sync: true }]],
177+
remarkPlugins: [[remarkNpm2Yarn, { sync: true }]],
179178
rehypePlugins: [
180179
[
181180
rehypeCodeblockMeta,
@@ -186,10 +185,10 @@ const config: Config = {
186185
],
187186
},
188187
blog: {
189-
remarkPlugins: [remarkRawMarkdown, [remarkNpm2Yarn, { sync: true }]],
188+
remarkPlugins: [[remarkNpm2Yarn, { sync: true }]],
190189
},
191190
pages: {
192-
remarkPlugins: [remarkRawMarkdown, [remarkNpm2Yarn, { sync: true }]],
191+
remarkPlugins: [[remarkNpm2Yarn, { sync: true }]],
193192
},
194193
theme: {
195194
customCss: './src/css/custom.css',
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import dedent from 'dedent';
2+
import assert from 'node:assert';
3+
import { describe, test } from 'node:test';
4+
import { processMarkdown } from '../plugins/process-markdown.ts';
5+
6+
describe('processMarkdown', () => {
7+
test('strips MDX import statements', async () => {
8+
const input = dedent`
9+
import Tabs from '@theme/Tabs';
10+
import TabItem from '@theme/TabItem';
11+
12+
Some content here.
13+
`;
14+
15+
const { content: result } = await processMarkdown(input);
16+
assert.ok(!result.includes('import'));
17+
assert.ok(result.includes('Some content here.'));
18+
});
19+
20+
test('transforms Tabs with two TabItems into labeled sections', async () => {
21+
const input = dedent`
22+
Before tabs.
23+
24+
<Tabs groupId="config" queryString="config">
25+
<TabItem value="static" label="Static" default>
26+
27+
Static content here.
28+
29+
</TabItem>
30+
<TabItem value="dynamic" label="Dynamic">
31+
32+
Dynamic content here.
33+
34+
</TabItem>
35+
</Tabs>
36+
37+
After tabs.
38+
`;
39+
40+
const { content: result } = await processMarkdown(input);
41+
42+
assert.ok(!result.includes('<Tabs'));
43+
assert.ok(!result.includes('<TabItem'));
44+
assert.ok(!result.includes('</Tabs>'));
45+
assert.ok(!result.includes('</TabItem>'));
46+
assert.ok(result.includes('**Static:**'));
47+
assert.ok(result.includes('Static content here.'));
48+
assert.ok(result.includes('**Dynamic:**'));
49+
assert.ok(result.includes('Dynamic content here.'));
50+
assert.ok(result.includes('Before tabs.'));
51+
assert.ok(result.includes('After tabs.'));
52+
});
53+
54+
test('transforms nested Tabs', async () => {
55+
const input = dedent`
56+
<Tabs groupId='framework' queryString="framework">
57+
<TabItem value='expo' label='Expo' default>
58+
59+
Expo instructions.
60+
61+
</TabItem>
62+
<TabItem value='community-cli' label='Community CLI'>
63+
64+
CLI instructions.
65+
66+
<Tabs>
67+
<TabItem value='kotlin' label='Kotlin' default>
68+
69+
Kotlin code.
70+
71+
</TabItem>
72+
<TabItem value='java' label='Java'>
73+
74+
Java code.
75+
76+
</TabItem>
77+
</Tabs>
78+
79+
More CLI content.
80+
81+
</TabItem>
82+
</Tabs>
83+
`;
84+
85+
const { content: result } = await processMarkdown(input);
86+
87+
assert.ok(!result.includes('<Tabs'));
88+
assert.ok(!result.includes('<TabItem'));
89+
assert.ok(result.includes('**Expo:**'));
90+
assert.ok(result.includes('Expo instructions.'));
91+
assert.ok(result.includes('**Community CLI:**'));
92+
assert.ok(result.includes('CLI instructions.'));
93+
assert.ok(result.includes('**Kotlin:**'));
94+
assert.ok(result.includes('Kotlin code.'));
95+
assert.ok(result.includes('**Java:**'));
96+
assert.ok(result.includes('Java code.'));
97+
});
98+
99+
test('handles single TabItem without label', async () => {
100+
const input = dedent`
101+
<Tabs>
102+
<TabItem value="only" label="Only Option">
103+
104+
Only content.
105+
106+
</TabItem>
107+
</Tabs>
108+
`;
109+
110+
const { content: result } = await processMarkdown(input);
111+
112+
assert.ok(!result.includes('<Tabs'));
113+
// Single tab should not have a label
114+
assert.ok(!result.includes('**Only Option:**'));
115+
assert.ok(result.includes('Only content.'));
116+
});
117+
118+
test('transforms static2dynamic code fences', async () => {
119+
const input = dedent`
120+
Before code.
121+
122+
\`\`\`js name="Example" snack static2dynamic
123+
import * as React from 'react';
124+
import { createStaticNavigation } from '@react-navigation/native';
125+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
126+
127+
function HomeScreen() {
128+
return null;
129+
}
130+
131+
const MyStack = createNativeStackNavigator({
132+
screens: {
133+
Home: HomeScreen,
134+
},
135+
});
136+
137+
const Navigation = createStaticNavigation(MyStack);
138+
139+
export default function App() {
140+
return <Navigation />;
141+
}
142+
\`\`\`
143+
144+
After code.
145+
`;
146+
147+
const { content: result } = await processMarkdown(input);
148+
149+
assert.ok(result.includes('**Static:**'));
150+
assert.ok(result.includes('**Dynamic:**'));
151+
// The dynamic version should have NavigationContainer
152+
assert.ok(result.includes('NavigationContainer'));
153+
// Meta attributes should be stripped from fence lines
154+
assert.ok(!result.includes('static2dynamic'));
155+
// Check that 'snack' doesn't appear in fence meta (it may appear in code content)
156+
assert.ok(!result.match(/^```\w*.*snack/m));
157+
assert.ok(!result.match(/^```\w*.*name=/m));
158+
assert.ok(result.includes('Before code.'));
159+
assert.ok(result.includes('After code.'));
160+
});
161+
162+
test('strips code fence meta attributes', async () => {
163+
const input = dedent`
164+
\`\`\`js name="Test" snack
165+
const x = 1;
166+
\`\`\`
167+
168+
\`\`\`bash npm2yarn
169+
npm install something
170+
\`\`\`
171+
`;
172+
173+
const { content: result } = await processMarkdown(input);
174+
175+
assert.ok(!result.includes('name='));
176+
assert.ok(!result.includes('snack'));
177+
assert.ok(!result.includes('npm2yarn'));
178+
assert.ok(result.includes('```js'));
179+
assert.ok(result.includes('```bash'));
180+
assert.ok(result.includes('const x = 1;'));
181+
assert.ok(result.includes('npm install something'));
182+
});
183+
184+
test('converts admonitions to blockquotes', async () => {
185+
const input = dedent`
186+
Some text.
187+
188+
:::warning
189+
190+
This is a warning.
191+
192+
::::
193+
194+
More text.
195+
`;
196+
197+
const { content: result } = await processMarkdown(input);
198+
199+
assert.ok(!result.includes(':::'));
200+
assert.ok(result.includes('> **Warning:**'));
201+
assert.ok(result.includes('> This is a warning.'));
202+
assert.ok(result.includes('Some text.'));
203+
assert.ok(result.includes('More text.'));
204+
});
205+
206+
test('converts info admonitions', async () => {
207+
const input = dedent`
208+
:::info
209+
210+
Some info here.
211+
212+
::::
213+
`;
214+
215+
const { content: result } = await processMarkdown(input);
216+
217+
assert.ok(result.includes('> **Info:**'));
218+
assert.ok(result.includes('> Some info here.'));
219+
});
220+
221+
test('cleans up extra blank lines', async () => {
222+
const input = 'Line 1.\n\n\n\n\nLine 2.';
223+
const { content: result } = await processMarkdown(input);
224+
225+
assert.ok(!result.includes('\n\n\n'));
226+
assert.ok(result.includes('Line 1.\n\nLine 2.'));
227+
});
228+
229+
test('handles Tabs with code blocks inside', async () => {
230+
const input = dedent`
231+
<Tabs groupId="config" queryString="config">
232+
<TabItem value="static" label="Static" default>
233+
234+
\`\`\`js
235+
const x = createStackNavigator({});
236+
\`\`\`
237+
238+
</TabItem>
239+
<TabItem value="dynamic" label="Dynamic">
240+
241+
\`\`\`js
242+
function MyStack() {
243+
return <Stack.Navigator />;
244+
}
245+
\`\`\`
246+
247+
</TabItem>
248+
</Tabs>
249+
`;
250+
251+
const { content: result } = await processMarkdown(input);
252+
253+
assert.ok(result.includes('**Static:**'));
254+
assert.ok(result.includes('createStackNavigator'));
255+
assert.ok(result.includes('**Dynamic:**'));
256+
assert.ok(result.includes('Stack.Navigator'));
257+
assert.ok(!result.includes('<Tabs'));
258+
});
259+
260+
test('preserves content outside of MDX constructs', async () => {
261+
const input = dedent`
262+
# Title
263+
264+
Regular paragraph with **bold** and [links](https://example.com).
265+
266+
- List item 1
267+
- List item 2
268+
269+
> A blockquote.
270+
`;
271+
272+
const { content: result } = await processMarkdown(input);
273+
274+
assert.strictEqual(result, input);
275+
});
276+
277+
test('strips frontmatter and returns parsed data', async () => {
278+
const input = dedent`
279+
---
280+
id: getting-started
281+
title: Getting started
282+
description: Learn how to get started
283+
---
284+
285+
Some content here.
286+
`;
287+
288+
const { frontmatter, content } = await processMarkdown(input);
289+
290+
assert.strictEqual(frontmatter.title, 'Getting started');
291+
assert.strictEqual(frontmatter.description, 'Learn how to get started');
292+
assert.strictEqual(frontmatter.id, 'getting-started');
293+
assert.ok(!content.includes('---'));
294+
assert.ok(content.includes('Some content here.'));
295+
});
296+
});

0 commit comments

Comments
 (0)