docxmlater 10.0.4 → 10.1.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 (63) hide show
  1. package/dist/core/Document.d.ts +22 -0
  2. package/dist/core/Document.d.ts.map +1 -1
  3. package/dist/core/Document.js +170 -26
  4. package/dist/core/Document.js.map +1 -1
  5. package/dist/core/DocumentParser.d.ts.map +1 -1
  6. package/dist/core/DocumentParser.js +64 -1
  7. package/dist/core/DocumentParser.js.map +1 -1
  8. package/dist/elements/Hyperlink.d.ts +6 -0
  9. package/dist/elements/Hyperlink.d.ts.map +1 -1
  10. package/dist/elements/Hyperlink.js +23 -0
  11. package/dist/elements/Hyperlink.js.map +1 -1
  12. package/dist/elements/StructuredDocumentTag.d.ts +23 -1
  13. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  14. package/dist/elements/StructuredDocumentTag.js +97 -0
  15. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  16. package/dist/elements/TableCell.d.ts +5 -0
  17. package/dist/elements/TableCell.d.ts.map +1 -1
  18. package/dist/elements/TableCell.js +13 -0
  19. package/dist/elements/TableCell.js.map +1 -1
  20. package/dist/elements/TableRow.d.ts +3 -0
  21. package/dist/elements/TableRow.d.ts.map +1 -1
  22. package/dist/elements/TableRow.js +10 -0
  23. package/dist/elements/TableRow.js.map +1 -1
  24. package/dist/formatting/AbstractNumbering.d.ts +4 -0
  25. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  26. package/dist/formatting/AbstractNumbering.js +15 -0
  27. package/dist/formatting/AbstractNumbering.js.map +1 -1
  28. package/dist/formatting/NumberingInstance.d.ts +6 -0
  29. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  30. package/dist/formatting/NumberingInstance.js +55 -1
  31. package/dist/formatting/NumberingInstance.js.map +1 -1
  32. package/dist/formatting/NumberingLevel.d.ts +4 -1
  33. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  34. package/dist/formatting/NumberingLevel.js +17 -0
  35. package/dist/formatting/NumberingLevel.js.map +1 -1
  36. package/dist/formatting/Style.d.ts +6 -0
  37. package/dist/formatting/Style.d.ts.map +1 -1
  38. package/dist/formatting/Style.js +20 -0
  39. package/dist/formatting/Style.js.map +1 -1
  40. package/dist/formatting/StylesManager.d.ts +23 -0
  41. package/dist/formatting/StylesManager.d.ts.map +1 -1
  42. package/dist/formatting/StylesManager.js +65 -0
  43. package/dist/formatting/StylesManager.js.map +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
  48. package/dist/tracking/DocumentTrackingContext.js +30 -7
  49. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/core/Document.ts +287 -31
  52. package/src/core/DocumentParser.ts +76 -1
  53. package/src/elements/Hyperlink.ts +47 -0
  54. package/src/elements/StructuredDocumentTag.ts +230 -1
  55. package/src/elements/TableCell.ts +36 -1
  56. package/src/elements/TableRow.ts +24 -1
  57. package/src/formatting/AbstractNumbering.ts +31 -0
  58. package/src/formatting/NumberingInstance.ts +88 -1
  59. package/src/formatting/NumberingLevel.ts +37 -3
  60. package/src/formatting/Style.ts +46 -0
  61. package/src/formatting/StylesManager.ts +125 -0
  62. package/src/index.ts +2 -2
  63. package/src/tracking/DocumentTrackingContext.ts +38 -7
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { XMLBuilder, XMLElement } from '../xml/XMLBuilder';
10
+ import { NumberingLevel } from './NumberingLevel';
10
11
 
