676 lines
32 KiB
JavaScript
676 lines
32 KiB
JavaScript
// 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 '<div class="dv-doc">' + blocks(body, styles) + '</div>';
|
||
}
|
||
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 += '<ul>'; list = true; }
|
||
if (!isList && list) { html += '</ul>'; list = false; }
|
||
html += para(c, pPr, styles, isList);
|
||
} else {
|
||
if (list) { html += '</ul>'; list = false; }
|
||
if (c.localName === 'tbl') html += table(c, styles);
|
||
else if (c.localName === 'sdt') html += blocks(kid(c, 'sdtContent'), styles);
|
||
}
|
||
}
|
||
return html + (list ? '</ul>' : '');
|
||
}
|
||
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 || ' ') + '</' + tag + '>';
|
||
}
|
||
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' ? '<u class="dv-link">' + inner + '</u>' : 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 += '<br>';
|
||
}
|
||
if (!t) return '';
|
||
if (on('b')) t = '<b>' + t + '</b>';
|
||
if (on('i')) t = '<i>' + t + '</i>';
|
||
if (kid(pr, 'u') && attr(kid(pr, 'u'), 'val') !== 'none') t = '<u>' + t + '</u>';
|
||
if (on('strike')) t = '<s>' + t + '</s>';
|
||
// 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 = '<span style="font-size:' + Math.max(9, Math.min(36, sz / 2)) + 'pt">' + t + '</span>';
|
||
const va = attr(kid(pr, 'vertAlign'), 'val');
|
||
if (va === 'superscript') t = '<sup>' + t + '</sup>';
|
||
else if (va === 'subscript') t = '<sub>' + t + '</sub>';
|
||
return t;
|
||
}
|
||
function table(tbl, styles) {
|
||
let html = '<div class="dv-scroll"><table class="dv-table">';
|
||
for (const tr of kids(tbl, 'tr')) {
|
||
html += '<tr>';
|
||
for (const tc of kids(tr, 'tc')) {
|
||
const span = attr(kid(kid(tc, 'tcPr'), 'gridSpan'), 'val');
|
||
html += '<td' + (span > 1 ? ' colspan="' + span + '"' : '') + '>' + blocks(tc, styles) + '</td>';
|
||
}
|
||
html += '</tr>';
|
||
}
|
||
return html + '</table></div>';
|
||
}
|
||
|
||
// ---------- 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 = '<div class="dv-scroll dv-grid"><table class="dv-sheet"><thead><tr><th></th>';
|
||
for (let c = 0; c < cols; c++) html += '<th>' + esc(names && names[c] != null ? names[c] : colName(c)) + '</th>';
|
||
html += '</tr></thead><tbody>';
|
||
rows.slice(0, MAX_ROWS).forEach((r, i) => {
|
||
html += '<tr><th>' + (i + 1) + '</th>';
|
||
for (let c = 0; c < cols; c++) {
|
||
const v = r[c] == null ? '' : r[c];
|
||
html += '<td' + (typeof v === 'number' ? ' class="n"' : '') + '>' + esc(v) + '</td>';
|
||
}
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table></div>';
|
||
return {html, cut};
|
||
}
|
||
// Several sheets: tabs over the grid.
|
||
function sheets(list) {
|
||
if (!list.length) return '<p class="dv-note">в книге нет листов</p>';
|
||
let html = '';
|
||
if (list.length > 1) {
|
||
html += '<div class="dv-tabs">' + list.map((s, i) => '<button type="button" data-sheet="' + i + '"' + (i ? '' : ' class="on"') + '>' + esc(s.name) + '</button>').join('') + '</div>';
|
||
}
|
||
list.forEach((s, i) => {
|
||
const g = grid(s.rows, s.cols);
|
||
html += '<div class="dv-pane" data-pane="' + i + '"' + (i ? ' hidden' : '') + '>' + (s.note ? '<p class="dv-note">' + esc(s.note) + '</p>' : '') + g.html +
|
||
(g.cut ? '<p class="dv-note">показаны первые ' + MAX_ROWS + ' строк и ' + MAX_COLS + ' столбцов</p>' : '') + '</div>';
|
||
});
|
||
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 += '<br>';
|
||
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 += '<h' + l + '>' + inline(c) + '</h' + l + '>'; }
|
||
else if (c.localName === 'p') html += '<p>' + (inline(c) || ' ') + '</p>';
|
||
else if (c.localName === 'list') html += '<ul>' + kids(c, 'list-item').map(li => '<li>' + walk(li) + '</li>').join('') + '</ul>';
|
||
else if (c.localName === 'table') html += '<div class="dv-scroll"><table class="dv-table">' + Array.from(c.getElementsByTagNameNS('*', 'table-row')).map(r =>
|
||
'<tr>' + kids(r, 'table-cell').map(td => '<td>' + walk(td) + '</td>').join('') + '</tr>').join('') + '</table></div>';
|
||
else if (c.localName === 'section') html += walk(c);
|
||
}
|
||
return html;
|
||
};
|
||
return '<div class="dv-doc">' + walk(body) + '</div>';
|
||
}
|
||
|
||
// ---------- 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 '<p class="dv-note">старый формат .doc: показан только текст, без оформления</p><div class="dv-doc">' +
|
||
text.split(/\r\n|\r|\n|\x07/).filter(l => l.trim()).map(l => '<p>' + esc(l) + '</p>').join('') + '</div>';
|
||
}
|
||
|
||
// ---------- 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 '<p class="dv-note">в базе нет таблиц</p>';
|
||
return sheets(list);
|
||
}
|
||
|
||
// Extract text from PDF content streams without a heavyweight renderer or OCR.
|
||
// Flate-compressed page streams are inflated with the browser's native API.
|
||
async function pdf(buf) {
|
||
const input = new Uint8Array(buf);
|
||
let source = '';
|
||
for (let i = 0; i < input.length; i += 0x8000) source += String.fromCharCode(...input.subarray(i, i + 0x8000));
|
||
if (!source.startsWith('%PDF-')) throw new Error('это не PDF-файл');
|
||
const objects = /\b(\d+)\s+\d+\s+obj\b/g, pages = [];
|
||
let inflatedBytes = 0, textBytes = 0, match;
|
||
async function inflate(data) {
|
||
const reader = new Blob([data]).stream().pipeThrough(new DecompressionStream('deflate')).getReader();
|
||
const chunks = []; let total = 0;
|
||
for (;;) {
|
||
const part = await reader.read();
|
||
if (part.done) break;
|
||
total += part.value.length;
|
||
if (total > 16 * 1024 * 1024 || inflatedBytes + total > 32 * 1024 * 1024) {
|
||
await reader.cancel(); throw new Error('PDF stream слишком велик');
|
||
}
|
||
chunks.push(part.value);
|
||
}
|
||
const result = new Uint8Array(total); let at = 0;
|
||
chunks.forEach(chunk => { result.set(chunk, at); at += chunk.length; });
|
||
inflatedBytes += total;
|
||
return result;
|
||
}
|
||
while ((match = objects.exec(source))) {
|
||
const bodyStart = objects.lastIndex, possibleEnd = source.indexOf('endobj', bodyStart);
|
||
if (possibleEnd < 0) break;
|
||
const probe = source.slice(bodyStart, possibleEnd), stream = /\bstream\r?\n/.exec(probe);
|
||
if (!stream) { objects.lastIndex = possibleEnd + 6; continue; }
|
||
const dict = probe.slice(0, stream.index);
|
||
let start = bodyStart + stream.index + stream[0].length;
|
||
const length = /\/Length\s+(\d+)\b/.exec(dict);
|
||
let end = length ? start + Number(length[1]) : source.indexOf('endstream', start);
|
||
if (end < start) { objects.lastIndex = possibleEnd + 6; continue; }
|
||
let endObj = source.indexOf('endobj', length ? end : end + 9);
|
||
if (endObj < 0) break;
|
||
if (/\/Subtype\s*\/Image|\/Type\s*\/(?:ObjStm|XObject|Font|FontFile)/.test(dict)) { objects.lastIndex = endObj + 6; continue; }
|
||
let raw = source.slice(start, end);
|
||
if (!length) raw = raw.replace(/[\r\n]+$/, '');
|
||
let data = Uint8Array.from(raw, c => c.charCodeAt(0) & 255);
|
||
if (dict.includes('/FlateDecode') && !dict.includes('/ASCII85Decode')) {
|
||
try { data = await inflate(data); }
|
||
catch (e) { objects.lastIndex = endObj + 6; continue; }
|
||
} else if (/\/Filter\b/.test(dict)) { objects.lastIndex = endObj + 6; continue; }
|
||
let content = '';
|
||
for (let i = 0; i < data.length; i += 0x8000) content += String.fromCharCode(...data.subarray(i, i + 0x8000));
|
||
if (content.includes('BT') && (content.includes('Tj') || content.includes('TJ'))) {
|
||
pages.push(content);
|
||
textBytes += content.length;
|
||
if (textBytes >= 2 * 1024 * 1024) break;
|
||
}
|
||
objects.lastIndex = endObj + 6;
|
||
}
|
||
const decode = bytes => {
|
||
if (bytes.length > 1 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
||
const swapped = new Uint8Array(bytes.length - 2);
|
||
for (let i = 2; i + 1 < bytes.length; i += 2) { swapped[i - 2] = bytes[i + 1]; swapped[i - 1] = bytes[i]; }
|
||
return new TextDecoder('utf-16le').decode(swapped);
|
||
}
|
||
return new TextDecoder('windows-1252').decode(bytes);
|
||
};
|
||
function readString(s, at, literal) {
|
||
const bytes = [];
|
||
if (literal) {
|
||
let depth = 1, i = at + 1;
|
||
for (; i < s.length && depth; i++) {
|
||
let c = s.charCodeAt(i);
|
||
if (c === 92) {
|
||
c = s.charCodeAt(++i);
|
||
if (c === 110) bytes.push(10); else if (c === 114) bytes.push(13); else if (c === 116) bytes.push(9);
|
||
else if (c === 98) bytes.push(8); else if (c === 102) bytes.push(12);
|
||
else if (c === 10) { /* escaped line break */ }
|
||
else if (c === 13) { if (s.charCodeAt(i + 1) === 10) i++; }
|
||
else if (c >= 48 && c <= 55) {
|
||
let oct = String.fromCharCode(c);
|
||
for (let n = 0; n < 2 && /[0-7]/.test(s.charAt(i + 1)); n++) oct += s.charAt(++i);
|
||
bytes.push(parseInt(oct, 8) & 255);
|
||
} else bytes.push(c & 255);
|
||
} else if (c === 40) { depth++; bytes.push(c); }
|
||
else if (c === 41) { if (--depth) bytes.push(c); }
|
||
else bytes.push(c & 255);
|
||
}
|
||
return {value: decode(Uint8Array.from(bytes)), end: i};
|
||
}
|
||
let hex = '', i = at + 1;
|
||
for (; i < s.length && s.charAt(i) !== '>'; i++) if (!/\s/.test(s.charAt(i))) hex += s.charAt(i);
|
||
if (hex.length % 2) hex += '0';
|
||
for (let i = 0; i + 1 < hex.length; i += 2) bytes.push(parseInt(hex.slice(i, i + 2), 16));
|
||
return {value: decode(Uint8Array.from(bytes)), end: i + 1};
|
||
}
|
||
function textFrom(content) {
|
||
let out = '', inText = false, i = 0, operands = [];
|
||
const token = () => {
|
||
while (i < content.length && /[\x00\t\n\f\r ]/.test(content.charAt(i))) i++;
|
||
if (content.charAt(i) === '%') { while (i < content.length && content.charAt(i) !== '\n') i++; return token(); }
|
||
if (i >= content.length) return null;
|
||
const c = content.charAt(i);
|
||
if (c === '(') { const v = readString(content, i, true); i = v.end; return {type: 'str', value: v.value}; }
|
||
if (c === '<' && content.charAt(i + 1) !== '<') { const v = readString(content, i, false); i = v.end; return {type: 'str', value: v.value}; }
|
||
if (c === '[') {
|
||
i++; const values = [];
|
||
while (i < content.length) {
|
||
while (/[\x00\t\n\f\r ]/.test(content.charAt(i))) i++;
|
||
if (content.charAt(i) === ']') { i++; break; }
|
||
if (content.charAt(i) === '(' || (content.charAt(i) === '<' && content.charAt(i + 1) !== '<')) {
|
||
const v = readString(content, i, content.charAt(i) === '('); i = v.end; values.push(v.value);
|
||
} else {
|
||
const number = /^[+-]?(?:\d+\.?\d*|\.\d+)/.exec(content.slice(i));
|
||
if (number) { values.push(Number(number[0])); i += number[0].length; } else i++;
|
||
}
|
||
}
|
||
return {type: 'array', value: values};
|
||
}
|
||
if (c === '/') { i++; while (i < content.length && !/[\x00\t\n\f\r ()<>\[\]{}\/%]/.test(content.charAt(i))) i++; return {type: 'name'}; }
|
||
const start = i++;
|
||
while (i < content.length && !/[\x00\t\n\f\r ()<>\[\]{}\/%]/.test(content.charAt(i))) i++;
|
||
return {type: 'word', value: content.slice(start, i)};
|
||
};
|
||
while (i < content.length) {
|
||
const t = token();
|
||
if (!t) break;
|
||
if (t.type !== 'word' || !/^[A-Za-z*'"]+$/.test(t.value)) { operands.push(t); continue; }
|
||
const op = t.value;
|
||
if (op === 'BT') { inText = true; operands = []; continue; }
|
||
if (op === 'ET') { inText = false; out += '\n'; operands = []; continue; }
|
||
if (inText && ['Tj', 'TJ', "'", '"'].includes(op)) {
|
||
if (op === "'" || op === '"') out += '\n';
|
||
const v = operands[operands.length - 1];
|
||
if (v && v.type === 'str') out += v.value;
|
||
else if (v && v.type === 'array') out += v.value.map(x => typeof x === 'number' ? (x < -120 ? ' ' : '') : x).join('');
|
||
} else if (inText && op === 'Tm') {
|
||
if (out && !out.endsWith('\n')) out += '\n';
|
||
} else if (inText && (op === 'T*' || op === 'Td' || op === 'TD')) {
|
||
if (op === 'T*' || Number(operands[operands.length - 1]?.value) !== 0) out += '\n';
|
||
else out += ' ';
|
||
}
|
||
operands = [];
|
||
}
|
||
return out;
|
||
}
|
||
const text = pages.map(textFrom).join('\n').replace(/[\t ]+\n/g, '\n').replace(/ {2,}/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
||
if (!text) throw new Error('в PDF не найден извлекаемый текст (сканированные страницы без OCR не читаются)');
|
||
return text.slice(0, 120000);
|
||
}
|
||
|
||
window.DocView = {docx, xlsx, ods, odt, csv, doc, pdf, sqlite, bindTabs};
|
||
})();
|