@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.
- package/package.json +30 -0
- package/src/auto_sum.test.ts +71 -0
- package/src/auto_sum.ts +83 -0
- package/src/canvas_cf.ts +135 -0
- package/src/canvas_chart.ts +687 -0
- package/src/canvas_fill.ts +156 -0
- package/src/canvas_images.ts +99 -0
- package/src/canvas_layout.test.ts +159 -0
- package/src/canvas_layout.ts +239 -0
- package/src/canvas_renderer.ts +1010 -0
- package/src/canvas_shape.ts +242 -0
- package/src/canvas_sparkline.ts +163 -0
- package/src/canvas_text.ts +454 -0
- package/src/cell_editor.ts +558 -0
- package/src/clipboard.test.ts +145 -0
- package/src/clipboard.ts +341 -0
- package/src/command_history.ts +102 -0
- package/src/csv_parser.test.ts +130 -0
- package/src/csv_parser.ts +172 -0
- package/src/data_validation.test.ts +95 -0
- package/src/data_validation.ts +114 -0
- package/src/editing_layer.test.ts +309 -0
- package/src/excel_cf_evaluate.test.ts +293 -0
- package/src/excel_cf_evaluate.ts +719 -0
- package/src/excel_cf_icons.ts +108 -0
- package/src/excel_cf_types.ts +67 -0
- package/src/excel_numfmt.test.ts +607 -0
- package/src/excel_numfmt.ts +1033 -0
- package/src/excel_parser.test.ts +1061 -0
- package/src/excel_parser.ts +393 -0
- package/src/excel_pattern_fills.ts +64 -0
- package/src/excel_table_styles.ts +160 -0
- package/src/excel_utils.test.ts +108 -0
- package/src/excel_utils.ts +162 -0
- package/src/fast-formula-parser.d.ts +53 -0
- package/src/fill_logic.ts +257 -0
- package/src/fill_patterns.test.ts +90 -0
- package/src/fill_patterns.ts +71 -0
- package/src/flash_fill.test.ts +75 -0
- package/src/flash_fill.ts +221 -0
- package/src/formula_deps.ts +189 -0
- package/src/formula_engine.test.ts +348 -0
- package/src/formula_engine.ts +401 -0
- package/src/formula_highlight.test.ts +81 -0
- package/src/formula_highlight.ts +139 -0
- package/src/formula_suggest.ts +100 -0
- package/src/hidden_sheets.test.ts +49 -0
- package/src/index.ts +149 -0
- package/src/keyboard_nav.test.ts +394 -0
- package/src/keyboard_nav.ts +891 -0
- package/src/move_logic.ts +63 -0
- package/src/numfmt_type.test.ts +158 -0
- package/src/numfmt_type.ts +231 -0
- package/src/ooxml_cell_format.ts +70 -0
- package/src/ooxml_chart.test.ts +85 -0
- package/src/ooxml_chart.ts +347 -0
- package/src/ooxml_chart_types.ts +207 -0
- package/src/ooxml_color.ts +339 -0
- package/src/ooxml_drawing.test.ts +87 -0
- package/src/ooxml_drawing.ts +287 -0
- package/src/ooxml_pivot.test.ts +195 -0
- package/src/ooxml_pivot.ts +468 -0
- package/src/ooxml_pivot_types.ts +165 -0
- package/src/ooxml_shape.ts +355 -0
- package/src/ooxml_sheet.ts +1271 -0
- package/src/ooxml_styles.ts +556 -0
- package/src/ooxml_table.test.ts +70 -0
- package/src/ooxml_table.ts +131 -0
- package/src/ooxml_workbook.test.ts +40 -0
- package/src/ooxml_workbook.ts +259 -0
- package/src/ooxml_zip.ts +33 -0
- package/src/pivot_model.ts +237 -0
- package/src/pivot_recompute.test.ts +210 -0
- package/src/pivot_recompute.ts +413 -0
- package/src/selection_model.ts +364 -0
- package/src/spreadsheet_commands.test.ts +772 -0
- package/src/spreadsheet_commands.ts +1805 -0
- package/src/structured_refs.test.ts +128 -0
- package/src/structured_refs.ts +160 -0
- package/src/style_helpers.test.ts +33 -0
- package/src/style_helpers.ts +30 -0
- package/src/template_builder.test.ts +139 -0
- package/src/template_builder.ts +36 -0
- package/src/types.ts +239 -0
- package/src/workbook_model.test.ts +536 -0
- package/src/workbook_model.ts +911 -0
- package/src/xlsx_round_trip.test.ts +279 -0
- package/src/xlsx_writer.test.ts +488 -0
- package/src/xlsx_writer.ts +1549 -0
- package/src/xml_entities.ts +23 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// OOXML Drawing Parser
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Parses xl/drawings/drawingN.xml files to extract:
|
|
6
|
+
// - Two-cell anchors with EMU offsets
|
|
7
|
+
// - Chart references (rId → chart path)
|
|
8
|
+
// - Shape/textbox/connector objects
|
|
9
|
+
// - Image references
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
import type { DrawingAnchor, ParsedDrawing } from "./ooxml_chart_types";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export interface DrawingRelation {
|
|
19
|
+
type: "chart" | "image" | "shape";
|
|
20
|
+
anchor: DrawingAnchor;
|
|
21
|
+
rId?: string;
|
|
22
|
+
drawing?: ParsedDrawing;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Parser
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse a drawing XML file and extract all anchor objects.
|
|
31
|
+
*/
|
|
32
|
+
export function parseDrawingXml(
|
|
33
|
+
xml: string,
|
|
34
|
+
parseXml: (xml: string) => Record<string, unknown>,
|
|
35
|
+
): DrawingRelation[] {
|
|
36
|
+
const doc = parseXml(xml);
|
|
37
|
+
const wsDrawing = doc["xdr:wsDr"] ?? doc["wsDr"] ?? doc;
|
|
38
|
+
if (!wsDrawing || typeof wsDrawing !== "object") return [];
|
|
39
|
+
|
|
40
|
+
const results: DrawingRelation[] = [];
|
|
41
|
+
const obj = wsDrawing as Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
// Two-cell anchors
|
|
44
|
+
const twoCellAnchors = ensureArray(obj["xdr:twoCellAnchor"] ?? obj["twoCellAnchor"]);
|
|
45
|
+
for (const anchorNode of twoCellAnchors) {
|
|
46
|
+
if (!anchorNode || typeof anchorNode !== "object") continue;
|
|
47
|
+
const anchor = parseTwoCellAnchor(anchorNode as Record<string, unknown>);
|
|
48
|
+
if (!anchor) continue;
|
|
49
|
+
|
|
50
|
+
const a = anchorNode as Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
// Check for chart frame
|
|
53
|
+
const graphicFrame = a["xdr:graphicFrame"] ?? a["graphicFrame"];
|
|
54
|
+
if (graphicFrame && typeof graphicFrame === "object") {
|
|
55
|
+
const rId = extractChartRId(graphicFrame as Record<string, unknown>);
|
|
56
|
+
if (rId) {
|
|
57
|
+
results.push({ type: "chart", anchor, rId });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for shape
|
|
63
|
+
const sp = a["xdr:sp"] ?? a["sp"];
|
|
64
|
+
if (sp && typeof sp === "object") {
|
|
65
|
+
const drawing = parseShapeNode(sp as Record<string, unknown>, anchor);
|
|
66
|
+
results.push({ type: "shape", anchor, drawing });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for picture (image)
|
|
71
|
+
const pic = a["xdr:pic"] ?? a["pic"];
|
|
72
|
+
if (pic && typeof pic === "object") {
|
|
73
|
+
const rId = extractPicRId(pic as Record<string, unknown>);
|
|
74
|
+
if (rId) {
|
|
75
|
+
results.push({ type: "image", anchor, rId });
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for connection shape (connector)
|
|
81
|
+
const cxnSp = a["xdr:cxnSp"] ?? a["cxnSp"];
|
|
82
|
+
if (cxnSp && typeof cxnSp === "object") {
|
|
83
|
+
const drawing = parseConnectorNode(cxnSp as Record<string, unknown>, anchor);
|
|
84
|
+
results.push({ type: "shape", anchor, drawing });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// One-cell anchors (typically for images)
|
|
89
|
+
const oneCellAnchors = ensureArray(obj["xdr:oneCellAnchor"] ?? obj["oneCellAnchor"]);
|
|
90
|
+
for (const anchorNode of oneCellAnchors) {
|
|
91
|
+
if (!anchorNode || typeof anchorNode !== "object") continue;
|
|
92
|
+
const a = anchorNode as Record<string, unknown>;
|
|
93
|
+
|
|
94
|
+
const from = parseAnchorPoint(a, "xdr:from", "from");
|
|
95
|
+
if (!from) continue;
|
|
96
|
+
|
|
97
|
+
// Use extent for width/height
|
|
98
|
+
const anchor: DrawingAnchor = {
|
|
99
|
+
from,
|
|
100
|
+
to: { row: from.row + 5, col: from.col + 3 }, // approximate
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const pic = a["xdr:pic"] ?? a["pic"];
|
|
104
|
+
if (pic && typeof pic === "object") {
|
|
105
|
+
const rId = extractPicRId(pic as Record<string, unknown>);
|
|
106
|
+
if (rId) {
|
|
107
|
+
results.push({ type: "image", anchor, rId });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// Anchor parsing
|
|
117
|
+
// =============================================================================
|
|
118
|
+
|
|
119
|
+
function parseTwoCellAnchor(node: Record<string, unknown>): DrawingAnchor | undefined {
|
|
120
|
+
const from = parseAnchorPoint(node, "xdr:from", "from");
|
|
121
|
+
const to = parseAnchorPoint(node, "xdr:to", "to");
|
|
122
|
+
if (!from || !to) return undefined;
|
|
123
|
+
return { from, to };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseAnchorPoint(
|
|
127
|
+
node: Record<string, unknown>,
|
|
128
|
+
key1: string,
|
|
129
|
+
key2: string,
|
|
130
|
+
): DrawingAnchor["from"] | undefined {
|
|
131
|
+
const pt = node[key1] ?? node[key2];
|
|
132
|
+
if (!pt || typeof pt !== "object") return undefined;
|
|
133
|
+
const o = pt as Record<string, unknown>;
|
|
134
|
+
|
|
135
|
+
const col = parseIntVal(o["xdr:col"] ?? o["col"]);
|
|
136
|
+
const row = parseIntVal(o["xdr:row"] ?? o["row"]);
|
|
137
|
+
const colOffset = parseIntVal(o["xdr:colOff"] ?? o["colOff"]);
|
|
138
|
+
const rowOffset = parseIntVal(o["xdr:rowOff"] ?? o["rowOff"]);
|
|
139
|
+
|
|
140
|
+
if (col === undefined || row === undefined) return undefined;
|
|
141
|
+
return { row, col, rowOffset, colOffset };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Shape parsing
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
function parseShapeNode(sp: Record<string, unknown>, anchor: DrawingAnchor): ParsedDrawing {
|
|
149
|
+
const spPr = sp["xdr:spPr"] ?? sp["spPr"] ?? {};
|
|
150
|
+
const txBody = sp["xdr:txBody"] ?? sp["txBody"];
|
|
151
|
+
|
|
152
|
+
// Geometry
|
|
153
|
+
const prstGeom = (spPr as Record<string, unknown>)["a:prstGeom"]
|
|
154
|
+
?? (spPr as Record<string, unknown>)["prstGeom"];
|
|
155
|
+
const geometry = prstGeom && typeof prstGeom === "object"
|
|
156
|
+
? ((prstGeom as Record<string, unknown>)["@_prst"] as string | undefined) ?? "rect"
|
|
157
|
+
: "rect";
|
|
158
|
+
|
|
159
|
+
// Fill
|
|
160
|
+
const fillColor = extractFillColor(spPr as Record<string, unknown>);
|
|
161
|
+
const outlineColor = extractOutlineColor(spPr as Record<string, unknown>);
|
|
162
|
+
|
|
163
|
+
// Text
|
|
164
|
+
const text = extractTextContent(txBody);
|
|
165
|
+
|
|
166
|
+
const isTextBox = text !== undefined && geometry === "rect";
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
type: isTextBox ? "textbox" : "shape",
|
|
170
|
+
geometry,
|
|
171
|
+
text,
|
|
172
|
+
fillColor,
|
|
173
|
+
outlineColor,
|
|
174
|
+
anchor,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseConnectorNode(cxnSp: Record<string, unknown>, anchor: DrawingAnchor): ParsedDrawing {
|
|
179
|
+
const spPr = cxnSp["xdr:spPr"] ?? cxnSp["spPr"] ?? {};
|
|
180
|
+
const outlineColor = extractOutlineColor(spPr as Record<string, unknown>);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
type: "connector",
|
|
184
|
+
geometry: "line",
|
|
185
|
+
outlineColor,
|
|
186
|
+
anchor,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Color extraction
|
|
192
|
+
// =============================================================================
|
|
193
|
+
|
|
194
|
+
function extractFillColor(spPr: Record<string, unknown>): string | undefined {
|
|
195
|
+
const solidFill = spPr["a:solidFill"] ?? spPr["solidFill"];
|
|
196
|
+
if (solidFill && typeof solidFill === "object") {
|
|
197
|
+
return extractSrgb(solidFill as Record<string, unknown>);
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function extractOutlineColor(spPr: Record<string, unknown>): string | undefined {
|
|
203
|
+
const ln = spPr["a:ln"] ?? spPr["ln"];
|
|
204
|
+
if (!ln || typeof ln !== "object") return undefined;
|
|
205
|
+
const solidFill = (ln as Record<string, unknown>)["a:solidFill"]
|
|
206
|
+
?? (ln as Record<string, unknown>)["solidFill"];
|
|
207
|
+
if (solidFill && typeof solidFill === "object") {
|
|
208
|
+
return extractSrgb(solidFill as Record<string, unknown>);
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function extractSrgb(fill: Record<string, unknown>): string | undefined {
|
|
214
|
+
const srgb = fill["a:srgbClr"] ?? fill["srgbClr"];
|
|
215
|
+
if (srgb && typeof srgb === "object") {
|
|
216
|
+
const val = (srgb as Record<string, unknown>)["@_val"];
|
|
217
|
+
if (typeof val === "string") return `#${val}`;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// Text extraction
|
|
224
|
+
// =============================================================================
|
|
225
|
+
|
|
226
|
+
function extractTextContent(txBody: unknown): string | undefined {
|
|
227
|
+
if (!txBody || typeof txBody !== "object") return undefined;
|
|
228
|
+
const paras = ensureArray((txBody as Record<string, unknown>)["a:p"] ?? (txBody as Record<string, unknown>)["p"]);
|
|
229
|
+
const texts: string[] = [];
|
|
230
|
+
for (const para of paras) {
|
|
231
|
+
if (!para || typeof para !== "object") continue;
|
|
232
|
+
const runs = ensureArray((para as Record<string, unknown>)["a:r"] ?? (para as Record<string, unknown>)["r"]);
|
|
233
|
+
for (const run of runs) {
|
|
234
|
+
if (!run || typeof run !== "object") continue;
|
|
235
|
+
const t = (run as Record<string, unknown>)["a:t"] ?? (run as Record<string, unknown>)["t"];
|
|
236
|
+
if (typeof t === "string") texts.push(t);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return texts.length > 0 ? texts.join("") : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// Relationship extraction
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
function extractChartRId(graphicFrame: Record<string, unknown>): string | undefined {
|
|
247
|
+
const graphic = graphicFrame["a:graphic"] ?? graphicFrame["graphic"];
|
|
248
|
+
if (!graphic || typeof graphic !== "object") return undefined;
|
|
249
|
+
const graphicData = (graphic as Record<string, unknown>)["a:graphicData"]
|
|
250
|
+
?? (graphic as Record<string, unknown>)["graphicData"];
|
|
251
|
+
if (!graphicData || typeof graphicData !== "object") return undefined;
|
|
252
|
+
const chartRef = (graphicData as Record<string, unknown>)["c:chart"]
|
|
253
|
+
?? (graphicData as Record<string, unknown>)["chart"];
|
|
254
|
+
if (!chartRef || typeof chartRef !== "object") return undefined;
|
|
255
|
+
const rId = (chartRef as Record<string, unknown>)["@_r:id"]
|
|
256
|
+
?? (chartRef as Record<string, unknown>)["@_id"];
|
|
257
|
+
return typeof rId === "string" ? rId : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function extractPicRId(pic: Record<string, unknown>): string | undefined {
|
|
261
|
+
const blipFill = pic["xdr:blipFill"] ?? pic["blipFill"];
|
|
262
|
+
if (!blipFill || typeof blipFill !== "object") return undefined;
|
|
263
|
+
const blip = (blipFill as Record<string, unknown>)["a:blip"]
|
|
264
|
+
?? (blipFill as Record<string, unknown>)["blip"];
|
|
265
|
+
if (!blip || typeof blip !== "object") return undefined;
|
|
266
|
+
const rId = (blip as Record<string, unknown>)["@_r:embed"]
|
|
267
|
+
?? (blip as Record<string, unknown>)["@_embed"];
|
|
268
|
+
return typeof rId === "string" ? rId : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// =============================================================================
|
|
272
|
+
// Helpers
|
|
273
|
+
// =============================================================================
|
|
274
|
+
|
|
275
|
+
function ensureArray(val: unknown): unknown[] {
|
|
276
|
+
if (val === undefined || val === null) return [];
|
|
277
|
+
return Array.isArray(val) ? val : [val];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseIntVal(val: unknown): number | undefined {
|
|
281
|
+
if (typeof val === "number") return val;
|
|
282
|
+
if (typeof val === "string") {
|
|
283
|
+
const n = parseInt(val, 10);
|
|
284
|
+
return isNaN(n) ? undefined : n;
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { XMLParser } from "fast-xml-parser";
|
|
3
|
+
import {
|
|
4
|
+
parsePivotTableXml,
|
|
5
|
+
parsePivotCacheDefinitionXml,
|
|
6
|
+
extractPivotTableRIds,
|
|
7
|
+
extractWorkbookPivotCaches,
|
|
8
|
+
} from "./ooxml_pivot";
|
|
9
|
+
|
|
10
|
+
const parser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "@_",
|
|
13
|
+
isArray: (tagName) =>
|
|
14
|
+
tagName === "Relationship" ||
|
|
15
|
+
tagName === "pivotField" ||
|
|
16
|
+
tagName === "cacheField" ||
|
|
17
|
+
tagName === "field" ||
|
|
18
|
+
tagName === "pageField" ||
|
|
19
|
+
tagName === "dataField" ||
|
|
20
|
+
tagName === "pivotCache",
|
|
21
|
+
});
|
|
22
|
+
const parseXml = (xml: string) => parser.parse(xml) as Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// PivotTable
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
const PIVOT_TABLE_XML = `<?xml version="1.0"?>
|
|
29
|
+
<pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
30
|
+
name="SalesByRegion" cacheId="1" rowGrandTotals="1" colGrandTotals="1">
|
|
31
|
+
<location ref="A3:E12" firstHeaderRow="1" firstDataCol="1" firstDataRow="2"/>
|
|
32
|
+
<pivotFields count="3">
|
|
33
|
+
<pivotField axis="axisRow" defaultSubtotal="1" sumSubtotal="1"/>
|
|
34
|
+
<pivotField axis="axisCol" defaultSubtotal="1"/>
|
|
35
|
+
<pivotField dataField="1" showAll="0"/>
|
|
36
|
+
</pivotFields>
|
|
37
|
+
<rowFields count="1">
|
|
38
|
+
<field x="0"/>
|
|
39
|
+
</rowFields>
|
|
40
|
+
<colFields count="1">
|
|
41
|
+
<field x="1"/>
|
|
42
|
+
</colFields>
|
|
43
|
+
<dataFields count="1">
|
|
44
|
+
<dataField name="Sum of Revenue" fld="2" subtotal="sum"/>
|
|
45
|
+
</dataFields>
|
|
46
|
+
<pivotTableStyleInfo name="PivotStyleLight16" showRowStripes="1"/>
|
|
47
|
+
</pivotTableDefinition>`;
|
|
48
|
+
|
|
49
|
+
describe("parsePivotTableXml", () => {
|
|
50
|
+
it("extracts the basic shape of a pivot table", () => {
|
|
51
|
+
const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
|
|
52
|
+
expect(pivot.name).toBe("SalesByRegion");
|
|
53
|
+
expect(pivot.cacheId).toBe(1);
|
|
54
|
+
expect(pivot.ref).toBe("A3:E12");
|
|
55
|
+
expect(pivot.firstHeaderRow).toBe(1);
|
|
56
|
+
expect(pivot.firstDataCol).toBe(1);
|
|
57
|
+
expect(pivot.firstDataRow).toBe(2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("classifies fields onto axes", () => {
|
|
61
|
+
const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
|
|
62
|
+
expect(pivot.fields).toHaveLength(3);
|
|
63
|
+
expect(pivot.fields[0].axis).toBe("row");
|
|
64
|
+
expect(pivot.fields[1].axis).toBe("column");
|
|
65
|
+
// Field 2 is referenced by the dataField but doesn't carry an axis attr.
|
|
66
|
+
expect(pivot.fields[2].axis).toBeUndefined();
|
|
67
|
+
expect(pivot.fields[0].subtotalFunction).toBe("sum");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("captures the row/col field ordering", () => {
|
|
71
|
+
const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
|
|
72
|
+
expect(pivot.rowFieldIndices).toEqual([0]);
|
|
73
|
+
expect(pivot.colFieldIndices).toEqual([1]);
|
|
74
|
+
expect(pivot.pageFieldIndices).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("parses the values area", () => {
|
|
78
|
+
const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
|
|
79
|
+
expect(pivot.dataFields).toEqual([
|
|
80
|
+
{ name: "Sum of Revenue", fieldIndex: 2, subtotal: "sum", numFmt: undefined },
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("captures display flags and style", () => {
|
|
85
|
+
const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
|
|
86
|
+
expect(pivot.styleName).toBe("PivotStyleLight16");
|
|
87
|
+
expect(pivot.display.rowGrandTotals).toBe(true);
|
|
88
|
+
expect(pivot.display.colGrandTotals).toBe(true);
|
|
89
|
+
expect(pivot.display.showRowStripes).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// PivotCache
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
const PIVOT_CACHE_XML = `<?xml version="1.0"?>
|
|
98
|
+
<pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
99
|
+
refreshOnLoad="1" recordCount="4">
|
|
100
|
+
<cacheSource type="worksheet">
|
|
101
|
+
<worksheetSource sheet="Data" ref="A1:C5"/>
|
|
102
|
+
</cacheSource>
|
|
103
|
+
<cacheFields count="3">
|
|
104
|
+
<cacheField name="Region">
|
|
105
|
+
<sharedItems>
|
|
106
|
+
<s v="North"/>
|
|
107
|
+
<s v="South"/>
|
|
108
|
+
</sharedItems>
|
|
109
|
+
</cacheField>
|
|
110
|
+
<cacheField name="Quarter">
|
|
111
|
+
<sharedItems>
|
|
112
|
+
<s v="Q1"/>
|
|
113
|
+
<s v="Q2"/>
|
|
114
|
+
</sharedItems>
|
|
115
|
+
</cacheField>
|
|
116
|
+
<cacheField name="Revenue" numFmtId="4">
|
|
117
|
+
<sharedItems containsNumber="1">
|
|
118
|
+
<n v="100"/>
|
|
119
|
+
<n v="200"/>
|
|
120
|
+
<n v="300"/>
|
|
121
|
+
<n v="400"/>
|
|
122
|
+
</sharedItems>
|
|
123
|
+
</cacheField>
|
|
124
|
+
</cacheFields>
|
|
125
|
+
</pivotCacheDefinition>`;
|
|
126
|
+
|
|
127
|
+
describe("parsePivotCacheDefinitionXml", () => {
|
|
128
|
+
it("captures source range and refresh flag", () => {
|
|
129
|
+
const cache = parsePivotCacheDefinitionXml(PIVOT_CACHE_XML, 1, parseXml);
|
|
130
|
+
expect(cache.id).toBe(1);
|
|
131
|
+
expect(cache.refreshOnLoad).toBe(true);
|
|
132
|
+
expect(cache.source).toEqual({
|
|
133
|
+
type: "worksheet",
|
|
134
|
+
sheetName: "Data",
|
|
135
|
+
ref: "A1:C5",
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("parses string cache items", () => {
|
|
140
|
+
const cache = parsePivotCacheDefinitionXml(PIVOT_CACHE_XML, 1, parseXml);
|
|
141
|
+
expect(cache.fields[0].name).toBe("Region");
|
|
142
|
+
expect(cache.fields[0].items).toEqual([
|
|
143
|
+
{ kind: "string", value: "North" },
|
|
144
|
+
{ kind: "string", value: "South" },
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("parses numeric cache items and keeps numeric flag", () => {
|
|
149
|
+
const cache = parsePivotCacheDefinitionXml(PIVOT_CACHE_XML, 1, parseXml);
|
|
150
|
+
const revenue = cache.fields[2];
|
|
151
|
+
expect(revenue.containsNumber).toBe(true);
|
|
152
|
+
expect(revenue.items.map((i) => i.kind)).toEqual([
|
|
153
|
+
"number",
|
|
154
|
+
"number",
|
|
155
|
+
"number",
|
|
156
|
+
"number",
|
|
157
|
+
]);
|
|
158
|
+
expect((revenue.items[0] as { kind: "number"; value: number }).value).toBe(100);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Relationship + workbook helpers
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
const SHEET_RELS_XML = `<?xml version="1.0"?>
|
|
167
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
168
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" Target="../pivotTables/pivotTable1.xml"/>
|
|
169
|
+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing1.xml"/>
|
|
170
|
+
</Relationships>`;
|
|
171
|
+
|
|
172
|
+
describe("extractPivotTableRIds", () => {
|
|
173
|
+
it("returns only pivot-table relationships", () => {
|
|
174
|
+
const map = extractPivotTableRIds(parseXml(SHEET_RELS_XML));
|
|
175
|
+
expect(map.size).toBe(1);
|
|
176
|
+
expect(map.get("rId1")).toBe("../pivotTables/pivotTable1.xml");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const WORKBOOK_XML = `<?xml version="1.0"?>
|
|
181
|
+
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
182
|
+
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
183
|
+
<pivotCaches>
|
|
184
|
+
<pivotCache cacheId="1" r:id="rId7"/>
|
|
185
|
+
<pivotCache cacheId="2" r:id="rId8"/>
|
|
186
|
+
</pivotCaches>
|
|
187
|
+
</workbook>`;
|
|
188
|
+
|
|
189
|
+
describe("extractWorkbookPivotCaches", () => {
|
|
190
|
+
it("returns cacheId → rId mapping", () => {
|
|
191
|
+
const map = extractWorkbookPivotCaches(parseXml(WORKBOOK_XML));
|
|
192
|
+
expect(map.get(1)).toBe("rId7");
|
|
193
|
+
expect(map.get(2)).toBe("rId8");
|
|
194
|
+
});
|
|
195
|
+
});
|