@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,339 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+
3
+ // =============================================================================
4
+ // Types
5
+ // =============================================================================
6
+
7
+ export interface ThemeData {
8
+ colors: string[]; // 12 entries: lt1, dk1, lt2, dk2, accent1-6, hlink, folHlink
9
+ majorFont: string;
10
+ minorFont: string;
11
+ }
12
+
13
+ export interface OoxmlColor {
14
+ rgb?: string;
15
+ theme?: number;
16
+ indexed?: number;
17
+ tint?: number;
18
+ auto?: boolean;
19
+ }
20
+
21
+ // =============================================================================
22
+ // Default indexed color palette (64 colors, indices 0-63)
23
+ // =============================================================================
24
+
25
+ export const DEFAULT_INDEXED_COLORS: string[] = [
26
+ "000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", // 0-7
27
+ "000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", // 8-15
28
+ "800000", "008000", "000080", "808000", "800080", "008080", "C0C0C0", "808080", // 16-23
29
+ "9999FF", "993366", "FFFFCC", "CCFFFF", "660066", "FF8080", "0066CC", "CCCCFF", // 24-31
30
+ "000080", "FF00FF", "FFFF00", "00FFFF", "800080", "800000", "008080", "0000FF", // 32-39
31
+ "00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99", // 40-47
32
+ "3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696", // 48-55
33
+ "003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333", // 56-63
34
+ ];
35
+
36
+ // =============================================================================
37
+ // Theme parsing
38
+ // =============================================================================
39
+
40
+ const xmlParser = new XMLParser({
41
+ ignoreAttributes: false,
42
+ attributeNamePrefix: "@_",
43
+ processEntities: false,
44
+ isArray: (tagName) => tagName === "a:srgbClr" || tagName === "a:sysClr",
45
+ });
46
+
47
+ export function parseTheme(themeXml: string): ThemeData {
48
+ const doc = xmlParser.parse(themeXml);
49
+ const themeElements = doc?.["a:theme"]?.["a:themeElements"];
50
+ if (!themeElements) {
51
+ return { colors: defaultThemeColors(), majorFont: "Calibri", minorFont: "Calibri" };
52
+ }
53
+
54
+ // Color scheme
55
+ const clrScheme = themeElements["a:clrScheme"];
56
+ const colors = parseColorScheme(clrScheme);
57
+
58
+ // Font scheme
59
+ const fontScheme = themeElements["a:fontScheme"];
60
+ const majorFont = extractFontName(fontScheme?.["a:majorFont"]) ?? "Calibri";
61
+ const minorFont = extractFontName(fontScheme?.["a:minorFont"]) ?? "Calibri";
62
+
63
+ return { colors, majorFont, minorFont };
64
+ }
65
+
66
+ /** XML color scheme order: dk1, lt1, dk2, lt2, accent1-6, hlink, folHlink */
67
+ const COLOR_SCHEME_KEYS = [
68
+ "a:dk1", "a:lt1", "a:dk2", "a:lt2",
69
+ "a:accent1", "a:accent2", "a:accent3", "a:accent4", "a:accent5", "a:accent6",
70
+ "a:hlink", "a:folHlink",
71
+ ];
72
+
73
+ function parseColorScheme(clrScheme: Record<string, unknown> | undefined): string[] {
74
+ if (!clrScheme) return defaultThemeColors();
75
+
76
+ const colors: string[] = [];
77
+ for (const key of COLOR_SCHEME_KEYS) {
78
+ const el = clrScheme[key] as Record<string, unknown> | undefined;
79
+ colors.push(extractSchemeColor(el));
80
+ }
81
+
82
+ // Reorder: XML is [dk1, lt1, dk2, lt2, ...] but theme index mapping needs
83
+ // [lt1, dk1, lt2, dk2, ...] — swap 0↔1 and 2↔3
84
+ const reordered = [...colors];
85
+ reordered[0] = colors[1]; // lt1
86
+ reordered[1] = colors[0]; // dk1
87
+ reordered[2] = colors[3]; // lt2
88
+ reordered[3] = colors[2]; // dk2
89
+ return reordered;
90
+ }
91
+
92
+ function extractSchemeColor(el: Record<string, unknown> | undefined): string {
93
+ if (!el) return "000000";
94
+
95
+ // srgbClr has @_val attribute directly
96
+ const srgb = el["a:srgbClr"];
97
+ if (srgb) {
98
+ const val = Array.isArray(srgb) ? srgb[0] : srgb;
99
+ const hex = typeof val === "object" && val !== null ? (val as Record<string, string>)["@_val"] : undefined;
100
+ if (hex) return hex;
101
+ }
102
+
103
+ // sysClr has @_lastClr attribute
104
+ const sys = el["a:sysClr"];
105
+ if (sys) {
106
+ const val = Array.isArray(sys) ? sys[0] : sys;
107
+ const lastClr = typeof val === "object" && val !== null ? (val as Record<string, string>)["@_lastClr"] : undefined;
108
+ if (lastClr) return lastClr;
109
+ }
110
+
111
+ return "000000";
112
+ }
113
+
114
+ function extractFontName(fontEl: Record<string, unknown> | undefined): string | undefined {
115
+ if (!fontEl) return undefined;
116
+ const latin = fontEl["a:latin"] as Record<string, string> | undefined;
117
+ return latin?.["@_typeface"];
118
+ }
119
+
120
+ export function defaultThemeColors(): string[] {
121
+ return [
122
+ "FFFFFF", "000000", "E7E6E6", "44546A",
123
+ "4472C4", "ED7D31", "A5A5A5", "FFC000",
124
+ "5B9BD5", "70AD47", "0563C1", "954F72",
125
+ ];
126
+ }
127
+
128
+ // =============================================================================
129
+ // Color resolution
130
+ // =============================================================================
131
+
132
+ /** Parse a color element's attributes into an OoxmlColor. */
133
+ export function parseColorElement(attrs: Record<string, string>): OoxmlColor {
134
+ const color: OoxmlColor = {};
135
+ if (attrs["@_rgb"] !== undefined) color.rgb = attrs["@_rgb"];
136
+ if (attrs["@_theme"] !== undefined) color.theme = parseInt(attrs["@_theme"], 10);
137
+ if (attrs["@_indexed"] !== undefined) color.indexed = parseInt(attrs["@_indexed"], 10);
138
+ if (attrs["@_tint"] !== undefined) color.tint = parseFloat(attrs["@_tint"]);
139
+ if (attrs["@_auto"] !== undefined) color.auto = attrs["@_auto"] === "1" || attrs["@_auto"] === "true";
140
+ return color;
141
+ }
142
+
143
+ /**
144
+ * Resolve an OOXML color to a CSS hex string (6-char, no #).
145
+ * Returns undefined if the color is auto/unresolvable.
146
+ */
147
+ export function resolveColor(
148
+ color: OoxmlColor | undefined,
149
+ theme: ThemeData,
150
+ indexedColors: string[],
151
+ context: "font" | "fill" | "border",
152
+ ): string | undefined {
153
+ if (!color) return undefined;
154
+
155
+ // Auto color: black for font, undefined for fill/border
156
+ if (color.auto) {
157
+ return context === "font" ? "000000" : undefined;
158
+ }
159
+
160
+ let hex: string | undefined;
161
+
162
+ if (color.rgb !== undefined) {
163
+ // RGB can be AARRGGBB (8 chars) or RRGGBB (6 chars)
164
+ hex = color.rgb.length === 8 ? color.rgb.slice(2) : color.rgb;
165
+ } else if (color.theme !== undefined) {
166
+ const idx = color.theme;
167
+ if (idx >= 0 && idx < theme.colors.length) {
168
+ hex = theme.colors[idx];
169
+ }
170
+ } else if (color.indexed !== undefined) {
171
+ const idx = color.indexed;
172
+ if (idx >= 0 && idx < indexedColors.length) {
173
+ hex = indexedColors[idx];
174
+ }
175
+ }
176
+
177
+ if (!hex) return undefined;
178
+
179
+ // Apply tint
180
+ if (color.tint !== undefined && color.tint !== 0) {
181
+ hex = applyTint(hex, color.tint);
182
+ }
183
+
184
+ return hex;
185
+ }
186
+
187
+ // =============================================================================
188
+ // Canonical color format
189
+ // =============================================================================
190
+ //
191
+ // Internal canonical color format throughout @lotics/xlsx is `#RRGGBB`
192
+ // (uppercase, hash-prefixed). Every consumer reads/writes that format.
193
+ // Inputs from outside the package (parser, tools, frontend pickers) flow
194
+ // through `normalizeColor()` at ingress; OOXML writers convert via
195
+ // `hexToArgb()` (8-char ARGB) or `hexToOoxmlRgb()` (6-char RGB for chart XML)
196
+ // at egress.
197
+
198
+ /** Prefix a 6-char internal hex (`RRGGBB`) with `#`. */
199
+ export function prefixHex(hex6: string): string {
200
+ if (hex6.length !== 6) return "#000000";
201
+ return `#${hex6.toUpperCase()}`;
202
+ }
203
+
204
+ /**
205
+ * Coerce a color string in any supported input format to canonical `#RRGGBB`.
206
+ * Accepts `#RRGGBB`, `RRGGBB`, `#RGB`, `rgb(r, g, b)`, `rgba(r, g, b, a)`.
207
+ * Returns undefined for empty or unparseable input.
208
+ */
209
+ export function normalizeColor(input: string | undefined): string | undefined {
210
+ if (input === undefined) return undefined;
211
+ const s = input.trim();
212
+ if (s === "") return undefined;
213
+
214
+ // rgb()/rgba()
215
+ const rgbMatch = s.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
216
+ if (rgbMatch) {
217
+ return `#${toHex2(parseInt(rgbMatch[1], 10))}${toHex2(parseInt(rgbMatch[2], 10))}${toHex2(parseInt(rgbMatch[3], 10))}`;
218
+ }
219
+
220
+ // #RRGGBB
221
+ const hex6Match = s.match(/^#([0-9a-fA-F]{6})$/);
222
+ if (hex6Match) return `#${hex6Match[1].toUpperCase()}`;
223
+
224
+ // RRGGBB (no hash)
225
+ const bareHex6Match = s.match(/^([0-9a-fA-F]{6})$/);
226
+ if (bareHex6Match) return `#${bareHex6Match[1].toUpperCase()}`;
227
+
228
+ // #RGB shorthand
229
+ const hex3Match = s.match(/^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/);
230
+ if (hex3Match) {
231
+ const r = hex3Match[1];
232
+ const g = hex3Match[2];
233
+ const b = hex3Match[3];
234
+ return `#${(r + r + g + g + b + b).toUpperCase()}`;
235
+ }
236
+
237
+ return undefined;
238
+ }
239
+
240
+ /**
241
+ * Convert a normalized `#RRGGBB` color to OOXML 8-char ARGB (`FFRRGGBB`).
242
+ * Accepts unnormalized input as a courtesy — runs `normalizeColor` first.
243
+ */
244
+ export function hexToArgb(input: string | undefined): string {
245
+ const norm = normalizeColor(input);
246
+ if (!norm) return "FF000000";
247
+ return `FF${norm.slice(1)}`;
248
+ }
249
+
250
+ /**
251
+ * Convert a normalized `#RRGGBB` color to OOXML 6-char hex (`RRGGBB`),
252
+ * used by drawing/chart XML where `<a:srgbClr val="…"/>` requires no alpha.
253
+ */
254
+ export function hexToOoxmlRgb(input: string | undefined): string {
255
+ const norm = normalizeColor(input);
256
+ if (!norm) return "000000";
257
+ return norm.slice(1);
258
+ }
259
+
260
+ // =============================================================================
261
+ // Tint algorithm
262
+ // =============================================================================
263
+
264
+ /** Apply Excel tint to a 6-char hex color. Returns 6-char hex. */
265
+ export function applyTint(hex: string, tint: number): string {
266
+ const r = parseInt(hex.slice(0, 2), 16);
267
+ const g = parseInt(hex.slice(2, 4), 16);
268
+ const b = parseInt(hex.slice(4, 6), 16);
269
+
270
+ const { h, s, l } = rgbToHsl(r, g, b);
271
+
272
+ let newL: number;
273
+ if (tint < 0) {
274
+ newL = l * (1 + tint);
275
+ } else {
276
+ newL = l + (1 - l) * tint;
277
+ }
278
+ newL = Math.max(0, Math.min(1, newL));
279
+
280
+ const { r: nr, g: ng, b: nb } = hslToRgb(h, s, newL);
281
+ return toHex2(nr) + toHex2(ng) + toHex2(nb);
282
+ }
283
+
284
+ function toHex2(n: number): string {
285
+ const clamped = Math.max(0, Math.min(255, Math.round(n)));
286
+ return clamped.toString(16).padStart(2, "0").toUpperCase();
287
+ }
288
+
289
+ function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
290
+ const rf = r / 255;
291
+ const gf = g / 255;
292
+ const bf = b / 255;
293
+
294
+ const max = Math.max(rf, gf, bf);
295
+ const min = Math.min(rf, gf, bf);
296
+ const l = (max + min) / 2;
297
+
298
+ if (max === min) return { h: 0, s: 0, l };
299
+
300
+ const d = max - min;
301
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
302
+
303
+ let h: number;
304
+ if (max === rf) {
305
+ h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
306
+ } else if (max === gf) {
307
+ h = ((bf - rf) / d + 2) / 6;
308
+ } else {
309
+ h = ((rf - gf) / d + 4) / 6;
310
+ }
311
+
312
+ return { h, s, l };
313
+ }
314
+
315
+ function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
316
+ if (s === 0) {
317
+ const v = Math.round(l * 255);
318
+ return { r: v, g: v, b: v };
319
+ }
320
+
321
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
322
+ const p = 2 * l - q;
323
+
324
+ return {
325
+ r: Math.round(hueToRgb(p, q, h + 1 / 3) * 255),
326
+ g: Math.round(hueToRgb(p, q, h) * 255),
327
+ b: Math.round(hueToRgb(p, q, h - 1 / 3) * 255),
328
+ };
329
+ }
330
+
331
+ function hueToRgb(p: number, q: number, t: number): number {
332
+ let tt = t;
333
+ if (tt < 0) tt += 1;
334
+ if (tt > 1) tt -= 1;
335
+ if (tt < 1 / 6) return p + (q - p) * 6 * tt;
336
+ if (tt < 1 / 2) return q;
337
+ if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
338
+ return p;
339
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { XMLParser } from "fast-xml-parser";
3
+ import { parseDrawingXml } from "./ooxml_drawing";
4
+
5
+ const parser = new XMLParser({
6
+ ignoreAttributes: false,
7
+ attributeNamePrefix: "@_",
8
+ isArray: (tagName) =>
9
+ tagName === "xdr:twoCellAnchor" || tagName === "xdr:oneCellAnchor",
10
+ });
11
+ const parseXml = (xml: string) => parser.parse(xml) as Record<string, unknown>;
12
+
13
+ const CHART_DRAWING_XML = `<?xml version="1.0"?>
14
+ <xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart">
15
+ <xdr:twoCellAnchor>
16
+ <xdr:from><xdr:col>1</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>2</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
17
+ <xdr:to><xdr:col>5</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>10</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
18
+ <xdr:graphicFrame>
19
+ <a:graphic><a:graphicData><c:chart r:id="rId1"/></a:graphicData></a:graphic>
20
+ </xdr:graphicFrame>
21
+ </xdr:twoCellAnchor>
22
+ </xdr:wsDr>`;
23
+
24
+ const SHAPE_DRAWING_XML = `<?xml version="1.0"?>
25
+ <xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
26
+ <xdr:twoCellAnchor>
27
+ <xdr:from><xdr:col>0</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
28
+ <xdr:to><xdr:col>3</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>4</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
29
+ <xdr:sp>
30
+ <xdr:spPr>
31
+ <a:prstGeom prst="rect"/>
32
+ <a:solidFill><a:srgbClr val="FF0000"/></a:solidFill>
33
+ </xdr:spPr>
34
+ <xdr:txBody><a:p><a:r><a:t>Hello</a:t></a:r></a:p></xdr:txBody>
35
+ </xdr:sp>
36
+ </xdr:twoCellAnchor>
37
+ </xdr:wsDr>`;
38
+
39
+ const CONNECTOR_DRAWING_XML = `<?xml version="1.0"?>
40
+ <xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
41
+ <xdr:twoCellAnchor>
42
+ <xdr:from><xdr:col>1</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>1</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
43
+ <xdr:to><xdr:col>4</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>4</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
44
+ <xdr:cxnSp>
45
+ <xdr:spPr>
46
+ <a:ln><a:solidFill><a:srgbClr val="0000FF"/></a:solidFill></a:ln>
47
+ </xdr:spPr>
48
+ </xdr:cxnSp>
49
+ </xdr:twoCellAnchor>
50
+ </xdr:wsDr>`;
51
+
52
+ describe("parseDrawingXml", () => {
53
+ it("parses two-cell anchor coordinates", () => {
54
+ const results = parseDrawingXml(CHART_DRAWING_XML, parseXml);
55
+ expect(results).toHaveLength(1);
56
+ const anchor = results[0].anchor;
57
+ expect(anchor.from).toEqual({ row: 2, col: 1, rowOffset: 0, colOffset: 0 });
58
+ expect(anchor.to).toEqual({ row: 10, col: 5, rowOffset: 0, colOffset: 0 });
59
+ });
60
+
61
+ it("identifies chart graphic frame and extracts rId", () => {
62
+ const results = parseDrawingXml(CHART_DRAWING_XML, parseXml);
63
+ expect(results[0].type).toBe("chart");
64
+ expect(results[0].rId).toBe("rId1");
65
+ });
66
+
67
+ it("parses shape with text and fill", () => {
68
+ const results = parseDrawingXml(SHAPE_DRAWING_XML, parseXml);
69
+ expect(results).toHaveLength(1);
70
+ expect(results[0].type).toBe("shape");
71
+ expect(results[0].drawing).toBeDefined();
72
+ expect(results[0].drawing!.type).toBe("textbox");
73
+ expect(results[0].drawing!.text).toBe("Hello");
74
+ expect(results[0].drawing!.fillColor).toBe("#FF0000");
75
+ expect(results[0].drawing!.geometry).toBe("rect");
76
+ });
77
+
78
+ it("parses connector with outline color", () => {
79
+ const results = parseDrawingXml(CONNECTOR_DRAWING_XML, parseXml);
80
+ expect(results).toHaveLength(1);
81
+ expect(results[0].type).toBe("shape");
82
+ expect(results[0].drawing).toBeDefined();
83
+ expect(results[0].drawing!.type).toBe("connector");
84
+ expect(results[0].drawing!.geometry).toBe("line");
85
+ expect(results[0].drawing!.outlineColor).toBe("#0000FF");
86
+ });
87
+ });