// Light previews of office files for the file manager, with no libraries: // docx, xlsx and ods are zip archives of XML, opened with the browser's own // DecompressionStream; csv and tsv are parsed here; an old binary .doc only // gives up its text. Each turns into plain HTML the page styles itself. (function () { 'use strict'; const esc = s => String(s).replace(/[&<>"]/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"'}[c])); // ---------- zip ---------- // Reads the central directory; a file is inflated only when asked for. function openZip(buf) { const b = buf instanceof Uint8Array ? buf : new Uint8Array(buf); const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); let eocd = -1; for (let i = b.length - 22; i >= Math.max(0, b.length - 65557); i--) { if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; } } if (eocd < 0) throw new Error('не zip-архив'); const count = dv.getUint16(eocd + 10, true); let p = dv.getUint32(eocd + 16, true); const files = new Map(); const dec = new TextDecoder(); for (let n = 0; n < count && dv.getUint32(p, true) === 0x02014b50; n++) { const method = dv.getUint16(p + 10, true), size = dv.getUint32(p + 20, true); const nameLen = dv.getUint16(p + 28, true), extra = dv.getUint16(p + 30, true), comment = dv.getUint16(p + 32, true); const local = dv.getUint32(p + 42, true); files.set(dec.decode(b.subarray(p + 46, p + 46 + nameLen)), {method, size, local}); p += 46 + nameLen + extra + comment; } async function bytes(name) { const f = files.get(name); if (!f) return null; const start = f.local + 30 + dv.getUint16(f.local + 26, true) + dv.getUint16(f.local + 28, true); const raw = b.subarray(start, start + f.size); if (f.method === 0) return raw; if (f.method !== 8) throw new Error('сжатие zip, которое браузер не знает'); const out = new Blob([raw]).stream().pipeThrough(new DecompressionStream('deflate-raw')); return new Uint8Array(await new Response(out).arrayBuffer()); } async function xml(name) { const raw = await bytes(name); return raw ? new DOMParser().parseFromString(new TextDecoder().decode(raw), 'application/xml') : null; } return {files, bytes, xml}; } // Children of an element by local name, whatever the namespace prefix. const kids = (el, name) => el ? Array.from(el.children).filter(c => c.localName === name) : []; const kid = (el, name) => kids(el, name)[0] || null; const attr = (el, name) => { if (!el) return null; for (const a of el.attributes) if (a.localName === name) return a.value; return null; }; // ---------- docx ---------- async function docx(buf) { const z = openZip(buf); const doc = await z.xml('word/document.xml'); if (!doc) throw new Error('в архиве нет word/document.xml'); // Style ids to names, to know headings whatever the language of Word. const styles = {}; const st = await z.xml('word/styles.xml'); if (st) for (const s of st.getElementsByTagNameNS('*', 'style')) { styles[attr(s, 'styleId')] = (attr(kid(s, 'name'), 'val') || '').toLowerCase(); } const body = doc.getElementsByTagNameNS('*', 'body')[0]; return '
' + blocks(body, styles) + '
'; } function blocks(el, styles) { let html = '', list = false; for (const c of el ? el.children : []) { if (c.localName === 'p') { const pPr = kid(c, 'pPr'); const isList = !!kid(pPr, 'numPr'); if (isList && !list) { html += ''; list = false; } html += para(c, pPr, styles, isList); } else { if (list) { html += ''; list = false; } if (c.localName === 'tbl') html += table(c, styles); else if (c.localName === 'sdt') html += blocks(kid(c, 'sdtContent'), styles); } } return html + (list ? '' : ''); } function para(p, pPr, styles, isList) { const name = styles[attr(kid(pPr, 'pStyle'), 'val')] || ''; const h = /^heading (\d)/.exec(name); const tag = isList ? 'li' : name === 'title' ? 'h1' : h ? 'h' + Math.min(6, +h[1] + 1) : 'p'; const jc = attr(kid(pPr, 'jc'), 'val'); const align = jc === 'center' ? 'center' : (jc === 'right' || jc === 'end') ? 'right' : (jc === 'both' ? 'justify' : ''); const inner = runs(p); return '<' + tag + (align ? ' style="text-align:' + align + '"' : '') + '>' + (inner || ' ') + ''; } function runs(el) { let html = ''; for (const c of el.children) { if (c.localName === 'r') html += run(c); else if (c.localName === 'hyperlink' || c.localName === 'smartTag' || c.localName === 'ins' || c.localName === 'sdt' || c.localName === 'sdtContent' || c.localName === 'fldSimple') { const inner = runs(c); html += c.localName === 'hyperlink' ? '' + inner + '' : inner; } } return html; } function run(r) { const pr = kid(r, 'rPr'); const on = n => { const e = kid(pr, n); return !!e && attr(e, 'val') !== '0' && attr(e, 'val') !== 'false'; }; let t = ''; for (const c of r.children) { if (c.localName === 't') t += esc(c.textContent); else if (c.localName === 'tab') t += ' '; else if (c.localName === 'br' || c.localName === 'cr') t += '
'; } if (!t) return ''; if (on('b')) t = '' + t + ''; if (on('i')) t = '' + t + ''; if (kid(pr, 'u') && attr(kid(pr, 'u'), 'val') !== 'none') t = '' + t + ''; if (on('strike')) t = '' + t + ''; // A size set on the run itself, as many documents do instead of styles. const sz = +attr(kid(pr, 'sz'), 'val'); if (sz && sz !== 24) t = '' + t + ''; const va = attr(kid(pr, 'vertAlign'), 'val'); if (va === 'superscript') t = '' + t + ''; else if (va === 'subscript') t = '' + t + ''; return t; } function table(tbl, styles) { let html = '
'; for (const tr of kids(tbl, 'tr')) { html += ''; for (const tc of kids(tr, 'tc')) { const span = attr(kid(kid(tc, 'tcPr'), 'gridSpan'), 'val'); html += ' 1 ? ' colspan="' + span + '"' : '') + '>' + blocks(tc, styles) + ''; } html += ''; } return html + '
'; } // ---------- tables: one grid per sheet ---------- const MAX_ROWS = 2000, MAX_COLS = 60; function colName(i) { let s = ''; for (i++; i > 0; i = Math.floor((i - 1) / 26)) s = String.fromCharCode(65 + (i - 1) % 26) + s; return s; } function grid(rows, names) { let cols = names ? names.length : 0; for (const r of rows) cols = Math.max(cols, r.length); cols = Math.min(cols, MAX_COLS); const cut = rows.length > MAX_ROWS || rows.some(r => r.length > MAX_COLS); let html = '
'; for (let c = 0; c < cols; c++) html += ''; html += ''; rows.slice(0, MAX_ROWS).forEach((r, i) => { html += ''; for (let c = 0; c < cols; c++) { const v = r[c] == null ? '' : r[c]; html += '' + esc(v) + ''; } html += ''; }); html += '
' + esc(names && names[c] != null ? names[c] : colName(c)) + '
' + (i + 1) + '
'; return {html, cut}; } // Several sheets: tabs over the grid. function sheets(list) { if (!list.length) return '

