Skip to content

Commit 48a7a87

Browse files
committed
fix: add slugify pattern
1 parent 8a11728 commit 48a7a87

1 file changed

Lines changed: 33 additions & 1 deletion

File tree

server/utils/readme.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)