11
12
  /**
12
13
  * Properties for creating a numbering instance
@@ -29,6 +30,7 @@ export class NumberingInstance {
29
30
  private numId: number;
30
31
  private abstractNumId: number;
31
32
  private levelOverrides = new Map<number, number>();
33
+ private fullLevelOverrides = new Map<number, NumberingLevel>();
32
34
 
33
35
  /**
34
36
  * Creates a new numbering instance
@@ -144,6 +146,44 @@ export class NumberingInstance {
144
146
  return this.levelOverrides.get(level);
145
147
  }
146
148
 
149
+ /**
150
+ * Sets a full level definition override for a specific level
151
+ * This replaces the entire level definition from the abstract numbering
152
+ * (ECMA-376 §17.9.8 - w:lvlOverride with full w:lvl child)
153
+ *
154
+ * @param level The level index (0-based)
155
+ * @param levelDef The full NumberingLevel definition to use as override
156
+ */
157
+ setFullLevelOverride(level: number, levelDef: NumberingLevel): this {
158
+ if (level < 0) {
159
+ throw new Error('Level index must be non-negative');
160
+ }
161
+ this.fullLevelOverrides.set(level, levelDef);
162
+ return this;
163
+ }
164
+
165
+ /**
166
+ * Gets a full level definition override for a specific level
167
+ */
168
+ getFullLevelOverride(level: number): NumberingLevel | undefined {
169
+ return this.fullLevelOverrides.get(level);
170
+ }
171
+
172
+ /**
173
+ * Gets all full level definition overrides
174
+ */
175
+ getFullLevelOverrides(): Map<number, NumberingLevel> {
176
+ return new Map(this.fullLevelOverrides);
177
+ }
178
+
179
+ /**
180
+ * Clears a full level override
181
+ */
182
+ clearFullLevelOverride(level: number): this {
183
+ this.fullLevelOverrides.delete(level);
184
+ return this;
185
+ }
186
+
147
187
  /**
148
188
  * Generates the WordprocessingML XML for this numbering instance
149
189
  */
@@ -157,6 +197,8 @@ export class NumberingInstance {
157
197
 
158
198
  // Add level overrides if any are set
159
199
  for (const [level, startValue] of this.levelOverrides) {
200
+ // Skip levels that have a full level override (they take precedence)
201
+ if (this.fullLevelOverrides.has(level)) continue;
160
202
  children.push({
161
203
  name: 'w:lvlOverride',
162
204
  attributes: { 'w:ilvl': level.toString() },
@@ -166,6 +208,23 @@ export class NumberingInstance {
166
208
  });
167
209
  }
168
210
 
211
+ // Add full level overrides
212
+ for (const [level, levelDef] of this.fullLevelOverrides) {
213
+ const overrideChildren: XMLElement[] = [];
214
+ // Include startOverride if also set for this level
215
+ if (this.levelOverrides.has(level)) {
216
+ overrideChildren.push(
217
+ XMLBuilder.wSelf('startOverride', { 'w:val': this.levelOverrides.get(level)!.toString() })
218
+ );
219
+ }
220
+ overrideChildren.push(levelDef.toXML());
221
+ children.push({
222
+ name: 'w:lvlOverride',
223
+ attributes: { 'w:ilvl': level.toString() },
224
+ children: overrideChildren,
225
+ });
226
+ }
227
+
169
228
  return XMLBuilder.w('num', { 'w:numId': this.numId.toString() }, children);
170
229
  }
171
230
 
@@ -204,9 +263,37 @@ export class NumberingInstance {
204
263
  }
205
264
  const abstractNumId = parseInt(abstractNumIdMatch[1], 10);
206
265
 
207
- return new NumberingInstance({
266
+ const instance = new NumberingInstance({
208
267
  numId,
209
268
  abstractNumId,
210
269
  });
270
+
271
+ // Parse level overrides (w:lvlOverride)
272
+ const lvlOverrideRegex = /<w:lvlOverride[^>]*w:ilvl="(\d+)"[^>]*>([\s\S]*?)<\/w:lvlOverride>/g;
273
+ let match: RegExpExecArray | null;
274
+ while ((match = lvlOverrideRegex.exec(xml)) !== null) {
275
+ const levelStr = match[1]!;
276
+ const content = match[2]!;
277
+ const level = parseInt(levelStr, 10);
278
+
279
+ // Check for startOverride
280
+ const startOverrideMatch = /<w:startOverride[^>]*w:val="([^"]+)"/.exec(content);
281
+ if (startOverrideMatch?.[1]) {
282
+ instance.setLevelOverride(level, parseInt(startOverrideMatch[1], 10));
283
+ }
284
+
285
+ // Check for full w:lvl element
286
+ const lvlMatch = /<w:lvl[^>]*>[\s\S]*?<\/w:lvl>/.exec(content);
287
+ if (lvlMatch) {
288
+ try {
289
+ const levelDef = NumberingLevel.fromXML(lvlMatch[0]);
290
+ instance.setFullLevelOverride(level, levelDef);
291
+ } catch {
292
+ // Skip invalid level definitions
293
+ }
294
+ }
295
+ }
296
+
297
+ return instance;
211
298
  }
