/* global */ /* ============================================================ converters.jsx — pure conversion functions - CLI → Markdown - Markdown rendering (to HTML) - PDF text extraction (uses pdf.js, loaded lazily) - PDF table extraction - DOCX → Markdown (uses mammoth, loaded lazily) - Excel → HTML/PDF (uses SheetJS) - OCR (uses tesseract.js, loaded lazily) ============================================================ */ // ---------- CLI → MARKDOWN ---------- const CLI_EMOJI_RE = /^([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}\u{2300}-\u{23FF}\u{2B00}-\u{2BFF}]\uFE0F?)\s+/u; const CLI_BOX_CHARS = /[┌┐└┘├┤┬┴┼─│╔╗╚╝╠╣╦╩╬═║┏┓┗┛┣┫┳┻╋━┃]/; const CLI_HSEP_CHARS = /[─━═]/; function _isTableContentRow(line) { return /[│║┃]/.test(line) && !CLI_HSEP_CHARS.test(line); } function _isAnyTableLine(line) { return CLI_BOX_CHARS.test(line); } function _splitTableRow(line) { const cells = line.split(/[│║┃]/).map(c => c.trim()); if (cells.length && cells[0] === '') cells.shift(); if (cells.length && cells[cells.length - 1] === '') cells.pop(); return cells; } function _convertCliTable(tableLines) { const contentRows = tableLines.filter(_isTableContentRow).map(_splitTableRow); if (contentRows.length === 0) return tableLines.join('\n'); const cols = Math.max(...contentRows.map(r => r.length)); contentRows.forEach(r => { while (r.length < cols) r.push(''); }); const [header, ...body] = contentRows; const lines = []; lines.push('| ' + header.join(' | ') + ' |'); lines.push('|' + header.map(() => ' --- ').join('|') + '|'); for (const row of body) lines.push('| ' + row.join(' | ') + ' |'); return lines.join('\n'); } function _looksLikeCode(t) { if (!t) return false; if (/^[a-zA-Z_][\w.]*__[\w]+/.test(t)) return true; if (/^[a-zA-Z_][\w.]*\s*\(.*\)\s*$/.test(t)) return true; if (/^(npm|pnpm|yarn|bun|git|cd|ls|cat|curl|cargo|python|node|pip|brew|docker|gitnexus|mcp__|sudo|rm|cp|mv)\b/.test(t)) return true; if (/^\$\s+/.test(t)) return true; return false; } function _stripIndent(line) { const m = line.match(/^(\s*)(.*)$/); return { indent: m[1].length, body: m[2] }; } function convertCliToMarkdown(input, opts = {}) { const { headerStyle = 'auto', stripEmojis = false, fenceCode = true, tightLists = true, } = opts; if (!input || !input.trim()) return ''; const rawLines = input.replace(/\r\n?/g, '\n').split('\n'); const indents = rawLines.filter(l => l.trim().length > 0).map(l => l.match(/^\s*/)[0].length); const minIndent = indents.length ? Math.min(...indents) : 0; const lines = rawLines.map(l => l.length >= minIndent ? l.slice(minIndent) : l); const out = []; let i = 0; let firstHeaderUsed = false; const pushBlank = () => { if (out.length === 0) return; if (out[out.length - 1] === '') return; out.push(''); }; while (i < lines.length) { const line = lines[i]; const { body } = _stripIndent(line); if (!body) { pushBlank(); i++; continue; } // Table block if (_isAnyTableLine(line)) { const tableLines = []; while (i < lines.length && _isAnyTableLine(lines[i])) { if (lines[i].trim()) tableLines.push(lines[i]); i++; } pushBlank(); out.push(_convertCliTable(tableLines)); pushBlank(); continue; } const emojiMatch = body.match(CLI_EMOJI_RE); const isShortLine = body.length < 80; const looksLikeHeader = emojiMatch && isShortLine && !body.endsWith('.') && !body.includes('```'); if (looksLikeHeader && headerStyle !== 'none') { const text = stripEmojis ? body.replace(CLI_EMOJI_RE, '') : body; pushBlank(); if (headerStyle === 'bold') { out.push(`**${text}**`); } else { const level = firstHeaderUsed ? 3 : 2; out.push('#'.repeat(level) + ' ' + text); firstHeaderUsed = true; } pushBlank(); i++; continue; } if (/^[-*•·]\s+/.test(body)) { const cleaned = body.replace(/^[•·]\s+/, '- ').replace(/^\*\s+/, '- '); out.push(cleaned); i++; if (tightLists) { while (i < lines.length && !lines[i].trim() && i + 1 < lines.length && /^\s*[-*•·]\s+/.test(lines[i + 1])) i++; } continue; } if (fenceCode && _looksLikeCode(body)) { const codeLines = []; while (i < lines.length) { const cur = lines[i]; const t = cur.trim(); if (!t) { if (i + 1 < lines.length && _looksLikeCode(lines[i + 1].trim())) { codeLines.push(''); i++; continue; } break; } if (_looksLikeCode(t)) { codeLines.push(t); i++; } else break; } pushBlank(); out.push('```'); out.push(...codeLines); out.push('```'); pushBlank(); continue; } let text = body; if (stripEmojis) text = text.replace(CLI_EMOJI_RE, ''); out.push(text); i++; } return out.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n'; } // ---------- CHAT → MARKDOWN ---------- // // Designed for "free-form chat text with structural cues" — e.g. pasted from // claude.ai web chat, ChatGPT, or anywhere people write prose with bare-line // section titles, bullets like "key: value →", and inline // patterns. Different from CLI→Markdown (which expects emoji-led headers, // box-drawing tables, and tool calls). // // Heuristics: // 1. Strip common leading indentation across the whole input. // 2. First non-empty short bare line (no terminal punctuation, < 40 chars, // no bullet/marker prefix) → `# H1` (if h1FromFirstLine). // 3. Subsequent short bare lines isolated by blank lines and followed by // another block → `## H2` (if h2FromBareLines). // 4. Bullets (`-` / `*` / `•` / `·`) → normalized to `- `. // 5. Inline `key: value →` or `key = value` patterns → wrap the key (or // key:value) in backticks (if inlineCodePairs). // 6. Standalone non-bullet line of the form `...` // where `` contains `<…>` segments → split into prose-line // ending with `:` + a ```text fenced block (if fencePlaceholders). // 7. Tight lists: collapse blanks between consecutive bullets. const CHAT_KV_ARROW_RE = /^([A-Za-z_][A-Za-z0-9_.-]*\s*:\s*[^\s→][^→]*?)\s*(→|->|=>)\s*(.+)$/; const CHAT_IDENT_ARROW_RE = /^([A-Za-z_][A-Za-z0-9_.-]*)\s+(→|->|=>)\s*(.+)$/; const CHAT_KV_EQUALS_RE = /^(\S{1,15})\s*=\s*(.+)$/; // Detects " :" — placeholder comes BEFORE the colon const CHAT_PLACEHOLDER_TAIL_RE = /^(.+?)\s+((?:[\w-]*<[^>\s]+>)+[\w-]*)\s*[::]\s*$/; const CHAT_HEADING_TERMINAL = /[.。!!??::;;,,]$/; function _chatLooksLikeHeading(text) { const t = text.trim(); if (!t) return false; if (t.length > 40) return false; if (/^[-*•·]\s+/.test(t)) return false; if (/^#{1,6}\s/.test(t)) return false; if (CHAT_HEADING_TERMINAL.test(t)) return false; return true; } function _chatTransformInline(text, opts) { let s = text; if (opts.inlineCodePairs !== false) { // Priority: key:value → / identifier → / key = value let m = s.match(CHAT_KV_ARROW_RE); if (m) { s = '`' + m[1].trim() + '` ' + m[2] + ' ' + m[3]; return s; } m = s.match(CHAT_IDENT_ARROW_RE); if (m) { s = '`' + m[1] + '` ' + m[2] + ' ' + m[3]; return s; } m = s.match(CHAT_KV_EQUALS_RE); if (m) { s = '`' + m[1] + '` = ' + m[2]; return s; } } return s; } function _chatSplitToBlocks(lines) { const blocks = []; let cur = []; for (const l of lines) { if (l.trim() === '') { if (cur.length) { blocks.push(cur); cur = []; } } else { cur.push(l); } } if (cur.length) blocks.push(cur); return blocks; } function convertChatToMarkdown(input, opts = {}) { const { h1FromFirstLine = true, h2FromBareLines = true, inlineCodePairs = true, fencePlaceholders = true, tightLists = true, } = opts; if (!input || !input.trim()) return ''; const raw = input.replace(/\r\n?/g, '\n').split('\n'); const indents = raw.filter(l => l.trim().length > 0).map(l => l.match(/^\s*/)[0].length); const minIndent = indents.length ? Math.min(...indents) : 0; const lines = raw.map(l => l.length >= minIndent ? l.slice(minIndent) : l); const blocks = _chatSplitToBlocks(lines); const out = []; let h1Used = false; const pushBlank = () => { if (out.length && out[out.length - 1] !== '') out.push(''); }; for (let bi = 0; bi < blocks.length; bi++) { const block = blocks[bi]; // Single-line block — candidate for heading or placeholder split if (block.length === 1) { const raw = block[0]; const text = raw.trim(); if (/^#{1,6}\s/.test(text) || text.startsWith('```')) { pushBlank(); out.push(text); pushBlank(); continue; } // " :" split → prose ends with ":" then fence if (fencePlaceholders) { const pm = text.match(CHAT_PLACEHOLDER_TAIL_RE); if (pm) { pushBlank(); out.push(pm[1].trim() + ':'); out.push(''); out.push('```text'); out.push(pm[2].trim()); out.push('```'); pushBlank(); continue; } } // First-line H1 if (h1FromFirstLine && !h1Used && _chatLooksLikeHeading(text)) { out.push('# ' + text); pushBlank(); h1Used = true; continue; } // Subsequent isolated short line → H2 (only if not the last block) if (h2FromBareLines && _chatLooksLikeHeading(text) && bi + 1 < blocks.length) { pushBlank(); out.push('## ' + text); pushBlank(); continue; } // Otherwise plain paragraph out.push(_chatTransformInline(text, opts)); pushBlank(); continue; } // Multi-line block: classify line by line let inList = false; for (const rawLine of block) { const t = rawLine.trim(); if (/^#{1,6}\s/.test(t) || t.startsWith('```')) { if (inList && !tightLists) out.push(''); out.push(t); inList = false; continue; } if (/^[-*•·]\s+/.test(t)) { const body = t.replace(/^[-*•·]\s+/, ''); out.push('- ' + _chatTransformInline(body, opts)); inList = true; continue; } // Indented continuation of a previous bullet — emit as continuation if (inList && /^\s{2,}\S/.test(rawLine)) { out.push(' ' + _chatTransformInline(t, opts)); continue; } // Plain prose line inside multi-line block if (inList) { pushBlank(); inList = false; } // Try the placeholder-tail split here too if (fencePlaceholders) { const pm = t.match(CHAT_PLACEHOLDER_TAIL_RE); if (pm) { out.push(pm[1].trim() + ':'); out.push(''); out.push('```text'); out.push(pm[2].trim()); out.push('```'); pushBlank(); continue; } } out.push(_chatTransformInline(t, opts)); } pushBlank(); } return out.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n'; } // ---------- Markdown rendering (mini renderer + GFM-ish output style) ---------- function escapeHtml(str) { return str.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function renderInline(text) { let s = escapeHtml(text); // links [text](url) s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // images ![alt](url) s = s.replace(/!]*>([^<]*)<\/a>/g, (m) => m); // no-op; placeholder if needed // code s = s.replace(/`([^`]+)`/g, '$1'); // bold s = s.replace(/\*\*([^*]+)\*\*/g, '$1'); // italic s = s.replace(/(^|[^*])\*([^*]+)\*/g, '$1$2'); // strikethrough s = s.replace(/~~([^~]+)~~/g, '$1'); return s; } function renderMarkdown(md) { if (!md) return ''; const lines = md.split('\n'); const out = []; let i = 0; while (i < lines.length) { const line = lines[i]; // code fence if (/^```/.test(line)) { const code = []; i++; while (i < lines.length && !/^```/.test(lines[i])) { code.push(escapeHtml(lines[i])); i++; } i++; out.push(`
${code.join('\n')}
`); continue; } // hr if (/^(-{3,}|_{3,}|\*{3,})\s*$/.test(line)) { out.push('
'); i++; continue; } // table if (/^\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\|[\s\-:|]+\|\s*$/.test(lines[i + 1])) { const header = line.split('|').slice(1, -1).map(c => c.trim()); i += 2; const rows = []; while (i < lines.length && /^\|.*\|\s*$/.test(lines[i])) { rows.push(lines[i].split('|').slice(1, -1).map(c => c.trim())); i++; } let html = ''; header.forEach(h => html += ``); html += ''; rows.forEach(r => { html += ''; r.forEach(c => html += ``); html += ''; }); html += '
${renderInline(h)}
${renderInline(c)}
'; out.push(html); continue; } // headings const h = line.match(/^(#{1,6})\s+(.*)$/); if (h) { out.push(`${renderInline(h[2])}`); i++; continue; } // blockquote if (/^>\s*/.test(line)) { const block = []; while (i < lines.length && /^>\s*/.test(lines[i])) { block.push(lines[i].replace(/^>\s?/, '')); i++; } out.push(`
${renderInline(block.join(' '))}
`); continue; } // unordered list if (/^[-*+]\s+/.test(line)) { const items = []; while (i < lines.length && /^[-*+]\s+/.test(lines[i])) { items.push(lines[i].replace(/^[-*+]\s+/, '')); i++; } out.push('
    ' + items.map(it => `
  • ${renderInline(it)}
  • `).join('') + '
'); continue; } // ordered list if (/^\d+\.\s+/.test(line)) { const items = []; while (i < lines.length && /^\d+\.\s+/.test(lines[i])) { items.push(lines[i].replace(/^\d+\.\s+/, '')); i++; } out.push('
    ' + items.map(it => `
  1. ${renderInline(it)}
  2. `).join('') + '
'); continue; } if (!line.trim()) { i++; continue; } const para = [line]; i++; while (i < lines.length && lines[i].trim() && !/^(#{1,6}\s|```|[-*+]\s|\d+\.\s|>|\|)/.test(lines[i])) { para.push(lines[i]); i++; } out.push(`

${renderInline(para.join(' '))}

`); } return out.join('\n'); } // ---------- HTML → Markdown (for DOCX conversion) ---------- function htmlToMarkdown(html) { const container = document.createElement('div'); container.innerHTML = html; function walk(node, ctx = {}) { if (node.nodeType === Node.TEXT_NODE) { return node.nodeValue.replace(/\s+/g, ' '); } if (node.nodeType !== Node.ELEMENT_NODE) return ''; const tag = node.tagName.toLowerCase(); const kids = Array.from(node.childNodes).map(n => walk(n, ctx)).join(''); switch (tag) { case 'h1': return `\n\n# ${kids.trim()}\n\n`; case 'h2': return `\n\n## ${kids.trim()}\n\n`; case 'h3': return `\n\n### ${kids.trim()}\n\n`; case 'h4': return `\n\n#### ${kids.trim()}\n\n`; case 'h5': return `\n\n##### ${kids.trim()}\n\n`; case 'h6': return `\n\n###### ${kids.trim()}\n\n`; case 'p': return `\n\n${kids.trim()}\n\n`; case 'br': return '\n'; case 'hr': return '\n\n---\n\n'; case 'strong': case 'b': return `**${kids}**`; case 'em': case 'i': return `*${kids}*`; case 'u': return `${kids}`; case 'del': case 's': return `~~${kids}~~`; case 'code': return `\`${kids}\``; case 'pre': return `\n\n\`\`\`\n${node.textContent.trimEnd()}\n\`\`\`\n\n`; case 'a': { const href = node.getAttribute('href') || ''; return `[${kids}](${href})`; } case 'img': { const src = node.getAttribute('src') || ''; const alt = node.getAttribute('alt') || ''; return `![${alt}](${src})`; } case 'blockquote': { const inner = kids.trim().split('\n').map(l => `> ${l}`).join('\n'); return `\n\n${inner}\n\n`; } case 'ul': case 'ol': { const isOrdered = tag === 'ol'; const items = Array.from(node.children).filter(c => c.tagName.toLowerCase() === 'li'); const lines = items.map((li, idx) => { const itemText = walk(li, { ...ctx, inList: true }).trim().replace(/\n+/g, ' '); return `${isOrdered ? `${idx + 1}.` : '-'} ${itemText}`; }); return `\n${lines.join('\n')}\n\n`; } case 'li': return kids; case 'table': { const rows = Array.from(node.querySelectorAll('tr')); if (!rows.length) return ''; const headerCells = Array.from(rows[0].children).map(c => c.textContent.trim().replace(/\s+/g, ' ')); const bodyRows = rows.slice(1).map(tr => Array.from(tr.children).map(c => c.textContent.trim().replace(/\s+/g, ' '))); let md = '\n\n| ' + headerCells.join(' | ') + ' |\n'; md += '|' + headerCells.map(() => ' --- ').join('|') + '|\n'; bodyRows.forEach(r => { md += '| ' + r.join(' | ') + ' |\n'; }); return md + '\n'; } case 'tr': case 'td': case 'th': case 'thead': case 'tbody': return kids; default: return kids; } } let md = walk(container); md = md.replace(/\n{3,}/g, '\n\n').trim() + '\n'; return md; } // ---------- PDF parsing (pdf.js) ---------- let _pdfjsReady = null; const PDFJS_URL = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'; const PDFJS_WORKER = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; function loadPdfJs() { if (_pdfjsReady) return _pdfjsReady; _pdfjsReady = loadScript(PDFJS_URL).then(() => { if (window.pdfjsLib) { window.pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER; } return window.pdfjsLib; }); return _pdfjsReady; } async function extractPdfTextItems(file, onProgress) { const pdfjsLib = await loadPdfJs(); const buf = await readFileAsArrayBuffer(file); const doc = await pdfjsLib.getDocument({ data: buf }).promise; const pages = []; for (let p = 1; p <= doc.numPages; p++) { if (onProgress) onProgress(p, doc.numPages); const page = await doc.getPage(p); const content = await page.getTextContent(); // Each item: { str, transform: [a,b,c,d,e,f], width, height, fontName } const items = content.items.map(it => ({ str: it.str, x: it.transform[4], y: it.transform[5], width: it.width, height: it.height || Math.abs(it.transform[3]), fontName: it.fontName, })); pages.push({ pageNum: p, items }); } return pages; } // Group text items into lines by Y coordinate function groupItemsIntoLines(items) { if (!items.length) return []; // Sort by y descending (pdf coordinates increase upward), then x const sorted = [...items].sort((a, b) => (b.y - a.y) || (a.x - b.x)); const lines = []; const Y_TOL = 3; for (const it of sorted) { const last = lines[lines.length - 1]; if (last && Math.abs(last.y - it.y) <= Y_TOL) { last.items.push(it); last.y = (last.y + it.y) / 2; } else { lines.push({ y: it.y, items: [it] }); } } // Sort each line's items by x lines.forEach(l => l.items.sort((a, b) => a.x - b.x)); // Join with spaces (adding spaces if x gap is large) return lines.map(l => { let text = ''; let lastEnd = null; for (const it of l.items) { if (lastEnd != null) { const gap = it.x - lastEnd; if (gap > it.height * 0.5) text += ' '; } text += it.str; lastEnd = it.x + it.width; } return { y: l.y, text: text.trim(), heightAvg: l.items.reduce((s, x) => s + x.height, 0) / l.items.length }; }).filter(l => l.text); } async function pdfToMarkdown(file, opts = {}, onProgress) { const { pageBreaks = false, detectHeadings = true } = opts; const pages = await extractPdfTextItems(file, onProgress); // Compute heading threshold across all pages let allHeights = []; for (const p of pages) { const lines = groupItemsIntoLines(p.items); p._lines = lines; allHeights.push(...lines.map(l => l.heightAvg)); } allHeights.sort((a, b) => a - b); const median = allHeights.length ? allHeights[Math.floor(allHeights.length / 2)] : 12; const h2thresh = median * 1.25; const h1thresh = median * 1.6; const out = []; for (let pi = 0; pi < pages.length; pi++) { const p = pages[pi]; if (pi > 0 && pageBreaks) out.push('', '---', ''); for (const line of p._lines) { const t = line.text; if (!t) continue; if (detectHeadings && line.heightAvg >= h1thresh) { out.push('', '# ' + t, ''); } else if (detectHeadings && line.heightAvg >= h2thresh) { out.push('', '## ' + t, ''); } else if (/^[•·●]\s/.test(t)) { out.push('- ' + t.replace(/^[•·●]\s*/, '')); } else if (/^\d+[\.、]\s/.test(t) && t.length < 80 && detectHeadings) { out.push('', '## ' + t, ''); } else { out.push(t); } } } return out.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n'; } // PDF → table data (heuristic) async function pdfToTables(file, opts = {}, onProgress) { const { joinLines = true } = opts; const pages = await extractPdfTextItems(file, onProgress); const sheets = []; pages.forEach((p, idx) => { const lines = groupItemsIntoLines(p.items); // Try to detect columns by clustering X positions across items const allX = []; for (const l of lines) for (const it of l.items) allX.push(Math.round(it.x)); if (!allX.length) return; // Cluster X positions allX.sort((a, b) => a - b); const clusters = []; const GAP = 12; for (const x of allX) { const last = clusters[clusters.length - 1]; if (last && x - last.max <= GAP) { last.xs.push(x); last.max = x; } else { clusters.push({ xs: [x], max: x }); } } // Use cluster medians as column starts const cols = clusters.map(c => c.xs[Math.floor(c.xs.length / 2)]).sort((a, b) => a - b); if (cols.length < 2) return; // not table-shaped // Build rows: for each line, assign items to nearest column const rows = lines.map(l => { const row = new Array(cols.length).fill(''); for (const it of l.items) { // find best column let bestI = 0, bestD = Infinity; for (let i = 0; i < cols.length; i++) { const d = Math.abs(it.x - cols[i]); if (d < bestD) { bestD = d; bestI = i; } } row[bestI] = (row[bestI] ? row[bestI] + ' ' : '') + it.str; } return row.map(c => c.trim()); }).filter(r => r.some(c => c)); if (rows.length < 2) return; sheets.push({ name: `Page ${p.pageNum}`, rows }); }); return sheets; } function tableToCsv(rows) { return rows.map(r => r.map(cell => { const s = String(cell ?? ''); if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"'; return s; }).join(',')).join('\n'); } // ---------- DOCX → MARKDOWN (mammoth) ---------- let _mammothReady = null; function loadMammoth() { if (_mammothReady) return _mammothReady; _mammothReady = loadScript('https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.7.0/mammoth.browser.min.js') .then(() => window.mammoth); return _mammothReady; } async function docxToMarkdown(file) { const mammoth = await loadMammoth(); const buf = await readFileAsArrayBuffer(file); const result = await mammoth.convertToHtml( { arrayBuffer: buf }, { styleMap: [ "p[style-name='Heading 1'] => h1:fresh", "p[style-name='Heading 2'] => h2:fresh", "p[style-name='Heading 3'] => h3:fresh", "p[style-name='Heading 4'] => h4:fresh", "p[style-name='Quote'] => blockquote:fresh", "p[style-name='Code'] => pre:fresh", ], } ); return htmlToMarkdown(result.value); } async function docxToHtml(file) { const mammoth = await loadMammoth(); const buf = await readFileAsArrayBuffer(file); const result = await mammoth.convertToHtml({ arrayBuffer: buf }); return result.value; } // ---------- Excel (SheetJS) ---------- let _sheetjsReady = null; function loadSheetJs() { if (_sheetjsReady) return _sheetjsReady; _sheetjsReady = loadScript('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js') .then(() => window.XLSX); return _sheetjsReady; } async function excelToSheets(file) { const XLSX = await loadSheetJs(); const buf = await readFileAsArrayBuffer(file); const wb = XLSX.read(buf, { type: 'array' }); const sheets = wb.SheetNames.map(name => { const ws = wb.Sheets[name]; const rows = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '', raw: false }); return { name, rows }; }); return sheets; } async function sheetsToXlsxBlob(sheets) { const XLSX = await loadSheetJs(); const wb = XLSX.utils.book_new(); for (const s of sheets) { const ws = XLSX.utils.aoa_to_sheet(s.rows); XLSX.utils.book_append_sheet(wb, ws, (s.name || 'Sheet').slice(0, 31)); } const out = XLSX.write(wb, { type: 'array', bookType: 'xlsx' }); return new Blob([out], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); } // ---------- OCR (Tesseract.js) ---------- let _tesseractReady = null; function loadTesseract() { if (_tesseractReady) return _tesseractReady; _tesseractReady = loadScript('https://cdn.jsdelivr.net/npm/tesseract.js@5.0.5/dist/tesseract.min.js') .then(() => window.Tesseract); return _tesseractReady; } async function ocrImage(file, lang, onProgress) { const Tesseract = await loadTesseract(); const worker = await Tesseract.createWorker(lang, 1, { logger: m => { if (onProgress && m.status === 'recognizing text') { onProgress(m.progress); } }, }); const dataUrl = await readFileAsDataURL(file); const { data } = await worker.recognize(dataUrl); await worker.terminate(); return data.text; } // ---------- Export ---------- Object.assign(window, { convertCliToMarkdown, convertChatToMarkdown, renderMarkdown, htmlToMarkdown, pdfToMarkdown, pdfToTables, docxToMarkdown, docxToHtml, excelToSheets, sheetsToXlsxBlob, tableToCsv, ocrImage, loadPdfJs, loadMammoth, loadSheetJs, loadTesseract, });