documonster 0.20.1 → 0.20.2

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 (32) hide show
  1. package/README.md +1 -1
  2. package/README_zh.md +1 -1
  3. package/dist/esm/modules/excel/core/cell.js +5 -6
  4. package/dist/esm/modules/excel/core/pivot-table.js +17 -9
  5. package/dist/esm/modules/excel/stream/xlsb-worksheet-reader.js +4 -6
  6. package/dist/esm/modules/excel/utils/cell-format-parse.js +112 -174
  7. package/dist/esm/modules/excel/utils/cell-format.js +42 -946
  8. package/dist/esm/modules/formula/functions/text.js +23 -631
  9. package/dist/esm/modules/pdf/excel-bridge.js +5 -5
  10. package/dist/esm/utils/number-format-render.js +814 -0
  11. package/dist/esm/utils/number-format.js +477 -0
  12. package/dist/esm/utils/utils.base.js +3 -70
  13. package/dist/iife/documonster.archive.iife.min.js +1 -1
  14. package/dist/iife/documonster.csv.iife.min.js +1 -1
  15. package/dist/iife/documonster.draw.iife.min.js +1 -1
  16. package/dist/iife/documonster.excel.iife.min.js +50 -50
  17. package/dist/iife/documonster.formula.iife.min.js +1 -1
  18. package/dist/iife/documonster.markdown.iife.min.js +1 -1
  19. package/dist/iife/documonster.mermaid.iife.min.js +1 -1
  20. package/dist/iife/documonster.pdf.iife.min.js +18 -18
  21. package/dist/iife/documonster.stream.iife.min.js +1 -1
  22. package/dist/iife/documonster.word.iife.min.js +60 -60
  23. package/dist/iife/documonster.xml.iife.min.js +1 -1
  24. package/dist/types/modules/excel/core/cell.d.ts +11 -0
  25. package/dist/types/modules/excel/core/pivot-table.d.ts +7 -0
  26. package/dist/types/modules/excel/utils/cell-format-parse.d.ts +13 -9
  27. package/dist/types/modules/excel/utils/cell-format.d.ts +21 -33
  28. package/dist/types/modules/excel/utils/text-metrics.d.ts +1 -0
  29. package/dist/types/utils/number-format-render.d.ts +47 -0
  30. package/dist/types/utils/number-format.d.ts +215 -0
  31. package/dist/types/utils/utils.base.d.ts +1 -9
  32. package/package.json +1 -1
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Excel number formats, read once.
3
+ *
4
+ * **Why this exists.** A number format such as `\$0.0,,"M"` or `[h]:mm:ss` is a small language,
5
+ * and this codebase used to read it with a separate scanner per consumer: the XLSX/XLSB readers
6
+ * deciding whether a serial is a date, the display formatter, the date-kind classifier, the
7
+ * pivot cache writer, the `TEXT` function and the input parser. Each one stripped a different
8
+ * subset of the constructs that make a character *literal* — quotes, backslash escapes,
9
+ * `_x` padding, `*x` fill, bracket tags — so each one mistook a different literal for a code.
10
+ * `\$0.0,,\M` turned a five-billion-dollar figure into `Invalid Date` on load because one of them
11
+ * read the escaped `M` as a month; `0_m` was a date to one reader and a number to another.
12
+ *
13
+ * Patching each scanner is how they drifted apart in the first place. So there is now one
14
+ * tokenizer: every consumer asks it what a format says, and a character is a code or a literal
15
+ * for all of them or for none.
16
+ *
17
+ * What it models:
18
+ *
19
+ * - **Sections** split on `;`, except inside quotes, brackets or after a backslash.
20
+ * - **Literals**: `"quoted"`, `\x`, and any character that is not a code.
21
+ * - **Layout**: `_x` (space the width of `x`) and `*x` (repeat `x` to fill).
22
+ * - **Bracket tags**: colours, conditions (`[>=100]`), locale/currency (`[$€-407]`), elapsed time
23
+ * (`[h]`, `[mm]`, `[ss]`) and anything else (`[DBNum1]`), each kept apart so a consumer can act
24
+ * on the one it needs.
25
+ * - **Date and time codes**, with each `m`/`mm` resolved to month or minute where it stands —
26
+ * Excel's rule is positional (minute directly after an hour or directly before a second), so
27
+ * `yyyy-mm-dd hh:mm` holds one of each and no format-wide flag can describe it.
28
+ * - **Number codes**: `0 # ?`, `.`, `,`, `%`, `E+`/`E-`, `/` and `@`, and the word `General`.
29
+ *
30
+ * `e` and `g` (era years in East Asian locales) are deliberately literals: an unquoted word such
31
+ * as `zero` in `0;-0;zero` must stay a word, and no locale this library renders uses them.
32
+ * `b` (Buddhist year) is a code, as it was for the XLSX reader before this module existed.
33
+ */
34
+ /** English month names, January first: what `mmm`/`mmmm`/`mmmmm` print and what input parsing accepts. */
35
+ export const MONTH_NAMES = [
36
+ "January",
37
+ "February",
38
+ "March",
39
+ "April",
40
+ "May",
41
+ "June",
42
+ "July",
43
+ "August",
44
+ "September",
45
+ "October",
46
+ "November",
47
+ "December"
48
+ ];
49
+ /** English weekday names, Sunday first: what `ddd`/`dddd` print and what input parsing accepts. */
50
+ export const DAY_NAMES = [
51
+ "Sunday",
52
+ "Monday",
53
+ "Tuesday",
54
+ "Wednesday",
55
+ "Thursday",
56
+ "Friday",
57
+ "Saturday"
58
+ ];
59
+ // =============================================================================
60
+ // Sections
61
+ // =============================================================================
62
+ /**
63
+ * Split a format into its `;`-separated sections (positive; negative; zero; text).
64
+ *
65
+ * A `;` inside `"…"`, inside `[…]`, or taken as the argument of `\`, `_` or `*` is part of a section,
66
+ * not a separator — the same rules `tokenizeFormatSection` reads a character by.
67
+ */
68
+ export function splitFormatSections(fmt) {
69
+ const sections = [];
70
+ let start = 0;
71
+ let inQuote = false;
72
+ let inBracket = false;
73
+ for (let i = 0; i < fmt.length; i++) {
74
+ const ch = fmt[i];
75
+ if (inQuote) {
76
+ if (ch === '"') {
77
+ inQuote = false;
78
+ }
79
+ }
80
+ else if (inBracket) {
81
+ if (ch === "]") {
82
+ inBracket = false;
83
+ }
84
+ }
85
+ else if (ch === "\\" || ch === "_" || ch === "*") {
86
+ // Each takes the next character as its argument, whatever it is: `0_;0` pads with the width
87
+ // of `;`, and `0_";0` with the width of `"` — neither ends a section or opens a literal.
88
+ i++;
89
+ }
90
+ else if (ch === '"') {
91
+ inQuote = true;
92
+ }
93
+ else if (ch === "[") {
94
+ inBracket = true;
95
+ }
96
+ else if (ch === ";") {
97
+ sections.push(fmt.slice(start, i));
98
+ start = i + 1;
99
+ }
100
+ }
101
+ sections.push(fmt.slice(start));
102
+ return sections;
103
+ }
104
+ // =============================================================================
105
+ // Tokenizer
106
+ // =============================================================================
107
+ const COLOR_RE = /^(?:black|blue|cyan|green|magenta|red|white|yellow|color\s*\d+)$/i;
108
+ const CONDITION_RE = /^(<>|<=|>=|=|<|>)\s*(-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)$/i;
109
+ const ELAPSED_RE = /^(h+|m+|s+)$/i;
110
+ function bracketToken(body) {
111
+ if (ELAPSED_RE.test(body)) {
112
+ return {
113
+ kind: "elapsed",
114
+ unit: body[0].toLowerCase(),
115
+ width: body.length,
116
+ fraction: 0
117
+ };
118
+ }
119
+ const condition = CONDITION_RE.exec(body);
120
+ if (condition) {
121
+ return { kind: "condition", op: condition[1], value: Number(condition[2]) };
122
+ }
123
+ if (body.startsWith("$")) {
124
+ const dash = body.indexOf("-");
125
+ return { kind: "locale", symbol: body.slice(1, dash === -1 ? body.length : dash) };
126
+ }
127
+ if (COLOR_RE.test(body)) {
128
+ return { kind: "color", name: body };
129
+ }
130
+ return { kind: "bracket", text: body };
131
+ }
132
+ /** Length of the run of `letter` (case-insensitive) starting at `i`. */
133
+ function runLength(section, i, letter) {
134
+ let j = i;
135
+ while (j < section.length && section[j].toLowerCase() === letter) {
136
+ j++;
137
+ }
138
+ return j - i;
139
+ }
140
+ function startsWithIgnoreCase(section, i, word) {
141
+ return section.slice(i, i + word.length).toLowerCase() === word.toLowerCase();
142
+ }
143
+ /**
144
+ * Read one section into tokens.
145
+ *
146
+ * Every construct that makes a character literal is recognised here and only here, so no consumer
147
+ * can mistake `\M`, `"M"`, `_M` or `*M` for a month.
148
+ */
149
+ export function tokenizeFormatSection(section) {
150
+ const raw = [];
151
+ let i = 0;
152
+ while (i < section.length) {
153
+ const ch = section[i];
154
+ const lower = ch.toLowerCase();
155
+ if (ch === '"') {
156
+ const end = section.indexOf('"', i + 1);
157
+ const stop = end === -1 ? section.length : end;
158
+ raw.push({ kind: "literal", text: section.slice(i + 1, stop) });
159
+ i = stop + 1;
160
+ continue;
161
+ }
162
+ if (ch === "\\") {
163
+ if (i + 1 < section.length) {
164
+ raw.push({ kind: "literal", text: section[i + 1] });
165
+ }
166
+ i += 2;
167
+ continue;
168
+ }
169
+ if (ch === "_" || ch === "*") {
170
+ if (i + 1 < section.length) {
171
+ raw.push(ch === "_"
172
+ ? { kind: "pad", char: section[i + 1] }
173
+ : { kind: "fill", char: section[i + 1] });
174
+ }
175
+ i += 2;
176
+ continue;
177
+ }
178
+ if (ch === "[") {
179
+ const end = section.indexOf("]", i + 1);
180
+ if (end === -1) {
181
+ raw.push({ kind: "literal", text: section.slice(i) });
182
+ break;
183
+ }
184
+ raw.push(bracketToken(section.slice(i + 1, end)));
185
+ i = end + 1;
186
+ continue;
187
+ }
188
+ if (startsWithIgnoreCase(section, i, "General")) {
189
+ raw.push({ kind: "general" });
190
+ i += 7;
191
+ continue;
192
+ }
193
+ if (startsWithIgnoreCase(section, i, "AM/PM")) {
194
+ raw.push({ kind: "ampm", short: false, lower: ch === "a" });
195
+ i += 5;
196
+ continue;
197
+ }
198
+ if (startsWithIgnoreCase(section, i, "A/P")) {
199
+ raw.push({ kind: "ampm", short: true, lower: ch === "a" });
200
+ i += 3;
201
+ continue;
202
+ }
203
+ if (lower === "e" && (section[i + 1] === "+" || section[i + 1] === "-")) {
204
+ raw.push({ kind: "exponent", sign: section[i + 1], upper: ch === "E" });
205
+ i += 2;
206
+ continue;
207
+ }
208
+ if (lower === "b" && (section[i + 1] === "1" || section[i + 1] === "2")) {
209
+ // `B1`/`B2` switch calendars (Gregorian/Hijri); they print nothing.
210
+ raw.push({ kind: "bracket", text: section.slice(i, i + 2) });
211
+ i += 2;
212
+ continue;
213
+ }
214
+ if (lower === "y" || lower === "d" || lower === "h" || lower === "s" || lower === "b") {
215
+ const width = runLength(section, i, lower);
216
+ const part = lower === "y"
217
+ ? "year"
218
+ : lower === "b"
219
+ ? "buddhistYear"
220
+ : lower === "d"
221
+ ? "day"
222
+ : lower === "h"
223
+ ? "hour"
224
+ : "second";
225
+ raw.push({ kind: "date", part, width, fraction: 0 });
226
+ i += width;
227
+ continue;
228
+ }
229
+ if (lower === "m") {
230
+ const width = runLength(section, i, "m");
231
+ raw.push({ kind: "m", width });
232
+ i += width;
233
+ continue;
234
+ }
235
+ switch (ch) {
236
+ case "0":
237
+ case "#":
238
+ case "?":
239
+ raw.push({ kind: "digit", char: ch });
240
+ break;
241
+ case ".":
242
+ raw.push({ kind: "decimal" });
243
+ break;
244
+ case ",":
245
+ raw.push({ kind: "thousands" });
246
+ break;
247
+ case "%":
248
+ raw.push({ kind: "percent" });
249
+ break;
250
+ case "/":
251
+ raw.push({ kind: "slash" });
252
+ break;
253
+ case "@":
254
+ raw.push({ kind: "text" });
255
+ break;
256
+ case "1":
257
+ case "2":
258
+ case "3":
259
+ case "4":
260
+ case "5":
261
+ case "6":
262
+ case "7":
263
+ case "8":
264
+ case "9":
265
+ raw.push({ kind: "numeral", text: ch });
266
+ break;
267
+ default:
268
+ raw.push({ kind: "literal", text: ch });
269
+ }
270
+ i++;
271
+ }
272
+ return foldSecondFractions(resolveMonthOrMinute(raw));
273
+ }
274
+ /**
275
+ * Attach `.0`, `.00`, … after a seconds code to that code.
276
+ *
277
+ * In `ss.00` and `[ss].00` the point and zeros are fractions of a second, not a decimal number layout,
278
+ * and every consumer — the renderer rounding to them, the input parser reading them back — needs to know
279
+ * that. Deciding it here, once, is what keeps the two from reading the same format differently.
280
+ */
281
+ function foldSecondFractions(tokens) {
282
+ const out = [];
283
+ for (let k = 0; k < tokens.length; k++) {
284
+ const token = tokens[k];
285
+ const isSecond = (token.kind === "date" && token.part === "second") ||
286
+ (token.kind === "elapsed" && token.unit === "s");
287
+ if (!isSecond || tokens[k + 1]?.kind !== "decimal") {
288
+ out.push(token);
289
+ continue;
290
+ }
291
+ let zeros = 0;
292
+ for (let j = k + 2; j < tokens.length; j++) {
293
+ const next = tokens[j];
294
+ if (next.kind !== "digit" || next.char !== "0") {
295
+ break;
296
+ }
297
+ zeros++;
298
+ }
299
+ if (zeros === 0) {
300
+ out.push(token);
301
+ continue;
302
+ }
303
+ out.push({ ...token, fraction: zeros });
304
+ k += 1 + zeros;
305
+ }
306
+ return out;
307
+ }
308
+ /**
309
+ * Excel's rule for `m` and `mm`, measured against Excel itself (33 formats). `mmm` and longer are always
310
+ * months. A shorter run is a minute when
311
+ *
312
+ * - the time code before it, skipping year, day and month-name codes, is an hour (`h dd mm`, `h mmm mm`,
313
+ * `[h] mm`);
314
+ * - the code right after it is a seconds code (`mm:ss`), but not an elapsed `[ss]` and not across a day or year
315
+ * code (`mm dd ss` is a month);
316
+ * - or the time code before it is a seconds code and no minute has appeared yet (`ss mm`, but the second
317
+ * `mm` of `h:mm:ss mm` is a month).
318
+ *
319
+ * Anything else is a month (`dd mm`, `mm h`).
320
+ */
321
+ function resolveMonthOrMinute(raw) {
322
+ const isCode = (t) => t.kind === "m" || t.kind === "date" || t.kind === "elapsed";
323
+ const isCalendar = (t) => (t.kind === "m" && t.width >= 3) ||
324
+ (t.kind === "date" && t.part !== "hour" && t.part !== "minute" && t.part !== "second");
325
+ let minuteSeen = false;
326
+ const out = [];
327
+ raw.forEach((token, index) => {
328
+ if (token.kind !== "m") {
329
+ out.push(token);
330
+ return;
331
+ }
332
+ if (token.width >= 3) {
333
+ out.push({ kind: "date", part: "month", width: token.width, fraction: 0 });
334
+ return;
335
+ }
336
+ let before;
337
+ for (let k = index - 1; k >= 0; k--) {
338
+ if (isCode(raw[k]) && !isCalendar(raw[k])) {
339
+ before = raw[k];
340
+ break;
341
+ }
342
+ }
343
+ let after;
344
+ for (let k = index + 1; k < raw.length; k++) {
345
+ if (isCode(raw[k])) {
346
+ after = raw[k];
347
+ break;
348
+ }
349
+ }
350
+ const isPlain = (t, part) => t?.kind === "date" && t.part === part;
351
+ const minute = isPlain(before, "hour") ||
352
+ (before?.kind === "elapsed" && before.unit === "h") ||
353
+ isPlain(after, "second") ||
354
+ (isPlain(before, "second") && !minuteSeen);
355
+ minuteSeen || (minuteSeen = minute);
356
+ out.push({ kind: "date", part: minute ? "minute" : "month", width: token.width, fraction: 0 });
357
+ });
358
+ return out;
359
+ }
360
+ // =============================================================================
361
+ // Parsed formats
362
+ // =============================================================================
363
+ function describeSection(source) {
364
+ const tokens = tokenizeFormatSection(source);
365
+ let hasDate = false;
366
+ let hasTime = false;
367
+ let hasElapsed = false;
368
+ let hasText = false;
369
+ let hasGeneral = false;
370
+ let condition;
371
+ for (const token of tokens) {
372
+ switch (token.kind) {
373
+ case "date":
374
+ if (token.part === "hour" || token.part === "minute" || token.part === "second") {
375
+ hasTime = true;
376
+ }
377
+ else {
378
+ hasDate = true;
379
+ }
380
+ break;
381
+ case "ampm":
382
+ hasTime = true;
383
+ break;
384
+ case "elapsed":
385
+ hasElapsed = true;
386
+ break;
387
+ case "text":
388
+ hasText = true;
389
+ break;
390
+ case "general":
391
+ hasGeneral = true;
392
+ break;
393
+ case "condition":
394
+ condition ?? (condition = { op: token.op, value: token.value });
395
+ break;
396
+ }
397
+ }
398
+ return { source, tokens, hasDate, hasTime, hasElapsed, hasText, hasGeneral, condition };
399
+ }
400
+ /** Formats seen in a workbook are few and asked about very often; past this the cache restarts. */
401
+ const PARSE_CACHE_LIMIT = 1024;
402
+ const parseCache = new Map();
403
+ /**
404
+ * Parse a whole format. Results are cached: a reader asks about the same handful of formats once
405
+ * per cell.
406
+ */
407
+ export function parseNumberFormat(fmt) {
408
+ const cached = parseCache.get(fmt);
409
+ if (cached !== undefined) {
410
+ return cached;
411
+ }
412
+ const sections = splitFormatSections(fmt).map(describeSection);
413
+ let textSection;
414
+ let numberSections = sections;
415
+ if (sections.length >= 4) {
416
+ textSection = sections[3];
417
+ numberSections = sections.slice(0, 3);
418
+ }
419
+ else if (sections[sections.length - 1].hasText) {
420
+ textSection = sections[sections.length - 1];
421
+ numberSections = sections.length > 1 ? sections.slice(0, -1) : [];
422
+ }
423
+ const parsed = { source: fmt, sections, numberSections, textSection };
424
+ if (parseCache.size >= PARSE_CACHE_LIMIT) {
425
+ parseCache.clear();
426
+ }
427
+ parseCache.set(fmt, parsed);
428
+ return parsed;
429
+ }
430
+ /**
431
+ * The date and time facts of a format, read from its **first** section — the one that applies to
432
+ * a positive serial, and therefore to every date. `";;;dd"` hides numbers; it is not a date format.
433
+ */
434
+ export function numberFormatFacets(fmt) {
435
+ if (fmt === null || fmt === undefined || fmt.trim() === "") {
436
+ return { date: false, time: false, elapsed: false, text: false, general: true };
437
+ }
438
+ const first = parseNumberFormat(fmt).sections[0];
439
+ return {
440
+ date: first.hasDate,
441
+ time: first.hasTime,
442
+ elapsed: first.hasElapsed,
443
+ text: first.hasText,
444
+ general: first.hasGeneral
445
+ };
446
+ }
447
+ /**
448
+ * Whether a cell carrying this format holds a date: its first section names a calendar or clock code
449
+ * outside every literal, and is not a text format.
450
+ *
451
+ * This is the rule every reader applies when deciding whether a stored serial becomes a `Date`.
452
+ *
453
+ * **An elapsed tag on its own does not count.** `[h]` or `[ss]` is a quantity of time, and a `Date` would
454
+ * misstate it — and could not hold it past a few thousand years. `[h]:mm:ss` still counts, through its
455
+ * clock codes, which is the reading these readers have always given both forms.
456
+ */
457
+ export function isDateFmt(fmt) {
458
+ if (!fmt) {
459
+ return false;
460
+ }
461
+ // Read the cached section directly: the readers ask this once per numeric cell.
462
+ const first = parseNumberFormat(fmt).sections[0];
463
+ return !first.hasText && (first.hasDate || first.hasTime);
464
+ }
465
+ /** Whether a format is `General` — or empty, which Excel treats the same way. */
466
+ export function isGeneralFormat(fmt) {
467
+ if (fmt === null || fmt === undefined || fmt.trim() === "") {
468
+ return true;
469
+ }
470
+ const { sections } = parseNumberFormat(fmt);
471
+ return (sections.length === 1 &&
472
+ sections[0].hasGeneral &&
473
+ sections[0].tokens.every(t => t.kind === "general" ||
474
+ t.kind === "color" ||
475
+ (t.kind === "locale" && t.symbol === "") ||
476
+ (t.kind === "literal" && t.text.trim() === "")));
477
+ }
@@ -163,76 +163,9 @@ export function validInt(value) {
163
163
  const i = typeof value === "number" ? value : parseInt(value, 10);
164
164
  return Number.isNaN(i) ? 0 : i;
165
165
  }
