@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
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { unzipXlsx, decodeUtf8 } from "./ooxml_zip";
|
|
3
|
+
import { parseTheme, defaultThemeColors } from "./ooxml_color";
|
|
4
|
+
import type { ThemeData } from "./ooxml_color";
|
|
5
|
+
import { parseStyles, emptyStyleResolver } from "./ooxml_styles";
|
|
6
|
+
import { parseWorkbook, parseWorkbookRels, parseSharedStrings } from "./ooxml_workbook";
|
|
7
|
+
import { parseSheet, emptySheet } from "./ooxml_sheet";
|
|
8
|
+
import { parseChartXml } from "./ooxml_chart";
|
|
9
|
+
import { parseDrawingXml } from "./ooxml_drawing";
|
|
10
|
+
import type { DrawingRelation } from "./ooxml_drawing";
|
|
11
|
+
import { parseTableXml, extractTableRIds } from "./ooxml_table";
|
|
12
|
+
import {
|
|
13
|
+
parsePivotCacheDefinitionXml,
|
|
14
|
+
parsePivotTableXml,
|
|
15
|
+
extractPivotTableRIds,
|
|
16
|
+
extractWorkbookPivotCaches,
|
|
17
|
+
} from "./ooxml_pivot";
|
|
18
|
+
import { parseVmlDrawing } from "./ooxml_shape";
|
|
19
|
+
import type { ParsedSpreadsheet, ParsedSheet, PrintTitles } from "./types";
|
|
20
|
+
import type { ParsedChart } from "./ooxml_chart_types";
|
|
21
|
+
import type { ParsedPivotCache, ParsedPivotTable } from "./ooxml_pivot_types";
|
|
22
|
+
import { colLettersToNum } from "./excel_utils";
|
|
23
|
+
|
|
24
|
+
// Re-export all public types from the canonical source
|
|
25
|
+
export type {
|
|
26
|
+
ParsedSpreadsheet,
|
|
27
|
+
ParsedSheet,
|
|
28
|
+
ParsedImage,
|
|
29
|
+
FreezePane,
|
|
30
|
+
ParsedColumn,
|
|
31
|
+
ParsedRow,
|
|
32
|
+
CellContent,
|
|
33
|
+
RichTextPart,
|
|
34
|
+
RichTextFont,
|
|
35
|
+
ParsedCell,
|
|
36
|
+
CellStyle,
|
|
37
|
+
BorderStyle,
|
|
38
|
+
MergedCellRange,
|
|
39
|
+
} from "./types";
|
|
40
|
+
export { PasswordProtectedError } from "./types";
|
|
41
|
+
|
|
42
|
+
export type {
|
|
43
|
+
ParsedConditionalFormat,
|
|
44
|
+
ParsedCfRule,
|
|
45
|
+
ParsedColorScaleRule,
|
|
46
|
+
ParsedDataBarRule,
|
|
47
|
+
ParsedIconSetRule,
|
|
48
|
+
ParsedStyleRule,
|
|
49
|
+
CfThreshold,
|
|
50
|
+
ParsedCfStyle,
|
|
51
|
+
} from "./excel_cf_types";
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Parsing
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
export async function parseExcelFile(
|
|
58
|
+
url: string,
|
|
59
|
+
signal?: AbortSignal,
|
|
60
|
+
): Promise<ParsedSpreadsheet> {
|
|
61
|
+
const response = await fetch(url, { signal });
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
return parseExcelBuffer(await response.arrayBuffer());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse an in-memory .xlsx buffer into a ParsedSpreadsheet.
|
|
70
|
+
* Use this when you already have the bytes (e.g. from File.arrayBuffer()).
|
|
71
|
+
*/
|
|
72
|
+
export function parseExcelBuffer(arrayBuffer: ArrayBuffer): ParsedSpreadsheet {
|
|
73
|
+
const zip = unzipXlsx(arrayBuffer);
|
|
74
|
+
return parseExcelFromZip(zip);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse from already-unzipped entries. Use this when you need both the
|
|
79
|
+
* ParsedSpreadsheet and the raw ZIP entries (e.g. for delta export).
|
|
80
|
+
*/
|
|
81
|
+
export function parseExcelFromZip(zip: Record<string, Uint8Array>): ParsedSpreadsheet {
|
|
82
|
+
// Parse theme
|
|
83
|
+
const themeEntry = findEntry(zip, "xl/theme/theme1.xml");
|
|
84
|
+
const theme: ThemeData = themeEntry
|
|
85
|
+
? parseTheme(decodeUtf8(themeEntry))
|
|
86
|
+
: { colors: defaultThemeColors(), majorFont: "Calibri", minorFont: "Calibri" };
|
|
87
|
+
|
|
88
|
+
// Parse styles
|
|
89
|
+
const stylesEntry = findEntry(zip, "xl/styles.xml");
|
|
90
|
+
const styles = stylesEntry
|
|
91
|
+
? parseStyles(decodeUtf8(stylesEntry), theme)
|
|
92
|
+
: emptyStyleResolver();
|
|
93
|
+
|
|
94
|
+
// Parse workbook
|
|
95
|
+
const wbEntry = findEntry(zip, "xl/workbook.xml");
|
|
96
|
+
if (!wbEntry) {
|
|
97
|
+
return { sheets: [emptySheet("Sheet1")], activeSheetIndex: 0 };
|
|
98
|
+
}
|
|
99
|
+
const workbookInfo = parseWorkbook(decodeUtf8(wbEntry));
|
|
100
|
+
|
|
101
|
+
// Parse workbook relationships
|
|
102
|
+
const wbRelsEntry = findEntry(zip, "xl/_rels/workbook.xml.rels");
|
|
103
|
+
const wbRels = wbRelsEntry ? parseWorkbookRels(decodeUtf8(wbRelsEntry)) : new Map<string, string>();
|
|
104
|
+
|
|
105
|
+
// Parse shared strings
|
|
106
|
+
const ssEntry = findEntry(zip, "xl/sharedStrings.xml");
|
|
107
|
+
const sharedStrings = ssEntry
|
|
108
|
+
? parseSharedStrings(decodeUtf8(ssEntry), theme, styles.indexedColors)
|
|
109
|
+
: [];
|
|
110
|
+
|
|
111
|
+
// Generic XML parser for chart/drawing/table/comment files
|
|
112
|
+
const xmlParser = new XMLParser({
|
|
113
|
+
ignoreAttributes: false,
|
|
114
|
+
attributeNamePrefix: "@_",
|
|
115
|
+
processEntities: false,
|
|
116
|
+
isArray: (tagName) =>
|
|
117
|
+
tagName === "Relationship" ||
|
|
118
|
+
tagName === "xdr:twoCellAnchor" ||
|
|
119
|
+
tagName === "xdr:oneCellAnchor" ||
|
|
120
|
+
tagName === "tableColumn" ||
|
|
121
|
+
tagName === "pivotField" ||
|
|
122
|
+
tagName === "cacheField" ||
|
|
123
|
+
tagName === "field" ||
|
|
124
|
+
tagName === "pageField" ||
|
|
125
|
+
tagName === "dataField" ||
|
|
126
|
+
tagName === "pivotCache",
|
|
127
|
+
});
|
|
128
|
+
const parseXmlDoc = (xml: string) => xmlParser.parse(xml) as Record<string, unknown>;
|
|
129
|
+
|
|
130
|
+
// -------------------------------------------------------------------------
|
|
131
|
+
// Pivot caches (workbook scope)
|
|
132
|
+
// -------------------------------------------------------------------------
|
|
133
|
+
// workbook.xml lists each pivotCache by cacheId + r:id; the rId resolves
|
|
134
|
+
// through the workbook rels to a pivotCacheDefinition path. Multiple pivot
|
|
135
|
+
// tables on different sheets may share a cache, so we parse caches once
|
|
136
|
+
// here and reference them from per-sheet pivot tables below.
|
|
137
|
+
const pivotCaches = new Map<number, ParsedPivotCache>();
|
|
138
|
+
if (wbEntry) {
|
|
139
|
+
const wbDoc = parseXmlDoc(decodeUtf8(wbEntry));
|
|
140
|
+
const wbCacheMap = extractWorkbookPivotCaches(wbDoc);
|
|
141
|
+
for (const [cacheId, rId] of wbCacheMap) {
|
|
142
|
+
const cacheTarget = wbRels.get(rId);
|
|
143
|
+
if (!cacheTarget) continue;
|
|
144
|
+
const cachePath = cacheTarget.startsWith("/")
|
|
145
|
+
? cacheTarget.slice(1)
|
|
146
|
+
: `xl/${cacheTarget}`;
|
|
147
|
+
const cacheEntry = findEntry(zip, cachePath);
|
|
148
|
+
if (!cacheEntry) continue;
|
|
149
|
+
try {
|
|
150
|
+
pivotCaches.set(
|
|
151
|
+
cacheId,
|
|
152
|
+
parsePivotCacheDefinitionXml(decodeUtf8(cacheEntry), cacheId, parseXmlDoc),
|
|
153
|
+
);
|
|
154
|
+
} catch {
|
|
155
|
+
// Defensive: malformed cache shouldn't break workbook load. Pivot
|
|
156
|
+
// tables referencing this cache will simply render as empty.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Parse every sheet. Hidden/veryHidden sheets are parsed too but tagged via
|
|
162
|
+
// `hidden` so renderers can skip them while data consumers can read them.
|
|
163
|
+
const sheets: ParsedSheet[] = [];
|
|
164
|
+
let activeSheetIndex = 0;
|
|
165
|
+
|
|
166
|
+
let visibleIndex = 0;
|
|
167
|
+
for (let i = 0; i < workbookInfo.sheets.length; i++) {
|
|
168
|
+
const sheetInfo = workbookInfo.sheets[i];
|
|
169
|
+
const isHidden = sheetInfo.state === "hidden" || sheetInfo.state === "veryHidden";
|
|
170
|
+
|
|
171
|
+
const relTarget = wbRels.get(sheetInfo.rId);
|
|
172
|
+
if (!relTarget) continue;
|
|
173
|
+
|
|
174
|
+
const sheetPath = relTarget.startsWith("/")
|
|
175
|
+
? relTarget.slice(1)
|
|
176
|
+
: `xl/${relTarget}`;
|
|
177
|
+
const sheetEntry = findEntry(zip, sheetPath);
|
|
178
|
+
if (!sheetEntry) continue;
|
|
179
|
+
|
|
180
|
+
const sheetRelsPath = sheetPath.replace(/([^/]+)$/, "_rels/$1.rels");
|
|
181
|
+
const sheetRelsEntry = findEntry(zip, sheetRelsPath);
|
|
182
|
+
const sheetRelsXml = sheetRelsEntry ? decodeUtf8(sheetRelsEntry) : undefined;
|
|
183
|
+
|
|
184
|
+
const parsed = parseSheet(
|
|
185
|
+
decodeUtf8(sheetEntry),
|
|
186
|
+
sheetRelsXml,
|
|
187
|
+
styles,
|
|
188
|
+
sharedStrings,
|
|
189
|
+
theme,
|
|
190
|
+
styles.indexedColors,
|
|
191
|
+
zip,
|
|
192
|
+
workbookInfo.date1904,
|
|
193
|
+
);
|
|
194
|
+
parsed.name = sheetInfo.name;
|
|
195
|
+
if (isHidden) parsed.hidden = true;
|
|
196
|
+
|
|
197
|
+
const printTitlesRaw = workbookInfo.printTitlesBySheet.get(i);
|
|
198
|
+
if (printTitlesRaw) {
|
|
199
|
+
const pt = parsePrintTitlesRef(printTitlesRaw);
|
|
200
|
+
if (pt) parsed.printTitles = pt;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Extract charts, tables, drawings, and threaded comments from sheet rels
|
|
204
|
+
if (sheetRelsXml) {
|
|
205
|
+
const sheetDir = sheetPath.substring(0, sheetPath.lastIndexOf("/") + 1);
|
|
206
|
+
const relsDoc = parseXmlDoc(sheetRelsXml);
|
|
207
|
+
const relsRoot = (relsDoc["Relationships"] ?? relsDoc) as Record<string, unknown>;
|
|
208
|
+
const relArr = (Array.isArray(relsRoot["Relationship"])
|
|
209
|
+
? relsRoot["Relationship"]
|
|
210
|
+
: relsRoot["Relationship"] ? [relsRoot["Relationship"]] : []) as Array<Record<string, string>>;
|
|
211
|
+
|
|
212
|
+
const relMap = new Map<string, { type: string; target: string }>();
|
|
213
|
+
for (const rel of relArr) {
|
|
214
|
+
const id = rel["@_Id"];
|
|
215
|
+
const target = rel["@_Target"];
|
|
216
|
+
const type = rel["@_Type"] ?? "";
|
|
217
|
+
if (id && target) relMap.set(id, { type, target });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- Drawings (contains charts and additional images) ---
|
|
221
|
+
const charts: ParsedChart[] = [];
|
|
222
|
+
const drawingRelations: DrawingRelation[] = [];
|
|
223
|
+
for (const [, rel] of relMap) {
|
|
224
|
+
if (!rel.type.includes("/drawing") || rel.type.includes("drawingML")) continue;
|
|
225
|
+
const drawingPath = resolveRelTarget(rel.target, sheetDir);
|
|
226
|
+
const drawingEntry = findEntry(zip, drawingPath);
|
|
227
|
+
if (!drawingEntry) continue;
|
|
228
|
+
|
|
229
|
+
const drawingXml = decodeUtf8(drawingEntry);
|
|
230
|
+
const relations = parseDrawingXml(drawingXml, parseXmlDoc);
|
|
231
|
+
drawingRelations.push(...relations);
|
|
232
|
+
|
|
233
|
+
// Parse drawing rels (for chart and image targets)
|
|
234
|
+
const drawingRelsPath = drawingPath.replace(/([^/]+)$/, "_rels/$1.rels");
|
|
235
|
+
const drawingRelsEntry = findEntry(zip, drawingRelsPath);
|
|
236
|
+
const drawingRels = new Map<string, string>();
|
|
237
|
+
if (drawingRelsEntry) {
|
|
238
|
+
const drawingRelsDoc = parseXmlDoc(decodeUtf8(drawingRelsEntry));
|
|
239
|
+
const dr = (drawingRelsDoc["Relationships"] ?? drawingRelsDoc) as Record<string, unknown>;
|
|
240
|
+
const drArr = (Array.isArray(dr["Relationship"])
|
|
241
|
+
? dr["Relationship"]
|
|
242
|
+
: dr["Relationship"] ? [dr["Relationship"]] : []) as Array<Record<string, string>>;
|
|
243
|
+
for (const r of drArr) {
|
|
244
|
+
if (r["@_Id"] && r["@_Target"]) drawingRels.set(r["@_Id"], r["@_Target"]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const drawingDir = drawingPath.substring(0, drawingPath.lastIndexOf("/") + 1);
|
|
249
|
+
|
|
250
|
+
// Load charts referenced by drawing anchors
|
|
251
|
+
for (const dr of relations) {
|
|
252
|
+
if (dr.type !== "chart" || !dr.rId) continue;
|
|
253
|
+
const chartTarget = drawingRels.get(dr.rId);
|
|
254
|
+
if (!chartTarget) continue;
|
|
255
|
+
const chartPath = resolveRelTarget(chartTarget, drawingDir);
|
|
256
|
+
const chartEntry = findEntry(zip, chartPath);
|
|
257
|
+
if (!chartEntry) continue;
|
|
258
|
+
const chartData = parseChartXml(decodeUtf8(chartEntry), parseXmlDoc);
|
|
259
|
+
charts.push({ ...chartData, anchor: dr.anchor });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (charts.length > 0) parsed.charts = charts;
|
|
264
|
+
|
|
265
|
+
// Collect shape/textbox/connector drawings
|
|
266
|
+
const drawings = drawingRelations
|
|
267
|
+
.filter((dr) => dr.type === "shape" && dr.drawing)
|
|
268
|
+
.map((dr) => dr.drawing!);
|
|
269
|
+
if (drawings.length > 0) parsed.drawings = drawings;
|
|
270
|
+
|
|
271
|
+
// --- VML drawings (legacy form controls) ---
|
|
272
|
+
for (const [, rel] of relMap) {
|
|
273
|
+
if (!rel.type.includes("vmlDrawing")) continue;
|
|
274
|
+
const vmlPath = resolveRelTarget(rel.target, sheetDir);
|
|
275
|
+
const vmlEntry = findEntry(zip, vmlPath);
|
|
276
|
+
if (!vmlEntry) continue;
|
|
277
|
+
const vmlDrawings = parseVmlDrawing(decodeUtf8(vmlEntry), parseXmlDoc);
|
|
278
|
+
if (vmlDrawings.length > 0) {
|
|
279
|
+
if (!parsed.drawings) parsed.drawings = [];
|
|
280
|
+
parsed.drawings.push(...vmlDrawings);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- Tables ---
|
|
285
|
+
const tableRIds = extractTableRIds(relsDoc);
|
|
286
|
+
const tables = [];
|
|
287
|
+
for (const [, target] of tableRIds) {
|
|
288
|
+
const tablePath = resolveRelTarget(target, sheetDir);
|
|
289
|
+
const tableEntry = findEntry(zip, tablePath);
|
|
290
|
+
if (!tableEntry) continue;
|
|
291
|
+
tables.push(parseTableXml(decodeUtf8(tableEntry), parseXmlDoc));
|
|
292
|
+
}
|
|
293
|
+
if (tables.length > 0) parsed.tables = tables;
|
|
294
|
+
|
|
295
|
+
// --- Pivot tables ---
|
|
296
|
+
const pivotRIds = extractPivotTableRIds(relsDoc);
|
|
297
|
+
const pivotTables: ParsedPivotTable[] = [];
|
|
298
|
+
for (const [, target] of pivotRIds) {
|
|
299
|
+
const pivotPath = resolveRelTarget(target, sheetDir);
|
|
300
|
+
const pivotEntry = findEntry(zip, pivotPath);
|
|
301
|
+
if (!pivotEntry) continue;
|
|
302
|
+
try {
|
|
303
|
+
pivotTables.push(
|
|
304
|
+
parsePivotTableXml(decodeUtf8(pivotEntry), parseXmlDoc),
|
|
305
|
+
);
|
|
306
|
+
} catch {
|
|
307
|
+
// Defensive: skip malformed pivots rather than failing the whole load.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (pivotTables.length > 0) parsed.pivotTables = pivotTables;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
sheets.push(parsed);
|
|
314
|
+
|
|
315
|
+
if (i === workbookInfo.activeSheetIndex) {
|
|
316
|
+
activeSheetIndex = visibleIndex;
|
|
317
|
+
}
|
|
318
|
+
visibleIndex++;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (sheets.length === 0) {
|
|
322
|
+
return { sheets: [emptySheet("Sheet1")], activeSheetIndex: 0 };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (activeSheetIndex >= sheets.length) activeSheetIndex = 0;
|
|
326
|
+
|
|
327
|
+
const result: ParsedSpreadsheet = { sheets, activeSheetIndex };
|
|
328
|
+
if (workbookInfo.namedRanges.size > 0) {
|
|
329
|
+
result.namedRanges = workbookInfo.namedRanges;
|
|
330
|
+
}
|
|
331
|
+
if (workbookInfo.fullCalcOnLoad) {
|
|
332
|
+
result.fullCalcOnLoad = true;
|
|
333
|
+
}
|
|
334
|
+
if (pivotCaches.size > 0) {
|
|
335
|
+
result.pivotCaches = pivotCaches;
|
|
336
|
+
}
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// =============================================================================
|
|
341
|
+
// Helpers
|
|
342
|
+
// =============================================================================
|
|
343
|
+
|
|
344
|
+
function findEntry(zip: Record<string, Uint8Array>, path: string): Uint8Array | undefined {
|
|
345
|
+
if (zip[path]) return zip[path];
|
|
346
|
+
const lower = path.toLowerCase();
|
|
347
|
+
for (const key of Object.keys(zip)) {
|
|
348
|
+
if (key.toLowerCase() === lower) return zip[key];
|
|
349
|
+
}
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Resolve a relationship target path relative to a base directory.
|
|
355
|
+
* Handles absolute paths (leading /), relative paths with ../, and plain relative paths.
|
|
356
|
+
*/
|
|
357
|
+
function resolveRelTarget(target: string, baseDir: string): string {
|
|
358
|
+
if (target.startsWith("/")) return target.slice(1);
|
|
359
|
+
const baseParts = baseDir.split("/").filter(Boolean);
|
|
360
|
+
const targetParts = target.split("/");
|
|
361
|
+
const resolved = [...baseParts];
|
|
362
|
+
for (const part of targetParts) {
|
|
363
|
+
if (part === "..") {
|
|
364
|
+
resolved.pop();
|
|
365
|
+
} else if (part !== ".") {
|
|
366
|
+
resolved.push(part);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return resolved.join("/");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Parse a `_xlnm.Print_Titles` raw reference string into repeat row/col ranges.
|
|
374
|
+
* Example: `"Sheet1!$A:$B,Sheet1!$1:$2"` → `{ repeatRows: [1, 2], repeatCols: [1, 2] }`.
|
|
375
|
+
*/
|
|
376
|
+
function parsePrintTitlesRef(raw: string): PrintTitles | undefined {
|
|
377
|
+
const result: PrintTitles = {};
|
|
378
|
+
for (const segment of raw.split(",")) {
|
|
379
|
+
const afterBang = segment.includes("!") ? segment.split("!")[1] : segment;
|
|
380
|
+
if (!afterBang) continue;
|
|
381
|
+
const rowMatch = afterBang.match(/^\$(\d+):\$(\d+)$/);
|
|
382
|
+
if (rowMatch) {
|
|
383
|
+
result.repeatRows = [parseInt(rowMatch[1], 10), parseInt(rowMatch[2], 10)];
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const colMatch = afterBang.match(/^\$([A-Z]+):\$([A-Z]+)$/i);
|
|
387
|
+
if (colMatch) {
|
|
388
|
+
result.repeatCols = [colLettersToNum(colMatch[1]), colLettersToNum(colMatch[2])];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return result.repeatRows || result.repeatCols ? result : undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps ExcelJS FillPattern types to CSS background-image strings.
|
|
3
|
+
* Uses repeating-linear-gradient for stripe/grid patterns
|
|
4
|
+
* and inline SVG data URIs for dot patterns.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
type PatternGenerator = (fg: string, bg: string) => string;
|
|
8
|
+
|
|
9
|
+
const PATTERNS: Record<string, PatternGenerator> = {
|
|
10
|
+
darkVertical: (fg, bg) =>
|
|
11
|
+
`repeating-linear-gradient(90deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 4px)`,
|
|
12
|
+
darkHorizontal: (fg, bg) =>
|
|
13
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 4px)`,
|
|
14
|
+
darkDown: (fg, bg) =>
|
|
15
|
+
`repeating-linear-gradient(45deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 5px)`,
|
|
16
|
+
darkUp: (fg, bg) =>
|
|
17
|
+
`repeating-linear-gradient(-45deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 5px)`,
|
|
18
|
+
darkGrid: (fg, bg) =>
|
|
19
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 1px, transparent 1px, transparent 4px), ` +
|
|
20
|
+
`repeating-linear-gradient(90deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 4px)`,
|
|
21
|
+
darkTrellis: (fg, bg) =>
|
|
22
|
+
`repeating-linear-gradient(45deg, ${fg} 0px, ${fg} 1px, transparent 1px, transparent 5px), ` +
|
|
23
|
+
`repeating-linear-gradient(-45deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 5px)`,
|
|
24
|
+
|
|
25
|
+
lightVertical: (fg, bg) =>
|
|
26
|
+
`repeating-linear-gradient(90deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 8px)`,
|
|
27
|
+
lightHorizontal: (fg, bg) =>
|
|
28
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 8px)`,
|
|
29
|
+
lightDown: (fg, bg) =>
|
|
30
|
+
`repeating-linear-gradient(45deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 8px)`,
|
|
31
|
+
lightUp: (fg, bg) =>
|
|
32
|
+
`repeating-linear-gradient(-45deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 8px)`,
|
|
33
|
+
lightGrid: (fg, bg) =>
|
|
34
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 1px, transparent 1px, transparent 8px), ` +
|
|
35
|
+
`repeating-linear-gradient(90deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 8px)`,
|
|
36
|
+
lightTrellis: (fg, bg) =>
|
|
37
|
+
`repeating-linear-gradient(45deg, ${fg} 0px, ${fg} 1px, transparent 1px, transparent 8px), ` +
|
|
38
|
+
`repeating-linear-gradient(-45deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 8px)`,
|
|
39
|
+
|
|
40
|
+
mediumGray: (fg, bg) =>
|
|
41
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 3px)`,
|
|
42
|
+
darkGray: (fg, bg) =>
|
|
43
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 2px, ${bg} 2px, ${bg} 3px)`,
|
|
44
|
+
lightGray: (fg, bg) =>
|
|
45
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 5px)`,
|
|
46
|
+
|
|
47
|
+
// gray125 = 12.5% gray (every 8th pixel filled)
|
|
48
|
+
gray125: (fg, bg) =>
|
|
49
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 8px)`,
|
|
50
|
+
|
|
51
|
+
// gray0625 = 6.25% gray (every 16th pixel filled)
|
|
52
|
+
gray0625: (fg, bg) =>
|
|
53
|
+
`repeating-linear-gradient(0deg, ${fg} 0px, ${fg} 1px, ${bg} 1px, ${bg} 16px)`,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function getPatternFillCSS(
|
|
57
|
+
pattern: string,
|
|
58
|
+
fgColor: string,
|
|
59
|
+
bgColor: string,
|
|
60
|
+
): string | undefined {
|
|
61
|
+
const generator = PATTERNS[pattern];
|
|
62
|
+
if (!generator) return undefined;
|
|
63
|
+
return generator(fgColor, bgColor);
|
|
64
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Excel Table Styles
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Built-in table style definitions for structured tables.
|
|
6
|
+
// Excel has ~80 built-in styles (TableStyleLight1-21, TableStyleMedium1-28,
|
|
7
|
+
// TableStyleDark1-11). Each style defines colors for header, total,
|
|
8
|
+
// first/last column, and banded row/column stripes.
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export interface TableStyleDef {
|
|
16
|
+
/** Style name (e.g., "TableStyleMedium2"). */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Header row background. */
|
|
19
|
+
headerBg: string;
|
|
20
|
+
/** Header row text color. */
|
|
21
|
+
headerFg: string;
|
|
22
|
+
/** Total row background. */
|
|
23
|
+
totalBg: string;
|
|
24
|
+
/** Total row text color. */
|
|
25
|
+
totalFg: string;
|
|
26
|
+
/** Odd row stripe background. */
|
|
27
|
+
stripe1Bg: string;
|
|
28
|
+
/** Even row stripe background (empty = transparent). */
|
|
29
|
+
stripe2Bg: string;
|
|
30
|
+
/** First column highlight. */
|
|
31
|
+
firstColBg?: string;
|
|
32
|
+
/** Last column highlight. */
|
|
33
|
+
lastColBg?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Built-in styles
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
const LIGHT_STYLES: TableStyleDef[] = buildStyleSet("TableStyleLight", [
|
|
41
|
+
{ header: "#000000", headerFg: "#FFFFFF", s1: "#D9E2F3", s2: "" },
|
|
42
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#D6E4F0", s2: "" },
|
|
43
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#FCE4D6", s2: "" },
|
|
44
|
+
{ header: "#A5A5A5", headerFg: "#FFFFFF", s1: "#EDEDED", s2: "" },
|
|
45
|
+
{ header: "#FFC000", headerFg: "#FFFFFF", s1: "#FFF2CC", s2: "" },
|
|
46
|
+
{ header: "#5B9BD5", headerFg: "#FFFFFF", s1: "#DDEBF7", s2: "" },
|
|
47
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#E2EFDA", s2: "" },
|
|
48
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#D6E4F0", s2: "#EDF2F9" },
|
|
49
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#FCE4D6", s2: "#FDF0E8" },
|
|
50
|
+
{ header: "#A5A5A5", headerFg: "#FFFFFF", s1: "#EDEDED", s2: "#F6F6F6" },
|
|
51
|
+
{ header: "#FFC000", headerFg: "#000000", s1: "#FFF2CC", s2: "#FFF8E6" },
|
|
52
|
+
{ header: "#5B9BD5", headerFg: "#FFFFFF", s1: "#DDEBF7", s2: "#EDF4FB" },
|
|
53
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#E2EFDA", s2: "#F0F7EC" },
|
|
54
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#B4C6E7", s2: "#D6E4F0" },
|
|
55
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#F8CBAD", s2: "#FCE4D6" },
|
|
56
|
+
{ header: "#A5A5A5", headerFg: "#FFFFFF", s1: "#DBDBDB", s2: "#EDEDED" },
|
|
57
|
+
{ header: "#FFC000", headerFg: "#000000", s1: "#FFE699", s2: "#FFF2CC" },
|
|
58
|
+
{ header: "#5B9BD5", headerFg: "#FFFFFF", s1: "#BDD7EE", s2: "#DDEBF7" },
|
|
59
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#C6E0B4", s2: "#E2EFDA" },
|
|
60
|
+
{ header: "#000000", headerFg: "#FFFFFF", s1: "#F2F2F2", s2: "" },
|
|
61
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#D6E4F0", s2: "" },
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const MEDIUM_STYLES: TableStyleDef[] = buildStyleSet("TableStyleMedium", [
|
|
65
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#D6E4F0", s2: "" },
|
|
66
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#FCE4D6", s2: "" },
|
|
67
|
+
{ header: "#A5A5A5", headerFg: "#FFFFFF", s1: "#EDEDED", s2: "" },
|
|
68
|
+
{ header: "#FFC000", headerFg: "#000000", s1: "#FFF2CC", s2: "" },
|
|
69
|
+
{ header: "#5B9BD5", headerFg: "#FFFFFF", s1: "#DDEBF7", s2: "" },
|
|
70
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#E2EFDA", s2: "" },
|
|
71
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#D6E4F0", s2: "#EDF2F9" },
|
|
72
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#FCE4D6", s2: "#FDF0E8" },
|
|
73
|
+
{ header: "#A5A5A5", headerFg: "#FFFFFF", s1: "#EDEDED", s2: "#F6F6F6" },
|
|
74
|
+
{ header: "#FFC000", headerFg: "#000000", s1: "#FFF2CC", s2: "#FFF8E6" },
|
|
75
|
+
{ header: "#5B9BD5", headerFg: "#FFFFFF", s1: "#DDEBF7", s2: "#EDF4FB" },
|
|
76
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#E2EFDA", s2: "#F0F7EC" },
|
|
77
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#B4C6E7", s2: "#D6E4F0" },
|
|
78
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#F8CBAD", s2: "#FCE4D6" },
|
|
79
|
+
{ header: "#A5A5A5", headerFg: "#FFFFFF", s1: "#DBDBDB", s2: "#EDEDED" },
|
|
80
|
+
{ header: "#FFC000", headerFg: "#000000", s1: "#FFE699", s2: "#FFF2CC" },
|
|
81
|
+
{ header: "#5B9BD5", headerFg: "#FFFFFF", s1: "#BDD7EE", s2: "#DDEBF7" },
|
|
82
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#C6E0B4", s2: "#E2EFDA" },
|
|
83
|
+
{ header: "#000000", headerFg: "#FFFFFF", s1: "#D9D9D9", s2: "#F2F2F2" },
|
|
84
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#8FAADC", s2: "#B4C6E7" },
|
|
85
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#F4B183", s2: "#F8CBAD" },
|
|
86
|
+
{ header: "#A5A5A5", headerFg: "#FFFFFF", s1: "#C9C9C9", s2: "#DBDBDB" },
|
|
87
|
+
{ header: "#FFC000", headerFg: "#000000", s1: "#FFD966", s2: "#FFE699" },
|
|
88
|
+
{ header: "#5B9BD5", headerFg: "#FFFFFF", s1: "#9DC3E6", s2: "#BDD7EE" },
|
|
89
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#A9D18E", s2: "#C6E0B4" },
|
|
90
|
+
{ header: "#264478", headerFg: "#FFFFFF", s1: "#D6DCE4", s2: "#E9ECF1" },
|
|
91
|
+
{ header: "#9B57A0", headerFg: "#FFFFFF", s1: "#E8D5EA", s2: "#F3EAF4" },
|
|
92
|
+
{ header: "#636363", headerFg: "#FFFFFF", s1: "#E0E0E0", s2: "#F0F0F0" },
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const DARK_STYLES: TableStyleDef[] = buildStyleSet("TableStyleDark", [
|
|
96
|
+
{ header: "#000000", headerFg: "#FFFFFF", s1: "#737373", s2: "#595959" },
|
|
97
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#44546A", s2: "#2F3B52" },
|
|
98
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#C55A11", s2: "#843C0C" },
|
|
99
|
+
{ header: "#A5A5A5", headerFg: "#FFFFFF", s1: "#7F7F7F", s2: "#595959" },
|
|
100
|
+
{ header: "#FFC000", headerFg: "#000000", s1: "#BF8F00", s2: "#806000" },
|
|
101
|
+
{ header: "#5B9BD5", headerFg: "#FFFFFF", s1: "#2E75B6", s2: "#1F4E79" },
|
|
102
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#548235", s2: "#375623" },
|
|
103
|
+
{ header: "#000000", headerFg: "#FFFFFF", s1: "#404040", s2: "#262626" },
|
|
104
|
+
{ header: "#4472C4", headerFg: "#FFFFFF", s1: "#305496", s2: "#203764" },
|
|
105
|
+
{ header: "#ED7D31", headerFg: "#FFFFFF", s1: "#BF8F00", s2: "#806000" },
|
|
106
|
+
{ header: "#70AD47", headerFg: "#FFFFFF", s1: "#375623", s2: "#1D3311" },
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// Style lookup
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
const ALL_STYLES = new Map<string, TableStyleDef>();
|
|
114
|
+
|
|
115
|
+
function initStyles(): void {
|
|
116
|
+
if (ALL_STYLES.size > 0) return;
|
|
117
|
+
for (const s of [...LIGHT_STYLES, ...MEDIUM_STYLES, ...DARK_STYLES]) {
|
|
118
|
+
ALL_STYLES.set(s.name, s);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get a table style definition by name.
|
|
124
|
+
* Returns a default light style if not found.
|
|
125
|
+
*/
|
|
126
|
+
export function getTableStyle(name: string): TableStyleDef {
|
|
127
|
+
initStyles();
|
|
128
|
+
return ALL_STYLES.get(name) ?? LIGHT_STYLES[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get all available table style names.
|
|
133
|
+
*/
|
|
134
|
+
export function getTableStyleNames(): string[] {
|
|
135
|
+
initStyles();
|
|
136
|
+
return Array.from(ALL_STYLES.keys());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Builder
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
interface StyleInput {
|
|
144
|
+
header: string;
|
|
145
|
+
headerFg: string;
|
|
146
|
+
s1: string;
|
|
147
|
+
s2: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildStyleSet(prefix: string, inputs: StyleInput[]): TableStyleDef[] {
|
|
151
|
+
return inputs.map((input, i) => ({
|
|
152
|
+
name: `${prefix}${i + 1}`,
|
|
153
|
+
headerBg: input.header,
|
|
154
|
+
headerFg: input.headerFg,
|
|
155
|
+
totalBg: input.header,
|
|
156
|
+
totalFg: input.headerFg,
|
|
157
|
+
stripe1Bg: input.s1,
|
|
158
|
+
stripe2Bg: input.s2,
|
|
159
|
+
}));
|
|
160
|
+
}
|