Bruno Quaresma
2025-05-02 14:44:01 -03:00
committed by GitHub
parent 544259b809
commit 3be6487f02
4 changed files with 203 additions and 1 deletions

View File

@ -74,3 +74,24 @@ export const WithTable: Story = {
| cell 1 | cell 2 | 3 | 4 | `,
},
};
export const GFMAlerts: Story = {
args: {
children: `
> [!NOTE]
> Useful information that users should know, even when skimming content.
> [!TIP]
> Helpful advice for doing things better or more easily.
> [!IMPORTANT]
> Key information users need to know to achieve their goal.
> [!WARNING]
> Urgent info that needs immediate user attention to avoid problems.
> [!CAUTION]
> Advises about risks or negative outcomes of certain actions.
`,
},
};

View File

@ -8,12 +8,20 @@ import {
TableRow,
} from "components/Table/Table";
import isEqual from "lodash/isEqual";
import { type FC, memo } from "react";
import {
type FC,
type HTMLProps,
type ReactElement,
type ReactNode,
isValidElement,
memo,
} from "react";
import ReactMarkdown, { type Options } from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";
import gfm from "remark-gfm";
import colors from "theme/tailwindColors";
import { cn } from "utils/cn";
interface MarkdownProps {
/**
@ -114,6 +122,30 @@ export const Markdown: FC<MarkdownProps> = (props) => {
return <TableCell>{children}</TableCell>;
},
/**
* 2025-02-10 - The RemarkGFM plugin that we use currently doesn't have
* support for special alert messages like this:
* ```
* > [!IMPORTANT]
* > This module will only work with Git versions >=2.34, and...
* ```
* Have to intercept all blockquotes and see if their content is
* formatted like an alert.
*/
blockquote: (parseProps) => {
const { node: _node, children, ...renderProps } = parseProps;
const alertContent = parseChildrenAsAlertContent(children);
if (alertContent === null) {
return <blockquote {...renderProps}>{children}</blockquote>;
}
return (
<MarkdownGfmAlert alertType={alertContent.type} {...renderProps}>
{alertContent.children}
</MarkdownGfmAlert>
);
},
...components,
}}
>
@ -197,6 +229,149 @@ export const InlineMarkdown: FC<InlineMarkdownProps> = (props) => {
export const MemoizedMarkdown = memo(Markdown, isEqual);
export const MemoizedInlineMarkdown = memo(InlineMarkdown, isEqual);
const githubFlavoredMarkdownAlertTypes = [
"tip",
"note",
"important",
"warning",
"caution",
];
type AlertContent = Readonly<{
type: string;
children: readonly ReactNode[];
}>;
function parseChildrenAsAlertContent(
jsxChildren: ReactNode,
): AlertContent | null {
// Have no idea why the plugin parses the data by mixing node types
// like this. Have to do a good bit of nested filtering.
if (!Array.isArray(jsxChildren)) {
return null;
}
const mainParentNode = jsxChildren.find((node): node is ReactElement =>
isValidElement(node),
);
let parentChildren = mainParentNode?.props.children;
if (typeof parentChildren === "string") {
// Children will only be an array if the parsed text contains other
// content that can be turned into HTML. If there aren't any, you
// just get one big string
parentChildren = parentChildren.split("\n");
}
if (!Array.isArray(parentChildren)) {
return null;
}
const outputContent = parentChildren
.filter((el) => {
if (isValidElement(el)) {
return true;
}
return typeof el === "string" && el !== "\n";
})
.map((el) => {
if (!isValidElement(el)) {
return el;
}
if (el.type !== "a") {
return el;
}
const recastProps = el.props as Record<string, unknown> & {
children?: ReactNode;
};
if (recastProps.target === "_blank") {
return el;
}
return {
...el,
props: {
...recastProps,
target: "_blank",
children: (
<>
{recastProps.children}
<span className="sr-only"> (link opens in new tab)</span>
</>
),
},
};
});
const [firstEl, ...remainingChildren] = outputContent;
if (typeof firstEl !== "string") {
return null;
}
const alertType = firstEl
.trim()
.toLowerCase()
.replace("!", "")
.replace("[", "")
.replace("]", "");
if (!githubFlavoredMarkdownAlertTypes.includes(alertType)) {
return null;
}
const hasLeadingLinebreak =
isValidElement(remainingChildren[0]) && remainingChildren[0].type === "br";
if (hasLeadingLinebreak) {
remainingChildren.shift();
}
return {
type: alertType,
children: remainingChildren,
};
}
type MarkdownGfmAlertProps = Readonly<
HTMLProps<HTMLElement> & {
alertType: string;
}
>;
const MarkdownGfmAlert: FC<MarkdownGfmAlertProps> = ({
alertType,
children,
...delegatedProps
}) => {
return (
<div className="pb-6">
<aside
{...delegatedProps}
className={cn(
"border-0 border-l-4 border-solid border-border p-4 text-white",
"[&_p]:m-0 [&_p]:mb-2",
alertType === "important" &&
"border-highlight-purple [&_p:first-child]:text-highlight-purple",
alertType === "warning" &&
"border-border-warning [&_p:first-child]:text-border-warning",
alertType === "note" &&
"border-highlight-sky [&_p:first-child]:text-highlight-sky",
alertType === "tip" &&
"border-highlight-green [&_p:first-child]:text-highlight-green",
alertType === "caution" &&
"border-highlight-red [&_p:first-child]:text-highlight-red",
)}
>
<p className="font-bold">
{alertType[0]?.toUpperCase() + alertType.slice(1).toLowerCase()}
</p>
{children}
</aside>
</div>
);
};
const markdownStyles: Interpolation<Theme> = (theme: Theme) => ({
fontSize: 16,
lineHeight: "24px",

View File

@ -29,6 +29,7 @@
--surface-orange: 34 100% 92%;
--surface-sky: 201 94% 86%;
--surface-red: 0 93% 94%;
--surface-purple: 251 91% 95%;
--border-default: 240 6% 90%;
--border-success: 142 76% 36%;
--border-warning: 30.66, 97.16%, 72.35%;
@ -41,6 +42,7 @@
--highlight-green: 143 64% 24%;
--highlight-grey: 240 5% 65%;
--highlight-sky: 201 90% 27%;
--highlight-red: 0 74% 42%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
@ -69,6 +71,7 @@
--surface-orange: 13 81% 15%;
--surface-sky: 204 80% 16%;
--surface-red: 0 75% 15%;
--surface-purple: 261 73% 23%;
--border-default: 240 4% 16%;
--border-success: 142 76% 36%;
--border-warning: 30.66, 97.16%, 72.35%;
@ -80,6 +83,7 @@
--highlight-green: 141 79% 85%;
--highlight-grey: 240 4% 46%;
--highlight-sky: 198 93% 60%;
--highlight-red: 0 91% 71%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;

View File

@ -53,6 +53,7 @@ module.exports = {
orange: "hsl(var(--surface-orange))",
sky: "hsl(var(--surface-sky))",
red: "hsl(var(--surface-red))",
purple: "hsl(var(--surface-purple))",
},
border: {
DEFAULT: "hsl(var(--border-default))",
@ -69,6 +70,7 @@ module.exports = {
green: "hsl(var(--highlight-green))",
grey: "hsl(var(--highlight-grey))",
sky: "hsl(var(--highlight-sky))",
red: "hsl(var(--highlight-red))",
},
},
keyframes: {