···144144 @apply decoration-accent text-accent;
145145}
146146147147-.readme :deep(a[target='_blank']::after) {
147147+.readme :deep(a[target='_blank']:not(:has(img))::after) {
148148 /* I don't know what kind of sorcery this is, but it ensures this icon can't wrap to a new line on its own. */
149149 content: '__';
150150 @apply inline i-carbon:launch rtl-flip ms-1 opacity-50;
+59-27
server/utils/readme.ts
···1313 id: string // Provider identifier
1414 name: string
1515 domains: string[] // Associated domains
1616+ path?: string
1617 icon?: string // Provider icon name
1718}
1819···7475 domains: ['vite.new'],
7576 icon: 'vite',
7677 },
7878+ {
7979+ id: 'typescript-playground',
8080+ name: 'TypeScript Playground',
8181+ domains: ['typescriptlang.org'],
8282+ path: '/play',
8383+ icon: 'typescript',
8484+ },
7785]
78867987/**
···86948795 for (const provider of PLAYGROUND_PROVIDERS) {
8896 for (const domain of provider.domains) {
8989- if (hostname === domain || hostname.endsWith(`.${domain}`)) {
9797+ if (
9898+ (hostname === domain || hostname.endsWith(`.${domain}`)) &&
9999+ (!provider.path || parsed.pathname.startsWith(provider.path))
100100+ ) {
90101 return provider
91102 }
92103 }
···210221 return true
211222}
212223224224+const replaceHtmlLink = (html: string) => {
225225+ return html.replace(/href="([^"]+)"/g, (match, href) => {
226226+ if (isNpmJsUrlThatCanBeRedirected(new URL(href, 'https://www.npmjs.com'))) {
227227+ const newHref = href.replace(/^https?:\/\/(www\.)?npmjs\.com/, '')
228228+ return `href="${newHref}"`
229229+ }
230230+ return match
231231+ })
232232+}
233233+213234/**
214235 * Resolve a relative URL to an absolute URL.
215236 * If repository info is available, resolve to provider's raw file URLs.
···390411 return `<img src="${resolvedHref}"${altAttr}${titleAttr}>`
391412 }
392413393393- // Resolve link URLs, add security attributes, and collect playground links
414414+ // // Resolve link URLs, add security attributes, and collect playground links
394415 renderer.link = function ({ href, title, tokens }: Tokens.Link) {
395395- const resolvedHref = resolveUrl(href, packageName, repoInfo)
396416 const text = this.parser.parseInline(tokens)
397417 const titleAttr = title ? ` title="${title}"` : ''
398398-399399- const isExternal = resolvedHref.startsWith('http://') || resolvedHref.startsWith('https://')
400400- const relAttr = isExternal ? ' rel="nofollow noreferrer noopener"' : ''
401401- const targetAttr = isExternal ? ' target="_blank"' : ''
402402-403403- // Check if this is a playground link
404404- const provider = matchPlaygroundProvider(resolvedHref)
405405- if (provider && !seenUrls.has(resolvedHref)) {
406406- seenUrls.add(resolvedHref)
407407-408408- // Extract label from link text (strip HTML tags for plain text)
409409- const plainText = text.replace(/<[^>]*>/g, '').trim()
410410-411411- collectedLinks.push({
412412- url: resolvedHref,
413413- provider: provider.id,
414414- providerName: provider.name,
415415- label: plainText || title || provider.name,
416416- })
417417- }
418418+ const plainText = text.replace(/<[^>]*>/g, '').trim()
418419419419- const hrefValue = resolvedHref.startsWith('#') ? resolvedHref.toLowerCase() : resolvedHref
420420+ const intermediateTitleAttr = `${` data-title-intermediate="${plainText || title}"`}`
420421421421- return `<a href="${hrefValue}"${titleAttr}${relAttr}${targetAttr}>${text}</a>`
422422+ return `<a href="${href}"${titleAttr}${intermediateTitleAttr}>${text}</a>`
422423 }
423424424425 // GitHub-style callouts: > [!NOTE], > [!TIP], etc.
···436437 return `<blockquote>${body}</blockquote>\n`
437438 }
438439439439- marked.setOptions({ renderer })
440440+ marked.setOptions({
441441+ renderer,
442442+ walkTokens: token => {
443443+ if (token.type === 'html') {
444444+ token.text = replaceHtmlLink(token.text)
445445+ }
446446+ },
447447+ })
440448441449 const rawHtml = marked.parse(content) as string
442450···494502 return { tagName, attribs }
495503 },
496504 a: (tagName, attribs) => {
505505+ if (!attribs.href) {
506506+ return { tagName, attribs }
507507+ }
508508+509509+ const resolvedHref = resolveUrl(attribs.href, packageName, repoInfo)
510510+511511+ const provider = matchPlaygroundProvider(resolvedHref)
512512+ if (provider && !seenUrls.has(resolvedHref)) {
513513+ seenUrls.add(resolvedHref)
514514+515515+ collectedLinks.push({
516516+ url: resolvedHref,
517517+ provider: provider.id,
518518+ providerName: provider.name,
519519+ /**
520520+ * We need to set some data attribute before hand because `transformTags` doesn't
521521+ * provide the text of the element. This will automatically be removed, because there
522522+ * is an allow list for link attributes.
523523+ * */
524524+ label: attribs['data-title-intermediate'] || provider.name,
525525+ })
526526+ }
527527+497528 // Add security attributes for external links
498498- if (attribs.href && hasProtocol(attribs.href, { acceptRelative: true })) {
529529+ if (resolvedHref && hasProtocol(resolvedHref, { acceptRelative: true })) {
499530 attribs.rel = 'nofollow noreferrer noopener'
500531 attribs.target = '_blank'
501532 }
533533+ attribs.href = resolvedHref
502534 return { tagName, attribs }
503535 },
504536 div: prefixId,