@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.
- package/AGENTS.md +53 -4
- package/ARCHITECTURE.md +75 -11
- package/README.md +62 -3
- package/core/redline-validation.js +156 -0
- package/core/types.js +35 -8
- package/core/word-xml.js +90 -0
- package/dist/docx-redline-js.esm.js +3195 -2592
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +71 -67
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/VALIDATION.md +104 -0
- package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
- package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
- package/docs/plans/2026-05-31-architectural changes.md +591 -0
- package/engine/format-application.js +13 -14
- package/engine/format-span-application.js +7 -6
- package/engine/formatting-removal.js +15 -12
- package/engine/oxml-engine.js +146 -55
- package/engine/reconstruction-mapper.js +35 -8
- package/engine/reconstruction-mode.js +14 -13
- package/engine/reconstruction-writer.js +97 -78
- package/engine/rpr-helpers.js +34 -32
- package/engine/run-builders.js +150 -39
- package/engine/surgical-diff-application.js +216 -0
- package/engine/surgical-mode.js +84 -519
- package/engine/surgical-run-splitting.js +96 -0
- package/engine/surgical-spans.js +169 -0
- package/engine/table-cell-context.js +15 -13
- package/engine/table-mode.js +39 -35
- package/index.d.ts +172 -0
- package/index.js +50 -47
- package/package.json +10 -2
- package/pipeline/ingestion-export.js +1 -0
- package/pipeline/ingestion-paragraph.js +37 -12
- package/pipeline/ingestion-table.js +11 -8
- package/scripts/build.mjs +40 -0
- package/scripts/check-types.mjs +29 -0
- package/scripts/export-validation-fixtures.mjs +125 -0
- package/scripts/lib/minimal-zip.mjs +155 -0
- package/scripts/run-tests.mjs +43 -0
- package/scripts/validate-fixtures-xsd.sh +37 -0
- package/scripts/word-com-differential.ps1 +133 -0
- package/scripts/word-com-smoke.ps1 +48 -0
- package/services/comment-locator.js +10 -9
- package/services/revision-comment-management.js +115 -1
- package/services/standalone-operation-runner.js +119 -69
- 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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
194
|
+
function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMetadata, author, options = {}) {
|
|
143
195
|
const generateRedlines = options.generateRedlines !== false;
|
|
144
|
-
const paragraph = xmlDoc
|
|
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
|
|
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
|
|
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
|
|
166
|
-
ins
|
|
167
|
-
ins.setAttribute('w:
|
|
168
|
-
ins.setAttribute('w:
|
|
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
|
-
|
|
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,
|
|
477
|
+
function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionMetadata, author, options = {}) {
|
|
430
478
|
const generateRedlines = options.generateRedlines !== false;
|
|
431
|
-
const paragraph = xmlDoc
|
|
432
|
-
const run = xmlDoc
|
|
433
|
-
const textNode = xmlDoc
|
|
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
|
|
441
|
-
ins
|
|
442
|
-
ins.setAttribute('w:
|
|
443
|
-
ins.setAttribute('w:
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
471
|
-
const wrappedParagraph = xmlDoc
|
|
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
|
|
476
|
-
|
|
477
|
-
ins.setAttribute('w:
|
|
478
|
-
ins.setAttribute('w:
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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,
|
|
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
|
|
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 (
|
|
90
|
-
tableXml = `<w:ins w:id="${
|
|
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
|
}
|