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.
Files changed (184) hide show
  1. package/README.md +158 -7
  2. package/dist/core/Document.d.ts +97 -3
  3. package/dist/core/Document.d.ts.map +1 -1
  4. package/dist/core/Document.js +727 -50
  5. package/dist/core/Document.js.map +1 -1
  6. package/dist/core/DocumentContent.d.ts.map +1 -1
  7. package/dist/core/DocumentContent.js +0 -8
  8. package/dist/core/DocumentContent.js.map +1 -1
  9. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  10. package/dist/core/DocumentGenerator.js +9 -5
  11. package/dist/core/DocumentGenerator.js.map +1 -1
  12. package/dist/core/DocumentParser.d.ts.map +1 -1
  13. package/dist/core/DocumentParser.js +573 -101
  14. package/dist/core/DocumentParser.js.map +1 -1
  15. package/dist/core/RelationshipManager.d.ts.map +1 -1
  16. package/dist/core/RelationshipManager.js +4 -3
  17. package/dist/core/RelationshipManager.js.map +1 -1
  18. package/dist/elements/Bookmark.d.ts +7 -0
  19. package/dist/elements/Bookmark.d.ts.map +1 -1
  20. package/dist/elements/Bookmark.js +24 -4
  21. package/dist/elements/Bookmark.js.map +1 -1
  22. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  23. package/dist/elements/BookmarkManager.js +4 -3
  24. package/dist/elements/BookmarkManager.js.map +1 -1
  25. package/dist/elements/CommonTypes.d.ts +2 -2
  26. package/dist/elements/CommonTypes.d.ts.map +1 -1
  27. package/dist/elements/CommonTypes.js +14 -1
  28. package/dist/elements/CommonTypes.js.map +1 -1
  29. package/dist/elements/Field.d.ts +1 -1
  30. package/dist/elements/Field.d.ts.map +1 -1
  31. package/dist/elements/Field.js +1 -1
  32. package/dist/elements/Field.js.map +1 -1
  33. package/dist/elements/Footer.d.ts +2 -0
  34. package/dist/elements/Footer.d.ts.map +1 -1
  35. package/dist/elements/Footer.js +6 -0
  36. package/dist/elements/Footer.js.map +1 -1
  37. package/dist/elements/Header.d.ts +2 -0
  38. package/dist/elements/Header.d.ts.map +1 -1
  39. package/dist/elements/Header.js +6 -0
  40. package/dist/elements/Header.js.map +1 -1
  41. package/dist/elements/Image.d.ts.map +1 -1
  42. package/dist/elements/Image.js +3 -0
  43. package/dist/elements/Image.js.map +1 -1
  44. package/dist/elements/Paragraph.d.ts +81 -1
  45. package/dist/elements/Paragraph.d.ts.map +1 -1
  46. package/dist/elements/Paragraph.js +515 -21
  47. package/dist/elements/Paragraph.js.map +1 -1
  48. package/dist/elements/Revision.d.ts +0 -1
  49. package/dist/elements/Revision.d.ts.map +1 -1
  50. package/dist/elements/Revision.js +0 -12
  51. package/dist/elements/Revision.js.map +1 -1
  52. package/dist/elements/RevisionManager.d.ts +0 -1
  53. package/dist/elements/RevisionManager.d.ts.map +1 -1
  54. package/dist/elements/RevisionManager.js +0 -2
  55. package/dist/elements/RevisionManager.js.map +1 -1
  56. package/dist/elements/Run.d.ts +16 -4
  57. package/dist/elements/Run.d.ts.map +1 -1
  58. package/dist/elements/Run.js +114 -22
  59. package/dist/elements/Run.js.map +1 -1
  60. package/dist/elements/Section.d.ts +7 -1
  61. package/dist/elements/Section.d.ts.map +1 -1
  62. package/dist/elements/Section.js +185 -4
  63. package/dist/elements/Section.js.map +1 -1
  64. package/dist/elements/Shape.js.map +1 -1
  65. package/dist/elements/Table.d.ts +30 -1
  66. package/dist/elements/Table.d.ts.map +1 -1
  67. package/dist/elements/Table.js +357 -40
  68. package/dist/elements/Table.js.map +1 -1
  69. package/dist/elements/TableCell.d.ts +3 -0
  70. package/dist/elements/TableCell.d.ts.map +1 -1
  71. package/dist/elements/TableCell.js +30 -3
  72. package/dist/elements/TableCell.js.map +1 -1
  73. package/dist/elements/TableGridChange.d.ts +0 -1
  74. package/dist/elements/TableGridChange.d.ts.map +1 -1
  75. package/dist/elements/TableGridChange.js +0 -10
  76. package/dist/elements/TableGridChange.js.map +1 -1
  77. package/dist/elements/TableRow.d.ts +4 -0
  78. package/dist/elements/TableRow.d.ts.map +1 -1
  79. package/dist/elements/TableRow.js +31 -3
  80. package/dist/elements/TableRow.js.map +1 -1
  81. package/dist/formatting/AbstractNumbering.d.ts +5 -0
  82. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  83. package/dist/formatting/AbstractNumbering.js +22 -0
  84. package/dist/formatting/AbstractNumbering.js.map +1 -1
  85. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  86. package/dist/formatting/NumberingLevel.js +3 -3
  87. package/dist/formatting/NumberingLevel.js.map +1 -1
  88. package/dist/formatting/Style.d.ts +1 -0
  89. package/dist/formatting/Style.d.ts.map +1 -1
  90. package/dist/formatting/Style.js +25 -59
  91. package/dist/formatting/Style.js.map +1 -1
  92. package/dist/formatting/StylesManager.d.ts +1 -0
  93. package/dist/formatting/StylesManager.d.ts.map +1 -1
  94. package/dist/formatting/StylesManager.js +12 -0
  95. package/dist/formatting/StylesManager.js.map +1 -1
  96. package/dist/helpers/CleanupHelper.js.map +1 -1
  97. package/dist/images/ImageOptimizer.d.ts.map +1 -1
  98. package/dist/images/ImageOptimizer.js +0 -1
  99. package/dist/images/ImageOptimizer.js.map +1 -1
  100. package/dist/index.d.ts +1 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js.map +1 -1
  103. package/dist/managers/DrawingManager.d.ts.map +1 -1
  104. package/dist/managers/DrawingManager.js +4 -2
  105. package/dist/managers/DrawingManager.js.map +1 -1
  106. package/dist/types/formatting.d.ts +2 -2
  107. package/dist/types/formatting.d.ts.map +1 -1
  108. package/dist/types/formatting.js.map +1 -1
  109. package/dist/utils/ChangelogGenerator.d.ts +2 -2
  110. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  111. package/dist/utils/ChangelogGenerator.js +4 -5
  112. package/dist/utils/ChangelogGenerator.js.map +1 -1
  113. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  114. package/dist/utils/InMemoryRevisionAcceptor.js +0 -1
  115. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  116. package/dist/utils/RevisionAwareProcessor.d.ts +2 -2
  117. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  118. package/dist/utils/RevisionAwareProcessor.js +2 -2
  119. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  120. package/dist/utils/SelectiveRevisionAcceptor.d.ts +0 -2
  121. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  122. package/dist/utils/SelectiveRevisionAcceptor.js +0 -26
  123. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  124. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  125. package/dist/utils/ShadingResolver.js.map +1 -1
  126. package/dist/utils/acceptRevisions.js +1 -1
  127. package/dist/utils/acceptRevisions.js.map +1 -1
  128. package/dist/utils/stripTrackedChanges.js +1 -1
  129. package/dist/utils/stripTrackedChanges.js.map +1 -1
  130. package/dist/utils/units.d.ts.map +1 -1
  131. package/dist/utils/units.js +1 -1
  132. package/dist/utils/units.js.map +1 -1
  133. package/dist/validation/RevisionAutoFixer.d.ts +2 -1
  134. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  135. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  136. package/package.json +10 -1
  137. package/src/constants/CLAUDE.md +28 -0
  138. package/src/core/CLAUDE.md +4 -0
  139. package/src/core/Document.ts +1755 -85
  140. package/src/core/DocumentContent.ts +0 -11
  141. package/src/core/DocumentGenerator.ts +11 -12
  142. package/src/core/DocumentParser.ts +599 -138
  143. package/src/core/RelationshipManager.ts +6 -3
  144. package/src/elements/Bookmark.ts +39 -4
  145. package/src/elements/BookmarkManager.ts +4 -3
  146. package/src/elements/CLAUDE.md +18 -2
  147. package/src/elements/CommonTypes.ts +35 -8
  148. package/src/elements/Field.ts +1 -1
  149. package/src/elements/Footer.ts +23 -0
  150. package/src/elements/Header.ts +25 -0
  151. package/src/elements/Image.ts +5 -0
  152. package/src/elements/Paragraph.ts +1069 -41
  153. package/src/elements/Revision.ts +0 -19
  154. package/src/elements/RevisionManager.ts +1 -3
  155. package/src/elements/Run.ts +265 -35
  156. package/src/elements/Section.ts +214 -8
  157. package/src/elements/Shape.ts +1 -1
  158. package/src/elements/Table.ts +850 -61
  159. package/src/elements/TableCell.ts +84 -10
  160. package/src/elements/TableGridChange.ts +2 -16
  161. package/src/elements/TableRow.ts +94 -9
  162. package/src/formatting/AbstractNumbering.ts +42 -1
  163. package/src/formatting/CLAUDE.md +4 -0
  164. package/src/formatting/NumberingLevel.ts +11 -7
  165. package/src/formatting/Style.ts +39 -71
  166. package/src/formatting/StylesManager.ts +36 -0
  167. package/src/helpers/CleanupHelper.ts +1 -1
  168. package/src/images/ImageOptimizer.ts +0 -3
  169. package/src/index.ts +1 -1
  170. package/src/managers/DrawingManager.ts +5 -3
  171. package/src/tracking/CLAUDE.md +30 -0
  172. package/src/types/CLAUDE.md +39 -0
  173. package/src/types/formatting.ts +2 -2
  174. package/src/utils/CLAUDE.md +15 -0
  175. package/src/utils/ChangelogGenerator.ts +4 -5
  176. package/src/utils/InMemoryRevisionAcceptor.ts +0 -9
  177. package/src/utils/RevisionAwareProcessor.ts +2 -3
  178. package/src/utils/SelectiveRevisionAcceptor.ts +0 -39
  179. package/src/utils/ShadingResolver.ts +0 -1
  180. package/src/utils/acceptRevisions.ts +1 -1
  181. package/src/utils/stripTrackedChanges.ts +1 -1
  182. package/src/utils/units.ts +2 -1
  183. package/src/validation/CLAUDE.md +40 -0
  184. package/src/validation/RevisionAutoFixer.ts +2 -1
