xls-codec 0.0.0 → 1.0.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/dist/biff/cursor.cjs +73 -0
  4. package/dist/biff/cursor.d.cts +28 -0
  5. package/dist/biff/cursor.d.ts +28 -0
  6. package/dist/biff/cursor.js +72 -0
  7. package/dist/biff/errors.cjs +18 -0
  8. package/dist/biff/errors.d.cts +5 -0
  9. package/dist/biff/errors.d.ts +5 -0
  10. package/dist/biff/errors.js +17 -0
  11. package/dist/biff/record-types.cjs +108 -0
  12. package/dist/biff/record-types.d.cts +73 -0
  13. package/dist/biff/record-types.d.ts +73 -0
  14. package/dist/biff/record-types.js +73 -0
  15. package/dist/biff/records.cjs +40 -0
  16. package/dist/biff/records.d.cts +2 -0
  17. package/dist/biff/records.d.ts +2 -0
  18. package/dist/biff/records.js +38 -0
  19. package/dist/biff/rk.cjs +29 -0
  20. package/dist/biff/rk.d.cts +5 -0
  21. package/dist/biff/rk.d.ts +5 -0
  22. package/dist/biff/rk.js +28 -0
  23. package/dist/biff/strings.cjs +66 -0
  24. package/dist/biff/strings.d.cts +14 -0
  25. package/dist/biff/strings.d.ts +14 -0
  26. package/dist/biff/strings.js +63 -0
  27. package/dist/biff/substreams.cjs +71 -0
  28. package/dist/biff/substreams.d.cts +21 -0
  29. package/dist/biff/substreams.d.ts +21 -0
  30. package/dist/biff/substreams.js +69 -0
  31. package/dist/container.cjs +46 -0
  32. package/dist/container.d.cts +15 -0
  33. package/dist/container.d.ts +15 -0
  34. package/dist/container.js +44 -0
  35. package/dist/content.cjs +230 -0
  36. package/dist/content.d.cts +20 -0
  37. package/dist/content.d.ts +20 -0
  38. package/dist/content.js +228 -0
  39. package/dist/index.cjs +74 -0
  40. package/dist/index.d.cts +15 -0
  41. package/dist/index.d.ts +15 -0
  42. package/dist/index.js +15 -0
  43. package/dist/number-format.cjs +298 -0
  44. package/dist/number-format.d.cts +32 -0
  45. package/dist/number-format.d.ts +32 -0
  46. package/dist/number-format.js +296 -0
  47. package/dist/records-DVIqXFKk.d.cts +20 -0
  48. package/dist/records-DVIqXFKk.d.ts +20 -0
  49. package/dist/serial.cjs +62 -0
  50. package/dist/serial.d.cts +7 -0
  51. package/dist/serial.d.ts +7 -0
  52. package/dist/serial.js +59 -0
  53. package/dist/units.cjs +30 -0
  54. package/dist/units.d.cts +11 -0
  55. package/dist/units.d.ts +11 -0
  56. package/dist/units.js +28 -0
  57. package/dist/workbook/globals.cjs +106 -0
  58. package/dist/workbook/globals.d.cts +45 -0
  59. package/dist/workbook/globals.d.ts +45 -0
  60. package/dist/workbook/globals.js +104 -0
  61. package/dist/workbook/sheet.cjs +372 -0
  62. package/dist/workbook/sheet.d.cts +58 -0
  63. package/dist/workbook/sheet.d.ts +58 -0
  64. package/dist/workbook/sheet.js +371 -0
  65. package/package.json +85 -2
