@cj-tech-master/excelts 9.3.1 → 9.4.0-canary.20260421000114.3ec32dd

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 (147) hide show
  1. package/dist/browser/index.browser.d.ts +1 -0
  2. package/dist/browser/index.browser.js +1 -0
  3. package/dist/browser/index.d.ts +2 -0
  4. package/dist/browser/index.js +3 -0
  5. package/dist/browser/modules/excel/cell.d.ts +19 -0
  6. package/dist/browser/modules/excel/cell.js +21 -0
  7. package/dist/browser/modules/excel/note.d.ts +3 -23
  8. package/dist/browser/modules/excel/note.js +8 -2
  9. package/dist/browser/modules/excel/utils/cell-format.js +85 -13
  10. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +17 -5
  11. package/dist/browser/modules/excel/utils/ooxml-paths.js +46 -19
  12. package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
  13. package/dist/browser/modules/excel/workbook.browser.js +49 -0
  14. package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
  15. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
  16. package/dist/browser/modules/excel/xlsx/xform/comment/comments-xform.d.ts +8 -0
  17. package/dist/browser/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
  18. package/dist/browser/modules/excel/xlsx/xform/comment/vml-client-data-xform.d.ts +6 -0
  19. package/dist/browser/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
  20. package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
  21. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +54 -14
  22. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +3 -3
  23. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +16 -15
  24. package/dist/browser/modules/formula/compile/binder.js +48 -6
  25. package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
  26. package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
  27. package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
  28. package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
  29. package/dist/browser/modules/formula/functions/_shared.js +47 -0
  30. package/dist/browser/modules/formula/functions/conditional.js +103 -22
  31. package/dist/browser/modules/formula/functions/date.js +105 -23
  32. package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
  33. package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
  34. package/dist/browser/modules/formula/functions/engineering.js +103 -151
  35. package/dist/browser/modules/formula/functions/financial.js +210 -184
  36. package/dist/browser/modules/formula/functions/lookup.js +224 -157
  37. package/dist/browser/modules/formula/functions/math.d.ts +26 -0
  38. package/dist/browser/modules/formula/functions/math.js +249 -69
  39. package/dist/browser/modules/formula/functions/statistical.js +221 -171
  40. package/dist/browser/modules/formula/functions/text.js +112 -52
  41. package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
  42. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
  43. package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
  44. package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
  45. package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
  46. package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
  47. package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
  48. package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
  49. package/dist/browser/modules/formula/runtime/values.js +20 -2
  50. package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
  51. package/dist/browser/modules/formula/syntax/ast.js +1 -0
  52. package/dist/browser/modules/formula/syntax/parser.js +29 -7
  53. package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
  54. package/dist/browser/modules/formula/syntax/token-types.js +9 -0
  55. package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
  56. package/dist/cjs/index.js +9 -2
  57. package/dist/cjs/modules/excel/cell.js +21 -0
  58. package/dist/cjs/modules/excel/note.js +8 -2
  59. package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
  60. package/dist/cjs/modules/excel/utils/ooxml-paths.js +49 -24
  61. package/dist/cjs/modules/excel/workbook.browser.js +49 -0
  62. package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
  63. package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
  64. package/dist/cjs/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
  65. package/dist/cjs/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
  66. package/dist/cjs/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
  67. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +53 -13
  68. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +15 -14
  69. package/dist/cjs/modules/formula/compile/binder.js +48 -6
  70. package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
  71. package/dist/cjs/modules/formula/functions/_shared.js +48 -0
  72. package/dist/cjs/modules/formula/functions/conditional.js +103 -22
  73. package/dist/cjs/modules/formula/functions/date.js +104 -22
  74. package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
  75. package/dist/cjs/modules/formula/functions/engineering.js +109 -157
  76. package/dist/cjs/modules/formula/functions/financial.js +209 -183
  77. package/dist/cjs/modules/formula/functions/lookup.js +224 -157
  78. package/dist/cjs/modules/formula/functions/math.js +254 -70
  79. package/dist/cjs/modules/formula/functions/statistical.js +222 -172
  80. package/dist/cjs/modules/formula/functions/text.js +112 -52
  81. package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
  82. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
  83. package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
  84. package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
  85. package/dist/cjs/modules/formula/runtime/values.js +21 -2
  86. package/dist/cjs/modules/formula/syntax/parser.js +29 -7
  87. package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
  88. package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
  89. package/dist/esm/index.browser.js +1 -0
  90. package/dist/esm/index.js +3 -0
  91. package/dist/esm/modules/excel/cell.js +21 -0
  92. package/dist/esm/modules/excel/note.js +8 -2
  93. package/dist/esm/modules/excel/utils/cell-format.js +85 -13
  94. package/dist/esm/modules/excel/utils/ooxml-paths.js +46 -19
  95. package/dist/esm/modules/excel/workbook.browser.js +49 -0
  96. package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
  97. package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
  98. package/dist/esm/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
  99. package/dist/esm/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
  100. package/dist/esm/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
  101. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +54 -14
  102. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +16 -15
  103. package/dist/esm/modules/formula/compile/binder.js +48 -6
  104. package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
  105. package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
  106. package/dist/esm/modules/formula/functions/_shared.js +47 -0
  107. package/dist/esm/modules/formula/functions/conditional.js +103 -22
  108. package/dist/esm/modules/formula/functions/date.js +105 -23
  109. package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
  110. package/dist/esm/modules/formula/functions/engineering.js +103 -151
  111. package/dist/esm/modules/formula/functions/financial.js +210 -184
  112. package/dist/esm/modules/formula/functions/lookup.js +224 -157
  113. package/dist/esm/modules/formula/functions/math.js +249 -69
  114. package/dist/esm/modules/formula/functions/statistical.js +221 -171
  115. package/dist/esm/modules/formula/functions/text.js +112 -52
  116. package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
  117. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
  118. package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
  119. package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
  120. package/dist/esm/modules/formula/runtime/values.js +20 -2
  121. package/dist/esm/modules/formula/syntax/ast.js +1 -0
  122. package/dist/esm/modules/formula/syntax/parser.js +29 -7
  123. package/dist/esm/modules/formula/syntax/token-types.js +9 -0
  124. package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
  125. package/dist/iife/excelts.iife.js +3576 -3360
  126. package/dist/iife/excelts.iife.js.map +1 -1
  127. package/dist/iife/excelts.iife.min.js +34 -34
  128. package/dist/types/index.browser.d.ts +1 -0
  129. package/dist/types/index.d.ts +2 -0
  130. package/dist/types/modules/excel/cell.d.ts +19 -0
  131. package/dist/types/modules/excel/note.d.ts +3 -23
  132. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +17 -5
  133. package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
  134. package/dist/types/modules/excel/xlsx/xform/comment/comments-xform.d.ts +8 -0
  135. package/dist/types/modules/excel/xlsx/xform/comment/vml-client-data-xform.d.ts +6 -0
  136. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +3 -3
  137. package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
  138. package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
  139. package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
  140. package/dist/types/modules/formula/functions/math.d.ts +26 -0
  141. package/dist/types/modules/formula/materialize/types.d.ts +15 -0
  142. package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
  143. package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
  144. package/dist/types/modules/formula/runtime/values.d.ts +13 -0
  145. package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
  146. package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
  147. package/package.json +4 -4
