From 57ba489c896cff5eb6096ddf886bca26d52e481d Mon Sep 17 00:00:00 2001 From: Menard Rosal Date: Wed, 5 Aug 2026 10:30:00 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(utils):=20handle=20hash=20inside?= =?UTF-8?q?=20heading=20text=20when=20creating=20excerpt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The excerpt auto-generated from a page's first H1 heading was incorrect when the heading text contained a hash character (e.g. "C#", "F#"). - "# C# Programming Guide" produced "Programming Guide" instead of skipping the heading and using the first paragraph - The old regexes used a negated character class that stopped at any interior hash, treating it as a closing ATX marker Two regexes in createExcerpt() are fixed: 1. Single-hash title removal now matches the whole line so headings containing "C#"/"F#" are skipped entirely like any other H1 2. The ATX heading marker stripper now allows hash characters inside the heading text and only strips an optional trailing run of hashes anchored at the end of the line Also add regression tests for C#/F# headings, trailing closing hashes, and hash characters in plain paragraph text. Closes #12305 --- .../src/__tests__/markdownUtils.test.ts | 32 +++++++++++++++++++ .../docusaurus-utils/src/markdownUtils.ts | 11 +++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts index a81e8cfff103..ba03831c7575 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts @@ -47,6 +47,38 @@ describe('createExcerpt', () => { ); }); + it('skips H1 heading whose text contains a hash character (e.g. "C#", "F#")', () => { + expect( + createExcerpt(dedent` + + # C# Programming Guide + + This paragraph should become the description. + `), + ).toBe('This paragraph should become the description.'); + }); + + it('skips H1 heading with trailing closing hashes and interior hash', () => { + expect( + createExcerpt(dedent` + + # F# Programming Guide # + + This paragraph should become the description. + `), + ).toBe('This paragraph should become the description.'); + }); + + it('keeps hash characters inside non-heading text', () => { + expect( + createExcerpt(dedent` + The language C# is used with .NET, and F# is a functional alternative. + + Nunc porttitor libero nec vulputate venenatis. + `), + ).toBe('The language C# is used with .NET, and F# is a functional alternative.'); + }); + it('creates excerpt for regular content with alternate title', () => { expect( createExcerpt(dedent` diff --git a/packages/docusaurus-utils/src/markdownUtils.ts b/packages/docusaurus-utils/src/markdownUtils.ts index 216d0122e7ad..700ee9c7fb1c 100644 --- a/packages/docusaurus-utils/src/markdownUtils.ts +++ b/packages/docusaurus-utils/src/markdownUtils.ts @@ -145,10 +145,15 @@ export function createExcerpt(fileString: string): string | undefined { const cleanedLine = fileLine // Remove HTML tags. .replace(/<[^>]*>/g, '') - // Remove Title headers - .replace(/^#[^#]+#?/gm, '') + // Remove Title headers (single-# ATX headings are skipped entirely) + // Note: the heading text may legitimately contain "#" (e.g. "C#", "F#"), + // so we must match the whole line instead of using a negated class + .replace(/^#.*$/gm, '') // Remove Markdown + ATX-style headers - .replace(/^#{1,6}\s*(?[^#]*?)\s*#{0,6}/gm, '$1') + // Note: we must not forbid "#" inside the heading text (e.g. "C#", "F#") + // Only the leading marker and an optional trailing closing sequence are + // stripped, anchored to the end of the line to avoid eating interior "#" + .replace(/^#{1,6}\s*(?.*?)\s*#*$/gm, '$1') // Remove emphasis. .replace(/(?[*_]{1,3})(?.*?)\1/g, '$2') // Remove strikethroughs.