@ansonlai/docx-redline-js 0.1.4 → 0.2.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 (47) hide show
  1. package/AGENTS.md +53 -4
  2. package/ARCHITECTURE.md +75 -11
  3. package/README.md +62 -3
  4. package/core/redline-validation.js +156 -0
  5. package/core/types.js +35 -8
  6. package/core/word-xml.js +90 -0
  7. package/dist/docx-redline-js.esm.js +3195 -2592
  8. package/dist/docx-redline-js.esm.js.map +4 -4
  9. package/dist/docx-redline-js.esm.min.js +71 -67
  10. package/dist/docx-redline-js.esm.min.js.map +4 -4
  11. package/docs/VALIDATION.md +104 -0
  12. package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
  13. package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
  14. package/docs/plans/2026-05-31-architectural changes.md +591 -0
  15. package/engine/format-application.js +13 -14
  16. package/engine/format-span-application.js +7 -6
  17. package/engine/formatting-removal.js +15 -12
  18. package/engine/oxml-engine.js +146 -55
  19. package/engine/reconstruction-mapper.js +35 -8
  20. package/engine/reconstruction-mode.js +14 -13
  21. package/engine/reconstruction-writer.js +97 -78
  22. package/engine/rpr-helpers.js +34 -32
  23. package/engine/run-builders.js +150 -39
  24. package/engine/surgical-diff-application.js +216 -0
  25. package/engine/surgical-mode.js +84 -519
  26. package/engine/surgical-run-splitting.js +96 -0
  27. package/engine/surgical-spans.js +169 -0
  28. package/engine/table-cell-context.js +15 -13
  29. package/engine/table-mode.js +39 -35
  30. package/index.d.ts +172 -0
  31. package/index.js +50 -47
  32. package/package.json +10 -2
  33. package/pipeline/ingestion-export.js +1 -0
  34. package/pipeline/ingestion-paragraph.js +37 -12
  35. package/pipeline/ingestion-table.js +11 -8
  36. package/scripts/build.mjs +40 -0
  37. package/scripts/check-types.mjs +29 -0
  38. package/scripts/export-validation-fixtures.mjs +125 -0
  39. package/scripts/lib/minimal-zip.mjs +155 -0
  40. package/scripts/run-tests.mjs +43 -0
  41. package/scripts/validate-fixtures-xsd.sh +37 -0
  42. package/scripts/word-com-differential.ps1 +133 -0
  43. package/scripts/word-com-smoke.ps1 +48 -0
  44. package/services/comment-locator.js +10 -9
  45. package/services/revision-comment-management.js +115 -1
  46. package/services/standalone-operation-runner.js +119 -69
  47. package/services/table-reconciliation.js +7 -8
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  import { createParser, createSerializer } from '../adapters/xml-adapter.js';
9
+ import { createRevisionMetadata, seedRevisionIdsFromDocument } from '../core/types.js';
10
+ import { createWordElement } from '../core/word-xml.js';
9
11
  import {
10
12
  applyRedlineToOxml,
11
13
  reconcileMarkdownTableOoxml,
@@ -66,6 +68,70 @@ function normalizeBodySectionOrder(xmlDoc) {
66
68
  normalizeBodySectionOrderStandalone(xmlDoc);
67
69
  }
68
70
 
71
+ function directFieldCharType(node) {
72
+ if (!node || node.nodeType !== 1 || node.namespaceURI !== NS_W || node.localName !== 'r') return '';
73
+ const fldChar = Array.from(node.childNodes || []).find(
74
+ child => child && child.nodeType === 1 && child.namespaceURI === NS_W && child.localName === 'fldChar'
75
+ ) || null;
76
+ return fldChar?.getAttribute('w:fldCharType') || fldChar?.getAttribute('fldCharType') || '';
77
+ }
78
+
79
+ function hasInstrText(node) {
80
+ if (!node || node.nodeType !== 1) return false;
81
+ return Array.from(node.childNodes || []).some(
82
+ child => child && child.nodeType === 1 && child.namespaceURI === NS_W && child.localName === 'instrText'
83
+ );
84
+ }
85
+
86
+ function removeProofErrNodes(paragraph) {
87
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, 'proofErr') || [])) {
88
+ node.parentNode?.removeChild(node);
89
+ }
90
+ }
91
+
92
+ function unlinkFieldsInParagraph(paragraph) {
93
+ const children = Array.from(paragraph?.childNodes || []);
94
+ let inField = false;
95
+ let seenSeparate = false;
96
+
97
+ for (const child of children) {
98
+ if (child.nodeType !== 1) continue;
99
+
100
+ const fieldType = directFieldCharType(child);
101
+ if (fieldType === 'begin') {
102
+ inField = true;
103
+ seenSeparate = false;
104
+ child.parentNode?.removeChild(child);
105
+ continue;
106
+ }
107
+
108
+ if (!inField) continue;
109
+
110
+ if (fieldType === 'separate') {
111
+ seenSeparate = true;
112
+ child.parentNode?.removeChild(child);
113
+ continue;
114
+ }
115
+
116
+ if (fieldType === 'end') {
117
+ inField = false;
118
+ seenSeparate = false;
119
+ child.parentNode?.removeChild(child);
120
+ continue;
121
+ }
122
+
123
+ if (!seenSeparate || hasInstrText(child)) {
124
+ child.parentNode?.removeChild(child);
125
+ }
126
+ }
127
+ }
128
+
129
+ function preprocessRedlineTargetParagraph(targetParagraph) {
130
+ if (!targetParagraph) return;
131
+ removeProofErrNodes(targetParagraph);
132
+ unlinkFieldsInParagraph(targetParagraph);
133
+ }
134
+
69
135
  function getDirectWordChild(element, localName) {
70
136
  if (!element) return null;
71
137
  return Array.from(element.childNodes || []).find(
@@ -97,51 +163,37 @@ function computeTableStructuralDedupeKey(xmlDoc, containingTable, modifiedText)
97
163
  return `table:${tableIndex}|payload:${normalizedPayload}`;
98
164
  }
99
165
 
100
- function getNextTrackedChangeId(xmlDoc) {
101
- let maxId = 999;
102
- const revisionNodes = [
103
- ...Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'ins')),
104
- ...Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'del'))
105
- ];
106
- for (const node of revisionNodes) {
107
- const raw = node.getAttribute('w:id') || node.getAttribute('id') || '';
108
- const parsed = Number.parseInt(raw, 10);
109
- if (Number.isFinite(parsed)) maxId = Math.max(maxId, parsed);
110
- }
111
- return maxId + 1;
112
- }
113
-
114
166
  function ensureListProperties(xmlDoc, paragraph, ilvl, numId) {
115
167
  let pPr = getDirectWordChild(paragraph, 'pPr');
116
168
  if (!pPr) {
117
- pPr = xmlDoc.createElementNS(NS_W, 'w:pPr');
169
+ pPr = createWordElement(xmlDoc, 'w:pPr');
118
170
  paragraph.insertBefore(pPr, paragraph.firstChild);
119
171
  }
120
172
 
121
173
  let numPr = getDirectWordChild(pPr, 'numPr');
122
174
  if (!numPr) {
123
- numPr = xmlDoc.createElementNS(NS_W, 'w:numPr');
175
+ numPr = createWordElement(xmlDoc, 'w:numPr');
124
176
  pPr.appendChild(numPr);
125
177
  }
126
178
 
127
179
  let ilvlEl = getDirectWordChild(numPr, 'ilvl');
128
180
  if (!ilvlEl) {
129
- ilvlEl = xmlDoc.createElementNS(NS_W, 'w:ilvl');
181
+ ilvlEl = createWordElement(xmlDoc, 'w:ilvl');
130
182
  numPr.appendChild(ilvlEl);
131
183
  }
132
184
  ilvlEl.setAttribute('w:val', String(Math.max(0, Number.parseInt(ilvl, 10) || 0)));
133
185
 
134
186
  let numIdEl = getDirectWordChild(numPr, 'numId');
135
187
  if (!numIdEl) {
136
- numIdEl = xmlDoc.createElementNS(NS_W, 'w:numId');
188
+ numIdEl = createWordElement(xmlDoc, 'w:numId');
137
189
  numPr.appendChild(numIdEl);
138
190
  }
139
191
  numIdEl.setAttribute('w:val', String(numId));
140
192
  }
