@hedeqiang/dsh-nl2sql 0.0.0-stage → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/xlsx.js ADDED
@@ -0,0 +1,1001 @@
1
+ // Self-contained XLSX writer/reader, CSV parser and column type inference.
2
+ // No npm dependencies: only node:zlib and node:buffer.
3
+
4
+ import zlib from 'node:zlib';
5
+ import { Buffer } from 'node:buffer';
6
+
7
+ /* ------------------------------------------------------------------ */
8
+ /* Shared helpers */
9
+ /* ------------------------------------------------------------------ */
10
+
11
+ const MAX_SHEET_ROWS = 1048576; // Excel hard limit (rows incl. header/title)
12
+ const MAX_CELL_CHARS = 32767;
13
+ const ILLEGAL_XML_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
14
+ const WIDE_CHAR_RE = /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F300}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u;
15
+
16
+ /** Remove characters that are illegal in XML 1.0 (keeps \t \n \r). */
17
+ function stripIllegal(s) {
18
+ return s.replace(ILLEGAL_XML_RE, '');
19
+ }
20
+
21
+ /** Escape text for XML element/attribute content. */
22
+ function esc(s) {
23
+ return stripIllegal(String(s))
24
+ .replace(/&/g, '&amp;')
25
+ .replace(/</g, '&lt;')
26
+ .replace(/>/g, '&gt;')
27
+ .replace(/"/g, '&quot;');
28
+ }
29
+
30
+ /** Escape a cell string: XML escape plus OOXML `_xHHHH_` literal protection. */
31
+ function escCellText(s) {
32
+ return esc(s).replace(/_x([0-9A-Fa-f]{4})_/g, '_x005F_x$1_');
33
+ }
34
+
35
+ /** Decode XML entities (named + numeric) and OOXML `_xHHHH_` escapes. */
36
+ function decodeXml(s, ooxml = false) {
37
+ if (!s) return '';
38
+ let out = s.indexOf('&') < 0 ? s : s.replace(/&(#x[0-9a-fA-F]+|#\d+|lt|gt|amp|quot|apos);/g, (m, e) => {
39
+ switch (e) {
40
+ case 'lt': return '<';
41
+ case 'gt': return '>';
42
+ case 'amp': return '&';
43
+ case 'quot': return '"';
44
+ case 'apos': return "'";
45
+ default: {
46
+ const cp = e[1] === 'x' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
47
+ try { return String.fromCodePoint(cp); } catch { return ''; }
48
+ }
49
+ }
50
+ });
51
+ if (ooxml && out.indexOf('_x') >= 0) {
52
+ out = out.replace(/_x([0-9A-Fa-f]{4})_/g, (m, h) => String.fromCharCode(parseInt(h, 16)));
53
+ }
54
+ return out;
55
+ }
56
+
57
+ /** Parse XML attributes into a plain object (keys keep their prefixes). */
58
+ function parseAttrs(s) {
59
+ const out = {};
60
+ if (!s) return out;
61
+ const re = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
62
+ let m;
63
+ while ((m = re.exec(s))) out[m[1]] = decodeXml(m[2] ?? m[3]);
64
+ return out;
65
+ }
66
+
67
+ /** 0-based column index → letters (0 → A). */
68
+ function colName(i) {
69
+ let s = '';
70
+ i += 1;
71
+ while (i > 0) {
72
+ const r = (i - 1) % 26;
73
+ s = String.fromCharCode(65 + r) + s;
74
+ i = Math.floor((i - 1) / 26);
75
+ }
76
+ return s;
77
+ }
78
+
79
+ /** Letters → 0-based column index (A → 0). */
80
+ function colIndex(letters) {
81
+ let n = 0;
82
+ for (let i = 0; i < letters.length; i++) n = n * 26 + (letters.charCodeAt(i) & 0x1f);
83
+ return n - 1;
84
+ }
85
+
86
+ /** Display width estimate: CJK / full-width characters count double. */
87
+ function displayWidth(s) {
88
+ let w = 0;
89
+ for (const ch of s) w += WIDE_CHAR_RE.test(ch) ? 2 : 1;
90
+ return w;
91
+ }
92
+
93
+ const pad2 = (n) => String(n).padStart(2, '0');
94
+
95
+ /** Format a Date (UTC parts) as `YYYY-MM-DD HH:MM:SS` or `YYYY-MM-DD`. */
96
+ function fmtDate(d, withTime) {
97
+ const s = `${String(d.getUTCFullYear()).padStart(4, '0')}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
98
+ return withTime ? `${s} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}` : s;
99
+ }
100
+
101
+ /* ------------------------------------------------------------------ */
102
+ /* ZIP writer */
103
+ /* ------------------------------------------------------------------ */
104
+
105
+ /** DOS date/time words for a JS Date (local time). */
106
+ function dosDateTime(d) {
107
+ const year = Math.max(1980, d.getFullYear());
108
+ return {
109
+ time: (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1),
110
+ date: ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Build a deflate-compressed zip archive.
116
+ * @param {{name: string, data: Buffer}[]} files
117
+ * @returns {Buffer}
118
+ */
119
+ function buildZip(files) {
120
+ const { time, date } = dosDateTime(new Date());
121
+ const chunks = [];
122
+ const central = [];
123
+ let offset = 0;
124
+ for (const f of files) {
125
+ const name = Buffer.from(f.name, 'utf8');
126
+ const data = f.data;
127
+ const crc = zlib.crc32(data) >>> 0;
128
+ const comp = zlib.deflateRawSync(data, { level: 6 });
129
+ const local = Buffer.alloc(30);
130
+ local.writeUInt32LE(0x04034b50, 0);
131
+ local.writeUInt16LE(20, 4); // version needed
132
+ local.writeUInt16LE(0x0800, 6); // UTF-8 names
133
+ local.writeUInt16LE(8, 8); // deflate
134
+ local.writeUInt16LE(time, 10);
135
+ local.writeUInt16LE(date, 12);
136
+ local.writeUInt32LE(crc, 14);
137
+ local.writeUInt32LE(comp.length, 18);
138
+ local.writeUInt32LE(data.length, 22);
139
+ local.writeUInt16LE(name.length, 26);
140
+ local.writeUInt16LE(0, 28);
141
+ chunks.push(local, name, comp);
142
+
143
+ const cd = Buffer.alloc(46);
144
+ cd.writeUInt32LE(0x02014b50, 0);
145
+ cd.writeUInt16LE(20, 4); // version made by
146
+ cd.writeUInt16LE(20, 6); // version needed
147
+ cd.writeUInt16LE(0x0800, 8);
148
+ cd.writeUInt16LE(8, 10);
149
+ cd.writeUInt16LE(time, 12);
150
+ cd.writeUInt16LE(date, 14);
151
+ cd.writeUInt32LE(crc, 16);
152
+ cd.writeUInt32LE(comp.length, 20);
153
+ cd.writeUInt32LE(data.length, 24);
154
+ cd.writeUInt16LE(name.length, 28);
155
+ // extra len, comment len, disk start, internal attrs = 0
156
+ cd.writeUInt32LE(0, 38); // external attrs
157
+ cd.writeUInt32LE(offset, 42);
158
+ central.push(cd, name);
159
+ offset += 30 + name.length + comp.length;
160
+ }
161
+ const cdBuf = Buffer.concat(central);
162
+ const eocd = Buffer.alloc(22);
163
+ eocd.writeUInt32LE(0x06054b50, 0);
164
+ eocd.writeUInt16LE(files.length, 8);
165
+ eocd.writeUInt16LE(files.length, 10);
166
+ eocd.writeUInt32LE(cdBuf.length, 12);
167
+ eocd.writeUInt32LE(offset, 16);
168
+ return Buffer.concat([...chunks, cdBuf, eocd]);
169
+ }
170
+
171
+ /* ------------------------------------------------------------------ */
172
+ /* XLSX writer */
173
+ /* ------------------------------------------------------------------ */
174
+
175
+ const XML_HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
176
+ const NS_MAIN = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
177
+ const NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
178
+ const NS_PKG_REL = 'http://schemas.openxmlformats.org/package/2006/relationships';
179
+
180
+ // cellXfs indices
181
+ const S_TITLE = 1, S_NOTE = 2, S_HEADER = 3, S_INT = 4, S_DEC = 5;
182
+
183
+ const STYLES_XML = XML_HEAD +
184
+ `<styleSheet xmlns="${NS_MAIN}">` +
185
+ '<numFmts count="2"><numFmt numFmtId="164" formatCode="#,##0"/><numFmt numFmtId="165" formatCode="#,##0.00"/></numFmts>' +
186
+ '<fonts count="4">' +
187
+ '<font><sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/><scheme val="minor"/></font>' +
188
+ '<font><b/><sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/><scheme val="minor"/></font>' +
189
+ '<font><i/><sz val="10"/><color rgb="FF808080"/><name val="Calibri"/><family val="2"/><scheme val="minor"/></font>' +
190
+ '<font><b/><sz val="14"/><color theme="1"/><name val="Calibri"/><family val="2"/><scheme val="minor"/></font>' +
191
+ '</fonts>' +
192
+ '<fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill>' +
193
+ '<fill><patternFill patternType="solid"><fgColor rgb="FFEFEFEF"/><bgColor indexed="64"/></patternFill></fill></fills>' +
194
+ '<borders count="2"><border><left/><right/><top/><bottom/><diagonal/></border>' +
195
+ '<border><left/><right/><top/><bottom style="thin"><color rgb="FFA0A0A0"/></bottom><diagonal/></border></borders>' +
196
+ '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>' +
197
+ '<cellXfs count="6">' +
198
+ '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>' +
199
+ '<xf numFmtId="0" fontId="3" fillId="0" borderId="0" xfId="0" applyFont="1"/>' +
200
+ '<xf numFmtId="0" fontId="2" fillId="0" borderId="0" xfId="0" applyFont="1"/>' +
201
+ '<xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1"/>' +
202
+ '<xf numFmtId="164" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>' +
203
+ '<xf numFmtId="165" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>' +
204
+ '</cellXfs>' +
205
+ '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>' +
206
+ '<dxfs count="0"/><tableStyles count="0" defaultTableStyle="TableStyleMedium2" defaultPivotStyle="PivotStyleLight16"/>' +
207
+ '</styleSheet>';
208
+
209
+ const SHEET_NAME_BAD_RE = /[\[\]:*?/\\]/g;
210
+
211
+ /** Make Excel-safe, unique (case-insensitive), non-empty sheet names. */
212
+ function sanitizeSheetNames(names) {
213
+ const used = new Set(['history']); // "History" is reserved by Excel
214
+ return names.map((raw, i) => {
215
+ let base = stripIllegal(String(raw ?? '')).replace(SHEET_NAME_BAD_RE, '_').replace(/[\r\n\t]/g, ' ').trim();
216
+ base = base.replace(/^'+|'+$/g, '').trim();
217
+ if (!base) base = `Sheet${i + 1}`;
218
+ base = Array.from(base).slice(0, 31).join('');
219
+ let name = base;
220
+ for (let n = 2; used.has(name.toLowerCase()); n++) {
221
+ const suffix = ` (${n})`;
222
+ name = Array.from(base).slice(0, 31 - suffix.length).join('').trimEnd() + suffix;
223
+ }
224
+ used.add(name.toLowerCase());
225
+ return name;
226
+ });
227
+ }
228
+
229
+ const NUM_STR_RE = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/;
230
+
231
+ /**
232
+ * Decide whether a value in a 'number' column can be written as a numeric cell.
233
+ * @returns {number|null} the number, or null when it must stay text
234
+ */
235
+ function asNumber(v) {
236
+ if (typeof v === 'number') {
237
+ if (!Number.isFinite(v)) return null;
238
+ if (Number.isInteger(v) && !Number.isSafeInteger(v)) return null;
239
+ return v;
240
+ }
241
+ if (typeof v !== 'string') return null;
242
+ const s = v.trim();
243
+ if (!s || !NUM_STR_RE.test(s)) return null;
244
+ const digits = s.replace(/[eE].*$/, '').replace(/\D/g, '');
245
+ if (digits.length >= 16) return null; // IDs / precision loss
246
+ if (/^[-+]?0\d/.test(s)) return null; // leading-zero codes
247
+ const n = Number(s);
248
+ if (!Number.isFinite(n)) return null;
249
+ if (Number.isInteger(n) && !Number.isSafeInteger(n)) return null;
250
+ return n;
251
+ }
252
+
253
+ /** Convert any non-number value to the cell's display string. */
254
+ function toText(v) {
255
+ if (typeof v === 'string') return v;
256
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? '' : fmtDate(v, true);
257
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return Buffer.from(v).toString('base64');
258
+ if (typeof v === 'object') {
259
+ try { return JSON.stringify(v, (k, x) => (typeof x === 'bigint' ? x.toString() : x)); } catch { return String(v); }
260
+ }
261
+ return String(v);
262
+ }
263
+
264
+ /** Write string chunks into Buffers periodically to avoid giant JS strings. */
265
+ class XmlSink {
266
+ constructor() { this.parts = []; this.bufs = []; this.len = 0; }
267
+ push(s) {
268
+ this.parts.push(s);
269
+ this.len += s.length;
270
+ if (this.len > 4_000_000) this.flush();
271
+ }
272
+ flush() {
273
+ if (this.parts.length) this.bufs.push(Buffer.from(this.parts.join(''), 'utf8'));
274
+ this.parts = [];
275
+ this.len = 0;
276
+ }
277
+ toBuffer() { this.flush(); return Buffer.concat(this.bufs); }
278
+ }
279
+
280
+ /**
281
+ * Write sheets to an .xlsx workbook.
282
+ * @param {{name: string, columns: {name: string, kind?: 'number'|'time'|'text'}[], rows: any[][], title?: string, note?: string}[]} sheets
283
+ * @param {{creator?: string}} [opts]
284
+ * @returns {Buffer}
285
+ */
286
+ export function writeXlsx(sheets, { creator } = {}) {
287
+ if (!Array.isArray(sheets) || sheets.length === 0) sheets = [{ name: 'Sheet1', columns: [], rows: [] }];
288
+ const names = sanitizeSheetNames(sheets.map((s) => s?.name));
289
+ const sst = new Map();
290
+ let sstCount = 0;
291
+ const strIndex = (s) => {
292
+ sstCount++;
293
+ let i = sst.get(s);
294
+ if (i === undefined) { i = sst.size; sst.set(s, i); }
295
+ return i;
296
+ };
297
+ const filterNames = [];
298
+
299
+ const sheetBufs = sheets.map((sheet, si) => {
300
+ const columns = Array.isArray(sheet?.columns) ? sheet.columns : [];
301
+ let rows = Array.isArray(sheet?.rows) ? sheet.rows : [];
302
+ let title = sheet?.title != null && sheet.title !== '' ? String(sheet.title) : '';
303
+ let note = sheet?.note != null && sheet.note !== '' ? String(sheet.note) : '';
304
+ const ncol = Math.max(columns.length, 1);
305
+
306
+ // Layout: [title][note][blank] header data...
307
+ let headerRow = title || note ? (title ? 1 : 0) + (note ? 1 : 0) + 2 : 1;
308
+ if (rows.length > MAX_SHEET_ROWS - headerRow) {
309
+ // Ensure title + note rows exist: title, note, blank, header.
310
+ if (!title) title = names[si];
311
+ headerRow = 4;
312
+ const keep = MAX_SHEET_ROWS - headerRow;
313
+ const msg = `数据已截断:仅保留前 ${keep} 行(共 ${rows.length} 行)`;
314
+ note = note ? `${note};${msg}` : msg;
315
+ rows = rows.slice(0, keep);
316
+ }
317
+
318
+ const kinds = columns.map((c) => c?.kind || 'text');
319
+ const out = new XmlSink();
320
+ const lastRow = headerRow + rows.length;
321
+ const lastCol = colName(ncol - 1);
322
+ out.push(XML_HEAD + `<worksheet xmlns="${NS_MAIN}" xmlns:r="${NS_R}">`);
323
+ out.push(`<dimension ref="A1:${lastCol}${Math.max(lastRow, 1)}"/>`);
324
+ out.push(`<sheetViews><sheetView workbookViewId="0"${si === 0 ? ' tabSelected="1"' : ''}>`);
325
+ if (columns.length) {
326
+ out.push(`<pane ySplit="${headerRow}" topLeftCell="A${headerRow + 1}" activePane="bottomLeft" state="frozen"/>` +
327
+ `<selection pane="bottomLeft" activeCell="A${headerRow + 1}" sqref="A${headerRow + 1}"/>`);
328
+ }
329
+ out.push('</sheetView></sheetViews><sheetFormatPr defaultRowHeight="15"/>');
330
+
331
+ // Column widths from header + first 200 values.
332
+ if (columns.length) {
333
+ out.push('<cols>');
334
+ columns.forEach((c, j) => {
335
+ let w = displayWidth(String(c?.name ?? ''));
336
+ const n = Math.min(rows.length, 200);
337
+ for (let r = 0; r < n; r++) {
338
+ const v = rows[r]?.[j];
339
+ if (v == null) continue;
340
+ const num = kinds[j] === 'number' ? asNumber(v) : null;
341
+ const s = num !== null ? num.toLocaleString('en-US', { maximumFractionDigits: 2 }) : toText(v);
342
+ const firstLine = s.length > 200 ? s.slice(0, 200) : s;
343
+ w = Math.max(w, displayWidth(firstLine.split('\n')[0]));
344
+ }
345
+ const width = Math.min(60, Math.max(8, w + 2));
346
+ out.push(`<col min="${j + 1}" max="${j + 1}" width="${width}" customWidth="1"/>`);
347
+ });
348
+ out.push('</cols>');
349
+ }
350
+
351
+ out.push('<sheetData>');
352
+ const strCell = (ref, s, style) => {
353
+ s = stripIllegal(s);
354
+ if (s.length > MAX_CELL_CHARS) s = s.slice(0, MAX_CELL_CHARS);
355
+ return `<c r="${ref}" t="s"${style ? ` s="${style}"` : ''}><v>${strIndex(s)}</v></c>`;
356
+ };
357
+ let rowNo = 1;
358
+ if (title) out.push(`<row r="${rowNo}">${strCell(`A${rowNo}`, title, S_TITLE)}</row>`), rowNo++;
359
+ if (note) out.push(`<row r="${rowNo}">${strCell(`A${rowNo}`, note, S_NOTE)}</row>`), rowNo++;
360
+ if (columns.length) {
361
+ let h = `<row r="${headerRow}">`;
362
+ columns.forEach((c, j) => { h += strCell(colName(j) + headerRow, String(c?.name ?? ''), S_HEADER); });
363
+ out.push(h + '</row>');
364
+ }
365
+ const colRefs = columns.map((_, j) => colName(j));
366
+ for (let i = 0; i < rows.length; i++) {
367
+ const r = headerRow + 1 + i;
368
+ const row = rows[i];
369
+ let x = `<row r="${r}">`;
370
+ if (Array.isArray(row)) {
371
+ const n = Math.max(columns.length, 0) || row.length;
372
+ for (let j = 0; j < n && j < row.length; j++) {
373
+ const v = row[j];
374
+ if (v == null || v === '') continue;
375
+ const ref = (colRefs[j] ?? colName(j)) + r;
376
+ const num = kinds[j] === 'number' ? asNumber(v) : null;
377
+ if (num !== null) {
378
+ x += `<c r="${ref}" s="${Number.isInteger(num) ? S_INT : S_DEC}"><v>${num}</v></c>`;
379
+ } else {
380
+ x += strCell(ref, toText(v), 0);
381
+ }
382
+ }
383
+ }
384
+ out.push(x + '</row>');
385
+ }
386
+ out.push('</sheetData>');
387
+ if (columns.length) {
388
+ const ref = `A${headerRow}:${lastCol}${lastRow}`;
389
+ out.push(`<autoFilter ref="${ref}"/>`);
390
+ filterNames.push({ si, ref: `'${names[si].replace(/'/g, "''")}'!$A$${headerRow}:$${lastCol}$${lastRow}` });
391
+ }
392
+ out.push('<pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/></worksheet>');
393
+ return out.toBuffer();
394
+ });
395
+
396
+ // Shared strings
397
+ const ss = new XmlSink();
398
+ ss.push(XML_HEAD + `<sst xmlns="${NS_MAIN}" count="${sstCount}" uniqueCount="${sst.size}">`);
399
+ for (const s of sst.keys()) {
400
+ const preserve = /^\s|\s$|\n/.test(s) ? ' xml:space="preserve"' : '';
401
+ ss.push(`<si><t${preserve}>${escCellText(s)}</t></si>`);
402
+ }
403
+ ss.push('</sst>');
404
+
405
+ const n = sheets.length;
406
+ const sheetIdx = Array.from({ length: n }, (_, i) => i + 1);
407
+ const contentTypes = XML_HEAD +
408
+ '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
409
+ '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
410
+ '<Default Extension="xml" ContentType="application/xml"/>' +
411
+ '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' +
412
+ sheetIdx.map((i) => `<Override PartName="/xl/worksheets/sheet${i}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join('') +
413
+ '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>' +
414
+ '<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>' +
415
+ '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' +
416
+ '<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' +
417
+ '</Types>';
418
+ const rootRels = XML_HEAD + `<Relationships xmlns="${NS_PKG_REL}">` +
419
+ `<Relationship Id="rId1" Type="${NS_R}/officeDocument" Target="xl/workbook.xml"/>` +
420
+ `<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>` +
421
+ `<Relationship Id="rId3" Type="${NS_R}/extended-properties" Target="docProps/app.xml"/>` +
422
+ '</Relationships>';
423
+ const app = XML_HEAD +
424
+ '<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">' +
425
+ `<Application>${esc(creator || 'Microsoft Excel')}</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop>` +
426
+ `<HeadingPairs><vt:vector size="2" baseType="variant"><vt:variant><vt:lpstr>Worksheets</vt:lpstr></vt:variant><vt:variant><vt:i4>${n}</vt:i4></vt:variant></vt:vector></HeadingPairs>` +
427
+ `<TitlesOfParts><vt:vector size="${n}" baseType="lpstr">${names.map((s) => `<vt:lpstr>${esc(s)}</vt:lpstr>`).join('')}</vt:vector></TitlesOfParts>` +
428
+ '<LinksUpToDate>false</LinksUpToDate><SharedDoc>false</SharedDoc><HyperlinksChanged>false</HyperlinksChanged><AppVersion>16.0300</AppVersion>' +
429
+ '</Properties>';
430
+ const now = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
431
+ const core = XML_HEAD +
432
+ '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' +
433
+ (creator ? `<dc:creator>${esc(creator)}</dc:creator><cp:lastModifiedBy>${esc(creator)}</cp:lastModifiedBy>` : '') +
434
+ `<dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>` +
435
+ '</cp:coreProperties>';
436
+ const workbook = XML_HEAD + `<workbook xmlns="${NS_MAIN}" xmlns:r="${NS_R}">` +
437
+ '<workbookPr/><bookViews><workbookView xWindow="0" yWindow="0" windowWidth="28800" windowHeight="16000" activeTab="0"/></bookViews><sheets>' +
438
+ names.map((s, i) => `<sheet name="${esc(s)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join('') + '</sheets>' +
439
+ (filterNames.length ? '<definedNames>' + filterNames.map((f) => `<definedName name="_xlnm._FilterDatabase" localSheetId="${f.si}" hidden="1">${esc(f.ref)}</definedName>`).join('') + '</definedNames>' : '') +
440
+ '<calcPr calcId="191029"/></workbook>';
441
+ const wbRels = XML_HEAD + `<Relationships xmlns="${NS_PKG_REL}">` +
442
+ sheetIdx.map((i) => `<Relationship Id="rId${i}" Type="${NS_R}/worksheet" Target="worksheets/sheet${i}.xml"/>`).join('') +
443
+ `<Relationship Id="rId${n + 1}" Type="${NS_R}/styles" Target="styles.xml"/>` +
444
+ `<Relationship Id="rId${n + 2}" Type="${NS_R}/sharedStrings" Target="sharedStrings.xml"/>` +
445
+ '</Relationships>';
446
+
447
+ const b = (s) => Buffer.from(s, 'utf8');
448
+ return buildZip([
449
+ { name: '[Content_Types].xml', data: b(contentTypes) },
450
+ { name: '_rels/.rels', data: b(rootRels) },
451
+ { name: 'docProps/app.xml', data: b(app) },
452
+ { name: 'docProps/core.xml', data: b(core) },
453
+ { name: 'xl/workbook.xml', data: b(workbook) },
454
+ { name: 'xl/_rels/workbook.xml.rels', data: b(wbRels) },
455
+ { name: 'xl/styles.xml', data: b(STYLES_XML) },
456
+ ...sheetBufs.map((data, i) => ({ name: `xl/worksheets/sheet${i + 1}.xml`, data })),
457
+ { name: 'xl/sharedStrings.xml', data: ss.toBuffer() },
458
+ ]);
459
+ }
460
+
461
+ /* ------------------------------------------------------------------ */
462
+ /* ZIP reader */
463
+ /* ------------------------------------------------------------------ */
464
+
465
+ const unsupported = (why) => new Error(`不支持的 Excel 文件:${why}`);
466
+
467
+ /**
468
+ * Parse a zip central directory.
469
+ * @returns {Map<string, {name, method, flags, csize, usize, offset}>}
470
+ */
471
+ function readZipEntries(buf) {
472
+ if (buf.length < 22) throw unsupported('文件过小或已损坏');
473
+ const sig = buf.readUInt32LE(0);
474
+ if (sig === 0xe011cfd0) {
475
+ if (buf.includes(Buffer.from('EncryptionInfo', 'utf16le'))) throw unsupported('文件已加密(受密码保护),请取消密码后重新导出');
476
+ throw new Error('暂不支持旧版 .xls,请另存为 .xlsx 或 CSV');
477
+ }
478
+ if (sig !== 0x04034b50 && sig !== 0x06054b50) throw unsupported('不是有效的 .xlsx(zip)文件');
479
+ let eocd = -1;
480
+ for (let i = buf.length - 22, stop = Math.max(0, buf.length - 22 - 65535); i >= stop; i--) {
481
+ if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
482
+ }
483
+ if (eocd < 0) throw unsupported('找不到 zip 目录,文件可能已损坏');
484
+ const count = buf.readUInt16LE(eocd + 10);
485
+ const cdOffset = buf.readUInt32LE(eocd + 16);
486
+ if (count === 0xffff || cdOffset === 0xffffffff) throw unsupported('不支持 zip64 格式');
487
+ const entries = new Map();
488
+ let p = cdOffset;
489
+ for (let i = 0; i < count; i++) {
490
+ if (p + 46 > buf.length || buf.readUInt32LE(p) !== 0x02014b50) throw unsupported('zip 目录已损坏');
491
+ const flags = buf.readUInt16LE(p + 8);
492
+ const method = buf.readUInt16LE(p + 10);
493
+ const csize = buf.readUInt32LE(p + 20);
494
+ const usize = buf.readUInt32LE(p + 24);
495
+ const nameLen = buf.readUInt16LE(p + 28);
496
+ const extraLen = buf.readUInt16LE(p + 30);
497
+ const commentLen = buf.readUInt16LE(p + 32);
498
+ const offset = buf.readUInt32LE(p + 42);
499
+ const name = buf.toString('utf8', p + 46, p + 46 + nameLen).replace(/\\/g, '/').replace(/^\//, '');
500
+ entries.set(name, { name, flags, method, csize, usize, offset });
501
+ p += 46 + nameLen + extraLen + commentLen;
502
+ }
503
+ return entries;
504
+ }
505
+
506
+ /** Extract one zip entry as a Buffer. */
507
+ function extractEntry(buf, e) {
508
+ if (e.flags & 1) throw unsupported('文件已加密(受密码保护),请取消密码后重新导出');
509
+ if (buf.readUInt32LE(e.offset) !== 0x04034b50) throw unsupported(`zip 条目已损坏(${e.name})`);
510
+ const start = e.offset + 30 + buf.readUInt16LE(e.offset + 26) + buf.readUInt16LE(e.offset + 28);
511
+ const raw = buf.subarray(start, start + e.csize);
512
+ if (e.method === 0) return raw;
513
+ if (e.method === 8) {
514
+ // Cap decompressed size (zip bombs): 512 MB per part.
515
+ try { return zlib.inflateRawSync(raw, { maxOutputLength: 512 * 1024 * 1024 }); } catch (err) { throw unsupported(`解压失败(${e.name}):${err.code === 'ERR_BUFFER_TOO_LARGE' ? '解压后超过 512 MB' : err.message}`); }
516
+ }
517
+ throw unsupported(`不支持的压缩方式 ${e.method}(${e.name})`);
518
+ }
519
+
520
+ /* ------------------------------------------------------------------ */
521
+ /* XLSX reader */
522
+ /* ------------------------------------------------------------------ */
523
+
524
+ const BUILTIN_DATE_FMTS = new Set([14, 15, 16, 17, 18, 19, 20, 21, 22, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 45, 46, 47, 50, 51, 52, 53, 54, 55, 56, 57, 58]);
525
+
526
+ /** True when a custom number format code renders a date/time. */
527
+ function isDateFormat(code) {
528
+ if (!code || /^general$/i.test(code)) return false;
529
+ const s = code
530
+ .replace(/"[^"]*"/g, '')
531
+ .replace(/\[[^\]]*\]/g, '')
532
+ .replace(/\\./g, '')
533
+ .replace(/[_*]./g, '');
534
+ return /[ymdhs]/i.test(s);
535
+ }
536
+
537
+ /** Resolve a relationship target against the source part's directory. */
538
+ function resolvePart(baseDir, target) {
539
+ if (target.startsWith('/')) return target.slice(1);
540
+ const parts = (baseDir ? baseDir.split('/') : []).concat(target.split('/'));
541
+ const out = [];
542
+ for (const p of parts) {
543
+ if (!p || p === '.') continue;
544
+ if (p === '..') out.pop(); else out.push(p);
545
+ }
546
+ return out.join('/');
547
+ }
548
+
549
+ /** Parse a .rels part into Map<Id, {target, type}>. */
550
+ function parseRels(xml, baseDir) {
551
+ const out = new Map();
552
+ if (!xml) return out;
553
+ const re = /<(?:\w+:)?Relationship\b([^>]*)>/g;
554
+ let m;
555
+ while ((m = re.exec(xml))) {
556
+ const a = parseAttrs(m[1]);
557
+ if (a.TargetMode === 'External' || !a.Target) continue;
558
+ out.set(a.Id, { target: resolvePart(baseDir, a.Target), type: a.Type || '' });
559
+ }
560
+ return out;
561
+ }
562
+
563
+ /** Collect the text of all `<t>` elements in an `<si>` / `<is>` block (ignoring phonetic runs). */
564
+ function collectText(xml) {
565
+ const body = xml.replace(/<(?:\w+:)?rPh\b[\s\S]*?<\/(?:\w+:)?rPh>/g, '');
566
+ let s = '';
567
+ const re = /<(?:\w+:)?t\b[^>]*?(?:\/>|>([\s\S]*?)<\/(?:\w+:)?t>)/g;
568
+ let m;
569
+ while ((m = re.exec(body))) if (m[1]) s += m[1];
570
+ return decodeXml(s, true);
571
+ }
572
+
573
+ /** Excel serial number → ISO-like string. */
574
+ function serialToIso(serial, date1904) {
575
+ if (!Number.isFinite(serial)) return null;
576
+ if (!date1904 && serial >= 60 && serial < 61) return '1900-02-29';
577
+ const secs = Math.round(serial * 86400);
578
+ const days = Math.floor(secs / 86400);
579
+ const rem = secs - days * 86400;
580
+ if (days === 0 && rem > 0 && !date1904) {
581
+ return `${pad2(Math.floor(rem / 3600))}:${pad2(Math.floor(rem / 60) % 60)}:${pad2(rem % 60)}`;
582
+ }
583
+ let base;
584
+ if (date1904) base = Date.UTC(1904, 0, 1);
585
+ else base = serial < 60 ? Date.UTC(1899, 11, 31) : Date.UTC(1899, 11, 30);
586
+ const d = new Date(base + secs * 1000);
587
+ return fmtDate(d, rem !== 0);
588
+ }
589
+
590
+ /** Read a zip part as UTF-8 text (or null when missing). */
591
+ function makePartReader(buf, entries) {
592
+ const lower = new Map([...entries.keys()].map((k) => [k.toLowerCase(), k]));
593
+ return (name) => {
594
+ const e = entries.get(name) || entries.get(lower.get(name.toLowerCase()));
595
+ if (!e) return null;
596
+ const data = extractEntry(buf, e);
597
+ if (data.length > 0x1fffffe8 / 2) throw unsupported(`工作表过大(${name}),请拆分后再导入`);
598
+ let s = data.toString('utf8');
599
+ if (s.charCodeAt(0) === 0xfeff) s = s.slice(1);
600
+ return s;
601
+ };
602
+ }
603
+
604
+ /**
605
+ * Parse one worksheet XML into rows.
606
+ * @returns {{rows: any[][], truncated: boolean}}
607
+ */
608
+ function parseSheet(xml, ctx, maxRows) {
609
+ const rows = [];
610
+ let truncated = false;
611
+ const sdStart = xml.search(/<(?:\w+:)?sheetData\b/);
612
+ if (sdStart < 0) return { rows, truncated };
613
+ const rowRe = /<(?:\w+:)?row\b([^>]*?)(?:\/>|>([\s\S]*?)<\/(?:\w+:)?row>)/g;
614
+ const cellRe = /<(?:\w+:)?c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/(?:\w+:)?c>)/g;
615
+ rowRe.lastIndex = sdStart;
616
+ let firstRow = -1;
617
+ let prevRow = 0;
618
+ let lastNonEmpty = -1;
619
+ let m;
620
+ while ((m = rowRe.exec(xml))) {
621
+ const ra = /\br="(\d+)"/.exec(m[1]);
622
+ const rowNo = ra ? parseInt(ra[1], 10) : prevRow + 1;
623
+ prevRow = rowNo;
624
+ if (!m[2]) continue;
625
+ const vals = [];
626
+ let any = false;
627
+ let prevCol = -1;
628
+ let c;
629
+ cellRe.lastIndex = 0;
630
+ while ((c = cellRe.exec(m[2]))) {
631
+ const attrs = c[1];
632
+ const refM = /\br="([A-Za-z]+)\d*"/.exec(attrs);
633
+ const col = refM ? colIndex(refM[1].toUpperCase()) : prevCol + 1;
634
+ prevCol = col;
635
+ const inner = c[2];
636
+ if (!inner) continue;
637
+ const t = (/\bt="(\w+)"/.exec(attrs) || [])[1] || 'n';
638
+ let v = null;
639
+ if (t === 'inlineStr') {
640
+ const is = /<(?:\w+:)?is\b[^>]*>([\s\S]*?)<\/(?:\w+:)?is>/.exec(inner);
641
+ v = is ? collectText(is[1]) : null;
642
+ } else {
643
+ const vm = /<(?:\w+:)?v\b[^>]*>([\s\S]*?)<\/(?:\w+:)?v>/.exec(inner);
644
+ if (vm) {
645
+ const raw = vm[1];
646
+ if (t === 's') v = ctx.sst[parseInt(raw, 10)] ?? null;
647
+ else if (t === 'str') v = decodeXml(raw, true);
648
+ else if (t === 'b') v = raw.trim() === '1' || raw.trim() === 'true';
649
+ else if (t === 'e') v = null;
650
+ else if (t === 'd') v = decodeXml(raw).replace('T', ' ').replace(/(\.\d+)?Z?$/, '').replace(/ 00:00:00$/, '');
651
+ else {
652
+ const n = Number(raw);
653
+ if (raw.trim() === '' || !Number.isFinite(n)) v = null;
654
+ else {
655
+ const sm = /\bs="(\d+)"/.exec(attrs);
656
+ v = sm && ctx.dateStyles.has(parseInt(sm[1], 10)) ? serialToIso(n, ctx.date1904) : n;
657
+ }
658
+ }
659
+ }
660
+ }
661
+ if (v === null || v === undefined) continue;
662
+ vals[col] = v;
663
+ if (v !== '') any = true;
664
+ }
665
+ if (!any) continue;
666
+ for (let i = 0; i < vals.length; i++) if (vals[i] === undefined) vals[i] = null;
667
+ if (firstRow < 0) firstRow = rowNo;
668
+ const idx = rowNo - firstRow;
669
+ if (idx >= maxRows) { truncated = true; break; }
670
+ rows[idx] = vals;
671
+ lastNonEmpty = Math.max(lastNonEmpty, idx);
672
+ }
673
+ rows.length = lastNonEmpty + 1;
674
+ for (let i = 0; i < rows.length; i++) if (!rows[i]) rows[i] = [];
675
+ return { rows, truncated };
676
+ }
677
+
678
+ /**
679
+ * Read an .xlsx workbook.
680
+ * @param {Buffer|Uint8Array} buffer
681
+ * @param {{maxRows?: number}} [opts]
682
+ * @returns {{name: string, rows: any[][], truncated: boolean}[]}
683
+ */
684
+ export function readXlsx(buffer, { maxRows = 200000 } = {}) {
685
+ const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
686
+ const entries = readZipEntries(buf);
687
+ const read = makePartReader(buf, entries);
688
+
689
+ const rootRels = parseRels(read('_rels/.rels'), '');
690
+ let wbPath = 'xl/workbook.xml';
691
+ for (const r of rootRels.values()) if (/\/officeDocument$/.test(r.type)) wbPath = r.target;
692
+ const wbXml = read(wbPath);
693
+ if (!wbXml) throw unsupported('缺少 workbook.xml,可能不是 Excel 工作簿');
694
+ const wbDir = wbPath.includes('/') ? wbPath.slice(0, wbPath.lastIndexOf('/')) : '';
695
+ const relsPath = (wbDir ? wbDir + '/' : '') + '_rels/' + wbPath.slice(wbPath.lastIndexOf('/') + 1) + '.rels';
696
+ const rels = parseRels(read(relsPath), wbDir);
697
+
698
+ const wbPr = /<(?:\w+:)?workbookPr\b([^>]*)>/.exec(wbXml);
699
+ const date1904 = wbPr ? /\bdate1904="(1|true)"/.test(wbPr[1]) : false;
700
+
701
+ let sstPath = null, stylesPath = null;
702
+ for (const r of rels.values()) {
703
+ if (/\/sharedStrings$/.test(r.type)) sstPath = r.target;
704
+ else if (/\/styles$/.test(r.type)) stylesPath = r.target;
705
+ }
706
+ const sstXml = read(sstPath || (wbDir ? wbDir + '/' : '') + 'sharedStrings.xml');
707
+ const sst = [];
708
+ if (sstXml) {
709
+ const re = /<(?:\w+:)?si\b[^>]*?(?:\/>|>([\s\S]*?)<\/(?:\w+:)?si>)/g;
710
+ let m;
711
+ while ((m = re.exec(sstXml))) sst.push(m[1] ? collectText(m[1]) : '');
712
+ }
713
+
714
+ const dateStyles = new Set();
715
+ const stXml = read(stylesPath || (wbDir ? wbDir + '/' : '') + 'styles.xml');
716
+ if (stXml) {
717
+ const custom = new Map();
718
+ const nf = /<(?:\w+:)?numFmt\b([^>]*)>/g;
719
+ let m;
720
+ while ((m = nf.exec(stXml))) {
721
+ const a = parseAttrs(m[1]);
722
+ custom.set(parseInt(a.numFmtId, 10), a.formatCode || '');
723
+ }
724
+ const xfs = /<(?:\w+:)?cellXfs\b[^>]*>([\s\S]*?)<\/(?:\w+:)?cellXfs>/.exec(stXml);
725
+ if (xfs) {
726
+ const xre = /<(?:\w+:)?xf\b([^>]*)>/g;
727
+ let i = 0;
728
+ while ((m = xre.exec(xfs[1]))) {
729
+ const id = parseInt(parseAttrs(m[1]).numFmtId || '0', 10);
730
+ if (custom.has(id) ? isDateFormat(custom.get(id)) : BUILTIN_DATE_FMTS.has(id)) dateStyles.add(i);
731
+ i++;
732
+ }
733
+ }
734
+ }
735
+
736
+ const ctx = { sst, dateStyles, date1904 };
737
+ const out = [];
738
+ const sheetRe = /<(?:\w+:)?sheet\b([^>]*)>/g;
739
+ let m;
740
+ while ((m = sheetRe.exec(wbXml))) {
741
+ const a = parseAttrs(m[1]);
742
+ const ridKey = Object.keys(a).find((k) => k === 'r:id' || /:id$/.test(k));
743
+ const rel = ridKey ? rels.get(a[ridKey]) : null;
744
+ const name = a.name ?? `Sheet${out.length + 1}`;
745
+ if (!rel || (rel.type && !/\/worksheet$/.test(rel.type))) { out.push({ name, rows: [], truncated: false }); continue; }
746
+ const xml = read(rel.target);
747
+ const res = xml ? parseSheet(xml, ctx, maxRows) : { rows: [], truncated: false };
748
+ out.push({ name, rows: res.rows, truncated: res.truncated });
749
+ }
750
+ return out;
751
+ }
752
+
753
+ /* ------------------------------------------------------------------ */
754
+ /* CSV */
755
+ /* ------------------------------------------------------------------ */
756
+
757
+ /** Decode a CSV buffer, detecting BOMs, UTF-8 and GB18030. */
758
+ function decodeText(buf) {
759
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) return { text: new TextDecoder('utf-16le').decode(buf.subarray(2)), encoding: 'utf-16le' };
760
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) return { text: new TextDecoder('utf-16be').decode(buf.subarray(2)), encoding: 'utf-16be' };
761
+ const body = buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf ? buf.subarray(3) : buf;
762
+ try {
763
+ return { text: new TextDecoder('utf-8', { fatal: true }).decode(body), encoding: 'utf-8' };
764
+ } catch {
765
+ try {
766
+ return { text: new TextDecoder('gb18030').decode(body), encoding: 'gb18030' };
767
+ } catch {
768
+ return { text: body.toString('latin1'), encoding: 'latin1' };
769
+ }
770
+ }
771
+ }
772
+
773
+ /** Pick the delimiter with the most consistent per-line count in the sample. */
774
+ function sniffDelimiter(text) {
775
+ const sample = text.slice(0, 65536);
776
+ const cands = [',', '\t', ';', '|'];
777
+ const counts = cands.map(() => []);
778
+ let cur = cands.map(() => 0);
779
+ let inQ = false;
780
+ let lines = 0;
781
+ for (let i = 0; i < sample.length && lines < 20; i++) {
782
+ const ch = sample[i];
783
+ if (ch === '"') { inQ = !inQ; continue; }
784
+ if (inQ) continue;
785
+ if (ch === '\n' || ch === '\r') {
786
+ if (ch === '\r' && sample[i + 1] === '\n') i++;
787
+ cands.forEach((_, k) => counts[k].push(cur[k]));
788
+ lines++;
789
+ cur = cands.map(() => 0);
790
+ continue;
791
+ }
792
+ const k = cands.indexOf(ch);
793
+ if (k >= 0) cur[k]++;
794
+ }
795
+ if (lines < 20 && cur.some((x) => x > 0)) cands.forEach((_, k) => counts[k].push(cur[k]));
796
+ let best = ',', bestScore = -1;
797
+ cands.forEach((d, k) => {
798
+ const arr = counts[k];
799
+ if (!arr.length) return;
800
+ const freq = new Map();
801
+ for (const x of arr) if (x > 0) freq.set(x, (freq.get(x) || 0) + 1);
802
+ let mode = 0, f = 0;
803
+ for (const [x, c] of freq) if (c > f || (c === f && x > mode)) { mode = x; f = c; }
804
+ if (!mode) return;
805
+ const score = (f / arr.length) * 1000 + Math.min(mode, 999);
806
+ if (score > bestScore) { bestScore = score; best = d; }
807
+ });
808
+ return best;
809
+ }
810
+
811
+ /**
812
+ * Parse CSV/TSV bytes (RFC 4180, lenient). Values stay strings.
813
+ * @param {Buffer|Uint8Array} input
814
+ * @param {{maxRows?: number}} [opts]
815
+ * @returns {{rows: string[][], delimiter: string, encoding: string, truncated: boolean}}
816
+ */
817
+ export function parseCsv(input, { maxRows = 200000 } = {}) {
818
+ const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
819
+ let { text, encoding } = decodeText(buf);
820
+ if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
821
+ const delimiter = sniffDelimiter(text);
822
+ const rows = [];
823
+ let row = [];
824
+ let field = '';
825
+ let inQ = false;
826
+ let quotedField = false;
827
+ let truncated = false;
828
+ const n = text.length;
829
+ const endRow = () => {
830
+ row.push(field);
831
+ rows.push(row);
832
+ row = [];
833
+ field = '';
834
+ quotedField = false;
835
+ };
836
+ for (let i = 0; i < n; i++) {
837
+ const ch = text[i];
838
+ if (inQ) {
839
+ if (ch === '"') {
840
+ if (text[i + 1] === '"') { field += '"'; i++; } else inQ = false;
841
+ } else field += ch;
842
+ continue;
843
+ }
844
+ if (ch === '"' && field === '' && !quotedField) { inQ = true; quotedField = true; continue; }
845
+ if (ch === delimiter) { row.push(field); field = ''; quotedField = false; continue; }
846
+ if (ch === '\n' || ch === '\r') {
847
+ if (ch === '\r' && text[i + 1] === '\n') i++;
848
+ endRow();
849
+ if (rows.length > maxRows) { truncated = true; break; }
850
+ continue;
851
+ }
852
+ field += ch;
853
+ }
854
+ if (!truncated && (field !== '' || row.length || quotedField)) endRow();
855
+ if (rows.length > maxRows) { rows.length = maxRows; truncated = true; }
856
+ while (rows.length && rows[rows.length - 1].every((v) => v === '')) rows.pop();
857
+ return { rows, delimiter, encoding, truncated };
858
+ }
859
+
860
+ /* ------------------------------------------------------------------ */
861
+ /* Header normalization and type inference */
862
+ /* ------------------------------------------------------------------ */
863
+
864
+ /**
865
+ * Clean header names: trim, whitespace → `_`, empty → `column_N`, ≤64 chars, unique (case-insensitive).
866
+ * @param {any[]} header
867
+ * @returns {string[]}
868
+ */
869
+ export function normalizeHeader(header) {
870
+ const used = new Set();
871
+ return (header || []).map((h, i) => {
872
+ let s = stripIllegal(String(h ?? '')).trim().replace(/\s+/g, '_');
873
+ if (!s) s = `column_${i + 1}`;
874
+ s = Array.from(s).slice(0, 64).join('');
875
+ let name = s;
876
+ for (let k = 2; used.has(name.toLowerCase()); k++) {
877
+ const suf = `_${k}`;
878
+ name = Array.from(s).slice(0, 64 - suf.length).join('') + suf;
879
+ }
880
+ used.add(name.toLowerCase());
881
+ return name;
882
+ });
883
+ }
884
+
885
+ const INT_RE = /^[-+]?(?:0|[1-9]\d{0,14})$/;
886
+ const REAL_RE = /^[-+]?(?:(?:0|[1-9]\d{0,2}(?:,\d{3})+|[1-9]\d*)(?:\.\d+)?|\.\d+|0\.\d+)(?:[eE][-+]?\d+)?$/;
887
+ const DATE_RE = /^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$|^(\d{4})年(\d{1,2})月(\d{1,2})日$/;
888
+ const DATETIME_RE = /^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})(?:[T ]+|日\s*)(\d{1,2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?\s*(?:Z|[+-]\d{2}:?\d{2})?$/;
889
+
890
+ function validYmd(y, m, d) {
891
+ y = +y; m = +m; d = +d;
892
+ if (m < 1 || m > 12 || d < 1) return false;
893
+ const dim = new Date(Date.UTC(y, m, 0)).getUTCDate();
894
+ return d <= dim;
895
+ }
896
+
897
+ /** Parse a date string → `YYYY-MM-DD` or null. */
898
+ function parseDate(s) {
899
+ const m = DATE_RE.exec(s);
900
+ if (!m) return null;
901
+ const [y, mo, d] = m[1] ? [m[1], m[2], m[3]] : [m[4], m[5], m[6]];
902
+ return validYmd(y, mo, d) ? `${y}-${pad2(+mo)}-${pad2(+d)}` : null;
903
+ }
904
+
905
+ /** Parse a datetime string → `YYYY-MM-DD HH:MM:SS` or null. */
906
+ function parseDateTime(s) {
907
+ const m = DATETIME_RE.exec(s);
908
+ if (!m) return null;
909
+ const [, y, mo, d, h, mi, se] = m;
910
+ if (!validYmd(y, mo, d) || +h > 23 || +mi > 59 || (se && +se > 59)) return null;
911
+ return `${y}-${pad2(+mo)}-${pad2(+d)} ${pad2(+h)}:${mi}:${se || '00'}`;
912
+ }
913
+
914
+ /** Classify a numeric string; long digit strings (IDs) are not numeric. */
915
+ function numericClass(s) {
916
+ if (INT_RE.test(s)) return 'int';
917
+ if (!REAL_RE.test(s)) return null;
918
+ const mant = s.replace(/[eE].*$/, '');
919
+ if (mant.replace(/\D/g, '').length > 15) return null;
920
+ return 'real';
921
+ }
922
+
923
+ /**
924
+ * Infer SQL-ish column types from string/number cell values.
925
+ * @param {any[]} header
926
+ * @param {any[][]} rows
927
+ * @returns {{name: string, type: 'INTEGER'|'REAL'|'TEXT'|'DATE'|'DATETIME', kind: 'number'|'time'|'text'}[]}
928
+ */
929
+ export function inferColumns(header, rows) {
930
+ const names = normalizeHeader(header);
931
+ return names.map((name, j) => {
932
+ let seen = 0, allInt = true, allNum = true, allDate = true, allTime = true;
933
+ for (let r = 0; r < rows.length && seen < 2000; r++) {
934
+ const v = rows[r]?.[j];
935
+ if (v == null) continue;
936
+ if (typeof v === 'number') {
937
+ if (!Number.isFinite(v)) { allInt = allNum = allDate = allTime = false; }
938
+ else {
939
+ if (!(Number.isSafeInteger(v) && Math.abs(v) < 1e15)) allInt = false;
940
+ allDate = allTime = false;
941
+ }
942
+ seen++;
943
+ } else {
944
+ const s = typeof v === 'string' ? v.trim() : typeof v === 'bigint' ? v.toString() : null;
945
+ if (s === '') continue;
946
+ seen++;
947
+ if (s === null) { allInt = allNum = allDate = allTime = false; break; }
948
+ const nc = numericClass(s);
949
+ if (nc !== 'int') allInt = false;
950
+ if (!nc) allNum = false;
951
+ const isD = parseDate(s) !== null;
952
+ if (!isD) allDate = false;
953
+ if (!isD && parseDateTime(s) === null) allTime = false;
954
+ }
955
+ if (!allInt && !allNum && !allDate && !allTime) break;
956
+ }
957
+ let type = 'TEXT';
958
+ if (seen === 0) type = 'TEXT';
959
+ else if (allInt) type = 'INTEGER';
960
+ else if (allNum) type = 'REAL';
961
+ else if (allDate) type = 'DATE';
962
+ else if (allTime) type = 'DATETIME';
963
+ const kind = type === 'INTEGER' || type === 'REAL' ? 'number' : type === 'TEXT' ? 'text' : 'time';
964
+ return { name, type, kind };
965
+ });
966
+ }
967
+
968
+ /**
969
+ * Convert a raw cell value to the stored value for a column type.
970
+ * Unparseable values are returned unchanged (as strings).
971
+ * @param {any} value
972
+ * @param {'INTEGER'|'REAL'|'TEXT'|'DATE'|'DATETIME'} type
973
+ * @returns {string|number|null}
974
+ */
975
+ export function coerceValue(value, type) {
976
+ if (value == null) return null;
977
+ if (typeof value === 'string' && value.trim() === '') return null;
978
+ switch (type) {
979
+ case 'INTEGER':
980
+ case 'REAL': {
981
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
982
+ const s = String(value).trim();
983
+ if (!numericClass(s)) return s;
984
+ const n = Number(s.replace(/,/g, ''));
985
+ return Number.isFinite(n) ? n : s;
986
+ }
987
+ case 'DATE': {
988
+ const s = String(value).trim();
989
+ return parseDate(s) ?? parseDateTime(s)?.slice(0, 10) ?? s;
990
+ }
991
+ case 'DATETIME': {
992
+ const s = String(value).trim();
993
+ const dt = parseDateTime(s);
994
+ if (dt) return dt;
995
+ const d = parseDate(s);
996
+ return d ? `${d} 00:00:00` : s;
997
+ }
998
+ default:
999
+ return typeof value === 'string' ? value : toText(value);
1000
+ }
1001
+ }