@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,242 @@
1
+ // =============================================================================
2
+ // Canvas Shape Renderer
3
+ // =============================================================================
4
+ //
5
+ // Draws parsed drawing objects on the canvas:
6
+ // - Shapes (preset geometries from ooxml_shape.ts)
7
+ // - Text boxes (rect with text content)
8
+ // - Connectors (lines with optional arrowheads)
9
+ // - Form controls (checkbox, dropdown, button as simple shapes)
10
+ // =============================================================================
11
+
12
+ import type { ParsedDrawing, DrawingAnchor } from "./ooxml_chart_types";
13
+ import type { LayoutEngine } from "./canvas_layout";
14
+ import { getShapePath } from "./ooxml_shape";
15
+ import { emuToPixels } from "./canvas_images";
16
+
17
+ // =============================================================================
18
+ // Main entry
19
+ // =============================================================================
20
+
21
+ export function drawDrawing(
22
+ ctx: CanvasRenderingContext2D,
23
+ drawing: ParsedDrawing,
24
+ layout: LayoutEngine,
25
+ scrollX: number,
26
+ scrollY: number,
27
+ ): void {
28
+ const { x, y, w, h } = anchorToPixels(drawing.anchor, layout, scrollX, scrollY);
29
+ if (w <= 0 || h <= 0) return;
30
+
31
+ switch (drawing.type) {
32
+ case "shape":
33
+ drawShape(ctx, drawing, x, y, w, h);
34
+ break;
35
+ case "textbox":
36
+ drawTextBox(ctx, drawing, x, y, w, h);
37
+ break;
38
+ case "connector":
39
+ drawConnector(ctx, drawing, x, y, w, h);
40
+ break;
41
+ case "group":
42
+ // Group children would be drawn recursively, but we handle them flat
43
+ drawShape(ctx, drawing, x, y, w, h);
44
+ break;
45
+ default:
46
+ drawUnknown(ctx, drawing, x, y, w, h);
47
+ }
48
+ }
49
+
50
+ // =============================================================================
51
+ // Shape rendering
52
+ // =============================================================================
53
+
54
+ function drawShape(
55
+ ctx: CanvasRenderingContext2D,
56
+ drawing: ParsedDrawing,
57
+ x: number, y: number, w: number, h: number,
58
+ ): void {
59
+ const geometry = drawing.geometry ?? "rect";
60
+ const shapePath = getShapePath(geometry);
61
+
62
+ ctx.save();
63
+
64
+ // Fill
65
+ if (drawing.fillColor) {
66
+ ctx.fillStyle = drawing.fillColor;
67
+ ctx.beginPath();
68
+ shapePath.draw(ctx, x, y, w, h);
69
+ ctx.fill();
70
+ }
71
+
72
+ // Outline
73
+ ctx.strokeStyle = drawing.outlineColor ?? "#000000";
74
+ ctx.lineWidth = drawing.outlineWidth ?? 1;
75
+ ctx.beginPath();
76
+ shapePath.draw(ctx, x, y, w, h);
77
+ ctx.stroke();
78
+
79
+ // Text label (if shape has text)
80
+ if (drawing.text) {
81
+ drawCenteredText(ctx, drawing.text, x, y, w, h);
82
+ }
83
+
84
+ ctx.restore();
85
+ }
86
+
87
+ // =============================================================================
88
+ // Text box rendering
89
+ // =============================================================================
90
+
91
+ function drawTextBox(
92
+ ctx: CanvasRenderingContext2D,
93
+ drawing: ParsedDrawing,
94
+ x: number, y: number, w: number, h: number,
95
+ ): void {
96
+ ctx.save();
97
+
98
+ // Background (optional)
99
+ if (drawing.fillColor) {
100
+ ctx.fillStyle = drawing.fillColor;
101
+ ctx.fillRect(x, y, w, h);
102
+ }
103
+
104
+ // Border (optional)
105
+ if (drawing.outlineColor) {
106
+ ctx.strokeStyle = drawing.outlineColor;
107
+ ctx.lineWidth = drawing.outlineWidth ?? 1;
108
+ ctx.strokeRect(x, y, w, h);
109
+ }
110
+
111
+ // Text content
112
+ if (drawing.text) {
113
+ drawWrappedText(ctx, drawing.text, x + 4, y + 4, w - 8, h - 8);
114
+ }
115
+
116
+ ctx.restore();
117
+ }
118
+
119
+ // =============================================================================
120
+ // Connector rendering
121
+ // =============================================================================
122
+
123
+ function drawConnector(
124
+ ctx: CanvasRenderingContext2D,
125
+ drawing: ParsedDrawing,
126
+ x: number, y: number, w: number, h: number,
127
+ ): void {
128
+ ctx.save();
129
+ ctx.strokeStyle = drawing.outlineColor ?? "#000000";
130
+ ctx.lineWidth = drawing.outlineWidth ?? 1;
131
+ ctx.beginPath();
132
+ ctx.moveTo(x, y);
133
+ ctx.lineTo(x + w, y + h);
134
+ ctx.stroke();
135
+
136
+ // Arrow head at end
137
+ const angle = Math.atan2(h, w);
138
+ const headLen = 8;
139
+ ctx.beginPath();
140
+ ctx.moveTo(x + w, y + h);
141
+ ctx.lineTo(
142
+ x + w - headLen * Math.cos(angle - Math.PI / 6),
143
+ y + h - headLen * Math.sin(angle - Math.PI / 6),
144
+ );
145
+ ctx.moveTo(x + w, y + h);
146
+ ctx.lineTo(
147
+ x + w - headLen * Math.cos(angle + Math.PI / 6),
148
+ y + h - headLen * Math.sin(angle + Math.PI / 6),
149
+ );
150
+ ctx.stroke();
151
+
152
+ ctx.restore();
153
+ }
154
+
155
+ // =============================================================================
156
+ // Unknown type (fallback)
157
+ // =============================================================================
158
+
159
+ function drawUnknown(
160
+ ctx: CanvasRenderingContext2D,
161
+ drawing: ParsedDrawing,
162
+ x: number, y: number, w: number, h: number,
163
+ ): void {
164
+ ctx.save();
165
+ ctx.fillStyle = "#F9FAFB";
166
+ ctx.strokeStyle = "#D1D5DB";
167
+ ctx.lineWidth = 1;
168
+ ctx.setLineDash([4, 2]);
169
+ ctx.fillRect(x, y, w, h);
170
+ ctx.strokeRect(x, y, w, h);
171
+ ctx.setLineDash([]);
172
+
173
+ ctx.fillStyle = "#9CA3AF";
174
+ ctx.font = "11px -apple-system, sans-serif";
175
+ ctx.textAlign = "center";
176
+ ctx.textBaseline = "middle";
177
+ ctx.fillText(`[${drawing.type}]`, x + w / 2, y + h / 2);
178
+ ctx.restore();
179
+ }
180
+
181
+ // =============================================================================
182
+ // Text helpers
183
+ // =============================================================================
184
+
185
+ function drawCenteredText(
186
+ ctx: CanvasRenderingContext2D,
187
+ text: string,
188
+ x: number, y: number, w: number, h: number,
189
+ ): void {
190
+ ctx.fillStyle = "#1F2937";
191
+ ctx.font = "12px -apple-system, BlinkMacSystemFont, sans-serif";
192
+ ctx.textAlign = "center";
193
+ ctx.textBaseline = "middle";
194
+ ctx.fillText(text, x + w / 2, y + h / 2, w - 8);
195
+ }
196
+
197
+ function drawWrappedText(
198
+ ctx: CanvasRenderingContext2D,
199
+ text: string,
200
+ x: number, y: number, w: number, h: number,
201
+ ): void {
202
+ ctx.fillStyle = "#1F2937";
203
+ ctx.font = "12px -apple-system, BlinkMacSystemFont, sans-serif";
204
+ ctx.textAlign = "left";
205
+ ctx.textBaseline = "top";
206
+
207
+ const lineHeight = 16;
208
+ const words = text.split(/\s+/);
209
+ let line = "";
210
+ let cy = y;
211
+
212
+ for (const word of words) {
213
+ const testLine = line ? `${line} ${word}` : word;
214
+ if (ctx.measureText(testLine).width > w && line) {
215
+ ctx.fillText(line, x, cy, w);
216
+ line = word;
217
+ cy += lineHeight;
218
+ if (cy + lineHeight > y + h) return;
219
+ } else {
220
+ line = testLine;
221
+ }
222
+ }
223
+ if (line) ctx.fillText(line, x, cy, w);
224
+ }
225
+
226
+ // =============================================================================
227
+ // Anchor conversion
228
+ // =============================================================================
229
+
230
+ function anchorToPixels(
231
+ anchor: DrawingAnchor,
232
+ layout: LayoutEngine,
233
+ scrollX: number,
234
+ scrollY: number,
235
+ ): { x: number; y: number; w: number; h: number } {
236
+ const x1 = (layout.colX[anchor.from.col] ?? 0) + emuToPixels(anchor.from.colOffset ?? 0) - scrollX;
237
+ const y1 = (layout.rowY[anchor.from.row] ?? 0) + emuToPixels(anchor.from.rowOffset ?? 0) - scrollY;
238
+ const x2 = (layout.colX[anchor.to.col] ?? 0) + emuToPixels(anchor.to.colOffset ?? 0) - scrollX;
239
+ const y2 = (layout.rowY[anchor.to.row] ?? 0) + emuToPixels(anchor.to.rowOffset ?? 0) - scrollY;
240
+
241
+ return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
242
+ }
@@ -0,0 +1,163 @@
1
+ // =============================================================================
2
+ // Canvas Sparkline Renderer
3
+ // =============================================================================
4
+ //
5
+ // Draws mini charts within a single cell.
6
+ // Three types: line, column, winLoss (stacked).
7
+ // =============================================================================
8
+
9
+ // =============================================================================
10
+ // Types
11
+ // =============================================================================
12
+
13
+ export interface SparklineRenderInput {
14
+ /** Sparkline type. */
15
+ type: "line" | "column" | "stacked";
16
+ /** Data values for this sparkline. */
17
+ values: number[];
18
+ /** Series color. */
19
+ color: string;
20
+ /** Cell position/size. */
21
+ x: number;
22
+ y: number;
23
+ w: number;
24
+ h: number;
25
+ }
26
+
27
+ // =============================================================================
28
+ // Main entry
29
+ // =============================================================================
30
+
31
+ export function drawSparkline(
32
+ ctx: CanvasRenderingContext2D,
33
+ input: SparklineRenderInput,
34
+ ): void {
35
+ const { type, values, color, x, y, w, h } = input;
36
+ if (values.length === 0) return;
37
+
38
+ const padding = 2;
39
+ const pX = x + padding;
40
+ const pY = y + padding;
41
+ const pW = w - padding * 2;
42
+ const pH = h - padding * 2;
43
+
44
+ if (pW <= 0 || pH <= 0) return;
45
+
46
+ switch (type) {
47
+ case "line":
48
+ drawLineSparkline(ctx, values, color, pX, pY, pW, pH);
49
+ break;
50
+ case "column":
51
+ drawColumnSparkline(ctx, values, color, pX, pY, pW, pH);
52
+ break;
53
+ case "stacked":
54
+ drawWinLossSparkline(ctx, values, color, pX, pY, pW, pH);
55
+ break;
56
+ }
57
+ }
58
+
59
+ // =============================================================================
60
+ // Line sparkline
61
+ // =============================================================================
62
+
63
+ function drawLineSparkline(
64
+ ctx: CanvasRenderingContext2D,
65
+ values: number[],
66
+ color: string,
67
+ x: number, y: number, w: number, h: number,
68
+ ): void {
69
+ const min = arrayMin(values);
70
+ const max = arrayMax(values);
71
+ const range = max - min || 1;
72
+
73
+ ctx.strokeStyle = color;
74
+ ctx.lineWidth = 1.5;
75
+ ctx.lineJoin = "round";
76
+ ctx.beginPath();
77
+
78
+ for (let i = 0; i < values.length; i++) {
79
+ const px = x + (i / Math.max(values.length - 1, 1)) * w;
80
+ const py = y + h - ((values[i] - min) / range) * h;
81
+ if (i === 0) ctx.moveTo(px, py);
82
+ else ctx.lineTo(px, py);
83
+ }
84
+ ctx.stroke();
85
+ }
86
+
87
+ // =============================================================================
88
+ // Column sparkline
89
+ // =============================================================================
90
+
91
+ function drawColumnSparkline(
92
+ ctx: CanvasRenderingContext2D,
93
+ values: number[],
94
+ color: string,
95
+ x: number, y: number, w: number, h: number,
96
+ ): void {
97
+ const min = arrayMin([...values, 0]);
98
+ const max = arrayMax([...values, 0]);
99
+ const range = max - min || 1;
100
+ const zeroY = y + h - ((0 - min) / range) * h;
101
+
102
+ const barW = Math.max(1, (w / values.length) * 0.8);
103
+ const gap = (w / values.length) * 0.2;
104
+
105
+ for (let i = 0; i < values.length; i++) {
106
+ const val = values[i];
107
+ const barX = x + i * (barW + gap);
108
+ const barH = Math.abs((val / range) * h);
109
+
110
+ ctx.fillStyle = val >= 0 ? color : "#E11D48";
111
+ if (val >= 0) {
112
+ ctx.fillRect(barX, zeroY - barH, barW, barH);
113
+ } else {
114
+ ctx.fillRect(barX, zeroY, barW, barH);
115
+ }
116
+ }
117
+ }
118
+
119
+ // =============================================================================
120
+ // Win/Loss sparkline
121
+ // =============================================================================
122
+
123
+ function drawWinLossSparkline(
124
+ ctx: CanvasRenderingContext2D,
125
+ values: number[],
126
+ color: string,
127
+ x: number, y: number, w: number, h: number,
128
+ ): void {
129
+ const barW = Math.max(1, (w / values.length) * 0.8);
130
+ const gap = (w / values.length) * 0.2;
131
+ const halfH = h / 2;
132
+ const barH = halfH - 1;
133
+
134
+ for (let i = 0; i < values.length; i++) {
135
+ const val = values[i];
136
+ const barX = x + i * (barW + gap);
137
+
138
+ if (val > 0) {
139
+ ctx.fillStyle = color;
140
+ ctx.fillRect(barX, y + halfH - barH, barW, barH);
141
+ } else if (val < 0) {
142
+ ctx.fillStyle = "#E11D48";
143
+ ctx.fillRect(barX, y + halfH + 1, barW, barH);
144
+ }
145
+ // val === 0: no bar
146
+ }
147
+ }
148
+
149
+ // =============================================================================
150
+ // Helpers
151
+ // =============================================================================
152
+
153
+ function arrayMin(arr: number[]): number {
154
+ let min = arr[0] ?? 0;
155
+ for (let i = 1; i < arr.length; i++) if (arr[i] < min) min = arr[i];
156
+ return min;
157
+ }
158
+
159
+ function arrayMax(arr: number[]): number {
160
+ let max = arr[0] ?? 0;
161
+ for (let i = 1; i < arr.length; i++) if (arr[i] > max) max = arr[i];
162
+ return max;
163
+ }