UNPKG

xml-xlsx-lite

Version:

🚀 Lightweight Excel XLSX generator with full Excel features: dynamic pivot tables, charts, styles, and Chinese support. Fast, TypeScript-friendly Excel file creation library. | 輕量級 Excel XLSX 生成器,支援樞紐分析表、圖表、樣式,完整繁體中文支援。

1,576 lines (1,563 loc) 150 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var JSZip2 = require('jszip'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var JSZip2__default = /*#__PURE__*/_interopDefault(JSZip2); // xml-xlsx-lite – Minimal XLSX writer using raw XML + JSZip // https://github.com/mikemikex1/xml-xlsx-lite var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/utils.ts var COL_A_CODE = "A".charCodeAt(0); var EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30)); function colToNumber(col) { let n = 0; for (let i = 0; i < col.length; i++) { n = n * 26 + (col.charCodeAt(i) - COL_A_CODE + 1); } return n; } function numberToCol(n) { let col = ""; while (n > 0) { const rem = (n - 1) % 26; col = String.fromCharCode(COL_A_CODE + rem) + col; n = Math.floor((n - 1) / 26); } return col; } function parseAddress(addr) { const m = /^([A-Z]+)(\d+)$/.exec(addr.toUpperCase()); if (!m) throw new Error(`Invalid cell address: ${addr}`); return { col: colToNumber(m[1]), row: parseInt(m[2], 10) }; } function addrFromRC(row, col) { return `${numberToCol(col)}${row}`; } function isDate(val) { return val instanceof Date; } function excelSerialFromDate(d) { const msPerDay = 24 * 60 * 60 * 1e3; const diff = (Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) - EXCEL_EPOCH.getTime()) / msPerDay; return diff; } function getCellType(value) { if (value === null || value === void 0) return null; if (typeof value === "number") return "n"; if (typeof value === "boolean") return "b"; if (isDate(value)) return "d"; if (typeof value === "string") return "inlineStr"; return "inlineStr"; } function escapeXmlText(str) { return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"); } function escapeXmlAttr(str) { return escapeXmlText(str); } function hashPassword(password) { let hash = 0; for (let i = 0; i < password.length; i++) { const char = password.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return Math.abs(hash).toString(16); } function verifyPassword(password, hash) { return hashPassword(password) === hash; } function generateUniqueId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } // src/cell.ts var CellModel = class { constructor(address) { this.address = address; this.value = null; this.type = null; this.options = {}; } /** * 設定儲存格值 */ setValue(value) { this.value = value; } /** * 設定儲存格選項 */ setOptions(options) { this.options = { ...this.options, ...options }; } /** * 清除儲存格內容 */ clear() { this.value = null; this.type = null; this.options = {}; } /** * 檢查儲存格是否為空 */ isEmpty() { return this.value === null || this.value === void 0 || this.value === ""; } /** * 取得儲存格顯示值 */ getDisplayValue() { if (this.value === null || this.value === void 0) return ""; if (this.value instanceof Date) return this.value.toLocaleDateString(); return String(this.value); } }; // src/protection.ts var WorksheetProtection = class { constructor() { this._isProtected = false; this._passwordHash = null; this._options = {}; this._options = { selectLockedCells: false, selectUnlockedCells: true, formatCells: false, formatColumns: false, formatRows: false, insertColumns: false, insertRows: false, insertHyperlinks: false, deleteColumns: false, deleteRows: false, sort: false, autoFilter: false, pivotTables: false, objects: false, scenarios: false }; } /** * 保護工作表 */ protect(password, options) { this._isProtected = true; if (password) { this._passwordHash = hashPassword(password); } if (options) { this._options = { ...this._options, ...options }; } } /** * 解除工作表保護 */ unprotect(password) { if (this._isProtected && this._passwordHash && password) { if (!verifyPassword(password, this._passwordHash)) { throw new Error("Incorrect password"); } } this._isProtected = false; this._passwordHash = null; } /** * 檢查工作表是否受保護 */ isProtected() { return this._isProtected; } /** * 取得保護選項 */ getProtectionOptions() { if (!this._isProtected) return null; return { ...this._options }; } /** * 檢查操作是否被允許 */ isOperationAllowed(operation) { if (!this._isProtected) return true; return this._options[operation] || false; } /** * 驗證密碼 */ validatePassword(password) { if (!this._passwordHash) return true; return verifyPassword(password, this._passwordHash); } }; var WorkbookProtection = class { constructor() { this._isProtected = false; this._passwordHash = null; this._options = {}; this._options = { structure: false, windows: false }; } /** * 保護工作簿 */ protect(password, options) { this._isProtected = true; if (password) { this._passwordHash = hashPassword(password); } if (options) { this._options = { ...this._options, ...options }; } } /** * 解除工作簿保護 */ unprotect(password) { if (this._isProtected && this._passwordHash && password) { if (!verifyPassword(password, this._passwordHash)) { throw new Error("Incorrect password"); } } this._isProtected = false; this._passwordHash = null; } /** * 檢查工作簿是否受保護 */ isProtected() { return this._isProtected; } /** * 取得保護選項 */ getProtectionOptions() { if (!this._isProtected) return null; return { ...this._options }; } /** * 檢查操作是否被允許 */ isOperationAllowed(operation) { if (!this._isProtected) return true; return this._options[operation] || false; } /** * 驗證密碼 */ validatePassword(password) { if (!this._passwordHash) return true; return verifyPassword(password, this._passwordHash); } }; // src/worksheet.ts var WorksheetImpl = class { constructor(name) { this._cells = /* @__PURE__ */ new Map(); this._maxRow = 0; this._maxCol = 0; // Phase 3: 合併儲存格管理 this._mergedRanges = /* @__PURE__ */ new Set(); // Phase 6: 工作表保護 this._protection = new WorksheetProtection(); // Phase 6: 圖表支援 this._charts = /* @__PURE__ */ new Map(); this.name = name; } getCell(address) { if (!this._cells.has(address)) { const cell = new CellModel(address); this._cells.set(address, cell); const { row, col } = parseAddress(address); this._maxRow = Math.max(this._maxRow, row); this._maxCol = Math.max(this._maxCol, col); } return this._cells.get(address); } setCell(address, value, options = {}) { if (this._protection.isProtected() && !this._protection.isOperationAllowed("formatCells")) { throw new Error("Worksheet is protected. Cannot modify cells."); } const cell = this.getCell(address); cell.value = value; cell.type = getCellType(value); cell.options = { ...cell.options, ...options }; const { row, col } = parseAddress(address); this._maxRow = Math.max(this._maxRow, row); this._maxCol = Math.max(this._maxCol, col); return cell; } *rows() { const rowMap = /* @__PURE__ */ new Map(); for (const [addr, cell] of this._cells) { const { row, col } = parseAddress(addr); if (!rowMap.has(row)) rowMap.set(row, /* @__PURE__ */ new Map()); rowMap.get(row).set(col, cell); } const sortedRows = Array.from(rowMap.keys()).sort((a, b) => a - b); for (const row of sortedRows) { yield [row, rowMap.get(row)]; } } // Phase 3: 合併儲存格實現 mergeCells(range) { if (this._protection.isProtected() && !this._protection.isOperationAllowed("formatCells")) { throw new Error("Worksheet is protected. Cannot merge cells."); } if (!/^[A-Z]+\d+:[A-Z]+\d+$/.test(range)) { throw new Error(`Invalid range format: ${range}. Expected format: A1:B3`); } const [start, end] = range.split(":"); const startAddr = parseAddress(start); const endAddr = parseAddress(end); if (startAddr.row > endAddr.row || startAddr.col > endAddr.col) { throw new Error(`Invalid range: start position must be before end position`); } for (const existingRange of this._mergedRanges) { if (this._rangesOverlap(range, existingRange)) { throw new Error(`Range ${range} overlaps with existing merged range ${existingRange}`); } } this._mergedRanges.add(range); const mainCell = this.getCell(start); mainCell.options.mergeRange = range; for (let row = startAddr.row; row <= endAddr.row; row++) { for (let col = startAddr.col; col <= endAddr.col; col++) { if (row === startAddr.row && col === startAddr.col) continue; const addr = addrFromRC(row, col); const cell = this.getCell(addr); cell.value = null; cell.options.mergedInto = range; } } } unmergeCells(range) { if (this._protection.isProtected() && !this._protection.isOperationAllowed("formatCells")) { throw new Error("Worksheet is protected. Cannot unmerge cells."); } if (!this._mergedRanges.has(range)) { throw new Error(`Range ${range} is not merged`); } this._mergedRanges.delete(range); const [start, end] = range.split(":"); const startAddr = parseAddress(start); const endAddr = parseAddress(end); for (let row = startAddr.row; row <= endAddr.row; row++) { for (let col = startAddr.col; col <= endAddr.col; col++) { const addr = addrFromRC(row, col); if (this._cells.has(addr)) { const cell = this._cells.get(addr); delete cell.options.mergeRange; delete cell.options.mergedInto; } } } } getMergedRanges() { return Array.from(this._mergedRanges).sort(); } // Phase 3: 欄寬/列高設定 setColumnWidth(column, width) { if (this._protection.isProtected() && !this._protection.isOperationAllowed("formatColumns")) { throw new Error("Worksheet is protected. Cannot modify column width."); } const colNum = typeof column === "string" ? colToNumber(column) : column; if (width < 0) { throw new Error(`Column width cannot be negative: ${width}`); } if (!this._columnWidths) this._columnWidths = /* @__PURE__ */ new Map(); this._columnWidths.set(colNum, width); } getColumnWidth(column) { const colNum = typeof column === "string" ? colToNumber(column) : column; if (!this._columnWidths) return 8.43; return this._columnWidths.get(colNum) || 8.43; } setRowHeight(row, height) { if (this._protection.isProtected() && !this._protection.isOperationAllowed("formatRows")) { throw new Error("Worksheet is protected. Cannot modify row height."); } if (height < 0) { throw new Error(`Row height cannot be negative: ${height}`); } if (!this._rowHeights) this._rowHeights = /* @__PURE__ */ new Map(); this._rowHeights.set(row, height); } getRowHeight(row) { if (!this._rowHeights) return 15; return this._rowHeights.get(row) || 15; } // 凍結窗格 freezePanes(row, column) { this._freezeRow = row; this._freezeCol = column; } unfreezePanes() { this._freezeRow = void 0; this._freezeCol = void 0; } getFreezePanes() { return { row: this._freezeRow, column: this._freezeCol }; } // Phase 3: 公式支援 setFormula(address, formula, options = {}) { if (this._protection.isProtected() && !this._protection.isOperationAllowed("formatCells")) { throw new Error("Worksheet is protected. Cannot set formulas."); } const cell = this.getCell(address); cell.options.formula = formula; cell.options.formulaType = "shared"; cell.options.numFmt = "General"; cell.options.font = { bold: true }; cell.options.alignment = { horizontal: "center", vertical: "middle" }; cell.options.border = { style: "thin", color: "black" }; cell.options.fill = { type: "pattern", patternType: "solid", fgColor: "#FFFF00" }; return cell; } getFormula(address) { const cell = this.getCell(address); return cell.options.formula || null; } validateFormula(formula) { return true; } getFormulaDependencies(address) { return []; } // Phase 6: 工作表保護 protect(password, options) { this._protection.protect(password, options); } unprotect(password) { this._protection.unprotect(password); } isProtected() { return this._protection.isProtected(); } getProtectionOptions() { return this._protection.getProtectionOptions(); } // Phase 6: 圖表支援 addChart(chart) { if (this._protection.isProtected() && !this._protection.isOperationAllowed("objects")) { throw new Error("Worksheet is protected. Cannot add charts."); } this._charts.set(chart.name, chart); } removeChart(chartName) { if (this._protection.isProtected() && !this._protection.isOperationAllowed("objects")) { throw new Error("Worksheet is protected. Cannot remove charts."); } this._charts.delete(chartName); } getCharts() { return Array.from(this._charts.values()); } getChart(chartName) { return this._charts.get(chartName); } // 內部方法 _rangesOverlap(range1, range2) { const [start1, end1] = range1.split(":").map(parseAddress); const [start2, end2] = range2.split(":").map(parseAddress); return !(end1.row < start2.row || start1.row > end2.row || end1.col < start2.col || start1.col > end2.col); } // 取得內部屬性(用於 XML 生成) get _maxRowValue() { return this._maxRow; } get _maxColValue() { return this._maxCol; } get _columnWidthsValue() { return this._columnWidths; } get _rowHeightsValue() { return this._rowHeights; } }; // src/charts.ts var ChartImpl = class { constructor(name, type, data, options = {}, position = { row: 1, col: 1 }) { this.name = name; this.type = type; this.data = data; this.options = { title: "", xAxisTitle: "", yAxisTitle: "", width: 400, height: 300, showLegend: true, showDataLabels: false, showGridlines: true, theme: "light", ...options }; this.position = position; } /** * 添加資料系列 */ addSeries(series) { this.data.push(series); } /** * 移除資料系列 */ removeSeries(seriesName) { const index = this.data.findIndex((s) => s.series === seriesName); if (index !== -1) { this.data.splice(index, 1); } } /** * 更新圖表選項 */ updateOptions(options) { this.options = { ...this.options, ...options }; } /** * 移動圖表位置 */ moveTo(row, col) { this.position = { row, col }; } /** * 調整圖表大小 */ resize(width, height) { this.options.width = width; this.options.height = height; } /** * 取得圖表 XML 表示 */ toXml() { const chartId = generateUniqueId(); const chartXml = this._buildChartXml(chartId); const drawingXml = this._buildDrawingXml(chartId); return { chartXml, drawingXml, chartId }; } /** * 驗證圖表資料 */ validate() { if (this.data.length === 0) return false; for (const series of this.data) { if (!series.series || !series.categories || !series.values) { return false; } } return true; } /** * 建立圖表 XML */ _buildChartXml(chartId) { const parts = [ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">', "<c:chart>", this._buildChartTitle(), this._buildChartType(), this._buildChartSeries(), this._buildChartAxes(), this._buildChartLegend(), "</c:chart>", "</c:chartSpace>" ]; return parts.join(""); } /** * 建立圖表標題 */ _buildChartTitle() { if (!this.options.title) return ""; return [ "<c:title>", "<c:tx>", "<c:rich>", "<a:bodyPr/>", "<a:lstStyle/>", "<a:p>", "<a:r>", "<a:t>" + this.options.title + "</a:t>", "</a:r>", "</a:p>", "</c:rich>", "</c:tx>", "</c:title>" ].join(""); } /** * 建立圖表類型 */ _buildChartType() { const chartTypeMap = { column: "bar", line: "line", pie: "pie", bar: "bar", area: "area", scatter: "scatter", doughnut: "doughnut", radar: "radar" }; const xmlType = chartTypeMap[this.type] || "bar"; if (xmlType === "bar") { return [ "<c:barChart>", '<c:barDir val="col"/>', '<c:grouping val="clustered"/>', this._buildChartSeries(), "</c:barChart>" ].join(""); } else if (xmlType === "line") { return [ "<c:lineChart>", '<c:grouping val="standard"/>', this._buildChartSeries(), "</c:lineChart>" ].join(""); } else if (xmlType === "pie") { return [ "<c:pieChart>", this._buildChartSeries(), "</c:pieChart>" ].join(""); } return [ "<c:barChart>", '<c:barDir val="col"/>', '<c:grouping val="clustered"/>', this._buildChartSeries(), "</c:barChart>" ].join(""); } /** * 建立圖表資料系列 */ _buildChartSeries() { return this.data.map((series, index) => [ "<c:ser>", '<c:idx val="' + index + '"/>', '<c:order val="' + index + '"/>', "<c:tx>", "<c:strRef>", "<c:f>" + series.series + "</c:f>", "</c:strRef>", "</c:tx>", "<c:cat>", "<c:strRef>", "<c:f>" + series.categories + "</c:f>", "</c:strRef>", "</c:cat>", "<c:val>", "<c:numRef>", "<c:f>" + series.values + "</c:f>", "</c:numRef>", "</c:val>", "</c:ser>" ].join("")).join(""); } /** * 建立圖表軸線 */ _buildChartAxes() { if (this.type === "pie" || this.type === "doughnut") { return ""; } return [ "<c:catAx>", '<c:axId val="100"/>', "<c:scaling>", '<c:orientation val="minMax"/>', "</c:scaling>", '<c:axPos val="b"/>', '<c:crossAx val="200"/>', '<c:tickLblPos val="nextTo"/>', '<c:crosses val="autoZero"/>', "</c:catAx>", "<c:valAx>", '<c:axId val="200"/>', "<c:scaling>", '<c:orientation val="minMax"/>', "</c:scaling>", '<c:axPos val="l"/>', '<c:crossAx val="100"/>', '<c:tickLblPos val="nextTo"/>', '<c:crosses val="autoZero"/>', "</c:valAx>" ].join(""); } /** * 建立圖表圖例 */ _buildChartLegend() { if (!this.options.showLegend) return ""; return [ "<c:legend>", '<c:legendPos val="r"/>', "<c:layout/>", '<c:overlay val="0"/>', "</c:legend>" ].join(""); } /** * 建立繪圖 XML */ _buildDrawingXml(chartId) { const parts = [ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', '<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">', "<xdr:twoCellAnchor>", "<xdr:from>", "<xdr:col>" + (this.position.col - 1) + "</xdr:col>", "<xdr:colOff>0</xdr:colOff>", "<xdr:row>" + (this.position.row - 1) + "</xdr:row>", "<xdr:rowOff>0</xdr:rowOff>", "</xdr:from>", "<xdr:to>", "<xdr:col>" + (this.position.col + Math.floor(this.options.width / 100)) + "</xdr:col>", "<xdr:colOff>0</xdr:colOff>", "<xdr:row>" + (this.position.row + Math.floor(this.options.height / 20)) + "</xdr:row>", "<xdr:rowOff>0</xdr:rowOff>", "</xdr:to>", '<xdr:graphicFrame macro="">', "<xdr:nvGraphicFramePr>", '<xdr:cNvPr id="' + chartId + '" name="' + this.name + '"/>', "<xdr:cNvGraphicFramePr/>", "</xdr:nvGraphicFramePr>", "<xdr:xfrm>", '<a:off x="0" y="0"/>', '<a:ext cx="' + this.options.width + '" cy="' + this.options.height + '"/>', "</xdr:xfrm>", "<a:graphic>", '<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">', '<c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:embed="rId' + chartId + '"/>', "</a:graphicData>", "</a:graphic>", "</xdr:graphicFrame>", "</xdr:twoCellAnchor>", "</xdr:wsDr>" ]; return parts.join(""); } }; var ChartFactory = class { /** * 建立柱狀圖 */ static createColumnChart(name, data, options, position) { return new ChartImpl(name, "column", data, options, position); } /** * 建立折線圖 */ static createLineChart(name, data, options, position) { return new ChartImpl(name, "line", data, options, position); } /** * 建立圓餅圖 */ static createPieChart(name, data, options, position) { return new ChartImpl(name, "pie", data, options, position); } /** * 建立長條圖 */ static createBarChart(name, data, options, position) { return new ChartImpl(name, "bar", data, options, position); } /** * 建立面積圖 */ static createAreaChart(name, data, options, position) { return new ChartImpl(name, "area", data, options, position); } /** * 建立散佈圖 */ static createScatterChart(name, data, options, position) { return new ChartImpl(name, "scatter", data, options, position); } }; // src/pivot-table-impl.ts var PivotTableImpl = class { constructor(name, config) { this.name = name; this.config = config; this.data = []; this.pivotData = []; this.rowFields = []; this.columnFields = []; this.valueFields = []; this.filters = /* @__PURE__ */ new Map(); this.initializeFields(); } /** * 初始化欄位分類 */ initializeFields() { for (const field of this.config.fields) { switch (field.type) { case "row": this.rowFields.push(field); break; case "column": this.columnFields.push(field); break; case "value": this.valueFields.push(field); break; } } } /** * 設定來源資料 */ setSourceData(data) { this.data = data; this.refresh(); } /** * 重新整理樞紐分析表 */ refresh() { if (this.data.length === 0) return; const sourceData = this.parseSourceData(); this.buildPivotData(sourceData); } /** * 解析來源資料 */ parseSourceData() { const sourceRange = this.config.sourceRange; const { startRow, startCol, endRow, endCol } = this.parseRange(sourceRange); const sourceData = []; for (let row = startRow; row <= endRow; row++) { const rowData = []; for (let col = startCol; col <= endCol; col++) { addrFromRC(row, col); rowData.push(this.data[row - startRow]?.[col - startCol] || ""); } sourceData.push(rowData); } return sourceData; } /** * 解析範圍字串 */ parseRange(range) { const parts = range.split(":"); const start = parseAddress(parts[0]); const end = parseAddress(parts[1]); return { startRow: start.row, startCol: start.col, endRow: end.row, endCol: end.col }; } /** * 建立樞紐分析表資料 */ buildPivotData(sourceData) { const headers = sourceData[0] || []; const dataRows = sourceData.slice(1); const fieldIndexMap = /* @__PURE__ */ new Map(); headers.forEach((header, index) => { fieldIndexMap.set(header, index); }); const uniqueValues = this.collectUniqueValues(dataRows, fieldIndexMap); this.pivotData = this.createPivotStructure(uniqueValues, dataRows, fieldIndexMap); } /** * 收集唯一值 */ collectUniqueValues(dataRows, fieldIndexMap) { const uniqueValues = /* @__PURE__ */ new Map(); for (const field of [...this.rowFields, ...this.columnFields]) { const index = fieldIndexMap.get(field.sourceColumn); if (index !== void 0) { uniqueValues.set(field.sourceColumn, /* @__PURE__ */ new Set()); } } for (const row of dataRows) { for (const field of [...this.rowFields, ...this.columnFields]) { const index = fieldIndexMap.get(field.sourceColumn); if (index !== void 0 && row[index] !== void 0) { uniqueValues.get(field.sourceColumn).add(row[index]); } } } return uniqueValues; } /** * 建立樞紐分析表結構 */ createPivotStructure(uniqueValues, dataRows, fieldIndexMap) { const pivotData = []; const titleRow = this.createTitleRow(uniqueValues); pivotData.push(titleRow); const dataRowsResult = this.createDataRows(uniqueValues, dataRows, fieldIndexMap); pivotData.push(...dataRowsResult); return pivotData; } /** * 建立標題行 */ createTitleRow(uniqueValues) { const titleRow = []; for (const field of this.rowFields) { const values = Array.from(uniqueValues.get(field.sourceColumn) || []); titleRow.push(field.customName || field.sourceColumn); titleRow.push(...values); } for (const field of this.columnFields) { const values = Array.from(uniqueValues.get(field.sourceColumn) || []); titleRow.push(field.customName || field.sourceColumn); titleRow.push(...values); } for (const field of this.valueFields) { titleRow.push(field.customName || field.sourceColumn); } return titleRow; } /** * 建立資料行 */ createDataRows(uniqueValues, dataRows, fieldIndexMap) { const result = []; for (const row of dataRows) { const pivotRow = []; for (const field of this.rowFields) { const index = fieldIndexMap.get(field.sourceColumn); if (index !== void 0) { pivotRow.push(row[index]); } } for (const field of this.valueFields) { const index = fieldIndexMap.get(field.sourceColumn); if (index !== void 0) { pivotRow.push(row[index]); } } result.push(pivotRow); } return result; } /** * 更新來源資料 */ updateSourceData(sourceRange) { this.config.sourceRange = sourceRange; this.refresh(); } /** * 取得欄位 */ getField(fieldName) { return this.config.fields.find((field) => field.name === fieldName); } /** * 添加欄位 */ addField(field) { this.config.fields.push(field); this.initializeFields(); this.refresh(); } /** * 移除欄位 */ removeField(fieldName) { const index = this.config.fields.findIndex((field) => field.name === fieldName); if (index !== -1) { this.config.fields.splice(index, 1); this.initializeFields(); this.refresh(); } } /** * 重新排序欄位 */ reorderFields(fieldOrder) { const newFields = []; for (const fieldName of fieldOrder) { const field = this.config.fields.find((f) => f.name === fieldName); if (field) { newFields.push(field); } } this.config.fields = newFields; this.initializeFields(); this.refresh(); } /** * 應用篩選 */ applyFilter(fieldName, filterValues) { this.filters.set(fieldName, filterValues); this.refresh(); } /** * 清除篩選 */ clearFilters() { this.filters.clear(); this.refresh(); } /** * 取得資料 */ getData() { return this.pivotData; } /** * 匯出到工作表 */ exportToWorksheet(worksheetName) { throw new Error("exportToWorksheet requires workbook instance"); } }; var esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"); async function addPivotToWorkbookBuffer(buf, opt) { const zip = await JSZip2__default.default.loadAsync(buf); await PivotBuilder.attach(zip, opt); return await zip.generateAsync({ type: "nodebuffer" }); } var PivotBuilder = class { static async attach(zip, opt) { const refreshOnLoad = opt.refreshOnLoad ?? true; const styleName = opt.styleName ?? "PivotStyleMedium9"; const map = await WorkbookMap.build(zip); const { firstRow, lastRow } = rangeRows(opt.sourceRange); const recordCount = lastRow - firstRow; if (recordCount <= 0) throw new Error("sourceRange \u5FC5\u9808\u5305\u542B\u81F3\u5C11 1 \u7B46\u8CC7\u6599\uFF08\u542B\u6A19\u984C\u5217\uFF09"); const srcSheetPath = map.sheetFileByName(opt.sourceSheet); const srcSheetXml = await mustRead(zip, srcSheetPath); const headers = extractHeadersFromSheetXml(srcSheetXml, opt.sourceRange); const types = inferFieldTypesFromSheetXml(srcSheetXml, opt.sourceRange, headers); const name2idx = new Map(headers.map((h, i) => [h, i])); const need = (n) => { if (!name2idx.has(n)) throw new Error(`\u627E\u4E0D\u5230\u6B04\u4F4D\uFF1A${n}`); return name2idx.get(n); }; const rowIdxs = (opt.layout.rows ?? []).map((f) => need(f.name)); const colIdxs = (opt.layout.cols ?? []).map((f) => need(f.name)); const valIdxs = opt.layout.values.map((v) => need(v.name)); opt.layout.values.forEach((v) => { const i = need(v.name); if (types[i] !== "number") throw new Error(`\u503C\u6B04\u4F4D\u5FC5\u9808\u70BA\u6578\u503C\uFF1A${v.name}`); }); const cacheId = await map.nextPivotCacheId(); const ptId = await map.nextPivotTableId(); const cacheDefPath = `xl/pivotCache/pivotCacheDefinition${cacheId}.xml`; const cacheRecPath = `xl/pivotCache/pivotCacheRecords${cacheId}.xml`; const ptPath = `xl/pivotTables/pivotTable${ptId}.xml`; zip.file(cacheDefPath, genCacheDefinitionXml({ sourceSheet: opt.sourceSheet, sourceRange: opt.sourceRange, headers, types, recordCount, refreshOnLoad })); zip.file(cacheRecPath, genEmptyCacheRecordsXml(recordCount)); await map.addPivotCache(cacheId, cacheDefPath); zip.file(ptPath, genPivotTableXml({ cacheId, headers, rowIdxs, colIdxs, valIdxs, values: opt.layout.values, anchorCell: opt.anchorCell, styleName })); const tgtSheetPath = map.sheetFileByName(opt.targetSheet); await map.linkPivotToSheet(tgtSheetPath, ptPath); await ensureContentTypes(zip, { cacheDefPath, cacheRecPath, ptPath }); } }; var WorkbookMap = class _WorkbookMap { constructor(zip, wbXml, wbRelsXml, sheetIdToTarget, nameToRid) { this.zip = zip; this.wbXml = wbXml; this.wbRelsXml = wbRelsXml; this.sheetIdToTarget = sheetIdToTarget; this.nameToRid = nameToRid; } static async build(zip) { const wbXml = await mustRead(zip, "xl/workbook.xml"); const wbRelsXml = await mustRead(zip, "xl/_rels/workbook.xml.rels"); const nameToRid = /* @__PURE__ */ new Map(); const sheetIdToTarget = /* @__PURE__ */ new Map(); for (const m of wbXml.matchAll(/<sheet[^>]*name="([^"]+)"[^>]*r:id="([^"]+)"/g)) { nameToRid.set(m[1], m[2]); } for (const m of wbRelsXml.matchAll(/<Relationship[^>]*Id="([^"]+)"[^>]*Type="[^"]*worksheet"[^>]*Target="([^"]+)"/g)) { sheetIdToTarget.set(m[1], `xl/${m[2].replace(/^\.\.\//, "")}`); } return new _WorkbookMap(zip, wbXml, wbRelsXml, sheetIdToTarget, nameToRid); } sheetFileByName(name) { const rid = this.nameToRid.get(name); if (!rid) throw new Error(`workbook \u627E\u4E0D\u5230\u5DE5\u4F5C\u8868\uFF1A${name}`); const target = this.sheetIdToTarget.get(rid); if (!target) throw new Error(`rels \u627E\u4E0D\u5230 target\uFF1A${name}(${rid})`); return target; } async nextPivotCacheId() { const ids = Array.from(this.wbXml.matchAll(/<pivotCache[^>]*cacheId="(\d+)"/g)).map((m) => +m[1]); return (ids.length ? Math.max(...ids) : 0) + 1; } async nextPivotTableId() { const files = this.zip.folder("xl/pivotTables")?.file(/.*/g) ?? []; const ids = files.map((f) => +(f.name.match(/pivotTable(\d+)\.xml$/)?.[1] ?? 0)); return (ids.length ? Math.max(...ids) : 0) + 1; } async addPivotCache(cacheId, cacheDefPath) { let rel = await mustRead(this.zip, "xl/_rels/workbook.xml.rels"); const newRelId = nextRelId(rel); rel = rel.replace( /<\/Relationships>\s*$/i, ` <Relationship Id="${newRelId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" Target="pivotCache/${cacheDefPath.split("/").pop()}"/> </Relationships>` ); this.zip.file("xl/_rels/workbook.xml.rels", rel); let wb = await mustRead(this.zip, "xl/workbook.xml"); if (/<pivotCaches>/.test(wb)) { wb = wb.replace( /<pivotCaches>([\s\S]*?)<\/pivotCaches>/, (_m, inner) => `<pivotCaches>${inner}<pivotCache cacheId="${cacheId}" r:id="${newRelId}"/></pivotCaches>` ); } else { wb = wb.replace( /<\/workbook>\s*$/i, ` <pivotCaches><pivotCache cacheId="${cacheId}" r:id="${newRelId}"/></pivotCaches> </workbook>` ); } this.zip.file("xl/workbook.xml", wb); } async linkPivotToSheet(sheetPath, ptPath) { const relPath = sheetPath.replace(/worksheets\/(sheet\d+\.xml)$/, "worksheets/_rels/$1.rels"); let rel = await readOrNull(this.zip, relPath) ?? `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> </Relationships>`; const newRelId = nextRelId(rel); rel = rel.replace( /<\/Relationships>\s*$/i, ` <Relationship Id="${newRelId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" Target="../pivotTables/${ptPath.split("/").pop()}"/> </Relationships>` ); this.zip.file(relPath, rel); const ws = await mustRead(this.zip, sheetPath); if (!/<sheetData/.test(ws)) { this.zip.file(sheetPath, ws.replace(/<\/worksheet>\s*$/i, ` <sheetData/> </worksheet>`)); } } }; function nextRelId(relXml) { const ids = Array.from(relXml.matchAll(/Id="rId(\d+)"/g)).map((m) => +m[1]); return `rId${(ids.length ? Math.max(...ids) : 0) + 1}`; } function genCacheDefinitionXml(p) { const fields = p.headers.map((h, i) => { const isNum = p.types[i] === "number"; const shared = isNum ? `<sharedItems containsNumber="1"/>` : `<sharedItems/>`; return `<cacheField name="${esc(h)}" numFmtId="0">${shared}</cacheField>`; }).join(""); const refresh = p.refreshOnLoad ? ` refreshOnLoad="1"` : ``; return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" invalid="1" recordCount="${p.recordCount}"${refresh}> <cacheSource type="worksheet"> <worksheetSource sheet="${esc(p.sourceSheet)}" ref="${esc(p.sourceRange)}"/> </cacheSource> <cacheFields count="${p.headers.length}"> ${fields} </cacheFields> </pivotCacheDefinition>`; } function genEmptyCacheRecordsXml(recordCount) { return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <pivotCacheRecords xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${recordCount}"> </pivotCacheRecords>`; } function genPivotTableXml(p) { const fields = p.headers.map((_, i) => { if (p.rowIdxs.includes(i)) return `<pivotField axis="axisRow" showAll="0"/>`; if (p.colIdxs.includes(i)) return `<pivotField axis="axisCol" showAll="0"/>`; if (p.valIdxs.includes(i)) return `<pivotField dataField="1"/>`; return `<pivotField/>`; }).join(""); const rowFields = p.rowIdxs.length ? `<rowFields count="${p.rowIdxs.length}">${p.rowIdxs.map((x) => `<field x="${x}"/>`).join("")}</rowFields>` : ""; const colFields = p.colIdxs.length ? `<colFields count="${p.colIdxs.length}">${p.colIdxs.map((x) => `<field x="${x}"/>`).join("")}</colFields>` : ""; const dataFields = p.values.map((v, i) => { const fld = p.valIdxs[i]; const subtotal = v.agg ?? "sum"; const name = esc(v.displayName ?? v.name); const numFmtId = v.numFmtId ?? 0; return `<dataField fld="${fld}" baseField="0" baseItem="0" subtotal="${subtotal}" name="${name}" numFmtId="${numFmtId}"/>`; }).join(""); return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" name="PivotTable${p.cacheId}" cacheId="${p.cacheId}" dataCaption="\u503C" updatedVersion="3" createdVersion="3" useAutoFormatting="1" applyNumberFormats="1" applyBorderFormats="1" applyFontFormats="1" applyPatternFormats="1" applyAlignmentFormats="1" applyWidthHeightFormats="1"> <location ref="${esc(p.anchorCell)}" firstHeaderRow="1" firstDataRow="2" firstDataCol="1"/> <pivotFields count="${p.headers.length}"> ${fields} </pivotFields> ${rowFields} ${colFields} <dataFields count="${p.values.length}"> ${dataFields} </dataFields> <pivotTableStyleInfo name="${esc(p.styleName)}" showRowHeaders="1" showColHeaders="1" showRowStripes="0" showColStripes="0" showLastColumn="0"/> </pivotTableDefinition>`; } async function ensureContentTypes(zip, p) { const path = "[Content_Types].xml"; let xml = await mustRead(zip, path); const ensure = (part, type) => { const tag = `<Override PartName="/${part}" ContentType="${type}"/>`; if (!xml.includes(tag)) xml = xml.replace(/<\/Types>\s*$/i, ` ${tag} </Types>`); }; ensure(p.cacheDefPath, "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"); ensure(p.cacheRecPath, "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"); ensure(p.ptPath, "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"); zip.file(path, xml); } function extractHeadersFromSheetXml(sheetXml, range) { const { firstCol, lastCol, firstRow } = rangeColsRows(range); const headers = []; for (let c = firstCol; c <= lastCol; c++) { const addr = colNumToName(c) + firstRow; const cell = findCell(sheetXml, addr); headers.push(readCellText(cell) || `F${c - firstCol + 1}`); } return headers; } function inferFieldTypesFromSheetXml(sheetXml, range, headers) { const { firstCol, lastCol, firstRow, lastRow } = rangeColsRows(range); const types = new Array(headers.length).fill("text"); for (let r = firstRow + 1; r <= Math.min(lastRow, firstRow + 20); r++) { for (let c = firstCol; c <= lastCol; c++) { const m = findCell(sheetXml, colNumToName(c) + r); if (!m) continue; if (/<v>\s*-?\d+(\.\d+)?\s*<\/v>/.test(m)) types[c - firstCol] = "number"; } } return types; } function findCell(sheetXml, addr) { const re = new RegExp(`<c[^>]*\\br="${addr}"[^>]*>([\\s\\S]*?)</c>`, "i"); const m = sheetXml.match(re); return m ? m[0] : null; } function readCellText(cellXml) { if (!cellXml) return ""; let m = cellXml.match(/<is>\s*<t>([\s\S]*?)<\/t>\s*<\/is>/); if (m) return decodeXml(m[1]); m = cellXml.match(/<v>([\s\S]*?)<\/v>/); return m ? decodeXml(m[1]) : ""; } function decodeXml(s) { return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&apos;/g, "'"); } function colNameToNum(name) { let n = 0; for (let i = 0; i < name.length; i++) n = n * 26 + (name.charCodeAt(i) - 64); return n; } function colNumToName(n) { let s = ""; while (n > 0) { n--; s = String.fromCharCode(65 + n % 26) + s; n = Math.floor(n / 26); } return s; } function rangeColsRows(a1) { const m = a1.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/i); if (!m) throw new Error(`\u975E\u6CD5\u7BC4\u570D\uFF1A${a1}`); const firstCol = colNameToNum(m[1].toUpperCase()), firstRow = +m[2], lastCol = colNameToNum(m[3].toUpperCase()), lastRow = +m[4]; return { firstCol, firstRow, lastCol, lastRow }; } function rangeRows(a1) { const { firstRow, lastRow } = rangeColsRows(a1); return { firstRow, lastRow }; } async function mustRead(zip, path) { const f = zip.file(path); if (!f) throw new Error(`\u627E\u4E0D\u5230\u6A94\u6848\uFF1A${path}`); return await f.async("string"); } async function readOrNull(zip, path) { const f = zip.file(path); return f ? await f.async("string") : null; } // src/pivot-table.ts var PivotTableImpl2 = class { constructor(name, config, workbook) { this._sourceData = []; this._processedData = []; this._fieldValues = /* @__PURE__ */ new Map(); this._pivotCache = /* @__PURE__ */ new Map(); this.name = name; this.config = config; this._workbook = workbook; this._cacheId = this._generateCacheId(); this._tableId = this._generateTableId(); this._loadSourceData(); this._processData(); } refresh() { this._loadSourceData(); this._processData(); } updateSourceData(sourceRange) { this.config.sourceRange = sourceRange; this.refresh(); } getField(fieldName) { return this.config.fields.find((field) => field.name === fieldName); } addField(field) { if (this.getField(field.name)) { throw new Error(`Field "${field.name}" already exists in pivot table.`); } this.config.fields.push(field); this.refresh(); } removeField(fieldName) { const index = this.config.fields.findIndex((field) => field.name === fieldName); if (index === -1) { throw new Error(`Field "${fieldName}" not found in pivot table.`); } this.config.fields.splice(index, 1); this.refresh(); } reorderFields(fieldOrder) { const newFields = []; for (const fieldName of fieldOrder) { const field = this.getField(fieldName); if (field) { newFields.push(field); } } this.config.fields = newFields; this.refresh(); } applyFilter(fieldName, filterValues) { const field = this.getField(fieldName); if (field) { field.filterValues = filterValues; this.refresh(); } } clearFilters() { for (const field of this.config.fields) { field.filterValues = void 0; } this.refresh(); } getData() { return this._processedData; } exportToWorksheet(worksheetName) { const ws = this._workbook.getWorksheet(worksheetName); this._clearTargetWorksheet(ws); this._writePivotData(ws); this._applyPivotStyles(ws); return ws; } /** * 生成 PivotCache 定義 XML(不包含記錄) */ generatePivotCacheXml() { this.config.fields.filter((f) => f.type === "row"); this.config.fields.filter((f) => f.type === "column"); this.config.fields.filter((f) => f.type === "value"); this.config.fields.filter((f) => f.type === "filter"); let xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" id="${this._cacheId}" recordCount="${this._sourceData.length - 1}" refreshOnLoad="1">`; xml += ` <cacheSource type="worksheet"> <worksheetSource ref="${this.config.sourceRange}" sheet="${this._getSourceSheetName()}"/> </cacheSource>`; xml += ` <cacheFields count="${this.config.fields.length}">`; for (const field of this.config.fields) { xml += this._generateCacheFieldXml(field); } xml += ` </cacheFields>`; xml += ` </pivotCacheDefinition>`; return xml; } /** * 生成 PivotCache 記錄 XML(獨立檔案) */ generatePivotCacheRecordsXml() { let xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <pivotCacheRecords xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" count="${this._sourceData.length - 1}">`; for (let i = 1; i < this._sourceData.length; i++) { xml += this._generateCacheRecordXml(this._sourceData[i], i); } xml += ` </pivotCacheRecords>`; return xml; } /** * 生成 PivotCache 關聯 XML */ generatePivotCacheRelsXml() { return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords" Target="pivotCacheRecords${this._cacheId}.xml"/> </Relationships>`; } /** * 生成 PivotTable XML */ generatePivotTableXml() { const rowFields = this.config.fields.filter((f) => f.type === "row"); const columnFields = this.config.fields.filter((f) => f.type === "column"); const valueFields = this.config.fields.filter((f) => f.type === "value"); const filterFields = this.config.fields.filter((f) => f.type === "filter"); let xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="xr" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" name="${this.name}" cacheId="${this._cacheId}" dataCaption="Values" applyNumberFormatsInPivot="0" applyBorderFormatsInPivot="0" applyFontFormatsInPivot="0" applyPatternFormatsInPivot="0" applyAlignmentFormatsInPivot="0" applyWidthHeightFormatsInPivot="0" dataOnRows="0" dataPosition="0" grandTotalCaption="Grand Total" multipleFieldFilters="0" showDrill="1" showMemberPropertyTips="0" showMissing="0" showMultipleLabel="0" showPageMultipleLabel="0" showPageSubtotals="0" showRowGrandTotals="1" showRowSubtotals="1" showColGrandTotals="1" showColSubtotals="0" showItems="1" showDataDropDown="1" showError="0" showExpandCollapse="1" showOutline="1" showEmptyRow="0" showEmptyCol="0" showHeaders="1" compact="1" outline="1" outlineData="1" gridDropZones="0" indent="0" pageWrap="0" pageOverThenDown="0" pageDownThenOver="0" pageFieldsInReportFilter="0" pageWrapCount="0" pageBreakBetweenGroups="0" subtotalHiddenItems="0" printTitles="0" fieldPrintTitles="0" itemPrintTitles="0" mergeTitles="0" markAutoFormat="0" autoFormat="0" applyStyles="0" baseStyles="0" customListSort="1" applyDataValidation="0" enableDrill="1" fieldListSortAscending="0" mdxSubqueries="0" customSubtotals="0" visualTotals="1" showDataAs="0" calculatedMembers="0" visualTotalsFilters="0" showPageBreaks="0" useAutoFormat="0" pageGrandTotals="0" subtotalPageItems="0" rowGrandTotals="1" colGrandTotals="1" fieldSort="1" compactData="1" printDrill="0" itemDrill="0" drillThrough="0" fieldList="0" nonAutoSortDefault="0" showNew="0" autoShow="0" rankBy="0" defaultSubtotal="1" multipleItemSelectionMode="0" manualUpdate="0" showCalcMbrs="0" calculatedMembersInFilters="0" visualTotalsForSets="0" showASubtotalForPstTop="0" showAllDrill="0" showValue="1" expandMembersInDetail="0" dateFormatInPivot="0" pivotShowAs="0" enableWizard="0" enableDrill="1" enableFieldDialog="0" preserveFormatting="1" autoFormat="0" autoRepublish="0" showPageMultipleLabel="0" showPageSubtotals="0" showRowGrandTotals="1" showRowSubtotals="1" showColGrandTotals="1" showColSubtotals="0" showItems="1" showDataDropDown="1" showError="0" showExpandCollapse