<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>undoc — WASM Playground</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f1117; --surface: #1a1d27; --surface2: #242736;
--border: #2e3347; --accent: #6c8cff; --accent-dim: #3d4f99;
--text: #e2e4ee; --text-dim: #8890a8; --err: #ff6b6b; --ok: #6bffb3;
--radius: 10px; --mono: 'Fira Code', 'Cascadia Code', Consolas, monospace;
}
body { background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-height: 100vh; display: flex; flex-direction: column;
align-items: center; padding: 2rem 1rem; gap: 1.5rem; }
header { text-align: center; }
header h1 { font-size: 1.75rem; letter-spacing: -0.5px; }
header h1 span { color: var(--accent); }
header p { color: var(--text-dim); margin-top: 0.35rem; font-size: 0.9rem; }
.drop-zone { width: 100%; max-width: 720px; border: 2px dashed var(--border);
border-radius: var(--radius); padding: 2.5rem 2rem; text-align: center;
cursor: pointer; transition: border-color 0.15s, background 0.15s;
background: var(--surface); user-select: none; }
.drop-zone:hover, .drop-zone.dragover { border-color: var(--accent); background: #1e2136; }
.drop-zone svg { width: 40px; height: 40px; margin-bottom: 0.75rem; opacity: 0.6; }
.drop-zone p { color: var(--text-dim); font-size: 0.9rem; }
.drop-zone p strong { color: var(--text); }
#file-input { display: none; }
.result-card { width: 100%; max-width: 720px; background: var(--surface);
border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.card-header { display: flex; align-items: center; justify-content: space-between;
padding: 0.75rem 1rem; background: var(--surface2);
border-bottom: 1px solid var(--border); gap: 0.5rem; flex-wrap: wrap; }
.file-info { font-size: 0.85rem; color: var(--text-dim); }
.file-info strong { color: var(--text); }
.format-badge { font-size: 0.75rem; font-weight: 600; padding: 0.15rem 0.5rem;
border-radius: 4px; background: var(--accent-dim); color: var(--accent);
text-transform: uppercase; letter-spacing: 0.05em; }
.tabs { display: flex; gap: 0.25rem; }
.tab { padding: 0.3rem 0.75rem; border-radius: 5px; border: 1px solid transparent;
background: transparent; color: var(--text-dim); font-size: 0.8rem;
cursor: pointer; transition: all 0.12s; }
.tab:hover { color: var(--text); background: var(--surface); }
.tab.active { color: var(--accent); border-color: var(--accent-dim);
background: #1a2050; }
.copy-btn { padding: 0.3rem 0.75rem; border-radius: 5px;
border: 1px solid var(--border); background: transparent;
color: var(--text-dim); font-size: 0.8rem; cursor: pointer;
transition: all 0.12s; margin-left: auto; }
.copy-btn:hover { color: var(--text); border-color: var(--accent); }
pre { padding: 1rem; font-family: var(--mono); font-size: 0.82rem;
line-height: 1.6; overflow: auto; max-height: 500px;
white-space: pre-wrap; word-break: break-word; color: var(--text); }
.loading { color: var(--text-dim); font-size: 0.9rem; }
.error-container { width: 100%; max-width: 720px; background: #2a1a1a;
border: 1px solid var(--err); border-radius: var(--radius);
padding: 1rem; color: var(--err); font-size: 0.85rem; }
.hidden { display: none !important; }
</style>
</head>
<body>
<header>
<h1><span>undoc</span> WASM Playground</h1>
<p>Extract DOCX · XLSX · PPTX to Markdown, Text, or JSON — in-browser, no upload</p>
</header>
<div class="drop-zone" id="drop-zone">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M12 16V4m0 0L8 8m4-4l4 4"/>
<path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2"/>
</svg>
<p><strong>Drop a .docx, .xlsx, or .pptx file here</strong></p>
<p>or <strong>click to browse</strong></p>
<input type="file" id="file-input" accept=".docx,.xlsx,.pptx">
</div>
<p class="loading hidden" id="loading">Parsing…</p>
<div class="error-container hidden" id="error-container"></div>
<div class="result-card hidden" id="result-card">
<div class="card-header">
<span class="file-info" id="file-info"></span>
<span class="format-badge" id="format-badge"></span>
<div class="tabs" id="tabs">
<button class="tab active" data-tab="markdown">Markdown</button>
<button class="tab" data-tab="text">Text</button>
<button class="tab" data-tab="json">JSON</button>
</div>
<button class="copy-btn" id="copy-btn">Copy</button>
</div>
<pre id="output"></pre>
</div>
<script type="module">
import init, { parse } from './pkg/undoc_wasm.js';
const wasmReady = init();
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const loadingEl = document.getElementById('loading');
const errorContainer = document.getElementById('error-container');
const resultCard = document.getElementById('result-card');
const outputEl = document.getElementById('output');
const fileInfoEl = document.getElementById('file-info');
const formatBadge = document.getElementById('format-badge');
const copyBtn = document.getElementById('copy-btn');
let tabs = {};
let activeTab = 'markdown';
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) processFile(file);
});
fileInput.addEventListener('change', e => {
const file = e.target.files[0];
if (file) processFile(file);
});
document.getElementById('tabs').addEventListener('click', e => {
const btn = e.target.closest('.tab');
if (!btn) return;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
activeTab = btn.dataset.tab;
outputEl.textContent = tabs[activeTab] ?? '';
});
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(outputEl.textContent);
copyBtn.textContent = 'Copied!';
setTimeout(() => copyBtn.textContent = 'Copy', 1500);
});
async function processFile(file) {
showLoading();
await wasmReady;
try {
const bytes = new Uint8Array(await file.arrayBuffer());
const doc = parse(bytes);
const md = doc.toMarkdown();
const txt = doc.toText();
const json = prettyJson(doc.toJson());
const fmt = doc.format();
tabs = { markdown: md, text: txt, json };
formatBadge.textContent = fmt;
const strong = document.createElement('strong');
strong.textContent = file.name;
fileInfoEl.replaceChildren(strong, ` — ${(file.size / 1024).toFixed(1)} KB`);
showResult();
} catch (err) {
showError(err);
}
}
function prettyJson(raw) {
try { return JSON.stringify(JSON.parse(raw), null, 2); } catch { return raw; }
}
function showLoading() {
loadingEl.classList.remove('hidden');
errorContainer.classList.add('hidden');
resultCard.classList.add('hidden');
}
function showResult() {
loadingEl.classList.add('hidden');
errorContainer.classList.add('hidden');
resultCard.classList.remove('hidden');
outputEl.textContent = tabs[activeTab] ?? '';
}
function showError(err) {
loadingEl.classList.add('hidden');
resultCard.classList.add('hidden');
errorContainer.classList.remove('hidden');
errorContainer.textContent = String(err);
}
</script>
</body>
</html>