@cj-tech-master/excelts 2.0.0 → 2.0.1-canary.20251228093548.379d895
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/excelts.esm.js +217 -126
- package/dist/browser/excelts.esm.js.map +1 -1
- package/dist/browser/excelts.esm.min.js +37 -34
- package/dist/browser/excelts.iife.js +217 -126
- package/dist/browser/excelts.iife.js.map +1 -1
- package/dist/browser/excelts.iife.min.js +37 -34
- package/dist/cjs/doc/pivot-table.js +37 -3
- package/dist/cjs/doc/worksheet.js +0 -1
- package/dist/cjs/stream/xlsx/worksheet-writer.js +0 -1
- package/dist/cjs/utils/datetime.js +7 -156
- package/dist/cjs/xlsx/xform/book/sheet-xform.js +3 -2
- package/dist/cjs/xlsx/xform/book/workbook-properties-xform.js +1 -1
- package/dist/cjs/xlsx/xform/core/content-types-xform.js +12 -6
- package/dist/cjs/xlsx/xform/pivot-table/cache-field.js +11 -4
- package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +4 -11
- package/dist/cjs/xlsx/xform/pivot-table/pivot-table-xform.js +124 -17
- package/dist/cjs/xlsx/xform/sheet/page-setup-xform.js +4 -3
- package/dist/cjs/xlsx/xform/sheet/row-xform.js +1 -1
- package/dist/cjs/xlsx/xform/sheet/sheet-format-properties-xform.js +6 -3
- package/dist/cjs/xlsx/xform/sheet/sheet-view-xform.js +8 -4
- package/dist/cjs/xlsx/xform/sheet/worksheet-xform.js +2 -2
- package/dist/cjs/xlsx/xform/style/font-xform.js +36 -23
- package/dist/cjs/xlsx/xform/table/auto-filter-xform.js +3 -1
- package/dist/cjs/xlsx/xform/table/table-column-xform.js +2 -1
- package/dist/cjs/xlsx/xform/table/table-xform.js +5 -9
- package/dist/esm/doc/pivot-table.js +37 -3
- package/dist/esm/doc/worksheet.js +0 -1
- package/dist/esm/stream/xlsx/worksheet-writer.js +0 -1
- package/dist/esm/utils/datetime.js +7 -153
- package/dist/esm/xlsx/xform/book/sheet-xform.js +3 -2
- package/dist/esm/xlsx/xform/book/workbook-properties-xform.js +1 -1
- package/dist/esm/xlsx/xform/core/content-types-xform.js +12 -6
- package/dist/esm/xlsx/xform/pivot-table/cache-field.js +11 -4
- package/dist/esm/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +4 -11
- package/dist/esm/xlsx/xform/pivot-table/pivot-table-xform.js +124 -17
- package/dist/esm/xlsx/xform/sheet/page-setup-xform.js +4 -3
- package/dist/esm/xlsx/xform/sheet/row-xform.js +1 -1
- package/dist/esm/xlsx/xform/sheet/sheet-format-properties-xform.js +6 -3
- package/dist/esm/xlsx/xform/sheet/sheet-view-xform.js +8 -4
- package/dist/esm/xlsx/xform/sheet/worksheet-xform.js +2 -2
- package/dist/esm/xlsx/xform/style/font-xform.js +36 -23
- package/dist/esm/xlsx/xform/table/auto-filter-xform.js +3 -1
- package/dist/esm/xlsx/xform/table/table-column-xform.js +2 -1
- package/dist/esm/xlsx/xform/table/table-xform.js +5 -9
- package/dist/types/csv/csv-core.d.ts +0 -6
- package/dist/types/doc/pivot-table.d.ts +5 -1
- package/dist/types/stream/xlsx/workbook-reader.d.ts +1 -1
- package/dist/types/stream/xlsx/worksheet-reader.d.ts +1 -1
- package/dist/types/stream/xlsx/worksheet-writer.d.ts +1 -1
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/datetime.d.ts +0 -29
- package/dist/types/xlsx/xform/pivot-table/cache-field.d.ts +5 -1
- package/dist/types/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +0 -5
- package/dist/types/xlsx/xform/pivot-table/pivot-table-xform.d.ts +0 -3
- package/dist/types/xlsx/xform/sheet/sheet-format-properties-xform.d.ts +1 -1
- package/dist/types/xlsx/xform/sheet/worksheet-xform.d.ts +1 -1
- package/dist/types/xlsx/xform/style/font-xform.d.ts +1 -0
- package/dist/types/xlsx/xform/table/table-xform.d.ts +0 -4
- package/package.json +1 -1
|
@@ -10,10 +10,13 @@ class SheetFormatPropertiesXform extends base_xform_1.BaseXform {
|
|
|
10
10
|
if (model) {
|
|
11
11
|
const attributes = {
|
|
12
12
|
defaultRowHeight: model.defaultRowHeight,
|
|
13
|
-
outlineLevelRow
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// Only output outlineLevelRow/Col when non-zero (matches Excel behavior)
|
|
14
|
+
outlineLevelRow: model.outlineLevelRow || undefined,
|
|
15
|
+
outlineLevelCol: model.outlineLevelCol || undefined,
|
|
16
|
+
// Only output dyDescent if explicitly set (MS extension, not ECMA-376 standard)
|
|
17
|
+
"x14ac:dyDescent": model.dyDescent || undefined
|
|
16
18
|
};
|
|
19
|
+
// Only output defaultColWidth if explicitly set
|
|
17
20
|
if (model.defaultColWidth) {
|
|
18
21
|
attributes.defaultColWidth = model.defaultColWidth;
|
|
19
22
|
}
|
|
@@ -23,16 +23,20 @@ class SheetViewXform extends base_xform_1.BaseXform {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
render(xmlStream, model) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// Build initial attributes with correct order to match Excel output
|
|
27
|
+
const initialAttrs = {};
|
|
28
|
+
if (model.tabSelected) {
|
|
29
|
+
initialAttrs.tabSelected = "1";
|
|
30
|
+
}
|
|
31
|
+
initialAttrs.workbookViewId = model.workbookViewId || 0;
|
|
32
|
+
xmlStream.openNode("sheetView", initialAttrs);
|
|
29
33
|
const add = function (name, value, included) {
|
|
30
34
|
if (included) {
|
|
31
35
|
xmlStream.addAttribute(name, value);
|
|
32
36
|
}
|
|
33
37
|
};
|
|
34
38
|
add("rightToLeft", "1", model.rightToLeft === true);
|
|
35
|
-
|
|
39
|
+
// tabSelected is now in initialAttrs
|
|
36
40
|
add("showRuler", "0", model.showRuler === false);
|
|
37
41
|
add("showRowColHeaders", "0", model.showRowColHeaders === false);
|
|
38
42
|
add("showGridLines", "0", model.showGridLines === false);
|
|
@@ -515,6 +515,6 @@ WorkSheetXform.WORKSHEET_ATTRIBUTES = {
|
|
|
515
515
|
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
516
516
|
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
517
517
|
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
518
|
-
"
|
|
519
|
-
"
|
|
518
|
+
"xmlns:x14ac": "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac",
|
|
519
|
+
"mc:Ignorable": "x14ac"
|
|
520
520
|
};
|
|
@@ -12,35 +12,48 @@ class FontXform extends base_xform_1.BaseXform {
|
|
|
12
12
|
constructor(options) {
|
|
13
13
|
super();
|
|
14
14
|
this.options = options || FontXform.OPTIONS;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
// Define properties in render order (Excel's expected order)
|
|
16
|
+
const fontProperties = [
|
|
17
|
+
{ tag: "b", prop: "bold", xform: new boolean_xform_1.BooleanXform({ tag: "b", attr: "val" }) },
|
|
18
|
+
{ tag: "i", prop: "italic", xform: new boolean_xform_1.BooleanXform({ tag: "i", attr: "val" }) },
|
|
19
|
+
{ tag: "u", prop: "underline", xform: new underline_xform_1.UnderlineXform() },
|
|
20
|
+
{ tag: "strike", prop: "strike", xform: new boolean_xform_1.BooleanXform({ tag: "strike", attr: "val" }) },
|
|
21
|
+
{
|
|
22
|
+
tag: "condense",
|
|
23
|
+
prop: "condense",
|
|
24
|
+
xform: new boolean_xform_1.BooleanXform({ tag: "condense", attr: "val" })
|
|
25
|
+
},
|
|
26
|
+
{ tag: "extend", prop: "extend", xform: new boolean_xform_1.BooleanXform({ tag: "extend", attr: "val" }) },
|
|
27
|
+
{ tag: "outline", prop: "outline", xform: new boolean_xform_1.BooleanXform({ tag: "outline", attr: "val" }) },
|
|
28
|
+
{ tag: "shadow", prop: "shadow", xform: new boolean_xform_1.BooleanXform({ tag: "shadow", attr: "val" }) },
|
|
29
|
+
{ tag: "sz", prop: "size", xform: new integer_xform_1.IntegerXform({ tag: "sz", attr: "val" }) },
|
|
30
|
+
{ tag: "color", prop: "color", xform: new color_xform_1.ColorXform() },
|
|
31
|
+
{
|
|
32
|
+
tag: this.options.fontNameTag,
|
|
33
|
+
prop: "name",
|
|
34
|
+
xform: new string_xform_1.StringXform({ tag: this.options.fontNameTag, attr: "val" })
|
|
35
|
+
},
|
|
36
|
+
{ tag: "family", prop: "family", xform: new integer_xform_1.IntegerXform({ tag: "family", attr: "val" }) },
|
|
37
|
+
{ tag: "scheme", prop: "scheme", xform: new string_xform_1.StringXform({ tag: "scheme", attr: "val" }) },
|
|
38
|
+
{ tag: "charset", prop: "charset", xform: new integer_xform_1.IntegerXform({ tag: "charset", attr: "val" }) },
|
|
39
|
+
{
|
|
40
|
+
tag: "vertAlign",
|
|
41
|
+
prop: "vertAlign",
|
|
42
|
+
xform: new string_xform_1.StringXform({ tag: "vertAlign", attr: "val" })
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
// Build map and renderOrder from single source of truth
|
|
46
|
+
this.map = Object.fromEntries(fontProperties.map(p => [p.tag, { prop: p.prop, xform: p.xform }]));
|
|
47
|
+
this.renderOrder = fontProperties.map(p => p.tag);
|
|
35
48
|
}
|
|
36
49
|
get tag() {
|
|
37
50
|
return this.options.tagName;
|
|
38
51
|
}
|
|
39
52
|
render(xmlStream, model) {
|
|
40
|
-
const { map } = this;
|
|
53
|
+
const { map, renderOrder } = this;
|
|
41
54
|
xmlStream.openNode(this.options.tagName);
|
|
42
|
-
|
|
43
|
-
map[tag].xform.render(xmlStream, model[
|
|
55
|
+
renderOrder.forEach(tag => {
|
|
56
|
+
map[tag].xform.render(xmlStream, model[map[tag].prop]);
|
|
44
57
|
});
|
|
45
58
|
xmlStream.closeNode();
|
|
46
59
|
}
|
|
@@ -20,7 +20,9 @@ class AutoFilterXform extends base_xform_1.BaseXform {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
render(xmlStream, model) {
|
|
23
|
-
xmlStream.openNode(this.tag, {
|
|
23
|
+
xmlStream.openNode(this.tag, {
|
|
24
|
+
ref: model.autoFilterRef
|
|
25
|
+
});
|
|
24
26
|
model.columns.forEach((column) => {
|
|
25
27
|
this.map.filterColumn.render(xmlStream, column);
|
|
26
28
|
});
|
|
@@ -18,7 +18,8 @@ class TableColumnXform extends base_xform_1.BaseXform {
|
|
|
18
18
|
id: model.id.toString(),
|
|
19
19
|
name: model.name,
|
|
20
20
|
totalsRowLabel: model.totalsRowLabel,
|
|
21
|
-
totalsRowFunction
|
|
21
|
+
// Excel doesn't output totalsRowFunction when value is 'none'
|
|
22
|
+
totalsRowFunction: model.totalsRowFunction === "none" ? undefined : model.totalsRowFunction,
|
|
22
23
|
dxfId: model.dxfId
|
|
23
24
|
});
|
|
24
25
|
}
|
|
@@ -43,8 +43,8 @@ class TableXform extends base_xform_1.BaseXform {
|
|
|
43
43
|
displayName: model.displayName || model.name,
|
|
44
44
|
ref: model.tableRef,
|
|
45
45
|
totalsRowCount: model.totalsRow ? "1" : undefined,
|
|
46
|
-
|
|
47
|
-
headerRowCount: model.headerRow ?
|
|
46
|
+
// Excel doesn't output headerRowCount when it's 1 (default) or when there's a header row
|
|
47
|
+
headerRowCount: model.headerRow ? undefined : "0"
|
|
48
48
|
});
|
|
49
49
|
this.map.autoFilter.render(xmlStream, model);
|
|
50
50
|
this.map.tableColumns.render(xmlStream, model.columns);
|
|
@@ -65,7 +65,8 @@ class TableXform extends base_xform_1.BaseXform {
|
|
|
65
65
|
displayName: attributes.displayName || attributes.name,
|
|
66
66
|
tableRef: attributes.ref,
|
|
67
67
|
totalsRow: attributes.totalsRowCount === "1",
|
|
68
|
-
|
|
68
|
+
// ECMA-376: headerRowCount defaults to 1, so missing attribute means has header
|
|
69
|
+
headerRow: attributes.headerRowCount !== "0"
|
|
69
70
|
};
|
|
70
71
|
break;
|
|
71
72
|
default:
|
|
@@ -124,10 +125,5 @@ class TableXform extends base_xform_1.BaseXform {
|
|
|
124
125
|
}
|
|
125
126
|
exports.TableXform = TableXform;
|
|
126
127
|
TableXform.TABLE_ATTRIBUTES = {
|
|
127
|
-
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
128
|
-
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
129
|
-
"mc:Ignorable": "xr xr3",
|
|
130
|
-
"xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision",
|
|
131
|
-
"xmlns:xr3": "http://schemas.microsoft.com/office/spreadsheetml/2016/revision3"
|
|
132
|
-
// 'xr:uid': '{00000000-000C-0000-FFFF-FFFF00000000}',
|
|
128
|
+
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
133
129
|
};
|
|
@@ -34,8 +34,11 @@ function createTableSourceAdapter(table) {
|
|
|
34
34
|
const endRow = startRow + tableModel.rows.length; // header row + data rows
|
|
35
35
|
const endCol = startCol + columnNames.length - 1;
|
|
36
36
|
const shortRange = colCache.encode(startRow, startCol, endRow, endCol);
|
|
37
|
+
// Use the worksheet name (not table name) for pivotCacheDefinition's worksheetSource
|
|
38
|
+
// The sheet attribute in worksheetSource must reference the actual worksheet name
|
|
39
|
+
const worksheetName = table.worksheet.name;
|
|
37
40
|
return {
|
|
38
|
-
name:
|
|
41
|
+
name: worksheetName,
|
|
39
42
|
getRow(rowNumber) {
|
|
40
43
|
if (rowNumber === 1) {
|
|
41
44
|
return { values: headerRow };
|
|
@@ -184,12 +187,43 @@ function makeCacheFields(source, fieldNamesWithSharedItems) {
|
|
|
184
187
|
}
|
|
185
188
|
return toSortedArray(uniqueValues);
|
|
186
189
|
};
|
|
190
|
+
// Calculate min/max for numeric fields
|
|
191
|
+
const getMinMax = (columnIndex) => {
|
|
192
|
+
const columnValues = source.getColumn(columnIndex).values;
|
|
193
|
+
let min = Infinity;
|
|
194
|
+
let max = -Infinity;
|
|
195
|
+
let hasNumeric = false;
|
|
196
|
+
for (let i = 2; i < columnValues.length; i++) {
|
|
197
|
+
const v = columnValues[i];
|
|
198
|
+
if (typeof v === "number" && !isNaN(v)) {
|
|
199
|
+
hasNumeric = true;
|
|
200
|
+
if (v < min) {
|
|
201
|
+
min = v;
|
|
202
|
+
}
|
|
203
|
+
if (v > max) {
|
|
204
|
+
max = v;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return hasNumeric ? { minValue: min, maxValue: max } : null;
|
|
209
|
+
};
|
|
187
210
|
// Build result array
|
|
188
211
|
const result = [];
|
|
189
212
|
for (const columnIndex of range(1, names.length)) {
|
|
190
213
|
const name = names[columnIndex];
|
|
191
|
-
|
|
192
|
-
|
|
214
|
+
if (sharedItemsFields.has(name)) {
|
|
215
|
+
result.push({ name, sharedItems: aggregate(columnIndex) });
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Numeric field - calculate min/max
|
|
219
|
+
const minMax = getMinMax(columnIndex);
|
|
220
|
+
result.push({
|
|
221
|
+
name,
|
|
222
|
+
sharedItems: null,
|
|
223
|
+
minValue: minMax?.minValue,
|
|
224
|
+
maxValue: minMax?.maxValue
|
|
225
|
+
});
|
|
226
|
+
}
|
|
193
227
|
}
|
|
194
228
|
return result;
|
|
195
229
|
}
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
const PAD2 = Array.from({ length: 60 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));
|
|
12
12
|
// Character codes for fast comparison
|
|
13
13
|
const C_0 = 48;
|
|
14
|
-
const C_9 = 57;
|
|
15
14
|
const C_DASH = 45;
|
|
16
15
|
const C_SLASH = 47;
|
|
17
16
|
const C_COLON = 58;
|
|
@@ -187,158 +186,6 @@ const AUTO_DETECT = [
|
|
|
187
186
|
[29, [parseISOMsOffset]]
|
|
188
187
|
];
|
|
189
188
|
// ============================================================================
|
|
190
|
-
// Public API
|
|
191
|
-
// ============================================================================
|
|
192
|
-
/**
|
|
193
|
-
* Quick pre-filter to reject non-date strings
|
|
194
|
-
*/
|
|
195
|
-
export function mightBeDate(s) {
|
|
196
|
-
const len = s.length;
|
|
197
|
-
if (len < 10 || len > 30) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
const c0 = s.charCodeAt(0);
|
|
201
|
-
const c1 = s.charCodeAt(1);
|
|
202
|
-
// Must start with digits
|
|
203
|
-
if (c0 < C_0 || c0 > C_9 || c1 < C_0 || c1 > C_9) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
const c2 = s.charCodeAt(2);
|
|
207
|
-
// Either YYYY format (4 digits) or MM/DD format (separator at pos 2)
|
|
208
|
-
return (c2 >= C_0 && c2 <= C_9) || c2 === C_DASH || c2 === C_SLASH;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Parse a date string
|
|
212
|
-
*
|
|
213
|
-
* @param value - String to parse
|
|
214
|
-
* @param formats - Specific formats to try (optional, auto-detects ISO if omitted)
|
|
215
|
-
* @returns Date object or null
|
|
216
|
-
*
|
|
217
|
-
* @example
|
|
218
|
-
* parseDate("2024-12-26") // auto-detect ISO
|
|
219
|
-
* parseDate("12-26-2024", ["MM-DD-YYYY"]) // explicit US format
|
|
220
|
-
*/
|
|
221
|
-
export function parseDate(value, formats) {
|
|
222
|
-
if (!value || typeof value !== "string") {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
const s = value.trim();
|
|
226
|
-
if (!s) {
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
// Explicit formats
|
|
230
|
-
if (formats?.length) {
|
|
231
|
-
for (const fmt of formats) {
|
|
232
|
-
const result = PARSERS[fmt]?.(s);
|
|
233
|
-
if (result) {
|
|
234
|
-
return result;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
// Auto-detect by length
|
|
240
|
-
const len = s.length;
|
|
241
|
-
for (const [l, parsers] of AUTO_DETECT) {
|
|
242
|
-
if (len === l) {
|
|
243
|
-
for (const p of parsers) {
|
|
244
|
-
const result = p(s);
|
|
245
|
-
if (result) {
|
|
246
|
-
return result;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Format a Date to string
|
|
255
|
-
*
|
|
256
|
-
* @param date - Date to format
|
|
257
|
-
* @param format - Format template (optional, defaults to ISO)
|
|
258
|
-
* @param utc - Use UTC time (default: false)
|
|
259
|
-
* @returns Formatted string or empty string if invalid
|
|
260
|
-
*
|
|
261
|
-
* @example
|
|
262
|
-
* formatDate(date) // "2024-12-26T10:30:00.000+08:00"
|
|
263
|
-
* formatDate(date, "YYYY-MM-DD", true) // "2024-12-26"
|
|
264
|
-
*/
|
|
265
|
-
export function formatDate(date, format, utc = false) {
|
|
266
|
-
if (!(date instanceof Date)) {
|
|
267
|
-
return "";
|
|
268
|
-
}
|
|
269
|
-
const t = date.getTime();
|
|
270
|
-
if (t !== t) {
|
|
271
|
-
return "";
|
|
272
|
-
} // NaN check (faster than isNaN)
|
|
273
|
-
// Default ISO format - direct string building (faster than toISOString)
|
|
274
|
-
if (!format) {
|
|
275
|
-
if (utc) {
|
|
276
|
-
const y = date.getUTCFullYear();
|
|
277
|
-
const M = date.getUTCMonth() + 1;
|
|
278
|
-
const D = date.getUTCDate();
|
|
279
|
-
const H = date.getUTCHours();
|
|
280
|
-
const m = date.getUTCMinutes();
|
|
281
|
-
const s = date.getUTCSeconds();
|
|
282
|
-
const ms = date.getUTCMilliseconds();
|
|
283
|
-
return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}Z`;
|
|
284
|
-
}
|
|
285
|
-
const y = date.getFullYear();
|
|
286
|
-
const M = date.getMonth() + 1;
|
|
287
|
-
const D = date.getDate();
|
|
288
|
-
const H = date.getHours();
|
|
289
|
-
const m = date.getMinutes();
|
|
290
|
-
const s = date.getSeconds();
|
|
291
|
-
const ms = date.getMilliseconds();
|
|
292
|
-
return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}${tzOffset(date)}`;
|
|
293
|
-
}
|
|
294
|
-
// Fast paths for common formats
|
|
295
|
-
if (format === "YYYY-MM-DD") {
|
|
296
|
-
return utc
|
|
297
|
-
? `${date.getUTCFullYear()}-${PAD2[date.getUTCMonth() + 1]}-${PAD2[date.getUTCDate()]}`
|
|
298
|
-
: `${date.getFullYear()}-${PAD2[date.getMonth() + 1]}-${PAD2[date.getDate()]}`;
|
|
299
|
-
}
|
|
300
|
-
return renderFormat(date, format, utc);
|
|
301
|
-
}
|
|
302
|
-
function tzOffset(d) {
|
|
303
|
-
const off = -d.getTimezoneOffset();
|
|
304
|
-
const sign = off >= 0 ? "+" : "-";
|
|
305
|
-
const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
|
|
306
|
-
const m = Math.abs(off) % 60;
|
|
307
|
-
return `${sign}${PAD2[h]}:${PAD2[m]}`;
|
|
308
|
-
}
|
|
309
|
-
function renderFormat(d, fmt, utc) {
|
|
310
|
-
// Handle escaped sections [...]
|
|
311
|
-
const esc = [];
|
|
312
|
-
let out = fmt.replace(/\[([^\]]*)\]/g, (_, c) => {
|
|
313
|
-
esc.push(c);
|
|
314
|
-
return `\x00${esc.length - 1}\x00`;
|
|
315
|
-
});
|
|
316
|
-
// Get components once
|
|
317
|
-
const y = utc ? d.getUTCFullYear() : d.getFullYear();
|
|
318
|
-
const M = utc ? d.getUTCMonth() + 1 : d.getMonth() + 1;
|
|
319
|
-
const D = utc ? d.getUTCDate() : d.getDate();
|
|
320
|
-
const H = utc ? d.getUTCHours() : d.getHours();
|
|
321
|
-
const m = utc ? d.getUTCMinutes() : d.getMinutes();
|
|
322
|
-
const s = utc ? d.getUTCSeconds() : d.getSeconds();
|
|
323
|
-
const ms = utc ? d.getUTCMilliseconds() : d.getMilliseconds();
|
|
324
|
-
// Replace tokens
|
|
325
|
-
out = out
|
|
326
|
-
.replace(/YYYY/g, String(y))
|
|
327
|
-
.replace(/SSS/g, ms < 10 ? `00${ms}` : ms < 100 ? `0${ms}` : String(ms))
|
|
328
|
-
.replace(/MM/g, PAD2[M])
|
|
329
|
-
.replace(/DD/g, PAD2[D])
|
|
330
|
-
.replace(/HH/g, PAD2[H])
|
|
331
|
-
.replace(/mm/g, PAD2[m])
|
|
332
|
-
.replace(/ss/g, PAD2[s])
|
|
333
|
-
.replace(/Z/g, utc ? "Z" : tzOffset(d));
|
|
334
|
-
// Restore escaped
|
|
335
|
-
if (esc.length) {
|
|
336
|
-
// oxlint-disable-next-line no-control-regex
|
|
337
|
-
out = out.replace(/\x00(\d+)\x00/g, (_, i) => esc[+i]);
|
|
338
|
-
}
|
|
339
|
-
return out;
|
|
340
|
-
}
|
|
341
|
-
// ============================================================================
|
|
342
189
|
// High-performance batch processors (class-based for state encapsulation)
|
|
343
190
|
// ============================================================================
|
|
344
191
|
/**
|
|
@@ -411,6 +258,13 @@ export class DateParser {
|
|
|
411
258
|
return out;
|
|
412
259
|
}
|
|
413
260
|
}
|
|
261
|
+
function tzOffset(d) {
|
|
262
|
+
const off = -d.getTimezoneOffset();
|
|
263
|
+
const sign = off >= 0 ? "+" : "-";
|
|
264
|
+
const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
|
|
265
|
+
const m = Math.abs(off) % 60;
|
|
266
|
+
return `${sign}${PAD2[h]}:${PAD2[m]}`;
|
|
267
|
+
}
|
|
414
268
|
/**
|
|
415
269
|
* Optimized date formatter for batch processing
|
|
416
270
|
*
|
|
@@ -3,9 +3,10 @@ import { BaseXform } from "../base-xform.js";
|
|
|
3
3
|
class WorksheetXform extends BaseXform {
|
|
4
4
|
render(xmlStream, model) {
|
|
5
5
|
xmlStream.leafNode("sheet", {
|
|
6
|
-
sheetId: model.id,
|
|
7
6
|
name: model.name,
|
|
8
|
-
|
|
7
|
+
sheetId: model.id,
|
|
8
|
+
// Excel doesn't output state when it's 'visible' (default)
|
|
9
|
+
state: model.state === "visible" ? undefined : model.state,
|
|
9
10
|
"r:id": model.rId
|
|
10
11
|
});
|
|
11
12
|
}
|
|
@@ -38,7 +38,7 @@ class ContentTypesXform extends BaseXform {
|
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
if ((model.pivotTables || []).length) {
|
|
41
|
-
// Add content types for
|
|
41
|
+
// Add content types for pivot cache (definition and records)
|
|
42
42
|
(model.pivotTables || []).forEach((pivotTable) => {
|
|
43
43
|
const n = pivotTable.tableNumber;
|
|
44
44
|
xmlStream.leafNode("Override", {
|
|
@@ -49,10 +49,6 @@ class ContentTypesXform extends BaseXform {
|
|
|
49
49
|
PartName: `/xl/pivotCache/pivotCacheRecords${n}.xml`,
|
|
50
50
|
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
|
|
51
51
|
});
|
|
52
|
-
xmlStream.leafNode("Override", {
|
|
53
|
-
PartName: `/xl/pivotTables/pivotTable${n}.xml`,
|
|
54
|
-
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
|
|
55
|
-
});
|
|
56
52
|
});
|
|
57
53
|
}
|
|
58
54
|
xmlStream.leafNode("Override", {
|
|
@@ -78,6 +74,16 @@ class ContentTypesXform extends BaseXform {
|
|
|
78
74
|
});
|
|
79
75
|
});
|
|
80
76
|
}
|
|
77
|
+
// Add pivot table overrides after tables (matches Excel order)
|
|
78
|
+
if ((model.pivotTables || []).length) {
|
|
79
|
+
(model.pivotTables || []).forEach((pivotTable) => {
|
|
80
|
+
const n = pivotTable.tableNumber;
|
|
81
|
+
xmlStream.leafNode("Override", {
|
|
82
|
+
PartName: `/xl/pivotTables/pivotTable${n}.xml`,
|
|
83
|
+
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
81
87
|
if (model.drawings) {
|
|
82
88
|
model.drawings.forEach((drawing) => {
|
|
83
89
|
xmlStream.leafNode("Override", {
|
|
@@ -86,7 +92,7 @@ class ContentTypesXform extends BaseXform {
|
|
|
86
92
|
});
|
|
87
93
|
});
|
|
88
94
|
}
|
|
89
|
-
if (model.commentRefs) {
|
|
95
|
+
if (model.commentRefs && model.commentRefs.length) {
|
|
90
96
|
xmlStream.leafNode("Default", {
|
|
91
97
|
Extension: "vml",
|
|
92
98
|
ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { xmlEncode } from "../../../utils/utils.js";
|
|
2
2
|
class CacheField {
|
|
3
|
-
constructor({ name, sharedItems }) {
|
|
3
|
+
constructor({ name, sharedItems, minValue, maxValue }) {
|
|
4
4
|
// string type
|
|
5
5
|
//
|
|
6
6
|
// {
|
|
@@ -14,10 +14,14 @@ class CacheField {
|
|
|
14
14
|
//
|
|
15
15
|
// {
|
|
16
16
|
// 'name': 'D',
|
|
17
|
-
// 'sharedItems': null
|
|
17
|
+
// 'sharedItems': null,
|
|
18
|
+
// 'minValue': 5,
|
|
19
|
+
// 'maxValue': 45
|
|
18
20
|
// }
|
|
19
21
|
this.name = name;
|
|
20
22
|
this.sharedItems = sharedItems;
|
|
23
|
+
this.minValue = minValue;
|
|
24
|
+
this.maxValue = maxValue;
|
|
21
25
|
}
|
|
22
26
|
render() {
|
|
23
27
|
// PivotCache Field: http://www.datypic.com/sc/ooxml/e-ssml_cacheField-1.html
|
|
@@ -26,9 +30,12 @@ class CacheField {
|
|
|
26
30
|
const escapedName = xmlEncode(this.name);
|
|
27
31
|
// integer types
|
|
28
32
|
if (this.sharedItems === null) {
|
|
29
|
-
//
|
|
33
|
+
// Build minValue/maxValue attributes if available
|
|
34
|
+
const minMaxAttrs = this.minValue !== undefined && this.maxValue !== undefined
|
|
35
|
+
? ` minValue="${this.minValue}" maxValue="${this.maxValue}"`
|
|
36
|
+
: "";
|
|
30
37
|
return `<cacheField name="${escapedName}" numFmtId="0">
|
|
31
|
-
<sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" />
|
|
38
|
+
<sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1"${minMaxAttrs} />
|
|
32
39
|
</cacheField>`;
|
|
33
40
|
}
|
|
34
41
|
// string types - escape XML special characters in each shared item value
|
|
@@ -43,17 +43,17 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
43
43
|
*/
|
|
44
44
|
renderNew(xmlStream, model) {
|
|
45
45
|
const { source, cacheFields } = model;
|
|
46
|
+
// Record count = number of data rows (excluding header row)
|
|
47
|
+
const recordCount = source.getSheetValues().slice(2).length;
|
|
46
48
|
xmlStream.openXml(XmlStream.StdDocAttributes);
|
|
47
49
|
xmlStream.openNode(this.tag, {
|
|
48
50
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
49
51
|
"r:id": "rId1",
|
|
50
52
|
refreshOnLoad: "1", // important for our implementation to work
|
|
51
|
-
refreshedBy: "Author",
|
|
52
|
-
refreshedDate: "45125.026046874998",
|
|
53
53
|
createdVersion: "8",
|
|
54
54
|
refreshedVersion: "8",
|
|
55
55
|
minRefreshableVersion: "3",
|
|
56
|
-
recordCount
|
|
56
|
+
recordCount
|
|
57
57
|
});
|
|
58
58
|
xmlStream.openNode("cacheSource", { type: "worksheet" });
|
|
59
59
|
xmlStream.leafNode("worksheetSource", {
|
|
@@ -77,8 +77,6 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
77
77
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
78
78
|
"r:id": model.rId || "rId1",
|
|
79
79
|
refreshOnLoad: model.refreshOnLoad || "1",
|
|
80
|
-
refreshedBy: model.refreshedBy || "Author",
|
|
81
|
-
refreshedDate: model.refreshedDate || "45125.026046874998",
|
|
82
80
|
createdVersion: model.createdVersion || "8",
|
|
83
81
|
refreshedVersion: model.refreshedVersion || "8",
|
|
84
82
|
minRefreshableVersion: model.minRefreshableVersion || "3",
|
|
@@ -112,8 +110,6 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
112
110
|
cacheFields: [],
|
|
113
111
|
rId: attributes["r:id"],
|
|
114
112
|
refreshOnLoad: attributes.refreshOnLoad,
|
|
115
|
-
refreshedBy: attributes.refreshedBy,
|
|
116
|
-
refreshedDate: attributes.refreshedDate,
|
|
117
113
|
createdVersion: attributes.createdVersion,
|
|
118
114
|
refreshedVersion: attributes.refreshedVersion,
|
|
119
115
|
minRefreshableVersion: attributes.minRefreshableVersion,
|
|
@@ -178,9 +174,6 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
178
174
|
}
|
|
179
175
|
PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES = {
|
|
180
176
|
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
181
|
-
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
182
|
-
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
183
|
-
"mc:Ignorable": "xr",
|
|
184
|
-
"xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
|
|
177
|
+
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
185
178
|
};
|
|
186
179
|
export { PivotCacheDefinitionXform };
|