diff --git a/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts index 2f4baa74ccd6..42df2a38d4f1 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts @@ -833,6 +833,53 @@ describe('parseMarkdownHeadingId', () => { id: 'id', }); }); + + it('does not parse empty id', () => { + expect(parseMarkdownHeadingId('## a {#}')).toEqual({ + text: '## a {#}', + id: undefined, + }); + }); + + it('can parse id with more characters', () => { + expect(parseMarkdownHeadingId('## a {#你好}')).toEqual({ + text: '## a', + id: '你好', + }); + + expect(parseMarkdownHeadingId('## a {#2022.1.1}')).toEqual({ + text: '## a', + id: '2022.1.1', + }); + + expect(parseMarkdownHeadingId('## a {#a#b}')).toEqual({ + text: '## a', + id: 'a#b', + }); + }); + + // The actual behavior is unspecified, just need to ensure it stays consistent + it('handles unmatched boundaries', () => { + expect(parseMarkdownHeadingId('## a {# a {#bcd}')).toEqual({ + text: '## a {# a', + id: 'bcd', + }); + + expect(parseMarkdownHeadingId('## a {#bcd}}')).toEqual({ + text: '## a {#bcd}}', + id: undefined, + }); + + expect(parseMarkdownHeadingId('## a {#b{cd}')).toEqual({ + text: '## a', + id: 'b{cd', + }); + + expect(parseMarkdownHeadingId('## a {#b{#b}')).toEqual({ + text: '## a {#b', + id: 'b', + }); + }); }); describe('writeMarkdownHeadingId', () => { diff --git a/packages/docusaurus-utils/src/markdownUtils.ts b/packages/docusaurus-utils/src/markdownUtils.ts index fd11f5e66708..512a09bf916d 100644 --- a/packages/docusaurus-utils/src/markdownUtils.ts +++ b/packages/docusaurus-utils/src/markdownUtils.ts @@ -14,8 +14,8 @@ import {createSlugger, type Slugger, type SluggerOptions} from './slugger'; // content. Most parsing is still done in MDX through the mdx-loader. /** - * Parses custom ID from a heading. The ID must be composed of letters, - * underscores, and dashes only. + * Parses custom ID from a heading. The ID can contain any characters except + * `{#` and `}`. * * @param heading e.g. `## Some heading {#some-heading}` where the last * character must be `}` for the ID to be recognized @@ -26,9 +26,9 @@ export function parseMarkdownHeadingId(heading: string): { */ text: string; /** The heading ID. e.g. `some-heading` */ - id?: string; + id: string | undefined; } { - const customHeadingIdRegex = /\s*\{#(?[\w-]+)\}$/; + const customHeadingIdRegex = /\s*\{#(?(?:.(?!\{#|\}))*.)\}$/; const matches = customHeadingIdRegex.exec(heading); if (matches) { return {