Files
lpra/internal/server/web/static/docview.js
T

676 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}[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 || '&nbsp;') + '</' + 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 += '&emsp;';
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 += '&emsp;';
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) || '&nbsp;') + '</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};
})();