141
193
 
142
- function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionId, author, dateIso, options = {}) {
194
+ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMetadata, author, options = {}) {
143
195
  const generateRedlines = options.generateRedlines !== false;
144
- const paragraph = xmlDoc.createElementNS(NS_W, 'w:p');
196
+ const paragraph = createWordElement(xmlDoc, 'w:p');
145
197
 
146
198
  const anchorPPr = getDirectWordChild(anchorParagraph, 'pPr');
147
199
  if (anchorPPr) {
@@ -149,23 +201,24 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionId,
149
201
  }
150
202
  ensureListProperties(xmlDoc, paragraph, entry.ilvl, entry.numId);
151
203
 
152
- const run = xmlDoc.createElementNS(NS_W, 'w:r');
204
+ const run = createWordElement(xmlDoc, 'w:r');
153
205
  const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
154
206
  const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
155
207
  if (anchorRunPr) {
156
208
  run.appendChild(anchorRunPr.cloneNode(true));
157
209
  }
158
210
 
159
- const textNode = xmlDoc.createElementNS(NS_W, 'w:t');
211
+ const textNode = createWordElement(xmlDoc, 'w:t');
160
212
  const safeText = String(entry.text || '').trim();
161
213
  if (/^\s|\s$/.test(safeText)) textNode.setAttribute('xml:space', 'preserve');
162
214
  textNode.textContent = safeText;
163
215
  run.appendChild(textNode);
164
216
  if (generateRedlines) {
165
- const ins = xmlDoc.createElementNS(NS_W, 'w:ins');
166
- ins.setAttribute('w:id', String(revisionId));
167
- ins.setAttribute('w:author', author || 'Browser Demo AI');
168
- ins.setAttribute('w:date', dateIso);
217
+ const metadata = revisionMetadata || createRevisionMetadata(author);
218
+ const ins = createWordElement(xmlDoc, 'w:ins');
219
+ ins.setAttribute('w:id', String(metadata.id));
220
+ ins.setAttribute('w:author', metadata.author);
221
+ ins.setAttribute('w:date', metadata.date);
169
222
  ins.appendChild(run);
170
223
  paragraph.appendChild(ins);
171
224
  } else {
@@ -287,9 +340,6 @@ function applyExplicitRangeListInsertions({
287
340
  if (!parent || explicitRangeParagraphs.some(paragraph => paragraph.parentNode !== parent)) return false;
288
341
 
289
342
  const tailInsertionPoint = explicitRangeParagraphs[explicitRangeParagraphs.length - 1].nextSibling;
290
- const dateIso = generateRedlines ? new Date().toISOString() : null;
291
- let revisionId = generateRedlines ? getNextTrackedChangeId(xmlDoc) : null;
292
-
293
343
  for (const entry of insertionEntries) {
294
344
  const referenceParagraph = entry.insertBeforeOriginalIndex != null
295
345
  ? explicitRangeParagraphs[entry.insertBeforeOriginalIndex]
@@ -305,12 +355,10 @@ function applyExplicitRangeListInsertions({
305
355
  numId: entry.numId,
306
356
  text: entry.text
307
357
  },
308
- revisionId,
358
+ generateRedlines ? createRevisionMetadata(author) : null,
309
359
  author,
310
- dateIso,
311
360
  { generateRedlines }
312
361
  );
313
- if (generateRedlines) revisionId += 1;
314
362
 
315
363
  if (entry.insertBeforeOriginalIndex != null) {
316
364
  parent.insertBefore(listParagraph, referenceParagraph);
@@ -426,21 +474,22 @@ function deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, modi
426
474
  return null;
427
475
  }
428
476
 
429
- function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionId, author, dateIso, options = {}) {
477
+ function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionMetadata, author, options = {}) {
430
478
  const generateRedlines = options.generateRedlines !== false;
431
- const paragraph = xmlDoc.createElementNS(NS_W, 'w:p');
432
- const run = xmlDoc.createElementNS(NS_W, 'w:r');
433
- const textNode = xmlDoc.createElementNS(NS_W, 'w:t');
479
+ const paragraph = createWordElement(xmlDoc, 'w:p');
480
+ const run = createWordElement(xmlDoc, 'w:r');
481
+ const textNode = createWordElement(xmlDoc, 'w:t');
434
482
  const safeText = String(text || '');
435
483
  if (/^\s|\s$/.test(safeText)) textNode.setAttribute('xml:space', 'preserve');
436
484
  textNode.textContent = safeText;
437
485
  run.appendChild(textNode);
438
486
 
439
487
  if (generateRedlines) {
440
- const ins = xmlDoc.createElementNS(NS_W, 'w:ins');
441
- ins.setAttribute('w:id', String(revisionId));
442
- ins.setAttribute('w:author', author || 'Browser Demo AI');
443
- ins.setAttribute('w:date', dateIso);
488
+ const metadata = revisionMetadata || createRevisionMetadata(author);
489
+ const ins = createWordElement(xmlDoc, 'w:ins');
490
+ ins.setAttribute('w:id', String(metadata.id));
491
+ ins.setAttribute('w:author', metadata.author);
492
+ ins.setAttribute('w:date', metadata.date);
444
493
  ins.appendChild(run);
445
494
  paragraph.appendChild(ins);
446
495
  } else {
@@ -451,31 +500,32 @@ function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionId, author, d
451
500
  }
452
501
 
453
502
  function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
454
- const paragraph = xmlDoc.createElementNS(NS_W, 'w:p');
503
+ const paragraph = createWordElement(xmlDoc, 'w:p');
455
504
  const anchorPPr = getDirectWordChild(anchorParagraph, 'pPr');
456
505
  if (anchorPPr) paragraph.appendChild(anchorPPr.cloneNode(true));
457
506
 
458
- const run = xmlDoc.createElementNS(NS_W, 'w:r');
507
+ const run = createWordElement(xmlDoc, 'w:r');
459
508
  const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
460
509
  const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
461
510
  if (anchorRunPr) run.appendChild(anchorRunPr.cloneNode(true));
462
511
 
463
- const textNode = xmlDoc.createElementNS(NS_W, 'w:t');
512
+ const textNode = createWordElement(xmlDoc, 'w:t');
464
513
  textNode.textContent = '';
465
514
  run.appendChild(textNode);
466
515
  paragraph.appendChild(run);
467
516
  return paragraph;
468
517
  }
469
518
 
470
- function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionId, author, dateIso) {
471
- const wrappedParagraph = xmlDoc.createElementNS(NS_W, 'w:p');
519
+ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, author) {
520
+ const wrappedParagraph = createWordElement(xmlDoc, 'w:p');
472
521
  const pPr = getDirectWordChild(paragraph, 'pPr');
473
522
  if (pPr) wrappedParagraph.appendChild(pPr.cloneNode(true));
474
523
 
475
- const ins = xmlDoc.createElementNS(NS_W, 'w:ins');
476
- ins.setAttribute('w:id', String(revisionId));
477
- ins.setAttribute('w:author', author || 'Browser Demo AI');
478
- ins.setAttribute('w:date', dateIso);
524
+ const ins = createWordElement(xmlDoc, 'w:ins');
525
+ const metadata = revisionMetadata || createRevisionMetadata(author);
526
+ ins.setAttribute('w:id', String(metadata.id));
527
+ ins.setAttribute('w:author', metadata.author);
528
+ ins.setAttribute('w:date', metadata.date);
479
529
 
480
530
  for (const child of Array.from(paragraph.childNodes || [])) {
481
531
  if (child?.nodeType === 1 && child.namespaceURI === NS_W && child.localName === 'pPr') continue;
@@ -486,7 +536,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionId, author,
486
536
  return wrappedParagraph;
487
537
  }
488
538
 
489
- async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisionId, author, dateIso, options = {}) {
539
+ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisionMetadata, author, options = {}) {
490
540
  const generateRedlines = options.generateRedlines !== false;
491
541
  const serializer = createSerializer();
492
542
  const templateParagraph = buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph);
@@ -513,9 +563,8 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisi
513
563
  return buildFallbackInsertedPlainParagraph(
514
564
  xmlDoc,
515
565
  text,
516
- revisionId,
566
+ revisionMetadata,
517
567
  author,
518
- dateIso,
519
568
  { generateRedlines }
520
569
  );
521
570
  }
@@ -524,7 +573,7 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisi
524
573
  return sourceParagraph;
525
574
  }
526
575
 
527
- return wrapParagraphContentInInsertion(xmlDoc, sourceParagraph, revisionId, author, dateIso);
576
+ return wrapParagraphContentInInsertion(xmlDoc, sourceParagraph, revisionMetadata, author);
528
577
  }
