From 1f3d95c1ac729545b8096d903e6815847ad01a57 Mon Sep 17 00:00:00 2001 From: Innei Date: Sun, 29 Oct 2023 17:23:56 +0800 Subject: [PATCH] feat: support gfm alerts Signed-off-by: Innei --- src/components/icons/status.tsx | 26 ++---- src/components/ui/link/MLink.tsx | 1 - src/components/ui/markdown/Markdown.tsx | 2 + src/components/ui/markdown/customize.md | 11 +++ src/components/ui/markdown/parsers/alert.tsx | 92 +++++++++++++++++++ .../ui/markdown/parsers/container.tsx | 1 + 6 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 src/components/ui/markdown/parsers/alert.tsx diff --git a/src/components/icons/status.tsx b/src/components/icons/status.tsx index 8aebf51622..22f8a51b13 100644 --- a/src/components/icons/status.tsx +++ b/src/components/icons/status.tsx @@ -44,26 +44,16 @@ export function ClaritySuccessLine(props: SVGProps) { export function IonInformation(props: SVGProps) { return ( - - - + ) diff --git a/src/components/ui/link/MLink.tsx b/src/components/ui/link/MLink.tsx index 7c672a5bb2..a94fa21760 100644 --- a/src/components/ui/link/MLink.tsx +++ b/src/components/ui/link/MLink.tsx @@ -84,7 +84,6 @@ export const MLink: FC<{ const showRichLink = !!parsedType && !!parsedName - console.log(text || parsedName, 'text || parsedName') return ( = ins: InsertRule, kateX: KateXRule, container: ContainerRule, + alerts: AlertsRule, ...additionalParserRules, }, diff --git a/src/components/ui/markdown/customize.md b/src/components/ui/markdown/customize.md index 249e0b27be..42e1c00211 100644 --- a/src/components/ui/markdown/customize.md +++ b/src/components/ui/markdown/customize.md @@ -198,3 +198,14 @@ Inline https://github.com/Innei ``` [Innei 太菜了]{GH@Innei} + +## Alerts + +> [!NOTE] +> Highlights information that users should take into account, even when skimming. + +> [!IMPORTANT] +> Crucial information necessary for users to succeed. + +> [!WARNING] +> Critical content demanding immediate user attention due to potential risks. diff --git a/src/components/ui/markdown/parsers/alert.tsx b/src/components/ui/markdown/parsers/alert.tsx new file mode 100644 index 0000000000..d9da62df2d --- /dev/null +++ b/src/components/ui/markdown/parsers/alert.tsx @@ -0,0 +1,92 @@ +import clsx from 'clsx' +import { blockRegex, Priority } from 'markdown-to-jsx' +import type { MarkdownToJSX } from 'markdown-to-jsx' + +import { + FluentShieldError20Regular, + FluentWarning28Regular, + IonInformation, +} from '~/components/icons/status' + +import { Markdown } from '../Markdown' + +const textColorMap = { + NOTE: 'text-always-blue-500 dark:text-always-blue-400', + IMPORTANT: 'text-accent', + WARNING: 'text-amber-500 dark:text-amber-400', +} as any + +const borderColorMap = { + NOTE: 'border-always-blue-500 dark:border-always-blue-400', + IMPORTANT: 'border-accent', + WARNING: 'border-amber-500 dark:border-amber-400', +} as any + +const typedIconMap = { + NOTE: IonInformation, + IMPORTANT: FluentWarning28Regular, + WARNING: FluentShieldError20Regular, +} as any + +/** + * + * > [!NOTE] + * > Highlights information that users should take into account, even when skimming. + */ +const ALERT_BLOCKQUOTE_R = + // /^( *> *\[!(NOTE|IMPORTANT|WARNING)\] *\n(?: *>.*(?:\n|$))+)/m + // /^( *> *\[!(?:(?NOTE|IMPORTANT|WARNING))\](?.*(\n *>.*?)*))(?=\n *> *\[(NOTE|IMPORTANT|WARNING)\]|$)/ + // /^(> *\[!(?NOTE|IMPORTANT|WARNING)\](?(?:\n?.*?)*))((?:\n{2,})|$)/ + // /^*> *\[!(?NOTE|IMPORTANT|WARNING)\](?(?:\n? *>.*?)*?)(?=\n{2,}|$)/ + // /^( *> *\[!(?NOTE|IMPORTANT|WARNING)\].*?(?:\n(?! *> *\[(NOTE|IMPORTANT|WARNING)\]).*?)*)/ + + // /^(> \[!(?NOTE|IMPORTANT|WARNING)\])(?(?:\n? *>.*?(?!\n *> *\[(?:NOTE|IMPORTANT|WARNING)\]))*)/ + /^(> \[!(?NOTE|IMPORTANT|WARNING)\].*?)(?(?:\n *>.*?)*)(?=\n{2,}|$)/ + +export const AlertsRule: MarkdownToJSX.Rule = { + match: blockRegex(ALERT_BLOCKQUOTE_R), + order: Priority.HIGH, + parse(capture) { + return { + raw: capture[0], + parsed: { + ...capture.groups, + }, + } + }, + react(node, output, state) { + const { type, body } = node.parsed + const bodyClean = body.replace(/^> */gm, '') + + const typePrefix = type[0] + type.toLowerCase().slice(1) + + const Icon = typedIconMap[type] || typedIconMap.info + return ( +
+ + + + {typePrefix} + +
+ + + {bodyClean} + +
+ ) + }, +} diff --git a/src/components/ui/markdown/parsers/container.tsx b/src/components/ui/markdown/parsers/container.tsx index b03092483a..9b365fc3a0 100644 --- a/src/components/ui/markdown/parsers/container.tsx +++ b/src/components/ui/markdown/parsers/container.tsx @@ -20,6 +20,7 @@ const shouldCatchContainerName = [ 'warning', 'note', ].join('|') + export const ContainerRule: MarkdownToJSX.Rule = { match: blockRegex( new RegExp(