diff --git a/site/src/components/Latency/Latency.tsx b/site/src/components/Latency/Latency.tsx index 136d79bb5b8f0..95f69e1c2c0c6 100644 --- a/site/src/components/Latency/Latency.tsx +++ b/site/src/components/Latency/Latency.tsx @@ -34,11 +34,10 @@ export const Latency: FC = ({ const notAvailableText = "Latency not available"; return ( - <> +
{notAvailableText} - - +
); } diff --git a/site/src/components/Markdown/Markdown.stories.tsx b/site/src/components/Markdown/Markdown.stories.tsx index b2351c1d43153..15a529afd3002 100644 --- a/site/src/components/Markdown/Markdown.stories.tsx +++ b/site/src/components/Markdown/Markdown.stories.tsx @@ -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. + `, + }, +}; diff --git a/site/src/components/Markdown/Markdown.test.tsx b/site/src/components/Markdown/Markdown.test.tsx new file mode 100644 index 0000000000000..7f9852b6fb454 --- /dev/null +++ b/site/src/components/Markdown/Markdown.test.tsx @@ -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( + + {children} + , + ); +}; + +describe("Markdown", () => { + it("renders GFM alerts without links correctly", () => { + const markdown = `> [!NOTE] +> Useful information that users should know, even when skimming content.`; + + renderWithProviders({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}); + + // 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}); + + // 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}); + + // 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}); + + // Should render as a regular blockquote + const blockquote = screen.getByText( + "This is a regular blockquote without alert syntax.", + ); + expect(blockquote).toBeInTheDocument(); + }); +}); diff --git a/site/src/components/Markdown/Markdown.tsx b/site/src/components/Markdown/Markdown.tsx index ba7bcbf29a903..0453d835a0750 100644 --- a/site/src/components/Markdown/Markdown.tsx +++ b/site/src/components/Markdown/Markdown.tsx @@ -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) { 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