docxmlater 10.3.5 → 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 (185) hide show
  1. package/README.md +158 -7
  2. package/dist/core/Document.d.ts +102 -3
  3. package/dist/core/Document.d.ts.map +1 -1
  4. package/dist/core/Document.js +775 -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 +588 -102
  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 +1 -0
  42. package/dist/elements/Image.d.ts.map +1 -1
  43. package/dist/elements/Image.js +17 -2
  44. package/dist/elements/Image.js.map +1 -1
  45. package/dist/elements/Paragraph.d.ts +81 -1
  46. package/dist/elements/Paragraph.d.ts.map +1 -1
  47. package/dist/elements/Paragraph.js +515 -21
  48. package/dist/elements/Paragraph.js.map +1 -1
  49. package/dist/elements/Revision.d.ts +0 -1
  50. package/dist/elements/Revision.d.ts.map +1 -1
  51. package/dist/elements/Revision.js +0 -12
  52. package/dist/elements/Revision.js.map +1 -1
  53. package/dist/elements/RevisionManager.d.ts +0 -1
  54. package/dist/elements/RevisionManager.d.ts.map +1 -1
  55. package/dist/elements/RevisionManager.js +0 -2
  56. package/dist/elements/RevisionManager.js.map +1 -1
  57. package/dist/elements/Run.d.ts +16 -4
  58. package/dist/elements/Run.d.ts.map +1 -1
  59. package/dist/elements/Run.js +114 -22
  60. package/dist/elements/Run.js.map +1 -1
  61. package/dist/elements/Section.d.ts +7 -1
  62. package/dist/elements/Section.d.ts.map +1 -1
  63. package/dist/elements/Section.js +185 -4
  64. package/dist/elements/Section.js.map +1 -1
  65. package/dist/elements/Shape.js.map +1 -1
  66. package/dist/elements/Table.d.ts +30 -1
  67. package/dist/elements/Table.d.ts.map +1 -1
  68. package/dist/elements/Table.js +357 -40
  69. package/dist/elements/Table.js.map +1 -1
  70. package/dist/elements/TableCell.d.ts +3 -0
  71. package/dist/elements/TableCell.d.ts.map +1 -1
  72. package/dist/elements/TableCell.js +30 -3
  73. package/dist/elements/TableCell.js.map +1 -1
  74. package/dist/elements/TableGridChange.d.ts +0 -1
  75. package/dist/elements/TableGridChange.d.ts.map +1 -1
  76. package/dist/elements/TableGridChange.js +0 -10
  77. package/dist/elements/TableGridChange.js.map +1 -1
  78. package/dist/elements/TableRow.d.ts +4 -0
  79. package/dist/elements/TableRow.d.ts.map +1 -1
  80. package/dist/elements/TableRow.js +31 -3
  81. package/dist/elements/TableRow.js.map +1 -1
  82. package/dist/formatting/AbstractNumbering.d.ts +5 -0
  83. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  84. package/dist/formatting/AbstractNumbering.js +22 -0
  85. package/dist/formatting/AbstractNumbering.js.map +1 -1
  86. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  87. package/dist/formatting/NumberingLevel.js +3 -3
  88. package/dist/formatting/NumberingLevel.js.map +1 -1
  89. package/dist/formatting/Style.d.ts +1 -0
  90. package/dist/formatting/Style.d.ts.map +1 -1
  91. package/dist/formatting/Style.js +25 -59
  92. package/dist/formatting/Style.js.map +1 -1
  93. package/dist/formatting/StylesManager.d.ts +1 -0
  94. package/dist/formatting/StylesManager.d.ts.map +1 -1
  95. package/dist/formatting/StylesManager.js +12 -0
  96. package/dist/formatting/StylesManager.js.map +1 -1
  97. package/dist/helpers/CleanupHelper.js.map +1 -1
  98. package/dist/images/ImageOptimizer.d.ts.map +1 -1
  99. package/dist/images/ImageOptimizer.js +0 -1
  100. package/dist/images/ImageOptimizer.js.map +1 -1
  101. package/dist/index.d.ts +1 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js.map +1 -1
  104. package/dist/managers/DrawingManager.d.ts.map +1 -1
  105. package/dist/managers/DrawingManager.js +4 -2
  106. package/dist/managers/DrawingManager.js.map +1 -1
  107. package/dist/types/formatting.d.ts +2 -2
  108. package/dist/types/formatting.d.ts.map +1 -1
  109. package/dist/types/formatting.js.map +1 -1
  110. package/dist/utils/ChangelogGenerator.d.ts +2 -2
  111. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  112. package/dist/utils/ChangelogGenerator.js +4 -5
  113. package/dist/utils/ChangelogGenerator.js.map +1 -1
  114. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  115. package/dist/utils/InMemoryRevisionAcceptor.js +0 -1
  116. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  117. package/dist/utils/RevisionAwareProcessor.d.ts +2 -2
  118. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  119. package/dist/utils/RevisionAwareProcessor.js +2 -2
  120. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  121. package/dist/utils/SelectiveRevisionAcceptor.d.ts +0 -2
  122. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  123. package/dist/utils/SelectiveRevisionAcceptor.js +0 -26
  124. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  125. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  126. package/dist/utils/ShadingResolver.js.map +1 -1
  127. package/dist/utils/acceptRevisions.js +1 -1
  128. package/dist/utils/acceptRevisions.js.map +1 -1
  129. package/dist/utils/stripTrackedChanges.js +1 -1
  130. package/dist/utils/stripTrackedChanges.js.map +1 -1
  131. package/dist/utils/units.d.ts.map +1 -1
  132. package/dist/utils/units.js +1 -1
  133. package/dist/utils/units.js.map +1 -1
  134. package/dist/validation/RevisionAutoFixer.d.ts +2 -1
  135. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  136. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  137. package/package.json +10 -1
  138. package/src/constants/CLAUDE.md +28 -0
  139. package/src/core/CLAUDE.md +4 -0
  140. package/src/core/Document.ts +1888 -137
  141. package/src/core/DocumentContent.ts +0 -11
  142. package/src/core/DocumentGenerator.ts +11 -12
  143. package/src/core/DocumentParser.ts +620 -139
  144. package/src/core/RelationshipManager.ts +6 -3
  145. package/src/elements/Bookmark.ts +39 -4
  146. package/src/elements/BookmarkManager.ts +4 -3
  147. package/src/elements/CLAUDE.md +18 -2
  148. package/src/elements/CommonTypes.ts +35 -8
  149. package/src/elements/Field.ts +1 -1
  150. package/src/elements/Footer.ts +23 -0
  151. package/src/elements/Header.ts +25 -0
  152. package/src/elements/Image.ts +28 -5
  153. package/src/elements/Paragraph.ts +1069 -41
  154. package/src/elements/Revision.ts +0 -19
  155. package/src/elements/RevisionManager.ts +1 -3
  156. package/src/elements/Run.ts +265 -35
  157. package/src/elements/Section.ts +214 -8
  158. package/src/elements/Shape.ts +1 -1
  159. package/src/elements/Table.ts +850 -61
  160. package/src/elements/TableCell.ts +84 -10
  161. package/src/elements/TableGridChange.ts +2 -16
  162. package/src/elements/TableRow.ts +94 -9
  163. package/src/formatting/AbstractNumbering.ts +42 -1
  164. package/src/formatting/CLAUDE.md +4 -0
  165. package/src/formatting/NumberingLevel.ts +11 -7
  166. package/src/formatting/Style.ts +39 -71
  167. package/src/formatting/StylesManager.ts +36 -0
  168. package/src/helpers/CleanupHelper.ts +1 -1
  169. package/src/images/ImageOptimizer.ts +0 -3
  170. package/src/index.ts +1 -1
  171. package/src/managers/DrawingManager.ts +5 -3
  172. package/src/tracking/CLAUDE.md +30 -0
  173. package/src/types/CLAUDE.md +39 -0
  174. package/src/types/formatting.ts +2 -2
  175. package/src/utils/CLAUDE.md +15 -0
  176. package/src/utils/ChangelogGenerator.ts +4 -5
  177. package/src/utils/InMemoryRevisionAcceptor.ts +0 -9
  178. package/src/utils/RevisionAwareProcessor.ts +2 -3
  179. package/src/utils/SelectiveRevisionAcceptor.ts +0 -39
  180. package/src/utils/ShadingResolver.ts +0 -1
  181. package/src/utils/acceptRevisions.ts +1 -1
  182. package/src/utils/stripTrackedChanges.ts +1 -1
  183. package/src/utils/units.ts +2 -1
  184. package/src/validation/CLAUDE.md +40 -0
  185. package/src/validation/RevisionAutoFixer.ts +2 -1
@@ -3,17 +3,14 @@
3
3
  * Provides a simple interface for creating DOCX files without managing ZIP and XML manually
4
4
  */
5
5
 
6
- import { AlternateContent } from '../elements/AlternateContent';
7
6
  import { Bookmark } from '../elements/Bookmark';
8
7
  import { BookmarkManager } from '../elements/BookmarkManager';
9
8
  import { Comment } from '../elements/Comment';
10
- import { CustomXmlBlock } from '../elements/CustomXml';
11
9
  import { PreservedElement } from '../elements/PreservedElement';
12
- import { MathParagraph } from '../elements/MathElement';
13
10
  import { CommentManager } from '../elements/CommentManager';
14
11
  import { Endnote } from '../elements/Endnote';
15
12
  import { EndnoteManager } from '../elements/EndnoteManager';
16
- import { Field } from '../elements/Field';
13
+ import { Field, ComplexField } from '../elements/Field';
17
14
  import { Footnote } from '../elements/Footnote';
18
15
  import { FootnoteManager } from '../elements/FootnoteManager';
19
16
  import { Footer } from '../elements/Footer';
@@ -77,7 +74,7 @@ function getLogger(): ILogger {
77
74
  // cleanupRevisionMetadata - cleanup metadata files after in-memory acceptance
78
75
  import { acceptAllRevisions, cleanupRevisionMetadata } from '../utils/acceptRevisions';
79
76
  // In-memory revision acceptance - used AFTER parsing, allows subsequent modifications
80
- import { acceptRevisionsInMemory, AcceptRevisionsResult } from '../utils/InMemoryRevisionAcceptor';
77
+ import { acceptRevisionsInMemory } from '../utils/InMemoryRevisionAcceptor';
81
78
  import { stripTrackedChanges } from '../utils/stripTrackedChanges';
82
79
  import { diffText, diffHasUnchangedParts } from '../utils/textDiff';
83
80
  import { XMLBuilder } from '../xml/XMLBuilder';
@@ -333,9 +330,6 @@ export class Document {
333
330
  // TOC auto-population setting
334
331
  private autoPopulateTOCs = false;
335
332
 
336
- // TOC field instruction sync setting (default: OFF to preserve original instructions)
337
- private autoSyncTOCStyles = false;
338
-
339
333
  // Flag to skip document.xml regeneration after stripping tracked changes
340
334
  // When true, save() and toBuffer() will preserve the manually cleaned XML
341
335
  private skipDocumentXmlRegeneration = false;
@@ -558,6 +552,211 @@ export class Document {
558
552
  return doc;
559
553
  }
560
554
 
555
+ /**
556
+ * Creates a Document from Markdown text
557
+ *
558
+ * Parses common Markdown syntax and builds a DOCX document. Supports:
559
+ * - Headings (`#` through `######`)
560
+ * - Bold (`**text**`), italic (`*text*`), bold+italic (`***text***`)
561
+ * - Strikethrough (`~~text~~`)
562
+ * - Inline code (`` `code` ``) rendered in Courier New
563
+ * - Links (`[text](url)`)
564
+ * - Bullet lists (`- ` or `* `)
565
+ * - Numbered lists (`1. `)
566
+ * - Tables (`| col | col |` with `| --- |` separator)
567
+ * - Horizontal rules (`---`, `***`, `___`)
568
+ * - Blank lines as paragraph separators
569
+ *
570
+ * @param markdown - Markdown text to convert
571
+ * @param options - Optional document options
572
+ * @returns New Document populated with the parsed content
573
+ *
574
+ * @example
575
+ * ```typescript
576
+ * const doc = Document.fromMarkdown(`
577
+ * # Report Title
578
+ *
579
+ * This is the **introduction** with *emphasis*.
580
+ *
581
+ * ## Data
582
+ *
583
+ * | Name | Value |
584
+ * | --- | --- |
585
+ * | Alpha | 100 |
586
+ *
587
+ * - First item
588
+ * - Second item
589
+ * `);
590
+ * await doc.save('output.docx');
591
+ * ```
592
+ */
593
+ static fromMarkdown(markdown: string, options?: DocumentOptions): Document {
594
+ const doc = Document.create(options);
595
+ const lines = markdown.split('\n');
596
+
597
+ let i = 0;
598
+ while (i < lines.length) {
599
+ const line = lines[i]!;
600
+
601
+ // Skip blank lines
602
+ if (line.trim() === '') {
603
+ i++;
604
+ continue;
605
+ }
606
+
607
+ // Horizontal rule: ---, ***, ___ (3+ of same char, optional spaces)
608
+ if (/^\s{0,3}([-]{3,}|[*]{3,}|[_]{3,})\s*$/.test(line)) {
609
+ doc.addHorizontalRule();
610
+ i++;
611
+ continue;
612
+ }
613
+
614
+ // Heading
615
+ const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line);
616
+ if (headingMatch) {
617
+ const level = headingMatch[1]!.length as 1 | 2 | 3 | 4 | 5 | 6;
618
+ const text = headingMatch[2]!;
619
+ const para = doc.addHeading('', level);
620
+ Document.applyInlineMarkdown(para, text);
621
+ i++;
622
+ continue;
623
+ }
624
+
625
+ // Table (starts with |)
626
+ if (line.trimStart().startsWith('|')) {
627
+ const tableLines: string[] = [];
628
+ while (i < lines.length && lines[i]!.trimStart().startsWith('|')) {
629
+ tableLines.push(lines[i]!);
630
+ i++;
631
+ }
632
+ const table = Document.parseMarkdownTable(tableLines);
633
+ if (table) {
634
+ doc.addTable(table);
635
+ }
636
+ continue;
637
+ }
638
+
639
+ // Bullet list item
640
+ const bulletMatch = /^(\s*)[-*+]\s+(.+)$/.exec(line);
641
+ if (bulletMatch) {
642
+ const text = bulletMatch[2]!;
643
+ const para = doc.createParagraph();
644
+ Document.applyInlineMarkdown(para, text);
645
+ para.setStyle('ListBullet');
646
+ i++;
647
+ continue;
648
+ }
649
+
650
+ // Numbered list item
651
+ const numberMatch = /^(\s*)\d+[.)]\s+(.+)$/.exec(line);
652
+ if (numberMatch) {
653
+ const text = numberMatch[2]!;
654
+ const para = doc.createParagraph();
655
+ Document.applyInlineMarkdown(para, text);
656
+ para.setStyle('ListNumber');
657
+ i++;
658
+ continue;
659
+ }
660
+
661
+ // Regular paragraph (may span multiple non-blank lines)
662
+ const paraLines: string[] = [line];
663
+ i++;
664
+ while (
665
+ i < lines.length &&
666
+ lines[i]!.trim() !== '' &&
667
+ !lines[i]!.trim().startsWith('#') &&
668
+ !lines[i]!.trim().startsWith('|') &&
669
+ !/^\s{0,3}([-]{3,}|[*]{3,}|[_]{3,})\s*$/.test(lines[i]!) &&
670
+ !/^(\s*)[-*+]\s+/.test(lines[i]!) &&
671
+ !/^(\s*)\d+[.)]\s+/.test(lines[i]!)
672
+ ) {
673
+ paraLines.push(lines[i]!);
674
+ i++;
675
+ }
676
+
677
+ const para = doc.createParagraph();
678
+ Document.applyInlineMarkdown(para, paraLines.join(' '));
679
+ }
680
+
681
+ return doc;
682
+ }
683
+
684
+ /**
685
+ * Parses inline Markdown formatting and adds runs to a paragraph.
686
+ * Handles bold, italic, strikethrough, inline code, and links.
687
+ * @internal
688
+ */
689
+ private static applyInlineMarkdown(para: Paragraph, text: string): void {
690
+ // Regex to match inline elements in priority order
691
+ const inlinePattern =
692
+ /(\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*|~~(.+?)~~|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\))/g;
693
+
694
+ let lastIndex = 0;
695
+ let match: RegExpExecArray | null;
696
+
697
+ while ((match = inlinePattern.exec(text)) !== null) {
698
+ // Add plain text before this match
699
+ if (match.index > lastIndex) {
700
+ para.addText(text.slice(lastIndex, match.index));
701
+ }
702
+
703
+ if (match[2] !== undefined) {
704
+ // ***bold+italic***
705
+ para.addText(match[2], { bold: true, italic: true });
706
+ } else if (match[3] !== undefined) {
707
+ // **bold**
708
+ para.addText(match[3], { bold: true });
709
+ } else if (match[4] !== undefined) {
710
+ // *italic*
711
+ para.addText(match[4], { italic: true });
712
+ } else if (match[5] !== undefined) {
713
+ // ~~strikethrough~~
714
+ para.addText(match[5], { strike: true });
715
+ } else if (match[6] !== undefined) {
716
+ // `inline code`
717
+ para.addText(match[6], { font: 'Courier New' });
718
+ } else if (match[7] !== undefined && match[8] !== undefined) {
719
+ // [text](url)
720
+ para.addHyperlink(new Hyperlink({ url: match[8], text: match[7] }));
721
+ }
722
+
723
+ lastIndex = match.index + match[0]!.length;
724
+ }
725
+
726
+ // Add remaining plain text
727
+ if (lastIndex < text.length) {
728
+ para.addText(text.slice(lastIndex));
729
+ }
730
+ }
731
+
732
+ /**
733
+ * Parses Markdown table lines into a Table.
734
+ * @internal
735
+ */
736
+ private static parseMarkdownTable(lines: string[]): Table | null {
737
+ if (lines.length < 2) return null;
738
+
739
+ const parseRow = (line: string): string[] =>
740
+ line
741
+ .replace(/^\|/, '')
742
+ .replace(/\|$/, '')
743
+ .split('|')
744
+ .map((cell) => cell.trim());
745
+
746
+ const rows: string[][] = [];
747
+ for (let i = 0; i < lines.length; i++) {
748
+ const cells = parseRow(lines[i]!);
749
+
750
+ // Skip separator row (| --- | --- |)
751
+ if (cells.every((c) => /^:?-+:?$/.test(c))) continue;
752
+
753
+ rows.push(cells);
754
+ }
755
+
756
+ if (rows.length === 0) return null;
757
+ return Table.fromArray(rows);
758
+ }
759
+
561
760
  /**
562
761
  * Loads an existing Word document from a file path
563
762
  *
@@ -1461,6 +1660,83 @@ export class Document {
1461
1660
  return para;
1462
1661
  }
1463
1662
 
1663
+ /**
1664
+ * Creates a heading paragraph and appends it to the document
1665
+ *
1666
+ * Convenience method that creates a paragraph with the given text and
1667
+ * applies a heading style (Heading1–Heading9). Equivalent to:
1668
+ * ```typescript
1669
+ * doc.createParagraph(text).setStyle(`Heading${level}`);
1670
+ * ```
1671
+ *
1672
+ * @param text - Heading text content
1673
+ * @param level - Heading level 1–9 (default: 1)
1674
+ * @returns The created Paragraph for further customization
1675
+ *
1676
+ * @example
1677
+ * ```typescript
1678
+ * doc.addHeading('Introduction', 1);
1679
+ * doc.addHeading('Background', 2);
1680
+ * doc.addHeading('Methods', 2);
1681
+ * doc.addHeading('Data Collection', 3);
1682
+ * ```
1683
+ */
1684
+ addHeading(text: string, level: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 = 1): Paragraph {
1685
+ return this.createParagraph(text).setStyle(`Heading${level}`);
1686
+ }
1687
+
1688
+ /**
1689
+ * Inserts a page break into the document
1690
+ *
1691
+ * Creates a paragraph containing a page break element and appends it
1692
+ * to the document body. This is the standard way to force a new page
1693
+ * in OOXML (a paragraph with `w:br w:type="page"`).
1694
+ *
1695
+ * @returns The created Paragraph (allows further content after the break)
1696
+ *
1697
+ * @example
1698
+ * ```typescript
1699
+ * doc.addHeading('Chapter 1', 1);
1700
+ * doc.createParagraph('Chapter 1 content...');
1701
+ * doc.addPageBreak();
1702
+ * doc.addHeading('Chapter 2', 1);
1703
+ * ```
1704
+ */
1705
+ addPageBreak(): Paragraph {
1706
+ const para = this.createParagraph();
1707
+ const run = new Run('');
1708
+ run.addBreak('page');
1709
+ para.addRun(run);
1710
+ return para;
1711
+ }
1712
+
1713
+ /**
1714
+ * Inserts a horizontal rule into the document
1715
+ *
1716
+ * Creates an empty paragraph with a bottom border that renders as a
1717
+ * horizontal line. Uses a thin single-line border, which is the standard
1718
+ * OOXML approach for horizontal rules (no dedicated HR element exists).
1719
+ *
1720
+ * @param color - Border color in hex without # (default: 'auto')
1721
+ * @param size - Border thickness in eighths of a point (default: 4, ~0.5pt)
1722
+ * @returns The created Paragraph
1723
+ *
1724
+ * @example
1725
+ * ```typescript
1726
+ * doc.createParagraph('Above the line');
1727
+ * doc.addHorizontalRule();
1728
+ * doc.createParagraph('Below the line');
1729
+ *
1730
+ * // Custom color and thickness
1731
+ * doc.addHorizontalRule('FF0000', 12);
1732
+ * ```
1733
+ */
1734
+ addHorizontalRule(color = 'auto', size = 4): Paragraph {
1735
+ const para = this.createParagraph();
1736
+ para.setBorder({ bottom: { style: 'single', size, color, space: 1 } });
1737
+ return para;
1738
+ }
1739
+
1464
1740
  /**
1465
1741
  * Adds an existing table to the document body
1466
1742
  *
@@ -1545,6 +1821,32 @@ export class Document {
1545
1821
  return table;
1546
1822
  }
1547
1823
 
1824
+ /**
1825
+ * Creates a table from CSV data and appends it to the document
1826
+ *
1827
+ * Parses the CSV string into a table using `Table.fromCSV()` and adds
1828
+ * it to the document body. Handles quoted fields, commas in values,
1829
+ * and other RFC 4180 features.
1830
+ *
1831
+ * @param csv - CSV string to parse
1832
+ * @param delimiter - Field delimiter (default: ',')
1833
+ * @returns The created Table
1834
+ *
1835
+ * @example
1836
+ * ```typescript
1837
+ * doc.createTableFromCSV('Name,Age\nAlice,30\nBob,25');
1838
+ *
1839
+ * // From a TSV string
1840
+ * doc.createTableFromCSV(tsvData, '\t');
1841
+ * ```
1842
+ */
1843
+ createTableFromCSV(csv: string, delimiter = ','): Table {
1844
+ const table = Table.fromCSV(csv, delimiter);
1845
+ table._setStylesManager(this.stylesManager);
1846
+ this.bodyElements.push(table);
1847
+ return table;
1848
+ }
1849
+
1548
1850
  /**
1549
1851
  * Populates all TOCs in document XML
1550
1852
  * Extracted from replaceTableOfContents for reuse
@@ -2194,6 +2496,19 @@ export class Document {
2194
2496
  this.updateContentTypesWithImagesHeadersFootersAndComments();
2195
2497
  }
2196
2498
 
2499
+ /**
2500
+ * Saves the document to a file. Uses atomic write (temp file + rename) for crash safety.
2501
+ * Always call dispose() after saving when done with the document.
2502
+ *
2503
+ * @param filePath - Output file path
2504
+ * @throws {FileOperationError} If the file cannot be written
2505
+ *
2506
+ * @example
2507
+ * ```typescript
2508
+ * await doc.save('output.docx');
2509
+ * doc.dispose();
2510
+ * ```
2511
+ */
2197
2512
  async save(filePath: string): Promise<void> {
2198
2513
  const logger = getLogger();
2199
2514
  logger.info('Saving document', { path: filePath, paragraphs: this.getParagraphCount() });
@@ -2236,7 +2551,7 @@ export class Document {
2236
2551
  const { promises: fs } = await import('fs');
2237
2552
  await fs.unlink(tempPath);
2238
2553
  } catch (cleanupErr) {
2239
- logger.debug('Failed to clean up temp file', { tempPath, error: String(cleanupErr) });
2554
+ logger.warn('Failed to clean up temp file', { tempPath, error: String(cleanupErr) });
2240
2555
  }
2241
2556
  throw error; // Re-throw original error
2242
2557
  } finally {
@@ -2320,6 +2635,123 @@ export class Document {
2320
2635
  }
2321
2636
  }
2322
2637
 
