Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: support GFM alerts in markdown#17662

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
BrunoQuaresma merged 2 commits intomainfrombq/support-alerts-notes
May 2, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletionssite/src/components/Markdown/Markdown.stories.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
`,
},
};
177 changes: 176 additions & 1 deletionsite/src/components/Markdown/Markdown.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
/**
Expand DownExpand Up@@ -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,
}}
>
Expand DownExpand Up@@ -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",
Expand Down
4 changes: 4 additions & 0 deletionssite/src/index.css
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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%;
Expand All@@ -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%;
Expand DownExpand Up@@ -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%;
Expand All@@ -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%;
Expand Down
2 changes: 2 additions & 0 deletionssite/tailwind.config.js
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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))",
Expand All@@ -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: {
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp