Skip to content

Commit 890e68c

Browse files
committed
Support Markdown comments
1 parent 452cd08 commit 890e68c

5 files changed

Lines changed: 156 additions & 5 deletions

File tree

frontend/bun.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"dependencies": {
1313
"@solidjs/router": "^0.16.1",
1414
"@tanstack/solid-query": "^5.99.0",
15+
"dompurify": "^3.4.7",
16+
"marked": "^18.0.4",
1517
"solid-js": "^1.9.12",
1618
"uplot": "^1.6.32"
1719
},

frontend/src/pages/IssueDetail.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { A, useParams } from "@solidjs/router";
22
import { createQuery, createMutation, useQueryClient } from "@tanstack/solid-query";
3+
import DOMPurify from "dompurify";
4+
import { marked } from "marked";
35
import { createSignal, createMemo, Show, For } from "solid-js";
46
import { api } from "~/api/client";
57
import { queryKeys } from "~/queries/keys";
@@ -50,6 +52,43 @@ const ACTIVITY_LABELS: Record<string, string> = {
5052
regression: "Regression detected",
5153
};
5254

55+
const COMMENT_MARKDOWN_TAGS = [
56+
"a",
57+
"blockquote",
58+
"br",
59+
"code",
60+
"del",
61+
"em",
62+
"h1",
63+
"h2",
64+
"h3",
65+
"h4",
66+
"h5",
67+
"h6",
68+
"hr",
69+
"li",
70+
"ol",
71+
"p",
72+
"pre",
73+
"strong",
74+
"table",
75+
"tbody",
76+
"td",
77+
"th",
78+
"thead",
79+
"tr",
80+
"ul",
81+
];
82+
83+
function renderCommentMarkdown(text: string): string {
84+
const html = marked.parse(text, { async: false, breaks: true, gfm: true });
85+
86+
return DOMPurify.sanitize(html, {
87+
ALLOWED_ATTR: ["href", "title"],
88+
ALLOWED_TAGS: COMMENT_MARKDOWN_TAGS,
89+
});
90+
}
91+
5392
function activityKindLabel(kind: string): string {
5493
return ACTIVITY_LABELS[kind] ?? kind;
5594
}
@@ -766,7 +805,10 @@ export default function IssueDetail() {
766805
<For each={commentsQuery.data}>
767806
{(comment) => (
768807
<div class="comment">
769-
<div class="comment__text">{comment.text}</div>
808+
<div
809+
class="comment__text markdown-body"
810+
innerHTML={renderCommentMarkdown(comment.text)}
811+
/>
770812
<div class="comment__meta">
771813
<span class="text-secondary text-sm">{relativeTime(comment.created_at)}</span>
772814
<button
@@ -796,7 +838,7 @@ export default function IssueDetail() {
796838
rows={2}
797839
/>
798840
<div class="comment-compose__actions">
799-
<span class="text-xs text-secondary">Ctrl+Enter to post</span>
841+
<span class="text-xs text-secondary">Markdown supported. Ctrl+Enter to post</span>
800842
<Button
801843
variant="secondary"
802844
size="sm"

frontend/src/pages/ProjectIssues.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import EmptyState from "~/components/ui/EmptyState";
1414
import ErrorState from "~/components/ui/ErrorState";
1515
import IconArrowLeft from "~icons/lucide/arrow-left";
1616
import IconArrowRight from "~icons/lucide/arrow-right";
17+
import IconMessageSquare from "~icons/lucide/message-square";
1718

1819
const STATUSES = ["unresolved", "resolved", "ignored"] as const;
1920
const SORT_OPTIONS = [
@@ -355,8 +356,10 @@ export default function ProjectIssues() {
355356
<span
356357
class="comment-count-badge"
357358
title={commentLabel(issue.comment_count)}
359+
aria-label={commentLabel(issue.comment_count)}
358360
>
359-
{commentLabel(issue.comment_count)}
361+
<IconMessageSquare aria-hidden="true" />
362+
{formatNumber(issue.comment_count)}
360363
</span>
361364
</Show>
362365
</div>

frontend/src/styles/globals.css

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ pre, code {
595595
.comment-count-badge {
596596
display: inline-flex;
597597
align-items: center;
598+
gap: 4px;
598599
border-radius: var(--radius-pill);
599600
border: 1px solid var(--color-border);
600601
background: var(--color-surface-1);
@@ -606,6 +607,11 @@ pre, code {
606607
white-space: nowrap;
607608
}
608609

610+
.comment-count-badge svg {
611+
width: 12px;
612+
height: 12px;
613+
}
614+
609615
.data-table td .culprit {
610616
margin-top: 2px;
611617
font-size: 11px;
@@ -2077,8 +2083,98 @@ label.field-label--sm {
20772083
.comment__text {
20782084
font-size: 13px;
20792085
line-height: 1.5;
2080-
white-space: pre-wrap;
2081-
word-break: break-word;
2086+
overflow-wrap: anywhere;
2087+
}
2088+
2089+
.comment__text.markdown-body > :first-child {
2090+
margin-top: 0;
2091+
}
2092+
2093+
.comment__text.markdown-body > :last-child {
2094+
margin-bottom: 0;
2095+
}
2096+
2097+
.comment__text.markdown-body p,
2098+
.comment__text.markdown-body ul,
2099+
.comment__text.markdown-body ol,
2100+
.comment__text.markdown-body blockquote,
2101+
.comment__text.markdown-body pre,
2102+
.comment__text.markdown-body table,
2103+
.comment__text.markdown-body hr {
2104+
margin: 0 0 8px;
2105+
}
2106+
2107+
.comment__text.markdown-body h1,
2108+
.comment__text.markdown-body h2,
2109+
.comment__text.markdown-body h3,
2110+
.comment__text.markdown-body h4,
2111+
.comment__text.markdown-body h5,
2112+
.comment__text.markdown-body h6 {
2113+
margin: 10px 0 6px;
2114+
font-size: 13px;
2115+
line-height: 1.35;
2116+
font-weight: 600;
2117+
}
2118+
2119+
.comment__text.markdown-body a {
2120+
color: var(--color-text-primary);
2121+
text-decoration: underline;
2122+
text-underline-offset: 2px;
2123+
}
2124+
2125+
.comment__text.markdown-body ul,
2126+
.comment__text.markdown-body ol {
2127+
padding-left: 18px;
2128+
}
2129+
2130+
.comment__text.markdown-body blockquote {
2131+
padding-left: 10px;
2132+
border-left: 2px solid var(--color-border);
2133+
color: var(--color-text-secondary);
2134+
}
2135+
2136+
.comment__text.markdown-body code {
2137+
padding: 1px 4px;
2138+
border-radius: 4px;
2139+
background: var(--color-surface-2);
2140+
font-size: 12px;
2141+
}
2142+
2143+
.comment__text.markdown-body pre {
2144+
overflow: auto;
2145+
padding: 8px 10px;
2146+
border: 1px solid var(--color-border);
2147+
border-radius: var(--radius);
2148+
background: var(--color-surface-0);
2149+
}
2150+
2151+
.comment__text.markdown-body pre code {
2152+
padding: 0;
2153+
border-radius: 0;
2154+
background: transparent;
2155+
}
2156+
2157+
.comment__text.markdown-body table {
2158+
display: block;
2159+
max-width: 100%;
2160+
overflow: auto;
2161+
border-collapse: collapse;
2162+
}
2163+
2164+
.comment__text.markdown-body th,
2165+
.comment__text.markdown-body td {
2166+
padding: 4px 8px;
2167+
border: 1px solid var(--color-border);
2168+
}
2169+
2170+
.comment__text.markdown-body th {
2171+
font-weight: 600;
2172+
background: var(--color-surface-2);
2173+
}
2174+
2175+
.comment__text.markdown-body hr {
2176+
border: 0;
2177+
border-top: 1px solid var(--color-border);
20822178
}
20832179

20842180
.comment__meta {

0 commit comments

Comments
 (0)