Skip to content

Commit 7fb4414

Browse files
committed
fix: IDs may conflict
1 parent a4d07f7 commit 7fb4414

2 files changed

Lines changed: 17 additions & 2 deletions

File tree

server/utils/readme.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ function withUserContentPrefix(value: string): string {
272272
return value.startsWith(USER_CONTENT_PREFIX) ? value : `${USER_CONTENT_PREFIX}${value}`
273273
}
274274

275+
function toUserContentId(value: string): string {
276+
return `${USER_CONTENT_PREFIX}${value}`
277+
}
278+
275279
function toUserContentHash(value: string): string {
276280
return `#${withUserContentPrefix(value)}`
277281
}
@@ -446,13 +450,13 @@ export async function renderReadmeHtml(
446450
const count = usedSlugs.get(slug) ?? 0
447451
usedSlugs.set(slug, count + 1)
448452
const uniqueSlug = count === 0 ? slug : `${slug}-${count}`
449-
const id = withUserContentPrefix(uniqueSlug)
453+
const id = toUserContentId(uniqueSlug)
450454

451455
if (plainText) {
452456
toc.push({ text: plainText, id, depth })
453457
}
454458

455-
return `<h${semanticLevel} id="${id}" data-level="${depth}"><a href="#${uniqueSlug}">${plainText}</a></h${semanticLevel}>\n`
459+
return `<h${semanticLevel} id="${id}" data-level="${depth}"><a href="#${id}">${plainText}</a></h${semanticLevel}>\n`
456460
}
457461

458462
renderer.heading = function ({ tokens, depth }: Tokens.Heading) {

test/unit/server/utils/readme.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,17 @@ describe('Issue #1323 — single-pass rendering correctness', () => {
642642
expect(ids).toEqual(['user-content-api', 'user-content-api-1', 'user-content-api-2'])
643643
})
644644

645+
it('does not collide when heading text already starts with user-content-', async () => {
646+
const md = ['# Title', '', '# user-content-title'].join('\n')
647+
648+
const result = await renderReadmeHtml(md, 'test-pkg')
649+
650+
const ids = Array.from(result.html.matchAll(/id="(user-content-[^"]+)"/g), m => m[1])
651+
expect(ids).toEqual(['user-content-title', 'user-content-user-content-title'])
652+
expect(new Set(ids).size).toBe(ids.length)
653+
expect(result.toc.map(t => t.id)).toEqual(ids)
654+
})
655+
645656
it('heading semantic levels are sequential even when mixing heading types', async () => {
646657
// h1 (md) → h3, h3 (html) → should be h4 (max = lastSemantic + 1),
647658
// not jump to h5 or h6 because it was processed in a later pass.

0 commit comments

Comments
 (0)