в книге нет листов

'; let html = ''; if (list.length > 1) { html += '
' + list.map((s, i) => '').join('') + '
'; } list.forEach((s, i) => { const g = grid(s.rows, s.cols); html += '
' + (s.note ? '

' + esc(s.note) + '

' : '') + g.html + (g.cut ? '

показаны первые ' + MAX_ROWS + ' строк и ' + MAX_COLS + ' столбцов

' : '') + '
'; }); return html; } function bindTabs(root) { root.querySelectorAll('[data-sheet]').forEach(b => b.onclick = () => { root.querySelectorAll('[data-sheet]').forEach(x => x.classList.toggle('on', x === b)); root.querySelectorAll('[data-pane]').forEach(p => p.hidden = p.dataset.pane !== b.dataset.sheet); }); } // ---------- xlsx ---------- async function xlsx(buf) { const z = openZip(buf); const wb = await z.xml('xl/workbook.xml'); if (!wb) throw new Error('в архиве нет xl/workbook.xml'); const rels = {}; const rx = await z.xml('xl/_rels/workbook.xml.rels'); if (rx) for (const r of rx.getElementsByTagNameNS('*', 'Relationship')) rels[r.getAttribute('Id')] = r.getAttribute('Target'); const shared = []; const ss = await z.xml('xl/sharedStrings.xml'); if (ss) for (const si of ss.getElementsByTagNameNS('*', 'si')) { shared.push(Array.from(si.getElementsByTagNameNS('*', 't')).filter(t => t.parentElement.localName !== 'rPh').map(t => t.textContent).join('')); } const list = []; for (const s of wb.getElementsByTagNameNS('*', 'sheet')) { let target = rels[attr(s, 'id')] || ''; target = target.startsWith('/') ? target.slice(1) : 'xl/' + target.replace(/^\.\//, ''); const doc = await z.xml(target); list.push({name: s.getAttribute('name') || '?', rows: doc ? xlsxRows(doc, shared) : []}); } return sheets(list); } function xlsxRows(doc, shared) { const rows = []; for (const row of doc.getElementsByTagNameNS('*', 'row')) { const ri = (+row.getAttribute('r') || rows.length + 1) - 1; if (ri >= MAX_ROWS) break; const out = rows[ri] = rows[ri] || []; let next = 0; for (const c of kids(row, 'c')) { const ref = /^([A-Z]+)/.exec(c.getAttribute('r') || ''); let ci = next; if (ref) { ci = 0; for (const ch of ref[1]) ci = ci * 26 + ch.charCodeAt(0) - 64; ci--; } next = ci + 1; if (ci >= MAX_COLS + 1) continue; const t = c.getAttribute('t'), v = kid(c, 'v'); let val = v ? v.textContent : ''; if (t === 's') val = shared[+val] || ''; else if (t === 'inlineStr') val = Array.from(c.getElementsByTagNameNS('*', 't')).map(x => x.textContent).join(''); else if (t === 'b') val = val === '1' ? 'TRUE' : 'FALSE'; else if (t !== 'str' && t !== 'e' && val !== '' && !isNaN(val)) val = +val; out[ci] = val; } } for (let i = 0; i < rows.length; i++) rows[i] = rows[i] || []; return rows; } // ---------- ods / odt ---------- async function ods(buf) { const z = openZip(buf); const doc = await z.xml('content.xml'); if (!doc) throw new Error('в архиве нет content.xml'); const list = []; for (const t of doc.getElementsByTagNameNS('*', 'table')) { if (t.parentElement && t.parentElement.localName === 'table-cell') continue; const rows = []; for (const r of t.getElementsByTagNameNS('*', 'table-row')) { const cells = []; for (const c of r.children) { if (c.localName !== 'table-cell' && c.localName !== 'covered-table-cell') continue; const rep = Math.min(+attr(c, 'number-columns-repeated') || 1, MAX_COLS + 1 - cells.length); const num = attr(c, 'value'); const text = Array.from(c.getElementsByTagNameNS('*', 'p')).map(p => p.textContent).join('\n'); const val = attr(c, 'value-type') === 'float' && num != null ? +num : text; for (let i = 0; i < rep; i++) cells.push(val); if (cells.length > MAX_COLS) break; } while (cells.length && cells[cells.length - 1] === '') cells.pop(); const rep = Math.min(+attr(r, 'number-rows-repeated') || 1, MAX_ROWS + 1 - rows.length); for (let i = 0; i < rep; i++) rows.push(cells); if (rows.length > MAX_ROWS) break; } while (rows.length && !rows[rows.length - 1].length) rows.pop(); list.push({name: attr(t, 'name') || '?', rows}); } return sheets(list); } async function odt(buf) { const z = openZip(buf); const doc = await z.xml('content.xml'); const body = doc && doc.getElementsByTagNameNS('*', 'text')[0]; if (!body) throw new Error('в архиве нет текста'); // Runs of spaces, tabs and breaks are elements of their own in ODF. const inline = el => { let html = ''; for (const n of el.childNodes) { if (n.nodeType === 3) html += esc(n.nodeValue); else if (n.localName === 's') html += ' '.repeat(+attr(n, 'c') || 1); else if (n.localName === 'tab') html += ' '; else if (n.localName === 'line-break') html += '
'; else if (n.nodeType === 1) html += inline(n); } return html; }; const walk = el => { let html = ''; for (const c of el.children) { if (c.localName === 'h') { const l = Math.min(6, (+attr(c, 'outline-level') || 1) + 1); html += '' + inline(c) + ''; } else if (c.localName === 'p') html += '

