docxmlater 10.3.6 → 10.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +158 -7
  2. package/dist/core/Document.d.ts +97 -3
  3. package/dist/core/Document.d.ts.map +1 -1
  4. package/dist/core/Document.js +727 -50
  5. package/dist/core/Document.js.map +1 -1
  6. package/dist/core/DocumentContent.d.ts.map +1 -1
  7. package/dist/core/DocumentContent.js +0 -8
  8. package/dist/core/DocumentContent.js.map +1 -1
  9. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  10. package/dist/core/DocumentGenerator.js +9 -5
  11. package/dist/core/DocumentGenerator.js.map +1 -1
  12. package/dist/core/DocumentParser.d.ts.map +1 -1
  13. package/dist/core/DocumentParser.js +573 -101
  14. package/dist/core/DocumentParser.js.map +1 -1
  15. package/dist/core/RelationshipManager.d.ts.map +1 -1
  16. package/dist/core/RelationshipManager.js +4 -3
  17. package/dist/core/RelationshipManager.js.map +1 -1
  18. package/dist/elements/Bookmark.d.ts +7 -0
  19. package/dist/elements/Bookmark.d.ts.map +1 -1
  20. package/dist/elements/Bookmark.js +24 -4
  21. package/dist/elements/Bookmark.js.map +1 -1
  22. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  23. package/dist/elements/BookmarkManager.js +4 -3
  24. package/dist/elements/BookmarkManager.js.map +1 -1
  25. package/dist/elements/CommonTypes.d.ts +2 -2
  26. package/dist/elements/CommonTypes.d.ts.map +1 -1
  27. package/dist/elements/CommonTypes.js +14 -1
  28. package/dist/elements/CommonTypes.js.map +1 -1
  29. package/dist/elements/Field.d.ts +1 -1
  30. package/dist/elements/Field.d.ts.map +1 -1
  31. package/dist/elements/Field.js +1 -1
  32. package/dist/elements/Field.js.map +1 -1
  33. package/dist/elements/Footer.d.ts +2 -0
  34. package/dist/elements/Footer.d.ts.map +1 -1
  35. package/dist/elements/Footer.js +6 -0
  36. package/dist/elements/Footer.js.map +1 -1
  37. package/dist/elements/Header.d.ts +2 -0
  38. package/dist/elements/Header.d.ts.map +1 -1
  39. package/dist/elements/Header.js +6 -0
  40. package/dist/elements/Header.js.map +1 -1
  41. package/dist/elements/Image.d.ts.map +1 -1
  42. package/dist/elements/Image.js +3 -0
  43. package/dist/elements/Image.js.map +1 -1
  44. package/dist/elements/Paragraph.d.ts +81 -1
  45. package/dist/elements/Paragraph.d.ts.map +1 -1
  46. package/dist/elements/Paragraph.js +515 -21
  47. package/dist/elements/Paragraph.js.map +1 -1
  48. package/dist/elements/Revision.d.ts +0 -1
  49. package/dist/elements/Revision.d.ts.map +1 -1
  50. package/dist/elements/Revision.js +0 -12
  51. package/dist/elements/Revision.js.map +1 -1
  52. package/dist/elements/RevisionManager.d.ts +0 -1
  53. package/dist/elements/RevisionManager.d.ts.map +1 -1
  54. package/dist/elements/RevisionManager.js +0 -2
  55. package/dist/elements/RevisionManager.js.map +1 -1
  56. package/dist/elements/Run.d.ts +16 -4
  57. package/dist/elements/Run.d.ts.map +1 -1
  58. package/dist/elements/Run.js +114 -22
  59. package/dist/elements/Run.js.map +1 -1
  60. package/dist/elements/Section.d.ts +7 -1
  61. package/dist/elements/Section.d.ts.map +1 -1
  62. package/dist/elements/Section.js +185 -4
  63. package/dist/elements/Section.js.map +1 -1
  64. package/dist/elements/Shape.js.map +1 -1
  65. package/dist/elements/Table.d.ts +30 -1
  66. package/dist/elements/Table.d.ts.map +1 -1
  67. package/dist/elements/Table.js +357 -40
  68. package/dist/elements/Table.js.map +1 -1
  69. package/dist/elements/TableCell.d.ts +3 -0
  70. package/dist/elements/TableCell.d.ts.map +1 -1
  71. package/dist/elements/TableCell.js +30 -3
  72. package/dist/elements/TableCell.js.map +1 -1
  73. package/dist/elements/TableGridChange.d.ts +0 -1
  74. package/dist/elements/TableGridChange.d.ts.map +1 -1
  75. package/dist/elements/TableGridChange.js +0 -10
  76. package/dist/elements/TableGridChange.js.map +1 -1
  77. package/dist/elements/TableRow.d.ts +4 -0
  78. package/dist/elements/TableRow.d.ts.map +1 -1
  79. package/dist/elements/TableRow.js +31 -3
  80. package/dist/elements/TableRow.js.map +1 -1
  81. package/dist/formatting/AbstractNumbering.d.ts +5 -0
  82. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  83. package/dist/formatting/AbstractNumbering.js +22 -0
  84. package/dist/formatting/AbstractNumbering.js.map +1 -1
  85. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  86. package/dist/formatting/NumberingLevel.js +3 -3
  87. package/dist/formatting/NumberingLevel.js.map +1 -1
  88. package/dist/formatting/Style.d.ts +1 -0
  89. package/dist/formatting/Style.d.ts.map +1 -1
  90. package/dist/formatting/Style.js +25 -59
  91. package/dist/formatting/Style.js.map +1 -1
  92. package/dist/formatting/StylesManager.d.ts +1 -0
  93. package/dist/formatting/StylesManager.d.ts.map +1 -1
  94. package/dist/formatting/StylesManager.js +12 -0
  95. package/dist/formatting/StylesManager.js.map +1 -1
  96. package/dist/helpers/CleanupHelper.js.map +1 -1
  97. package/dist/images/ImageOptimizer.d.ts.map +1 -1
  98. package/dist/images/ImageOptimizer.js +0 -1
  99. package/dist/images/ImageOptimizer.js.map +1 -1
  100. package/dist/index.d.ts +1 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js.map +1 -1
  103. package/dist/managers/DrawingManager.d.ts.map +1 -1
  104. package/dist/managers/DrawingManager.js +4 -2
  105. package/dist/managers/DrawingManager.js.map +1 -1
  106. package/dist/types/formatting.d.ts +2 -2
  107. package/dist/types/formatting.d.ts.map +1 -1
  108. package/dist/types/formatting.js.map +1 -1
  109. package/dist/utils/ChangelogGenerator.d.ts +2 -2
  110. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  111. package/dist/utils/ChangelogGenerator.js +4 -5
  112. package/dist/utils/ChangelogGenerator.js.map +1 -1
  113. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  114. package/dist/utils/InMemoryRevisionAcceptor.js +0 -1
  115. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  116. package/dist/utils/RevisionAwareProcessor.d.ts +2 -2
  117. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  118. package/dist/utils/RevisionAwareProcessor.js +2 -2
  119. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  120. package/dist/utils/SelectiveRevisionAcceptor.d.ts +0 -2
  121. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  122. package/dist/utils/SelectiveRevisionAcceptor.js +0 -26
  123. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  124. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  125. package/dist/utils/ShadingResolver.js.map +1 -1
  126. package/dist/utils/acceptRevisions.js +1 -1
  127. package/dist/utils/acceptRevisions.js.map +1 -1
  128. package/dist/utils/stripTrackedChanges.js +1 -1
  129. package/dist/utils/stripTrackedChanges.js.map +1 -1
  130. package/dist/utils/units.d.ts.map +1 -1
  131. package/dist/utils/units.js +1 -1
  132. package/dist/utils/units.js.map +1 -1
  133. package/dist/validation/RevisionAutoFixer.d.ts +2 -1
  134. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  135. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  136. package/package.json +10 -1
  137. package/src/constants/CLAUDE.md +28 -0
  138. package/src/core/CLAUDE.md +4 -0
  139. package/src/core/Document.ts +1755 -85
  140. package/src/core/DocumentContent.ts +0 -11
  141. package/src/core/DocumentGenerator.ts +11 -12
  142. package/src/core/DocumentParser.ts +599 -138
  143. package/src/core/RelationshipManager.ts +6 -3
  144. package/src/elements/Bookmark.ts +39 -4
  145. package/src/elements/BookmarkManager.ts +4 -3
  146. package/src/elements/CLAUDE.md +18 -2
  147. package/src/elements/CommonTypes.ts +35 -8
  148. package/src/elements/Field.ts +1 -1
  149. package/src/elements/Footer.ts +23 -0
  150. package/src/elements/Header.ts +25 -0
  151. package/src/elements/Image.ts +5 -0
  152. package/src/elements/Paragraph.ts +1069 -41
  153. package/src/elements/Revision.ts +0 -19
  154. package/src/elements/RevisionManager.ts +1 -3
  155. package/src/elements/Run.ts +265 -35
  156. package/src/elements/Section.ts +214 -8
  157. package/src/elements/Shape.ts +1 -1
  158. package/src/elements/Table.ts +850 -61
  159. package/src/elements/TableCell.ts +84 -10
  160. package/src/elements/TableGridChange.ts +2 -16
  161. package/src/elements/TableRow.ts +94 -9
  162. package/src/formatting/AbstractNumbering.ts +42 -1
  163. package/src/formatting/CLAUDE.md +4 -0
  164. package/src/formatting/NumberingLevel.ts +11 -7
  165. package/src/formatting/Style.ts +39 -71
  166. package/src/formatting/StylesManager.ts +36 -0
  167. package/src/helpers/CleanupHelper.ts +1 -1
  168. package/src/images/ImageOptimizer.ts +0 -3
  169. package/src/index.ts +1 -1
  170. package/src/managers/DrawingManager.ts +5 -3
  171. package/src/tracking/CLAUDE.md +30 -0
  172. package/src/types/CLAUDE.md +39 -0
  173. package/src/types/formatting.ts +2 -2
  174. package/src/utils/CLAUDE.md +15 -0
  175. package/src/utils/ChangelogGenerator.ts +4 -5
  176. package/src/utils/InMemoryRevisionAcceptor.ts +0 -9
  177. package/src/utils/RevisionAwareProcessor.ts +2 -3
  178. package/src/utils/SelectiveRevisionAcceptor.ts +0 -39
  179. package/src/utils/ShadingResolver.ts +0 -1
  180. package/src/utils/acceptRevisions.ts +1 -1
  181. package/src/utils/stripTrackedChanges.ts +1 -1
  182. package/src/utils/units.ts +2 -1
  183. package/src/validation/CLAUDE.md +40 -0
  184. package/src/validation/RevisionAutoFixer.ts +2 -1
