@@ -8,12 +8,20 @@ import {
8
8
TableRow ,
9
9
} from "components/Table/Table" ;
10
10
import isEqual from "lodash/isEqual" ;
11
- import { type FC , memo } from "react" ;
11
+ import {
12
+ type FC ,
13
+ type HTMLProps ,
14
+ type ReactElement ,
15
+ type ReactNode ,
16
+ isValidElement ,
17
+ memo ,
18
+ } from "react" ;
12
19
import ReactMarkdown , { type Options } from "react-markdown" ;
13
20
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" ;
14
21
import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism" ;
15
22
import gfm from "remark-gfm" ;
16
23
import colors from "theme/tailwindColors" ;
24
+ import { cn } from "utils/cn" ;
17
25
18
26
interface MarkdownProps {
19
27
/**
@@ -114,6 +122,30 @@ export const Markdown: FC<MarkdownProps> = (props) => {
114
122
return < TableCell > { children } </ TableCell > ;
115
123
} ,
116
124
125
+ /**
126
+ * 2025-02-10 - The RemarkGFM plugin that we use currently doesn't have
127
+ * support for special alert messages like this:
128
+ * ```
129
+ * > [!IMPORTANT]
130
+ * > This module will only work with Git versions >=2.34, and...
131
+ * ```
132
+ * Have to intercept all blockquotes and see if their content is
133
+ * formatted like an alert.
134
+ */
135
+ blockquote : ( parseProps ) => {
136
+ const { node : _node , children, ...renderProps } = parseProps ;
137
+ const alertContent = parseChildrenAsAlertContent ( children ) ;
138
+ if ( alertContent === null ) {
139
+ return < blockquote { ...renderProps } > { children } </ blockquote > ;
140
+ }
141
+
142
+ return (
143
+ < MarkdownGfmAlert alertType = { alertContent . type } { ...renderProps } >
144
+ { alertContent . children }
145
+ </ MarkdownGfmAlert >
146
+ ) ;
147
+ } ,
148
+
117
149
...components ,
118
150
} }
119
151
>
@@ -197,6 +229,149 @@ export const InlineMarkdown: FC<InlineMarkdownProps> = (props) => {
197
229
export const MemoizedMarkdown = memo ( Markdown , isEqual ) ;
198
230
export const MemoizedInlineMarkdown = memo ( InlineMarkdown , isEqual ) ;
199
231
232
+ const githubFlavoredMarkdownAlertTypes = [
233
+ "tip" ,
234
+ "note" ,
235
+ "important" ,
236
+ "warning" ,
237
+ "caution" ,
238
+ ] ;
239
+
240
+ type AlertContent = Readonly < {
241
+ type : string ;
242
+ children : readonly ReactNode [ ] ;
243
+ } > ;
244
+
245
+ function parseChildrenAsAlertContent (
246
+ jsxChildren : ReactNode ,
247
+ ) : AlertContent | null {
248
+ // Have no idea why the plugin parses the data by mixing node types
249
+ // like this. Have to do a good bit of nested filtering.
250
+ if ( ! Array . isArray ( jsxChildren ) ) {
251
+ return null ;
252
+ }
253
+
254
+ const mainParentNode = jsxChildren . find ( ( node ) : node is ReactElement =>
255
+ isValidElement ( node ) ,
256
+ ) ;
257
+ let parentChildren = mainParentNode ?. props . children ;
258
+ if ( typeof parentChildren === "string" ) {
259
+ // Children will only be an array if the parsed text contains other
260
+ // content that can be turned into HTML. If there aren't any, you
261
+ // just get one big string
262
+ parentChildren = parentChildren . split ( "\n" ) ;
263
+ }
264
+ if ( ! Array . isArray ( parentChildren ) ) {
265
+ return null ;
266
+ }
267
+
268
+ const outputContent = parentChildren
269
+ . filter ( ( el ) => {
270
+ if ( isValidElement ( el ) ) {
271
+ return true ;
272
+ }
273
+ return typeof el === "string" && el !== "\n" ;
274
+ } )
275
+ . map ( ( el ) => {
276
+ if ( ! isValidElement ( el ) ) {
277
+ return el ;
278
+ }
279
+ if ( el . type !== "a" ) {
280
+ return el ;
281
+ }
282
+
283
+ const recastProps = el . props as Record < string , unknown > & {
284
+ children ?: ReactNode ;
285
+ } ;
286
+ if ( recastProps . target === "_blank" ) {
287
+ return el ;
288
+ }
289
+
290
+ return {
291
+ ...el ,
292
+ props : {
293
+ ...recastProps ,
294
+ target : "_blank" ,
295
+ children : (
296
+ < >
297
+ { recastProps . children }
298
+ < span className = "sr-only" > (link opens in new tab)</ span >
299
+ </ >
300
+ ) ,
301
+ } ,
302
+ } ;
303
+ } ) ;
304
+ const [ firstEl , ...remainingChildren ] = outputContent ;
305
+ if ( typeof firstEl !== "string" ) {
306
+ return null ;
307
+ }
308
+
309
+ const alertType = firstEl
310
+ . trim ( )
311
+ . toLowerCase ( )
312
+ . replace ( "!" , "" )
313
+ . replace ( "[" , "" )
314
+ . replace ( "]" , "" ) ;
315
+ if ( ! githubFlavoredMarkdownAlertTypes . includes ( alertType ) ) {
316
+ return null ;
317
+ }
318
+
319
+ const hasLeadingLinebreak =
320
+ isValidElement ( remainingChildren [ 0 ] ) && remainingChildren [ 0 ] . type === "br" ;
321
+ if ( hasLeadingLinebreak ) {
322
+ remainingChildren . shift ( ) ;
323
+ }
324
+
325
+ return {
326
+ type : alertType ,
327
+ children : remainingChildren ,
328
+ } ;
329
+ }
330
+
331
+ type MarkdownGfmAlertProps = Readonly <
332
+ HTMLProps < HTMLElement > & {
333
+ alertType : string ;
334
+ }
335
+ > ;
336
+
337
+ const MarkdownGfmAlert : FC < MarkdownGfmAlertProps > = ( {
338
+ alertType,
339
+ children,
340
+ ...delegatedProps
341
+ } ) => {
342
+ return (
343
+ < div className = "pb-6" >
344
+ < aside
345
+ { ...delegatedProps }
346
+ className = { cn (
347
+ "border-0 border-l-4 border-solid border-border p-4 text-white" ,
348
+ "[&_p]:m-0 [&_p]:mb-2" ,
349
+
350
+ alertType === "important" &&
351
+ "border-highlight-purple [&_p:first-child]:text-highlight-purple" ,
352
+
353
+ alertType === "warning" &&
354
+ "border-border-warning [&_p:first-child]:text-border-warning" ,
355
+
356
+ alertType === "note" &&
357
+ "border-highlight-sky [&_p:first-child]:text-highlight-sky" ,
358
+
359
+ alertType === "tip" &&
360
+ "border-highlight-green [&_p:first-child]:text-highlight-green" ,
361
+
362
+ alertType === "caution" &&
363
+ "border-highlight-red [&_p:first-child]:text-highlight-red" ,
364
+ ) }
365
+ >
366
+ < p className = "font-bold" >
367
+ { alertType [ 0 ] ?. toUpperCase ( ) + alertType . slice ( 1 ) . toLowerCase ( ) }
368
+ </ p >
369
+ { children }
370
+ </ aside >
371
+ </ div >
372
+ ) ;
373
+ } ;
374
+
200
375
const markdownStyles : Interpolation < Theme > = ( theme : Theme ) => ( {
201
376
fontSize : 16 ,
202
377
lineHeight : "24px" ,
0 commit comments