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.
- package/README.md +158 -7
- package/dist/core/Document.d.ts +102 -3
- package/dist/core/Document.d.ts.map +1 -1
- package/dist/core/Document.js +775 -50
- package/dist/core/Document.js.map +1 -1
- package/dist/core/DocumentContent.d.ts.map +1 -1
- package/dist/core/DocumentContent.js +0 -8
- package/dist/core/DocumentContent.js.map +1 -1
- package/dist/core/DocumentGenerator.d.ts.map +1 -1
- package/dist/core/DocumentGenerator.js +9 -5
- package/dist/core/DocumentGenerator.js.map +1 -1
- package/dist/core/DocumentParser.d.ts.map +1 -1
- package/dist/core/DocumentParser.js +588 -102
- package/dist/core/DocumentParser.js.map +1 -1
- package/dist/core/RelationshipManager.d.ts.map +1 -1
- package/dist/core/RelationshipManager.js +4 -3
- package/dist/core/RelationshipManager.js.map +1 -1
- package/dist/elements/Bookmark.d.ts +7 -0
- package/dist/elements/Bookmark.d.ts.map +1 -1
- package/dist/elements/Bookmark.js +24 -4
- package/dist/elements/Bookmark.js.map +1 -1
- package/dist/elements/BookmarkManager.d.ts.map +1 -1
- package/dist/elements/BookmarkManager.js +4 -3
- package/dist/elements/BookmarkManager.js.map +1 -1
- package/dist/elements/CommonTypes.d.ts +2 -2
- package/dist/elements/CommonTypes.d.ts.map +1 -1
- package/dist/elements/CommonTypes.js +14 -1
- package/dist/elements/CommonTypes.js.map +1 -1
- package/dist/elements/Field.d.ts +1 -1
- package/dist/elements/Field.d.ts.map +1 -1
- package/dist/elements/Field.js +1 -1
- package/dist/elements/Field.js.map +1 -1
- package/dist/elements/Footer.d.ts +2 -0
- package/dist/elements/Footer.d.ts.map +1 -1
- package/dist/elements/Footer.js +6 -0
- package/dist/elements/Footer.js.map +1 -1
- package/dist/elements/Header.d.ts +2 -0
- package/dist/elements/Header.d.ts.map +1 -1
- package/dist/elements/Header.js +6 -0
- package/dist/elements/Header.js.map +1 -1
- package/dist/elements/Image.d.ts +1 -0
- package/dist/elements/Image.d.ts.map +1 -1
- package/dist/elements/Image.js +17 -2
- package/dist/elements/Image.js.map +1 -1
- package/dist/elements/Paragraph.d.ts +81 -1
- package/dist/elements/Paragraph.d.ts.map +1 -1
- package/dist/elements/Paragraph.js +515 -21
- package/dist/elements/Paragraph.js.map +1 -1
- package/dist/elements/Revision.d.ts +0 -1
- package/dist/elements/Revision.d.ts.map +1 -1
- package/dist/elements/Revision.js +0 -12
- package/dist/elements/Revision.js.map +1 -1
- package/dist/elements/RevisionManager.d.ts +0 -1
- package/dist/elements/RevisionManager.d.ts.map +1 -1
- package/dist/elements/RevisionManager.js +0 -2
- package/dist/elements/RevisionManager.js.map +1 -1
- package/dist/elements/Run.d.ts +16 -4
- package/dist/elements/Run.d.ts.map +1 -1
- package/dist/elements/Run.js +114 -22
- package/dist/elements/Run.js.map +1 -1
- package/dist/elements/Section.d.ts +7 -1
- package/dist/elements/Section.d.ts.map +1 -1
- package/dist/elements/Section.js +185 -4
- package/dist/elements/Section.js.map +1 -1
- package/dist/elements/Shape.js.map +1 -1
- package/dist/elements/Table.d.ts +30 -1
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +357 -40
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableCell.d.ts +3 -0
- package/dist/elements/TableCell.d.ts.map +1 -1
- package/dist/elements/TableCell.js +30 -3
- package/dist/elements/TableCell.js.map +1 -1
- package/dist/elements/TableGridChange.d.ts +0 -1
- package/dist/elements/TableGridChange.d.ts.map +1 -1
- package/dist/elements/TableGridChange.js +0 -10
- package/dist/elements/TableGridChange.js.map +1 -1
- package/dist/elements/TableRow.d.ts +4 -0
- package/dist/elements/TableRow.d.ts.map +1 -1
- package/dist/elements/TableRow.js +31 -3
- package/dist/elements/TableRow.js.map +1 -1
- package/dist/formatting/AbstractNumbering.d.ts +5 -0
- package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
- package/dist/formatting/AbstractNumbering.js +22 -0
- package/dist/formatting/AbstractNumbering.js.map +1 -1
- package/dist/formatting/NumberingLevel.d.ts.map +1 -1
- package/dist/formatting/NumberingLevel.js +3 -3
- package/dist/formatting/NumberingLevel.js.map +1 -1
- package/dist/formatting/Style.d.ts +1 -0
- package/dist/formatting/Style.d.ts.map +1 -1
- package/dist/formatting/Style.js +25 -59
- package/dist/formatting/Style.js.map +1 -1
- package/dist/formatting/StylesManager.d.ts +1 -0
- package/dist/formatting/StylesManager.d.ts.map +1 -1
- package/dist/formatting/StylesManager.js +12 -0
- package/dist/formatting/StylesManager.js.map +1 -1
- package/dist/helpers/CleanupHelper.js.map +1 -1
- package/dist/images/ImageOptimizer.d.ts.map +1 -1
- package/dist/images/ImageOptimizer.js +0 -1
- package/dist/images/ImageOptimizer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/DrawingManager.d.ts.map +1 -1
- package/dist/managers/DrawingManager.js +4 -2
- package/dist/managers/DrawingManager.js.map +1 -1
- package/dist/types/formatting.d.ts +2 -2
- package/dist/types/formatting.d.ts.map +1 -1
- package/dist/types/formatting.js.map +1 -1
- package/dist/utils/ChangelogGenerator.d.ts +2 -2
- package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
- package/dist/utils/ChangelogGenerator.js +4 -5
- package/dist/utils/ChangelogGenerator.js.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.js +0 -1
- package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
- package/dist/utils/RevisionAwareProcessor.d.ts +2 -2
- package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
- package/dist/utils/RevisionAwareProcessor.js +2 -2
- package/dist/utils/RevisionAwareProcessor.js.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.d.ts +0 -2
- package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.js +0 -26
- package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/utils/ShadingResolver.d.ts.map +1 -1
- package/dist/utils/ShadingResolver.js.map +1 -1
- package/dist/utils/acceptRevisions.js +1 -1
- package/dist/utils/acceptRevisions.js.map +1 -1
- package/dist/utils/stripTrackedChanges.js +1 -1
- package/dist/utils/stripTrackedChanges.js.map +1 -1
- package/dist/utils/units.d.ts.map +1 -1
- package/dist/utils/units.js +1 -1
- package/dist/utils/units.js.map +1 -1
- package/dist/validation/RevisionAutoFixer.d.ts +2 -1
- package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
- package/dist/validation/RevisionAutoFixer.js.map +1 -1
- package/package.json +10 -1
- package/src/constants/CLAUDE.md +28 -0
- package/src/core/CLAUDE.md +4 -0
- package/src/core/Document.ts +1888 -137
- package/src/core/DocumentContent.ts +0 -11
- package/src/core/DocumentGenerator.ts +11 -12
- package/src/core/DocumentParser.ts +620 -139
- package/src/core/RelationshipManager.ts +6 -3
- package/src/elements/Bookmark.ts +39 -4
- package/src/elements/BookmarkManager.ts +4 -3
- package/src/elements/CLAUDE.md +18 -2
- package/src/elements/CommonTypes.ts +35 -8
- package/src/elements/Field.ts +1 -1
- package/src/elements/Footer.ts +23 -0
- package/src/elements/Header.ts +25 -0
- package/src/elements/Image.ts +28 -5
- package/src/elements/Paragraph.ts +1069 -41
- package/src/elements/Revision.ts +0 -19
- package/src/elements/RevisionManager.ts +1 -3
- package/src/elements/Run.ts +265 -35
- package/src/elements/Section.ts +214 -8
- package/src/elements/Shape.ts +1 -1
- package/src/elements/Table.ts +850 -61
- package/src/elements/TableCell.ts +84 -10
- package/src/elements/TableGridChange.ts +2 -16
- package/src/elements/TableRow.ts +94 -9
- package/src/formatting/AbstractNumbering.ts +42 -1
- package/src/formatting/CLAUDE.md +4 -0
- package/src/formatting/NumberingLevel.ts +11 -7
- package/src/formatting/Style.ts +39 -71
- package/src/formatting/StylesManager.ts +36 -0
- package/src/helpers/CleanupHelper.ts +1 -1
- package/src/images/ImageOptimizer.ts +0 -3
- package/src/index.ts +1 -1
- package/src/managers/DrawingManager.ts +5 -3
- package/src/tracking/CLAUDE.md +30 -0
- package/src/types/CLAUDE.md +39 -0
- package/src/types/formatting.ts +2 -2
- package/src/utils/CLAUDE.md +15 -0
- package/src/utils/ChangelogGenerator.ts +4 -5
- package/src/utils/InMemoryRevisionAcceptor.ts +0 -9
- package/src/utils/RevisionAwareProcessor.ts +2 -3
- package/src/utils/SelectiveRevisionAcceptor.ts +0 -39
- package/src/utils/ShadingResolver.ts +0 -1
- package/src/utils/acceptRevisions.ts +1 -1
- package/src/utils/stripTrackedChanges.ts +1 -1
- package/src/utils/units.ts +2 -1
- package/src/validation/CLAUDE.md +40 -0
- package/src/validation/RevisionAutoFixer.ts +2 -1
package/src/core/Document.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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, (
|
|
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, (
|
|
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
|
-
|
|
8579
|
+
// hasTableSwitch tracked via \t switch parsing below
|
|
8035
8580
|
|
|
8036
8581
|
// Normalize whitespace and quotes: trim input and replace " with " for consistent parsing
|
|
8037
8582
|
const normalizedText = instrText.trim().replace(/"/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
|
-
|
|
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
|
-
*
|
|
10051
|
-
*
|
|
10052
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
13934
|
-
*
|
|
13935
|
-
*
|
|
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
|
-
|
|
13938
|
-
|
|
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
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
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
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
const
|
|
13955
|
-
|
|
13956
|
-
|
|
13957
|
-
|
|
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
|
|
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
|
-
*
|
|
13971
|
-
*
|
|
13972
|
-
*
|
|
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
|
-
|
|
13975
|
-
let
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
if (
|
|
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
|
-
|
|
13993
|
-
|
|
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
|
-
|
|
13996
|
-
|
|
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, '&')
|
|
15325
|
+
.replace(/</g, '<')
|
|
15326
|
+
.replace(/>/g, '>')
|
|
15327
|
+
.replace(/"/g, '"');
|
|
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.
|