@niicojs/excel 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +241 -8
- package/dist/index.cjs +1485 -167
- package/dist/index.d.cts +376 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +376 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1485 -168
- package/package.json +1 -1
- package/src/index.ts +9 -1
- package/src/pivot-cache.ts +10 -1
- package/src/pivot-table.ts +176 -40
- package/src/range.ts +15 -2
- package/src/shared-strings.ts +65 -16
- package/src/styles.ts +192 -21
- package/src/table.ts +386 -0
- package/src/types.ts +74 -2
- package/src/utils/address.ts +4 -1
- package/src/utils/xml.ts +0 -7
- package/src/workbook.ts +426 -41
- package/src/worksheet.ts +484 -27
package/src/styles.ts
CHANGED
|
@@ -63,6 +63,13 @@ const normalizeColor = (color: string): string => {
|
|
|
63
63
|
return c;
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
interface StyleColor {
|
|
67
|
+
rgb?: string;
|
|
68
|
+
theme?: string;
|
|
69
|
+
tint?: string;
|
|
70
|
+
indexed?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
/**
|
|
67
74
|
* Manages the styles (xl/styles.xml)
|
|
68
75
|
*/
|
|
@@ -77,6 +84,62 @@ export class Styles {
|
|
|
77
84
|
|
|
78
85
|
// Cache for style deduplication
|
|
79
86
|
private _styleCache: Map<string, number> = new Map();
|
|
87
|
+
private _styleObjectCache: Map<number, CellStyle> = new Map();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generate a deterministic cache key for a style object.
|
|
91
|
+
* More efficient than JSON.stringify as it avoids the overhead of
|
|
92
|
+
* full JSON serialization and produces a consistent key regardless
|
|
93
|
+
* of property order.
|
|
94
|
+
*/
|
|
95
|
+
private _getStyleKey(style: CellStyle): string {
|
|
96
|
+
// Use a delimiter that won't appear in values
|
|
97
|
+
const SEP = '\x00';
|
|
98
|
+
|
|
99
|
+
// Build key from all style properties in a fixed order
|
|
100
|
+
const parts: string[] = [
|
|
101
|
+
style.bold ? '1' : '0',
|
|
102
|
+
style.italic ? '1' : '0',
|
|
103
|
+
style.underline === true ? '1' : style.underline === 'single' ? 's' : style.underline === 'double' ? 'd' : '0',
|
|
104
|
+
style.strike ? '1' : '0',
|
|
105
|
+
style.fontSize?.toString() ?? '',
|
|
106
|
+
style.fontName ?? '',
|
|
107
|
+
style.fontColor ?? '',
|
|
108
|
+
style.fontColorTheme?.toString() ?? '',
|
|
109
|
+
style.fontColorTint?.toString() ?? '',
|
|
110
|
+
style.fontColorIndexed?.toString() ?? '',
|
|
111
|
+
style.fill ?? '',
|
|
112
|
+
style.fillTheme?.toString() ?? '',
|
|
113
|
+
style.fillTint?.toString() ?? '',
|
|
114
|
+
style.fillIndexed?.toString() ?? '',
|
|
115
|
+
style.fillBgColor ?? '',
|
|
116
|
+
style.fillBgTheme?.toString() ?? '',
|
|
117
|
+
style.fillBgTint?.toString() ?? '',
|
|
118
|
+
style.fillBgIndexed?.toString() ?? '',
|
|
119
|
+
style.numberFormat ?? '',
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Border properties
|
|
123
|
+
if (style.border) {
|
|
124
|
+
parts.push(style.border.top ?? '', style.border.bottom ?? '', style.border.left ?? '', style.border.right ?? '');
|
|
125
|
+
} else {
|
|
126
|
+
parts.push('', '', '', '');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Alignment properties
|
|
130
|
+
if (style.alignment) {
|
|
131
|
+
parts.push(
|
|
132
|
+
style.alignment.horizontal ?? '',
|
|
133
|
+
style.alignment.vertical ?? '',
|
|
134
|
+
style.alignment.wrapText ? '1' : '0',
|
|
135
|
+
style.alignment.textRotation?.toString() ?? '',
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
parts.push('', '', '0', '');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return parts.join(SEP);
|
|
142
|
+
}
|
|
80
143
|
|
|
81
144
|
/**
|
|
82
145
|
* Parse styles from XML content
|
|
@@ -197,7 +260,18 @@ export class Styles {
|
|
|
197
260
|
if ('sz' in child) font.size = parseFloat(getAttr(child, 'val') || '11');
|
|
198
261
|
if ('name' in child) font.name = getAttr(child, 'val');
|
|
199
262
|
if ('color' in child) {
|
|
200
|
-
|
|
263
|
+
const color: StyleColor = {};
|
|
264
|
+
const rgb = getAttr(child, 'rgb');
|
|
265
|
+
const theme = getAttr(child, 'theme');
|
|
266
|
+
const tint = getAttr(child, 'tint');
|
|
267
|
+
const indexed = getAttr(child, 'indexed');
|
|
268
|
+
if (rgb) color.rgb = rgb;
|
|
269
|
+
if (theme) color.theme = theme;
|
|
270
|
+
if (tint) color.tint = tint;
|
|
271
|
+
if (indexed) color.indexed = indexed;
|
|
272
|
+
if (color.rgb || color.theme || color.tint || color.indexed) {
|
|
273
|
+
font.color = color;
|
|
274
|
+
}
|
|
201
275
|
}
|
|
202
276
|
}
|
|
203
277
|
|
|
@@ -216,10 +290,32 @@ export class Styles {
|
|
|
216
290
|
const pfChildren = getChildren(child, 'patternFill');
|
|
217
291
|
for (const pfChild of pfChildren) {
|
|
218
292
|
if ('fgColor' in pfChild) {
|
|
219
|
-
|
|
293
|
+
const color: StyleColor = {};
|
|
294
|
+
const rgb = getAttr(pfChild, 'rgb');
|
|
295
|
+
const theme = getAttr(pfChild, 'theme');
|
|
296
|
+
const tint = getAttr(pfChild, 'tint');
|
|
297
|
+
const indexed = getAttr(pfChild, 'indexed');
|
|
298
|
+
if (rgb) color.rgb = rgb;
|
|
299
|
+
if (theme) color.theme = theme;
|
|
300
|
+
if (tint) color.tint = tint;
|
|
301
|
+
if (indexed) color.indexed = indexed;
|
|
302
|
+
if (color.rgb || color.theme || color.tint || color.indexed) {
|
|
303
|
+
fill.fgColor = color;
|
|
304
|
+
}
|
|
220
305
|
}
|
|
221
306
|
if ('bgColor' in pfChild) {
|
|
222
|
-
|
|
307
|
+
const color: StyleColor = {};
|
|
308
|
+
const rgb = getAttr(pfChild, 'rgb');
|
|
309
|
+
const theme = getAttr(pfChild, 'theme');
|
|
310
|
+
const tint = getAttr(pfChild, 'tint');
|
|
311
|
+
const indexed = getAttr(pfChild, 'indexed');
|
|
312
|
+
if (rgb) color.rgb = rgb;
|
|
313
|
+
if (theme) color.theme = theme;
|
|
314
|
+
if (tint) color.tint = tint;
|
|
315
|
+
if (indexed) color.indexed = indexed;
|
|
316
|
+
if (color.rgb || color.theme || color.tint || color.indexed) {
|
|
317
|
+
fill.bgColor = color;
|
|
318
|
+
}
|
|
223
319
|
}
|
|
224
320
|
}
|
|
225
321
|
}
|
|
@@ -270,6 +366,9 @@ export class Styles {
|
|
|
270
366
|
* Get a style by index
|
|
271
367
|
*/
|
|
272
368
|
getStyle(index: number): CellStyle {
|
|
369
|
+
const cached = this._styleObjectCache.get(index);
|
|
370
|
+
if (cached) return { ...cached };
|
|
371
|
+
|
|
273
372
|
const xf = this._cellXfs[index];
|
|
274
373
|
if (!xf) return {};
|
|
275
374
|
|
|
@@ -288,11 +387,24 @@ export class Styles {
|
|
|
288
387
|
if (font.strike) style.strike = true;
|
|
289
388
|
if (font.size) style.fontSize = font.size;
|
|
290
389
|
if (font.name) style.fontName = font.name;
|
|
291
|
-
if (font.color) style.fontColor = font.color;
|
|
390
|
+
if (font.color?.rgb) style.fontColor = font.color.rgb;
|
|
391
|
+
if (font.color?.theme) style.fontColorTheme = Number(font.color.theme);
|
|
392
|
+
if (font.color?.tint) style.fontColorTint = Number(font.color.tint);
|
|
393
|
+
if (font.color?.indexed) style.fontColorIndexed = Number(font.color.indexed);
|
|
292
394
|
}
|
|
293
395
|
|
|
294
396
|
if (fill && fill.fgColor) {
|
|
295
|
-
style.fill = fill.fgColor;
|
|
397
|
+
if (fill.fgColor.rgb) style.fill = fill.fgColor.rgb;
|
|
398
|
+
if (fill.fgColor.theme) style.fillTheme = Number(fill.fgColor.theme);
|
|
399
|
+
if (fill.fgColor.tint) style.fillTint = Number(fill.fgColor.tint);
|
|
400
|
+
if (fill.fgColor.indexed) style.fillIndexed = Number(fill.fgColor.indexed);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (fill && fill.bgColor) {
|
|
404
|
+
if (fill.bgColor.rgb) style.fillBgColor = fill.bgColor.rgb;
|
|
405
|
+
if (fill.bgColor.theme) style.fillBgTheme = Number(fill.bgColor.theme);
|
|
406
|
+
if (fill.bgColor.tint) style.fillBgTint = Number(fill.bgColor.tint);
|
|
407
|
+
if (fill.bgColor.indexed) style.fillBgIndexed = Number(fill.bgColor.indexed);
|
|
296
408
|
}
|
|
297
409
|
|
|
298
410
|
if (border) {
|
|
@@ -319,6 +431,7 @@ export class Styles {
|
|
|
319
431
|
};
|
|
320
432
|
}
|
|
321
433
|
|
|
434
|
+
this._styleObjectCache.set(index, { ...style });
|
|
322
435
|
return style;
|
|
323
436
|
}
|
|
324
437
|
|
|
@@ -327,7 +440,7 @@ export class Styles {
|
|
|
327
440
|
* Uses caching to deduplicate identical styles
|
|
328
441
|
*/
|
|
329
442
|
createStyle(style: CellStyle): number {
|
|
330
|
-
const key =
|
|
443
|
+
const key = this._getStyleKey(style);
|
|
331
444
|
const cached = this._styleCache.get(key);
|
|
332
445
|
if (cached !== undefined) {
|
|
333
446
|
return cached;
|
|
@@ -367,11 +480,26 @@ export class Styles {
|
|
|
367
480
|
const index = this._cellXfs.length;
|
|
368
481
|
this._cellXfs.push(xf);
|
|
369
482
|
this._styleCache.set(key, index);
|
|
483
|
+
this._styleObjectCache.set(index, { ...style });
|
|
370
484
|
|
|
371
485
|
return index;
|
|
372
486
|
}
|
|
373
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Clone an existing style by index, optionally overriding fields.
|
|
490
|
+
*/
|
|
491
|
+
cloneStyle(index: number, overrides: Partial<CellStyle> = {}): number {
|
|
492
|
+
const baseStyle = this.getStyle(index);
|
|
493
|
+
return this.createStyle({ ...baseStyle, ...overrides });
|
|
494
|
+
}
|
|
495
|
+
|
|
374
496
|
private _findOrCreateFont(style: CellStyle): number {
|
|
497
|
+
const color = this._toStyleColor(
|
|
498
|
+
style.fontColor,
|
|
499
|
+
style.fontColorTheme,
|
|
500
|
+
style.fontColorTint,
|
|
501
|
+
style.fontColorIndexed,
|
|
502
|
+
);
|
|
375
503
|
const font: StyleFont = {
|
|
376
504
|
bold: style.bold || false,
|
|
377
505
|
italic: style.italic || false,
|
|
@@ -379,7 +507,7 @@ export class Styles {
|
|
|
379
507
|
strike: style.strike || false,
|
|
380
508
|
size: style.fontSize,
|
|
381
509
|
name: style.fontName,
|
|
382
|
-
color
|
|
510
|
+
color,
|
|
383
511
|
};
|
|
384
512
|
|
|
385
513
|
// Try to find existing font
|
|
@@ -392,7 +520,7 @@ export class Styles {
|
|
|
392
520
|
f.strike === font.strike &&
|
|
393
521
|
f.size === font.size &&
|
|
394
522
|
f.name === font.name &&
|
|
395
|
-
f.color
|
|
523
|
+
this._colorsEqual(f.color, font.color)
|
|
396
524
|
) {
|
|
397
525
|
return i;
|
|
398
526
|
}
|
|
@@ -404,12 +532,15 @@ export class Styles {
|
|
|
404
532
|
}
|
|
405
533
|
|
|
406
534
|
private _findOrCreateFill(style: CellStyle): number {
|
|
407
|
-
|
|
535
|
+
const fgColor = this._toStyleColor(style.fill, style.fillTheme, style.fillTint, style.fillIndexed);
|
|
536
|
+
const bgColor = this._toStyleColor(style.fillBgColor, style.fillBgTheme, style.fillBgTint, style.fillBgIndexed);
|
|
537
|
+
|
|
538
|
+
if (!fgColor && !bgColor) return 0;
|
|
408
539
|
|
|
409
540
|
// Try to find existing fill
|
|
410
541
|
for (let i = 0; i < this._fills.length; i++) {
|
|
411
542
|
const f = this._fills[i];
|
|
412
|
-
if (f.fgColor
|
|
543
|
+
if (this._colorsEqual(f.fgColor, fgColor) && this._colorsEqual(f.bgColor, bgColor)) {
|
|
413
544
|
return i;
|
|
414
545
|
}
|
|
415
546
|
}
|
|
@@ -417,7 +548,8 @@ export class Styles {
|
|
|
417
548
|
// Create new fill
|
|
418
549
|
this._fills.push({
|
|
419
550
|
type: 'solid',
|
|
420
|
-
fgColor:
|
|
551
|
+
fgColor: fgColor || undefined,
|
|
552
|
+
bgColor: bgColor || undefined,
|
|
421
553
|
});
|
|
422
554
|
return this._fills.length - 1;
|
|
423
555
|
}
|
|
@@ -542,7 +674,16 @@ export class Styles {
|
|
|
542
674
|
if (font.underline) children.push(createElement('u', {}, []));
|
|
543
675
|
if (font.strike) children.push(createElement('strike', {}, []));
|
|
544
676
|
if (font.size) children.push(createElement('sz', { val: String(font.size) }, []));
|
|
545
|
-
if (font.color)
|
|
677
|
+
if (font.color) {
|
|
678
|
+
const attrs: Record<string, string> = {};
|
|
679
|
+
if (font.color.rgb) attrs.rgb = normalizeColor(font.color.rgb);
|
|
680
|
+
if (font.color.theme) attrs.theme = font.color.theme;
|
|
681
|
+
if (font.color.tint) attrs.tint = font.color.tint;
|
|
682
|
+
if (font.color.indexed) attrs.indexed = font.color.indexed;
|
|
683
|
+
if (Object.keys(attrs).length > 0) {
|
|
684
|
+
children.push(createElement('color', attrs, []));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
546
687
|
if (font.name) children.push(createElement('name', { val: font.name }, []));
|
|
547
688
|
return createElement('font', {}, children);
|
|
548
689
|
}
|
|
@@ -550,21 +691,51 @@ export class Styles {
|
|
|
550
691
|
private _buildFillNode(fill: StyleFill): XmlNode {
|
|
551
692
|
const patternChildren: XmlNode[] = [];
|
|
552
693
|
if (fill.fgColor) {
|
|
553
|
-
const
|
|
554
|
-
|
|
694
|
+
const attrs: Record<string, string> = {};
|
|
695
|
+
if (fill.fgColor.rgb) attrs.rgb = normalizeColor(fill.fgColor.rgb);
|
|
696
|
+
if (fill.fgColor.theme) attrs.theme = fill.fgColor.theme;
|
|
697
|
+
if (fill.fgColor.tint) attrs.tint = fill.fgColor.tint;
|
|
698
|
+
if (fill.fgColor.indexed) attrs.indexed = fill.fgColor.indexed;
|
|
699
|
+
if (Object.keys(attrs).length > 0) {
|
|
700
|
+
patternChildren.push(createElement('fgColor', attrs, []));
|
|
701
|
+
}
|
|
555
702
|
// For solid fills, bgColor is required (indexed 64 = system background)
|
|
556
|
-
if (fill.type === 'solid') {
|
|
703
|
+
if (fill.type === 'solid' && !fill.bgColor) {
|
|
557
704
|
patternChildren.push(createElement('bgColor', { indexed: '64' }, []));
|
|
558
705
|
}
|
|
559
706
|
}
|
|
560
|
-
if (fill.bgColor
|
|
561
|
-
const
|
|
562
|
-
|
|
707
|
+
if (fill.bgColor) {
|
|
708
|
+
const attrs: Record<string, string> = {};
|
|
709
|
+
if (fill.bgColor.rgb) attrs.rgb = normalizeColor(fill.bgColor.rgb);
|
|
710
|
+
if (fill.bgColor.theme) attrs.theme = fill.bgColor.theme;
|
|
711
|
+
if (fill.bgColor.tint) attrs.tint = fill.bgColor.tint;
|
|
712
|
+
if (fill.bgColor.indexed) attrs.indexed = fill.bgColor.indexed;
|
|
713
|
+
if (Object.keys(attrs).length > 0) {
|
|
714
|
+
patternChildren.push(createElement('bgColor', attrs, []));
|
|
715
|
+
}
|
|
563
716
|
}
|
|
564
717
|
const patternFill = createElement('patternFill', { patternType: fill.type || 'none' }, patternChildren);
|
|
565
718
|
return createElement('fill', {}, [patternFill]);
|
|
566
719
|
}
|
|
567
720
|
|
|
721
|
+
private _toStyleColor(rgb?: string, theme?: number, tint?: number, indexed?: number): StyleColor | undefined {
|
|
722
|
+
if (rgb) {
|
|
723
|
+
return { rgb };
|
|
724
|
+
}
|
|
725
|
+
const color: StyleColor = {};
|
|
726
|
+
if (theme !== undefined) color.theme = String(theme);
|
|
727
|
+
if (tint !== undefined) color.tint = String(tint);
|
|
728
|
+
if (indexed !== undefined) color.indexed = String(indexed);
|
|
729
|
+
if (color.theme || color.tint || color.indexed) return color;
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private _colorsEqual(a?: StyleColor, b?: StyleColor): boolean {
|
|
734
|
+
if (!a && !b) return true;
|
|
735
|
+
if (!a || !b) return false;
|
|
736
|
+
return a.rgb === b.rgb && a.theme === b.theme && a.tint === b.tint && a.indexed === b.indexed;
|
|
737
|
+
}
|
|
738
|
+
|
|
568
739
|
private _buildBorderNode(border: StyleBorder): XmlNode {
|
|
569
740
|
const children: XmlNode[] = [];
|
|
570
741
|
if (border.left) children.push(createElement('left', { style: border.left }, []));
|
|
@@ -616,13 +787,13 @@ interface StyleFont {
|
|
|
616
787
|
strike: boolean;
|
|
617
788
|
size?: number;
|
|
618
789
|
name?: string;
|
|
619
|
-
color?:
|
|
790
|
+
color?: StyleColor;
|
|
620
791
|
}
|
|
621
792
|
|
|
622
793
|
interface StyleFill {
|
|
623
794
|
type: string;
|
|
624
|
-
fgColor?:
|
|
625
|
-
bgColor?:
|
|
795
|
+
fgColor?: StyleColor;
|
|
796
|
+
bgColor?: StyleColor;
|
|
626
797
|
}
|
|
627
798
|
|
|
628
799
|
interface StyleBorder {
|