Skip to content

Commit d5509e3

Browse files
authored
refactor(mdx-loader): improve remark heading plugin unit tests (#11754)
* migrate mdx unit tests to support MDX syntax instead of commonmark * improve test structure * improve test structure * add md/mdx format tests for headingIds
1 parent 49619fd commit d5509e3

File tree

1 file changed

+92
-63
lines changed
  • packages/docusaurus-mdx-loader/src/remark/headings/__tests__

1 file changed

+92
-63
lines changed

packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.ts

Lines changed: 92 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,39 @@ import u from 'unist-builder';
1111
import {removePosition} from 'unist-util-remove-position';
1212
import {toString} from 'mdast-util-to-string';
1313
import {visit} from 'unist-util-visit';
14+
import {escapeMarkdownHeadingIds} from '@docusaurus/utils';
1415
import plugin from '../index';
1516
import type {PluginOptions} from '../index';
1617
import type {Plugin} from 'unified';
1718
import type {Parent} from 'unist';
19+
import type {Root} from 'mdast';
1820

1921
async function process(
20-
doc: string,
22+
input: string,
2123
plugins: Plugin[] = [],
2224
options: PluginOptions = {anchorsMaintainCase: false},
23-
) {
25+
format: 'md' | 'mdx' = 'mdx',
26+
): Promise<Root> {
2427
const {remark} = await import('remark');
25-
const processor = await remark().use({
26-
plugins: [...plugins, [plugin, options]],
28+
29+
let content = input;
30+
let formatPlugins: Plugin[] = [];
31+
32+
if (format === 'mdx') {
33+
const {default: mdx} = await import('remark-mdx');
34+
// Preprocess the input to support our invalid heading ids syntax
35+
content = escapeMarkdownHeadingIds(input);
36+
formatPlugins = [mdx];
37+
}
38+
39+
const processor = remark().use({
40+
plugins: [...formatPlugins, ...plugins, [plugin, options]],
2741
});
28-
const result = await processor.run(processor.parse(doc));
42+
43+
const result = await processor.run(processor.parse(content));
2944
removePosition(result, {force: true});
30-
return result;
45+
46+
return result as unknown as Root;
3147
}
3248

3349
function heading(label: string | null, id: string) {
@@ -236,6 +252,7 @@ describe('headings remark plugin', () => {
236252
const result = await process(
237253
'# <span class="normal-header">Normal</span>\n',
238254
);
255+
239256
const expected = u('root', [
240257
u(
241258
'heading',
@@ -244,80 +261,92 @@ describe('headings remark plugin', () => {
244261
data: {hProperties: {id: 'normal'}, id: 'normal'},
245262
},
246263
[
247-
u('html', '<span class="normal-header">'),
248-
u('text', 'Normal'),
249-
u('html', '</span>'),
264+
u('mdxJsxTextElement', {
265+
name: 'span',
266+
attributes: [
267+
u('mdxJsxAttribute', {
268+
name: 'class',
269+
value: 'normal-header',
270+
}),
271+
],
272+
children: [u('text', 'Normal')],
273+
}),
250274
],
251275
),
252276
]);
253277

254278
expect(result).toEqual(expected);
255279
});
256280

257-
it('creates custom headings ids', async () => {
258-
const result = await process(`
259-
# Heading One {#custom_h1}
281+
describe('creates custom headings ids', () => {
282+
async function headingIdFor(input: string, format: 'md' | 'mdx' = 'mdx') {
283+
const result = await process(
284+
input,
285+
[],
286+
{anchorsMaintainCase: false},
287+
format,
288+
);
289+
const headers: {text: string; id: string}[] = [];
290+
visit(result, 'heading', (node) => {
291+
headers.push({
292+
text: toString(node),
293+
id: (node.data! as {id: string}).id,
294+
});
295+
});
296+
expect(headers).toHaveLength(1);
297+
return headers[0]!.id;
298+
}
260299

261-
## Heading Two {#custom-heading-two}
300+
describe('historical syntax', () => {
301+
// Shared test because it's the same syntax for both md and mdx
302+
async function testHeadingIds(format: 'md' | 'mdx') {
303+
await expect(
304+
headingIdFor('# Heading One {#custom_h1}', format),
305+
).resolves.toEqual('custom_h1');
306+
await expect(
307+
headingIdFor('## Heading Two {#custom-heading-two}', format),
308+
).resolves.toEqual('custom-heading-two');
262309

263-
# With *Bold* {#custom-with-bold}
310+
await expect(
311+
headingIdFor('# With *Bold* {#custom-with-bold}', format),
312+
).resolves.toEqual('custom-with-bold');
264313

265-
# With *Bold* hello{#custom-with-bold-hello}
314+
await expect(
315+
headingIdFor('# With *Bold* hello{#custom-with-bold-hello}', format),
316+
).resolves.toEqual('custom-with-bold-hello');
266317

267-
# With *Bold* hello2 {#custom-with-bold-hello2}
318+
await expect(
319+
headingIdFor(
320+
'# With *Bold* hello2 {#custom-with-bold-hello2}',
321+
format,
322+
),
323+
).resolves.toEqual('custom-with-bold-hello2');
268324

269-
# Snake-cased ID {#this_is_custom_id}
325+
await expect(
326+
headingIdFor('# Snake-cased ID {#this_is_custom_id}', format),
327+
).resolves.toEqual('this_is_custom_id');
270328

271-
# No custom ID
329+
await expect(headingIdFor('# No custom ID', format)).resolves.toEqual(
330+
'no-custom-id',
331+
);
272332

273-
# {#id-only}
333+
await expect(headingIdFor('# {#id-only}', format)).resolves.toEqual(
334+
'id-only',
335+
);
274336

275-
# {#text-after} custom ID
276-
`);
337+
// in this case, we don't parse the heading id: the id is the text slug
338+
await expect(
339+
headingIdFor('# {#text-after} custom ID', format),
340+
).resolves.toEqual('text-after-custom-id');
341+
}
342+
it('works for format CommonMark', async () => {
343+
await testHeadingIds('md');
344+
});
277345

278-
const headers: {text: string; id: string}[] = [];
279-
visit(result, 'heading', (node) => {
280-
headers.push({text: toString(node), id: node.data!.id as string});
346+
it('works for format MDX', async () => {
347+
await testHeadingIds('mdx');
348+
});
281349
});
282-
283-
expect(headers).toEqual([
284-
{
285-
id: 'custom_h1',
286-
text: 'Heading One',
287-
},
288-
{
289-
id: 'custom-heading-two',
290-
text: 'Heading Two',
291-
},
292-
{
293-
id: 'custom-with-bold',
294-
text: 'With Bold',
295-
},
296-
{
297-
id: 'custom-with-bold-hello',
298-
text: 'With Bold hello',
299-
},
300-
{
301-
id: 'custom-with-bold-hello2',
302-
text: 'With Bold hello2',
303-
},
304-
{
305-
id: 'this_is_custom_id',
306-
text: 'Snake-cased ID',
307-
},
308-
{
309-
id: 'no-custom-id',
310-
text: 'No custom ID',
311-
},
312-
{
313-
id: 'id-only',
314-
text: '',
315-
},
316-
{
317-
id: 'text-after-custom-id',
318-
text: '{#text-after} custom ID',
319-
},
320-
]);
321350
});
322351

323352
it('preserve anchors case then "anchorsMaintainCase" option is set', async () => {

0 commit comments

Comments
 (0)