Skip to content

Commit cad008c

Browse files
feat: add clickable anchor links to readme headings (#1417)
1 parent 1e45cdc commit cad008c

File tree

3 files changed

+15
-20
lines changed

3 files changed

+15
-20
lines changed

app/components/Readme.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ function handleClick(event: MouseEvent) {
150150
@apply inline i-lucide:external-link rtl-flip ms-1 opacity-50;
151151
}
152152
153+
.readme :deep(a[href^='#']::after) {
154+
/* 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. */
155+
content: '__';
156+
@apply inline i-carbon:link rtl-flip ms-1 opacity-0;
157+
}
158+
159+
.readme :deep(a[href^='#']:hover::after) {
160+
@apply opacity-100;
161+
}
162+
153163
.readme :deep(code) {
154164
@apply font-mono;
155165
font-size: 0.875em;

server/utils/readme.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,6 @@ const isNpmJsUrlThatCanBeRedirected = (url: URL) => {
221221
return true
222222
}
223223

224-
const replaceHtmlLink = (html: string) => {
225-
return html.replace(/href="([^"]+)"/g, (match, href) => {
226-
if (isNpmJsUrlThatCanBeRedirected(new URL(href, 'https://www.npmjs.com'))) {
227-
const newHref = href.replace(/^https?:\/\/(www\.)?npmjs\.com/, '')
228-
return `href="${newHref}"`
229-
}
230-
return match
231-
})
232-
}
233-
234224
/**
235225
* Resolve a relative URL to an absolute URL.
236226
* If repository info is available, resolve to provider's raw file URLs.
@@ -387,7 +377,8 @@ export async function renderReadmeHtml(
387377
toc.push({ text: plainText, id, depth })
388378
}
389379

390-
return `<h${semanticLevel} id="${id}" data-level="${depth}">${text}</h${semanticLevel}>\n`
380+
/** The link href uses the unique slug WITHOUT the 'user-content-' prefix, because that will later be added for all links. */
381+
return `<h${semanticLevel} id="${id}" data-level="${depth}"><a href="#${uniqueSlug}">${plainText}</a></h${semanticLevel}>\n`
391382
}
392383

393384
// Syntax highlighting for code blocks (uses shared highlighter)
@@ -443,14 +434,7 @@ ${html}
443434
return `<blockquote>${body}</blockquote>\n`
444435
}
445436

446-
marked.setOptions({
447-
renderer,
448-
walkTokens: token => {
449-
if (token.type === 'html') {
450-
token.text = replaceHtmlLink(token.text)
451-
}
452-
},
453-
})
437+
marked.setOptions({ renderer })
454438

455439
const rawHtml = marked.parse(content) as string
456440

test/unit/server/utils/readme.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,8 @@ describe('HTML output', () => {
388388
const markdown = `# Title\n\nSome **bold** text and a [link](https://example.com).`
389389
const result = await renderReadmeHtml(markdown, 'test-pkg')
390390

391-
expect(result.html).toBe(`<h3 id="user-content-title" data-level="1">Title</h3>
391+
expect(result.html)
392+
.toBe(`<h3 id="user-content-title" data-level="1"><a href="#user-content-title">Title</a></h3>
392393
<p>Some <strong>bold</strong> text and a <a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank">link</a>.</p>
393394
`)
394395
})

0 commit comments

Comments
 (0)