@lotics/xlsx 0.1.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 (90) hide show
  1. package/package.json +30 -0
  2. package/src/auto_sum.test.ts +71 -0
  3. package/src/auto_sum.ts +83 -0
  4. package/src/canvas_cf.ts +135 -0
  5. package/src/canvas_chart.ts +687 -0
  6. package/src/canvas_fill.ts +156 -0
  7. package/src/canvas_images.ts +99 -0
  8. package/src/canvas_layout.test.ts +159 -0
  9. package/src/canvas_layout.ts +239 -0
  10. package/src/canvas_renderer.ts +1010 -0
  11. package/src/canvas_shape.ts +242 -0
  12. package/src/canvas_sparkline.ts +163 -0
  13. package/src/canvas_text.ts +454 -0
  14. package/src/cell_editor.ts +558 -0
  15. package/src/clipboard.test.ts +145 -0
  16. package/src/clipboard.ts +341 -0
  17. package/src/command_history.ts +102 -0
  18. package/src/csv_parser.test.ts +130 -0
  19. package/src/csv_parser.ts +172 -0
  20. package/src/data_validation.test.ts +95 -0
  21. package/src/data_validation.ts +114 -0
  22. package/src/editing_layer.test.ts +309 -0
  23. package/src/excel_cf_evaluate.test.ts +293 -0
  24. package/src/excel_cf_evaluate.ts +719 -0
  25. package/src/excel_cf_icons.ts +108 -0
  26. package/src/excel_cf_types.ts +67 -0
  27. package/src/excel_numfmt.test.ts +607 -0
  28. package/src/excel_numfmt.ts +1033 -0
  29. package/src/excel_parser.test.ts +1061 -0
  30. package/src/excel_parser.ts +393 -0
  31. package/src/excel_pattern_fills.ts +64 -0
  32. package/src/excel_table_styles.ts +160 -0
  33. package/src/excel_utils.test.ts +108 -0
  34. package/src/excel_utils.ts +162 -0
  35. package/src/fast-formula-parser.d.ts +53 -0
  36. package/src/fill_logic.ts +257 -0
  37. package/src/fill_patterns.test.ts +90 -0
  38. package/src/fill_patterns.ts +71 -0
  39. package/src/flash_fill.test.ts +75 -0
  40. package/src/flash_fill.ts +221 -0
  41. package/src/formula_deps.ts +189 -0
  42. package/src/formula_engine.test.ts +348 -0
  43. package/src/formula_engine.ts +401 -0
  44. package/src/formula_highlight.test.ts +81 -0
  45. package/src/formula_highlight.ts +139 -0
  46. package/src/formula_suggest.ts +100 -0
  47. package/src/hidden_sheets.test.ts +49 -0
  48. package/src/index.ts +149 -0
  49. package/src/keyboard_nav.test.ts +394 -0
  50. package/src/keyboard_nav.ts +891 -0
  51. package/src/move_logic.ts +63 -0
  52. package/src/numfmt_type.test.ts +158 -0
  53. package/src/numfmt_type.ts +231 -0
  54. package/src/ooxml_cell_format.ts +70 -0
  55. package/src/ooxml_chart.test.ts +85 -0
  56. package/src/ooxml_chart.ts +347 -0
  57. package/src/ooxml_chart_types.ts +207 -0
  58. package/src/ooxml_color.ts +339 -0
  59. package/src/ooxml_drawing.test.ts +87 -0
  60. package/src/ooxml_drawing.ts +287 -0
  61. package/src/ooxml_pivot.test.ts +195 -0
  62. package/src/ooxml_pivot.ts +468 -0
  63. package/src/ooxml_pivot_types.ts +165 -0
  64. package/src/ooxml_shape.ts +355 -0
  65. package/src/ooxml_sheet.ts +1271 -0
  66. package/src/ooxml_styles.ts +556 -0
  67. package/src/ooxml_table.test.ts +70 -0
  68. package/src/ooxml_table.ts +131 -0
  69. package/src/ooxml_workbook.test.ts +40 -0
  70. package/src/ooxml_workbook.ts +259 -0
  71. package/src/ooxml_zip.ts +33 -0
  72. package/src/pivot_model.ts +237 -0
  73. package/src/pivot_recompute.test.ts +210 -0
  74. package/src/pivot_recompute.ts +413 -0
  75. package/src/selection_model.ts +364 -0
  76. package/src/spreadsheet_commands.test.ts +772 -0
  77. package/src/spreadsheet_commands.ts +1805 -0
  78. package/src/structured_refs.test.ts +128 -0
  79. package/src/structured_refs.ts +160 -0
  80. package/src/style_helpers.test.ts +33 -0
  81. package/src/style_helpers.ts +30 -0
  82. package/src/template_builder.test.ts +139 -0
  83. package/src/template_builder.ts +36 -0
  84. package/src/types.ts +239 -0
  85. package/src/workbook_model.test.ts +536 -0
  86. package/src/workbook_model.ts +911 -0
  87. package/src/xlsx_round_trip.test.ts +279 -0
  88. package/src/xlsx_writer.test.ts +488 -0
  89. package/src/xlsx_writer.ts +1549 -0
  90. package/src/xml_entities.ts +23 -0
