@@ -32,6 +32,9 @@ import annotate from './annotate'
3232import alerts from './alerts'
3333import removeHtmlComments from 'remark-remove-comments'
3434import remarkStringify from 'remark-stringify'
35+ import { visit } from 'unist-util-visit'
36+ import type { Parents , Root , Text } from 'mdast'
37+ import type { Info , State } from 'mdast-util-to-markdown'
3538import type { Context , UnifiedProcessor } from '@/content-render/types'
3639import type { CollectedHeading } from '@/frame/lib/get-mini-toc-items'
3740
@@ -84,12 +87,49 @@ export function createMarkdownOnlyProcessor(context: Context): UnifiedProcessor
8487 unified ( )
8588 . use ( remarkParse )
8689 . use ( gfm )
90+ . use ( markUnescapedAlertMarkers )
8791 // TypeScript's unified plugin types do not model context-bound transformer factories.
8892 . use ( rewriteLocalLinks as unknown as ( ctx : Context ) => void , context )
89- . use ( remarkStringify ) as unknown as UnifiedProcessor
93+ . use ( remarkStringify , {
94+ handlers : { text : textWithAlertMarkers } ,
95+ } ) as unknown as UnifiedProcessor
9096 )
9197}
9298
99+ // GitHub alert markers stop rendering as alerts once remark-stringify escapes their brackets.
100+ const ALERT_MARKER = / ^ \[ ! ( 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 ) \] (? = \s | $ ) / i
101+
102+ // remark-parse drops the backslash from `\[!NOTE]`, so only the source shows an intentionally literal marker.
103+ const unescapedAlertMarkers = new WeakSet < Text > ( )
104+
105+ function markUnescapedAlertMarkers ( ) {
106+ return ( tree : Root , file : { value : unknown } ) => {
107+ const source = String ( file . value )
108+ visit ( tree , 'text' , ( node ) => {
109+ const offset = node . position ?. start . offset
110+ if ( offset !== undefined && source [ offset ] === '[' && ALERT_MARKER . test ( node . value ) ) {
111+ unescapedAlertMarkers . add ( node )
112+ }
113+ } )
114+ }
115+ }
116+
117+ function textWithAlertMarkers (
118+ node : Text ,
119+ parent : Parents | undefined ,
120+ state : State ,
121+ info : Info ,
122+ ) : string {
123+ const marker = node . value . match ( ALERT_MARKER ) ?. [ 0 ]
124+ const startsBlockquoteParagraph =
125+ parent ?. type === 'paragraph' &&
126+ parent . children [ 0 ] === node &&
127+ state . stack [ state . stack . length - 3 ] === 'blockquote'
128+ if ( ! marker || ! startsBlockquoteParagraph || ! unescapedAlertMarkers . has ( node ) )
129+ return state . safe ( node . value , info )
130+ return marker + state . safe ( node . value . slice ( marker . length ) , { ...info , before : ']' } )
131+ }
132+
93133export function createMinimalProcessor ( context : Context ) : UnifiedProcessor {
94134 return (
95135 unified ( )
0 commit comments