@@ -866,27 +866,8 @@ export class Revision {
866
866
  };
867
867
  }
868
868
 
869
- private createDeletedHyperlinkXml(hyperlink: import('./Hyperlink').Hyperlink): XMLElement {
870
- // Get the hyperlink's normal XML
871
- const hyperlinkXml = hyperlink.toXML();
872
-
873
- // Transform nested runs: w:t -> w:delText (or w:delInstrText for field instructions)
874
- if (hyperlinkXml.children) {
875
- hyperlinkXml.children = hyperlinkXml.children.map((child) => {
876
- if (typeof child === 'object' && child.name === 'w:r') {
877
- // Transform the run's text elements
878
- return this.convertRunXmlToDeleted(child);
879
- }
880
- return child;
881
- });
882
- }
883
-
884
- return hyperlinkXml;
885
- }
886
-
887
869
  /**
888
870
  * Converts a run XMLElement to use deleted text elements
889
- * Helper for createDeletedHyperlinkXml
890
871
  */
891
872
  private convertRunXmlToDeleted(runXml: XMLElement): XMLElement {
892
873
  const deletedTextElement = this.isFieldInstruction ? 'w:delInstrText' : 'w:delText';
@@ -67,8 +67,6 @@ export class RevisionManager {
67
67
  private revisionsByTypeCache = new Map<RevisionType, Revision[]>();
68
68
  private revisionsByAuthorCache = new Map<string, Revision[]>();
69
69
  private revisionsByCategoryCache = new Map<RevisionCategory, Revision[]>();
70
- private cacheValid = true;
71
-
72
70
  /**
73
71
  * Invalidates all caches. Called when revisions are added/removed.
74
72
  * @private
@@ -77,7 +75,7 @@ export class RevisionManager {
77
75
  this.revisionsByTypeCache.clear();
78
76
  this.revisionsByAuthorCache.clear();
79
77
  this.revisionsByCategoryCache.clear();
80
- this.cacheValid = false;
78
+ // Cache cleared
81
79
  }
82
80
 
83
81
  /**
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { deepClone } from '../utils/deepClone';
7
7
  import { formatDateForXml } from '../utils/dateFormatting';
8
+ import { isEqualFormatting } from '../utils/formatting';
8
9
  import { logSerialization, logTextDirection } from '../utils/diagnostics';
9
10
  import { defaultLogger } from '../utils/logger';
10
11
  import { normalizeColor, validateRunText } from '../utils/validation';
@@ -17,7 +18,6 @@ import {
17
18
  ShadingConfig,
18
19
  buildShadingAttributes,
19
20
  ExtendedBorderStyle,
20
- BorderDefinition,
21
21
  } from './CommonTypes';
22
22
  import type { RunPropertyChange } from './PropertyChangeTypes';
23
23
  // Type-only import to avoid circular dependency (Revision imports Run)
@@ -71,6 +71,8 @@ export interface RunContent {
71
71
  value?: string;
72
72
  /** Break type (only for 'break' type) */
73
73
  breakType?: BreakType;
74
+ /** Break clear type (only for 'break' with type='textWrapping') per ECMA-376 §17.3.3.1 */
75
+ breakClear?: 'none' | 'left' | 'right' | 'all';
74
76
  /** Field character subtype (only for 'fieldChar' type) */
75
77
  fieldCharType?: 'begin' | 'separate' | 'end';
76
78
  /** Whether the field char is marked dirty */
@@ -135,6 +137,18 @@ export type ShadingPattern = CommonShadingPattern;
135
137
  */
136
138
  export type CharacterShading = ShadingConfig;
137
139
 
140
+ /**
141
+ * Language configuration per ECMA-376 CT_Language (§17.3.2.20)
142
+ */
143
+ export interface LanguageConfig {
144
+ /** Language for Latin text (e.g., 'en-US') */
145
+ val?: string;
146
+ /** Language for East Asian text (e.g., 'ja-JP', 'zh-CN') */
147
+ eastAsia?: string;
148
+ /** Language for complex script / bidirectional text (e.g., 'ar-SA', 'he-IL') */
149
+ bidi?: string;
150
+ }
151
+
138
152
  /**
139
153
  * East Asian typography layout options
140
154
  */
@@ -269,8 +283,8 @@ export interface RunFormatting {
269
283
  position?: number;
270
284
  /** Kerning threshold in half-points (font size at which kerning starts) */
271
285
  kerning?: number;
272
- /** Language code (e.g., 'en-US', 'fr-FR', 'es-ES') */
273
- language?: string;
286
+ /** Language code (e.g., 'en-US', 'fr-FR', 'es-ES') or CT_Language object */
287
+ language?: string | LanguageConfig;
274
288
  /** Underline text. Use "none" to explicitly override style underline. */
275
289
  underline?: boolean | 'single' | 'double' | 'thick' | 'dotted' | 'dash' | 'none';
276
290
  /** Underline color in hex format (without #) per ECMA-376 Part 1 §17.3.2.40 */
@@ -289,6 +303,8 @@ export interface RunFormatting {
289
303
  subscript?: boolean;
290
304
  /** Superscript */
291
305
  superscript?: boolean;
306
+ /** Explicit baseline vertical alignment (w:vertAlign w:val="baseline" per §17.18.96) */
307
+ vertAlignBaseline?: boolean;
292
308
  /** Font name */
293
309
  font?: string;
294
310
  /** Font size in points (half-points for Word) */
@@ -312,7 +328,7 @@ export interface RunFormatting {
312
328
  * Applied to themeColor to create darker variations
313
329
  */
314
330
  themeShade?: number;
315
- /** Highlight color */
331
+ /** Highlight color per ECMA-376 ST_HighlightColor ('none' explicitly removes highlight) */
316
332
  highlight?:
317
333
  | 'yellow'
318
334
  | 'green'
@@ -329,7 +345,8 @@ export interface RunFormatting {
329
345
  | 'darkGray'
330
346
  | 'lightGray'
331
347
  | 'black'
332
- | 'white';
348
+ | 'white'
349
+ | 'none';
333
350
  /** Small caps */
334
351
  smallCaps?: boolean;
335
352
  /** All caps */
@@ -582,6 +599,82 @@ export class Run {
582
599
  .join('');
583
600
  }
584
601
 
602
+ /**
603
+ * Gets only the literal text content, excluding special characters
604
+ *
605
+ * Unlike `getText()` which maps tabs to `\t` and breaks to `\n`,
606
+ * this method returns only the actual text content from `w:t` elements,
607
+ * ignoring tabs, breaks, carriage returns, hyphens, and field chars.
608
+ * Useful for word counting, search indexing, and plain-text extraction.
609
+ *
610
+ * @returns Text content only, with no special character representations
611
+ *
612
+ * @example
613
+ * ```typescript
614
+ * const run = new Run('');
615
+ * run.appendText('Hello');
616
+ * run.addTab();
617
+ * run.appendText('World');
618
+ *
619
+ * run.getText(); // "Hello\tWorld"
620
+ * run.getPlainText(); // "HelloWorld"
621
+ * ```
622
+ */
623
+ getPlainText(): string {
624
+ return this.content
625
+ .filter((c) => c.type === 'text')
626
+ .map((c) => c.value || '')
627
+ .join('');
628
+ }
629
+
630
+ /**
631
+ * Checks whether this run is equal to another (same text and formatting)
632
+ *
633
+ * Compares both the text content (via `getText()`) and the formatting
634
+ * properties (deep equality). Useful for deduplication, testing, and
635
+ * determining whether two runs can be merged.
636
+ *
637
+ * @param other - Run to compare against
638
+ * @returns True if both text and formatting are identical
639
+ *
640
+ * @example
641
+ * ```typescript
642
+ * const a = new Run('Hello', { bold: true });
643
+ * const b = new Run('Hello', { bold: true });
644
+ * const c = new Run('Hello', { italic: true });
645
+ *
646
+ * a.equals(b); // true
647
+ * a.equals(c); // false
648
+ * ```
649
+ */
650
+ equals(other: Run): boolean {
651
+ if (this.getText() !== other.getText()) return false;
652
+ return isEqualFormatting(
653
+ this.getFormatting() as unknown as Record<string, unknown>,
654
+ other.getFormatting() as unknown as Record<string, unknown>
655
+ );
656
+ }
657
+
658
+ /**
659
+ * Checks whether this run has the same formatting as another (ignoring text)
660
+ *
661
+ * @param other - Run to compare formatting against
662
+ * @returns True if formatting is identical
663
+ *
664
+ * @example
665
+ * ```typescript
666
+ * const a = new Run('Hello', { bold: true });
667
+ * const b = new Run('World', { bold: true });
668
+ * a.hasSameFormatting(b); // true (different text, same formatting)
669
+ * ```
670
+ */
671
+ hasSameFormatting(other: Run): boolean {
672
+ return isEqualFormatting(
673
+ this.getFormatting() as unknown as Record<string, unknown>,
674
+ other.getFormatting() as unknown as Record<string, unknown>
675
+ );
676
+ }
677
+
585
678
  /**
586
679
  * Sets the text content of the run
587
680
  *
@@ -1319,11 +1412,11 @@ export class Run {
1319
1412
  }
1320
1413
 
1321
1414
  /**
1322
- * Sets language
1323
- * Per ECMA-376 Part 1 §17.3.2.20
1324
- * @param language - Language code (e.g., 'en-US', 'fr-FR', 'es-ES')
1415
+ * Sets language per ECMA-376 Part 1 §17.3.2.20 (CT_Language)
1416
+ * @param language - Language code string (sets w:val) or LanguageConfig object
1417
+ * with val (Latin), eastAsia (CJK), bidi (RTL/complex script) attributes
1325
1418
  */
1326
- setLanguage(language: string): this {
1419
+ setLanguage(language: string | LanguageConfig): this {
1327
1420
  const previousValue = this.formatting.language;
1328
1421
  this.formatting.language = language;
1329
1422
  if (this.trackingContext?.isEnabled() && previousValue !== language) {
@@ -2150,6 +2243,9 @@ export class Run {
2150
2243
  if (contentElement.breakType) {
2151
2244
  attrs['w:type'] = contentElement.breakType;
2152
2245
  }
2246
+ if (contentElement.breakClear) {
2247
+ attrs['w:clear'] = contentElement.breakClear;
2248
+ }
2153
2249
  runChildren.push(
2154
2250
  XMLBuilder.wSelf('br', Object.keys(attrs).length > 0 ? attrs : undefined)
2155
2251
  );
@@ -2530,8 +2626,8 @@ export class Run {
2530
2626
  * run.addBreak('column'); // Column break
2531
2627
  * ```
2532
2628
  */
2533
- addBreak(breakType?: BreakType): this {
2534
- this.content.push({ type: 'break', breakType });
2629
+ addBreak(breakType?: BreakType, breakClear?: 'none' | 'left' | 'right' | 'all'): this {
2630
+ this.content.push({ type: 'break', breakType, breakClear });
2535
2631
  return this;
2536
2632
  }
2537
2633
 
@@ -2642,43 +2738,50 @@ export class Run {
2642
2738
  }
2643
2739
 
2644
2740
  // 3. w:b — Bold
2645
- if (formatting.bold) {
2646
- rPrChildren.push(XMLBuilder.wSelf('b', { 'w:val': '1' }));
2741
+ // Emit val="0" when explicitly false to override style-inherited bold
2742
+ if (formatting.bold !== undefined) {
2743
+ rPrChildren.push(XMLBuilder.wSelf('b', { 'w:val': formatting.bold ? '1' : '0' }));
2647
2744
  }
2648
2745
 
2649
2746
  // 4. w:bCs — Bold complex script
2650
- if (formatting.complexScriptBold) {
2651
- rPrChildren.push(XMLBuilder.wSelf('bCs', { 'w:val': '1' }));
2747
+ if (formatting.complexScriptBold !== undefined) {
2748
+ rPrChildren.push(
2749
+ XMLBuilder.wSelf('bCs', { 'w:val': formatting.complexScriptBold ? '1' : '0' })
2750
+ );
2652
2751
  }
2653
2752
 
2654
2753
  // 5. w:i — Italic
2655
- if (formatting.italic) {
2656
- rPrChildren.push(XMLBuilder.wSelf('i', { 'w:val': '1' }));
2754
+ if (formatting.italic !== undefined) {
2755
+ rPrChildren.push(XMLBuilder.wSelf('i', { 'w:val': formatting.italic ? '1' : '0' }));
2657
2756
  }
2658
2757
 
2659
2758
  // 6. w:iCs — Italic complex script
2660
- if (formatting.complexScriptItalic) {
2661
- rPrChildren.push(XMLBuilder.wSelf('iCs', { 'w:val': '1' }));
2759
+ if (formatting.complexScriptItalic !== undefined) {
2760
+ rPrChildren.push(
2761
+ XMLBuilder.wSelf('iCs', { 'w:val': formatting.complexScriptItalic ? '1' : '0' })
2762
+ );
2662
2763
  }
2663
2764
 
2664
- // 7. w:caps — All caps
2665
- if (formatting.allCaps) {
2666
- rPrChildren.push(XMLBuilder.wSelf('caps', { 'w:val': '1' }));
2765
+ // 7. w:caps — All caps (emit val="0" when explicitly false to override style)
2766
+ if (formatting.allCaps !== undefined) {
2767
+ rPrChildren.push(XMLBuilder.wSelf('caps', { 'w:val': formatting.allCaps ? '1' : '0' }));
2667
2768
  }
2668
2769
 
2669
2770
  // 8. w:smallCaps — Small caps
2670
- if (formatting.smallCaps) {
2671
- rPrChildren.push(XMLBuilder.wSelf('smallCaps', { 'w:val': '1' }));
2771
+ if (formatting.smallCaps !== undefined) {
2772
+ rPrChildren.push(
2773
+ XMLBuilder.wSelf('smallCaps', { 'w:val': formatting.smallCaps ? '1' : '0' })
2774
+ );
2672
2775
  }
2673
2776
 
2674
2777
  // 9. w:strike — Single strikethrough
2675
- if (formatting.strike) {
2676
- rPrChildren.push(XMLBuilder.wSelf('strike', { 'w:val': '1' }));
2778
+ if (formatting.strike !== undefined) {
2779
+ rPrChildren.push(XMLBuilder.wSelf('strike', { 'w:val': formatting.strike ? '1' : '0' }));
2677
2780
  }
2678
2781
 
2679
2782
  // 10. w:dstrike — Double strikethrough
2680
- if (formatting.dstrike) {
2681
- rPrChildren.push(XMLBuilder.wSelf('dstrike', { 'w:val': '1' }));
2783
+ if (formatting.dstrike !== undefined) {
2784
+ rPrChildren.push(XMLBuilder.wSelf('dstrike', { 'w:val': formatting.dstrike ? '1' : '0' }));
2682
2785
  }
2683
2786
 
2684
2787
  // 11. w:outline — Outline text effect
@@ -2707,8 +2810,12 @@ export class Run {
2707
2810
  }
2708
2811
 
2709
2812
  // 16. w:snapToGrid — Snap to grid
2710
- if (formatting.snapToGrid) {
2711
- rPrChildren.push(XMLBuilder.wSelf('snapToGrid', { 'w:val': '1' }));
2813
+ // Per ECMA-376 §17.3.2.34, the default when absent is true (snap to grid ON).
2814
+ // Must emit w:val="0" when explicitly false to preserve the override on round-trip.
2815
+ if (formatting.snapToGrid !== undefined) {
2816
+ rPrChildren.push(
2817
+ XMLBuilder.wSelf('snapToGrid', { 'w:val': formatting.snapToGrid ? '1' : '0' })
2818
+ );
2712
2819
  }
2713
2820
 
2714
2821
  // 17. w:vanish — Hidden text
@@ -2839,12 +2946,13 @@ export class Run {
2839
2946
  rPrChildren.push(XMLBuilder.wSelf('fitText', { 'w:val': formatting.fitText }));
2840
2947
  }
2841
2948
 
2842
- // 32. w:vertAlign — Subscript/superscript
2949
+ // 32. w:vertAlign — Subscript/superscript/baseline per ECMA-376 §17.18.96
2843
2950
  if (formatting.subscript) {
2844
2951
  rPrChildren.push(XMLBuilder.wSelf('vertAlign', { 'w:val': 'subscript' }));
2845
- }
2846
- if (formatting.superscript) {
2952
+ } else if (formatting.superscript) {
2847
2953
  rPrChildren.push(XMLBuilder.wSelf('vertAlign', { 'w:val': 'superscript' }));
2954
+ } else if (formatting.vertAlignBaseline) {
2955
+ rPrChildren.push(XMLBuilder.wSelf('vertAlign', { 'w:val': 'baseline' }));
2848
2956
  }
2849
2957
 
2850
2958
  // 33. w:rtl — Right-to-left text
@@ -2862,9 +2970,19 @@ export class Run {
2862
2970
  rPrChildren.push(XMLBuilder.wSelf('em', { 'w:val': formatting.emphasis }));
2863
2971
  }
2864
2972
 
2865
- // 36. w:lang — Language
2973
+ // 36. w:lang — Language (CT_Language: w:val, w:eastAsia, w:bidi)
2866
2974
  if (formatting.language) {
2867
- rPrChildren.push(XMLBuilder.wSelf('lang', { 'w:val': formatting.language }));
2975
+ const langAttrs: Record<string, string> = {};
2976
+ if (typeof formatting.language === 'string') {
2977
+ langAttrs['w:val'] = formatting.language;
2978
+ } else {
2979
+ if (formatting.language.val) langAttrs['w:val'] = formatting.language.val;
2980
+ if (formatting.language.eastAsia) langAttrs['w:eastAsia'] = formatting.language.eastAsia;
2981
+ if (formatting.language.bidi) langAttrs['w:bidi'] = formatting.language.bidi;
2982
+ }
2983
+ if (Object.keys(langAttrs).length > 0) {
2984
+ rPrChildren.push(XMLBuilder.wSelf('lang', langAttrs));
2985
+ }
2868
2986
  }
2869
2987
 
2870
2988
  // 37. w:eastAsianLayout — East Asian layout
@@ -2986,6 +3104,118 @@ export class Run {
2986
3104
  return this;
2987
3105
  }
2988
3106
 
3107
+ /**
3108
+ * Splits this run at a character offset, returning the removed tail as a new run
3109
+ *
3110
+ * The current run keeps content before the offset; the returned run gets content
3111
+ * from the offset onward. Both runs share the same formatting (deep-cloned).
3112
+ * Content-aware: correctly handles splits within text elements and preserves
3113
+ * special elements (tabs, breaks, soft hyphens) at their correct positions.
3114
+ *
3115
+ * Character offsets follow the same mapping as `getText()`:
3116
+ * text characters count by length, tabs/breaks/CR each count as 1.
3117
+ *
3118
+ * @param offset - Character position to split at (0-based). Content from this
3119
+ * position onward moves to the new run.
3120
+ * @returns A new Run containing content from `offset` onward, with cloned formatting.
3121
+ * Returns a new empty Run if offset >= text length. Returns a clone of the full
3122
+ * run (and empties this one) if offset <= 0.
3123
+ *
3124
+ * @example
3125
+ * ```typescript
3126
+ * const run = new Run('Hello World', { bold: true });
3127
+ * const tail = run.splitAt(5);
3128
+ * run.getText(); // "Hello"
3129
+ * tail.getText(); // " World"
3130
+ * tail.getFormatting().bold; // true (formatting preserved)
3131
+ * ```
3132
+ *
3133
+ * @example
3134
+ * ```typescript
3135
+ * // Split within content containing tabs
3136
+ * const run = new Run('');
3137
+ * run.appendText('Name');
3138
+ * run.addTab();
3139
+ * run.appendText('Value');
3140
+ * const tail = run.splitAt(5); // Split after "Name\t"
3141
+ * run.getText(); // "Name\t"
3142
+ * tail.getText(); // "Value"
3143
+ * ```
3144
+ */
3145
+ splitAt(offset: number): Run {
3146
+ const totalLength = this.getText().length;
3147
+
3148
+ // Edge case: split at or past end — return empty run
3149
+ if (offset >= totalLength) {
3150
+ return Run.createFromContent([], deepClone(this.formatting));
3151
+ }
3152
+
3153
+ // Edge case: split at or before start — move everything to new run
3154
+ if (offset <= 0) {
3155
+ const allContent = this.content;
3156
+ this.content = [];
3157
+ return Run.createFromContent(allContent, deepClone(this.formatting));
3158
+ }
3159
+
3160
+ const beforeContent: RunContent[] = [];
3161
+ const afterContent: RunContent[] = [];
3162
+ let charPos = 0;
3163
+ let splitDone = false;
3164
+
3165
+ for (const item of this.content) {
3166
+ if (splitDone) {
3167
+ afterContent.push(deepClone(item));
3168
+ continue;
3169
+ }
3170
+
3171
+ const itemLength = this.getContentItemLength(item);
3172
+
3173
+ if (charPos + itemLength <= offset) {
3174
+ // Entire item goes to "before"
3175
+ beforeContent.push(deepClone(item));
3176
+ charPos += itemLength;
3177
+ } else if (charPos >= offset) {
3178
+ // Entire item goes to "after"
3179
+ afterContent.push(deepClone(item));
3180
+ splitDone = true;
3181
+ } else {
3182
+ // Split is within this item — only text elements can be split mid-item
3183
+ if (item.type === 'text' && item.value) {
3184
+ const splitIndex = offset - charPos;
3185
+ beforeContent.push({ type: 'text', value: item.value.slice(0, splitIndex) });
3186
+ afterContent.push({ type: 'text', value: item.value.slice(splitIndex) });
3187
+ } else {
3188
+ // Non-text item at boundary goes to "after"
3189
+ afterContent.push(deepClone(item));
3190
+ }
3191
+ splitDone = true;
3192
+ }
3193
+ }
3194
+
3195
+ this.content = beforeContent;
3196
+ return Run.createFromContent(afterContent, deepClone(this.formatting));
3197
+ }
3198
+
3199
+ /**
3200
+ * Gets the character length contribution of a single content item.
3201
+ * Matches the mapping used by getText().
3202
+ * @internal
3203
+ */
3204
+ private getContentItemLength(item: RunContent): number {
3205
+ switch (item.type) {
3206
+ case 'text':
3207
+ return (item.value || '').length;
3208
+ case 'tab':
3209
+ case 'break':
3210
+ case 'carriageReturn':
3211
+ case 'softHyphen':
3212
+ case 'noBreakHyphen':
3213
+ return 1;
3214
+ default:
3215
+ return 0;
3216
+ }
3217
+ }
3218
+
2989
3219
  /**
2990
3220
  * Clears run formatting properties that conflict with a style definition.
2991
3221
  * Uses smart clearing: only removes properties that DIFFER from the style.