Skip to content

Commit 30a602c

Browse files
fix: GFM alerts not displaying properly when links are present
Fixes #19409 Improved the parseChildrenAsAlertContent function to properly handle GFM alerts that contain links by: 1. Searching through all content elements to find the alert type pattern 2. Properly removing the alert type text from the rendered content 3. Filtering out null elements after processing This ensures that alerts with links render correctly as alerts rather than falling back to regular blockquotes. Co-authored-by: carryologist <158531215+carryologist@users.noreply.github.com>
1 parent f2ee89c commit 30a602c

File tree

3 files changed

+161
-26
lines changed

3 files changed

+161
-26
lines changed

site/src/components/Markdown/Markdown.stories.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,24 @@ export const GFMAlerts: Story = {
9595
`,
9696
},
9797
};
98+
99+
export const GFMAlertsWithLinks: Story = {
100+
args: {
101+
children: `
102+
> [!NOTE]
103+
> This template is centrally managed by CI/CD in the [coder/templates](https://github.com/coder/templates) repository.
104+
105+
> [!TIP]
106+
> Check out the [documentation](https://docs.coder.com) for more information.
107+
108+
> [!IMPORTANT]
109+
> Make sure to read the [security guidelines](https://coder.com/security) before proceeding.
110+
111+
> [!WARNING]
112+
> This action may affect your [workspace settings](https://coder.com/settings).
113+
114+
> [!CAUTION]
115+
> Deleting this will remove all data. See [backup guide](https://coder.com/backup) first.
116+
`,
117+
},
118+
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { AppProviders } from "App";
3+
import { createTestQueryClient } from "testHelpers/renderHelpers";
4+
import { Markdown } from "./Markdown";
5+
6+
const renderWithProviders = (children: React.ReactNode) => {
7+
return render(
8+
<AppProviders queryClient={createTestQueryClient()}>
9+
{children}
10+
</AppProviders>
11+
);
12+
};
13+
14+
describe("Markdown", () => {
15+
it("renders GFM alerts without links correctly", () => {
16+
const markdown = `> [!NOTE]
17+
> Useful information that users should know, even when skimming content.`;
18+
19+
renderWithProviders(<Markdown>{markdown}</Markdown>);
20+
21+
// Should render as an alert, not a regular blockquote
22+
const alert = screen.getByRole("complementary");
23+
expect(alert).toBeInTheDocument();
24+
expect(alert).toHaveTextContent("Useful information that users should know, even when skimming content.");
25+
});
26+
27+
it("renders GFM alerts with links correctly", () => {
28+
const markdown = `> [!NOTE]
29+
> This template is centrally managed by CI/CD in the [coder/templates](https://github.com/coder/templates) repository.`;
30+
31+
renderWithProviders(<Markdown>{markdown}</Markdown>);
32+
33+
// Should render as an alert, not a regular blockquote
34+
const alert = screen.getByRole("complementary");
35+
expect(alert).toBeInTheDocument();
36+
// The alert should contain the content (the alert type might be included)
37+
expect(alert).toHaveTextContent(/This template is centrally managed by CI\/CD in the.*repository/);
38+
39+
// Should contain the link
40+
const link = screen.getByRole("link", { name: /coder\/templates/ });
41+
expect(link).toBeInTheDocument();
42+
expect(link).toHaveAttribute("href", "https://github.com/coder/templates");
43+
});
44+
45+
it("renders multiple GFM alerts with links correctly", () => {
46+
const markdown = `> [!TIP]
47+
> Check out the [documentation](https://docs.coder.com) for more information.
48+
49+
> [!WARNING]
50+
> This action may affect your [workspace settings](https://coder.com/settings).`;
51+
52+
renderWithProviders(<Markdown>{markdown}</Markdown>);
53+
54+
// Should render both alerts
55+
const alerts = screen.getAllByRole("complementary");
56+
expect(alerts).toHaveLength(2);
57+
58+
// Check first alert (TIP)
59+
expect(alerts[0]).toHaveTextContent(/Check out the.*documentation.*for more information/);
60+
const docLink = screen.getByRole("link", { name: /documentation/ });
61+
expect(docLink).toHaveAttribute("href", "https://docs.coder.com");
62+
63+
// Check second alert (WARNING)
64+
expect(alerts[1]).toHaveTextContent(/This action may affect your.*workspace settings/);
65+
const settingsLink = screen.getByRole("link", { name: /workspace settings/ });
66+
expect(settingsLink).toHaveAttribute("href", "https://coder.com/settings");
67+
});
68+
69+
it("falls back to regular blockquote for invalid alert types", () => {
70+
const markdown = `> [!INVALID]
71+
> This should render as a regular blockquote.`;
72+
73+
renderWithProviders(<Markdown>{markdown}</Markdown>);
74+
75+
// Should render as a regular blockquote, not an alert
76+
// Use a more specific selector since blockquote doesn't have an accessible role
77+
const blockquote = screen.getByText(/\[!INVALID\].*This should render as a regular blockquote/);
78+
expect(blockquote).toBeInTheDocument();
79+
});
80+
81+
it("renders regular blockquotes without alert syntax", () => {
82+
const markdown = `> This is a regular blockquote without alert syntax.`;
83+
84+
renderWithProviders(<Markdown>{markdown}</Markdown>);
85+
86+
// Should render as a regular blockquote
87+
const blockquote = screen.getByText("This is a regular blockquote without alert syntax.");
88+
expect(blockquote).toBeInTheDocument();
89+
});
90+
});

site/src/components/Markdown/Markdown.tsx

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -301,31 +301,55 @@ function parseChildrenAsAlertContent(
301301
},
302302
};
303303
});
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-
};
304+
305+
// Find the alert type by looking for the first string that contains the alert pattern
306+
let alertType: string | null = null;
307+
let alertTypeIndex = -1;
308+
309+
for (let i = 0; i < outputContent.length; i++) {
310+
const el = outputContent[i];
311+
if (typeof el === "string") {
312+
const trimmed = el.trim();
313+
// Check if this string contains an alert pattern like [!NOTE], [!TIP], etc.
314+
const alertMatch = trimmed.match(/^\[!([A-Z]+)\]/);
315+
if (alertMatch) {
316+
alertType = alertMatch[1].toLowerCase();
317+
alertTypeIndex = i;
318+
319+
// Remove the alert type from this string and keep the rest
320+
const remainingText = trimmed.replace(/^\[!([A-Z]+)\]\s*/, "").trim();
321+
if (remainingText) {
322+
// Replace the current element with the remaining text
323+
outputContent[i] = remainingText;
324+
} else {
325+
// If nothing remains, mark for removal
326+
outputContent[i] = null;
327+
}
328+
break;
329+
}
330+
}
331+
}
332+
333+
if (!alertType || !githubFlavoredMarkdownAlertTypes.includes(alertType)) {
334+
return null;
335+
}
336+
337+
// Remove null elements and get the remaining content
338+
const remainingChildren = outputContent.filter((el, index) => {
339+
// Keep all elements except null ones
340+
return el !== null;
341+
});
342+
343+
const hasLeadingLinebreak =
344+
isValidElement(remainingChildren[0]) && remainingChildren[0].type === "br";
345+
if (hasLeadingLinebreak) {
346+
remainingChildren.shift();
347+
}
348+
349+
return {
350+
type: alertType,
351+
children: remainingChildren,
352+
};
329353
}
330354

331355
type MarkdownGfmAlertProps = Readonly<
@@ -425,4 +449,4 @@ const markdownStyles: Interpolation<Theme> = (theme: Theme) => ({
425449
color: theme.palette.error.light,
426450
},
427451
},
428-
});
452+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy