@niicojs/excel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ import { parseXml, findElement, getChildren, XmlNode, stringifyXml, createElement, createText } from './utils/xml';
2
+
3
+ /**
4
+ * Manages the shared strings table from xl/sharedStrings.xml
5
+ * Excel stores strings in a shared table to reduce file size
6
+ */
7
+ export class SharedStrings {
8
+ private strings: string[] = [];
9
+ private stringToIndex: Map<string, number> = new Map();
10
+ private _dirty = false;
11
+
12
+ /**
13
+ * Parse shared strings from XML content
14
+ */
15
+ static parse(xml: string): SharedStrings {
16
+ const ss = new SharedStrings();
17
+ const parsed = parseXml(xml);
18
+ const sst = findElement(parsed, 'sst');
19
+ if (!sst) return ss;
20
+
21
+ const children = getChildren(sst, 'sst');
22
+ for (const child of children) {
23
+ if ('si' in child) {
24
+ const siChildren = getChildren(child, 'si');
25
+ const text = ss.extractText(siChildren);
26
+ ss.strings.push(text);
27
+ ss.stringToIndex.set(text, ss.strings.length - 1);
28
+ }
29
+ }
30
+
31
+ return ss;
32
+ }
33
+
34
+ /**
35
+ * Extract text from a string item (si element)
36
+ * Handles both simple <t> elements and rich text <r> elements
37
+ */
38
+ private extractText(nodes: XmlNode[]): string {
39
+ let text = '';
40
+ for (const node of nodes) {
41
+ if ('t' in node) {
42
+ // Simple text: <t>value</t>
43
+ const tChildren = getChildren(node, 't');
44
+ for (const child of tChildren) {
45
+ if ('#text' in child) {
46
+ text += child['#text'] as string;
47
+ }
48
+ }
49
+ } else if ('r' in node) {
50
+ // Rich text: <r><t>value</t></r>
51
+ const rChildren = getChildren(node, 'r');
52
+ for (const rChild of rChildren) {
53
+ if ('t' in rChild) {
54
+ const tChildren = getChildren(rChild, 't');
55
+ for (const child of tChildren) {
56
+ if ('#text' in child) {
57
+ text += child['#text'] as string;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ return text;
65
+ }
66
+
67
+ /**
68
+ * Get a string by index
69
+ */
70
+ getString(index: number): string | undefined {
71
+ return this.strings[index];
72
+ }
73
+
74
+ /**
75
+ * Add a string and return its index
76
+ * If the string already exists, returns the existing index
77
+ */
78
+ addString(str: string): number {
79
+ const existing = this.stringToIndex.get(str);
80
+ if (existing !== undefined) {
81
+ return existing;
82
+ }
83
+ const index = this.strings.length;
84
+ this.strings.push(str);
85
+ this.stringToIndex.set(str, index);
86
+ this._dirty = true;
87
+ return index;
88
+ }
89
+
90
+ /**
91
+ * Check if the shared strings table has been modified
92
+ */
93
+ get dirty(): boolean {
94
+ return this._dirty;
95
+ }
96
+
97
+ /**
98
+ * Get the count of strings
99
+ */
100
+ get count(): number {
101
+ return this.strings.length;
102
+ }
103
+
104
+ /**
105
+ * Generate XML for the shared strings table
106
+ */
107
+ toXml(): string {
108
+ const siElements: XmlNode[] = [];
109
+ for (const str of this.strings) {
110
+ const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [
111
+ createText(str),
112
+ ]);
113
+ const siElement = createElement('si', {}, [tElement]);
114
+ siElements.push(siElement);
115
+ }
116
+
117
+ const sst = createElement(
118
+ 'sst',
119
+ {
120
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
121
+ count: String(this.strings.length),
122
+ uniqueCount: String(this.strings.length),
123
+ },
124
+ siElements,
125
+ );
126
+
127
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([sst])}`;
128
+ }
129
+ }
package/src/styles.ts ADDED
@@ -0,0 +1,588 @@
1
+ import type { CellStyle, BorderType } from './types';
2
+ import { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';
3
+
4
+ /**
5
+ * Normalize a color to ARGB format (8 hex chars).
6
+ * Accepts: "#RGB", "#RRGGBB", "RGB", "RRGGBB", "AARRGGBB", "#AARRGGBB"
7
+ */
8
+ const normalizeColor = (color: string): string => {
9
+ let c = color.replace(/^#/, '').toUpperCase();
10
+
11
+ // Handle shorthand 3-char format (e.g., "FFF" -> "FFFFFF")
12
+ if (c.length === 3) {
13
+ c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];
14
+ }
15
+
16
+ // Add alpha channel if not present (6 chars -> 8 chars)
17
+ if (c.length === 6) {
18
+ c = 'FF' + c;
19
+ }
20
+
21
+ return c;
22
+ };
23
+
24
+ /**
25
+ * Manages the styles (xl/styles.xml)
26
+ */
27
+ export class Styles {
28
+ private _numFmts: Map<number, string> = new Map();
29
+ private _fonts: StyleFont[] = [];
30
+ private _fills: StyleFill[] = [];
31
+ private _borders: StyleBorder[] = [];
32
+ private _cellXfs: CellXf[] = []; // Cell formats (combined style index)
33
+ private _xmlNodes: XmlNode[] | null = null;
34
+ private _dirty = false;
35
+
36
+ // Cache for style deduplication
37
+ private _styleCache: Map<string, number> = new Map();
38
+
39
+ /**
40
+ * Parse styles from XML content
41
+ */
42
+ static parse(xml: string): Styles {
43
+ const styles = new Styles();
44
+ styles._xmlNodes = parseXml(xml);
45
+
46
+ const styleSheet = findElement(styles._xmlNodes, 'styleSheet');
47
+ if (!styleSheet) return styles;
48
+
49
+ const children = getChildren(styleSheet, 'styleSheet');
50
+
51
+ // Parse number formats
52
+ const numFmts = findElement(children, 'numFmts');
53
+ if (numFmts) {
54
+ for (const child of getChildren(numFmts, 'numFmts')) {
55
+ if ('numFmt' in child) {
56
+ const id = parseInt(getAttr(child, 'numFmtId') || '0', 10);
57
+ const code = getAttr(child, 'formatCode') || '';
58
+ styles._numFmts.set(id, code);
59
+ }
60
+ }
61
+ }
62
+
63
+ // Parse fonts
64
+ const fonts = findElement(children, 'fonts');
65
+ if (fonts) {
66
+ for (const child of getChildren(fonts, 'fonts')) {
67
+ if ('font' in child) {
68
+ styles._fonts.push(styles._parseFont(child));
69
+ }
70
+ }
71
+ }
72
+
73
+ // Parse fills
74
+ const fills = findElement(children, 'fills');
75
+ if (fills) {
76
+ for (const child of getChildren(fills, 'fills')) {
77
+ if ('fill' in child) {
78
+ styles._fills.push(styles._parseFill(child));
79
+ }
80
+ }
81
+ }
82
+
83
+ // Parse borders
84
+ const borders = findElement(children, 'borders');
85
+ if (borders) {
86
+ for (const child of getChildren(borders, 'borders')) {
87
+ if ('border' in child) {
88
+ styles._borders.push(styles._parseBorder(child));
89
+ }
90
+ }
91
+ }
92
+
93
+ // Parse cellXfs (cell formats)
94
+ const cellXfs = findElement(children, 'cellXfs');
95
+ if (cellXfs) {
96
+ for (const child of getChildren(cellXfs, 'cellXfs')) {
97
+ if ('xf' in child) {
98
+ styles._cellXfs.push(styles._parseCellXf(child));
99
+ }
100
+ }
101
+ }
102
+
103
+ return styles;
104
+ }
105
+
106
+ /**
107
+ * Create an empty styles object with defaults
108
+ */
109
+ static createDefault(): Styles {
110
+ const styles = new Styles();
111
+
112
+ // Default font (Calibri 11)
113
+ styles._fonts.push({
114
+ bold: false,
115
+ italic: false,
116
+ underline: false,
117
+ strike: false,
118
+ size: 11,
119
+ name: 'Calibri',
120
+ color: undefined,
121
+ });
122
+
123
+ // Default fills (none and gray125 pattern are required)
124
+ styles._fills.push({ type: 'none' });
125
+ styles._fills.push({ type: 'gray125' });
126
+
127
+ // Default border (none)
128
+ styles._borders.push({});
129
+
130
+ // Default cell format
131
+ styles._cellXfs.push({
132
+ fontId: 0,
133
+ fillId: 0,
134
+ borderId: 0,
135
+ numFmtId: 0,
136
+ });
137
+
138
+ return styles;
139
+ }
140
+
141
+ private _parseFont(node: XmlNode): StyleFont {
142
+ const font: StyleFont = {
143
+ bold: false,
144
+ italic: false,
145
+ underline: false,
146
+ strike: false,
147
+ };
148
+
149
+ const children = getChildren(node, 'font');
150
+ for (const child of children) {
151
+ if ('b' in child) font.bold = true;
152
+ if ('i' in child) font.italic = true;
153
+ if ('u' in child) font.underline = true;
154
+ if ('strike' in child) font.strike = true;
155
+ if ('sz' in child) font.size = parseFloat(getAttr(child, 'val') || '11');
156
+ if ('name' in child) font.name = getAttr(child, 'val');
157
+ if ('color' in child) {
158
+ font.color = getAttr(child, 'rgb') || getAttr(child, 'theme');
159
+ }
160
+ }
161
+
162
+ return font;
163
+ }
164
+
165
+ private _parseFill(node: XmlNode): StyleFill {
166
+ const fill: StyleFill = { type: 'none' };
167
+ const children = getChildren(node, 'fill');
168
+
169
+ for (const child of children) {
170
+ if ('patternFill' in child) {
171
+ const pattern = getAttr(child, 'patternType');
172
+ fill.type = pattern || 'none';
173
+
174
+ const pfChildren = getChildren(child, 'patternFill');
175
+ for (const pfChild of pfChildren) {
176
+ if ('fgColor' in pfChild) {
177
+ fill.fgColor = getAttr(pfChild, 'rgb') || getAttr(pfChild, 'theme');
178
+ }
179
+ if ('bgColor' in pfChild) {
180
+ fill.bgColor = getAttr(pfChild, 'rgb') || getAttr(pfChild, 'theme');
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ return fill;
187
+ }
188
+
189
+ private _parseBorder(node: XmlNode): StyleBorder {
190
+ const border: StyleBorder = {};
191
+ const children = getChildren(node, 'border');
192
+
193
+ for (const child of children) {
194
+ const style = getAttr(child, 'style') as BorderType | undefined;
195
+ if ('left' in child && style) border.left = style;
196
+ if ('right' in child && style) border.right = style;
197
+ if ('top' in child && style) border.top = style;
198
+ if ('bottom' in child && style) border.bottom = style;
199
+ }
200
+
201
+ return border;
202
+ }
203
+
204
+ private _parseCellXf(node: XmlNode): CellXf {
205
+ return {
206
+ fontId: parseInt(getAttr(node, 'fontId') || '0', 10),
207
+ fillId: parseInt(getAttr(node, 'fillId') || '0', 10),
208
+ borderId: parseInt(getAttr(node, 'borderId') || '0', 10),
209
+ numFmtId: parseInt(getAttr(node, 'numFmtId') || '0', 10),
210
+ alignment: this._parseAlignment(node),
211
+ };
212
+ }
213
+
214
+ private _parseAlignment(node: XmlNode): AlignmentStyle | undefined {
215
+ const children = getChildren(node, 'xf');
216
+ const alignNode = findElement(children, 'alignment');
217
+ if (!alignNode) return undefined;
218
+
219
+ return {
220
+ horizontal: getAttr(alignNode, 'horizontal') as AlignmentStyle['horizontal'],
221
+ vertical: getAttr(alignNode, 'vertical') as AlignmentStyle['vertical'],
222
+ wrapText: getAttr(alignNode, 'wrapText') === '1',
223
+ textRotation: parseInt(getAttr(alignNode, 'textRotation') || '0', 10),
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Get a style by index
229
+ */
230
+ getStyle(index: number): CellStyle {
231
+ const xf = this._cellXfs[index];
232
+ if (!xf) return {};
233
+
234
+ const font = this._fonts[xf.fontId];
235
+ const fill = this._fills[xf.fillId];
236
+ const border = this._borders[xf.borderId];
237
+ const numFmt = this._numFmts.get(xf.numFmtId);
238
+
239
+ const style: CellStyle = {};
240
+
241
+ if (font) {
242
+ if (font.bold) style.bold = true;
243
+ if (font.italic) style.italic = true;
244
+ if (font.underline) style.underline = true;
245
+ if (font.strike) style.strike = true;
246
+ if (font.size) style.fontSize = font.size;
247
+ if (font.name) style.fontName = font.name;
248
+ if (font.color) style.fontColor = font.color;
249
+ }
250
+
251
+ if (fill && fill.fgColor) {
252
+ style.fill = fill.fgColor;
253
+ }
254
+
255
+ if (border) {
256
+ if (border.top || border.bottom || border.left || border.right) {
257
+ style.border = {
258
+ top: border.top,
259
+ bottom: border.bottom,
260
+ left: border.left,
261
+ right: border.right,
262
+ };
263
+ }
264
+ }
265
+
266
+ if (numFmt) {
267
+ style.numberFormat = numFmt;
268
+ }
269
+
270
+ if (xf.alignment) {
271
+ style.alignment = {
272
+ horizontal: xf.alignment.horizontal,
273
+ vertical: xf.alignment.vertical,
274
+ wrapText: xf.alignment.wrapText,
275
+ textRotation: xf.alignment.textRotation,
276
+ };
277
+ }
278
+
279
+ return style;
280
+ }
281
+
282
+ /**
283
+ * Create a style and return its index
284
+ * Uses caching to deduplicate identical styles
285
+ */
286
+ createStyle(style: CellStyle): number {
287
+ const key = JSON.stringify(style);
288
+ const cached = this._styleCache.get(key);
289
+ if (cached !== undefined) {
290
+ return cached;
291
+ }
292
+
293
+ this._dirty = true;
294
+
295
+ // Create or find font
296
+ const fontId = this._findOrCreateFont(style);
297
+
298
+ // Create or find fill
299
+ const fillId = this._findOrCreateFill(style);
300
+
301
+ // Create or find border
302
+ const borderId = this._findOrCreateBorder(style);
303
+
304
+ // Create or find number format
305
+ const numFmtId = style.numberFormat ? this._findOrCreateNumFmt(style.numberFormat) : 0;
306
+
307
+ // Create cell format
308
+ const xf: CellXf = {
309
+ fontId,
310
+ fillId,
311
+ borderId,
312
+ numFmtId,
313
+ };
314
+
315
+ if (style.alignment) {
316
+ xf.alignment = {
317
+ horizontal: style.alignment.horizontal,
318
+ vertical: style.alignment.vertical,
319
+ wrapText: style.alignment.wrapText,
320
+ textRotation: style.alignment.textRotation,
321
+ };
322
+ }
323
+
324
+ const index = this._cellXfs.length;
325
+ this._cellXfs.push(xf);
326
+ this._styleCache.set(key, index);
327
+
328
+ return index;
329
+ }
330
+
331
+ private _findOrCreateFont(style: CellStyle): number {
332
+ const font: StyleFont = {
333
+ bold: style.bold || false,
334
+ italic: style.italic || false,
335
+ underline: style.underline === true || style.underline === 'single' || style.underline === 'double',
336
+ strike: style.strike || false,
337
+ size: style.fontSize,
338
+ name: style.fontName,
339
+ color: style.fontColor,
340
+ };
341
+
342
+ // Try to find existing font
343
+ for (let i = 0; i < this._fonts.length; i++) {
344
+ const f = this._fonts[i];
345
+ if (
346
+ f.bold === font.bold &&
347
+ f.italic === font.italic &&
348
+ f.underline === font.underline &&
349
+ f.strike === font.strike &&
350
+ f.size === font.size &&
351
+ f.name === font.name &&
352
+ f.color === font.color
353
+ ) {
354
+ return i;
355
+ }
356
+ }
357
+
358
+ // Create new font
359
+ this._fonts.push(font);
360
+ return this._fonts.length - 1;
361
+ }
362
+
363
+ private _findOrCreateFill(style: CellStyle): number {
364
+ if (!style.fill) return 0;
365
+
366
+ // Try to find existing fill
367
+ for (let i = 0; i < this._fills.length; i++) {
368
+ const f = this._fills[i];
369
+ if (f.fgColor === style.fill) {
370
+ return i;
371
+ }
372
+ }
373
+
374
+ // Create new fill
375
+ this._fills.push({
376
+ type: 'solid',
377
+ fgColor: style.fill,
378
+ });
379
+ return this._fills.length - 1;
380
+ }
381
+
382
+ private _findOrCreateBorder(style: CellStyle): number {
383
+ if (!style.border) return 0;
384
+
385
+ const border: StyleBorder = {
386
+ top: style.border.top,
387
+ bottom: style.border.bottom,
388
+ left: style.border.left,
389
+ right: style.border.right,
390
+ };
391
+
392
+ // Try to find existing border
393
+ for (let i = 0; i < this._borders.length; i++) {
394
+ const b = this._borders[i];
395
+ if (b.top === border.top && b.bottom === border.bottom && b.left === border.left && b.right === border.right) {
396
+ return i;
397
+ }
398
+ }
399
+
400
+ // Create new border
401
+ this._borders.push(border);
402
+ return this._borders.length - 1;
403
+ }
404
+
405
+ private _findOrCreateNumFmt(format: string): number {
406
+ // Check if already exists
407
+ for (const [id, code] of this._numFmts) {
408
+ if (code === format) return id;
409
+ }
410
+
411
+ // Create new (custom formats start at 164)
412
+ const id = Math.max(164, ...Array.from(this._numFmts.keys())) + 1;
413
+ this._numFmts.set(id, format);
414
+ return id;
415
+ }
416
+
417
+ /**
418
+ * Check if styles have been modified
419
+ */
420
+ get dirty(): boolean {
421
+ return this._dirty;
422
+ }
423
+
424
+ /**
425
+ * Generate XML for styles
426
+ */
427
+ toXml(): string {
428
+ const children: XmlNode[] = [];
429
+
430
+ // Number formats
431
+ if (this._numFmts.size > 0) {
432
+ const numFmtNodes: XmlNode[] = [];
433
+ for (const [id, code] of this._numFmts) {
434
+ numFmtNodes.push(createElement('numFmt', { numFmtId: String(id), formatCode: code }, []));
435
+ }
436
+ children.push(createElement('numFmts', { count: String(numFmtNodes.length) }, numFmtNodes));
437
+ }
438
+
439
+ // Fonts
440
+ const fontNodes: XmlNode[] = this._fonts.map((font) => this._buildFontNode(font));
441
+ children.push(createElement('fonts', { count: String(fontNodes.length) }, fontNodes));
442
+
443
+ // Fills
444
+ const fillNodes: XmlNode[] = this._fills.map((fill) => this._buildFillNode(fill));
445
+ children.push(createElement('fills', { count: String(fillNodes.length) }, fillNodes));
446
+
447
+ // Borders
448
+ const borderNodes: XmlNode[] = this._borders.map((border) => this._buildBorderNode(border));
449
+ children.push(createElement('borders', { count: String(borderNodes.length) }, borderNodes));
450
+
451
+ // Cell style xfs (required but we just add a default)
452
+ children.push(
453
+ createElement('cellStyleXfs', { count: '1' }, [
454
+ createElement('xf', { numFmtId: '0', fontId: '0', fillId: '0', borderId: '0' }, []),
455
+ ]),
456
+ );
457
+
458
+ // Cell xfs
459
+ const xfNodes: XmlNode[] = this._cellXfs.map((xf) => this._buildXfNode(xf));
460
+ children.push(createElement('cellXfs', { count: String(xfNodes.length) }, xfNodes));
461
+
462
+ // Cell styles (required)
463
+ children.push(
464
+ createElement('cellStyles', { count: '1' }, [
465
+ createElement('cellStyle', { name: 'Normal', xfId: '0', builtinId: '0' }, []),
466
+ ]),
467
+ );
468
+
469
+ const styleSheet = createElement(
470
+ 'styleSheet',
471
+ { xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' },
472
+ children,
473
+ );
474
+
475
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([styleSheet])}`;
476
+ }
477
+
478
+ private _buildFontNode(font: StyleFont): XmlNode {
479
+ const children: XmlNode[] = [];
480
+ if (font.bold) children.push(createElement('b', {}, []));
481
+ if (font.italic) children.push(createElement('i', {}, []));
482
+ if (font.underline) children.push(createElement('u', {}, []));
483
+ if (font.strike) children.push(createElement('strike', {}, []));
484
+ if (font.size) children.push(createElement('sz', { val: String(font.size) }, []));
485
+ if (font.color) children.push(createElement('color', { rgb: normalizeColor(font.color) }, []));
486
+ if (font.name) children.push(createElement('name', { val: font.name }, []));
487
+ return createElement('font', {}, children);
488
+ }
489
+
490
+ private _buildFillNode(fill: StyleFill): XmlNode {
491
+ const patternChildren: XmlNode[] = [];
492
+ if (fill.fgColor) {
493
+ const rgb = normalizeColor(fill.fgColor);
494
+ patternChildren.push(createElement('fgColor', { rgb }, []));
495
+ // For solid fills, bgColor is required (indexed 64 = system background)
496
+ if (fill.type === 'solid') {
497
+ patternChildren.push(createElement('bgColor', { indexed: '64' }, []));
498
+ }
499
+ }
500
+ if (fill.bgColor && fill.type !== 'solid') {
501
+ const rgb = normalizeColor(fill.bgColor);
502
+ patternChildren.push(createElement('bgColor', { rgb }, []));
503
+ }
504
+ const patternFill = createElement('patternFill', { patternType: fill.type || 'none' }, patternChildren);
505
+ return createElement('fill', {}, [patternFill]);
506
+ }
507
+
508
+ private _buildBorderNode(border: StyleBorder): XmlNode {
509
+ const children: XmlNode[] = [];
510
+ if (border.left) children.push(createElement('left', { style: border.left }, []));
511
+ if (border.right) children.push(createElement('right', { style: border.right }, []));
512
+ if (border.top) children.push(createElement('top', { style: border.top }, []));
513
+ if (border.bottom) children.push(createElement('bottom', { style: border.bottom }, []));
514
+ // Add empty elements if not present (required by Excel)
515
+ if (!border.left) children.push(createElement('left', {}, []));
516
+ if (!border.right) children.push(createElement('right', {}, []));
517
+ if (!border.top) children.push(createElement('top', {}, []));
518
+ if (!border.bottom) children.push(createElement('bottom', {}, []));
519
+ children.push(createElement('diagonal', {}, []));
520
+ return createElement('border', {}, children);
521
+ }
522
+
523
+ private _buildXfNode(xf: CellXf): XmlNode {
524
+ const attrs: Record<string, string> = {
525
+ numFmtId: String(xf.numFmtId),
526
+ fontId: String(xf.fontId),
527
+ fillId: String(xf.fillId),
528
+ borderId: String(xf.borderId),
529
+ };
530
+
531
+ if (xf.fontId > 0) attrs.applyFont = '1';
532
+ if (xf.fillId > 0) attrs.applyFill = '1';
533
+ if (xf.borderId > 0) attrs.applyBorder = '1';
534
+ if (xf.numFmtId > 0) attrs.applyNumberFormat = '1';
535
+
536
+ const children: XmlNode[] = [];
537
+ if (xf.alignment) {
538
+ const alignAttrs: Record<string, string> = {};
539
+ if (xf.alignment.horizontal) alignAttrs.horizontal = xf.alignment.horizontal;
540
+ if (xf.alignment.vertical) alignAttrs.vertical = xf.alignment.vertical;
541
+ if (xf.alignment.wrapText) alignAttrs.wrapText = '1';
542
+ if (xf.alignment.textRotation) alignAttrs.textRotation = String(xf.alignment.textRotation);
543
+ children.push(createElement('alignment', alignAttrs, []));
544
+ attrs.applyAlignment = '1';
545
+ }
546
+
547
+ return createElement('xf', attrs, children);
548
+ }
549
+ }
550
+
551
+ // Internal types for style components
552
+ interface StyleFont {
553
+ bold: boolean;
554
+ italic: boolean;
555
+ underline: boolean;
556
+ strike: boolean;
557
+ size?: number;
558
+ name?: string;
559
+ color?: string;
560
+ }
561
+
562
+ interface StyleFill {
563
+ type: string;
564
+ fgColor?: string;
565
+ bgColor?: string;
566
+ }
567
+
568
+ interface StyleBorder {
569
+ top?: BorderType;
570
+ bottom?: BorderType;
571
+ left?: BorderType;
572
+ right?: BorderType;
573
+ }
574
+
575
+ interface CellXf {
576
+ fontId: number;
577
+ fillId: number;
578
+ borderId: number;
579
+ numFmtId: number;
580
+ alignment?: AlignmentStyle;
581
+ }
582
+
583
+ interface AlignmentStyle {
584
+ horizontal?: 'left' | 'center' | 'right' | 'justify';
585
+ vertical?: 'top' | 'middle' | 'bottom';
586
+ wrapText?: boolean;
587
+ textRotation?: number;
588
+ }