reamkit 1.15.4 → 1.16.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 (94) hide show
  1. package/dist/esm/core/converter/facade.d.ts +5 -0
  2. package/dist/esm/core/converter/ream.d.ts +6 -0
  3. package/dist/esm/core/converter/ream.js +34 -8
  4. package/dist/esm/core/document-model/index.d.ts +1 -1
  5. package/dist/esm/core/document-model/types.d.ts +187 -1
  6. package/dist/esm/core/drawingml/chart-geometry.d.ts +28 -0
  7. package/dist/esm/core/drawingml/chart-geometry.js +371 -88
  8. package/dist/esm/core/drawingml/chart-parser.js +262 -11
  9. package/dist/esm/core/drawingml/colors.d.ts +6 -6
  10. package/dist/esm/core/drawingml/colors.js +58 -18
  11. package/dist/esm/core/drawingml/preset-geometry.js +7 -6
  12. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  13. package/dist/esm/core/drawingml/theme-parser.d.ts +41 -0
  14. package/dist/esm/core/drawingml/theme-parser.js +74 -1
  15. package/dist/esm/core/font/ttf-subset.js +26 -1
  16. package/dist/esm/core/images.js +172 -43
  17. package/dist/esm/core/indexed-colors.d.ts +11 -0
  18. package/dist/esm/core/indexed-colors.js +81 -0
  19. package/dist/esm/core/ir/sheet.d.ts +61 -0
  20. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  21. package/dist/esm/core/number-format.js +800 -0
  22. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  23. package/dist/esm/core/opc/alternate-content.js +72 -0
  24. package/dist/esm/core/opc/core-properties.js +1 -0
  25. package/dist/esm/core/opc/package.d.ts +14 -1
  26. package/dist/esm/core/opc/package.js +43 -4
  27. package/dist/esm/core/opc/relationships.js +1 -0
  28. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  29. package/dist/esm/core/opc/xml-entities.js +91 -0
  30. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  31. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  32. package/dist/esm/core/vector.d.ts +13 -0
  33. package/dist/esm/excel/activex-parser.js +1 -0
  34. package/dist/esm/excel/column-bands.d.ts +26 -1
  35. package/dist/esm/excel/column-bands.js +156 -20
  36. package/dist/esm/excel/comments-parser.js +5 -3
  37. package/dist/esm/excel/conditional-format.d.ts +25 -2
  38. package/dist/esm/excel/conditional-format.js +63 -19
  39. package/dist/esm/excel/escaped-text.d.ts +9 -0
  40. package/dist/esm/excel/escaped-text.js +20 -0
  41. package/dist/esm/excel/form-control-parser.js +1 -0
  42. package/dist/esm/excel/formula/dates.js +1 -1
  43. package/dist/esm/excel/header-footer.d.ts +9 -1
  44. package/dist/esm/excel/header-footer.js +162 -14
  45. package/dist/esm/excel/index.d.ts +1 -1
  46. package/dist/esm/excel/index.js +2 -2
  47. package/dist/esm/excel/pivot-table-parser.js +1 -0
  48. package/dist/esm/excel/print-model.d.ts +75 -0
  49. package/dist/esm/excel/print-model.js +898 -76
  50. package/dist/esm/excel/printer-settings.d.ts +16 -0
  51. package/dist/esm/excel/printer-settings.js +36 -0
  52. package/dist/esm/excel/rich-value.d.ts +13 -0
  53. package/dist/esm/excel/rich-value.js +120 -0
  54. package/dist/esm/excel/shared-strings-parser.js +11 -7
  55. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  56. package/dist/esm/excel/sheet-drawing.js +26 -3
  57. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  58. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  59. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  60. package/dist/esm/excel/sheet-to-flow.js +551 -36
  61. package/dist/esm/excel/slicer-parser.js +1 -0
  62. package/dist/esm/excel/styles-parser.d.ts +30 -2
  63. package/dist/esm/excel/styles-parser.js +177 -40
  64. package/dist/esm/excel/table-parser.js +1 -0
  65. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  66. package/dist/esm/excel/vml-drawing.js +158 -0
  67. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  68. package/dist/esm/excel/workbook-parser.js +5 -1
  69. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  70. package/dist/esm/excel/worksheet-parser.js +150 -61
  71. package/dist/esm/excel/xlsx-reader.js +242 -11
  72. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  73. package/dist/esm/html/html-writer.js +11 -2
  74. package/dist/esm/layout/page-doc.d.ts +26 -0
  75. package/dist/esm/layout/styled-layout.d.ts +2 -0
  76. package/dist/esm/layout/styled-layout.js +493 -64
  77. package/dist/esm/pdf/cid-font.js +20 -9
  78. package/dist/esm/pdf/objects.d.ts +13 -0
  79. package/dist/esm/pdf/objects.js +18 -1
  80. package/dist/esm/pdf/styled-page-emitter.js +164 -32
  81. package/dist/esm/pdf/text-page-renderer.js +1 -0
  82. package/dist/esm/pdf/vector-graphics.d.ts +3 -1
  83. package/dist/esm/pdf/vector-graphics.js +19 -2
  84. package/dist/esm/pptx/pptx-reader.js +1 -0
  85. package/dist/esm/pptx/slide-parser.js +12 -1
  86. package/dist/esm/svg/svg-writer.js +23 -4
  87. package/dist/esm/word/document-parser.js +1 -0
  88. package/dist/esm/word/drawing-parser.d.ts +34 -1
  89. package/dist/esm/word/drawing-parser.js +52 -2
  90. package/dist/esm/word/numbering-parser.js +1 -0
  91. package/dist/esm/word/settings-parser.js +1 -0
  92. package/dist/esm/word/styles-parser.js +1 -0
  93. package/package.json +7 -3
  94. package/dist/esm/excel/number-format.js +0 -476
