Skip to content

Commit 1b15d32

Browse files
committed
feat(message): add markdown rendering with sanitization for message content
1 parent c61ef38 commit 1b15d32

File tree

4 files changed

+289
-9
lines changed

4 files changed

+289
-9
lines changed

frontend/package-lock.json

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

frontend/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
"format": "prettier --write .",
1414
"lint": "prettier --check . && eslint ."
1515
},
16+
"dependencies": {
17+
"marked": "^12.0.2",
18+
"sanitize-html": "^2.13.0"
19+
},
1620
"devDependencies": {
21+
"@types/sanitize-html": "^2.13.0",
1722
"@eslint/compat": "^1.4.0",
1823
"@eslint/js": "^9.36.0",
1924
"@sveltejs/adapter-auto": "^6.1.0",
@@ -32,4 +37,4 @@
3237
"typescript-eslint": "^8.44.1",
3338
"vite": "^7.1.7"
3439
}
35-
}
40+
}

frontend/src/lib/components/MessageNode.svelte

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { onDestroy, onMount, tick } from 'svelte';
33
import type { Message } from '../types';
44
import { fetchAttachmentBlob, fetchAttachmentRange } from '../apiClient';
5+
import { renderMarkdown } from '../markdown';
56
67
export let message: Message;
78
export let level = 0;
@@ -50,6 +51,7 @@
5051
let videoPreviewSource: string | null = null;
5152
let childNodes: Message[] = [];
5253
let computedIdCopied = false;
54+
let renderedMarkdown = '';
5355
5456
const closeModal = () => {
5557
modalAttachment = null;
@@ -199,6 +201,7 @@
199201
$: attachmentDisplayUrl = encodedDataUrl ?? attachmentUrl ?? null;
200202
$: videoPreviewSource = videoPreviewUrl ?? attachmentDisplayUrl;
201203
$: childNodes = (message.children ?? []) as Message[];
204+
$: renderedMarkdown = !isAttachment ? renderMarkdown(message.content ?? '') : '';
202205
203206
const loadAttachmentBlob = async () => {
204207
if (!message.key) return;
@@ -426,7 +429,13 @@
426429
{#if message.title}
427430
<div class="message-title">{message.title}</div>
428431
{/if}
429-
<p>{message.content}</p>
432+
{#if renderedMarkdown}
433+
<div class="message-body" aria-label="Message body">
434+
{@html renderedMarkdown}
435+
</div>
436+
{:else}
437+
<p class="message-plain">{message.content}</p>
438+
{/if}
430439
{/if}
431440
{#if createdAtLabel}
432441
<div class="timestamp">Created {createdAtLabel}</div>
@@ -559,6 +568,78 @@
559568
word-break: break-word;
560569
}
561570
571+
.message-plain {
572+
margin: 0;
573+
line-height: 1.5;
574+
white-space: pre-wrap;
575+
word-break: break-word;
576+
}
577+
578+
.message-body {
579+
display: flex;
580+
flex-direction: column;
581+
gap: 0.5rem;
582+
line-height: 1.5;
583+
}
584+
585+
.message-body :global(*) {
586+
max-width: 100%;
587+
}
588+
589+
.message-body :global(p),
590+
.message-body :global(li),
591+
.message-body :global(td),
592+
.message-body :global(th) {
593+
margin: 0;
594+
white-space: pre-wrap;
595+
word-break: break-word;
596+
}
597+
598+
.message-body :global(p + p),
599+
.message-body :global(p + ul),
600+
.message-body :global(p + ol),
601+
.message-body :global(ul + p),
602+
.message-body :global(ol + p),
603+
.message-body :global(blockquote + p) {
604+
margin-top: 0.35rem;
605+
}
606+
607+
.message-body :global(ul),
608+
.message-body :global(ol) {
609+
padding-left: 1.25rem;
610+
}
611+
612+
.message-body :global(blockquote) {
613+
margin: 0;
614+
padding-left: 0.85rem;
615+
border-left: 3px solid rgba(148, 163, 184, 0.4);
616+
color: #cbd5f5;
617+
}
618+
619+
.message-body :global(code) {
620+
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
621+
font-size: 0.9em;
622+
background: rgba(15, 23, 42, 0.65);
623+
padding: 0.1rem 0.25rem;
624+
border-radius: 0.25rem;
625+
}
626+
627+
.message-body :global(pre) {
628+
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
629+
font-size: 0.85rem;
630+
background: rgba(15, 23, 42, 0.75);
631+
padding: 0.65rem;
632+
border-radius: 0.4rem;
633+
overflow-x: auto;
634+
white-space: pre;
635+
}
636+
637+
.message-body :global(a) {
638+
color: #93c5fd;
639+
text-decoration: underline;
640+
word-break: break-all;
641+
}
642+
562643
.message-title {
563644
margin: 0 0 0.25rem 0;
564645
font-weight: 700;

0 commit comments

Comments
 (0)