panache 2.35.0

An LSP, formatter, and linter for Pandoc markdown, Quarto, and RMarkdown
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Panache Playground</title>
        <link rel="stylesheet"
              href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
              integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
              crossorigin="anonymous">
        <style>
      :root {
        --playground-surface: #ffffff;
        --playground-border: #d9e2ee;
        --playground-muted: #5f728a;
        --playground-accent: #0b6bcb;
      }

      body {
        margin: 0;
        font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
        color: #1e2a3a;
        background:
          radial-gradient(circle at 0% 0%, #eef6ff 0%, transparent 40%),
          radial-gradient(circle at 100% 100%, #f9f4ec 0%, transparent 45%),
          #f7f9fc;
      }

      .playground-container {
        max-width: 1440px;
      }

      .playground-title {
        font-weight: 650;
        letter-spacing: 0.01em;
      }

      .playground-panel {
        background: color-mix(in srgb, var(--playground-surface) 90%, #f4f8fd 10%);
        border: 1px solid var(--playground-border);
        border-radius: 0.9rem;
      }

      .pane-title {
        display: block;
        margin: 0 0 0.35rem;
        font-size: 0.8rem;
        text-transform: uppercase;
        letter-spacing: 0.06em;
        color: var(--playground-muted);
        font-weight: 700;
      }

      .editor {
        width: 100%;
        min-height: clamp(18rem, 58vh, 45rem);
        border: 1px solid var(--playground-border);
        border-radius: 0.7rem;
        background: #fff;
        padding: 0.75rem;
        resize: vertical;
        box-sizing: border-box;
        font-family: "IBM Plex Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
        font-size: 0.92rem;
        line-height: 1.45;
      }

      .editor:focus {
        outline: 0;
        border-color: var(--playground-accent);
        box-shadow: 0 0 0 0.2rem rgb(11 107 203 / 0.18);
      }

      .editor[readonly] {
        background: #f5f8fc;
      }

      .status-message {
        margin-top: 0.75rem;
        font-size: 0.88rem;
        color: var(--playground-muted);
      }

      .status-message.error {
        color: #a31616;
      }

      @media (max-width: 768px) {
        .editor {
          min-height: 16rem;
        }
      }
        </style>
    </head>
    <body>
        <main class="container-fluid py-3 py-md-4">
            <div class="playground-container mx-auto px-2 px-md-3">
                <header class="mb-3 mb-md-4">
                    <h1 class="playground-title h3 mb-1">Panache Playground</h1>
                    <p class="text-secondary mb-0">Format Quarto and Markdown in real time using panache-wasm.</p>
                </header>

                <section class="playground-panel shadow-sm">
                    <div class="p-3 p-md-4">
                        <div class="row g-3 align-items-end mb-1">
                            <div class="col-12 col-sm-6 col-lg-2">
                                <label class="form-label fw-semibold mb-1" for="flavor">Flavor</label>
                                <select id="flavor" class="form-select">
                                    <option value="pandoc">Pandoc</option>
                                    <option value="quarto" selected>Quarto</option>
                                    <option value="rmarkdown">R Markdown</option>
                                    <option value="gfm">GFM</option>
                                    <option value="common-mark">CommonMark</option>
                                </select>
                            </div>
                            <div class="col-12 col-sm-6 col-lg-2">
                                <label class="form-label fw-semibold mb-1" for="lw">Line width</label>
                                <input id="lw" class="form-control" type="number" min="10" max="200" value="80">
                            </div>
                            <div class="col-12 col-sm-6 col-lg-2">
                                <label class="form-label fw-semibold mb-1" for="wrap">Wrap</label>
                                <select id="wrap" class="form-select">
                                    <option value="reflow" selected>Reflow</option>
                                    <option value="preserve">Preserve</option>
                                    <option value="sentence">Sentence</option>
                                </select>
                            </div>
                            <div class="col-12 col-sm-6 col-lg-2">
                                <label class="form-label fw-semibold mb-1" for="blank-lines">Blank lines</label>
                                <select id="blank-lines" class="form-select">
                                    <option value="collapse" selected>Collapse</option>
                                    <option value="preserve">Preserve</option>
                                </select>
                            </div>
                            <div class="col-12 col-sm-6 col-lg-2">
                                <label class="form-label fw-semibold mb-1" for="line-ending">Line ending</label>
                                <select id="line-ending" class="form-select">
                                    <option value="auto" selected>Auto</option>
                                    <option value="lf">LF</option>
                                    <option value="crlf">CRLF</option>
                                </select>
                            </div>
                            <div class="col-12 col-lg-2">
                                <div class="d-flex flex-wrap gap-2 justify-content-lg-end">
                                    <button id="copy-output" class="btn btn-outline-primary" type="button">Copy output</button>
                                    <button id="reset-input" class="btn btn-outline-secondary" type="button">Reset sample</button>
                                </div>
                            </div>
                        </div>

                        <div class="row g-3 align-items-end mb-1">
                            <div class="col-12 col-sm-6 col-lg-3">
                                <label class="form-label fw-semibold mb-1" for="math-delimiter-style">Math delimiters</label>
                                <select id="math-delimiter-style" class="form-select">
                                    <option value="preserve" selected>Preserve</option>
                                    <option value="dollars">Dollars ($)</option>
                                    <option value="backslash">Backslash (\\)</option>
                                </select>
                            </div>
                            <div class="col-12 col-sm-6 col-lg-2">
                                <label class="form-label fw-semibold mb-1" for="math-indent">Math indent</label>
                                <input id="math-indent" class="form-control" type="number" min="0" max="20" value="0">
                            </div>
                            <div class="col-12 col-sm-6 col-lg-2">
                                <label class="form-label fw-semibold mb-1" for="tab-stops">Tab stops</label>
                                <select id="tab-stops" class="form-select">
                                    <option value="normalize" selected>Normalize</option>
                                    <option value="preserve">Preserve</option>
                                </select>
                            </div>
                            <div class="col-12 col-sm-6 col-lg-2">
                                <label class="form-label fw-semibold mb-1" for="tab-width">Tab width</label>
                                <input id="tab-width" class="form-control" type="number" min="1" max="16" value="4">
                            </div>
                        </div>

                        <div class="row g-3 mt-1">
                            <div class="col-12 col-xl-6">
                                <label class="pane-title" for="input">Input</label>
                                <textarea id="input"
                                          class="editor"
                                          placeholder="Paste Quarto or Markdown here..."></textarea>
                            </div>
                            <div class="col-12 col-xl-6">
                                <label class="pane-title" for="output">Formatted output</label>
                                <textarea id="output" class="editor" readonly></textarea>
                            </div>
                        </div>

                        <p id="status" class="status-message mb-0" role="status" aria-live="polite"></p>
                    </div>
                </section>
            </div>
        </main>
        <script type="module">
      import init, { format_qmd_with_options } from "./pkg/panache_wasm.js";

      const SAMPLE = "---\ntitle: Demo\n---\n\nThis is a very long line that should be wrapped.\n\n```{r}\nprint(1)\n```\n\n$$\nA &= B \\\\\nC &= D\n$$\n";

      const $ = (id) => document.getElementById(id);
      const statusEl = $("status");

      function setStatus(text, isError = false) {
        statusEl.textContent = text;
        statusEl.classList.toggle("error", isError);
      }

      function readInt(id, fallback) {
        const value = parseInt($(id).value || String(fallback), 10);
        return Number.isNaN(value) ? fallback : value;
      }

      function currentOptions() {
        return {
          flavor: $("flavor").value,
          lw: readInt("lw", 80),
          wrap: $("wrap").value,
          blankLines: $("blank-lines").value,
          lineEnding: $("line-ending").value,
          mathDelimiterStyle: $("math-delimiter-style").value,
          mathIndent: readInt("math-indent", 0),
          tabStops: $("tab-stops").value,
          tabWidth: readInt("tab-width", 4),
        };
      }

      function validateOptions(options) {
        if (options.lw < 10 || options.lw > 200) {
          return "Line width must be between 10 and 200.";
        }
        if (options.tabWidth < 1 || options.tabWidth > 16) {
          return "Tab width must be between 1 and 16.";
        }
        if (options.mathIndent < 0 || options.mathIndent > 20) {
          return "Math indent must be between 0 and 20.";
        }
        return null;
      }

      function updateQueryString(options) {
        const params = new URLSearchParams({
          flavor: options.flavor,
          lw: String(options.lw),
          wrap: options.wrap,
          blankLines: options.blankLines,
          lineEnding: options.lineEnding,
          mathDelimiterStyle: options.mathDelimiterStyle,
          mathIndent: String(options.mathIndent),
          tabStops: options.tabStops,
          tabWidth: String(options.tabWidth),
        });
        history.replaceState(null, "", "?" + params.toString());
      }

      function applyQueryString() {
        const query = new URLSearchParams(location.search);
        const controls = {
          flavor: "flavor",
          lw: "lw",
          wrap: "wrap",
          blankLines: "blank-lines",
          lineEnding: "line-ending",
          mathDelimiterStyle: "math-delimiter-style",
          mathIndent: "math-indent",
          tabStops: "tab-stops",
          tabWidth: "tab-width",
        };

        for (const [key, id] of Object.entries(controls)) {
          const value = query.get(key);
          if (value !== null) {
            $(id).value = value;
          }
        }

      }

      function applyFormat() {
        const input = $("input").value;
        const options = currentOptions();
        const validationError = validateOptions(options);

        if (validationError) {
          $("output").value = "";
          setStatus(validationError, true);
          return;
        }

        try {
          $("output").value = format_qmd_with_options(
            input,
            options.lw,
            options.flavor,
            options.wrap,
            options.blankLines,
            options.lineEnding,
            options.mathDelimiterStyle,
            options.tabStops,
            options.tabWidth,
            options.mathIndent,
          );
          updateQueryString(options);
          setStatus("Formatted successfully.");
        } catch (error) {
          $("output").value = "";
          setStatus("Formatting failed: " + (error?.message || String(error)), true);
        }
      }

      function resetSample() {
        $("input").value = SAMPLE;
        applyFormat();
      }

      async function main() {
        await init();

        applyQueryString();

        $("input").addEventListener("input", applyFormat);
        [
          "flavor",
          "lw",
          "wrap",
          "blank-lines",
          "line-ending",
          "math-delimiter-style",
          "math-indent",
          "tab-stops",
          "tab-width",
        ].forEach((id) => {
          $(id).addEventListener("input", applyFormat);
        });
        $("reset-input").addEventListener("click", resetSample);
        $("copy-output").addEventListener("click", async () => {
          try {
            await navigator.clipboard.writeText($("output").value);
            setStatus("Output copied to clipboard.");
          } catch {
            setStatus("Could not copy output in this browser context.", true);
          }
        });

        resetSample();
      }

      main().catch((error) => {
        setStatus("Failed to initialize playground: " + (error?.message || String(error)), true);
      });
        </script>
    </body>
</html>