@@ -157,6 +157,25 @@ const ALLOWED_ATTR: Record<string, string[]> = {
157157// GitHub-style callout types
158158// Format: > [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION]
159159
160+ /**
161+ * Generate a GitHub-style slug from heading text.
162+ * - Convert to lowercase
163+ * - Remove HTML tags
164+ * - Replace spaces with hyphens
165+ * - Remove special characters (keep alphanumeric, hyphens, underscores)
166+ * - Collapse multiple hyphens
167+ */
168+ function slugify ( text : string ) : string {
169+ return text
170+ . replace ( / < [ ^ > ] * > / g, '' ) // Strip HTML tags
171+ . toLowerCase ( )
172+ . trim ( )
173+ . replace ( / \s + / g, '-' ) // Spaces to hyphens
174+ . replace ( / [ ^ \w \u4e00 - \u9fff \u3040 - \u309f \u30a0 - \u30ff - ] / g, '' ) // Keep alphanumeric, CJK, hyphens
175+ . replace ( / - + / g, '-' ) // Collapse multiple hyphens
176+ . replace ( / ^ - | - $ / g, '' ) // Trim leading/trailing hyphens
177+ }
178+
160179/**
161180 * Resolve a relative URL to an absolute URL.
162181 * If repository info is available, resolve to provider's raw file URLs.
@@ -240,6 +259,9 @@ export async function renderReadmeHtml(
240259 const collectedLinks : PlaygroundLink [ ] = [ ]
241260 const seenUrls = new Set < string > ( )
242261
262+ // Track used heading slugs to handle duplicates (GitHub-style: foo, foo-1, foo-2)
263+ const usedSlugs = new Map < string , number > ( )
264+
243265 // Track heading hierarchy to ensure sequential order for accessibility
244266 // Page h1 = package name, h2 = "Readme" section heading
245267 // So README starts at h3, and we ensure no levels are skipped
@@ -262,7 +284,17 @@ export async function renderReadmeHtml(
262284
263285 lastSemanticLevel = semanticLevel
264286 const text = this . parser . parseInline ( tokens )
265- return `<h${ semanticLevel } data-level="${ depth } ">${ text } </h${ semanticLevel } >\n`
287+
288+ // Generate GitHub-style slug for anchor links
289+ let slug = slugify ( text )
290+ if ( ! slug ) slug = 'heading' // Fallback for empty headings
291+
292+ // Handle duplicate slugs (GitHub-style: foo, foo-1, foo-2)
293+ const count = usedSlugs . get ( slug ) ?? 0
294+ usedSlugs . set ( slug , count + 1 )
295+ const id = count === 0 ? slug : `${ slug } -${ count } `
296+
297+ return `<h${ semanticLevel } id="${ id } " data-level="${ depth } ">${ text } </h${ semanticLevel } >\n`
266298 }
267299
268300 // Syntax highlighting for code blocks (uses shared highlighter)
0 commit comments