@@ -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 in style context.
1512
- // Values are preserved in formatting for round-trip and style use.
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
- if (prev.cellSpacing !== undefined) {
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
- for (const side of ['top', 'start', 'bottom', 'end'] as const) {
1723
- const val = (prev.cellMargins as Record<string, number | undefined>)[side];
1724
- if (val !== undefined) {
1725
- cmChildren.push(XMLBuilder.wSelf(side, { 'w:w': val, 'w:type': 'dxa' }));
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
- if (prev.tblStyleRowBandSize !== undefined) {
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 columnCount = this.getColumnCount();
1871
- row = new TableRow(columnCount);
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
- const columnCount = this.getColumnCount();
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
- // Clone row by creating new cells with same content
2499
- const cells = row.getCells();
2500
- const clonedRow = new TableRow(0);
3258
+ clonedTable.addRow(row.clone());
3259
+ }
2501
3260
 
2502
- for (const cell of cells) {
2503
- const cellFormatting = cell.getFormatting();
2504
- const clonedCell = new TableCell(deepClone(cellFormatting));
3261
+ return clonedTable;
3262
+ }
2505
3263
 
2506
- // Clone paragraphs in cell
2507
- for (const para of cell.getParagraphs()) {
2508
- clonedCell.addParagraph(para.clone());
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
- clonedRow.addCell(clonedCell);
2512
- }
3296
+ const sourceRow = this.rows[rowIndex]!;
3297
+ const inserted: TableRow[] = [];
2513
3298
 
2514
- clonedTable.addRow(clonedRow);
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 clonedTable;
3306
+ return inserted;
2518
3307
  }
2519
3308
  }