@@ -0,0 +1,228 @@
1
+ import "./biff/record-types.js";
2
+ import { BiffFormatError, readRecords } from "./biff/records.js";
3
+ import { readWorkbookStream } from "./container.js";
4
+ import { groupRecords, splitSubstreams } from "./biff/substreams.js";
5
+ import { classifyNumberFormat } from "./number-format.js";
6
+ import { serialToIsoDate, serialToIsoDateTime, serialToIsoTime } from "./serial.js";
7
+ import { formatCodeOf, readWorkbookGlobals } from "./workbook/globals.js";
8
+ import { readSheetRecords } from "./workbook/sheet.js";
9
+ import { PAGE_SIZE_LETTER, assembleTree } from "document-schema.js";
10
+ //#region src/content.ts
11
+ /** Which BoundSheet8 dt values name a sheet this reader maps. 0x00 is a worksheet or dialog sheet; macro sheets, chart sheets, and VBA modules carry no cell table for ContentSheet to hold. */
12
+ const SHEET_TYPE_WORKSHEET = 0;
13
+ /**
14
+ * Print settings this package emits rather than reads.
15
+ *
16
+ * ContentSheetPrintSettings makes pageSize, margins, gridlines, headers, and pageOrder REQUIRED, so a sheet cannot be produced without them, and BIFF8 spreads the real values across the Setup, LeftMargin/RightMargin/TopMargin/BottomMargin, PrintGrid, and PrintRowCol records plus a paper-size code table. None of those is read yet, so these are Excel's own documented "Normal" preset -- the same constants ooxml.js falls back to for an xlsx carrying no pageMargins element -- and they are honest defaults rather than the file's own settings. Reading the real ones is tracked as remaining scope rather than guessed at from unverified field offsets.
17
+ */
18
+ const POINTS_PER_INCH = 72;
19
+ const DEFAULT_PRINT_SETTINGS = {
20
+ pageSize: PAGE_SIZE_LETTER,
21
+ margins: {
22
+ topPt: .75 * POINTS_PER_INCH,
23
+ rightPt: .7 * POINTS_PER_INCH,
24
+ bottomPt: .75 * POINTS_PER_INCH,
25
+ leftPt: .7 * POINTS_PER_INCH
26
+ },
27
+ gridlines: false,
28
+ headers: false,
29
+ pageOrder: "downThenOver"
30
+ };
31
+ /**
32
+ * Reads a .xls file's bytes into a ContentDocument.
33
+ *
34
+ * The counterpart of ooxml.js's readXlsxContent, producing the same shape from the older format.
35
+ */
36
+ function readXlsContent(bytes) {
37
+ const substreams = splitSubstreams(groupRecords(readRecords(readWorkbookStream(bytes))));
38
+ const globalsSubstream = substreams[0];
39
+ if (globalsSubstream === void 0) throw new BiffFormatError("workbook stream holds no substreams, so it carries no globals substream");
40
+ if (globalsSubstream.records.some((rec) => rec.type === 47)) throw new BiffFormatError("workbook is encrypted (its globals substream carries a FilePass record); this reader does not decrypt");
41
+ const globals = readWorkbookGlobals(globalsSubstream.records);
42
+ return {
43
+ kind: "spreadsheet",
44
+ metadata: {},
45
+ sheets: globals.sheets.filter((entry) => entry.sheetType === SHEET_TYPE_WORKSHEET).map((entry) => readSheet(entry, substreams, globals))
46
+ };
47
+ }
48
+ /** The tree-form read: readXlsContent composed with the schema's own structural transform, exactly as ooxml.js's readXlsx wraps readXlsxContent. */
49
+ function readXls(bytes) {
50
+ return assembleTree(readXlsContent(bytes));
51
+ }
52
+ /**
53
+ * Locates a sheet's own substream and maps it.
54
+ *
55
+ * The substream is found by the byte offset BoundSheet8's lbPlyPos names, not by position: the order sheets appear in the workbook (which is BoundSheet8 order, and therefore the order of `globals.sheets`) is not required to match the order their substreams were written in. A sheet whose substream cannot be found still produces a ContentSheet, empty -- losing the sheet entirely would be a worse answer than losing its cells, since its name and position are real information the workbook did state.
56
+ */
57
+ function readSheet(entry, substreams, globals) {
58
+ const substream = substreams.find((candidate) => candidate.offset === entry.bofPosition && candidate.documentType === 16);
59
+ const raw = substream === void 0 ? {
60
+ cells: [],
61
+ rows: [],
62
+ columns: [],
63
+ merges: []
64
+ } : readSheetRecords(substream.records, globals.sharedStrings);
65
+ return {
66
+ name: entry.name,
67
+ cells: mapCells(raw, globals),
68
+ columns: mapColumns(raw),
69
+ rows: mapRows(raw),
70
+ images: [],
71
+ printSettings: DEFAULT_PRINT_SETTINGS
72
+ };
73
+ }
74
+ function mapRows(raw) {
75
+ const rows = [];
76
+ for (const row of raw.rows) {
77
+ if (row.heightPt === void 0 && !row.hidden) continue;
78
+ const entry = { index: row.index };
79
+ if (row.heightPt !== void 0) entry.heightPt = row.heightPt;
80
+ if (row.hidden) entry.hidden = true;
81
+ rows.push(entry);
82
+ }
83
+ return rows;
84
+ }
85
+ function mapColumns(raw) {
86
+ const columns = [];
87
+ for (const column of raw.columns) {
88
+ if (column.widthPt === void 0 && !column.hidden) continue;
89
+ const entry = { index: column.index };
90
+ if (column.widthPt !== void 0) entry.widthPt = column.widthPt;
91
+ if (column.hidden) entry.hidden = true;
92
+ columns.push(entry);
93
+ }
94
+ return columns;
95
+ }
96
+ /** Maps the raw cells, then stamps merged-range spans onto their anchor cells. */
97
+ function mapCells(raw, globals) {
98
+ const cells = [];
99
+ for (const cell of raw.cells) {
100
+ const mapped = mapCell(cell, globals);
101
+ if (mapped !== void 0) cells.push(mapped);
102
+ }
103
+ applyMerges(cells, raw);
104
+ return cells;
105
+ }
106
+ /**
107
+ * Maps one raw cell, or drops it.
108
+ *
109
+ * A blank cell carrying no merge is dropped: ContentSheet's cell array is documented as sparse, holding only cells with something to show, and a Blank or MulBlank record states formatting this reader does not map yet. Dropping it keeps the array honest rather than filling a sheet with thousands of empty entries -- applyMerges below re-materialises the few blanks that anchor a merged range.
110
+ */
111
+ function mapCell(cell, globals) {
112
+ if (cell.value.kind === "blank") return;
113
+ const formatCode = formatCodeOf(globals, cell.xfIndex);
114
+ const value = resolveValue(cell, formatCode, globals.date1904);
115
+ const mapped = {
116
+ row: cell.row,
117
+ column: cell.column,
118
+ value,
119
+ displayText: displayTextOf(value)
120
+ };
121
+ if (formatCode !== void 0) mapped.numberFormatCode = formatCode;
122
+ return mapped;
123
+ }
124
+ /**
125
+ * Resolves a raw value into a ContentCellValue, classifying a number through its own format code.
126
+ *
127
+ * This is where BIFF8's lack of temporal and percentage cell types is undone: every date, time, percentage, and currency amount is stored as a bare number, and only the format its XF points at says which. A format naming a date the calendar does not have (the 1900 system's phantom leap day, or a negative serial) degrades to the plain number rather than emitting an invalid ISO string.
128
+ */
129
+ function resolveValue(cell, formatCode, date1904) {
130
+ if (cell.value.kind === "blank") return { kind: "empty" };
131
+ if (cell.value.kind !== "number") return cell.value;
132
+ const num = cell.value.value;
133
+ if (formatCode === void 0) return {
134
+ kind: "number",
135
+ value: num
136
+ };
137
+ const format = classifyNumberFormat(formatCode);
138
+ switch (format.kind) {
139
+ case "percentage": return {
140
+ kind: "percentage",
141
+ value: num
142
+ };
143
+ case "currency": return format.code === void 0 ? {
144
+ kind: "currency",
145
+ value: num
146
+ } : {
147
+ kind: "currency",
148
+ value: num,
149
+ currency: format.code
150
+ };
151
+ case "date": {
152
+ const iso = serialToIsoDate(num, date1904);
153
+ return iso === void 0 ? {
154
+ kind: "number",
155
+ value: num
156
+ } : {
157
+ kind: "date",
158
+ value: iso
159
+ };
160
+ }
161
+ case "time": {
162
+ const iso = serialToIsoTime(num);
163
+ return iso === void 0 ? {
164
+ kind: "number",
165
+ value: num
166
+ } : {
167
+ kind: "time",
168
+ value: iso
169
+ };
170
+ }
171
+ case "dateTime": {
172
+ const iso = serialToIsoDateTime(num, date1904);
173
+ return iso === void 0 ? {
174
+ kind: "number",
175
+ value: num
176
+ } : {
177
+ kind: "dateTime",
178
+ value: iso
179
+ };
180
+ }
181
+ default: return {
182
+ kind: "number",
183
+ value: num
184
+ };
185
+ }
186
+ }
187
+ /** The typed value's own spelling, matching ooxml.js's derivation exactly so the same cell reads identically from either format. Deliberately not the producer's rendered string: this package classifies number formats but does not render through them. */
188
+ function displayTextOf(value) {
189
+ switch (value.kind) {
190
+ case "number":
191
+ case "percentage":
192
+ case "currency": return String(value.value);
193
+ case "boolean": return value.value ? "TRUE" : "FALSE";
194
+ case "date":
195
+ case "time":
196
+ case "dateTime":
197
+ case "string":
198
+ case "error": return value.value;
199
+ case "empty": return "";
200
+ default: return "";
201
+ }
202
+ }
203
+ /**
204
+ * Stamps each merged range's span onto its anchor cell, materialising an empty anchor when the range's top-left cell had no value of its own.
205
+ *
206
+ * ContentSheetCell documents colSpan/rowSpan as belonging to the anchor cell alone, and only when greater than one. A merged range whose anchor is blank is common -- merging cells in Excel keeps only the top-left value, and a range merged over an empty cell has no value anywhere -- so the anchor is created here rather than left absent, which would lose the merge entirely.
207
+ */
208
+ function applyMerges(cells, raw) {
209
+ for (const range of raw.merges) {
210
+ const rowSpan = range.endRow - range.startRow + 1;
211
+ const colSpan = range.endColumn - range.startColumn + 1;
212
+ if (rowSpan <= 1 && colSpan <= 1) continue;
213
+ let anchor = cells.find((cell) => cell.row === range.startRow && cell.column === range.startColumn);
214
+ if (anchor === void 0) {
215
+ anchor = {
216
+ row: range.startRow,
217
+ column: range.startColumn,
218
+ value: { kind: "empty" },
219
+ displayText: ""
220
+ };
221
+ cells.push(anchor);
222
+ }
223
+ if (colSpan > 1) anchor.colSpan = colSpan;
224
+ if (rowSpan > 1) anchor.rowSpan = rowSpan;
225
+ }
226
+ }
227
+ //#endregion
228
+ export { readXls, readXlsContent };
package/dist/index.cjs ADDED
@@ -0,0 +1,74 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_biff_record_types = require("./biff/record-types.cjs");
3
+ const require_biff_records = require("./biff/records.cjs");
4
+ const require_container = require("./container.cjs");
5
+ const require_biff_substreams = require("./biff/substreams.cjs");
6
+ const require_number_format = require("./number-format.cjs");
7
+ const require_serial = require("./serial.cjs");
8
+ const require_biff_cursor = require("./biff/cursor.cjs");
9
+ const require_biff_strings = require("./biff/strings.cjs");
10
+ const require_workbook_globals = require("./workbook/globals.cjs");
11
+ const require_biff_errors = require("./biff/errors.cjs");
12
+ const require_biff_rk = require("./biff/rk.cjs");
13
+ const require_units = require("./units.cjs");
14
+ const require_workbook_sheet = require("./workbook/sheet.cjs");
15
+ const require_content = require("./content.cjs");
16
+ exports.BIFF8_VERSION = require_biff_record_types.BIFF8_VERSION;
17
+ exports.BOF_TYPE_CHART = require_biff_record_types.BOF_TYPE_CHART;
18
+ exports.BOF_TYPE_MACRO = require_biff_record_types.BOF_TYPE_MACRO;
19
+ exports.BOF_TYPE_WORKBOOK = require_biff_record_types.BOF_TYPE_WORKBOOK;
20
+ exports.BOF_TYPE_WORKSHEET = require_biff_record_types.BOF_TYPE_WORKSHEET;
21
+ exports.BUILTIN_NUMBER_FORMATS = require_number_format.BUILTIN_NUMBER_FORMATS;
22
+ exports.BiffFormatError = require_biff_records.BiffFormatError;
23
+ exports.BlockCursor = require_biff_cursor.BlockCursor;
24
+ exports.MAX_RECORD_DATA_SIZE = require_biff_record_types.MAX_RECORD_DATA_SIZE;
25
+ exports.RECORD_ARRAY = require_biff_record_types.RECORD_ARRAY;
26
+ exports.RECORD_BLANK = require_biff_record_types.RECORD_BLANK;
27
+ exports.RECORD_BOF = require_biff_record_types.RECORD_BOF;
28
+ exports.RECORD_BOOLERR = require_biff_record_types.RECORD_BOOLERR;
29
+ exports.RECORD_BOUNDSHEET8 = require_biff_record_types.RECORD_BOUNDSHEET8;
30
+ exports.RECORD_COLINFO = require_biff_record_types.RECORD_COLINFO;
31
+ exports.RECORD_CONTINUE = require_biff_record_types.RECORD_CONTINUE;
32
+ exports.RECORD_DATE1904 = require_biff_record_types.RECORD_DATE1904;
33
+ exports.RECORD_DEFAULTROWHEIGHT = require_biff_record_types.RECORD_DEFAULTROWHEIGHT;
34
+ exports.RECORD_DEFCOLWIDTH = require_biff_record_types.RECORD_DEFCOLWIDTH;
35
+ exports.RECORD_DIMENSIONS = require_biff_record_types.RECORD_DIMENSIONS;
36
+ exports.RECORD_EOF = require_biff_record_types.RECORD_EOF;
37
+ exports.RECORD_FILEPASS = require_biff_record_types.RECORD_FILEPASS;
38
+ exports.RECORD_FONT = require_biff_record_types.RECORD_FONT;
39
+ exports.RECORD_FORMAT = require_biff_record_types.RECORD_FORMAT;
40
+ exports.RECORD_FORMULA = require_biff_record_types.RECORD_FORMULA;
41
+ exports.RECORD_LABEL = require_biff_record_types.RECORD_LABEL;
42
+ exports.RECORD_LABELSST = require_biff_record_types.RECORD_LABELSST;
43
+ exports.RECORD_MERGECELLS = require_biff_record_types.RECORD_MERGECELLS;
44
+ exports.RECORD_MULBLANK = require_biff_record_types.RECORD_MULBLANK;
45
+ exports.RECORD_MULRK = require_biff_record_types.RECORD_MULRK;
46
+ exports.RECORD_NUMBER = require_biff_record_types.RECORD_NUMBER;
47
+ exports.RECORD_RK = require_biff_record_types.RECORD_RK;
48
+ exports.RECORD_ROW = require_biff_record_types.RECORD_ROW;
49
+ exports.RECORD_SHRFMLA = require_biff_record_types.RECORD_SHRFMLA;
50
+ exports.RECORD_SST = require_biff_record_types.RECORD_SST;
51
+ exports.RECORD_STRING = require_biff_record_types.RECORD_STRING;
52
+ exports.RECORD_TABLE = require_biff_record_types.RECORD_TABLE;
53
+ exports.RECORD_XF = require_biff_record_types.RECORD_XF;
54
+ exports.classifyNumberFormat = require_number_format.classifyNumberFormat;
55
+ exports.columnWidthToPoints = require_units.columnWidthToPoints;
56
+ exports.decodeRkNumber = require_biff_rk.decodeRkNumber;
57
+ exports.errorTextOf = require_biff_errors.errorTextOf;
58
+ exports.formatCodeOf = require_workbook_globals.formatCodeOf;
59
+ exports.groupRecords = require_biff_substreams.groupRecords;
60
+ exports.isXlsFile = require_container.isXlsFile;
61
+ exports.readRecords = require_biff_records.readRecords;
62
+ exports.readRichExtendedString = require_biff_strings.readRichExtendedString;
63
+ exports.readSheetRecords = require_workbook_sheet.readSheetRecords;
64
+ exports.readShortXLUnicodeString = require_biff_strings.readShortXLUnicodeString;
65
+ exports.readWorkbookGlobals = require_workbook_globals.readWorkbookGlobals;
66
+ exports.readWorkbookStream = require_container.readWorkbookStream;
67
+ exports.readXLUnicodeString = require_biff_strings.readXLUnicodeString;
68
+ exports.readXls = require_content.readXls;
69
+ exports.readXlsContent = require_content.readXlsContent;
70
+ exports.serialToIsoDate = require_serial.serialToIsoDate;
71
+ exports.serialToIsoDateTime = require_serial.serialToIsoDateTime;
72
+ exports.serialToIsoTime = require_serial.serialToIsoTime;
73
+ exports.splitSubstreams = require_biff_substreams.splitSubstreams;
74
+ exports.twipsToPoints = require_units.twipsToPoints;
@@ -0,0 +1,15 @@
1
+ import { BlockCursor } from "./biff/cursor.cjs";
2
+ import { errorTextOf } from "./biff/errors.cjs";
3
+ import { BIFF8_VERSION, BOF_TYPE_CHART, BOF_TYPE_MACRO, BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, MAX_RECORD_DATA_SIZE, RECORD_ARRAY, RECORD_BLANK, RECORD_BOF, RECORD_BOOLERR, RECORD_BOUNDSHEET8, RECORD_COLINFO, RECORD_CONTINUE, RECORD_DATE1904, RECORD_DEFAULTROWHEIGHT, RECORD_DEFCOLWIDTH, RECORD_DIMENSIONS, RECORD_EOF, RECORD_FILEPASS, RECORD_FONT, RECORD_FORMAT, RECORD_FORMULA, RECORD_LABEL, RECORD_LABELSST, RECORD_MERGECELLS, RECORD_MULBLANK, RECORD_MULRK, RECORD_NUMBER, RECORD_RK, RECORD_ROW, RECORD_SHRFMLA, RECORD_SST, RECORD_STRING, RECORD_TABLE, RECORD_XF } from "./biff/record-types.cjs";
4
+ import { n as BiffRecord, r as readRecords, t as BiffFormatError } from "./records-DVIqXFKk.cjs";
5
+ import { decodeRkNumber } from "./biff/rk.cjs";
6
+ import { readRichExtendedString, readShortXLUnicodeString, readXLUnicodeString } from "./biff/strings.cjs";
7
+ import { RecordGroup, Substream, groupRecords, splitSubstreams } from "./biff/substreams.cjs";
8
+ import { isXlsFile, readWorkbookStream } from "./container.cjs";
9
+ import { XlsContentDocument, readXls, readXlsContent } from "./content.cjs";
10
+ import { BUILTIN_NUMBER_FORMATS, NumberFormatClass, classifyNumberFormat } from "./number-format.cjs";
11
+ import { serialToIsoDate, serialToIsoDateTime, serialToIsoTime } from "./serial.cjs";
12
+ import { columnWidthToPoints, twipsToPoints } from "./units.cjs";
13
+ import { CellFormat, SheetEntry, WorkbookGlobals, formatCodeOf, readWorkbookGlobals } from "./workbook/globals.cjs";
14
+ import { RawCell, RawCellValue, RawColumn, RawRange, RawRow, RawSheet, readSheetRecords } from "./workbook/sheet.cjs";
15
+ export { BIFF8_VERSION, BOF_TYPE_CHART, BOF_TYPE_MACRO, BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, BUILTIN_NUMBER_FORMATS, BiffFormatError, BiffRecord, BlockCursor, CellFormat, MAX_RECORD_DATA_SIZE, NumberFormatClass, RECORD_ARRAY, RECORD_BLANK, RECORD_BOF, RECORD_BOOLERR, RECORD_BOUNDSHEET8, RECORD_COLINFO, RECORD_CONTINUE, RECORD_DATE1904, RECORD_DEFAULTROWHEIGHT, RECORD_DEFCOLWIDTH, RECORD_DIMENSIONS, RECORD_EOF, RECORD_FILEPASS, RECORD_FONT, RECORD_FORMAT, RECORD_FORMULA, RECORD_LABEL, RECORD_LABELSST, RECORD_MERGECELLS, RECORD_MULBLANK, RECORD_MULRK, RECORD_NUMBER, RECORD_RK, RECORD_ROW, RECORD_SHRFMLA, RECORD_SST, RECORD_STRING, RECORD_TABLE, RECORD_XF, RawCell, RawCellValue, RawColumn, RawRange, RawRow, RawSheet, RecordGroup, SheetEntry, Substream, WorkbookGlobals, XlsContentDocument, classifyNumberFormat, columnWidthToPoints, decodeRkNumber, errorTextOf, formatCodeOf, groupRecords, isXlsFile, readRecords, readRichExtendedString, readSheetRecords, readShortXLUnicodeString, readWorkbookGlobals, readWorkbookStream, readXLUnicodeString, readXls, readXlsContent, serialToIsoDate, serialToIsoDateTime, serialToIsoTime, splitSubstreams, twipsToPoints };
@@ -0,0 +1,15 @@
1
+ import { BlockCursor } from "./biff/cursor.js";
2
+ import { errorTextOf } from "./biff/errors.js";
3
+ import { BIFF8_VERSION, BOF_TYPE_CHART, BOF_TYPE_MACRO, BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, MAX_RECORD_DATA_SIZE, RECORD_ARRAY, RECORD_BLANK, RECORD_BOF, RECORD_BOOLERR, RECORD_BOUNDSHEET8, RECORD_COLINFO, RECORD_CONTINUE, RECORD_DATE1904, RECORD_DEFAULTROWHEIGHT, RECORD_DEFCOLWIDTH, RECORD_DIMENSIONS, RECORD_EOF, RECORD_FILEPASS, RECORD_FONT, RECORD_FORMAT, RECORD_FORMULA, RECORD_LABEL, RECORD_LABELSST, RECORD_MERGECELLS, RECORD_MULBLANK, RECORD_MULRK, RECORD_NUMBER, RECORD_RK, RECORD_ROW, RECORD_SHRFMLA, RECORD_SST, RECORD_STRING, RECORD_TABLE, RECORD_XF } from "./biff/record-types.js";
4
+ import { n as BiffRecord, r as readRecords, t as BiffFormatError } from "./records-DVIqXFKk.js";
5
+ import { decodeRkNumber } from "./biff/rk.js";
6
+ import { readRichExtendedString, readShortXLUnicodeString, readXLUnicodeString } from "./biff/strings.js";
7
+ import { RecordGroup, Substream, groupRecords, splitSubstreams } from "./biff/substreams.js";
8
+ import { isXlsFile, readWorkbookStream } from "./container.js";
9
+ import { XlsContentDocument, readXls, readXlsContent } from "./content.js";
10
+ import { BUILTIN_NUMBER_FORMATS, NumberFormatClass, classifyNumberFormat } from "./number-format.js";
11
+ import { serialToIsoDate, serialToIsoDateTime, serialToIsoTime } from "./serial.js";
12
+ import { columnWidthToPoints, twipsToPoints } from "./units.js";
13
+ import { CellFormat, SheetEntry, WorkbookGlobals, formatCodeOf, readWorkbookGlobals } from "./workbook/globals.js";
14
+ import { RawCell, RawCellValue, RawColumn, RawRange, RawRow, RawSheet, readSheetRecords } from "./workbook/sheet.js";
15
+ export { BIFF8_VERSION, BOF_TYPE_CHART, BOF_TYPE_MACRO, BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, BUILTIN_NUMBER_FORMATS, BiffFormatError, BiffRecord, BlockCursor, CellFormat, MAX_RECORD_DATA_SIZE, NumberFormatClass, RECORD_ARRAY, RECORD_BLANK, RECORD_BOF, RECORD_BOOLERR, RECORD_BOUNDSHEET8, RECORD_COLINFO, RECORD_CONTINUE, RECORD_DATE1904, RECORD_DEFAULTROWHEIGHT, RECORD_DEFCOLWIDTH, RECORD_DIMENSIONS, RECORD_EOF, RECORD_FILEPASS, RECORD_FONT, RECORD_FORMAT, RECORD_FORMULA, RECORD_LABEL, RECORD_LABELSST, RECORD_MERGECELLS, RECORD_MULBLANK, RECORD_MULRK, RECORD_NUMBER, RECORD_RK, RECORD_ROW, RECORD_SHRFMLA, RECORD_SST, RECORD_STRING, RECORD_TABLE, RECORD_XF, RawCell, RawCellValue, RawColumn, RawRange, RawRow, RawSheet, RecordGroup, SheetEntry, Substream, WorkbookGlobals, XlsContentDocument, classifyNumberFormat, columnWidthToPoints, decodeRkNumber, errorTextOf, formatCodeOf, groupRecords, isXlsFile, readRecords, readRichExtendedString, readSheetRecords, readShortXLUnicodeString, readWorkbookGlobals, readWorkbookStream, readXLUnicodeString, readXls, readXlsContent, serialToIsoDate, serialToIsoDateTime, serialToIsoTime, splitSubstreams, twipsToPoints };
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import { BIFF8_VERSION, BOF_TYPE_CHART, BOF_TYPE_MACRO, BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, MAX_RECORD_DATA_SIZE, RECORD_ARRAY, RECORD_BLANK, RECORD_BOF, RECORD_BOOLERR, RECORD_BOUNDSHEET8, RECORD_COLINFO, RECORD_CONTINUE, RECORD_DATE1904, RECORD_DEFAULTROWHEIGHT, RECORD_DEFCOLWIDTH, RECORD_DIMENSIONS, RECORD_EOF, RECORD_FILEPASS, RECORD_FONT, RECORD_FORMAT, RECORD_FORMULA, RECORD_LABEL, RECORD_LABELSST, RECORD_MERGECELLS, RECORD_MULBLANK, RECORD_MULRK, RECORD_NUMBER, RECORD_RK, RECORD_ROW, RECORD_SHRFMLA, RECORD_SST, RECORD_STRING, RECORD_TABLE, RECORD_XF } from "./biff/record-types.js";
2
+ import { BiffFormatError, readRecords } from "./biff/records.js";
3
+ import { isXlsFile, readWorkbookStream } from "./container.js";
4
+ import { groupRecords, splitSubstreams } from "./biff/substreams.js";
5
+ import { BUILTIN_NUMBER_FORMATS, classifyNumberFormat } from "./number-format.js";
6
+ import { serialToIsoDate, serialToIsoDateTime, serialToIsoTime } from "./serial.js";
7
+ import { BlockCursor } from "./biff/cursor.js";
8
+ import { readRichExtendedString, readShortXLUnicodeString, readXLUnicodeString } from "./biff/strings.js";
9
+ import { formatCodeOf, readWorkbookGlobals } from "./workbook/globals.js";
10
+ import { errorTextOf } from "./biff/errors.js";
11
+ import { decodeRkNumber } from "./biff/rk.js";
12
+ import { columnWidthToPoints, twipsToPoints } from "./units.js";
13
+ import { readSheetRecords } from "./workbook/sheet.js";
14
+ import { readXls, readXlsContent } from "./content.js";
15
+ export { BIFF8_VERSION, BOF_TYPE_CHART, BOF_TYPE_MACRO, BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, BUILTIN_NUMBER_FORMATS, BiffFormatError, BlockCursor, MAX_RECORD_DATA_SIZE, RECORD_ARRAY, RECORD_BLANK, RECORD_BOF, RECORD_BOOLERR, RECORD_BOUNDSHEET8, RECORD_COLINFO, RECORD_CONTINUE, RECORD_DATE1904, RECORD_DEFAULTROWHEIGHT, RECORD_DEFCOLWIDTH, RECORD_DIMENSIONS, RECORD_EOF, RECORD_FILEPASS, RECORD_FONT, RECORD_FORMAT, RECORD_FORMULA, RECORD_LABEL, RECORD_LABELSST, RECORD_MERGECELLS, RECORD_MULBLANK, RECORD_MULRK, RECORD_NUMBER, RECORD_RK, RECORD_ROW, RECORD_SHRFMLA, RECORD_SST, RECORD_STRING, RECORD_TABLE, RECORD_XF, classifyNumberFormat, columnWidthToPoints, decodeRkNumber, errorTextOf, formatCodeOf, groupRecords, isXlsFile, readRecords, readRichExtendedString, readSheetRecords, readShortXLUnicodeString, readWorkbookGlobals, readWorkbookStream, readXLUnicodeString, readXls, readXlsContent, serialToIsoDate, serialToIsoDateTime, serialToIsoTime, splitSubstreams, twipsToPoints };
@@ -0,0 +1,298 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/number-format.ts
3
+ /** Excel honours at most four sections (positive; negative; zero; text); a fifth is malformed and is dropped rather than guessed at. */
4
+ const MAX_SECTIONS = 4;
5
+ /** Mirrors String.prototype.charAt's past-the-end contract, but over a CODE POINT array, so a rare astral currency symbol stays one token instead of splitting into two lone surrogates. */
6
+ function at(chars, index) {
7
+ return chars[index] ?? "";
8
+ }
9
+ function tokenize(formatCode) {
10
+ const chars = [...formatCode];
11
+ const tokens = [];
12
+ let index = 0;
13
+ while (index < chars.length) {
14
+ const char = at(chars, index);
15
+ if (char === "\"") {
16
+ let text = "";
17
+ index += 1;
18
+ while (index < chars.length && at(chars, index) !== "\"") {
19
+ text += at(chars, index);
20
+ index += 1;
21
+ }
22
+ index += 1;
23
+ tokens.push({
24
+ kind: "literal",
25
+ text
26
+ });
27
+ continue;
28
+ }
29
+ if (char === "\\" || char === "_" || char === "*") {
30
+ tokens.push({
31
+ kind: "literal",
32
+ text: at(chars, index + 1)
33
+ });
34
+ index += 2;
35
+ continue;
36
+ }
37
+ if (char === "[") {
38
+ let body = "";
39
+ index += 1;
40
+ while (index < chars.length && at(chars, index) !== "]") {
41
+ body += at(chars, index);
42
+ index += 1;
43
+ }
44
+ index += 1;
45
+ tokens.push({
46
+ kind: "bracket",
47
+ body
48
+ });
49
+ continue;
50
+ }
51
+ if (char === ";") {
52
+ tokens.push({ kind: "separator" });
53
+ index += 1;
54
+ continue;
55
+ }
56
+ tokens.push({
57
+ kind: "code",
58
+ char
59
+ });
60
+ index += 1;
61
+ }
62
+ return tokens;
63
+ }
64
+ /** Splits on separator tokens only: a ';' inside a quote or bracket was already consumed as part of that token, so it can never split a section here. */
65
+ function splitSections(tokens) {
66
+ const sections = [];
67
+ let current = [];
68
+ for (const token of tokens) {
69
+ if (token.kind === "separator") {
70
+ sections.push(current);
71
+ current = [];
72
+ continue;
73
+ }
74
+ current.push(token);
75
+ }
76
+ sections.push(current);
77
+ return sections.slice(0, MAX_SECTIONS);
78
+ }
79
+ /** The Unicode Currency_Symbol category IS the definition of "this character means money", so it is tested directly rather than against a hand-listed subset that would omit whichever symbol a real file happens to use. */
80
+ const CURRENCY_SYMBOL = /\p{Sc}/u;
81
+ /** `[$GBP-809]` carries an ISO 4217 code; `[$£-809]` carries a display symbol instead. Only the three-ASCII-letter shape counts as a code, because ContentCellValue's `currency` field is documented as the ISO code and there is no faithful symbol-to-code mapping ('$' alone is USD, CAD, AUD and a dozen others). */
82
+ function isIsoCurrencyCodeShape(marker) {
83
+ if (marker.length !== 3) return false;
84
+ for (const char of marker) {
85
+ const upper = char.toUpperCase();
86
+ if (upper < "A" || upper > "Z") return false;
87
+ }
88
+ return true;
89
+ }
90
+ /** An elapsed-time bucket is a bracket holding one repeated h/m/s and nothing else -- the marker that the value is a DURATION, which may legitimately exceed 24 hours, rather than a time of day. */
91
+ function isElapsedBracketBody(body) {
92
+ let letter;
93
+ for (const char of body) {
94
+ const lower = char.toLowerCase();
95
+ if (letter === void 0) {
96
+ if (lower !== "h" && lower !== "m" && lower !== "s") return false;
97
+ letter = lower;
98
+ } else if (lower !== letter) return false;
99
+ }
100
+ return letter !== void 0;
101
+ }
102
+ function classifyBracket(body) {
103
+ if (body.startsWith("$")) {
104
+ const rest = body.slice(1);
105
+ const dashIndex = rest.indexOf("-");
106
+ const marker = dashIndex === -1 ? rest : rest.slice(0, dashIndex);
107
+ if (marker === "") return { kind: "none" };
108
+ return isIsoCurrencyCodeShape(marker) ? {
109
+ kind: "currency",
110
+ code: marker.toUpperCase()
111
+ } : { kind: "currency" };
112
+ }
113
+ return isElapsedBracketBody(body) ? { kind: "elapsed" } : { kind: "none" };
114
+ }
115
+ const AMPM_MARKERS = ["am/pm", "a/p"];
116
+ const AMPM_LETTER = "ampm";
117
+ function matchesAt(chars, index, marker) {
118
+ return [...marker].every((char, offset) => at(chars, index + offset).toLowerCase() === char);
119
+ }
120
+ function codeRunsOf(section) {
121
+ const chars = [];
122
+ for (const token of section) if (token.kind === "code") chars.push(token.char);
123
+ const runs = [];
124
+ let index = 0;
125
+ while (index < chars.length) {
126
+ const marker = AMPM_MARKERS.find((candidate) => matchesAt(chars, index, candidate));
127
+ if (marker !== void 0) {
128
+ runs.push({
129
+ letter: AMPM_LETTER,
130
+ length: marker.length
131
+ });
132
+ index += marker.length;
133
+ continue;
134
+ }
135
+ const char = at(chars, index).toLowerCase();
136
+ let length = 0;
137
+ while (index + length < chars.length && at(chars, index + length).toLowerCase() === char) length += 1;
138
+ runs.push({
139
+ letter: char,
140
+ length
141
+ });
142
+ index += length;
143
+ }
144
+ return runs;
145
+ }
146
+ /** The letters an ambiguous 'm' looks past its neighbours for. 'm' itself is excluded: an unresolved 'm' carries no information for resolving another, so `hh:mm:mm` resolves both against the 'hh'. */
147
+ const RESOLVING_LETTERS = [
148
+ "y",
149
+ "d",
150
+ "h",
151
+ "s"
152
+ ];
153
+ function nearestResolvingLetter(runs, from, step) {
154
+ for (let index = from + step; index >= 0 && index < runs.length; index += step) {
155
+ const run = runs[index];
156
+ if (run !== void 0 && RESOLVING_LETTERS.includes(run.letter)) return run.letter;
157
+ }
158
+ }
159
+ /** Excel's minutes-vs-months rule: 'm'/'mm' is minutes when the nearest preceding date/time code is an hour or the nearest following one is a second, and a month otherwise. 'mmm' and longer are always month names. This is what makes `yyyy-mm-dd hh:mm:ss` resolve its two identical 'mm' runs oppositely. */
160
+ function monthRunIsMinutes(runs, index) {
161
+ return nearestResolvingLetter(runs, index, -1) === "h" || nearestResolvingLetter(runs, index, 1) === "s";
162
+ }
163
+ const PLAIN_NUMBER = { kind: "number" };
164
+ /** Digit placeholders ('0' required, '#' suppressed, '?' space-padded), the decimal and thousands separators. Scientific notation's 'e' is handled at its own run, since a bare 'e' also occurs inside the literal word "General". */
165
+ const NUMERIC_CODES = [
166
+ "0",
167
+ "#",
168
+ "?",
169
+ ".",
170
+ ","
171
+ ];
172
+ function collectSignals(section) {
173
+ const signals = {
174
+ hasDate: false,
175
+ hasTime: false,
176
+ hasElapsed: false,
177
+ hasPercent: false,
178
+ hasNumeric: false,
179
+ hasText: false,
180
+ hasCurrency: false
181
+ };
182
+ for (const token of section) {
183
+ if (token.kind === "literal" && CURRENCY_SYMBOL.test(token.text)) signals.hasCurrency = true;
184
+ if (token.kind === "bracket") {
185
+ const meaning = classifyBracket(token.body);
186
+ if (meaning.kind === "elapsed") signals.hasElapsed = true;
187
+ if (meaning.kind === "currency") {
188
+ signals.hasCurrency = true;
189
+ if (signals.currencyCode === void 0 && meaning.code !== void 0) signals.currencyCode = meaning.code;
190
+ }
191
+ }
192
+ }
193
+ const runs = codeRunsOf(section);
194
+ runs.forEach((run, index) => {
195
+ if (run.letter === "y" || run.letter === "d") {
196
+ signals.hasDate = true;
197
+ return;
198
+ }
199
+ if (run.letter === "h" || run.letter === "s" || run.letter === AMPM_LETTER) {
200
+ signals.hasTime = true;
201
+ return;
202
+ }
203
+ if (run.letter === "m") {
204
+ if (run.length <= 2 && monthRunIsMinutes(runs, index)) signals.hasTime = true;
205
+ else signals.hasDate = true;
206
+ return;
207
+ }
208
+ if (run.letter === "e") {
209
+ const next = runs[index + 1];
210
+ signals.hasNumeric = signals.hasNumeric || next?.letter === "+" || next?.letter === "-";
211
+ return;
212
+ }
213
+ if (run.letter === "%") {
214
+ signals.hasPercent = true;
215
+ return;
216
+ }
217
+ if (run.letter === "@") {
218
+ signals.hasText = true;
219
+ return;
220
+ }
221
+ if (NUMERIC_CODES.includes(run.letter)) {
222
+ signals.hasNumeric = true;
223
+ return;
224
+ }
225
+ if (CURRENCY_SYMBOL.test(run.letter)) signals.hasCurrency = true;
226
+ });
227
+ return signals;
228
+ }
229
+ /** Precedence when a format carries several signals at once, most specific first: an elapsed-time bracket beats everything (the only marker separating a duration from a time of day); any date code beats any time code (a format with both is a genuine combined date-and-time); a percent sign beats a currency marker (`[$GBP-809]0.00%` is still a percentage); and a text placeholder only wins when the section has no numeric placeholder to be a number with. */
230
+ function classifySection(section) {
231
+ const signals = collectSignals(section);
232
+ if (signals.hasElapsed) return { kind: "elapsedTime" };
233
+ if (signals.hasDate) return signals.hasTime ? { kind: "dateTime" } : { kind: "date" };
234
+ if (signals.hasTime) return { kind: "time" };
235
+ if (signals.hasPercent) return { kind: "percentage" };
236
+ if (signals.hasCurrency) {
237
+ const code = signals.currencyCode;
238
+ return code === void 0 ? { kind: "currency" } : {
239
+ kind: "currency",
240
+ code
241
+ };
242
+ }
243
+ if (signals.hasText && !signals.hasNumeric) return { kind: "text" };
244
+ return PLAIN_NUMBER;
245
+ }
246
+ /** Classifies a format code, reading the FIRST section only. Sections two through four are the negative/zero/text renderings of the same underlying value: they differ in colour, parentheses, and literal text, never in what kind of thing the cell holds, and a cell whose value happens to be negative must not classify differently from the identical cell holding a positive one. */
247
+ function classifyNumberFormat(formatCode) {
248
+ const first = splitSections(tokenize(formatCode))[0];
249
+ return first === void 0 ? PLAIN_NUMBER : classifySection(first);
250
+ }
251
+ /**
252
+ * The built-in format codes, which a file never writes into its own Format records and every reader is expected to know.
253
+ *
254
+ * [MS-XLS] 2.4.126 constrains a Format record's own ifmt to 5-8, 23-26, 41-44, 63-66, and 164-382, so an XF pointing at any other identifier resolves through this table instead. The codes are ECMA-376 Part 1 SS18.8.30's table, which BIFF8 and xlsx share.
255
+ *
256
+ * Ids 23-36 are deliberately absent: that table leaves them reserved, and inventing codes for them would fabricate a mapping no specification defines -- an XF pointing at one resolves to no code at all, which the caller reports as absent rather than silently substituting General. These strings are fed through the SAME classifyNumberFormat as a producer-declared code, never a second table of pre-decided kinds, so the two feeds cannot drift apart.
257
+ */
258
+ const BUILTIN_NUMBER_FORMATS = /* @__PURE__ */ new Map([
259
+ [0, "General"],
260
+ [1, "0"],
261
+ [2, "0.00"],
262
+ [3, "#,##0"],
263
+ [4, "#,##0.00"],
264
+ [5, "$#,##0_);($#,##0)"],
265
+ [6, "$#,##0_);[Red]($#,##0)"],
266
+ [7, "$#,##0.00_);($#,##0.00)"],
267
+ [8, "$#,##0.00_);[Red]($#,##0.00)"],
268
+ [9, "0%"],
269
+ [10, "0.00%"],
270
+ [11, "0.00E+00"],
271
+ [12, "# ?/?"],
272
+ [13, "# ??/??"],
273
+ [14, "mm-dd-yy"],
274
+ [15, "d-mmm-yy"],
275
+ [16, "d-mmm"],
276
+ [17, "mmm-yy"],
277
+ [18, "h:mm AM/PM"],
278
+ [19, "h:mm:ss AM/PM"],
279
+ [20, "h:mm"],
280
+ [21, "h:mm:ss"],
281
+ [22, "m/d/yy h:mm"],
282
+ [37, "#,##0 ;(#,##0)"],
283
+ [38, "#,##0 ;[Red](#,##0)"],
284
+ [39, "#,##0.00;(#,##0.00)"],
285
+ [40, "#,##0.00;[Red](#,##0.00)"],
286
+ [41, "_(* #,##0_);_(* \\(#,##0\\);_(* \"-\"_);_(@_)"],
287
+ [42, "_(\"$\"* #,##0_);_(\"$\"* \\(#,##0\\);_(\"$\"* \"-\"_);_(@_)"],
288
+ [43, "_(* #,##0.00_);_(* \\(#,##0.00\\);_(* \"-\"??_);_(@_)"],
289
+ [44, "_(\"$\"* #,##0.00_);_(\"$\"* \\(#,##0.00\\);_(\"$\"* \"-\"??_);_(@_)"],
290
+ [45, "mm:ss"],
291
+ [46, "[h]:mm:ss"],
292
+ [47, "mmss.0"],
293
+ [48, "##0.0E+0"],
294
+ [49, "@"]
295
+ ]);
296
+ //#endregion
297
+ exports.BUILTIN_NUMBER_FORMATS = BUILTIN_NUMBER_FORMATS;
298
+ exports.classifyNumberFormat = classifyNumberFormat;