529
578
 
530
579
  async function tryExplicitDecimalHeaderListConversion({
@@ -772,8 +821,10 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
772
821
  const parser = createParser();
773
822
  const serializer = createSerializer();
774
823
  const xmlDoc = parser.parseFromString(documentXml, 'application/xml');
824
+ seedRevisionIdsFromDocument(xmlDoc);
775
825
  const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'redline', runtimeContext, { onInfo, onWarn });
776
826
  const targetParagraph = resolved.paragraph;
827
+ preprocessRedlineTargetParagraph(targetParagraph);
777
828
  const currentParagraphText = getParagraphText(targetParagraph).trim();
778
829
  const containingTable = findContainingWordElement(targetParagraph, 'tbl');
779
830
  const rawTableStructuralCandidate = !!containingTable
@@ -868,8 +919,6 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
868
919
  const parent = targetParagraph.parentNode;
869
920
  if (!parent) throw new Error('Target paragraph has no parent for adjacency list insertion');
870
921
 
871
- const dateIso = generateRedlines ? new Date().toISOString() : null;
872
- const revisionId = generateRedlines ? getNextTrackedChangeId(xmlDoc) : null;
873
922
  const listParagraph = buildInsertedListParagraph(
874
923
  xmlDoc,
875
924
  targetParagraph,
@@ -879,9 +928,8 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
879
928
  markerType: 'numbered',
880
929
  text: adjacencyInsertionCandidate.text
881
930
  },
882
- revisionId,
931
+ generateRedlines ? createRevisionMetadata(author) : null,
883
932
  author,
884
- dateIso,
885
933
  { generateRedlines }
886
934
  );
