<!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>