@@ -11,23 +11,39 @@ import u from 'unist-builder';
1111import { removePosition } from 'unist-util-remove-position' ;
1212import { toString } from 'mdast-util-to-string' ;
1313import { visit } from 'unist-util-visit' ;
14+ import { escapeMarkdownHeadingIds } from '@docusaurus/utils' ;
1415import plugin from '../index' ;
1516import type { PluginOptions } from '../index' ;
1617import type { Plugin } from 'unified' ;
1718import type { Parent } from 'unist' ;
19+ import type { Root } from 'mdast' ;
1820
1921async 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
3349function 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