@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
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
|
+
});
|
package/src/auto_sum.ts
ADDED
|
@@ -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
|
+
}
|
package/src/canvas_cf.ts
ADDED
|
@@ -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
|
+
}
|