@@ -11,6 +11,7 @@ export { Range } from "./modules/excel/range.js";
11
11
  export { Image } from "./modules/excel/image.js";
12
12
  export * from "./modules/excel/anchor.js";
13
13
  export { Table } from "./modules/excel/table.js";
14
+ export { Note } from "./modules/excel/note.js";
14
15
  export { DataValidations } from "./modules/excel/data-validations.js";
15
16
  export { FormCheckbox } from "./modules/excel/form-control.js";
16
17
  export * from "./modules/excel/enums.js";
@@ -14,6 +14,7 @@ export { Range } from "./modules/excel/range.js";
14
14
  export { Image } from "./modules/excel/image.js";
15
15
  export * from "./modules/excel/anchor.js";
16
16
  export { Table } from "./modules/excel/table.js";
17
+ export { Note } from "./modules/excel/note.js";
17
18
  export { DataValidations } from "./modules/excel/data-validations.js";
18
19
  export { FormCheckbox } from "./modules/excel/form-control.js";
19
20
  // Note: the formula engine lives at the `./formula` subpath so it stays
@@ -7,6 +7,7 @@ export { Range } from "./modules/excel/range.js";
7
7
  export { Image } from "./modules/excel/image.js";
8
8
  export * from "./modules/excel/anchor.js";
9
9
  export { Table } from "./modules/excel/table.js";
10
+ export { Note } from "./modules/excel/note.js";
10
11
  export { DataValidations } from "./modules/excel/data-validations.js";
11
12
  export { FormCheckbox } from "./modules/excel/form-control.js";
12
13
  export { WorkbookWriter } from "./modules/excel/stream/workbook-writer.js";
@@ -35,6 +36,7 @@ export type { NodeInput } from "./modules/excel/stream/workbook-reader.js";
35
36
  export { decodeCol, encodeCol, decodeRow, encodeRow, decodeCell, encodeCell, decodeRange, encodeRange } from "./modules/excel/utils/address.js";
36
37
  export type { CellAddress, SheetRange, Origin } from "./modules/excel/utils/address.js";
37
38
  export type { SheetToJSONOptions, AddJSONOptions, AddAOAOptions } from "./modules/excel/worksheet.js";
39
+ export { getCellDisplayText, formatCellValue, isDateDisplayFormat } from "./modules/excel/utils/cell-format.js";
38
40
  export { dateToExcel, excelToDate } from "./utils/utils.base.js";
39
41
  export { base64ToUint8Array, uint8ArrayToBase64 } from "./utils/utils.base.js";
40
42
  export { xmlEncode, xmlDecode } from "./modules/xml/encode.js";
@@ -10,6 +10,7 @@ export { Range } from "./modules/excel/range.js";
10
10
  export { Image } from "./modules/excel/image.js";
11
11
  export * from "./modules/excel/anchor.js";
12
12
  export { Table } from "./modules/excel/table.js";
13
+ export { Note } from "./modules/excel/note.js";
13
14
  export { DataValidations } from "./modules/excel/data-validations.js";
14
15
  export { FormCheckbox } from "./modules/excel/form-control.js";
15
16
  // Note: the formula engine lives at the `./formula` subpath so it stays
