@niicojs/excel 0.3.4 → 0.3.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","sources":["../src/types.ts","../src/cell.ts","../src/range.ts","../src/table.ts","../src/worksheet.ts","../src/shared-strings.ts","../src/styles.ts","../src/pivot-cache.ts","../src/pivot-table.ts","../src/workbook.ts","../src/utils/address.ts"],"sourcesContent":["/**\r\n * Cell value types - what a cell can contain\r\n */\r\nexport type CellValue = number | string | boolean | Date | null | CellError;\r\n\r\n/**\r\n * Represents an Excel error value\r\n */\r\nexport interface CellError {\r\n error: ErrorType;\r\n}\r\n\r\nexport type ErrorType = '#NULL!' | '#DIV/0!' | '#VALUE!' | '#REF!' | '#NAME?' | '#NUM!' | '#N/A' | '#GETTING_DATA';\r\n\r\n/**\r\n * Discriminator for cell content type\r\n */\r\nexport type CellType = 'number' | 'string' | 'boolean' | 'date' | 'error' | 'empty';\r\n\r\n/**\r\n * Date handling strategy when serializing cell values.\r\n */\r\nexport type DateHandling = 'jsDate' | 'excelSerial' | 'isoString';\r\n\r\n/**\r\n * Style definition for cells\r\n */\r\nexport interface CellStyle {\r\n bold?: boolean;\r\n italic?: boolean;\r\n underline?: boolean | 'single' | 'double';\r\n strike?: boolean;\r\n fontSize?: number;\r\n fontName?: string;\r\n fontColor?: string;\r\n fontColorTheme?: number;\r\n fontColorTint?: number;\r\n fontColorIndexed?: number;\r\n fill?: string;\r\n fillTheme?: number;\r\n fillTint?: number;\r\n fillIndexed?: number;\r\n fillBgColor?: string;\r\n fillBgTheme?: number;\r\n fillBgTint?: number;\r\n fillBgIndexed?: number;\r\n border?: BorderStyle;\r\n alignment?: Alignment;\r\n numberFormat?: string;\r\n}\r\n\r\nexport interface BorderStyle {\r\n top?: BorderType;\r\n bottom?: BorderType;\r\n left?: BorderType;\r\n right?: BorderType;\r\n}\r\n\r\nexport type BorderType = 'thin' | 'medium' | 'thick' | 'double' | 'dotted' | 'dashed';\r\n\r\nexport interface Alignment {\r\n horizontal?: 'left' | 'center' | 'right' | 'justify';\r\n vertical?: 'top' | 'middle' | 'bottom';\r\n wrapText?: boolean;\r\n textRotation?: number;\r\n}\r\n\r\n/**\r\n * Cell address with 0-indexed row and column\r\n */\r\nexport interface CellAddress {\r\n row: number;\r\n col: number;\r\n}\r\n\r\n/**\r\n * Range address with start and end cells\r\n */\r\nexport interface RangeAddress {\r\n start: CellAddress;\r\n end: CellAddress;\r\n}\r\n\r\n/**\r\n * Internal cell data representation\r\n */\r\nexport interface CellData {\r\n /** Cell type: n=number, s=string (shared), str=inline string, b=boolean, e=error, d=date */\r\n t?: 'n' | 's' | 'str' | 'b' | 'e' | 'd';\r\n /** Raw value */\r\n v?: number | string | boolean;\r\n /** Formula (without leading =) */\r\n f?: string;\r\n /** Style index */\r\n s?: number;\r\n /** Formatted text (cached) */\r\n w?: string;\r\n /** Number format */\r\n z?: string;\r\n /** Array formula range */\r\n F?: string;\r\n /** Dynamic array formula flag */\r\n D?: boolean;\r\n /** Shared formula index */\r\n si?: number;\r\n}\r\n\r\n/**\r\n * Sheet definition from workbook.xml\r\n */\r\nexport interface SheetDefinition {\r\n name: string;\r\n sheetId: number;\r\n rId: string;\r\n}\r\n\r\n/**\r\n * Relationship definition\r\n */\r\nexport interface Relationship {\r\n id: string;\r\n type: string;\r\n target: string;\r\n}\r\n\r\n/**\r\n * Pivot table aggregation functions\r\n */\r\nexport type AggregationType = 'sum' | 'count' | 'average' | 'min' | 'max';\r\n\r\n/**\r\n * Sort order for pivot fields.\r\n */\r\nexport type PivotSortOrder = 'asc' | 'desc';\r\n\r\n/**\r\n * Filter configuration for pivot fields.\r\n */\r\nexport interface PivotFieldFilter {\r\n include?: string[];\r\n exclude?: string[];\r\n}\r\n\r\n/**\r\n * Configuration for a value field in a pivot table\r\n */\r\nexport interface PivotValueConfig {\r\n /** Source field name (column header) */\r\n field: string;\r\n /** Aggregation function (default: 'sum') */\r\n aggregation?: AggregationType;\r\n /** Display name (e.g., \"Sum of Sales\") */\r\n name?: string;\r\n /** Number format (e.g., '$#,##0.00', '0.00%') */\r\n numberFormat?: string;\r\n}\r\n\r\n/**\r\n * Configuration for creating a pivot table\r\n */\r\nexport interface PivotTableConfig {\n /** Name of the pivot table */\n name: string;\n /** Source data range with sheet name (e.g., \"Sheet1!A1:D100\") */\n source: string;\n /** Target cell where pivot table will be placed (e.g., \"Sheet2!A3\") */\n target: string;\n /** Refresh the pivot table data when the file is opened (default: true) */\n refreshOnLoad?: boolean;\n /** Save pivot cache data in the file (default: true) */\n saveData?: boolean;\n}\n\r\n/**\r\n * Internal representation of a pivot cache field\r\n */\r\nexport interface PivotCacheField {\n /** Field name (from header row) */\n name: string;\n /** Field index (0-based) */\n index: number;\n /** Whether this field contains numbers */\n isNumeric: boolean;\n /** Whether this field contains dates */\n isDate: boolean;\n /** Whether this field contains boolean values */\n hasBoolean: boolean;\n /** Whether this field contains blank (null/undefined) values */\n hasBlank: boolean;\n /** Number format ID for this field (cache field numFmtId) */\n numFmtId?: number;\n /** Unique string values (for shared items) */\n sharedItems: string[];\n /** Min numeric value */\n minValue?: number;\n /** Max numeric value */\n maxValue?: number;\n /** Min date value (for date fields) */\n minDate?: Date;\n /** Max date value (for date fields) */\n maxDate?: Date;\n}\n\r\n/**\r\n * Pivot field axis assignment\r\n */\r\nexport type PivotFieldAxis = 'row' | 'column' | 'filter' | 'value';\r\n\r\n/**\r\n * Configuration for creating a sheet from an array of objects\r\n */\r\nexport interface SheetFromDataConfig<T extends object = Record<string, unknown>> {\r\n /** Name of the sheet to create */\r\n name: string;\r\n /** Array of objects with the same structure */\r\n data: T[];\r\n /** Column definitions (optional - defaults to all keys from first object) */\r\n columns?: ColumnConfig<T>[];\r\n /** Apply header styling (bold text) (default: true) */\r\n headerStyle?: boolean;\r\n /** Starting cell address (default: 'A1') */\r\n startCell?: string;\r\n}\r\n\r\n/**\r\n * Column configuration for sheet data\r\n */\r\nexport interface ColumnConfig<T = Record<string, unknown>> {\r\n /** Key from the object to use for this column */\r\n key: keyof T;\r\n /** Header text (optional - defaults to key name) */\r\n header?: string;\r\n /** Cell style for data cells in this column */\r\n style?: CellStyle;\r\n}\r\n\r\n/**\r\n * Rich cell value with optional formula and style.\r\n * Use this when you need to set value, formula, or style for individual cells.\r\n */\r\nexport interface RichCellValue {\r\n /** Cell value */\r\n value?: CellValue;\r\n /** Formula (without leading '=') */\r\n formula?: string;\r\n /** Cell style */\r\n style?: CellStyle;\r\n}\r\n\r\n/**\r\n * Configuration for creating an Excel Table (ListObject)\r\n */\r\nexport interface TableConfig {\r\n /** Table name (must be unique within the workbook) */\r\n name: string;\r\n /** Data range including headers (e.g., \"A1:D10\") */\r\n range: string;\r\n /** First row contains headers (default: true) */\r\n headerRow?: boolean;\r\n /** Show total row at the bottom (default: false) */\r\n totalRow?: boolean;\r\n /** Table style configuration */\r\n style?: TableStyleConfig;\r\n}\r\n\r\n/**\r\n * Table style configuration options\r\n */\r\nexport interface TableStyleConfig {\r\n /** Built-in table style name (e.g., \"TableStyleMedium2\", \"TableStyleLight1\") */\r\n name?: string;\r\n /** Show banded/alternating row colors (default: true) */\r\n showRowStripes?: boolean;\r\n /** Show banded/alternating column colors (default: false) */\r\n showColumnStripes?: boolean;\r\n /** Highlight first column with special formatting (default: false) */\r\n showFirstColumn?: boolean;\r\n /** Highlight last column with special formatting (default: false) */\r\n showLastColumn?: boolean;\r\n}\r\n\r\n/**\r\n * Aggregation functions available for table total row\r\n */\r\nexport type TableTotalFunction = 'sum' | 'count' | 'average' | 'min' | 'max' | 'stdDev' | 'var' | 'countNums' | 'none';\r\n\r\n/**\r\n * Configuration for converting a sheet to JSON objects.\r\n */\r\nexport interface SheetToJsonConfig {\r\n /**\r\n * Field names to use for each column.\r\n * If provided, the first row of data starts at row 1 (or startRow).\r\n * If not provided, the first row is used as field names.\r\n */\r\n fields?: string[];\r\n\r\n /**\r\n * Starting row (0-based). Defaults to 0.\r\n * If fields are not provided, this row contains the headers.\r\n * If fields are provided, this is the first data row.\r\n */\r\n startRow?: number;\r\n\r\n /**\r\n * Starting column (0-based). Defaults to 0.\r\n */\r\n startCol?: number;\r\n\r\n /**\r\n * Ending row (0-based, inclusive). Defaults to the last row with data.\r\n */\r\n endRow?: number;\r\n\r\n /**\r\n * Ending column (0-based, inclusive). Defaults to the last column with data.\r\n */\r\n endCol?: number;\r\n\r\n /**\r\n * If true, stop reading when an empty row is encountered. Defaults to true.\r\n */\r\n stopOnEmptyRow?: boolean;\r\n\r\n /**\r\n * How to serialize Date values. Defaults to 'jsDate'.\r\n */\r\n dateHandling?: DateHandling;\r\n\r\n /**\n * If true, return formatted text (as displayed in Excel) instead of raw values.\n * All values will be returned as strings. Defaults to false.\n */\n asText?: boolean;\n\n /**\n * Locale to use for formatting when asText is true.\n * Defaults to the workbook locale.\n */\n locale?: string;\n}\n","import type { CellValue, CellType, CellStyle, CellData, ErrorType } from './types';\nimport type { Worksheet } from './worksheet';\nimport { parseAddress, toAddress } from './utils/address';\nimport { formatCellValue } from './utils/format';\n\n// Excel epoch: December 31, 1899 (accounting for the 1900 leap year bug)\nconst EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 31));\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n// Excel error types\nconst ERROR_TYPES: Set<string> = new Set([\n '#NULL!',\n '#DIV/0!',\n '#VALUE!',\n '#REF!',\n '#NAME?',\n '#NUM!',\n '#N/A',\n '#GETTING_DATA',\n]);\n\n/**\n * Represents a single cell in a worksheet\n */\nexport class Cell {\n private _row: number;\n private _col: number;\n private _data: CellData;\n private _worksheet: Worksheet;\n private _dirty = false;\n\n constructor(worksheet: Worksheet, row: number, col: number, data?: CellData) {\n this._worksheet = worksheet;\n this._row = row;\n this._col = col;\n this._data = data || {};\n }\n\n /**\n * Get the cell address (e.g., 'A1')\n */\n get address(): string {\n return toAddress(this._row, this._col);\n }\n\n /**\n * Get the 0-based row index\n */\n get row(): number {\n return this._row;\n }\n\n /**\n * Get the 0-based column index\n */\n get col(): number {\n return this._col;\n }\n\n /**\n * Get the cell type\n */\n get type(): CellType {\n const t = this._data.t;\n if (!t && this._data.v === undefined && !this._data.f) {\n return 'empty';\n }\n switch (t) {\n case 'n':\n // Check if this is actually a date stored as number\n if (this._isDateFormat()) {\n return 'date';\n }\n return 'number';\n case 's':\n case 'str':\n return 'string';\n case 'b':\n return 'boolean';\n case 'e':\n return 'error';\n case 'd':\n return 'date';\n default:\n // If no type but has value, infer from value\n if (typeof this._data.v === 'number') {\n if (this._isDateFormat()) {\n return 'date';\n }\n return 'number';\n }\n if (typeof this._data.v === 'string') return 'string';\n if (typeof this._data.v === 'boolean') return 'boolean';\n return 'empty';\n }\n }\n\n /**\n * Get the cell value\n */\n get value(): CellValue {\n const t = this._data.t;\n const v = this._data.v;\n\n if (v === undefined && !this._data.f) {\n return null;\n }\n\n switch (t) {\n case 'n': {\n const numVal = typeof v === 'number' ? v : parseFloat(String(v));\n // Check if this is actually a date stored as number\n if (this._isDateFormat()) {\n return this._excelDateToJs(numVal);\n }\n return numVal;\n }\n case 's':\n // Shared string reference\n if (typeof v === 'number') {\n return this._worksheet.workbook.sharedStrings.getString(v) ?? '';\n }\n return String(v);\n case 'str':\n // Inline string\n return String(v);\n case 'b':\n return v === 1 || v === '1' || v === true;\n case 'e':\n return { error: String(v) as ErrorType };\n case 'd':\n // ISO 8601 date string\n return new Date(String(v));\n default:\n // No type specified - try to infer\n if (typeof v === 'number') {\n // Check if this might be a date based on number format\n if (this._isDateFormat()) {\n return this._excelDateToJs(v);\n }\n return v;\n }\n if (typeof v === 'string') {\n if (ERROR_TYPES.has(v)) {\n return { error: v as ErrorType };\n }\n return v;\n }\n if (typeof v === 'boolean') return v;\n return null;\n }\n }\n\n /**\n * Set the cell value\n */\n set value(val: CellValue) {\n this._dirty = true;\n\n if (val === null || val === undefined) {\n this._data.v = undefined;\n this._data.t = undefined;\n this._data.f = undefined;\n return;\n }\n\n if (typeof val === 'number') {\n this._data.v = val;\n this._data.t = 'n';\n } else if (typeof val === 'string') {\n // Store as shared string\n const index = this._worksheet.workbook.sharedStrings.addString(val);\n this._data.v = index;\n this._data.t = 's';\n } else if (typeof val === 'boolean') {\n this._data.v = val ? 1 : 0;\n this._data.t = 'b';\n } else if (val instanceof Date) {\n // Store as Excel serial number with date format for maximum compatibility\n this._data.v = this._jsDateToExcel(val);\n this._data.t = 'n';\n // Apply a default date format if no style is set\n if (this._data.s === undefined) {\n this._data.s = this._worksheet.workbook.styles.createStyle({ numberFormat: 'yyyy-mm-dd' });\n }\n } else if ('error' in val) {\n this._data.v = val.error;\n this._data.t = 'e';\n }\n\n // Clear formula when setting value directly\n this._data.f = undefined;\n }\n\n /**\n * Write a 2D array of values starting at this cell\n */\n set values(data: CellValue[][]) {\n for (let r = 0; r < data.length; r++) {\n const row = data[r];\n for (let c = 0; c < row.length; c++) {\n const cell = this._worksheet.cell(this._row + r, this._col + c);\n cell.value = row[c];\n }\n }\n }\n\n /**\n * Get the formula (without leading '=')\n */\n get formula(): string | undefined {\n return this._data.f;\n }\n\n /**\n * Set the formula (without leading '=')\n */\n set formula(f: string | undefined) {\n this._dirty = true;\n if (f === undefined) {\n this._data.f = undefined;\n } else {\n // Remove leading '=' if present\n this._data.f = f.startsWith('=') ? f.slice(1) : f;\n }\n }\n\n /**\n * Get the formatted text (as displayed in Excel)\n */\n get text(): string {\n return this.textWithLocale();\n }\n\n /**\n * Get the formatted text using a specific locale\n */\n textWithLocale(locale?: string): string {\n if (this._data.w) {\n return this._data.w;\n }\n const val = this.value;\n if (val === null) return '';\n if (typeof val === 'object' && 'error' in val) return val.error;\n\n if (val instanceof Date || typeof val === 'number') {\n const formatted = formatCellValue(val, this.style, locale ?? this._worksheet.workbook.locale);\n if (formatted !== null) return formatted;\n }\n\n if (val instanceof Date) return val.toISOString().split('T')[0];\n return String(val);\n }\n\n /**\n * Get the style index\n */\n get styleIndex(): number | undefined {\n return this._data.s;\n }\n\n /**\n * Set the style index\n */\n set styleIndex(index: number | undefined) {\n this._dirty = true;\n this._data.s = index;\n }\n\n /**\n * Get the cell style\n */\n get style(): CellStyle {\n if (this._data.s === undefined) {\n return {};\n }\n return this._worksheet.workbook.styles.getStyle(this._data.s);\n }\n\n /**\n * Set the cell style (merges with existing)\n */\n set style(style: CellStyle) {\n this._dirty = true;\n const currentStyle = this.style;\n const merged = { ...currentStyle, ...style };\n this._data.s = this._worksheet.workbook.styles.createStyle(merged);\n }\n\n /**\n * Check if cell has been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Get internal cell data\n */\n get data(): CellData {\n return this._data;\n }\n\n /**\n * Check if this cell has a date number format\n */\n private _isDateFormat(): boolean {\n if (this._data.s === undefined) {\n return false;\n }\n const style = this._worksheet.workbook.styles.getStyle(this._data.s);\n if (!style.numberFormat) {\n return false;\n }\n // Common date format patterns\n const fmt = style.numberFormat.toLowerCase();\n return (\n fmt.includes('y') ||\n fmt.includes('m') ||\n fmt.includes('d') ||\n fmt.includes('h') ||\n fmt.includes('s') ||\n fmt === 'general date' ||\n fmt === 'short date' ||\n fmt === 'long date'\n );\n }\n\n /**\n * Convert Excel serial date to JavaScript Date\n * Used when reading dates stored as numbers with date formats\n */\n _excelDateToJs(serial: number): Date {\n // Excel incorrectly considers 1900 a leap year\n // Dates after Feb 28, 1900 need adjustment\n const adjusted = serial >= 60 ? serial - 1 : serial;\n const ms = Math.round(adjusted * MS_PER_DAY);\n return new Date(EXCEL_EPOCH.getTime() + ms);\n }\n\n /**\n * Convert JavaScript Date to Excel serial date\n * Used when writing dates as numbers for Excel compatibility\n */\n _jsDateToExcel(date: Date): number {\n const ms = date.getTime() - EXCEL_EPOCH.getTime();\n let serial = ms / MS_PER_DAY;\n // Account for Excel's 1900 leap year bug\n if (serial >= 60) {\n serial += 1;\n }\n return serial;\n }\n}\n\n/**\n * Parse a cell address or row/col to get row and col indices\n */\nexport const parseCellRef = (rowOrAddress: number | string, col?: number): { row: number; col: number } => {\n if (typeof rowOrAddress === 'string') {\n return parseAddress(rowOrAddress);\n }\n if (col === undefined) {\n throw new Error('Column must be provided when row is a number');\n }\n return { row: rowOrAddress, col };\n};\n","import type { CellValue, CellStyle, RangeAddress } from './types';\nimport type { Worksheet } from './worksheet';\nimport { toAddress, normalizeRange } from './utils/address';\n\n/**\n * Represents a range of cells in a worksheet\n */\nexport class Range {\n private _worksheet: Worksheet;\n private _range: RangeAddress;\n\n constructor(worksheet: Worksheet, range: RangeAddress) {\n this._worksheet = worksheet;\n this._range = normalizeRange(range);\n }\n\n /**\n * Get the range address as a string\n */\n get address(): string {\n const start = toAddress(this._range.start.row, this._range.start.col);\n const end = toAddress(this._range.end.row, this._range.end.col);\n if (start === end) return start;\n return `${start}:${end}`;\n }\n\n /**\n * Get the number of rows in the range\n */\n get rowCount(): number {\n return this._range.end.row - this._range.start.row + 1;\n }\n\n /**\n * Get the number of columns in the range\n */\n get colCount(): number {\n return this._range.end.col - this._range.start.col + 1;\n }\n\n /**\n * Get all values in the range as a 2D array\n */\n get values(): CellValue[][] {\n return this.getValues();\n }\n\n /**\n * Get all values in the range as a 2D array with options\n */\n getValues(options: { createMissing?: boolean } = {}): CellValue[][] {\n const { createMissing = true } = options;\n const result: CellValue[][] = [];\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row: CellValue[] = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n if (createMissing) {\n const cell = this._worksheet.cell(r, c);\n row.push(cell.value);\n } else {\n const cell = this._worksheet.getCellIfExists(r, c);\n row.push(cell?.value ?? null);\n }\n }\n result.push(row);\n }\n return result;\n }\n\n /**\n * Set values in the range from a 2D array\n */\n set values(data: CellValue[][]) {\n for (let r = 0; r < data.length && r < this.rowCount; r++) {\n const row = data[r];\n for (let c = 0; c < row.length && c < this.colCount; c++) {\n const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);\n cell.value = row[c];\n }\n }\n }\n\n /**\n * Get all formulas in the range as a 2D array\n */\n get formulas(): (string | undefined)[][] {\n const result: (string | undefined)[][] = [];\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row: (string | undefined)[] = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n const cell = this._worksheet.cell(r, c);\n row.push(cell.formula);\n }\n result.push(row);\n }\n return result;\n }\n\n /**\n * Set formulas in the range from a 2D array\n */\n set formulas(data: (string | undefined)[][]) {\n for (let r = 0; r < data.length && r < this.rowCount; r++) {\n const row = data[r];\n for (let c = 0; c < row.length && c < this.colCount; c++) {\n const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);\n cell.formula = row[c];\n }\n }\n }\n\n /**\n * Get the style of the top-left cell\n */\n get style(): CellStyle {\n return this._worksheet.cell(this._range.start.row, this._range.start.col).style;\n }\n\n /**\n * Set style for all cells in the range\n */\n set style(style: CellStyle) {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n const cell = this._worksheet.cell(r, c);\n cell.style = style;\n }\n }\n }\n\n /**\n * Iterate over all cells in the range\n */\n *[Symbol.iterator]() {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n yield this._worksheet.cell(r, c);\n }\n }\n }\n\n /**\n * Iterate over cells row by row\n */\n *rows() {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n row.push(this._worksheet.cell(r, c));\n }\n yield row;\n }\n }\n}\n","import type { TableConfig, TableStyleConfig, TableTotalFunction, RangeAddress } from './types';\nimport type { Worksheet } from './worksheet';\nimport { parseRange, toAddress, toRange } from './utils/address';\nimport { createElement, stringifyXml, XmlNode } from './utils/xml';\n\n/**\n * Maps table total function names to SUBTOTAL function numbers\n * SUBTOTAL uses 101-111 for functions that ignore hidden values\n */\nconst TOTAL_FUNCTION_NUMBERS: Record<TableTotalFunction, number> = {\n average: 101,\n count: 102,\n countNums: 103,\n max: 104,\n min: 105,\n stdDev: 107,\n sum: 109,\n var: 110,\n none: 0,\n};\n\n/**\n * Maps table total function names to XML attribute values\n */\nconst TOTAL_FUNCTION_NAMES: Record<TableTotalFunction, string> = {\n average: 'average',\n count: 'count',\n countNums: 'countNums',\n max: 'max',\n min: 'min',\n stdDev: 'stdDev',\n sum: 'sum',\n var: 'var',\n none: 'none',\n};\n\n/**\n * Represents an Excel Table (ListObject) with auto-filter, banded styling, and total row.\n */\nexport class Table {\n private _name: string;\n private _displayName: string;\n private _worksheet: Worksheet;\n private _range: RangeAddress;\n private _baseRange: RangeAddress;\n private _totalRow: boolean;\n private _autoFilter: boolean;\n private _style: TableStyleConfig;\n private _columns: TableColumn[] = [];\n private _id: number;\n private _dirty = true;\n private _headerRow: boolean;\n\n constructor(worksheet: Worksheet, config: TableConfig, tableId: number) {\n this._worksheet = worksheet;\n this._name = config.name;\n this._displayName = config.name;\n this._range = parseRange(config.range);\n this._baseRange = { start: { ...this._range.start }, end: { ...this._range.end } };\n this._totalRow = config.totalRow === true; // Default false\n this._autoFilter = true; // Tables have auto-filter by default\n this._headerRow = config.headerRow !== false;\n this._id = tableId;\n\n // Expand range to include total row if enabled\n if (this._totalRow) {\n this._range.end.row++;\n }\n\n // Set default style\n this._style = {\n name: config.style?.name ?? 'TableStyleMedium2',\n showRowStripes: config.style?.showRowStripes !== false, // Default true\n showColumnStripes: config.style?.showColumnStripes === true, // Default false\n showFirstColumn: config.style?.showFirstColumn === true, // Default false\n showLastColumn: config.style?.showLastColumn === true, // Default false\n };\n\n // Extract column names from worksheet headers\n this._extractColumns();\n }\n\n /**\n * Get the table name\n */\n get name(): string {\n return this._name;\n }\n\n /**\n * Get the table display name\n */\n get displayName(): string {\n return this._displayName;\n }\n\n /**\n * Get the table ID\n */\n get id(): number {\n return this._id;\n }\n\n /**\n * Get the worksheet this table belongs to\n */\n get worksheet(): Worksheet {\n return this._worksheet;\n }\n\n /**\n * Get the table range address string\n */\n get range(): string {\n return toRange(this._range);\n }\n\n /**\n * Get the base range excluding total row\n */\n get baseRange(): string {\n return toRange(this._baseRange);\n }\n\n /**\n * Get the table range as RangeAddress\n */\n get rangeAddress(): RangeAddress {\n return { ...this._range };\n }\n\n /**\n * Get column names\n */\n get columns(): string[] {\n return this._columns.map((c) => c.name);\n }\n\n /**\n * Check if table has a total row\n */\n get hasTotalRow(): boolean {\n return this._totalRow;\n }\n\n /**\n * Check if table has a header row\n */\n get hasHeaderRow(): boolean {\n return this._headerRow;\n }\n\n /**\n * Check if table has auto-filter enabled\n */\n get hasAutoFilter(): boolean {\n return this._autoFilter;\n }\n\n /**\n * Get the current style configuration\n */\n get style(): TableStyleConfig {\n return { ...this._style };\n }\n\n /**\n * Check if the table has been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Set a total function for a column\n * @param columnName - Name of the column (header text)\n * @param fn - Aggregation function to use\n * @returns this for method chaining\n */\n setTotalFunction(columnName: string, fn: TableTotalFunction): this {\n if (!this._totalRow) {\n throw new Error('Cannot set total function: table does not have a total row enabled');\n }\n\n const column = this._columns.find((c) => c.name === columnName);\n if (!column) {\n throw new Error(`Column not found: ${columnName}`);\n }\n\n column.totalFunction = fn;\n this._dirty = true;\n\n // Write the formula to the total row cell\n this._writeTotalRowFormula(column);\n\n return this;\n }\n\n /**\n * Get total function for a column if set\n */\n getTotalFunction(columnName: string): TableTotalFunction | undefined {\n const column = this._columns.find((c) => c.name === columnName);\n return column?.totalFunction;\n }\n\n /**\n * Enable or disable auto-filter\n * @param enabled - Whether auto-filter should be enabled\n * @returns this for method chaining\n */\n setAutoFilter(enabled: boolean): this {\n this._autoFilter = enabled;\n this._dirty = true;\n return this;\n }\n\n /**\n * Update table style configuration\n * @param style - Style options to apply\n * @returns this for method chaining\n */\n setStyle(style: Partial<TableStyleConfig>): this {\n if (style.name !== undefined) this._style.name = style.name;\n if (style.showRowStripes !== undefined) this._style.showRowStripes = style.showRowStripes;\n if (style.showColumnStripes !== undefined) this._style.showColumnStripes = style.showColumnStripes;\n if (style.showFirstColumn !== undefined) this._style.showFirstColumn = style.showFirstColumn;\n if (style.showLastColumn !== undefined) this._style.showLastColumn = style.showLastColumn;\n this._dirty = true;\n return this;\n }\n\n /**\n * Enable or disable the total row\n * @param enabled - Whether total row should be shown\n * @returns this for method chaining\n */\n setTotalRow(enabled: boolean): this {\n if (this._totalRow === enabled) return this;\n\n this._totalRow = enabled;\n this._dirty = true;\n\n if (enabled) {\n this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };\n this._range.end.row++;\n } else {\n this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };\n for (const col of this._columns) {\n col.totalFunction = undefined;\n }\n }\n\n return this;\n }\n\n /**\n * Extract column names from the header row of the worksheet\n */\n private _extractColumns(): void {\n const headerRow = this._range.start.row;\n const startCol = this._range.start.col;\n const endCol = this._range.end.col;\n\n for (let col = startCol; col <= endCol; col++) {\n const cell = this._headerRow ? this._worksheet.getCellIfExists(headerRow, col) : undefined;\n const value = cell?.value;\n const name = value != null ? String(value) : `Column${col - startCol + 1}`;\n\n this._columns.push({\n id: col - startCol + 1,\n name,\n colIndex: col,\n });\n }\n }\n\n /**\n * Write the SUBTOTAL formula to a total row cell\n */\n private _writeTotalRowFormula(column: TableColumn): void {\n if (!this._totalRow || !column.totalFunction || column.totalFunction === 'none') {\n return;\n }\n\n const totalRowIndex = this._range.end.row;\n const cell = this._worksheet.cell(totalRowIndex, column.colIndex);\n\n // Generate SUBTOTAL formula with structured reference\n const funcNum = TOTAL_FUNCTION_NUMBERS[column.totalFunction];\n // Use structured reference: SUBTOTAL(109,[ColumnName])\n const formula = `SUBTOTAL(${funcNum},[${column.name}])`;\n cell.formula = formula;\n }\n\n /**\n * Get the auto-filter range (excludes total row if present)\n */\n private _getAutoFilterRange(): string {\n const start = toAddress(this._range.start.row, this._range.start.col);\n\n // Auto-filter excludes the total row\n let endRow = this._range.end.row;\n if (this._totalRow) {\n endRow--;\n }\n\n const end = toAddress(endRow, this._range.end.col);\n return `${start}:${end}`;\n }\n\n /**\n * Generate the table definition XML\n */\n toXml(): string {\n const children: XmlNode[] = [];\n\n // Auto-filter element\n if (this._autoFilter) {\n const autoFilterRef = this._getAutoFilterRange();\n children.push(createElement('autoFilter', { ref: autoFilterRef }, []));\n }\n\n // Table columns\n const columnNodes: XmlNode[] = this._columns.map((col) => {\n const attrs: Record<string, string> = {\n id: String(col.id),\n name: col.name,\n };\n\n // Add total function if specified\n if (this._totalRow && col.totalFunction && col.totalFunction !== 'none') {\n attrs.totalsRowFunction = TOTAL_FUNCTION_NAMES[col.totalFunction];\n }\n\n return createElement('tableColumn', attrs, []);\n });\n\n children.push(createElement('tableColumns', { count: String(columnNodes.length) }, columnNodes));\n\n // Table style info\n const styleAttrs: Record<string, string> = {\n name: this._style.name || 'TableStyleMedium2',\n showFirstColumn: this._style.showFirstColumn ? '1' : '0',\n showLastColumn: this._style.showLastColumn ? '1' : '0',\n showRowStripes: this._style.showRowStripes !== false ? '1' : '0',\n showColumnStripes: this._style.showColumnStripes ? '1' : '0',\n };\n children.push(createElement('tableStyleInfo', styleAttrs, []));\n\n // Build table attributes\n const tableRef = toRange(this._range);\n const tableAttrs: Record<string, string> = {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n id: String(this._id),\n name: this._name,\n displayName: this._displayName,\n ref: tableRef,\n };\n\n if (!this._headerRow) {\n tableAttrs.headerRowCount = '0';\n }\n\n if (this._totalRow) {\n tableAttrs.totalsRowCount = '1';\n } else {\n tableAttrs.totalsRowShown = '0';\n }\n\n // Build complete table node\n const tableNode = createElement('table', tableAttrs, children);\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([tableNode])}`;\n }\n}\n\n/**\n * Internal column representation\n */\ninterface TableColumn {\n id: number;\n name: string;\n colIndex: number;\n totalFunction?: TableTotalFunction;\n}\n","import type { CellData, RangeAddress, SheetToJsonConfig, CellValue, DateHandling, TableConfig } from './types';\r\nimport type { Workbook } from './workbook';\r\nimport { Cell, parseCellRef } from './cell';\r\nimport { Range } from './range';\r\nimport { Table } from './table';\r\nimport { parseRange, toAddress, parseAddress, letterToCol } from './utils/address';\r\nimport {\r\n parseXml,\r\n findElement,\r\n getChildren,\r\n getAttr,\r\n XmlNode,\r\n stringifyXml,\r\n createElement,\r\n createText,\r\n} from './utils/xml';\r\n\r\n/**\r\n * Represents a worksheet in a workbook\r\n */\r\nexport class Worksheet {\r\n private _name: string;\r\n private _workbook: Workbook;\r\n private _cells: Map<string, Cell> = new Map();\r\n private _xmlNodes: XmlNode[] | null = null;\r\n private _dirty = false;\r\n private _mergedCells: Set<string> = new Set();\r\n private _sheetData: XmlNode[] = [];\r\n private _columnWidths: Map<number, number> = new Map();\r\n private _rowHeights: Map<number, number> = new Map();\r\n private _frozenPane: { row: number; col: number } | null = null;\r\n private _dataBoundsCache: { minRow: number; maxRow: number; minCol: number; maxCol: number } | null = null;\r\n private _boundsDirty = true;\r\n private _tables: Table[] = [];\r\n private _preserveXml = false;\r\n private _tableRelIds: string[] | null = null;\r\n private _sheetViewsDirty = false;\r\n private _colsDirty = false;\r\n private _tablePartsDirty = false;\r\n\r\n constructor(workbook: Workbook, name: string) {\r\n this._workbook = workbook;\r\n this._name = name;\r\n }\r\n\r\n /**\r\n * Get the workbook this sheet belongs to\r\n */\r\n get workbook(): Workbook {\r\n return this._workbook;\r\n }\r\n\r\n /**\r\n * Get the sheet name\r\n */\r\n get name(): string {\r\n return this._name;\r\n }\r\n\r\n /**\r\n * Set the sheet name\r\n */\r\n set name(value: string) {\r\n this._name = value;\r\n this._dirty = true;\r\n }\r\n\r\n /**\r\n * Parse worksheet XML content\r\n */\r\n parse(xml: string): void {\r\n this._xmlNodes = parseXml(xml);\r\n this._preserveXml = true;\r\n const worksheet = findElement(this._xmlNodes, 'worksheet');\r\n if (!worksheet) return;\r\n\r\n const worksheetChildren = getChildren(worksheet, 'worksheet');\r\n\r\n // Parse sheet views (freeze panes)\r\n const sheetViews = findElement(worksheetChildren, 'sheetViews');\r\n if (sheetViews) {\r\n const viewChildren = getChildren(sheetViews, 'sheetViews');\r\n const sheetView = findElement(viewChildren, 'sheetView');\r\n if (sheetView) {\r\n const sheetViewChildren = getChildren(sheetView, 'sheetView');\r\n const pane = findElement(sheetViewChildren, 'pane');\r\n if (pane && getAttr(pane, 'state') === 'frozen') {\r\n const xSplit = parseInt(getAttr(pane, 'xSplit') || '0', 10);\r\n const ySplit = parseInt(getAttr(pane, 'ySplit') || '0', 10);\r\n if (xSplit > 0 || ySplit > 0) {\r\n this._frozenPane = { row: ySplit, col: xSplit };\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Parse sheet data (cells)\r\n const sheetData = findElement(worksheetChildren, 'sheetData');\r\n if (sheetData) {\r\n this._sheetData = getChildren(sheetData, 'sheetData');\r\n this._parseSheetData(this._sheetData);\r\n }\r\n\r\n // Parse column widths\r\n const cols = findElement(worksheetChildren, 'cols');\r\n if (cols) {\r\n const colChildren = getChildren(cols, 'cols');\r\n for (const col of colChildren) {\r\n if (!('col' in col)) continue;\r\n const min = parseInt(getAttr(col, 'min') || '0', 10);\r\n const max = parseInt(getAttr(col, 'max') || '0', 10);\r\n const width = parseFloat(getAttr(col, 'width') || '0');\r\n if (!Number.isFinite(width) || width <= 0) continue;\r\n if (min > 0 && max > 0) {\r\n for (let idx = min; idx <= max; idx++) {\r\n this._columnWidths.set(idx - 1, width);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Parse merged cells\r\n const mergeCells = findElement(worksheetChildren, 'mergeCells');\r\n if (mergeCells) {\r\n const mergeChildren = getChildren(mergeCells, 'mergeCells');\r\n for (const mergeCell of mergeChildren) {\r\n if ('mergeCell' in mergeCell) {\r\n const ref = getAttr(mergeCell, 'ref');\r\n if (ref) {\r\n this._mergedCells.add(ref);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Parse the sheetData element to extract cells\r\n */\r\n private _parseSheetData(rows: XmlNode[]): void {\r\n for (const rowNode of rows) {\r\n if (!('row' in rowNode)) continue;\r\n\r\n const rowIndex = parseInt(getAttr(rowNode, 'r') || '0', 10) - 1;\r\n const rowHeight = parseFloat(getAttr(rowNode, 'ht') || '0');\r\n if (rowIndex >= 0 && Number.isFinite(rowHeight) && rowHeight > 0) {\r\n this._rowHeights.set(rowIndex, rowHeight);\r\n }\r\n\r\n const rowChildren = getChildren(rowNode, 'row');\r\n for (const cellNode of rowChildren) {\r\n if (!('c' in cellNode)) continue;\r\n\r\n const ref = getAttr(cellNode, 'r');\r\n if (!ref) continue;\r\n\r\n const { row, col } = parseAddress(ref);\r\n const cellData = this._parseCellNode(cellNode);\r\n const cell = new Cell(this, row, col, cellData);\r\n this._cells.set(ref, cell);\r\n }\r\n }\r\n\r\n this._boundsDirty = true;\r\n }\r\n\r\n /**\r\n * Parse a cell XML node to CellData\r\n */\r\n private _parseCellNode(node: XmlNode): CellData {\r\n const data: CellData = {};\r\n\r\n // Type attribute\r\n const t = getAttr(node, 't');\r\n if (t) {\r\n data.t = t as CellData['t'];\r\n }\r\n\r\n // Style attribute\r\n const s = getAttr(node, 's');\r\n if (s) {\r\n data.s = parseInt(s, 10);\r\n }\r\n\r\n const children = getChildren(node, 'c');\r\n\r\n // Value element\r\n const vNode = findElement(children, 'v');\r\n if (vNode) {\r\n const vChildren = getChildren(vNode, 'v');\r\n for (const child of vChildren) {\r\n if ('#text' in child) {\r\n const text = child['#text'] as string;\r\n // Parse based on type\r\n if (data.t === 's') {\r\n data.v = parseInt(text, 10); // Shared string index\r\n } else if (data.t === 'b') {\r\n data.v = text === '1' ? 1 : 0;\r\n } else if (data.t === 'e' || data.t === 'str') {\r\n data.v = text;\r\n } else {\r\n // Number or default\r\n data.v = parseFloat(text);\r\n }\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // Formula element\r\n const fNode = findElement(children, 'f');\r\n if (fNode) {\r\n const fChildren = getChildren(fNode, 'f');\r\n for (const child of fChildren) {\r\n if ('#text' in child) {\r\n data.f = child['#text'] as string;\r\n break;\r\n }\r\n }\r\n\r\n // Check for shared formula\r\n const si = getAttr(fNode, 'si');\r\n if (si) {\r\n data.si = parseInt(si, 10);\r\n }\r\n\r\n // Check for array formula range\r\n const ref = getAttr(fNode, 'ref');\r\n if (ref) {\r\n data.F = ref;\r\n }\r\n }\r\n\r\n // Inline string (is element)\r\n const isNode = findElement(children, 'is');\r\n if (isNode) {\r\n data.t = 'str';\r\n const isChildren = getChildren(isNode, 'is');\r\n const tNode = findElement(isChildren, 't');\r\n if (tNode) {\r\n const tChildren = getChildren(tNode, 't');\r\n for (const child of tChildren) {\r\n if ('#text' in child) {\r\n data.v = child['#text'] as string;\r\n break;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return data;\r\n }\r\n\r\n /**\r\n * Get a cell by address or row/col\r\n */\r\n cell(rowOrAddress: number | string, col?: number): Cell {\r\n const { row, col: c } = parseCellRef(rowOrAddress, col);\r\n const address = toAddress(row, c);\r\n\r\n let cell = this._cells.get(address);\r\n if (!cell) {\r\n cell = new Cell(this, row, c);\r\n this._cells.set(address, cell);\r\n this._boundsDirty = true;\r\n }\r\n\r\n return cell;\r\n }\r\n\r\n /**\r\n * Get an existing cell without creating it.\r\n */\r\n getCellIfExists(rowOrAddress: number | string, col?: number): Cell | undefined {\r\n const { row, col: c } = parseCellRef(rowOrAddress, col);\r\n const address = toAddress(row, c);\r\n return this._cells.get(address);\r\n }\r\n\r\n /**\r\n * Get a range of cells\r\n */\r\n range(rangeStr: string): Range;\r\n range(startRow: number, startCol: number, endRow: number, endCol: number): Range;\r\n range(startRowOrRange: number | string, startCol?: number, endRow?: number, endCol?: number): Range {\r\n let rangeAddr: RangeAddress;\r\n\r\n if (typeof startRowOrRange === 'string') {\r\n rangeAddr = parseRange(startRowOrRange);\r\n } else {\r\n if (startCol === undefined || endRow === undefined || endCol === undefined) {\r\n throw new Error('All range parameters must be provided');\r\n }\r\n rangeAddr = {\r\n start: { row: startRowOrRange, col: startCol },\r\n end: { row: endRow, col: endCol },\r\n };\r\n }\r\n\r\n return new Range(this, rangeAddr);\r\n }\r\n\r\n /**\r\n * Merge cells in the given range\r\n */\r\n mergeCells(rangeOrStart: string, end?: string): void {\r\n let rangeStr: string;\r\n if (end) {\r\n rangeStr = `${rangeOrStart}:${end}`;\r\n } else {\r\n rangeStr = rangeOrStart;\r\n }\r\n this._mergedCells.add(rangeStr);\r\n this._dirty = true;\r\n }\r\n\r\n /**\r\n * Unmerge cells in the given range\r\n */\r\n unmergeCells(rangeStr: string): void {\r\n this._mergedCells.delete(rangeStr);\r\n this._dirty = true;\r\n }\r\n\r\n /**\r\n * Get all merged cell ranges\r\n */\r\n get mergedCells(): string[] {\r\n return Array.from(this._mergedCells);\r\n }\r\n\r\n /**\r\n * Check if the worksheet has been modified\r\n */\r\n get dirty(): boolean {\r\n if (this._dirty) return true;\r\n for (const cell of this._cells.values()) {\r\n if (cell.dirty) return true;\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * Get all cells in the worksheet\r\n */\r\n get cells(): Map<string, Cell> {\r\n return this._cells;\r\n }\r\n\r\n /**\r\n * Set a column width (0-based index or column letter)\r\n */\r\n setColumnWidth(col: number | string, width: number): void {\r\n if (!Number.isFinite(width) || width <= 0) {\r\n throw new Error('Column width must be a positive number');\r\n }\r\n\r\n const colIndex = typeof col === 'number' ? col : letterToCol(col);\r\n if (colIndex < 0) {\r\n throw new Error(`Invalid column: ${col}`);\r\n }\r\n\r\n this._columnWidths.set(colIndex, width);\r\n this._colsDirty = true;\r\n this._dirty = true;\r\n }\r\n\r\n /**\r\n * Get a column width if set\r\n */\r\n getColumnWidth(col: number | string): number | undefined {\r\n const colIndex = typeof col === 'number' ? col : letterToCol(col);\r\n return this._columnWidths.get(colIndex);\r\n }\r\n\r\n /**\r\n * Set a row height (0-based index)\r\n */\r\n setRowHeight(row: number, height: number): void {\r\n if (!Number.isFinite(height) || height <= 0) {\r\n throw new Error('Row height must be a positive number');\r\n }\r\n if (row < 0) {\r\n throw new Error('Row index must be >= 0');\r\n }\r\n\r\n this._rowHeights.set(row, height);\r\n this._colsDirty = true;\r\n this._dirty = true;\r\n }\r\n\r\n /**\r\n * Get a row height if set\r\n */\r\n getRowHeight(row: number): number | undefined {\r\n return this._rowHeights.get(row);\r\n }\r\n\r\n /**\r\n * Freeze panes at a given row/column split (counts from top-left)\r\n */\r\n freezePane(rowSplit: number, colSplit: number): void {\r\n if (rowSplit < 0 || colSplit < 0) {\r\n throw new Error('Freeze pane splits must be >= 0');\r\n }\r\n if (rowSplit === 0 && colSplit === 0) {\r\n this._frozenPane = null;\r\n } else {\r\n this._frozenPane = { row: rowSplit, col: colSplit };\r\n }\r\n this._sheetViewsDirty = true;\r\n this._dirty = true;\r\n }\r\n\r\n /**\r\n * Get current frozen pane configuration\r\n */\r\n getFrozenPane(): { row: number; col: number } | null {\r\n return this._frozenPane ? { ...this._frozenPane } : null;\r\n }\r\n\r\n /**\r\n * Get all tables in the worksheet\r\n */\r\n get tables(): Table[] {\r\n return [...this._tables];\r\n }\r\n\r\n /**\r\n * Get column width entries\r\n * @internal\r\n */\r\n getColumnWidths(): Map<number, number> {\r\n return new Map(this._columnWidths);\r\n }\r\n\r\n /**\r\n * Get row height entries\r\n * @internal\r\n */\r\n getRowHeights(): Map<number, number> {\r\n return new Map(this._rowHeights);\r\n }\r\n\r\n /**\r\n * Set table relationship IDs for tableParts generation.\r\n * @internal\r\n */\r\n setTableRelIds(ids: string[] | null): void {\r\n this._tableRelIds = ids ? [...ids] : null;\r\n this._tablePartsDirty = true;\r\n }\r\n\r\n /**\r\n * Create an Excel Table (ListObject) from a data range.\r\n *\r\n * Tables provide structured data features like auto-filter, banded styling,\r\n * and total row with aggregation functions.\r\n *\r\n * @param config - Table configuration\r\n * @returns Table instance for method chaining\r\n *\r\n * @example\r\n * ```typescript\r\n * // Create a table with default styling\r\n * const table = sheet.createTable({\r\n * name: 'SalesData',\r\n * range: 'A1:D10',\r\n * });\r\n *\r\n * // Create a table with total row\r\n * const table = sheet.createTable({\r\n * name: 'SalesData',\r\n * range: 'A1:D10',\r\n * totalRow: true,\r\n * style: { name: 'TableStyleMedium2' }\r\n * });\r\n *\r\n * table.setTotalFunction('Sales', 'sum');\r\n * ```\r\n */\r\n createTable(config: TableConfig): Table {\r\n // Validate table name is unique within the workbook\r\n for (const sheet of this._workbook.sheetNames) {\r\n const ws = this._workbook.sheet(sheet);\r\n for (const table of ws._tables) {\r\n if (table.name === config.name) {\r\n throw new Error(`Table name already exists: ${config.name}`);\r\n }\r\n }\r\n }\r\n\r\n // Validate table name format (Excel rules: no spaces at start/end, alphanumeric + underscore)\r\n if (!config.name || !/^[A-Za-z_\\\\][A-Za-z0-9_.\\\\]*$/.test(config.name)) {\r\n throw new Error(\r\n `Invalid table name: ${config.name}. Names must start with a letter or underscore and contain only alphanumeric characters, underscores, or periods.`,\r\n );\r\n }\r\n\r\n // Create the table with a unique ID from the workbook\r\n const tableId = this._workbook.getNextTableId();\r\n const table = new Table(this, config, tableId);\r\n\r\n this._tables.push(table);\r\n this._tablePartsDirty = true;\r\n this._dirty = true;\r\n\r\n return table;\r\n }\r\n\r\n /**\r\n * Convert sheet data to an array of JSON objects.\n *\r\n * @param config - Configuration options\n * @returns Array of objects where keys are field names and values are cell values\n *\n * @example\n * ```typescript\r\n * // Using first row as headers\r\n * const data = sheet.toJson();\r\n *\r\n * // Using custom field names\r\n * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });\r\n *\r\n * // Starting from a specific row/column\r\n * const data = sheet.toJson({ startRow: 2, startCol: 1 });\r\n * ```\r\n */\r\n toJson<T = Record<string, CellValue>>(config: SheetToJsonConfig = {}): T[] {\n const {\r\n fields,\r\n startRow = 0,\r\n startCol = 0,\r\n endRow,\r\n endCol,\r\n stopOnEmptyRow = true,\n dateHandling = this._workbook.dateHandling,\n asText = false,\n locale,\n } = config;\n\r\n // Get the bounds of data in the sheet\r\n const bounds = this._getDataBounds();\r\n if (!bounds) {\r\n return [];\r\n }\r\n\r\n const effectiveEndRow = endRow ?? bounds.maxRow;\r\n const effectiveEndCol = endCol ?? bounds.maxCol;\r\n\r\n // Determine field names\r\n let fieldNames: string[];\r\n let dataStartRow: number;\r\n\r\n if (fields) {\r\n // Use provided field names, data starts at startRow\r\n fieldNames = fields;\r\n dataStartRow = startRow;\r\n } else {\r\n // Use first row as headers\r\n fieldNames = [];\r\n for (let col = startCol; col <= effectiveEndCol; col++) {\r\n const cell = this._cells.get(toAddress(startRow, col));\r\n const value = cell?.value;\r\n fieldNames.push(value != null ? String(value) : `column${col}`);\r\n }\r\n dataStartRow = startRow + 1;\r\n }\r\n\r\n // Read data rows\r\n const result: T[] = [];\r\n\r\n for (let row = dataStartRow; row <= effectiveEndRow; row++) {\r\n const obj: Record<string, CellValue | string> = {};\r\n let hasData = false;\r\n\r\n for (let colOffset = 0; colOffset < fieldNames.length; colOffset++) {\r\n const col = startCol + colOffset;\r\n const cell = this._cells.get(toAddress(row, col));\r\n\r\n let value: CellValue | string;\r\n\r\n if (asText) {\n // Return formatted text instead of raw value\n value = cell?.textWithLocale(locale) ?? '';\n if (value !== '') {\n hasData = true;\n }\n } else {\r\n value = cell?.value ?? null;\r\n if (value instanceof Date) {\r\n value = this._serializeDate(value, dateHandling, cell);\r\n }\r\n if (value !== null) {\r\n hasData = true;\r\n }\r\n }\r\n\r\n const fieldName = fieldNames[colOffset];\r\n if (fieldName) {\r\n obj[fieldName] = value;\r\n }\r\n }\r\n\r\n // Stop on empty row if configured\r\n if (stopOnEmptyRow && !hasData) {\r\n break;\r\n }\r\n\r\n result.push(obj as T);\r\n }\r\n\r\n return result;\r\n }\r\n\r\n private _serializeDate(value: Date, dateHandling: DateHandling, cell?: Cell | null): CellValue | number | string {\r\n if (dateHandling === 'excelSerial') {\r\n return cell?._jsDateToExcel(value) ?? value;\r\n }\r\n\r\n if (dateHandling === 'isoString') {\r\n return value.toISOString();\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Get the bounds of data in the sheet (min/max row and column with data)\r\n */\r\n private _getDataBounds(): { minRow: number; maxRow: number; minCol: number; maxCol: number } | null {\r\n if (!this._boundsDirty && this._dataBoundsCache) {\r\n return this._dataBoundsCache;\r\n }\r\n\r\n if (this._cells.size === 0) {\r\n this._dataBoundsCache = null;\r\n this._boundsDirty = false;\r\n return null;\r\n }\r\n\r\n let minRow = Infinity;\r\n let maxRow = -Infinity;\r\n let minCol = Infinity;\r\n let maxCol = -Infinity;\r\n\r\n for (const cell of this._cells.values()) {\r\n if (cell.value !== null) {\r\n minRow = Math.min(minRow, cell.row);\r\n maxRow = Math.max(maxRow, cell.row);\r\n minCol = Math.min(minCol, cell.col);\r\n maxCol = Math.max(maxCol, cell.col);\r\n }\r\n }\r\n\r\n if (minRow === Infinity) {\r\n this._dataBoundsCache = null;\r\n this._boundsDirty = false;\r\n return null;\r\n }\r\n\r\n this._dataBoundsCache = { minRow, maxRow, minCol, maxCol };\r\n this._boundsDirty = false;\r\n return this._dataBoundsCache;\r\n }\r\n\r\n /**\r\n * Generate XML for this worksheet\r\n */\r\n toXml(): string {\r\n const preserved = this._preserveXml && this._xmlNodes ? this._buildPreservedWorksheet() : null;\r\n // Build sheetData from cells\r\n const sheetDataNode = this._buildSheetDataNode();\r\n\r\n // Build worksheet structure\r\n const worksheetChildren: XmlNode[] = [];\r\n\r\n // Sheet views (freeze panes)\r\n if (this._frozenPane) {\r\n const paneAttrs: Record<string, string> = { state: 'frozen' };\r\n const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);\r\n paneAttrs.topLeftCell = topLeftCell;\r\n if (this._frozenPane.col > 0) {\r\n paneAttrs.xSplit = String(this._frozenPane.col);\r\n }\r\n if (this._frozenPane.row > 0) {\r\n paneAttrs.ySplit = String(this._frozenPane.row);\r\n }\r\n\r\n let activePane = 'bottomRight';\r\n if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {\r\n activePane = 'bottomLeft';\r\n } else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {\r\n activePane = 'topRight';\r\n }\r\n\r\n paneAttrs.activePane = activePane;\r\n const paneNode = createElement('pane', paneAttrs, []);\r\n const selectionNode = createElement(\r\n 'selection',\r\n { pane: activePane, activeCell: topLeftCell, sqref: topLeftCell },\r\n [],\r\n );\r\n\r\n const sheetViewNode = createElement('sheetView', { workbookViewId: '0' }, [paneNode, selectionNode]);\r\n worksheetChildren.push(createElement('sheetViews', {}, [sheetViewNode]));\r\n }\r\n\r\n // Column widths\r\n if (this._columnWidths.size > 0) {\r\n const colNodes: XmlNode[] = [];\r\n const entries = Array.from(this._columnWidths.entries()).sort((a, b) => a[0] - b[0]);\r\n for (const [colIndex, width] of entries) {\r\n colNodes.push(\r\n createElement(\r\n 'col',\r\n {\r\n min: String(colIndex + 1),\r\n max: String(colIndex + 1),\r\n width: String(width),\r\n customWidth: '1',\r\n },\r\n [],\r\n ),\r\n );\r\n }\r\n worksheetChildren.push(createElement('cols', {}, colNodes));\r\n }\r\n\r\n worksheetChildren.push(sheetDataNode);\r\n\r\n // Add merged cells if any\r\n if (this._mergedCells.size > 0) {\r\n const mergeCellNodes: XmlNode[] = [];\r\n for (const ref of this._mergedCells) {\r\n mergeCellNodes.push(createElement('mergeCell', { ref }, []));\r\n }\r\n const mergeCellsNode = createElement('mergeCells', { count: String(this._mergedCells.size) }, mergeCellNodes);\r\n worksheetChildren.push(mergeCellsNode);\r\n }\r\n\r\n // Add table parts if any tables exist\r\n const tablePartsNode = this._buildTablePartsNode();\r\n if (tablePartsNode) {\r\n worksheetChildren.push(tablePartsNode);\r\n }\r\n\r\n const worksheetNode = createElement(\r\n 'worksheet',\r\n {\r\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\r\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\r\n },\r\n worksheetChildren,\r\n );\r\n\r\n if (preserved) {\r\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([preserved])}`;\r\n }\r\n\r\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([worksheetNode])}`;\r\n }\r\n\r\n private _buildSheetDataNode(): XmlNode {\r\n const rowMap = new Map<number, Cell[]>();\r\n for (const cell of this._cells.values()) {\r\n const row = cell.row;\r\n if (!rowMap.has(row)) {\r\n rowMap.set(row, []);\r\n }\r\n rowMap.get(row)!.push(cell);\r\n }\r\n\r\n for (const rowIdx of this._rowHeights.keys()) {\r\n if (!rowMap.has(rowIdx)) {\r\n rowMap.set(rowIdx, []);\r\n }\r\n }\r\n\r\n const sortedRows = Array.from(rowMap.entries()).sort((a, b) => a[0] - b[0]);\r\n const rowNodes: XmlNode[] = [];\r\n for (const [rowIdx, cells] of sortedRows) {\r\n cells.sort((a, b) => a.col - b.col);\r\n\r\n const cellNodes: XmlNode[] = [];\r\n for (const cell of cells) {\r\n const cellNode = this._buildCellNode(cell);\r\n cellNodes.push(cellNode);\r\n }\r\n\r\n const rowAttrs: Record<string, string> = { r: String(rowIdx + 1) };\r\n const rowHeight = this._rowHeights.get(rowIdx);\r\n if (rowHeight !== undefined) {\r\n rowAttrs.ht = String(rowHeight);\r\n rowAttrs.customHeight = '1';\r\n }\r\n const rowNode = createElement('row', rowAttrs, cellNodes);\r\n rowNodes.push(rowNode);\r\n }\r\n\r\n return createElement('sheetData', {}, rowNodes);\r\n }\r\n\r\n private _buildSheetViewsNode(): XmlNode | null {\r\n if (!this._frozenPane) return null;\r\n const paneAttrs: Record<string, string> = { state: 'frozen' };\r\n const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);\r\n paneAttrs.topLeftCell = topLeftCell;\r\n if (this._frozenPane.col > 0) {\r\n paneAttrs.xSplit = String(this._frozenPane.col);\r\n }\r\n if (this._frozenPane.row > 0) {\r\n paneAttrs.ySplit = String(this._frozenPane.row);\r\n }\r\n\r\n let activePane = 'bottomRight';\r\n if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {\r\n activePane = 'bottomLeft';\r\n } else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {\r\n activePane = 'topRight';\r\n }\r\n\r\n paneAttrs.activePane = activePane;\r\n const paneNode = createElement('pane', paneAttrs, []);\r\n const selectionNode = createElement(\r\n 'selection',\r\n { pane: activePane, activeCell: topLeftCell, sqref: topLeftCell },\r\n [],\r\n );\r\n\r\n const sheetViewNode = createElement('sheetView', { workbookViewId: '0' }, [paneNode, selectionNode]);\r\n return createElement('sheetViews', {}, [sheetViewNode]);\r\n }\r\n\r\n private _buildColsNode(): XmlNode | null {\r\n if (this._columnWidths.size === 0) return null;\r\n const colNodes: XmlNode[] = [];\r\n const entries = Array.from(this._columnWidths.entries()).sort((a, b) => a[0] - b[0]);\r\n for (const [colIndex, width] of entries) {\r\n colNodes.push(\r\n createElement(\r\n 'col',\r\n {\r\n min: String(colIndex + 1),\r\n max: String(colIndex + 1),\r\n width: String(width),\r\n customWidth: '1',\r\n },\r\n [],\r\n ),\r\n );\r\n }\r\n return createElement('cols', {}, colNodes);\r\n }\r\n\r\n private _buildMergeCellsNode(): XmlNode | null {\r\n if (this._mergedCells.size === 0) return null;\r\n const mergeCellNodes: XmlNode[] = [];\r\n for (const ref of this._mergedCells) {\r\n mergeCellNodes.push(createElement('mergeCell', { ref }, []));\r\n }\r\n return createElement('mergeCells', { count: String(this._mergedCells.size) }, mergeCellNodes);\r\n }\r\n\r\n private _buildTablePartsNode(): XmlNode | null {\r\n if (this._tables.length === 0) return null;\r\n const tablePartNodes: XmlNode[] = [];\r\n for (let i = 0; i < this._tables.length; i++) {\r\n const relId =\r\n this._tableRelIds && this._tableRelIds.length === this._tables.length ? this._tableRelIds[i] : `rId${i + 1}`;\r\n tablePartNodes.push(createElement('tablePart', { 'r:id': relId }, []));\r\n }\r\n return createElement('tableParts', { count: String(this._tables.length) }, tablePartNodes);\r\n }\r\n\r\n private _buildPreservedWorksheet(): XmlNode | null {\r\n if (!this._xmlNodes) return null;\r\n const worksheet = findElement(this._xmlNodes, 'worksheet');\r\n if (!worksheet) return null;\r\n\r\n const children = getChildren(worksheet, 'worksheet');\r\n\r\n const upsertChild = (tag: string, node: XmlNode | null) => {\r\n const existingIndex = children.findIndex((child) => tag in child);\r\n if (node) {\r\n if (existingIndex >= 0) {\r\n children[existingIndex] = node;\r\n } else {\r\n children.push(node);\r\n }\r\n } else if (existingIndex >= 0) {\r\n children.splice(existingIndex, 1);\r\n }\r\n };\r\n\r\n if (this._sheetViewsDirty) {\r\n const sheetViewsNode = this._buildSheetViewsNode();\r\n upsertChild('sheetViews', sheetViewsNode);\r\n }\r\n\r\n if (this._colsDirty) {\r\n const colsNode = this._buildColsNode();\r\n upsertChild('cols', colsNode);\r\n }\r\n\r\n const sheetDataNode = this._buildSheetDataNode();\r\n upsertChild('sheetData', sheetDataNode);\r\n\r\n const mergeCellsNode = this._buildMergeCellsNode();\r\n upsertChild('mergeCells', mergeCellsNode);\r\n\r\n if (this._tablePartsDirty) {\r\n const tablePartsNode = this._buildTablePartsNode();\r\n upsertChild('tableParts', tablePartsNode);\r\n }\r\n\r\n return worksheet;\r\n }\r\n\r\n /**\r\n * Build a cell XML node from a Cell object\r\n */\r\n private _buildCellNode(cell: Cell): XmlNode {\r\n const data = cell.data;\r\n const attrs: Record<string, string> = { r: cell.address };\r\n\r\n if (data.t && data.t !== 'n') {\r\n attrs.t = data.t;\r\n }\r\n if (data.s !== undefined) {\r\n attrs.s = String(data.s);\r\n }\r\n\r\n const children: XmlNode[] = [];\r\n\r\n // Formula\r\n if (data.f) {\r\n const fAttrs: Record<string, string> = {};\r\n if (data.F) fAttrs.ref = data.F;\r\n if (data.si !== undefined) fAttrs.si = String(data.si);\r\n children.push(createElement('f', fAttrs, [createText(data.f)]));\r\n }\r\n\r\n // Value\r\n if (data.v !== undefined) {\r\n children.push(createElement('v', {}, [createText(String(data.v))]));\r\n }\r\n\r\n return createElement('c', attrs, children);\r\n }\r\n}\r\n","import {\n parseXml,\n findElement,\n getChildren,\n getAttr,\n XmlNode,\n stringifyXml,\n createElement,\n createText,\n} from './utils/xml';\n\n/**\n * Manages the shared strings table from xl/sharedStrings.xml\n * Excel stores strings in a shared table to reduce file size\n */\nexport class SharedStrings {\n private entries: SharedStringEntry[] = [];\n private stringToIndex: Map<string, number> = new Map();\n private _dirty = false;\n private _totalCount = 0;\n\n /**\n * Parse shared strings from XML content\n */\n static parse(xml: string): SharedStrings {\n const ss = new SharedStrings();\n const parsed = parseXml(xml);\n const sst = findElement(parsed, 'sst');\n if (!sst) return ss;\n\n const countAttr = getAttr(sst, 'count');\n if (countAttr) {\n const total = parseInt(countAttr, 10);\n if (Number.isFinite(total) && total >= 0) {\n ss._totalCount = total;\n }\n }\n\n const children = getChildren(sst, 'sst');\n for (const child of children) {\n if ('si' in child) {\n const siChildren = getChildren(child, 'si');\n const text = ss.extractText(siChildren);\n ss.entries.push({ text, node: child });\n ss.stringToIndex.set(text, ss.entries.length - 1);\n }\n }\n\n if (ss._totalCount === 0 && ss.entries.length > 0) {\n ss._totalCount = ss.entries.length;\n }\n\n return ss;\n }\n\n /**\n * Extract text from a string item (si element)\n * Handles both simple <t> elements and rich text <r> elements\n */\n private extractText(nodes: XmlNode[]): string {\n let text = '';\n for (const node of nodes) {\n if ('t' in node) {\n // Simple text: <t>value</t>\n const tChildren = getChildren(node, 't');\n for (const child of tChildren) {\n if ('#text' in child) {\n text += child['#text'] as string;\n }\n }\n } else if ('r' in node) {\n // Rich text: <r><t>value</t></r>\n const rChildren = getChildren(node, 'r');\n for (const rChild of rChildren) {\n if ('t' in rChild) {\n const tChildren = getChildren(rChild, 't');\n for (const child of tChildren) {\n if ('#text' in child) {\n text += child['#text'] as string;\n }\n }\n }\n }\n }\n }\n return text;\n }\n\n /**\n * Get a string by index\n */\n getString(index: number): string | undefined {\n return this.entries[index]?.text;\n }\n\n /**\n * Add a string and return its index\n * If the string already exists, returns the existing index\n */\n addString(str: string): number {\n const existing = this.stringToIndex.get(str);\n if (existing !== undefined) {\n this._totalCount++;\n this._dirty = true;\n return existing;\n }\n const index = this.entries.length;\n const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [\n createText(str),\n ]);\n const siElement = createElement('si', {}, [tElement]);\n this.entries.push({ text: str, node: siElement });\n this.stringToIndex.set(str, index);\n this._totalCount++;\n this._dirty = true;\n return index;\n }\n\n /**\n * Check if the shared strings table has been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Get the count of strings\n */\n get count(): number {\n return this.entries.length;\n }\n\n /**\n * Get total usage count of shared strings\n */\n get totalCount(): number {\n return Math.max(this._totalCount, this.entries.length);\n }\n\n /**\n * Get all unique shared strings in insertion order.\n */\n getAllStrings(): string[] {\n return this.entries.map((entry) => entry.text);\n }\n\n /**\n * Generate XML for the shared strings table\n */\n toXml(): string {\n const siElements: XmlNode[] = [];\n for (const entry of this.entries) {\n if (entry.node) {\n siElements.push(entry.node);\n } else {\n const str = entry.text;\n const tElement = createElement(\n 't',\n str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {},\n [createText(str)],\n );\n const siElement = createElement('si', {}, [tElement]);\n siElements.push(siElement);\n }\n }\n\n const totalCount = Math.max(this._totalCount, this.entries.length);\n const sst = createElement(\n 'sst',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n count: String(totalCount),\n uniqueCount: String(this.entries.length),\n },\n siElements,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([sst])}`;\n }\n}\n\ninterface SharedStringEntry {\n text: string;\n node?: XmlNode;\n}\n","import type { CellStyle, BorderType } from './types';\nimport { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';\n\n/**\n * Excel built-in number format IDs (0-163 are reserved).\n * These formats don't need to be defined in the numFmts element.\n */\nconst BUILTIN_NUM_FMTS: Map<string, number> = new Map([\n ['General', 0],\n ['0', 1],\n ['0.00', 2],\n ['#,##0', 3],\n ['#,##0.00', 4],\n ['0%', 9],\n ['0.00%', 10],\n ['0.00E+00', 11],\n ['# ?/?', 12],\n ['# ??/??', 13],\n ['mm-dd-yy', 14],\n ['d-mmm-yy', 15],\n ['d-mmm', 16],\n ['mmm-yy', 17],\n ['h:mm AM/PM', 18],\n ['h:mm:ss AM/PM', 19],\n ['h:mm', 20],\n ['h:mm:ss', 21],\n ['m/d/yy h:mm', 22],\n ['#,##0 ;(#,##0)', 37],\n ['#,##0 ;[Red](#,##0)', 38],\n ['#,##0.00;(#,##0.00)', 39],\n ['#,##0.00;[Red](#,##0.00)', 40],\n ['mm:ss', 45],\n ['[h]:mm:ss', 46],\n ['mmss.0', 47],\n ['##0.0E+0', 48],\n ['@', 49],\n]);\n\n/**\n * Reverse lookup: built-in format ID -> format code\n */\nconst BUILTIN_NUM_FMT_CODES: Map<number, string> = new Map(\n Array.from(BUILTIN_NUM_FMTS.entries()).map(([code, id]) => [id, code]),\n);\n\n/**\n * Normalize a color to ARGB format (8 hex chars).\n * Accepts: \"#RGB\", \"#RRGGBB\", \"RGB\", \"RRGGBB\", \"AARRGGBB\", \"#AARRGGBB\"\n */\nconst normalizeColor = (color: string): string => {\n let c = color.replace(/^#/, '').toUpperCase();\n\n // Handle shorthand 3-char format (e.g., \"FFF\" -> \"FFFFFF\")\n if (c.length === 3) {\n c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];\n }\n\n // Add alpha channel if not present (6 chars -> 8 chars)\n if (c.length === 6) {\n c = 'FF' + c;\n }\n\n return c;\n};\n\ninterface StyleColor {\n rgb?: string;\n theme?: string;\n tint?: string;\n indexed?: string;\n}\n\n/**\n * Manages the styles (xl/styles.xml)\n */\nexport class Styles {\n private _numFmts: Map<number, string> = new Map();\n private _fonts: StyleFont[] = [];\n private _fills: StyleFill[] = [];\n private _borders: StyleBorder[] = [];\n private _cellXfs: CellXf[] = []; // Cell formats (combined style index)\n private _xmlNodes: XmlNode[] | null = null;\n private _dirty = false;\n\n // Cache for style deduplication\n private _styleCache: Map<string, number> = new Map();\n private _styleObjectCache: Map<number, CellStyle> = new Map();\n\n /**\n * Generate a deterministic cache key for a style object.\n * More efficient than JSON.stringify as it avoids the overhead of\n * full JSON serialization and produces a consistent key regardless\n * of property order.\n */\n private _getStyleKey(style: CellStyle): string {\n // Use a delimiter that won't appear in values\n const SEP = '\\x00';\n\n // Build key from all style properties in a fixed order\n const parts: string[] = [\n style.bold ? '1' : '0',\n style.italic ? '1' : '0',\n style.underline === true ? '1' : style.underline === 'single' ? 's' : style.underline === 'double' ? 'd' : '0',\n style.strike ? '1' : '0',\n style.fontSize?.toString() ?? '',\n style.fontName ?? '',\n style.fontColor ?? '',\n style.fontColorTheme?.toString() ?? '',\n style.fontColorTint?.toString() ?? '',\n style.fontColorIndexed?.toString() ?? '',\n style.fill ?? '',\n style.fillTheme?.toString() ?? '',\n style.fillTint?.toString() ?? '',\n style.fillIndexed?.toString() ?? '',\n style.fillBgColor ?? '',\n style.fillBgTheme?.toString() ?? '',\n style.fillBgTint?.toString() ?? '',\n style.fillBgIndexed?.toString() ?? '',\n style.numberFormat ?? '',\n ];\n\n // Border properties\n if (style.border) {\n parts.push(style.border.top ?? '', style.border.bottom ?? '', style.border.left ?? '', style.border.right ?? '');\n } else {\n parts.push('', '', '', '');\n }\n\n // Alignment properties\n if (style.alignment) {\n parts.push(\n style.alignment.horizontal ?? '',\n style.alignment.vertical ?? '',\n style.alignment.wrapText ? '1' : '0',\n style.alignment.textRotation?.toString() ?? '',\n );\n } else {\n parts.push('', '', '0', '');\n }\n\n return parts.join(SEP);\n }\n\n /**\n * Parse styles from XML content\n */\n static parse(xml: string): Styles {\n const styles = new Styles();\n styles._xmlNodes = parseXml(xml);\n\n const styleSheet = findElement(styles._xmlNodes, 'styleSheet');\n if (!styleSheet) return styles;\n\n const children = getChildren(styleSheet, 'styleSheet');\n\n // Parse number formats\n const numFmts = findElement(children, 'numFmts');\n if (numFmts) {\n for (const child of getChildren(numFmts, 'numFmts')) {\n if ('numFmt' in child) {\n const id = parseInt(getAttr(child, 'numFmtId') || '0', 10);\n const code = getAttr(child, 'formatCode') || '';\n styles._numFmts.set(id, code);\n }\n }\n }\n\n // Parse fonts\n const fonts = findElement(children, 'fonts');\n if (fonts) {\n for (const child of getChildren(fonts, 'fonts')) {\n if ('font' in child) {\n styles._fonts.push(styles._parseFont(child));\n }\n }\n }\n\n // Parse fills\n const fills = findElement(children, 'fills');\n if (fills) {\n for (const child of getChildren(fills, 'fills')) {\n if ('fill' in child) {\n styles._fills.push(styles._parseFill(child));\n }\n }\n }\n\n // Parse borders\n const borders = findElement(children, 'borders');\n if (borders) {\n for (const child of getChildren(borders, 'borders')) {\n if ('border' in child) {\n styles._borders.push(styles._parseBorder(child));\n }\n }\n }\n\n // Parse cellXfs (cell formats)\n const cellXfs = findElement(children, 'cellXfs');\n if (cellXfs) {\n for (const child of getChildren(cellXfs, 'cellXfs')) {\n if ('xf' in child) {\n styles._cellXfs.push(styles._parseCellXf(child));\n }\n }\n }\n\n return styles;\n }\n\n /**\n * Create an empty styles object with defaults\n */\n static createDefault(): Styles {\n const styles = new Styles();\n\n // Default font (Calibri 11)\n styles._fonts.push({\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n size: 11,\n name: 'Calibri',\n color: undefined,\n });\n\n // Default fills (none and gray125 pattern are required)\n styles._fills.push({ type: 'none' });\n styles._fills.push({ type: 'gray125' });\n\n // Default border (none)\n styles._borders.push({});\n\n // Default cell format\n styles._cellXfs.push({\n fontId: 0,\n fillId: 0,\n borderId: 0,\n numFmtId: 0,\n });\n\n return styles;\n }\n\n private _parseFont(node: XmlNode): StyleFont {\n const font: StyleFont = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n };\n\n const children = getChildren(node, 'font');\n for (const child of children) {\n if ('b' in child) font.bold = true;\n if ('i' in child) font.italic = true;\n if ('u' in child) font.underline = true;\n if ('strike' in child) font.strike = true;\n if ('sz' in child) font.size = parseFloat(getAttr(child, 'val') || '11');\n if ('name' in child) font.name = getAttr(child, 'val');\n if ('color' in child) {\n const color: StyleColor = {};\n const rgb = getAttr(child, 'rgb');\n const theme = getAttr(child, 'theme');\n const tint = getAttr(child, 'tint');\n const indexed = getAttr(child, 'indexed');\n if (rgb) color.rgb = rgb;\n if (theme) color.theme = theme;\n if (tint) color.tint = tint;\n if (indexed) color.indexed = indexed;\n if (color.rgb || color.theme || color.tint || color.indexed) {\n font.color = color;\n }\n }\n }\n\n return font;\n }\n\n private _parseFill(node: XmlNode): StyleFill {\n const fill: StyleFill = { type: 'none' };\n const children = getChildren(node, 'fill');\n\n for (const child of children) {\n if ('patternFill' in child) {\n const pattern = getAttr(child, 'patternType');\n fill.type = pattern || 'none';\n\n const pfChildren = getChildren(child, 'patternFill');\n for (const pfChild of pfChildren) {\n if ('fgColor' in pfChild) {\n const color: StyleColor = {};\n const rgb = getAttr(pfChild, 'rgb');\n const theme = getAttr(pfChild, 'theme');\n const tint = getAttr(pfChild, 'tint');\n const indexed = getAttr(pfChild, 'indexed');\n if (rgb) color.rgb = rgb;\n if (theme) color.theme = theme;\n if (tint) color.tint = tint;\n if (indexed) color.indexed = indexed;\n if (color.rgb || color.theme || color.tint || color.indexed) {\n fill.fgColor = color;\n }\n }\n if ('bgColor' in pfChild) {\n const color: StyleColor = {};\n const rgb = getAttr(pfChild, 'rgb');\n const theme = getAttr(pfChild, 'theme');\n const tint = getAttr(pfChild, 'tint');\n const indexed = getAttr(pfChild, 'indexed');\n if (rgb) color.rgb = rgb;\n if (theme) color.theme = theme;\n if (tint) color.tint = tint;\n if (indexed) color.indexed = indexed;\n if (color.rgb || color.theme || color.tint || color.indexed) {\n fill.bgColor = color;\n }\n }\n }\n }\n }\n\n return fill;\n }\n\n private _parseBorder(node: XmlNode): StyleBorder {\n const border: StyleBorder = {};\n const children = getChildren(node, 'border');\n\n for (const child of children) {\n const style = getAttr(child, 'style') as BorderType | undefined;\n if ('left' in child && style) border.left = style;\n if ('right' in child && style) border.right = style;\n if ('top' in child && style) border.top = style;\n if ('bottom' in child && style) border.bottom = style;\n }\n\n return border;\n }\n\n private _parseCellXf(node: XmlNode): CellXf {\n return {\n fontId: parseInt(getAttr(node, 'fontId') || '0', 10),\n fillId: parseInt(getAttr(node, 'fillId') || '0', 10),\n borderId: parseInt(getAttr(node, 'borderId') || '0', 10),\n numFmtId: parseInt(getAttr(node, 'numFmtId') || '0', 10),\n alignment: this._parseAlignment(node),\n };\n }\n\n private _parseAlignment(node: XmlNode): AlignmentStyle | undefined {\n const children = getChildren(node, 'xf');\n const alignNode = findElement(children, 'alignment');\n if (!alignNode) return undefined;\n\n return {\n horizontal: getAttr(alignNode, 'horizontal') as AlignmentStyle['horizontal'],\n vertical: getAttr(alignNode, 'vertical') as AlignmentStyle['vertical'],\n wrapText: getAttr(alignNode, 'wrapText') === '1',\n textRotation: parseInt(getAttr(alignNode, 'textRotation') || '0', 10),\n };\n }\n\n /**\n * Get a style by index\n */\n getStyle(index: number): CellStyle {\n const cached = this._styleObjectCache.get(index);\n if (cached) return { ...cached };\n\n const xf = this._cellXfs[index];\n if (!xf) return {};\n\n const font = this._fonts[xf.fontId];\n const fill = this._fills[xf.fillId];\n const border = this._borders[xf.borderId];\n // Check custom formats first, then fall back to built-in format codes\n const numFmt = this._numFmts.get(xf.numFmtId) ?? BUILTIN_NUM_FMT_CODES.get(xf.numFmtId);\n\n const style: CellStyle = {};\n\n if (font) {\n if (font.bold) style.bold = true;\n if (font.italic) style.italic = true;\n if (font.underline) style.underline = true;\n if (font.strike) style.strike = true;\n if (font.size) style.fontSize = font.size;\n if (font.name) style.fontName = font.name;\n if (font.color?.rgb) style.fontColor = font.color.rgb;\n if (font.color?.theme) style.fontColorTheme = Number(font.color.theme);\n if (font.color?.tint) style.fontColorTint = Number(font.color.tint);\n if (font.color?.indexed) style.fontColorIndexed = Number(font.color.indexed);\n }\n\n if (fill && fill.fgColor) {\n if (fill.fgColor.rgb) style.fill = fill.fgColor.rgb;\n if (fill.fgColor.theme) style.fillTheme = Number(fill.fgColor.theme);\n if (fill.fgColor.tint) style.fillTint = Number(fill.fgColor.tint);\n if (fill.fgColor.indexed) style.fillIndexed = Number(fill.fgColor.indexed);\n }\n\n if (fill && fill.bgColor) {\n if (fill.bgColor.rgb) style.fillBgColor = fill.bgColor.rgb;\n if (fill.bgColor.theme) style.fillBgTheme = Number(fill.bgColor.theme);\n if (fill.bgColor.tint) style.fillBgTint = Number(fill.bgColor.tint);\n if (fill.bgColor.indexed) style.fillBgIndexed = Number(fill.bgColor.indexed);\n }\n\n if (border) {\n if (border.top || border.bottom || border.left || border.right) {\n style.border = {\n top: border.top,\n bottom: border.bottom,\n left: border.left,\n right: border.right,\n };\n }\n }\n\n if (numFmt) {\n style.numberFormat = numFmt;\n }\n\n if (xf.alignment) {\n style.alignment = {\n horizontal: xf.alignment.horizontal,\n vertical: xf.alignment.vertical,\n wrapText: xf.alignment.wrapText,\n textRotation: xf.alignment.textRotation,\n };\n }\n\n this._styleObjectCache.set(index, { ...style });\n return style;\n }\n\n /**\n * Create a style and return its index\n * Uses caching to deduplicate identical styles\n */\n createStyle(style: CellStyle): number {\n const key = this._getStyleKey(style);\n const cached = this._styleCache.get(key);\n if (cached !== undefined) {\n return cached;\n }\n\n this._dirty = true;\n\n // Create or find font\n const fontId = this._findOrCreateFont(style);\n\n // Create or find fill\n const fillId = this._findOrCreateFill(style);\n\n // Create or find border\n const borderId = this._findOrCreateBorder(style);\n\n // Create or find number format\n const numFmtId = style.numberFormat ? this._findOrCreateNumFmt(style.numberFormat) : 0;\n\n // Create cell format\n const xf: CellXf = {\n fontId,\n fillId,\n borderId,\n numFmtId,\n };\n\n if (style.alignment) {\n xf.alignment = {\n horizontal: style.alignment.horizontal,\n vertical: style.alignment.vertical,\n wrapText: style.alignment.wrapText,\n textRotation: style.alignment.textRotation,\n };\n }\n\n const index = this._cellXfs.length;\n this._cellXfs.push(xf);\n this._styleCache.set(key, index);\n this._styleObjectCache.set(index, { ...style });\n\n return index;\n }\n\n /**\n * Clone an existing style by index, optionally overriding fields.\n */\n cloneStyle(index: number, overrides: Partial<CellStyle> = {}): number {\n const baseStyle = this.getStyle(index);\n return this.createStyle({ ...baseStyle, ...overrides });\n }\n\n private _findOrCreateFont(style: CellStyle): number {\n const color = this._toStyleColor(\n style.fontColor,\n style.fontColorTheme,\n style.fontColorTint,\n style.fontColorIndexed,\n );\n const font: StyleFont = {\n bold: style.bold || false,\n italic: style.italic || false,\n underline: style.underline === true || style.underline === 'single' || style.underline === 'double',\n strike: style.strike || false,\n size: style.fontSize,\n name: style.fontName,\n color,\n };\n\n // Try to find existing font\n for (let i = 0; i < this._fonts.length; i++) {\n const f = this._fonts[i];\n if (\n f.bold === font.bold &&\n f.italic === font.italic &&\n f.underline === font.underline &&\n f.strike === font.strike &&\n f.size === font.size &&\n f.name === font.name &&\n this._colorsEqual(f.color, font.color)\n ) {\n return i;\n }\n }\n\n // Create new font\n this._fonts.push(font);\n return this._fonts.length - 1;\n }\n\n private _findOrCreateFill(style: CellStyle): number {\n const fgColor = this._toStyleColor(style.fill, style.fillTheme, style.fillTint, style.fillIndexed);\n const bgColor = this._toStyleColor(style.fillBgColor, style.fillBgTheme, style.fillBgTint, style.fillBgIndexed);\n\n if (!fgColor && !bgColor) return 0;\n\n // Try to find existing fill\n for (let i = 0; i < this._fills.length; i++) {\n const f = this._fills[i];\n if (this._colorsEqual(f.fgColor, fgColor) && this._colorsEqual(f.bgColor, bgColor)) {\n return i;\n }\n }\n\n // Create new fill\n this._fills.push({\n type: 'solid',\n fgColor: fgColor || undefined,\n bgColor: bgColor || undefined,\n });\n return this._fills.length - 1;\n }\n\n private _findOrCreateBorder(style: CellStyle): number {\n if (!style.border) return 0;\n\n const border: StyleBorder = {\n top: style.border.top,\n bottom: style.border.bottom,\n left: style.border.left,\n right: style.border.right,\n };\n\n // Try to find existing border\n for (let i = 0; i < this._borders.length; i++) {\n const b = this._borders[i];\n if (b.top === border.top && b.bottom === border.bottom && b.left === border.left && b.right === border.right) {\n return i;\n }\n }\n\n // Create new border\n this._borders.push(border);\n return this._borders.length - 1;\n }\n\n private _findOrCreateNumFmt(format: string): number {\n // Check built-in formats first (IDs 0-163)\n const builtinId = BUILTIN_NUM_FMTS.get(format);\n if (builtinId !== undefined) {\n return builtinId;\n }\n\n // Check if already exists in custom formats\n for (const [id, code] of this._numFmts) {\n if (code === format) return id;\n }\n\n // Create new custom format (IDs 164+)\n const existingIds = Array.from(this._numFmts.keys());\n const id = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 164;\n this._numFmts.set(id, format);\n return id;\n }\n\n /**\n * Get or create a number format ID for the given format string.\n * Returns built-in IDs (0-163) for standard formats, or creates custom IDs (164+).\n * @param format - The number format string (e.g., '0.00', '#,##0', '$#,##0.00')\n */\n getOrCreateNumFmtId(format: string): number {\n this._dirty = true;\n return this._findOrCreateNumFmt(format);\n }\n\n /**\n * Check if styles have been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Generate XML for styles\n */\n toXml(): string {\n const children: XmlNode[] = [];\n\n // Number formats\n if (this._numFmts.size > 0) {\n const numFmtNodes: XmlNode[] = [];\n for (const [id, code] of this._numFmts) {\n numFmtNodes.push(createElement('numFmt', { numFmtId: String(id), formatCode: code }, []));\n }\n children.push(createElement('numFmts', { count: String(numFmtNodes.length) }, numFmtNodes));\n }\n\n // Fonts\n const fontNodes: XmlNode[] = this._fonts.map((font) => this._buildFontNode(font));\n children.push(createElement('fonts', { count: String(fontNodes.length) }, fontNodes));\n\n // Fills\n const fillNodes: XmlNode[] = this._fills.map((fill) => this._buildFillNode(fill));\n children.push(createElement('fills', { count: String(fillNodes.length) }, fillNodes));\n\n // Borders\n const borderNodes: XmlNode[] = this._borders.map((border) => this._buildBorderNode(border));\n children.push(createElement('borders', { count: String(borderNodes.length) }, borderNodes));\n\n // Cell style xfs (required but we just add a default)\n children.push(\n createElement('cellStyleXfs', { count: '1' }, [\n createElement('xf', { numFmtId: '0', fontId: '0', fillId: '0', borderId: '0' }, []),\n ]),\n );\n\n // Cell xfs\n const xfNodes: XmlNode[] = this._cellXfs.map((xf) => this._buildXfNode(xf));\n children.push(createElement('cellXfs', { count: String(xfNodes.length) }, xfNodes));\n\n // Cell styles (required)\n children.push(\n createElement('cellStyles', { count: '1' }, [\n createElement('cellStyle', { name: 'Normal', xfId: '0', builtinId: '0' }, []),\n ]),\n );\n\n const styleSheet = createElement(\n 'styleSheet',\n { xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' },\n children,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([styleSheet])}`;\n }\n\n private _buildFontNode(font: StyleFont): XmlNode {\n const children: XmlNode[] = [];\n if (font.bold) children.push(createElement('b', {}, []));\n if (font.italic) children.push(createElement('i', {}, []));\n if (font.underline) children.push(createElement('u', {}, []));\n if (font.strike) children.push(createElement('strike', {}, []));\n if (font.size) children.push(createElement('sz', { val: String(font.size) }, []));\n if (font.color) {\n const attrs: Record<string, string> = {};\n if (font.color.rgb) attrs.rgb = normalizeColor(font.color.rgb);\n if (font.color.theme) attrs.theme = font.color.theme;\n if (font.color.tint) attrs.tint = font.color.tint;\n if (font.color.indexed) attrs.indexed = font.color.indexed;\n if (Object.keys(attrs).length > 0) {\n children.push(createElement('color', attrs, []));\n }\n }\n if (font.name) children.push(createElement('name', { val: font.name }, []));\n return createElement('font', {}, children);\n }\n\n private _buildFillNode(fill: StyleFill): XmlNode {\n const patternChildren: XmlNode[] = [];\n if (fill.fgColor) {\n const attrs: Record<string, string> = {};\n if (fill.fgColor.rgb) attrs.rgb = normalizeColor(fill.fgColor.rgb);\n if (fill.fgColor.theme) attrs.theme = fill.fgColor.theme;\n if (fill.fgColor.tint) attrs.tint = fill.fgColor.tint;\n if (fill.fgColor.indexed) attrs.indexed = fill.fgColor.indexed;\n if (Object.keys(attrs).length > 0) {\n patternChildren.push(createElement('fgColor', attrs, []));\n }\n // For solid fills, bgColor is required (indexed 64 = system background)\n if (fill.type === 'solid' && !fill.bgColor) {\n patternChildren.push(createElement('bgColor', { indexed: '64' }, []));\n }\n }\n if (fill.bgColor) {\n const attrs: Record<string, string> = {};\n if (fill.bgColor.rgb) attrs.rgb = normalizeColor(fill.bgColor.rgb);\n if (fill.bgColor.theme) attrs.theme = fill.bgColor.theme;\n if (fill.bgColor.tint) attrs.tint = fill.bgColor.tint;\n if (fill.bgColor.indexed) attrs.indexed = fill.bgColor.indexed;\n if (Object.keys(attrs).length > 0) {\n patternChildren.push(createElement('bgColor', attrs, []));\n }\n }\n const patternFill = createElement('patternFill', { patternType: fill.type || 'none' }, patternChildren);\n return createElement('fill', {}, [patternFill]);\n }\n\n private _toStyleColor(rgb?: string, theme?: number, tint?: number, indexed?: number): StyleColor | undefined {\n if (rgb) {\n return { rgb };\n }\n const color: StyleColor = {};\n if (theme !== undefined) color.theme = String(theme);\n if (tint !== undefined) color.tint = String(tint);\n if (indexed !== undefined) color.indexed = String(indexed);\n if (color.theme || color.tint || color.indexed) return color;\n return undefined;\n }\n\n private _colorsEqual(a?: StyleColor, b?: StyleColor): boolean {\n if (!a && !b) return true;\n if (!a || !b) return false;\n return a.rgb === b.rgb && a.theme === b.theme && a.tint === b.tint && a.indexed === b.indexed;\n }\n\n private _buildBorderNode(border: StyleBorder): XmlNode {\n const children: XmlNode[] = [];\n if (border.left) children.push(createElement('left', { style: border.left }, []));\n if (border.right) children.push(createElement('right', { style: border.right }, []));\n if (border.top) children.push(createElement('top', { style: border.top }, []));\n if (border.bottom) children.push(createElement('bottom', { style: border.bottom }, []));\n // Add empty elements if not present (required by Excel)\n if (!border.left) children.push(createElement('left', {}, []));\n if (!border.right) children.push(createElement('right', {}, []));\n if (!border.top) children.push(createElement('top', {}, []));\n if (!border.bottom) children.push(createElement('bottom', {}, []));\n children.push(createElement('diagonal', {}, []));\n return createElement('border', {}, children);\n }\n\n private _buildXfNode(xf: CellXf): XmlNode {\n const attrs: Record<string, string> = {\n numFmtId: String(xf.numFmtId),\n fontId: String(xf.fontId),\n fillId: String(xf.fillId),\n borderId: String(xf.borderId),\n };\n\n if (xf.fontId > 0) attrs.applyFont = '1';\n if (xf.fillId > 0) attrs.applyFill = '1';\n if (xf.borderId > 0) attrs.applyBorder = '1';\n if (xf.numFmtId > 0) attrs.applyNumberFormat = '1';\n\n const children: XmlNode[] = [];\n if (xf.alignment) {\n const alignAttrs: Record<string, string> = {};\n if (xf.alignment.horizontal) alignAttrs.horizontal = xf.alignment.horizontal;\n if (xf.alignment.vertical) alignAttrs.vertical = xf.alignment.vertical;\n if (xf.alignment.wrapText) alignAttrs.wrapText = '1';\n if (xf.alignment.textRotation) alignAttrs.textRotation = String(xf.alignment.textRotation);\n children.push(createElement('alignment', alignAttrs, []));\n attrs.applyAlignment = '1';\n }\n\n return createElement('xf', attrs, children);\n }\n}\n\n// Internal types for style components\ninterface StyleFont {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n size?: number;\n name?: string;\n color?: StyleColor;\n}\n\ninterface StyleFill {\n type: string;\n fgColor?: StyleColor;\n bgColor?: StyleColor;\n}\n\ninterface StyleBorder {\n top?: BorderType;\n bottom?: BorderType;\n left?: BorderType;\n right?: BorderType;\n}\n\ninterface CellXf {\n fontId: number;\n fillId: number;\n borderId: number;\n numFmtId: number;\n alignment?: AlignmentStyle;\n}\n\ninterface AlignmentStyle {\n horizontal?: 'left' | 'center' | 'right' | 'justify';\n vertical?: 'top' | 'middle' | 'bottom';\n wrapText?: boolean;\n textRotation?: number;\n}\n","import type { PivotCacheField, CellValue } from './types';\nimport type { Styles } from './styles';\nimport { createElement, stringifyXml, XmlNode } from './utils/xml';\n\n/**\n * Manages the pivot cache (definition and records) for a pivot table.\n * The cache stores source data metadata and cached values.\n */\nexport class PivotCache {\n private _cacheId: number;\n private _fileIndex: number;\n private _sourceSheet: string;\n private _sourceRange: string;\n private _fields: PivotCacheField[] = [];\n private _records: CellValue[][] = [];\n private _recordCount = 0;\n private _saveData = true;\n private _refreshOnLoad = true; // Default to true\n // Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>\n private _sharedItemsIndexMap: Map<number, Map<string, number>> = new Map();\n private _blankItemIndexMap: Map<number, number> = new Map();\n private _styles: Styles | null = null;\n\n constructor(cacheId: number, sourceSheet: string, sourceRange: string, fileIndex: number) {\n this._cacheId = cacheId;\n this._fileIndex = fileIndex;\n this._sourceSheet = sourceSheet;\n this._sourceRange = sourceRange;\n }\n\n /**\n * Set styles reference for number format resolution.\n * @internal\n */\n setStyles(styles: Styles): void {\n this._styles = styles;\n }\n\n\n /**\n * Get the cache ID\n */\n get cacheId(): number {\n return this._cacheId;\n }\n\n /**\n * Get the file index for this cache (used for file naming).\n */\n get fileIndex(): number {\n return this._fileIndex;\n }\n\n /**\n * Set refreshOnLoad option\n */\n set refreshOnLoad(value: boolean) {\n this._refreshOnLoad = value;\n }\n\n /**\n * Set saveData option\n */\n set saveData(value: boolean) {\n this._saveData = value;\n }\n\n /**\n * Get refreshOnLoad option\n */\n get refreshOnLoad(): boolean {\n return this._refreshOnLoad;\n }\n\n /**\n * Get saveData option\n */\n get saveData(): boolean {\n return this._saveData;\n }\n\n /**\n * Get the source sheet name\n */\n get sourceSheet(): string {\n return this._sourceSheet;\n }\n\n /**\n * Get the source range\n */\n get sourceRange(): string {\n return this._sourceRange;\n }\n\n /**\n * Get the full source reference (Sheet!Range)\n */\n get sourceRef(): string {\n return `${this._sourceSheet}!${this._sourceRange}`;\n }\n\n /**\n * Get the fields in this cache\n */\n get fields(): PivotCacheField[] {\n return this._fields;\n }\n\n /**\n * Get the number of data records\n */\n get recordCount(): number {\n return this._recordCount;\n }\n\n /**\n * Build the cache from source data.\n * @param headers - Array of column header names\n * @param data - 2D array of data rows (excluding headers)\n */\n buildFromData(headers: string[], data: CellValue[][]): void {\n this._recordCount = data.length;\n\n // Initialize fields from headers\n this._fields = headers.map((name, index) => ({\n name,\n index,\n isNumeric: true,\n isDate: false,\n hasBoolean: false,\n hasBlank: false,\n numFmtId: undefined,\n sharedItems: [],\n minValue: undefined,\n maxValue: undefined,\n minDate: undefined,\n maxDate: undefined,\n }));\n\n // Use Maps for unique value collection during analysis\n const sharedItemsMaps: Map<string, string>[] = this._fields.map(() => new Map<string, string>());\n\n // Analyze data to determine field types and collect unique values\n for (const row of data) {\n for (let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++) {\n const value = row[colIdx];\n const field = this._fields[colIdx];\n\n if (value === null || value === undefined) {\n field.hasBlank = true;\n continue;\n }\n\n if (typeof value === 'string') {\n field.isNumeric = false;\n const map = sharedItemsMaps[colIdx];\n if (!map.has(value)) {\n map.set(value, value);\n }\n } else if (typeof value === 'number') {\n if (field.isDate) {\n const d = this._excelSerialToDate(value);\n if (!field.minDate || d < field.minDate) {\n field.minDate = d;\n }\n if (!field.maxDate || d > field.maxDate) {\n field.maxDate = d;\n }\n } else {\n if (field.minValue === undefined || value < field.minValue) {\n field.minValue = value;\n }\n if (field.maxValue === undefined || value > field.maxValue) {\n field.maxValue = value;\n }\n }\n } else if (value instanceof Date) {\n field.isDate = true;\n field.isNumeric = false;\n if (!field.minDate || value < field.minDate) {\n field.minDate = value;\n }\n if (!field.maxDate || value > field.maxDate) {\n field.maxDate = value;\n }\n } else if (typeof value === 'boolean') {\n field.isNumeric = false;\n field.hasBoolean = true;\n }\n }\n }\n\n // Resolve number formats if styles are available\n if (this._styles) {\n const dateFmtId = this._styles.getOrCreateNumFmtId('mm-dd-yy');\n for (const field of this._fields) {\n if (field.isDate) {\n field.numFmtId = dateFmtId;\n }\n }\n }\n\n // Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation\n this._sharedItemsIndexMap.clear();\n this._blankItemIndexMap.clear();\n for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {\n const field = this._fields[colIdx];\n const map = sharedItemsMaps[colIdx];\n\n // Convert Map values to array (maintains insertion order in ES6+)\n field.sharedItems = Array.from(map.values());\n\n // Build reverse lookup Map: value -> index\n if (field.sharedItems.length > 0) {\n const indexMap = new Map<string, number>();\n for (let i = 0; i < field.sharedItems.length; i++) {\n indexMap.set(field.sharedItems[i], i);\n }\n this._sharedItemsIndexMap.set(colIdx, indexMap);\n\n if (field.hasBlank) {\n const blankIndex = field.sharedItems.length;\n this._blankItemIndexMap.set(colIdx, blankIndex);\n }\n }\n }\n\n // Store records\n this._records = data;\n }\n\n /**\n * Get field by name\n */\n getField(name: string): PivotCacheField | undefined {\n return this._fields.find((f) => f.name === name);\n }\n\n /**\n * Get field index by name\n */\n getFieldIndex(name: string): number {\n const field = this._fields.find((f) => f.name === name);\n return field ? field.index : -1;\n }\n\n /**\n * Generate the pivotCacheDefinition XML\n */\n toDefinitionXml(recordsRelId: string): string {\n const cacheFieldNodes: XmlNode[] = this._fields.map((field) => {\n const sharedItemsAttrs: Record<string, string> = {};\n const sharedItemChildren: XmlNode[] = [];\n\n if (field.sharedItems.length > 0) {\n // String field with shared items\n const total = field.hasBlank ? field.sharedItems.length + 1 : field.sharedItems.length;\n sharedItemsAttrs.count = String(total);\n sharedItemsAttrs.containsString = '1';\n\n if (field.hasBlank) {\n sharedItemsAttrs.containsBlank = '1';\n }\n\n for (const item of field.sharedItems) {\n sharedItemChildren.push(createElement('s', { v: item }, []));\n }\n if (field.hasBlank) {\n sharedItemChildren.push(createElement('m', {}, []));\n }\n } else if (field.isDate) {\n sharedItemsAttrs.containsSemiMixedTypes = '0';\n sharedItemsAttrs.containsString = '0';\n sharedItemsAttrs.containsDate = '1';\n sharedItemsAttrs.containsNonDate = '0';\n if (field.hasBlank) {\n sharedItemsAttrs.containsBlank = '1';\n }\n if (field.minDate) {\n sharedItemsAttrs.minDate = this._formatDate(field.minDate);\n }\n if (field.maxDate) {\n const maxDate = new Date(field.maxDate.getTime() + 24 * 60 * 60 * 1000);\n sharedItemsAttrs.maxDate = this._formatDate(maxDate);\n }\n } else if (field.isNumeric) {\n // Numeric field - use \"0\"/\"1\" for boolean attributes as Excel expects\n sharedItemsAttrs.containsSemiMixedTypes = '0';\n sharedItemsAttrs.containsString = '0';\n sharedItemsAttrs.containsNumber = '1';\n if (field.hasBlank) {\n sharedItemsAttrs.containsBlank = '1';\n }\n // Check if all values are integers\n if (field.minValue !== undefined && field.maxValue !== undefined) {\n const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);\n if (isInteger) {\n sharedItemsAttrs.containsInteger = '1';\n }\n sharedItemsAttrs.minValue = this._formatNumber(field.minValue);\n sharedItemsAttrs.maxValue = this._formatNumber(field.maxValue);\n }\n } else if (field.hasBoolean) {\n // Boolean-only field (no strings, no numbers)\n if (field.hasBlank) {\n sharedItemsAttrs.containsBlank = '1';\n }\n sharedItemsAttrs.count = field.hasBlank ? '3' : '2';\n sharedItemChildren.push(createElement('b', { v: '0' }, []));\n sharedItemChildren.push(createElement('b', { v: '1' }, []));\n if (field.hasBlank) {\n sharedItemChildren.push(createElement('m', {}, []));\n }\n } else if (field.hasBlank) {\n // Field that only contains blanks\n sharedItemsAttrs.containsBlank = '1';\n }\n\n const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);\n const cacheFieldAttrs: Record<string, string> = { name: field.name, numFmtId: String(field.numFmtId ?? 0) };\n return createElement('cacheField', cacheFieldAttrs, [sharedItemsNode]);\n });\n\n const cacheFieldsNode = createElement('cacheFields', { count: String(this._fields.length) }, cacheFieldNodes);\n\n const worksheetSourceNode = createElement(\n 'worksheetSource',\n { ref: this._sourceRange, sheet: this._sourceSheet },\n [],\n );\n const cacheSourceNode = createElement('cacheSource', { type: 'worksheet' }, [worksheetSourceNode]);\n\n // Build attributes - align with Excel expectations\n const definitionAttrs: Record<string, string> = {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n 'r:id': recordsRelId,\n };\n\n if (this._refreshOnLoad) {\n definitionAttrs.refreshOnLoad = '1';\n }\n\n definitionAttrs.refreshedBy = 'User';\n definitionAttrs.refreshedVersion = '8';\n definitionAttrs.minRefreshableVersion = '3';\n definitionAttrs.createdVersion = '8';\n if (!this._saveData) {\n definitionAttrs.saveData = '0';\n definitionAttrs.recordCount = '0';\n } else {\n definitionAttrs.recordCount = String(this._recordCount);\n }\n\n const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [cacheSourceNode, cacheFieldsNode]);\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([definitionNode])}`;\n }\n\n /**\n * Generate the pivotCacheRecords XML\n */\n toRecordsXml(): string {\n const recordNodes: XmlNode[] = [];\n\n for (const row of this._records) {\n const fieldNodes: XmlNode[] = [];\n\n for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {\n const value = colIdx < row.length ? row[colIdx] : null;\n\n if (value === null || value === undefined) {\n // Missing value\n const blankIndex = this._blankItemIndexMap.get(colIdx);\n if (blankIndex !== undefined) {\n fieldNodes.push(createElement('x', { v: String(blankIndex) }, []));\n } else {\n fieldNodes.push(createElement('m', {}, []));\n }\n } else if (typeof value === 'string') {\n // String value - use index into sharedItems via O(1) Map lookup\n const indexMap = this._sharedItemsIndexMap.get(colIdx);\n const idx = indexMap?.get(value);\n if (idx !== undefined) {\n fieldNodes.push(createElement('x', { v: String(idx) }, []));\n } else {\n // Direct string value (shouldn't happen if cache is built correctly)\n fieldNodes.push(createElement('s', { v: value }, []));\n }\n } else if (typeof value === 'number') {\n if (this._fields[colIdx]?.isDate) {\n const d = this._excelSerialToDate(value);\n fieldNodes.push(createElement('d', { v: this._formatDate(d) }, []));\n } else {\n fieldNodes.push(createElement('n', { v: String(value) }, []));\n }\n } else if (typeof value === 'boolean') {\n fieldNodes.push(createElement('b', { v: value ? '1' : '0' }, []));\n } else if (value instanceof Date) {\n fieldNodes.push(createElement('d', { v: this._formatDate(value) }, []));\n } else {\n // Unknown type, treat as missing\n fieldNodes.push(createElement('m', {}, []));\n }\n }\n\n recordNodes.push(createElement('r', {}, fieldNodes));\n }\n\n const recordsNode = createElement(\n 'pivotCacheRecords',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n count: String(this._recordCount),\n },\n recordNodes,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([recordsNode])}`;\n }\n\n private _formatDate(value: Date): string {\n return value.toISOString().replace(/\\.\\d{3}Z$/, '');\n }\n\n private _formatNumber(value: number): string {\n if (Number.isInteger(value)) {\n return String(value);\n }\n if (Math.abs(value) >= 1000000) {\n return value.toFixed(16).replace(/0+$/, '').replace(/\\.$/, '');\n }\n return String(value);\n }\n\n\n private _excelSerialToDate(serial: number): Date {\n // Excel epoch: December 31, 1899\n const EXCEL_EPOCH = Date.UTC(1899, 11, 31);\n const MS_PER_DAY = 24 * 60 * 60 * 1000;\n const adjusted = serial >= 60 ? serial - 1 : serial;\n const ms = Math.round(adjusted * MS_PER_DAY);\n return new Date(EXCEL_EPOCH + ms);\n }\n\n\n}\n","import type { AggregationType, PivotFieldAxis, PivotFieldFilter, PivotSortOrder, PivotValueConfig } from './types';\nimport type { Styles } from './styles';\nimport { PivotCache } from './pivot-cache';\nimport { createElement, stringifyXml, XmlNode } from './utils/xml';\n\n/**\n * Internal structure for tracking field assignments\n */\ninterface FieldAssignment {\n fieldName: string;\n fieldIndex: number;\n axis: PivotFieldAxis;\n aggregation?: AggregationType;\n displayName?: string;\n numFmtId?: number;\n sortOrder?: PivotSortOrder;\n filter?: PivotFieldFilter;\n}\n\n/**\n * Represents an Excel pivot table with a fluent API for configuration.\n */\nexport class PivotTable {\n private _name: string;\n private _cache: PivotCache;\n private _targetSheet: string;\n private _targetCell: string;\n private _targetRow: number;\n private _targetCol: number;\n\n private _rowFields: FieldAssignment[] = [];\n private _columnFields: FieldAssignment[] = [];\n private _valueFields: FieldAssignment[] = [];\n private _filterFields: FieldAssignment[] = [];\n private _fieldAssignments: Map<number, FieldAssignment> = new Map();\n\n private _pivotTableIndex: number;\n private _cacheFileIndex: number;\n private _styles: Styles | null = null;\n\n constructor(\n name: string,\n cache: PivotCache,\n targetSheet: string,\n targetCell: string,\n targetRow: number,\n targetCol: number,\n pivotTableIndex: number,\n cacheFileIndex: number,\n ) {\n this._name = name;\n this._cache = cache;\n this._targetSheet = targetSheet;\n this._targetCell = targetCell;\n this._targetRow = targetRow;\n this._targetCol = targetCol;\n this._pivotTableIndex = pivotTableIndex;\n this._cacheFileIndex = cacheFileIndex;\n }\n\n /**\n * Get the pivot table name\n */\n get name(): string {\n return this._name;\n }\n\n /**\n * Get the target sheet name\n */\n get targetSheet(): string {\n return this._targetSheet;\n }\n\n /**\n * Get the target cell address\n */\n get targetCell(): string {\n return this._targetCell;\n }\n\n /**\n * Get the pivot cache\n */\n get cache(): PivotCache {\n return this._cache;\n }\n\n /**\n * Get the pivot table index (for file naming)\n */\n get index(): number {\n return this._pivotTableIndex;\n }\n\n /**\n * Get the pivot cache file index used for rels.\n * @internal\n */\n get cacheFileIndex(): number {\n return this._cacheFileIndex;\n }\n\n /**\n * Set the styles reference for number format resolution\n * @internal\n */\n setStyles(styles: Styles): this {\n this._styles = styles;\n return this;\n }\n\n /**\n * Add a field to the row area\n * @param fieldName - Name of the source field (column header)\n */\n addRowField(fieldName: string): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n const assignment: FieldAssignment = {\n fieldName,\n fieldIndex,\n axis: 'row',\n };\n this._rowFields.push(assignment);\n this._fieldAssignments.set(fieldIndex, assignment);\n\n return this;\n }\n\n /**\n * Add a field to the column area\n * @param fieldName - Name of the source field (column header)\n */\n addColumnField(fieldName: string): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n const assignment: FieldAssignment = {\n fieldName,\n fieldIndex,\n axis: 'column',\n };\n this._columnFields.push(assignment);\n this._fieldAssignments.set(fieldIndex, assignment);\n\n return this;\n }\n\n /**\n * Add a field to the values area with aggregation.\n *\n * Supports two call signatures:\n * - Positional: `addValueField(fieldName, aggregation?, displayName?, numberFormat?)`\n * - Object: `addValueField({ field, aggregation?, name?, numberFormat? })`\n *\n * @example\n * // Positional arguments\n * pivot.addValueField('Sales', 'sum', 'Total Sales', '$#,##0.00');\n *\n * // Object form\n * pivot.addValueField({ field: 'Sales', aggregation: 'sum', name: 'Total Sales', numberFormat: '$#,##0.00' });\n */\n addValueField(config: PivotValueConfig): this;\n addValueField(fieldName: string, aggregation?: AggregationType, displayName?: string, numberFormat?: string): this;\n addValueField(\n fieldNameOrConfig: string | PivotValueConfig,\n aggregation: AggregationType = 'sum',\n displayName?: string,\n numberFormat?: string,\n ): this {\n // Normalize arguments to a common form\n let fieldName: string;\n let agg: AggregationType;\n let name: string | undefined;\n let format: string | undefined;\n\n if (typeof fieldNameOrConfig === 'object') {\n fieldName = fieldNameOrConfig.field;\n agg = fieldNameOrConfig.aggregation ?? 'sum';\n name = fieldNameOrConfig.name;\n format = fieldNameOrConfig.numberFormat;\n } else {\n fieldName = fieldNameOrConfig;\n agg = aggregation;\n name = displayName;\n format = numberFormat;\n }\n\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n const defaultName = `${agg.charAt(0).toUpperCase() + agg.slice(1)} of ${fieldName}`;\n\n // Resolve numFmtId immediately if format is provided and styles are available\n let numFmtId: number | undefined;\n if (format && this._styles) {\n numFmtId = this._styles.getOrCreateNumFmtId(format);\n }\n\n const assignment: FieldAssignment = {\n fieldName,\n fieldIndex,\n axis: 'value',\n aggregation: agg,\n displayName: name || defaultName,\n numFmtId,\n };\n this._valueFields.push(assignment);\n this._fieldAssignments.set(fieldIndex, assignment);\n\n return this;\n }\n\n /**\n * Add a field to the filter (page) area\n * @param fieldName - Name of the source field (column header)\n */\n addFilterField(fieldName: string): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n const assignment: FieldAssignment = {\n fieldName,\n fieldIndex,\n axis: 'filter',\n };\n this._filterFields.push(assignment);\n this._fieldAssignments.set(fieldIndex, assignment);\n\n return this;\n }\n\n /**\n * Set a sort order for a row or column field\n * @param fieldName - Name of the field to sort\n * @param order - Sort order ('asc' or 'desc')\n */\n sortField(fieldName: string, order: PivotSortOrder): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n const assignment = this._fieldAssignments.get(fieldIndex);\n if (!assignment) {\n throw new Error(`Field is not assigned to pivot table: ${fieldName}`);\n }\n if (assignment.axis !== 'row' && assignment.axis !== 'column') {\n throw new Error(`Sort is only supported for row or column fields: ${fieldName}`);\n }\n\n assignment.sortOrder = order;\n return this;\n }\n\n /**\n * Filter items for a row, column, or filter field\n * @param fieldName - Name of the field to filter\n * @param filter - Filter configuration with include or exclude list\n */\n filterField(fieldName: string, filter: PivotFieldFilter): this {\n const fieldIndex = this._cache.getFieldIndex(fieldName);\n if (fieldIndex < 0) {\n throw new Error(`Field not found in source data: ${fieldName}`);\n }\n\n const assignment = this._fieldAssignments.get(fieldIndex);\n if (!assignment) {\n throw new Error(`Field is not assigned to pivot table: ${fieldName}`);\n }\n\n if (filter.include && filter.exclude) {\n throw new Error('Cannot use both include and exclude in the same filter');\n }\n\n assignment.filter = filter;\n return this;\n }\n\n /**\n * Generate the pivotTableDefinition XML\n */\n toXml(): string {\n const children: XmlNode[] = [];\n\n // Calculate location (estimate based on fields)\n const locationRef = this._calculateLocationRef();\n\n // Calculate first data row/col offsets (1-based, relative to pivot table)\n // firstHeaderRow: row offset of column headers (usually 1)\n // firstDataRow: row offset where data starts (after filters and column headers)\n // firstDataCol: column offset where data starts (after row labels)\n const filterRowCount = this._filterFields.length > 0 ? this._filterFields.length + 1 : 0;\n const headerRows = this._columnFields.length > 0 ? 1 : 0;\n const firstDataRow = filterRowCount + headerRows + 1;\n const firstDataCol = this._rowFields.length > 0 ? this._rowFields.length : 1;\n\n const locationNode = createElement(\n 'location',\n {\n ref: locationRef,\n firstHeaderRow: String(filterRowCount + 1),\n firstDataRow: String(firstDataRow),\n firstDataCol: String(firstDataCol),\n },\n [],\n );\n children.push(locationNode);\n\n // Build pivotFields (one per source field)\n const pivotFieldNodes: XmlNode[] = [];\n for (const cacheField of this._cache.fields) {\n const fieldNode = this._buildPivotFieldNode(cacheField.index);\n pivotFieldNodes.push(fieldNode);\n }\n children.push(createElement('pivotFields', { count: String(pivotFieldNodes.length) }, pivotFieldNodes));\n\n // Row fields\n if (this._rowFields.length > 0) {\n const rowFieldNodes = this._rowFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));\n children.push(createElement('rowFields', { count: String(rowFieldNodes.length) }, rowFieldNodes));\n\n // Row items\n const rowItemNodes = this._buildRowItems();\n children.push(createElement('rowItems', { count: String(rowItemNodes.length) }, rowItemNodes));\n }\n\n // Column fields\n if (this._columnFields.length > 0) {\n const colFieldNodes = this._columnFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));\n // If we have multiple value fields, add -2 to indicate where \"Values\" header goes\n if (this._valueFields.length > 1) {\n colFieldNodes.push(createElement('field', { x: '-2' }, []));\n }\n children.push(createElement('colFields', { count: String(colFieldNodes.length) }, colFieldNodes));\n\n // Column items - need to account for multiple value fields\n const colItemNodes = this._buildColItems();\n children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));\n } else if (this._valueFields.length > 1) {\n // If no column fields but we have multiple values, need colFields with -2 (data field indicator)\n children.push(createElement('colFields', { count: '1' }, [createElement('field', { x: '-2' }, [])]));\n\n // Column items for each value field\n const colItemNodes: XmlNode[] = [];\n for (let i = 0; i < this._valueFields.length; i++) {\n colItemNodes.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));\n }\n children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));\n } else if (this._valueFields.length === 1) {\n // Single value field - just add a single column item\n children.push(createElement('colItems', { count: '1' }, [createElement('i', {}, [])]));\n }\n\n // Page (filter) fields\n if (this._filterFields.length > 0) {\n const pageFieldNodes = this._filterFields.map((f) =>\n createElement('pageField', { fld: String(f.fieldIndex), hier: '-1' }, []),\n );\n children.push(createElement('pageFields', { count: String(pageFieldNodes.length) }, pageFieldNodes));\n }\n\n // Data fields (values)\n if (this._valueFields.length > 0) {\n const dataFieldNodes = this._valueFields.map((f) => {\n const attrs: Record<string, string> = {\n name: f.displayName || f.fieldName,\n fld: String(f.fieldIndex),\n baseField: '0',\n baseItem: '0',\n subtotal: f.aggregation || 'sum',\n };\n\n if (f.numFmtId !== undefined) {\n attrs.numFmtId = String(f.numFmtId);\n }\n\n return createElement('dataField', attrs, []);\n });\n children.push(createElement('dataFields', { count: String(dataFieldNodes.length) }, dataFieldNodes));\n }\n\n // Pivot table style\n children.push(\n createElement(\n 'pivotTableStyleInfo',\n {\n name: 'PivotStyleMedium9',\n showRowHeaders: '1',\n showColHeaders: '1',\n showRowStripes: '0',\n showColStripes: '0',\n showLastColumn: '1',\n },\n [],\n ),\n );\n\n const pivotTableNode = createElement(\n 'pivotTableDefinition',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n name: this._name,\n cacheId: String(this._cache.cacheId),\n applyNumberFormats: '1',\n applyBorderFormats: '0',\n applyFontFormats: '0',\n applyPatternFormats: '0',\n applyAlignmentFormats: '0',\n applyWidthHeightFormats: '1',\n dataCaption: 'Values',\n updatedVersion: '8',\n minRefreshableVersion: '3',\n useAutoFormatting: '1',\n rowGrandTotals: '1',\n colGrandTotals: '1',\n itemPrintTitles: '1',\n createdVersion: '8',\n indent: '0',\n outline: '1',\n outlineData: '1',\n multipleFieldFilters: '0',\n },\n children,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([pivotTableNode])}`;\n }\n\n /**\n * Build a pivotField node for a given field index\n */\n private _buildPivotFieldNode(fieldIndex: number): XmlNode {\n const attrs: Record<string, string> = {};\n const children: XmlNode[] = [];\n\n // Check if this field is assigned to an axis\n const rowField = this._rowFields.find((f) => f.fieldIndex === fieldIndex);\n const colField = this._columnFields.find((f) => f.fieldIndex === fieldIndex);\n const filterField = this._filterFields.find((f) => f.fieldIndex === fieldIndex);\n const valueField = this._valueFields.find((f) => f.fieldIndex === fieldIndex);\n\n // Get the assignment to check for sort/filter options\n const assignment = rowField || colField || filterField;\n\n if (rowField) {\n attrs.axis = 'axisRow';\n attrs.showAll = '0';\n\n // Add sort order if specified\n if (rowField.sortOrder) {\n attrs.sortType = rowField.sortOrder === 'asc' ? 'ascending' : 'descending';\n }\n\n // Add items for shared values\n const cacheField = this._cache.fields[fieldIndex];\n if (cacheField && cacheField.sharedItems.length > 0) {\n const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);\n children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));\n }\n } else if (colField) {\n attrs.axis = 'axisCol';\n attrs.showAll = '0';\n\n // Add sort order if specified\n if (colField.sortOrder) {\n attrs.sortType = colField.sortOrder === 'asc' ? 'ascending' : 'descending';\n }\n\n const cacheField = this._cache.fields[fieldIndex];\n if (cacheField && cacheField.sharedItems.length > 0) {\n const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);\n children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));\n }\n } else if (filterField) {\n attrs.axis = 'axisPage';\n attrs.showAll = '0';\n const cacheField = this._cache.fields[fieldIndex];\n if (cacheField && cacheField.sharedItems.length > 0) {\n const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);\n children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));\n }\n } else if (valueField) {\n attrs.dataField = '1';\n attrs.showAll = '0';\n } else {\n attrs.showAll = '0';\n }\n\n return createElement('pivotField', attrs, children);\n }\n\n /**\n * Build item nodes for a pivot field, with optional filtering\n */\n private _buildItemNodes(sharedItems: string[], filter?: PivotFieldFilter): XmlNode[] {\n const itemNodes: XmlNode[] = [];\n\n for (let i = 0; i < sharedItems.length; i++) {\n const itemValue = sharedItems[i];\n const itemAttrs: Record<string, string> = { x: String(i) };\n\n // Check if this item should be hidden\n if (filter) {\n let hidden = false;\n if (filter.exclude && filter.exclude.includes(itemValue)) {\n hidden = true;\n } else if (filter.include && !filter.include.includes(itemValue)) {\n hidden = true;\n }\n if (hidden) {\n itemAttrs.h = '1';\n }\n }\n\n itemNodes.push(createElement('item', itemAttrs, []));\n }\n\n // Add default subtotal item\n itemNodes.push(createElement('item', { t: 'default' }, []));\n\n return itemNodes;\n }\n\n /**\n * Build row items based on unique values in row fields\n */\n private _buildRowItems(): XmlNode[] {\n const items: XmlNode[] = [];\n\n if (this._rowFields.length === 0) return items;\n\n // Get unique values from first row field\n const firstRowField = this._rowFields[0];\n const cacheField = this._cache.fields[firstRowField.fieldIndex];\n\n if (cacheField && cacheField.sharedItems.length > 0) {\n for (let i = 0; i < cacheField.sharedItems.length; i++) {\n items.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));\n }\n }\n\n // Add grand total row\n items.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));\n\n return items;\n }\n\n /**\n * Build column items based on unique values in column fields\n */\n private _buildColItems(): XmlNode[] {\n const items: XmlNode[] = [];\n\n if (this._columnFields.length === 0) return items;\n\n // Get unique values from first column field\n const firstColField = this._columnFields[0];\n const cacheField = this._cache.fields[firstColField.fieldIndex];\n\n if (cacheField && cacheField.sharedItems.length > 0) {\n if (this._valueFields.length > 1) {\n // Multiple value fields - need nested items for each column value + value field combination\n for (let colIdx = 0; colIdx < cacheField.sharedItems.length; colIdx++) {\n for (let valIdx = 0; valIdx < this._valueFields.length; valIdx++) {\n const xNodes: XmlNode[] = [\n createElement('x', colIdx === 0 ? {} : { v: String(colIdx) }, []),\n createElement('x', valIdx === 0 ? {} : { v: String(valIdx) }, []),\n ];\n items.push(createElement('i', {}, xNodes));\n }\n }\n } else {\n // Single value field - simple column items\n for (let i = 0; i < cacheField.sharedItems.length; i++) {\n items.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));\n }\n }\n }\n\n // Add grand total column(s)\n if (this._valueFields.length > 1) {\n // Grand total for each value field\n for (let valIdx = 0; valIdx < this._valueFields.length; valIdx++) {\n const xNodes: XmlNode[] = [\n createElement('x', {}, []),\n createElement('x', valIdx === 0 ? {} : { v: String(valIdx) }, []),\n ];\n items.push(createElement('i', { t: 'grand' }, xNodes));\n }\n } else {\n items.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));\n }\n\n return items;\n }\n\n /**\n * Calculate the location reference for the pivot table output\n */\n private _calculateLocationRef(): string {\n // Estimate output size based on fields\n const numRows = this._estimateRowCount();\n const numCols = this._estimateColCount();\n\n const startRow = this._targetRow;\n const startCol = this._targetCol;\n const endRow = startRow + numRows - 1;\n const endCol = startCol + numCols - 1;\n\n return `${this._colToLetter(startCol)}${startRow}:${this._colToLetter(endCol)}${endRow}`;\n }\n\n /**\n * Estimate number of rows in pivot table output\n */\n private _estimateRowCount(): number {\n let count = 1; // Header row\n\n // Add filter area rows\n count += this._filterFields.length;\n\n // Add row labels (unique values in row fields)\n if (this._rowFields.length > 0) {\n const firstRowField = this._rowFields[0];\n const cacheField = this._cache.fields[firstRowField.fieldIndex];\n count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total\n } else {\n count += 1; // At least one data row\n }\n\n return Math.max(count, 3);\n }\n\n /**\n * Estimate number of columns in pivot table output\n */\n private _estimateColCount(): number {\n let count = 0;\n\n // Row label columns\n count += Math.max(this._rowFields.length, 1);\n\n // Column labels (unique values in column fields)\n if (this._columnFields.length > 0) {\n const firstColField = this._columnFields[0];\n const cacheField = this._cache.fields[firstColField.fieldIndex];\n count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total\n } else {\n // Value columns\n count += Math.max(this._valueFields.length, 1);\n }\n\n return Math.max(count, 2);\n }\n\n /**\n * Convert 0-based column index to letter (A, B, ..., Z, AA, etc.)\n */\n private _colToLetter(col: number): string {\n let result = '';\n let n = col;\n while (n >= 0) {\n result = String.fromCharCode((n % 26) + 65) + result;\n n = Math.floor(n / 26) - 1;\n }\n return result;\n }\n}\n","import { readFile, writeFile } from 'fs/promises';\nimport type {\n SheetDefinition,\n Relationship,\n PivotTableConfig,\n CellValue,\n SheetFromDataConfig,\n ColumnConfig,\n RichCellValue,\n DateHandling,\n} from './types';\nimport { Worksheet } from './worksheet';\nimport { SharedStrings } from './shared-strings';\nimport { Styles } from './styles';\nimport { PivotTable } from './pivot-table';\nimport { PivotCache } from './pivot-cache';\nimport { readZip, writeZip, readZipText, writeZipText, ZipFiles } from './utils/zip';\nimport { parseAddress, parseRange, toAddress } from './utils/address';\nimport { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';\n\n/**\n * Represents an Excel workbook (.xlsx file)\n */\nexport class Workbook {\n private _files: ZipFiles = new Map();\n private _sheets: Map<string, Worksheet> = new Map();\n private _sheetDefs: SheetDefinition[] = [];\n private _relationships: Relationship[] = [];\n private _sharedStrings: SharedStrings;\n private _styles: Styles;\n private _dirty = false;\n\n // Pivot table support\n private _pivotTables: PivotTable[] = [];\n private _pivotCaches: PivotCache[] = [];\n private _nextCacheId = 5;\n private _nextCacheFileIndex = 1;\n\n // Table support\n private _nextTableId = 1;\n\n // Date serialization handling\n private _dateHandling: DateHandling = 'jsDate';\n\n private _locale = 'fr-FR';\n\n private constructor() {\n this._sharedStrings = new SharedStrings();\n this._styles = Styles.createDefault();\n }\n\n /**\n * Load a workbook from a file path\n */\n static async fromFile(path: string): Promise<Workbook> {\n const data = await readFile(path);\n return Workbook.fromBuffer(new Uint8Array(data));\n }\n\n /**\n * Load a workbook from a buffer\n */\n static async fromBuffer(data: Uint8Array): Promise<Workbook> {\n const workbook = new Workbook();\n workbook._files = await readZip(data);\n\n // Parse workbook.xml for sheet definitions\n const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');\n if (workbookXml) {\n workbook._parseWorkbook(workbookXml);\n }\n\n // Parse relationships\n const relsXml = readZipText(workbook._files, 'xl/_rels/workbook.xml.rels');\n if (relsXml) {\n workbook._parseRelationships(relsXml);\n }\n\n // Parse shared strings\n const sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml');\n if (sharedStringsXml) {\n workbook._sharedStrings = SharedStrings.parse(sharedStringsXml);\n }\n\n // Parse styles\n const stylesXml = readZipText(workbook._files, 'xl/styles.xml');\n if (stylesXml) {\n workbook._styles = Styles.parse(stylesXml);\n }\n\n return workbook;\n }\n\n /**\n * Create a new empty workbook\n */\n static create(): Workbook {\n const workbook = new Workbook();\n workbook._dirty = true;\n\n return workbook;\n }\n\n /**\n * Get sheet names\n */\n get sheetNames(): string[] {\n return this._sheetDefs.map((s) => s.name);\n }\n\n /**\n * Get number of sheets\n */\n get sheetCount(): number {\n return this._sheetDefs.length;\n }\n\n /**\n * Get shared strings table\n */\n get sharedStrings(): SharedStrings {\n return this._sharedStrings;\n }\n\n /**\n * Get styles\n */\n get styles(): Styles {\n return this._styles;\n }\n\n /**\n * Get the workbook date handling strategy.\n */\n get dateHandling(): DateHandling {\n return this._dateHandling;\n }\n\n /**\n * Set the workbook date handling strategy.\n */\n set dateHandling(value: DateHandling) {\n this._dateHandling = value;\n }\n\n /**\n * Get the workbook locale for formatting.\n */\n get locale(): string {\n return this._locale;\n }\n\n /**\n * Set the workbook locale for formatting.\n */\n set locale(value: string) {\n this._locale = value;\n }\n\n /**\n * Get the next unique table ID for this workbook.\n * Table IDs must be unique across all worksheets.\n * @internal\n */\n getNextTableId(): number {\n return this._nextTableId++;\n }\n\n /**\n * Get a worksheet by name or index\n */\n sheet(nameOrIndex: string | number): Worksheet {\n let def: SheetDefinition | undefined;\n\n if (typeof nameOrIndex === 'number') {\n def = this._sheetDefs[nameOrIndex];\n } else {\n def = this._sheetDefs.find((s) => s.name === nameOrIndex);\n }\n\n if (!def) {\n throw new Error(`Sheet not found: ${nameOrIndex}`);\n }\n\n // Return cached worksheet if available\n if (this._sheets.has(def.name)) {\n return this._sheets.get(def.name)!;\n }\n\n // Load worksheet\n const worksheet = new Worksheet(this, def.name);\n\n // Find the relationship to get the file path\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n const sheetXml = readZipText(this._files, sheetPath);\n if (sheetXml) {\n worksheet.parse(sheetXml);\n }\n }\n\n this._sheets.set(def.name, worksheet);\n return worksheet;\n }\n\n /**\n * Add a new worksheet\n */\n addSheet(name: string, index?: number): Worksheet {\n // Check for duplicate name\n if (this._sheetDefs.some((s) => s.name === name)) {\n throw new Error(`Sheet already exists: ${name}`);\n }\n\n this._dirty = true;\n\n // Generate new sheet ID and relationship ID\n const sheetId = Math.max(0, ...this._sheetDefs.map((s) => s.sheetId)) + 1;\n const rId = `rId${Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1}`;\n\n const def: SheetDefinition = { name, sheetId, rId };\n\n // Add relationship\n this._relationships.push({\n id: rId,\n type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',\n target: `worksheets/sheet${sheetId}.xml`,\n });\n\n // Insert at index or append\n if (index !== undefined && index >= 0 && index < this._sheetDefs.length) {\n this._sheetDefs.splice(index, 0, def);\n } else {\n this._sheetDefs.push(def);\n }\n\n // Create worksheet\n const worksheet = new Worksheet(this, name);\n this._sheets.set(name, worksheet);\n\n return worksheet;\n }\n\n /**\n * Delete a worksheet by name or index\n */\n deleteSheet(nameOrIndex: string | number): void {\n let index: number;\n\n if (typeof nameOrIndex === 'number') {\n index = nameOrIndex;\n } else {\n index = this._sheetDefs.findIndex((s) => s.name === nameOrIndex);\n }\n\n if (index < 0 || index >= this._sheetDefs.length) {\n throw new Error(`Sheet not found: ${nameOrIndex}`);\n }\n\n if (this._sheetDefs.length === 1) {\n throw new Error('Cannot delete the last sheet');\n }\n\n this._dirty = true;\n\n const def = this._sheetDefs[index];\n this._sheetDefs.splice(index, 1);\n this._sheets.delete(def.name);\n\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n this._files.delete(sheetPath);\n }\n\n // Remove relationship\n const relIndex = this._relationships.findIndex((r) => r.id === def.rId);\n if (relIndex >= 0) {\n this._relationships.splice(relIndex, 1);\n }\n }\n\n /**\n * Rename a worksheet\n */\n renameSheet(oldName: string, newName: string): void {\n const def = this._sheetDefs.find((s) => s.name === oldName);\n if (!def) {\n throw new Error(`Sheet not found: ${oldName}`);\n }\n\n if (this._sheetDefs.some((s) => s.name === newName)) {\n throw new Error(`Sheet already exists: ${newName}`);\n }\n\n this._dirty = true;\n\n // Update cached worksheet\n const worksheet = this._sheets.get(oldName);\n if (worksheet) {\n worksheet.name = newName;\n this._sheets.delete(oldName);\n this._sheets.set(newName, worksheet);\n }\n\n def.name = newName;\n }\n\n /**\n * Copy a worksheet\n */\n copySheet(sourceName: string, newName: string): Worksheet {\n const source = this.sheet(sourceName);\n const copy = this.addSheet(newName);\n\n // Copy all cells\n for (const [address, cell] of source.cells) {\n const newCell = copy.cell(address);\n newCell.value = cell.value;\n if (cell.formula) {\n newCell.formula = cell.formula;\n }\n if (cell.styleIndex !== undefined) {\n newCell.styleIndex = cell.styleIndex;\n }\n }\n\n // Copy column widths\n for (const [col, width] of source.getColumnWidths()) {\n copy.setColumnWidth(col, width);\n }\n\n // Copy row heights\n for (const [row, height] of source.getRowHeights()) {\n copy.setRowHeight(row, height);\n }\n\n // Copy frozen panes\n const frozen = source.getFrozenPane();\n if (frozen) {\n copy.freezePane(frozen.row, frozen.col);\n }\n\n // Copy merged cells\n for (const mergedRange of source.mergedCells) {\n copy.mergeCells(mergedRange);\n }\n\n // Copy tables\n for (const table of source.tables) {\n const tableName = this._createUniqueTableName(table.name, newName);\n const newTable = copy.createTable({\n name: tableName,\n range: table.baseRange,\n totalRow: table.hasTotalRow,\n headerRow: table.hasHeaderRow,\n style: table.style,\n });\n\n if (!table.hasAutoFilter) {\n newTable.setAutoFilter(false);\n }\n\n if (table.hasTotalRow) {\n for (const columnName of table.columns) {\n const fn = table.getTotalFunction(columnName);\n if (fn) {\n newTable.setTotalFunction(columnName, fn);\n }\n }\n }\n }\n\n return copy;\n }\n\n private _createUniqueTableName(base: string, sheetName: string): string {\n const normalizedSheet = sheetName.replace(/[^A-Za-z0-9_.]/g, '_');\n const sanitizedBase = this._sanitizeTableName(`${base}_${normalizedSheet || 'Sheet'}`);\n let candidate = sanitizedBase;\n let counter = 1;\n\n while (this._hasTableName(candidate)) {\n candidate = `${sanitizedBase}_${counter++}`;\n }\n\n return candidate;\n }\n\n private _sanitizeTableName(name: string): string {\n let result = name.replace(/[^A-Za-z0-9_.]/g, '_');\n if (!/^[A-Za-z_]/.test(result)) {\n result = `_${result}`;\n }\n if (result.length === 0) {\n result = 'Table';\n }\n return result;\n }\n\n private _hasTableName(name: string): boolean {\n for (const sheetName of this.sheetNames) {\n const ws = this.sheet(sheetName);\n for (const table of ws.tables) {\n if (table.name === name) return true;\n }\n }\n return false;\n }\n\n /**\n * Create a new worksheet from an array of objects.\n *\n * The first row contains headers (object keys or custom column headers),\n * and subsequent rows contain the object values.\n *\n * @param config - Configuration for the sheet creation\n * @returns The created Worksheet\n *\n * @example\n * ```typescript\n * const data = [\n * { name: 'Alice', age: 30, city: 'Paris' },\n * { name: 'Bob', age: 25, city: 'London' },\n * { name: 'Charlie', age: 35, city: 'Berlin' },\n * ];\n *\n * // Simple usage - all object keys become columns\n * const sheet = wb.addSheetFromData({\n * name: 'People',\n * data: data,\n * });\n *\n * // With custom column configuration\n * const sheet2 = wb.addSheetFromData({\n * name: 'People Custom',\n * data: data,\n * columns: [\n * { key: 'name', header: 'Full Name' },\n * { key: 'age', header: 'Age (years)' },\n * ],\n * });\n *\n * // With rich cell values (value, formula, style)\n * const dataWithFormulas = [\n * { product: 'Widget', price: 10, qty: 5, total: { formula: 'B2*C2', style: { bold: true } } },\n * { product: 'Gadget', price: 20, qty: 3, total: { formula: 'B3*C3', style: { bold: true } } },\n * ];\n * const sheet3 = wb.addSheetFromData({\n * name: 'With Formulas',\n * data: dataWithFormulas,\n * });\n * ```\n */\n addSheetFromData<T extends object>(config: SheetFromDataConfig<T>): Worksheet {\n const { name, data, columns, headerStyle = true, startCell = 'A1' } = config;\n\n if (!data?.length) return this.addSheet(name);\n\n // Create the new sheet\n const sheet = this.addSheet(name);\n\n // Parse start cell\n const startAddr = parseAddress(startCell);\n let startRow = startAddr.row;\n const startCol = startAddr.col;\n\n // Determine columns to use\n const columnConfigs: ColumnConfig<T>[] = columns ?? this._inferColumns(data[0]);\n\n // Write header row\n for (let colIdx = 0; colIdx < columnConfigs.length; colIdx++) {\n const colConfig = columnConfigs[colIdx];\n const headerText = colConfig.header ?? String(colConfig.key);\n const cell = sheet.cell(startRow, startCol + colIdx);\n cell.value = headerText;\n\n // Apply header style if enabled\n if (headerStyle) {\n cell.style = { bold: true };\n }\n }\n\n // Move to data rows\n startRow++;\n\n // Write data rows\n for (let rowIdx = 0; rowIdx < data.length; rowIdx++) {\n const rowData = data[rowIdx];\n\n for (let colIdx = 0; colIdx < columnConfigs.length; colIdx++) {\n const colConfig = columnConfigs[colIdx];\n const value = rowData[colConfig.key];\n const cell = sheet.cell(startRow + rowIdx, startCol + colIdx);\n\n // Check if value is a rich cell definition\n if (this._isRichCellValue(value)) {\n const richValue = value as RichCellValue;\n if (richValue.value !== undefined) cell.value = richValue.value;\n if (richValue.formula !== undefined) cell.formula = richValue.formula;\n if (richValue.style !== undefined) cell.style = richValue.style;\n } else {\n // Convert value to CellValue\n cell.value = this._toCellValue(value);\n }\n\n // Apply column style if defined (merged with cell style)\n if (colConfig.style) {\n cell.style = { ...cell.style, ...colConfig.style };\n }\n }\n }\n\n return sheet;\n }\n\n /**\n * Check if a value is a rich cell value object with value, formula, or style fields\n */\n private _isRichCellValue(value: unknown): value is RichCellValue {\n if (value === null || value === undefined) {\n return false;\n }\n if (typeof value !== 'object' || value instanceof Date) {\n return false;\n }\n // Check if it has at least one of the rich cell properties\n const obj = value as Record<string, unknown>;\n return 'value' in obj || 'formula' in obj || 'style' in obj;\n }\n\n /**\n * Infer column configuration from the first data object\n */\n private _inferColumns<T extends object>(sample: T): ColumnConfig<T>[] {\n return (Object.keys(sample) as (keyof T)[]).map((key) => ({\n key,\n }));\n }\n\n /**\n * Convert an unknown value to a CellValue\n */\n private _toCellValue(value: unknown): CellValue {\n if (value === null || value === undefined) {\n return null;\n }\n if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {\n return value;\n }\n if (value instanceof Date) {\n return value;\n }\n if (typeof value === 'object' && 'error' in value) {\n return value as CellValue;\n }\n // Convert other types to string\n return String(value);\n }\n\n /**\n * Create a pivot table from source data.\n *\n * @param config - Pivot table configuration\n * @returns PivotTable instance for fluent configuration\n *\n * @example\n * ```typescript\n * const pivot = wb.createPivotTable({\n * name: 'SalesPivot',\n * source: 'DataSheet!A1:D100',\n * target: 'PivotSheet!A3',\n * });\n *\n * pivot\n * .addRowField('Region')\n * .addColumnField('Product')\n * .addValueField('Sales', 'sum', 'Total Sales');\n * ```\n */\n createPivotTable(config: PivotTableConfig): PivotTable {\n this._dirty = true;\n\n // Parse source reference (Sheet!Range)\n const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);\n\n // Parse target reference\n const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);\n\n // Ensure target sheet exists\n if (!this._sheetDefs.some((s) => s.name === targetSheet)) {\n this.addSheet(targetSheet);\n }\n\n // Parse target cell address\n const targetAddr = parseAddress(targetCell);\n\n // Get source worksheet and extract data\n const sourceWs = this.sheet(sourceSheet);\n const { headers, data } = this._extractSourceData(sourceWs, sourceRange);\n\n // Create pivot cache\n const cacheId = this._nextCacheId++;\n const cacheFileIndex = this._nextCacheFileIndex++;\n const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);\n cache.setStyles(this._styles);\n cache.buildFromData(headers, data);\n // refreshOnLoad defaults to true; only disable if explicitly set to false\n if (config.refreshOnLoad === false) {\n cache.refreshOnLoad = false;\n }\n // saveData defaults to true; only disable if explicitly set to false\n if (config.saveData === false) {\n cache.saveData = false;\n }\n this._pivotCaches.push(cache);\n\n // Create pivot table\n const pivotTableIndex = this._pivotTables.length + 1;\n const pivotTable = new PivotTable(\n config.name,\n cache,\n targetSheet,\n targetCell,\n targetAddr.row + 1, // Convert to 1-based\n targetAddr.col,\n pivotTableIndex,\n cacheFileIndex,\n );\n\n // Set styles reference for number format resolution\n pivotTable.setStyles(this._styles);\n\n this._pivotTables.push(pivotTable);\n\n return pivotTable;\n }\n\n /**\n * Parse a sheet reference like \"Sheet1!A1:D100\" into sheet name and range\n */\n private _parseSheetRef(ref: string): { sheetName: string; range: string } {\n const match = ref.match(/^(.+?)!(.+)$/);\n if (!match) {\n throw new Error(`Invalid reference format: ${ref}. Expected \"SheetName!Range\"`);\n }\n return { sheetName: match[1], range: match[2] };\n }\n\n /**\n * Extract headers and data from a source range\n */\n private _extractSourceData(sheet: Worksheet, rangeStr: string): { headers: string[]; data: CellValue[][] } {\n const range = parseRange(rangeStr);\n const headers: string[] = [];\n const data: CellValue[][] = [];\n\n // First row is headers\n for (let col = range.start.col; col <= range.end.col; col++) {\n const cell = sheet.cell(toAddress(range.start.row, col));\n headers.push(String(cell.value ?? `Column${col + 1}`));\n }\n\n // Remaining rows are data\n for (let row = range.start.row + 1; row <= range.end.row; row++) {\n const rowData: CellValue[] = [];\n for (let col = range.start.col; col <= range.end.col; col++) {\n const cell = sheet.cell(toAddress(row, col));\n rowData.push(cell.value);\n }\n data.push(rowData);\n }\n\n return { headers, data };\n }\n\n\n /**\n * Save the workbook to a file\n */\n async toFile(path: string): Promise<void> {\n const buffer = await this.toBuffer();\n await writeFile(path, buffer);\n }\n\n /**\n * Save the workbook to a buffer\n */\n async toBuffer(): Promise<Uint8Array> {\n // Update files map with modified content\n this._updateFiles();\n\n // Write ZIP\n return writeZip(this._files);\n }\n\n private _parseWorkbook(xml: string): void {\n const parsed = parseXml(xml);\n const workbook = findElement(parsed, 'workbook');\n if (!workbook) return;\n\n const children = getChildren(workbook, 'workbook');\n const sheets = findElement(children, 'sheets');\n if (!sheets) return;\n\n for (const child of getChildren(sheets, 'sheets')) {\n if ('sheet' in child) {\n const name = getAttr(child, 'name');\n const sheetId = getAttr(child, 'sheetId');\n const rId = getAttr(child, 'r:id');\n\n if (name && sheetId && rId) {\n this._sheetDefs.push({\n name,\n sheetId: parseInt(sheetId, 10),\n rId,\n });\n }\n }\n }\n }\n\n private _parseRelationships(xml: string): void {\n const parsed = parseXml(xml);\n const rels = findElement(parsed, 'Relationships');\n if (!rels) return;\n\n for (const child of getChildren(rels, 'Relationships')) {\n if ('Relationship' in child) {\n const id = getAttr(child, 'Id');\n const type = getAttr(child, 'Type');\n const target = getAttr(child, 'Target');\n\n if (id && type && target) {\n this._relationships.push({ id, type, target });\n }\n }\n }\n }\n\n private _updateFiles(): void {\n const relationshipInfo = this._buildRelationshipInfo();\n\n // Update workbook.xml\n this._updateWorkbookXml(relationshipInfo.pivotCacheRelIds);\n\n // Update relationships\n this._updateRelationshipsXml(relationshipInfo.relNodes);\n\n // Update content types\n this._updateContentTypes();\n\n // Update shared strings if modified\n if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {\n writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());\n }\n\n // Update styles if modified or if file doesn't exist yet\n if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {\n writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());\n }\n\n // Update worksheets (needed for pivot table targets)\n for (const [name, worksheet] of this._sheets) {\n if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {\n const def = this._sheetDefs.find((s) => s.name === name);\n if (def) {\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n writeZipText(this._files, sheetPath, worksheet.toXml());\n }\n }\n }\n }\n\n // Update pivot tables\n if (this._pivotTables.length > 0) {\n this._updatePivotTableFiles();\n }\n\n // Update tables (sets table rel IDs for tableParts)\n this._updateTableFiles();\n\n // Update worksheets to align tableParts with relationship IDs\n for (const [name, worksheet] of this._sheets) {\n if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {\n const def = this._sheetDefs.find((s) => s.name === name);\n if (def) {\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n writeZipText(this._files, sheetPath, worksheet.toXml());\n }\n }\n }\n }\n }\n\n private _updateWorkbookXml(pivotCacheRelIds: Map<number, string>): void {\n const sheetNodes: XmlNode[] = this._sheetDefs.map((def) =>\n createElement('sheet', { name: def.name, sheetId: String(def.sheetId), 'r:id': def.rId }, []),\n );\n\n const sheetsNode = createElement('sheets', {}, sheetNodes);\n\n const children: XmlNode[] = [sheetsNode];\n\n // Add pivot caches if any\n if (this._pivotCaches.length > 0) {\n const pivotCacheNodes: XmlNode[] = this._pivotCaches.map((cache) => {\n const cacheRelId = pivotCacheRelIds.get(cache.cacheId);\n if (!cacheRelId) {\n throw new Error(`Missing pivot cache relationship ID for cache ${cache.cacheId}`);\n }\n return createElement('pivotCache', { cacheId: String(cache.cacheId), 'r:id': cacheRelId }, []);\n });\n children.push(createElement('pivotCaches', {}, pivotCacheNodes));\n }\n\n const workbookNode = createElement(\n 'workbook',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n },\n children,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([workbookNode])}`;\n writeZipText(this._files, 'xl/workbook.xml', xml);\n }\n\n private _updateRelationshipsXml(relNodes: XmlNode[]): void {\n const relsNode = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n relNodes,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([relsNode])}`;\n writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);\n }\n\n private _buildRelationshipInfo(): { relNodes: XmlNode[]; pivotCacheRelIds: Map<number, string> } {\n const relNodes: XmlNode[] = this._relationships.map((rel) =>\n createElement('Relationship', { Id: rel.id, Type: rel.type, Target: rel.target }, []),\n );\n\n const reservedRelIds = new Set<string>(relNodes.map((node) => getAttr(node, 'Id') || '').filter(Boolean));\n let nextRelId = Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;\n\n const allocateRelId = (): string => {\n while (reservedRelIds.has(`rId${nextRelId}`)) {\n nextRelId++;\n }\n const id = `rId${nextRelId}`;\n nextRelId++;\n reservedRelIds.add(id);\n return id;\n };\n\n // Add shared strings relationship if needed\n if (this._sharedStrings.count > 0) {\n const hasSharedStrings = this._relationships.some(\n (r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',\n );\n if (!hasSharedStrings) {\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: allocateRelId(),\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',\n Target: 'sharedStrings.xml',\n },\n [],\n ),\n );\n }\n }\n\n // Add styles relationship if needed\n const hasStyles = this._relationships.some(\n (r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',\n );\n if (!hasStyles) {\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: allocateRelId(),\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',\n Target: 'styles.xml',\n },\n [],\n ),\n );\n }\n\n // Add pivot cache relationships\n const pivotCacheRelIds = new Map<number, string>();\n for (const cache of this._pivotCaches) {\n const id = allocateRelId();\n pivotCacheRelIds.set(cache.cacheId, id);\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: id,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',\n Target: `pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,\n },\n [],\n ),\n );\n }\n\n return { relNodes, pivotCacheRelIds };\n }\n\n private _updateContentTypes(): void {\n const types: XmlNode[] = [\n createElement(\n 'Default',\n { Extension: 'rels', ContentType: 'application/vnd.openxmlformats-package.relationships+xml' },\n [],\n ),\n createElement('Default', { Extension: 'xml', ContentType: 'application/xml' }, []),\n createElement(\n 'Override',\n {\n PartName: '/xl/workbook.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml',\n },\n [],\n ),\n createElement(\n 'Override',\n {\n PartName: '/xl/styles.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml',\n },\n [],\n ),\n ];\n\n // Add shared strings if present\n if (this._sharedStrings.count > 0) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: '/xl/sharedStrings.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml',\n },\n [],\n ),\n );\n }\n\n // Add worksheets\n for (const def of this._sheetDefs) {\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/${rel.target}`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml',\n },\n [],\n ),\n );\n }\n }\n\n // Add pivot cache definitions and records\n for (const cache of this._pivotCaches) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml',\n },\n [],\n ),\n );\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml',\n },\n [],\n ),\n );\n }\n\n // Add pivot tables\n for (const pivotTable of this._pivotTables) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotTables/pivotTable${pivotTable.index}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml',\n },\n [],\n ),\n );\n }\n\n // Add tables\n let tableIndex = 1;\n for (const def of this._sheetDefs) {\n const worksheet = this._sheets.get(def.name);\n if (worksheet) {\n for (let i = 0; i < worksheet.tables.length; i++) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/tables/table${tableIndex}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml',\n },\n [],\n ),\n );\n tableIndex++;\n }\n }\n }\n\n const existingTypesXml = readZipText(this._files, '[Content_Types].xml');\n const existingKeys = new Set(\n types\n .map((t) => {\n if ('Default' in t) {\n const a = t[':@'] as Record<string, string> | undefined;\n return `Default:${a?.['@_Extension'] || ''}`;\n }\n if ('Override' in t) {\n const a = t[':@'] as Record<string, string> | undefined;\n return `Override:${a?.['@_PartName'] || ''}`;\n }\n return '';\n })\n .filter(Boolean),\n );\n if (existingTypesXml) {\n const parsed = parseXml(existingTypesXml);\n const typesElement = findElement(parsed, 'Types');\n if (typesElement) {\n const existingNodes = getChildren(typesElement, 'Types');\n for (const node of existingNodes) {\n if ('Default' in node || 'Override' in node) {\n const type = 'Default' in node ? 'Default' : 'Override';\n const attrs = node[':@'] as Record<string, string> | undefined;\n const key =\n type === 'Default'\n ? `Default:${attrs?.['@_Extension'] || ''}`\n : `Override:${attrs?.['@_PartName'] || ''}`;\n if (!existingKeys.has(key)) {\n types.push(node);\n existingKeys.add(key);\n }\n }\n }\n }\n }\n\n const typesNode = createElement(\n 'Types',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types' },\n types,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([typesNode])}`;\n writeZipText(this._files, '[Content_Types].xml', xml);\n\n // Also ensure _rels/.rels exists\n const rootRelsXml = readZipText(this._files, '_rels/.rels');\n if (!rootRelsXml) {\n const rootRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n [\n createElement(\n 'Relationship',\n {\n Id: 'rId1',\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',\n Target: 'xl/workbook.xml',\n },\n [],\n ),\n ],\n );\n writeZipText(\n this._files,\n '_rels/.rels',\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([rootRels])}`,\n );\n }\n }\n\n /**\n * Generate all pivot table related files\n */\n private _updatePivotTableFiles(): void {\n // Track which sheets have pivot tables for their .rels files\n const sheetPivotTables: Map<string, PivotTable[]> = new Map();\n\n for (const pivotTable of this._pivotTables) {\n const sheetName = pivotTable.targetSheet;\n if (!sheetPivotTables.has(sheetName)) {\n sheetPivotTables.set(sheetName, []);\n }\n sheetPivotTables.get(sheetName)!.push(pivotTable);\n }\n\n // Generate pivot cache files\n for (let i = 0; i < this._pivotCaches.length; i++) {\n const cache = this._pivotCaches[i];\n\n // Pivot cache definition\n const definitionPath = `xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`;\n writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));\n\n // Pivot cache records\n const recordsPath = `xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`;\n writeZipText(this._files, recordsPath, cache.toRecordsXml());\n\n // Pivot cache definition relationships (link to records)\n const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cache.fileIndex}.xml.rels`;\n const cacheRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n [\n createElement(\n 'Relationship',\n {\n Id: 'rId1',\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',\n Target: `pivotCacheRecords${cache.fileIndex}.xml`,\n },\n [],\n ),\n ],\n );\n writeZipText(\n this._files,\n cacheRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([cacheRels])}`,\n );\n }\n\n // Generate pivot table files\n for (let i = 0; i < this._pivotTables.length; i++) {\n const pivotTable = this._pivotTables[i];\n const ptIdx = pivotTable.index;\n\n // Pivot table definition\n const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;\n writeZipText(this._files, ptPath, pivotTable.toXml());\n\n // Pivot table relationships (link to cache definition)\n const cacheIdx = pivotTable.cacheFileIndex;\n const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;\n const ptRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n [\n createElement(\n 'Relationship',\n {\n Id: 'rId1',\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',\n Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`,\n },\n [],\n ),\n ],\n );\n writeZipText(\n this._files,\n ptRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([ptRels])}`,\n );\n }\n\n // Generate worksheet relationships for pivot tables\n for (const [sheetName, pivotTables] of sheetPivotTables) {\n const def = this._sheetDefs.find((s) => s.name === sheetName);\n if (!def) continue;\n\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (!rel) continue;\n\n // Extract sheet file name from target path\n const sheetFileName = rel.target.split('/').pop();\n const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;\n\n const existingRelsXml = readZipText(this._files, sheetRelsPath);\n let relNodes: XmlNode[] = [];\n let nextRelId = 1;\n const reservedRelIds = new Set<string>();\n\n if (existingRelsXml) {\n const parsed = parseXml(existingRelsXml);\n const relsElement = findElement(parsed, 'Relationships');\n if (relsElement) {\n const existingRelNodes = getChildren(relsElement, 'Relationships');\n for (const relNode of existingRelNodes) {\n if ('Relationship' in relNode) {\n relNodes.push(relNode);\n const id = getAttr(relNode, 'Id');\n if (id) {\n reservedRelIds.add(id);\n const idNum = parseInt(id.replace('rId', ''), 10);\n if (idNum >= nextRelId) {\n nextRelId = idNum + 1;\n }\n }\n }\n }\n }\n }\n\n const allocateRelId = (): string => {\n while (reservedRelIds.has(`rId${nextRelId}`)) {\n nextRelId++;\n }\n const id = `rId${nextRelId}`;\n nextRelId++;\n reservedRelIds.add(id);\n return id;\n };\n\n for (const pt of pivotTables) {\n const target = `../pivotTables/pivotTable${pt.index}.xml`;\n const existing = relNodes.some(\n (node) =>\n getAttr(node, 'Type') ===\n 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' &&\n getAttr(node, 'Target') === target,\n );\n if (existing) continue;\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: allocateRelId(),\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',\n Target: target,\n },\n [],\n ),\n );\n }\n\n const sheetRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n relNodes,\n );\n writeZipText(\n this._files,\n sheetRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([sheetRels])}`,\n );\n }\n }\n\n /**\n * Generate all table related files\n */\n private _updateTableFiles(): void {\n // Collect all tables with their global indices\n let globalTableIndex = 1;\n const sheetTables: Map<string, { table: import('./table').Table; globalIndex: number }[]> = new Map();\n\n for (const def of this._sheetDefs) {\n const worksheet = this._sheets.get(def.name);\n if (!worksheet) continue;\n\n const tables = worksheet.tables;\n if (tables.length === 0) continue;\n\n const tableInfos: { table: import('./table').Table; globalIndex: number }[] = [];\n for (const table of tables) {\n tableInfos.push({ table, globalIndex: globalTableIndex });\n globalTableIndex++;\n }\n sheetTables.set(def.name, tableInfos);\n }\n\n // Generate table files\n for (const [, tableInfos] of sheetTables) {\n for (const { table, globalIndex } of tableInfos) {\n const tablePath = `xl/tables/table${globalIndex}.xml`;\n writeZipText(this._files, tablePath, table.toXml());\n }\n }\n\n // Generate worksheet relationships for tables\n for (const [sheetName, tableInfos] of sheetTables) {\n const def = this._sheetDefs.find((s) => s.name === sheetName);\n if (!def) continue;\n\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (!rel) continue;\n\n // Extract sheet file name from target path\n const sheetFileName = rel.target.split('/').pop();\n const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;\n\n // Check if there are already pivot table relationships for this sheet\n const existingRelsXml = readZipText(this._files, sheetRelsPath);\n let nextRelId = 1;\n const relNodes: XmlNode[] = [];\n const reservedRelIds = new Set<string>();\n\n if (existingRelsXml) {\n // Parse existing rels and find max rId\n const parsed = parseXml(existingRelsXml);\n const relsElement = findElement(parsed, 'Relationships');\n if (relsElement) {\n const existingRelNodes = getChildren(relsElement, 'Relationships');\n for (const relNode of existingRelNodes) {\n if ('Relationship' in relNode) {\n relNodes.push(relNode);\n const id = getAttr(relNode, 'Id');\n if (id) {\n reservedRelIds.add(id);\n const idNum = parseInt(id.replace('rId', ''), 10);\n if (idNum >= nextRelId) {\n nextRelId = idNum + 1;\n }\n }\n }\n }\n }\n }\n\n const allocateRelId = (): string => {\n while (reservedRelIds.has(`rId${nextRelId}`)) {\n nextRelId++;\n }\n const id = `rId${nextRelId}`;\n nextRelId++;\n reservedRelIds.add(id);\n return id;\n };\n\n // Add table relationships\n const tableRelIds: string[] = [];\n for (const { globalIndex } of tableInfos) {\n const target = `../tables/table${globalIndex}.xml`;\n const existing = relNodes.some(\n (node) =>\n getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&\n getAttr(node, 'Target') === target,\n );\n if (existing) {\n const existingRel = relNodes.find(\n (node) =>\n getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&\n getAttr(node, 'Target') === target,\n );\n const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;\n tableRelIds.push(existingId ?? allocateRelId());\n continue;\n }\n const id = allocateRelId();\n tableRelIds.push(id);\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: id,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',\n Target: target,\n },\n [],\n ),\n );\n }\n\n const worksheet = this._sheets.get(sheetName);\n if (worksheet) {\n worksheet.setTableRelIds(tableRelIds);\n }\n\n const sheetRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n relNodes,\n );\n writeZipText(\n this._files,\n sheetRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([sheetRels])}`,\n );\n }\n }\n}\n","import type { CellAddress, RangeAddress } from '../types';\n\n/**\n * Converts a column index (0-based) to Excel column letters (A, B, ..., Z, AA, AB, ...)\n * @param col - 0-based column index\n * @returns Column letter(s)\n */\nexport const colToLetter = (col: number): string => {\n let result = '';\n let n = col;\n while (n >= 0) {\n result = String.fromCharCode((n % 26) + 65) + result;\n n = Math.floor(n / 26) - 1;\n }\n return result;\n};\n\n/**\n * Converts Excel column letters to a 0-based column index\n * @param letters - Column letter(s) like 'A', 'B', 'AA'\n * @returns 0-based column index\n */\nexport const letterToCol = (letters: string): number => {\n const upper = letters.toUpperCase();\n let col = 0;\n for (let i = 0; i < upper.length; i++) {\n col = col * 26 + (upper.charCodeAt(i) - 64);\n }\n return col - 1;\n};\n\n/**\n * Parses an Excel cell address (e.g., 'A1', '$B$2') to row/col indices\n * @param address - Cell address string\n * @returns CellAddress with 0-based row and col\n */\nexport const parseAddress = (address: string): CellAddress => {\n // Remove $ signs for absolute references\n const clean = address.replace(/\\$/g, '');\n const match = clean.match(/^([A-Z]+)(\\d+)$/i);\n if (!match) {\n throw new Error(`Invalid cell address: ${address}`);\n }\n const rowNumber = +match[2];\n if (rowNumber <= 0) throw new Error(`Invalid cell address: ${address}`);\n\n const col = letterToCol(match[1].toUpperCase());\n const row = rowNumber - 1; // Convert to 0-based\n return { row, col };\n};\n\n/**\n * Converts row/col indices to an Excel cell address\n * @param row - 0-based row index\n * @param col - 0-based column index\n * @returns Cell address string like 'A1'\n */\nexport const toAddress = (row: number, col: number): string => {\n return `${colToLetter(col)}${row + 1}`;\n};\n\n/**\n * Parses an Excel range (e.g., 'A1:B10') to start/end addresses\n * @param range - Range string\n * @returns RangeAddress with start and end\n */\nexport const parseRange = (range: string): RangeAddress => {\n const parts = range.split(':');\n if (parts.length === 1) {\n // Single cell range\n const addr = parseAddress(parts[0]);\n return { start: addr, end: addr };\n }\n if (parts.length !== 2) {\n throw new Error(`Invalid range: ${range}`);\n }\n return {\n start: parseAddress(parts[0]),\n end: parseAddress(parts[1]),\n };\n};\n\n/**\n * Converts a RangeAddress to a range string\n * @param range - RangeAddress object\n * @returns Range string like 'A1:B10'\n */\nexport const toRange = (range: RangeAddress): string => {\n const start = toAddress(range.start.row, range.start.col);\n const end = toAddress(range.end.row, range.end.col);\n if (start === end) {\n return start;\n }\n return `${start}:${end}`;\n};\n\n/**\n * Normalizes a range so start is always top-left and end is bottom-right\n */\nexport const normalizeRange = (range: RangeAddress): RangeAddress => {\n return {\n start: {\n row: Math.min(range.start.row, range.end.row),\n col: Math.min(range.start.col, range.end.col),\n },\n end: {\n row: Math.max(range.start.row, range.end.row),\n col: Math.max(range.start.col, range.end.col),\n },\n };\n};\n\n/**\n * Checks if an address is within a range\n */\nexport const isInRange = (addr: CellAddress, range: RangeAddress): boolean => {\n const norm = normalizeRange(range);\n return (\n addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col\n );\n};\n"],"names":["__cell.Cell"],"mappings":"AAAA;AACA;AACA;AACO,KAAA,SAAA,+BAAA,IAAA,UAAA,SAAA;AACP;AACA;AACA;AACO,UAAA,SAAA;AACP,WAAA,SAAA;AACA;AACO,KAAA,SAAA;AACP;AACA;AACA;AACO,KAAA,QAAA;AACP;AACA;AACA;AACO,KAAA,YAAA;AACP;AACA;AACA;AACO,UAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAA,WAAA;AACA,gBAAA,SAAA;AACA;AACA;AACO,UAAA,WAAA;AACP,UAAA,UAAA;AACA,aAAA,UAAA;AACA,WAAA,UAAA;AACA,YAAA,UAAA;AACA;AACO,KAAA,UAAA;AACA,UAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,YAAA;AACP,WAAA,WAAA;AACA,SAAA,WAAA;AACA;AACA;AACA;AACA;AACO,UAAA,QAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAiBA;AACA;AACA;AACO,KAAA,eAAA;AACP;AACA;AACA;AACO,KAAA,cAAA;AACP;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA,kBAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAA,IAAA;AACA;AACA,cAAA,IAAA;AACA;AACA;AACA;AACA;AACO,KAAA,cAAA;AACP;AACA;AACA;AACO,UAAA,mBAAA,oBAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA,cAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,YAAA,KAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA,YAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,aAAA;AACP;AACA,YAAA,SAAA;AACA;AACA;AACA;AACA,YAAA,SAAA;AACA;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAA,gBAAA;AACA;AACA;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,kBAAA;AACP;AACA;AACA;AACO,UAAA,iBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mBAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/SA;AACA;AACA;AACO,cAAA,IAAA;AACP;AACA;AACA;AACA;AACA;AACA,2BAAA,SAAA,mCAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAA,QAAA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,mBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAA,IAAA;AACA;AACA;AACA;AACA;AACA,yBAAA,IAAA;AACA;;AC5FA;AACA;AACA;AACO,cAAA,KAAA;AACP;AACA;AACA,2BAAA,SAAA,SAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA,QAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA,KAAA,MAAA,CAAA,QAAA,KAAA,SAAA,CAAmCA,IAAgB;AACnD;AACA;AACA;AACA,YAAA,SAAA,CAAsBA,IAAgB;AACtC;;ACzDA;AACA;AACA;AACO,cAAA,KAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2BAAA,SAAA,UAAA,WAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,gBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6CAAA,kBAAA;AACA;AACA;AACA;AACA,0CAAA,kBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oBAAA,OAAA,CAAA,gBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/GA;AACA;AACA;AACO,cAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,QAAA;AACA;AACA;AACA;AACA,oBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAA,IAAA;AACA;AACA;AACA;AACA,kEAAA,IAAA;AACA;AACA;AACA;AACA,6BAAA,KAAA;AACA,+EAAA,KAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,GAAA,SAAA,IAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,KAAA;AACA;AACA;AACA;AACA;AACA,uBAAA,GAAA;AACA;AACA;AACA;AACA;AACA,qBAAA,GAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAA,WAAA,GAAA,KAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAA,MAAA,SAAA,SAAA,YAAA,iBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACtMA;AACA;AACA;AACA;AACO,cAAA,aAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+BAAA,aAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9CA;AACA;AACA;AACO,cAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+BAAA,MAAA;AACA;AACA;AACA;AACA,4BAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAAA,SAAA;AACA;AACA;AACA;AACA;AACA,uBAAA,SAAA;AACA;AACA;AACA;AACA,0CAAA,OAAA,CAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrEA;AACA;AACA;AACA;AACO,cAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAAA,SAAA;AACA;AACA;AACA;AACA,4BAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3FA;AACA;AACA;AACO,cAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qCAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,gBAAA;AACA,mDAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wCAAA,cAAA;AACA;AACA;AACA;AACA;AACA;AACA,2CAAA,gBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9HA;AACA;AACA;AACO,cAAA,QAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mCAAA,OAAA,CAAA,QAAA;AACA;AACA;AACA;AACA,4BAAA,UAAA,GAAA,OAAA,CAAA,QAAA;AACA;AACA;AACA;AACA,qBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yBAAA,aAAA;AACA;AACA;AACA;AACA,kBAAA,MAAA;AACA;AACA;AACA;AACA,wBAAA,YAAA;AACA;AACA;AACA;AACA,4BAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yCAAA,SAAA;AACA;AACA;AACA;AACA,4CAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oDAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAAA,mBAAA,MAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAAA,gBAAA,GAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,OAAA;AACA;AACA;AACA;AACA,gBAAA,OAAA,CAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACjMA;AACA;AACA;AACA;AACA;AACO,cAAA,YAAA,uBAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACO,cAAA,UAAA,qBAAA,YAAA;AACP;AACA;AACA;AACA;AACA;AACO,cAAA,OAAA,UAAA,YAAA;;;;"}
1
+ {"version":3,"file":"index.d.cts","sources":["../src/types.ts","../src/cell.ts","../src/range.ts","../src/table.ts","../src/worksheet.ts","../src/shared-strings.ts","../src/styles.ts","../src/pivot-table.ts","../src/workbook.ts","../src/utils/address.ts"],"sourcesContent":["/**\n * Cell value types - what a cell can contain\n */\nexport type CellValue = number | string | boolean | Date | null | CellError;\n\n/**\n * Represents an Excel error value\n */\nexport interface CellError {\n error: ErrorType;\n}\n\nexport type ErrorType = '#NULL!' | '#DIV/0!' | '#VALUE!' | '#REF!' | '#NAME?' | '#NUM!' | '#N/A' | '#GETTING_DATA';\n\n/**\n * Discriminator for cell content type\n */\nexport type CellType = 'number' | 'string' | 'boolean' | 'date' | 'error' | 'empty';\n\n/**\n * Date handling strategy when serializing cell values.\n */\nexport type DateHandling = 'jsDate' | 'excelSerial' | 'isoString';\n\n/**\n * Style definition for cells\n */\nexport interface CellStyle {\n bold?: boolean;\n italic?: boolean;\n underline?: boolean | 'single' | 'double';\n strike?: boolean;\n fontSize?: number;\n fontName?: string;\n fontColor?: string;\n fontColorTheme?: number;\n fontColorTint?: number;\n fontColorIndexed?: number;\n fill?: string;\n fillTheme?: number;\n fillTint?: number;\n fillIndexed?: number;\n fillBgColor?: string;\n fillBgTheme?: number;\n fillBgTint?: number;\n fillBgIndexed?: number;\n border?: BorderStyle;\n alignment?: Alignment;\n numberFormat?: string;\n}\n\nexport interface BorderStyle {\n top?: BorderType;\n bottom?: BorderType;\n left?: BorderType;\n right?: BorderType;\n}\n\nexport type BorderType = 'thin' | 'medium' | 'thick' | 'double' | 'dotted' | 'dashed';\n\nexport interface Alignment {\n horizontal?: 'left' | 'center' | 'right' | 'justify';\n vertical?: 'top' | 'middle' | 'bottom';\n wrapText?: boolean;\n textRotation?: number;\n}\n\n/**\n * Cell address with 0-indexed row and column\n */\nexport interface CellAddress {\n row: number;\n col: number;\n}\n\n/**\n * Range address with start and end cells\n */\nexport interface RangeAddress {\n start: CellAddress;\n end: CellAddress;\n}\n\n/**\n * Internal cell data representation\n */\nexport interface CellData {\n /** Cell type: n=number, s=string (shared), str=inline string, b=boolean, e=error, d=date */\n t?: 'n' | 's' | 'str' | 'b' | 'e' | 'd';\n /** Raw value */\n v?: number | string | boolean;\n /** Formula (without leading =) */\n f?: string;\n /** Style index */\n s?: number;\n /** Formatted text (cached) */\n w?: string;\n /** Number format */\n z?: string;\n /** Array formula range */\n F?: string;\n /** Dynamic array formula flag */\n D?: boolean;\n /** Shared formula index */\n si?: number;\n}\n\n/**\n * Sheet definition from workbook.xml\n */\nexport interface SheetDefinition {\n name: string;\n sheetId: number;\n rId: string;\n}\n\n/**\n * Relationship definition\n */\nexport interface Relationship {\n id: string;\n type: string;\n target: string;\n}\n\n/**\n * Configuration for creating a sheet from an array of objects\n */\nexport interface SheetFromDataConfig<T extends object = Record<string, unknown>> {\n /** Name of the sheet to create */\n name: string;\n /** Array of objects with the same structure */\n data: T[];\n /** Column definitions (optional - defaults to all keys from first object) */\n columns?: ColumnConfig<T>[];\n /** Apply header styling (bold text) (default: true) */\n headerStyle?: boolean;\n /** Starting cell address (default: 'A1') */\n startCell?: string;\n}\n\n/**\n * Column configuration for sheet data\n */\nexport interface ColumnConfig<T = Record<string, unknown>> {\n /** Key from the object to use for this column */\n key: keyof T;\n /** Header text (optional - defaults to key name) */\n header?: string;\n /** Cell style for data cells in this column */\n style?: CellStyle;\n}\n\n/**\n * Rich cell value with optional formula and style.\n * Use this when you need to set value, formula, or style for individual cells.\n */\nexport interface RichCellValue {\n /** Cell value */\n value?: CellValue;\n /** Formula (without leading '=') */\n formula?: string;\n /** Cell style */\n style?: CellStyle;\n}\n\n/**\n * Configuration for creating an Excel Table (ListObject)\n */\nexport interface TableConfig {\n /** Table name (must be unique within the workbook) */\n name: string;\n /** Data range including headers (e.g., \"A1:D10\") */\n range: string;\n /** First row contains headers (default: true) */\n headerRow?: boolean;\n /** Show total row at the bottom (default: false) */\n totalRow?: boolean;\n /** Table style configuration */\n style?: TableStyleConfig;\n}\n\n/**\n * Table style configuration options\n */\nexport interface TableStyleConfig {\n /** Built-in table style name (e.g., \"TableStyleMedium2\", \"TableStyleLight1\") */\n name?: string;\n /** Show banded/alternating row colors (default: true) */\n showRowStripes?: boolean;\n /** Show banded/alternating column colors (default: false) */\n showColumnStripes?: boolean;\n /** Highlight first column with special formatting (default: false) */\n showFirstColumn?: boolean;\n /** Highlight last column with special formatting (default: false) */\n showLastColumn?: boolean;\n}\n\n/**\n * Pivot table aggregation functions.\n */\nexport type PivotAggregationType = 'sum' | 'count' | 'average' | 'min' | 'max';\n\n/**\n * Pivot field sort order.\n */\nexport type PivotSortOrder = 'asc' | 'desc';\n\n/**\n * Filter definition for pivot fields.\n * Use either include or exclude, not both.\n */\nexport type PivotFieldFilter = { include: string[] } | { exclude: string[] };\n\n/**\n * Value field configuration for pivot tables.\n */\nexport interface PivotValueConfig {\n /** Source field name */\n field: string;\n /** Aggregation type (default: 'sum') */\n aggregation?: PivotAggregationType;\n /** Display name for the value field */\n name?: string;\n /** Number format to apply to values */\n numberFormat?: string;\n}\n\n/**\n * Configuration for creating a PivotTable.\n */\nexport interface PivotTableConfig {\n /** Pivot table name */\n name: string;\n /** Source data range with sheet name, e.g. \"Data!A1:E100\" */\n source: string;\n /** Target cell with sheet name, e.g. \"Summary!A3\" */\n target: string;\n /** Refresh pivot when opening workbook (default: true) */\n refreshOnLoad?: boolean;\n}\n\n/**\n * Aggregation functions available for table total row\n */\nexport type TableTotalFunction = 'sum' | 'count' | 'average' | 'min' | 'max' | 'stdDev' | 'var' | 'countNums' | 'none';\n\n/**\n * Configuration for converting a sheet to JSON objects.\n */\nexport interface SheetToJsonConfig {\n /**\n * Field names to use for each column.\n * If provided, the first row of data starts at row 1 (or startRow).\n * If not provided, the first row is used as field names.\n */\n fields?: string[];\n\n /**\n * Starting row (0-based). Defaults to 0.\n * If fields are not provided, this row contains the headers.\n * If fields are provided, this is the first data row.\n */\n startRow?: number;\n\n /**\n * Starting column (0-based). Defaults to 0.\n */\n startCol?: number;\n\n /**\n * Ending row (0-based, inclusive). Defaults to the last row with data.\n */\n endRow?: number;\n\n /**\n * Ending column (0-based, inclusive). Defaults to the last column with data.\n */\n endCol?: number;\n\n /**\n * If true, stop reading when an empty row is encountered. Defaults to true.\n */\n stopOnEmptyRow?: boolean;\n\n /**\n * How to serialize Date values. Defaults to 'jsDate'.\n */\n dateHandling?: DateHandling;\n\n /**\n * If true, return formatted text (as displayed in Excel) instead of raw values.\n * All values will be returned as strings. Defaults to false.\n */\n asText?: boolean;\n\n /**\n * Locale to use for formatting when asText is true.\n * Defaults to the workbook locale.\n */\n locale?: string;\n}\n\n/**\n * Options for reading a workbook from file/buffer.\n */\nexport interface WorkbookReadOptions {\n /**\n * Enable lazy parsing of ZIP entries and XML parts.\n * Defaults to true.\n */\n lazy?: boolean;\n}\n","import type { CellValue, CellType, CellStyle, CellData, ErrorType } from './types';\nimport type { Worksheet } from './worksheet';\nimport { parseAddress, toAddress } from './utils/address';\nimport { formatCellValue } from './utils/format';\n\n// Excel epoch: December 31, 1899 (accounting for the 1900 leap year bug)\nconst EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 31));\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n// Excel error types\nconst ERROR_TYPES: Set<string> = new Set([\n '#NULL!',\n '#DIV/0!',\n '#VALUE!',\n '#REF!',\n '#NAME?',\n '#NUM!',\n '#N/A',\n '#GETTING_DATA',\n]);\n\n/**\n * Represents a single cell in a worksheet\n */\nexport class Cell {\n private _row: number;\n private _col: number;\n private _data: CellData;\n private _worksheet: Worksheet;\n private _dirty = false;\n\n constructor(worksheet: Worksheet, row: number, col: number, data?: CellData) {\n this._worksheet = worksheet;\n this._row = row;\n this._col = col;\n this._data = data || {};\n }\n\n /**\n * Get the cell address (e.g., 'A1')\n */\n get address(): string {\n return toAddress(this._row, this._col);\n }\n\n /**\n * Get the 0-based row index\n */\n get row(): number {\n return this._row;\n }\n\n /**\n * Get the 0-based column index\n */\n get col(): number {\n return this._col;\n }\n\n /**\n * Get the cell type\n */\n get type(): CellType {\n const t = this._data.t;\n if (!t && this._data.v === undefined && !this._data.f) {\n return 'empty';\n }\n switch (t) {\n case 'n':\n // Check if this is actually a date stored as number\n if (this._isDateFormat()) {\n return 'date';\n }\n return 'number';\n case 's':\n case 'str':\n return 'string';\n case 'b':\n return 'boolean';\n case 'e':\n return 'error';\n case 'd':\n return 'date';\n default:\n // If no type but has value, infer from value\n if (typeof this._data.v === 'number') {\n if (this._isDateFormat()) {\n return 'date';\n }\n return 'number';\n }\n if (typeof this._data.v === 'string') return 'string';\n if (typeof this._data.v === 'boolean') return 'boolean';\n return 'empty';\n }\n }\n\n /**\n * Get the cell value\n */\n get value(): CellValue {\n const t = this._data.t;\n const v = this._data.v;\n\n if (v === undefined && !this._data.f) {\n return null;\n }\n\n switch (t) {\n case 'n': {\n const numVal = typeof v === 'number' ? v : parseFloat(String(v));\n // Check if this is actually a date stored as number\n if (this._isDateFormat()) {\n return this._excelDateToJs(numVal);\n }\n return numVal;\n }\n case 's':\n // Shared string reference\n if (typeof v === 'number') {\n return this._worksheet.workbook.sharedStrings.getString(v) ?? '';\n }\n return String(v);\n case 'str':\n // Inline string\n return String(v);\n case 'b':\n return v === 1 || v === '1' || v === true;\n case 'e':\n return { error: String(v) as ErrorType };\n case 'd':\n // ISO 8601 date string\n return new Date(String(v));\n default:\n // No type specified - try to infer\n if (typeof v === 'number') {\n // Check if this might be a date based on number format\n if (this._isDateFormat()) {\n return this._excelDateToJs(v);\n }\n return v;\n }\n if (typeof v === 'string') {\n if (ERROR_TYPES.has(v)) {\n return { error: v as ErrorType };\n }\n return v;\n }\n if (typeof v === 'boolean') return v;\n return null;\n }\n }\n\n /**\n * Set the cell value\n */\n set value(val: CellValue) {\n this._dirty = true;\n\n if (val === null || val === undefined) {\n this._data.v = undefined;\n this._data.t = undefined;\n this._data.f = undefined;\n return;\n }\n\n if (typeof val === 'number') {\n this._data.v = val;\n this._data.t = 'n';\n } else if (typeof val === 'string') {\n // Store as shared string\n const index = this._worksheet.workbook.sharedStrings.addString(val);\n this._data.v = index;\n this._data.t = 's';\n } else if (typeof val === 'boolean') {\n this._data.v = val ? 1 : 0;\n this._data.t = 'b';\n } else if (val instanceof Date) {\n // Store as Excel serial number with date format for maximum compatibility\n this._data.v = this._jsDateToExcel(val);\n this._data.t = 'n';\n // Apply a default date format if no style is set\n if (this._data.s === undefined) {\n this._data.s = this._worksheet.workbook.styles.createStyle({ numberFormat: 'yyyy-mm-dd' });\n }\n } else if ('error' in val) {\n this._data.v = val.error;\n this._data.t = 'e';\n }\n\n // Clear formula when setting value directly\n this._data.f = undefined;\n }\n\n /**\n * Write a 2D array of values starting at this cell\n */\n set values(data: CellValue[][]) {\n for (let r = 0; r < data.length; r++) {\n const row = data[r];\n for (let c = 0; c < row.length; c++) {\n const cell = this._worksheet.cell(this._row + r, this._col + c);\n cell.value = row[c];\n }\n }\n }\n\n /**\n * Get the formula (without leading '=')\n */\n get formula(): string | undefined {\n return this._data.f;\n }\n\n /**\n * Set the formula (without leading '=')\n */\n set formula(f: string | undefined) {\n this._dirty = true;\n if (f === undefined) {\n this._data.f = undefined;\n } else {\n // Remove leading '=' if present\n this._data.f = f.startsWith('=') ? f.slice(1) : f;\n }\n }\n\n /**\n * Get the formatted text (as displayed in Excel)\n */\n get text(): string {\n return this.textWithLocale();\n }\n\n /**\n * Get the formatted text using a specific locale\n */\n textWithLocale(locale?: string): string {\n if (this._data.w) {\n return this._data.w;\n }\n const val = this.value;\n if (val === null) return '';\n if (typeof val === 'object' && 'error' in val) return val.error;\n\n if (val instanceof Date || typeof val === 'number') {\n const formatted = formatCellValue(val, this.style, locale ?? this._worksheet.workbook.locale);\n if (formatted !== null) return formatted;\n }\n\n if (val instanceof Date) return val.toISOString().split('T')[0];\n return String(val);\n }\n\n /**\n * Get the style index\n */\n get styleIndex(): number | undefined {\n return this._data.s;\n }\n\n /**\n * Set the style index\n */\n set styleIndex(index: number | undefined) {\n this._dirty = true;\n this._data.s = index;\n }\n\n /**\n * Get the cell style\n */\n get style(): CellStyle {\n if (this._data.s === undefined) {\n return {};\n }\n return this._worksheet.workbook.styles.getStyle(this._data.s);\n }\n\n /**\n * Set the cell style (merges with existing)\n */\n set style(style: CellStyle) {\n this._dirty = true;\n const currentStyle = this.style;\n const merged = { ...currentStyle, ...style };\n this._data.s = this._worksheet.workbook.styles.createStyle(merged);\n }\n\n /**\n * Check if cell has been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Get internal cell data\n */\n get data(): CellData {\n return this._data;\n }\n\n /**\n * Check if this cell has a date number format\n */\n private _isDateFormat(): boolean {\n if (this._data.s === undefined) {\n return false;\n }\n const style = this._worksheet.workbook.styles.getStyle(this._data.s);\n if (!style.numberFormat) {\n return false;\n }\n // Common date format patterns\n const fmt = style.numberFormat.toLowerCase();\n return (\n fmt.includes('y') ||\n fmt.includes('m') ||\n fmt.includes('d') ||\n fmt.includes('h') ||\n fmt.includes('s') ||\n fmt === 'general date' ||\n fmt === 'short date' ||\n fmt === 'long date'\n );\n }\n\n /**\n * Convert Excel serial date to JavaScript Date\n * Used when reading dates stored as numbers with date formats\n */\n _excelDateToJs(serial: number): Date {\n // Excel incorrectly considers 1900 a leap year\n // Dates after Feb 28, 1900 need adjustment\n const adjusted = serial >= 60 ? serial - 1 : serial;\n const ms = Math.round(adjusted * MS_PER_DAY);\n return new Date(EXCEL_EPOCH.getTime() + ms);\n }\n\n /**\n * Convert JavaScript Date to Excel serial date\n * Used when writing dates as numbers for Excel compatibility\n */\n _jsDateToExcel(date: Date): number {\n const ms = date.getTime() - EXCEL_EPOCH.getTime();\n let serial = ms / MS_PER_DAY;\n // Account for Excel's 1900 leap year bug\n if (serial >= 60) {\n serial += 1;\n }\n return serial;\n }\n}\n\n/**\n * Parse a cell address or row/col to get row and col indices\n */\nexport const parseCellRef = (rowOrAddress: number | string, col?: number): { row: number; col: number } => {\n if (typeof rowOrAddress === 'string') {\n return parseAddress(rowOrAddress);\n }\n if (col === undefined) {\n throw new Error('Column must be provided when row is a number');\n }\n return { row: rowOrAddress, col };\n};\n","import type { CellValue, CellStyle, RangeAddress } from './types';\nimport type { Worksheet } from './worksheet';\nimport { toAddress, normalizeRange } from './utils/address';\n\n/**\n * Represents a range of cells in a worksheet\n */\nexport class Range {\n private _worksheet: Worksheet;\n private _range: RangeAddress;\n\n constructor(worksheet: Worksheet, range: RangeAddress) {\n this._worksheet = worksheet;\n this._range = normalizeRange(range);\n }\n\n /**\n * Get the range address as a string\n */\n get address(): string {\n const start = toAddress(this._range.start.row, this._range.start.col);\n const end = toAddress(this._range.end.row, this._range.end.col);\n if (start === end) return start;\n return `${start}:${end}`;\n }\n\n /**\n * Get the number of rows in the range\n */\n get rowCount(): number {\n return this._range.end.row - this._range.start.row + 1;\n }\n\n /**\n * Get the number of columns in the range\n */\n get colCount(): number {\n return this._range.end.col - this._range.start.col + 1;\n }\n\n /**\n * Get all values in the range as a 2D array\n */\n get values(): CellValue[][] {\n return this.getValues();\n }\n\n /**\n * Get all values in the range as a 2D array with options\n */\n getValues(options: { createMissing?: boolean } = {}): CellValue[][] {\n const { createMissing = true } = options;\n const result: CellValue[][] = [];\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row: CellValue[] = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n if (createMissing) {\n const cell = this._worksheet.cell(r, c);\n row.push(cell.value);\n } else {\n const cell = this._worksheet.getCellIfExists(r, c);\n row.push(cell?.value ?? null);\n }\n }\n result.push(row);\n }\n return result;\n }\n\n /**\n * Set values in the range from a 2D array\n */\n set values(data: CellValue[][]) {\n for (let r = 0; r < data.length && r < this.rowCount; r++) {\n const row = data[r];\n for (let c = 0; c < row.length && c < this.colCount; c++) {\n const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);\n cell.value = row[c];\n }\n }\n }\n\n /**\n * Get all formulas in the range as a 2D array\n */\n get formulas(): (string | undefined)[][] {\n const result: (string | undefined)[][] = [];\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row: (string | undefined)[] = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n const cell = this._worksheet.cell(r, c);\n row.push(cell.formula);\n }\n result.push(row);\n }\n return result;\n }\n\n /**\n * Set formulas in the range from a 2D array\n */\n set formulas(data: (string | undefined)[][]) {\n for (let r = 0; r < data.length && r < this.rowCount; r++) {\n const row = data[r];\n for (let c = 0; c < row.length && c < this.colCount; c++) {\n const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);\n cell.formula = row[c];\n }\n }\n }\n\n /**\n * Get the style of the top-left cell\n */\n get style(): CellStyle {\n return this._worksheet.cell(this._range.start.row, this._range.start.col).style;\n }\n\n /**\n * Set style for all cells in the range\n */\n set style(style: CellStyle) {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n const cell = this._worksheet.cell(r, c);\n cell.style = style;\n }\n }\n }\n\n /**\n * Iterate over all cells in the range\n */\n *[Symbol.iterator]() {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n yield this._worksheet.cell(r, c);\n }\n }\n }\n\n /**\n * Iterate over cells row by row\n */\n *rows() {\n for (let r = this._range.start.row; r <= this._range.end.row; r++) {\n const row = [];\n for (let c = this._range.start.col; c <= this._range.end.col; c++) {\n row.push(this._worksheet.cell(r, c));\n }\n yield row;\n }\n }\n}\n","import type { TableConfig, TableStyleConfig, TableTotalFunction, RangeAddress } from './types';\nimport type { Worksheet } from './worksheet';\nimport { parseRange, toAddress, toRange } from './utils/address';\nimport { createElement, stringifyXml, XmlNode } from './utils/xml';\n\n/**\n * Maps table total function names to SUBTOTAL function numbers\n * SUBTOTAL uses 101-111 for functions that ignore hidden values\n */\nconst TOTAL_FUNCTION_NUMBERS: Record<TableTotalFunction, number> = {\n average: 101,\n count: 102,\n countNums: 103,\n max: 104,\n min: 105,\n stdDev: 107,\n sum: 109,\n var: 110,\n none: 0,\n};\n\n/**\n * Maps table total function names to XML attribute values\n */\nconst TOTAL_FUNCTION_NAMES: Record<TableTotalFunction, string> = {\n average: 'average',\n count: 'count',\n countNums: 'countNums',\n max: 'max',\n min: 'min',\n stdDev: 'stdDev',\n sum: 'sum',\n var: 'var',\n none: 'none',\n};\n\n/**\n * Represents an Excel Table (ListObject) with auto-filter, banded styling, and total row.\n */\nexport class Table {\n private _name: string;\n private _displayName: string;\n private _worksheet: Worksheet;\n private _range: RangeAddress;\n private _baseRange: RangeAddress;\n private _totalRow: boolean;\n private _autoFilter: boolean;\n private _style: TableStyleConfig;\n private _columns: TableColumn[] = [];\n private _id: number;\n private _dirty = true;\n private _headerRow: boolean;\n\n constructor(worksheet: Worksheet, config: TableConfig, tableId: number) {\n this._worksheet = worksheet;\n this._name = config.name;\n this._displayName = config.name;\n this._range = parseRange(config.range);\n this._baseRange = { start: { ...this._range.start }, end: { ...this._range.end } };\n this._totalRow = config.totalRow === true; // Default false\n this._autoFilter = true; // Tables have auto-filter by default\n this._headerRow = config.headerRow !== false;\n this._id = tableId;\n\n // Expand range to include total row if enabled\n if (this._totalRow) {\n this._range.end.row++;\n }\n\n // Set default style\n this._style = {\n name: config.style?.name ?? 'TableStyleMedium2',\n showRowStripes: config.style?.showRowStripes !== false, // Default true\n showColumnStripes: config.style?.showColumnStripes === true, // Default false\n showFirstColumn: config.style?.showFirstColumn === true, // Default false\n showLastColumn: config.style?.showLastColumn === true, // Default false\n };\n\n // Extract column names from worksheet headers\n this._extractColumns();\n }\n\n /**\n * Get the table name\n */\n get name(): string {\n return this._name;\n }\n\n /**\n * Get the table display name\n */\n get displayName(): string {\n return this._displayName;\n }\n\n /**\n * Get the table ID\n */\n get id(): number {\n return this._id;\n }\n\n /**\n * Get the worksheet this table belongs to\n */\n get worksheet(): Worksheet {\n return this._worksheet;\n }\n\n /**\n * Get the table range address string\n */\n get range(): string {\n return toRange(this._range);\n }\n\n /**\n * Get the base range excluding total row\n */\n get baseRange(): string {\n return toRange(this._baseRange);\n }\n\n /**\n * Get the table range as RangeAddress\n */\n get rangeAddress(): RangeAddress {\n return { ...this._range };\n }\n\n /**\n * Get column names\n */\n get columns(): string[] {\n return this._columns.map((c) => c.name);\n }\n\n /**\n * Check if table has a total row\n */\n get hasTotalRow(): boolean {\n return this._totalRow;\n }\n\n /**\n * Check if table has a header row\n */\n get hasHeaderRow(): boolean {\n return this._headerRow;\n }\n\n /**\n * Check if table has auto-filter enabled\n */\n get hasAutoFilter(): boolean {\n return this._autoFilter;\n }\n\n /**\n * Get the current style configuration\n */\n get style(): TableStyleConfig {\n return { ...this._style };\n }\n\n /**\n * Check if the table has been modified\n */\n get dirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Set a total function for a column\n * @param columnName - Name of the column (header text)\n * @param fn - Aggregation function to use\n * @returns this for method chaining\n */\n setTotalFunction(columnName: string, fn: TableTotalFunction): this {\n if (!this._totalRow) {\n throw new Error('Cannot set total function: table does not have a total row enabled');\n }\n\n const column = this._columns.find((c) => c.name === columnName);\n if (!column) {\n throw new Error(`Column not found: ${columnName}`);\n }\n\n column.totalFunction = fn;\n this._dirty = true;\n\n // Write the formula to the total row cell\n this._writeTotalRowFormula(column);\n\n return this;\n }\n\n /**\n * Get total function for a column if set\n */\n getTotalFunction(columnName: string): TableTotalFunction | undefined {\n const column = this._columns.find((c) => c.name === columnName);\n return column?.totalFunction;\n }\n\n /**\n * Enable or disable auto-filter\n * @param enabled - Whether auto-filter should be enabled\n * @returns this for method chaining\n */\n setAutoFilter(enabled: boolean): this {\n this._autoFilter = enabled;\n this._dirty = true;\n return this;\n }\n\n /**\n * Update table style configuration\n * @param style - Style options to apply\n * @returns this for method chaining\n */\n setStyle(style: Partial<TableStyleConfig>): this {\n if (style.name !== undefined) this._style.name = style.name;\n if (style.showRowStripes !== undefined) this._style.showRowStripes = style.showRowStripes;\n if (style.showColumnStripes !== undefined) this._style.showColumnStripes = style.showColumnStripes;\n if (style.showFirstColumn !== undefined) this._style.showFirstColumn = style.showFirstColumn;\n if (style.showLastColumn !== undefined) this._style.showLastColumn = style.showLastColumn;\n this._dirty = true;\n return this;\n }\n\n /**\n * Enable or disable the total row\n * @param enabled - Whether total row should be shown\n * @returns this for method chaining\n */\n setTotalRow(enabled: boolean): this {\n if (this._totalRow === enabled) return this;\n\n this._totalRow = enabled;\n this._dirty = true;\n\n if (enabled) {\n this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };\n this._range.end.row++;\n } else {\n this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };\n for (const col of this._columns) {\n col.totalFunction = undefined;\n }\n }\n\n return this;\n }\n\n /**\n * Extract column names from the header row of the worksheet\n */\n private _extractColumns(): void {\n const headerRow = this._range.start.row;\n const startCol = this._range.start.col;\n const endCol = this._range.end.col;\n\n for (let col = startCol; col <= endCol; col++) {\n const cell = this._headerRow ? this._worksheet.getCellIfExists(headerRow, col) : undefined;\n const value = cell?.value;\n const name = value != null ? String(value) : `Column${col - startCol + 1}`;\n\n this._columns.push({\n id: col - startCol + 1,\n name,\n colIndex: col,\n });\n }\n }\n\n /**\n * Write the SUBTOTAL formula to a total row cell\n */\n private _writeTotalRowFormula(column: TableColumn): void {\n if (!this._totalRow || !column.totalFunction || column.totalFunction === 'none') {\n return;\n }\n\n const totalRowIndex = this._range.end.row;\n const cell = this._worksheet.cell(totalRowIndex, column.colIndex);\n\n // Generate SUBTOTAL formula with structured reference\n const funcNum = TOTAL_FUNCTION_NUMBERS[column.totalFunction];\n // Use structured reference: SUBTOTAL(109,[ColumnName])\n const formula = `SUBTOTAL(${funcNum},[${column.name}])`;\n cell.formula = formula;\n }\n\n /**\n * Get the auto-filter range (excludes total row if present)\n */\n private _getAutoFilterRange(): string {\n const start = toAddress(this._range.start.row, this._range.start.col);\n\n // Auto-filter excludes the total row\n let endRow = this._range.end.row;\n if (this._totalRow) {\n endRow--;\n }\n\n const end = toAddress(endRow, this._range.end.col);\n return `${start}:${end}`;\n }\n\n /**\n * Generate the table definition XML\n */\n toXml(): string {\n const children: XmlNode[] = [];\n\n // Auto-filter element\n if (this._autoFilter) {\n const autoFilterRef = this._getAutoFilterRange();\n children.push(createElement('autoFilter', { ref: autoFilterRef }, []));\n }\n\n // Table columns\n const columnNodes: XmlNode[] = this._columns.map((col) => {\n const attrs: Record<string, string> = {\n id: String(col.id),\n name: col.name,\n };\n\n // Add total function if specified\n if (this._totalRow && col.totalFunction && col.totalFunction !== 'none') {\n attrs.totalsRowFunction = TOTAL_FUNCTION_NAMES[col.totalFunction];\n }\n\n return createElement('tableColumn', attrs, []);\n });\n\n children.push(createElement('tableColumns', { count: String(columnNodes.length) }, columnNodes));\n\n // Table style info\n const styleAttrs: Record<string, string> = {\n name: this._style.name || 'TableStyleMedium2',\n showFirstColumn: this._style.showFirstColumn ? '1' : '0',\n showLastColumn: this._style.showLastColumn ? '1' : '0',\n showRowStripes: this._style.showRowStripes !== false ? '1' : '0',\n showColumnStripes: this._style.showColumnStripes ? '1' : '0',\n };\n children.push(createElement('tableStyleInfo', styleAttrs, []));\n\n // Build table attributes\n const tableRef = toRange(this._range);\n const tableAttrs: Record<string, string> = {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n id: String(this._id),\n name: this._name,\n displayName: this._displayName,\n ref: tableRef,\n };\n\n if (!this._headerRow) {\n tableAttrs.headerRowCount = '0';\n }\n\n if (this._totalRow) {\n tableAttrs.totalsRowCount = '1';\n } else {\n tableAttrs.totalsRowShown = '0';\n }\n\n // Build complete table node\n const tableNode = createElement('table', tableAttrs, children);\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([tableNode])}`;\n }\n}\n\n/**\n * Internal column representation\n */\ninterface TableColumn {\n id: number;\n name: string;\n colIndex: number;\n totalFunction?: TableTotalFunction;\n}\n","import type { CellData, RangeAddress, SheetToJsonConfig, CellValue, DateHandling, TableConfig } from './types';\nimport type { Workbook } from './workbook';\nimport { Cell, parseCellRef } from './cell';\nimport { Range } from './range';\nimport { Table } from './table';\nimport { parseRange, toAddress, parseAddress, letterToCol } from './utils/address';\nimport { findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement, createText, parseXml } from './utils/xml';\n\n/**\n * Represents a worksheet in a workbook\n */\nexport class Worksheet {\n private _name: string;\n private _workbook: Workbook;\n private _cells: Map<string, Cell> = new Map();\n private _xmlNodes: XmlNode[] | null = null;\n private _dirty = false;\n private _mergedCells: Set<string> = new Set();\n private _columnWidths: Map<number, number> = new Map();\n private _rowHeights: Map<number, number> = new Map();\n private _frozenPane: { row: number; col: number } | null = null;\n private _dataBoundsCache: { minRow: number; maxRow: number; minCol: number; maxCol: number } | null = null;\n private _boundsDirty = true;\n private _tables: Table[] = [];\n private _preserveXml = false;\n private _rawXml: string | null = null;\n private _lazyParse = false;\n private _tableRelIds: string[] | null = null;\n private _pivotTableRelIds: string[] | null = null;\n private _sheetViewsDirty = false;\n private _colsDirty = false;\n private _tablePartsDirty = false;\n private _pivotTablePartsDirty = false;\n\n constructor(workbook: Workbook, name: string) {\n this._workbook = workbook;\n this._name = name;\n }\n\n /**\n * Get the workbook this sheet belongs to\n */\n get workbook(): Workbook {\n return this._workbook;\n }\n\n /**\n * Get the sheet name\n */\n get name(): string {\n return this._name;\n }\n\n /**\n * Set the sheet name\n */\n set name(value: string) {\n this._name = value;\n this._dirty = true;\n }\n\n /**\n * Parse worksheet XML content\n */\n parse(xml: string, options: { lazy?: boolean } = {}): void {\n this._rawXml = xml;\n this._xmlNodes = null;\n this._preserveXml = true;\n this._lazyParse = options.lazy ?? true;\n if (!this._lazyParse) {\n this._ensureParsed();\n }\n }\n\n private _ensureParsed(): void {\n if (!this._lazyParse) return;\n if (!this._rawXml) {\n this._lazyParse = false;\n return;\n }\n\n this._xmlNodes = parseXml(this._rawXml);\n this._preserveXml = true;\n const worksheet = findElement(this._xmlNodes, 'worksheet');\n if (!worksheet) {\n this._lazyParse = false;\n return;\n }\n\n const worksheetChildren = getChildren(worksheet, 'worksheet');\n\n // Parse sheet views (freeze panes)\n const sheetViews = findElement(worksheetChildren, 'sheetViews');\n if (sheetViews) {\n const viewChildren = getChildren(sheetViews, 'sheetViews');\n const sheetView = findElement(viewChildren, 'sheetView');\n if (sheetView) {\n const sheetViewChildren = getChildren(sheetView, 'sheetView');\n const pane = findElement(sheetViewChildren, 'pane');\n if (pane && getAttr(pane, 'state') === 'frozen') {\n const xSplit = parseInt(getAttr(pane, 'xSplit') || '0', 10);\n const ySplit = parseInt(getAttr(pane, 'ySplit') || '0', 10);\n if (xSplit > 0 || ySplit > 0) {\n this._frozenPane = { row: ySplit, col: xSplit };\n }\n }\n }\n }\n\n // Parse sheet data (cells)\n const sheetData = findElement(worksheetChildren, 'sheetData');\n if (sheetData) {\n const rows = getChildren(sheetData, 'sheetData');\n this._parseSheetData(rows);\n }\n\n // Parse column widths\n const cols = findElement(worksheetChildren, 'cols');\n if (cols) {\n const colChildren = getChildren(cols, 'cols');\n for (const col of colChildren) {\n if (!('col' in col)) continue;\n const min = parseInt(getAttr(col, 'min') || '0', 10);\n const max = parseInt(getAttr(col, 'max') || '0', 10);\n const width = parseFloat(getAttr(col, 'width') || '0');\n if (!Number.isFinite(width) || width <= 0) continue;\n if (min > 0 && max > 0) {\n for (let idx = min; idx <= max; idx++) {\n this._columnWidths.set(idx - 1, width);\n }\n }\n }\n }\n\n // Parse merged cells\n const mergeCells = findElement(worksheetChildren, 'mergeCells');\n if (mergeCells) {\n const mergeChildren = getChildren(mergeCells, 'mergeCells');\n for (const mergeCell of mergeChildren) {\n if ('mergeCell' in mergeCell) {\n const ref = getAttr(mergeCell, 'ref');\n if (ref) {\n this._mergedCells.add(ref);\n }\n }\n }\n }\n\n this._lazyParse = false;\n }\n\n /**\n * Parse the sheetData element to extract cells\n */\n private _parseSheetData(rows: XmlNode[]): void {\n for (const rowNode of rows) {\n if (!('row' in rowNode)) continue;\n\n const rowIndex = parseInt(getAttr(rowNode, 'r') || '0', 10) - 1;\n const rowHeight = parseFloat(getAttr(rowNode, 'ht') || '0');\n if (rowIndex >= 0 && Number.isFinite(rowHeight) && rowHeight > 0) {\n this._rowHeights.set(rowIndex, rowHeight);\n }\n\n const rowChildren = getChildren(rowNode, 'row');\n for (const cellNode of rowChildren) {\n if (!('c' in cellNode)) continue;\n\n const ref = getAttr(cellNode, 'r');\n if (!ref) continue;\n\n const { row, col } = parseAddress(ref);\n const cellData = this._parseCellNode(cellNode);\n const cell = new Cell(this, row, col, cellData);\n this._cells.set(ref, cell);\n }\n }\n\n this._boundsDirty = true;\n }\n\n /**\n * Parse a cell XML node to CellData\n */\n private _parseCellNode(node: XmlNode): CellData {\n const data: CellData = {};\n\n // Type attribute\n const t = getAttr(node, 't');\n if (t) {\n data.t = t as CellData['t'];\n }\n\n // Style attribute\n const s = getAttr(node, 's');\n if (s) {\n data.s = parseInt(s, 10);\n }\n\n const children = getChildren(node, 'c');\n\n // Value element\n const vNode = findElement(children, 'v');\n if (vNode) {\n const vChildren = getChildren(vNode, 'v');\n for (const child of vChildren) {\n if ('#text' in child) {\n const text = child['#text'] as string;\n // Parse based on type\n if (data.t === 's') {\n data.v = parseInt(text, 10); // Shared string index\n } else if (data.t === 'b') {\n data.v = text === '1' ? 1 : 0;\n } else if (data.t === 'e' || data.t === 'str') {\n data.v = text;\n } else {\n // Number or default\n data.v = parseFloat(text);\n }\n break;\n }\n }\n }\n\n // Formula element\n const fNode = findElement(children, 'f');\n if (fNode) {\n const fChildren = getChildren(fNode, 'f');\n for (const child of fChildren) {\n if ('#text' in child) {\n data.f = child['#text'] as string;\n break;\n }\n }\n\n // Check for shared formula\n const si = getAttr(fNode, 'si');\n if (si) {\n data.si = parseInt(si, 10);\n }\n\n // Check for array formula range\n const ref = getAttr(fNode, 'ref');\n if (ref) {\n data.F = ref;\n }\n }\n\n // Inline string (is element)\n const isNode = findElement(children, 'is');\n if (isNode) {\n data.t = 'str';\n const isChildren = getChildren(isNode, 'is');\n const tNode = findElement(isChildren, 't');\n if (tNode) {\n const tChildren = getChildren(tNode, 't');\n for (const child of tChildren) {\n if ('#text' in child) {\n data.v = child['#text'] as string;\n break;\n }\n }\n }\n }\n\n return data;\n }\n\n /**\n * Get a cell by address or row/col\n */\n cell(rowOrAddress: number | string, col?: number): Cell {\n this._ensureParsed();\n const { row, col: c } = parseCellRef(rowOrAddress, col);\n const address = toAddress(row, c);\n\n let cell = this._cells.get(address);\n if (!cell) {\n cell = new Cell(this, row, c);\n this._cells.set(address, cell);\n this._boundsDirty = true;\n }\n\n return cell;\n }\n\n /**\n * Get an existing cell without creating it.\n */\n getCellIfExists(rowOrAddress: number | string, col?: number): Cell | undefined {\n this._ensureParsed();\n const { row, col: c } = parseCellRef(rowOrAddress, col);\n const address = toAddress(row, c);\n return this._cells.get(address);\n }\n\n /**\n * Get a range of cells\n */\n range(rangeStr: string): Range;\n range(startRow: number, startCol: number, endRow: number, endCol: number): Range;\n range(startRowOrRange: number | string, startCol?: number, endRow?: number, endCol?: number): Range {\n this._ensureParsed();\n let rangeAddr: RangeAddress;\n\n if (typeof startRowOrRange === 'string') {\n rangeAddr = parseRange(startRowOrRange);\n } else {\n if (startCol === undefined || endRow === undefined || endCol === undefined) {\n throw new Error('All range parameters must be provided');\n }\n rangeAddr = {\n start: { row: startRowOrRange, col: startCol },\n end: { row: endRow, col: endCol },\n };\n }\n\n return new Range(this, rangeAddr);\n }\n\n /**\n * Merge cells in the given range\n */\n mergeCells(rangeOrStart: string, end?: string): void {\n this._ensureParsed();\n let rangeStr: string;\n if (end) {\n rangeStr = `${rangeOrStart}:${end}`;\n } else {\n rangeStr = rangeOrStart;\n }\n this._mergedCells.add(rangeStr);\n this._dirty = true;\n }\n\n /**\n * Unmerge cells in the given range\n */\n unmergeCells(rangeStr: string): void {\n this._ensureParsed();\n this._mergedCells.delete(rangeStr);\n this._dirty = true;\n }\n\n /**\n * Get all merged cell ranges\n */\n get mergedCells(): string[] {\n this._ensureParsed();\n return Array.from(this._mergedCells);\n }\n\n /**\n * Check if the worksheet has been modified\n */\n get dirty(): boolean {\n this._ensureParsed();\n if (this._dirty) return true;\n for (const cell of this._cells.values()) {\n if (cell.dirty) return true;\n }\n return false;\n }\n\n /**\n * Get all cells in the worksheet\n */\n get cells(): Map<string, Cell> {\n this._ensureParsed();\n return this._cells;\n }\n\n /**\n * Set a column width (0-based index or column letter)\n */\n setColumnWidth(col: number | string, width: number): void {\n this._ensureParsed();\n if (!Number.isFinite(width) || width <= 0) {\n throw new Error('Column width must be a positive number');\n }\n\n const colIndex = typeof col === 'number' ? col : letterToCol(col);\n if (colIndex < 0) {\n throw new Error(`Invalid column: ${col}`);\n }\n\n this._columnWidths.set(colIndex, width);\n this._colsDirty = true;\n this._dirty = true;\n }\n\n /**\n * Get a column width if set\n */\n getColumnWidth(col: number | string): number | undefined {\n this._ensureParsed();\n const colIndex = typeof col === 'number' ? col : letterToCol(col);\n return this._columnWidths.get(colIndex);\n }\n\n /**\n * Set a row height (0-based index)\n */\n setRowHeight(row: number, height: number): void {\n this._ensureParsed();\n if (!Number.isFinite(height) || height <= 0) {\n throw new Error('Row height must be a positive number');\n }\n if (row < 0) {\n throw new Error('Row index must be >= 0');\n }\n\n this._rowHeights.set(row, height);\n this._colsDirty = true;\n this._dirty = true;\n }\n\n /**\n * Get a row height if set\n */\n getRowHeight(row: number): number | undefined {\n this._ensureParsed();\n return this._rowHeights.get(row);\n }\n\n /**\n * Freeze panes at a given row/column split (counts from top-left)\n */\n freezePane(rowSplit: number, colSplit: number): void {\n this._ensureParsed();\n if (rowSplit < 0 || colSplit < 0) {\n throw new Error('Freeze pane splits must be >= 0');\n }\n if (rowSplit === 0 && colSplit === 0) {\n this._frozenPane = null;\n } else {\n this._frozenPane = { row: rowSplit, col: colSplit };\n }\n this._sheetViewsDirty = true;\n this._dirty = true;\n }\n\n /**\n * Get current frozen pane configuration\n */\n getFrozenPane(): { row: number; col: number } | null {\n this._ensureParsed();\n return this._frozenPane ? { ...this._frozenPane } : null;\n }\n\n /**\n * Get all tables in the worksheet\n */\n get tables(): Table[] {\n this._ensureParsed();\n return [...this._tables];\n }\n\n /**\n * Get column width entries\n * @internal\n */\n getColumnWidths(): Map<number, number> {\n this._ensureParsed();\n return new Map(this._columnWidths);\n }\n\n /**\n * Get row height entries\n * @internal\n */\n getRowHeights(): Map<number, number> {\n this._ensureParsed();\n return new Map(this._rowHeights);\n }\n\n /**\n * Set table relationship IDs for tableParts generation.\n * @internal\n */\n setTableRelIds(ids: string[] | null): void {\n this._ensureParsed();\n this._tableRelIds = ids ? [...ids] : null;\n this._tablePartsDirty = true;\n }\n\n /**\n * Set pivot table relationship IDs for pivotTableParts generation.\n * @internal\n */\n setPivotTableRelIds(ids: string[] | null): void {\n this._ensureParsed();\n this._pivotTableRelIds = ids ? [...ids] : null;\n this._pivotTablePartsDirty = true;\n }\n\n /**\n * Create an Excel Table (ListObject) from a data range.\n *\n * Tables provide structured data features like auto-filter, banded styling,\n * and total row with aggregation functions.\n *\n * @param config - Table configuration\n * @returns Table instance for method chaining\n *\n * @example\n * ```typescript\n * // Create a table with default styling\n * const table = sheet.createTable({\n * name: 'SalesData',\n * range: 'A1:D10',\n * });\n *\n * // Create a table with total row\n * const table = sheet.createTable({\n * name: 'SalesData',\n * range: 'A1:D10',\n * totalRow: true,\n * style: { name: 'TableStyleMedium2' }\n * });\n *\n * table.setTotalFunction('Sales', 'sum');\n * ```\n */\n createTable(config: TableConfig): Table {\n this._ensureParsed();\n // Validate table name is unique within the workbook\n for (const sheet of this._workbook.sheetNames) {\n const ws = this._workbook.sheet(sheet);\n for (const table of ws._tables) {\n if (table.name === config.name) {\n throw new Error(`Table name already exists: ${config.name}`);\n }\n }\n }\n\n // Validate table name format (Excel rules: no spaces at start/end, alphanumeric + underscore)\n if (!config.name || !/^[A-Za-z_\\\\][A-Za-z0-9_.\\\\]*$/.test(config.name)) {\n throw new Error(\n `Invalid table name: ${config.name}. Names must start with a letter or underscore and contain only alphanumeric characters, underscores, or periods.`,\n );\n }\n\n // Create the table with a unique ID from the workbook\n const tableId = this._workbook.getNextTableId();\n const table = new Table(this, config, tableId);\n\n this._tables.push(table);\n this._tablePartsDirty = true;\n this._dirty = true;\n\n return table;\n }\n\n /**\n * Convert sheet data to an array of JSON objects.\n *\n * @param config - Configuration options\n * @returns Array of objects where keys are field names and values are cell values\n *\n * @example\n * ```typescript\n * // Using first row as headers\n * const data = sheet.toJson();\n *\n * // Using custom field names\n * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });\n *\n * // Starting from a specific row/column\n * const data = sheet.toJson({ startRow: 2, startCol: 1 });\n * ```\n */\n toJson<T = Record<string, CellValue>>(config: SheetToJsonConfig = {}): T[] {\n this._ensureParsed();\n const {\n fields,\n startRow = 0,\n startCol = 0,\n endRow,\n endCol,\n stopOnEmptyRow = true,\n dateHandling = this._workbook.dateHandling,\n asText = false,\n locale,\n } = config;\n\n // Get the bounds of data in the sheet\n const bounds = this._getDataBounds();\n if (!bounds) {\n return [];\n }\n\n const effectiveEndRow = endRow ?? bounds.maxRow;\n const effectiveEndCol = endCol ?? bounds.maxCol;\n\n // Determine field names\n let fieldNames: string[];\n let dataStartRow: number;\n\n if (fields) {\n // Use provided field names, data starts at startRow\n fieldNames = fields;\n dataStartRow = startRow;\n } else {\n // Use first row as headers\n fieldNames = [];\n for (let col = startCol; col <= effectiveEndCol; col++) {\n const cell = this._cells.get(toAddress(startRow, col));\n const value = cell?.value;\n fieldNames.push(value != null ? String(value) : `column${col}`);\n }\n dataStartRow = startRow + 1;\n }\n\n // Read data rows\n const result: T[] = [];\n\n for (let row = dataStartRow; row <= effectiveEndRow; row++) {\n const obj: Record<string, CellValue | string> = {};\n let hasData = false;\n\n for (let colOffset = 0; colOffset < fieldNames.length; colOffset++) {\n const col = startCol + colOffset;\n const cell = this._cells.get(toAddress(row, col));\n\n let value: CellValue | string;\n\n if (asText) {\n // Return formatted text instead of raw value\n value = cell?.textWithLocale(locale) ?? '';\n if (value !== '') {\n hasData = true;\n }\n } else {\n value = cell?.value ?? null;\n if (value instanceof Date) {\n value = this._serializeDate(value, dateHandling, cell);\n }\n if (value !== null) {\n hasData = true;\n }\n }\n\n const fieldName = fieldNames[colOffset];\n if (fieldName) {\n obj[fieldName] = value;\n }\n }\n\n // Stop on empty row if configured\n if (stopOnEmptyRow && !hasData) {\n break;\n }\n\n result.push(obj as T);\n }\n\n return result;\n }\n\n private _serializeDate(value: Date, dateHandling: DateHandling, cell?: Cell | null): CellValue | number | string {\n if (dateHandling === 'excelSerial') {\n return cell?._jsDateToExcel(value) ?? value;\n }\n\n if (dateHandling === 'isoString') {\n return value.toISOString();\n }\n\n return value;\n }\n\n /**\n * Get the bounds of data in the sheet (min/max row and column with data)\n */\n private _getDataBounds(): { minRow: number; maxRow: number; minCol: number; maxCol: number } | null {\n if (!this._boundsDirty && this._dataBoundsCache) {\n return this._dataBoundsCache;\n }\n\n if (this._cells.size === 0) {\n this._dataBoundsCache = null;\n this._boundsDirty = false;\n return null;\n }\n\n let minRow = Infinity;\n let maxRow = -Infinity;\n let minCol = Infinity;\n let maxCol = -Infinity;\n\n for (const cell of this._cells.values()) {\n if (cell.value !== null) {\n minRow = Math.min(minRow, cell.row);\n maxRow = Math.max(maxRow, cell.row);\n minCol = Math.min(minCol, cell.col);\n maxCol = Math.max(maxCol, cell.col);\n }\n }\n\n if (minRow === Infinity) {\n this._dataBoundsCache = null;\n this._boundsDirty = false;\n return null;\n }\n\n this._dataBoundsCache = { minRow, maxRow, minCol, maxCol };\n this._boundsDirty = false;\n return this._dataBoundsCache;\n }\n\n /**\n * Generate XML for this worksheet\n */\n toXml(): string {\n if (this._lazyParse && !this._dirty && this._rawXml) {\n return this._rawXml;\n }\n\n this._ensureParsed();\n const preserved = this._preserveXml && this._xmlNodes ? this._buildPreservedWorksheet() : null;\n // Build sheetData from cells\n const sheetDataNode = this._buildSheetDataNode();\n\n // Build worksheet structure\n const worksheetChildren: XmlNode[] = [];\n\n // Sheet views (freeze panes)\n if (this._frozenPane) {\n const paneAttrs: Record<string, string> = { state: 'frozen' };\n const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);\n paneAttrs.topLeftCell = topLeftCell;\n if (this._frozenPane.col > 0) {\n paneAttrs.xSplit = String(this._frozenPane.col);\n }\n if (this._frozenPane.row > 0) {\n paneAttrs.ySplit = String(this._frozenPane.row);\n }\n\n let activePane = 'bottomRight';\n if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {\n activePane = 'bottomLeft';\n } else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {\n activePane = 'topRight';\n }\n\n paneAttrs.activePane = activePane;\n const paneNode = createElement('pane', paneAttrs, []);\n const selectionNode = createElement(\n 'selection',\n { pane: activePane, activeCell: topLeftCell, sqref: topLeftCell },\n [],\n );\n\n const sheetViewNode = createElement('sheetView', { workbookViewId: '0' }, [paneNode, selectionNode]);\n worksheetChildren.push(createElement('sheetViews', {}, [sheetViewNode]));\n }\n\n // Column widths\n if (this._columnWidths.size > 0) {\n const colNodes: XmlNode[] = [];\n const entries = Array.from(this._columnWidths.entries()).sort((a, b) => a[0] - b[0]);\n for (const [colIndex, width] of entries) {\n colNodes.push(\n createElement(\n 'col',\n {\n min: String(colIndex + 1),\n max: String(colIndex + 1),\n width: String(width),\n customWidth: '1',\n },\n [],\n ),\n );\n }\n worksheetChildren.push(createElement('cols', {}, colNodes));\n }\n\n worksheetChildren.push(sheetDataNode);\n\n // Add merged cells if any\n if (this._mergedCells.size > 0) {\n const mergeCellNodes: XmlNode[] = [];\n for (const ref of this._mergedCells) {\n mergeCellNodes.push(createElement('mergeCell', { ref }, []));\n }\n const mergeCellsNode = createElement('mergeCells', { count: String(this._mergedCells.size) }, mergeCellNodes);\n worksheetChildren.push(mergeCellsNode);\n }\n\n // Add table parts if any tables exist\n const tablePartsNode = this._buildTablePartsNode();\n if (tablePartsNode) {\n worksheetChildren.push(tablePartsNode);\n }\n\n const pivotTablePartsNode = this._buildPivotTablePartsNode();\n if (pivotTablePartsNode) {\n worksheetChildren.push(pivotTablePartsNode);\n }\n\n const worksheetNode = createElement(\n 'worksheet',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n },\n worksheetChildren,\n );\n\n if (preserved) {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([preserved])}`;\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([worksheetNode])}`;\n }\n\n private _buildSheetDataNode(): XmlNode {\n const rowMap = new Map<number, Cell[]>();\n for (const cell of this._cells.values()) {\n const row = cell.row;\n if (!rowMap.has(row)) {\n rowMap.set(row, []);\n }\n rowMap.get(row)!.push(cell);\n }\n\n for (const rowIdx of this._rowHeights.keys()) {\n if (!rowMap.has(rowIdx)) {\n rowMap.set(rowIdx, []);\n }\n }\n\n const sortedRows = Array.from(rowMap.entries()).sort((a, b) => a[0] - b[0]);\n const rowNodes: XmlNode[] = [];\n for (const [rowIdx, cells] of sortedRows) {\n cells.sort((a, b) => a.col - b.col);\n\n const cellNodes: XmlNode[] = [];\n for (const cell of cells) {\n const cellNode = this._buildCellNode(cell);\n cellNodes.push(cellNode);\n }\n\n const rowAttrs: Record<string, string> = { r: String(rowIdx + 1) };\n const rowHeight = this._rowHeights.get(rowIdx);\n if (rowHeight !== undefined) {\n rowAttrs.ht = String(rowHeight);\n rowAttrs.customHeight = '1';\n }\n const rowNode = createElement('row', rowAttrs, cellNodes);\n rowNodes.push(rowNode);\n }\n\n return createElement('sheetData', {}, rowNodes);\n }\n\n private _buildSheetViewsNode(): XmlNode | null {\n if (!this._frozenPane) return null;\n const paneAttrs: Record<string, string> = { state: 'frozen' };\n const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);\n paneAttrs.topLeftCell = topLeftCell;\n if (this._frozenPane.col > 0) {\n paneAttrs.xSplit = String(this._frozenPane.col);\n }\n if (this._frozenPane.row > 0) {\n paneAttrs.ySplit = String(this._frozenPane.row);\n }\n\n let activePane = 'bottomRight';\n if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {\n activePane = 'bottomLeft';\n } else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {\n activePane = 'topRight';\n }\n\n paneAttrs.activePane = activePane;\n const paneNode = createElement('pane', paneAttrs, []);\n const selectionNode = createElement(\n 'selection',\n { pane: activePane, activeCell: topLeftCell, sqref: topLeftCell },\n [],\n );\n\n const sheetViewNode = createElement('sheetView', { workbookViewId: '0' }, [paneNode, selectionNode]);\n return createElement('sheetViews', {}, [sheetViewNode]);\n }\n\n private _buildColsNode(): XmlNode | null {\n if (this._columnWidths.size === 0) return null;\n const colNodes: XmlNode[] = [];\n const entries = Array.from(this._columnWidths.entries()).sort((a, b) => a[0] - b[0]);\n for (const [colIndex, width] of entries) {\n colNodes.push(\n createElement(\n 'col',\n {\n min: String(colIndex + 1),\n max: String(colIndex + 1),\n width: String(width),\n customWidth: '1',\n },\n [],\n ),\n );\n }\n return createElement('cols', {}, colNodes);\n }\n\n private _buildMergeCellsNode(): XmlNode | null {\n if (this._mergedCells.size === 0) return null;\n const mergeCellNodes: XmlNode[] = [];\n for (const ref of this._mergedCells) {\n mergeCellNodes.push(createElement('mergeCell', { ref }, []));\n }\n return createElement('mergeCells', { count: String(this._mergedCells.size) }, mergeCellNodes);\n }\n\n private _buildTablePartsNode(): XmlNode | null {\n if (this._tables.length === 0) return null;\n const tablePartNodes: XmlNode[] = [];\n for (let i = 0; i < this._tables.length; i++) {\n const relId =\n this._tableRelIds && this._tableRelIds.length === this._tables.length ? this._tableRelIds[i] : `rId${i + 1}`;\n tablePartNodes.push(createElement('tablePart', { 'r:id': relId }, []));\n }\n return createElement('tableParts', { count: String(this._tables.length) }, tablePartNodes);\n }\n\n private _buildPivotTablePartsNode(): XmlNode | null {\n if (!this._pivotTableRelIds || this._pivotTableRelIds.length === 0) return null;\n const pivotPartNodes: XmlNode[] = this._pivotTableRelIds.map((relId) =>\n createElement('pivotTablePart', { 'r:id': relId }, []),\n );\n return createElement('pivotTableParts', { count: String(pivotPartNodes.length) }, pivotPartNodes);\n }\n\n private _buildPreservedWorksheet(): XmlNode | null {\n if (!this._xmlNodes) return null;\n const worksheet = findElement(this._xmlNodes, 'worksheet');\n if (!worksheet) return null;\n\n const children = getChildren(worksheet, 'worksheet');\n\n const upsertChild = (tag: string, node: XmlNode | null) => {\n const existingIndex = children.findIndex((child) => tag in child);\n if (node) {\n if (existingIndex >= 0) {\n children[existingIndex] = node;\n } else {\n children.push(node);\n }\n } else if (existingIndex >= 0) {\n children.splice(existingIndex, 1);\n }\n };\n\n if (this._sheetViewsDirty) {\n const sheetViewsNode = this._buildSheetViewsNode();\n upsertChild('sheetViews', sheetViewsNode);\n }\n\n if (this._colsDirty) {\n const colsNode = this._buildColsNode();\n upsertChild('cols', colsNode);\n }\n\n const sheetDataNode = this._buildSheetDataNode();\n upsertChild('sheetData', sheetDataNode);\n\n const mergeCellsNode = this._buildMergeCellsNode();\n upsertChild('mergeCells', mergeCellsNode);\n\n if (this._tablePartsDirty) {\n const tablePartsNode = this._buildTablePartsNode();\n upsertChild('tableParts', tablePartsNode);\n }\n\n if (this._pivotTablePartsDirty) {\n const pivotTablePartsNode = this._buildPivotTablePartsNode();\n upsertChild('pivotTableParts', pivotTablePartsNode);\n }\n\n return worksheet;\n }\n\n /**\n * Build a cell XML node from a Cell object\n */\n private _buildCellNode(cell: Cell): XmlNode {\n const data = cell.data;\n const attrs: Record<string, string> = { r: cell.address };\n\n if (data.t && data.t !== 'n') {\n attrs.t = data.t;\n }\n if (data.s !== undefined) {\n attrs.s = String(data.s);\n }\n\n const children: XmlNode[] = [];\n\n // Formula\n if (data.f) {\n const fAttrs: Record<string, string> = {};\n if (data.F) fAttrs.ref = data.F;\n if (data.si !== undefined) fAttrs.si = String(data.si);\n children.push(createElement('f', fAttrs, [createText(data.f)]));\n }\n\n // Value\n if (data.v !== undefined) {\n children.push(createElement('v', {}, [createText(String(data.v))]));\n }\n\n return createElement('c', attrs, children);\n }\n}\n","import {\n parseXml,\n findElement,\n getChildren,\n getAttr,\n XmlNode,\n stringifyXml,\n createElement,\n createText,\n} from './utils/xml';\n\n/**\n * Manages the shared strings table from xl/sharedStrings.xml\n * Excel stores strings in a shared table to reduce file size\n */\nexport class SharedStrings {\n private entries: SharedStringEntry[] = [];\n private stringToIndex: Map<string, number> = new Map();\n private _dirty = false;\n private _totalCount = 0;\n private _rawXml: string | null = null;\n private _parsed = false;\n\n /**\n * Parse shared strings from XML content\n */\n static parse(xml: string): SharedStrings {\n const ss = new SharedStrings();\n ss._rawXml = xml;\n ss._parse();\n return ss;\n }\n\n private _parse(): void {\n if (this._parsed) return;\n if (!this._rawXml) {\n this._parsed = true;\n return;\n }\n\n const parsed = parseXml(this._rawXml);\n const sst = findElement(parsed, 'sst');\n if (!sst) {\n this._parsed = true;\n return;\n }\n\n const countAttr = getAttr(sst, 'count');\n if (countAttr) {\n const total = parseInt(countAttr, 10);\n if (Number.isFinite(total) && total >= 0) {\n this._totalCount = total;\n }\n }\n\n const children = getChildren(sst, 'sst');\n for (const child of children) {\n if ('si' in child) {\n const siChildren = getChildren(child, 'si');\n const text = this.extractText(siChildren);\n this.entries.push({ text, node: child });\n this.stringToIndex.set(text, this.entries.length - 1);\n }\n }\n\n if (this._totalCount === 0 && this.entries.length > 0) {\n this._totalCount = this.entries.length;\n }\n\n this._parsed = true;\n }\n\n /**\n * Extract text from a string item (si element)\n * Handles both simple <t> elements and rich text <r> elements\n */\n private extractText(nodes: XmlNode[]): string {\n let text = '';\n for (const node of nodes) {\n if ('t' in node) {\n // Simple text: <t>value</t>\n const tChildren = getChildren(node, 't');\n for (const child of tChildren) {\n if ('#text' in child) {\n text += child['#text'] as string;\n }\n }\n } else if ('r' in node) {\n // Rich text: <r><t>value</t></r>\n const rChildren = getChildren(node, 'r');\n for (const rChild of rChildren) {\n if ('t' in rChild) {\n const tChildren = getChildren(rChild, 't');\n for (const child of tChildren) {\n if ('#text' in child) {\n text += child['#text'] as string;\n }\n }\n }\n }\n }\n }\n return text;\n }\n\n /**\n * Get a string by index\n */\n getString(index: number): string | undefined {\n this._parse();\n return this.entries[index]?.text;\n }\n\n /**\n * Add a string and return its index\n * If the string already exists, returns the existing index\n */\n addString(str: string): number {\n this._parse();\n const existing = this.stringToIndex.get(str);\n if (existing !== undefined) {\n this._totalCount++;\n this._dirty = true;\n return existing;\n }\n const index = this.entries.length;\n const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [\n createText(str),\n ]);\n const siElement = createElement('si', {}, [tElement]);\n this.entries.push({ text: str, node: siElement });\n this.stringToIndex.set(str, index);\n this._totalCount++;\n this._dirty = true;\n return index;\n }\n\n /**\n * Check if the shared strings table has been modified\n */\n get dirty(): boolean {\n this._parse();\n return this._dirty;\n }\n\n /**\n * Get the count of strings\n */\n get count(): number {\n this._parse();\n return this.entries.length;\n }\n\n /**\n * Get total usage count of shared strings\n */\n get totalCount(): number {\n this._parse();\n return Math.max(this._totalCount, this.entries.length);\n }\n\n /**\n * Get all unique shared strings in insertion order.\n */\n getAllStrings(): string[] {\n this._parse();\n return this.entries.map((entry) => entry.text);\n }\n\n /**\n * Generate XML for the shared strings table\n */\n toXml(): string {\n this._parse();\n const siElements: XmlNode[] = [];\n for (const entry of this.entries) {\n if (entry.node) {\n siElements.push(entry.node);\n } else {\n const str = entry.text;\n const tElement = createElement(\n 't',\n str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {},\n [createText(str)],\n );\n const siElement = createElement('si', {}, [tElement]);\n siElements.push(siElement);\n }\n }\n\n const totalCount = Math.max(this._totalCount, this.entries.length);\n const sst = createElement(\n 'sst',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n count: String(totalCount),\n uniqueCount: String(this.entries.length),\n },\n siElements,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([sst])}`;\n }\n}\n\ninterface SharedStringEntry {\n text: string;\n node?: XmlNode;\n}\n","import type { CellStyle, BorderType } from './types';\nimport { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';\n\n/**\n * Excel built-in number format IDs (0-163 are reserved).\n * These formats don't need to be defined in the numFmts element.\n */\nconst BUILTIN_NUM_FMTS: Map<string, number> = new Map([\n ['General', 0],\n ['0', 1],\n ['0.00', 2],\n ['#,##0', 3],\n ['#,##0.00', 4],\n ['0%', 9],\n ['0.00%', 10],\n ['0.00E+00', 11],\n ['# ?/?', 12],\n ['# ??/??', 13],\n ['mm-dd-yy', 14],\n ['d-mmm-yy', 15],\n ['d-mmm', 16],\n ['mmm-yy', 17],\n ['h:mm AM/PM', 18],\n ['h:mm:ss AM/PM', 19],\n ['h:mm', 20],\n ['h:mm:ss', 21],\n ['m/d/yy h:mm', 22],\n ['#,##0 ;(#,##0)', 37],\n ['#,##0 ;[Red](#,##0)', 38],\n ['#,##0.00;(#,##0.00)', 39],\n ['#,##0.00;[Red](#,##0.00)', 40],\n ['mm:ss', 45],\n ['[h]:mm:ss', 46],\n ['mmss.0', 47],\n ['##0.0E+0', 48],\n ['@', 49],\n]);\n\n/**\n * Reverse lookup: built-in format ID -> format code\n */\nconst BUILTIN_NUM_FMT_CODES: Map<number, string> = new Map(\n Array.from(BUILTIN_NUM_FMTS.entries()).map(([code, id]) => [id, code]),\n);\n\n/**\n * Normalize a color to ARGB format (8 hex chars).\n * Accepts: \"#RGB\", \"#RRGGBB\", \"RGB\", \"RRGGBB\", \"AARRGGBB\", \"#AARRGGBB\"\n */\nconst normalizeColor = (color: string): string => {\n let c = color.replace(/^#/, '').toUpperCase();\n\n // Handle shorthand 3-char format (e.g., \"FFF\" -> \"FFFFFF\")\n if (c.length === 3) {\n c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];\n }\n\n // Add alpha channel if not present (6 chars -> 8 chars)\n if (c.length === 6) {\n c = 'FF' + c;\n }\n\n return c;\n};\n\ninterface StyleColor {\n rgb?: string;\n theme?: string;\n tint?: string;\n indexed?: string;\n}\n\n/**\n * Manages the styles (xl/styles.xml)\n */\nexport class Styles {\n private _numFmts: Map<number, string> = new Map();\n private _fonts: StyleFont[] = [];\n private _fills: StyleFill[] = [];\n private _borders: StyleBorder[] = [];\n private _cellXfs: CellXf[] = []; // Cell formats (combined style index)\n private _xmlNodes: XmlNode[] | null = null;\n private _rawXml: string | null = null;\n private _parsed = false;\n private _dirty = false;\n\n // Cache for style deduplication\n private _styleCache: Map<string, number> = new Map();\n private _styleObjectCache: Map<number, CellStyle> = new Map();\n\n /**\n * Generate a deterministic cache key for a style object.\n * More efficient than JSON.stringify as it avoids the overhead of\n * full JSON serialization and produces a consistent key regardless\n * of property order.\n */\n private _getStyleKey(style: CellStyle): string {\n // Use a delimiter that won't appear in values\n const SEP = '\\x00';\n\n // Build key from all style properties in a fixed order\n const parts: string[] = [\n style.bold ? '1' : '0',\n style.italic ? '1' : '0',\n style.underline === true ? '1' : style.underline === 'single' ? 's' : style.underline === 'double' ? 'd' : '0',\n style.strike ? '1' : '0',\n style.fontSize?.toString() ?? '',\n style.fontName ?? '',\n style.fontColor ?? '',\n style.fontColorTheme?.toString() ?? '',\n style.fontColorTint?.toString() ?? '',\n style.fontColorIndexed?.toString() ?? '',\n style.fill ?? '',\n style.fillTheme?.toString() ?? '',\n style.fillTint?.toString() ?? '',\n style.fillIndexed?.toString() ?? '',\n style.fillBgColor ?? '',\n style.fillBgTheme?.toString() ?? '',\n style.fillBgTint?.toString() ?? '',\n style.fillBgIndexed?.toString() ?? '',\n style.numberFormat ?? '',\n ];\n\n // Border properties\n if (style.border) {\n parts.push(style.border.top ?? '', style.border.bottom ?? '', style.border.left ?? '', style.border.right ?? '');\n } else {\n parts.push('', '', '', '');\n }\n\n // Alignment properties\n if (style.alignment) {\n parts.push(\n style.alignment.horizontal ?? '',\n style.alignment.vertical ?? '',\n style.alignment.wrapText ? '1' : '0',\n style.alignment.textRotation?.toString() ?? '',\n );\n } else {\n parts.push('', '', '0', '');\n }\n\n return parts.join(SEP);\n }\n\n /**\n * Parse styles from XML content\n */\n static parse(xml: string): Styles {\n const styles = new Styles();\n styles._rawXml = xml;\n styles._parse();\n return styles;\n }\n\n private _parse(): void {\n if (this._parsed) return;\n if (!this._rawXml) {\n this._parsed = true;\n return;\n }\n\n this._xmlNodes = parseXml(this._rawXml);\n\n if (!this._xmlNodes) {\n this._parsed = true;\n return;\n }\n\n const styleSheet = findElement(this._xmlNodes, 'styleSheet');\n if (!styleSheet) {\n this._parsed = true;\n return;\n }\n\n const children = getChildren(styleSheet, 'styleSheet');\n\n // Parse number formats\n const numFmts = findElement(children, 'numFmts');\n if (numFmts) {\n for (const child of getChildren(numFmts, 'numFmts')) {\n if ('numFmt' in child) {\n const id = parseInt(getAttr(child, 'numFmtId') || '0', 10);\n const code = getAttr(child, 'formatCode') || '';\n this._numFmts.set(id, code);\n }\n }\n }\n\n // Parse fonts\n const fonts = findElement(children, 'fonts');\n if (fonts) {\n for (const child of getChildren(fonts, 'fonts')) {\n if ('font' in child) {\n this._fonts.push(this._parseFont(child));\n }\n }\n }\n\n // Parse fills\n const fills = findElement(children, 'fills');\n if (fills) {\n for (const child of getChildren(fills, 'fills')) {\n if ('fill' in child) {\n this._fills.push(this._parseFill(child));\n }\n }\n }\n\n // Parse borders\n const borders = findElement(children, 'borders');\n if (borders) {\n for (const child of getChildren(borders, 'borders')) {\n if ('border' in child) {\n this._borders.push(this._parseBorder(child));\n }\n }\n }\n\n // Parse cellXfs (cell formats)\n const cellXfs = findElement(children, 'cellXfs');\n if (cellXfs) {\n for (const child of getChildren(cellXfs, 'cellXfs')) {\n if ('xf' in child) {\n this._cellXfs.push(this._parseCellXf(child));\n }\n }\n }\n\n this._parsed = true;\n }\n\n /**\n * Create an empty styles object with defaults\n */\n static createDefault(): Styles {\n const styles = new Styles();\n styles._parsed = true;\n\n // Default font (Calibri 11)\n styles._fonts.push({\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n size: 11,\n name: 'Calibri',\n color: undefined,\n });\n\n // Default fills (none and gray125 pattern are required)\n styles._fills.push({ type: 'none' });\n styles._fills.push({ type: 'gray125' });\n\n // Default border (none)\n styles._borders.push({});\n\n // Default cell format\n styles._cellXfs.push({\n fontId: 0,\n fillId: 0,\n borderId: 0,\n numFmtId: 0,\n });\n\n return styles;\n }\n\n private _parseFont(node: XmlNode): StyleFont {\n const font: StyleFont = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n };\n\n const children = getChildren(node, 'font');\n for (const child of children) {\n if ('b' in child) font.bold = true;\n if ('i' in child) font.italic = true;\n if ('u' in child) font.underline = true;\n if ('strike' in child) font.strike = true;\n if ('sz' in child) font.size = parseFloat(getAttr(child, 'val') || '11');\n if ('name' in child) font.name = getAttr(child, 'val');\n if ('color' in child) {\n const color: StyleColor = {};\n const rgb = getAttr(child, 'rgb');\n const theme = getAttr(child, 'theme');\n const tint = getAttr(child, 'tint');\n const indexed = getAttr(child, 'indexed');\n if (rgb) color.rgb = rgb;\n if (theme) color.theme = theme;\n if (tint) color.tint = tint;\n if (indexed) color.indexed = indexed;\n if (color.rgb || color.theme || color.tint || color.indexed) {\n font.color = color;\n }\n }\n }\n\n return font;\n }\n\n private _parseFill(node: XmlNode): StyleFill {\n const fill: StyleFill = { type: 'none' };\n const children = getChildren(node, 'fill');\n\n for (const child of children) {\n if ('patternFill' in child) {\n const pattern = getAttr(child, 'patternType');\n fill.type = pattern || 'none';\n\n const pfChildren = getChildren(child, 'patternFill');\n for (const pfChild of pfChildren) {\n if ('fgColor' in pfChild) {\n const color: StyleColor = {};\n const rgb = getAttr(pfChild, 'rgb');\n const theme = getAttr(pfChild, 'theme');\n const tint = getAttr(pfChild, 'tint');\n const indexed = getAttr(pfChild, 'indexed');\n if (rgb) color.rgb = rgb;\n if (theme) color.theme = theme;\n if (tint) color.tint = tint;\n if (indexed) color.indexed = indexed;\n if (color.rgb || color.theme || color.tint || color.indexed) {\n fill.fgColor = color;\n }\n }\n if ('bgColor' in pfChild) {\n const color: StyleColor = {};\n const rgb = getAttr(pfChild, 'rgb');\n const theme = getAttr(pfChild, 'theme');\n const tint = getAttr(pfChild, 'tint');\n const indexed = getAttr(pfChild, 'indexed');\n if (rgb) color.rgb = rgb;\n if (theme) color.theme = theme;\n if (tint) color.tint = tint;\n if (indexed) color.indexed = indexed;\n if (color.rgb || color.theme || color.tint || color.indexed) {\n fill.bgColor = color;\n }\n }\n }\n }\n }\n\n return fill;\n }\n\n private _parseBorder(node: XmlNode): StyleBorder {\n const border: StyleBorder = {};\n const children = getChildren(node, 'border');\n\n for (const child of children) {\n const style = getAttr(child, 'style') as BorderType | undefined;\n if ('left' in child && style) border.left = style;\n if ('right' in child && style) border.right = style;\n if ('top' in child && style) border.top = style;\n if ('bottom' in child && style) border.bottom = style;\n }\n\n return border;\n }\n\n private _parseCellXf(node: XmlNode): CellXf {\n return {\n fontId: parseInt(getAttr(node, 'fontId') || '0', 10),\n fillId: parseInt(getAttr(node, 'fillId') || '0', 10),\n borderId: parseInt(getAttr(node, 'borderId') || '0', 10),\n numFmtId: parseInt(getAttr(node, 'numFmtId') || '0', 10),\n alignment: this._parseAlignment(node),\n };\n }\n\n private _parseAlignment(node: XmlNode): AlignmentStyle | undefined {\n const children = getChildren(node, 'xf');\n const alignNode = findElement(children, 'alignment');\n if (!alignNode) return undefined;\n\n return {\n horizontal: getAttr(alignNode, 'horizontal') as AlignmentStyle['horizontal'],\n vertical: getAttr(alignNode, 'vertical') as AlignmentStyle['vertical'],\n wrapText: getAttr(alignNode, 'wrapText') === '1',\n textRotation: parseInt(getAttr(alignNode, 'textRotation') || '0', 10),\n };\n }\n\n /**\n * Get a style by index\n */\n getStyle(index: number): CellStyle {\n this._parse();\n const cached = this._styleObjectCache.get(index);\n if (cached) return { ...cached };\n\n const xf = this._cellXfs[index];\n if (!xf) return {};\n\n const font = this._fonts[xf.fontId];\n const fill = this._fills[xf.fillId];\n const border = this._borders[xf.borderId];\n // Check custom formats first, then fall back to built-in format codes\n const numFmt = this._numFmts.get(xf.numFmtId) ?? BUILTIN_NUM_FMT_CODES.get(xf.numFmtId);\n\n const style: CellStyle = {};\n\n if (font) {\n if (font.bold) style.bold = true;\n if (font.italic) style.italic = true;\n if (font.underline) style.underline = true;\n if (font.strike) style.strike = true;\n if (font.size) style.fontSize = font.size;\n if (font.name) style.fontName = font.name;\n if (font.color?.rgb) style.fontColor = font.color.rgb;\n if (font.color?.theme) style.fontColorTheme = Number(font.color.theme);\n if (font.color?.tint) style.fontColorTint = Number(font.color.tint);\n if (font.color?.indexed) style.fontColorIndexed = Number(font.color.indexed);\n }\n\n if (fill && fill.fgColor) {\n if (fill.fgColor.rgb) style.fill = fill.fgColor.rgb;\n if (fill.fgColor.theme) style.fillTheme = Number(fill.fgColor.theme);\n if (fill.fgColor.tint) style.fillTint = Number(fill.fgColor.tint);\n if (fill.fgColor.indexed) style.fillIndexed = Number(fill.fgColor.indexed);\n }\n\n if (fill && fill.bgColor) {\n if (fill.bgColor.rgb) style.fillBgColor = fill.bgColor.rgb;\n if (fill.bgColor.theme) style.fillBgTheme = Number(fill.bgColor.theme);\n if (fill.bgColor.tint) style.fillBgTint = Number(fill.bgColor.tint);\n if (fill.bgColor.indexed) style.fillBgIndexed = Number(fill.bgColor.indexed);\n }\n\n if (border) {\n if (border.top || border.bottom || border.left || border.right) {\n style.border = {\n top: border.top,\n bottom: border.bottom,\n left: border.left,\n right: border.right,\n };\n }\n }\n\n if (numFmt) {\n style.numberFormat = numFmt;\n }\n\n if (xf.alignment) {\n style.alignment = {\n horizontal: xf.alignment.horizontal,\n vertical: xf.alignment.vertical,\n wrapText: xf.alignment.wrapText,\n textRotation: xf.alignment.textRotation,\n };\n }\n\n this._styleObjectCache.set(index, { ...style });\n return style;\n }\n\n /**\n * Create a style and return its index\n * Uses caching to deduplicate identical styles\n */\n createStyle(style: CellStyle): number {\n this._parse();\n const key = this._getStyleKey(style);\n const cached = this._styleCache.get(key);\n if (cached !== undefined) {\n return cached;\n }\n\n this._dirty = true;\n\n // Create or find font\n const fontId = this._findOrCreateFont(style);\n\n // Create or find fill\n const fillId = this._findOrCreateFill(style);\n\n // Create or find border\n const borderId = this._findOrCreateBorder(style);\n\n // Create or find number format\n const numFmtId = style.numberFormat ? this._findOrCreateNumFmt(style.numberFormat) : 0;\n\n // Create cell format\n const xf: CellXf = {\n fontId,\n fillId,\n borderId,\n numFmtId,\n };\n\n if (style.alignment) {\n xf.alignment = {\n horizontal: style.alignment.horizontal,\n vertical: style.alignment.vertical,\n wrapText: style.alignment.wrapText,\n textRotation: style.alignment.textRotation,\n };\n }\n\n const index = this._cellXfs.length;\n this._cellXfs.push(xf);\n this._styleCache.set(key, index);\n this._styleObjectCache.set(index, { ...style });\n\n return index;\n }\n\n /**\n * Clone an existing style by index, optionally overriding fields.\n */\n cloneStyle(index: number, overrides: Partial<CellStyle> = {}): number {\n this._parse();\n const baseStyle = this.getStyle(index);\n return this.createStyle({ ...baseStyle, ...overrides });\n }\n\n private _findOrCreateFont(style: CellStyle): number {\n const color = this._toStyleColor(\n style.fontColor,\n style.fontColorTheme,\n style.fontColorTint,\n style.fontColorIndexed,\n );\n const font: StyleFont = {\n bold: style.bold || false,\n italic: style.italic || false,\n underline: style.underline === true || style.underline === 'single' || style.underline === 'double',\n strike: style.strike || false,\n size: style.fontSize,\n name: style.fontName,\n color,\n };\n\n // Try to find existing font\n for (let i = 0; i < this._fonts.length; i++) {\n const f = this._fonts[i];\n if (\n f.bold === font.bold &&\n f.italic === font.italic &&\n f.underline === font.underline &&\n f.strike === font.strike &&\n f.size === font.size &&\n f.name === font.name &&\n this._colorsEqual(f.color, font.color)\n ) {\n return i;\n }\n }\n\n // Create new font\n this._fonts.push(font);\n return this._fonts.length - 1;\n }\n\n private _findOrCreateFill(style: CellStyle): number {\n const fgColor = this._toStyleColor(style.fill, style.fillTheme, style.fillTint, style.fillIndexed);\n const bgColor = this._toStyleColor(style.fillBgColor, style.fillBgTheme, style.fillBgTint, style.fillBgIndexed);\n\n if (!fgColor && !bgColor) return 0;\n\n // Try to find existing fill\n for (let i = 0; i < this._fills.length; i++) {\n const f = this._fills[i];\n if (this._colorsEqual(f.fgColor, fgColor) && this._colorsEqual(f.bgColor, bgColor)) {\n return i;\n }\n }\n\n // Create new fill\n this._fills.push({\n type: 'solid',\n fgColor: fgColor || undefined,\n bgColor: bgColor || undefined,\n });\n return this._fills.length - 1;\n }\n\n private _findOrCreateBorder(style: CellStyle): number {\n if (!style.border) return 0;\n\n const border: StyleBorder = {\n top: style.border.top,\n bottom: style.border.bottom,\n left: style.border.left,\n right: style.border.right,\n };\n\n // Try to find existing border\n for (let i = 0; i < this._borders.length; i++) {\n const b = this._borders[i];\n if (b.top === border.top && b.bottom === border.bottom && b.left === border.left && b.right === border.right) {\n return i;\n }\n }\n\n // Create new border\n this._borders.push(border);\n return this._borders.length - 1;\n }\n\n private _findOrCreateNumFmt(format: string): number {\n // Check built-in formats first (IDs 0-163)\n const builtinId = BUILTIN_NUM_FMTS.get(format);\n if (builtinId !== undefined) {\n return builtinId;\n }\n\n // Check if already exists in custom formats\n for (const [id, code] of this._numFmts) {\n if (code === format) return id;\n }\n\n // Create new custom format (IDs 164+)\n const existingIds = Array.from(this._numFmts.keys());\n const id = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 164;\n this._numFmts.set(id, format);\n return id;\n }\n\n /**\n * Get or create a number format ID for the given format string.\n * Returns built-in IDs (0-163) for standard formats, or creates custom IDs (164+).\n * @param format - The number format string (e.g., '0.00', '#,##0', '$#,##0.00')\n */\n getOrCreateNumFmtId(format: string): number {\n this._parse();\n this._dirty = true;\n return this._findOrCreateNumFmt(format);\n }\n\n /**\n * Check if styles have been modified\n */\n get dirty(): boolean {\n this._parse();\n return this._dirty;\n }\n\n /**\n * Generate XML for styles\n */\n toXml(): string {\n this._parse();\n const children: XmlNode[] = [];\n\n // Number formats\n if (this._numFmts.size > 0) {\n const numFmtNodes: XmlNode[] = [];\n for (const [id, code] of this._numFmts) {\n numFmtNodes.push(createElement('numFmt', { numFmtId: String(id), formatCode: code }, []));\n }\n children.push(createElement('numFmts', { count: String(numFmtNodes.length) }, numFmtNodes));\n }\n\n // Fonts\n const fontNodes: XmlNode[] = this._fonts.map((font) => this._buildFontNode(font));\n children.push(createElement('fonts', { count: String(fontNodes.length) }, fontNodes));\n\n // Fills\n const fillNodes: XmlNode[] = this._fills.map((fill) => this._buildFillNode(fill));\n children.push(createElement('fills', { count: String(fillNodes.length) }, fillNodes));\n\n // Borders\n const borderNodes: XmlNode[] = this._borders.map((border) => this._buildBorderNode(border));\n children.push(createElement('borders', { count: String(borderNodes.length) }, borderNodes));\n\n // Cell style xfs (required but we just add a default)\n children.push(\n createElement('cellStyleXfs', { count: '1' }, [\n createElement('xf', { numFmtId: '0', fontId: '0', fillId: '0', borderId: '0' }, []),\n ]),\n );\n\n // Cell xfs\n const xfNodes: XmlNode[] = this._cellXfs.map((xf) => this._buildXfNode(xf));\n children.push(createElement('cellXfs', { count: String(xfNodes.length) }, xfNodes));\n\n // Cell styles (required)\n children.push(\n createElement('cellStyles', { count: '1' }, [\n createElement('cellStyle', { name: 'Normal', xfId: '0', builtinId: '0' }, []),\n ]),\n );\n\n const styleSheet = createElement(\n 'styleSheet',\n { xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' },\n children,\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([styleSheet])}`;\n }\n\n private _buildFontNode(font: StyleFont): XmlNode {\n const children: XmlNode[] = [];\n if (font.bold) children.push(createElement('b', {}, []));\n if (font.italic) children.push(createElement('i', {}, []));\n if (font.underline) children.push(createElement('u', {}, []));\n if (font.strike) children.push(createElement('strike', {}, []));\n if (font.size) children.push(createElement('sz', { val: String(font.size) }, []));\n if (font.color) {\n const attrs: Record<string, string> = {};\n if (font.color.rgb) attrs.rgb = normalizeColor(font.color.rgb);\n if (font.color.theme) attrs.theme = font.color.theme;\n if (font.color.tint) attrs.tint = font.color.tint;\n if (font.color.indexed) attrs.indexed = font.color.indexed;\n if (Object.keys(attrs).length > 0) {\n children.push(createElement('color', attrs, []));\n }\n }\n if (font.name) children.push(createElement('name', { val: font.name }, []));\n return createElement('font', {}, children);\n }\n\n private _buildFillNode(fill: StyleFill): XmlNode {\n const patternChildren: XmlNode[] = [];\n if (fill.fgColor) {\n const attrs: Record<string, string> = {};\n if (fill.fgColor.rgb) attrs.rgb = normalizeColor(fill.fgColor.rgb);\n if (fill.fgColor.theme) attrs.theme = fill.fgColor.theme;\n if (fill.fgColor.tint) attrs.tint = fill.fgColor.tint;\n if (fill.fgColor.indexed) attrs.indexed = fill.fgColor.indexed;\n if (Object.keys(attrs).length > 0) {\n patternChildren.push(createElement('fgColor', attrs, []));\n }\n // For solid fills, bgColor is required (indexed 64 = system background)\n if (fill.type === 'solid' && !fill.bgColor) {\n patternChildren.push(createElement('bgColor', { indexed: '64' }, []));\n }\n }\n if (fill.bgColor) {\n const attrs: Record<string, string> = {};\n if (fill.bgColor.rgb) attrs.rgb = normalizeColor(fill.bgColor.rgb);\n if (fill.bgColor.theme) attrs.theme = fill.bgColor.theme;\n if (fill.bgColor.tint) attrs.tint = fill.bgColor.tint;\n if (fill.bgColor.indexed) attrs.indexed = fill.bgColor.indexed;\n if (Object.keys(attrs).length > 0) {\n patternChildren.push(createElement('bgColor', attrs, []));\n }\n }\n const patternFill = createElement('patternFill', { patternType: fill.type || 'none' }, patternChildren);\n return createElement('fill', {}, [patternFill]);\n }\n\n private _toStyleColor(rgb?: string, theme?: number, tint?: number, indexed?: number): StyleColor | undefined {\n if (rgb) {\n return { rgb };\n }\n const color: StyleColor = {};\n if (theme !== undefined) color.theme = String(theme);\n if (tint !== undefined) color.tint = String(tint);\n if (indexed !== undefined) color.indexed = String(indexed);\n if (color.theme || color.tint || color.indexed) return color;\n return undefined;\n }\n\n private _colorsEqual(a?: StyleColor, b?: StyleColor): boolean {\n if (!a && !b) return true;\n if (!a || !b) return false;\n return a.rgb === b.rgb && a.theme === b.theme && a.tint === b.tint && a.indexed === b.indexed;\n }\n\n private _buildBorderNode(border: StyleBorder): XmlNode {\n const children: XmlNode[] = [];\n if (border.left) children.push(createElement('left', { style: border.left }, []));\n if (border.right) children.push(createElement('right', { style: border.right }, []));\n if (border.top) children.push(createElement('top', { style: border.top }, []));\n if (border.bottom) children.push(createElement('bottom', { style: border.bottom }, []));\n // Add empty elements if not present (required by Excel)\n if (!border.left) children.push(createElement('left', {}, []));\n if (!border.right) children.push(createElement('right', {}, []));\n if (!border.top) children.push(createElement('top', {}, []));\n if (!border.bottom) children.push(createElement('bottom', {}, []));\n children.push(createElement('diagonal', {}, []));\n return createElement('border', {}, children);\n }\n\n private _buildXfNode(xf: CellXf): XmlNode {\n const attrs: Record<string, string> = {\n numFmtId: String(xf.numFmtId),\n fontId: String(xf.fontId),\n fillId: String(xf.fillId),\n borderId: String(xf.borderId),\n };\n\n if (xf.fontId > 0) attrs.applyFont = '1';\n if (xf.fillId > 0) attrs.applyFill = '1';\n if (xf.borderId > 0) attrs.applyBorder = '1';\n if (xf.numFmtId > 0) attrs.applyNumberFormat = '1';\n\n const children: XmlNode[] = [];\n if (xf.alignment) {\n const alignAttrs: Record<string, string> = {};\n if (xf.alignment.horizontal) alignAttrs.horizontal = xf.alignment.horizontal;\n if (xf.alignment.vertical) alignAttrs.vertical = xf.alignment.vertical;\n if (xf.alignment.wrapText) alignAttrs.wrapText = '1';\n if (xf.alignment.textRotation) alignAttrs.textRotation = String(xf.alignment.textRotation);\n children.push(createElement('alignment', alignAttrs, []));\n attrs.applyAlignment = '1';\n }\n\n return createElement('xf', attrs, children);\n }\n}\n\n// Internal types for style components\ninterface StyleFont {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n size?: number;\n name?: string;\n color?: StyleColor;\n}\n\ninterface StyleFill {\n type: string;\n fgColor?: StyleColor;\n bgColor?: StyleColor;\n}\n\ninterface StyleBorder {\n top?: BorderType;\n bottom?: BorderType;\n left?: BorderType;\n right?: BorderType;\n}\n\ninterface CellXf {\n fontId: number;\n fillId: number;\n borderId: number;\n numFmtId: number;\n alignment?: AlignmentStyle;\n}\n\ninterface AlignmentStyle {\n horizontal?: 'left' | 'center' | 'right' | 'justify';\n vertical?: 'top' | 'middle' | 'bottom';\n wrapText?: boolean;\n textRotation?: number;\n}\n","import type {\n PivotAggregationType,\n PivotFieldFilter,\n PivotSortOrder,\n PivotTableConfig,\n PivotValueConfig,\n RangeAddress,\n CellValue,\n} from './types';\nimport type { Workbook } from './workbook';\nimport type { Worksheet } from './worksheet';\nimport { createElement, stringifyXml, XmlNode } from './utils/xml';\nimport { toAddress, toRange } from './utils/address';\n\nconst AGGREGATION_TO_XML: Record<PivotAggregationType, string> = {\n sum: 'sum',\n count: 'count',\n average: 'average',\n min: 'min',\n max: 'max',\n};\n\nconst SORT_TO_XML: Record<PivotSortOrder, 'ascending' | 'descending'> = {\n asc: 'ascending',\n desc: 'descending',\n};\n\ninterface PivotValueField {\n field: string;\n aggregation: PivotAggregationType;\n name: string;\n numberFormat?: string;\n}\n\ninterface PivotFieldMeta {\n name: string;\n sourceCol: number;\n}\n\ninterface PivotNumericInfo {\n nonNullCount: number;\n numericCount: number;\n min: number;\n max: number;\n hasNumeric: boolean;\n allIntegers: boolean;\n}\n\ninterface PivotCacheData {\n rowCount: number;\n recordNodes: XmlNode[];\n sharedItemIndexByField: Array<Map<string, number> | null>;\n sharedItemsByField: Array<XmlNode[] | null>;\n distinctItemsByField: Array<Exclude<CellValue, null>[] | null>;\n numericInfoByField: PivotNumericInfo[];\n isAxisFieldByIndex: boolean[];\n isValueFieldByIndex: boolean[];\n}\n\n/**\n * Represents an Excel PivotTable with a fluent configuration API.\n */\nexport class PivotTable {\n private _workbook: Workbook;\n private _name: string;\n private _sourceSheetName: string;\n private _sourceSheet: Worksheet;\n private _sourceRange: RangeAddress;\n private _targetSheetName: string;\n private _targetCell: { row: number; col: number };\n private _refreshOnLoad: boolean;\n private _cacheId: number;\n private _pivotId: number;\n private _cachePartIndex: number;\n private _fields: PivotFieldMeta[];\n\n private _rowFields: string[] = [];\n private _columnFields: string[] = [];\n private _filterFields: string[] = [];\n private _valueFields: PivotValueField[] = [];\n private _sortOrders: Map<string, PivotSortOrder> = new Map();\n private _filters: Map<string, PivotFieldFilter> = new Map();\n\n constructor(\n workbook: Workbook,\n config: PivotTableConfig,\n sourceSheetName: string,\n sourceSheet: Worksheet,\n sourceRange: RangeAddress,\n targetSheetName: string,\n targetCell: { row: number; col: number },\n cacheId: number,\n pivotId: number,\n cachePartIndex: number,\n fields: PivotFieldMeta[],\n ) {\n this._workbook = workbook;\n this._name = config.name;\n this._sourceSheetName = sourceSheetName;\n this._sourceSheet = sourceSheet;\n this._sourceRange = sourceRange;\n this._targetSheetName = targetSheetName;\n this._targetCell = targetCell;\n this._refreshOnLoad = config.refreshOnLoad !== false;\n this._cacheId = cacheId;\n this._pivotId = pivotId;\n this._cachePartIndex = cachePartIndex;\n this._fields = fields;\n }\n\n get name(): string {\n return this._name;\n }\n\n get sourceSheetName(): string {\n return this._sourceSheetName;\n }\n\n get sourceRange(): RangeAddress {\n return { start: { ...this._sourceRange.start }, end: { ...this._sourceRange.end } };\n }\n\n get targetSheetName(): string {\n return this._targetSheetName;\n }\n\n get targetCell(): { row: number; col: number } {\n return { ...this._targetCell };\n }\n\n get refreshOnLoad(): boolean {\n return this._refreshOnLoad;\n }\n\n get cacheId(): number {\n return this._cacheId;\n }\n\n get pivotId(): number {\n return this._pivotId;\n }\n\n get cachePartIndex(): number {\n return this._cachePartIndex;\n }\n\n addRowField(fieldName: string): this {\n this._assertFieldExists(fieldName);\n if (!this._rowFields.includes(fieldName)) {\n this._rowFields.push(fieldName);\n }\n return this;\n }\n\n addColumnField(fieldName: string): this {\n this._assertFieldExists(fieldName);\n if (!this._columnFields.includes(fieldName)) {\n this._columnFields.push(fieldName);\n }\n return this;\n }\n\n addFilterField(fieldName: string): this {\n this._assertFieldExists(fieldName);\n if (!this._filterFields.includes(fieldName)) {\n this._filterFields.push(fieldName);\n }\n return this;\n }\n\n addValueField(\n fieldName: string,\n aggregation?: PivotAggregationType,\n displayName?: string,\n numberFormat?: string,\n ): this;\n addValueField(config: PivotValueConfig): this;\n addValueField(\n fieldNameOrConfig: string | PivotValueConfig,\n aggregation: PivotAggregationType = 'sum',\n displayName?: string,\n numberFormat?: string,\n ): this {\n let config: PivotValueConfig;\n\n if (typeof fieldNameOrConfig === 'string') {\n config = {\n field: fieldNameOrConfig,\n aggregation,\n name: displayName,\n numberFormat,\n };\n } else {\n config = fieldNameOrConfig;\n }\n\n this._assertFieldExists(config.field);\n\n const resolvedAggregation = config.aggregation ?? 'sum';\n const resolvedName = config.name ?? `${this._aggregationLabel(resolvedAggregation)} of ${config.field}`;\n\n this._valueFields.push({\n field: config.field,\n aggregation: resolvedAggregation,\n name: resolvedName,\n numberFormat: config.numberFormat,\n });\n\n return this;\n }\n\n sortField(fieldName: string, order: PivotSortOrder): this {\n this._assertFieldExists(fieldName);\n if (!this._rowFields.includes(fieldName) && !this._columnFields.includes(fieldName)) {\n throw new Error(`Cannot sort field \"${fieldName}\": only row or column fields can be sorted`);\n }\n this._sortOrders.set(fieldName, order);\n return this;\n }\n\n filterField(fieldName: string, filter: PivotFieldFilter): this {\n this._assertFieldExists(fieldName);\n\n const hasInclude = 'include' in filter;\n const hasExclude = 'exclude' in filter;\n if ((hasInclude && hasExclude) || (!hasInclude && !hasExclude)) {\n throw new Error('Pivot filter must contain either include or exclude');\n }\n\n const values = hasInclude ? filter.include : filter.exclude;\n if (!values || values.length === 0) {\n throw new Error('Pivot filter values cannot be empty');\n }\n\n this._filters.set(fieldName, filter);\n return this;\n }\n\n toPivotCacheDefinitionXml(): string {\n const cacheData = this._buildPivotCacheData();\n return this._buildPivotCacheDefinitionXml(cacheData);\n }\n\n toPivotCacheRecordsXml(): string {\n const cacheData = this._buildPivotCacheData();\n return this._buildPivotCacheRecordsXml(cacheData);\n }\n\n toPivotCacheDefinitionRelsXml(): string {\n const relsRoot = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n [\n createElement(\n 'Relationship',\n {\n Id: 'rId1',\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',\n Target: `pivotCacheRecords${this._cachePartIndex}.xml`,\n },\n [],\n ),\n ],\n );\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([relsRoot])}`;\n }\n\n /**\n * @internal\n */\n buildPivotPartsXml(): {\n cacheDefinitionXml: string;\n cacheRecordsXml: string;\n cacheRelsXml: string;\n pivotTableXml: string;\n } {\n const cacheData = this._buildPivotCacheData();\n return {\n cacheDefinitionXml: this._buildPivotCacheDefinitionXml(cacheData),\n cacheRecordsXml: this._buildPivotCacheRecordsXml(cacheData),\n cacheRelsXml: this.toPivotCacheDefinitionRelsXml(),\n pivotTableXml: this._buildPivotTableDefinitionXml(cacheData),\n };\n }\n\n toPivotTableDefinitionXml(): string {\n const cacheData = this._buildPivotCacheData();\n return this._buildPivotTableDefinitionXml(cacheData);\n }\n\n private _buildPivotCacheDefinitionXml(cacheData: PivotCacheData): string {\n const cacheFieldNodes = this._fields.map((field, index) => this._buildCacheFieldNode(field, index, cacheData));\n\n const attrs: Record<string, string> = {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',\n 'mc:Ignorable': 'xr',\n 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',\n 'r:id': 'rId1',\n createdVersion: '8',\n minRefreshableVersion: '3',\n refreshedVersion: '8',\n refreshOnLoad: this._refreshOnLoad ? '1' : '0',\n recordCount: String(cacheData.rowCount),\n };\n\n const cacheSourceNode = createElement('cacheSource', { type: 'worksheet' }, [\n createElement('worksheetSource', { sheet: this._sourceSheetName, ref: toRange(this._sourceRange) }, []),\n ]);\n\n const cacheFieldsNode = createElement('cacheFields', { count: String(cacheFieldNodes.length) }, cacheFieldNodes);\n\n const extLstNode = createElement('extLst', {}, [\n createElement(\n 'ext',\n {\n uri: '{725AE2AE-9491-48be-B2B4-4EB974FC3084}',\n 'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main',\n },\n [createElement('x14:pivotCacheDefinition', {}, [])],\n ),\n ]);\n\n const root = createElement('pivotCacheDefinition', attrs, [cacheSourceNode, cacheFieldsNode, extLstNode]);\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([root])}`;\n }\n\n private _buildPivotCacheRecordsXml(cacheData: PivotCacheData): string {\n const recordNodes = cacheData.recordNodes;\n const root = createElement(\n 'pivotCacheRecords',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',\n 'mc:Ignorable': 'xr',\n 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',\n count: String(recordNodes.length),\n },\n recordNodes,\n );\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([root])}`;\n }\n\n private _buildPivotTableDefinitionXml(cacheData: PivotCacheData): string {\n const effectiveValueFields = this._valueFields.length > 0 ? [this._valueFields[0]] : [];\n const sourceFieldCount = this._fields.length;\n const pivotFields: XmlNode[] = [];\n const effectiveRowFieldName = this._rowFields[0];\n const rowFieldIndexes = effectiveRowFieldName ? [this._fieldIndex(effectiveRowFieldName)] : [];\n const colFieldIndexes = this._columnFields.length > 0 ? [this._fieldIndex(this._columnFields[0])] : [];\n const valueFieldIndexes = new Set<number>(\n effectiveValueFields.map((valueField) => this._fieldIndex(valueField.field)),\n );\n\n for (let index = 0; index < this._fields.length; index++) {\n const field = this._fields[index];\n const attrs: Record<string, string> = { showAll: '0' };\n\n if (rowFieldIndexes.includes(index)) {\n attrs.axis = 'axisRow';\n } else if (colFieldIndexes.includes(index)) {\n attrs.axis = 'axisCol';\n }\n\n if (valueFieldIndexes.has(index)) {\n attrs.dataField = '1';\n }\n\n const sortOrder = this._sortOrders.get(field.name);\n if (sortOrder) {\n attrs.sortType = SORT_TO_XML[sortOrder];\n }\n\n const children: XmlNode[] = [];\n if (rowFieldIndexes.includes(index) || colFieldIndexes.includes(index)) {\n const distinctItems = cacheData.distinctItemsByField[index] ?? [];\n const itemNodes: XmlNode[] = distinctItems.map((_item, itemIndex) =>\n createElement('item', { x: String(itemIndex) }, []),\n );\n itemNodes.push(createElement('item', { t: 'default' }, []));\n children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));\n }\n\n pivotFields.push(createElement('pivotField', attrs, children));\n }\n\n const children: XmlNode[] = [];\n\n const locationRef = this._buildTargetAreaRef(cacheData);\n children.push(\n createElement(\n 'location',\n {\n ref: locationRef,\n firstHeaderRow: '1',\n firstDataRow: '1',\n firstDataCol: String(Math.max(1, this._rowFields.length + 1)),\n },\n [],\n ),\n );\n\n children.push(createElement('pivotFields', { count: String(sourceFieldCount) }, pivotFields));\n\n if (rowFieldIndexes.length > 0) {\n children.push(\n createElement(\n 'rowFields',\n { count: String(rowFieldIndexes.length) },\n rowFieldIndexes.map((fieldIndex) => createElement('field', { x: String(fieldIndex) }, [])),\n ),\n );\n\n const distinctRowItems = cacheData.distinctItemsByField[rowFieldIndexes[0]] ?? [];\n const rowItemNodes: XmlNode[] = [];\n if (distinctRowItems.length > 0) {\n rowItemNodes.push(createElement('i', {}, [createElement('x', {}, [])]));\n for (let itemIndex = 1; itemIndex < distinctRowItems.length; itemIndex++) {\n rowItemNodes.push(createElement('i', {}, [createElement('x', { v: String(itemIndex) }, [])]));\n }\n }\n rowItemNodes.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));\n children.push(createElement('rowItems', { count: String(rowItemNodes.length) }, rowItemNodes));\n }\n\n if (colFieldIndexes.length > 0) {\n children.push(\n createElement(\n 'colFields',\n { count: String(colFieldIndexes.length) },\n colFieldIndexes.map((fieldIndex) => createElement('field', { x: String(fieldIndex) }, [])),\n ),\n );\n }\n\n // Excel expects colItems even when no explicit column fields are configured.\n children.push(createElement('colItems', { count: '1' }, [createElement('i', {}, [])]));\n\n if (this._filterFields.length > 0) {\n children.push(\n createElement(\n 'pageFields',\n { count: String(this._filterFields.length) },\n this._filterFields.map((field, index) =>\n createElement('pageField', { fld: String(this._fieldIndex(field)), hier: '-1', item: String(index) }, []),\n ),\n ),\n );\n }\n\n if (effectiveValueFields.length > 0) {\n children.push(\n createElement(\n 'dataFields',\n { count: String(effectiveValueFields.length) },\n effectiveValueFields.map((valueField) => {\n const attrs: Record<string, string> = {\n name: valueField.name,\n fld: String(this._fieldIndex(valueField.field)),\n baseField: '0',\n baseItem: '0',\n subtotal: AGGREGATION_TO_XML[valueField.aggregation],\n };\n\n if (valueField.numberFormat) {\n attrs.numFmtId = String(this._workbook.styles.getOrCreateNumFmtId(valueField.numberFormat));\n }\n\n return createElement('dataField', attrs, []);\n }),\n ),\n );\n }\n\n children.push(\n createElement(\n 'pivotTableStyleInfo',\n {\n name: 'PivotStyleMedium9',\n showRowHeaders: '1',\n showColHeaders: '1',\n showRowStripes: '0',\n showColStripes: '0',\n showLastColumn: '1',\n },\n [],\n ),\n );\n\n const attrs: Record<string, string> = {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n name: this._name,\n cacheId: String(this._cacheId),\n dataCaption: 'Values',\n applyNumberFormats: '1',\n applyBorderFormats: '0',\n applyFontFormats: '0',\n applyPatternFormats: '0',\n applyAlignmentFormats: '0',\n applyWidthHeightFormats: '1',\n updatedVersion: '8',\n minRefreshableVersion: '3',\n createdVersion: '8',\n useAutoFormatting: '1',\n rowGrandTotals: '1',\n colGrandTotals: '1',\n itemPrintTitles: '1',\n indent: '0',\n multipleFieldFilters: this._filters.size > 0 ? '1' : '0',\n outline: '1',\n outlineData: '1',\n };\n\n const root = createElement('pivotTableDefinition', attrs, children);\n return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([root])}`;\n }\n\n private _buildCacheFieldNode(field: PivotFieldMeta, fieldIndex: number, cacheData: PivotCacheData): XmlNode {\n const info = cacheData.numericInfoByField[fieldIndex];\n const isAxisField = cacheData.isAxisFieldByIndex[fieldIndex];\n const isValueField = cacheData.isValueFieldByIndex[fieldIndex];\n const allNonNullAreNumbers = info.nonNullCount > 0 && info.numericCount === info.nonNullCount;\n\n if (isValueField || (!isAxisField && allNonNullAreNumbers)) {\n const minValue = info.hasNumeric ? info.min : 0;\n const maxValue = info.hasNumeric ? info.max : 0;\n const hasInteger = info.hasNumeric ? info.allIntegers : true;\n\n const attrs: Record<string, string> = {\n containsSemiMixedTypes: '0',\n containsString: '0',\n containsNumber: '1',\n minValue: String(minValue),\n maxValue: String(maxValue),\n };\n if (hasInteger) {\n attrs.containsInteger = '1';\n }\n\n return createElement('cacheField', { name: field.name, numFmtId: '0' }, [\n createElement('sharedItems', attrs, []),\n ]);\n }\n\n if (!isAxisField) {\n return createElement('cacheField', { name: field.name, numFmtId: '0' }, [createElement('sharedItems', {}, [])]);\n }\n\n const sharedItems = cacheData.sharedItemsByField[fieldIndex] ?? [];\n return createElement('cacheField', { name: field.name, numFmtId: '0' }, [\n createElement('sharedItems', { count: String(sharedItems.length) }, sharedItems),\n ]);\n }\n\n private _buildTargetAreaRef(cacheData: PivotCacheData): string {\n const start = this._targetCell;\n const estimatedRows = Math.max(3, this._estimateOutputRows(cacheData));\n const estimatedCols = Math.max(1, this._rowFields.length + Math.max(1, this._valueFields.length));\n\n const endRow = start.row + estimatedRows - 1;\n const endCol = start.col + estimatedCols - 1;\n\n return `${toAddress(start.row, start.col)}:${toAddress(endRow, endCol)}`;\n }\n\n private _estimateOutputRows(cacheData: PivotCacheData): number {\n if (this._rowFields.length === 0) {\n return 3;\n }\n\n const rowFieldIndex = this._fieldIndex(this._rowFields[0]);\n const distinctItems = cacheData.distinctItemsByField[rowFieldIndex] ?? [];\n return Math.max(3, distinctItems.length + 2);\n }\n\n private _buildPivotCacheData(): PivotCacheData {\n const rowCount = Math.max(0, this._sourceRange.end.row - this._sourceRange.start.row);\n const fieldCount = this._fields.length;\n const recordNodes: XmlNode[] = new Array(rowCount);\n const sharedItemIndexByField: Array<Map<string, number> | null> = new Array(fieldCount).fill(null);\n const sharedItemsByField: Array<XmlNode[] | null> = new Array(fieldCount).fill(null);\n const distinctItemsByField: Array<Exclude<CellValue, null>[] | null> = new Array(fieldCount).fill(null);\n const numericInfoByField: PivotNumericInfo[] = new Array(fieldCount);\n const isAxisFieldByIndex: boolean[] = new Array(fieldCount);\n const isValueFieldByIndex: boolean[] = new Array(fieldCount);\n\n const effectiveRowField = this._rowFields[0] ?? null;\n const effectiveColumnField = this._columnFields[0] ?? null;\n const filterFields = new Set(this._filterFields);\n const valueFields = new Set(this._valueFields.map((valueField) => valueField.field));\n\n for (let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {\n const fieldName = this._fields[fieldIndex].name;\n const isAxisField =\n fieldName === effectiveRowField || fieldName === effectiveColumnField || filterFields.has(fieldName);\n const isValueField = valueFields.has(fieldName);\n\n isAxisFieldByIndex[fieldIndex] = isAxisField;\n isValueFieldByIndex[fieldIndex] = isValueField;\n\n if (isAxisField) {\n sharedItemIndexByField[fieldIndex] = new Map<string, number>();\n sharedItemsByField[fieldIndex] = [];\n distinctItemsByField[fieldIndex] = [];\n }\n\n numericInfoByField[fieldIndex] = {\n nonNullCount: 0,\n numericCount: 0,\n min: 0,\n max: 0,\n hasNumeric: false,\n allIntegers: true,\n };\n }\n\n for (let rowOffset = 0; rowOffset < rowCount; rowOffset++) {\n const row = this._sourceRange.start.row + 1 + rowOffset;\n const valueNodes: XmlNode[] = [];\n\n for (let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {\n const field = this._fields[fieldIndex];\n const cellValue = this._sourceSheet.getCellIfExists(row, field.sourceCol)?.value ?? null;\n\n if (cellValue !== null) {\n const numericInfo = numericInfoByField[fieldIndex];\n numericInfo.nonNullCount++;\n\n if (typeof cellValue === 'number' && Number.isFinite(cellValue)) {\n numericInfo.numericCount++;\n if (!numericInfo.hasNumeric) {\n numericInfo.min = cellValue;\n numericInfo.max = cellValue;\n numericInfo.hasNumeric = true;\n } else {\n if (cellValue < numericInfo.min) numericInfo.min = cellValue;\n if (cellValue > numericInfo.max) numericInfo.max = cellValue;\n }\n if (!Number.isInteger(cellValue)) {\n numericInfo.allIntegers = false;\n }\n }\n\n if (isAxisFieldByIndex[fieldIndex]) {\n const distinctMap = sharedItemIndexByField[fieldIndex]!;\n const key = this._distinctKey(cellValue as Exclude<CellValue, null>);\n let index = distinctMap.get(key);\n if (index === undefined) {\n index = distinctMap.size;\n distinctMap.set(key, index);\n distinctItemsByField[fieldIndex]!.push(cellValue as Exclude<CellValue, null>);\n const sharedNode = this._buildSharedItemNode(cellValue as Exclude<CellValue, null>);\n if (sharedNode) {\n sharedItemsByField[fieldIndex]!.push(sharedNode);\n }\n }\n valueNodes.push(createElement('x', { v: String(index) }, []));\n continue;\n }\n }\n\n valueNodes.push(this._buildRawCacheValueNode(cellValue));\n }\n\n recordNodes[rowOffset] = createElement('r', {}, valueNodes);\n }\n\n return {\n rowCount,\n recordNodes,\n sharedItemIndexByField,\n sharedItemsByField,\n distinctItemsByField,\n numericInfoByField,\n isAxisFieldByIndex,\n isValueFieldByIndex,\n };\n }\n\n private _buildSharedItemNode(value: Exclude<CellValue, null>): XmlNode | null {\n if (typeof value === 'string') {\n return { s: [], ':@': { '@_v': value } } as XmlNode;\n }\n\n if (typeof value === 'number') {\n return createElement('n', { v: String(value) }, []);\n }\n\n if (typeof value === 'boolean') {\n return createElement('b', { v: value ? '1' : '0' }, []);\n }\n\n if (value instanceof Date) {\n return createElement('d', { v: value.toISOString() }, []);\n }\n\n return null;\n }\n\n private _buildRawCacheValueNode(value: CellValue): XmlNode {\n if (value === null) {\n return createElement('m', {}, []);\n }\n\n if (typeof value === 'string') {\n return { s: [], ':@': { '@_v': value } } as XmlNode;\n }\n\n if (typeof value === 'number') {\n return createElement('n', { v: String(value) }, []);\n }\n\n if (typeof value === 'boolean') {\n return createElement('b', { v: value ? '1' : '0' }, []);\n }\n\n if (value instanceof Date) {\n return createElement('d', { v: value.toISOString() }, []);\n }\n\n return createElement('m', {}, []);\n }\n\n private _assertFieldExists(fieldName: string): void {\n if (!this._fields.some((field) => field.name === fieldName)) {\n throw new Error(`Pivot field not found: ${fieldName}`);\n }\n }\n\n private _fieldIndex(fieldName: string): number {\n const index = this._fields.findIndex((field) => field.name === fieldName);\n if (index < 0) {\n throw new Error(`Pivot field not found: ${fieldName}`);\n }\n return index;\n }\n\n private _aggregationLabel(aggregation: PivotAggregationType): string {\n switch (aggregation) {\n case 'sum':\n return 'Sum';\n case 'count':\n return 'Count';\n case 'average':\n return 'Average';\n case 'min':\n return 'Min';\n case 'max':\n return 'Max';\n }\n }\n\n private _distinctKey(value: Exclude<CellValue, null>): string {\n if (value instanceof Date) {\n return `d:${value.toISOString()}`;\n }\n if (typeof value === 'string') {\n return `s:${value}`;\n }\n if (typeof value === 'number') {\n return `n:${value}`;\n }\n if (typeof value === 'boolean') {\n return `b:${value ? 1 : 0}`;\n }\n if (typeof value === 'object' && value && 'error' in value) {\n return `e:${value.error}`;\n }\n return 'u:';\n }\n}\n","import { readFile, writeFile } from 'fs/promises';\nimport type {\n SheetDefinition,\n Relationship,\n CellValue,\n SheetFromDataConfig,\n ColumnConfig,\n RichCellValue,\n DateHandling,\n PivotTableConfig,\n RangeAddress,\n WorkbookReadOptions,\n} from './types';\nimport { Worksheet } from './worksheet';\nimport { SharedStrings } from './shared-strings';\nimport { Styles } from './styles';\nimport { PivotTable } from './pivot-table';\nimport { readZip, writeZip, readZipText, writeZipText, ZipStore, createZipStore } from './utils/zip';\nimport { parseAddress, parseSheetAddress, parseSheetRange } from './utils/address';\nimport { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';\n\n/**\n * Represents an Excel workbook (.xlsx file)\n */\nexport class Workbook {\n private _files: ZipStore = createZipStore();\n private _sheets: Map<string, Worksheet> = new Map();\n private _sheetDefs: SheetDefinition[] = [];\n private _relationships: Relationship[] = [];\n private _sharedStrings: SharedStrings | null = null;\n private _styles: Styles | null = null;\n private _sharedStringsXml: string | null = null;\n private _stylesXml: string | null = null;\n private _lazy = true;\n private _dirty = false;\n\n // Table support\n private _nextTableId = 1;\n\n // Pivot table support\n private _pivotTables: PivotTable[] = [];\n private _nextPivotTableId = 1;\n private _nextPivotCacheId = 1;\n\n // Date serialization handling\n private _dateHandling: DateHandling = 'jsDate';\n\n private _locale = 'fr-FR';\n\n private constructor() {\n // Lazy init\n }\n\n /**\n * Load a workbook from a file path\n */\n static async fromFile(path: string, options: WorkbookReadOptions = {}): Promise<Workbook> {\n const data = await readFile(path);\n return Workbook.fromBuffer(new Uint8Array(data), options);\n }\n\n /**\n * Load a workbook from a buffer\n */\n static async fromBuffer(data: Uint8Array, options: WorkbookReadOptions = {}): Promise<Workbook> {\n const workbook = new Workbook();\n workbook._lazy = options.lazy ?? true;\n workbook._files = await readZip(data, { lazy: workbook._lazy });\n\n // Parse workbook.xml for sheet definitions\n const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');\n if (workbookXml) {\n workbook._parseWorkbook(workbookXml);\n }\n\n // Parse relationships\n const relsXml = readZipText(workbook._files, 'xl/_rels/workbook.xml.rels');\n if (relsXml) {\n workbook._parseRelationships(relsXml);\n }\n\n // Parse shared strings\n // Store shared strings/styles XML for lazy parse\n workbook._sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml') ?? null;\n workbook._stylesXml = readZipText(workbook._files, 'xl/styles.xml') ?? null;\n\n return workbook;\n }\n\n /**\n * Create a new empty workbook\n */\n static create(): Workbook {\n const workbook = new Workbook();\n workbook._dirty = true;\n workbook._lazy = false;\n workbook._sharedStrings = new SharedStrings();\n workbook._styles = Styles.createDefault();\n\n return workbook;\n }\n\n /**\n * Get sheet names\n */\n get sheetNames(): string[] {\n return this._sheetDefs.map((s) => s.name);\n }\n\n /**\n * Get number of sheets\n */\n get sheetCount(): number {\n return this._sheetDefs.length;\n }\n\n /**\n * Get shared strings table\n */\n get sharedStrings(): SharedStrings {\n if (!this._sharedStrings) {\n if (this._sharedStringsXml) {\n this._sharedStrings = SharedStrings.parse(this._sharedStringsXml);\n } else {\n this._sharedStrings = new SharedStrings();\n }\n }\n return this._sharedStrings;\n }\n\n /**\n * Get styles\n */\n get styles(): Styles {\n if (!this._styles) {\n if (this._stylesXml) {\n this._styles = Styles.parse(this._stylesXml);\n } else {\n this._styles = Styles.createDefault();\n }\n }\n return this._styles;\n }\n\n /**\n * Get the workbook date handling strategy.\n */\n get dateHandling(): DateHandling {\n return this._dateHandling;\n }\n\n /**\n * Set the workbook date handling strategy.\n */\n set dateHandling(value: DateHandling) {\n this._dateHandling = value;\n }\n\n /**\n * Get the workbook locale for formatting.\n */\n get locale(): string {\n return this._locale;\n }\n\n /**\n * Set the workbook locale for formatting.\n */\n set locale(value: string) {\n this._locale = value;\n }\n\n /**\n * Get the next unique table ID for this workbook.\n * Table IDs must be unique across all worksheets.\n * @internal\n */\n getNextTableId(): number {\n return this._nextTableId++;\n }\n\n /**\n * Get all pivot tables in the workbook.\n */\n get pivotTables(): PivotTable[] {\n return [...this._pivotTables];\n }\n\n /**\n * Create a new pivot table.\n */\n createPivotTable(config: PivotTableConfig): PivotTable {\n if (!config.name || config.name.trim().length === 0) {\n throw new Error('Pivot table name is required');\n }\n\n if (this._pivotTables.some((pivot) => pivot.name === config.name)) {\n throw new Error(`Pivot table name already exists: ${config.name}`);\n }\n\n const sourceRef = parseSheetRange(config.source);\n const targetRef = parseSheetAddress(config.target);\n\n const sourceSheet = this.sheet(sourceRef.sheet);\n this.sheet(targetRef.sheet);\n\n const sourceRange = this._normalizeRange(sourceRef.range);\n if (sourceRange.start.row >= sourceRange.end.row) {\n throw new Error('Pivot source range must include a header row and at least one data row');\n }\n\n const fields = this._extractPivotFields(sourceSheet, sourceRange);\n\n const cacheId = this._nextPivotCacheId++;\n const pivotId = this._nextPivotTableId++;\n const cachePartIndex = this._pivotTables.length + 1;\n\n const pivot = new PivotTable(\n this,\n config,\n sourceRef.sheet,\n sourceSheet,\n sourceRange,\n targetRef.sheet,\n targetRef.address,\n cacheId,\n pivotId,\n cachePartIndex,\n fields,\n );\n\n this._pivotTables.push(pivot);\n this._dirty = true;\n\n return pivot;\n }\n\n private _extractPivotFields(\n sourceSheet: Worksheet,\n sourceRange: RangeAddress,\n ): { name: string; sourceCol: number }[] {\n const fields: { name: string; sourceCol: number }[] = [];\n const seen = new Set<string>();\n\n for (let col = sourceRange.start.col; col <= sourceRange.end.col; col++) {\n const headerCell = sourceSheet.getCellIfExists(sourceRange.start.row, col);\n const rawHeader = headerCell?.value;\n const name = rawHeader == null ? `Column${col - sourceRange.start.col + 1}` : String(rawHeader).trim();\n\n if (!name) {\n throw new Error(`Pivot source header is empty at column ${col + 1}`);\n }\n\n if (seen.has(name)) {\n throw new Error(`Duplicate pivot source header: ${name}`);\n }\n\n seen.add(name);\n fields.push({ name, sourceCol: col });\n }\n\n return fields;\n }\n\n private _normalizeRange(range: RangeAddress): RangeAddress {\n return {\n start: {\n row: Math.min(range.start.row, range.end.row),\n col: Math.min(range.start.col, range.end.col),\n },\n end: {\n row: Math.max(range.start.row, range.end.row),\n col: Math.max(range.start.col, range.end.col),\n },\n };\n }\n\n /**\n * Get a worksheet by name or index\n */\n sheet(nameOrIndex: string | number): Worksheet {\n let def: SheetDefinition | undefined;\n\n if (typeof nameOrIndex === 'number') {\n def = this._sheetDefs[nameOrIndex];\n } else {\n def = this._sheetDefs.find((s) => s.name === nameOrIndex);\n }\n\n if (!def) {\n throw new Error(`Sheet not found: ${nameOrIndex}`);\n }\n\n // Return cached worksheet if available\n if (this._sheets.has(def.name)) {\n return this._sheets.get(def.name)!;\n }\n\n // Load worksheet\n const worksheet = new Worksheet(this, def.name);\n\n // Find the relationship to get the file path\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n const sheetXml = readZipText(this._files, sheetPath);\n if (sheetXml) {\n worksheet.parse(sheetXml, { lazy: this._lazy });\n }\n }\n\n this._sheets.set(def.name, worksheet);\n return worksheet;\n }\n\n /**\n * Add a new worksheet\n */\n addSheet(name: string, index?: number): Worksheet {\n // Check for duplicate name\n if (this._sheetDefs.some((s) => s.name === name)) {\n throw new Error(`Sheet already exists: ${name}`);\n }\n\n this._dirty = true;\n\n // Generate new sheet ID and relationship ID\n const sheetId = Math.max(0, ...this._sheetDefs.map((s) => s.sheetId)) + 1;\n const rId = `rId${Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1}`;\n\n const def: SheetDefinition = { name, sheetId, rId };\n\n // Add relationship\n this._relationships.push({\n id: rId,\n type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',\n target: `worksheets/sheet${sheetId}.xml`,\n });\n\n // Insert at index or append\n if (index !== undefined && index >= 0 && index < this._sheetDefs.length) {\n this._sheetDefs.splice(index, 0, def);\n } else {\n this._sheetDefs.push(def);\n }\n\n // Create worksheet\n const worksheet = new Worksheet(this, name);\n this._sheets.set(name, worksheet);\n\n return worksheet;\n }\n\n /**\n * Delete a worksheet by name or index\n */\n deleteSheet(nameOrIndex: string | number): void {\n let index: number;\n\n if (typeof nameOrIndex === 'number') {\n index = nameOrIndex;\n } else {\n index = this._sheetDefs.findIndex((s) => s.name === nameOrIndex);\n }\n\n if (index < 0 || index >= this._sheetDefs.length) {\n throw new Error(`Sheet not found: ${nameOrIndex}`);\n }\n\n if (this._sheetDefs.length === 1) {\n throw new Error('Cannot delete the last sheet');\n }\n\n this._dirty = true;\n\n const def = this._sheetDefs[index];\n this._sheetDefs.splice(index, 1);\n this._sheets.delete(def.name);\n\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n this._files.delete(sheetPath);\n }\n\n // Remove relationship\n const relIndex = this._relationships.findIndex((r) => r.id === def.rId);\n if (relIndex >= 0) {\n this._relationships.splice(relIndex, 1);\n }\n }\n\n /**\n * Rename a worksheet\n */\n renameSheet(oldName: string, newName: string): void {\n const def = this._sheetDefs.find((s) => s.name === oldName);\n if (!def) {\n throw new Error(`Sheet not found: ${oldName}`);\n }\n\n if (this._sheetDefs.some((s) => s.name === newName)) {\n throw new Error(`Sheet already exists: ${newName}`);\n }\n\n this._dirty = true;\n\n // Update cached worksheet\n const worksheet = this._sheets.get(oldName);\n if (worksheet) {\n worksheet.name = newName;\n this._sheets.delete(oldName);\n this._sheets.set(newName, worksheet);\n }\n\n def.name = newName;\n }\n\n /**\n * Copy a worksheet\n */\n copySheet(sourceName: string, newName: string): Worksheet {\n const source = this.sheet(sourceName);\n const copy = this.addSheet(newName);\n\n // Copy all cells\n for (const [address, cell] of source.cells) {\n const newCell = copy.cell(address);\n newCell.value = cell.value;\n if (cell.formula) {\n newCell.formula = cell.formula;\n }\n if (cell.styleIndex !== undefined) {\n newCell.styleIndex = cell.styleIndex;\n }\n }\n\n // Copy column widths\n for (const [col, width] of source.getColumnWidths()) {\n copy.setColumnWidth(col, width);\n }\n\n // Copy row heights\n for (const [row, height] of source.getRowHeights()) {\n copy.setRowHeight(row, height);\n }\n\n // Copy frozen panes\n const frozen = source.getFrozenPane();\n if (frozen) {\n copy.freezePane(frozen.row, frozen.col);\n }\n\n // Copy merged cells\n for (const mergedRange of source.mergedCells) {\n copy.mergeCells(mergedRange);\n }\n\n // Copy tables\n for (const table of source.tables) {\n const tableName = this._createUniqueTableName(table.name, newName);\n const newTable = copy.createTable({\n name: tableName,\n range: table.baseRange,\n totalRow: table.hasTotalRow,\n headerRow: table.hasHeaderRow,\n style: table.style,\n });\n\n if (!table.hasAutoFilter) {\n newTable.setAutoFilter(false);\n }\n\n if (table.hasTotalRow) {\n for (const columnName of table.columns) {\n const fn = table.getTotalFunction(columnName);\n if (fn) {\n newTable.setTotalFunction(columnName, fn);\n }\n }\n }\n }\n\n return copy;\n }\n\n private _createUniqueTableName(base: string, sheetName: string): string {\n const normalizedSheet = sheetName.replace(/[^A-Za-z0-9_.]/g, '_');\n const sanitizedBase = this._sanitizeTableName(`${base}_${normalizedSheet || 'Sheet'}`);\n let candidate = sanitizedBase;\n let counter = 1;\n\n while (this._hasTableName(candidate)) {\n candidate = `${sanitizedBase}_${counter++}`;\n }\n\n return candidate;\n }\n\n private _sanitizeTableName(name: string): string {\n let result = name.replace(/[^A-Za-z0-9_.]/g, '_');\n if (!/^[A-Za-z_]/.test(result)) {\n result = `_${result}`;\n }\n if (result.length === 0) {\n result = 'Table';\n }\n return result;\n }\n\n private _hasTableName(name: string): boolean {\n for (const sheetName of this.sheetNames) {\n const ws = this.sheet(sheetName);\n for (const table of ws.tables) {\n if (table.name === name) return true;\n }\n }\n return false;\n }\n\n /**\n * Create a new worksheet from an array of objects.\n *\n * The first row contains headers (object keys or custom column headers),\n * and subsequent rows contain the object values.\n *\n * @param config - Configuration for the sheet creation\n * @returns The created Worksheet\n *\n * @example\n * ```typescript\n * const data = [\n * { name: 'Alice', age: 30, city: 'Paris' },\n * { name: 'Bob', age: 25, city: 'London' },\n * { name: 'Charlie', age: 35, city: 'Berlin' },\n * ];\n *\n * // Simple usage - all object keys become columns\n * const sheet = wb.addSheetFromData({\n * name: 'People',\n * data: data,\n * });\n *\n * // With custom column configuration\n * const sheet2 = wb.addSheetFromData({\n * name: 'People Custom',\n * data: data,\n * columns: [\n * { key: 'name', header: 'Full Name' },\n * { key: 'age', header: 'Age (years)' },\n * ],\n * });\n *\n * // With rich cell values (value, formula, style)\n * const dataWithFormulas = [\n * { product: 'Widget', price: 10, qty: 5, total: { formula: 'B2*C2', style: { bold: true } } },\n * { product: 'Gadget', price: 20, qty: 3, total: { formula: 'B3*C3', style: { bold: true } } },\n * ];\n * const sheet3 = wb.addSheetFromData({\n * name: 'With Formulas',\n * data: dataWithFormulas,\n * });\n * ```\n */\n addSheetFromData<T extends object>(config: SheetFromDataConfig<T>): Worksheet {\n const { name, data, columns, headerStyle = true, startCell = 'A1' } = config;\n\n if (!data?.length) return this.addSheet(name);\n\n // Create the new sheet\n const sheet = this.addSheet(name);\n\n // Parse start cell\n const startAddr = parseAddress(startCell);\n let startRow = startAddr.row;\n const startCol = startAddr.col;\n\n // Determine columns to use\n const columnConfigs: ColumnConfig<T>[] = columns ?? this._inferColumns(data[0]);\n\n // Write header row\n for (let colIdx = 0; colIdx < columnConfigs.length; colIdx++) {\n const colConfig = columnConfigs[colIdx];\n const headerText = colConfig.header ?? String(colConfig.key);\n const cell = sheet.cell(startRow, startCol + colIdx);\n cell.value = headerText;\n\n // Apply header style if enabled\n if (headerStyle) {\n cell.style = { bold: true };\n }\n }\n\n // Move to data rows\n startRow++;\n\n // Write data rows\n for (let rowIdx = 0; rowIdx < data.length; rowIdx++) {\n const rowData = data[rowIdx];\n\n for (let colIdx = 0; colIdx < columnConfigs.length; colIdx++) {\n const colConfig = columnConfigs[colIdx];\n const value = rowData[colConfig.key];\n const cell = sheet.cell(startRow + rowIdx, startCol + colIdx);\n\n // Check if value is a rich cell definition\n if (this._isRichCellValue(value)) {\n const richValue = value as RichCellValue;\n if (richValue.value !== undefined) cell.value = richValue.value;\n if (richValue.formula !== undefined) cell.formula = richValue.formula;\n if (richValue.style !== undefined) cell.style = richValue.style;\n } else {\n // Convert value to CellValue\n cell.value = this._toCellValue(value);\n }\n\n // Apply column style if defined (merged with cell style)\n if (colConfig.style) {\n cell.style = { ...cell.style, ...colConfig.style };\n }\n }\n }\n\n return sheet;\n }\n\n /**\n * Check if a value is a rich cell value object with value, formula, or style fields\n */\n private _isRichCellValue(value: unknown): value is RichCellValue {\n if (value === null || value === undefined) {\n return false;\n }\n if (typeof value !== 'object' || value instanceof Date) {\n return false;\n }\n // Check if it has at least one of the rich cell properties\n const obj = value as Record<string, unknown>;\n return 'value' in obj || 'formula' in obj || 'style' in obj;\n }\n\n /**\n * Infer column configuration from the first data object\n */\n private _inferColumns<T extends object>(sample: T): ColumnConfig<T>[] {\n return (Object.keys(sample) as (keyof T)[]).map((key) => ({\n key,\n }));\n }\n\n /**\n * Convert an unknown value to a CellValue\n */\n private _toCellValue(value: unknown): CellValue {\n if (value === null || value === undefined) {\n return null;\n }\n if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {\n return value;\n }\n if (value instanceof Date) {\n return value;\n }\n if (typeof value === 'object' && 'error' in value) {\n return value as CellValue;\n }\n // Convert other types to string\n return String(value);\n }\n\n /**\n * Save the workbook to a file\n */\n async toFile(path: string): Promise<void> {\n const buffer = await this.toBuffer();\n await writeFile(path, buffer);\n }\n\n /**\n * Save the workbook to a buffer\n */\n async toBuffer(): Promise<Uint8Array> {\n // Update files map with modified content\n this._updateFiles();\n\n // Write ZIP\n return writeZip(this._files);\n }\n\n private _parseWorkbook(xml: string): void {\n const parsed = parseXml(xml);\n const workbook = findElement(parsed, 'workbook');\n if (!workbook) return;\n\n const children = getChildren(workbook, 'workbook');\n const sheets = findElement(children, 'sheets');\n if (!sheets) return;\n\n for (const child of getChildren(sheets, 'sheets')) {\n if ('sheet' in child) {\n const name = getAttr(child, 'name');\n const sheetId = getAttr(child, 'sheetId');\n const rId = getAttr(child, 'r:id');\n\n if (name && sheetId && rId) {\n this._sheetDefs.push({\n name,\n sheetId: parseInt(sheetId, 10),\n rId,\n });\n }\n }\n }\n }\n\n private _parseRelationships(xml: string): void {\n const parsed = parseXml(xml);\n const rels = findElement(parsed, 'Relationships');\n if (!rels) return;\n\n for (const child of getChildren(rels, 'Relationships')) {\n if ('Relationship' in child) {\n const id = getAttr(child, 'Id');\n const type = getAttr(child, 'Type');\n const target = getAttr(child, 'Target');\n\n if (id && type && target) {\n this._relationships.push({ id, type, target });\n }\n }\n }\n }\n\n private _updateFiles(): void {\n const relationshipInfo = this._buildRelationshipInfo();\n\n // Update workbook.xml\n this._updateWorkbookXml(relationshipInfo.pivotCacheRelByTarget);\n\n // Update relationships\n this._updateRelationshipsXml(relationshipInfo.relNodes);\n\n // Update content types\n this._updateContentTypes();\n\n // Update shared strings if modified\n if (this._sharedStrings) {\n if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {\n writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());\n }\n } else if (this._sharedStringsXml) {\n writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStringsXml);\n }\n\n // Update styles if modified or if file doesn't exist yet\n if (this._styles) {\n if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {\n writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());\n }\n } else if (this._stylesXml) {\n writeZipText(this._files, 'xl/styles.xml', this._stylesXml);\n }\n\n // Update worksheets\n for (const [name, worksheet] of this._sheets) {\n if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {\n const def = this._sheetDefs.find((s) => s.name === name);\n if (def) {\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n writeZipText(this._files, sheetPath, worksheet.toXml());\n }\n }\n }\n }\n\n // Update tables (sets table rel IDs for tableParts)\n this._updateTableFiles();\n\n // Update pivot tables (sets pivot rel IDs for pivotTableParts)\n this._updatePivotFiles();\n\n // Update worksheets to align tableParts with relationship IDs\n for (const [name, worksheet] of this._sheets) {\n if (worksheet.dirty || this._dirty || worksheet.tables.length > 0 || this._pivotTables.length > 0) {\n const def = this._sheetDefs.find((s) => s.name === name);\n if (def) {\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n const sheetPath = `xl/${rel.target}`;\n writeZipText(this._files, sheetPath, worksheet.toXml());\n }\n }\n }\n }\n }\n\n private _updateWorkbookXml(pivotCacheRelByTarget: Map<string, string>): void {\n const sheetNodes: XmlNode[] = this._sheetDefs.map((def) =>\n createElement('sheet', { name: def.name, sheetId: String(def.sheetId), 'r:id': def.rId }, []),\n );\n\n const sheetsNode = createElement('sheets', {}, sheetNodes);\n\n const children: XmlNode[] = [sheetsNode];\n\n if (this._pivotTables.length > 0) {\n const pivotCacheNodes: XmlNode[] = [];\n for (const pivot of this._pivotTables) {\n const target = `pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;\n const relId = pivotCacheRelByTarget.get(target);\n if (!relId) continue;\n pivotCacheNodes.push(createElement('pivotCache', { cacheId: String(pivot.cacheId), 'r:id': relId }, []));\n }\n\n if (pivotCacheNodes.length > 0) {\n children.push(createElement('pivotCaches', {}, pivotCacheNodes));\n }\n }\n\n const workbookNode = createElement(\n 'workbook',\n {\n xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',\n 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',\n },\n children,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([workbookNode])}`;\n writeZipText(this._files, 'xl/workbook.xml', xml);\n }\n\n private _updateRelationshipsXml(relNodes: XmlNode[]): void {\n const relsNode = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n relNodes,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([relsNode])}`;\n writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);\n }\n\n private _buildRelationshipInfo(): { relNodes: XmlNode[]; pivotCacheRelByTarget: Map<string, string> } {\n const relNodes: XmlNode[] = this._relationships.map((rel) =>\n createElement('Relationship', { Id: rel.id, Type: rel.type, Target: rel.target }, []),\n );\n const pivotCacheRelByTarget = new Map<string, string>();\n\n const reservedRelIds = new Set<string>(relNodes.map((node) => getAttr(node, 'Id') || '').filter(Boolean));\n let nextRelId = Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;\n\n const allocateRelId = (): string => {\n while (reservedRelIds.has(`rId${nextRelId}`)) {\n nextRelId++;\n }\n const id = `rId${nextRelId}`;\n nextRelId++;\n reservedRelIds.add(id);\n return id;\n };\n\n // Add shared strings relationship if needed\n const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;\n if (shouldIncludeSharedStrings) {\n const hasSharedStrings = this._relationships.some(\n (r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',\n );\n if (!hasSharedStrings) {\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: allocateRelId(),\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',\n Target: 'sharedStrings.xml',\n },\n [],\n ),\n );\n }\n }\n\n // Add styles relationship if needed\n const hasStyles = this._relationships.some(\n (r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',\n );\n if (!hasStyles) {\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: allocateRelId(),\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',\n Target: 'styles.xml',\n },\n [],\n ),\n );\n }\n\n for (const pivot of this._pivotTables) {\n const target = `pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;\n const hasPivotCacheRel = relNodes.some(\n (node) =>\n getAttr(node, 'Type') ===\n 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition' &&\n getAttr(node, 'Target') === target,\n );\n\n if (!hasPivotCacheRel) {\n const id = allocateRelId();\n pivotCacheRelByTarget.set(target, id);\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: id,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',\n Target: target,\n },\n [],\n ),\n );\n } else {\n const existing = relNodes.find(\n (node) =>\n getAttr(node, 'Type') ===\n 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition' &&\n getAttr(node, 'Target') === target,\n );\n const existingId = existing ? getAttr(existing, 'Id') : undefined;\n if (existingId) {\n pivotCacheRelByTarget.set(target, existingId);\n }\n }\n }\n\n return { relNodes, pivotCacheRelByTarget };\n }\n\n private _updateContentTypes(): void {\n const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;\n const types: XmlNode[] = [\n createElement(\n 'Default',\n { Extension: 'rels', ContentType: 'application/vnd.openxmlformats-package.relationships+xml' },\n [],\n ),\n createElement('Default', { Extension: 'xml', ContentType: 'application/xml' }, []),\n createElement(\n 'Override',\n {\n PartName: '/xl/workbook.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml',\n },\n [],\n ),\n createElement(\n 'Override',\n {\n PartName: '/xl/styles.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml',\n },\n [],\n ),\n ];\n\n // Add shared strings if present\n if (shouldIncludeSharedStrings) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: '/xl/sharedStrings.xml',\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml',\n },\n [],\n ),\n );\n }\n\n // Add worksheets\n for (const def of this._sheetDefs) {\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (rel) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/${rel.target}`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml',\n },\n [],\n ),\n );\n }\n }\n\n // Add tables\n let tableIndex = 1;\n for (const def of this._sheetDefs) {\n const worksheet = this._sheets.get(def.name);\n if (worksheet) {\n for (let i = 0; i < worksheet.tables.length; i++) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/tables/table${tableIndex}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml',\n },\n [],\n ),\n );\n tableIndex++;\n }\n }\n }\n\n // Add pivot caches and pivot tables\n for (const pivot of this._pivotTables) {\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml',\n },\n [],\n ),\n );\n\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotCache/pivotCacheRecords${pivot.cachePartIndex}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml',\n },\n [],\n ),\n );\n\n types.push(\n createElement(\n 'Override',\n {\n PartName: `/xl/pivotTables/pivotTable${pivot.pivotId}.xml`,\n ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml',\n },\n [],\n ),\n );\n }\n\n const existingTypesXml = readZipText(this._files, '[Content_Types].xml');\n const existingKeys = new Set(\n types\n .map((t) => {\n if ('Default' in t) {\n const a = t[':@'] as Record<string, string> | undefined;\n return `Default:${a?.['@_Extension'] || ''}`;\n }\n if ('Override' in t) {\n const a = t[':@'] as Record<string, string> | undefined;\n return `Override:${a?.['@_PartName'] || ''}`;\n }\n return '';\n })\n .filter(Boolean),\n );\n if (existingTypesXml) {\n const parsed = parseXml(existingTypesXml);\n const typesElement = findElement(parsed, 'Types');\n if (typesElement) {\n const existingNodes = getChildren(typesElement, 'Types');\n for (const node of existingNodes) {\n if ('Default' in node || 'Override' in node) {\n const type = 'Default' in node ? 'Default' : 'Override';\n const attrs = node[':@'] as Record<string, string> | undefined;\n const key =\n type === 'Default'\n ? `Default:${attrs?.['@_Extension'] || ''}`\n : `Override:${attrs?.['@_PartName'] || ''}`;\n if (!existingKeys.has(key)) {\n types.push(node);\n existingKeys.add(key);\n }\n }\n }\n }\n }\n\n const typesNode = createElement(\n 'Types',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types' },\n types,\n );\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([typesNode])}`;\n writeZipText(this._files, '[Content_Types].xml', xml);\n\n // Also ensure _rels/.rels exists\n const rootRelsXml = readZipText(this._files, '_rels/.rels');\n if (!rootRelsXml) {\n const rootRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n [\n createElement(\n 'Relationship',\n {\n Id: 'rId1',\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',\n Target: 'xl/workbook.xml',\n },\n [],\n ),\n ],\n );\n writeZipText(\n this._files,\n '_rels/.rels',\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([rootRels])}`,\n );\n }\n }\n\n /**\n * Generate all table related files\n */\n private _updateTableFiles(): void {\n // Collect all tables with their global indices\n let globalTableIndex = 1;\n const sheetTables: Map<string, { table: import('./table').Table; globalIndex: number }[]> = new Map();\n\n for (const def of this._sheetDefs) {\n const worksheet = this._sheets.get(def.name);\n if (!worksheet) continue;\n\n const tables = worksheet.tables;\n if (tables.length === 0) continue;\n\n const tableInfos: { table: import('./table').Table; globalIndex: number }[] = [];\n for (const table of tables) {\n tableInfos.push({ table, globalIndex: globalTableIndex });\n globalTableIndex++;\n }\n sheetTables.set(def.name, tableInfos);\n }\n\n // Generate table files\n for (const [, tableInfos] of sheetTables) {\n for (const { table, globalIndex } of tableInfos) {\n const tablePath = `xl/tables/table${globalIndex}.xml`;\n writeZipText(this._files, tablePath, table.toXml());\n }\n }\n\n // Generate worksheet relationships for tables\n for (const [sheetName, tableInfos] of sheetTables) {\n const def = this._sheetDefs.find((s) => s.name === sheetName);\n if (!def) continue;\n\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (!rel) continue;\n\n // Extract sheet file name from target path\n const sheetFileName = rel.target.split('/').pop();\n const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;\n\n // Check if there are already pivot table relationships for this sheet\n const existingRelsXml = readZipText(this._files, sheetRelsPath);\n let nextRelId = 1;\n const relNodes: XmlNode[] = [];\n const reservedRelIds = new Set<string>();\n\n if (existingRelsXml) {\n // Parse existing rels and find max rId\n const parsed = parseXml(existingRelsXml);\n const relsElement = findElement(parsed, 'Relationships');\n if (relsElement) {\n const existingRelNodes = getChildren(relsElement, 'Relationships');\n for (const relNode of existingRelNodes) {\n if ('Relationship' in relNode) {\n relNodes.push(relNode);\n const id = getAttr(relNode, 'Id');\n if (id) {\n reservedRelIds.add(id);\n const idNum = parseInt(id.replace('rId', ''), 10);\n if (idNum >= nextRelId) {\n nextRelId = idNum + 1;\n }\n }\n }\n }\n }\n }\n\n const allocateRelId = (): string => {\n while (reservedRelIds.has(`rId${nextRelId}`)) {\n nextRelId++;\n }\n const id = `rId${nextRelId}`;\n nextRelId++;\n reservedRelIds.add(id);\n return id;\n };\n\n // Add table relationships\n const tableRelIds: string[] = [];\n for (const { globalIndex } of tableInfos) {\n const target = `../tables/table${globalIndex}.xml`;\n const existing = relNodes.some(\n (node) =>\n getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&\n getAttr(node, 'Target') === target,\n );\n if (existing) {\n const existingRel = relNodes.find(\n (node) =>\n getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&\n getAttr(node, 'Target') === target,\n );\n const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;\n tableRelIds.push(existingId ?? allocateRelId());\n continue;\n }\n const id = allocateRelId();\n tableRelIds.push(id);\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: id,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',\n Target: target,\n },\n [],\n ),\n );\n }\n\n const worksheet = this._sheets.get(sheetName);\n if (worksheet) {\n worksheet.setTableRelIds(tableRelIds);\n }\n\n const sheetRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n relNodes,\n );\n writeZipText(\n this._files,\n sheetRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([sheetRels])}`,\n );\n }\n }\n\n /**\n * Generate pivot cache/table parts and worksheet relationships.\n */\n private _updatePivotFiles(): void {\n if (this._pivotTables.length === 0) {\n return;\n }\n\n for (const pivot of this._pivotTables) {\n const pivotParts = pivot.buildPivotPartsXml();\n\n const pivotCachePath = `xl/pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;\n writeZipText(this._files, pivotCachePath, pivotParts.cacheDefinitionXml);\n\n const pivotCacheRecordsPath = `xl/pivotCache/pivotCacheRecords${pivot.cachePartIndex}.xml`;\n writeZipText(this._files, pivotCacheRecordsPath, pivotParts.cacheRecordsXml);\n\n const pivotCacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${pivot.cachePartIndex}.xml.rels`;\n writeZipText(this._files, pivotCacheRelsPath, pivotParts.cacheRelsXml);\n\n const pivotTablePath = `xl/pivotTables/pivotTable${pivot.pivotId}.xml`;\n writeZipText(this._files, pivotTablePath, pivotParts.pivotTableXml);\n }\n\n const pivotsBySheet = new Map<string, PivotTable[]>();\n for (const pivot of this._pivotTables) {\n const existing = pivotsBySheet.get(pivot.targetSheetName) ?? [];\n existing.push(pivot);\n pivotsBySheet.set(pivot.targetSheetName, existing);\n }\n\n for (const [sheetName, pivots] of pivotsBySheet) {\n const def = this._sheetDefs.find((s) => s.name === sheetName);\n if (!def) continue;\n\n const rel = this._relationships.find((r) => r.id === def.rId);\n if (!rel) continue;\n\n const sheetFileName = rel.target.split('/').pop();\n if (!sheetFileName) continue;\n\n const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;\n const existingRelsXml = readZipText(this._files, sheetRelsPath);\n\n let nextRelId = 1;\n const relNodes: XmlNode[] = [];\n const reservedRelIds = new Set<string>();\n\n if (existingRelsXml) {\n const parsed = parseXml(existingRelsXml);\n const relsElement = findElement(parsed, 'Relationships');\n if (relsElement) {\n for (const relNode of getChildren(relsElement, 'Relationships')) {\n if ('Relationship' in relNode) {\n relNodes.push(relNode);\n const id = getAttr(relNode, 'Id');\n if (id) {\n reservedRelIds.add(id);\n const idNum = parseInt(id.replace('rId', ''), 10);\n if (idNum >= nextRelId) {\n nextRelId = idNum + 1;\n }\n }\n }\n }\n }\n }\n\n const allocateRelId = (): string => {\n while (reservedRelIds.has(`rId${nextRelId}`)) {\n nextRelId++;\n }\n const id = `rId${nextRelId}`;\n nextRelId++;\n reservedRelIds.add(id);\n return id;\n };\n\n const pivotRelIds: string[] = [];\n for (const pivot of pivots) {\n const target = `../pivotTables/pivotTable${pivot.pivotId}.xml`;\n const existing = relNodes.find(\n (node) =>\n getAttr(node, 'Type') ===\n 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' &&\n getAttr(node, 'Target') === target,\n );\n\n if (existing) {\n const existingId = getAttr(existing, 'Id');\n pivotRelIds.push(existingId ?? allocateRelId());\n continue;\n }\n\n const id = allocateRelId();\n pivotRelIds.push(id);\n relNodes.push(\n createElement(\n 'Relationship',\n {\n Id: id,\n Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',\n Target: target,\n },\n [],\n ),\n );\n }\n\n const worksheet = this._sheets.get(sheetName);\n if (worksheet) {\n worksheet.setPivotTableRelIds(pivotRelIds);\n }\n\n const sheetRels = createElement(\n 'Relationships',\n { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },\n relNodes,\n );\n\n writeZipText(\n this._files,\n sheetRelsPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n${stringifyXml([sheetRels])}`,\n );\n }\n }\n}\n","import type { CellAddress, RangeAddress } from '../types';\n\n/**\n * Converts a column index (0-based) to Excel column letters (A, B, ..., Z, AA, AB, ...)\n * @param col - 0-based column index\n * @returns Column letter(s)\n */\nexport const colToLetter = (col: number): string => {\n let result = '';\n let n = col;\n while (n >= 0) {\n result = String.fromCharCode((n % 26) + 65) + result;\n n = Math.floor(n / 26) - 1;\n }\n return result;\n};\n\n/**\n * Converts Excel column letters to a 0-based column index\n * @param letters - Column letter(s) like 'A', 'B', 'AA'\n * @returns 0-based column index\n */\nexport const letterToCol = (letters: string): number => {\n const upper = letters.toUpperCase();\n let col = 0;\n for (let i = 0; i < upper.length; i++) {\n col = col * 26 + (upper.charCodeAt(i) - 64);\n }\n return col - 1;\n};\n\n/**\n * Parses an Excel cell address (e.g., 'A1', '$B$2') to row/col indices\n * @param address - Cell address string\n * @returns CellAddress with 0-based row and col\n */\nexport const parseAddress = (address: string): CellAddress => {\n // Remove $ signs for absolute references\n const clean = address.replace(/\\$/g, '');\n const match = clean.match(/^([A-Z]+)(\\d+)$/i);\n if (!match) {\n throw new Error(`Invalid cell address: ${address}`);\n }\n const rowNumber = +match[2];\n if (rowNumber <= 0) throw new Error(`Invalid cell address: ${address}`);\n\n const col = letterToCol(match[1].toUpperCase());\n const row = rowNumber - 1; // Convert to 0-based\n return { row, col };\n};\n\n/**\n * Converts row/col indices to an Excel cell address\n * @param row - 0-based row index\n * @param col - 0-based column index\n * @returns Cell address string like 'A1'\n */\nexport const toAddress = (row: number, col: number): string => {\n return `${colToLetter(col)}${row + 1}`;\n};\n\n/**\n * Parses an Excel range (e.g., 'A1:B10') to start/end addresses\n * @param range - Range string\n * @returns RangeAddress with start and end\n */\nexport const parseRange = (range: string): RangeAddress => {\n const parts = range.split(':');\n if (parts.length === 1) {\n // Single cell range\n const addr = parseAddress(parts[0]);\n return { start: addr, end: addr };\n }\n if (parts.length !== 2) {\n throw new Error(`Invalid range: ${range}`);\n }\n return {\n start: parseAddress(parts[0]),\n end: parseAddress(parts[1]),\n };\n};\n\n/**\n * Converts a RangeAddress to a range string\n * @param range - RangeAddress object\n * @returns Range string like 'A1:B10'\n */\nexport const toRange = (range: RangeAddress): string => {\n const start = toAddress(range.start.row, range.start.col);\n const end = toAddress(range.end.row, range.end.col);\n if (start === end) {\n return start;\n }\n return `${start}:${end}`;\n};\n\n/**\n * Parses a qualified sheet + address reference.\n * Supports Sheet!A1 and 'Sheet Name'!A1.\n */\nexport const parseSheetAddress = (reference: string): { sheet: string; address: CellAddress } => {\n const exclamationIndex = reference.lastIndexOf('!');\n if (exclamationIndex <= 0 || exclamationIndex >= reference.length - 1) {\n throw new Error(`Invalid sheet address reference: ${reference}`);\n }\n\n const rawSheet = reference.slice(0, exclamationIndex);\n const addressPart = reference.slice(exclamationIndex + 1);\n const sheet = unquoteSheetName(rawSheet);\n\n return {\n sheet,\n address: parseAddress(addressPart),\n };\n};\n\n/**\n * Parses a qualified sheet + range reference.\n * Supports Sheet!A1:B10 and 'Sheet Name'!A1:B10.\n */\nexport const parseSheetRange = (reference: string): { sheet: string; range: RangeAddress } => {\n const exclamationIndex = reference.lastIndexOf('!');\n if (exclamationIndex <= 0 || exclamationIndex >= reference.length - 1) {\n throw new Error(`Invalid sheet range reference: ${reference}`);\n }\n\n const rawSheet = reference.slice(0, exclamationIndex);\n const rangePart = reference.slice(exclamationIndex + 1);\n const sheet = unquoteSheetName(rawSheet);\n\n return {\n sheet,\n range: parseRange(rangePart),\n };\n};\n\nconst unquoteSheetName = (sheet: string): string => {\n const trimmed = sheet.trim();\n if (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\") && trimmed.length >= 2) {\n return trimmed.slice(1, -1).replace(/''/g, \"'\");\n }\n return trimmed;\n};\n\n/**\n * Normalizes a range so start is always top-left and end is bottom-right\n */\nexport const normalizeRange = (range: RangeAddress): RangeAddress => {\n return {\n start: {\n row: Math.min(range.start.row, range.end.row),\n col: Math.min(range.start.col, range.end.col),\n },\n end: {\n row: Math.max(range.start.row, range.end.row),\n col: Math.max(range.start.col, range.end.col),\n },\n };\n};\n\n/**\n * Checks if an address is within a range\n */\nexport const isInRange = (addr: CellAddress, range: RangeAddress): boolean => {\n const norm = normalizeRange(range);\n return (\n addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col\n );\n};\n"],"names":["__cell.Cell"],"mappings":"AAAA;AACA;AACA;AACO,KAAA,SAAA,+BAAA,IAAA,UAAA,SAAA;AACP;AACA;AACA;AACO,UAAA,SAAA;AACP,WAAA,SAAA;AACA;AACO,KAAA,SAAA;AACP;AACA;AACA;AACO,KAAA,QAAA;AACP;AACA;AACA;AACO,KAAA,YAAA;AACP;AACA;AACA;AACO,UAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAA,WAAA;AACA,gBAAA,SAAA;AACA;AACA;AACO,UAAA,WAAA;AACP,UAAA,UAAA;AACA,aAAA,UAAA;AACA,WAAA,UAAA;AACA,YAAA,UAAA;AACA;AACO,KAAA,UAAA;AACA,UAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,YAAA;AACP,WAAA,WAAA;AACA,SAAA,WAAA;AACA;AACA;AACA;AACA;AACO,UAAA,QAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAiBA;AACA;AACA;AACO,UAAA,mBAAA,oBAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA,cAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,YAAA,KAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA,YAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,aAAA;AACP;AACA,YAAA,SAAA;AACA;AACA;AACA;AACA,YAAA,SAAA;AACA;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAA,gBAAA;AACA;AACA;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,oBAAA;AACP;AACA;AACA;AACO,KAAA,cAAA;AACP;AACA;AACA;AACA;AACO,KAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA,kBAAA,oBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,kBAAA;AACP;AACA;AACA;AACO,UAAA,iBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mBAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,mBAAA;AACP;AACA;AACA;AACA;AACA;AACA;;ACxRA;AACA;AACA;AACO,cAAA,IAAA;AACP;AACA;AACA;AACA;AACA;AACA,2BAAA,SAAA,mCAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAA,QAAA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,mBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAA,IAAA;AACA;AACA;AACA;AACA;AACA,yBAAA,IAAA;AACA;;AC5FA;AACA;AACA;AACO,cAAA,KAAA;AACP;AACA;AACA,2BAAA,SAAA,SAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA,QAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,SAAA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA,KAAA,MAAA,CAAA,QAAA,KAAA,SAAA,CAAmCA,IAAgB;AACnD;AACA;AACA;AACA,YAAA,SAAA,CAAsBA,IAAgB;AACtC;;ACzDA;AACA;AACA;AACO,cAAA,KAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2BAAA,SAAA,UAAA,WAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,gBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6CAAA,kBAAA;AACA;AACA;AACA;AACA,0CAAA,kBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oBAAA,OAAA,CAAA,gBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/GA;AACA;AACA;AACO,cAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,QAAA;AACA;AACA;AACA;AACA,oBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAA,IAAA;AACA;AACA;AACA;AACA,kEAAA,IAAA;AACA;AACA;AACA;AACA,6BAAA,KAAA;AACA,+EAAA,KAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,GAAA,SAAA,IAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,KAAA;AACA;AACA;AACA;AACA;AACA,uBAAA,GAAA;AACA;AACA;AACA;AACA;AACA,qBAAA,GAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAA,WAAA,GAAA,KAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAA,MAAA,SAAA,SAAA,YAAA,iBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AClNA;AACA;AACA;AACA;AACO,cAAA,aAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+BAAA,aAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACjDA;AACA;AACA;AACO,cAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+BAAA,MAAA;AACA;AACA;AACA;AACA;AACA,4BAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAAA,SAAA;AACA;AACA;AACA;AACA;AACA,uBAAA,SAAA;AACA;AACA;AACA;AACA,0CAAA,OAAA,CAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACvEA,UAAA,cAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,QAAA,UAAA,gBAAA,wCAAA,SAAA,eAAA,YAAA;AACA;AACA;AACA,yEAAA,cAAA;AACA;AACA;AACA,uBAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mDAAA,oBAAA;AACA,0BAAA,gBAAA;AACA,wCAAA,cAAA;AACA,2CAAA,gBAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACzEA;AACA;AACA;AACO,cAAA,QAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4CAAA,mBAAA,GAAA,OAAA,CAAA,QAAA;AACA;AACA;AACA;AACA,4BAAA,UAAA,YAAA,mBAAA,GAAA,OAAA,CAAA,QAAA;AACA;AACA;AACA;AACA,qBAAA,QAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yBAAA,aAAA;AACA;AACA;AACA;AACA,kBAAA,MAAA;AACA;AACA;AACA;AACA,wBAAA,YAAA;AACA;AACA;AACA;AACA,4BAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAA,UAAA;AACA;AACA;AACA;AACA,6BAAA,gBAAA,GAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA,yCAAA,SAAA;AACA;AACA;AACA;AACA,4CAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oDAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAAA,mBAAA,MAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,OAAA;AACA;AACA;AACA;AACA,gBAAA,OAAA,CAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AChLA;AACA;AACA;AACA;AACA;AACO,cAAA,YAAA,uBAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACO,cAAA,UAAA,qBAAA,YAAA;AACP;AACA;AACA;AACA;AACA;AACO,cAAA,OAAA,UAAA,YAAA;AACP;AACA;AACA;AACA;AACO,cAAA,iBAAA;AACP;AACA,aAAA,WAAA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,eAAA;AACP;AACA,WAAA,YAAA;AACA;;;;"}