undoc 0.5.0

High-performance Microsoft Office document extraction to Markdown
Documentation
<!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 &middot; XLSX &middot; PPTX to Markdown, Text, or JSON &mdash; 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&hellip;</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>