docxmlater 10.3.6 → 10.4.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/README.md +158 -7
- package/dist/core/Document.d.ts +97 -3
- package/dist/core/Document.d.ts.map +1 -1
- package/dist/core/Document.js +727 -50
- package/dist/core/Document.js.map +1 -1
- package/dist/core/DocumentContent.d.ts.map +1 -1
- package/dist/core/DocumentContent.js +0 -8
- package/dist/core/DocumentContent.js.map +1 -1
- package/dist/core/DocumentGenerator.d.ts.map +1 -1
- package/dist/core/DocumentGenerator.js +9 -5
- package/dist/core/DocumentGenerator.js.map +1 -1
- package/dist/core/DocumentParser.d.ts.map +1 -1
- package/dist/core/DocumentParser.js +573 -101
- package/dist/core/DocumentParser.js.map +1 -1
- package/dist/core/RelationshipManager.d.ts.map +1 -1
- package/dist/core/RelationshipManager.js +4 -3
- package/dist/core/RelationshipManager.js.map +1 -1
- package/dist/elements/Bookmark.d.ts +7 -0
- package/dist/elements/Bookmark.d.ts.map +1 -1
- package/dist/elements/Bookmark.js +24 -4
- package/dist/elements/Bookmark.js.map +1 -1
- package/dist/elements/BookmarkManager.d.ts.map +1 -1
- package/dist/elements/BookmarkManager.js +4 -3
- package/dist/elements/BookmarkManager.js.map +1 -1
- package/dist/elements/CommonTypes.d.ts +2 -2
- package/dist/elements/CommonTypes.d.ts.map +1 -1
- package/dist/elements/CommonTypes.js +14 -1
- package/dist/elements/CommonTypes.js.map +1 -1
- package/dist/elements/Field.d.ts +1 -1
- package/dist/elements/Field.d.ts.map +1 -1
- package/dist/elements/Field.js +1 -1
- package/dist/elements/Field.js.map +1 -1
- package/dist/elements/Footer.d.ts +2 -0
- package/dist/elements/Footer.d.ts.map +1 -1
- package/dist/elements/Footer.js +6 -0
- package/dist/elements/Footer.js.map +1 -1
- package/dist/elements/Header.d.ts +2 -0
- package/dist/elements/Header.d.ts.map +1 -1
- package/dist/elements/Header.js +6 -0
- package/dist/elements/Header.js.map +1 -1
- package/dist/elements/Image.d.ts.map +1 -1
- package/dist/elements/Image.js +3 -0
- package/dist/elements/Image.js.map +1 -1
- package/dist/elements/Paragraph.d.ts +81 -1
- package/dist/elements/Paragraph.d.ts.map +1 -1
- package/dist/elements/Paragraph.js +515 -21
- package/dist/elements/Paragraph.js.map +1 -1
- package/dist/elements/Revision.d.ts +0 -1
- package/dist/elements/Revision.d.ts.map +1 -1
- package/dist/elements/Revision.js +0 -12
- package/dist/elements/Revision.js.map +1 -1
- package/dist/elements/RevisionManager.d.ts +0 -1
- package/dist/elements/RevisionManager.d.ts.map +1 -1
- package/dist/elements/RevisionManager.js +0 -2
- package/dist/elements/RevisionManager.js.map +1 -1
- package/dist/elements/Run.d.ts +16 -4
- package/dist/elements/Run.d.ts.map +1 -1
- package/dist/elements/Run.js +114 -22
- package/dist/elements/Run.js.map +1 -1
- package/dist/elements/Section.d.ts +7 -1
- package/dist/elements/Section.d.ts.map +1 -1
- package/dist/elements/Section.js +185 -4
- package/dist/elements/Section.js.map +1 -1
- package/dist/elements/Shape.js.map +1 -1
- package/dist/elements/Table.d.ts +30 -1
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +357 -40
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableCell.d.ts +3 -0
- package/dist/elements/TableCell.d.ts.map +1 -1
- package/dist/elements/TableCell.js +30 -3
- package/dist/elements/TableCell.js.map +1 -1
- package/dist/elements/TableGridChange.d.ts +0 -1
- package/dist/elements/TableGridChange.d.ts.map +1 -1
- package/dist/elements/TableGridChange.js +0 -10
- package/dist/elements/TableGridChange.js.map +1 -1
- package/dist/elements/TableRow.d.ts +4 -0
- package/dist/elements/TableRow.d.ts.map +1 -1
- package/dist/elements/TableRow.js +31 -3
- package/dist/elements/TableRow.js.map +1 -1
- package/dist/formatting/AbstractNumbering.d.ts +5 -0
- package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
- package/dist/formatting/AbstractNumbering.js +22 -0
- package/dist/formatting/AbstractNumbering.js.map +1 -1
- package/dist/formatting/NumberingLevel.d.ts.map +1 -1
- package/dist/formatting/NumberingLevel.js +3 -3
- package/dist/formatting/NumberingLevel.js.map +1 -1
- package/dist/formatting/Style.d.ts +1 -0
- package/dist/formatting/Style.d.ts.map +1 -1
- package/dist/formatting/Style.js +25 -59
- package/dist/formatting/Style.js.map +1 -1
- package/dist/formatting/StylesManager.d.ts +1 -0
- package/dist/formatting/StylesManager.d.ts.map +1 -1
- package/dist/formatting/StylesManager.js +12 -0
- package/dist/formatting/StylesManager.js.map +1 -1
- package/dist/helpers/CleanupHelper.js.map +1 -1
- package/dist/images/ImageOptimizer.d.ts.map +1 -1
- package/dist/images/ImageOptimizer.js +0 -1
- package/dist/images/ImageOptimizer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/DrawingManager.d.ts.map +1 -1
- package/dist/managers/DrawingManager.js +4 -2
- package/dist/managers/DrawingManager.js.map +1 -1
- package/dist/types/formatting.d.ts +2 -2
- package/dist/types/formatting.d.ts.map +1 -1
- package/dist/types/formatting.js.map +1 -1
- package/dist/utils/ChangelogGenerator.d.ts +2 -2
- package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
- package/dist/utils/ChangelogGenerator.js +4 -5
- package/dist/utils/ChangelogGenerator.js.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.js +0 -1
- package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
- package/dist/utils/RevisionAwareProcessor.d.ts +2 -2
- package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
- package/dist/utils/RevisionAwareProcessor.js +2 -2
- package/dist/utils/RevisionAwareProcessor.js.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.d.ts +0 -2
- package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.js +0 -26
- package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/utils/ShadingResolver.d.ts.map +1 -1
- package/dist/utils/ShadingResolver.js.map +1 -1
- package/dist/utils/acceptRevisions.js +1 -1
- package/dist/utils/acceptRevisions.js.map +1 -1
- package/dist/utils/stripTrackedChanges.js +1 -1
- package/dist/utils/stripTrackedChanges.js.map +1 -1
- package/dist/utils/units.d.ts.map +1 -1
- package/dist/utils/units.js +1 -1
- package/dist/utils/units.js.map +1 -1
- package/dist/validation/RevisionAutoFixer.d.ts +2 -1
- package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
- package/dist/validation/RevisionAutoFixer.js.map +1 -1
- package/package.json +10 -1
- package/src/constants/CLAUDE.md +28 -0
- package/src/core/CLAUDE.md +4 -0
- package/src/core/Document.ts +1755 -85
- package/src/core/DocumentContent.ts +0 -11
- package/src/core/DocumentGenerator.ts +11 -12
- package/src/core/DocumentParser.ts +599 -138
- package/src/core/RelationshipManager.ts +6 -3
- package/src/elements/Bookmark.ts +39 -4
- package/src/elements/BookmarkManager.ts +4 -3
- package/src/elements/CLAUDE.md +18 -2
- package/src/elements/CommonTypes.ts +35 -8
- package/src/elements/Field.ts +1 -1
- package/src/elements/Footer.ts +23 -0
- package/src/elements/Header.ts +25 -0
- package/src/elements/Image.ts +5 -0
- package/src/elements/Paragraph.ts +1069 -41
- package/src/elements/Revision.ts +0 -19
- package/src/elements/RevisionManager.ts +1 -3
- package/src/elements/Run.ts +265 -35
- package/src/elements/Section.ts +214 -8
- package/src/elements/Shape.ts +1 -1
- package/src/elements/Table.ts +850 -61
- package/src/elements/TableCell.ts +84 -10
- package/src/elements/TableGridChange.ts +2 -16
- package/src/elements/TableRow.ts +94 -9
- package/src/formatting/AbstractNumbering.ts +42 -1
- package/src/formatting/CLAUDE.md +4 -0
- package/src/formatting/NumberingLevel.ts +11 -7
- package/src/formatting/Style.ts +39 -71
- package/src/formatting/StylesManager.ts +36 -0
- package/src/helpers/CleanupHelper.ts +1 -1
- package/src/images/ImageOptimizer.ts +0 -3
- package/src/index.ts +1 -1
- package/src/managers/DrawingManager.ts +5 -3
- package/src/tracking/CLAUDE.md +30 -0
- package/src/types/CLAUDE.md +39 -0
- package/src/types/formatting.ts +2 -2
- package/src/utils/CLAUDE.md +15 -0
- package/src/utils/ChangelogGenerator.ts +4 -5
- package/src/utils/InMemoryRevisionAcceptor.ts +0 -9
- package/src/utils/RevisionAwareProcessor.ts +2 -3
- package/src/utils/SelectiveRevisionAcceptor.ts +0 -39
- package/src/utils/ShadingResolver.ts +0 -1
- package/src/utils/acceptRevisions.ts +1 -1
- package/src/utils/stripTrackedChanges.ts +1 -1
- package/src/utils/units.ts +2 -1
- package/src/validation/CLAUDE.md +40 -0
- package/src/validation/RevisionAutoFixer.ts +2 -1
package/src/elements/Table.ts
CHANGED
|
@@ -5,23 +5,17 @@
|
|
|
5
5
|
import { Paragraph } from './Paragraph';
|
|
6
6
|
import { TableRow, RowFormatting } from './TableRow';
|
|
7
7
|
import { TableCell, CellFormatting } from './TableCell';
|
|
8
|
-
import { Run } from './Run';
|
|
9
8
|
import { Revision } from './Revision';
|
|
10
9
|
import { XMLBuilder, XMLElement } from '../xml/XMLBuilder';
|
|
11
10
|
import { deepClone } from '../utils/deepClone';
|
|
12
11
|
import { TableGridChange } from './TableGridChange';
|
|
13
|
-
import { formatDateForXml } from '../utils/dateFormatting';
|
|
14
12
|
import {
|
|
15
13
|
TableAlignment as CommonTableAlignment,
|
|
16
14
|
BorderStyle,
|
|
17
|
-
BorderDefinition,
|
|
18
|
-
TableBorderDefinitions,
|
|
19
15
|
HorizontalAnchor,
|
|
20
16
|
VerticalAnchor,
|
|
21
17
|
HorizontalAlignment,
|
|
22
18
|
VerticalAlignment,
|
|
23
|
-
WidthType,
|
|
24
|
-
ShadingPattern,
|
|
25
19
|
ShadingConfig,
|
|
26
20
|
buildShadingAttributes,
|
|
27
21
|
} from './CommonTypes';
|
|
@@ -119,7 +113,7 @@ export interface TablePositionProperties {
|
|
|
119
113
|
/**
|
|
120
114
|
* Table width type
|
|
121
115
|
*/
|
|
122
|
-
export type TableWidthType = 'auto' | 'dxa' | 'pct';
|
|
116
|
+
export type TableWidthType = 'auto' | 'dxa' | 'nil' | 'pct';
|
|
123
117
|
|
|
124
118
|
/**
|
|
125
119
|
* Table shading/background
|
|
@@ -176,6 +170,7 @@ export interface TableFormatting {
|
|
|
176
170
|
cellSpacingType?: TableWidthType; // Cell spacing type
|
|
177
171
|
cellMargins?: TableCellMargins; // Default cell margins (padding) for all cells
|
|
178
172
|
indent?: number; // Left indent in twips
|
|
173
|
+
indentType?: TableWidthType; // Indent type (auto, dxa, nil, pct)
|
|
179
174
|
tblLook?: string; // Table look flags (appearance settings)
|
|
180
175
|
shading?: TableShading; // Table background shading
|
|
181
176
|
// Batch 1 properties
|
|
@@ -325,6 +320,103 @@ export class Table {
|
|
|
325
320
|
return this;
|
|
326
321
|
}
|
|
327
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Adds a row from an array of cell text values
|
|
325
|
+
*
|
|
326
|
+
* Creates a TableRow with cells populated from the string array and appends
|
|
327
|
+
* it to the table. This is the single-row counterpart to `Table.fromArray()`.
|
|
328
|
+
*
|
|
329
|
+
* @param cells - Array of text values, one per cell
|
|
330
|
+
* @returns The created TableRow for further customization
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* const table = Table.fromArray([['Name', 'Age', 'City']]);
|
|
335
|
+
* table.addRowFromArray(['Alice', '30', 'NYC']);
|
|
336
|
+
* table.addRowFromArray(['Bob', '25', 'London']);
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
addRowFromArray(cells: string[]): TableRow {
|
|
340
|
+
const row = new TableRow();
|
|
341
|
+
for (const text of cells) {
|
|
342
|
+
row.createCell(text || undefined);
|
|
343
|
+
}
|
|
344
|
+
this.rows.push(row);
|
|
345
|
+
row._setParentTable(this);
|
|
346
|
+
return row;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Adds a summary/totals row computed from column data
|
|
351
|
+
*
|
|
352
|
+
* Appends a new row where each cell's value is computed by applying an
|
|
353
|
+
* aggregation function to that column's data rows (header row excluded).
|
|
354
|
+
* By default, numeric columns are summed and non-numeric columns show a
|
|
355
|
+
* count. The first column shows a label.
|
|
356
|
+
*
|
|
357
|
+
* @param options - Summary configuration
|
|
358
|
+
* @param options.label - Label for the first cell (default: 'Total')
|
|
359
|
+
* @param options.startRow - First data row index to include (default: 1, skipping header)
|
|
360
|
+
* @param options.compute - Custom function to compute a cell value from its column's text values.
|
|
361
|
+
* Receives `(values: string[], colIndex: number)`. If not provided, defaults to
|
|
362
|
+
* numeric sum for parseable columns, empty string otherwise.
|
|
363
|
+
* @returns The created summary TableRow
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* const table = Table.fromArray([
|
|
368
|
+
* ['Product', 'Price', 'Qty'],
|
|
369
|
+
* ['Widget', '10', '5'],
|
|
370
|
+
* ['Gadget', '25', '2'],
|
|
371
|
+
* ]);
|
|
372
|
+
* table.addSummaryRow();
|
|
373
|
+
* // Adds: ['Total', '35', '7']
|
|
374
|
+
* ```
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```typescript
|
|
378
|
+
* // Custom compute: average instead of sum
|
|
379
|
+
* table.addSummaryRow({
|
|
380
|
+
* label: 'Average',
|
|
381
|
+
* compute: (values) => {
|
|
382
|
+
* const nums = values.map(Number).filter(n => !isNaN(n));
|
|
383
|
+
* return nums.length ? (nums.reduce((a, b) => a + b, 0) / nums.length).toFixed(1) : '';
|
|
384
|
+
* },
|
|
385
|
+
* });
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
addSummaryRow(options?: {
|
|
389
|
+
label?: string;
|
|
390
|
+
startRow?: number;
|
|
391
|
+
compute?: (values: string[], colIndex: number) => string;
|
|
392
|
+
}): TableRow {
|
|
393
|
+
const label = options?.label ?? 'Total';
|
|
394
|
+
const startRow = options?.startRow ?? 1;
|
|
395
|
+
const colCount = this.getColumnCount();
|
|
396
|
+
|
|
397
|
+
const defaultCompute = (values: string[]): string => {
|
|
398
|
+
const nums = values.map((v) => parseFloat(v)).filter((n) => !isNaN(n));
|
|
399
|
+
if (nums.length === 0) return '';
|
|
400
|
+
return String(nums.reduce((a, b) => a + b, 0));
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const compute = options?.compute ?? defaultCompute;
|
|
404
|
+
const cells: string[] = [];
|
|
405
|
+
|
|
406
|
+
for (let c = 0; c < colCount; c++) {
|
|
407
|
+
if (c === 0) {
|
|
408
|
+
cells.push(label);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const colTexts = this.getColumnTexts(c);
|
|
413
|
+
const dataValues = colTexts.slice(startRow);
|
|
414
|
+
cells.push(compute(dataValues, c));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return this.addRowFromArray(cells);
|
|
418
|
+
}
|
|
419
|
+
|
|
328
420
|
/**
|
|
329
421
|
* Creates a new row and adds it to the table
|
|
330
422
|
*
|
|
@@ -459,6 +551,46 @@ export class Table {
|
|
|
459
551
|
return row ? row.getCell(columnIndex) : undefined;
|
|
460
552
|
}
|
|
461
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Sets the text content of a cell, replacing any existing content
|
|
556
|
+
*
|
|
557
|
+
* Convenience method that locates the cell at (row, col) and sets its
|
|
558
|
+
* text. If the cell has existing paragraphs, the first paragraph's text
|
|
559
|
+
* is replaced and extra paragraphs are removed. If the cell is empty,
|
|
560
|
+
* a new paragraph is created.
|
|
561
|
+
*
|
|
562
|
+
* @param rowIndex - Row index (0-based)
|
|
563
|
+
* @param colIndex - Column index (0-based)
|
|
564
|
+
* @param text - New text content for the cell
|
|
565
|
+
* @returns This table for chaining, or this table (no-op if cell not found)
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```typescript
|
|
569
|
+
* const table = new Table(3, 3);
|
|
570
|
+
* table.setCell(0, 0, 'Name');
|
|
571
|
+
* table.setCell(0, 1, 'Age');
|
|
572
|
+
* table.setCell(1, 0, 'Alice');
|
|
573
|
+
* table.setCell(1, 1, '30');
|
|
574
|
+
* ```
|
|
575
|
+
*/
|
|
576
|
+
setCell(rowIndex: number, colIndex: number, text: string): this {
|
|
577
|
+
const cell = this.getCell(rowIndex, colIndex);
|
|
578
|
+
if (!cell) return this;
|
|
579
|
+
|
|
580
|
+
const paragraphs = cell.getParagraphs();
|
|
581
|
+
if (paragraphs.length > 0) {
|
|
582
|
+
paragraphs[0]!.setText(text);
|
|
583
|
+
// Remove extra paragraphs
|
|
584
|
+
for (let p = paragraphs.length - 1; p >= 1; p--) {
|
|
585
|
+
cell.removeParagraph(p);
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
cell.createParagraph(text);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return this;
|
|
592
|
+
}
|
|
593
|
+
|
|
462
594
|
/**
|
|
463
595
|
* Sets the table width
|
|
464
596
|
*
|
|
@@ -774,6 +906,25 @@ export class Table {
|
|
|
774
906
|
return this;
|
|
775
907
|
}
|
|
776
908
|
|
|
909
|
+
/**
|
|
910
|
+
* Sets the indent width type per ECMA-376 ST_TblWidth
|
|
911
|
+
*/
|
|
912
|
+
setIndentType(type: TableWidthType): this {
|
|
913
|
+
const prev = this.formatting.indentType;
|
|
914
|
+
this.formatting.indentType = type;
|
|
915
|
+
if (this.trackingContext?.isEnabled() && prev !== type) {
|
|
916
|
+
this.trackingContext.trackTableChange(this, 'indentType', prev, type);
|
|
917
|
+
}
|
|
918
|
+
return this;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Gets the indent width type
|
|
923
|
+
*/
|
|
924
|
+
getIndentType(): TableWidthType | undefined {
|
|
925
|
+
return this.formatting.indentType;
|
|
926
|
+
}
|
|
927
|
+
|
|
777
928
|
/**
|
|
778
929
|
* Sets table style reference
|
|
779
930
|
* @param style - Table style ID (e.g., 'Table1', 'TableGrid')
|
|
@@ -1508,8 +1659,8 @@ export class Table {
|
|
|
1508
1659
|
|
|
1509
1660
|
// 5-6. tblStyleRowBandSize / tblStyleColBandSize
|
|
1510
1661
|
// Only valid within table style definitions (CT_TblPrBase in w:style),
|
|
1511
|
-
// not in direct tblPr. Style.ts handles serialization
|
|
1512
|
-
// Values are preserved in formatting for
|
|
1662
|
+
// not in direct tblPr per Transitional schema. Style.ts handles serialization.
|
|
1663
|
+
// Values are preserved in formatting for style use and tblPrChange serialization.
|
|
1513
1664
|
|
|
1514
1665
|
// 7. tblW
|
|
1515
1666
|
if (this.formatting.width !== undefined) {
|
|
@@ -1543,7 +1694,7 @@ export class Table {
|
|
|
1543
1694
|
tblPrChildren.push(
|
|
1544
1695
|
XMLBuilder.wSelf('tblInd', {
|
|
1545
1696
|
'w:w': this.formatting.indent,
|
|
1546
|
-
'w:type': 'dxa',
|
|
1697
|
+
'w:type': this.formatting.indentType || 'dxa',
|
|
1547
1698
|
})
|
|
1548
1699
|
);
|
|
1549
1700
|
}
|
|
@@ -1659,12 +1810,54 @@ export class Table {
|
|
|
1659
1810
|
'w:author': this.tblPrChange.author,
|
|
1660
1811
|
'w:date': this.tblPrChange.date,
|
|
1661
1812
|
};
|
|
1813
|
+
// tblPrChange previousProperties — same CT_TblPr order as main tblPr per ECMA-376 §17.4.58:
|
|
1814
|
+
// tblStyle, tblpPr, tblOverlap, bidiVisual, tblStyleRowBandSize, tblStyleColBandSize,
|
|
1815
|
+
// tblW, jc, tblCellSpacing, tblInd, tblBorders, shd, tblLayout, tblCellMar, tblLook,
|
|
1816
|
+
// tblCaption, tblDescription
|
|
1662
1817
|
const prevTblPrChildren: XMLElement[] = [];
|
|
1663
1818
|
const prev = this.tblPrChange.previousProperties;
|
|
1664
1819
|
if (prev) {
|
|
1820
|
+
// 1. tblStyle
|
|
1665
1821
|
if (prev.style) {
|
|
1666
1822
|
prevTblPrChildren.push(XMLBuilder.wSelf('tblStyle', { 'w:val': prev.style }));
|
|
1667
1823
|
}
|
|
1824
|
+
// 2. tblpPr (floating table position) — passthrough if present
|
|
1825
|
+
if (prev.position) {
|
|
1826
|
+
const posAttrs: Record<string, string | number> = {};
|
|
1827
|
+
const pos = prev.position;
|
|
1828
|
+
if (pos.x !== undefined) posAttrs['w:tblpX'] = pos.x;
|
|
1829
|
+
if (pos.y !== undefined) posAttrs['w:tblpY'] = pos.y;
|
|
1830
|
+
if (pos.horizontalAnchor) posAttrs['w:horzAnchor'] = pos.horizontalAnchor;
|
|
1831
|
+
if (pos.verticalAnchor) posAttrs['w:vertAnchor'] = pos.verticalAnchor;
|
|
1832
|
+
if (pos.leftFromText !== undefined) posAttrs['w:leftFromText'] = pos.leftFromText;
|
|
1833
|
+
if (pos.rightFromText !== undefined) posAttrs['w:rightFromText'] = pos.rightFromText;
|
|
1834
|
+
if (pos.topFromText !== undefined) posAttrs['w:topFromText'] = pos.topFromText;
|
|
1835
|
+
if (pos.bottomFromText !== undefined) posAttrs['w:bottomFromText'] = pos.bottomFromText;
|
|
1836
|
+
if (Object.keys(posAttrs).length > 0) {
|
|
1837
|
+
prevTblPrChildren.push(XMLBuilder.wSelf('tblpPr', posAttrs));
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
// 3. tblOverlap
|
|
1841
|
+
if (prev.overlap) {
|
|
1842
|
+
prevTblPrChildren.push(XMLBuilder.wSelf('tblOverlap', { 'w:val': prev.overlap }));
|
|
1843
|
+
}
|
|
1844
|
+
// 4. bidiVisual
|
|
1845
|
+
if (prev.bidiVisual) {
|
|
1846
|
+
prevTblPrChildren.push(XMLBuilder.wSelf('bidiVisual'));
|
|
1847
|
+
}
|
|
1848
|
+
// 5. tblStyleRowBandSize
|
|
1849
|
+
if (prev.tblStyleRowBandSize !== undefined) {
|
|
1850
|
+
prevTblPrChildren.push(
|
|
1851
|
+
XMLBuilder.wSelf('tblStyleRowBandSize', { 'w:val': prev.tblStyleRowBandSize })
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
// 6. tblStyleColBandSize
|
|
1855
|
+
if (prev.tblStyleColBandSize !== undefined) {
|
|
1856
|
+
prevTblPrChildren.push(
|
|
1857
|
+
XMLBuilder.wSelf('tblStyleColBandSize', { 'w:val': prev.tblStyleColBandSize })
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
// 7. tblW
|
|
1668
1861
|
if (prev.width !== undefined) {
|
|
1669
1862
|
prevTblPrChildren.push(
|
|
1670
1863
|
XMLBuilder.wSelf('tblW', {
|
|
@@ -1673,14 +1866,25 @@ export class Table {
|
|
|
1673
1866
|
})
|
|
1674
1867
|
);
|
|
1675
1868
|
}
|
|
1869
|
+
// 8. jc
|
|
1676
1870
|
if (prev.alignment) {
|
|
1677
1871
|
prevTblPrChildren.push(XMLBuilder.wSelf('jc', { 'w:val': prev.alignment }));
|
|
1678
1872
|
}
|
|
1873
|
+
// 9. tblCellSpacing
|
|
1874
|
+
if (prev.cellSpacing !== undefined) {
|
|
1875
|
+
const csAttrs: Record<string, string | number> = {
|
|
1876
|
+
'w:w': prev.cellSpacing,
|
|
1877
|
+
'w:type': prev.cellSpacingType || 'dxa',
|
|
1878
|
+
};
|
|
1879
|
+
prevTblPrChildren.push(XMLBuilder.wSelf('tblCellSpacing', csAttrs));
|
|
1880
|
+
}
|
|
1881
|
+
// 10. tblInd
|
|
1679
1882
|
if (prev.indent !== undefined) {
|
|
1680
1883
|
prevTblPrChildren.push(
|
|
1681
|
-
XMLBuilder.wSelf('tblInd', { 'w:w': prev.indent, 'w:type': 'dxa' })
|
|
1884
|
+
XMLBuilder.wSelf('tblInd', { 'w:w': prev.indent, 'w:type': prev.indentType || 'dxa' })
|
|
1682
1885
|
);
|
|
1683
1886
|
}
|
|
1887
|
+
// 11. tblBorders
|
|
1684
1888
|
if (prev.borders) {
|
|
1685
1889
|
const borderChildren: XMLElement[] = [];
|
|
1686
1890
|
const bNames = ['top', 'left', 'bottom', 'right', 'insideH', 'insideV'] as const;
|
|
@@ -1698,56 +1902,53 @@ export class Table {
|
|
|
1698
1902
|
prevTblPrChildren.push(XMLBuilder.w('tblBorders', undefined, borderChildren));
|
|
1699
1903
|
}
|
|
1700
1904
|
}
|
|
1905
|
+
// 12. shd
|
|
1701
1906
|
if (prev.shading) {
|
|
1702
1907
|
const shadingAttrs = buildShadingAttributes(prev.shading);
|
|
1703
1908
|
if (Object.keys(shadingAttrs).length > 0) {
|
|
1704
1909
|
prevTblPrChildren.push(XMLBuilder.wSelf('shd', shadingAttrs));
|
|
1705
1910
|
}
|
|
1706
1911
|
}
|
|
1912
|
+
// 13. tblLayout
|
|
1707
1913
|
if (prev.layout) {
|
|
1708
1914
|
prevTblPrChildren.push(XMLBuilder.wSelf('tblLayout', { 'w:type': prev.layout }));
|
|
1709
1915
|
}
|
|
1710
|
-
|
|
1711
|
-
const csAttrs: Record<string, string | number> = {
|
|
1712
|
-
'w:w': prev.cellSpacing,
|
|
1713
|
-
'w:type': prev.cellSpacingType || 'dxa',
|
|
1714
|
-
};
|
|
1715
|
-
prevTblPrChildren.push(XMLBuilder.wSelf('tblCellSpacing', csAttrs));
|
|
1716
|
-
}
|
|
1717
|
-
if (prev.bidiVisual) {
|
|
1718
|
-
prevTblPrChildren.push(XMLBuilder.wSelf('bidiVisual'));
|
|
1719
|
-
}
|
|
1916
|
+
// 14. tblCellMar
|
|
1720
1917
|
if (prev.cellMargins) {
|
|
1721
1918
|
const cmChildren: XMLElement[] = [];
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1919
|
+
if (prev.cellMargins.top !== undefined) {
|
|
1920
|
+
cmChildren.push(
|
|
1921
|
+
XMLBuilder.wSelf('top', { 'w:w': prev.cellMargins.top, 'w:type': 'dxa' })
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
if (prev.cellMargins.left !== undefined) {
|
|
1925
|
+
cmChildren.push(
|
|
1926
|
+
XMLBuilder.wSelf('left', { 'w:w': prev.cellMargins.left, 'w:type': 'dxa' })
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
if (prev.cellMargins.bottom !== undefined) {
|
|
1930
|
+
cmChildren.push(
|
|
1931
|
+
XMLBuilder.wSelf('bottom', { 'w:w': prev.cellMargins.bottom, 'w:type': 'dxa' })
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
if (prev.cellMargins.right !== undefined) {
|
|
1935
|
+
cmChildren.push(
|
|
1936
|
+
XMLBuilder.wSelf('right', { 'w:w': prev.cellMargins.right, 'w:type': 'dxa' })
|
|
1937
|
+
);
|
|
1727
1938
|
}
|
|
1728
1939
|
if (cmChildren.length > 0) {
|
|
1729
1940
|
prevTblPrChildren.push(XMLBuilder.w('tblCellMar', undefined, cmChildren));
|
|
1730
1941
|
}
|
|
1731
1942
|
}
|
|
1943
|
+
// 15. tblLook
|
|
1732
1944
|
if (prev.tblLook) {
|
|
1733
1945
|
prevTblPrChildren.push(XMLBuilder.wSelf('tblLook', buildTblLookAttributes(prev.tblLook)));
|
|
1734
1946
|
}
|
|
1735
|
-
|
|
1736
|
-
prevTblPrChildren.push(
|
|
1737
|
-
XMLBuilder.wSelf('tblStyleRowBandSize', { 'w:val': prev.tblStyleRowBandSize })
|
|
1738
|
-
);
|
|
1739
|
-
}
|
|
1740
|
-
if (prev.tblStyleColBandSize !== undefined) {
|
|
1741
|
-
prevTblPrChildren.push(
|
|
1742
|
-
XMLBuilder.wSelf('tblStyleColBandSize', { 'w:val': prev.tblStyleColBandSize })
|
|
1743
|
-
);
|
|
1744
|
-
}
|
|
1745
|
-
if (prev.overlap) {
|
|
1746
|
-
prevTblPrChildren.push(XMLBuilder.wSelf('tblOverlap', { 'w:val': prev.overlap }));
|
|
1747
|
-
}
|
|
1947
|
+
// 16. tblCaption
|
|
1748
1948
|
if (prev.caption) {
|
|
1749
1949
|
prevTblPrChildren.push(XMLBuilder.wSelf('tblCaption', { 'w:val': prev.caption }));
|
|
1750
1950
|
}
|
|
1951
|
+
// 17. tblDescription
|
|
1751
1952
|
if (prev.description) {
|
|
1752
1953
|
prevTblPrChildren.push(XMLBuilder.wSelf('tblDescription', { 'w:val': prev.description }));
|
|
1753
1954
|
}
|
|
@@ -1816,6 +2017,10 @@ export class Table {
|
|
|
1816
2017
|
*/
|
|
1817
2018
|
removeRow(index: number): boolean {
|
|
1818
2019
|
if (index >= 0 && index < this.rows.length) {
|
|
2020
|
+
// Per ECMA-376 §17.4.38, a table must contain at least one row
|
|
2021
|
+
if (this.rows.length <= 1 && !this.trackingContext?.isEnabled()) {
|
|
2022
|
+
return false;
|
|
2023
|
+
}
|
|
1819
2024
|
// When tracking enabled, mark cells with cellDel and wrap content in w:del
|
|
1820
2025
|
if (this.trackingContext?.isEnabled()) {
|
|
1821
2026
|
const author = this.trackingContext.getAuthor();
|
|
@@ -1865,10 +2070,13 @@ export class Table {
|
|
|
1865
2070
|
if (index < 0) index = 0;
|
|
1866
2071
|
if (index > this.rows.length) index = this.rows.length;
|
|
1867
2072
|
|
|
1868
|
-
// Create new row if not provided, matching the column count
|
|
2073
|
+
// Create new row if not provided, matching the grid column count
|
|
2074
|
+
// Use getTotalGridSpan() instead of getColumnCount() to account for merged cells
|
|
1869
2075
|
if (!row) {
|
|
1870
|
-
const
|
|
1871
|
-
|
|
2076
|
+
const gridColumns = this.formatting.tableGrid
|
|
2077
|
+
? this.formatting.tableGrid.length
|
|
2078
|
+
: Math.max(...this.rows.map((r) => r.getTotalGridSpan()), 1);
|
|
2079
|
+
row = new TableRow(gridColumns);
|
|
1872
2080
|
}
|
|
1873
2081
|
|
|
1874
2082
|
// Insert the row
|
|
@@ -1995,6 +2203,283 @@ export class Table {
|
|
|
1995
2203
|
return Math.max(...this.rows.map((row) => row.getCellCount()));
|
|
1996
2204
|
}
|
|
1997
2205
|
|
|
2206
|
+
/**
|
|
2207
|
+
* Gets all cells at a given column index across all rows
|
|
2208
|
+
*
|
|
2209
|
+
* Returns the cell at position `colIndex` from each row. Rows that have
|
|
2210
|
+
* fewer cells than the requested index are skipped (their slot is not
|
|
2211
|
+
* included in the result). This uses simple cell-index addressing, not
|
|
2212
|
+
* grid-span-aware column mapping.
|
|
2213
|
+
*
|
|
2214
|
+
* @param colIndex - Column index (0-based)
|
|
2215
|
+
* @returns Array of cells at that column position
|
|
2216
|
+
*
|
|
2217
|
+
* @example
|
|
2218
|
+
* ```typescript
|
|
2219
|
+
* // Sum values in the third column
|
|
2220
|
+
* const cells = table.getColumnCells(2);
|
|
2221
|
+
* const total = cells.reduce((sum, cell) => sum + Number(cell.getText()), 0);
|
|
2222
|
+
* ```
|
|
2223
|
+
*/
|
|
2224
|
+
getColumnCells(colIndex: number): TableCell[] {
|
|
2225
|
+
const cells: TableCell[] = [];
|
|
2226
|
+
for (const row of this.rows) {
|
|
2227
|
+
const cell = row.getCell(colIndex);
|
|
2228
|
+
if (cell) cells.push(cell);
|
|
2229
|
+
}
|
|
2230
|
+
return cells;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* Gets the text content of all cells in a column as a string array
|
|
2235
|
+
*
|
|
2236
|
+
* Like `getColumnCells()` but returns just the text values, skipping
|
|
2237
|
+
* the cell object layer. Rows with fewer cells than the column index
|
|
2238
|
+
* are skipped.
|
|
2239
|
+
*
|
|
2240
|
+
* @param colIndex - Column index (0-based)
|
|
2241
|
+
* @returns Array of cell text values
|
|
2242
|
+
*
|
|
2243
|
+
* @example
|
|
2244
|
+
* ```typescript
|
|
2245
|
+
* const names = table.getColumnTexts(0); // ['Name', 'Alice', 'Bob']
|
|
2246
|
+
* const total = table.getColumnTexts(2)
|
|
2247
|
+
* .slice(1)
|
|
2248
|
+
* .reduce((sum, v) => sum + Number(v), 0);
|
|
2249
|
+
* ```
|
|
2250
|
+
*/
|
|
2251
|
+
getColumnTexts(colIndex: number): string[] {
|
|
2252
|
+
return this.getColumnCells(colIndex).map((cell) => cell.getText());
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* Transforms the text content of every cell in a column
|
|
2257
|
+
*
|
|
2258
|
+
* Calls the transform function for each cell in the column with the
|
|
2259
|
+
* current text and row index. The return value replaces the cell's content.
|
|
2260
|
+
* Useful for formatting values, applying calculations, or normalizing data.
|
|
2261
|
+
*
|
|
2262
|
+
* @param colIndex - Column index (0-based)
|
|
2263
|
+
* @param transform - Function that receives cell text and row index, returns new text
|
|
2264
|
+
* @returns This table for chaining
|
|
2265
|
+
*
|
|
2266
|
+
* @example
|
|
2267
|
+
* ```typescript
|
|
2268
|
+
* // Uppercase all values in column 0
|
|
2269
|
+
* table.mapColumn(0, (text) => text.toUpperCase());
|
|
2270
|
+
*
|
|
2271
|
+
* // Format numbers in column 2 (skip header row)
|
|
2272
|
+
* table.mapColumn(2, (text, rowIndex) =>
|
|
2273
|
+
* rowIndex === 0 ? text : `$${Number(text).toFixed(2)}`
|
|
2274
|
+
* );
|
|
2275
|
+
*
|
|
2276
|
+
* // Add prefix to a column
|
|
2277
|
+
* table.mapColumn(1, (text) => `ID-${text}`);
|
|
2278
|
+
* ```
|
|
2279
|
+
*/
|
|
2280
|
+
mapColumn(colIndex: number, transform: (text: string, rowIndex: number) => string): this {
|
|
2281
|
+
for (let r = 0; r < this.rows.length; r++) {
|
|
2282
|
+
const cell = this.rows[r]!.getCell(colIndex);
|
|
2283
|
+
if (!cell) continue;
|
|
2284
|
+
|
|
2285
|
+
const currentText = cell.getText();
|
|
2286
|
+
const newText = transform(currentText, r);
|
|
2287
|
+
|
|
2288
|
+
if (newText !== currentText) {
|
|
2289
|
+
// Clear existing paragraphs and set new text
|
|
2290
|
+
const paragraphs = cell.getParagraphs();
|
|
2291
|
+
if (paragraphs.length > 0) {
|
|
2292
|
+
// Preserve first paragraph's formatting, replace text
|
|
2293
|
+
const firstPara = paragraphs[0]!;
|
|
2294
|
+
firstPara.setText(newText);
|
|
2295
|
+
// Remove extra paragraphs if cell had multiple
|
|
2296
|
+
for (let p = paragraphs.length - 1; p >= 1; p--) {
|
|
2297
|
+
cell.removeParagraph(p);
|
|
2298
|
+
}
|
|
2299
|
+
} else {
|
|
2300
|
+
cell.createParagraph(newText);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
return this;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/**
|
|
2309
|
+
* Iterates over every cell in the table, providing row and column indices
|
|
2310
|
+
*
|
|
2311
|
+
* Calls the callback for each cell with its row index, column index,
|
|
2312
|
+
* and the cell itself. Useful for bulk operations like formatting or
|
|
2313
|
+
* data extraction without manual nested loops.
|
|
2314
|
+
*
|
|
2315
|
+
* @param callback - Function called for each cell. Return `false` to stop iteration early.
|
|
2316
|
+
*
|
|
2317
|
+
* @example
|
|
2318
|
+
* ```typescript
|
|
2319
|
+
* // Apply shading to every other row
|
|
2320
|
+
* table.forEachCell((rowIndex, colIndex, cell) => {
|
|
2321
|
+
* if (rowIndex % 2 === 1) {
|
|
2322
|
+
* cell.setShading({ fill: 'F2F2F2', pattern: 'clear' });
|
|
2323
|
+
* }
|
|
2324
|
+
* });
|
|
2325
|
+
*
|
|
2326
|
+
* // Find a cell by content
|
|
2327
|
+
* let found: TableCell | undefined;
|
|
2328
|
+
* table.forEachCell((row, col, cell) => {
|
|
2329
|
+
* if (cell.getText().includes('Total')) {
|
|
2330
|
+
* found = cell;
|
|
2331
|
+
* return false; // stop early
|
|
2332
|
+
* }
|
|
2333
|
+
* });
|
|
2334
|
+
* ```
|
|
2335
|
+
*/
|
|
2336
|
+
forEachCell(
|
|
2337
|
+
callback: (rowIndex: number, colIndex: number, cell: TableCell) => void | false
|
|
2338
|
+
): void {
|
|
2339
|
+
for (let r = 0; r < this.rows.length; r++) {
|
|
2340
|
+
const cells = this.rows[r]!.getCells();
|
|
2341
|
+
for (let c = 0; c < cells.length; c++) {
|
|
2342
|
+
const result = callback(r, c, cells[c]!);
|
|
2343
|
+
if (result === false) return;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
/**
|
|
2349
|
+
* Finds the first cell matching a predicate, with its coordinates
|
|
2350
|
+
*
|
|
2351
|
+
* Iterates all cells in row-major order and returns the first one for
|
|
2352
|
+
* which the predicate returns true, along with its row and column index.
|
|
2353
|
+
*
|
|
2354
|
+
* @param predicate - Function that tests each cell
|
|
2355
|
+
* @returns Object with row, col, and cell, or undefined if not found
|
|
2356
|
+
*
|
|
2357
|
+
* @example
|
|
2358
|
+
* ```typescript
|
|
2359
|
+
* const result = table.findCell((cell) => cell.getText().includes('Total'));
|
|
2360
|
+
* if (result) {
|
|
2361
|
+
* console.log(`Found at row ${result.row}, col ${result.col}`);
|
|
2362
|
+
* }
|
|
2363
|
+
* ```
|
|
2364
|
+
*/
|
|
2365
|
+
findCell(
|
|
2366
|
+
predicate: (cell: TableCell, rowIndex: number, colIndex: number) => boolean
|
|
2367
|
+
): { row: number; col: number; cell: TableCell } | undefined {
|
|
2368
|
+
for (let r = 0; r < this.rows.length; r++) {
|
|
2369
|
+
const cells = this.rows[r]!.getCells();
|
|
2370
|
+
for (let c = 0; c < cells.length; c++) {
|
|
2371
|
+
if (predicate(cells[c]!, r, c)) {
|
|
2372
|
+
return { row: r, col: c, cell: cells[c]! };
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
return undefined;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
/**
|
|
2380
|
+
* Returns indices of rows matching a predicate
|
|
2381
|
+
*
|
|
2382
|
+
* Tests each row by passing all its cells to the predicate function.
|
|
2383
|
+
* Returns an array of row indices where the predicate returned true.
|
|
2384
|
+
*
|
|
2385
|
+
* @param predicate - Function that tests a row's cells
|
|
2386
|
+
* @returns Array of matching row indices (0-based)
|
|
2387
|
+
*
|
|
2388
|
+
* @example
|
|
2389
|
+
* ```typescript
|
|
2390
|
+
* // Find rows where the first cell contains "Total"
|
|
2391
|
+
* const totals = table.filterRows((cells) =>
|
|
2392
|
+
* cells[0]?.getText().includes('Total') ?? false
|
|
2393
|
+
* );
|
|
2394
|
+
*
|
|
2395
|
+
* // Find rows where all cells are empty
|
|
2396
|
+
* const empty = table.filterRows((cells) =>
|
|
2397
|
+
* cells.every((c) => c.getText().trim() === '')
|
|
2398
|
+
* );
|
|
2399
|
+
* ```
|
|
2400
|
+
*/
|
|
2401
|
+
filterRows(predicate: (cells: TableCell[], rowIndex: number) => boolean): number[] {
|
|
2402
|
+
const indices: number[] = [];
|
|
2403
|
+
for (let r = 0; r < this.rows.length; r++) {
|
|
2404
|
+
if (predicate(this.rows[r]!.getCells(), r)) {
|
|
2405
|
+
indices.push(r);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return indices;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* Removes rows where all cells are empty (no text content)
|
|
2413
|
+
*
|
|
2414
|
+
* A row is considered empty if every cell's trimmed text is the empty string.
|
|
2415
|
+
* Respects the ECMA-376 constraint that at least one row must remain — if all
|
|
2416
|
+
* rows are empty, the first row is kept.
|
|
2417
|
+
*
|
|
2418
|
+
* @returns Number of rows removed
|
|
2419
|
+
*
|
|
2420
|
+
* @example
|
|
2421
|
+
* ```typescript
|
|
2422
|
+
* const table = Table.fromArray([
|
|
2423
|
+
* ['Name', 'Age'],
|
|
2424
|
+
* ['', ''],
|
|
2425
|
+
* ['Alice', '30'],
|
|
2426
|
+
* ['', ''],
|
|
2427
|
+
* ]);
|
|
2428
|
+
* table.removeEmptyRows(); // Returns 2, table now has 2 rows
|
|
2429
|
+
* ```
|
|
2430
|
+
*/
|
|
2431
|
+
removeEmptyRows(): number {
|
|
2432
|
+
const emptyIndices = this.filterRows((cells) => cells.every((c) => c.getText().trim() === ''));
|
|
2433
|
+
|
|
2434
|
+
// Keep at least one row (ECMA-376 requires >= 1 row)
|
|
2435
|
+
const toRemove =
|
|
2436
|
+
emptyIndices.length === this.rows.length
|
|
2437
|
+
? emptyIndices.slice(1) // keep first row
|
|
2438
|
+
: emptyIndices;
|
|
2439
|
+
|
|
2440
|
+
// Remove in reverse order to preserve indices
|
|
2441
|
+
for (let i = toRemove.length - 1; i >= 0; i--) {
|
|
2442
|
+
this.removeRow(toRemove[i]!);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
return toRemove.length;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* Removes columns where all cells are empty (no text content)
|
|
2450
|
+
*
|
|
2451
|
+
* A column is considered empty if every cell at that column index has
|
|
2452
|
+
* trimmed text equal to the empty string.
|
|
2453
|
+
*
|
|
2454
|
+
* @returns Number of columns removed
|
|
2455
|
+
*
|
|
2456
|
+
* @example
|
|
2457
|
+
* ```typescript
|
|
2458
|
+
* const table = Table.fromArray([
|
|
2459
|
+
* ['Name', '', 'Age'],
|
|
2460
|
+
* ['Alice', '', '30'],
|
|
2461
|
+
* ]);
|
|
2462
|
+
* table.removeEmptyColumns(); // Returns 1, middle column removed
|
|
2463
|
+
* ```
|
|
2464
|
+
*/
|
|
2465
|
+
removeEmptyColumns(): number {
|
|
2466
|
+
const colCount = this.getColumnCount();
|
|
2467
|
+
let removed = 0;
|
|
2468
|
+
|
|
2469
|
+
// Check columns in reverse order to preserve indices
|
|
2470
|
+
for (let c = colCount - 1; c >= 0; c--) {
|
|
2471
|
+
const columnCells = this.getColumnCells(c);
|
|
2472
|
+
const isEmpty = columnCells.every((cell) => cell.getText().trim() === '');
|
|
2473
|
+
|
|
2474
|
+
if (isEmpty) {
|
|
2475
|
+
this.removeColumn(c);
|
|
2476
|
+
removed++;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
return removed;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
1998
2483
|
/**
|
|
1999
2484
|
* Sets specific widths for table columns
|
|
2000
2485
|
*
|
|
@@ -2039,6 +2524,53 @@ export class Table {
|
|
|
2039
2524
|
return new Table(rows, columns, formatting);
|
|
2040
2525
|
}
|
|
2041
2526
|
|
|
2527
|
+
/**
|
|
2528
|
+
* Creates a table from a 2D string array
|
|
2529
|
+
*
|
|
2530
|
+
* Each inner array becomes a row; each string becomes a cell with a single
|
|
2531
|
+
* paragraph. The table column count is determined by the longest row.
|
|
2532
|
+
* Shorter rows are padded with empty cells to keep the grid rectangular.
|
|
2533
|
+
*
|
|
2534
|
+
* @param data - 2D array of cell text values
|
|
2535
|
+
* @param formatting - Optional table formatting
|
|
2536
|
+
* @returns New Table populated with the data
|
|
2537
|
+
*
|
|
2538
|
+
* @example
|
|
2539
|
+
* ```typescript
|
|
2540
|
+
* const table = Table.fromArray([
|
|
2541
|
+
* ['Name', 'Age', 'City'],
|
|
2542
|
+
* ['Alice', '30', 'New York'],
|
|
2543
|
+
* ['Bob', '25', 'London'],
|
|
2544
|
+
* ]);
|
|
2545
|
+
* ```
|
|
2546
|
+
*
|
|
2547
|
+
* @example
|
|
2548
|
+
* ```typescript
|
|
2549
|
+
* // With formatting
|
|
2550
|
+
* const table = Table.fromArray(
|
|
2551
|
+
* [['Header 1', 'Header 2'], ['Data 1', 'Data 2']],
|
|
2552
|
+
* { alignment: 'center', layout: 'fixed' }
|
|
2553
|
+
* );
|
|
2554
|
+
* ```
|
|
2555
|
+
*/
|
|
2556
|
+
static fromArray(data: string[][], formatting?: TableFormatting): Table {
|
|
2557
|
+
if (data.length === 0) return new Table(0, 0, formatting);
|
|
2558
|
+
|
|
2559
|
+
const maxCols = Math.max(...data.map((row) => row.length));
|
|
2560
|
+
const table = new Table(0, 0, formatting);
|
|
2561
|
+
|
|
2562
|
+
for (const rowData of data) {
|
|
2563
|
+
const row = new TableRow();
|
|
2564
|
+
for (let c = 0; c < maxCols; c++) {
|
|
2565
|
+
const text = c < rowData.length ? rowData[c] : undefined;
|
|
2566
|
+
row.createCell(text || undefined);
|
|
2567
|
+
}
|
|
2568
|
+
table.addRow(row);
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
return table;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2042
2574
|
/**
|
|
2043
2575
|
* Merges cells into a single cell (uses columnSpan and rowSpan)
|
|
2044
2576
|
* @param startRow - Starting row index (0-based)
|
|
@@ -2349,7 +2881,10 @@ export class Table {
|
|
|
2349
2881
|
*/
|
|
2350
2882
|
insertRows(startIndex: number, count: number): TableRow[] {
|
|
2351
2883
|
const insertedRows: TableRow[] = [];
|
|
2352
|
-
|
|
2884
|
+
// Use grid column count (accounting for merged cells) instead of cell count
|
|
2885
|
+
const columnCount = this.formatting.tableGrid
|
|
2886
|
+
? this.formatting.tableGrid.length
|
|
2887
|
+
: Math.max(...this.rows.map((r) => r.getTotalGridSpan()), 1);
|
|
2353
2888
|
|
|
2354
2889
|
for (let i = 0; i < count; i++) {
|
|
2355
2890
|
const row = new TableRow(columnCount);
|
|
@@ -2468,6 +3003,235 @@ export class Table {
|
|
|
2468
3003
|
return this;
|
|
2469
3004
|
}
|
|
2470
3005
|
|
|
3006
|
+
/**
|
|
3007
|
+
* Extracts table content as a 2D string array
|
|
3008
|
+
*
|
|
3009
|
+
* Returns an array of rows, where each row is an array of cell text values.
|
|
3010
|
+
* Multi-paragraph cells join their text with newlines. Rows with fewer cells
|
|
3011
|
+
* than the widest row are NOT padded — the inner arrays may have different lengths.
|
|
3012
|
+
*
|
|
3013
|
+
* @returns 2D array of cell text content
|
|
3014
|
+
*
|
|
3015
|
+
* @example
|
|
3016
|
+
* ```typescript
|
|
3017
|
+
* const table = new Table(2, 3);
|
|
3018
|
+
* table.getCell(0, 0)!.createParagraph('Name');
|
|
3019
|
+
* table.getCell(0, 1)!.createParagraph('Age');
|
|
3020
|
+
* table.getCell(1, 0)!.createParagraph('Alice');
|
|
3021
|
+
* table.getCell(1, 1)!.createParagraph('30');
|
|
3022
|
+
*
|
|
3023
|
+
* const data = table.toArray();
|
|
3024
|
+
* // [['Name', 'Age', ''], ['Alice', '30', '']]
|
|
3025
|
+
* ```
|
|
3026
|
+
*/
|
|
3027
|
+
toArray(): string[][] {
|
|
3028
|
+
return this.rows.map((row) => row.getCells().map((cell) => cell.getText()));
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/**
|
|
3032
|
+
* Converts table content to a plain text representation
|
|
3033
|
+
*
|
|
3034
|
+
* Renders the table as tab-separated values with rows on separate lines.
|
|
3035
|
+
* Useful for logging, debugging, or simple text export.
|
|
3036
|
+
*
|
|
3037
|
+
* @param columnSeparator - Separator between columns (default: tab)
|
|
3038
|
+
* @param rowSeparator - Separator between rows (default: newline)
|
|
3039
|
+
* @returns Plain text representation of the table
|
|
3040
|
+
*
|
|
3041
|
+
* @example
|
|
3042
|
+
* ```typescript
|
|
3043
|
+
* console.log(table.toPlainText());
|
|
3044
|
+
* // "Name\tAge\nAlice\t30"
|
|
3045
|
+
*
|
|
3046
|
+
* // CSV-style output
|
|
3047
|
+
* console.log(table.toPlainText(',', '\n'));
|
|
3048
|
+
* // "Name,Age\nAlice,30"
|
|
3049
|
+
* ```
|
|
3050
|
+
*/
|
|
3051
|
+
toPlainText(columnSeparator = '\t', rowSeparator = '\n'): string {
|
|
3052
|
+
return this.rows
|
|
3053
|
+
.map((row) =>
|
|
3054
|
+
row
|
|
3055
|
+
.getCells()
|
|
3056
|
+
.map((cell) => cell.getText())
|
|
3057
|
+
.join(columnSeparator)
|
|
3058
|
+
)
|
|
3059
|
+
.join(rowSeparator);
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
/**
|
|
3063
|
+
* Exports table content as a CSV string
|
|
3064
|
+
*
|
|
3065
|
+
* Produces RFC 4180-compliant CSV: fields containing commas, quotes,
|
|
3066
|
+
* or newlines are quoted, and embedded quotes are escaped by doubling.
|
|
3067
|
+
*
|
|
3068
|
+
* @param delimiter - Field delimiter (default: ',')
|
|
3069
|
+
* @returns CSV string
|
|
3070
|
+
*
|
|
3071
|
+
* @example
|
|
3072
|
+
* ```typescript
|
|
3073
|
+
* const csv = table.toCSV();
|
|
3074
|
+
* // "Name,Age,City\nAlice,30,New York\nBob,25,London"
|
|
3075
|
+
*
|
|
3076
|
+
* // TSV variant
|
|
3077
|
+
* const tsv = table.toCSV('\t');
|
|
3078
|
+
* ```
|
|
3079
|
+
*/
|
|
3080
|
+
toCSV(delimiter = ','): string {
|
|
3081
|
+
return this.rows
|
|
3082
|
+
.map((row) =>
|
|
3083
|
+
row
|
|
3084
|
+
.getCells()
|
|
3085
|
+
.map((cell) => {
|
|
3086
|
+
const text = cell.getText();
|
|
3087
|
+
// Quote field if it contains delimiter, quotes, or newlines
|
|
3088
|
+
if (text.includes(delimiter) || text.includes('"') || text.includes('\n')) {
|
|
3089
|
+
return '"' + text.replace(/"/g, '""') + '"';
|
|
3090
|
+
}
|
|
3091
|
+
return text;
|
|
3092
|
+
})
|
|
3093
|
+
.join(delimiter)
|
|
3094
|
+
)
|
|
3095
|
+
.join('\n');
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
/**
|
|
3099
|
+
* Creates a table from a CSV string
|
|
3100
|
+
*
|
|
3101
|
+
* Parses RFC 4180-compliant CSV: handles quoted fields, escaped quotes
|
|
3102
|
+
* (doubled `""`), and newlines within quoted fields.
|
|
3103
|
+
*
|
|
3104
|
+
* @param csv - CSV string to parse
|
|
3105
|
+
* @param delimiter - Field delimiter (default: ',')
|
|
3106
|
+
* @param formatting - Optional table formatting
|
|
3107
|
+
* @returns New Table populated with the parsed data
|
|
3108
|
+
*
|
|
3109
|
+
* @example
|
|
3110
|
+
* ```typescript
|
|
3111
|
+
* const table = Table.fromCSV('Name,Age\nAlice,30\nBob,25');
|
|
3112
|
+
*
|
|
3113
|
+
* // TSV input
|
|
3114
|
+
* const table2 = Table.fromCSV('Name\tAge\nAlice\t30', '\t');
|
|
3115
|
+
*
|
|
3116
|
+
* // Handles quoted fields
|
|
3117
|
+
* const table3 = Table.fromCSV('"City, State",Pop\n"New York, NY",8336817');
|
|
3118
|
+
* ```
|
|
3119
|
+
*/
|
|
3120
|
+
static fromCSV(csv: string, delimiter = ',', formatting?: TableFormatting): Table {
|
|
3121
|
+
const rows: string[][] = [];
|
|
3122
|
+
let currentRow: string[] = [];
|
|
3123
|
+
let currentField = '';
|
|
3124
|
+
let inQuotes = false;
|
|
3125
|
+
let i = 0;
|
|
3126
|
+
|
|
3127
|
+
while (i < csv.length) {
|
|
3128
|
+
const ch = csv[i]!;
|
|
3129
|
+
|
|
3130
|
+
if (inQuotes) {
|
|
3131
|
+
if (ch === '"') {
|
|
3132
|
+
// Check for escaped quote ""
|
|
3133
|
+
if (i + 1 < csv.length && csv[i + 1] === '"') {
|
|
3134
|
+
currentField += '"';
|
|
3135
|
+
i += 2;
|
|
3136
|
+
} else {
|
|
3137
|
+
// End of quoted field
|
|
3138
|
+
inQuotes = false;
|
|
3139
|
+
i++;
|
|
3140
|
+
}
|
|
3141
|
+
} else {
|
|
3142
|
+
currentField += ch;
|
|
3143
|
+
i++;
|
|
3144
|
+
}
|
|
3145
|
+
} else {
|
|
3146
|
+
if (ch === '"') {
|
|
3147
|
+
inQuotes = true;
|
|
3148
|
+
i++;
|
|
3149
|
+
} else if (ch === delimiter) {
|
|
3150
|
+
currentRow.push(currentField);
|
|
3151
|
+
currentField = '';
|
|
3152
|
+
i++;
|
|
3153
|
+
} else if (ch === '\n') {
|
|
3154
|
+
currentRow.push(currentField);
|
|
3155
|
+
currentField = '';
|
|
3156
|
+
rows.push(currentRow);
|
|
3157
|
+
currentRow = [];
|
|
3158
|
+
i++;
|
|
3159
|
+
} else if (ch === '\r') {
|
|
3160
|
+
// Handle \r\n
|
|
3161
|
+
if (i + 1 < csv.length && csv[i + 1] === '\n') {
|
|
3162
|
+
i++;
|
|
3163
|
+
}
|
|
3164
|
+
currentRow.push(currentField);
|
|
3165
|
+
currentField = '';
|
|
3166
|
+
rows.push(currentRow);
|
|
3167
|
+
currentRow = [];
|
|
3168
|
+
i++;
|
|
3169
|
+
} else {
|
|
3170
|
+
currentField += ch;
|
|
3171
|
+
i++;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// Push last field and row
|
|
3177
|
+
if (currentField || currentRow.length > 0) {
|
|
3178
|
+
currentRow.push(currentField);
|
|
3179
|
+
rows.push(currentRow);
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
if (rows.length === 0) return new Table(0, 0, formatting);
|
|
3183
|
+
return Table.fromArray(rows, formatting);
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
/**
|
|
3187
|
+
* Creates a new table with rows and columns swapped
|
|
3188
|
+
*
|
|
3189
|
+
* Returns a new Table where the original rows become columns and columns
|
|
3190
|
+
* become rows. Cell text content is preserved; cell formatting from the
|
|
3191
|
+
* source is deep-cloned to the transposed position. The original table
|
|
3192
|
+
* is not modified.
|
|
3193
|
+
*
|
|
3194
|
+
* For non-rectangular tables (rows with different cell counts), the
|
|
3195
|
+
* result is padded to the longest row's length with empty cells.
|
|
3196
|
+
*
|
|
3197
|
+
* @returns New Table with transposed data
|
|
3198
|
+
*
|
|
3199
|
+
* @example
|
|
3200
|
+
* ```typescript
|
|
3201
|
+
* const table = Table.fromArray([
|
|
3202
|
+
* ['Name', 'Alice', 'Bob'],
|
|
3203
|
+
* ['Age', '30', '25'],
|
|
3204
|
+
* ]);
|
|
3205
|
+
* const transposed = table.transpose();
|
|
3206
|
+
* transposed.toArray();
|
|
3207
|
+
* // [['Name', 'Age'], ['Alice', '30'], ['Bob', '25']]
|
|
3208
|
+
* ```
|
|
3209
|
+
*/
|
|
3210
|
+
transpose(): Table {
|
|
3211
|
+
const srcRows = this.rows.length;
|
|
3212
|
+
if (srcRows === 0) return new Table(0, 0);
|
|
3213
|
+
|
|
3214
|
+
const srcCols = this.getColumnCount();
|
|
3215
|
+
if (srcCols === 0) return new Table(0, 0);
|
|
3216
|
+
|
|
3217
|
+
const transposed = new Table(0, 0);
|
|
3218
|
+
|
|
3219
|
+
for (let c = 0; c < srcCols; c++) {
|
|
3220
|
+
const newRow = new TableRow();
|
|
3221
|
+
for (let r = 0; r < srcRows; r++) {
|
|
3222
|
+
const srcCell = this.getCell(r, c);
|
|
3223
|
+
if (srcCell) {
|
|
3224
|
+
newRow.addCell(srcCell.clone());
|
|
3225
|
+
} else {
|
|
3226
|
+
newRow.createCell();
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
transposed.addRow(newRow);
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
return transposed;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
2471
3235
|
/**
|
|
2472
3236
|
* Creates a deep clone of this table
|
|
2473
3237
|
*
|
|
@@ -2487,33 +3251,58 @@ export class Table {
|
|
|
2487
3251
|
* ```
|
|
2488
3252
|
*/
|
|
2489
3253
|
clone(): Table {
|
|
2490
|
-
// Clone formatting
|
|
2491
3254
|
const clonedFormatting: TableFormatting = deepClone(this.formatting);
|
|
2492
|
-
|
|
2493
|
-
// Create new table with same structure
|
|
2494
3255
|
const clonedTable = new Table(0, 0, clonedFormatting);
|
|
2495
3256
|
|
|
2496
|
-
// Clone all rows
|
|
2497
3257
|
for (const row of this.rows) {
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
const clonedRow = new TableRow(0);
|
|
3258
|
+
clonedTable.addRow(row.clone());
|
|
3259
|
+
}
|
|
2501
3260
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const clonedCell = new TableCell(deepClone(cellFormatting));
|
|
3261
|
+
return clonedTable;
|
|
3262
|
+
}
|
|
2505
3263
|
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
3264
|
+
/**
|
|
3265
|
+
* Duplicates a row at the given index, inserting copies after it
|
|
3266
|
+
*
|
|
3267
|
+
* Creates deep clones of the specified row (including all cell content,
|
|
3268
|
+
* formatting, and merged cell spans) and inserts them immediately after
|
|
3269
|
+
* the source row. Useful for template-based document generation where
|
|
3270
|
+
* a styled row needs to be repeated with different data.
|
|
3271
|
+
*
|
|
3272
|
+
* @param rowIndex - Index of the row to duplicate (0-based)
|
|
3273
|
+
* @param count - Number of copies to insert (default 1)
|
|
3274
|
+
* @returns Array of the newly inserted rows
|
|
3275
|
+
* @throws RangeError if rowIndex is out of bounds
|
|
3276
|
+
*
|
|
3277
|
+
* @example
|
|
3278
|
+
* ```typescript
|
|
3279
|
+
* // Create a table with a header and one data row
|
|
3280
|
+
* const table = new Table(2, 3);
|
|
3281
|
+
* table.getCell(0, 0)?.createParagraph('Name');
|
|
3282
|
+
* table.getCell(1, 0)?.createParagraph('Alice');
|
|
3283
|
+
*
|
|
3284
|
+
* // Duplicate the data row twice for more entries
|
|
3285
|
+
* const [row2, row3] = table.duplicateRow(1, 2);
|
|
3286
|
+
* row2.getCell(0)?.getParagraphs()[0]?.setText('Bob');
|
|
3287
|
+
* row3.getCell(0)?.getParagraphs()[0]?.setText('Charlie');
|
|
3288
|
+
* ```
|
|
3289
|
+
*/
|
|
3290
|
+
duplicateRow(rowIndex: number, count = 1): TableRow[] {
|
|
3291
|
+
if (rowIndex < 0 || rowIndex >= this.rows.length) {
|
|
3292
|
+
throw new RangeError(`Row index ${rowIndex} is out of bounds (0-${this.rows.length - 1})`);
|
|
3293
|
+
}
|
|
3294
|
+
if (count < 1) return [];
|
|
2510
3295
|
|
|
2511
|
-
|
|
2512
|
-
|
|
3296
|
+
const sourceRow = this.rows[rowIndex]!;
|
|
3297
|
+
const inserted: TableRow[] = [];
|
|
2513
3298
|
|
|
2514
|
-
|
|
3299
|
+
for (let i = 0; i < count; i++) {
|
|
3300
|
+
const clonedRow = sourceRow.clone();
|
|
3301
|
+
this.rows.splice(rowIndex + 1 + i, 0, clonedRow);
|
|
3302
|
+
clonedRow._setParentTable(this);
|
|
3303
|
+
inserted.push(clonedRow);
|
|
2515
3304
|
}
|
|
2516
3305
|
|
|
2517
|
-
return
|
|
3306
|
+
return inserted;
|
|
2518
3307
|
}
|
|
2519
3308
|
}
|