@@ -635,132 +635,127 @@ sei-tendermint
635635 return repoMap [ compName ] || 'sei-protocol/sei-chain' ;
636636 } ;
637637
638- const cleanedContent = content
639- . replace ( / \] \( h t t p s : \/ \/ [ ^ ) ] + \) / g, '' )
640- . replace ( / \[ / g, '' )
641- . replace ( / \] / g, '' ) ;
638+ const renderLink = ( key , href , text , marginClass = 'ml-1' ) => (
639+ < a
640+ key = { key }
641+ href = { href }
642+ target = "_blank"
643+ rel = "noopener noreferrer"
644+ className = { `px-2 py-0.5 text-sm font-medium no-underline ${ marginClass } ` }
645+ style = { linkBaseStyle ( ) }
646+ onMouseEnter = { ( e ) => applyLinkHover ( e , true ) }
647+ onMouseLeave = { ( e ) => applyLinkHover ( e , false ) } >
648+ { text }
649+ </ a >
650+ ) ;
642651
643- const urlPattern = / ( h t t p s : \/ \/ g i t h u b \. c o m \/ [ ^ \s ) ] + ) / g;
644- const parts = cleanedContent . split ( urlPattern ) ;
652+ // Friendly label for a bare GitHub URL that has no explicit markdown label.
653+ const labelForUrl = ( url ) => {
654+ if ( url . includes ( '/compare/' ) ) {
655+ const m = url . match ( / \/ c o m p a r e \/ ( [ ^ / \s ] + ) / ) ;
656+ if ( m ) return `Compare ${ m [ 1 ] . replace ( '...' , ' → ' ) } ` ;
657+ } else if ( url . includes ( '/releases/tag/' ) ) {
658+ const m = url . match ( / \/ r e l e a s e s \/ t a g \/ ( [ ^ / \s ] + ) / ) ;
659+ if ( m ) return `Release ${ m [ 1 ] } ` ;
660+ } else if ( url . includes ( '/pull/' ) ) {
661+ const m = url . match ( / \/ p u l l \/ ( \d + ) / ) ;
662+ if ( m ) return `#${ m [ 1 ] } ` ;
663+ }
664+ return url ;
665+ } ;
645666
646- return (
647- < span className = "text-base" >
648- { parts . map ( ( part , i ) => {
649- if ( part . match ( urlPattern ) ) {
650- let linkText = part ;
651- if ( part . includes ( '/compare/' ) ) {
652- const compareMatch = part . match ( / \/ c o m p a r e \/ ( [ ^ / \s ] + ) / ) ;
653- if ( compareMatch ) linkText = `Compare ${ compareMatch [ 1 ] . replace ( '...' , ' → ' ) } ` ;
654- } else if ( part . includes ( '/releases/tag/' ) ) {
655- const tagMatch = part . match ( / \/ r e l e a s e s \/ t a g \/ ( [ ^ / \s ] + ) / ) ;
656- if ( tagMatch ) linkText = `Release ${ tagMatch [ 1 ] } ` ;
657- } else if ( part . includes ( '/pull/' ) ) {
658- const prMatch = part . match ( / \/ p u l l \/ ( \d + ) / ) ;
659- if ( prMatch ) linkText = `#${ prMatch [ 1 ] } ` ;
660- }
661- return (
662- < a
663- key = { i }
664- href = { part }
665- target = "_blank"
666- rel = "noopener noreferrer"
667- className = "px-2 py-0.5 text-sm font-medium no-underline ml-1"
668- style = { linkBaseStyle ( ) }
669- onMouseEnter = { ( e ) => applyLinkHover ( e , true ) }
670- onMouseLeave = { ( e ) => applyLinkHover ( e , false ) } >
671- { linkText }
672- </ a >
673- ) ;
674- }
667+ // Render a run of text that contains no [label](url) markdown links. Bare
668+ // GitHub URLs, "Compare X → Y" strings, and bare PR numbers are still linked
669+ // heuristically so entries authored without markdown link syntax keep working.
670+ const renderPlainSegment = ( text , keyPrefix ) => {
671+ const stripped = text . replace ( / \[ / g, '' ) . replace ( / \] / g, '' ) ;
672+ const urlPattern = / ( h t t p s : \/ \/ g i t h u b \. c o m \/ [ ^ \s ) ] + ) / g;
673+ const parts = stripped . split ( urlPattern ) ;
674+
675+ return parts . map ( ( part , i ) => {
676+ const key = `${ keyPrefix } -${ i } ` ;
677+ if ( part . match ( urlPattern ) ) {
678+ return renderLink ( key , part , labelForUrl ( part ) ) ;
679+ }
675680
676- const compareMatch = part . match (
677- / C o m p a r e \s + ( (?: s e i - [ a - z ] + - ) ? ( v ? \d + \. \d + \. \d + (?: \. \d + ) ? (?: b e t a | a l p h a | r c \d * ) ? (?: - r e l e a s e ) ? ) ) (?: \s * [ → - ] \s * | \. \. \. ) + ( v ? \d + \. \d + \. \d + (?: \. \d + ) ? (?: b e t a | a l p h a | r c \d * ) ? (?: - r e l e a s e ) ? ) / i
681+ const compareMatch = part . match (
682+ / C o m p a r e \s + ( (?: s e i - [ a - z ] + - ) ? ( v ? \d + \. \d + \. \d + (?: \. \d + ) ? (?: b e t a | a l p h a | r c \d * ) ? (?: - r e l e a s e ) ? ) ) (?: \s * [ → - ] \s * | \. \. \. ) + ( v ? \d + \. \d + \. \d + (?: \. \d + ) ? (?: b e t a | a l p h a | r c \d * ) ? (?: - r e l e a s e ) ? ) / i
683+ ) ;
684+ if ( compareMatch ) {
685+ const [ fullMatch , fromVersion , , toVersion ] = compareMatch ;
686+ const cleanFromVersion = fromVersion . replace ( / ^ s e i - [ a - z ] + - / , '' ) ;
687+ const cleanToVersion = toVersion . replace ( / ^ s e i - [ a - z ] + - / , '' ) ;
688+ const repoPath = getRepoUrl ( componentName ) ;
689+ const compareUrl = `https://github.com/${ repoPath } /compare/${ cleanFromVersion } ...${ cleanToVersion } ` ;
690+ return (
691+ < span key = { key } >
692+ { part . replace ( fullMatch , '' ) }
693+ { renderLink ( `${ key } -cmp` , compareUrl , `Compare ${ cleanFromVersion } → ${ cleanToVersion } ` ) }
694+ </ span >
678695 ) ;
679- if ( compareMatch ) {
680- const [ fullMatch , fromVersion , , toVersion ] = compareMatch ;
681- const cleanFromVersion = fromVersion . replace ( / ^ s e i - [ a - z ] + - / , '' ) ;
682- const cleanToVersion = toVersion . replace ( / ^ s e i - [ a - z ] + - / , '' ) ;
683- const repoPath = getRepoUrl ( componentName ) ;
684- const compareUrl = `https://github.com/${ repoPath } /compare/${ cleanFromVersion } ...${ cleanToVersion } ` ;
685- return (
686- < span key = { i } >
687- { part . replace ( fullMatch , '' ) }
688- < a
689- href = { compareUrl }
690- target = "_blank"
691- rel = "noopener noreferrer"
692- className = "px-2 py-0.5 text-sm font-medium no-underline ml-1"
693- style = { linkBaseStyle ( ) }
694- onMouseEnter = { ( e ) => applyLinkHover ( e , true ) }
695- onMouseLeave = { ( e ) => applyLinkHover ( e , false ) } >
696- Compare { cleanFromVersion } → { cleanToVersion }
697- </ a >
698- </ span >
699- ) ;
700- }
696+ }
701697
702- const prParts = part . split ( / ( \b \d { 1 , 4 } \b | # \d + ) / g) ;
703- return (
704- < span key = { i } >
705- { prParts . map ( ( subPart , j ) => {
706- const trimmedPart = subPart . trim ( ) ;
707- if ( trimmedPart . match ( / ^ # \d + $ / ) ) {
708- const num = trimmedPart . slice ( 1 ) ;
698+ const prParts = part . split ( / ( \b \d { 1 , 4 } \b | # \d + ) / g) ;
699+ return (
700+ < span key = { key } >
701+ { prParts . map ( ( subPart , j ) => {
702+ const trimmedPart = subPart . trim ( ) ;
703+ if ( trimmedPart . match ( / ^ # \d + $ / ) ) {
704+ const num = trimmedPart . slice ( 1 ) ;
705+ const prevParts = prParts . slice ( 0 , j ) . join ( '' ) ;
706+ const inlineComponentMatch = prevParts . match ( / \b ( s e i - c h a i n | s e i - t e n d e r m i n t | s e i - c o s m o s | s e i - d b | s e i - w a s m d | s e i - i a v l | s e i - i b c - g o | t m - d b ) \s * $ / i) ;
707+ const effectiveComponent = inlineComponentMatch ? inlineComponentMatch [ 1 ] : componentName ;
708+ const repoPath = getRepoUrl ( effectiveComponent ) ;
709+ return renderLink ( `${ key } -${ j } ` , `https://github.com/${ repoPath } /pull/${ num } ` , `#${ num } ` ) ;
710+ } else if ( trimmedPart . match ( / ^ \d { 1 , 4 } $ / ) && parseInt ( trimmedPart , 10 ) > 0 ) {
711+ const prevPart = prParts [ j - 1 ] || '' ;
712+ const nextPart = prParts [ j + 1 ] || '' ;
713+ const followedByNonPR = nextPart . match ( / ^ \s * ( h o p | l i m i t | v e r s i o n | v \d | \. \d | p x | m s | s \b | m b | g b | k b ) / i) ;
714+ const precededByNonPR = prevPart . match ( / ( v | v e r s i o n | \d \. ) \s * $ / i) ;
715+ const isLikelyPRNumber =
716+ ( j < 3 || prevPart . includes ( '•' ) || prevPart . includes ( '\n' ) || prevPart . includes ( '#' ) ) &&
717+ ! followedByNonPR &&
718+ ! precededByNonPR &&
719+ parseInt ( trimmedPart , 10 ) >= 10 ;
720+
721+ if ( isLikelyPRNumber ) {
709722 const prevParts = prParts . slice ( 0 , j ) . join ( '' ) ;
710723 const inlineComponentMatch = prevParts . match ( / \b ( s e i - c h a i n | s e i - t e n d e r m i n t | s e i - c o s m o s | s e i - d b | s e i - w a s m d | s e i - i a v l | s e i - i b c - g o | t m - d b ) \s * $ / i) ;
711724 const effectiveComponent = inlineComponentMatch ? inlineComponentMatch [ 1 ] : componentName ;
712725 const repoPath = getRepoUrl ( effectiveComponent ) ;
713- return (
714- < a
715- key = { j }
716- href = { `https://github.com/${ repoPath } /pull/${ num } ` }
717- target = "_blank"
718- rel = "noopener noreferrer"
719- className = "px-2 py-0.5 text-sm font-medium no-underline ml-1"
720- style = { linkBaseStyle ( ) }
721- onMouseEnter = { ( e ) => applyLinkHover ( e , true ) }
722- onMouseLeave = { ( e ) => applyLinkHover ( e , false ) } >
723- #{ num }
724- </ a >
725- ) ;
726- } else if ( trimmedPart . match ( / ^ \d { 1 , 4 } $ / ) && parseInt ( trimmedPart , 10 ) > 0 ) {
727- const prevPart = prParts [ j - 1 ] || '' ;
728- const nextPart = prParts [ j + 1 ] || '' ;
729- const followedByNonPR = nextPart . match ( / ^ \s * ( h o p | l i m i t | v e r s i o n | v \d | \. \d | p x | m s | s \b | m b | g b | k b ) / i) ;
730- const precededByNonPR = prevPart . match ( / ( v | v e r s i o n | \d \. ) \s * $ / i) ;
731- const isLikelyPRNumber =
732- ( j < 3 || prevPart . includes ( '•' ) || prevPart . includes ( '\n' ) || prevPart . includes ( '#' ) ) &&
733- ! followedByNonPR &&
734- ! precededByNonPR &&
735- parseInt ( trimmedPart , 10 ) >= 10 ;
736-
737- if ( isLikelyPRNumber ) {
738- const prevParts = prParts . slice ( 0 , j ) . join ( '' ) ;
739- const inlineComponentMatch = prevParts . match ( / \b ( s e i - c h a i n | s e i - t e n d e r m i n t | s e i - c o s m o s | s e i - d b | s e i - w a s m d | s e i - i a v l | s e i - i b c - g o | t m - d b ) \s * $ / i) ;
740- const effectiveComponent = inlineComponentMatch ? inlineComponentMatch [ 1 ] : componentName ;
741- const repoPath = getRepoUrl ( effectiveComponent ) ;
742- return (
743- < a
744- key = { j }
745- href = { `https://github.com/${ repoPath } /pull/${ trimmedPart } ` }
746- target = "_blank"
747- rel = "noopener noreferrer"
748- className = "px-2 py-0.5 text-sm font-medium no-underline mr-1"
749- style = { linkBaseStyle ( ) }
750- onMouseEnter = { ( e ) => applyLinkHover ( e , true ) }
751- onMouseLeave = { ( e ) => applyLinkHover ( e , false ) } >
752- #{ trimmedPart }
753- </ a >
754- ) ;
755- }
726+ return renderLink ( `${ key } -${ j } ` , `https://github.com/${ repoPath } /pull/${ trimmedPart } ` , `#${ trimmedPart } ` , 'mr-1' ) ;
756727 }
757- return < span key = { j } > { subPart } </ span > ;
758- } ) }
759- </ span >
760- ) ;
761- } ) }
762- </ span >
763- ) ;
728+ }
729+ return < span key = { `${ key } -${ j } ` } > { subPart } </ span > ;
730+ } ) }
731+ </ span >
732+ ) ;
733+ } ) ;
734+ } ;
735+
736+ // Honor explicit [label](url) markdown links first: the rendered href is the
737+ // real target from the source, never a number re-derived from the label.
738+ // Upstream entries sometimes label an issue (e.g. #1850) while linking the PR
739+ // that fixed it (e.g. /pull/1853), so the source URL must win.
740+ const markdownLinkPattern = / \[ ( [ ^ \] ] + ) \] \( ( h t t p s ? : \/ \/ [ ^ ) ] + ) \) / g;
741+ const tokens = content . split ( markdownLinkPattern ) ;
742+
743+ const nodes = [ ] ;
744+ for ( let i = 0 ; i < tokens . length ; i ++ ) {
745+ const mod = i % 3 ;
746+ if ( mod === 0 ) {
747+ if ( tokens [ i ] ) {
748+ nodes . push ( < span key = { `seg-${ i } ` } > { renderPlainSegment ( tokens [ i ] , `seg-${ i } ` ) } </ span > ) ;
749+ }
750+ } else if ( mod === 1 ) {
751+ const label = tokens [ i ] ;
752+ const url = tokens [ i + 1 ] || '' ;
753+ nodes . push ( renderLink ( `md-${ i } ` , url , label ) ) ;
754+ }
755+ // mod === 2 holds the URL already consumed alongside its label above.
756+ }
757+
758+ return < span className = "text-base" > { nodes } </ span > ;
764759 } ;
765760
766761 const renderContent = ( body ) => {
@@ -779,11 +774,10 @@ sei-tendermint
779774 for ( let j = 1 ; j < lines . length ; j ++ ) {
780775 const line = lines [ j ] . trim ( ) ;
781776 if ( line . startsWith ( '*' ) || line . startsWith ( '-' ) || line . startsWith ( '•' ) ) {
782- let cleanLine = line
777+ const cleanLine = line
783778 . replace ( / ^ \* \s * / , '' )
784779 . replace ( / ^ - \s * / , '' )
785780 . replace ( / ^ • \s * / , '' ) ;
786- cleanLine = cleanLine . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ^ ] + \) / g, '$1' ) ;
787781 bulletPoints . push ( cleanLine ) ;
788782 }
789783 }
@@ -837,11 +831,10 @@ sei-tendermint
837831 for ( const line of compContent . split ( '\n' ) ) {
838832 const trimmedLine = line . trim ( ) ;
839833 if ( trimmedLine . startsWith ( '*' ) || trimmedLine . startsWith ( '-' ) || trimmedLine . startsWith ( '•' ) ) {
840- let cleanLine = trimmedLine
834+ const cleanLine = trimmedLine
841835 . replace ( / ^ \* \s * / , '' )
842836 . replace ( / ^ - \s * / , '' )
843837 . replace ( / ^ • \s * / , '' ) ;
844- cleanLine = cleanLine . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ^ ] + \) / g, '$1' ) ;
845838 bulletPoints . push ( cleanLine ) ;
846839 } else if ( trimmedLine && ! trimmedLine . match ( / ^ [ a - z A - Z ] [ a - z A - Z 0 - 9 - ] * : ? \s * (?: \( .* \) ) ? \s * $ / ) ) {
847840 bulletPoints . push ( trimmedLine ) ;
0 commit comments