Skip to content

Commit dcda67f

Browse files
committed
fix: heading & anchor for ALLOW_ATTR
1 parent 84272f9 commit dcda67f

File tree

1 file changed

+20
-7
lines changed

1 file changed

+20
-7
lines changed

server/utils/readme.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,16 @@ function toUserContentHash(value: string): string {
280280
return `#${withUserContentPrefix(value)}`
281281
}
282282

283+
function normalizePreservedAnchorAttrs(attrs: string): string {
284+
const cleanedAttrs = attrs
285+
.replace(/\s+href\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
286+
.replace(/\s+rel\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
287+
.replace(/\s+target\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
288+
.trim()
289+
290+
return cleanedAttrs ? ` ${cleanedAttrs}` : ''
291+
}
292+
283293
const isNpmJsUrlThatCanBeRedirected = (url: URL) => {
284294
if (!npmJsHosts.has(url.host)) {
285295
return false
@@ -440,7 +450,7 @@ export async function renderReadmeHtml(
440450
let lastSemanticLevel = 2 // Start after h2 (the "Readme" section heading)
441451

442452
// Shared heading processing for both markdown and HTML headings
443-
function processHeading(depth: number, plainText: string) {
453+
function processHeading(depth: number, plainText: string, preservedAttrs = '') {
444454
const semanticLevel = calculateSemanticDepth(depth, lastSemanticLevel)
445455
lastSemanticLevel = semanticLevel
446456

@@ -456,7 +466,7 @@ export async function renderReadmeHtml(
456466
toc.push({ text: plainText, id, depth })
457467
}
458468

459-
return `<h${semanticLevel} id="${id}" data-level="${depth}"><a href="#${id}">${plainText}</a></h${semanticLevel}>\n`
469+
return `<h${semanticLevel} id="${id}" data-level="${depth}"${preservedAttrs}><a href="#${id}">${plainText}</a></h${semanticLevel}>\n`
460470
}
461471

462472
renderer.heading = function ({ tokens, depth }: Tokens.Heading) {
@@ -468,18 +478,21 @@ export async function renderReadmeHtml(
468478
// Intercept HTML headings so they get id, TOC entry, and correct semantic level.
469479
// Also intercept raw HTML <a> tags so playground links are collected in the same pass.
470480
const htmlHeadingRe = /<h([1-6])(\s[^>]*)?>([\s\S]*?)<\/h\1>/gi
471-
const htmlAnchorRe = /<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi
481+
const htmlAnchorRe = /<a(\s[^>]*?)href=(["'])([^"']*)\2([^>]*)>([\s\S]*?)<\/a>/gi
472482
renderer.html = function ({ text }: Tokens.HTML) {
473-
let result = text.replace(htmlHeadingRe, (_, level, _attrs, inner) => {
483+
let result = text.replace(htmlHeadingRe, (_, level, attrs, inner) => {
474484
const depth = parseInt(level)
475485
const plainText = decodeHtmlEntities(stripHtmlTags(inner).trim())
476-
return processHeading(depth, plainText).trimEnd()
486+
const align = /\balign=(["'])(.*?)\1/i.exec(attrs)?.[2]
487+
const preservedAttrs = align ? ` align="${align}"` : ''
488+
return processHeading(depth, plainText, preservedAttrs).trimEnd()
477489
})
478490
// Process raw HTML <a> tags for playground link collection and URL resolution
479-
result = result.replace(htmlAnchorRe, (_full, href, inner) => {
491+
result = result.replace(htmlAnchorRe, (_full, beforeHref, _quote, href, afterHref, inner) => {
480492
const label = decodeHtmlEntities(stripHtmlTags(inner).trim())
481493
const { resolvedHref, extraAttrs } = processLink(href, label)
482-
return `<a href="${resolvedHref}"${extraAttrs}>${inner}</a>`
494+
const preservedAttrs = normalizePreservedAnchorAttrs(`${beforeHref ?? ''}${afterHref ?? ''}`)
495+
return `<a${preservedAttrs} href="${resolvedHref}"${extraAttrs}>${inner}</a>`
483496
})
484497
return result
485498
}

0 commit comments

Comments
 (0)