@ansonlai/docx-redline-js 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +36 -10
- package/README.md +83 -6
- package/adapters/xml-adapter.js +73 -10
- package/core/list-targeting.js +3 -0
- package/core/paragraph-targeting.js +33 -7
- package/core/redline-validation.js +22 -0
- package/core/types.js +122 -27
- package/core/xml-query.js +3 -1
- package/dist/docx-redline-js.esm.js +1124 -545
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +79 -78
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +687 -0
- package/docs/VALIDATION.md +81 -2
- package/docs/WORD-MANUAL-REVIEW.md +138 -0
- package/docs/plans/2026-08-30-reliability-testing-improvements.md +488 -0
- package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +210 -0
- package/docs/plans/{2026-03-01-release-0.1.4-design.md → completed/2026-03-01-release-0.1.4-design.md} +2 -0
- package/docs/plans/{2026-03-01-release-0.1.4.md → completed/2026-03-01-release-0.1.4.md} +5 -3
- package/docs/plans/{2026-05-31-architectural changes.md → completed/2026-05-31-architectural changes.md } +2 -0
- package/docs/plans/completed/2026-08-02-reliability-improvements.md +1155 -0
- package/docs/test-comparison-dashboard.html +95 -0
- package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +22 -0
- package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +24 -0
- package/docs/validation-reports/2026-08-30-phase-3-coverage.md +73 -0
- package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +82 -0
- package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +114 -0
- package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +79 -0
- package/engine/format-extraction.js +1 -1
- package/engine/formatting-removal.js +84 -99
- package/engine/oxml-engine.js +176 -83
- package/engine/reconstruction-mapper.js +276 -79
- package/engine/reconstruction-mode.js +20 -6
- package/engine/reconstruction-writer.js +117 -72
- package/engine/run-builders.js +3 -3
- package/engine/surgical-mode.js +3 -2
- package/engine/table-mode.js +27 -16
- package/index.d.ts +95 -3
- package/index.js +14 -13
- package/orchestration/list-structural-fallback.js +16 -39
- package/package.json +22 -4
- package/pipeline/diff-engine.js +174 -55
- package/pipeline/ingestion-export.js +39 -24
- package/pipeline/ingestion-paragraph.js +7 -5
- package/pipeline/list-generation.js +27 -18
- package/pipeline/patching.js +2 -3
- package/pipeline/pipeline.js +65 -36
- package/pipeline/serialization.js +13 -5
- package/scripts/build-test-dashboard.mjs +43 -0
- package/scripts/check-types.mjs +16 -24
- package/scripts/export-validation-fixtures.mjs +191 -45
- package/scripts/fetch-superdoc-corpus.mjs +61 -0
- package/scripts/generate-test-dashboard.mjs +199 -0
- package/scripts/inspect-visual-evidence.mjs +271 -0
- package/scripts/lib/minimal-zip.mjs +199 -18
- package/scripts/lib/word-coverage-catalogue.mjs +207 -0
- package/scripts/lib/word-coverage-metadata.mjs +93 -0
- package/scripts/lib/zip-reader.mjs +64 -0
- package/scripts/package-superdoc-word-fixtures.ps1 +64 -0
- package/scripts/prepare-corpus-word-visual-review.mjs +84 -0
- package/scripts/prepare-superdoc-word-corpus.mjs +284 -0
- package/scripts/prepare-word-review.mjs +77 -0
- package/scripts/prepare-word-visual-review.mjs +90 -0
- package/scripts/render-agenda-multilevel.mjs +70 -0
- package/scripts/render-case22.mjs +73 -0
- package/scripts/render-case40.ps1 +35 -0
- package/scripts/render-multilevel-bullet-images.py +58 -0
- package/scripts/render-multilevel-bullet-visual.ps1 +32 -0
- package/scripts/render-multilevel-cases.mjs +80 -0
- package/scripts/report-coverage-gaps.mjs +103 -0
- package/scripts/report-word-coverage.mjs +71 -0
- package/scripts/sample-multimodal-visual-check.mjs +221 -0
- package/scripts/test-multilevel-bullet-visual.mjs +187 -0
- package/scripts/word-com-corpus-suite.ps1 +43 -0
- package/scripts/word-com-corpus-visual-suite.ps1 +116 -0
- package/scripts/word-com-differential.ps1 +158 -16
- package/scripts/word-com-suite.ps1 +19 -0
- package/scripts/word-com-visual-suite.ps1 +132 -0
- package/services/comment-engine.js +51 -46
- package/services/comment-locator.js +0 -1
- package/services/comment-package.js +11 -10
- package/services/numbering-service.js +1 -1
- package/services/revision-comment-management.js +31 -10
- package/services/standalone-docx-plumbing.js +45 -34
- package/services/standalone-operation-runner.js +315 -75
- package/services/table-reconciliation.js +23 -11
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
markParagraphMarkDeleted,
|
|
12
12
|
markParagraphMarkInserted
|
|
13
13
|
} from './run-builders.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { getFirstElementByTagNSOrTag } from '../core/xml-query.js';
|
|
15
|
+
import { NS_W } from '../core/types.js';
|
|
16
|
+
import { createWordElement, isWordElement } from '../core/word-xml.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Applies diffs to reconstruction context and writes updated XML.
|
|
@@ -28,9 +29,8 @@ import { createWordElement } from '../core/word-xml.js';
|
|
|
28
29
|
*/
|
|
29
30
|
export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, formatHints, generateRedlines = true) {
|
|
30
31
|
const {
|
|
31
|
-
paragraphs,
|
|
32
|
-
|
|
33
|
-
containerFragments,
|
|
32
|
+
paragraphs,
|
|
33
|
+
containerFragments,
|
|
34
34
|
sentinelMapByStart,
|
|
35
35
|
referenceMap,
|
|
36
36
|
replacementContainers,
|
|
@@ -55,12 +55,23 @@ export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, aut
|
|
|
55
55
|
|
|
56
56
|
let currentOriginalIndex = 0;
|
|
57
57
|
let currentInsertOffset = 0;
|
|
58
|
+
// A replacement is represented as one or more deletions followed by an
|
|
59
|
+
// insertion. Remember where that deleted range began so replacement text
|
|
60
|
+
// inherits the formatting at the start of the range, rather than the last
|
|
61
|
+
// deleted character (which may be a superscript ordinal, footnote-style
|
|
62
|
+
// run, or another narrow formatting boundary).
|
|
63
|
+
let pendingReplacementStart = null;
|
|
58
64
|
const emittedCommentMarkers = new WeakSet();
|
|
59
|
-
|
|
60
|
-
for (const [op, text] of diffs) {
|
|
61
|
-
if (op === 0 || op === -1) {
|
|
62
|
-
const type = op === 0 ? 'equal' : 'delete';
|
|
63
|
-
|
|
65
|
+
|
|
66
|
+
for (const [op, text] of diffs) {
|
|
67
|
+
if (op === 0 || op === -1) {
|
|
68
|
+
const type = op === 0 ? 'equal' : 'delete';
|
|
69
|
+
if (op === 0) {
|
|
70
|
+
pendingReplacementStart = null;
|
|
71
|
+
} else if (pendingReplacementStart === null) {
|
|
72
|
+
pendingReplacementStart = currentOriginalIndex;
|
|
73
|
+
}
|
|
74
|
+
let offset = 0;
|
|
64
75
|
|
|
65
76
|
while (offset < text.length) {
|
|
66
77
|
const chunkStart = currentOriginalIndex + offset;
|
|
@@ -99,11 +110,14 @@ export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, aut
|
|
|
99
110
|
currentOriginalIndex += text.length;
|
|
100
111
|
continue;
|
|
101
112
|
}
|
|
102
|
-
|
|
103
|
-
if (op === 1) {
|
|
104
|
-
const
|
|
105
|
-
?
|
|
106
|
-
:
|
|
113
|
+
|
|
114
|
+
if (op === 1) {
|
|
115
|
+
const propertyIndex = pendingReplacementStart !== null
|
|
116
|
+
? pendingReplacementStart
|
|
117
|
+
: (currentOriginalIndex > 0 && !isParagraphStart(currentOriginalIndex)
|
|
118
|
+
? currentOriginalIndex - 1
|
|
119
|
+
: currentOriginalIndex);
|
|
120
|
+
const properties = getRunProperties(propertyIndex);
|
|
107
121
|
|
|
108
122
|
const appendResult = appendTextToCurrent(
|
|
109
123
|
xmlDoc,
|
|
@@ -127,69 +141,93 @@ export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, aut
|
|
|
127
141
|
);
|
|
128
142
|
currentParagraph = appendResult.currentParagraph;
|
|
129
143
|
currentInsertOffset += text.length;
|
|
144
|
+
pendingReplacementStart = null;
|
|
130
145
|
}
|
|
131
146
|
}
|
|
132
147
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
target
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
148
|
+
const paragraphSet = new Set(paragraphs);
|
|
149
|
+
const insertionAnchors = new Map();
|
|
150
|
+
paragraphs.forEach(paragraph => {
|
|
151
|
+
const container = paragraph.parentNode;
|
|
152
|
+
if (!container || insertionAnchors.has(container)) return;
|
|
153
|
+
let anchor = paragraph.nextSibling;
|
|
154
|
+
while (anchor && paragraphSet.has(anchor)) anchor = anchor.nextSibling;
|
|
155
|
+
insertionAnchors.set(container, anchor);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
paragraphs.forEach(paragraph => {
|
|
159
|
+
if (paragraph.parentNode) {
|
|
160
|
+
paragraph.parentNode.removeChild(paragraph);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
let hasDocumentTarget = false;
|
|
165
|
+
let serializedDocumentOutput = '';
|
|
166
|
+
|
|
167
|
+
containerFragments.forEach((fragment, container) => {
|
|
168
|
+
const replacement = replacementContainers.get(container);
|
|
169
|
+
const target = replacement || container;
|
|
170
|
+
|
|
171
|
+
if (target.nodeType === 9) {
|
|
172
|
+
hasDocumentTarget = true;
|
|
173
|
+
if (fragment.childNodes.length === 1) {
|
|
174
|
+
target.appendChild(fragment.firstChild);
|
|
175
|
+
} else {
|
|
176
|
+
serializedDocumentOutput = Array.from(fragment.childNodes)
|
|
177
|
+
.map(node => serializer.serializeToString(node))
|
|
178
|
+
.join('');
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const anchor = replacement ? null : insertionAnchors.get(container);
|
|
184
|
+
if (anchor && anchor.parentNode === target) {
|
|
185
|
+
target.insertBefore(fragment, anchor);
|
|
186
|
+
} else {
|
|
187
|
+
target.appendChild(fragment);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const oxml = (hasDocumentTarget && serializedDocumentOutput)
|
|
192
|
+
? serializedDocumentOutput
|
|
193
|
+
: serializer.serializeToString(xmlDoc);
|
|
194
|
+
|
|
195
|
+
return { oxml, hasChanges: true };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function appendTextToCurrent(
|
|
199
|
+
xmlDoc,
|
|
200
|
+
text,
|
|
201
|
+
type,
|
|
202
|
+
rPr,
|
|
203
|
+
wrapper,
|
|
204
|
+
baseIndex,
|
|
205
|
+
currentParagraphRef,
|
|
206
|
+
containerFragments,
|
|
207
|
+
sentinelMapByStart,
|
|
208
|
+
referenceMap,
|
|
209
|
+
replacementContainers,
|
|
210
|
+
getParagraphInfo,
|
|
211
|
+
createNewParagraph,
|
|
212
|
+
author,
|
|
213
|
+
formatHints = [],
|
|
214
|
+
insertOffset = 0,
|
|
177
215
|
generateRedlines = true,
|
|
178
216
|
emittedCommentMarkers = new WeakSet()
|
|
179
217
|
) {
|
|
180
|
-
let localBaseIndex = baseIndex;
|
|
181
|
-
let localInsertOffset = insertOffset;
|
|
182
|
-
let localParagraph = currentParagraphRef;
|
|
183
|
-
|
|
184
|
-
const parts = text.split(/([\n\uFFFC]|[\uE000-\uF8FF])/);
|
|
185
|
-
|
|
186
|
-
parts.forEach(part => {
|
|
187
|
-
const sentinelsAtOffset = sentinelMapByStart.get(localBaseIndex) || [];
|
|
218
|
+
let localBaseIndex = baseIndex;
|
|
219
|
+
let localInsertOffset = insertOffset;
|
|
220
|
+
let localParagraph = currentParagraphRef;
|
|
221
|
+
|
|
222
|
+
const parts = text.split(/([\n\uFFFC]|[\uE000-\uF8FF])/);
|
|
223
|
+
|
|
224
|
+
parts.forEach(part => {
|
|
225
|
+
const sentinelsAtOffset = sentinelMapByStart.get(localBaseIndex) || [];
|
|
188
226
|
const commentMarkers = sentinelsAtOffset.filter(sentinel => sentinel.isCommentMarker && !emittedCommentMarkers.has(sentinel.node));
|
|
189
227
|
|
|
190
228
|
commentMarkers.forEach(marker => {
|
|
191
229
|
emittedCommentMarkers.add(marker.node);
|
|
192
|
-
if (marker.node
|
|
230
|
+
if (isWordElement(marker.node, 'commentReference')) {
|
|
193
231
|
const run = createWordElement(xmlDoc, 'w:r');
|
|
194
232
|
run.appendChild(marker.node.cloneNode(true));
|
|
195
233
|
localParagraph.appendChild(run);
|
|
@@ -202,9 +240,9 @@ function appendTextToCurrent(
|
|
|
202
240
|
const info = getParagraphInfo(localBaseIndex + 1);
|
|
203
241
|
const nextParagraph = createNewParagraph(info.pPr);
|
|
204
242
|
if (generateRedlines && type === 'insert') {
|
|
205
|
-
markParagraphMarkInserted(xmlDoc,
|
|
243
|
+
markParagraphMarkInserted(xmlDoc, localParagraph, author);
|
|
206
244
|
} else if (generateRedlines && type === 'delete') {
|
|
207
|
-
markParagraphMarkDeleted(xmlDoc,
|
|
245
|
+
markParagraphMarkDeleted(xmlDoc, localParagraph, author);
|
|
208
246
|
}
|
|
209
247
|
|
|
210
248
|
const fragment = containerFragments.get(info.container);
|
|
@@ -222,13 +260,20 @@ function appendTextToCurrent(
|
|
|
222
260
|
if (sentinel) {
|
|
223
261
|
const clone = sentinel.node.cloneNode(true);
|
|
224
262
|
if (sentinel.isTextBox && sentinel.originalContainer) {
|
|
225
|
-
const newContainer =
|
|
263
|
+
const newContainer = getFirstElementByTagNSOrTag(clone, NS_W, 'txbxContent');
|
|
226
264
|
if (newContainer) {
|
|
227
265
|
while (newContainer.firstChild) newContainer.removeChild(newContainer.firstChild);
|
|
228
266
|
replacementContainers.set(sentinel.originalContainer, newContainer);
|
|
229
267
|
}
|
|
230
268
|
}
|
|
231
|
-
|
|
269
|
+
if (sentinel.wrapInRun) {
|
|
270
|
+
const run = createWordElement(xmlDoc, 'w:r');
|
|
271
|
+
if (sentinel.rPr) run.appendChild(sentinel.rPr.cloneNode(true));
|
|
272
|
+
run.appendChild(clone);
|
|
273
|
+
localParagraph.appendChild(run);
|
|
274
|
+
} else {
|
|
275
|
+
localParagraph.appendChild(clone);
|
|
276
|
+
}
|
|
232
277
|
}
|
|
233
278
|
localBaseIndex++;
|
|
234
279
|
if (type !== 'delete') localInsertOffset++;
|
package/engine/run-builders.js
CHANGED
|
@@ -21,7 +21,7 @@ import { createWordElement } from '../core/word-xml.js';
|
|
|
21
21
|
*/
|
|
22
22
|
export function createTrackChange(xmlDoc, type, run, author) {
|
|
23
23
|
const wrapper = createWordElement(xmlDoc, type === 'ins' ? 'w:ins' : 'w:del');
|
|
24
|
-
const metadata = createRevisionMetadata(author);
|
|
24
|
+
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
25
25
|
wrapper.setAttribute('w:id', String(metadata.id));
|
|
26
26
|
wrapper.setAttribute('w:author', metadata.author);
|
|
27
27
|
wrapper.setAttribute('w:date', metadata.date);
|
|
@@ -70,7 +70,7 @@ function markParagraphMark(xmlDoc, paragraph, author, type) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const marker = createWordElement(xmlDoc, type === 'ins' ? 'w:ins' : 'w:del');
|
|
73
|
-
const metadata = createRevisionMetadata(author);
|
|
73
|
+
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
74
74
|
marker.setAttribute('w:id', String(metadata.id));
|
|
75
75
|
marker.setAttribute('w:author', metadata.author);
|
|
76
76
|
marker.setAttribute('w:date', metadata.date);
|
|
@@ -311,7 +311,7 @@ export function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateR
|
|
|
311
311
|
*/
|
|
312
312
|
export function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
|
|
313
313
|
const rPrChange = createWordElement(xmlDoc, 'w:rPrChange');
|
|
314
|
-
const metadata = createRevisionMetadata(author);
|
|
314
|
+
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
315
315
|
rPrChange.setAttribute('w:id', String(metadata.id));
|
|
316
316
|
rPrChange.setAttribute('w:author', metadata.author);
|
|
317
317
|
rPrChange.setAttribute('w:date', dateStr || metadata.date);
|
package/engine/surgical-mode.js
CHANGED
|
@@ -27,9 +27,10 @@ import { withOoxmlSourceType } from '../core/word-xml.js';
|
|
|
27
27
|
* @param {Array} formatHints - Format hints
|
|
28
28
|
* @param {boolean} [generateRedlines=true] - Track change toggle
|
|
29
29
|
* @param {Element|null} [targetParagraph=null] - Optional scope paragraph
|
|
30
|
+
* @param {{ diffTimeoutSeconds?: number }} [diffOptions={}] - Diff configuration
|
|
30
31
|
* @returns {{ oxml: string, hasChanges: boolean, sourceType?: 'package'|'document'|'fragment' }}
|
|
31
32
|
*/
|
|
32
|
-
export function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null) {
|
|
33
|
+
export function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null, diffOptions = {}) {
|
|
33
34
|
void originalText;
|
|
34
35
|
|
|
35
36
|
const allParagraphs = targetParagraph
|
|
@@ -37,7 +38,7 @@ export function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer
|
|
|
37
38
|
: getDocumentParagraphs(xmlDoc);
|
|
38
39
|
|
|
39
40
|
const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
|
|
40
|
-
const diffs = computeWordDiffs(fullText, modifiedText);
|
|
41
|
+
const diffs = computeWordDiffs(fullText, modifiedText, diffOptions);
|
|
41
42
|
const spanIndex = buildSpanIndex(textSpans);
|
|
42
43
|
|
|
43
44
|
let originalPos = 0;
|
package/engine/table-mode.js
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
* Table-specific reconciliation and transformation flows.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
NS_W,
|
|
7
|
+
createRevisionMetadata,
|
|
8
|
+
getRevisionIdAllocatorForDocument,
|
|
9
|
+
setRevisionIdAllocatorForDocument
|
|
10
|
+
} from '../core/types.js';
|
|
6
11
|
import {
|
|
7
12
|
getElementsByTagNS,
|
|
8
13
|
getElementsByTagNSOrTag,
|
|
9
|
-
getFirstElementByTag,
|
|
10
14
|
getFirstElementByTagNS,
|
|
11
15
|
getFirstElementByTagNSOrTag,
|
|
12
16
|
getXmlParseError
|
|
13
17
|
} from '../core/xml-query.js';
|
|
14
|
-
import {
|
|
18
|
+
import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
15
19
|
import { log, error } from '../adapters/logger.js';
|
|
16
20
|
import { diffTablesWithVirtualGrid, serializeVirtualGridToOoxml, generateTableOoxml } from '../services/table-reconciliation.js';
|
|
17
21
|
import { parseTable } from '../pipeline/pipeline.js';
|
|
@@ -51,11 +55,15 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
|
|
|
51
55
|
return noChanges(serializer, xmlDoc);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
const options = {
|
|
58
|
+
const options = {
|
|
59
|
+
generateRedlines,
|
|
60
|
+
author,
|
|
61
|
+
revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
|
|
62
|
+
};
|
|
55
63
|
const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
|
|
56
64
|
const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
|
|
57
|
-
const
|
|
58
|
-
|
|
65
|
+
const reconciledDoc = parseOoxmlSafe(wrappedOxml, 'application/xml').doc;
|
|
66
|
+
if (!reconciledDoc) return noChanges(serializer, xmlDoc);
|
|
59
67
|
|
|
60
68
|
const parseError = getXmlParseError(reconciledDoc);
|
|
61
69
|
if (parseError) {
|
|
@@ -86,16 +94,17 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
|
|
|
86
94
|
* @param {boolean} generateRedlines - Track change toggle
|
|
87
95
|
* @returns {{ oxml: string, hasChanges: boolean }}
|
|
88
96
|
*/
|
|
89
|
-
export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
|
|
97
|
+
export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
|
|
98
|
+
const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
|
|
90
99
|
const tableData = parseTable(modifiedText);
|
|
91
100
|
if (!tableData || (tableData.rows.length === 0 && tableData.headers.length === 0)) {
|
|
92
101
|
log('[OxmlEngine] Failed to parse table data from Markdown');
|
|
93
102
|
return noChanges(serializer, xmlDoc);
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author });
|
|
97
|
-
const
|
|
98
|
-
|
|
105
|
+
const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator });
|
|
106
|
+
const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, 'application/xml').doc;
|
|
107
|
+
if (!tableDoc) return noChanges(serializer, xmlDoc);
|
|
99
108
|
|
|
100
109
|
const tableParseError = getXmlParseError(tableDoc);
|
|
101
110
|
if (tableParseError) {
|
|
@@ -123,14 +132,16 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
|
|
|
123
132
|
let parent = firstParagraph.parentNode;
|
|
124
133
|
|
|
125
134
|
if (parent && parent.nodeType === 9) {
|
|
126
|
-
const wrappedDoc =
|
|
127
|
-
`<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
|
|
128
|
-
'application/xml'
|
|
129
|
-
);
|
|
135
|
+
const wrappedDoc = parseOoxmlSafe(
|
|
136
|
+
`<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
|
|
137
|
+
'application/xml'
|
|
138
|
+
).doc;
|
|
139
|
+
if (!wrappedDoc) return noChanges(serializer, workingDoc);
|
|
130
140
|
const wrappedBody = getFirstElementByTagNS(wrappedDoc, NS_W, 'body');
|
|
131
141
|
paragraphs.forEach(p => wrappedBody.appendChild(wrappedDoc.importNode(p, true)));
|
|
132
142
|
|
|
133
|
-
workingDoc = wrappedDoc;
|
|
143
|
+
workingDoc = wrappedDoc;
|
|
144
|
+
setRevisionIdAllocatorForDocument(workingDoc, revisionIdAllocator);
|
|
134
145
|
paragraphs = getElementsByTagNS(workingDoc, NS_W, 'p');
|
|
135
146
|
firstParagraph = paragraphs[0];
|
|
136
147
|
parent = firstParagraph.parentNode;
|
|
@@ -154,7 +165,7 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
|
|
|
154
165
|
});
|
|
155
166
|
|
|
156
167
|
const del = createWordElement(workingDoc, 'w:del');
|
|
157
|
-
const metadata = createRevisionMetadata(author);
|
|
168
|
+
const metadata = createRevisionMetadata(author, workingDoc);
|
|
158
169
|
del.setAttribute('w:id', String(metadata.id));
|
|
159
170
|
del.setAttribute('w:author', metadata.author);
|
|
160
171
|
del.setAttribute('w:date', metadata.date);
|
package/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export type OoxmlSourceType = 'package' | 'document' | 'fragment';
|
|
2
2
|
export type RedlineStatus = 'ok' | 'no-op' | 'error';
|
|
3
|
-
export type ExistingRevisionsPolicy = 'reject-input' | 'accept-all-first';
|
|
3
|
+
export type ExistingRevisionsPolicy = 'reject-input' | 'accept-all-first' | 'accept-all-first-keep-normalized';
|
|
4
4
|
|
|
5
5
|
export interface RedlineError {
|
|
6
|
-
code: 'PARSE_ERROR' | 'TARGET_NOT_FOUND' | 'EXISTING_REVISIONS' | string;
|
|
6
|
+
code: 'PARSE_ERROR' | 'TARGET_NOT_FOUND' | 'EXISTING_REVISIONS' | 'DIFF_TOKEN_LIMIT' | string;
|
|
7
7
|
message: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -12,6 +12,8 @@ export interface RedlineOptions {
|
|
|
12
12
|
author?: string;
|
|
13
13
|
targetParagraphId?: string | null;
|
|
14
14
|
existingRevisions?: ExistingRevisionsPolicy;
|
|
15
|
+
removeFormatting?: boolean;
|
|
16
|
+
sanitizeInput?: boolean;
|
|
15
17
|
[key: string]: unknown;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -46,6 +48,8 @@ export interface AcceptTrackedChangesResult {
|
|
|
46
48
|
hasChanges: boolean;
|
|
47
49
|
acceptedCount: number;
|
|
48
50
|
warnings: string[];
|
|
51
|
+
status?: RedlineStatus;
|
|
52
|
+
error?: RedlineError;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export interface RejectTrackedChangesResult {
|
|
@@ -53,6 +57,8 @@ export interface RejectTrackedChangesResult {
|
|
|
53
57
|
hasChanges: boolean;
|
|
54
58
|
rejectedCount: number;
|
|
55
59
|
warnings: string[];
|
|
60
|
+
status?: RedlineStatus;
|
|
61
|
+
error?: RedlineError;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export interface DeleteCommentsResult {
|
|
@@ -61,6 +67,15 @@ export interface DeleteCommentsResult {
|
|
|
61
67
|
commentsRemoved: number;
|
|
62
68
|
referencesRemoved: number;
|
|
63
69
|
warnings: string[];
|
|
70
|
+
status?: RedlineStatus;
|
|
71
|
+
error?: RedlineError;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface IngestionTextResult {
|
|
75
|
+
text: string;
|
|
76
|
+
status: 'ok' | 'error';
|
|
77
|
+
error?: RedlineError;
|
|
78
|
+
warnings?: string[];
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
export interface XmlProvider {
|
|
@@ -85,6 +100,7 @@ export interface DocumentOperationResult {
|
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
export function configureXmlProvider(provider: XmlProvider): void;
|
|
103
|
+
export function parseOoxmlSafe(oxml: unknown, contentType?: string): { doc: Document | null; error: RedlineError | null; warnings: string[] };
|
|
88
104
|
export function configureLogger(logger: LoggerConfig): void;
|
|
89
105
|
export function setDefaultAuthor(name: string): void;
|
|
90
106
|
export function getDefaultAuthor(): string;
|
|
@@ -114,6 +130,8 @@ export function reconcileMarkdownTableOoxml(
|
|
|
114
130
|
|
|
115
131
|
export function ingestWordOoxmlToPlainText(oxml: string): string;
|
|
116
132
|
export function ingestWordOoxmlToMarkdown(oxml: string): string;
|
|
133
|
+
export function ingestWordOoxmlToPlainTextResult(oxml: unknown): IngestionTextResult;
|
|
134
|
+
export function ingestWordOoxmlToMarkdownResult(oxml: unknown): IngestionTextResult;
|
|
117
135
|
export function ingestOoxml(oxml: string): unknown;
|
|
118
136
|
export function preprocessMarkdown(text: string): { cleanText: string; formatHints: unknown[] };
|
|
119
137
|
export function serializeToOoxml(runModel: unknown[], pPrXml?: string | null, formatHints?: unknown[], options?: Record<string, unknown>): string;
|
|
@@ -158,7 +176,7 @@ export function ensureNumberingArtifactsInZip(zip: unknown, numberingXml: string
|
|
|
158
176
|
export function ensureCommentsArtifactsInZip(zip: unknown, commentsXml: string): Promise<unknown> | unknown;
|
|
159
177
|
export function createDynamicNumberingIdState(numberingXml?: string): unknown;
|
|
160
178
|
|
|
161
|
-
export function parseOoxml(ooxml: string): Document;
|
|
179
|
+
export function parseOoxml(ooxml: string): Document | null;
|
|
162
180
|
export function serializeOoxml(node: Node): string;
|
|
163
181
|
export function sanitizeAiResponse(text: string): string;
|
|
164
182
|
|
|
@@ -170,3 +188,77 @@ export class ReconciliationPipeline {
|
|
|
170
188
|
export class NumberingService {
|
|
171
189
|
constructor(...args: unknown[]);
|
|
172
190
|
}
|
|
191
|
+
|
|
192
|
+
// Supporting public utilities. These declarations intentionally expose stable
|
|
193
|
+
// boundary shapes while leaving internal planning records extensible.
|
|
194
|
+
export const NS_W: string;
|
|
195
|
+
export const WORD_MAIN_NS: string;
|
|
196
|
+
export const DiffOp: Readonly<Record<'EQUAL' | 'DELETE' | 'INSERT', string>>;
|
|
197
|
+
export const RunKind: Readonly<Record<string, string>>;
|
|
198
|
+
export const ContainerKind: Readonly<Record<string, string>>;
|
|
199
|
+
export const ContentType: Readonly<Record<string, string>>;
|
|
200
|
+
export const RoutePlanKind: Readonly<Record<string, string>>;
|
|
201
|
+
|
|
202
|
+
export function escapeXml(value: unknown): string;
|
|
203
|
+
export function applyFormattingRemovalToOoxml(oxml: string, options?: Record<string, unknown>): string;
|
|
204
|
+
export function removeFormattingFromRPr(rPr: Element, options?: Record<string, unknown>): Element;
|
|
205
|
+
|
|
206
|
+
export function buildCommentElement(comment: Record<string, unknown>, id?: string | number): string;
|
|
207
|
+
export function buildCommentsPartXml(comments: unknown[]): string;
|
|
208
|
+
|
|
209
|
+
export function reserveNextNumberingId(state: unknown): number;
|
|
210
|
+
export function reserveNextNumberingIdPair(state: unknown): { abstractNumId: number; numId: number };
|
|
211
|
+
export function overwriteParagraphNumIds(oxml: string, numId: string | number): string;
|
|
212
|
+
export function extractFirstParagraphNumId(oxml: string): string | null;
|
|
213
|
+
export function buildExplicitDecimalMultilevelNumberingXml(options?: Record<string, unknown>): string;
|
|
214
|
+
export function remapNumberingPayloadForDocument(numberingXml: string, state: unknown): unknown;
|
|
215
|
+
export function mergeNumberingXmlBySchemaOrder(baseXml: string, incomingXml: string): string;
|
|
216
|
+
|
|
217
|
+
export function buildListMarkdown(items: unknown[], options?: Record<string, unknown>): string;
|
|
218
|
+
export function inferNumberingStyleFromMarker(marker: string): string;
|
|
219
|
+
export function normalizeListItemsWithLevels(items: unknown[]): unknown[];
|
|
220
|
+
export function parseMarkdownListContent(text: string): unknown;
|
|
221
|
+
export function hasListItems(parsed: unknown): boolean;
|
|
222
|
+
|
|
223
|
+
export function buildReconciliationPlan(params?: Record<string, unknown>): Record<string, unknown>;
|
|
224
|
+
export function normalizeContentEscapesForRouting(content: string): string;
|
|
225
|
+
export function buildSingleLineListStructuralFallbackPlan(options?: Record<string, unknown>): Record<string, unknown> | null;
|
|
226
|
+
export function executeSingleLineListStructuralFallback(plan: unknown, options?: Record<string, unknown>): Promise<RedlineResult>;
|
|
227
|
+
export function resolveSingleLineListFallbackNumberingAction(plan: unknown, sequenceState?: unknown): Record<string, unknown>;
|
|
228
|
+
export function recordSingleLineListFallbackExplicitSequence(sequenceState: unknown, numberingKey: string | null, numId: string | number | null, startAt: number | null): void;
|
|
229
|
+
export function clearSingleLineListFallbackExplicitSequence(sequenceState: unknown, numberingKey: string | null): void;
|
|
230
|
+
export function enforceListBindingOnParagraphNodes(nodes: Node[], options?: Record<string, unknown>): number;
|
|
231
|
+
export function stripSingleLineListMarkerPrefix(text: string): string;
|
|
232
|
+
|
|
233
|
+
export function getParagraphText(paragraph: Element | null | undefined): string;
|
|
234
|
+
export function getDocumentParagraphNodes(xmlDoc: Document | Element | null | undefined): Element[];
|
|
235
|
+
export function normalizeWhitespaceForTargeting(text: string): string;
|
|
236
|
+
export function isMarkdownTableText(text: string): boolean;
|
|
237
|
+
export function parseParagraphReference(reference: unknown): unknown;
|
|
238
|
+
export function stripLeadingParagraphMarker(text: string): string;
|
|
239
|
+
export function splitLeadingParagraphMarker(text: string): Record<string, unknown>;
|
|
240
|
+
export function findContainingWordElement(node: Node | null, localName: string): Element | null;
|
|
241
|
+
export function findParagraphByReference(xmlDoc: Document | Element, reference: unknown): Element | null;
|
|
242
|
+
export function findParagraphByStrictText(xmlDoc: Document | Element, text: string): Element | null;
|
|
243
|
+
export function findParagraphByBestTextMatch(xmlDoc: Document | Element, text: string): Element | null;
|
|
244
|
+
export function resolveTargetParagraph(xmlDoc: Document | Element, options?: Record<string, unknown>): Element | null;
|
|
245
|
+
export function buildTargetReferenceSnapshot(paragraph: Element, options?: Record<string, unknown>): Record<string, unknown>;
|
|
246
|
+
export function resolveTargetParagraphWithSnapshot(xmlDoc: Document | Element, snapshot: unknown): Element | null;
|
|
247
|
+
export function resolveParagraphRangeByRefs(xmlDoc: Document | Element, references: unknown[]): Element[];
|
|
248
|
+
export function extractParagraphIdFromOoxml(oxml: string): string | null;
|
|
249
|
+
|
|
250
|
+
export function getParagraphListInfo(paragraph: Element): Record<string, unknown> | null;
|
|
251
|
+
export function collectContiguousListParagraphBlock(paragraph: Element): Element[];
|
|
252
|
+
export function synthesizeExpandedListScopeEdit(options: Record<string, unknown>): Record<string, unknown> | null;
|
|
253
|
+
export function planListInsertionOnlyEdit(options: Record<string, unknown>): Record<string, unknown> | null;
|
|
254
|
+
export function stripRedundantLeadingListMarkers(text: string): string;
|
|
255
|
+
|
|
256
|
+
export function inferTableReplacementParagraphBlock(options: Record<string, unknown>): Record<string, unknown> | null;
|
|
257
|
+
export function isLikelyStructuredTableSourceParagraph(paragraph: Element): boolean;
|
|
258
|
+
export function synthesizeTableMarkdownFromMultilineCellEdit(options: Record<string, unknown>): string | null;
|
|
259
|
+
|
|
260
|
+
export function getBodyElementFromDocument(xmlDoc: Document): Element | null;
|
|
261
|
+
export function insertBodyElementBeforeSectPr(body: Element, element: Element): Element;
|
|
262
|
+
export function normalizeBodySectionOrderStandalone(documentXml: string): string;
|
|
263
|
+
export function sanitizeNestedParagraphsInTables(documentXml: string): string;
|
|
264
|
+
export function getPackagePartName(part: unknown): string | null;
|
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
// Adapters
|
|
6
|
-
export { configureXmlProvider } from './adapters/xml-adapter.js';
|
|
6
|
+
export { configureXmlProvider, parseOoxmlSafe } from './adapters/xml-adapter.js';
|
|
7
7
|
export { configureLogger } from './adapters/logger.js';
|
|
8
8
|
export { setDefaultAuthor, getDefaultAuthor, setPlatform, getPlatform } from './adapters/config.js';
|
|
9
9
|
|
|
@@ -18,12 +18,7 @@ import { parseTable as parseMarkdownTable } from './pipeline/pipeline.js';
|
|
|
18
18
|
import { wrapInDocumentFragment as wrapInDocumentFragmentShared } from './pipeline/serialization.js';
|
|
19
19
|
import {
|
|
20
20
|
buildSingleLineListStructuralFallbackPlan,
|
|
21
|
-
executeSingleLineListStructuralFallback
|
|
22
|
-
resolveSingleLineListFallbackNumberingAction,
|
|
23
|
-
recordSingleLineListFallbackExplicitSequence,
|
|
24
|
-
clearSingleLineListFallbackExplicitSequence,
|
|
25
|
-
enforceListBindingOnParagraphNodes,
|
|
26
|
-
stripSingleLineListMarkerPrefix
|
|
21
|
+
executeSingleLineListStructuralFallback
|
|
27
22
|
} from './orchestration/list-structural-fallback.js';
|
|
28
23
|
import { withOoxmlSourceType } from './core/word-xml.js';
|
|
29
24
|
export { containsTrackedChanges } from './core/word-xml.js';
|
|
@@ -119,10 +114,14 @@ export { inferTableReplacementParagraphBlock, isLikelyStructuredTableSourceParag
|
|
|
119
114
|
*/
|
|
120
115
|
export async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedText, options = {}) {
|
|
121
116
|
const allowExistingListForFallback = options.listFallbackAllowExistingList !== false;
|
|
117
|
+
const fallbackModifiedText = options.sanitizeInput === true ? sanitizeAiResponse(modifiedText) : modifiedText;
|
|
118
|
+
const sanitizationWarnings = fallbackModifiedText !== modifiedText
|
|
119
|
+
? ['Input was sanitized; pass sanitizeInput: false to disable.']
|
|
120
|
+
: [];
|
|
122
121
|
const plan = buildSingleLineListStructuralFallbackPlan({
|
|
123
122
|
oxml,
|
|
124
123
|
originalText,
|
|
125
|
-
modifiedText,
|
|
124
|
+
modifiedText: fallbackModifiedText,
|
|
126
125
|
allowExistingList: allowExistingListForFallback
|
|
127
126
|
});
|
|
128
127
|
const preferListFallback = options.preferListStructuralFallback !== false;
|
|
@@ -143,7 +142,7 @@ export async function applyRedlineToOxmlWithListFallback(oxml, originalText, mod
|
|
|
143
142
|
return withOoxmlSourceType({
|
|
144
143
|
oxml: wrappedOxml,
|
|
145
144
|
hasChanges: true,
|
|
146
|
-
warnings: fallbackWarnings,
|
|
145
|
+
warnings: [...sanitizationWarnings, ...fallbackWarnings],
|
|
147
146
|
listStructuralFallbackApplied: true,
|
|
148
147
|
listStructuralFallbackKey: fallbackResult.listStructuralFallbackKey || null,
|
|
149
148
|
listStructuralFallbackNumberingXml: fallbackResult.numberingXml || null
|
|
@@ -232,7 +231,12 @@ export {
|
|
|
232
231
|
// Pipeline components
|
|
233
232
|
export { ReconciliationPipeline } from './pipeline/pipeline.js';
|
|
234
233
|
export { ingestOoxml } from './pipeline/ingestion.js';
|
|
235
|
-
export {
|
|
234
|
+
export {
|
|
235
|
+
ingestWordOoxmlToPlainText,
|
|
236
|
+
ingestWordOoxmlToMarkdown,
|
|
237
|
+
ingestWordOoxmlToPlainTextResult,
|
|
238
|
+
ingestWordOoxmlToMarkdownResult
|
|
239
|
+
} from './pipeline/ingestion-export.js';
|
|
236
240
|
export { preprocessMarkdown } from './pipeline/markdown-processor.js';
|
|
237
241
|
export { serializeToOoxml, wrapInDocumentFragment } from './pipeline/serialization.js';
|
|
238
242
|
|
|
@@ -260,7 +264,6 @@ export {
|
|
|
260
264
|
export { generateTableOoxml } from './services/table-reconciliation.js';
|
|
261
265
|
export { NumberingService } from './services/numbering-service.js';
|
|
262
266
|
export {
|
|
263
|
-
parseXmlStrictStandalone,
|
|
264
267
|
getBodyElementFromDocument,
|
|
265
268
|
insertBodyElementBeforeSectPr,
|
|
266
269
|
normalizeBodySectionOrderStandalone,
|
|
@@ -312,5 +315,3 @@ export {
|
|
|
312
315
|
planListInsertionOnlyEdit,
|
|
313
316
|
stripRedundantLeadingListMarkers
|
|
314
317
|
} from './core/list-targeting.js';
|
|
315
|
-
|
|
316
|
-
|