From dedb9e22987c2a9510a09567f47872a1b667f97c Mon Sep 17 00:00:00 2001 From: Aryan Sachin Date: Fri, 6 Nov 2026 02:31:24 +0530 Subject: [PATCH 1/2] =?UTF-8?q?=EF=BB=BFfix(utils):=20preserve=20trailing?= =?UTF-8?q?=20hash=20in=20markdown=20heading=20titles=20(#12644)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit According to CommonMark 0.31.2 section 4.2, an optional closing sequence of # characters on an ATX heading must be preceded by at least one space or tab. Previously, parseMarkdownContentTitle used `\s*(?:\{#*[\w-]+\}|#+)$` which matched trailing # characters even with zero preceding whitespace, causing document titles such as `# Learn C#` or `# Learn F#` to drop their trailing hash and become "Learn C" or "Learn F". This separates custom anchor ID removal from ATX closing sequence stripping and requires preceding whitespace (`\s+#+$`) for closing hashes, preserving words and identifiers ending in # (such as C# or F#). Closes #12644. Signed-off-by: Aryan Sachin --- .../src/__tests__/markdownUtils.test.ts | 54 +++++++++++++++++++ .../docusaurus-utils/src/markdownUtils.ts | 3 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts index 34648991ba35..a6c449851d3a 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts @@ -438,6 +438,60 @@ Lorem Ipsum }); }); + it('preserves trailing # when not preceded by space (e.g. C# or F#)', () => { + const markdownC = dedent` + + # Learn C# + + Lorem Ipsum + + `; + expect(parseMarkdownContentTitle(markdownC)).toEqual({ + content: markdownC, + contentTitle: 'Learn C#', + }); + + const markdownF = dedent` + + # Learn F# + + Lorem Ipsum + + `; + expect(parseMarkdownContentTitle(markdownF)).toEqual({ + content: markdownF, + contentTitle: 'Learn F#', + }); + }); + + it('preserves trailing # in title with custom anchor id', () => { + const markdown = dedent` + + # Learn C# {#learn-csharp} + + Lorem Ipsum + + `; + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown, + contentTitle: 'Learn C#', + }); + }); + + it('preserves trailing # in title with closing # sequence', () => { + const markdown = dedent` + + # Learn C# # + + Lorem Ipsum + + `; + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown, + contentTitle: 'Learn C#', + }); + }); + it('parses markdown h1 title at the top followed by h2 title', () => { const markdown = dedent` diff --git a/packages/docusaurus-utils/src/markdownUtils.ts b/packages/docusaurus-utils/src/markdownUtils.ts index 3a82a52cbb6e..8206de260c6a 100644 --- a/packages/docusaurus-utils/src/markdownUtils.ts +++ b/packages/docusaurus-utils/src/markdownUtils.ts @@ -334,7 +334,8 @@ export function parseMarkdownContentTitle( contentTitle: toTextContentTitle( regularTitleMatch .groups!.title!.trim() - .replace(/\s*(?:\{#*[\w-]+\}|#+)$/, ''), + .replace(/\s*\{#*[\w-]+\}$/, '') + .replace(/\s+#+$/, ''), ).trim(), }; } From faf439ec1b663e37aba4432613f09c5ae5add7bb Mon Sep 17 00:00:00 2001 From: Aryan Sachin Date: Sat, 7 Nov 2026 13:05:04 +0530 Subject: [PATCH 2/2] fix(utils): handle heading titles with closing hashes or custom IDs (#12644) Signed-off-by: Aryan Sachin --- .../src/__tests__/markdownUtils.test.ts | 28 +++++++++++++++++++ .../docusaurus-utils/src/markdownUtils.ts | 4 +-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts index a6c449851d3a..b24f8185c651 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts @@ -492,6 +492,34 @@ Lorem Ipsum }); }); + it('handles empty heading consisting only of closing hashes', () => { + const markdown = dedent` + + # ### + + Lorem Ipsum + + `; + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown, + contentTitle: '', + }); + }); + + it('preserves literal trailing hash when followed by custom heading id', () => { + const markdown = dedent` + + # Title # {#id} + + Lorem Ipsum + + `; + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown, + contentTitle: 'Title #', + }); + }); + it('parses markdown h1 title at the top followed by h2 title', () => { const markdown = dedent` diff --git a/packages/docusaurus-utils/src/markdownUtils.ts b/packages/docusaurus-utils/src/markdownUtils.ts index 8206de260c6a..53719f820c4b 100644 --- a/packages/docusaurus-utils/src/markdownUtils.ts +++ b/packages/docusaurus-utils/src/markdownUtils.ts @@ -334,8 +334,8 @@ export function parseMarkdownContentTitle( contentTitle: toTextContentTitle( regularTitleMatch .groups!.title!.trim() - .replace(/\s*\{#*[\w-]+\}$/, '') - .replace(/\s+#+$/, ''), + .replace(/(?:^|\s+)#+$/, '') + .replace(/\s*\{#*[\w-]+\}$/, ''), ).trim(), }; }