@@ -13,6 +13,7 @@ interface PlaygroundProvider {
1313 id : string // Provider identifier
1414 name : string
1515 domains : string [ ] // Associated domains
16+ path ?: string
1617 icon ?: string // Provider icon name
1718}
1819
@@ -74,6 +75,13 @@ const PLAYGROUND_PROVIDERS: PlaygroundProvider[] = [
7475 domains : [ 'vite.new' ] ,
7576 icon : 'vite' ,
7677 } ,
78+ {
79+ id : 'typescript-playground' ,
80+ name : 'TypeScript Playground' ,
81+ domains : [ 'typescriptlang.org' ] ,
82+ path : '/play' ,
83+ icon : 'typescript' ,
84+ } ,
7785]
7886
7987/**
@@ -86,7 +94,10 @@ function matchPlaygroundProvider(url: string): PlaygroundProvider | null {
8694
8795 for ( const provider of PLAYGROUND_PROVIDERS ) {
8896 for ( const domain of provider . domains ) {
89- if ( hostname === domain || hostname . endsWith ( `.${ domain } ` ) ) {
97+ if (
98+ ( hostname === domain || hostname . endsWith ( `.${ domain } ` ) ) &&
99+ ( ! provider . path || parsed . pathname . startsWith ( provider . path ) )
100+ ) {
90101 return provider
91102 }
92103 }
@@ -210,6 +221,16 @@ const isNpmJsUrlThatCanBeRedirected = (url: URL) => {
210221 return true
211222}
212223
224+ const replaceHtmlLink = ( html : string ) => {
225+ return html . replace ( / h r e f = " ( [ ^ " ] + ) " / g, ( match , href ) => {
226+ if ( isNpmJsUrlThatCanBeRedirected ( new URL ( href , 'https://www.npmjs.com' ) ) ) {
227+ const newHref = href . replace ( / ^ h t t p s ? : \/ \/ ( w w w \. ) ? n p m j s \. c o m / , '' )
228+ return `href="${ newHref } "`
229+ }
230+ return match
231+ } )
232+ }
233+
213234/**
214235 * Resolve a relative URL to an absolute URL.
215236 * If repository info is available, resolve to provider's raw file URLs.
@@ -390,35 +411,15 @@ ${html}
390411 return `<img src="${ resolvedHref } "${ altAttr } ${ titleAttr } >`
391412 }
392413
393- // Resolve link URLs, add security attributes, and collect playground links
414+ // // Resolve link URLs, add security attributes, and collect playground links
394415 renderer . link = function ( { href, title, tokens } : Tokens . Link ) {
395- const resolvedHref = resolveUrl ( href , packageName , repoInfo )
396416 const text = this . parser . parseInline ( tokens )
397417 const titleAttr = title ? ` title="${ title } "` : ''
418+ const plainText = text . replace ( / < [ ^ > ] * > / g, '' ) . trim ( )
398419
399- const isExternal = resolvedHref . startsWith ( 'http://' ) || resolvedHref . startsWith ( 'https://' )
400- const relAttr = isExternal ? ' rel="nofollow noreferrer noopener"' : ''
401- const targetAttr = isExternal ? ' target="_blank"' : ''
402-
403- // Check if this is a playground link
404- const provider = matchPlaygroundProvider ( resolvedHref )
405- if ( provider && ! seenUrls . has ( resolvedHref ) ) {
406- seenUrls . add ( resolvedHref )
407-
408- // Extract label from link text (strip HTML tags for plain text)
409- const plainText = text . replace ( / < [ ^ > ] * > / g, '' ) . trim ( )
410-
411- collectedLinks . push ( {
412- url : resolvedHref ,
413- provider : provider . id ,
414- providerName : provider . name ,
415- label : plainText || title || provider . name ,
416- } )
417- }
418-
419- const hrefValue = resolvedHref . startsWith ( '#' ) ? resolvedHref . toLowerCase ( ) : resolvedHref
420+ const intermediateTitleAttr = `${ ` data-title-intermediate="${ plainText || title } "` } `
420421
421- return `<a href="${ hrefValue } "${ titleAttr } ${ relAttr } ${ targetAttr } >${ text } </a>`
422+ return `<a href="${ href } "${ titleAttr } ${ intermediateTitleAttr } >${ text } </a>`
422423 }
423424
424425 // GitHub-style callouts: > [!NOTE], > [!TIP], etc.
@@ -436,7 +437,14 @@ ${html}
436437 return `<blockquote>${ body } </blockquote>\n`
437438 }
438439
439- marked . setOptions ( { renderer } )
440+ marked . setOptions ( {
441+ renderer,
442+ walkTokens : token => {
443+ if ( token . type === 'html' ) {
444+ token . text = replaceHtmlLink ( token . text )
445+ }
446+ } ,
447+ } )
440448
441449 const rawHtml = marked . parse ( content ) as string
442450
@@ -494,11 +502,35 @@ ${html}
494502 return { tagName, attribs }
495503 } ,
496504 a : ( tagName , attribs ) => {
505+ if ( ! attribs . href ) {
506+ return { tagName, attribs }
507+ }
508+
509+ const resolvedHref = resolveUrl ( attribs . href , packageName , repoInfo )
510+
511+ const provider = matchPlaygroundProvider ( resolvedHref )
512+ if ( provider && ! seenUrls . has ( resolvedHref ) ) {
513+ seenUrls . add ( resolvedHref )
514+
515+ collectedLinks . push ( {
516+ url : resolvedHref ,
517+ provider : provider . id ,
518+ providerName : provider . name ,
519+ /**
520+ * We need to set some data attribute before hand because `transformTags` doesn't
521+ * provide the text of the element. This will automatically be removed, because there
522+ * is an allow list for link attributes.
523+ * */
524+ label : attribs [ 'data-title-intermediate' ] || provider . name ,
525+ } )
526+ }
527+
497528 // Add security attributes for external links
498- if ( attribs . href && hasProtocol ( attribs . href , { acceptRelative : true } ) ) {
529+ if ( resolvedHref && hasProtocol ( resolvedHref , { acceptRelative : true } ) ) {
499530 attribs . rel = 'nofollow noreferrer noopener'
500531 attribs . target = '_blank'
501532 }
533+ attribs . href = resolvedHref
502534 return { tagName, attribs }
503535 } ,
504536 div : prefixId ,
0 commit comments