2638
+ /**
2639
+ * Generates the document as a base64-encoded string
2640
+ *
2641
+ * Produces the same DOCX content as `toBuffer()` but encoded as base64.
2642
+ * Useful for embedding in JSON API responses, storing in databases as text,
2643
+ * passing through systems that don't support binary data, or constructing
2644
+ * data URIs (see `toDataUri()`).
2645
+ *
2646
+ * @returns Promise resolving to a base64-encoded string of the DOCX file
2647
+ *
2648
+ * @example
2649
+ * ```typescript
2650
+ * // JSON API response
2651
+ * const base64 = await doc.toBase64();
2652
+ * res.json({ filename: 'report.docx', content: base64 });
2653
+ *
2654
+ * // Store in text-based database field
2655
+ * await db.insert({ docBase64: await doc.toBase64() });
2656
+ * ```
2657
+ */
2658
+ async toBase64(): Promise<string> {
2659
+ const buffer = await this.toBuffer();
2660
+ return buffer.toString('base64');
2661
+ }
2662
+
2663
+ /**
2664
+ * Generates the document as a data URI string
2665
+ *
2666
+ * Returns a complete `data:` URI with the DOCX MIME type and base64-encoded
2667
+ * content. Can be used directly as an `href` for download links, embedded
2668
+ * in HTML, or passed to APIs expecting data URIs.
2669
+ *
2670
+ * @returns Promise resolving to a data URI string
2671
+ *
2672
+ * @example
2673
+ * ```typescript
2674
+ * // HTML download link
2675
+ * const uri = await doc.toDataUri();
2676
+ * const html = `<a href="${uri}" download="report.docx">Download</a>`;
2677
+ *
2678
+ * // Embed in email HTML
2679
+ * const dataUri = await doc.toDataUri();
2680
+ * ```
2681
+ */
2682
+ async toDataUri(): Promise<string> {
2683
+ const base64 = await this.toBase64();
2684
+ return `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${base64}`;
2685
+ }
2686
+
2687
+ /**
2688
+ * Loads a document from a base64-encoded string
2689
+ *
2690
+ * The inverse of `toBase64()`. Creates a Document from a base64 string,
2691
+ * useful for receiving documents from JSON APIs or text-based storage.
2692
+ *
2693
+ * @param base64 - Base64-encoded DOCX content
2694
+ * @param options - Optional document configuration
2695
+ * @returns Promise resolving to a Document instance
2696
+ *
2697
+ * @example
2698
+ * ```typescript
2699
+ * // Receive from API
2700
+ * const doc = await Document.loadFromBase64(apiResponse.content);
2701
+ * console.log(doc.toPlainText());
2702
+ * ```
2703
+ */
2704
+ static async loadFromBase64(base64: string, options?: DocumentOptions): Promise<Document> {
2705
+ const buffer = Buffer.from(base64, 'base64');
2706
+ return Document.loadFromBuffer(buffer, options);
2707
+ }
2708
+
2709
+ /**
2710
+ * Creates an independent deep copy of this document
2711
+ *
2712
+ * Serializes the document to a buffer and reloads it, producing a
2713
+ * completely independent clone with its own body elements, styles,
2714
+ * numbering, images, and ZIP state. Changes to the clone do not
2715
+ * affect the original and vice versa.
2716
+ *
2717
+ * Essential for template-based batch generation: load a template
2718
+ * once, clone it N times, and fill each with different data.
2719
+ *
2720
+ * @returns Promise resolving to a new Document with identical content
2721
+ *
2722
+ * @example
2723
+ * ```typescript
2724
+ * // Template-based batch generation
2725
+ * const template = await Document.load('template.docx');
2726
+ *
2727
+ * for (const record of data) {
2728
+ * const doc = await template.clone();
2729
+ * doc.fillTemplate(record);
2730
+ * await doc.save(`output-${record.id}.docx`);
2731
+ * doc.dispose();
2732
+ * }
2733
+ *
2734
+ * template.dispose();
2735
+ * ```
2736
+ *
2737
+ * @example
2738
+ * ```typescript
2739
+ * // Fork a document for parallel modifications
2740
+ * const original = Document.create();
2741
+ * original.addHeading('Shared Title', 1);
2742
+ *
2743
+ * const version1 = await original.clone();
2744
+ * version1.createParagraph('Version 1 content');
2745
+ *
2746
+ * const version2 = await original.clone();
2747
+ * version2.createParagraph('Version 2 content');
2748
+ * ```
2749
+ */
2750
+ async clone(): Promise<Document> {
2751
+ const buffer = await this.toBuffer();
2752
+ return Document.loadFromBuffer(buffer);
2753
+ }
2754
+
2323
2755
  /**
2324
2756
  * Updates the document.xml file with current paragraphs
2325
2757
  */
@@ -2940,10 +3372,12 @@ export class Document {
2940
3372
  return divCount;
2941
3373
  }
2942
3374
 
3375
+ /** Gets the optimizeForBrowser web setting. */
2943
3376
  getOptimizeForBrowser(): boolean {
2944
3377
  return this._webSettings.optimizeForBrowser;
2945
3378
  }
2946
3379
 