' + (inline(c) || ' ') + '

'; else if (c.localName === 'list') html += ''; else if (c.localName === 'table') html += '
' + Array.from(c.getElementsByTagNameNS('*', 'table-row')).map(r => '' + kids(r, 'table-cell').map(td => '').join('') + '').join('') + '
' + walk(td) + '
'; else if (c.localName === 'section') html += walk(c); } return html; }; return '
' + walk(body) + '
'; } // ---------- csv / tsv ---------- function csv(text, tab) { const first = text.slice(0, text.indexOf('\n') >>> 0 || text.length); const sep = tab ? '\t' : (first.split(';').length > first.split(',').length ? ';' : ','); const rows = []; let row = [], f = '', q = false; for (let i = 0; i < text.length; i++) { const c = text[i]; if (q) { if (c === '"') { if (text[i + 1] === '"') { f += '"'; i++; } else q = false; } else f += c; } else if (c === '"' && f === '') q = true; else if (c === sep) { row.push(f); f = ''; } else if (c === '\n' || c === '\r') { if (c === '\r' && text[i + 1] === '\n') i++; row.push(f); f = ''; rows.push(row); row = []; if (rows.length > MAX_ROWS) break; } else f += c; } if (f !== '' || row.length) { row.push(f); rows.push(row); } return sheets([{name: '', rows: rows.map(r => r.map(v => v !== '' && !isNaN(v) && /^-?[\d.]+$/.test(v.trim()) ? +v : v))}]); } // ---------- old .doc ---------- // Word 97-2003 is a binary format; without a full parser all that can be // shown is the text, picked out of the document's WordDocument stream as // the runs that read as words. function cfbStream(b, want) { const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); if (dv.getUint32(0, true) !== 0xE011CFD0) return null; const ss = 1 << dv.getUint16(0x1E, true); const sector = n => 512 + n * ss; const fat = []; const addFat = n => { const o = sector(n); for (let i = 0; i < ss / 4; i++) fat.push(dv.getUint32(o + i * 4, true)); }; for (let i = 0; i < 109; i++) { const n = dv.getUint32(0x4C + i * 4, true); if (n < 0xFFFFFFFA) addFat(n); } for (let n = dv.getUint32(0x44, true), k = 0; n < 0xFFFFFFFA && k < 1000; k++) { const o = sector(n); for (let i = 0; i < ss / 4 - 1; i++) { const m = dv.getUint32(o + i * 4, true); if (m < 0xFFFFFFFA) addFat(m); } n = dv.getUint32(o + ss - 4, true); } const chain = (start, size) => { const out = new Uint8Array(size); let n = start, at = 0; for (let guard = 0; n < 0xFFFFFFFA && at < size && guard < 1e6; guard++) { const o = sector(n), len = Math.min(ss, size - at); if (o + len > b.length) break; out.set(b.subarray(o, o + len), at); at += len; n = fat[n]; } return out.subarray(0, at); }; const dirStart = dv.getUint32(0x30, true); const dir = chain(dirStart, ss * 64); const dd = new DataView(dir.buffer, dir.byteOffset, dir.byteLength); for (let o = 0; o + 128 <= dir.length; o += 128) { const nameLen = dd.getUint16(o + 0x40, true); let name = ''; for (let i = 0; i + 2 < nameLen; i += 2) name += String.fromCharCode(dd.getUint16(o + i, true)); if (name === want && dd.getUint8(o + 0x42) === 2) return chain(dd.getUint32(o + 0x74, true), dd.getUint32(o + 0x78, true)); } return null; } function doc(buf) { const all = new Uint8Array(buf); const b = cfbStream(all, 'WordDocument') || all; const ok = c => c === 9 || c === 10 || c === 13 || (c >= 32 && c < 127) || (c >= 0xA0 && c <= 0x24F) || (c >= 0x400 && c <= 0x4FF) || (c >= 0x2010 && c <= 0x2026) || c === 0x2116; const pieces = []; let cur = ''; const flush = min => { const letters = (cur.match(/[A-Za-zА-Яа-яЁё0-9]/g) || []).length; if (cur.trim().length >= min && letters >= cur.trim().length * 0.5) pieces.push(cur); cur = ''; }; for (let i = 0; i + 1 < b.length; i += 2) { const c = b[i] | (b[i + 1] << 8); if (ok(c)) cur += String.fromCharCode(c); else flush(3); } flush(4); let text = pieces.join('\n'); if (text.length < 40) { // a document that keeps its text 8-bit const p8 = []; for (let i = 0; i < b.length; i++) { const c = b[i]; if (c === 9 || c === 10 || c === 13 || (c >= 32 && c < 127)) cur += String.fromCharCode(c); else { if (cur.trim().length >= 8 && (cur.match(/[A-Za-z]/g) || []).length >= cur.length * 0.5) p8.push(cur); cur = ''; } } text = p8.join('\n'); } if (!text.trim()) throw new Error('текст не нашёлся'); return '