887
935
 
@@ -904,8 +952,6 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
904
952
  const parent = targetParagraph.parentNode;
905
953
  if (!parent) throw new Error('Target paragraph has no parent for plain adjacency insertion');
906
954
 
907
- const dateIso = generateRedlines ? new Date().toISOString() : null;
908
- let revisionId = generateRedlines ? getNextTrackedChangeId(xmlDoc) : null;
909
955
  const insertionPoint = plainAdjacencyInsertionCandidate.position === 'before'
910
956
  ? targetParagraph
911
957
  : targetParagraph.nextSibling;
@@ -915,13 +961,11 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
915
961
  xmlDoc,
916
962
  targetParagraph,
917
963
  paragraphText,
918
- revisionId,
964
+ generateRedlines ? createRevisionMetadata(author) : null,
919
965
  author,
920
- dateIso,
921
966
  { generateRedlines }
922
967
  );
923
968
  parent.insertBefore(xmlDoc.importNode(plainParagraph, true), insertionPoint);
924
- if (generateRedlines) revisionId += 1;
925
969
  }
926
970
 
927
971
  normalizeBodySectionOrder(xmlDoc);
@@ -943,19 +987,15 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
943
987
  const parent = targetParagraph.parentNode;
944
988
  if (!parent) throw new Error('Target paragraph has no parent for list insertion');
