@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,1271 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import type {
|
|
3
|
+
ParsedSheet,
|
|
4
|
+
ParsedRow,
|
|
5
|
+
ParsedCell,
|
|
6
|
+
ParsedColumn,
|
|
7
|
+
ParsedImage,
|
|
8
|
+
FreezePane,
|
|
9
|
+
MergedCellRange,
|
|
10
|
+
PageSetup,
|
|
11
|
+
HeaderFooter,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import type { StyleResolver } from "./ooxml_styles";
|
|
14
|
+
import type { SharedStringEntry } from "./ooxml_workbook";
|
|
15
|
+
import type { ThemeData } from "./ooxml_color";
|
|
16
|
+
import { parseColorElement, resolveColor, prefixHex } from "./ooxml_color";
|
|
17
|
+
import { decodeXmlEntities } from "./xml_entities";
|
|
18
|
+
import { formatCellValue, buildCellContent } from "./ooxml_cell_format";
|
|
19
|
+
import { colLettersToNum } from "./excel_utils";
|
|
20
|
+
import type {
|
|
21
|
+
ParsedConditionalFormat,
|
|
22
|
+
ParsedCfRule,
|
|
23
|
+
ParsedStyleRule,
|
|
24
|
+
CfThreshold,
|
|
25
|
+
} from "./excel_cf_types";
|
|
26
|
+
import type {
|
|
27
|
+
ParsedDataValidation,
|
|
28
|
+
ParsedSparklineGroup,
|
|
29
|
+
ParsedSparkline,
|
|
30
|
+
AutoFilterRange,
|
|
31
|
+
AutoFilterColumn,
|
|
32
|
+
} from "./ooxml_chart_types";
|
|
33
|
+
import { decodeUtf8 } from "./ooxml_zip";
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Constants
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
const MAX_ROWS_PER_SHEET = 10_000;
|
|
40
|
+
const MAX_IMAGES = 50;
|
|
41
|
+
|
|
42
|
+
// fast-xml-parser returns a string for `<el>foo</el>` but an object
|
|
43
|
+
// `{ "#text": "foo", "@_xml:space": "preserve", ... }` when the element
|
|
44
|
+
// carries any attribute. Excel emits `xml:space="preserve"` whenever cell
|
|
45
|
+
// or formula text contains leading/trailing whitespace or newlines, so
|
|
46
|
+
// every site that reads element text must normalize both shapes.
|
|
47
|
+
function extractElementText(node: unknown): string | undefined {
|
|
48
|
+
if (node === undefined || node === null) return undefined;
|
|
49
|
+
if (typeof node === "object") {
|
|
50
|
+
const text = (node as Record<string, unknown>)["#text"];
|
|
51
|
+
return text !== undefined ? String(text) : "";
|
|
52
|
+
}
|
|
53
|
+
return String(node);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// XML parser
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
const xmlParser = new XMLParser({
|
|
61
|
+
ignoreAttributes: false,
|
|
62
|
+
attributeNamePrefix: "@_",
|
|
63
|
+
processEntities: false,
|
|
64
|
+
isArray: (tagName) =>
|
|
65
|
+
tagName === "row" ||
|
|
66
|
+
tagName === "c" ||
|
|
67
|
+
tagName === "col" ||
|
|
68
|
+
tagName === "mergeCell" ||
|
|
69
|
+
tagName === "conditionalFormatting" ||
|
|
70
|
+
tagName === "cfRule" ||
|
|
71
|
+
tagName === "cfvo" ||
|
|
72
|
+
tagName === "color" ||
|
|
73
|
+
tagName === "hyperlink" ||
|
|
74
|
+
tagName === "Relationship" ||
|
|
75
|
+
tagName === "sheetView" ||
|
|
76
|
+
tagName === "xdr:twoCellAnchor" ||
|
|
77
|
+
tagName === "xdr:oneCellAnchor" ||
|
|
78
|
+
tagName === "dataValidation" ||
|
|
79
|
+
tagName === "filterColumn" ||
|
|
80
|
+
tagName === "filter" ||
|
|
81
|
+
tagName === "brk" ||
|
|
82
|
+
tagName === "ext",
|
|
83
|
+
// Preserve whitespace in cell values
|
|
84
|
+
trimValues: false,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const relsParser = new XMLParser({
|
|
88
|
+
ignoreAttributes: false,
|
|
89
|
+
attributeNamePrefix: "@_",
|
|
90
|
+
processEntities: false,
|
|
91
|
+
isArray: (tagName) => tagName === "Relationship",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const drawingParser = new XMLParser({
|
|
95
|
+
ignoreAttributes: false,
|
|
96
|
+
attributeNamePrefix: "@_",
|
|
97
|
+
processEntities: false,
|
|
98
|
+
isArray: (tagName) =>
|
|
99
|
+
tagName === "xdr:twoCellAnchor" ||
|
|
100
|
+
tagName === "xdr:oneCellAnchor" ||
|
|
101
|
+
tagName === "Relationship",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Main sheet parsing
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
export function parseSheet(
|
|
109
|
+
sheetXml: string,
|
|
110
|
+
sheetRelsXml: string | undefined,
|
|
111
|
+
styles: StyleResolver,
|
|
112
|
+
sharedStrings: SharedStringEntry[],
|
|
113
|
+
theme: ThemeData,
|
|
114
|
+
indexedColors: string[],
|
|
115
|
+
zipEntries: Record<string, Uint8Array>,
|
|
116
|
+
date1904: boolean,
|
|
117
|
+
): ParsedSheet {
|
|
118
|
+
const doc = xmlParser.parse(sheetXml);
|
|
119
|
+
const worksheet = doc?.["worksheet"];
|
|
120
|
+
if (!worksheet) return emptySheet("Sheet");
|
|
121
|
+
|
|
122
|
+
// Parse relationships for this sheet (hyperlinks, comments, drawings)
|
|
123
|
+
const rels = sheetRelsXml ? parseSheetRels(sheetRelsXml) : new Map<string, { target: string; type: string }>();
|
|
124
|
+
|
|
125
|
+
// Sheet format defaults
|
|
126
|
+
const sheetFormatPr = worksheet["sheetFormatPr"] as Record<string, string> | undefined;
|
|
127
|
+
const defaultRowHeight = sheetFormatPr?.["@_defaultRowHeight"]
|
|
128
|
+
? parseFloat(sheetFormatPr["@_defaultRowHeight"])
|
|
129
|
+
: 15;
|
|
130
|
+
const defaultColWidth = sheetFormatPr?.["@_defaultColWidth"]
|
|
131
|
+
? parseFloat(sheetFormatPr["@_defaultColWidth"])
|
|
132
|
+
: 8;
|
|
133
|
+
|
|
134
|
+
// Sheet views (freeze panes, grid lines)
|
|
135
|
+
const { freezePane, showGridLines } = parseSheetViews(worksheet);
|
|
136
|
+
|
|
137
|
+
// Column definitions
|
|
138
|
+
const { columns: parsedCols, colHidden, colOutlineLevels } = parseCols(worksheet, defaultColWidth);
|
|
139
|
+
|
|
140
|
+
// Hyperlinks map (cell ref → URL)
|
|
141
|
+
const hyperlinkMap = parseHyperlinks(worksheet, rels);
|
|
142
|
+
|
|
143
|
+
// Parse sheet data (rows + cells)
|
|
144
|
+
const {
|
|
145
|
+
rows,
|
|
146
|
+
totalRowCount,
|
|
147
|
+
truncated,
|
|
148
|
+
maxCol,
|
|
149
|
+
maxContentWidth,
|
|
150
|
+
rowOutlineLevels,
|
|
151
|
+
} = parseSheetData(worksheet, styles, sharedStrings, hyperlinkMap, defaultRowHeight, date1904);
|
|
152
|
+
|
|
153
|
+
// Build final columns from parsed col defs and data bounds
|
|
154
|
+
const colCount = Math.max(parsedCols.length > 0 ? parsedCols[parsedCols.length - 1].max : 0, maxCol);
|
|
155
|
+
const columns = buildColumns(parsedCols, colHidden, maxContentWidth, colCount, defaultColWidth);
|
|
156
|
+
|
|
157
|
+
// Merged cells
|
|
158
|
+
const mergedCells = parseMergedCells(worksheet);
|
|
159
|
+
|
|
160
|
+
// Conditional formatting
|
|
161
|
+
const conditionalFormats = parseConditionalFormats(worksheet, theme, indexedColors);
|
|
162
|
+
|
|
163
|
+
// Images
|
|
164
|
+
const images = parseImages(rels, zipEntries);
|
|
165
|
+
|
|
166
|
+
// Data validations
|
|
167
|
+
const dataValidations = parseDataValidations(worksheet);
|
|
168
|
+
|
|
169
|
+
// Sparklines (from sheet extension list)
|
|
170
|
+
const sparklineGroups = parseSparklineGroups(worksheet);
|
|
171
|
+
|
|
172
|
+
// Auto-filter
|
|
173
|
+
const autoFilter = parseAutoFilter(worksheet);
|
|
174
|
+
|
|
175
|
+
const result: ParsedSheet = {
|
|
176
|
+
name: "",
|
|
177
|
+
columns,
|
|
178
|
+
rows,
|
|
179
|
+
mergedCells,
|
|
180
|
+
totalRowCount,
|
|
181
|
+
truncated,
|
|
182
|
+
defaultRowHeight,
|
|
183
|
+
defaultColWidth,
|
|
184
|
+
showGridLines,
|
|
185
|
+
freezePane,
|
|
186
|
+
conditionalFormats,
|
|
187
|
+
images,
|
|
188
|
+
rowOutlineLevels,
|
|
189
|
+
colOutlineLevels,
|
|
190
|
+
};
|
|
191
|
+
if (dataValidations.length > 0) result.dataValidations = dataValidations;
|
|
192
|
+
if (sparklineGroups.length > 0) result.sparklineGroups = sparklineGroups;
|
|
193
|
+
if (autoFilter) result.autoFilter = autoFilter;
|
|
194
|
+
|
|
195
|
+
// Page breaks
|
|
196
|
+
const { rowPageBreaks, colPageBreaks } = parsePageBreaks(worksheet);
|
|
197
|
+
if (rowPageBreaks.length > 0) result.rowPageBreaks = rowPageBreaks;
|
|
198
|
+
if (colPageBreaks.length > 0) result.colPageBreaks = colPageBreaks;
|
|
199
|
+
|
|
200
|
+
// Page setup + margins (only emitted when diverging from defaults)
|
|
201
|
+
const pageSetup = parsePageSetupElement(worksheet);
|
|
202
|
+
if (pageSetup) result.pageSetup = pageSetup;
|
|
203
|
+
|
|
204
|
+
// Header/footer
|
|
205
|
+
const headerFooter = parseHeaderFooterElement(worksheet);
|
|
206
|
+
if (headerFooter) result.headerFooter = headerFooter;
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parsePageSetupElement(worksheet: Record<string, unknown>): PageSetup | undefined {
|
|
212
|
+
const ps = worksheet["pageSetup"] as Record<string, string> | undefined;
|
|
213
|
+
const pm = worksheet["pageMargins"] as Record<string, string> | undefined;
|
|
214
|
+
|
|
215
|
+
const out: PageSetup = {};
|
|
216
|
+
if (ps) {
|
|
217
|
+
const orient = ps["@_orientation"];
|
|
218
|
+
if (orient === "portrait" || orient === "landscape") out.orientation = orient;
|
|
219
|
+
if (ps["@_paperSize"]) out.paperSize = parseInt(ps["@_paperSize"], 10);
|
|
220
|
+
if (ps["@_fitToWidth"] !== undefined) out.fitToWidth = parseInt(ps["@_fitToWidth"], 10);
|
|
221
|
+
if (ps["@_fitToHeight"] !== undefined) out.fitToHeight = parseInt(ps["@_fitToHeight"], 10);
|
|
222
|
+
if (ps["@_scale"]) out.scale = parseInt(ps["@_scale"], 10);
|
|
223
|
+
}
|
|
224
|
+
if (pm) {
|
|
225
|
+
const margins: NonNullable<PageSetup["margins"]> = {};
|
|
226
|
+
const fields: Array<["left" | "right" | "top" | "bottom" | "header" | "footer", number]> = [
|
|
227
|
+
["left", 0.7],
|
|
228
|
+
["right", 0.7],
|
|
229
|
+
["top", 0.75],
|
|
230
|
+
["bottom", 0.75],
|
|
231
|
+
["header", 0.3],
|
|
232
|
+
["footer", 0.3],
|
|
233
|
+
];
|
|
234
|
+
let anyNonDefault = false;
|
|
235
|
+
for (const [key, def] of fields) {
|
|
236
|
+
const raw = pm[`@_${key}`];
|
|
237
|
+
if (raw === undefined) continue;
|
|
238
|
+
const val = parseFloat(raw);
|
|
239
|
+
if (!Number.isNaN(val) && Math.abs(val - def) > 1e-6) {
|
|
240
|
+
margins[key] = val;
|
|
241
|
+
anyNonDefault = true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (anyNonDefault) out.margins = margins;
|
|
245
|
+
}
|
|
246
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function parseHeaderFooterElement(worksheet: Record<string, unknown>): HeaderFooter | undefined {
|
|
250
|
+
const hf = worksheet["headerFooter"] as Record<string, unknown> | undefined;
|
|
251
|
+
if (!hf) return undefined;
|
|
252
|
+
const out: HeaderFooter = {};
|
|
253
|
+
const readText = (key: string): string | undefined => {
|
|
254
|
+
const val = hf[key];
|
|
255
|
+
let raw: string | undefined;
|
|
256
|
+
if (typeof val === "string") raw = val;
|
|
257
|
+
else if (val && typeof val === "object" && "#text" in val && typeof (val as { "#text": unknown })["#text"] === "string") {
|
|
258
|
+
raw = (val as { "#text": string })["#text"];
|
|
259
|
+
}
|
|
260
|
+
return raw !== undefined ? decodeXmlEntities(raw) : undefined;
|
|
261
|
+
};
|
|
262
|
+
const oh = readText("oddHeader");
|
|
263
|
+
const of = readText("oddFooter");
|
|
264
|
+
const eh = readText("evenHeader");
|
|
265
|
+
const ef = readText("evenFooter");
|
|
266
|
+
const fh = readText("firstHeader");
|
|
267
|
+
const ff = readText("firstFooter");
|
|
268
|
+
if (oh !== undefined) out.oddHeader = oh;
|
|
269
|
+
if (of !== undefined) out.oddFooter = of;
|
|
270
|
+
if (eh !== undefined) out.evenHeader = eh;
|
|
271
|
+
if (ef !== undefined) out.evenFooter = ef;
|
|
272
|
+
if (fh !== undefined) out.firstHeader = fh;
|
|
273
|
+
if (ff !== undefined) out.firstFooter = ff;
|
|
274
|
+
if (hf["@_differentOddEven"] === "1") out.differentOddEven = true;
|
|
275
|
+
if (hf["@_differentFirst"] === "1") out.differentFirst = true;
|
|
276
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// =============================================================================
|
|
280
|
+
// Sheet relationships
|
|
281
|
+
// =============================================================================
|
|
282
|
+
|
|
283
|
+
function parseSheetRels(relsXml: string): Map<string, { target: string; type: string }> {
|
|
284
|
+
const doc = relsParser.parse(relsXml);
|
|
285
|
+
const relationships = doc?.["Relationships"];
|
|
286
|
+
if (!relationships) return new Map();
|
|
287
|
+
|
|
288
|
+
const relArr = relationships["Relationship"] as Array<Record<string, string>> | undefined;
|
|
289
|
+
if (!relArr) return new Map();
|
|
290
|
+
|
|
291
|
+
const map = new Map<string, { target: string; type: string }>();
|
|
292
|
+
for (const rel of relArr) {
|
|
293
|
+
const id = rel["@_Id"];
|
|
294
|
+
const target = rel["@_Target"];
|
|
295
|
+
const type = rel["@_Type"] ?? "";
|
|
296
|
+
if (id && target) {
|
|
297
|
+
map.set(id, { target, type });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return map;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// Sheet views
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
function parseSheetViews(worksheet: Record<string, unknown>): {
|
|
308
|
+
freezePane?: FreezePane;
|
|
309
|
+
showGridLines: boolean;
|
|
310
|
+
} {
|
|
311
|
+
const views = worksheet["sheetViews"] as Record<string, unknown> | undefined;
|
|
312
|
+
if (!views) return { showGridLines: true };
|
|
313
|
+
|
|
314
|
+
const viewArr = views["sheetView"];
|
|
315
|
+
const firstView = Array.isArray(viewArr) ? viewArr[0] : viewArr;
|
|
316
|
+
if (!firstView || typeof firstView !== "object") return { showGridLines: true };
|
|
317
|
+
|
|
318
|
+
const view = firstView as Record<string, unknown>;
|
|
319
|
+
const showGridLines = view["@_showGridLines"] !== "0";
|
|
320
|
+
|
|
321
|
+
let freezePane: FreezePane | undefined;
|
|
322
|
+
const pane = view["pane"] as Record<string, string> | undefined;
|
|
323
|
+
if (pane?.["@_state"] === "frozen") {
|
|
324
|
+
const frozenCols = parseInt(pane["@_xSplit"] ?? "0", 10);
|
|
325
|
+
const frozenRows = parseInt(pane["@_ySplit"] ?? "0", 10);
|
|
326
|
+
if (frozenRows > 0 || frozenCols > 0) {
|
|
327
|
+
freezePane = { frozenRows, frozenCols };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { freezePane, showGridLines };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// =============================================================================
|
|
335
|
+
// Columns
|
|
336
|
+
// =============================================================================
|
|
337
|
+
|
|
338
|
+
interface ColDef {
|
|
339
|
+
min: number;
|
|
340
|
+
max: number;
|
|
341
|
+
width: number;
|
|
342
|
+
hidden: boolean;
|
|
343
|
+
outlineLevel: number;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseCols(
|
|
347
|
+
worksheet: Record<string, unknown>,
|
|
348
|
+
defaultColWidth: number,
|
|
349
|
+
): {
|
|
350
|
+
columns: ColDef[];
|
|
351
|
+
colHidden: Set<number>;
|
|
352
|
+
colOutlineLevels: Map<number, number>;
|
|
353
|
+
} {
|
|
354
|
+
const colsEl = worksheet["cols"] as Record<string, unknown> | undefined;
|
|
355
|
+
const colHidden = new Set<number>();
|
|
356
|
+
const colOutlineLevels = new Map<number, number>();
|
|
357
|
+
|
|
358
|
+
if (!colsEl) return { columns: [], colHidden, colOutlineLevels };
|
|
359
|
+
|
|
360
|
+
const colArr = colsEl["col"] as Array<Record<string, string>> | undefined;
|
|
361
|
+
if (!colArr) return { columns: [], colHidden, colOutlineLevels };
|
|
362
|
+
|
|
363
|
+
const columns: ColDef[] = [];
|
|
364
|
+
for (const col of colArr) {
|
|
365
|
+
const min = parseInt(col["@_min"] ?? "1", 10);
|
|
366
|
+
const max = parseInt(col["@_max"] ?? "1", 10);
|
|
367
|
+
const width = col["@_width"] ? parseFloat(col["@_width"]) : defaultColWidth;
|
|
368
|
+
const hidden = col["@_hidden"] === "1" || col["@_hidden"] === "true";
|
|
369
|
+
const outlineLevel = parseInt(col["@_outlineLevel"] ?? "0", 10);
|
|
370
|
+
|
|
371
|
+
columns.push({ min, max, width, hidden, outlineLevel });
|
|
372
|
+
|
|
373
|
+
// Expand into individual column entries
|
|
374
|
+
for (let c = min; c <= max; c++) {
|
|
375
|
+
if (hidden) colHidden.add(c);
|
|
376
|
+
if (outlineLevel > 0) colOutlineLevels.set(c, outlineLevel);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { columns, colHidden, colOutlineLevels };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function buildColumns(
|
|
384
|
+
colDefs: ColDef[],
|
|
385
|
+
colHidden: Set<number>,
|
|
386
|
+
maxContentWidth: Map<number, number>,
|
|
387
|
+
colCount: number,
|
|
388
|
+
_defaultColWidth: number,
|
|
389
|
+
): ParsedColumn[] {
|
|
390
|
+
// Build a map of column index → explicit width from col definitions
|
|
391
|
+
const explicitWidth = new Map<number, number>();
|
|
392
|
+
for (const def of colDefs) {
|
|
393
|
+
for (let c = def.min; c <= def.max; c++) {
|
|
394
|
+
explicitWidth.set(c, def.width);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const columns: ParsedColumn[] = [];
|
|
399
|
+
for (let c = 1; c <= colCount; c++) {
|
|
400
|
+
const excelWidth = explicitWidth.get(c);
|
|
401
|
+
const contentChars = maxContentWidth.get(c) ?? 0;
|
|
402
|
+
const hidden = colHidden.has(c);
|
|
403
|
+
|
|
404
|
+
let width: number;
|
|
405
|
+
if (excelWidth && excelWidth > 8) {
|
|
406
|
+
width = excelWidth;
|
|
407
|
+
} else {
|
|
408
|
+
width = Math.max(8, Math.min(contentChars + 2, 40));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
columns.push({ index: c, width, hidden });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return columns;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// =============================================================================
|
|
418
|
+
// Sheet data (rows + cells)
|
|
419
|
+
// =============================================================================
|
|
420
|
+
|
|
421
|
+
function parseSheetData(
|
|
422
|
+
worksheet: Record<string, unknown>,
|
|
423
|
+
styles: StyleResolver,
|
|
424
|
+
sharedStrings: SharedStringEntry[],
|
|
425
|
+
hyperlinkMap: Map<string, string>,
|
|
426
|
+
defaultRowHeight: number,
|
|
427
|
+
date1904: boolean,
|
|
428
|
+
): {
|
|
429
|
+
rows: ParsedRow[];
|
|
430
|
+
totalRowCount: number;
|
|
431
|
+
truncated: boolean;
|
|
432
|
+
maxCol: number;
|
|
433
|
+
maxContentWidth: Map<number, number>;
|
|
434
|
+
rowOutlineLevels: Map<number, number>;
|
|
435
|
+
} {
|
|
436
|
+
const sheetData = worksheet["sheetData"] as Record<string, unknown> | undefined;
|
|
437
|
+
if (!sheetData) {
|
|
438
|
+
return {
|
|
439
|
+
rows: [],
|
|
440
|
+
totalRowCount: 0,
|
|
441
|
+
truncated: false,
|
|
442
|
+
maxCol: 0,
|
|
443
|
+
maxContentWidth: new Map(),
|
|
444
|
+
rowOutlineLevels: new Map(),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const rowArr = sheetData["row"] as Array<Record<string, unknown>> | undefined;
|
|
449
|
+
if (!rowArr) {
|
|
450
|
+
return {
|
|
451
|
+
rows: [],
|
|
452
|
+
totalRowCount: 0,
|
|
453
|
+
truncated: false,
|
|
454
|
+
maxCol: 0,
|
|
455
|
+
maxContentWidth: new Map(),
|
|
456
|
+
rowOutlineLevels: new Map(),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const totalRowCount = rowArr.length > 0
|
|
461
|
+
? parseInt((rowArr[rowArr.length - 1]["@_r"] as string) ?? String(rowArr.length), 10)
|
|
462
|
+
: 0;
|
|
463
|
+
|
|
464
|
+
const rows: ParsedRow[] = [];
|
|
465
|
+
let maxCol = 0;
|
|
466
|
+
const maxContentWidth = new Map<number, number>();
|
|
467
|
+
const rowOutlineLevels = new Map<number, number>();
|
|
468
|
+
let rowCount = 0;
|
|
469
|
+
|
|
470
|
+
for (const rowEl of rowArr) {
|
|
471
|
+
const rowIndex = parseInt((rowEl["@_r"] as string) ?? "0", 10);
|
|
472
|
+
if (rowIndex === 0) continue;
|
|
473
|
+
|
|
474
|
+
// Track outline levels even if we skip the row
|
|
475
|
+
const outlineLevel = parseInt((rowEl["@_outlineLevel"] as string) ?? "0", 10);
|
|
476
|
+
if (outlineLevel > 0) {
|
|
477
|
+
rowOutlineLevels.set(rowIndex, outlineLevel);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Skip hidden rows
|
|
481
|
+
if (rowEl["@_hidden"] === "1" || rowEl["@_hidden"] === "true") continue;
|
|
482
|
+
|
|
483
|
+
// Truncation check
|
|
484
|
+
rowCount++;
|
|
485
|
+
if (rowCount > MAX_ROWS_PER_SHEET) continue;
|
|
486
|
+
|
|
487
|
+
const height = rowEl["@_ht"]
|
|
488
|
+
? parseFloat(rowEl["@_ht"] as string)
|
|
489
|
+
: defaultRowHeight;
|
|
490
|
+
|
|
491
|
+
const cellArr = rowEl["c"] as Array<Record<string, unknown>> | undefined;
|
|
492
|
+
const cells: ParsedCell[] = [];
|
|
493
|
+
|
|
494
|
+
if (cellArr) {
|
|
495
|
+
for (const cellEl of cellArr) {
|
|
496
|
+
const parsed = parseCell(cellEl, styles, sharedStrings, hyperlinkMap, date1904);
|
|
497
|
+
if (parsed) {
|
|
498
|
+
cells.push(parsed);
|
|
499
|
+
if (parsed.column > maxCol) maxCol = parsed.column;
|
|
500
|
+
|
|
501
|
+
// Track content width (sample first 200 rows)
|
|
502
|
+
if (rowCount <= 200) {
|
|
503
|
+
const currentMax = maxContentWidth.get(parsed.column) ?? 0;
|
|
504
|
+
if (parsed.value.length > currentMax) {
|
|
505
|
+
maxContentWidth.set(parsed.column, parsed.value.length);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
rows.push({
|
|
513
|
+
index: rowIndex,
|
|
514
|
+
height,
|
|
515
|
+
cells,
|
|
516
|
+
hidden: false,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
rows,
|
|
522
|
+
totalRowCount,
|
|
523
|
+
truncated: rowCount > MAX_ROWS_PER_SHEET,
|
|
524
|
+
maxCol,
|
|
525
|
+
maxContentWidth,
|
|
526
|
+
rowOutlineLevels,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// =============================================================================
|
|
531
|
+
// Cell parsing
|
|
532
|
+
// =============================================================================
|
|
533
|
+
|
|
534
|
+
/** Parse cell reference like "A1" into { col: 1, ref: "A1" } */
|
|
535
|
+
function parseCellRef(ref: string): { col: number; row: number } | undefined {
|
|
536
|
+
const match = ref.match(/^([A-Z]+)(\d+)$/);
|
|
537
|
+
if (!match) return undefined;
|
|
538
|
+
return { col: colLettersToNum(match[1]), row: parseInt(match[2], 10) };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function parseCell(
|
|
542
|
+
cellEl: Record<string, unknown>,
|
|
543
|
+
styles: StyleResolver,
|
|
544
|
+
sharedStrings: SharedStringEntry[],
|
|
545
|
+
hyperlinkMap: Map<string, string>,
|
|
546
|
+
date1904: boolean,
|
|
547
|
+
): ParsedCell | undefined {
|
|
548
|
+
const ref = cellEl["@_r"] as string | undefined;
|
|
549
|
+
if (!ref) return undefined;
|
|
550
|
+
|
|
551
|
+
const cellRef = parseCellRef(ref);
|
|
552
|
+
if (!cellRef) return undefined;
|
|
553
|
+
|
|
554
|
+
const type = (cellEl["@_t"] as string) ?? "";
|
|
555
|
+
const styleIndex = parseInt((cellEl["@_s"] as string) ?? "0", 10);
|
|
556
|
+
|
|
557
|
+
// Raw value
|
|
558
|
+
const rawValue = extractElementText(cellEl["v"]);
|
|
559
|
+
|
|
560
|
+
// Inline string: <c t="inlineStr"><is><t>text</t></is></c>
|
|
561
|
+
let inlineStr: string | undefined;
|
|
562
|
+
if (type === "inlineStr") {
|
|
563
|
+
const is = cellEl["is"] as Record<string, unknown> | undefined;
|
|
564
|
+
if (is) {
|
|
565
|
+
const raw = extractElementText(is["t"]);
|
|
566
|
+
inlineStr = raw !== undefined ? decodeXmlEntities(raw) : undefined;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Resolve shared string
|
|
571
|
+
const ssIndex = type === "s" && rawValue !== undefined ? parseInt(rawValue, 10) : -1;
|
|
572
|
+
const sharedString = ssIndex >= 0 && ssIndex < sharedStrings.length ? sharedStrings[ssIndex] : undefined;
|
|
573
|
+
|
|
574
|
+
// Number format
|
|
575
|
+
const numFmt = styles.getNumFmt(styleIndex);
|
|
576
|
+
|
|
577
|
+
// Format display value
|
|
578
|
+
const effectiveValue = type === "inlineStr" ? (inlineStr ?? "") : rawValue;
|
|
579
|
+
const displayValue = formatCellValue(effectiveValue, type, numFmt, sharedString, date1904);
|
|
580
|
+
|
|
581
|
+
// Build content
|
|
582
|
+
const hyperlink = hyperlinkMap.get(ref);
|
|
583
|
+
const content = buildCellContent(displayValue, sharedString, hyperlink);
|
|
584
|
+
|
|
585
|
+
// Style
|
|
586
|
+
const style = styles.resolveStyle(styleIndex);
|
|
587
|
+
|
|
588
|
+
// Formula (detect array formulas)
|
|
589
|
+
const fEl = cellEl["f"];
|
|
590
|
+
let formula: string | undefined;
|
|
591
|
+
let isArrayFormula = false;
|
|
592
|
+
let arrayRange: string | undefined;
|
|
593
|
+
if (fEl !== undefined && fEl !== null) {
|
|
594
|
+
if (typeof fEl === "object") {
|
|
595
|
+
const fObj = fEl as Record<string, string>;
|
|
596
|
+
formula = fObj["#text"];
|
|
597
|
+
if (fObj["@_t"] === "array") {
|
|
598
|
+
isArrayFormula = true;
|
|
599
|
+
arrayRange = fObj["@_ref"];
|
|
600
|
+
// Strip {} braces from array formula text
|
|
601
|
+
if (formula?.startsWith("{") && formula.endsWith("}")) {
|
|
602
|
+
formula = formula.slice(1, -1);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
formula = String(fEl);
|
|
607
|
+
}
|
|
608
|
+
if (formula !== undefined) formula = decodeXmlEntities(formula);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Compute typed value for lossless round-trip export
|
|
612
|
+
let typedValue: string | number | boolean | null;
|
|
613
|
+
let cellRawValue: number | undefined;
|
|
614
|
+
if (type === "s") {
|
|
615
|
+
typedValue = sharedString?.text ?? "";
|
|
616
|
+
} else if (type === "b") {
|
|
617
|
+
typedValue = rawValue === "1";
|
|
618
|
+
} else if (type === "e") {
|
|
619
|
+
typedValue = rawValue ?? null;
|
|
620
|
+
} else if (type === "str" || type === "inlineStr") {
|
|
621
|
+
typedValue = effectiveValue ?? "";
|
|
622
|
+
} else if (rawValue !== undefined) {
|
|
623
|
+
const num = parseFloat(rawValue);
|
|
624
|
+
if (!isNaN(num)) {
|
|
625
|
+
typedValue = num;
|
|
626
|
+
cellRawValue = num;
|
|
627
|
+
} else {
|
|
628
|
+
typedValue = rawValue;
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
typedValue = null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const cell: ParsedCell = { column: cellRef.col, value: displayValue, content, style };
|
|
635
|
+
cell.typedValue = typedValue;
|
|
636
|
+
cell.rawValue = cellRawValue;
|
|
637
|
+
cell.originalXfIndex = styleIndex;
|
|
638
|
+
if (numFmt !== "General") cell.numFmtCode = numFmt;
|
|
639
|
+
if (type === "e" && rawValue) cell.error = rawValue;
|
|
640
|
+
if (formula) cell.formula = formula;
|
|
641
|
+
if (isArrayFormula) cell.isArrayFormula = true;
|
|
642
|
+
if (arrayRange) cell.arrayRange = arrayRange;
|
|
643
|
+
|
|
644
|
+
return cell;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// =============================================================================
|
|
648
|
+
// Merged cells
|
|
649
|
+
// =============================================================================
|
|
650
|
+
|
|
651
|
+
function parseMergedCells(worksheet: Record<string, unknown>): MergedCellRange[] {
|
|
652
|
+
const mcEl = worksheet["mergeCells"] as Record<string, unknown> | undefined;
|
|
653
|
+
if (!mcEl) return [];
|
|
654
|
+
|
|
655
|
+
const mcArr = mcEl["mergeCell"] as Array<Record<string, string>> | undefined;
|
|
656
|
+
if (!mcArr) return [];
|
|
657
|
+
|
|
658
|
+
const ranges: MergedCellRange[] = [];
|
|
659
|
+
for (const mc of mcArr) {
|
|
660
|
+
const ref = mc["@_ref"];
|
|
661
|
+
if (!ref) continue;
|
|
662
|
+
|
|
663
|
+
const match = ref.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
664
|
+
if (!match) continue;
|
|
665
|
+
|
|
666
|
+
ranges.push({
|
|
667
|
+
startCol: colLettersToNum(match[1]),
|
|
668
|
+
startRow: parseInt(match[2], 10),
|
|
669
|
+
endCol: colLettersToNum(match[3]),
|
|
670
|
+
endRow: parseInt(match[4], 10),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return ranges;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// =============================================================================
|
|
678
|
+
// Hyperlinks
|
|
679
|
+
// =============================================================================
|
|
680
|
+
|
|
681
|
+
function parseHyperlinks(
|
|
682
|
+
worksheet: Record<string, unknown>,
|
|
683
|
+
rels: Map<string, { target: string; type: string }>,
|
|
684
|
+
): Map<string, string> {
|
|
685
|
+
const map = new Map<string, string>();
|
|
686
|
+
const hlEl = worksheet["hyperlinks"] as Record<string, unknown> | undefined;
|
|
687
|
+
if (!hlEl) return map;
|
|
688
|
+
|
|
689
|
+
const hlArr = hlEl["hyperlink"] as Array<Record<string, string>> | undefined;
|
|
690
|
+
if (!hlArr) return map;
|
|
691
|
+
|
|
692
|
+
for (const hl of hlArr) {
|
|
693
|
+
const ref = hl["@_ref"];
|
|
694
|
+
if (!ref) continue;
|
|
695
|
+
|
|
696
|
+
// External hyperlink via relationship
|
|
697
|
+
const rId = hl["@_r:id"];
|
|
698
|
+
if (rId) {
|
|
699
|
+
const rel = rels.get(rId);
|
|
700
|
+
if (rel) {
|
|
701
|
+
map.set(ref, rel.target);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Internal location
|
|
707
|
+
const location = hl["@_location"];
|
|
708
|
+
if (location) {
|
|
709
|
+
map.set(ref, `#${location}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return map;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// =============================================================================
|
|
717
|
+
// Images
|
|
718
|
+
// =============================================================================
|
|
719
|
+
|
|
720
|
+
function parseImages(
|
|
721
|
+
rels: Map<string, { target: string; type: string }>,
|
|
722
|
+
zipEntries: Record<string, Uint8Array>,
|
|
723
|
+
): ParsedImage[] {
|
|
724
|
+
const images: ParsedImage[] = [];
|
|
725
|
+
|
|
726
|
+
// Find drawing relationship
|
|
727
|
+
for (const [, rel] of rels) {
|
|
728
|
+
if (!rel.type.includes("drawing") || rel.type.includes("drawingML")) continue;
|
|
729
|
+
|
|
730
|
+
const drawingPath = resolveRelPath(rel.target);
|
|
731
|
+
const drawingEntry = findZipEntry(zipEntries, drawingPath);
|
|
732
|
+
if (!drawingEntry) continue;
|
|
733
|
+
|
|
734
|
+
const drawingXml = decodeUtf8(drawingEntry);
|
|
735
|
+
const doc = drawingParser.parse(drawingXml);
|
|
736
|
+
const wsDr = doc?.["xdr:wsDr"];
|
|
737
|
+
if (!wsDr) continue;
|
|
738
|
+
|
|
739
|
+
// Parse drawing relationships
|
|
740
|
+
const drawingRelsPath = drawingPath.replace(/([^/]+)$/, "_rels/$1.rels");
|
|
741
|
+
const drawingRelsEntry = findZipEntry(zipEntries, drawingRelsPath);
|
|
742
|
+
const drawingRels = drawingRelsEntry
|
|
743
|
+
? parseDrawingRels(decodeUtf8(drawingRelsEntry))
|
|
744
|
+
: new Map<string, string>();
|
|
745
|
+
|
|
746
|
+
// Two-cell anchors
|
|
747
|
+
const anchors = wsDr["xdr:twoCellAnchor"] as Array<Record<string, unknown>> | undefined;
|
|
748
|
+
if (anchors) {
|
|
749
|
+
for (const anchor of anchors) {
|
|
750
|
+
if (images.length >= MAX_IMAGES) break;
|
|
751
|
+
const image = extractImageFromAnchor(anchor, drawingRels, zipEntries, drawingPath);
|
|
752
|
+
if (image) images.push(image);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return images;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function parseDrawingRels(relsXml: string): Map<string, string> {
|
|
761
|
+
const doc = relsParser.parse(relsXml);
|
|
762
|
+
const relationships = doc?.["Relationships"];
|
|
763
|
+
if (!relationships) return new Map();
|
|
764
|
+
|
|
765
|
+
const relArr = relationships["Relationship"] as Array<Record<string, string>> | undefined;
|
|
766
|
+
if (!relArr) return new Map();
|
|
767
|
+
|
|
768
|
+
const map = new Map<string, string>();
|
|
769
|
+
for (const rel of relArr) {
|
|
770
|
+
const id = rel["@_Id"];
|
|
771
|
+
const target = rel["@_Target"];
|
|
772
|
+
if (id && target) {
|
|
773
|
+
map.set(id, target);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return map;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function extractImageFromAnchor(
|
|
780
|
+
anchor: Record<string, unknown>,
|
|
781
|
+
drawingRels: Map<string, string>,
|
|
782
|
+
zipEntries: Record<string, Uint8Array>,
|
|
783
|
+
drawingPath: string,
|
|
784
|
+
): ParsedImage | undefined {
|
|
785
|
+
// Get position
|
|
786
|
+
const from = anchor["xdr:from"] as Record<string, string> | undefined;
|
|
787
|
+
const to = anchor["xdr:to"] as Record<string, string> | undefined;
|
|
788
|
+
if (!from || !to) return undefined;
|
|
789
|
+
|
|
790
|
+
const tl = {
|
|
791
|
+
row: parseInt(from["xdr:row"] ?? "0", 10),
|
|
792
|
+
col: parseInt(from["xdr:col"] ?? "0", 10),
|
|
793
|
+
};
|
|
794
|
+
const br = {
|
|
795
|
+
row: parseInt(to["xdr:row"] ?? "0", 10) + 1,
|
|
796
|
+
col: parseInt(to["xdr:col"] ?? "0", 10) + 1,
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// Find the image reference
|
|
800
|
+
const pic = anchor["xdr:pic"] as Record<string, unknown> | undefined;
|
|
801
|
+
if (!pic) return undefined;
|
|
802
|
+
|
|
803
|
+
const blipFill = pic["xdr:blipFill"] as Record<string, unknown> | undefined;
|
|
804
|
+
if (!blipFill) return undefined;
|
|
805
|
+
|
|
806
|
+
const blip = blipFill["a:blip"] as Record<string, string> | undefined;
|
|
807
|
+
if (!blip) return undefined;
|
|
808
|
+
|
|
809
|
+
const embedId = blip["@_r:embed"];
|
|
810
|
+
if (!embedId) return undefined;
|
|
811
|
+
|
|
812
|
+
const imageRelTarget = drawingRels.get(embedId);
|
|
813
|
+
if (!imageRelTarget) return undefined;
|
|
814
|
+
|
|
815
|
+
// Resolve image path relative to drawing
|
|
816
|
+
const drawingDir = drawingPath.substring(0, drawingPath.lastIndexOf("/") + 1);
|
|
817
|
+
const imagePath = resolveRelativePath(drawingDir, imageRelTarget);
|
|
818
|
+
const imageEntry = findZipEntry(zipEntries, imagePath);
|
|
819
|
+
if (!imageEntry) return undefined;
|
|
820
|
+
|
|
821
|
+
// Build data URL
|
|
822
|
+
const ext = imagePath.split(".").pop()?.toLowerCase() ?? "png";
|
|
823
|
+
const mimeType = ext === "jpeg" || ext === "jpg" ? "image/jpeg" : ext === "gif" ? "image/gif" : "image/png";
|
|
824
|
+
const base64 = uint8ToBase64(imageEntry);
|
|
825
|
+
const dataUrl = `data:${mimeType};base64,${base64}`;
|
|
826
|
+
|
|
827
|
+
return { dataUrl, tl, br };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// =============================================================================
|
|
831
|
+
// Conditional formatting
|
|
832
|
+
// =============================================================================
|
|
833
|
+
|
|
834
|
+
function parseConditionalFormats(
|
|
835
|
+
worksheet: Record<string, unknown>,
|
|
836
|
+
theme: ThemeData,
|
|
837
|
+
indexedColors: string[],
|
|
838
|
+
): ParsedConditionalFormat[] {
|
|
839
|
+
const cfArr = worksheet["conditionalFormatting"] as Array<Record<string, unknown>> | undefined;
|
|
840
|
+
if (!cfArr) return [];
|
|
841
|
+
|
|
842
|
+
const result: ParsedConditionalFormat[] = [];
|
|
843
|
+
|
|
844
|
+
for (const cf of cfArr) {
|
|
845
|
+
const ref = cf["@_sqref"] as string;
|
|
846
|
+
if (!ref) continue;
|
|
847
|
+
|
|
848
|
+
const ruleArr = cf["cfRule"] as Array<Record<string, unknown>> | undefined;
|
|
849
|
+
if (!ruleArr) continue;
|
|
850
|
+
|
|
851
|
+
const rules: ParsedCfRule[] = [];
|
|
852
|
+
for (const ruleEl of ruleArr) {
|
|
853
|
+
const parsed = parseCfRule(ruleEl, theme, indexedColors);
|
|
854
|
+
if (parsed) rules.push(parsed);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (rules.length > 0) {
|
|
858
|
+
result.push({ ref, rules });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return result;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function parseCfRule(
|
|
866
|
+
el: Record<string, unknown>,
|
|
867
|
+
theme: ThemeData,
|
|
868
|
+
indexedColors: string[],
|
|
869
|
+
): ParsedCfRule | undefined {
|
|
870
|
+
const type = el["@_type"] as string;
|
|
871
|
+
const priority = parseInt((el["@_priority"] as string) ?? "1", 10);
|
|
872
|
+
const stopIfTrue = (el["@_stopIfTrue"] as string) === "1";
|
|
873
|
+
|
|
874
|
+
switch (type) {
|
|
875
|
+
case "colorScale": {
|
|
876
|
+
const colorScale = el["colorScale"] as Record<string, unknown> | undefined;
|
|
877
|
+
if (!colorScale) return undefined;
|
|
878
|
+
const colorArr = colorScale["color"] as Array<Record<string, string>> | undefined;
|
|
879
|
+
if (!colorArr || colorArr.length < 2) return undefined;
|
|
880
|
+
|
|
881
|
+
const colors = colorArr.map((c) => {
|
|
882
|
+
const ooxmlColor = parseColorElement(c);
|
|
883
|
+
const hex = resolveColor(ooxmlColor, theme, indexedColors, "fill");
|
|
884
|
+
return hex ? prefixHex(hex) : "#000000";
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
return { ruleType: "colorScale", priority, stopIfTrue, colors };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
case "dataBar": {
|
|
891
|
+
const dataBar = el["dataBar"] as Record<string, unknown> | undefined;
|
|
892
|
+
let color = "#638EC6";
|
|
893
|
+
if (dataBar) {
|
|
894
|
+
const colorArr = dataBar["color"] as Array<Record<string, string>> | undefined;
|
|
895
|
+
const colorEl = colorArr?.[0];
|
|
896
|
+
if (colorEl) {
|
|
897
|
+
const ooxmlColor = parseColorElement(colorEl);
|
|
898
|
+
const hex = resolveColor(ooxmlColor, theme, indexedColors, "fill");
|
|
899
|
+
if (hex) color = prefixHex(hex);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
ruleType: "dataBar",
|
|
905
|
+
priority,
|
|
906
|
+
stopIfTrue,
|
|
907
|
+
color,
|
|
908
|
+
minLength: parseInt((dataBar?.["@_minLength"] as string) ?? "10", 10),
|
|
909
|
+
maxLength: parseInt((dataBar?.["@_maxLength"] as string) ?? "90", 10),
|
|
910
|
+
showValue: (dataBar?.["@_showValue"] as string) !== "0",
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
case "iconSet": {
|
|
915
|
+
const iconSet = el["iconSet"] as Record<string, unknown> | undefined;
|
|
916
|
+
const cfvoArr = iconSet?.["cfvo"] as Array<Record<string, string>> | undefined;
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
ruleType: "iconSet",
|
|
920
|
+
priority,
|
|
921
|
+
stopIfTrue,
|
|
922
|
+
iconSet: (iconSet?.["@_iconSet"] as string) ?? "3TrafficLights1",
|
|
923
|
+
thresholds: (cfvoArr ?? []).map((cfvo): CfThreshold => ({
|
|
924
|
+
type: cfvo["@_type"] as CfThreshold["type"],
|
|
925
|
+
value: cfvo["@_val"] !== undefined ? parseFloat(cfvo["@_val"]) : undefined,
|
|
926
|
+
})),
|
|
927
|
+
showValue: (iconSet?.["@_showValue"] as string) !== "0",
|
|
928
|
+
reverse: (iconSet?.["@_reverse"] as string) === "1",
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
case "cellIs":
|
|
933
|
+
case "containsText":
|
|
934
|
+
case "top10":
|
|
935
|
+
case "aboveAverage":
|
|
936
|
+
case "timePeriod":
|
|
937
|
+
case "expression": {
|
|
938
|
+
return parseStyleCfRule(el, type, priority, stopIfTrue, theme, indexedColors);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
default:
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function parseStyleCfRule(
|
|
947
|
+
el: Record<string, unknown>,
|
|
948
|
+
type: "cellIs" | "containsText" | "top10" | "aboveAverage" | "timePeriod" | "expression",
|
|
949
|
+
priority: number,
|
|
950
|
+
stopIfTrue: boolean,
|
|
951
|
+
_theme: ThemeData,
|
|
952
|
+
_indexedColors: string[],
|
|
953
|
+
): ParsedStyleRule {
|
|
954
|
+
const rule: ParsedStyleRule = { ruleType: "style", priority, stopIfTrue, type };
|
|
955
|
+
|
|
956
|
+
if (type === "cellIs") {
|
|
957
|
+
rule.operator = el["@_operator"] as string | undefined;
|
|
958
|
+
const formula = el["formula"];
|
|
959
|
+
if (formula !== undefined) {
|
|
960
|
+
const arr = Array.isArray(formula) ? formula : [formula];
|
|
961
|
+
rule.formulae = arr
|
|
962
|
+
.map(extractElementText)
|
|
963
|
+
.filter((s): s is string => s !== undefined);
|
|
964
|
+
}
|
|
965
|
+
} else if (type === "containsText") {
|
|
966
|
+
rule.operator = el["@_operator"] as string | undefined;
|
|
967
|
+
rule.text = el["@_text"] as string | undefined;
|
|
968
|
+
} else if (type === "top10") {
|
|
969
|
+
rule.rank = el["@_rank"] !== undefined ? parseInt(el["@_rank"] as string, 10) : undefined;
|
|
970
|
+
rule.percent = el["@_percent"] === "1";
|
|
971
|
+
rule.bottom = el["@_bottom"] === "1";
|
|
972
|
+
} else if (type === "aboveAverage") {
|
|
973
|
+
rule.aboveAverage = el["@_aboveAverage"] !== "0";
|
|
974
|
+
} else if (type === "timePeriod") {
|
|
975
|
+
rule.timePeriod = el["@_timePeriod"] as string | undefined;
|
|
976
|
+
} else if (type === "expression") {
|
|
977
|
+
const formula = el["formula"];
|
|
978
|
+
if (formula !== undefined) {
|
|
979
|
+
const arr = Array.isArray(formula) ? formula : [formula];
|
|
980
|
+
rule.formulae = arr
|
|
981
|
+
.map(extractElementText)
|
|
982
|
+
.filter((s): s is string => s !== undefined);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return rule;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// =============================================================================
|
|
990
|
+
// Data validations
|
|
991
|
+
// =============================================================================
|
|
992
|
+
|
|
993
|
+
function parseDataValidations(worksheet: Record<string, unknown>): ParsedDataValidation[] {
|
|
994
|
+
const dvEl = worksheet["dataValidations"] as Record<string, unknown> | undefined;
|
|
995
|
+
if (!dvEl) return [];
|
|
996
|
+
|
|
997
|
+
const dvArr = dvEl["dataValidation"] as Array<Record<string, string>> | undefined;
|
|
998
|
+
if (!dvArr) return [];
|
|
999
|
+
|
|
1000
|
+
const result: ParsedDataValidation[] = [];
|
|
1001
|
+
for (const dv of dvArr) {
|
|
1002
|
+
const ref = dv["@_sqref"];
|
|
1003
|
+
if (!ref) continue;
|
|
1004
|
+
|
|
1005
|
+
const typeStr = dv["@_type"] ?? "none";
|
|
1006
|
+
const type = (["list", "whole", "decimal", "date", "time", "textLength", "custom", "none"].includes(typeStr)
|
|
1007
|
+
? typeStr
|
|
1008
|
+
: "none") as ParsedDataValidation["type"];
|
|
1009
|
+
|
|
1010
|
+
const operatorStr = dv["@_operator"];
|
|
1011
|
+
const validOperators = ["between", "notBetween", "equal", "notEqual", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual"];
|
|
1012
|
+
const operator = operatorStr && validOperators.includes(operatorStr)
|
|
1013
|
+
? operatorStr as ParsedDataValidation["operator"]
|
|
1014
|
+
: undefined;
|
|
1015
|
+
|
|
1016
|
+
const formula1Text = extractElementText(dv["formula1"]);
|
|
1017
|
+
const formula1 = formula1Text !== undefined ? decodeXmlEntities(formula1Text) : undefined;
|
|
1018
|
+
const formula2Text = extractElementText(dv["formula2"]);
|
|
1019
|
+
const formula2 = formula2Text !== undefined ? decodeXmlEntities(formula2Text) : undefined;
|
|
1020
|
+
const decodeAttr = (v: string | undefined) => v !== undefined ? decodeXmlEntities(v) : undefined;
|
|
1021
|
+
|
|
1022
|
+
result.push({
|
|
1023
|
+
ref,
|
|
1024
|
+
type,
|
|
1025
|
+
operator,
|
|
1026
|
+
formula1,
|
|
1027
|
+
formula2,
|
|
1028
|
+
showDropdown: dv["@_showDropDown"] !== "1",
|
|
1029
|
+
errorStyle: dv["@_errorStyle"] as ParsedDataValidation["errorStyle"],
|
|
1030
|
+
errorTitle: decodeAttr(dv["@_errorTitle"]),
|
|
1031
|
+
errorMessage: decodeAttr(dv["@_error"]),
|
|
1032
|
+
promptTitle: decodeAttr(dv["@_promptTitle"]),
|
|
1033
|
+
promptMessage: decodeAttr(dv["@_prompt"]),
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return result;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// =============================================================================
|
|
1041
|
+
// Sparklines (sheet extension list)
|
|
1042
|
+
// =============================================================================
|
|
1043
|
+
|
|
1044
|
+
function parseSparklineGroups(worksheet: Record<string, unknown>): ParsedSparklineGroup[] {
|
|
1045
|
+
const extLst = worksheet["extLst"] as Record<string, unknown> | undefined;
|
|
1046
|
+
if (!extLst) return [];
|
|
1047
|
+
|
|
1048
|
+
const extArr = extLst["ext"] as Array<Record<string, unknown>> | undefined;
|
|
1049
|
+
if (!extArr) return [];
|
|
1050
|
+
|
|
1051
|
+
const result: ParsedSparklineGroup[] = [];
|
|
1052
|
+
|
|
1053
|
+
for (const ext of extArr) {
|
|
1054
|
+
// Look for sparklineGroups in any x14 namespace variant
|
|
1055
|
+
const sparkGroups = ext["x14:sparklineGroups"]
|
|
1056
|
+
?? ext["sparklineGroups"] as Record<string, unknown> | undefined;
|
|
1057
|
+
if (!sparkGroups || typeof sparkGroups !== "object") continue;
|
|
1058
|
+
|
|
1059
|
+
const groupArr = ensureSparkArray(
|
|
1060
|
+
(sparkGroups as Record<string, unknown>)["x14:sparklineGroup"]
|
|
1061
|
+
?? (sparkGroups as Record<string, unknown>)["sparklineGroup"],
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
for (const group of groupArr) {
|
|
1065
|
+
if (!group || typeof group !== "object") continue;
|
|
1066
|
+
const g = group as Record<string, unknown>;
|
|
1067
|
+
const typeAttr = (g["@_type"] as string) ?? "line";
|
|
1068
|
+
const type = (["line", "column", "stacked"].includes(typeAttr) ? typeAttr : "line") as ParsedSparklineGroup["type"];
|
|
1069
|
+
const color = extractSparklineColor(g);
|
|
1070
|
+
|
|
1071
|
+
const sparklineContainer = g["x14:sparklines"] ?? g["sparklines"];
|
|
1072
|
+
if (!sparklineContainer || typeof sparklineContainer !== "object") continue;
|
|
1073
|
+
|
|
1074
|
+
const sparkArr = ensureSparkArray(
|
|
1075
|
+
(sparklineContainer as Record<string, unknown>)["x14:sparkline"]
|
|
1076
|
+
?? (sparklineContainer as Record<string, unknown>)["sparkline"],
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
const sparklines: ParsedSparkline[] = [];
|
|
1080
|
+
for (const sp of sparkArr) {
|
|
1081
|
+
if (!sp || typeof sp !== "object") continue;
|
|
1082
|
+
const s = sp as Record<string, unknown>;
|
|
1083
|
+
const dataRangeEl = s["xm:f"] ?? s["f"];
|
|
1084
|
+
const locationEl = s["xm:sqref"] ?? s["sqref"];
|
|
1085
|
+
const dataRange = extractElementText(dataRangeEl) ?? "";
|
|
1086
|
+
const locationRef = extractElementText(locationEl) ?? "";
|
|
1087
|
+
if (dataRange && locationRef) {
|
|
1088
|
+
sparklines.push({ dataRange, locationRef });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (sparklines.length > 0) {
|
|
1093
|
+
result.push({ type, sparklines, color });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function extractSparklineColor(group: Record<string, unknown>): string | undefined {
|
|
1102
|
+
const colorEl = group["x14:colorSeries"] ?? group["colorSeries"];
|
|
1103
|
+
if (!colorEl || typeof colorEl !== "object") return undefined;
|
|
1104
|
+
const rgb = (colorEl as Record<string, string>)["@_rgb"];
|
|
1105
|
+
if (!rgb) return undefined;
|
|
1106
|
+
// AARRGGBB → #RRGGBB
|
|
1107
|
+
return `#${rgb.length === 8 ? rgb.slice(2) : rgb}`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function ensureSparkArray(val: unknown): unknown[] {
|
|
1111
|
+
if (val === undefined || val === null) return [];
|
|
1112
|
+
return Array.isArray(val) ? val : [val];
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// =============================================================================
|
|
1116
|
+
// Page breaks
|
|
1117
|
+
// =============================================================================
|
|
1118
|
+
|
|
1119
|
+
function parsePageBreaks(worksheet: Record<string, unknown>): { rowPageBreaks: number[]; colPageBreaks: number[] } {
|
|
1120
|
+
const rowPageBreaks: number[] = [];
|
|
1121
|
+
const colPageBreaks: number[] = [];
|
|
1122
|
+
|
|
1123
|
+
const rowBreaksEl = worksheet["rowBreaks"] as Record<string, unknown> | undefined;
|
|
1124
|
+
if (rowBreaksEl) {
|
|
1125
|
+
const brkArr = ensureBreakArray(rowBreaksEl["brk"]);
|
|
1126
|
+
for (const brk of brkArr) {
|
|
1127
|
+
if (!brk || typeof brk !== "object") continue;
|
|
1128
|
+
const id = (brk as Record<string, string>)["@_id"];
|
|
1129
|
+
if (id) rowPageBreaks.push(parseInt(id, 10));
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const colBreaksEl = worksheet["colBreaks"] as Record<string, unknown> | undefined;
|
|
1134
|
+
if (colBreaksEl) {
|
|
1135
|
+
const brkArr = ensureBreakArray(colBreaksEl["brk"]);
|
|
1136
|
+
for (const brk of brkArr) {
|
|
1137
|
+
if (!brk || typeof brk !== "object") continue;
|
|
1138
|
+
const id = (brk as Record<string, string>)["@_id"];
|
|
1139
|
+
if (id) colPageBreaks.push(parseInt(id, 10));
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return { rowPageBreaks: rowPageBreaks.sort((a, b) => a - b), colPageBreaks: colPageBreaks.sort((a, b) => a - b) };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function ensureBreakArray(val: unknown): unknown[] {
|
|
1147
|
+
if (val === undefined || val === null) return [];
|
|
1148
|
+
return Array.isArray(val) ? val : [val];
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// =============================================================================
|
|
1152
|
+
// Auto-filter
|
|
1153
|
+
// =============================================================================
|
|
1154
|
+
|
|
1155
|
+
function parseAutoFilter(worksheet: Record<string, unknown>): AutoFilterRange | undefined {
|
|
1156
|
+
const afEl = worksheet["autoFilter"] as Record<string, unknown> | undefined;
|
|
1157
|
+
if (!afEl) return undefined;
|
|
1158
|
+
|
|
1159
|
+
const ref = afEl["@_ref"] as string | undefined;
|
|
1160
|
+
if (!ref) return undefined;
|
|
1161
|
+
|
|
1162
|
+
const columns: AutoFilterColumn[] = [];
|
|
1163
|
+
const fcArr = afEl["filterColumn"] as Array<Record<string, unknown>> | undefined;
|
|
1164
|
+
if (fcArr) {
|
|
1165
|
+
for (const fc of fcArr) {
|
|
1166
|
+
const colIndex = parseInt((fc["@_colId"] as string) ?? "0", 10);
|
|
1167
|
+
const filterValues: string[] = [];
|
|
1168
|
+
|
|
1169
|
+
const filtersEl = fc["filters"] as Record<string, unknown> | undefined;
|
|
1170
|
+
if (filtersEl) {
|
|
1171
|
+
const filterArr = filtersEl["filter"] as Array<Record<string, string>> | undefined;
|
|
1172
|
+
if (filterArr) {
|
|
1173
|
+
for (const f of filterArr) {
|
|
1174
|
+
const val = f["@_val"];
|
|
1175
|
+
if (val !== undefined) filterValues.push(decodeXmlEntities(val));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const customFilterEl = fc["customFilters"] as Record<string, unknown> | undefined;
|
|
1181
|
+
let customFilter: AutoFilterColumn["customFilter"];
|
|
1182
|
+
if (customFilterEl) {
|
|
1183
|
+
const cf = customFilterEl["customFilter"] as Record<string, string> | undefined;
|
|
1184
|
+
if (cf) {
|
|
1185
|
+
customFilter = {
|
|
1186
|
+
operator: cf["@_operator"] ?? "equal",
|
|
1187
|
+
value: decodeXmlEntities(cf["@_val"] ?? ""),
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
columns.push({
|
|
1193
|
+
colIndex,
|
|
1194
|
+
filterValues: filterValues.length > 0 ? filterValues : undefined,
|
|
1195
|
+
customFilter,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return { ref, columns };
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// =============================================================================
|
|
1204
|
+
// Utility helpers
|
|
1205
|
+
// =============================================================================
|
|
1206
|
+
|
|
1207
|
+
function resolveRelPath(target: string): string {
|
|
1208
|
+
// Targets in rels are relative to the sheet's directory
|
|
1209
|
+
// e.g., "../comments1.xml" from xl/worksheets/ → xl/comments1.xml
|
|
1210
|
+
if (target.startsWith("/")) return target.slice(1);
|
|
1211
|
+
return `xl/${target.replace(/^\.\.\//, "")}`;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function resolveRelativePath(basePath: string, relativePath: string): string {
|
|
1215
|
+
if (relativePath.startsWith("/")) return relativePath.slice(1);
|
|
1216
|
+
|
|
1217
|
+
const parts = basePath.split("/").filter(Boolean);
|
|
1218
|
+
const relParts = relativePath.split("/");
|
|
1219
|
+
|
|
1220
|
+
// Remove the file name from base path (keep directory)
|
|
1221
|
+
// basePath already ends with / so this is fine
|
|
1222
|
+
|
|
1223
|
+
const resolved = [...parts];
|
|
1224
|
+
for (const part of relParts) {
|
|
1225
|
+
if (part === "..") {
|
|
1226
|
+
resolved.pop();
|
|
1227
|
+
} else if (part !== ".") {
|
|
1228
|
+
resolved.push(part);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
return resolved.join("/");
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function findZipEntry(zipEntries: Record<string, Uint8Array>, path: string): Uint8Array | undefined {
|
|
1236
|
+
// Try exact path
|
|
1237
|
+
if (zipEntries[path]) return zipEntries[path];
|
|
1238
|
+
// Try with leading xl/ prefix variations
|
|
1239
|
+
if (zipEntries[`xl/${path}`]) return zipEntries[`xl/${path}`];
|
|
1240
|
+
// Try without leading slash
|
|
1241
|
+
const noSlash = path.replace(/^\//, "");
|
|
1242
|
+
if (zipEntries[noSlash]) return zipEntries[noSlash];
|
|
1243
|
+
return undefined;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function uint8ToBase64(bytes: Uint8Array): string {
|
|
1247
|
+
let binary = "";
|
|
1248
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1249
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1250
|
+
}
|
|
1251
|
+
return btoa(binary);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
export function emptySheet(name: string): ParsedSheet {
|
|
1255
|
+
return {
|
|
1256
|
+
name,
|
|
1257
|
+
columns: [],
|
|
1258
|
+
rows: [],
|
|
1259
|
+
mergedCells: [],
|
|
1260
|
+
totalRowCount: 0,
|
|
1261
|
+
truncated: false,
|
|
1262
|
+
defaultRowHeight: 15,
|
|
1263
|
+
defaultColWidth: 10,
|
|
1264
|
+
showGridLines: true,
|
|
1265
|
+
conditionalFormats: [],
|
|
1266
|
+
images: [],
|
|
1267
|
+
rowOutlineLevels: new Map(),
|
|
1268
|
+
colOutlineLevels: new Map(),
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|