212
299
  }
@@ -111,13 +111,16 @@ export interface NumberingLevelProperties {
111
111
  * - undefined (default): Restart when level-1 changes (standard behavior)
112
112
  */
113
113
  lvlRestart?: number;
114
+
115
+ /** Paragraph style ID linked to this numbering level (w:pStyle per ECMA-376 §17.9.23) */
116
+ pStyle?: string;
114
117
  }
115
118
 
116
119
  /**
117
120
  * Represents a single level in a numbering definition
118
121
  */
119
122
  export class NumberingLevel {
120
- private properties: Required<Omit<NumberingLevelProperties, 'lvlRestart' | 'underline'>> & Pick<NumberingLevelProperties, 'lvlRestart' | 'underline'>;
123
+ private properties: Required<Omit<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>> & Pick<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>;
121
124
 
122
125
  /**
123
126
  * Creates a new numbering level
@@ -149,6 +152,7 @@ export class NumberingLevel {
149
152
  italic: properties.italic !== undefined ? properties.italic : false,
150
153
  underline: properties.underline,
151
154
  lvlRestart: properties.lvlRestart, // undefined means default behavior (restart on level-1 change)
155
+ pStyle: properties.pStyle,
152
156
  };
153
157
 
154
158
  this.validate();
@@ -247,7 +251,7 @@ export class NumberingLevel {
247
251
  /**
248
252
  * Gets the level properties
249
253
  */
250
- getProperties(): Required<Omit<NumberingLevelProperties, 'lvlRestart' | 'underline'>> & Pick<NumberingLevelProperties, 'lvlRestart' | 'underline'> {
254
+ getProperties(): Required<Omit<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>> & Pick<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'> {
251
255
  return { ...this.properties };
252
256
  }
253
257
 
@@ -399,6 +403,23 @@ export class NumberingLevel {
399
403
  return this.properties.lvlRestart;
400
404
  }
401
405
 
406
+ /**
407
+ * Sets the paragraph style ID linked to this numbering level
408
+ * Links this level to a paragraph style definition (w:pStyle per ECMA-376 §17.9.23)
409
+ * @param styleId The paragraph style ID
410
+ */
411
+ setParagraphStyle(styleId: string): this {
412
+ this.properties.pStyle = styleId;
413
+ return this;
414
+ }
415
+
416
+ /**
417
+ * Gets the paragraph style ID linked to this numbering level
418
+ */
419
+ getParagraphStyle(): string | undefined {
420
+ return this.properties.pStyle;
421
+ }
422
+
402
423
  /**
403
424
  * Sets the numbering format (decimal, lowerLetter, bullet, etc.)
404
425
  * @param format The numbering format
@@ -447,7 +468,12 @@ export class NumberingLevel {
447
468
  );
448
469
  }
449
470
 
450
- // 4. pStyle not modeled, skipped
471
+ // 4. pStyle (paragraph style link)
472
+ if (this.properties.pStyle) {
473
+ children.push(
474
+ XMLBuilder.wSelf("pStyle", { "w:val": this.properties.pStyle })
475
+ );
476
+ }
451
477
 
452
478
  // 5. Legal numbering style
453
479
  if (this.properties.isLegalNumberingStyle) {
@@ -945,6 +971,13 @@ export class NumberingLevel {
945
971
  lvlRestart = parseInt(lvlRestartMatch[1], 10);
946
972
  }
947
973
 
974
+ // Extract pStyle (paragraph style link)
975
+ let pStyle: string | undefined;
976
+ const pStyleMatch = /<w:pStyle[^>]*w:val="([^"]+)"/.exec(xml);
977
+ if (pStyleMatch?.[1]) {
978
+ pStyle = pStyleMatch[1];
979
+ }
980
+
948
981
  // Extract indentation from <w:pPr><w:ind>
949
982
  let leftIndent = 720 + level * 360; // default
950
983
  let hangingIndent = 360; // default
@@ -1001,6 +1034,7 @@ export class NumberingLevel {
1001
1034
  underline,
1002
1035
  color,
1003
1036
  lvlRestart,
1037
+ pStyle,
1004
1038
  });
1005
1039
  }
1006
1040
  }
@@ -222,6 +222,10 @@ export interface StyleProperties {
222
222
  locked?: boolean;
223
223
  /** Personal - user-specific style */
224
224
  personal?: boolean;
225
+ /** Personal compose - style for composing new messages */
226
+ personalCompose?: boolean;
227
+ /** Personal reply - style for replying to messages */
228
+ personalReply?: boolean;
225
229
  /** Link - linked character/paragraph style ID */
226
230
  link?: string;
227
231
  /** Auto-redefine - update style from manual formatting */
@@ -440,6 +444,38 @@ export class Style {
440
444
  return this;
441
445
  }
442
446
 
447
+ /**
448
+ * Sets whether this is a personal compose style (for composing new messages)
449
+ * @param enabled - True to mark as personal compose
450
+ */
451
+ setPersonalCompose(enabled: boolean): this {
452
+ this.properties.personalCompose = enabled;
453
+ return this;
454
+ }
455
+
456
+ /**
457
+ * Gets whether this is a personal compose style
458
+ */
459
+ getPersonalCompose(): boolean {
460
+ return this.properties.personalCompose === true;
461
+ }
462
+
463
+ /**
464
+ * Sets whether this is a personal reply style (for replying to messages)
465
+ * @param enabled - True to mark as personal reply
466
+ */
467
+ setPersonalReply(enabled: boolean): this {
468
+ this.properties.personalReply = enabled;
469
+ return this;
470
+ }
471
+
472
+ /**
473
+ * Gets whether this is a personal reply style
474
+ */
475
+ getPersonalReply(): boolean {
476
+ return this.properties.personalReply === true;
477
+ }
478
+
443
479
  /**
444
480
  * Sets the linked style ID (for character/paragraph style linking)
445
481
  * @param styleId - ID of the linked style
@@ -794,6 +830,16 @@ export class Style {
794
830
  styleChildren.push(XMLBuilder.wSelf("personal"));
795
831
  }
796
832
 
833
+ // personalCompose - Style for composing new messages
834
+ if (this.properties.personalCompose) {
835
+ styleChildren.push(XMLBuilder.wSelf("personalCompose"));
836
+ }
837
+
838
+ // personalReply - Style for replying to messages
839
+ if (this.properties.personalReply) {
840
+ styleChildren.push(XMLBuilder.wSelf("personalReply"));
841
+ }
842
+
797
843
  // Add paragraph properties
798
844
  if (this.properties.paragraphFormatting || this.properties.numPr) {
799
845
  const pPr = this.generateParagraphProperties(
@@ -8,6 +8,43 @@ import { XMLBuilder } from "../xml/XMLBuilder";
8
8
  import { XMLParser } from "../xml/XMLParser";
9
9
  import { Style, StyleType } from "./Style";
10
10
 
11
+ /**
12
+ * Configuration for latent styles (w:latentStyles per ECMA-376 §17.7.4.6)
13
+ * Controls which built-in styles are shown in the Word UI gallery
14
+ */
15
+ export interface LatentStylesConfig {
16
+ /** Default locked state for built-in styles */
17
+ defaultLockedState?: boolean;
18
+ /** Default UI priority for built-in styles */
19
+ defaultUiPriority?: number;
20
+ /** Default semi-hidden state */
21
+ defaultSemiHidden?: boolean;
22
+ /** Default unhide-when-used state */
23
+ defaultUnhideWhenUsed?: boolean;
24
+ /** Default quick format flag */
25
+ defaultQFormat?: boolean;
26
+ /** Total count of style definitions */
27
+ count?: number;
28
+ }
29
+
30
+ /**
31
+ * Exception to latent style defaults (w:lsdException per ECMA-376 §17.7.4.7)
32
+ */
33
+ export interface LatentStyleException {
34
+ /** Style name */
35
+ name: string;
36
+ /** Override: locked state */
37
+ locked?: boolean;
38
+ /** Override: UI priority */
39
+ uiPriority?: number;
40
+ /** Override: semi-hidden */
41
+ semiHidden?: boolean;
42
+ /** Override: unhide when used */
43
+ unhideWhenUsed?: boolean;
44
+ /** Override: quick format */
45
+ qFormat?: boolean;
46
+ }
47
+
11
48
  /**
12
49
  * Result of XML validation
13
50
  */
@@ -37,6 +74,10 @@ export class StylesManager {
37
74
  // Track which specific styles have been modified (for selective merging)
38
75
  private _modifiedStyleIds = new Set<string>();
39
76
 
77
+ // Latent styles configuration
78
+ private latentStyles?: LatentStylesConfig;
79
+ private latentStyleExceptions: LatentStyleException[] = [];
80
+
40
81
  /**
41
82
  * Registry of built-in style factory functions
42
83
  * Maps style ID to factory function for lazy loading
@@ -411,6 +452,46 @@ export class StylesManager {
411
452
  return style;
412
453
  }
413
454
 
455
+ /**
456
+ * Sets the latent styles configuration
457
+ * @param config - Latent styles configuration
458
+ */
459
+ setLatentStyles(config: LatentStylesConfig): this {
460
+ this.latentStyles = config;
461
+ this._modified = true;
462
+ return this;
463
+ }
464
+
465
+ /**
466
+ * Gets the latent styles configuration
467
+ */
468
+ getLatentStyles(): LatentStylesConfig | undefined {
469
+ return this.latentStyles;
470
+ }
471
+
472
+ /**
473
+ * Adds a latent style exception
474
+ * @param exception - The exception to add
475
+ */
476
+ addLatentStyleException(exception: LatentStyleException): this {
477
+ // Replace existing exception for same name
478
+ const idx = this.latentStyleExceptions.findIndex(e => e.name === exception.name);
479
+ if (idx >= 0) {
480
+ this.latentStyleExceptions[idx] = exception;
481
+ } else {
482
+ this.latentStyleExceptions.push(exception);
483
+ }
484
+ this._modified = true;
485
+ return this;
486
+ }
487
+
488
+ /**
489
+ * Gets all latent style exceptions
490
+ */
491
+ getLatentStyleExceptions(): LatentStyleException[] {
492
+ return [...this.latentStyleExceptions];
493
+ }
494
+
414
495
  /**
415
496
  * Generates the complete styles.xml file
416
497
  * @returns XML string for word/styles.xml
@@ -424,6 +505,11 @@ export class StylesManager {
424
505
  // Add document defaults
425
506
  stylesChildren.push(this.generateDocDefaults());
426
507
 
508
+ // Add latent styles if configured (per ECMA-376 CT_Styles order: docDefaults, latentStyles, style*)
509
+ if (this.latentStyles) {
510
+ stylesChildren.push(this.generateLatentStyles());
511
+ }
512
+
427
513
  // Add all styles
428
514
  for (const style of this.getAllStyles()) {
429
515
  stylesChildren.push(style.toXML());
@@ -481,6 +567,45 @@ export class StylesManager {
481
567
  ]);
482
568
  }
483
569
 
570
+ /**
571
+ * Generates the latent styles XML element
572
+ */
573
+ private generateLatentStyles() {
574
+ if (!this.latentStyles) return XMLBuilder.w("latentStyles", {}, []);
575
+
576
+ const attrs: Record<string, string> = {};
577
+ if (this.latentStyles.defaultLockedState !== undefined) {
578
+ attrs["w:defLockedState"] = this.latentStyles.defaultLockedState ? "1" : "0";
579
+ }
580
+ if (this.latentStyles.defaultUiPriority !== undefined) {
581
+ attrs["w:defUIPriority"] = this.latentStyles.defaultUiPriority.toString();
582
+ }
583
+ if (this.latentStyles.defaultSemiHidden !== undefined) {
584
+ attrs["w:defSemiHidden"] = this.latentStyles.defaultSemiHidden ? "1" : "0";
585
+ }
586
+ if (this.latentStyles.defaultUnhideWhenUsed !== undefined) {
587
+ attrs["w:defUnhideWhenUsed"] = this.latentStyles.defaultUnhideWhenUsed ? "1" : "0";
588
+ }
589
+ if (this.latentStyles.defaultQFormat !== undefined) {
590
+ attrs["w:defQFormat"] = this.latentStyles.defaultQFormat ? "1" : "0";
591
+ }
592
+ if (this.latentStyles.count !== undefined) {
593
+ attrs["w:count"] = this.latentStyles.count.toString();
594
+ }
595
+
596
+ const children = this.latentStyleExceptions.map(exc => {
597
+ const excAttrs: Record<string, string> = { "w:name": exc.name };
598
+ if (exc.locked !== undefined) excAttrs["w:locked"] = exc.locked ? "1" : "0";
599
+ if (exc.uiPriority !== undefined) excAttrs["w:uiPriority"] = exc.uiPriority.toString();
600
+ if (exc.semiHidden !== undefined) excAttrs["w:semiHidden"] = exc.semiHidden ? "1" : "0";
601
+ if (exc.unhideWhenUsed !== undefined) excAttrs["w:unhideWhenUsed"] = exc.unhideWhenUsed ? "1" : "0";
602
+ if (exc.qFormat !== undefined) excAttrs["w:qFormat"] = exc.qFormat ? "1" : "0";
603
+ return XMLBuilder.wSelf("lsdException", excAttrs);
604
+ });
605
+
606
+ return XMLBuilder.w("latentStyles", attrs, children);
607
+ }
608
+
484
609
  /**
485
610
  * Creates a new StylesManager with built-in styles
486
611
  * @returns New StylesManager instance
package/src/index.ts CHANGED
@@ -103,7 +103,7 @@ export {
103
103
  isHyperlinkInstruction,
104
104
  ParsedHyperlinkInstruction,
105
105
  } from './elements/FieldHelpers';
106
- export { StructuredDocumentTag, SDTProperties, SDTLockType, SDTContent } from './elements/StructuredDocumentTag';
106
+ export { StructuredDocumentTag, SDTProperties, SDTLockType, SDTContent, SDTPlaceholder, SDTDataBinding, ContentControlType } from './elements/StructuredDocumentTag';
107
107
  export { TableOfContents, TOCProperties } from './elements/TableOfContents';
108
108
  export { TableOfContentsElement } from './elements/TableOfContentsElement';
109
109
  export { AlternateContent } from './elements/AlternateContent';
@@ -181,7 +181,7 @@ export {
181
181
  // =============================================================================
182
182
 
183
183
  export { Style, StyleType, StyleProperties } from './formatting/Style';
184
- export { StylesManager, ValidationResult } from './formatting/StylesManager';
184
+ export { StylesManager, ValidationResult, LatentStylesConfig, LatentStyleException } from './formatting/StylesManager';
185
185
  export {
186
186
  NumberingLevel,
187
187
  NumberFormat,
@@ -343,16 +343,47 @@ export class DocumentTrackingContext implements TrackingContext {
343
343
  }
344
344
 
345
345
  // Apply tblPrChange to each Table
346
+ // Per ECMA-376 §17.13.5.36, tblPrChange must contain FULL previous tblPr,
347
+ // not just the delta of changed properties.
346
348
  for (const [table, changes] of tableChanges) {
347
- this.applyElementPrChange(changes, (prevProps, getNextId, date) => {
348
- const existing = table.getTblPrChange();
349
- if (existing) {
350
- const merged = { ...(existing.previousProperties || {}), ...prevProps };
351
- table.setTblPrChange({ ...existing, previousProperties: merged });
349
+ // Build full snapshot: start from current formatting, roll back changed properties
350
+ const currentFormatting = table.getFormatting();
351
+ const fullPrevProps: Record<string, unknown> = {};
352
+
353
+ for (const [key, value] of Object.entries(currentFormatting)) {
354
+ if (value !== undefined) {
355
+ fullPrevProps[key] = value;
356
+ }
357
+ }
358
+
359
+ // Roll back changed properties to their previous values
360
+ let latestTimestamp = 0;
361
+ for (const change of changes) {
362
+ if (change.previousValue !== undefined) {
363
+ fullPrevProps[change.property] = change.previousValue;
352
364
  } else {
353
- table.setTblPrChange({ author: this.author, date, id: String(getNextId()), previousProperties: prevProps });
365
+ delete fullPrevProps[change.property];
354
366
  }
355
- });
367
+ if (change.timestamp > latestTimestamp) {
368
+ latestTimestamp = change.timestamp;
369
+ }
370
+ }
371
+
372
+ const date = formatDateForXml(new Date(latestTimestamp));
373
+
374
+ const existing = table.getTblPrChange();
375
+ if (existing) {
376
+ // Merge: existing previous state takes precedence (it's the ORIGINAL baseline)
377
+ const merged = { ...fullPrevProps, ...(existing.previousProperties || {}) };
378
+ table.setTblPrChange({ ...existing, previousProperties: merged });
379
+ } else {
380
+ table.setTblPrChange({
381
+ author: this.author,
382
+ date,
383
+ id: String(this.revisionManager.consumeNextId()),
384
+ previousProperties: fullPrevProps,
385
+ });
386
+ }
356
387
  }
357
388
 
358
389
  // Apply trPrChange to each TableRow