945
989
  const insertionPoint = targetParagraph.nextSibling;
946
- const dateIso = generateRedlines ? new Date().toISOString() : null;
947
- let revisionId = generateRedlines ? getNextTrackedChangeId(xmlDoc) : null;
948
990
  for (const entry of insertionOnlyPlan.entries) {
949
991
  const listParagraph = buildInsertedListParagraph(
950
992
  xmlDoc,
951
993
  targetParagraph,
952
994
  { ...entry, numId: insertionOnlyPlan.numId },
953
- revisionId,
995
+ generateRedlines ? createRevisionMetadata(author) : null,
954
996
  author,
955
- dateIso,
956
997
  { generateRedlines }
957
998
  );
958
- if (generateRedlines) revisionId += 1;
959
999
  parent.insertBefore(listParagraph, insertionPoint);
960
1000
  }
961
1001
  normalizeBodySectionOrder(xmlDoc);
@@ -1029,14 +1069,24 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
1029
1069
  ? await reconcileMarkdownTableOoxml(scopedXml, originalTextForApply, effectiveModifiedText, {
1030
1070
  author,
1031
1071
  generateRedlines,
1072
+ existingRevisions: options.existingRevisions,
1032
1073
  _isolatedTableCell: useTableScope
1033
1074
  })
1034
1075
  : await applyRedlineToOxml(scopedXml, originalTextForApply, effectiveModifiedText, {
1035
1076
  author,
1036
1077
  generateRedlines,
1078
+ existingRevisions: options.existingRevisions,
1037
1079
  _isolatedTableCell: useTableScope
1038
1080
  });
