From ebdce2f9569e6c3a513dafeb678fe8c985d1c835 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Almeida?=
<57680069+zeval@users.noreply.github.com>
Date: Sat, 2 May 2026 18:53:59 +0100
Subject: [PATCH] fix: prevent inline code from rendering diagrams (#773)
* fix: prevent inline code from rendering diagrams
* fix: address markdown renderer review feedback
* fix: remove redundant markdown block detection
---
.../shared/markdown-components.test.tsx | 55 +++++++++++++++++++
.../components/shared/markdown-components.tsx | 22 ++++++--
2 files changed, 71 insertions(+), 6 deletions(-)
create mode 100644 apps/web/components/shared/markdown-components.test.tsx
diff --git a/apps/web/components/shared/markdown-components.test.tsx b/apps/web/components/shared/markdown-components.test.tsx
new file mode 100644
index 0000000000..f94d747a06
--- /dev/null
+++ b/apps/web/components/shared/markdown-components.test.tsx
@@ -0,0 +1,55 @@
+import type { ReactNode } from "react";
+import ReactMarkdown from "react-markdown";
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it, vi } from "vitest";
+import { markdownComponents, remarkPlugins } from "./markdown-components";
+
+vi.mock("@/components/shared/mermaid-block", () => ({
+ MermaidBlock: ({ code }: { code: string }) =>
{code}
,
+}));
+
+vi.mock("@/components/task/chat/messages/code-block", () => ({
+ CodeBlock: ({ children, className }: { children: ReactNode; className?: string }) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("@/components/task/chat/messages/inline-code", () => ({
+ InlineCode: ({ children }: { children: ReactNode }) => {children},
+}));
+
+function renderMarkdown(source: string): string {
+ return renderToStaticMarkup(
+
+ {source}
+ ,
+ );
+}
+
+describe("markdownComponents", () => {
+ it("keeps mermaid keywords in inline code as inline code", () => {
+ const html = renderMarkdown("Metadata comes from `kanban`, `kanbanMulti`, repositories.");
+
+ expect(html).toContain('data-kind="inline"');
+ expect(html).toContain("kanban");
+ expect(html).not.toContain('data-kind="mermaid"');
+ });
+
+ it("renders fenced mermaid code as a mermaid block", () => {
+ const html = renderMarkdown("```mermaid\ngraph LR\nA-->B\n```");
+
+ expect(html).toContain('data-kind="mermaid"');
+ expect(html).toContain("graph LR");
+ });
+
+ it("renders non-mermaid fenced code as a code block", () => {
+ const html = renderMarkdown("```ts\nconst source = 'kanban';\n```");
+
+ expect(html).toContain('data-kind="block"');
+ expect(html).toContain("language-ts");
+ expect(html).toContain("const source");
+ expect(html).not.toContain('data-kind="mermaid"');
+ });
+});
diff --git a/apps/web/components/shared/markdown-components.tsx b/apps/web/components/shared/markdown-components.tsx
index e6dd392511..d31a3e1304 100644
--- a/apps/web/components/shared/markdown-components.tsx
+++ b/apps/web/components/shared/markdown-components.tsx
@@ -39,6 +39,15 @@ export function getTextContent(children: ReactNode): string {
return "";
}
+type MarkdownCodeProps = {
+ className?: string;
+ children?: ReactNode;
+};
+
+function isBlockCode(rawContent: string, hasLanguage: boolean): boolean {
+ return hasLanguage || rawContent.includes("\n");
+}
+
/**
* Shared markdown component overrides for ReactMarkdown.
* Element styles (headings, lists, inline code, etc.) are handled by
@@ -46,15 +55,16 @@ export function getTextContent(children: ReactNode): string {
* (code routing, link target, table overflow wrapper) remain here.
*/
export const markdownComponents = {
- code: ({ className, children }: { className?: string; children?: ReactNode }) => {
- const content = getTextContent(children).replace(/\n$/, "");
+ code: ({ className, children }: MarkdownCodeProps) => {
+ const rawContent = getTextContent(children);
+ const content = rawContent.replace(/\n$/, "");
const lang = className?.replace("language-", "") ?? null;
- if (isMermaidContent(lang, content)) {
+ const hasLanguage = className?.startsWith("language-") ?? false;
+ const isBlock = isBlockCode(rawContent, hasLanguage);
+ if (isBlock && isMermaidContent(lang, content)) {
return ;
}
- const hasLanguage = className?.startsWith("language-");
- const hasNewlines = content.includes("\n");
- if (hasLanguage || hasNewlines) {
+ if (isBlock) {
return {content};
}
return {content};