@@ -0,0 +1,468 @@
1
+ // =============================================================================
2
+ // OOXML Pivot Table Parser
3
+ // =============================================================================
4
+ //
5
+ // Parses pivot tables (`xl/pivotTables/*.xml`) and pivot caches
6
+ // (`xl/pivotCache/*.xml`). Coverage matches the 80% subset documented in
7
+ // `ooxml_pivot_types.ts`.
8
+ // =============================================================================
9
+
10
+ import type {
11
+ ParsedPivotCache,
12
+ ParsedPivotTable,
13
+ PivotAggregateFn,
14
+ PivotAxis,
15
+ PivotCacheField,
16
+ PivotCacheItem,
17
+ PivotCacheSource,
18
+ PivotDataField,
19
+ PivotDisplayOptions,
20
+ PivotFieldConfig,
21
+ } from "./ooxml_pivot_types";
22
+
23
+ // =============================================================================
24
+ // PivotTable parser
25
+ // =============================================================================
26
+
27
+ export function parsePivotTableXml(
28
+ xml: string,
29
+ parseXml: (xml: string) => Record<string, unknown>,
30
+ ): ParsedPivotTable {
31
+ const doc = parseXml(xml);
32
+ const root = (doc["pivotTableDefinition"] ?? doc) as Record<string, unknown>;
33
+
34
+ const name = strAttr(root, "name") ?? "PivotTable1";
35
+ const cacheId = intAttr(root, "cacheId") ?? 0;
36
+
37
+ const location = (root["location"] ?? {}) as Record<string, unknown>;
38
+ const ref = strAttr(location, "ref") ?? "A1:A1";
39
+ const firstHeaderRow = intAttr(location, "firstHeaderRow") ?? 0;
40
+ const firstDataCol = intAttr(location, "firstDataCol") ?? 0;
41
+ const firstDataRow = intAttr(location, "firstDataRow") ?? 1;
42
+
43
+ const pageFields = parsePageFields(root);
44
+ const pageFieldIndices = pageFields.map((p) => p.fld);
45
+ const fields = parsePivotFields(root, pageFields);
46
+ const rowFieldIndices = parseFieldIndexList(root, "rowFields");
47
+ const colFieldIndices = parseFieldIndexList(root, "colFields");
48
+ const dataFields = parseDataFields(root);
49
+ const display = parseDisplayOptions(root);
50
+
51
+ const styleInfo = root["pivotTableStyleInfo"];
52
+ const styleName =
53
+ styleInfo && typeof styleInfo === "object"
54
+ ? strAttr(styleInfo as Record<string, unknown>, "name")
55
+ : undefined;
56
+
57
+ return {
58
+ name,
59
+ cacheId,
60
+ ref,
61
+ firstHeaderRow,
62
+ firstDataCol,
63
+ firstDataRow,
64
+ fields,
65
+ rowFieldIndices,
66
+ colFieldIndices,
67
+ pageFieldIndices,
68
+ dataFields,
69
+ styleName,
70
+ display,
71
+ };
72
+ }
73
+
74
+ function parsePivotFields(
75
+ root: Record<string, unknown>,
76
+ pageFields: ParsedPageField[],
77
+ ): PivotFieldConfig[] {
78
+ const wrapper = root["pivotFields"];
79
+ if (!wrapper || typeof wrapper !== "object") return [];
80
+ const nodes = ensureArray(
81
+ (wrapper as Record<string, unknown>)["pivotField"],
82
+ );
83
+
84
+ // Map field index → selected page item so each pivot field that's on the
85
+ // page axis can carry its filter selection.
86
+ const pageSelectionByFld = new Map<number, number | null | undefined>();
87
+ for (const p of pageFields) pageSelectionByFld.set(p.fld, p.selectedItem);
88
+
89
+ const out: PivotFieldConfig[] = [];
90
+ for (let i = 0; i < nodes.length; i++) {
91
+ const node = nodes[i];
92
+ if (!node || typeof node !== "object") {
93
+ out.push(blankField());
94
+ continue;
95
+ }
96
+ const f = node as Record<string, unknown>;
97
+ const axisStr = strAttr(f, "axis");
98
+ out.push({
99
+ axis: axisFromOoxml(axisStr),
100
+ subtotalFunction: subtotalFromAttrs(f),
101
+ showSubtotal: boolAttr(f, "defaultSubtotal", true),
102
+ compact: boolAttr(f, "compact", true),
103
+ showAll: boolAttr(f, "showAll", false),
104
+ selectedPageItem: pageSelectionByFld.get(i),
105
+ });
106
+ }
107
+ return out;
108
+ }
109
+
110
+ function blankField(): PivotFieldConfig {
111
+ return {
112
+ showSubtotal: true,
113
+ compact: true,
114
+ showAll: false,
115
+ };
116
+ }
117
+
118
+ function axisFromOoxml(axis: string | undefined): PivotAxis | undefined {
119
+ switch (axis) {
120
+ case "axisRow":
121
+ return "row";
122
+ case "axisCol":
123
+ return "column";
124
+ case "axisPage":
125
+ return "page";
126
+ case "axisValues":
127
+ return "values";
128
+ default:
129
+ return undefined;
130
+ }
131
+ }
132
+
133
+ const SUBTOTAL_ATTR_TO_FN: Record<string, PivotAggregateFn> = {
134
+ sumSubtotal: "sum",
135
+ countSubtotal: "count",
136
+ countASubtotal: "count",
137
+ avgSubtotal: "average",
138
+ minSubtotal: "min",
139
+ maxSubtotal: "max",
140
+ productSubtotal: "product",
141
+ };
142
+
143
+ function subtotalFromAttrs(
144
+ f: Record<string, unknown>,
145
+ ): PivotAggregateFn | undefined {
146
+ for (const [attr, fn] of Object.entries(SUBTOTAL_ATTR_TO_FN)) {
147
+ if (boolAttr(f, attr, false)) return fn;
148
+ }
149
+ return undefined;
150
+ }
151
+
152
+ interface ParsedPageField {
153
+ fld: number;
154
+ /** Index into the cache items. `null` means "(All)" (no selection). */
155
+ selectedItem: number | null | undefined;
156
+ }
157
+
158
+ function parsePageItem(f: Record<string, unknown>): number | null | undefined {
159
+ // CT_PageField uses `@item` for the selected page item index. Absent or -1 → "(All)".
160
+ const v = f["@_item"];
161
+ if (v === undefined) return undefined;
162
+ if (typeof v === "number") return v < 0 ? null : v;
163
+ if (typeof v === "string") {
164
+ const n = parseInt(v, 10);
165
+ return Number.isFinite(n) ? (n < 0 ? null : n) : undefined;
166
+ }
167
+ return undefined;
168
+ }
169
+
170
+ function parseFieldIndexList(
171
+ root: Record<string, unknown>,
172
+ key: string,
173
+ ): number[] {
174
+ const wrapper = root[key];
175
+ if (!wrapper || typeof wrapper !== "object") return [];
176
+ const nodes = ensureArray((wrapper as Record<string, unknown>)["field"]);
177
+ const out: number[] = [];
178
+ for (const node of nodes) {
179
+ if (!node || typeof node !== "object") continue;
180
+ const idx = intAttr(node as Record<string, unknown>, "x");
181
+ if (idx !== undefined) out.push(idx);
182
+ }
183
+ return out;
184
+ }
185
+
186
+ function parsePageFields(root: Record<string, unknown>): ParsedPageField[] {
187
+ const wrapper = root["pageFields"];
188
+ if (!wrapper || typeof wrapper !== "object") return [];
189
+ const nodes = ensureArray((wrapper as Record<string, unknown>)["pageField"]);
190
+ const out: ParsedPageField[] = [];
191
+ for (const node of nodes) {
192
+ if (!node || typeof node !== "object") continue;
193
+ const f = node as Record<string, unknown>;
194
+ const idx = intAttr(f, "fld");
195
+ if (idx === undefined) continue;
196
+ out.push({ fld: idx, selectedItem: parsePageItem(f) });
197
+ }
198
+ return out;
199
+ }
200
+
201
+ const SUBTOTAL_ENUM_TO_FN: Record<string, PivotAggregateFn> = {
202
+ sum: "sum",
203
+ count: "count",
204
+ countNums: "countNums",
205
+ average: "average",
206
+ min: "min",
207
+ max: "max",
208
+ product: "product",
209
+ };
210
+
211
+ function parseDataFields(root: Record<string, unknown>): PivotDataField[] {
212
+ const wrapper = root["dataFields"];
213
+ if (!wrapper || typeof wrapper !== "object") return [];
214
+ const nodes = ensureArray((wrapper as Record<string, unknown>)["dataField"]);
215
+ const out: PivotDataField[] = [];
216
+ for (const node of nodes) {
217
+ if (!node || typeof node !== "object") continue;
218
+ const d = node as Record<string, unknown>;
219
+ const fieldIndex = intAttr(d, "fld");
220
+ if (fieldIndex === undefined) continue;
221
+ const subtotalRaw = strAttr(d, "subtotal") ?? "sum";
222
+ const subtotal = SUBTOTAL_ENUM_TO_FN[subtotalRaw] ?? "sum";
223
+ // OOXML CT_DataField writes a `numFmtId` referencing the styles table.
224
+ // Older / non-Excel writers occasionally emit a literal `numFmt` format
225
+ // string. Take whichever is present — `numFmtId` takes precedence.
226
+ const numFmt = strAttr(d, "numFmtId") ?? strAttr(d, "numFmt");
227
+ out.push({
228
+ name: strAttr(d, "name") ?? `Sum of field ${fieldIndex}`,
229
+ fieldIndex,
230
+ subtotal,
231
+ numFmt,
232
+ });
233
+ }
234
+ return out;
235
+ }
236
+
237
+ function parseDisplayOptions(
238
+ root: Record<string, unknown>,
239
+ ): PivotDisplayOptions {
240
+ // Grand totals live on the root element. Stripe flags live inside
241
+ // `<pivotTableStyleInfo>` (alongside the style name).
242
+ const styleInfo = root["pivotTableStyleInfo"];
243
+ const styleObj =
244
+ styleInfo && typeof styleInfo === "object"
245
+ ? (styleInfo as Record<string, unknown>)
246
+ : {};
247
+ return {
248
+ rowGrandTotals: boolAttr(root, "rowGrandTotals", true),
249
+ colGrandTotals: boolAttr(root, "colGrandTotals", true),
250
+ showRowStripes: boolAttr(styleObj, "showRowStripes", false),
251
+ showColStripes: boolAttr(styleObj, "showColStripes", false),
252
+ };
253
+ }
254
+
255
+ // =============================================================================
256
+ // PivotCache parser
257
+ // =============================================================================
258
+
259
+ export function parsePivotCacheDefinitionXml(
260
+ xml: string,
261
+ cacheId: number,
262
+ parseXml: (xml: string) => Record<string, unknown>,
263
+ ): ParsedPivotCache {
264
+ const doc = parseXml(xml);
265
+ const root = (doc["pivotCacheDefinition"] ?? doc) as Record<string, unknown>;
266
+
267
+ const refreshOnLoad = boolAttr(root, "refreshOnLoad", false);
268
+ const source = parseCacheSource(root);
269
+ const fields = parseCacheFields(root);
270
+
271
+ return { id: cacheId, source, fields, refreshOnLoad };
272
+ }
273
+
274
+ function parseCacheSource(root: Record<string, unknown>): PivotCacheSource {
275
+ const cacheSource = root["cacheSource"];
276
+ if (!cacheSource || typeof cacheSource !== "object") {
277
+ return { type: "external" };
278
+ }
279
+ const cs = cacheSource as Record<string, unknown>;
280
+ const sourceType = strAttr(cs, "type") ?? "worksheet";
281
+ if (sourceType === "consolidation") return { type: "consolidation" };
282
+ if (sourceType === "external") return { type: "external" };
283
+
284
+ const ws = cs["worksheetSource"];
285
+ if (!ws || typeof ws !== "object") return { type: "external" };
286
+ const w = ws as Record<string, unknown>;
287
+ const named = strAttr(w, "name");
288
+ if (named) return { type: "named", name: named };
289
+ const sheetName = strAttr(w, "sheet") ?? "";
290
+ const ref = strAttr(w, "ref") ?? "A1:A1";
291
+ return { type: "worksheet", sheetName, ref };
292
+ }
293
+
294
+ function parseCacheFields(root: Record<string, unknown>): PivotCacheField[] {
295
+ const wrapper = root["cacheFields"];
296
+ if (!wrapper || typeof wrapper !== "object") return [];
297
+ const nodes = ensureArray((wrapper as Record<string, unknown>)["cacheField"]);
298
+ const out: PivotCacheField[] = [];
299
+ for (const node of nodes) {
300
+ if (!node || typeof node !== "object") continue;
301
+ const f = node as Record<string, unknown>;
302
+ const name = strAttr(f, "name") ?? "";
303
+ const numFmtId = strAttr(f, "numFmtId");
304
+ const sharedItems = (f["sharedItems"] ?? {}) as Record<string, unknown>;
305
+ out.push({
306
+ name,
307
+ numFmt: numFmtId,
308
+ containsNumber: boolAttr(sharedItems, "containsNumber", false),
309
+ containsDate: boolAttr(sharedItems, "containsDate", false),
310
+ items: parseCacheItems(sharedItems),
311
+ });
312
+ }
313
+ return out;
314
+ }
315
+
316
+ function parseCacheItems(sharedItems: Record<string, unknown>): PivotCacheItem[] {
317
+ const out: PivotCacheItem[] = [];
318
+ // Each child of <sharedItems> is one of <s/>, <n/>, <b/>, <d/>, <e/>, <m/>
319
+ // and there can be zero or many of each interleaved. fast-xml-parser
320
+ // surfaces these as separate keys, so we walk every recognised key.
321
+ const sNodes = ensureArray(sharedItems["s"]);
322
+ for (const n of sNodes) {
323
+ const v = strAttr(asObj(n), "v");
324
+ if (v !== undefined) out.push({ kind: "string", value: v });
325
+ }
326
+ const nNodes = ensureArray(sharedItems["n"]);
327
+ for (const n of nNodes) {
328
+ const v = strAttr(asObj(n), "v");
329
+ const num = v !== undefined ? Number(v) : NaN;
330
+ if (Number.isFinite(num)) out.push({ kind: "number", value: num });
331
+ }
332
+ const bNodes = ensureArray(sharedItems["b"]);
333
+ for (const n of bNodes) {
334
+ const v = strAttr(asObj(n), "v");
335
+ out.push({ kind: "boolean", value: v === "1" || v === "true" });
336
+ }
337
+ const dNodes = ensureArray(sharedItems["d"]);
338
+ for (const n of dNodes) {
339
+ const v = strAttr(asObj(n), "v");
340
+ if (v !== undefined) out.push({ kind: "date", value: v });
341
+ }
342
+ const eNodes = ensureArray(sharedItems["e"]);
343
+ for (const n of eNodes) {
344
+ const v = strAttr(asObj(n), "v");
345
+ out.push({ kind: "error", value: v ?? "#N/A" });
346
+ }
347
+ const mNodes = ensureArray(sharedItems["m"]);
348
+ for (const _n of mNodes) {
349
+ out.push({ kind: "missing" });
350
+ }
351
+ return out;
352
+ }
353
+
354
+ // =============================================================================
355
+ // Relationship helpers
356
+ // =============================================================================
357
+
358
+ /**
359
+ * From a sheet's rels file, return rId → pivot table xml path.
360
+ * Caller resolves the path to a parser input.
361
+ */
362
+ export function extractPivotTableRIds(
363
+ rels: Record<string, unknown>,
364
+ ): Map<string, string> {
365
+ const result = new Map<string, string>();
366
+ const relationships = rels["Relationships"];
367
+ if (!relationships || typeof relationships !== "object") return result;
368
+
369
+ const relNodes = ensureArray(
370
+ (relationships as Record<string, unknown>)["Relationship"],
371
+ );
372
+ for (const rel of relNodes) {
373
+ if (!rel || typeof rel !== "object") continue;
374
+ const r = rel as Record<string, unknown>;
375
+ const type = strAttr(r, "Type");
376
+ if (type?.includes("/pivotTable")) {
377
+ const id = strAttr(r, "Id");
378
+ const target = strAttr(r, "Target");
379
+ if (id && target) result.set(id, target);
380
+ }
381
+ }
382
+ return result;
383
+ }
384
+
385
+ /**
386
+ * From a pivot table's rels file, return the pivotCacheDefinition path.
387
+ * Pivot tables reference exactly one cache.
388
+ */
389
+ export function extractPivotCacheTarget(
390
+ rels: Record<string, unknown>,
391
+ ): string | undefined {
392
+ const relationships = rels["Relationships"];
393
+ if (!relationships || typeof relationships !== "object") return undefined;
394
+ const relNodes = ensureArray(
395
+ (relationships as Record<string, unknown>)["Relationship"],
396
+ );
397
+ for (const rel of relNodes) {
398
+ if (!rel || typeof rel !== "object") continue;
399
+ const r = rel as Record<string, unknown>;
400
+ const type = strAttr(r, "Type");
401
+ if (type?.includes("/pivotCacheDefinition")) {
402
+ return strAttr(r, "Target");
403
+ }
404
+ }
405
+ return undefined;
406
+ }
407
+
408
+ /**
409
+ * From `xl/workbook.xml`, return cacheId → pivotCacheDefinition rId.
410
+ * Both attributes live on `<pivotCache>` elements inside `<pivotCaches>`.
411
+ */
412
+ export function extractWorkbookPivotCaches(
413
+ workbook: Record<string, unknown>,
414
+ ): Map<number, string> {
415
+ const out = new Map<number, string>();
416
+ const wb = (workbook["workbook"] ?? workbook) as Record<string, unknown>;
417
+ const caches = wb["pivotCaches"];
418
+ if (!caches || typeof caches !== "object") return out;
419
+ const nodes = ensureArray((caches as Record<string, unknown>)["pivotCache"]);
420
+ for (const n of nodes) {
421
+ if (!n || typeof n !== "object") continue;
422
+ const c = n as Record<string, unknown>;
423
+ const cacheId = intAttr(c, "cacheId");
424
+ // r:id stored as "@_r:id" by fast-xml-parser when colon namespaces preserved.
425
+ const rid = strAttr(c, "r:id") ?? strAttr(c, "rId");
426
+ if (cacheId !== undefined && rid) out.set(cacheId, rid);
427
+ }
428
+ return out;
429
+ }
430
+
431
+ // =============================================================================
432
+ // Helpers
433
+ // =============================================================================
434
+
435
+ function strAttr(obj: Record<string, unknown>, attr: string): string | undefined {
436
+ const val = obj[`@_${attr}`];
437
+ if (typeof val === "string") return val;
438
+ if (typeof val === "number") return String(val);
439
+ return undefined;
440
+ }
441
+
442
+ function intAttr(obj: Record<string, unknown>, attr: string): number | undefined {
443
+ const v = strAttr(obj, attr);
444
+ if (v === undefined) return undefined;
445
+ const n = parseInt(v, 10);
446
+ return Number.isFinite(n) ? n : undefined;
447
+ }
448
+
449
+ function boolAttr(
450
+ obj: Record<string, unknown>,
451
+ attr: string,
452
+ fallback: boolean,
453
+ ): boolean {
454
+ const v = strAttr(obj, attr);
455
+ if (v === undefined) return fallback;
456
+ if (v === "1" || v === "true") return true;
457
+ if (v === "0" || v === "false") return false;
458
+ return fallback;
459
+ }
460
+
461
+ function ensureArray(val: unknown): unknown[] {
462
+ if (val === undefined || val === null) return [];
463
+ return Array.isArray(val) ? val : [val];
464
+ }
465
+
466
+ function asObj(val: unknown): Record<string, unknown> {
467
+ return val && typeof val === "object" ? (val as Record<string, unknown>) : {};
468
+ }
@@ -0,0 +1,165 @@
1
+ // =============================================================================
2
+ // OOXML Pivot Table Types
3
+ // =============================================================================
4
+ //
5
+ // Types for parsed pivot tables (`xl/pivotTables/*.xml`) and their backing
6
+ // caches (`xl/pivotCache/*.xml`). The model is intentionally close to the
7
+ // OOXML shape so the writer can round-trip without heroic translation.
8
+ //
9
+ // Coverage targets the 80% subset:
10
+ // - row / column / value / page (filter) axes
11
+ // - subtotal aggregations: sum, count, average, min, max, product, countNums
12
+ // - source range from the same workbook (worksheetSource)
13
+ // - cached items: strings, numbers, dates, booleans, errors, missing
14
+ // - layout flags (banded rows/cols, grand totals, default style)
15
+ //
16
+ // Out of scope for this phase:
17
+ // - calculated fields (`<calculatedField>`)
18
+ // - calculated items
19
+ // - OLAP / cube sources (`<cacheHierarchies>`)
20
+ // - automatic date/time grouping
21
+ // - "showDataAs" non-default value transforms (% of total, running total, etc.)
22
+ // =============================================================================
23
+
24
+ /** Top-level pivot table parsed from `xl/pivotTables/pivotTableN.xml`. */
25
+ export interface ParsedPivotTable {
26
+ /** `name` attribute. Excel-unique, used as anchor for r:embed lookups. */
27
+ name: string;
28
+ /** `cacheId` attribute, links to a pivotCacheDefinition by id. */
29
+ cacheId: number;
30
+ /** Bounding range on the host sheet, e.g. "A3:E12". */
31
+ ref: string;
32
+ /** Number of header rows in `ref` (defaults to 1). */
33
+ firstHeaderRow: number;
34
+ /** Column offset within `ref` where data starts. */
35
+ firstDataCol: number;
36
+ /** Row offset within `ref` where data starts. */
37
+ firstDataRow: number;
38
+ /** Per-field configuration ordered by cache-field index. */
39
+ fields: PivotFieldConfig[];
40
+ /** Indices into `fields` placed on the row axis, in order. */
41
+ rowFieldIndices: number[];
42
+ /** Indices into `fields` placed on the column axis, in order. */
43
+ colFieldIndices: number[];
44
+ /** Indices into `fields` placed on the page (filter) axis, in order. */
45
+ pageFieldIndices: number[];
46
+ /** One entry per displayed value. References a cache field index. */
47
+ dataFields: PivotDataField[];
48
+ /** Optional table style name (e.g. "PivotStyleLight16"). */
49
+ styleName?: string;
50
+ /** Display flags. */
51
+ display: PivotDisplayOptions;
52
+ }
53
+
54
+ /**
55
+ * Configuration for a single source field. Each entry corresponds 1:1 with
56
+ * a `cacheField` in the pivot cache. Order matches cache order.
57
+ */
58
+ export interface PivotFieldConfig {
59
+ /** Where this field appears, if anywhere. */
60
+ axis?: PivotAxis;
61
+ /** Subtotal function applied when the field is on the row/col axis. */
62
+ subtotalFunction?: PivotAggregateFn;
63
+ /** Whether the field is configured to show subtotals. Default true. */
64
+ showSubtotal: boolean;
65
+ /** Whether the user has compacted this field in the layout. */
66
+ compact: boolean;
67
+ /** Whether to show all items (vs. only items with data). */
68
+ showAll: boolean;
69
+ /** When on the page axis, the chosen filter item index, or null for "(All)". */
70
+ selectedPageItem?: number | null;
71
+ }
72
+
73
+ /** A field placed on the values area, e.g. "Sum of Sales". */
74
+ export interface PivotDataField {
75
+ /** Display name shown in the layout (e.g. "Sum of Sales"). */
76
+ name: string;
77
+ /** Index into the pivot table's field list (= cache field index). */
78
+ fieldIndex: number;
79
+ /** Aggregation. */
80
+ subtotal: PivotAggregateFn;
81
+ /** Optional number format applied to the result cells. */
82
+ numFmt?: string;
83
+ }
84
+
85
+ /** Where a field appears in the pivot layout. */
86
+ export type PivotAxis = "row" | "column" | "page" | "values";
87
+
88
+ /**
89
+ * Aggregation functions supported in this phase. Maps directly to OOXML
90
+ * `subtotal`/`subtotalFunction` enum values.
91
+ */
92
+ export type PivotAggregateFn =
93
+ | "sum"
94
+ | "count"
95
+ | "countNums"
96
+ | "average"
97
+ | "min"
98
+ | "max"
99
+ | "product";
100
+
101
+ /** Layout / display flags. */
102
+ export interface PivotDisplayOptions {
103
+ /** Show grand total row (column-axis aggregate). Default true. */
104
+ rowGrandTotals: boolean;
105
+ /** Show grand total column (row-axis aggregate). Default true. */
106
+ colGrandTotals: boolean;
107
+ /** Banded rows (alternating fill). */
108
+ showRowStripes: boolean;
109
+ /** Banded columns (alternating fill). */
110
+ showColStripes: boolean;
111
+ }
112
+
113
+ /** Pivot cache parsed from `xl/pivotCache/pivotCacheDefinitionN.xml`. */
114
+ export interface ParsedPivotCache {
115
+ /** Cache id used by pivot tables to reference this cache. */
116
+ id: number;
117
+ /** The source range that feeds the pivot. */
118
+ source: PivotCacheSource;
119
+ /** Cached field metadata. Index matches `pivotTable.fields[]` order. */
120
+ fields: PivotCacheField[];
121
+ /** Whether the cache requires a refresh on next open (refreshOnLoad). */
122
+ refreshOnLoad: boolean;
123
+ }
124
+
125
+ /** What feeds the cache. We support same-workbook ranges and named refs. */
126
+ export type PivotCacheSource =
127
+ | { type: "worksheet"; sheetName: string; ref: string }
128
+ | { type: "named"; name: string }
129
+ | { type: "external" }
130
+ | { type: "consolidation" };
131
+
132
+ /** One source-column's cached metadata. */
133
+ export interface PivotCacheField {
134
+ /** Field caption (column header text). */
135
+ name: string;
136
+ /** Distinct items observed in this column. Order is the canonical sort
137
+ * order Excel uses when laying out the field on row/col axis. */
138
+ items: PivotCacheItem[];
139
+ /** When the column is purely numeric, may carry a number format. */
140
+ numFmt?: string;
141
+ /** True when the column type is numeric (containsNumber). */
142
+ containsNumber: boolean;
143
+ /** True when the column contains date serials. */
144
+ containsDate: boolean;
145
+ }
146
+
147
+ /** A discrete value in a cache field. */
148
+ export type PivotCacheItem =
149
+ | { kind: "string"; value: string }
150
+ | { kind: "number"; value: number }
151
+ | { kind: "boolean"; value: boolean }
152
+ | { kind: "date"; value: string } // ISO 8601
153
+ | { kind: "error"; value: string }
154
+ | { kind: "missing" };
155
+
156
+ /**
157
+ * Map from sheet path (e.g. "xl/worksheets/sheet1.xml") to the pivot tables
158
+ * placed on that sheet, alongside the workbook's caches indexed by cacheId.
159
+ */
160
+ export interface PivotBundle {
161
+ /** Pivot tables keyed by host sheet relative path. */
162
+ pivotsBySheet: Map<string, ParsedPivotTable[]>;
163
+ /** All pivot caches discovered, keyed by cacheId. */
164
+ caches: Map<number, ParsedPivotCache>;
165
+ }