wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<script lang="ts">
  import { onMount } from "svelte";
  import { Marked } from "marked";
  import DOMPurify from "dompurify";

  interface Props {
    content: string;
  }

  let { content }: Props = $props();

  let html = $state("");
  let mermaidLoaded = $state(false);
  let katexLoaded = $state(false);
  let renderContainer: HTMLDivElement | undefined = $state();

  const MERMAID_PLACEHOLDER = "data-mermaid-src";

  const parser = new Marked({
    gfm: true,
    breaks: false,
    renderer: {
      code({ text, lang }) {
        if (lang === "mermaid") {
          const escaped = text
            .replace(/&/g, "&amp;")
            .replace(/"/g, "&quot;");
          return `<div class="mermaid-block" ${MERMAID_PLACEHOLDER}="${escaped}"></div>`;
        }
        return false;
      },
    },
  });

  function preprocessMath(src: string): string {
    src = src.replace(/\$\$([\s\S]+?)\$\$/g, (_, math) => {
      const encoded = encodeURIComponent(math);
      return `<div class="math-display" data-math="${encoded}"></div>`;
    });
    src = src.replace(/(?<!\$)\$([^$\n]+?)\$(?!\$)/g, (_, math) => {
      const encoded = encodeURIComponent(math);
      return `<span class="math-inline" data-math="${encoded}"></span>`;
    });
    return src;
  }

  async function renderMath(container: HTMLDivElement) {
    if (!katexLoaded) return;
    const katex = (await import("katex")).default;
    container.querySelectorAll<HTMLElement>(".math-display, .math-inline").forEach((el) => {
      const math = decodeURIComponent(el.dataset.math ?? "");
      const displayMode = el.classList.contains("math-display");
      try {
        katex.render(math, el, { displayMode, throwOnError: false });
      } catch {
        el.textContent = math;
      }
    });
  }

  async function renderMermaid(container: HTMLDivElement) {
    if (!mermaidLoaded) return;
    const mermaid = (await import("mermaid")).default;
    const blocks = container.querySelectorAll<HTMLElement>(`[${MERMAID_PLACEHOLDER}]`);
    let idx = 0;
    for (const block of blocks) {
      const src = block.getAttribute(MERMAID_PLACEHOLDER) ?? "";
      const decoded = src
        .replace(/&amp;/g, "&")
        .replace(/&quot;/g, '"');
      const id = `mermaid-${Date.now()}-${idx++}`;
      try {
        const { svg } = await mermaid.render(id, decoded);
        block.innerHTML = svg;
      } catch (error) {
        block.innerHTML = `<pre class="mermaid-error">${String(error)}</pre>`;
      }
    }
  }

  async function buildHtml(src: string): Promise<string> {
    const preprocessed = preprocessMath(src);
    const raw = parser.parse(preprocessed) as string;
    return DOMPurify.sanitize(raw, {
      ADD_ATTR: ["data-math", MERMAID_PLACEHOLDER],
      FORCE_BODY: false,
    });
  }

  async function rerender() {
    html = await buildHtml(content);
    await Promise.resolve();
    if (renderContainer) {
      await renderMermaid(renderContainer);
      await renderMath(renderContainer);
    }
  }

  onMount(async () => {
    const [mermaidMod] = await Promise.all([
      import("mermaid"),
      import("katex"),
    ]);
    mermaidMod.default.initialize({ startOnLoad: false, theme: "neutral" });
    mermaidLoaded = true;
    katexLoaded = true;
    await rerender();
  });

  $effect(() => {
    void rerender();
    content;
  });
</script>

<svelte:head>
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/katex@0.16.44/dist/katex.min.css"
    crossorigin="anonymous"
  />
</svelte:head>

<div class="md-scroll">
  <div class="md-body" bind:this={renderContainer}>
    <!-- eslint-disable-next-line svelte/no-at-html-tags -->
    {@html html}
  </div>
</div>

<style>
  .md-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }

  .md-body {
    max-width: 800px;
    margin: 0 auto;
    padding: var(--wb-spacing-32) var(--wb-spacing-40) var(--wb-spacing-64);
    font-size: var(--wb-font-size-14);
    line-height: 1.7;
    color: var(--vscode-editor-foreground);
    font-family: var(--wsb-font-sans);
  }

  .md-body :global(h1),
  .md-body :global(h2),
  .md-body :global(h3),
  .md-body :global(h4),
  .md-body :global(h5),
  .md-body :global(h6) {
    font-weight: 650;
    line-height: 1.3;
    margin: 1.5em 0 0.5em;
    color: var(--vscode-editor-foreground);
  }

  .md-body :global(h1) { font-size: 1.8em; border-bottom: 1px solid var(--vscode-panel-border); padding-bottom: 0.3em; }
  .md-body :global(h2) { font-size: 1.4em; border-bottom: 1px solid var(--vscode-widget-border); padding-bottom: 0.2em; }
  .md-body :global(h3) { font-size: 1.15em; }

  .md-body :global(p) { margin: 0.8em 0; }

  .md-body :global(a) {
    color: var(--vscode-focusBorder);
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  .md-body :global(code) {
    font-family: var(--wsb-font-mono, monospace);
    font-size: 0.875em;
    background: var(--vscode-sideBar-background);
    border: 1px solid var(--vscode-widget-border);
    border-radius: var(--wb-corner-radius-3);
    padding: 0.15em 0.35em;
  }

  .md-body :global(pre) {
    background: var(--vscode-sideBar-background);
    border: 1px solid var(--vscode-widget-border);
    border-radius: var(--wb-corner-radius-6);
    padding: var(--wb-spacing-14) var(--wb-spacing-16);
    overflow-x: auto;
    font-size: var(--wb-font-size-13);
    line-height: 1.5;
  }

  .md-body :global(pre code) {
    background: none;
    border: none;
    padding: 0;
    font-size: inherit;
  }

  .md-body :global(blockquote) {
    margin: 0.8em 0;
    padding: 0.4em 0 0.4em 1em;
    border-left: 3px solid var(--vscode-focusBorder);
    color: var(--vscode-foreground);
  }

  .md-body :global(ul),
  .md-body :global(ol) {
    padding-left: 1.5em;
    margin: 0.5em 0;
  }

  .md-body :global(li) { margin: 0.25em 0; }

  .md-body :global(table) {
    border-collapse: collapse;
    width: 100%;
    margin: 1em 0;
    font-size: var(--wb-font-size-13);
  }

  .md-body :global(th),
  .md-body :global(td) {
    border: 1px solid var(--vscode-panel-border);
    padding: var(--wb-spacing-6) var(--wb-spacing-12);
    text-align: left;
  }

  .md-body :global(th) {
    background: var(--vscode-sideBar-background);
    font-weight: 600;
  }

  .md-body :global(tr:nth-child(even) td) {
    background: color-mix(in srgb, var(--vscode-sideBar-background) 40%, transparent);
  }

  .md-body :global(hr) {
    border: none;
    border-top: 1px solid var(--vscode-panel-border);
    margin: 1.5em 0;
  }

  .md-body :global(img) {
    max-width: 100%;
    border-radius: var(--wb-corner-radius-4);
  }

  .md-body :global(.mermaid-block) {
    margin: 1em 0;
    text-align: center;
    overflow-x: auto;
  }

  .md-body :global(.mermaid-block svg) {
    max-width: 100%;
    height: auto;
  }

  .md-body :global(.mermaid-error) {
    color: var(--vscode-errorForeground);
    font-size: var(--wb-font-size-12);
    padding: var(--wb-spacing-8);
    background: color-mix(in srgb, var(--vscode-errorForeground) 10%, transparent);
    border-radius: var(--wb-corner-radius-4);
    border: 1px solid color-mix(in srgb, var(--vscode-errorForeground) 30%, transparent);
  }

  .md-body :global(.math-display) {
    display: block;
    text-align: center;
    margin: 1em 0;
    overflow-x: auto;
  }

  .md-body :global(.math-inline) {
    display: inline;
  }
</style>