166
- /**
167
- * Split an Excel numFmt string by semicolons, respecting quoted strings and brackets.
168
- *
169
- * Excel numFmt can have up to 4 sections: `positive ; negative ; zero ; text`.
170
- * Semicolons inside `"..."` (literal text) or `[...]` (locale/color tags) must NOT
171
- * be treated as section separators.
172
- */
173
- export function splitFormatSections(fmt) {
174
- const sections = [];
175
- let current = "";
176
- let inQuote = false;
177
- let inBracket = false;
178
- for (let i = 0; i < fmt.length; i++) {
179
- const char = fmt[i];
180
- if (char === '"' && !inBracket) {
181
- inQuote = !inQuote;
182
- current += char;
183
- }
184
- else if (char === "[" && !inQuote) {
185
- inBracket = true;
186
- current += char;
187
- }
188
- else if (char === "]" && !inQuote) {
189
- inBracket = false;
190
- current += char;
191
- }
192
- else if (char === ";" && !inQuote && !inBracket) {
193
- sections.push(current);
194
- current = "";
195
- }
196
- else {
197
- current += char;
198
- }
199
- }
200
- sections.push(current);
201
- return sections;
202
- }
203
- /** Reusable regex — no capture groups, so safe for `test()`. */
204
- const DATE_FMT_RE = /[ymdhMsb]/;
205
- /** Strips bracket expressions `[...]` and quoted literals `"..."` from a format string. */
206
- const STRIP_BRACKETS_QUOTES_RE = /\[[^\]]*\]|"[^"]*"/g;
207
- /** Cache for isDateFmt results — typically only 5-20 unique formats per workbook,
208
- * but each may be tested hundreds of thousands of times during reconcile. */
209
- const _isDateFmtCache = new Map();
210
- export function isDateFmt(fmt) {
211
- if (!fmt) {
212
- return false;
213
- }
214
- const cached = _isDateFmtCache.get(fmt);
215
- if (cached !== undefined) {
216
- return cached;
217
- }
218
- // Only the first section (used for positive numbers / dates) determines
219
- // whether the format represents a date. The "@" text placeholder may
220
- // legitimately appear in later sections as a text fallback (e.g. "mm/dd/yyyy;@").
221
- const firstSection = splitFormatSections(fmt)[0];
222
- // Strip bracket expressions [...] (locale/color tags) and quoted literals "..."
223
- // before any further checks so that characters inside them are ignored.
224
- const clean = firstSection.replace(STRIP_BRACKETS_QUOTES_RE, "");
225
- // "@" in the cleaned section means it's a text format, not a date format.
226
- let result;
227
- if (clean.indexOf("@") > -1) {
228
- result = false;
229
- }
230
- else {
231
- result = DATE_FMT_RE.test(clean);
232
- }
233
- _isDateFmtCache.set(fmt, result);
234
- return result;
235
- }
166
+ // Number-format reading lives in one place; these names are kept here because every reader imports
167
+ // them from `@utils/utils`.
168
+ export { isDateFmt, splitFormatSections } from "./number-format.js";
236
169
  export function parseBoolean(value) {
237
170
  return value === true || value === "true" || value === 1 || value === "1";
238
171
  }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * documonster v0.20.1
2
+ * documonster v0.20.2
3
3
  * Zero-dependency TypeScript toolkit for Excel, Word, PDF, CSV, Markdown, XML & ZIP — one API across Node.js, Bun & browsers.
4
4
  * (c) 2025 cjnoname
5
5
  * Released under the Apache-2.0 License
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * documonster v0.20.1
2
+ * documonster v0.20.2
3
3
  * Zero-dependency TypeScript toolkit for Excel, Word, PDF, CSV, Markdown, XML & ZIP — one API across Node.js, Bun & browsers.
4
4
  * (c) 2025 cjnoname
5
5
  * Released under the Apache-2.0 License
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * documonster v0.20.1
2
+ * documonster v0.20.2
3
3
  * Zero-dependency TypeScript toolkit for Excel, Word, PDF, CSV, Markdown, XML & ZIP — one API across Node.js, Bun & browsers.
4
4
  * (c) 2025 cjnoname
5
5
  * Released under the Apache-2.0 License