Skip to content

fix: GFM alerts not displaying properly when links are present #19424

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
5 changes: 2 additions & 3 deletions site/src/components/Latency/Latency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ export const Latency: FC<LatencyProps> = ({
const notAvailableText = "Latency not available";
return (
<Tooltip title={notAvailableText}>
<>
<div>
<span css={{ ...visuallyHidden }}>{notAvailableText}</span>

<CircleHelpIcon className="ml-auto size-icon-sm" style={{ color }} />
</>
</div>
</Tooltip>
);
}
Expand Down
21 changes: 21 additions & 0 deletions site/src/components/Markdown/Markdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,24 @@ export const GFMAlerts: Story = {
`,
},
};

export const GFMAlertsWithLinks: Story = {
args: {
children: `
> [!NOTE]
> This template is centrally managed by CI/CD in the [coder/templates](https://github.com/coder/templates) repository.

> [!TIP]
> Check out the [documentation](https://docs.coder.com) for more information.

> [!IMPORTANT]
> Make sure to read the [security guidelines](https://coder.com/security) before proceeding.

> [!WARNING]
> This action may affect your [workspace settings](https://coder.com/settings).

> [!CAUTION]
> Deleting this will remove all data. See [backup guide](https://coder.com/backup) first.
`,
},
};
104 changes: 104 additions & 0 deletions site/src/components/Markdown/Markdown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { AppProviders } from "App";
import { createTestQueryClient } from "testHelpers/renderHelpers";
import { render, screen } from "@testing-library/react";
import { Markdown } from "./Markdown";

const renderWithProviders = (children: React.ReactNode) => {
return render(
<AppProviders queryClient={createTestQueryClient()}>
{children}
</AppProviders>,
);
};

describe("Markdown", () => {
it("renders GFM alerts without links correctly", () => {
const markdown = `> [!NOTE]
> Useful information that users should know, even when skimming content.`;

renderWithProviders(<Markdown>{markdown}</Markdown>);

// Should render as an alert, not a regular blockquote
const alert = screen.getByRole("complementary");
expect(alert).toBeInTheDocument();
expect(alert).toHaveTextContent(
"Useful information that users should know, even when skimming content.",
);
});

it("renders GFM alerts with links correctly", () => {
const markdown = `> [!NOTE]
> This template is centrally managed by CI/CD in the [coder/templates](https://github.com/coder/templates) repository.`;

renderWithProviders(<Markdown>{markdown}</Markdown>);

// Should render as an alert, not a regular blockquote
const alert = screen.getByRole("complementary");
expect(alert).toBeInTheDocument();
// The alert should contain the content (the alert type might be included)
expect(alert).toHaveTextContent(
/This template is centrally managed by CI\/CD in the.*repository/,
);

// Should contain the link
const link = screen.getByRole("link", { name: /coder\/templates/ });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "https://github.com/coder/templates");
});

it("renders multiple GFM alerts with links correctly", () => {
const markdown = `> [!TIP]
> Check out the [documentation](https://docs.coder.com) for more information.

> [!WARNING]
> This action may affect your [workspace settings](https://coder.com/settings).`;

renderWithProviders(<Markdown>{markdown}</Markdown>);

// Should render both alerts
const alerts = screen.getAllByRole("complementary");
expect(alerts).toHaveLength(2);

// Check first alert (TIP)
expect(alerts[0]).toHaveTextContent(
/Check out the.*documentation.*for more information/,
);
const docLink = screen.getByRole("link", { name: /documentation/ });
expect(docLink).toHaveAttribute("href", "https://docs.coder.com");

// Check second alert (WARNING)
expect(alerts[1]).toHaveTextContent(
/This action may affect your.*workspace settings/,
);
const settingsLink = screen.getByRole("link", {
name: /workspace settings/,
});
expect(settingsLink).toHaveAttribute("href", "https://coder.com/settings");
});

it("falls back to regular blockquote for invalid alert types", () => {
const markdown = `> [!INVALID]
> This should render as a regular blockquote.`;

renderWithProviders(<Markdown>{markdown}</Markdown>);

// Should render as a regular blockquote, not an alert
// Use a more specific selector since blockquote doesn't have an accessible role
const blockquote = screen.getByText(
/\[!INVALID\].*This should render as a regular blockquote/,
);
expect(blockquote).toBeInTheDocument();
});

it("renders regular blockquotes without alert syntax", () => {
const markdown = "> This is a regular blockquote without alert syntax.";

renderWithProviders(<Markdown>{markdown}</Markdown>);

// Should render as a regular blockquote
const blockquote = screen.getByText(
"This is a regular blockquote without alert syntax.",
);
expect(blockquote).toBeInTheDocument();
});
});
42 changes: 32 additions & 10 deletions site/src/components/Markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,21 +301,43 @@ function parseChildrenAsAlertContent(
},
};
});
const [firstEl, ...remainingChildren] = outputContent;
if (typeof firstEl !== "string") {
return null;

// Find the alert type by looking for the first string that contains the alert pattern
let alertType: string | null = null;

for (let i = 0; i < outputContent.length; i++) {
const el = outputContent[i];
if (typeof el === "string") {
const trimmed = el.trim();
// Check if this string contains an alert pattern like [!NOTE], [!TIP], etc.
const alertMatch = trimmed.match(/^\[!([A-Z]+)\]/);
if (alertMatch) {
alertType = alertMatch[1].toLowerCase();

// Remove the alert type from this string and keep the rest
const remainingText = trimmed.replace(/^\[!([A-Z]+)\]\s*/, "").trim();
if (remainingText) {
// Replace the current element with the remaining text
outputContent[i] = remainingText;
} else {
// If nothing remains, mark for removal
outputContent[i] = null;
}
break;
}
}
}

const alertType = firstEl
.trim()
.toLowerCase()
.replace("!", "")
.replace("[", "")
.replace("]", "");
if (!githubFlavoredMarkdownAlertTypes.includes(alertType)) {
if (!alertType || !githubFlavoredMarkdownAlertTypes.includes(alertType)) {
return null;
}

// Remove null elements and get the remaining content
const remainingChildren = outputContent.filter((el) => {
// Keep all elements except null ones
return el !== null;
});

const hasLeadingLinebreak =
isValidElement(remainingChildren[0]) && remainingChildren[0].type === "br";
if (hasLeadingLinebreak) {
Expand Down
Loading
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