|
2 | 2 | import { onDestroy, onMount, tick } from 'svelte'; |
3 | 3 | import type { Message } from '../types'; |
4 | 4 | import { fetchAttachmentBlob, fetchAttachmentRange } from '../apiClient'; |
| 5 | + import { renderMarkdown } from '../markdown'; |
5 | 6 |
|
6 | 7 | export let message: Message; |
7 | 8 | export let level = 0; |
|
50 | 51 | let videoPreviewSource: string | null = null; |
51 | 52 | let childNodes: Message[] = []; |
52 | 53 | let computedIdCopied = false; |
| 54 | + let renderedMarkdown = ''; |
53 | 55 |
|
54 | 56 | const closeModal = () => { |
55 | 57 | modalAttachment = null; |
|
199 | 201 | $: attachmentDisplayUrl = encodedDataUrl ?? attachmentUrl ?? null; |
200 | 202 | $: videoPreviewSource = videoPreviewUrl ?? attachmentDisplayUrl; |
201 | 203 | $: childNodes = (message.children ?? []) as Message[]; |
| 204 | + $: renderedMarkdown = !isAttachment ? renderMarkdown(message.content ?? '') : ''; |
202 | 205 |
|
203 | 206 | const loadAttachmentBlob = async () => { |
204 | 207 | if (!message.key) return; |
|
426 | 429 | {#if message.title} |
427 | 430 | <div class="message-title">{message.title}</div> |
428 | 431 | {/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} |
430 | 439 | {/if} |
431 | 440 | {#if createdAtLabel} |
432 | 441 | <div class="timestamp">Created {createdAtLabel}</div> |
|
559 | 568 | word-break: break-word; |
560 | 569 | } |
561 | 570 |
|
| 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 | +
|
562 | 643 | .message-title { |
563 | 644 | margin: 0 0 0.25rem 0; |
564 | 645 | font-weight: 700; |
|
0 commit comments