11import { 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'
310import sanitizeHtml from 'sanitize-html'
411
512export 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 > \[ ! ( N O T E | T I P | I M P O R T A N T | W A R N I N G | C A U T I O N ) \] (?: < b r > ) ? \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 }
0 commit comments