1414 * - CVE references (nvd.nist.gov/vuln/detail/CVE-YYYY-NNNNN)
1515 * - Python releases (python.org/downloads/release/python-XXXX/)
1616 *
17- * Bare "CVE-YYYY-NNNN" text (not already inside a link) is autolinked
18- * to the CVE record and rendered as a badge.
17+ * Bare "gh-NNNN" and " CVE-YYYY-NNNN" text (not already inside a link
18+ * or heading) is autolinked and rendered as a badge.
1919 */
2020import type { Root , Link , Paragraph , PhrasingContent } from "mdast" ;
2121import { SKIP , visit } from "unist-util-visit" ;
@@ -38,9 +38,14 @@ const DOCS = /^https?:\/\/docs\.python\.org\//i;
3838const PYPI = / ^ h t t p s ? : \/ \/ p y p i \. o r g \/ p r o j e c t \/ ( [ ^ / ] + ) \/ ? / i;
3939const GH_ISSUE = / ^ h t t p s ? : \/ \/ g i t h u b \. c o m \/ ( [ \w . - ] + ) \/ ( [ \w . - ] + ) \/ ( i s s u e s | p u l l ) \/ ( \d + ) \/ ? / i;
4040const CVE = / ^ h t t p s ? : \/ \/ n v d \. n i s t \. g o v \/ v u l n \/ d e t a i l \/ ( C V E - [ \d - ] + ) \/ ? / i;
41- /** Bare CVE IDs in plain text, e.g. "CVE-2026-19445" */
42- const CVE_TEXT = / \b C V E - \d { 4 } - \d { 4 , } \b / g;
43- const cveUrl = ( id : string ) => `https://www.cve.org/CVERecord?id=${ id } ` ;
41+
42+ /**
43+ * Bare references in plain text that get autolinked (outside links,
44+ * headings and code):
45+ * - "gh-156293" → python/cpython issue
46+ * - "CVE-2026-19445" → cve.org record
47+ */
48+ const BARE_REF = / \b (?: ( C V E - \d { 4 } - \d { 4 , } ) | g h - ( \d + ) ) \b / g;
4449const PY_RELEASE = / ^ h t t p s ? : \/ \/ (?: w w w \. ) ? p y t h o n \. o r g \/ d o w n l o a d s \/ r e l e a s e \/ ( p y t h o n - [ \w . ] + ) \/ ? / i;
4550const GITHUB =
4651 / ^ h t t p s ? : \/ \/ g i t h u b \. c o m \/ ( [ \w . - ] + ) (?: \/ ( [ \w . - ] + ) ) ? \/ ? $ / i;
@@ -336,7 +341,7 @@ export default function remarkPythonRefs() {
336341 }
337342 } ) ;
338343
339- // Pass 3: Autolink bare CVE IDs in text → badges
344+ // Pass 3: Autolink bare gh-NNNN issue refs and CVE IDs in text → badges
340345 visit ( tree , ( node : any , index , parent : any ) => {
341346 // Don't touch text that is already a link (or a reference definition),
342347 // or headings — Astro builds heading ids from text nodes only, so
@@ -352,26 +357,23 @@ export default function remarkPythonRefs() {
352357 if ( node . type !== "text" || index == null || ! parent ) return ;
353358
354359 const value : string = node . value ;
355- CVE_TEXT . lastIndex = 0 ;
356- if ( ! CVE_TEXT . test ( value ) ) {
357- CVE_TEXT . lastIndex = 0 ;
358- return ;
359- }
360-
361360 const parts : any [ ] = [ ] ;
362361 let lastIndex = 0 ;
363- CVE_TEXT . lastIndex = 0 ;
362+ BARE_REF . lastIndex = 0 ;
364363 let m : RegExpExecArray | null ;
365- while ( ( m = CVE_TEXT . exec ( value ) ) !== null ) {
364+ while ( ( m = BARE_REF . exec ( value ) ) !== null ) {
366365 if ( m . index > lastIndex ) {
367366 parts . push ( { type : "text" , value : value . slice ( lastIndex , m . index ) } ) ;
368367 }
369- const id = m [ 0 ] ;
370- const url = cveUrl ( id ) ;
371- collectRef ( "cve" , id , url ) ;
372- parts . push ( { type : "html" , value : buildBadgeHtml ( { type : "cve" , icon : shieldIcon , label : id , url } ) } ) ;
368+ const [ label , cve , ghNum ] = m ;
369+ const match : Match = cve
370+ ? { type : "cve" , icon : shieldIcon , label, url : `https://www.cve.org/CVERecord?id=${ cve } ` }
371+ : { type : "gh-issue" , icon : issueIcon , label, url : `https://github.com/python/cpython/issues/${ ghNum } ` } ;
372+ collectRef ( match . type , match . label , match . url ) ;
373+ parts . push ( { type : "html" , value : buildBadgeHtml ( match ) } ) ;
373374 lastIndex = m . index + m [ 0 ] . length ;
374375 }
376+ if ( parts . length === 0 ) return ;
375377 if ( lastIndex < value . length ) {
376378 parts . push ( { type : "text" , value : value . slice ( lastIndex ) } ) ;
377379 }
0 commit comments