@@ -45,6 +46,8 @@ export { DefinedNames } from "./modules/excel/defined-names.js";
45
46
  // =============================================================================
46
47
  // Cell address encoding/decoding (0-indexed)
47
48
  export { decodeCol, encodeCol, decodeRow, encodeRow, decodeCell, encodeCell, decodeRange, encodeRange } from "./modules/excel/utils/address.js";
49
+ // Cell display-text helpers (apply numFmt to produce an Excel-style string)
50
+ export { getCellDisplayText, formatCellValue, isDateDisplayFormat } from "./modules/excel/utils/cell-format.js";
48
51
  // Date conversion (Excel serial dates <-> JS Date)
49
52
  export { dateToExcel, excelToDate } from "./utils/utils.base.js";
50
53
  // Base64 utilities (cross-platform)
@@ -47,6 +47,7 @@ export interface NoteConfig {
47
47
  export interface NoteModel {
48
48
  type: string;
49
49
  note: NoteConfig;
50
+ author?: string;
50
51
  }
51
52
  export interface CellModel {
52
53
  address: string;
@@ -130,6 +131,24 @@ declare class Cell {
130
131
  get comment(): Note | undefined;
131
132
  set comment(comment: Note | NoteConfig | undefined);
132
133
  get text(): string;
134
+ /**
135
+ * The cell's display text — the value formatted the way Excel would render
136
+ * it, applying the cell's `numFmt`. For a Date cell with `numFmt` `"mm-dd-yy"`,
137
+ * this returns e.g. `"04-12-19"` rather than the JS `Date.prototype.toString()`
138
+ * output you'd get from `cell.text`.
139
+ *
140
+ * Handles primitive values, dates, and formula results. For rich text,
141
+ * hyperlinks, errors, and other complex types, falls back to `cell.text`.
142
+ *
143
+ * Note: numFmt codes that are locale-dependent in Excel (e.g. built-in
144
+ * numFmtId 14 renders as `dd.mm.yyyy` under German locale but is stored
145
+ * as `mm-dd-yy`) are applied literally — excelts does not perform
146
+ * Excel's locale-based format substitution. If you need a specific date
147
+ * style across cells regardless of per-cell numFmts, call the exported
148
+ * {@link getCellDisplayText} helper with a `dateFormat` argument, or use
149
+ * `worksheet.toJSON({ dateFormat })`.
150
+ */
151
+ get displayText(): string;
133
152
  get html(): string;
134
153
  toString(): string;
135
154
  get formula(): string | undefined;
@@ -1,6 +1,7 @@
1
1
  import { Enums } from "./enums.js";
2
2
  import { ExcelError, InvalidValueTypeError } from "./errors.js";
3
3
  import { Note } from "./note.js";
4
+ import { getCellDisplayText } from "./utils/cell-format.js";
4
5
  import { colCache } from "./utils/col-cache.js";
5
6
  import { copyStyle } from "./utils/copy-style.js";
6
7
  import { slideFormula } from "./utils/shared-formula.js";
@@ -271,6 +272,26 @@ class Cell {
271
272
  get text() {
272
273
  return this._value.toString();
273
274
  }
275
+ /**
276
+ * The cell's display text — the value formatted the way Excel would render
277
+ * it, applying the cell's `numFmt`. For a Date cell with `numFmt` `"mm-dd-yy"`,
278
+ * this returns e.g. `"04-12-19"` rather than the JS `Date.prototype.toString()`
279
+ * output you'd get from `cell.text`.
280
+ *
281
+ * Handles primitive values, dates, and formula results. For rich text,
282
+ * hyperlinks, errors, and other complex types, falls back to `cell.text`.
283
+ *
284
+ * Note: numFmt codes that are locale-dependent in Excel (e.g. built-in
285
+ * numFmtId 14 renders as `dd.mm.yyyy` under German locale but is stored
286
+ * as `mm-dd-yy`) are applied literally — excelts does not perform
287
+ * Excel's locale-based format substitution. If you need a specific date
288
+ * style across cells regardless of per-cell numFmts, call the exported
289
+ * {@link getCellDisplayText} helper with a `dateFormat` argument, or use
290
+ * `worksheet.toJSON({ dateFormat })`.
291
+ */
292
+ get displayText() {
293
+ return getCellDisplayText(this);
294
+ }
274
295
  get html() {
275
296
  return escapeHtml(this.text);
276
297
  }
@@ -1,29 +1,9 @@
1
- import type { Font } from "./types.js";
2
- interface NoteText {
3
- text: string;
4
- font?: Partial<Font>;
5
- }
6
- interface NoteConfig {
7
- margins?: {
8
- insetmode?: string;
9
- inset?: number[];
10
- };
11
- protection?: {
12
- locked?: string;
13
- lockText?: string;
14
- };
15
- editAs?: string;
16
- texts?: NoteText[];
17
- anchor?: string;
18
- }
19
- interface NoteModel {
20
- type: string;
21
- note: NoteConfig;
22
- }
1
+ import type { NoteConfig, NoteModel } from "./cell.js";
23
2
  declare class Note {
24
3
  note: string | NoteConfig | undefined;
4
+ author: string | undefined;
25
5
  static readonly DEFAULT_CONFIGS: NoteModel;
26
- constructor(note?: string | NoteConfig);
6
+ constructor(note?: string | NoteConfig, author?: string);
27
7
  get model(): NoteModel;
28
8
  set model(value: NoteModel);
29
9
  static fromModel(model: NoteModel): Note;
@@ -1,7 +1,8 @@
1
1
  import { deepMerge } from "./utils/under-dash.js";
2
2
  class Note {
3
- constructor(note) {
3
+ constructor(note, author) {
4
4
  this.note = note;
5
+ this.author = author;
5
6
  }
6
7
  get model() {
7
8
  let value;
@@ -26,7 +27,11 @@ class Note {
26
27
  break;
27
28
  }
28
29
  // Suitable for all cell comments
29
- return deepMerge({}, Note.DEFAULT_CONFIGS, value);
30
+ const result = deepMerge({}, Note.DEFAULT_CONFIGS, value);
31
+ if (this.author !== undefined) {
32
+ result.author = this.author;
33
+ }
34
+ return result;
30
35
  }
31
36
  set model(value) {
32
37
  const { note } = value;
@@ -37,6 +42,7 @@ class Note {
37
42
  else {
38
43
  this.note = note;
39
44
  }
45
+ this.author = value.author;
40
46
  }
41
47
  static fromModel(model) {
42
48
  const note = new Note();
@@ -20,7 +20,7 @@ const TABLE_FMT = {
20
20
  11: "0.00E+00",
21
21
  12: "# ?/?",
22
22
  13: "# ??/??",
23
- 14: "m/d/yy",
23
+ 14: "mm-dd-yy",
24
24
  15: "d-mmm-yy",
25
25
  16: "d-mmm",
26
26
  17: "mmm-yy",
@@ -210,6 +210,56 @@ const MONTHS_LONG = [
210
210
  const MONTHS_LETTER = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
211
211
  const DAYS_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
212
212
  const DAYS_LONG = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
213
+ /**
214
+ * Disambiguate each `mm` occurrence in a format string that has already been
215
+ * placeholder-substituted for the other date/time tokens.
216
+ *
217
+ * Excel's rule: `mm` is minutes when it's adjacent to an hour or seconds
218
+ * token (with no intervening date tokens); otherwise it's a zero-padded
219
+ * month. This must be decided per occurrence — a single format string can
220
+ * contain both roles (e.g. `"yyyy-mm-dd hh:mm:ss"`).
221
+ *
222
+ * The caller has already replaced `yyyy`/`yy` → `Y4/Y2`, month-name tokens
223
+ * `mmmmm/mmmm/mmm` → `MN5/MN4/MN3`, `dd`/`d` → `D2/D1`, `hh`/`h` → `H2/H1`,
224
+ * `ss`/`s` → `S2/S1`. So any remaining literal `mm` substrings here are
225
+ * ambiguous between minute and month.
226
+ *
227
+ * Returns the input with each `mm` replaced by either `\x00MI2\x00` (minutes)
228
+ * or `\x00M2\x00` (month, zero-padded).
229
+ */
230
+ function resolveMonthOrMinute(s) {
231
+ // Tokens that, when present between an `mm` and a time anchor, break the
232
+ // "adjacent time context" chain and push the `mm` back into month-land.
233
+ const DATE_TOKEN = /\x00(?:Y[24]|D[12]|MN[345])\x00/;
234
+ const HOUR_TOKEN = /\x00H[12]\x00/g;
235
+ const SEC_TOKEN = /\x00S[12]\x00/g;
236
+ let out = "";
237
+ let work = s;
238
+ let idx = work.search(/mm/i);
239
+ while (idx !== -1) {
240
+ const before = work.slice(0, idx);
241
+ const after = work.slice(idx + 2);
242
+ // Find the *nearest* hour token preceding this `mm` (scan from the right).
243
+ let nearestHourIdx = -1;
244
+ let m;
245
+ HOUR_TOKEN.lastIndex = 0;
246
+ while ((m = HOUR_TOKEN.exec(before)) !== null) {
247
+ nearestHourIdx = m.index;
248
+ }
249
+ // Find the *nearest* seconds token following this `mm`.
250
+ SEC_TOKEN.lastIndex = 0;
251
+ const secMatch = SEC_TOKEN.exec(after);
252
+ const nearestSecIdx = secMatch ? secMatch.index : -1;
253
+ const hourInRange = nearestHourIdx !== -1 && !DATE_TOKEN.test(before.slice(nearestHourIdx));
254
+ const secInRange = nearestSecIdx !== -1 && !DATE_TOKEN.test(after.slice(0, nearestSecIdx));
255
+ const isMinutes = hourInRange || secInRange;
256
+ out += before + (isMinutes ? "\x00MI2\x00" : "\x00M2\x00");
257
+ work = after;
258
+ idx = work.search(/mm/i);
259
+ }
260
+ out += work;
261
+ return out;
262
+ }
213
263
  /**
214
264
  * Format a date value using Excel date format
215
265
  * @param serial Excel serial number (days since 1900-01-01)
@@ -270,16 +320,14 @@ function formatDate(serial, fmt) {
270
320
  // Seconds (before mm to avoid confusion)
271
321
  result = result.replace(/ss/gi, "\x00S2\x00");
272
322
  result = result.replace(/\bs\b/gi, "\x00S1\x00");
273
- // Minutes/Month mm - context dependent
274
- // If near h or s, it's minutes; otherwise month
275
- // For simplicity, check if we already have hour tokens nearby
276
- const hasTimeContext = /\x00H[12]\x00.*mm|mm.*\x00S[12]\x00/i.test(result);
277
- if (hasTimeContext) {
278
- result = result.replace(/mm/gi, "\x00MI2\x00");
279
- }
280
- else {
281
- result = result.replace(/mm/gi, "\x00M2\x00");
282
- }
323
+ // Minutes/Month `mm` — position-dependent. Excel treats `mm` as minutes
324
+ // when the nearest neighboring time-token is an hour (before) or a
325
+ // seconds token (after); otherwise it's month. This must be decided **per
326
+ // occurrence**, because a single format string can contain both roles —
327
+ // e.g. in `"yyyy-mm-dd hh:mm:ss"` the first `mm` is month and the second
328
+ // is minutes. A single global `hasTimeContext` flag would miscategorise
329
+ // all `mm` as minutes in such mixed formats.
330
+ result = resolveMonthOrMinute(result);
283
331
  result = result.replace(/\bm\b/gi, "\x00M1\x00");
284
332
  // AM/PM
285
333
  result = result.replace(/AM\/PM/gi, "\x00AMPM\x00");
@@ -887,6 +935,16 @@ export function isDateDisplayFormat(fmt) {
887
935
  }
888
936
  return false;
889
937
  }
938
+ /**
939
+ * Default format applied to Date values whose numFmt is `General` or empty.
940
+ *
941
+ * Excel itself substitutes a locale-dependent short date in this case (US:
942
+ * `m/d/yyyy`). We pick an ISO-like `yyyy-mm-dd` so consumers who never set a
943
+ * `numFmt` still get a sensible, unambiguous rendering instead of the raw
944
+ * Excel serial number.
945
+ */
946
+ const DEFAULT_DATE_FORMAT = "yyyy-mm-dd";
947
+ const DEFAULT_DATETIME_FORMAT = "yyyy-mm-dd hh:mm:ss";
890
948
  /**
891
949
  * Format a value according to the given format string.
892
950
  * Handles Date objects with timezone-independent Excel serial conversion.
@@ -901,8 +959,22 @@ export function formatCellValue(value, fmt, dateFormat) {
901
959
  }
902
960
  return format(fmt, serial);
903
961
  }
904
- const actualFmt = dateFormat && isDateDisplayFormat(fmt) ? dateFormat : fmt;
905
- return format(actualFmt, serial);
962
+ // For Date values whose numFmt is missing or General, Excel substitutes a
963
+ // default short-date format. Without this, `format("General", serial)`
964
+ // would emit the raw Excel serial (e.g. "43567") — almost never what the
965
+ // caller wants. Pick a datetime-aware default based on whether the value
966
+ // carries a non-midnight time component.
967
+ let effectiveFmt;
968
+ if (dateFormat && isDateDisplayFormat(fmt)) {
969
+ effectiveFmt = dateFormat;
970
+ }
971
+ else if (!fmt || isGeneral(fmt)) {
972
+ effectiveFmt = serial % 1 === 0 ? DEFAULT_DATE_FORMAT : DEFAULT_DATETIME_FORMAT;
973
+ }
974
+ else {
975
+ effectiveFmt = fmt;
976
+ }
977
+ return format(effectiveFmt, serial);
906
978
  }
907
979
  return format(fmt, value);
908
980
  }
@@ -23,7 +23,11 @@ export declare function getDrawingNameFromPath(path: string): string | undefined
23
23
  export declare function getDrawingNameFromRelsPath(path: string): string | undefined;
24
24
  export declare function getVmlDrawingNameFromPath(path: string): string | undefined;
25
25
  export declare function getVmlDrawingHFNameFromPath(path: string): string | undefined;
26
- export declare function getCommentsIndexFromPath(path: string): string | undefined;
26
+ /**
27
+ * Check if a zip entry path is a comments XML file.
28
+ * Works for both `xl/comments1.xml` and `xl/comments/comment1.xml`.
29
+ */
30
+ export declare function isCommentsPath(path: string): boolean;
27
31
  export declare function getTableNameFromPath(path: string): string | undefined;
28
32
  export declare function getPivotTableNameFromPath(path: string): string | undefined;
29
33
  export declare function getPivotTableNameFromRelsPath(path: string): string | undefined;
@@ -59,6 +63,7 @@ export declare function pivotCacheDefinitionRelsPath(n: number | string): string
59
63
  export declare function pivotCacheRecordsPath(n: number | string): string;
60
64
  export declare function pivotCacheRecordsRelTarget(n: number | string): string;
61
65
  export declare function pivotTablePath(n: number | string): string;
66
+ export declare function pivotTablePathFromName(name: string): string;
62
67
  export declare function pivotTableRelsPath(n: number | string): string;
63
68
  export declare function externalLinkPath(n: number | string): string;
64
69
  export declare function externalLinkRelsPath(n: number | string): string;
@@ -80,12 +85,19 @@ export declare function commentsRelTargetFromWorksheet(sheetId: number | string)
80
85
  export declare function vmlDrawingRelTargetFromWorksheet(sheetId: number | string): string;
81
86
  export declare function vmlDrawingHFRelTargetFromWorksheet(sheetId: number | string): string;
82
87
  export declare function drawingRelTargetFromWorksheet(drawingName: string): string;
83
- export declare function vmlDrawingRelTargetFromWorksheetName(vmlName: string): string;
84
- export declare function commentsRelTargetFromWorksheetName(commentName: string): string;
85
88
  export declare function pivotTableRelTargetFromWorksheet(n: number | string): string;
86
- export declare function pivotTableRelTargetFromWorksheetName(pivotName: string): string;
87
89
  export declare function tableRelTargetFromWorksheet(target: string): string;
88
- export declare function tableRelTargetFromWorksheetName(name: string): string;
89
90
  export declare function mediaRelTargetFromRels(filename: string): string;
90
91
  export declare function ctrlPropPath(id: number | string): string;
91
92
  export declare function ctrlPropRelTargetFromWorksheet(id: number | string): string;
93
+ /**
94
+ * Resolve a relationship Target (relative or absolute) to a normalized zip path.
95
+ *
96
+ * OOXML relationship targets may be:
97
+ * - Relative: `../comments1.xml` (resolved against `baseDir`)
98
+ * - Absolute: `/xl/comments/comment1.xml` (leading slash stripped)
99
+ *
100
+ * @param baseDir The directory containing the source part (e.g. `xl/worksheets/`)
101
+ * @param target The raw Target value from the .rels file
102
+ */
103
+ export declare function resolveRelTarget(baseDir: string, target: string): string;
@@ -19,7 +19,9 @@ const drawingXmlRegex = /^xl\/drawings\/(drawing\d+)[.]xml$/;
19
19
  const drawingRelsXmlRegex = /^xl\/drawings\/_rels\/(drawing\d+)[.]xml[.]rels$/;
20
20
  const vmlDrawingRegex = /^xl\/drawings\/(vmlDrawing\d+)[.]vml$/;
21
21
  const vmlDrawingHFRegex = /^xl\/drawings\/(vmlDrawingHF\d+)[.]vml$/;
22
- const commentsXmlRegex = /^xl\/comments(\d+)[.]xml$/;
22
+ // Matches both flat layout (xl/comments1.xml) and subdirectory layout (xl/comments/comment1.xml).
23
+ // Both are valid OOXML — the actual path is determined by .rels, not by convention.
24
+ const commentsXmlRegex = /^xl\/(?:comments(\d+)|comments\/comment(\d+))[.]xml$/;
23
25
  const tableXmlRegex = /^xl\/tables\/(table\d+)[.]xml$/;
24
26
  const pivotTableXmlRegex = /^xl\/pivotTables\/(pivotTable\d+)[.]xml$/;
25
27
  const pivotTableRelsXmlRegex = /^xl\/pivotTables\/_rels\/(pivotTable\d+)[.]xml[.]rels$/;
@@ -81,9 +83,12 @@ export function getVmlDrawingHFNameFromPath(path) {
81
83
  const match = vmlDrawingHFRegex.exec(path);
82
84
  return match ? match[1] : undefined;
83
85
  }
84
- export function getCommentsIndexFromPath(path) {
85
- const match = commentsXmlRegex.exec(path);
86
- return match ? match[1] : undefined;
86
+ /**
87
+ * Check if a zip entry path is a comments XML file.
88
+ * Works for both `xl/comments1.xml` and `xl/comments/comment1.xml`.
89
+ */
90
+ export function isCommentsPath(path) {
91
+ return commentsXmlRegex.test(path);
87
92
  }
88
93
  export function getTableNameFromPath(path) {
89
94
  const match = tableXmlRegex.exec(path);
@@ -184,6 +189,9 @@ export function pivotCacheRecordsRelTarget(n) {
184
189
  export function pivotTablePath(n) {
185
190
  return `xl/pivotTables/pivotTable${n}.xml`;
186
191
  }
192
+ export function pivotTablePathFromName(name) {
193
+ return `xl/pivotTables/${name}.xml`;
194
+ }
187
195
  export function pivotTableRelsPath(n) {
188
196
  return `xl/pivotTables/_rels/pivotTable${n}.xml.rels`;
189
197
  }
@@ -232,29 +240,14 @@ export function drawingRelTargetFromWorksheet(drawingName) {
232
240
  // Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
233
241
  return `../drawings/${drawingName}.xml`;
234
242
  }
235
- export function vmlDrawingRelTargetFromWorksheetName(vmlName) {
236
- // For VML drawings when the caller already has the logical name (e.g. "vmlDrawing1").
237
- return `../drawings/${vmlName}.vml`;
238
- }
239
- export function commentsRelTargetFromWorksheetName(commentName) {
240
- // For comments when the caller already has the logical name (e.g. "comments1").
241
- return `../${commentName}.xml`;
242
- }
243
243
  export function pivotTableRelTargetFromWorksheet(n) {
244
244
  // Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
245
245
  return `../pivotTables/pivotTable${n}.xml`;
246
246
  }
247
- export function pivotTableRelTargetFromWorksheetName(pivotName) {
248
- // For pivot tables when the caller already has the logical name (e.g. "pivotTable1").
249
- return `../pivotTables/${pivotName}.xml`;
250
- }
251
247
  export function tableRelTargetFromWorksheet(target) {
252
248
  // Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
253
249
  return `../tables/${target}`;
254
250
  }
255
- export function tableRelTargetFromWorksheetName(name) {
256
- return `../tables/${name}.xml`;
257
- }
258
251
  export function mediaRelTargetFromRels(filename) {
259
252
  // Target from a rels file located under xl/*/_rels (base is one level deeper than xl/)
260
253
  return `../media/${filename}`;
@@ -267,3 +260,37 @@ export function ctrlPropRelTargetFromWorksheet(id) {
267
260
  // Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
268
261
  return `../ctrlProps/ctrlProp${id}.xml`;
269
262
  }
263
+ /**
264
+ * Resolve a relationship Target (relative or absolute) to a normalized zip path.
265
+ *
266
+ * OOXML relationship targets may be:
267
+ * - Relative: `../comments1.xml` (resolved against `baseDir`)
268
+ * - Absolute: `/xl/comments/comment1.xml` (leading slash stripped)
269
+ *
270
+ * @param baseDir The directory containing the source part (e.g. `xl/worksheets/`)
271
+ * @param target The raw Target value from the .rels file
272
+ */
273
+ export function resolveRelTarget(baseDir, target) {
274
+ // Absolute target — strip leading slash to get the zip path.
275
+ if (target.startsWith("/")) {
276
+ return target.slice(1);
277
+ }
278
+ // Ensure baseDir ends with "/" so the join works correctly.
279
+ const base = baseDir.endsWith("/") ? baseDir : baseDir + "/";
280
+ // Relative target — resolve against baseDir.
281
+ // Simple resolution: join base + target, then resolve `.` and `..` segments.
282
+ const parts = (base + target).split("/");
283
+ const resolved = [];
284
+ for (const part of parts) {
285
+ if (part === "." || part === "") {
286
+ continue;
287
+ }
288
+ else if (part === "..") {
289
+ resolved.pop();
290
+ }
291
+ else {
292
+ resolved.push(part);
293
+ }
294
+ }
295
+ return resolved.join("/");
296
+ }
@@ -603,6 +603,63 @@ declare class Workbook {
603
603
  * ```
604
604
  */
605
605
  calculateFormulas(): void;
606
+ /**
607
+ * Per-workbook registry of user-defined functions. The formula engine
608
+ * consults this map before the built-in 433-function registry, so a
609
+ * registered name either adds a new function (`MYFN`) or shadows a
610
+ * built-in (`IRR` → project-specific variant).
611
+ *
612
+ * Populated by {@link registerFunction}; read by the formula engine
613
+ * when the host calls `calculateFormulas()` — see
614
+ * `@formula/runtime/evaluator.ts::evaluateCall`.
615
+ */
616
+ userFunctions?: Map<string, {
617
+ minArity: number;
618
+ maxArity: number;
619
+ invoke: (args: unknown[]) => unknown;
620
+ volatile?: boolean;
621
+ }>;
622
+ /**
623
+ * Register (or replace) a custom formula function on this workbook.
624
+ *
625
+ * The function becomes visible to `calculateFormulas()` on this
626
+ * workbook only — the built-in registry stays untouched. Names are
627
+ * case-insensitive (normalised to uppercase) and must not include
628
+ * the `_XLFN.` prefix — the engine strips that automatically.
629
+ *
630
+ * @param name Function name (case-insensitive).
631
+ * @param fn Implementation. Receives already-evaluated RuntimeValue
632
+ * arguments; return a RuntimeValue. Wrap failures with
633
+ * `rvError("#VALUE!")` rather than throwing — throws are
634
+ * caught at the evaluator boundary and surface as
635
+ * `#VALUE!` so a buggy custom function doesn't tear
636
+ * down the whole calculation pass.
637
+ * @param options Optional arity bounds. Defaults to `minArity=0`,
638
+ * `maxArity=255` (Excel's universal argument cap), so
639
+ * simple variadic functions work without extra config.
640
+ * Set `volatile: true` when the function should be
641
+ * re-evaluated on every calc cycle (analogous to
642
+ * built-in `RAND`, `NOW`). Currently reserved for
643
+ * future use; the engine recomputes every formula on
644
+ * each `calculateFormulas()` call regardless.
645
+ *
646
+ * ```ts
647
+ * import { rvNumber } from "@cj-tech-master/excelts/formula";
648
+ * workbook.registerFunction("DOUBLE", ([x]) => {
649
+ * return rvNumber((x as any).value * 2);
650
+ * }, { minArity: 1, maxArity: 1 });
651
+ * ```
652
+ */
653
+ registerFunction(name: string, fn: (args: unknown[]) => unknown, options?: {
654
+ minArity?: number;
655
+ maxArity?: number;
656
+ volatile?: boolean;
657
+ }): void;
658
+ /**
659
+ * Remove a user-registered function. No-op when the name isn't
660
+ * registered; returns `true` when an entry was removed.
661
+ */
662
+ unregisterFunction(name: string): boolean;
606
663
  clearThemes(): void;
607
664
  /**
608
665
  * Add Image to Workbook and return the id
@@ -1060,6 +1060,55 @@ class Workbook {
1060
1060
  calculateFormulas() {
1061
1061
  invokeFormulaEngine(this);
1062
1062
  }
1063
+ /**
1064
+ * Register (or replace) a custom formula function on this workbook.
1065
+ *
1066
+ * The function becomes visible to `calculateFormulas()` on this
1067
+ * workbook only — the built-in registry stays untouched. Names are
1068
+ * case-insensitive (normalised to uppercase) and must not include
1069
+ * the `_XLFN.` prefix — the engine strips that automatically.
1070
+ *
1071
+ * @param name Function name (case-insensitive).
1072
+ * @param fn Implementation. Receives already-evaluated RuntimeValue
1073
+ * arguments; return a RuntimeValue. Wrap failures with
1074
+ * `rvError("#VALUE!")` rather than throwing — throws are
1075
+ * caught at the evaluator boundary and surface as
1076
+ * `#VALUE!` so a buggy custom function doesn't tear
1077
+ * down the whole calculation pass.
1078
+ * @param options Optional arity bounds. Defaults to `minArity=0`,
1079
+ * `maxArity=255` (Excel's universal argument cap), so
1080
+ * simple variadic functions work without extra config.
1081
+ * Set `volatile: true` when the function should be
1082
+ * re-evaluated on every calc cycle (analogous to
1083
+ * built-in `RAND`, `NOW`). Currently reserved for
1084
+ * future use; the engine recomputes every formula on
1085
+ * each `calculateFormulas()` call regardless.
1086
+ *
1087
+ * ```ts
1088
+ * import { rvNumber } from "@cj-tech-master/excelts/formula";
1089
+ * workbook.registerFunction("DOUBLE", ([x]) => {
1090
+ * return rvNumber((x as any).value * 2);
1091
+ * }, { minArity: 1, maxArity: 1 });
1092
+ * ```
1093
+ */
1094
+ registerFunction(name, fn, options) {
1095
+ if (!this.userFunctions) {
1096
+ this.userFunctions = new Map();
1097
+ }
1098
+ this.userFunctions.set(name.toUpperCase(), {
1099
+ minArity: options?.minArity ?? 0,
1100
+ maxArity: options?.maxArity ?? 255,
1101
+ invoke: fn,
1102
+ volatile: options?.volatile ?? false
1103
+ });
1104
+ }
1105
+ /**
1106
+ * Remove a user-registered function. No-op when the name isn't
1107
+ * registered; returns `true` when an entry was removed.
1108
+ */
1109
+ unregisterFunction(name) {
1110
+ return this.userFunctions?.delete(name.toUpperCase()) ?? false;
1111
+ }
1063
1112
  // ===========================================================================
1064
1113
  // Themes
1065
1114
  // ===========================================================================
@@ -17,7 +17,7 @@ const defaultNumFormats = {
17
17
  19: { f: "h:mm:ss AM/PM" },
18
18
  20: { f: "h:mm" },
19
19
  21: { f: "h:mm:ss" },
20
- 22: { f: 'm/d/yy "h":mm' },
20
+ 22: { f: "m/d/yy h:mm" },
21
21
  27: {
22
22
  "zh-tw": "[$-404]e/m/d",
23
23
  "zh-cn": 'yyyy"年"m"月"',
@@ -75,8 +75,8 @@ const defaultNumFormats = {
75
75
  },
76
76
  37: { f: "#,##0 ;(#,##0)" },
77
77
  38: { f: "#,##0 ;[Red](#,##0)" },
78
- 39: { f: "#,##0.00 ;(#,##0.00)" },
79
- 40: { f: "#,##0.00 ;[Red](#,##0.00)" },
78
+ 39: { f: "#,##0.00;(#,##0.00)" },
79
+ 40: { f: "#,##0.00;[Red](#,##0.00)" },
80
80
  45: { f: "mm:ss" },
81
81
  46: { f: "[h]:mm:ss" },
82
82
  47: { f: "mmss.0" },
@@ -18,7 +18,7 @@ class CommentXform extends BaseXform {
18
18
  const renderModel = model || this.model;
19
19
  xmlStream.openNode("comment", {
20
20
  ref: renderModel.ref,
21
- authorId: 0
21
+ authorId: renderModel.authorId ?? 0
22
22
  });
23
23
  xmlStream.openNode("text");
24
24
  if (renderModel && renderModel.note && renderModel.note.texts) {
@@ -41,7 +41,8 @@ class CommentXform extends BaseXform {
41
41
  note: {
42
42
  texts: []
43
43
  },
44
- ...node.attributes
44
+ ref: node.attributes.ref,
45
+ authorId: node.attributes.authorId != null ? Number(node.attributes.authorId) : undefined
45
46
  };
46
47
  return true;
47
48
  case "r":
@@ -8,6 +8,14 @@ declare class CommentsXform extends BaseXform<CommentsModel> {
8
8
  [key: string]: CommentXform;
9
9
  };
10
10
  parser: any;
11
+ /** Authors collected while parsing the <authors> element. */
12
+ private _authors;
13
+ /** Whether we are currently inside the <authors> element. */
14
+ private _inAuthors;
15
+ /** Whether we are currently inside an <author> element (collecting text). */
16
+ private _inAuthor;
17
+ /** Accumulator for the current <author> text content. */
18
+ private _currentAuthor;
11
19
  constructor();
12
20
  render(xmlStream: any, model?: CommentsModel): void;
13
21
  parseOpen(node: any): boolean;