@niicojs/excel 0.3.1 → 0.3.3

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/src/table.ts CHANGED
@@ -1,386 +1,386 @@
1
- import type { TableConfig, TableStyleConfig, TableTotalFunction, RangeAddress } from './types';
2
- import type { Worksheet } from './worksheet';
3
- import { parseRange, toAddress, toRange } from './utils/address';
4
- import { createElement, stringifyXml, XmlNode } from './utils/xml';
5
-
6
- /**
7
- * Maps table total function names to SUBTOTAL function numbers
8
- * SUBTOTAL uses 101-111 for functions that ignore hidden values
9
- */
10
- const TOTAL_FUNCTION_NUMBERS: Record<TableTotalFunction, number> = {
11
- average: 101,
12
- count: 102,
13
- countNums: 103,
14
- max: 104,
15
- min: 105,
16
- stdDev: 107,
17
- sum: 109,
18
- var: 110,
19
- none: 0,
20
- };
21
-
22
- /**
23
- * Maps table total function names to XML attribute values
24
- */
25
- const TOTAL_FUNCTION_NAMES: Record<TableTotalFunction, string> = {
26
- average: 'average',
27
- count: 'count',
28
- countNums: 'countNums',
29
- max: 'max',
30
- min: 'min',
31
- stdDev: 'stdDev',
32
- sum: 'sum',
33
- var: 'var',
34
- none: 'none',
35
- };
36
-
37
- /**
38
- * Represents an Excel Table (ListObject) with auto-filter, banded styling, and total row.
39
- */
40
- export class Table {
41
- private _name: string;
42
- private _displayName: string;
43
- private _worksheet: Worksheet;
44
- private _range: RangeAddress;
45
- private _baseRange: RangeAddress;
46
- private _totalRow: boolean;
47
- private _autoFilter: boolean;
48
- private _style: TableStyleConfig;
49
- private _columns: TableColumn[] = [];
50
- private _id: number;
51
- private _dirty = true;
52
- private _headerRow: boolean;
53
-
54
- constructor(worksheet: Worksheet, config: TableConfig, tableId: number) {
55
- this._worksheet = worksheet;
56
- this._name = config.name;
57
- this._displayName = config.name;
58
- this._range = parseRange(config.range);
59
- this._baseRange = { start: { ...this._range.start }, end: { ...this._range.end } };
60
- this._totalRow = config.totalRow === true; // Default false
61
- this._autoFilter = true; // Tables have auto-filter by default
62
- this._headerRow = config.headerRow !== false;
63
- this._id = tableId;
64
-
65
- // Expand range to include total row if enabled
66
- if (this._totalRow) {
67
- this._range.end.row++;
68
- }
69
-
70
- // Set default style
71
- this._style = {
72
- name: config.style?.name ?? 'TableStyleMedium2',
73
- showRowStripes: config.style?.showRowStripes !== false, // Default true
74
- showColumnStripes: config.style?.showColumnStripes === true, // Default false
75
- showFirstColumn: config.style?.showFirstColumn === true, // Default false
76
- showLastColumn: config.style?.showLastColumn === true, // Default false
77
- };
78
-
79
- // Extract column names from worksheet headers
80
- this._extractColumns();
81
- }
82
-
83
- /**
84
- * Get the table name
85
- */
86
- get name(): string {
87
- return this._name;
88
- }
89
-
90
- /**
91
- * Get the table display name
92
- */
93
- get displayName(): string {
94
- return this._displayName;
95
- }
96
-
97
- /**
98
- * Get the table ID
99
- */
100
- get id(): number {
101
- return this._id;
102
- }
103
-
104
- /**
105
- * Get the worksheet this table belongs to
106
- */
107
- get worksheet(): Worksheet {
108
- return this._worksheet;
109
- }
110
-
111
- /**
112
- * Get the table range address string
113
- */
114
- get range(): string {
115
- return toRange(this._range);
116
- }
117
-
118
- /**
119
- * Get the base range excluding total row
120
- */
121
- get baseRange(): string {
122
- return toRange(this._baseRange);
123
- }
124
-
125
- /**
126
- * Get the table range as RangeAddress
127
- */
128
- get rangeAddress(): RangeAddress {
129
- return { ...this._range };
130
- }
131
-
132
- /**
133
- * Get column names
134
- */
135
- get columns(): string[] {
136
- return this._columns.map((c) => c.name);
137
- }
138
-
139
- /**
140
- * Check if table has a total row
141
- */
142
- get hasTotalRow(): boolean {
143
- return this._totalRow;
144
- }
145
-
146
- /**
147
- * Check if table has a header row
148
- */
149
- get hasHeaderRow(): boolean {
150
- return this._headerRow;
151
- }
152
-
153
- /**
154
- * Check if table has auto-filter enabled
155
- */
156
- get hasAutoFilter(): boolean {
157
- return this._autoFilter;
158
- }
159
-
160
- /**
161
- * Get the current style configuration
162
- */
163
- get style(): TableStyleConfig {
164
- return { ...this._style };
165
- }
166
-
167
- /**
168
- * Check if the table has been modified
169
- */
170
- get dirty(): boolean {
171
- return this._dirty;
172
- }
173
-
174
- /**
175
- * Set a total function for a column
176
- * @param columnName - Name of the column (header text)
177
- * @param fn - Aggregation function to use
178
- * @returns this for method chaining
179
- */
180
- setTotalFunction(columnName: string, fn: TableTotalFunction): this {
181
- if (!this._totalRow) {
182
- throw new Error('Cannot set total function: table does not have a total row enabled');
183
- }
184
-
185
- const column = this._columns.find((c) => c.name === columnName);
186
- if (!column) {
187
- throw new Error(`Column not found: ${columnName}`);
188
- }
189
-
190
- column.totalFunction = fn;
191
- this._dirty = true;
192
-
193
- // Write the formula to the total row cell
194
- this._writeTotalRowFormula(column);
195
-
196
- return this;
197
- }
198
-
199
- /**
200
- * Get total function for a column if set
201
- */
202
- getTotalFunction(columnName: string): TableTotalFunction | undefined {
203
- const column = this._columns.find((c) => c.name === columnName);
204
- return column?.totalFunction;
205
- }
206
-
207
- /**
208
- * Enable or disable auto-filter
209
- * @param enabled - Whether auto-filter should be enabled
210
- * @returns this for method chaining
211
- */
212
- setAutoFilter(enabled: boolean): this {
213
- this._autoFilter = enabled;
214
- this._dirty = true;
215
- return this;
216
- }
217
-
218
- /**
219
- * Update table style configuration
220
- * @param style - Style options to apply
221
- * @returns this for method chaining
222
- */
223
- setStyle(style: Partial<TableStyleConfig>): this {
224
- if (style.name !== undefined) this._style.name = style.name;
225
- if (style.showRowStripes !== undefined) this._style.showRowStripes = style.showRowStripes;
226
- if (style.showColumnStripes !== undefined) this._style.showColumnStripes = style.showColumnStripes;
227
- if (style.showFirstColumn !== undefined) this._style.showFirstColumn = style.showFirstColumn;
228
- if (style.showLastColumn !== undefined) this._style.showLastColumn = style.showLastColumn;
229
- this._dirty = true;
230
- return this;
231
- }
232
-
233
- /**
234
- * Enable or disable the total row
235
- * @param enabled - Whether total row should be shown
236
- * @returns this for method chaining
237
- */
238
- setTotalRow(enabled: boolean): this {
239
- if (this._totalRow === enabled) return this;
240
-
241
- this._totalRow = enabled;
242
- this._dirty = true;
243
-
244
- if (enabled) {
245
- this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };
246
- this._range.end.row++;
247
- } else {
248
- this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };
249
- for (const col of this._columns) {
250
- col.totalFunction = undefined;
251
- }
252
- }
253
-
254
- return this;
255
- }
256
-
257
- /**
258
- * Extract column names from the header row of the worksheet
259
- */
260
- private _extractColumns(): void {
261
- const headerRow = this._range.start.row;
262
- const startCol = this._range.start.col;
263
- const endCol = this._range.end.col;
264
-
265
- for (let col = startCol; col <= endCol; col++) {
266
- const cell = this._headerRow ? this._worksheet.getCellIfExists(headerRow, col) : undefined;
267
- const value = cell?.value;
268
- const name = value != null ? String(value) : `Column${col - startCol + 1}`;
269
-
270
- this._columns.push({
271
- id: col - startCol + 1,
272
- name,
273
- colIndex: col,
274
- });
275
- }
276
- }
277
-
278
- /**
279
- * Write the SUBTOTAL formula to a total row cell
280
- */
281
- private _writeTotalRowFormula(column: TableColumn): void {
282
- if (!this._totalRow || !column.totalFunction || column.totalFunction === 'none') {
283
- return;
284
- }
285
-
286
- const totalRowIndex = this._range.end.row;
287
- const cell = this._worksheet.cell(totalRowIndex, column.colIndex);
288
-
289
- // Generate SUBTOTAL formula with structured reference
290
- const funcNum = TOTAL_FUNCTION_NUMBERS[column.totalFunction];
291
- // Use structured reference: SUBTOTAL(109,[ColumnName])
292
- const formula = `SUBTOTAL(${funcNum},[${column.name}])`;
293
- cell.formula = formula;
294
- }
295
-
296
- /**
297
- * Get the auto-filter range (excludes total row if present)
298
- */
299
- private _getAutoFilterRange(): string {
300
- const start = toAddress(this._range.start.row, this._range.start.col);
301
-
302
- // Auto-filter excludes the total row
303
- let endRow = this._range.end.row;
304
- if (this._totalRow) {
305
- endRow--;
306
- }
307
-
308
- const end = toAddress(endRow, this._range.end.col);
309
- return `${start}:${end}`;
310
- }
311
-
312
- /**
313
- * Generate the table definition XML
314
- */
315
- toXml(): string {
316
- const children: XmlNode[] = [];
317
-
318
- // Auto-filter element
319
- if (this._autoFilter) {
320
- const autoFilterRef = this._getAutoFilterRange();
321
- children.push(createElement('autoFilter', { ref: autoFilterRef }, []));
322
- }
323
-
324
- // Table columns
325
- const columnNodes: XmlNode[] = this._columns.map((col) => {
326
- const attrs: Record<string, string> = {
327
- id: String(col.id),
328
- name: col.name,
329
- };
330
-
331
- // Add total function if specified
332
- if (this._totalRow && col.totalFunction && col.totalFunction !== 'none') {
333
- attrs.totalsRowFunction = TOTAL_FUNCTION_NAMES[col.totalFunction];
334
- }
335
-
336
- return createElement('tableColumn', attrs, []);
337
- });
338
-
339
- children.push(createElement('tableColumns', { count: String(columnNodes.length) }, columnNodes));
340
-
341
- // Table style info
342
- const styleAttrs: Record<string, string> = {
343
- name: this._style.name || 'TableStyleMedium2',
344
- showFirstColumn: this._style.showFirstColumn ? '1' : '0',
345
- showLastColumn: this._style.showLastColumn ? '1' : '0',
346
- showRowStripes: this._style.showRowStripes !== false ? '1' : '0',
347
- showColumnStripes: this._style.showColumnStripes ? '1' : '0',
348
- };
349
- children.push(createElement('tableStyleInfo', styleAttrs, []));
350
-
351
- // Build table attributes
352
- const tableRef = toRange(this._range);
353
- const tableAttrs: Record<string, string> = {
354
- xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
355
- id: String(this._id),
356
- name: this._name,
357
- displayName: this._displayName,
358
- ref: tableRef,
359
- };
360
-
361
- if (!this._headerRow) {
362
- tableAttrs.headerRowCount = '0';
363
- }
364
-
365
- if (this._totalRow) {
366
- tableAttrs.totalsRowCount = '1';
367
- } else {
368
- tableAttrs.totalsRowShown = '0';
369
- }
370
-
371
- // Build complete table node
372
- const tableNode = createElement('table', tableAttrs, children);
373
-
374
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([tableNode])}`;
375
- }
376
- }
377
-
378
- /**
379
- * Internal column representation
380
- */
381
- interface TableColumn {
382
- id: number;
383
- name: string;
384
- colIndex: number;
385
- totalFunction?: TableTotalFunction;
386
- }
1
+ import type { TableConfig, TableStyleConfig, TableTotalFunction, RangeAddress } from './types';
2
+ import type { Worksheet } from './worksheet';
3
+ import { parseRange, toAddress, toRange } from './utils/address';
4
+ import { createElement, stringifyXml, XmlNode } from './utils/xml';
5
+
6
+ /**
7
+ * Maps table total function names to SUBTOTAL function numbers
8
+ * SUBTOTAL uses 101-111 for functions that ignore hidden values
9
+ */
10
+ const TOTAL_FUNCTION_NUMBERS: Record<TableTotalFunction, number> = {
11
+ average: 101,
12
+ count: 102,
13
+ countNums: 103,
14
+ max: 104,
15
+ min: 105,
16
+ stdDev: 107,
17
+ sum: 109,
18
+ var: 110,
19
+ none: 0,
20
+ };
21
+
22
+ /**
23
+ * Maps table total function names to XML attribute values
24
+ */
25
+ const TOTAL_FUNCTION_NAMES: Record<TableTotalFunction, string> = {
26
+ average: 'average',
27
+ count: 'count',
28
+ countNums: 'countNums',
29
+ max: 'max',
30
+ min: 'min',
31
+ stdDev: 'stdDev',
32
+ sum: 'sum',
33
+ var: 'var',
34
+ none: 'none',
35
+ };
36
+
37
+ /**
38
+ * Represents an Excel Table (ListObject) with auto-filter, banded styling, and total row.
39
+ */
40
+ export class Table {
41
+ private _name: string;
42
+ private _displayName: string;
43
+ private _worksheet: Worksheet;
44
+ private _range: RangeAddress;
45
+ private _baseRange: RangeAddress;
46
+ private _totalRow: boolean;
47
+ private _autoFilter: boolean;
48
+ private _style: TableStyleConfig;
49
+ private _columns: TableColumn[] = [];
50
+ private _id: number;
51
+ private _dirty = true;
52
+ private _headerRow: boolean;
53
+
54
+ constructor(worksheet: Worksheet, config: TableConfig, tableId: number) {
55
+ this._worksheet = worksheet;
56
+ this._name = config.name;
57
+ this._displayName = config.name;
58
+ this._range = parseRange(config.range);
59
+ this._baseRange = { start: { ...this._range.start }, end: { ...this._range.end } };
60
+ this._totalRow = config.totalRow === true; // Default false
61
+ this._autoFilter = true; // Tables have auto-filter by default
62
+ this._headerRow = config.headerRow !== false;
63
+ this._id = tableId;
64
+
65
+ // Expand range to include total row if enabled
66
+ if (this._totalRow) {
67
+ this._range.end.row++;
68
+ }
69
+
70
+ // Set default style
71
+ this._style = {
72
+ name: config.style?.name ?? 'TableStyleMedium2',
73
+ showRowStripes: config.style?.showRowStripes !== false, // Default true
74
+ showColumnStripes: config.style?.showColumnStripes === true, // Default false
75
+ showFirstColumn: config.style?.showFirstColumn === true, // Default false
76
+ showLastColumn: config.style?.showLastColumn === true, // Default false
77
+ };
78
+
79
+ // Extract column names from worksheet headers
80
+ this._extractColumns();
81
+ }
82
+
83
+ /**
84
+ * Get the table name
85
+ */
86
+ get name(): string {
87
+ return this._name;
88
+ }
89
+
90
+ /**
91
+ * Get the table display name
92
+ */
93
+ get displayName(): string {
94
+ return this._displayName;
95
+ }
96
+
97
+ /**
98
+ * Get the table ID
99
+ */
100
+ get id(): number {
101
+ return this._id;
102
+ }
103
+
104
+ /**
105
+ * Get the worksheet this table belongs to
106
+ */
107
+ get worksheet(): Worksheet {
108
+ return this._worksheet;
109
+ }
110
+
111
+ /**
112
+ * Get the table range address string
113
+ */
114
+ get range(): string {
115
+ return toRange(this._range);
116
+ }
117
+
118
+ /**
119
+ * Get the base range excluding total row
120
+ */
121
+ get baseRange(): string {
122
+ return toRange(this._baseRange);
123
+ }
124
+
125
+ /**
126
+ * Get the table range as RangeAddress
127
+ */
128
+ get rangeAddress(): RangeAddress {
129
+ return { ...this._range };
130
+ }
131
+
132
+ /**
133
+ * Get column names
134
+ */
135
+ get columns(): string[] {
136
+ return this._columns.map((c) => c.name);
137
+ }
138
+
139
+ /**
140
+ * Check if table has a total row
141
+ */
142
+ get hasTotalRow(): boolean {
143
+ return this._totalRow;
144
+ }
145
+
146
+ /**
147
+ * Check if table has a header row
148
+ */
149
+ get hasHeaderRow(): boolean {
150
+ return this._headerRow;
151
+ }
152
+
153
+ /**
154
+ * Check if table has auto-filter enabled
155
+ */
156
+ get hasAutoFilter(): boolean {
157
+ return this._autoFilter;
158
+ }
159
+
160
+ /**
161
+ * Get the current style configuration
162
+ */
163
+ get style(): TableStyleConfig {
164
+ return { ...this._style };
165
+ }
166
+
167
+ /**
168
+ * Check if the table has been modified
169
+ */
170
+ get dirty(): boolean {
171
+ return this._dirty;
172
+ }
173
+
174
+ /**
175
+ * Set a total function for a column
176
+ * @param columnName - Name of the column (header text)
177
+ * @param fn - Aggregation function to use
178
+ * @returns this for method chaining
179
+ */
180
+ setTotalFunction(columnName: string, fn: TableTotalFunction): this {
181
+ if (!this._totalRow) {
182
+ throw new Error('Cannot set total function: table does not have a total row enabled');
183
+ }
184
+
185
+ const column = this._columns.find((c) => c.name === columnName);
186
+ if (!column) {
187
+ throw new Error(`Column not found: ${columnName}`);
188
+ }
189
+
190
+ column.totalFunction = fn;
191
+ this._dirty = true;
192
+
193
+ // Write the formula to the total row cell
194
+ this._writeTotalRowFormula(column);
195
+
196
+ return this;
197
+ }
198
+
199
+ /**
200
+ * Get total function for a column if set
201
+ */
202
+ getTotalFunction(columnName: string): TableTotalFunction | undefined {
203
+ const column = this._columns.find((c) => c.name === columnName);
204
+ return column?.totalFunction;
205
+ }
206
+
207
+ /**
208
+ * Enable or disable auto-filter
209
+ * @param enabled - Whether auto-filter should be enabled
210
+ * @returns this for method chaining
211
+ */
212
+ setAutoFilter(enabled: boolean): this {
213
+ this._autoFilter = enabled;
214
+ this._dirty = true;
215
+ return this;
216
+ }
217
+
218
+ /**
219
+ * Update table style configuration
220
+ * @param style - Style options to apply
221
+ * @returns this for method chaining
222
+ */
223
+ setStyle(style: Partial<TableStyleConfig>): this {
224
+ if (style.name !== undefined) this._style.name = style.name;
225
+ if (style.showRowStripes !== undefined) this._style.showRowStripes = style.showRowStripes;
226
+ if (style.showColumnStripes !== undefined) this._style.showColumnStripes = style.showColumnStripes;
227
+ if (style.showFirstColumn !== undefined) this._style.showFirstColumn = style.showFirstColumn;
228
+ if (style.showLastColumn !== undefined) this._style.showLastColumn = style.showLastColumn;
229
+ this._dirty = true;
230
+ return this;
231
+ }
232
+
233
+ /**
234
+ * Enable or disable the total row
235
+ * @param enabled - Whether total row should be shown
236
+ * @returns this for method chaining
237
+ */
238
+ setTotalRow(enabled: boolean): this {
239
+ if (this._totalRow === enabled) return this;
240
+
241
+ this._totalRow = enabled;
242
+ this._dirty = true;
243
+
244
+ if (enabled) {
245
+ this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };
246
+ this._range.end.row++;
247
+ } else {
248
+ this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };
249
+ for (const col of this._columns) {
250
+ col.totalFunction = undefined;
251
+ }
252
+ }
253
+
254
+ return this;
255
+ }
256
+
257
+ /**
258
+ * Extract column names from the header row of the worksheet
259
+ */
260
+ private _extractColumns(): void {
261
+ const headerRow = this._range.start.row;
262
+ const startCol = this._range.start.col;
263
+ const endCol = this._range.end.col;
264
+
265
+ for (let col = startCol; col <= endCol; col++) {
266
+ const cell = this._headerRow ? this._worksheet.getCellIfExists(headerRow, col) : undefined;
267
+ const value = cell?.value;
268
+ const name = value != null ? String(value) : `Column${col - startCol + 1}`;
269
+
270
+ this._columns.push({
271
+ id: col - startCol + 1,
272
+ name,
273
+ colIndex: col,
274
+ });
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Write the SUBTOTAL formula to a total row cell
280
+ */
281
+ private _writeTotalRowFormula(column: TableColumn): void {
282
+ if (!this._totalRow || !column.totalFunction || column.totalFunction === 'none') {
283
+ return;
284
+ }
285
+
286
+ const totalRowIndex = this._range.end.row;
287
+ const cell = this._worksheet.cell(totalRowIndex, column.colIndex);
288
+
289
+ // Generate SUBTOTAL formula with structured reference
290
+ const funcNum = TOTAL_FUNCTION_NUMBERS[column.totalFunction];
291
+ // Use structured reference: SUBTOTAL(109,[ColumnName])
292
+ const formula = `SUBTOTAL(${funcNum},[${column.name}])`;
293
+ cell.formula = formula;
294
+ }
295
+
296
+ /**
297
+ * Get the auto-filter range (excludes total row if present)
298
+ */
299
+ private _getAutoFilterRange(): string {
300
+ const start = toAddress(this._range.start.row, this._range.start.col);
301
+
302
+ // Auto-filter excludes the total row
303
+ let endRow = this._range.end.row;
304
+ if (this._totalRow) {
305
+ endRow--;
306
+ }
307
+
308
+ const end = toAddress(endRow, this._range.end.col);
309
+ return `${start}:${end}`;
310
+ }
311
+
312
+ /**
313
+ * Generate the table definition XML
314
+ */
315
+ toXml(): string {
316
+ const children: XmlNode[] = [];
317
+
318
+ // Auto-filter element
319
+ if (this._autoFilter) {
320
+ const autoFilterRef = this._getAutoFilterRange();
321
+ children.push(createElement('autoFilter', { ref: autoFilterRef }, []));
322
+ }
323
+
324
+ // Table columns
325
+ const columnNodes: XmlNode[] = this._columns.map((col) => {
326
+ const attrs: Record<string, string> = {
327
+ id: String(col.id),
328
+ name: col.name,
329
+ };
330
+
331
+ // Add total function if specified
332
+ if (this._totalRow && col.totalFunction && col.totalFunction !== 'none') {
333
+ attrs.totalsRowFunction = TOTAL_FUNCTION_NAMES[col.totalFunction];
334
+ }
335
+
336
+ return createElement('tableColumn', attrs, []);
337
+ });
338
+
339
+ children.push(createElement('tableColumns', { count: String(columnNodes.length) }, columnNodes));
340
+
341
+ // Table style info
342
+ const styleAttrs: Record<string, string> = {
343
+ name: this._style.name || 'TableStyleMedium2',
344
+ showFirstColumn: this._style.showFirstColumn ? '1' : '0',
345
+ showLastColumn: this._style.showLastColumn ? '1' : '0',
346
+ showRowStripes: this._style.showRowStripes !== false ? '1' : '0',
347
+ showColumnStripes: this._style.showColumnStripes ? '1' : '0',
348
+ };
349
+ children.push(createElement('tableStyleInfo', styleAttrs, []));
350
+
351
+ // Build table attributes
352
+ const tableRef = toRange(this._range);
353
+ const tableAttrs: Record<string, string> = {
354
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
355
+ id: String(this._id),
356
+ name: this._name,
357
+ displayName: this._displayName,
358
+ ref: tableRef,
359
+ };
360
+
361
+ if (!this._headerRow) {
362
+ tableAttrs.headerRowCount = '0';
363
+ }
364
+
365
+ if (this._totalRow) {
366
+ tableAttrs.totalsRowCount = '1';
367
+ } else {
368
+ tableAttrs.totalsRowShown = '0';
369
+ }
370
+
371
+ // Build complete table node
372
+ const tableNode = createElement('table', tableAttrs, children);
373
+
374
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([tableNode])}`;
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Internal column representation
380
+ */
381
+ interface TableColumn {
382
+ id: number;
383
+ name: string;
384
+ colIndex: number;
385
+ totalFunction?: TableTotalFunction;
386
+ }