@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,355 @@
1
+ // =============================================================================
2
+ // OOXML Shape Parser
3
+ // =============================================================================
4
+ //
5
+ // Parses DrawingML shape geometries for Canvas rendering.
6
+ // Maps Excel preset geometry names to Canvas drawing paths.
7
+ // Supports ~20 common shapes; remaining geometries render as labeled rectangles.
8
+ // Also parses VML drawings (legacy Excel form controls).
9
+ // =============================================================================
10
+
11
+ import type { ParsedDrawing, DrawingAnchor } from "./ooxml_chart_types";
12
+
13
+ // =============================================================================
14
+ // Types
15
+ // =============================================================================
16
+
17
+ export interface ShapePath {
18
+ /** Canvas path drawing instructions. */
19
+ draw: (ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) => void;
20
+ }
21
+
22
+ // =============================================================================
23
+ // Preset geometry map
24
+ // =============================================================================
25
+
26
+ const SHAPE_PATHS: Record<string, ShapePath> = {
27
+ rect: {
28
+ draw(ctx, x, y, w, h) {
29
+ ctx.rect(x, y, w, h);
30
+ },
31
+ },
32
+ roundRect: {
33
+ draw(ctx, x, y, w, h) {
34
+ const r = Math.min(w, h) * 0.1;
35
+ ctx.moveTo(x + r, y);
36
+ ctx.lineTo(x + w - r, y);
37
+ ctx.arcTo(x + w, y, x + w, y + r, r);
38
+ ctx.lineTo(x + w, y + h - r);
39
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
40
+ ctx.lineTo(x + r, y + h);
41
+ ctx.arcTo(x, y + h, x, y + h - r, r);
42
+ ctx.lineTo(x, y + r);
43
+ ctx.arcTo(x, y, x + r, y, r);
44
+ },
45
+ },
46
+ ellipse: {
47
+ draw(ctx, x, y, w, h) {
48
+ ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, Math.PI * 2);
49
+ },
50
+ },
51
+ triangle: {
52
+ draw(ctx, x, y, w, h) {
53
+ ctx.moveTo(x + w / 2, y);
54
+ ctx.lineTo(x + w, y + h);
55
+ ctx.lineTo(x, y + h);
56
+ ctx.closePath();
57
+ },
58
+ },
59
+ diamond: {
60
+ draw(ctx, x, y, w, h) {
61
+ ctx.moveTo(x + w / 2, y);
62
+ ctx.lineTo(x + w, y + h / 2);
63
+ ctx.lineTo(x + w / 2, y + h);
64
+ ctx.lineTo(x, y + h / 2);
65
+ ctx.closePath();
66
+ },
67
+ },
68
+ rightArrow: {
69
+ draw(ctx, x, y, w, h) {
70
+ const headW = w * 0.4;
71
+ const bodyH = h * 0.4;
72
+ const bodyY = y + (h - bodyH) / 2;
73
+ ctx.moveTo(x, bodyY);
74
+ ctx.lineTo(x + w - headW, bodyY);
75
+ ctx.lineTo(x + w - headW, y);
76
+ ctx.lineTo(x + w, y + h / 2);
77
+ ctx.lineTo(x + w - headW, y + h);
78
+ ctx.lineTo(x + w - headW, bodyY + bodyH);
79
+ ctx.lineTo(x, bodyY + bodyH);
80
+ ctx.closePath();
81
+ },
82
+ },
83
+ leftArrow: {
84
+ draw(ctx, x, y, w, h) {
85
+ const headW = w * 0.4;
86
+ const bodyH = h * 0.4;
87
+ const bodyY = y + (h - bodyH) / 2;
88
+ ctx.moveTo(x + w, bodyY);
89
+ ctx.lineTo(x + headW, bodyY);
90
+ ctx.lineTo(x + headW, y);
91
+ ctx.lineTo(x, y + h / 2);
92
+ ctx.lineTo(x + headW, y + h);
93
+ ctx.lineTo(x + headW, bodyY + bodyH);
94
+ ctx.lineTo(x + w, bodyY + bodyH);
95
+ ctx.closePath();
96
+ },
97
+ },
98
+ upArrow: {
99
+ draw(ctx, x, y, w, h) {
100
+ const headH = h * 0.4;
101
+ const bodyW = w * 0.4;
102
+ const bodyX = x + (w - bodyW) / 2;
103
+ ctx.moveTo(x + w / 2, y);
104
+ ctx.lineTo(x + w, y + headH);
105
+ ctx.lineTo(bodyX + bodyW, y + headH);
106
+ ctx.lineTo(bodyX + bodyW, y + h);
107
+ ctx.lineTo(bodyX, y + h);
108
+ ctx.lineTo(bodyX, y + headH);
109
+ ctx.lineTo(x, y + headH);
110
+ ctx.closePath();
111
+ },
112
+ },
113
+ downArrow: {
114
+ draw(ctx, x, y, w, h) {
115
+ const headH = h * 0.4;
116
+ const bodyW = w * 0.4;
117
+ const bodyX = x + (w - bodyW) / 2;
118
+ ctx.moveTo(bodyX, y);
119
+ ctx.lineTo(bodyX + bodyW, y);
120
+ ctx.lineTo(bodyX + bodyW, y + h - headH);
121
+ ctx.lineTo(x + w, y + h - headH);
122
+ ctx.lineTo(x + w / 2, y + h);
123
+ ctx.lineTo(x, y + h - headH);
124
+ ctx.lineTo(bodyX, y + h - headH);
125
+ ctx.closePath();
126
+ },
127
+ },
128
+ pentagon: {
129
+ draw(ctx, x, y, w, h) {
130
+ const cx = x + w / 2;
131
+ for (let i = 0; i < 5; i++) {
132
+ const angle = (i * 2 * Math.PI) / 5 - Math.PI / 2;
133
+ const px = cx + (w / 2) * Math.cos(angle);
134
+ const py = y + h / 2 + (h / 2) * Math.sin(angle);
135
+ if (i === 0) ctx.moveTo(px, py);
136
+ else ctx.lineTo(px, py);
137
+ }
138
+ ctx.closePath();
139
+ },
140
+ },
141
+ hexagon: {
142
+ draw(ctx, x, y, w, h) {
143
+ const cx = x + w / 2;
144
+ for (let i = 0; i < 6; i++) {
145
+ const angle = (i * 2 * Math.PI) / 6 - Math.PI / 2;
146
+ const px = cx + (w / 2) * Math.cos(angle);
147
+ const py = y + h / 2 + (h / 2) * Math.sin(angle);
148
+ if (i === 0) ctx.moveTo(px, py);
149
+ else ctx.lineTo(px, py);
150
+ }
151
+ ctx.closePath();
152
+ },
153
+ },
154
+ star5: {
155
+ draw(ctx, x, y, w, h) {
156
+ drawStar(ctx, x + w / 2, y + h / 2, 5, w / 2, w / 4);
157
+ },
158
+ },
159
+ heart: {
160
+ draw(ctx, x, y, w, h) {
161
+ const cx = x + w / 2;
162
+ ctx.moveTo(cx, y + h);
163
+ ctx.bezierCurveTo(x - w * 0.1, y + h * 0.5, x, y, cx, y + h * 0.3);
164
+ ctx.bezierCurveTo(x + w, y, x + w * 1.1, y + h * 0.5, cx, y + h);
165
+ },
166
+ },
167
+ plus: {
168
+ draw(ctx, x, y, w, h) {
169
+ const arm = Math.min(w, h) / 3;
170
+ ctx.moveTo(x + arm, y);
171
+ ctx.lineTo(x + w - arm, y);
172
+ ctx.lineTo(x + w - arm, y + arm);
173
+ ctx.lineTo(x + w, y + arm);
174
+ ctx.lineTo(x + w, y + h - arm);
175
+ ctx.lineTo(x + w - arm, y + h - arm);
176
+ ctx.lineTo(x + w - arm, y + h);
177
+ ctx.lineTo(x + arm, y + h);
178
+ ctx.lineTo(x + arm, y + h - arm);
179
+ ctx.lineTo(x, y + h - arm);
180
+ ctx.lineTo(x, y + arm);
181
+ ctx.lineTo(x + arm, y + arm);
182
+ ctx.closePath();
183
+ },
184
+ },
185
+ flowChartProcess: {
186
+ draw(ctx, x, y, w, h) {
187
+ ctx.rect(x, y, w, h);
188
+ },
189
+ },
190
+ flowChartDecision: {
191
+ draw(ctx, x, y, w, h) {
192
+ ctx.moveTo(x + w / 2, y);
193
+ ctx.lineTo(x + w, y + h / 2);
194
+ ctx.lineTo(x + w / 2, y + h);
195
+ ctx.lineTo(x, y + h / 2);
196
+ ctx.closePath();
197
+ },
198
+ },
199
+ flowChartTerminator: {
200
+ draw(ctx, x, y, w, h) {
201
+ const r = h / 2;
202
+ ctx.moveTo(x + r, y);
203
+ ctx.lineTo(x + w - r, y);
204
+ ctx.arc(x + w - r, y + r, r, -Math.PI / 2, Math.PI / 2);
205
+ ctx.lineTo(x + r, y + h);
206
+ ctx.arc(x + r, y + r, r, Math.PI / 2, -Math.PI / 2);
207
+ },
208
+ },
209
+ };
210
+
211
+ // =============================================================================
212
+ // Public API
213
+ // =============================================================================
214
+
215
+ /**
216
+ * Get a shape path for a given preset geometry name.
217
+ * Returns the rect path as fallback for unknown geometries.
218
+ */
219
+ export function getShapePath(geometry: string): ShapePath {
220
+ return SHAPE_PATHS[geometry] ?? SHAPE_PATHS.rect;
221
+ }
222
+
223
+ /**
224
+ * List of supported preset geometry names.
225
+ */
226
+ export const SUPPORTED_GEOMETRIES = Object.keys(SHAPE_PATHS);
227
+
228
+ // =============================================================================
229
+ // Helpers
230
+ // =============================================================================
231
+
232
+ function drawStar(
233
+ ctx: CanvasRenderingContext2D,
234
+ cx: number,
235
+ cy: number,
236
+ points: number,
237
+ outerR: number,
238
+ innerR: number,
239
+ ): void {
240
+ const step = Math.PI / points;
241
+ ctx.moveTo(cx, cy - outerR);
242
+ for (let i = 0; i < 2 * points; i++) {
243
+ const r = i % 2 === 0 ? outerR : innerR;
244
+ const angle = i * step - Math.PI / 2;
245
+ ctx.lineTo(cx + r * Math.cos(angle), cy + r * Math.sin(angle));
246
+ }
247
+ ctx.closePath();
248
+ }
249
+
250
+ // =============================================================================
251
+ // VML Drawing Parser (legacy form controls)
252
+ // =============================================================================
253
+
254
+ /**
255
+ * ObjectType → drawing geometry and text prefix/suffix mapping.
256
+ */
257
+ const VML_CONTROL_MAP: Record<string, { geometry: string; type: ParsedDrawing["type"]; prefix?: (checked: boolean) => string; suffix?: string }> = {
258
+ Button: { geometry: "rect", type: "shape" },
259
+ Checkbox: { geometry: "rect", type: "shape", prefix: (checked) => checked ? "\u2611 " : "\u2610 " },
260
+ Radio: { geometry: "rect", type: "shape", prefix: (checked) => checked ? "\u25CF " : "\u25CB " },
261
+ Drop: { geometry: "rect", type: "shape", suffix: " \u25BC" },
262
+ List: { geometry: "rect", type: "shape" },
263
+ Scroll: { geometry: "rect", type: "shape" },
264
+ Spinner: { geometry: "rect", type: "shape" },
265
+ GBox: { geometry: "rect", type: "shape" },
266
+ Label: { geometry: "rect", type: "textbox" },
267
+ };
268
+
269
+ /**
270
+ * Parse VML drawings (legacy Excel form controls like buttons, checkboxes, etc.).
271
+ *
272
+ * VML uses `<v:shape>` elements with `<x:ClientData>` for form control metadata.
273
+ * The anchor in `<x:Anchor>` has 8 comma-separated values:
274
+ * col1, col1Off, row1, row1Off, col2, col2Off, row2, row2Off
275
+ */
276
+ export function parseVmlDrawing(
277
+ xml: string,
278
+ parseXml: (xml: string) => Record<string, unknown>,
279
+ ): ParsedDrawing[] {
280
+ const doc = parseXml(xml);
281
+ const root = doc["xml"] ?? doc;
282
+ if (!root || typeof root !== "object") return [];
283
+
284
+ const shapes = ensureVmlArray((root as Record<string, unknown>)["v:shape"]);
285
+ const results: ParsedDrawing[] = [];
286
+
287
+ for (const shapeNode of shapes) {
288
+ if (!shapeNode || typeof shapeNode !== "object") continue;
289
+ const shape = shapeNode as Record<string, unknown>;
290
+
291
+ const clientData = shape["x:ClientData"];
292
+ if (!clientData || typeof clientData !== "object") continue;
293
+ const cd = clientData as Record<string, unknown>;
294
+
295
+ const objectType = (cd["@_ObjectType"] as string | undefined) ?? "";
296
+ const controlDef = VML_CONTROL_MAP[objectType];
297
+ if (!controlDef) continue;
298
+
299
+ // Parse anchor: "col1, col1Off, row1, row1Off, col2, col2Off, row2, row2Off"
300
+ const anchorStr = cd["x:Anchor"];
301
+ if (typeof anchorStr !== "string") continue;
302
+ const parts = anchorStr.split(",").map((s) => parseInt(s.trim(), 10));
303
+ if (parts.length < 8 || parts.some((n) => isNaN(n))) continue;
304
+
305
+ const anchor: DrawingAnchor = {
306
+ from: { col: parts[0], colOffset: parts[1], row: parts[2], rowOffset: parts[3] },
307
+ to: { col: parts[4], colOffset: parts[5], row: parts[6], rowOffset: parts[7] },
308
+ };
309
+
310
+ // Extract text from <v:textbox><div>text</div>
311
+ let text = extractVmlText(shape["v:textbox"]);
312
+
313
+ // Check state for checkbox/radio
314
+ const checked = cd["x:Checked"] === "1" || cd["x:Checked"] === 1;
315
+
316
+ // Apply prefix/suffix based on control type
317
+ if (controlDef.prefix) {
318
+ text = controlDef.prefix(checked) + (text ?? "");
319
+ }
320
+ if (controlDef.suffix) {
321
+ text = (text ?? "") + controlDef.suffix;
322
+ }
323
+
324
+ // GBox uses dashed outline to distinguish from regular shapes
325
+ const outlineColor = objectType === "GBox" ? "#808080" : undefined;
326
+
327
+ results.push({
328
+ type: controlDef.type,
329
+ geometry: controlDef.geometry,
330
+ text: text ?? undefined,
331
+ outlineColor,
332
+ anchor,
333
+ });
334
+ }
335
+
336
+ return results;
337
+ }
338
+
339
+ /** Extract text content from a VML textbox node. */
340
+ function extractVmlText(textbox: unknown): string | undefined {
341
+ if (!textbox || typeof textbox !== "object") return undefined;
342
+ const tb = textbox as Record<string, unknown>;
343
+ const div = tb["div"] ?? tb["#text"];
344
+ if (typeof div === "string") return div;
345
+ if (div && typeof div === "object") {
346
+ const text = (div as Record<string, unknown>)["#text"];
347
+ if (typeof text === "string") return text;
348
+ }
349
+ return undefined;
350
+ }
351
+
352
+ function ensureVmlArray(val: unknown): unknown[] {
353
+ if (val === undefined || val === null) return [];
354
+ return Array.isArray(val) ? val : [val];
355
+ }