Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 6264b30

Browse files
committed
feat: add GitHub-style Markdown alert rendering
Implements a local remark plugin that detects [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION] markers in blockquotes and renders them as visually distinct alert blocks. - Local remarkGithubAlerts plugin visits blockquote nodes - Adds data attributes and CSS classes for alert styling - Custom blockquote component renders alert title and content - Styles use VS Code theme variables for light/dark/HC themes - Normal blockquotes remain unchanged - No new dependencies added - 32 tests covering unit and integration scenarios
1 parent ad25634 commit 6264b30

4 files changed

Lines changed: 509 additions & 0 deletions

File tree

webview-ui/src/components/common/MarkdownBlock.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import remarkMath from "remark-math"
77
import remarkGfm from "remark-gfm"
88

99
import { vscode } from "@src/utils/vscode"
10+
import remarkGithubAlerts, { ALERT_LABELS, type AlertType } from "@src/utils/remarkGithubAlerts"
1011

1112
import CodeBlock from "./CodeBlock"
1213
import MermaidBlock from "./MermaidBlock"
@@ -201,11 +202,94 @@ const StyledMarkdown = styled.div`
201202
tr:hover {
202203
background-color: var(--vscode-list-hoverBackground);
203204
}
205+
206+
/* GitHub-style Markdown alert styles */
207+
.markdown-alert {
208+
padding: 8px 16px;
209+
margin: 1em 0;
210+
border-left: 4px solid;
211+
border-radius: 2px;
212+
background-color: var(--vscode-textBlockQuote-background, rgba(127, 127, 127, 0.1));
213+
214+
> p:first-child {
215+
margin-top: 0.25em;
216+
}
217+
218+
> p:last-child {
219+
margin-bottom: 0.25em;
220+
}
221+
}
222+
223+
.markdown-alert-title {
224+
display: flex;
225+
align-items: center;
226+
gap: 6px;
227+
font-weight: 600;
228+
margin-bottom: 4px;
229+
}
230+
231+
.markdown-alert-title svg {
232+
flex-shrink: 0;
233+
}
234+
235+
.markdown-alert-note {
236+
border-left-color: var(--vscode-textLink-foreground, #3794ff);
237+
}
238+
239+
.markdown-alert-note .markdown-alert-title {
240+
color: var(--vscode-textLink-foreground, #3794ff);
241+
}
242+
243+
.markdown-alert-tip {
244+
border-left-color: var(--vscode-testing-iconPassed, #73c991);
245+
}
246+
247+
.markdown-alert-tip .markdown-alert-title {
248+
color: var(--vscode-testing-iconPassed, #73c991);
249+
}
250+
251+
.markdown-alert-important {
252+
border-left-color: var(--vscode-editorInfo-foreground, #a371f7);
253+
}
254+
255+
.markdown-alert-important .markdown-alert-title {
256+
color: var(--vscode-editorInfo-foreground, #a371f7);
257+
}
258+
259+
.markdown-alert-warning {
260+
border-left-color: var(--vscode-editorWarning-foreground, #cca700);
261+
}
262+
263+
.markdown-alert-warning .markdown-alert-title {
264+
color: var(--vscode-editorWarning-foreground, #cca700);
265+
}
266+
267+
.markdown-alert-caution {
268+
border-left-color: var(--vscode-editorError-foreground, #f85149);
269+
}
270+
271+
.markdown-alert-caution .markdown-alert-title {
272+
color: var(--vscode-editorError-foreground, #f85149);
273+
}
204274
`
205275