1039
- if (!result?.hasChanges) return { documentXml, hasChanges: false, numberingXml: null };
1081
+ if (!result?.hasChanges) {
1082
+ return {
1083
+ documentXml,
1084
+ hasChanges: false,
1085
+ numberingXml: null,
1086
+ status: result?.status || 'no-op',
1087
+ error: result?.error
1088
+ };
1089
+ }
1040
1090
  if (result.useNativeApi && !result.oxml) {
1041
1091
  const warning = 'Format-only fallback requires native Word API; browser demo skipped this operation.';
1042
1092
  onWarn(`[WARN] ${warning}`);
@@ -1074,7 +1124,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
1074
1124
  if (rawTableStructuralDedupeKey && tableStructuralDedupes && (useTableScope || containingTable)) {
1075
1125
  tableStructuralDedupes.add(rawTableStructuralDedupeKey);
1076
1126
  }
1077
- return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml };
1127
+ return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml, status: 'ok' };
1078
1128
  }
1079
1129
 
1080
1130
  async function applyHighlightToParagraphByExactText(documentXml, targetText, textToHighlight, color, author, targetRef = null, runtimeContext = null, options = {}) {
@@ -7,7 +7,7 @@
7
7
  import { computeWordLevelDiffOps } from '../pipeline/diff-engine.js';
8
8
  import { splitRunsAtDiffBoundaries, applyPatches } from '../pipeline/patching.js';
9
9
  import { serializeToOoxml } from '../pipeline/serialization.js';
10
- import { NS_W, getNextRevisionId, getRevisionTimestamp, createRevisionMetadata, escapeXml, RunKind } from '../core/types.js';
10
+ import { NS_W, createRevisionMetadata, escapeXml, RunKind } from '../core/types.js';
11
11
  import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
12
12
 
13
13
  /**
@@ -18,10 +18,9 @@ import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
18
18
  * @param {Object} options - { generateRedlines, author }
19
19
  * @returns {string} Complete w:tbl OOXML
20
20
  */
21
- export function generateTableOoxml(tableData, options = {}) {
22
- const { generateRedlines = false, author = 'AI' } = options;
23
- const date = getRevisionTimestamp();
24
- const revId = generateRedlines ? getNextRevisionId() : null;
21
+ export function generateTableOoxml(tableData, options = {}) {
22
+ const { generateRedlines = false, author = 'AI' } = options;
23
+ const tableInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
25
24
 
26
25
  // Determine number of columns
27
26
  const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
@@ -86,9 +85,9 @@ export function generateTableOoxml(tableData, options = {}) {
86
85
  let tableXml = `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
87
86
 
88
87
  // Wrap entire table in w:ins if generating redlines
89
- if (generateRedlines && revId) {
90
- tableXml = `<w:ins w:id="${revId}" w:author="${escapeXml(author)}" w:date="${date}">${tableXml}</w:ins>`;
91
- }
88
+ if (tableInsertMeta) {
89
+ tableXml = `<w:ins w:id="${tableInsertMeta.id}" w:author="${escapeXml(tableInsertMeta.author)}" w:date="${tableInsertMeta.date}">${tableXml}</w:ins>`;
90
+ }
92
91
 
93
92
  return tableXml;
94
93
  }