@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
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@lotics/xlsx",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./*": "./src/*.ts"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "typecheck": "tsgo --noEmit",
17
+ "lint": "oxlint",
18
+ "test": "vitest run"
19
+ },
20
+ "dependencies": {
21
+ "@formulajs/formulajs": "^4.5.6",
22
+ "fast-formula-parser": "^1.0.19",
23
+ "fast-xml-parser": "^5.5.6",
24
+ "fflate": "^0.8.2"
25
+ },
26
+ "devDependencies": {
27
+ "exceljs": "^4.4.0",
28
+ "vitest": "^4.1.7"
29
+ }
30
+ }
@@ -0,0 +1,71 @@
1
+ import { computeAutoSumFormula } from "./auto_sum";
2
+ import { WorkbookModel, SheetModel, StyleRegistry } from "./workbook_model";
3
+
4
+ function makeWorkbook(): WorkbookModel {
5
+ const styles = new StyleRegistry();
6
+ const sheet = new SheetModel("Sheet1");
7
+ return new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
8
+ }
9
+
10
+ describe("computeAutoSumFormula", () => {
11
+ test("detects contiguous numeric cells above", () => {
12
+ const wb = makeWorkbook();
13
+ wb.sheets[0].set("A1", 10);
14
+ wb.sheets[0].set("A2", 20);
15
+ wb.sheets[0].set("A3", 30);
16
+ // Active cell is A4 (row 4, col 1)
17
+ const result = computeAutoSumFormula(wb, 0, 4, 1);
18
+ expect(result).toBe("=SUM(A1:A3)");
19
+ });
20
+
21
+ test("detects contiguous numeric cells to the left", () => {
22
+ const wb = makeWorkbook();
23
+ wb.sheets[0].set("A1", 10);
24
+ wb.sheets[0].set("B1", 20);
25
+ wb.sheets[0].set("C1", 30);
26
+ // Active cell is D1 (row 1, col 4)
27
+ const result = computeAutoSumFormula(wb, 0, 1, 4);
28
+ expect(result).toBe("=SUM(A1:C1)");
29
+ });
30
+
31
+ test("prefers upward over leftward", () => {
32
+ const wb = makeWorkbook();
33
+ wb.sheets[0].set("B1", 10);
34
+ wb.sheets[0].set("B2", 20);
35
+ wb.sheets[0].set("A3", 5);
36
+ // Active cell is B3: has cells above AND to the left
37
+ const result = computeAutoSumFormula(wb, 0, 3, 2);
38
+ expect(result).toBe("=SUM(B1:B2)");
39
+ });
40
+
41
+ test("returns null when no contiguous numeric cells", () => {
42
+ const wb = makeWorkbook();
43
+ // A1 has text, not numeric
44
+ wb.sheets[0].set("A1", "hello");
45
+ const result = computeAutoSumFormula(wb, 0, 2, 1);
46
+ expect(result).toBeNull();
47
+ });
48
+
49
+ test("returns null for empty surroundings", () => {
50
+ const wb = makeWorkbook();
51
+ const result = computeAutoSumFormula(wb, 0, 5, 5);
52
+ expect(result).toBeNull();
53
+ });
54
+
55
+ test("single numeric cell above", () => {
56
+ const wb = makeWorkbook();
57
+ wb.sheets[0].set("A1", 42);
58
+ const result = computeAutoSumFormula(wb, 0, 2, 1);
59
+ expect(result).toBe("=SUM(A1:A1)");
60
+ });
61
+
62
+ test("stops at non-numeric cell", () => {
63
+ const wb = makeWorkbook();
64
+ wb.sheets[0].set("A1", "header");
65
+ wb.sheets[0].set("A2", 10);
66
+ wb.sheets[0].set("A3", 20);
67
+ // Active cell is A4 — should detect A2:A3 only
68
+ const result = computeAutoSumFormula(wb, 0, 4, 1);
69
+ expect(result).toBe("=SUM(A2:A3)");
70
+ });
71
+ });
@@ -0,0 +1,83 @@
1
+ // =============================================================================
2
+ // AutoSum
3
+ // =============================================================================
4
+ //
5
+ // Detects a contiguous numeric range above or to the left of the active cell
6
+ // and returns a SUM formula string. Returns null if no range is found.
7
+ // =============================================================================
8
+
9
+ import type { WorkbookModel } from "./workbook_model";
10
+ import { rowColToRef } from "./excel_utils";
11
+
12
+ /**
13
+ * Compute an AutoSum formula for the given cell position.
14
+ * Looks upward first for contiguous numeric cells, then left.
15
+ * Returns a formula string like "=SUM(A1:A5)" or null if no range found.
16
+ */
17
+ export function computeAutoSumFormula(
18
+ workbook: WorkbookModel,
19
+ sheetIndex: number,
20
+ row: number,
21
+ col: number,
22
+ ): string | null {
23
+ const sheet = workbook.sheets[sheetIndex];
24
+
25
+ // Look upward for contiguous numeric cells
26
+ const upRange = findContiguousNumericRange(sheet, row, col, -1, 0);
27
+ if (upRange) {
28
+ return `=SUM(${rowColToRef(upRange.startRow, upRange.startCol)}:${rowColToRef(upRange.endRow, upRange.endCol)})`;
29
+ }
30
+
31
+ // Look left for contiguous numeric cells
32
+ const leftRange = findContiguousNumericRange(sheet, row, col, 0, -1);
33
+ if (leftRange) {
34
+ return `=SUM(${rowColToRef(leftRange.startRow, leftRange.startCol)}:${rowColToRef(leftRange.endRow, leftRange.endCol)})`;
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ function findContiguousNumericRange(
41
+ sheet: { getCell(ref: string): { value: unknown } | undefined },
42
+ row: number,
43
+ col: number,
44
+ dRow: number,
45
+ dCol: number,
46
+ ): { startRow: number; startCol: number; endRow: number; endCol: number } | null {
47
+ let r = row + dRow;
48
+ let c = col + dCol;
49
+ let endR = -1;
50
+ let endC = -1;
51
+
52
+ // Walk in the direction, collecting contiguous numeric cells
53
+ while (r >= 1 && c >= 1) {
54
+ const ref = rowColToRef(r, c);
55
+ const cell = sheet.getCell(ref);
56
+ if (!cell || typeof cell.value !== "number") break;
57
+ if (endR === -1) {
58
+ endR = r;
59
+ endC = c;
60
+ }
61
+ r += dRow;
62
+ c += dCol;
63
+ }
64
+
65
+ if (endR === -1) return null;
66
+
67
+ // The range goes from the farthest cell found to the cell adjacent to the target
68
+ const startR = dRow !== 0 ? r - dRow : row;
69
+ const startC = dCol !== 0 ? c - dCol : col;
70
+
71
+ // Ensure we have at least one cell
72
+ if (startR === endR && startC === endC) {
73
+ return { startRow: startR, startCol: startC, endRow: endR, endCol: endC };
74
+ }
75
+
76
+ // Order so start < end
77
+ return {
78
+ startRow: Math.min(startR, endR),
79
+ startCol: Math.min(startC, endC),
80
+ endRow: Math.max(startR, endR),
81
+ endCol: Math.max(startC, endC),
82
+ };
83
+ }
@@ -0,0 +1,135 @@
1
+ // =============================================================================
2
+ // Canvas Conditional Formatting Renderer
3
+ // =============================================================================
4
+ //
5
+ // Draws conditional formatting overlays on the canvas:
6
+ // - Data bars (gradient or solid fill within cell)
7
+ // - Icon sets (SVG→Image→drawImage)
8
+ // - Color scale backgrounds (handled by CfOverlay.backgroundColor in cell draw)
9
+ // - Style overrides (font color/bold/italic from CF rules)
10
+ // =============================================================================
11
+
12
+ import type { CfOverlay } from "./excel_cf_evaluate";
13
+
14
+ // =============================================================================
15
+ // Data bar rendering
16
+ // =============================================================================
17
+
18
+ export function drawDataBar(
19
+ ctx: CanvasRenderingContext2D,
20
+ overlay: CfOverlay,
21
+ cellX: number,
22
+ cellY: number,
23
+ cellW: number,
24
+ cellH: number,
25
+ ): void {
26
+ if (overlay.dataBarWidth === undefined || overlay.dataBarColor === undefined) return;
27
+
28
+ const barHeight = Math.max(cellH - 6, 4);
29
+ const barY = cellY + (cellH - barHeight) / 2;
30
+ const barWidth = (overlay.dataBarWidth / 100) * (cellW - 4);
31
+
32
+ // Draw bar with gradient
33
+ const gradient = ctx.createLinearGradient(cellX + 2, barY, cellX + 2 + barWidth, barY);
34
+ gradient.addColorStop(0, overlay.dataBarColor);
35
+ gradient.addColorStop(1, adjustAlpha(overlay.dataBarColor, 0.3));
36
+ ctx.fillStyle = gradient;
37
+ ctx.fillRect(cellX + 2, barY, barWidth, barHeight);
38
+
39
+ // Border around bar
40
+ ctx.strokeStyle = overlay.dataBarColor;
41
+ ctx.lineWidth = 0.5;
42
+ ctx.strokeRect(cellX + 2, barY, barWidth, barHeight);
43
+ }
44
+
45
+ // =============================================================================
46
+ // Icon rendering
47
+ // =============================================================================
48
+
49
+ // Cache for parsed SVG → ImageBitmap
50
+ const iconCache = new Map<string, HTMLImageElement>();
51
+
52
+ export function drawIcon(
53
+ ctx: CanvasRenderingContext2D,
54
+ overlay: CfOverlay,
55
+ cellX: number,
56
+ cellY: number,
57
+ cellH: number,
58
+ ): void {
59
+ if (!overlay.iconSvg) return;
60
+
61
+ const iconSize = Math.min(cellH - 4, 16);
62
+ const iconY = cellY + (cellH - iconSize) / 2;
63
+ const iconX = cellX + 2;
64
+
65
+ // Try to draw from cache
66
+ let img = iconCache.get(overlay.iconSvg);
67
+ if (img && img.complete) {
68
+ ctx.drawImage(img, iconX, iconY, iconSize, iconSize);
69
+ return;
70
+ }
71
+
72
+ // Create and cache image element from SVG
73
+ if (!img) {
74
+ img = new Image();
75
+ const svgBlob = new Blob([overlay.iconSvg], { type: "image/svg+xml" });
76
+ img.src = URL.createObjectURL(svgBlob);
77
+ iconCache.set(overlay.iconSvg, img);
78
+ img.onload = () => {
79
+ // Redraw will pick up the cached image next frame
80
+ URL.revokeObjectURL(img!.src);
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Returns the horizontal offset needed if an icon is present,
87
+ * so cell text can be shifted right.
88
+ */
89
+ export function getIconOffset(overlay: CfOverlay | undefined, cellH: number): number {
90
+ if (!overlay?.iconSvg) return 0;
91
+ return Math.min(cellH - 4, 16) + 4; // icon width + gap
92
+ }
93
+
94
+ // =============================================================================
95
+ // Apply CF style overrides
96
+ // =============================================================================
97
+
98
+ /**
99
+ * Returns modified style properties from CF overlay.
100
+ * Call this before drawing cell text to apply CF color/bold/italic.
101
+ */
102
+ export function applyCfStyleOverrides(
103
+ overlay: CfOverlay | undefined,
104
+ fontColor: string,
105
+ fontBold: boolean,
106
+ fontItalic: boolean,
107
+ ): { fontColor: string; fontBold: boolean; fontItalic: boolean } {
108
+ if (!overlay) return { fontColor, fontBold, fontItalic };
109
+ return {
110
+ fontColor: overlay.fontColor ?? fontColor,
111
+ fontBold: overlay.fontBold ?? fontBold,
112
+ fontItalic: overlay.fontItalic ?? fontItalic,
113
+ };
114
+ }
115
+
116
+ // =============================================================================
117
+ // Helpers
118
+ // =============================================================================
119
+
120
+ function adjustAlpha(color: string, alpha: number): string {
121
+ // Parse rgb/rgba and return with new alpha
122
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
123
+ if (match) {
124
+ return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${alpha})`;
125
+ }
126
+ // Hex color
127
+ const hexMatch = color.match(/^#([0-9a-fA-F]{6})/);
128
+ if (hexMatch) {
129
+ const r = parseInt(hexMatch[1].slice(0, 2), 16);
130
+ const g = parseInt(hexMatch[1].slice(2, 4), 16);
131
+ const b = parseInt(hexMatch[1].slice(4, 6), 16);
132
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
133
+ }
134
+ return color;
135
+ }