Skip to content

Commit 53cd46f

Browse files
committed
code blocks, blockquote & links are now handled by the markdown renderer
1 parent 5cd51af commit 53cd46f

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

server/utils/changelog/markdown.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,57 @@
11
import { marked, type Tokens } from 'marked'
2-
import { ALLOWED_ATTR, ALLOWED_TAGS, calculateSemanticDepth, prefixId, slugify } from '../readme'
2+
import {
3+
ALLOWED_ATTR,
4+
ALLOWED_TAGS,
5+
calculateSemanticDepth,
6+
prefixId,
7+
replaceHtmlLink,
8+
slugify,
9+
} from '../readme'
310
import sanitizeHtml from 'sanitize-html'
411

512
export async function changelogRenderer() {
613
const renderer = new marked.Renderer()
714

15+
const shiki = await getShikiHighlighter()
16+
17+
renderer.link = function ({ href, title, tokens }: Tokens.Link) {
18+
const text = this.parser.parseInline(tokens)
19+
const titleAttr = title ? ` title="${title}"` : ''
20+
const plainText = text.replace(/<[^>]*>/g, '').trim()
21+
22+
const intermediateTitleAttr = `${` data-title-intermediate="${plainText || title}"`}`
23+
24+
return `<a href="${href}"${titleAttr}${intermediateTitleAttr} target="_blank">${text}</a>`
25+
}
26+
27+
// GitHub-style callouts: > [!NOTE], > [!TIP], etc.
28+
renderer.blockquote = function ({ tokens }: Tokens.Blockquote) {
29+
const body = this.parser.parse(tokens)
30+
31+
const calloutMatch = body.match(/^<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:<br>)?\s*/i)
32+
33+
if (calloutMatch?.[1]) {
34+
const calloutType = calloutMatch[1].toLowerCase()
35+
const cleanedBody = body.replace(calloutMatch[0], '<p>')
36+
return `<blockquote data-callout="${calloutType}">${cleanedBody}</blockquote>\n`
37+
}
38+
39+
return `<blockquote>${body}</blockquote>\n`
40+
}
41+
42+
// Syntax highlighting for code blocks (uses shared highlighter)
43+
renderer.code = ({ text, lang }: Tokens.Code) => {
44+
const html = highlightCodeSync(shiki, text, lang || 'text')
45+
// Add copy button
46+
return `<div class="readme-code-block" >
47+
<button type="button" class="readme-copy-button" aria-label="Copy code" check-icon="i-carbon:checkmark" copy-icon="i-carbon:copy" data-copy>
48+
<span class="i-carbon:copy" aria-hidden="true"></span>
49+
<span class="sr-only">Copy code</span>
50+
</button>
51+
${html}
52+
</div>`
53+
}
54+
855
return (markdown: string | null, releaseId: string | number) => {
956
// Collect table of contents items during parsing
1057
const toc: TocItem[] = []
@@ -19,7 +66,6 @@ export async function changelogRenderer() {
1966
// Track used heading slugs to handle duplicates (GitHub-style: foo, foo-1, foo-2)
2067
const usedSlugs = new Map<string, number>()
2168

22-
// settings will need to be added still
2369
let lastSemanticLevel = 2 // Start after h2 (the "Readme" section heading)
2470
renderer.heading = function ({ tokens, depth }: Tokens.Heading) {
2571
// Calculate the target semantic level based on document structure
@@ -59,6 +105,11 @@ export async function changelogRenderer() {
59105
return {
60106
html: marked.parse(markdown, {
61107
renderer,
108+
walkTokens: token => {
109+
if (token.type === 'html') {
110+
token.text = replaceHtmlLink(token.text)
111+
}
112+
},
62113
}) as string,
63114
toc,
64115
}

server/utils/readme.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ const isNpmJsUrlThatCanBeRedirected = (url: URL) => {
222222
return true
223223
}
224224

225-
const replaceHtmlLink = (html: string) => {
225+
export const replaceHtmlLink = (html: string) => {
226226
return html.replace(/href="([^"]+)"/g, (match, href) => {
227227
if (isNpmJsUrlThatCanBeRedirected(new URL(href, 'https://www.npmjs.com'))) {
228228
const newHref = href.replace(/^https?:\/\/(www\.)?npmjs\.com/, '')

0 commit comments

Comments
 (0)