206276
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
207277
const components = useMemo(
208278
() => ({
279+
blockquote: ({ children, className, ...props }: any) => {
280+
const alertType = props["data-alert-type"] as string | undefined
281+
if (!alertType) {
282+
return <blockquote {...props}>{children}</blockquote>
283+
}
284+
285+
const label = ALERT_LABELS[alertType.toUpperCase() as AlertType] || alertType
286+
return (
287+
<div className={className} {...props}>
288+
<p className="markdown-alert-title">{label}</p>
289+
{children}
290+
</div>
291+
)
292+
},
209293
table: ({ children, ...props }: any) => {
210294
return (
211295
<div className="table-wrapper">
@@ -309,6 +393,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
309393
remarkPlugins={[
310394
remarkGfm,
311395
remarkMath,
396+
remarkGithubAlerts,
312397
() => {
313398
return (tree: any) => {
314399
visit(tree, "code", (node: any) => {

webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,93 @@ describe("MarkdownBlock", () => {
115115
expect(screen.getByText("Third level ordered")).toBeInTheDocument()
116116
expect(screen.getByText("Back to first level")).toBeInTheDocument()
117117
})
118+
119+
describe("GitHub-style Markdown alerts", () => {
120+
it("should render a NOTE alert with title and content", async () => {
121+
const markdown = "> [!NOTE]\n> This is useful information."
122+
const { container } = render(<MarkdownBlock markdown={markdown} />)
123+
124+
await screen.findByText("Note")
125+
const alertEl = container.querySelector(".markdown-alert-note")
126+
expect(alertEl).toBeInTheDocument()
127+
expect(screen.getByText("Note")).toBeInTheDocument()
128+
expect(screen.getByText(/This is useful information/)).toBeInTheDocument()
129+
})
130+
131+
it("should render all five alert types", async () => {
132+
const types = [
133+
{ marker: "NOTE", label: "Note", cssClass: "markdown-alert-note" },
134+
{ marker: "TIP", label: "Tip", cssClass: "markdown-alert-tip" },
135+
{ marker: "IMPORTANT", label: "Important", cssClass: "markdown-alert-important" },
136+
{ marker: "WARNING", label: "Warning", cssClass: "markdown-alert-warning" },
137+
{ marker: "CAUTION", label: "Caution", cssClass: "markdown-alert-caution" },
138+
]
139+
140+
for (const { marker, label, cssClass } of types) {
141+
const markdown = `> [!${marker}]\n> Alert content for ${marker}.`
142+
const { container } = render(<MarkdownBlock markdown={markdown} />)
143+
144+
await screen.findByText(label)
145+
const alertEl = container.querySelector(`.${cssClass}`)
146+
expect(alertEl).toBeInTheDocument()
147+
}
148+
})
149+
150+
it("should render normal blockquotes unchanged", async () => {
151+
const markdown = "> This is a normal blockquote."
152+
const { container } = render(<MarkdownBlock markdown={markdown} />)
153+
154+
await screen.findByText(/This is a normal blockquote/)
155+
const blockquote = container.querySelector("blockquote")
156+
expect(blockquote).toBeInTheDocument()
157+
// Should NOT have alert classes
158+
const alertEl = container.querySelector(".markdown-alert")
159+
expect(alertEl).not.toBeInTheDocument()
160+
})
161+
162+
it("should render multiline alert content", async () => {
163+
const markdown = "> [!WARNING]\n> Line one.\n> Line two.\n> Line three."
164+
const { container } = render(<MarkdownBlock markdown={markdown} />)
165+
166+
await screen.findByText("Warning")
167+
const alertEl = container.querySelector(".markdown-alert-warning")
168+
expect(alertEl).toBeInTheDocument()
169+
expect(container.textContent).toContain("Line one.")
170+
expect(container.textContent).toContain("Line two.")
171+
expect(container.textContent).toContain("Line three.")
172+
})
173+
174+
it("should fall back to normal blockquote for unsupported markers", async () => {
175+
const markdown = "> [!DANGER]\n> This is unsupported."
176+
const { container } = render(<MarkdownBlock markdown={markdown} />)
177+
178+
await screen.findByText(/DANGER/)
179+
const blockquote = container.querySelector("blockquote")
180+
expect(blockquote).toBeInTheDocument()
181+
const alertEl = container.querySelector(".markdown-alert")
182+
expect(alertEl).not.toBeInTheDocument()
183+
})
184+
185+
it("should handle alert with inline formatting", async () => {
186+
const markdown = "> [!TIP]\n> Use `code` and **bold** text."
187+
const { container } = render(<MarkdownBlock markdown={markdown} />)
188+
189+
await screen.findByText("Tip")
190+
const alertEl = container.querySelector(".markdown-alert-tip")
191+
expect(alertEl).toBeInTheDocument()
192+
const codeEl = alertEl?.querySelector("code")
193+
expect(codeEl).toBeInTheDocument()
194+
expect(codeEl?.textContent).toBe("code")
195+
})
196+
197+
it("should handle alert marker with content on the same line", async () => {
198+
const markdown = "> [!CAUTION] Be careful!"
199+
const { container } = render(<MarkdownBlock markdown={markdown} />)
200+
201+
await screen.findByText("Caution")
202+
const alertEl = container.querySelector(".markdown-alert-caution")
203+
expect(alertEl).toBeInTheDocument()
204+
expect(container.textContent).toContain("Be careful!")
205+
})
206+
})
118207
})

0 commit comments

Comments
 (0)