@@ -0,0 +1,800 @@
1
+ import { INDEXED_COLORS } from "./indexed-colors.js";
2
+ //#region src/core/number-format.ts
3
+ var BUILTIN = new Map([
4
+ [0, "General"],
5
+ [1, "0"],
6
+ [2, "0.00"],
7
+ [3, "#,##0"],
8
+ [4, "#,##0.00"],
9
+ [5, "$#,##0_);($#,##0)"],
10
+ [6, "$#,##0_);[Red]($#,##0)"],
11
+ [7, "$#,##0.00_);($#,##0.00)"],
12
+ [8, "$#,##0.00_);[Red]($#,##0.00)"],
13
+ [9, "0%"],
14
+ [10, "0.00%"],
15
+ [11, "0.00E+00"],
16
+ [12, "# ?/?"],
17
+ [13, "# ??/??"],
18
+ [37, "#,##0_);(#,##0)"],
19
+ [38, "#,##0_);[Red](#,##0)"],
20
+ [39, "#,##0.00_);(#,##0.00)"],
21
+ [40, "#,##0.00_);[Red](#,##0.00)"],
22
+ [48, "##0.0E+0"],
23
+ [49, "@"]
24
+ ]);
25
+ var BUILTIN_DATE_FORMATS = new Map([
26
+ [14, "m/d/yyyy"],
27
+ [15, "d-mmm-yy"],
28
+ [16, "d-mmm"],
29
+ [17, "mmm-yy"],
30
+ [18, "h:mm AM/PM"],
31
+ [19, "h:mm:ss AM/PM"],
32
+ [20, "h:mm"],
33
+ [21, "h:mm:ss"],
34
+ [22, "m/d/yyyy h:mm"],
35
+ [45, "mm:ss"],
36
+ [46, "[h]:mm:ss"],
37
+ [47, "mm:ss.0"]
38
+ ]);
39
+ /**
40
+ * Render a cell's raw stored value through its number format (§18.8.30 built-ins
41
+ * and §18.8.31 custom codes). Handles `General`, text (`@`), the built-in date /
42
+ * time formats, custom date codes, and the numeric placeholder grammar (`0` `#`
43
+ * `,` `.` `%`, scientific notation, quoted literals, `;`-separated sections). An
44
+ * unknown id, or a value that does not parse as a number, falls back to a plain
45
+ * render of `rawValue`.
46
+ *
47
+ * @param rawValue The cell's raw stored value (a serial for dates).
48
+ * @param numFmtId The number-format id from the cell's `cellXf`.
49
+ * @param customFormats Custom format codes by id (from `<numFmts>`).
50
+ * @param date1904 The workbook date epoch (1904 vs the 1900 default).
51
+ * @returns The formatted display string.
52
+ */
53
+ function applyNumberFormat(rawValue, numFmtId, customFormats, date1904 = false) {
54
+ if (rawValue.length === 0) return "";
55
+ if (numFmtId === 0) return defaultNumberRender(rawValue);
56
+ if (numFmtId === 49) return rawValue;
57
+ const builtinDate = BUILTIN_DATE_FORMATS.get(numFmtId);
58
+ if (builtinDate !== void 0) return formatExcelDate(rawValue, builtinDate, date1904);
59
+ const custom = customFormats.get(numFmtId);
60
+ if (custom !== void 0 && (isDateFormat(custom) || hasElapsedToken(custom))) return formatExcelDate(rawValue, custom, date1904);
61
+ const format = custom ?? BUILTIN.get(numFmtId);
62
+ if (!format) return defaultNumberRender(rawValue);
63
+ if (format.trim().toLowerCase() === "general") return defaultNumberRender(rawValue);
64
+ return applyFormatString(rawValue, format);
65
+ }
66
+ /**
67
+ * The colour a number format paints its cell in (§18.8.31), as a 6-digit RRGGBB,
68
+ * or undefined when the section that applies declares none.
69
+ *
70
+ * A `[Red]` at the head of a section is part of the format, not decoration: the
71
+ * accounting codes write the negative section as `[Red]-#,##0.00`, and rendering
72
+ * it in the cell's own font colour loses the one signal the format exists for.
73
+ * `[Color 12]` indexes the same legacy palette a `<color indexed>` does.
74
+ *
75
+ * @param rawValue The cell's raw stored value — it selects the section.
76
+ * @param numFmtId The number-format id from the cell's `cellXf`.
77
+ * @param customFormats Custom format codes by id (from `<numFmts>`).
78
+ */
79
+ function numberFormatColorHex(rawValue, numFmtId, customFormats) {
80
+ const format = customFormats.get(numFmtId) ?? BUILTIN.get(numFmtId);
81
+ if (format === void 0 || !format.includes("[")) return void 0;
82
+ const sections = splitSections(format);
83
+ const n = Number(rawValue);
84
+ const idx = Number.isFinite(n) ? sectionIndexFor(n, sections) : 3;
85
+ if (idx < 0) return void 0;
86
+ return colorOfSection(sections[idx] ?? sections[0]);
87
+ }
88
+ var FORMAT_COLORS = new Map([
89
+ ["black", "000000"],
90
+ ["blue", "0000FF"],
91
+ ["cyan", "00FFFF"],
92
+ ["green", "00FF00"],
93
+ ["magenta", "FF00FF"],
94
+ ["red", "FF0000"],
95
+ ["white", "FFFFFF"],
96
+ ["yellow", "FFFF00"]
97
+ ]);
98
+ function colorOfSection(section) {
99
+ for (const m of section.matchAll(/\[([^\]]*)\]/g)) {
100
+ const body = m[1].trim().toLowerCase();
101
+ const named = FORMAT_COLORS.get(body);
102
+ if (named !== void 0) return named;
103
+ const indexed = /^color\s*(\d+)$/.exec(body);
104
+ if (indexed) {
105
+ const i = Number(indexed[1]) - 1;
106
+ if (i >= 0 && i < INDEXED_COLORS.length) return INDEXED_COLORS[i];
107
+ }
108
+ }
109
+ }
110
+ /**
111
+ * §18.8.31 — a section may carry a CONDITION in brackets (`[>999]`), and then
112
+ * the sections are no longer positive/negative/zero: the first whose test the
113
+ * value passes applies, and a section without one is the else. Returns the
114
+ * comparison, or undefined for a section that states none.
115
+ *
116
+ * The operator is what tells this bracket from the others a section may carry —
117
+ * `[Red]`, `[$€-407]`, `[h]` — none of which start with one.
118
+ */
119
+ function conditionOf(section) {
120
+ const m = /\[\s*(<=|>=|<>|<|>|=)\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*\]/.exec(section);
121
+ if (!m) return void 0;
122
+ const rhs = Number(m[2]);
123
+ return Number.isFinite(rhs) ? {
124
+ op: m[1],
125
+ rhs
126
+ } : void 0;
127
+ }
128
+ function conditionHolds(n, c) {
129
+ switch (c.op) {
130
+ case "<": return n < c.rhs;
131
+ case "<=": return n <= c.rhs;
132
+ case ">": return n > c.rhs;
133
+ case ">=": return n >= c.rhs;
134
+ case "=": return n === c.rhs;
135
+ default: return n !== c.rhs;
136
+ }
137
+ }
138
+ /**
139
+ * The section that applies to `n`, or -1 when the format is all conditions and
140
+ * the value passes none of them — Excel then shows the number plainly, which is
141
+ * what FormatChoiceTests.xlsx expects of `[<10]#" Wow"` at 11.
142
+ */
143
+ function sectionIndexFor(n, sections) {
144
+ const numeric = Math.min(sections.length, 3);
145
+ const conditions = sections.slice(0, numeric).map(conditionOf);
146
+ if (conditions.some((c) => c !== void 0)) {
147
+ for (let i = 0; i < numeric; i++) {
148
+ const c = conditions[i];
149
+ if (c === void 0 || conditionHolds(n, c)) return i;
150
+ }
151
+ return -1;
152
+ }
153
+ if (n > 0) return 0;
154
+ if (n < 0) return sections.length > 1 ? 1 : 0;
155
+ return sections.length > 2 ? 2 : 0;
156
+ }
157
+ /**
158
+ * Excel serial date → JS `Date`. The default 1900 epoch uses 1899-12-30 as day 0
159
+ * (so serial 1 is 1900-01-01) and inherits the Lotus 1-2-3 leap-year bug: serial 60
160
+ * is considered "1900-02-29" which never existed. For serial ≥ 61 the simple
161
+ * formula is exact; values < 60 are vanishingly rare in business sheets (and our
162
+ * render is approximate anyway).
163
+ *
164
+ * The 1904 epoch (legacy Mac Excel) uses 1904-01-01 as day 0 and has no leap bug.
165
+ * Files saved with `<workbookPr date1904="1"/>` store dates offset by exactly 1462
166
+ * days from the 1900-epoch interpretation.
167
+ */
168
+ function excelSerialToDate(serial, date1904) {
169
+ const ms = serial * 86400 * 1e3;
170
+ if (date1904) return new Date(Date.UTC(1904, 0, 1) + ms);
171
+ return new Date(Date.UTC(1899, 11, 30) + ms);
172
+ }
173
+ /**
174
+ * The inverse over a UTC calendar date: `(year, month0, day)` → the integer Excel
175
+ * serial day, using the same epoch. Round-trips {@link excelSerialToDate} exactly
176
+ * for an integer serial (the time-of-day is zero), so a serial → parts → serial
177
+ * loop is stable. Used by the formula engine's date functions and the `timePeriod`
178
+ * windows (E-SHEET W9) to map an injected reference date into serial space.
179
+ *
180
+ * @param month0 The 0-indexed month (0 = January).
181
+ */
182
+ function excelSerialFromUtcParts(year, month0, day, date1904) {
183
+ const epoch = date1904 ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
184
+ return Math.round((Date.UTC(year, month0, day) - epoch) / 864e5);
185
+ }
186
+ var MONTH_ABBR = [
187
+ "Jan",
188
+ "Feb",
189
+ "Mar",
190
+ "Apr",
191
+ "May",
192
+ "Jun",
193
+ "Jul",
194
+ "Aug",
195
+ "Sep",
196
+ "Oct",
197
+ "Nov",
198
+ "Dec"
199
+ ];
200
+ var MONTH_FULL = [
201
+ "January",
202
+ "February",
203
+ "March",
204
+ "April",
205
+ "May",
206
+ "June",
207
+ "July",
208
+ "August",
209
+ "September",
210
+ "October",
211
+ "November",
212
+ "December"
213
+ ];
214
+ var DAY_ABBR = [
215
+ "Sun",
216
+ "Mon",
217
+ "Tue",
218
+ "Wed",
219
+ "Thu",
220
+ "Fri",
221
+ "Sat"
222
+ ];
223
+ var DAY_FULL = [
224
+ "Sunday",
225
+ "Monday",
226
+ "Tuesday",
227
+ "Wednesday",
228
+ "Thursday",
229
+ "Friday",
230
+ "Saturday"
231
+ ];
232
+ function pad2(n) {
233
+ return String(n).padStart(2, "0");
234
+ }
235
+ /** An elapsed h/m/s count, truncated and zero-padded to its placeholder width. */
236
+ function elapsed(value, width) {
237
+ const whole = Math.floor(value);
238
+ const digits = String(Math.abs(whole)).padStart(width, "0");
239
+ return whole < 0 ? `-${digits}` : digits;
240
+ }
241
+ /**
242
+ * `[h]`, `[mm]`, `[ss]` — the elapsed-time brackets (§18.8.31), which suppress
243
+ * the wrap at 24 h / 60 min / 60 s and count from serial 0 instead.
244
+ *
245
+ * They carry no date letter outside the bracket, so a code like `[ss].00` reads
246
+ * as a plain numeric format to {@link isDateFormat} and lands in the placeholder
247
+ * grammar, where `[ss]` is stripped as if it were a colour and `.00` renders the
248
+ * serial as ".3".
249
+ */
250
+ function hasElapsedToken(code) {
251
+ return /\[(?:h+|m+|s+)\]/i.test(code.replace(/"[^"]*"/g, ""));
252
+ }
253
+ function isDateFormat(code) {
254
+ const cleaned = code.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "");
255
+ return /[dyhs]|m+/i.test(cleaned) && /[dyhs]/i.test(cleaned);
256
+ }
257
+ function tokenizeDateFormat(format) {
258
+ const tokens = [];
259
+ let i = 0;
260
+ while (i < format.length) {
261
+ const ch = format[i];
262
+ if (ch === "\"") {
263
+ let lit = "";
264
+ i++;
265
+ while (i < format.length && format[i] !== "\"") {
266
+ lit += format[i];
267
+ i++;
268
+ }
269
+ i++;
270
+ tokens.push({
271
+ kind: "lit",
272
+ text: lit
273
+ });
274
+ continue;
275
+ }
276
+ if (ch === "\\" && i + 1 < format.length) {
277
+ tokens.push({
278
+ kind: "lit",
279
+ text: format[i + 1]
280
+ });
281
+ i += 2;
282
+ continue;
283
+ }
284
+ if (ch === "[") {
285
+ let body = "";
286
+ i++;
287
+ while (i < format.length && format[i] !== "]") {
288
+ body += format[i];
289
+ i++;
290
+ }
291
+ i++;
292
+ if (/^h+$/i.test(body)) tokens.push({
293
+ kind: "elapsed-h",
294
+ text: body
295
+ });
296
+ else if (/^m+$/i.test(body)) tokens.push({
297
+ kind: "elapsed-m",
298
+ text: body
299
+ });
300
+ else if (/^s+$/i.test(body)) tokens.push({
301
+ kind: "elapsed-s",
302
+ text: body
303
+ });
304
+ continue;
305
+ }
306
+ if (ch === ".") {
307
+ const last = tokens[tokens.length - 1];
308
+ const run = /^\.(0+)/.exec(format.substring(i));
309
+ if (run && (last?.kind === "s" || last?.kind === "elapsed-s")) {
310
+ tokens.push({
311
+ kind: "subsec",
312
+ text: run[1]
313
+ });
314
+ i += run[0].length;
315
+ continue;
316
+ }
317
+ }
318
+ const lower = ch.toLowerCase();
319
+ if (lower === "y" || lower === "m" || lower === "d" || lower === "h" || lower === "s") {
320
+ let run = ch;
321
+ i++;
322
+ while (i < format.length && format[i].toLowerCase() === lower) {
323
+ run += format[i];
324
+ i++;
325
+ }
326
+ if (lower === "y") tokens.push({
327
+ kind: "y",
328
+ text: run
329
+ });
330
+ else if (lower === "d") tokens.push({
331
+ kind: "d",
332
+ text: run
333
+ });
334
+ else if (lower === "h") tokens.push({
335
+ kind: "h",
336
+ text: run
337
+ });
338
+ else if (lower === "s") tokens.push({
339
+ kind: "s",
340
+ text: run
341
+ });
342
+ else tokens.push({
343
+ kind: "m",
344
+ text: run
345
+ });
346
+ continue;
347
+ }
348
+ if (format.startsWith("AM/PM", i) || format.startsWith("A/P", i)) {
349
+ const len = format.startsWith("AM/PM", i) ? 5 : 3;
350
+ tokens.push({
351
+ kind: "ampm",
352
+ text: format.substring(i, i + len)
353
+ });
354
+ i += len;
355
+ continue;
356
+ }
357
+ tokens.push({
358
+ kind: "lit",
359
+ text: ch
360
+ });
361
+ i++;
362
+ }
363
+ return tokens;
364
+ }
365
+ function resolveMonthVsMinute(tokens) {
366
+ const resolved = tokens.map((t) => ({ ...t }));
367
+ for (let i = 0; i < resolved.length; i++) {
368
+ const t = resolved[i];
369
+ if (t.kind !== "m") continue;
370
+ let prev = i - 1;
371
+ while (prev >= 0 && resolved[prev].kind === "lit") prev--;
372
+ let next = i + 1;
373
+ while (next < resolved.length && resolved[next].kind === "lit") next++;
374
+ const prevKind = prev >= 0 ? resolved[prev].kind : void 0;
375
+ const prevIsHour = prevKind === "h" || prevKind === "elapsed-h";
376
+ const nextIsSec = next < resolved.length && resolved[next].kind === "s";
377
+ if (prevIsHour || nextIsSec) continue;
378
+ resolved[i] = {
379
+ kind: "M",
380
+ text: t.text
381
+ };
382
+ }
383
+ return resolved;
384
+ }
385
+ function formatExcelDate(rawValue, format, date1904) {
386
+ const serial = Number(rawValue);
387
+ if (!Number.isFinite(serial)) return rawValue;
388
+ format = splitSections(format)[0] ?? format;
389
+ const tokens = resolveMonthVsMinute(tokenizeDateFormat(format));
390
+ const subsecDigits = tokens.find((t) => t.kind === "subsec")?.text.length ?? 0;
391
+ const scale = 10 ** subsecDigits;
392
+ const totalSeconds = subsecDigits > 0 ? Math.round(serial * 86400 * scale) / scale : serial * 86400;
393
+ const date = excelSerialToDate(subsecDigits > 0 ? totalSeconds / 86400 : serial, date1904);
394
+ const year = date.getUTCFullYear();
395
+ const month = date.getUTCMonth() + 1;
396
+ const day = date.getUTCDate();
397
+ const weekday = date.getUTCDay();
398
+ const hour24 = date.getUTCHours();
399
+ const minute = date.getUTCMinutes();
400
+ const second = date.getUTCSeconds();
401
+ const hourValue = tokens.some((t) => t.kind === "ampm") ? hour24 % 12 === 0 ? 12 : hour24 % 12 : hour24;
402
+ const ampmLabel = (template) => {
403
+ const isUpper = template[0] === "A";
404
+ const isShort = template.length <= 3;
405
+ if (hour24 < 12) return isShort ? isUpper ? "A" : "a" : isUpper ? "AM" : "am";
406
+ return isShort ? isUpper ? "P" : "p" : isUpper ? "PM" : "pm";
407
+ };
408
+ let out = "";
409
+ for (const t of tokens) switch (t.kind) {
410
+ case "lit":
411
+ out += t.text;
412
+ break;
413
+ case "y":
414
+ out += t.text.length <= 2 ? pad2(year % 100) : String(year).padStart(4, "0");
415
+ break;
416
+ case "M": {
417
+ const len = t.text.length;
418
+ if (len === 5) out += MONTH_FULL[month - 1][0];
419
+ else if (len === 4) out += MONTH_FULL[month - 1];
420
+ else if (len === 3) out += MONTH_ABBR[month - 1];
421
+ else if (len === 2) out += pad2(month);
422
+ else out += String(month);
423
+ break;
424
+ }
425
+ case "d": {
426
+ const len = t.text.length;
427
+ if (len >= 4) out += DAY_FULL[weekday];
428
+ else if (len === 3) out += DAY_ABBR[weekday];
429
+ else if (len === 2) out += pad2(day);
430
+ else out += String(day);
431
+ break;
432
+ }
433
+ case "h":
434
+ out += t.text.length >= 2 ? pad2(hourValue) : String(hourValue);
435
+ break;
436
+ case "elapsed-h":
437
+ out += elapsed(totalSeconds / 3600, t.text.length);
438
+ break;
439
+ case "elapsed-m":
440
+ out += elapsed(totalSeconds / 60, t.text.length);
441
+ break;
442
+ case "elapsed-s":
443
+ out += elapsed(totalSeconds, t.text.length);
444
+ break;
445
+ case "subsec":
446
+ out += (totalSeconds - Math.floor(totalSeconds)).toFixed(subsecDigits).substring(1);
447
+ break;
448
+ case "m":
449
+ out += t.text.length >= 2 ? pad2(minute) : String(minute);
450
+ break;
451
+ case "s":
452
+ out += t.text.length >= 2 ? pad2(second) : String(second);
453
+ break;
454
+ case "ampm":
455
+ out += ampmLabel(t.text);
456
+ break;
457
+ }
458
+ return out;
459
+ }
460
+ function defaultNumberRender(rawValue) {
461
+ const n = Number(rawValue);
462
+ if (!Number.isFinite(n)) return rawValue;
463
+ return excelExponent(String(n));
464
+ }
465
+ /** Respell ECMAScript's `3e-104` as Excel's `3E-104` (§18.8.31). */
466
+ function excelExponent(rendered) {
467
+ const m = /^(-?[\d.]+)e([+-]?)(\d+)$/i.exec(rendered);
468
+ if (!m) return rendered;
469
+ return `${m[1]}E${m[2] === "-" ? "-" : "+"}${m[3].padStart(2, "0")}`;
470
+ }
471
+ /**
472
+ * A General number rounded to the decimals that fit `maxChars` characters.
473
+ *
474
+ * General is not a fixed format: a spreadsheet shows as many decimal places as
475
+ * the column has room for and ROUNDS to that, so 4.3900875881221957 in a
476
+ * column eight characters wide reads 4.390088. We rendered every stored digit
477
+ * and let the cell clip it, which turns the same value into 4.390087 — off by
478
+ * one in the last place shown, with nothing to say a digit was cut.
479
+ *
480
+ * The integer part is never dropped: a number too wide even without decimals
481
+ * keeps them all and the cell says so its own way (see hashOnOverflow).
482
+ *
483
+ * `fits` decides, and it must measure in the face this will be DRAWN in rather
484
+ * than count characters against the column's width in the document's unit.
485
+ * Counted that way we produced nine digits for a column that holds nine of
486
+ * Excel's and eight of ours, and the layout clipped the ninth — the very
487
+ * truncation this exists to prevent. Fewer digits than the reference shows is a
488
+ * coarser number; a clipped one is a different number.
489
+ *
490
+ * @param rawValue The cell's stored value.
491
+ * @param fits Whether a rendering will fit the cell.
492
+ */
493
+ function generalToWidth(rawValue, fits) {
494
+ const n = Number(rawValue);
495
+ if (!Number.isFinite(n)) return defaultNumberRender(rawValue);
496
+ const full = defaultNumberRender(rawValue);
497
+ if (fits(full)) return full;
498
+ const dot = full.indexOf(".");
499
+ if (dot < 0) return scientificToWidth(n, fits) ?? full;
500
+ let shortened = full;
501
+ for (let decimals = full.length - dot - 2; decimals >= 0; decimals--) {
502
+ const shifted = Number(`${Number(n.toPrecision(15))}e${decimals}`);
503
+ const rounded = shifted < 0 ? -Math.round(-shifted) : Math.round(shifted);
504
+ const back = Number(`${rounded}e${-decimals}`);
505
+ if (!Number.isFinite(back)) break;
506
+ shortened = defaultNumberRender(String(back));
507
+ if (fits(shortened)) return shortened;
508
+ }
509
+ return scientificToWidth(n, fits) ?? shortened;
510
+ }
511
+ /**
512
+ * `value` in scientific notation with as many mantissa decimals as `fits`
513
+ * allows, spelled Excel's way: an upper-case `E`, a signed exponent of at least
514
+ * two digits. Undefined when even a bare `1E+09` will not fit — the cell then
515
+ * says so with hashes rather than shrinking further.
516
+ */
517
+ function scientificToWidth(value, fits) {
518
+ for (let decimals = MAX_SCIENTIFIC_DECIMALS; decimals >= 0; decimals--) {
519
+ const [mantissa, exp] = value.toExponential(decimals).split("e");
520
+ const out = `${mantissa}E${exp.startsWith("-") ? "-" : "+"}${exp.replace(/^[-+]/, "").padStart(2, "0")}`;
521
+ if (fits(out)) return out;
522
+ }
523
+ }
524
+ /** Excel never shows more mantissa decimals than this under General. */
525
+ var MAX_SCIENTIFIC_DECIMALS = 10;
526
+ function applyFormatString(rawValue, format) {
527
+ const n = Number(rawValue);
528
+ const sections = splitSections(format);
529
+ if (!Number.isFinite(n)) {
530
+ const textSection = sections[3];
531
+ if (textSection !== void 0) return textSection.replace(/@/g, rawValue);
532
+ return rawValue;
533
+ }
534
+ const sectionIdx = sectionIndexFor(n, sections);
535
+ if (sectionIdx < 0) return defaultNumberRender(rawValue);
536
+ return applyNumericSection(n, sections[sectionIdx] ?? sections[0], sectionIdx === 1 && conditionOf(sections[0] ?? "") === void 0);
537
+ }
538
+ function splitSections(format) {
539
+ const out = [];
540
+ let current = "";
541
+ let inQuotes = false;
542
+ let bracketDepth = 0;
543
+ for (let i = 0; i < format.length; i++) {
544
+ const ch = format[i];
545
+ if (ch === "\"") {
546
+ inQuotes = !inQuotes;
547
+ current += ch;
548
+ continue;
549
+ }
550
+ if (!inQuotes) {
551
+ if (ch === "[") bracketDepth++;
552
+ else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
553
+ else if (ch === "\\" && i + 1 < format.length) {
554
+ current += ch + format[i + 1];
555
+ i++;
556
+ continue;
557
+ } else if (ch === ";" && bracketDepth === 0) {
558
+ out.push(current);
559
+ current = "";
560
+ continue;
561
+ }
562
+ }
563
+ current += ch;
564
+ }
565
+ out.push(current);
566
+ return out;
567
+ }
568
+ function countDigitPlaceholders(s) {
569
+ let n = 0;
570
+ for (const c of s) if (c === "0" || c === "#" || c === "?") n++;
571
+ return n;
572
+ }
573
+ /**
574
+ * `value` rounded to `decimals` places the way a spreadsheet rounds: on the
575
+ * DECIMAL number, half away from zero.
576
+ *
577
+ * `toFixed` rounds the binary double, which is not the number the file means.
578
+ * A rate stored as 0.0095 is 0.009499999999999999… in binary; times 100 that is
579
+ * 0.9499999999999998, and `toFixed(1)` dutifully answers "0.9" where Excel and
580
+ * every other reader show 1.0%. AverageTaxRates.xlsx had eleven of its
581
+ * percentages a tenth low for exactly this reason.
582
+ *
583
+ * Rounding to 15 significant digits first collapses the binary noise back to
584
+ * the decimal the author typed; shifting through a string exponent then keeps
585
+ * the scaling exact, so the final round sees 9.5 and not 9.499999999999998.
586
+ */
587
+ function toFixedDecimal(value, decimals) {
588
+ if (!Number.isFinite(value)) return value.toFixed(decimals);
589
+ const decimal = Number(value.toPrecision(15));
590
+ const shifted = Number(`${decimal}e${decimals}`);
591
+ if (!Number.isFinite(shifted)) return value.toFixed(decimals);
592
+ const rounded = shifted < 0 ? -Math.round(-shifted) : Math.round(shifted);
593
+ const back = Number(`${rounded}e${-decimals}`);
594
+ return Number.isFinite(back) ? back.toFixed(decimals) : value.toFixed(decimals);
595
+ }
596
+ function formatScientific(value, cleaned, negativeSection) {
597
+ const m = /^(.*?)([eE])([+-])(.*)$/.exec(cleaned);
598
+ if (!m) return cleaned;
599
+ const mantissaFmt = m[1];
600
+ const eChar = m[2];
601
+ const expSignFmt = m[3];
602
+ const expFmt = m[4];
603
+ const dot = mantissaFmt.indexOf(".");
604
+ const intDigits = Math.max(1, countDigitPlaceholders(dot >= 0 ? mantissaFmt.slice(0, dot) : mantissaFmt));
605
+ const decimals = dot >= 0 ? countDigitPlaceholders(mantissaFmt.slice(dot + 1)) : 0;
606
+ let exp = 0;
607
+ let mant = Math.abs(value);
608
+ if (mant !== 0) {
609
+ exp = Math.floor(Math.log10(mant));
610
+ exp -= (exp % intDigits + intDigits) % intDigits;
611
+ mant = mant / Math.pow(10, exp);
612
+ if (Number(mant.toFixed(decimals)) >= Math.pow(10, intDigits)) {
613
+ exp += intDigits;
614
+ mant = mant / Math.pow(10, intDigits);
615
+ }
616
+ }
617
+ const mantStr = mant.toFixed(decimals);
618
+ const expDigits = countDigitPlaceholders(expFmt) || 2;
619
+ const expStr = String(Math.abs(exp)).padStart(expDigits, "0");
620
+ return `${value < 0 && !negativeSection ? "-" : ""}${mantStr}${eChar}${exp < 0 ? "-" : expSignFmt === "+" ? "+" : ""}${expStr}`;
621
+ }
622
+ /**
623
+ * Resolve the `[...]` codes a numeric section may carry (§18.8.31).
624
+ *
625
+ * `[$SYMBOL-LOCALE]` is a currency tag and its SYMBOL is *rendered* — dropping
626
+ * the whole bracket, as a colour or a locale is dropped, silently loses the `$`
627
+ * from `[$$-409]#,##0`. The symbol becomes a quoted literal so the placeholder
628
+ * grammar treats it as text. Everything else in brackets (colours, locales,
629
+ * conditions) is decoration here and goes.
630
+ */
631
+ function resolveBracketCodes(format) {
632
+ return format.replace(/\[([^\]]*)\]/g, (_, body) => {
633
+ const symbol = /^\$([^-]*)(?:-.*)?$/.exec(body)?.[1]?.replace(/"/g, "") ?? "";
634
+ return symbol.length > 0 ? `"${symbol}"` : "";
635
+ });
636
+ }
637
+ /**
638
+ * §18.8.31 fraction formats — `# ?/?`, `# ??/??`, `?/?`, `# ?/16`.
639
+ *
640
+ * `?` is a digit placeholder like `#` but padded to width with spaces, and the
641
+ * count of them on the right of the slash bounds the denominator: `??` admits
642
+ * any denominator up to 99, so 25.378 renders as "25 31/82". A literal number
643
+ * there instead (`?/16`) fixes the denominator. Returns undefined when the
644
+ * section is not a fraction, leaving the ordinary grammar to run.
645
+ */
646
+ function formatFraction(value, cleaned, negativeSection) {
647
+ const m = /^(.*?)([0#?]+)\s*\/\s*([0#?]+|\d+)(.*)$/.exec(cleaned);
648
+ if (!m) return void 0;
649
+ const head = m[1];
650
+ const denFmt = m[3];
651
+ const tail = m[4];
652
+ const intMatch = /([0#?]+)([^0#?]*)$/.exec(head);
653
+ const hasInteger = intMatch !== null;
654
+ const literalPrefix = unquoteLiteral(head.substring(0, intMatch ? intMatch.index : head.length));
655
+ const separator = intMatch ? unquoteLiteral(intMatch[2]) : " ";
656
+ const magnitude = Math.abs(value);
657
+ const whole = hasInteger ? Math.floor(magnitude) : 0;
658
+ const rest = magnitude - whole;
659
+ const fixedDen = /^\d+$/.test(denFmt) ? Number(denFmt) : void 0;
660
+ const maxDen = fixedDen ?? Math.pow(10, denFmt.length) - 1;
661
+ if (!Number.isFinite(maxDen) || maxDen < 1) return void 0;
662
+ let num;
663
+ let den;
664
+ if (fixedDen !== void 0) {
665
+ den = fixedDen;
666
+ num = Math.round(rest * fixedDen);
667
+ } else [num, den] = bestRational(rest, maxDen);
668
+ const pieces = [];
669
+ if (hasInteger) {
670
+ let integer = whole;
671
+ if (den > 0 && num >= den) {
672
+ integer += Math.floor(num / den);
673
+ num %= den;
674
+ }
675
+ if (integer !== 0 || num === 0) pieces.push(String(integer));
676
+ }
677
+ if (num !== 0 || !hasInteger) pieces.push(`${num}/${den}`);
678
+ const sign = value < 0 && !negativeSection ? "-" : "";
679
+ const gap = separator.length > 0 ? separator : " ";
680
+ return `${literalPrefix}${sign}${pieces.join(gap)}${unquoteLiteral(tail)}`;
681
+ }
682
+ /**
683
+ * The closest `n/d` to `value` with `d <= maxDen`, by continued fractions —
684
+ * the same approximation Excel and LibreOffice show for a `?/?` format.
685
+ */
686
+ function bestRational(value, maxDen) {
687
+ if (value === 0) return [0, 1];
688
+ let [prevN, prevD] = [0, 1];
689
+ let [n, d] = [1, 0];
690
+ let x = value;
691
+ for (let i = 0; i < 64; i++) {
692
+ const a = Math.floor(x);
693
+ const nextN = a * n + prevN;
694
+ const nextD = a * d + prevD;
695
+ if (nextD > maxDen) break;
696
+ [prevN, prevD] = [n, d];
697
+ [n, d] = [nextN, nextD];
698
+ const frac = x - a;
699
+ if (frac < 1e-12) break;
700
+ x = 1 / frac;
701
+ }
702
+ if (d === 0) return [Math.round(value * maxDen), maxDen];
703
+ return [n, d];
704
+ }
705
+ function applyNumericSection(value, format, negativeSection) {
706
+ const cleaned = resolveBracketCodes(format);
707
+ if (cleaned.trim().toLowerCase() === "general") return defaultNumberRender(String(negativeSection ? Math.abs(value) : value));
708
+ const fraction = formatFraction(value, cleaned, negativeSection);
709
+ if (fraction !== void 0) return fraction;
710
+ if (/[eE][+-]/.test(cleaned)) return formatScientific(value, cleaned, negativeSection);
711
+ const isPercent = cleaned.includes("%");
712
+ let magnitude = Math.abs(value);
713
+ if (isPercent) magnitude *= 100;
714
+ const { intFormat, decFormat, literalPrefix, literalSuffix, scaleCommas } = splitNumberFormat(cleaned);
715
+ if (scaleCommas > 0) magnitude /= 1e3 ** scaleCommas;
716
+ let decimals = 0;
717
+ for (const c of decFormat) if (c === "0" || c === "#") decimals++;
718
+ const [intRaw, decRaw] = toFixedDecimal(magnitude, decimals).split(".");
719
+ const intStr = /,(?=\d)/.test(intFormat) || /[0#],[0#]/.test(intFormat) ? intRaw.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : intRaw;
720
+ let numberPart = intRaw === "0" && !/[0?]/.test(intFormat) ? "" : intStr;
721
+ if (decimals > 0) {
722
+ let dec = decRaw ?? "".padEnd(decimals, "0");
723
+ if (dec.length < decimals) dec = dec.padEnd(decimals, "0");
724
+ let kept = "";
725
+ for (let i = 0; i < decFormat.length; i++) {
726
+ const placeholder = decFormat[i];
727
+ if (placeholder === "0" || placeholder === "#") kept += dec[i] ?? "0";
728
+ }
729
+ for (let i = kept.length - 1; i >= 0 && kept[i] === "0" && decFormat[i] === "#"; i--) kept = kept.substring(0, i);
730
+ if (kept.length > 0) numberPart += "." + kept;
731
+ else if (decFormat.includes("0")) numberPart += "." + dec;
732
+ }
733
+ let signPrefix = "";
734
+ if (value < 0 && !negativeSection) signPrefix = "-";
735
+ return `${literalPrefix}${signPrefix}${numberPart}${isPercent ? "%" : ""}${literalSuffix}`;
736
+ }
737
+ function splitNumberFormat(cleaned) {
738
+ let firstDigit = -1;
739
+ let lastDigit = -1;
740
+ for (let i = 0; i < cleaned.length; i++) {
741
+ const ch = cleaned[i];
742
+ if (ch === "0" || ch === "#") {
743
+ if (firstDigit < 0) firstDigit = i;
744
+ lastDigit = i;
745
+ }
746
+ }
747
+ if (firstDigit < 0) return {
748
+ literalPrefix: unquoteLiteral(cleaned),
749
+ intFormat: "",
750
+ decFormat: "",
751
+ literalSuffix: "",
752
+ scaleCommas: 0
753
+ };
754
+ const literalPrefix = unquoteLiteral(cleaned.substring(0, firstDigit));
755
+ const rawSuffix = cleaned.substring(lastDigit + 1);
756
+ const scaleCommas = /^,+/.exec(rawSuffix)?.[0]?.length ?? 0;
757
+ const literalSuffix = unquoteLiteral(rawSuffix.substring(scaleCommas));
758
+ const digitRange = cleaned.substring(firstDigit, lastDigit + 1);
759
+ const dotIdx = digitRange.indexOf(".");
760
+ return {
761
+ literalPrefix,
762
+ intFormat: dotIdx >= 0 ? digitRange.substring(0, dotIdx) : digitRange,
763
+ decFormat: dotIdx >= 0 ? digitRange.substring(dotIdx + 1) : "",
764
+ literalSuffix,
765
+ scaleCommas
766
+ };
767
+ }
768
+ function unquoteLiteral(s) {
769
+ let out = "";
770
+ for (let i = 0; i < s.length; i++) {
771
+ const ch = s[i];
772
+ if (ch === "\"") {
773
+ i++;
774
+ while (i < s.length && s[i] !== "\"") {
775
+ out += s[i];
776
+ i++;
777
+ }
778
+ continue;
779
+ }
780
+ if (ch === "\\" && i + 1 < s.length) {
781
+ out += s[i + 1];
782
+ i++;
783
+ continue;
784
+ }
785
+ if (ch === "_" && i + 1 < s.length) {
786
+ out += " ";
787
+ i++;
788
+ continue;
789
+ }
790
+ if (ch === "*" && i + 1 < s.length) {
791
+ i++;
792
+ continue;
793
+ }
794
+ if (ch === "%") continue;
795
+ out += ch;
796
+ }
797
+ return out;
798
+ }
799
+ //#endregion
800
+ export { applyNumberFormat, excelSerialFromUtcParts, excelSerialToDate, generalToWidth, numberFormatColorHex };