summaryrefslogtreecommitdiffhomepage
path: root/src/features/markdown/ui/Markdown.svelte
blob: 72b892bed0c4f14e965b1009932b28001a21149c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<script lang="ts">
  import { renderMarkdown } from "../logic/markdown";

  let {
    text,
    streaming = false,
  }: {
    text: string;
    /** Balance dangling delimiters while the message is still generating. */
    streaming?: boolean;
  } = $props();

  // Pure transform; the HTML is already DOMPurify-sanitized in renderMarkdown.
  const html = $derived(renderMarkdown(text, { streaming }));

  let container: HTMLElement;

  // One delegated listener on the stable container handles every code block's
  // copy button — including blocks re-created when `html` changes (streaming),
  // since the listener lives on the container, not the buttons. Clipboard is the
  // edge effect; absent (insecure context) → no-op.
  $effect(() => {
    const el = container;
    if (el === undefined) return;

    const onClick = (event: Event): void => {
      const target = event.target;
      if (!(target instanceof Element)) return;
      const button = target.closest<HTMLButtonElement>("[data-copy]");
      if (button === null) return;

      const code = button.closest(".code-block")?.querySelector("code")?.textContent ?? "";
      const clipboard = navigator.clipboard;
      if (clipboard === undefined) return;

      void clipboard
        .writeText(code)
        .then(() => {
          const prev = button.textContent;
          button.textContent = "Copied";
          setTimeout(() => {
            button.textContent = prev;
          }, 1200);
        })
        .catch(() => {
          // Clipboard denied — leave the button as-is.
        });
    };

    el.addEventListener("click", onClick);
    return () => el.removeEventListener("click", onClick);
  });
</script>

<div class="markdown-body" bind:this={container}>
  <!-- {@html} is safe here: `html` is DOMPurify-sanitized inside renderMarkdown. -->
  {@html html}
</div>