старый формат .doc: показан только текст, без оформления

' + text.split(/\r\n|\r|\n|\x07/).filter(l => l.trim()).map(l => '

' + esc(l) + '

').join('') + '
'; } // ---------- sqlite ---------- // Reads the database file itself: the b-tree of sqlite_master for the // tables, then each table's b-tree for its first rows. No SQL engine. function sqlite(buf) { const b = new Uint8Array(buf); if (new TextDecoder().decode(b.subarray(0, 15)) !== 'SQLite format 3') throw new Error('не база SQLite'); const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); let ps = dv.getUint16(16); if (ps === 1) ps = 65536; const usable = ps - b[20]; const enc = dv.getUint32(56); const dec = new TextDecoder(enc === 2 ? 'utf-16le' : enc === 3 ? 'utf-16be' : 'utf-8'); const page = n => (n - 1) * ps; function varint(o) { let v = 0; for (let i = 0; i < 9; i++) { const c = b[o + i]; if (i === 8) return [v * 256 + c, o + 9]; v = v * 128 + (c & 0x7f); if (!(c & 0x80)) return [v, o + i + 1]; } } // A cell's payload, gathered from its overflow pages when it spills. function payload(o, size) { const max = usable - 35; if (size <= max) return b.subarray(o, o + size); const min = Math.floor((usable - 12) * 32 / 255) - 23; let local = min + (size - min) % (usable - 4); if (local > max) local = min; const out = new Uint8Array(size); out.set(b.subarray(o, o + local)); let at = local, next = dv.getUint32(o + local); for (let guard = 0; next && at < size && guard < 1e6; guard++) { const po = page(next), len = Math.min(usable - 4, size - at); out.set(b.subarray(po + 4, po + 4 + len), at); at += len; next = dv.getUint32(po); } return out; } function record(p) { const pv = new DataView(p.buffer, p.byteOffset, p.byteLength); let [hlen, o] = vi(p, 0); const types = []; while (o < hlen) { const r = vi(p, o); types.push(r[0]); o = r[1]; } let d = hlen; return types.map(t => { let v = null; if (t >= 1 && t <= 6) { const n = [0, 1, 2, 3, 4, 6, 8][t]; let x = 0; for (let i = 0; i < n; i++) x = x * 256 + p[d + i]; if (p[d] & 0x80) x -= Math.pow(2, 8 * n); v = n === 8 && Math.abs(x) > Number.MAX_SAFE_INTEGER ? pv.getBigInt64(d).toString() : x; d += n; } else if (t === 7) { v = pv.getFloat64(d); d += 8; } else if (t === 8) v = 0; else if (t === 9) v = 1; else if (t >= 12 && t % 2 === 0) { const n = (t - 12) / 2; v = '[blob ' + n + ' B]'; d += n; } else if (t >= 13) { const n = (t - 13) / 2; v = dec.decode(p.subarray(d, d + n)); d += n; } return v; }); } // varint over a payload buffer rather than the file function vi(p, o) { let v = 0; for (let i = 0; i < 9; i++) { const c = p[o + i]; if (i === 8) return [v * 256 + c, o + 9]; v = v * 128 + (c & 0x7f); if (!(c & 0x80)) return [v, o + i + 1]; } } // Walks a table b-tree in rowid order, up to limit rows; also counts all. function table(root, limit) { const rows = []; let count = 0; const walk = (n, depth) => { if (depth > 40) return; const h = page(n) + (n === 1 ? 100 : 0); const type = b[h], cells = dv.getUint16(h + 3); const ptrs = h + (type === 0x05 ? 12 : 8); if (type === 0x0d) { count += cells; for (let i = 0; i < cells && rows.length < limit; i++) { let o = page(n) + dv.getUint16(ptrs + i * 2), size, rowid; [size, o] = varint(o); [rowid, o] = varint(o); rows.push({rowid, values: record(payload(o, size))}); } } else if (type === 0x05) { for (let i = 0; i < cells; i++) walk(dv.getUint32(page(n) + dv.getUint16(ptrs + i * 2)), depth + 1); walk(dv.getUint32(h + 8), depth + 1); } }; walk(root, 0); return {rows, count}; } // Column names out of CREATE TABLE; an INTEGER PRIMARY KEY is the rowid. function columns(sql) { const m = /\(([\s\S]*)\)/.exec(sql || ''); if (!m) return {names: [], rowidCol: -1}; const parts = []; let depth = 0, cur = ''; for (const c of m[1]) { if (c === '(') depth++; if (c === ')') depth--; if (c === ',' && !depth) { parts.push(cur); cur = ''; } else cur += c; } parts.push(cur); const names = []; let rowidCol = -1; for (const p of parts) { const t = p.trim(); if (!t || /^(constraint|primary\s+key|unique|check|foreign\s+key)\b/i.test(t)) continue; const nm = /^("(?:[^"]|"")+"|`[^`]+`|\[[^\]]+\]|\S+)/.exec(t)[1].replace(/^["`\[]|["`\]]$/g, ''); if (/^\S+\s+integer\s+primary\s+key\b/i.test(t.replace(/^("(?:[^"]|"")+"|`[^`]+`|\[[^\]]+\])/, 'x'))) rowidCol = names.length; names.push(nm); } return {names, rowidCol}; } const master = table(1, 100000).rows.map(r => ({type: r.values[0], name: r.values[1], root: r.values[3], sql: r.values[4] || ''})); const list = []; for (const t of master.filter(x => x.type === 'table' && x.root).sort((a, c) => String(a.name).localeCompare(String(c.name)))) { if (/without\s+rowid/i.test(t.sql)) { list.push({name: t.name, rows: [], note: 'таблица WITHOUT ROWID - её строки здесь не показываются'}); continue; } const {names, rowidCol} = columns(t.sql); let data; try { data = table(t.root, MAX_ROWS); } catch (e) { list.push({name: t.name, rows: [], note: 'не удалось прочитать: ' + e.message}); continue; } const rows = data.rows.map(r => { const v = r.values.slice(); if (rowidCol >= 0 && v[rowidCol] == null) v[rowidCol] = r.rowid; return v; }); list.push({name: t.name, cols: names, rows, note: data.count + ' ' + (data.count % 10 === 1 && data.count % 100 !== 11 ? 'строка' : data.count % 10 >= 2 && data.count % 10 <= 4 && (data.count % 100 < 12 || data.count % 100 > 14) ? 'строки' : 'строк') + (data.count > rows.length ? ', показаны первые ' + rows.length : '')}); } if (!list.length) return '

в базе нет таблиц

'; return sheets(list); } window.DocView = {docx, xlsx, ods, odt, csv, doc, sqlite, bindTabs}; })();