3380
+ /** Sets the optimizeForBrowser web setting. */
2947
3381
  setOptimizeForBrowser(value: boolean): this {
2948
3382
  this._webSettings.optimizeForBrowser = value;
2949
3383
  this._webSettingsModified = true;
@@ -2952,10 +3386,12 @@ export class Document {
2952
3386
  return this;
2953
3387
  }
2954
3388
 
3389
+ /** Gets the allowPNG web setting. */
2955
3390
  getAllowPNG(): boolean {
2956
3391
  return this._webSettings.allowPNG;
2957
3392
  }
2958
3393
 
3394
+ /** Sets the allowPNG web setting. */
2959
3395
  setAllowPNG(value: boolean): this {
2960
3396
  this._webSettings.allowPNG = value;
2961
3397
  this._webSettingsModified = true;
@@ -4475,7 +4911,7 @@ export class Document {
4475
4911
  left: options?.cellMargins?.left ?? 115, // 0.08 inches
4476
4912
  right: options?.cellMargins?.right ?? 115, // 0.08 inches
4477
4913
  };
4478
- const skipSingleCellTables = options?.skipSingleCellTables !== false && !singleCellShading;
4914
+ // Note: skipSingleCellTables option is accepted but not yet implemented
4479
4915
 
4480
4916
  // Statistics
4481
4917
  let tablesProcessed = 0;
@@ -5069,7 +5505,7 @@ export class Document {
5069
5505
  validateNumberingReferences(): number {
5070
5506
  let fixed = 0;
5071
5507
  const existingNumIds = new Set<number>(
5072
- this.numberingManager.getAllInstances().map((i: any) => i.getNumId())
5508
+ this.numberingManager.getAllInstances().map((i) => i.getNumId())
5073
5509
  );
5074
5510
 
5075
5511
  for (const para of this.getAllParagraphs()) {
@@ -5261,6 +5697,24 @@ export class Document {
5261
5697
 
5262
5698
  let docPrId = 1;
5263
5699
 
5700
+ // Collect existing paraIds to avoid collisions when generating new ones
5701
+ const existingParaIds = new Set<string>();
5702
+ const paragraphsNeedingIds: Paragraph[] = [];
5703
+
5704
+ const generateUniqueParaId = (): string => {
5705
+ let id: string;
5706
+ do {
5707
+ // Generate 8-char uppercase hex string matching Word's w14:paraId format
5708
+ // Per ECMA-376, ST_LongHexNumber MaxExclusive is 80000000 (must be < 0x80000000)
5709
+ id = Math.floor(Math.random() * 0x7fffffff + 1)
5710
+ .toString(16)
5711
+ .toUpperCase()
5712
+ .padStart(8, '0');
5713
+ } while (existingParaIds.has(id));
5714
+ existingParaIds.add(id);
5715
+ return id;
5716
+ };
5717
+
5264
5718
  const processParagraph = (para: Paragraph) => {
5265
5719
  // Assign unique IDs to unregistered revisions
5266
5720
  for (const rev of para.getRevisions()) {
@@ -5281,6 +5735,13 @@ export class Document {
5281
5735
  item.getImageElement().setDocPrId(docPrId++);
5282
5736
  }
5283
5737
  }
5738
+
5739
+ // Track existing paraIds and paragraphs that need new ones
5740
+ if (para.formatting.paraId) {
5741
+ existingParaIds.add(para.formatting.paraId);
5742
+ } else {
5743
+ paragraphsNeedingIds.push(para);
5744
+ }
5284
5745
  };
5285
5746
 
5286
5747
  for (const element of this.bodyElements) {
@@ -5296,6 +5757,14 @@ export class Document {
5296
5757
  }
5297
5758
  }
5298
5759
  }
5760
+
5761
+ // Generate w14:paraId and w14:textId for paragraphs that lack them (Word 2010+ requirement)
5762
+ for (const para of paragraphsNeedingIds) {
5763
+ para.formatting.paraId = generateUniqueParaId();
5764
+ if (!para.formatting.textId) {
5765
+ para.formatting.textId = generateUniqueParaId();
5766
+ }
5767
+ }
5299
5768
  }
5300
5769
 
5301
5770
  /**
@@ -5741,6 +6210,103 @@ export class Document {
5741
6210
  return this.numberingManager.createMultiLevelList();
5742
6211
  }
5743
6212
 
6213
+ /**
6214
+ * Creates a bullet list from an array of text items and appends it to the document
6215
+ *
6216
+ * Handles all numbering plumbing internally: creates a bullet list definition,
6217
+ * creates paragraphs, and applies numbering to each one. Supports nested items
6218
+ * via `{ text, level }` objects.
6219
+ *
6220
+ * @param items - Array of strings or `{ text, level }` objects. Strings default to level 0.
6221
+ * @param formatting - Optional run formatting applied to all items
6222
+ * @returns Array of created Paragraphs
6223
+ *
6224
+ * @example
6225
+ * ```typescript
6226
+ * // Simple flat list
6227
+ * doc.addBulletListFromArray(['First item', 'Second item', 'Third item']);
6228
+ *
6229
+ * // Nested list
6230
+ * doc.addBulletListFromArray([
6231
+ * 'Top level',
6232
+ * { text: 'Nested item', level: 1 },
6233
+ * { text: 'Deeper item', level: 2 },
6234
+ * 'Back to top',
6235
+ * ]);
6236
+ *
6237
+ * // With formatting
6238
+ * doc.addBulletListFromArray(['Bold item'], { bold: true });
6239
+ * ```
6240
+ */
6241
+ addBulletListFromArray(
6242
+ items: (string | { text: string; level?: number })[],
6243
+ formatting?: RunFormatting
6244
+ ): Paragraph[] {
6245
+ if (items.length === 0) return [];
6246
+
6247
+ const numId = this.createBulletList();
6248
+ return this.addListItems(numId, items, formatting);
6249
+ }
6250
+
6251
+ /**
6252
+ * Creates a numbered list from an array of text items and appends it to the document
6253
+ *
6254
+ * Handles all numbering plumbing internally: creates a numbered list definition,
6255
+ * creates paragraphs, and applies numbering to each one. Supports nested items
6256
+ * via `{ text, level }` objects.
6257
+ *
6258
+ * @param items - Array of strings or `{ text, level }` objects. Strings default to level 0.
6259
+ * @param formatting - Optional run formatting applied to all items
6260
+ * @returns Array of created Paragraphs
6261
+ *
6262
+ * @example
6263
+ * ```typescript
6264
+ * // Simple numbered list
6265
+ * doc.addNumberedListFromArray(['First', 'Second', 'Third']);
6266
+ *
6267
+ * // Nested numbered list
6268
+ * doc.addNumberedListFromArray([
6269
+ * 'Chapter 1',
6270
+ * { text: 'Section 1.1', level: 1 },
6271
+ * { text: 'Section 1.2', level: 1 },
6272
+ * 'Chapter 2',
6273
+ * ]);
6274
+ * ```
6275
+ */
6276
+ addNumberedListFromArray(
6277
+ items: (string | { text: string; level?: number })[],
6278
+ formatting?: RunFormatting
6279
+ ): Paragraph[] {
6280
+ if (items.length === 0) return [];
6281
+
6282
+ const numId = this.createNumberedList();
6283
+ return this.addListItems(numId, items, formatting);
6284
+ }
6285
+
6286
+ /**
6287
+ * Internal helper that creates list paragraphs from items.
6288
+ * @internal
6289
+ */
6290
+ private addListItems(
6291
+ numId: number,
6292
+ items: (string | { text: string; level?: number })[],
6293
+ formatting?: RunFormatting
6294
+ ): Paragraph[] {
6295
+ const paragraphs: Paragraph[] = [];
6296
+
6297
+ for (const item of items) {
6298
+ const text = typeof item === 'string' ? item : item.text;
6299
+ const level = typeof item === 'string' ? 0 : (item.level ?? 0);
6300
+
6301
+ const para = this.createParagraph();
6302
+ para.addText(text, formatting);
6303
+ para.setNumbering(numId, level);
6304
+ paragraphs.push(para);
6305
+ }
6306
+
6307
+ return paragraphs;
6308
+ }
6309
+
5744
6310
  /**
5745
6311
  * Creates a new numbering instance that restarts numbering for an existing list
5746
6312
  *
@@ -7334,7 +7900,7 @@ export class Document {
7334
7900
  const fillPattern = new RegExp(`(w:fill=["'])${normalizedOld}(["'])`, 'gi');
7335
7901
 
7336
7902
  // Replace all occurrences
7337
- stylesXml = stylesXml.replace(fillPattern, (match, prefix, suffix) => {
7903
+ stylesXml = stylesXml.replace(fillPattern, (_match, prefix, suffix) => {
7338
7904
  updateCount++;
7339
7905
  return `${prefix}${normalizedNew}${suffix}`;
7340
7906
  });
@@ -7343,7 +7909,7 @@ export class Document {
7343
7909
  // Matches: w:color="A5A5A5" within shd elements
7344
7910
  const colorPattern = new RegExp(`(<w:shd[^>]*w:color=["'])${normalizedOld}(["'])`, 'gi');
7345
7911
 
7346
- stylesXml = stylesXml.replace(colorPattern, (match, prefix, suffix) => {
7912
+ stylesXml = stylesXml.replace(colorPattern, (_match, prefix, suffix) => {
7347
7913
  updateCount++;
7348
7914
  return `${prefix}${normalizedNew}${suffix}`;
7349
7915
  });
@@ -7413,27 +7979,6 @@ export class Document {
7413
7979
  * Helper method to process consecutive blank paragraphs
7414
7980
  * @private
7415
7981
  */
7416
- private processConsecutiveBlanks(
7417
- blanks: Paragraph[],
7418
- keepOne: boolean,
7419
- toRemove: Paragraph[]
7420
- ): void {
7421
- if (blanks.length === 0) return;
7422
-
7423
- if (keepOne && blanks.length > 1) {
7424
- // Keep the first one, remove the rest
7425
- for (let i = 1; i < blanks.length; i++) {
7426
- const blank = blanks[i];
7427
- if (blank) {
7428
- toRemove.push(blank);
7429
- }
7430
- }
7431
- } else if (!keepOne) {
7432
- // Remove all
7433
- toRemove.push(...blanks);
7434
- }
7435
- // If keepOne is true and there's only 1 blank, don't remove it
7436
- }
7437
7982
 
7438
7983
  /**
7439
7984
  * Standardizes all bullet list symbols formatting (font, size, bold, color)
@@ -8031,7 +8576,7 @@ export class Document {
8031
8576
  private parseTOCFieldInstruction(instrText: string): number[] {
8032
8577
  const levels = new Set<number>();
8033
8578
  let hasOutlineSwitch = false;
8034
- let hasTableSwitch = false;
8579
+ // hasTableSwitch tracked via \t switch parsing below
8035
8580
 
8036
8581
  // Normalize whitespace and quotes: trim input and replace &quot; with " for consistent parsing
8037
8582
  const normalizedText = instrText.trim().replace(/&quot;/g, '"');
@@ -8065,7 +8610,7 @@ export class Document {
8065
8610
  const tMatches = [...normalizedText.matchAll(tSwitchRegex)];
8066
8611
 
8067
8612
  for (const match of tMatches) {
8068
- hasTableSwitch = true;
8613
+ // \t switch found — heading levels extracted from table style mappings
8069
8614
  const content = (match[1] || '').trim();
8070
8615
  if (!content) continue;
8071
8616
 
@@ -8450,6 +8995,7 @@ export class Document {
8450
8995
  }
8451
8996
 
8452
8997
  // Helper function to extract heading info from a parsed paragraph object
8998
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8453
8999
  const extractHeading = (para: any): void => {
8454
9000
  const pPr = para['w:pPr'];
8455
9001
  if (!pPr?.['w:pStyle']) {
@@ -8584,52 +9130,6 @@ export class Document {
8584
9130
  return headings;
8585
9131
  }
8586
9132
 
8587
- /**
8588
- * Legacy method - searches only bodyElements (doesn't search inside tables)
8589
- * Kept for compatibility but not recommended
8590
- * @deprecated Use findHeadingsForTOCFromXML instead
8591
- */
8592
- private findHeadingsForTOC(
8593
- levels: number[]
8594
- ): { level: number; text: string; bookmark: string }[] {
8595
- const headings: { level: number; text: string; bookmark: string }[] = [];
8596
- const levelSet = new Set(levels);
8597
-
8598
- // Iterate through body elements
8599
- for (const element of this.bodyElements) {
8600
- if (element instanceof Paragraph) {
8601
- const para = element;
8602
- const formatting = para.getFormatting();
8603
-
8604
- // Check if paragraph has a heading style (handle both "Heading1" and "Heading 1")
8605
- if (formatting.style) {
8606
- const styleMatch = /Heading\s*(\d+)/i.exec(formatting.style);
8607
- if (styleMatch?.[1]) {
8608
- const headingLevel = parseInt(styleMatch[1], 10);
8609
-
8610
- // Check if this level should be included in TOC
8611
- if (levelSet.has(headingLevel)) {
8612
- const text = para.getText().trim();
8613
-
8614
- if (text) {
8615
- // Create or get bookmark for this heading
8616
- const bookmark = this.bookmarkManager.createHeadingBookmark(text);
8617
-
8618
- headings.push({
8619
- level: headingLevel,
8620
- text: text,
8621
- bookmark: bookmark.getName(),
8622
- });
8623
- }
8624
- }
8625
- }
8626
- }
8627
- }
8628
- }
8629
-
8630
- return headings;
8631
- }
8632
-
8633
9133
  /**
8634
9134
  * Generates TOC XML structure with populated entries
8635
9135
  *
@@ -10047,14 +10547,100 @@ export class Document {
10047
10547
  }
10048
10548
 
10049
10549
  /**
10050
- * Gets the RevisionManager for track changes operations
10051
- *
10052
- * Provides access to the RevisionManager for managing tracked changes
10053
- * (insertions, deletions, formatting changes, etc.) in the document.
10054
- *
10055
- * @returns The RevisionManager instance managing this document's revisions
10550
+ * Ensures a "Top of the Document" hyperlink exists above every 1x1 table
10551
+ * except the first one. Skips tables that already have a _top link in the
10552
+ * paragraph immediately above them.
10056
10553
  *
10057
- * @example
10554
+ * @param options Optional configuration
10555
+ * @param options.text Display text for inserted links (default: 'Top of the Document')
10556
+ * @param options.formatting Optional RunFormatting for the hyperlink
10557
+ * @returns Number of links inserted
10558
+ */
10559
+ ensureTopLinksAbove1x1Tables(options?: { text?: string; formatting?: RunFormatting }): number {
10560
+ const linkText = options?.text || 'Top of the Document';
10561
+ const formatting = options?.formatting;
10562
+
10563
+ // Ensure _top bookmark exists at document start
10564
+ this.addTopBookmark();
10565
+
10566
+ let insertedCount = 0;
10567
+ let oneByOneCount = 0;
10568
+
10569
+ // Work directly with bodyElements array since indices shift on insert
10570
+ let i = 0;
10571
+ while (i < this.bodyElements.length) {
10572
+ const element = this.bodyElements[i];
10573
+
10574
+ if (
10575
+ element instanceof Table &&
10576
+ element.getRowCount() === 1 &&
10577
+ element.getColumnCount() === 1
10578
+ ) {
10579
+ oneByOneCount++;
10580
+
10581
+ // Skip the first 1x1 table
10582
+ if (oneByOneCount > 1) {
10583
+ // Check if paragraph immediately before has a _top link
10584
+ const prevElement = i > 0 ? this.bodyElements[i - 1] : undefined;
10585
+ const hasLink =
10586
+ prevElement instanceof Paragraph && this._paragraphHasTopLink(prevElement);
10587
+
10588
+ if (!hasLink) {
10589
+ // Insert a paragraph with _top hyperlink before this table
10590
+ const para = new Paragraph();
10591
+ const link = Hyperlink.createInternal('_top', linkText, {
10592
+ color: '0000FF',
10593
+ underline: 'single',
10594
+ ...formatting,
10595
+ });
10596
+ para.addHyperlink(link);
10597
+ this.bodyElements.splice(i, 0, para);
10598
+ insertedCount++;
10599
+ i++; // Skip past the inserted paragraph
10600
+ }
10601
+ }
10602
+ }
10603
+
10604
+ i++;
10605
+ }
10606
+
10607
+ return insertedCount;
10608
+ }
10609
+
10610
+ /**
10611
+ * Checks whether a paragraph contains a hyperlink with `_top` anchor.
10612
+ * Handles inline Hyperlink elements, ComplexField HYPERLINK _top,
10613
+ * and PreservedElement raw XML passthrough (loaded docs).
10614
+ * @internal
10615
+ */
10616
+ private _paragraphHasTopLink(paragraph: Paragraph): boolean {
10617
+ for (const item of paragraph.getContent()) {
10618
+ if (item instanceof Hyperlink && item.getAnchor() === '_top') {
10619
+ return true;
10620
+ }
10621
+ if (item instanceof ComplexField && item.isHyperlinkField()) {
10622
+ const parsed = item.getParsedHyperlink();
10623
+ if (parsed?.anchor === '_top') {
10624
+ return true;
10625
+ }
10626
+ }
10627
+ // Loaded docs may have hyperlinks as PreservedElement (raw XML passthrough)
10628
+ if (item instanceof PreservedElement && item.getRawXml().includes('w:anchor="_top"')) {
10629
+ return true;
10630
+ }
10631
+ }
10632
+ return false;
10633
+ }
10634
+
10635
+ /**
10636
+ * Gets the RevisionManager for track changes operations
10637
+ *
10638
+ * Provides access to the RevisionManager for managing tracked changes
10639
+ * (insertions, deletions, formatting changes, etc.) in the document.
10640
+ *
10641
+ * @returns The RevisionManager instance managing this document's revisions
10642
+ *
10643
+ * @example
10058
10644
  * ```typescript
10059
10645
  * const revManager = doc.getRevisionManager();
10060
10646
  * const stats = revManager.getStats();
@@ -10731,6 +11317,7 @@ export class Document {
10731
11317
  * @param element - Element to bind
10732
11318
  * @internal
10733
11319
  */
11320
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10734
11321
  private bindTrackingToElement(element: any): void {
10735
11322
  // Set tracking context on element if it supports it
10736
11323
  if (element && typeof element._setTrackingContext === 'function') {
@@ -10970,6 +11557,7 @@ export class Document {
10970
11557
  this._settingsModified = true;
10971
11558
  }
10972
11559
 
11560
+ /** Gets whether even/odd page headers and footers are enabled (w:evenAndOddHeaders). */
10973
11561
  getEvenAndOddHeaders(): boolean {
10974
11562
  return this._evenAndOddHeaders ?? false;
10975
11563
  }
@@ -11063,6 +11651,72 @@ export class Document {
11063
11651
  this._modifiedBooleanSettings.add('defaultTabStop');
11064
11652
  }
11065
11653
 
11654
+ /**
11655
+ * Sets the default document font by updating the Normal style
11656
+ *
11657
+ * All unstyled text inherits from the Normal style, so this effectively
11658
+ * sets the font for the entire document. Optionally sets the font size too.
11659
+ *
11660
+ * @param fontName - Font family name (e.g., 'Calibri', 'Times New Roman', 'Arial')
11661
+ * @param sizeInPoints - Optional font size in points (e.g., 11, 12, 14)
11662
+ * @returns This document for chaining
11663
+ *
11664
+ * @example
11665
+ * ```typescript
11666
+ * const doc = Document.create();
11667
+ * doc.setDefaultFont('Times New Roman', 12);
11668
+ * doc.createParagraph('This text will be in Times New Roman 12pt');
11669
+ * ```
11670
+ */
11671
+ setDefaultFont(fontName: string, sizeInPoints?: number): this {
11672
+ let normalStyle = this.stylesManager.getStyle('Normal');
11673
+ if (!normalStyle) {
11674
+ normalStyle = new Style({
11675
+ styleId: 'Normal',
11676
+ name: 'Normal',
11677
+ type: 'paragraph',
11678
+ isDefault: true,
11679
+ });
11680
+ this.stylesManager.addStyle(normalStyle);
11681
+ }
11682
+
11683
+ const existing = normalStyle.getRunFormatting() ?? {};
11684
+ const updated: RunFormatting = { ...existing, font: fontName };
11685
+ if (sizeInPoints !== undefined) {
11686
+ updated.size = sizeInPoints;
11687
+ }
11688
+ normalStyle.setRunFormatting(updated);
11689
+ return this;
11690
+ }
11691
+
11692
+ /**
11693
+ * Sets the default document font size by updating the Normal style
11694
+ *
11695
+ * @param sizeInPoints - Font size in points (e.g., 10, 11, 12, 14)
11696
+ * @returns This document for chaining
11697
+ *
11698
+ * @example
11699
+ * ```typescript
11700
+ * doc.setDefaultFontSize(14);
11701
+ * ```
11702
+ */
11703
+ setDefaultFontSize(sizeInPoints: number): this {
11704
+ let normalStyle = this.stylesManager.getStyle('Normal');
11705
+ if (!normalStyle) {
11706
+ normalStyle = new Style({
11707
+ styleId: 'Normal',
11708
+ name: 'Normal',
11709
+ type: 'paragraph',
11710
+ isDefault: true,
11711
+ });
11712
+ this.stylesManager.addStyle(normalStyle);
11713
+ }
11714
+
11715
+ const existing = normalStyle.getRunFormatting() ?? {};
11716
+ normalStyle.setRunFormatting({ ...existing, size: sizeInPoints });
11717
+ return this;
11718
+ }
11719
+
11066
11720
  /**
11067
11721
  * Gets whether fields are updated on document open (w:updateFields)
11068
11722
  */
@@ -11546,19 +12200,31 @@ export class Document {
11546
12200
  return this.commentManager.getAllComments();
11547
12201
  }
11548
12202
 
12203
+ /** Returns the footnote manager for advanced footnote operations. */
11549
12204
  getFootnoteManager(): FootnoteManager {
11550
12205
  return this.footnoteManager;
11551
12206
  }
11552
12207
 
12208
+ /** Returns the endnote manager for advanced endnote operations. */
11553
12209
  getEndnoteManager(): EndnoteManager {
11554
12210
  return this.endnoteManager;
11555
12211
  }
11556
12212
 
12213
+ /**
12214
+ * Creates a new footnote with the given text and adds a reference in the document.
12215
+ * @param text - The footnote text content
12216
+ * @returns The created Footnote object
12217
+ */
11557
12218
  createFootnote(text: string): Footnote {
11558
12219
  this._footnotesModified = true;
11559
12220
  return this.footnoteManager.createFootnote(text);
11560
12221
  }
11561
12222
 
12223
+ /**
12224
+ * Creates a new endnote with the given text and adds a reference in the document.
12225
+ * @param text - The endnote text content
12226
+ * @returns The created Endnote object
12227
+ */
11562
12228
  createEndnote(text: string): Endnote {
11563
12229
  this._endnotesModified = true;
11564
12230
  return this.endnoteManager.createEndnote(text);
@@ -12332,6 +12998,34 @@ export class Document {
12332
12998
  return count;
12333
12999
  }
12334
13000
 
13001
+ /**
13002
+ * Removes all body content from the document
13003
+ *
13004
+ * Clears all paragraphs, tables, and other body elements while
13005
+ * preserving the document shell (styles, numbering, settings,
13006
+ * properties, headers, footers). The document remains valid and
13007
+ * new content can be added after clearing.
13008
+ *
13009
+ * @returns This document for chaining
13010
+ *
13011
+ * @example
13012
+ * ```typescript
13013
+ * // Clear and rebuild content
13014
+ * doc.clear();
13015
+ * doc.addHeading('Fresh Start', 1);
13016
+ * doc.createParagraph('New content here.');
13017
+ *
13018
+ * // Use as a template reset
13019
+ * const template = await Document.load('template.docx');
13020
+ * template.clear();
13021
+ * // Styles and settings preserved, content gone
13022
+ * ```
13023
+ */
13024
+ clear(): this {
13025
+ this.bodyElements = [];
13026
+ return this;
13027
+ }
13028
+
12335
13029
  /**
12336
13030
  * Cleans up resources and clears all managers
12337
13031
  * Call this after saving in long-running processes to free memory
@@ -13808,6 +14502,153 @@ export class Document {
13808
14502
  return trackChanges ? { count, revisions } : { count };
13809
14503
  }
13810
14504
 
14505
+ /**
14506
+ * Fills template placeholders with values using cross-run replacement
14507
+ *
14508
+ * Replaces `{{key}}` placeholders throughout the document (paragraphs
14509
+ * and table cells) with the corresponding values from the data object.
14510
+ * Uses cross-run matching, so placeholders that Word has fragmented across
14511
+ * multiple runs (e.g., `{{` in one run, `name` in another, `}}` in a third)
14512
+ * are found and replaced correctly.
14513
+ *
14514
+ * The replacement text inherits the formatting of the first run in the
14515
+ * matched placeholder. Delimiter style can be customized.
14516
+ *
14517
+ * @param data - Key-value pairs where keys match placeholder names
14518
+ * @param options - Template options
14519
+ * @param options.delimiters - Custom open/close delimiters (default: `['{{', '}}']`)
14520
+ * @returns Total number of replacements made
14521
+ *
14522
+ * @example
14523
+ * ```typescript
14524
+ * // Document contains: "Dear {{name}}, your order {{orderId}} is ready."
14525
+ * const count = doc.fillTemplate({
14526
+ * name: 'Alice',
14527
+ * orderId: 'ORD-12345',
14528
+ * });
14529
+ * // Result: "Dear Alice, your order ORD-12345 is ready."
14530
+ * ```
14531
+ *
14532
+ * @example
14533
+ * ```typescript
14534
+ * // Custom delimiters
14535
+ * doc.fillTemplate(
14536
+ * { title: 'Report', date: '2024-01-15' },
14537
+ * { delimiters: ['<<', '>>'] }
14538
+ * );
14539
+ * ```
14540
+ */
14541
+ fillTemplate(data: Record<string, string>, options?: { delimiters?: [string, string] }): number {
14542
+ const [open, close] = options?.delimiters ?? ['{{', '}}'];
14543
+ let totalCount = 0;
14544
+
14545
+ const allParagraphs = this.getAllParagraphs();
14546
+ for (const [key, value] of Object.entries(data)) {
14547
+ const placeholder = `${open}${key}${close}`;
14548
+ for (const para of allParagraphs) {
14549
+ totalCount += para.replaceTextCrossRun(placeholder, value);
14550
+ }
14551
+ }
14552
+
14553
+ return totalCount;
14554
+ }
14555
+
14556
+ /**
14557
+ * Finds all occurrences of text and applies highlight color
14558
+ *
14559
+ * Searches across run boundaries (handles Word-fragmented text) and
14560
+ * applies character highlight formatting to every match. Uses
14561
+ * `findTextCrossRun` + `applyFormattingToRange` internally.
14562
+ *
14563
+ * @param text - Text to search for
14564
+ * @param color - Highlight color (default: 'yellow')
14565
+ * @param options - Search options
14566
+ * @param options.caseSensitive - Match case exactly (default: false)
14567
+ * @returns Number of matches highlighted
14568
+ *
14569
+ * @example
14570
+ * ```typescript
14571
+ * // Highlight all occurrences of "important" in yellow
14572
+ * doc.findAndHighlight('important');
14573
+ *
14574
+ * // Red highlight, case-sensitive
14575
+ * doc.findAndHighlight('ERROR', 'red', { caseSensitive: true });
14576
+ * ```
14577
+ */
14578
+ findAndHighlight(
14579
+ text: string,
14580
+ color:
14581
+ | 'yellow'
14582
+ | 'green'
14583
+ | 'cyan'
14584
+ | 'magenta'
14585
+ | 'blue'
14586
+ | 'red'
14587
+ | 'darkBlue'
14588
+ | 'darkCyan'
14589
+ | 'darkGreen'
14590
+ | 'darkMagenta'
14591
+ | 'darkRed'
14592
+ | 'darkYellow'
14593
+ | 'darkGray'
14594
+ | 'lightGray'
14595
+ | 'black' = 'yellow',
14596
+ options?: { caseSensitive?: boolean }
14597
+ ): number {
14598
+ return this.findAndFormat(text, { highlight: color }, options);
14599
+ }
14600
+
14601
+ /**
14602
+ * Finds all occurrences of text and applies formatting
14603
+ *
14604
+ * Searches across run boundaries (handles Word-fragmented text) and
14605
+ * applies the specified run formatting to every match. This is the
14606
+ * general-purpose version of `findAndHighlight()`.
14607
+ *
14608
+ * @param text - Text to search for
14609
+ * @param formatting - RunFormatting to apply to matches
14610
+ * @param options - Search options
14611
+ * @param options.caseSensitive - Match case exactly (default: false)
14612
+ * @returns Number of matches formatted
14613
+ *
14614
+ * @example
14615
+ * ```typescript
14616
+ * // Bold all occurrences of "warning"
14617
+ * doc.findAndFormat('warning', { bold: true, color: 'FF0000' });
14618
+ *
14619
+ * // Strikethrough deprecated terms
14620
+ * doc.findAndFormat('deprecated', { strike: true, color: '888888' });
14621
+ *
14622
+ * // Apply multiple styles to a term
14623
+ * doc.findAndFormat('critical', {
14624
+ * bold: true,
14625
+ * highlight: 'red',
14626
+ * underline: 'single',
14627
+ * });
14628
+ * ```
14629
+ */
14630
+ findAndFormat(
14631
+ text: string,
14632
+ formatting: Partial<RunFormatting>,
14633
+ options?: { caseSensitive?: boolean }
14634
+ ): number {
14635
+ let totalMatches = 0;
14636
+
14637
+ for (const para of this.getAllParagraphs()) {
14638
+ const matches = para.findTextCrossRun(text, options);
14639
+
14640
+ // Apply formatting in reverse order to preserve offsets
14641
+ for (let i = matches.length - 1; i >= 0; i--) {
14642
+ const match = matches[i]!;
14643
+ para.applyFormattingToRange(match.offset, match.offset + match.text.length, formatting);
14644
+ }
14645
+
14646
+ totalMatches += matches.length;
14647
+ }
14648
+
14649
+ return totalMatches;
14650
+ }
14651
+
13811
14652
  /**
13812
14653
  * Gets the total word count in the document
13813
14654
  *
@@ -13930,73 +14771,867 @@ export class Document {
13930
14771
  }
13931
14772
 
13932
14773
  /**
13933
- * Removes a paragraph from the document
13934
- * @param paragraphOrIndex - The paragraph object or its index
13935
- * @returns True if the paragraph was removed, false otherwise
14774
+ * Returns comprehensive document statistics in a single call
14775
+ *
14776
+ * Aggregates word count, character counts, element counts, and structural
14777
+ * metrics. More efficient than calling individual methods since shared
14778
+ * data (like the paragraph list) is computed once.
14779
+ *
14780
+ * @returns Object with all document metrics
14781
+ *
14782
+ * @example
14783
+ * ```typescript
14784
+ * const stats = doc.getStatistics();
14785
+ * console.log(`Words: ${stats.words}, Pages (est): ${stats.paragraphs}`);
14786
+ * console.log(`Tables: ${stats.tables}, Images: ${stats.images}`);
14787
+ * ```
13936
14788
  */
13937
- removeParagraph(paragraphOrIndex: Paragraph | number): boolean {
13938
- let index: number;
14789
+ getStatistics(): {
14790
+ words: number;
14791
+ characters: number;
14792
+ charactersNoSpaces: number;
14793
+ paragraphs: number;
14794
+ tables: number;
14795
+ images: number;
14796
+ headings: number;
14797
+ lists: number;
14798
+ hyperlinks: number;
14799
+ bookmarks: number;
14800
+ footnotes: number;
14801
+ endnotes: number;
14802
+ comments: number;
14803
+ sections: number;
14804
+ } {
14805
+ const allParagraphs = this.getAllParagraphs();
14806
+ const tables = this.getTables();
13939
14807
 
13940
- if (typeof paragraphOrIndex === 'number') {
13941
- index = paragraphOrIndex;
13942
- } else {
13943
- // Find the index of the paragraph
13944
- index = this.bodyElements.indexOf(paragraphOrIndex);
14808
+ let words = 0;
14809
+ let characters = 0;
14810
+ let charactersNoSpaces = 0;
14811
+ let headings = 0;
14812
+ let lists = 0;
14813
+ const counted = new Set<Paragraph>();
14814
+
14815
+ for (const para of allParagraphs) {
14816
+ if (counted.has(para)) continue;
14817
+ counted.add(para);
14818
+
14819
+ const text = para.getText();
14820
+ characters += text.length;
14821
+ charactersNoSpaces += text.replace(/\s/g, '').length;
14822
+
14823
+ const trimmed = text.trim();
14824
+ if (trimmed) {
14825
+ words += trimmed.split(/\s+/).filter((w) => w.length > 0).length;
14826
+ }
14827
+
14828
+ if (para.detectHeadingLevel() !== null) headings++;
14829
+ if (para.hasNumbering()) lists++;
13945
14830
  }
13946
14831
 
13947
- if (index >= 0 && index < this.bodyElements.length) {
13948
- const element = this.bodyElements[index];
13949
- if (element instanceof Paragraph) {
13950
- // When tracking enabled, wrap content in w:del instead of removing
13951
- if (this.trackChangesEnabled && this.trackingContext.isEnabled()) {
13952
- const runs = element.getRuns();
13953
- if (runs.length > 0) {
13954
- const author = this.trackingContext.getAuthor();
13955
- const deletion = Revision.createDeletion(author, runs);
13956
- this.trackingContext.getRevisionManager().register(deletion);
13957
- element.addRevision(deletion);
14832
+ // Count table cell text too (for tables not traversed via getAllParagraphs)
14833
+ for (const table of tables) {
14834
+ for (const row of table.getRows()) {
14835
+ for (const cell of row.getCells()) {
14836
+ for (const para of cell.getParagraphs()) {
14837
+ if (counted.has(para)) continue;
14838
+ counted.add(para);
14839
+ const text = para.getText();
14840
+ characters += text.length;
14841
+ charactersNoSpaces += text.replace(/\s/g, '').length;
14842
+ const trimmed = text.trim();
14843
+ if (trimmed) {
14844
+ words += trimmed.split(/\s+/).filter((w) => w.length > 0).length;
14845
+ }
13958
14846
  }
13959
- return true;
13960
14847
  }
13961
- this.bodyElements.splice(index, 1);
13962
- return true;
13963
14848
  }
13964
14849
  }
13965
14850
 
13966
- return false;
14851
+ return {
14852
+ words,
14853
+ characters,
14854
+ charactersNoSpaces,
14855
+ paragraphs: allParagraphs.length,
14856
+ tables: tables.length,
14857
+ images: this.imageManager.getAllImages().length,
14858
+ headings,
14859
+ lists,
14860
+ hyperlinks: this.getHyperlinks().length,
14861
+ bookmarks: this.bookmarkManager.getAllBookmarks().length,
14862
+ footnotes: this.footnoteManager.getAllFootnotes().length,
14863
+ endnotes: this.endnoteManager.getAllEndnotes().length,
14864
+ comments: this.commentManager.getAllComments().length,
14865
+ sections: 1, // Base section; multi-section docs add via paragraph section properties
14866
+ };
13967
14867
  }
13968
14868
 
13969
14869
  /**
13970
- * Removes a table from the document
13971
- * @param tableOrIndex - The table object or its index
13972
- * @returns True if the table was removed, false otherwise
14870
+ * Iterates over top-level paragraphs in the document body (not inside tables)
14871
+ *
14872
+ * Calls the callback for each Paragraph that is a direct child of the body.
14873
+ * Paragraphs inside table cells are NOT included — use `getAllParagraphs()`
14874
+ * or `walkElements()` for those. Supports early termination by returning `false`.
14875
+ *
14876
+ * @param callback - Function called for each paragraph. Return `false` to stop.
14877
+ * @returns Number of paragraphs visited
14878
+ *
14879
+ * @example
14880
+ * ```typescript
14881
+ * // Bold all top-level paragraphs
14882
+ * doc.forEachParagraph((para) => {
14883
+ * para.getRuns().forEach(r => r.setBold(true));
14884
+ * });
14885
+ *
14886
+ * // Find first paragraph matching criteria
14887
+ * let found: Paragraph | undefined;
14888
+ * doc.forEachParagraph((para) => {
14889
+ * if (para.getText().includes('Summary')) {
14890
+ * found = para;
14891
+ * return false;
14892
+ * }
14893
+ * });
14894
+ * ```
13973
14895
  */
13974
- removeTable(tableOrIndex: Table | number): boolean {
13975
- let index: number;
13976
-
13977
- if (typeof tableOrIndex === 'number') {
13978
- // If number provided, find the nth table
13979
- const tables = this.getTables();
13980
- if (tableOrIndex >= 0 && tableOrIndex < tables.length) {
13981
- const table = tables[tableOrIndex];
13982
- if (!table) return false;
13983
- index = this.bodyElements.indexOf(table);
13984
- } else {
13985
- return false;
14896
+ forEachParagraph(callback: (paragraph: Paragraph, index: number) => void | false): number {
14897
+ let count = 0;
14898
+ let paraIndex = 0;
14899
+ for (const element of this.bodyElements) {
14900
+ if (element instanceof Paragraph) {
14901
+ const result = callback(element, paraIndex);
14902
+ count++;
14903
+ paraIndex++;
14904
+ if (result === false) break;
13986
14905
  }
13987
- } else {
13988
- // Find the index of the table
13989
- index = this.bodyElements.indexOf(tableOrIndex);
13990
14906
  }
14907
+ return count;
14908
+ }
13991
14909
 
13992
- if (index >= 0 && index < this.bodyElements.length) {
13993
- const element = this.bodyElements[index];
14910
+ /**
14911
+ * Iterates over top-level tables in the document body
14912
+ *
14913
+ * Calls the callback for each Table that is a direct child of the body.
14914
+ * Supports early termination by returning `false`.
14915
+ *
14916
+ * @param callback - Function called for each table. Return `false` to stop.
14917
+ * @returns Number of tables visited
14918
+ *
14919
+ * @example
14920
+ * ```typescript
14921
+ * // Remove empty rows from all tables
14922
+ * doc.forEachTable((table) => {
14923
+ * table.removeEmptyRows();
14924
+ * });
14925
+ *
14926
+ * // Find first table with more than 5 rows
14927
+ * let bigTable: Table | undefined;
14928
+ * doc.forEachTable((table) => {
14929
+ * if (table.getRowCount() > 5) {
14930
+ * bigTable = table;
14931
+ * return false;
14932
+ * }
14933
+ * });
14934
+ * ```
14935
+ */
14936
+ forEachTable(callback: (table: Table, index: number) => void | false): number {
14937
+ let count = 0;
14938
+ let tableIndex = 0;
14939
+ for (const element of this.bodyElements) {
13994
14940
  if (element instanceof Table) {
13995
- this.bodyElements.splice(index, 1);
13996
- return true;
14941
+ const result = callback(element, tableIndex);
14942
+ count++;
14943
+ tableIndex++;
14944
+ if (result === false) break;
13997
14945
  }
13998
14946
  }
13999
-
14947
+ return count;
14948
+ }
14949
+
14950
+ /**
14951
+ * Extracts all text content from the document as a plain string.
14952
+ * Concatenates text from all paragraphs (including those in tables),
14953
+ * separated by newlines.
14954
+ *
14955
+ * @param separator - String to insert between paragraphs (default: '\n')
14956
+ * @returns Plain text content of the entire document
14957
+ *
14958
+ * @example
14959
+ * ```typescript
14960
+ * const text = doc.toPlainText();
14961
+ * console.log(text);
14962
+ *
14963
+ * // With custom separator
14964
+ * const singleLine = doc.toPlainText(' ');
14965
+ * ```
14966
+ */
14967
+ toPlainText(separator = '\n'): string {
14968
+ const paragraphs = this.getAllParagraphs();
14969
+ return paragraphs.map((p) => p.getText()).join(separator);
14970
+ }
14971
+
14972
+ /**
14973
+ * Converts the document to Markdown format
14974
+ *
14975
+ * Iterates body elements in order and converts them to Markdown syntax:
14976
+ * - Headings → `#` / `##` / `###` etc.
14977
+ * - Bold/italic runs → `**bold**` / `*italic*`
14978
+ * - Hyperlinks → `[text](url)`
14979
+ * - Tables → pipe-delimited Markdown tables with alignment row
14980
+ * - Numbered/bulleted lists → `1.` / `-` prefixes
14981
+ * - Regular paragraphs → plain text with blank lines between
14982
+ *
14983
+ * Useful for AI/LLM pipelines, content migration, documentation
14984
+ * generation, and plain-text extraction with structure preserved.
14985
+ *
14986
+ * @returns Markdown string representation of the document
14987
+ *
14988
+ * @example
14989
+ * ```typescript
14990
+ * const md = doc.toMarkdown();
14991
+ * console.log(md);
14992
+ * // # Document Title
14993
+ * //
14994
+ * // Opening paragraph text.
14995
+ * //
14996
+ * // ## Section 1
14997
+ * //
14998
+ * // | Name | Age |
14999
+ * // | --- | --- |
15000
+ * // | Alice | 30 |
15001
+ * ```
15002
+ */
15003
+ toMarkdown(): string {
15004
+ const lines: string[] = [];
15005
+
15006
+ for (const element of this.bodyElements) {
15007
+ if (element instanceof Paragraph) {
15008
+ const mdLine = this.paragraphToMarkdown(element);
15009
+ if (mdLine !== null) {
15010
+ lines.push(mdLine);
15011
+ lines.push('');
15012
+ }
15013
+ } else if (element instanceof Table) {
15014
+ lines.push(...this.tableToMarkdown(element));
15015
+ lines.push('');
15016
+ }
15017
+ // Other element types (SDT, AlternateContent, etc.) are skipped
15018
+ }
15019
+
15020
+ // Remove trailing blank line
15021
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
15022
+ lines.pop();
15023
+ }
15024
+
15025
+ return lines.join('\n');
15026
+ }
15027
+
15028
+ /**
15029
+ * Converts a paragraph to a Markdown line.
15030
+ * @internal
15031
+ */
15032
+ private paragraphToMarkdown(para: Paragraph): string | null {
15033
+ const text = this.paragraphContentToMarkdown(para);
15034
+ if (!text && !para.hasNumbering()) return null;
15035
+
15036
+ // Headings
15037
+ const headingLevel = para.detectHeadingLevel();
15038
+ if (headingLevel !== null && headingLevel >= 1 && headingLevel <= 6) {
15039
+ return '#'.repeat(headingLevel) + ' ' + text;
15040
+ }
15041
+
15042
+ // Numbered/bulleted lists
15043
+ if (para.hasNumbering()) {
15044
+ const style = para.getStyle();
15045
+ const isBullet =
15046
+ style?.toLowerCase().includes('bullet') || style?.toLowerCase().includes('list bullet');
15047
+ return isBullet ? `- ${text}` : `1. ${text}`;
15048
+ }
15049
+
15050
+ return text;
15051
+ }
15052
+
15053
+ /**
15054
+ * Converts paragraph inline content to Markdown with formatting.
15055
+ * @internal
15056
+ */
15057
+ private paragraphContentToMarkdown(para: Paragraph): string {
15058
+ const parts: string[] = [];
15059
+
15060
+ for (const item of para.getContent()) {
15061
+ if (item instanceof Run) {
15062
+ const runText = item.getText();
15063
+ if (!runText) continue;
15064
+
15065
+ const fmt = item.getFormatting();
15066
+ let md = runText;
15067
+
15068
+ // Apply inline formatting (bold + italic combined)
15069
+ if (fmt.bold && fmt.italic) {
15070
+ md = `***${md}***`;
15071
+ } else if (fmt.bold) {
15072
+ md = `**${md}**`;
15073
+ } else if (fmt.italic) {
15074
+ md = `*${md}*`;
15075
+ }
15076
+
15077
+ if (fmt.strike) {
15078
+ md = `~~${md}~~`;
15079
+ }
15080
+
15081
+ // Inline code (monospace font detection)
15082
+ if (
15083
+ fmt.font &&
15084
+ /^(courier|consolas|monaco|menlo|source code|fira code|jetbrains mono)/i.test(fmt.font)
15085
+ ) {
15086
+ md = `\`${runText}\``;
15087
+ }
15088
+
15089
+ parts.push(md);
15090
+ } else if (item instanceof Hyperlink) {
15091
+ const url = item.getUrl() || '';
15092
+ const linkText = item.getText() || url;
15093
+ parts.push(`[${linkText}](${url})`);
15094
+ }
15095
+ // Revisions, fields, shapes, etc. — extract text if possible
15096
+ }
15097
+
15098
+ return parts.join('');
15099
+ }
15100
+
15101
+ /**
15102
+ * Converts a table to Markdown table lines.
15103
+ * @internal
15104
+ */
15105
+ private tableToMarkdown(table: Table): string[] {
15106
+ const data = table.toArray();
15107
+ if (data.length === 0) return [];
15108
+
15109
+ const colCount = Math.max(...data.map((row) => row.length));
15110
+ if (colCount === 0) return [];
15111
+
15112
+ // Normalize all rows to same column count
15113
+ const normalized = data.map((row) => {
15114
+ const padded = [...row];
15115
+ while (padded.length < colCount) padded.push('');
15116
+ // Escape pipes and normalize whitespace in cell text
15117
+ return padded.map((cell) => cell.replace(/\|/g, '\\|').replace(/\n/g, ' ').trim());
15118
+ });
15119
+
15120
+ const lines: string[] = [];
15121
+
15122
+ // Header row
15123
+ lines.push('| ' + normalized[0]!.join(' | ') + ' |');
15124
+
15125
+ // Separator row
15126
+ lines.push('| ' + normalized[0]!.map(() => '---').join(' | ') + ' |');
15127
+
15128
+ // Data rows
15129
+ for (let i = 1; i < normalized.length; i++) {
15130
+ lines.push('| ' + normalized[i]!.join(' | ') + ' |');
15131
+ }
15132
+
15133
+ return lines;
15134
+ }
15135
+
15136
+ /**
15137
+ * Converts the document to an HTML string
15138
+ *
15139
+ * Iterates body elements and renders them as semantic HTML:
15140
+ * - Headings → `<h1>` through `<h6>`
15141
+ * - Bold → `<strong>`, italic → `<em>`, strikethrough → `<s>`
15142
+ * - Inline code (monospace fonts) → `<code>`
15143
+ * - Hyperlinks → `<a href="...">`
15144
+ * - Tables → `<table>` with `<thead>` / `<tbody>`
15145
+ * - Bullet lists → `<ul><li>`, numbered lists → `<ol><li>`
15146
+ * - Regular paragraphs → `<p>`
15147
+ *
15148
+ * Useful for web display, email bodies, CMS import, and rich-text previews.
15149
+ *
15150
+ * @param options - Output options
15151
+ * @param options.wrapInDocument - Wrap in `<!DOCTYPE html>` with head/body (default: false)
15152
+ * @param options.title - Document title for the `<title>` tag (only when wrapInDocument is true)
15153
+ * @returns HTML string
15154
+ *
15155
+ * @example
15156
+ * ```typescript
15157
+ * // Fragment for embedding
15158
+ * const html = doc.toHTML();
15159
+ *
15160
+ * // Full HTML document
15161
+ * const page = doc.toHTML({ wrapInDocument: true, title: 'My Report' });
15162
+ * ```
15163
+ */
15164
+ toHTML(options?: { wrapInDocument?: boolean; title?: string }): string {
15165
+ const parts: string[] = [];
15166
+ let inList: 'ul' | 'ol' | null = null;
15167
+
15168
+ const closeList = () => {
15169
+ if (inList) {
15170
+ parts.push(`</${inList}>`);
15171
+ inList = null;
15172
+ }
15173
+ };
15174
+
15175
+ for (const element of this.bodyElements) {
15176
+ if (element instanceof Paragraph) {
15177
+ const headingLevel = element.detectHeadingLevel();
15178
+ const style = element.getStyle();
15179
+ const isBullet = style?.toLowerCase().includes('bullet') || style === 'ListBullet';
15180
+ const isNumber =
15181
+ style?.toLowerCase().includes('listnumber') ||
15182
+ style?.toLowerCase().includes('list number') ||
15183
+ style === 'ListNumber';
15184
+
15185
+ if (isBullet || isNumber) {
15186
+ const listType = isBullet ? 'ul' : 'ol';
15187
+ if (inList !== listType) {
15188
+ closeList();
15189
+ inList = listType;
15190
+ parts.push(`<${listType}>`);
15191
+ }
15192
+ parts.push(`<li>${this.paragraphContentToHTML(element)}</li>`);
15193
+ continue;
15194
+ }
15195
+
15196
+ closeList();
15197
+
15198
+ if (headingLevel !== null && headingLevel >= 1 && headingLevel <= 6) {
15199
+ parts.push(
15200
+ `<h${headingLevel}>${this.paragraphContentToHTML(element)}</h${headingLevel}>`
15201
+ );
15202
+ } else {
15203
+ const content = this.paragraphContentToHTML(element);
15204
+ if (content) {
15205
+ parts.push(`<p>${content}</p>`);
15206
+ }
15207
+ }
15208
+ } else if (element instanceof Table) {
15209
+ closeList();
15210
+ parts.push(this.tableToHTML(element));
15211
+ }
15212
+ }
15213
+
15214
+ closeList();
15215
+
15216
+ const body = parts.join('\n');
15217
+
15218
+ if (options?.wrapInDocument) {
15219
+ const title = options.title ? this.escapeHTML(options.title) : 'Document';
15220
+ return [
15221
+ '<!DOCTYPE html>',
15222
+ '<html>',
15223
+ '<head>',
15224
+ `<meta charset="utf-8">`,
15225
+ `<title>${title}</title>`,
15226
+ '</head>',
15227
+ '<body>',
15228
+ body,
15229
+ '</body>',
15230
+ '</html>',
15231
+ ].join('\n');
15232
+ }
15233
+
15234
+ return body;
15235
+ }
15236
+
15237
+ /**
15238
+ * Converts paragraph inline content to HTML.
15239
+ * @internal
15240
+ */
15241
+ private paragraphContentToHTML(para: Paragraph): string {
15242
+ const parts: string[] = [];
15243
+
15244
+ for (const item of para.getContent()) {
15245
+ if (item instanceof Run) {
15246
+ const text = item.getText();
15247
+ if (!text) continue;
15248
+
15249
+ const escaped = this.escapeHTML(text);
15250
+ const fmt = item.getFormatting();
15251
+
15252
+ // Detect monospace font
15253
+ const isMono =
15254
+ fmt.font &&
15255
+ /^(courier|consolas|monaco|menlo|source code|fira code|jetbrains mono)/i.test(fmt.font);
15256
+
15257
+ if (isMono) {
15258
+ parts.push(`<code>${escaped}</code>`);
15259
+ continue;
15260
+ }
15261
+
15262
+ let html = escaped;
15263
+ if (fmt.bold) html = `<strong>${html}</strong>`;
15264
+ if (fmt.italic) html = `<em>${html}</em>`;
15265
+ if (fmt.strike) html = `<s>${html}</s>`;
15266
+ if (fmt.underline && fmt.underline !== 'none') {
15267
+ html = `<u>${html}</u>`;
15268
+ }
15269
+
15270
+ parts.push(html);
15271
+ } else if (item instanceof Hyperlink) {
15272
+ const url = this.escapeHTML(item.getUrl() || '');
15273
+ const linkText = this.escapeHTML(item.getText() || url);
15274
+ parts.push(`<a href="${url}">${linkText}</a>`);
15275
+ }
15276
+ }
15277
+
15278
+ return parts.join('');
15279
+ }
15280
+
15281
+ /**
15282
+ * Converts a table to an HTML table string.
15283
+ * @internal
15284
+ */
15285
+ private tableToHTML(table: Table): string {
15286
+ const rows = table.getRows();
15287
+ if (rows.length === 0) return '';
15288
+
15289
+ const lines: string[] = ['<table>'];
15290
+
15291
+ // First row as thead
15292
+ const headerCells = rows[0]!.getCells();
15293
+ lines.push('<thead>');
15294
+ lines.push('<tr>');
15295
+ for (const cell of headerCells) {
15296
+ lines.push(`<th>${this.escapeHTML(cell.getText())}</th>`);
15297
+ }
15298
+ lines.push('</tr>');
15299
+ lines.push('</thead>');
15300
+
15301
+ // Remaining rows as tbody
15302
+ if (rows.length > 1) {
15303
+ lines.push('<tbody>');
15304
+ for (let r = 1; r < rows.length; r++) {
15305
+ lines.push('<tr>');
15306
+ for (const cell of rows[r]!.getCells()) {
15307
+ lines.push(`<td>${this.escapeHTML(cell.getText())}</td>`);
15308
+ }
15309
+ lines.push('</tr>');
15310
+ }
15311
+ lines.push('</tbody>');
15312
+ }
15313
+
15314
+ lines.push('</table>');
15315
+ return lines.join('\n');
15316
+ }
15317
+
15318
+ /**
15319
+ * Escapes HTML special characters.
15320
+ * @internal
15321
+ */
15322
+ private escapeHTML(text: string): string {
15323
+ return text
15324
+ .replace(/&/g, '&amp;')
15325
+ .replace(/</g, '&lt;')
15326
+ .replace(/>/g, '&gt;')
15327
+ .replace(/"/g, '&quot;');
15328
+ }
15329
+
15330
+ /**
15331
+ * Returns a JSON-serializable representation of the document structure.
15332
+ * Useful for debugging, inspection, and logging.
15333
+ *
15334
+ * @returns Object with document properties, statistics, and content summary
15335
+ *
15336
+ * @example
15337
+ * ```typescript
15338
+ * const json = doc.toJSON();
15339
+ * console.log(JSON.stringify(json, null, 2));
15340
+ * ```
15341
+ */
15342
+ toJSON(): {
15343
+ properties: DocumentProperties;
15344
+ stats: {
15345
+ paragraphs: number;
15346
+ tables: number;
15347
+ images: number;
15348
+ headings: number;
15349
+ sections: number;
15350
+ };
15351
+ headings: { level: number; text: string }[];
15352
+ body: { type: string; text?: string; style?: string }[];
15353
+ } {
15354
+ const paragraphs = this.getAllParagraphs();
15355
+ const tables = this.getTables();
15356
+ const headings = this.getHeadingHierarchy();
15357
+
15358
+ return {
15359
+ properties: this.getProperties(),
15360
+ stats: {
15361
+ paragraphs: paragraphs.length,
15362
+ tables: tables.length,
15363
+ images: this.imageManager.getImageCount(),
15364
+ headings: headings.length,
15365
+ sections: this.bodyElements.filter((el) => el instanceof Section).length || 1,
15366
+ },
15367
+ headings: headings.map((h) => ({ level: h.level, text: h.text })),
15368
+ body: this.bodyElements.map((el) => {
15369
+ if (el instanceof Paragraph) {
15370
+ return {
15371
+ type: 'paragraph',
15372
+ text: el.getText(),
15373
+ style: el.getStyle(),
15374
+ };
15375
+ }
15376
+ if (el instanceof Table) {
15377
+ return {
15378
+ type: 'table',
15379
+ text: `${el.getRows().length} rows x ${el.getRows()[0]?.getCells().length ?? 0} cols`,
15380
+ };
15381
+ }
15382
+ return { type: el.constructor.name };
15383
+ }),
15384
+ };
15385
+ }
15386
+
15387
+ /**
15388
+ * Finds all images in the document that have no alt text or only the default alt text.
15389
+ * Useful for accessibility auditing.
15390
+ *
15391
+ * @returns Array of Image elements missing meaningful alt text
15392
+ *
15393
+ * @example
15394
+ * ```typescript
15395
+ * const missing = doc.findImagesWithoutAltText();
15396
+ * console.log(`${missing.length} images need alt text`);
15397
+ * for (const img of missing) {
15398
+ * img.setAltText('Description of the image');
15399
+ * }
15400
+ * ```
15401
+ */
15402
+ findImagesWithoutAltText(): Image[] {
15403
+ const results: Image[] = [];
15404
+ for (const para of this.getAllParagraphs()) {
15405
+ for (const item of para.getContent()) {
15406
+ if (item instanceof ImageRun) {
15407
+ const image = item.getImageElement();
15408
+ const altText = image.getAltText();
15409
+ if (!altText || altText === 'Image') {
15410
+ results.push(image);
15411
+ }
15412
+ }
15413
+ if (item instanceof Revision) {
15414
+ for (const revContent of item.getContent()) {
15415
+ if (revContent instanceof ImageRun) {
15416
+ const image = revContent.getImageElement();
15417
+ const altText = image.getAltText();
15418
+ if (!altText || altText === 'Image') {
15419
+ results.push(image);
15420
+ }
15421
+ }
15422
+ }
15423
+ }
15424
+ }
15425
+ }
15426
+ return results;
15427
+ }
15428
+
15429
+ /**
15430
+ * Returns the heading hierarchy of the document as a flat list.
15431
+ * Each entry includes the heading level, text content, and the paragraph object.
15432
+ * Useful for accessibility auditing (detecting skipped levels) and TOC generation.
15433
+ *
15434
+ * @returns Array of heading entries sorted by document order
15435
+ *
15436
+ * @example
15437
+ * ```typescript
15438
+ * const headings = doc.getHeadingHierarchy();
15439
+ * for (const h of headings) {
15440
+ * console.log(`${' '.repeat(h.level - 1)}H${h.level}: ${h.text}`);
15441
+ * }
15442
+ *
15443
+ * // Check for skipped levels (accessibility issue)
15444
+ * for (let i = 1; i < headings.length; i++) {
15445
+ * if (headings[i].level - headings[i - 1].level > 1) {
15446
+ * console.warn(`Skipped heading level: H${headings[i - 1].level} -> H${headings[i].level}`);
15447
+ * }
15448
+ * }
15449
+ * ```
15450
+ */
15451
+ getHeadingHierarchy(): { level: number; text: string; paragraph: Paragraph }[] {
15452
+ const results: { level: number; text: string; paragraph: Paragraph }[] = [];
15453
+ for (const para of this.getAllParagraphs()) {
15454
+ const level = para.detectHeadingLevel();
15455
+ if (level !== null) {
15456
+ results.push({
15457
+ level,
15458
+ text: para.getText(),
15459
+ paragraph: para,
15460
+ });
15461
+ }
15462
+ }
15463
+ return results;
15464
+ }
15465
+
15466
+ /**
15467
+ * Groups body elements into sections delimited by headings
15468
+ *
15469
+ * Walks the body elements in order and splits them at each heading paragraph
15470
+ * at or above the specified level. Each section contains the heading paragraph
15471
+ * and all subsequent body elements until the next heading at that level or higher.
15472
+ *
15473
+ * Content before the first matching heading is returned as a section with
15474
+ * `heading: undefined` and `level: 0`.
15475
+ *
15476
+ * @param maxLevel - Maximum heading level to split on (default: 1, meaning only H1
15477
+ * starts a new section). Set to 2 to also split on H2, 3 for H1-H3, etc.
15478
+ * @returns Array of sections, each with heading info and content elements
15479
+ *
15480
+ * @example
15481
+ * ```typescript
15482
+ * // Split document by H1 headings (chapters)
15483
+ * const chapters = doc.extractByHeading(1);
15484
+ * for (const chapter of chapters) {
15485
+ * console.log(`Chapter: ${chapter.heading?.getText() ?? '(preamble)'}`);
15486
+ * console.log(` ${chapter.content.length} elements`);
15487
+ * }
15488
+ * ```
15489
+ *
15490
+ * @example
15491
+ * ```typescript
15492
+ * // Split by H1 and H2 (chapters and sections)
15493
+ * const sections = doc.extractByHeading(2);
15494
+ *
15495
+ * // Extract a specific section's content as markdown
15496
+ * const target = sections.find(s => s.heading?.getText() === 'Methods');
15497
+ * ```
15498
+ */
15499
+ extractByHeading(maxLevel = 1): {
15500
+ heading: Paragraph | undefined;
15501
+ level: number;
15502
+ content: BodyElement[];
15503
+ }[] {
15504
+ const sections: { heading: Paragraph | undefined; level: number; content: BodyElement[] }[] =
15505
+ [];
15506
+ let current: { heading: Paragraph | undefined; level: number; content: BodyElement[] } = {
15507
+ heading: undefined,
15508
+ level: 0,
15509
+ content: [],
15510
+ };
15511
+
15512
+ for (const element of this.bodyElements) {
15513
+ if (element instanceof Paragraph) {
15514
+ const headingLevel = element.detectHeadingLevel();
15515
+
15516
+ if (headingLevel !== null && headingLevel <= maxLevel) {
15517
+ // Save current section if it has any content or a heading
15518
+ if (current.heading || current.content.length > 0) {
15519
+ sections.push(current);
15520
+ }
15521
+ // Start a new section
15522
+ current = { heading: element, level: headingLevel, content: [] };
15523
+ continue;
15524
+ }
15525
+ }
15526
+
15527
+ current.content.push(element);
15528
+ }
15529
+
15530
+ // Push the last section
15531
+ if (current.heading || current.content.length > 0) {
15532
+ sections.push(current);
15533
+ }
15534
+
15535
+ return sections;
15536
+ }
15537
+
15538
+ /**
15539
+ * Returns all body elements between two reference elements (exclusive)
15540
+ *
15541
+ * Finds both elements in the body and returns everything between them.
15542
+ * The start and end elements themselves are NOT included in the result.
15543
+ * Returns an empty array if either element is not found or if start
15544
+ * appears after end.
15545
+ *
15546
+ * @param startElement - Element after which to begin collecting
15547
+ * @param endElement - Element before which to stop collecting
15548
+ * @returns Array of body elements between the two references
15549
+ *
15550
+ * @example
15551
+ * ```typescript
15552
+ * const headings = doc.getParagraphs().filter(p => p.detectHeadingLevel() === 1);
15553
+ * const chapter1Content = doc.getElementsBetween(headings[0], headings[1]);
15554
+ * ```
15555
+ */
15556
+ getElementsBetween(startElement: BodyElement, endElement: BodyElement): BodyElement[] {
15557
+ const startIndex = this.bodyElements.indexOf(startElement);
15558
+ const endIndex = this.bodyElements.indexOf(endElement);
15559
+
15560
+ if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
15561
+ return [];
15562
+ }
15563
+
15564
+ return this.bodyElements.slice(startIndex + 1, endIndex);
15565
+ }
15566
+
15567
+ /**
15568
+ * Removes a paragraph from the document
15569
+ * @param paragraphOrIndex - The paragraph object or its index
15570
+ * @returns True if the paragraph was removed, false otherwise
15571
+ */
15572
+ removeParagraph(paragraphOrIndex: Paragraph | number): boolean {
15573
+ let index: number;
15574
+
15575
+ if (typeof paragraphOrIndex === 'number') {
15576
+ index = paragraphOrIndex;
15577
+ } else {
15578
+ // Find the index of the paragraph
15579
+ index = this.bodyElements.indexOf(paragraphOrIndex);
15580
+ }
15581
+
15582
+ if (index >= 0 && index < this.bodyElements.length) {
15583
+ const element = this.bodyElements[index];
15584
+ if (element instanceof Paragraph) {
15585
+ // When tracking enabled, wrap content in w:del instead of removing
15586
+ if (this.trackChangesEnabled && this.trackingContext.isEnabled()) {
15587
+ const runs = element.getRuns();
15588
+ if (runs.length > 0) {
15589
+ const author = this.trackingContext.getAuthor();
15590
+ const deletion = Revision.createDeletion(author, runs);
15591
+ this.trackingContext.getRevisionManager().register(deletion);
15592
+ element.addRevision(deletion);
15593
+ }
15594
+ return true;
15595
+ }
15596
+ this.bodyElements.splice(index, 1);
15597
+ return true;
15598
+ }
15599
+ }
15600
+
15601
+ return false;
15602
+ }
15603
+
15604
+ /**
15605
+ * Removes a table from the document
15606
+ * @param tableOrIndex - The table object or its index
15607
+ * @returns True if the table was removed, false otherwise
15608
+ */
15609
+ removeTable(tableOrIndex: Table | number): boolean {
15610
+ let index: number;
15611
+
15612
+ if (typeof tableOrIndex === 'number') {
15613
+ // If number provided, find the nth table
15614
+ const tables = this.getTables();
15615
+ if (tableOrIndex >= 0 && tableOrIndex < tables.length) {
15616
+ const table = tables[tableOrIndex];
15617
+ if (!table) return false;
15618
+ index = this.bodyElements.indexOf(table);
15619
+ } else {
15620
+ return false;
15621
+ }
15622
+ } else {
15623
+ // Find the index of the table
15624
+ index = this.bodyElements.indexOf(tableOrIndex);
15625
+ }
15626
+
15627
+ if (index >= 0 && index < this.bodyElements.length) {
15628
+ const element = this.bodyElements[index];
15629
+ if (element instanceof Table) {
15630
+ this.bodyElements.splice(index, 1);
15631
+ return true;
15632
+ }
15633
+ }
15634
+
14000
15635
  return false;
14001
15636
  }
14002
15637
 
@@ -14373,6 +16008,122 @@ export class Document {
14373
16008
  return false;
14374
16009
  }
14375
16010
 
16011
+ /**
16012
+ * Removes a body element by reference
16013
+ *
16014
+ * Finds the element in the body and removes it. More convenient than
16015
+ * the index-based `removeBodyElementAt()` when you already have a
16016
+ * reference to the element.
16017
+ *
16018
+ * @param element - The element to remove
16019
+ * @returns True if removed, false if not found
16020
+ *
16021
+ * @example
16022
+ * ```typescript
16023
+ * // Remove a specific paragraph
16024
+ * const para = doc.getParagraphs().find(p => p.getText() === 'Delete me');
16025
+ * if (para) doc.removeElement(para);
16026
+ *
16027
+ * // Remove all tables
16028
+ * for (const table of doc.getTables()) {
16029
+ * doc.removeElement(table);
16030
+ * }
16031
+ * ```
16032
+ */
16033
+ removeElement(element: BodyElement): boolean {
16034
+ const index = this.bodyElements.indexOf(element);
16035
+ if (index === -1) return false;
16036
+ this.bodyElements.splice(index, 1);
16037
+ return true;
16038
+ }
16039
+
16040
+ /**
16041
+ * Inserts a body element after a reference element
16042
+ *
16043
+ * Finds the reference element in the body and inserts the new element
16044
+ * immediately after it. Returns false if the reference is not found.
16045
+ *
16046
+ * @param reference - The existing element to insert after
16047
+ * @param element - The element to insert
16048
+ * @returns True if inserted, false if reference not found
16049
+ *
16050
+ * @example
16051
+ * ```typescript
16052
+ * // Find a heading and insert a table after it
16053
+ * const heading = doc.getParagraphs().find(p => p.getText() === 'Data');
16054
+ * if (heading) {
16055
+ * doc.insertAfter(heading, table);
16056
+ * }
16057
+ *
16058
+ * // Split a paragraph and insert content between halves
16059
+ * const tail = para.splitAt(offset);
16060
+ * doc.insertAfter(para, newTable);
16061
+ * doc.insertAfter(newTable, tail);
16062
+ * ```
16063
+ */
16064
+ insertAfter(reference: BodyElement, element: BodyElement): boolean {
16065
+ const index = this.bodyElements.indexOf(reference);
16066
+ if (index === -1) return false;
16067
+ this.bodyElements.splice(index + 1, 0, element);
16068
+ return true;
16069
+ }
16070
+
16071
+ /**
16072
+ * Inserts a body element before a reference element
16073
+ *
16074
+ * Finds the reference element in the body and inserts the new element
16075
+ * immediately before it. Returns false if the reference is not found.
16076
+ *
16077
+ * @param reference - The existing element to insert before
16078
+ * @param element - The element to insert
16079
+ * @returns True if inserted, false if reference not found
16080
+ *
16081
+ * @example
16082
+ * ```typescript
16083
+ * // Insert a heading before a table
16084
+ * const table = doc.getTables()[0];
16085
+ * if (table) {
16086
+ * const heading = new Paragraph().addText('Table 1').setStyle('Heading2');
16087
+ * doc.insertBefore(table, heading);
16088
+ * }
16089
+ * ```
16090
+ */
16091
+ insertBefore(reference: BodyElement, element: BodyElement): boolean {
16092
+ const index = this.bodyElements.indexOf(reference);
16093
+ if (index === -1) return false;
16094
+ this.bodyElements.splice(index, 0, element);
16095
+ return true;
16096
+ }
16097
+
16098
+ /**
16099
+ * Replaces a body element with another
16100
+ *
16101
+ * Finds the old element in the body and replaces it in-place with the
16102
+ * new element. The new element occupies the same position. Returns false
16103
+ * if the old element is not found.
16104
+ *
16105
+ * @param oldElement - The element to replace
16106
+ * @param newElement - The replacement element
16107
+ * @returns True if replaced, false if old element not found
16108
+ *
16109
+ * @example
16110
+ * ```typescript
16111
+ * // Replace a placeholder paragraph with a table
16112
+ * const placeholder = doc.getParagraphs().find(
16113
+ * p => p.getText() === '{{INSERT_TABLE_HERE}}'
16114
+ * );
16115
+ * if (placeholder) {
16116
+ * doc.replaceElement(placeholder, dataTable);
16117
+ * }
16118
+ * ```
16119
+ */
16120
+ replaceElement(oldElement: BodyElement, newElement: BodyElement): boolean {
16121
+ const index = this.bodyElements.indexOf(oldElement);
16122
+ if (index === -1) return false;
16123
+ this.bodyElements[index] = newElement;
16124
+ return true;
16125
+ }
16126
+
14376
16127
  /**
14377
16128
  * Inserts a body element at a specific index, shifting existing elements forward.
14378
16129
  * @param index - The zero-based index at which to insert. Clamped to valid range.