From 113a71639387fb0c2aa4d30db5fb2e70e0f6dc35 Mon Sep 17 00:00:00 2001 From: Damian Stasik <920747+damianstasik@users.noreply.github.com> Date: Fri, 6 Sep 2024 21:07:12 +0200 Subject: [PATCH] Add support for GitHub's admonitions --- frontend/package-lock.json | 17 +++ frontend/package.json | 2 + .../src/components/Markdown/Blockquote.tsx | 104 ++++++++++++++++++ frontend/src/components/Markdown/H4.tsx | 17 +++ frontend/src/components/Markdown/P.tsx | 2 - frontend/src/components/Markdown/index.tsx | 4 + 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Markdown/Blockquote.tsx create mode 100644 frontend/src/components/Markdown/H4.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f381a3e..cbbfc497 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@tanstack/react-query-devtools": "^5.53.2", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "@types/react-is": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", "clsx": "^2.1.1", @@ -31,6 +32,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", + "react-is": "^18.3.1", "react-router-dom": "^6.26.1", "react-virtuoso": "^4.10.2", "rehype-raw": "^7.0.0", @@ -1612,6 +1614,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-is": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.3.0.tgz", + "integrity": "sha512-KZJpHUkAdzyKj/kUHJDc6N7KyidftICufJfOFpiG6haL/BDQNQt5i4n1XDUL/nDZAtGLHDSWRYpLzKTAKSvX6w==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -6115,6 +6126,12 @@ "react": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3a8dd6f0..a5c33327 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@tanstack/react-query-devtools": "^5.53.2", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "@types/react-is": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", "clsx": "^2.1.1", @@ -37,6 +38,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", + "react-is": "^18.3.1", "react-router-dom": "^6.26.1", "react-virtuoso": "^4.10.2", "rehype-raw": "^7.0.0", diff --git a/frontend/src/components/Markdown/Blockquote.tsx b/frontend/src/components/Markdown/Blockquote.tsx new file mode 100644 index 00000000..e5b16334 --- /dev/null +++ b/frontend/src/components/Markdown/Blockquote.tsx @@ -0,0 +1,104 @@ +import clsx from "clsx"; +import { BlockquoteHTMLAttributes, ReactNode } from "react"; +import { isElement } from "react-is"; + +const WARNING_MARK = "[!WARNING]"; +const IMPORTANT_MARK = "[!DANGER]"; +const NOTE_MARK = "[!NOTE]"; +const TIP_MARK = "[!TIP]"; +const CAUTION_MARK = "[!CAUTION]"; + +function getAdmonitionClassName(prefix: string) { + switch (prefix) { + case NOTE_MARK: + return "bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-100"; + case CAUTION_MARK: + return "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-100"; + case WARNING_MARK: + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-950 dark:text-yellow-100"; + case TIP_MARK: + return "bg-green-100 text-green-800 dark:bg-green-950 dark:text-green-100"; + case IMPORTANT_MARK: + return "bg-purple-100 text-purple-800 dark:bg-purple-950 dark:text-purple-100"; + } +} + +function getAdmonitionTitle(prefix: string) { + switch (prefix) { + case NOTE_MARK: + return "Note"; + case CAUTION_MARK: + return "Caution"; + case WARNING_MARK: + return "Warning"; + case TIP_MARK: + return "Tip"; + case IMPORTANT_MARK: + return "Important"; + } +} + +function getAdmonitionMatch(children: ReactNode) { + if (!Array.isArray(children)) { + return null; + } + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + + if (!isElement(child)) { + continue; + } + + const type = child.props.children; + + if (typeof type !== "string") { + continue; + } + + switch (type) { + case WARNING_MARK: + case CAUTION_MARK: + case NOTE_MARK: + case TIP_MARK: + case IMPORTANT_MARK: + return { + prefix: type, + content: children.slice(i + 1), + }; + } + } + + return null; +} + +export function MarkdownBlockquote({ + children, +}: BlockquoteHTMLAttributes) { + const admonitionMatch = getAdmonitionMatch(children); + + if (admonitionMatch) { + const { prefix, content } = admonitionMatch; + const className = getAdmonitionClassName(prefix); + const title = getAdmonitionTitle(prefix); + + return ( +
h6]:mt-4 [&>p]:mt-2 [li>&:first-child]:mt-0", + className, + )} + > + {title} + {content} +
+ ); + } + + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/Markdown/H4.tsx b/frontend/src/components/Markdown/H4.tsx new file mode 100644 index 00000000..479f972d --- /dev/null +++ b/frontend/src/components/Markdown/H4.tsx @@ -0,0 +1,17 @@ +import { HTMLAttributes } from "react"; +import { HeadingLink } from "../HeadingLink"; + +export function MarkdownH4({ + children, + id, +}: HTMLAttributes) { + return ( +
+ {children} + {id && } +
+ ); +} diff --git a/frontend/src/components/Markdown/P.tsx b/frontend/src/components/Markdown/P.tsx index 55832980..e61e40f4 100644 --- a/frontend/src/components/Markdown/P.tsx +++ b/frontend/src/components/Markdown/P.tsx @@ -14,8 +14,6 @@ function getAdmonitionClassName(prefix: string) { return "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-100"; case WARNING_MARK: return "bg-yellow-100 text-yellow-800 dark:bg-yellow-950 dark:text-yellow-100"; - default: - return ""; } } diff --git a/frontend/src/components/Markdown/index.tsx b/frontend/src/components/Markdown/index.tsx index 35044ce6..c7f14d52 100644 --- a/frontend/src/components/Markdown/index.tsx +++ b/frontend/src/components/Markdown/index.tsx @@ -25,6 +25,8 @@ import { MarkdownTh } from "./Th"; import { MarkdownImg } from "./Img"; import { MarkdownOl } from "./Ol"; import { MarkdownHr } from "./Hr"; +import { MarkdownBlockquote } from "./Blockquote"; +import { MarkdownH4 } from "./H4"; const production: Options = { development: false, @@ -38,6 +40,7 @@ const production: Options = { h1: MarkdownH1, h2: MarkdownH2, h3: MarkdownH3, + h4: MarkdownH4, p: MarkdownP, code: MarkdownCode, pre: MarkdownPre, @@ -47,6 +50,7 @@ const production: Options = { img: MarkdownImg, ol: MarkdownOl, hr: MarkdownHr, + blockquote: MarkdownBlockquote, }, };