Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/Readme.vue
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ function handleClick(event: MouseEvent) {
@apply decoration-accent text-accent;
}

.readme :deep(a[target='_blank']::after) {
.readme :deep(a[target='_blank']:not(:has(img))::after) {
/* 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. */
content: '__';
@apply inline i-carbon:launch rtl-flip ms-1 opacity-50;
Expand Down
86 changes: 59 additions & 27 deletions server/utils/readme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface PlaygroundProvider {
id: string // Provider identifier
name: string
domains: string[] // Associated domains
path?: string
icon?: string // Provider icon name
}

Expand Down Expand Up @@ -74,6 +75,13 @@ const PLAYGROUND_PROVIDERS: PlaygroundProvider[] = [
domains: ['vite.new'],
icon: 'vite',
},
{
id: 'typescript-playground',
name: 'Typescript Playground',
domains: ['typescriptlang.org'],
path: '/play',
icon: 'typescript',
},
Comment thread
essenmitsosse marked this conversation as resolved.
]

/**
Expand All @@ -86,7 +94,10 @@ function matchPlaygroundProvider(url: string): PlaygroundProvider | null {

for (const provider of PLAYGROUND_PROVIDERS) {
for (const domain of provider.domains) {
if (hostname === domain || hostname.endsWith(`.${domain}`)) {
if (
(hostname === domain || hostname.endsWith(`.${domain}`)) &&
(!provider.path || parsed.pathname.startsWith(provider.path))
) {
return provider
}
}
Expand Down Expand Up @@ -210,6 +221,16 @@ const isNpmJsUrlThatCanBeRedirected = (url: URL) => {
return true
}

const replaceHtmlLink = (html: string) => {
return html.replace(/href="([^"]+)"/g, (match, href) => {
if (isNpmJsUrlThatCanBeRedirected(new URL(href, 'https://www.npmjs.com'))) {
const newHref = href.replace(/^https?:\/\/(www\.)?npmjs\.com/, '')
return `href="${newHref}"`
}
return match
})
}
Comment thread
essenmitsosse marked this conversation as resolved.

/**
* Resolve a relative URL to an absolute URL.
* If repository info is available, resolve to provider's raw file URLs.
Expand Down Expand Up @@ -390,35 +411,15 @@ ${html}
return `<img src="${resolvedHref}"${altAttr}${titleAttr}>`
}

// Resolve link URLs, add security attributes, and collect playground links
// // Resolve link URLs, add security attributes, and collect playground links
renderer.link = function ({ href, title, tokens }: Tokens.Link) {
const resolvedHref = resolveUrl(href, packageName, repoInfo)
const text = this.parser.parseInline(tokens)
const titleAttr = title ? ` title="${title}"` : ''
const plainText = text.replace(/<[^>]*>/g, '').trim()

const isExternal = resolvedHref.startsWith('http://') || resolvedHref.startsWith('https://')
const relAttr = isExternal ? ' rel="nofollow noreferrer noopener"' : ''
const targetAttr = isExternal ? ' target="_blank"' : ''

// Check if this is a playground link
const provider = matchPlaygroundProvider(resolvedHref)
if (provider && !seenUrls.has(resolvedHref)) {
seenUrls.add(resolvedHref)

// Extract label from link text (strip HTML tags for plain text)
const plainText = text.replace(/<[^>]*>/g, '').trim()

collectedLinks.push({
url: resolvedHref,
provider: provider.id,
providerName: provider.name,
label: plainText || title || provider.name,
})
}

const hrefValue = resolvedHref.startsWith('#') ? resolvedHref.toLowerCase() : resolvedHref
const intermediateTitleAttr = `${` data-title-intermediate="${plainText || title}"`}`

return `<a href="${hrefValue}"${titleAttr}${relAttr}${targetAttr}>${text}</a>`
return `<a href="${href}"${titleAttr}${intermediateTitleAttr}>${text}</a>`
}

// GitHub-style callouts: > [!NOTE], > [!TIP], etc.
Expand All @@ -436,7 +437,14 @@ ${html}
return `<blockquote>${body}</blockquote>\n`
}

marked.setOptions({ renderer })
marked.setOptions({
renderer,
walkTokens: token => {
if (token.type === 'html') {
token.text = replaceHtmlLink(token.text)
}
},
})

const rawHtml = marked.parse(content) as string

Expand Down Expand Up @@ -494,11 +502,35 @@ ${html}
return { tagName, attribs }
},
a: (tagName, attribs) => {
if (!attribs.href) {
return { tagName, attribs }
}

const resolvedHref = resolveUrl(attribs.href, packageName, repoInfo)

const provider = matchPlaygroundProvider(resolvedHref)
if (provider && !seenUrls.has(resolvedHref)) {
seenUrls.add(resolvedHref)

collectedLinks.push({
url: resolvedHref,
provider: provider.id,
providerName: provider.name,
/**
* We need to set some data attribute before hand because `transformTags` doesn't
* provide the text of the element. This will automatically be removed, because there
* is an allow list for link attributes.
* */
label: attribs['data-title-intermediate'] || provider.name,
})
}

// Add security attributes for external links
if (attribs.href && hasProtocol(attribs.href, { acceptRelative: true })) {
if (resolvedHref && hasProtocol(resolvedHref, { acceptRelative: true })) {
attribs.rel = 'nofollow noreferrer noopener'
attribs.target = '_blank'
}
attribs.href = resolvedHref
return { tagName, attribs }
},
div: prefixId,
Expand Down
Loading