@@ -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 + h r e f \s * = \s * ( " [ ^ " ] * " | ' [ ^ ' ] * ' | [ ^ \s > ] + ) / gi, '' )
286+ . replace ( / \s + r e l \s * = \s * ( " [ ^ " ] * " | ' [ ^ ' ] * ' | [ ^ \s > ] + ) / gi, '' )
287+ . replace ( / \s + t a r g e t \s * = \s * ( " [ ^ " ] * " | ' [ ^ ' ] * ' | [ ^ \s > ] + ) / gi, '' )
288+ . trim ( )
289+
290+ return cleanedAttrs ? ` ${ cleanedAttrs } ` : ''
291+ }
292+
283293const 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 [ ^ > ] * h r e f = " ( [ ^ " ] * ) " [ ^ > ] * > ( [ \s \S ] * ?) < \/ a > / gi
481+ const htmlAnchorRe = / < a ( \s [ ^ > ] * ? ) h r e f = ( [ " ' ] ) ( [ ^ " ' ] * ) \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 = / \b a l i g n = ( [ " ' ] ) ( .* ?) \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