@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
package/engine/oxml-engine.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getElementsByTagNSOrTag,
|
|
13
13
|
getXmlParseError
|
|
14
14
|
} from '../core/xml-query.js';
|
|
15
|
-
import {
|
|
15
|
+
import { createSerializer, parseOoxmlSafe, serializeXml } from '../adapters/xml-adapter.js';
|
|
16
16
|
import { log, error } from '../adapters/logger.js';
|
|
17
17
|
import { extractFormattingFromOoxml } from './format-extraction.js';
|
|
18
18
|
import {
|
|
@@ -26,8 +26,13 @@ import { applyReconstructionMode } from './reconstruction-mode.js';
|
|
|
26
26
|
import { applyTableReconciliation, applyTextToTableTransformation } from './table-mode.js';
|
|
27
27
|
import { getDefaultAuthor } from '../adapters/config.js';
|
|
28
28
|
import { containsTrackedChanges, withOoxmlSourceType } from '../core/word-xml.js';
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
NS_W,
|
|
31
|
+
RevisionIdAllocator,
|
|
32
|
+
seedRevisionIdsFromDocument
|
|
33
|
+
} from '../core/types.js';
|
|
30
34
|
import { acceptTrackedChangesInOoxml } from '../services/revision-comment-management.js';
|
|
35
|
+
import { isDiffTokenLimitError } from '../pipeline/diff-engine.js';
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
38
|
* Applies redline track changes to OOXML by modifying the DOM in-place.
|
|
@@ -38,73 +43,107 @@ import { acceptTrackedChangesInOoxml } from '../services/revision-comment-manage
|
|
|
38
43
|
* @param {Object} [options={}] - Options
|
|
39
44
|
* @param {string} [options.author='AI'] - Author for track changes
|
|
40
45
|
* @param {string|null} [options.targetParagraphId=null] - Preferred paragraph identity for table wrappers
|
|
41
|
-
* @param {'reject-input'|'accept-all-first'} [options.existingRevisions='reject-input'] - Policy for source OOXML with tracked changes
|
|
46
|
+
* @param {'reject-input'|'accept-all-first'|'accept-all-first-keep-normalized'} [options.existingRevisions='reject-input'] - Policy for source OOXML with tracked changes
|
|
47
|
+
* @param {boolean} [options.removeFormatting=false] - Remove existing core formatting when text is otherwise unchanged
|
|
48
|
+
* @param {boolean} [options.sanitizeInput=false] - Strip a standalone leading assistant preface line
|
|
42
49
|
* @returns {Promise<{ oxml: string, hasChanges: boolean, sourceType?: 'package'|'document'|'fragment', status?: 'ok'|'no-op'|'error', error?: { code: string, message: string } }>}
|
|
43
50
|
*/
|
|
44
51
|
export async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}) {
|
|
52
|
+
const inputOoxml = oxml;
|
|
53
|
+
let workingOoxml = oxml;
|
|
54
|
+
originalText = typeof originalText === 'string' ? originalText : String(originalText ?? '');
|
|
55
|
+
modifiedText = typeof modifiedText === 'string' ? modifiedText : String(modifiedText ?? '');
|
|
45
56
|
const generateRedlines = options.generateRedlines ?? true;
|
|
46
57
|
const author = options.author || getDefaultAuthor();
|
|
47
|
-
const
|
|
48
|
-
|
|
58
|
+
const serializer = createSerializer();
|
|
59
|
+
let parseWarnings = [];
|
|
60
|
+
const operationWarnings = [];
|
|
61
|
+
let normalizedExistingRevisions = false;
|
|
62
|
+
const keepNormalizedNoOp = options.existingRevisions === 'accept-all-first-keep-normalized';
|
|
49
63
|
const finalize = result => {
|
|
50
64
|
const withStatus = { ...result };
|
|
65
|
+
if (normalizedExistingRevisions && withStatus.hasChanges === false && withStatus.status !== 'error') {
|
|
66
|
+
if (keepNormalizedNoOp) {
|
|
67
|
+
withStatus.oxml = workingOoxml;
|
|
68
|
+
withStatus.hasChanges = true;
|
|
69
|
+
withStatus.warnings = [
|
|
70
|
+
...(Array.isArray(withStatus.warnings) ? withStatus.warnings : []),
|
|
71
|
+
'Existing revisions were accepted before redlining.'
|
|
72
|
+
];
|
|
73
|
+
} else {
|
|
74
|
+
withStatus.oxml = inputOoxml;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const warnings = [...parseWarnings, ...operationWarnings, ...(Array.isArray(withStatus.warnings) ? withStatus.warnings : [])];
|
|
78
|
+
if (warnings.length > 0) {
|
|
79
|
+
withStatus.warnings = [...new Set(warnings)];
|
|
80
|
+
}
|
|
51
81
|
if (!withStatus.status) {
|
|
52
82
|
withStatus.status = withStatus.hasChanges ? 'ok' : 'no-op';
|
|
53
83
|
}
|
|
54
84
|
return withOoxmlSourceType(withStatus);
|
|
55
85
|
};
|
|
56
|
-
const
|
|
86
|
+
const finalizeUnchanged = () => {
|
|
87
|
+
if (normalizedExistingRevisions && keepNormalizedNoOp) {
|
|
88
|
+
return finalize({
|
|
89
|
+
oxml: workingOoxml,
|
|
90
|
+
hasChanges: true,
|
|
91
|
+
warnings: ['Existing revisions were accepted before redlining.']
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return finalize({ oxml: inputOoxml, hasChanges: false });
|
|
95
|
+
};
|
|
57
96
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} catch (e) {
|
|
62
|
-
error('[OxmlEngine] Failed to parse OXML:', e);
|
|
63
|
-
return finalize({
|
|
64
|
-
oxml,
|
|
65
|
-
hasChanges: false,
|
|
66
|
-
status: 'error',
|
|
67
|
-
error: { code: 'PARSE_ERROR', message: 'Could not parse OOXML input.' }
|
|
68
|
-
});
|
|
69
|
-
}
|
|
97
|
+
const parsed = parseOoxmlSafe(inputOoxml, 'text/xml');
|
|
98
|
+
parseWarnings = parsed.warnings;
|
|
99
|
+
let xmlDoc = parsed.doc;
|
|
70
100
|
|
|
71
|
-
const parseError = getXmlParseError(xmlDoc);
|
|
72
|
-
if (parseError) {
|
|
73
|
-
error
|
|
101
|
+
const parseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
|
|
102
|
+
if (parsed.error || parseError) {
|
|
103
|
+
const message = parsed.error?.message || parseError?.textContent || 'Could not parse OOXML input.';
|
|
104
|
+
error('[OxmlEngine] XML parse error:', message);
|
|
74
105
|
return finalize({
|
|
75
|
-
oxml,
|
|
106
|
+
oxml: inputOoxml,
|
|
76
107
|
hasChanges: false,
|
|
77
108
|
status: 'error',
|
|
78
|
-
error: { code: 'PARSE_ERROR', message
|
|
109
|
+
error: { code: 'PARSE_ERROR', message }
|
|
79
110
|
});
|
|
80
111
|
}
|
|
81
|
-
|
|
112
|
+
const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator
|
|
113
|
+
? options._revisionIdAllocator
|
|
114
|
+
: new RevisionIdAllocator();
|
|
115
|
+
seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
|
|
82
116
|
|
|
83
117
|
if (containsTrackedChanges(xmlDoc)) {
|
|
84
118
|
const existingRevisionsPolicy = options.existingRevisions || 'reject-input';
|
|
85
|
-
if (existingRevisionsPolicy === 'accept-all-first') {
|
|
119
|
+
if (existingRevisionsPolicy === 'accept-all-first' || existingRevisionsPolicy === 'accept-all-first-keep-normalized') {
|
|
86
120
|
log('[OxmlEngine] Existing revisions detected; accepting all input revisions before redlining');
|
|
87
|
-
const accepted = acceptTrackedChangesInOoxml(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
121
|
+
const accepted = acceptTrackedChangesInOoxml(inputOoxml, { allAuthors: true });
|
|
122
|
+
if (accepted.status === 'error') return finalize(accepted);
|
|
123
|
+
workingOoxml = accepted.oxml;
|
|
124
|
+
normalizedExistingRevisions = true;
|
|
125
|
+
const acceptedParsed = parseOoxmlSafe(workingOoxml, 'text/xml');
|
|
126
|
+
parseWarnings.push(...acceptedParsed.warnings);
|
|
127
|
+
xmlDoc = acceptedParsed.doc;
|
|
128
|
+
const acceptedParseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
|
|
129
|
+
if (acceptedParsed.error || acceptedParseError) {
|
|
130
|
+
const message = acceptedParsed.error?.message || acceptedParseError?.textContent || 'Could not parse OOXML after accepting existing revisions.';
|
|
131
|
+
error('[OxmlEngine] XML parse error after accepting existing revisions:', message);
|
|
93
132
|
return finalize({
|
|
94
|
-
oxml,
|
|
133
|
+
oxml: inputOoxml,
|
|
95
134
|
hasChanges: false,
|
|
96
135
|
status: 'error',
|
|
97
136
|
error: {
|
|
98
137
|
code: 'PARSE_ERROR',
|
|
99
|
-
message
|
|
138
|
+
message
|
|
100
139
|
}
|
|
101
140
|
});
|
|
102
141
|
}
|
|
103
|
-
seedRevisionIdsFromDocument(xmlDoc);
|
|
142
|
+
seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
|
|
104
143
|
} else {
|
|
105
144
|
log('[OxmlEngine] Existing revisions detected; rejecting input per existingRevisions policy');
|
|
106
145
|
return finalize({
|
|
107
|
-
oxml,
|
|
146
|
+
oxml: inputOoxml,
|
|
108
147
|
hasChanges: false,
|
|
109
148
|
status: 'error',
|
|
110
149
|
error: {
|
|
@@ -119,13 +158,20 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
119
158
|
if (initialTableCellContext.hasTableWrapper && initialTableCellContext.targetParagraph && !options._isolatedTableCell) {
|
|
120
159
|
log('[OxmlEngine] Isolating table-cell paragraph before diff');
|
|
121
160
|
const isolatedOxml = serializeParagraphOnly(xmlDoc, initialTableCellContext.targetParagraph, serializer);
|
|
122
|
-
|
|
123
|
-
...options,
|
|
124
|
-
_isolatedTableCell: true
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
const isolatedResult = await applyRedlineToOxml(isolatedOxml, originalText, modifiedText, {
|
|
162
|
+
...options,
|
|
163
|
+
_isolatedTableCell: true
|
|
164
|
+
});
|
|
165
|
+
if (!isolatedResult.hasChanges && isolatedResult.status === 'no-op') {
|
|
166
|
+
return finalizeUnchanged();
|
|
167
|
+
}
|
|
168
|
+
return isolatedResult;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const sanitizedText = options.sanitizeInput === true ? sanitizeAiResponse(modifiedText) : modifiedText;
|
|
172
|
+
if (sanitizedText !== modifiedText) {
|
|
173
|
+
operationWarnings.push('Input was sanitized; pass sanitizeInput: false to disable.');
|
|
174
|
+
}
|
|
129
175
|
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
130
176
|
|
|
131
177
|
const hasTextChanges = cleanModifiedText.trim() !== originalText.trim();
|
|
@@ -134,18 +180,30 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
134
180
|
const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
|
|
135
181
|
const hasExistingFormatting = existingFormatHints.length > 0;
|
|
136
182
|
const visibleText = textSpans.map(span => textSpanVisibleText(span)).join('');
|
|
183
|
+
const targetFound = originalText.includes('\n') || originalText.includes('\r')
|
|
184
|
+
? originalText
|
|
185
|
+
.split(/\r?\n/)
|
|
186
|
+
.map(normalizeTargetText)
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
.every(line => paragraphs.some(paragraph => {
|
|
189
|
+
const paragraphText = textSpans
|
|
190
|
+
.filter(span => span.paragraph === paragraph)
|
|
191
|
+
.map(textSpanVisibleText)
|
|
192
|
+
.join('');
|
|
193
|
+
return normalizeTargetText(paragraphText).includes(line);
|
|
194
|
+
}))
|
|
195
|
+
: visibleText.includes(originalText.trim())
|
|
196
|
+
|| visibleText.replace(/[\t\n\u2011]/g, '').includes(originalText.trim().replace(/[\t\n\u2011]/g, ''))
|
|
197
|
+
|| normalizeTargetText(visibleText).includes(normalizeTargetText(originalText));
|
|
137
198
|
if (
|
|
138
199
|
hasTextChanges
|
|
139
200
|
&& typeof originalText === 'string'
|
|
140
201
|
&& originalText.trim()
|
|
141
|
-
&& !
|
|
142
|
-
&& !visibleText.includes(originalText.trim())
|
|
143
|
-
&& !visibleText.replace(/[\t\n\u2011]/g, '').includes(originalText.trim().replace(/[\t\n\u2011]/g, ''))
|
|
144
|
-
&& !normalizeTargetText(visibleText).includes(normalizeTargetText(originalText))
|
|
202
|
+
&& !targetFound
|
|
145
203
|
) {
|
|
146
204
|
log('[OxmlEngine] Target text not found in OOXML');
|
|
147
205
|
return finalize({
|
|
148
|
-
oxml,
|
|
206
|
+
oxml: inputOoxml,
|
|
149
207
|
hasChanges: false,
|
|
150
208
|
status: 'error',
|
|
151
209
|
error: {
|
|
@@ -189,12 +247,20 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
189
247
|
|
|
190
248
|
log(`[OxmlEngine] Text changes: ${hasTextChanges}, New format hints: ${formatHints.length}, Existing format hints: ${existingFormatHints.length}`);
|
|
191
249
|
|
|
192
|
-
const needsFormatRemoval =
|
|
250
|
+
const needsFormatRemoval = options.removeFormatting === true
|
|
251
|
+
&& !hasTextChanges
|
|
252
|
+
&& !hasFormatHints
|
|
253
|
+
&& hasExistingFormatting;
|
|
193
254
|
|
|
194
|
-
if (!hasTextChanges && !hasFormatHints && !hasExistingFormatting) {
|
|
195
|
-
log('[OxmlEngine] No text changes, no format hints, and no existing formatting detected');
|
|
196
|
-
return
|
|
197
|
-
}
|
|
255
|
+
if (!hasTextChanges && !hasFormatHints && !hasExistingFormatting) {
|
|
256
|
+
log('[OxmlEngine] No text changes, no format hints, and no existing formatting detected');
|
|
257
|
+
return finalizeUnchanged();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!hasTextChanges && !hasFormatHints && hasExistingFormatting && !needsFormatRemoval) {
|
|
261
|
+
log('[OxmlEngine] No text or explicit formatting changes; preserving existing formatting');
|
|
262
|
+
return finalizeUnchanged();
|
|
263
|
+
}
|
|
198
264
|
|
|
199
265
|
if (needsFormatRemoval) {
|
|
200
266
|
log('[OxmlEngine] Format REMOVAL detected: applying surgical replacement in OOXML');
|
|
@@ -266,15 +332,16 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
266
332
|
const isTargetList = isListTargetLoose(cleanModifiedText);
|
|
267
333
|
const tableCellContext = initialTableCellContext;
|
|
268
334
|
|
|
269
|
-
log(`[OxmlEngine] Mode: ${hasTables ? 'SURGICAL' : 'RECONSTRUCTION'}, formatHints: ${formatHints.length}, isMarkdownTable: ${isMarkdownTable}, isTargetList: ${isTargetList}, isTableCellParagraph: ${tableCellContext.isTableCellParagraph}`);
|
|
270
|
-
|
|
271
|
-
|
|
335
|
+
log(`[OxmlEngine] Mode: ${hasTables ? 'SURGICAL' : 'RECONSTRUCTION'}, formatHints: ${formatHints.length}, isMarkdownTable: ${isMarkdownTable}, isTargetList: ${isTargetList}, isTableCellParagraph: ${tableCellContext.isTableCellParagraph}`);
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
if (isMarkdownTable && !hasTables) {
|
|
272
339
|
log('[OxmlEngine] Text-to-table transformation: generating new table from Markdown');
|
|
273
|
-
return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer,
|
|
340
|
+
return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
|
|
274
341
|
}
|
|
275
342
|
|
|
276
343
|
if (hasTables && isMarkdownTable) {
|
|
277
|
-
return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer,
|
|
344
|
+
return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
|
|
278
345
|
}
|
|
279
346
|
if (hasTables) {
|
|
280
347
|
const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph
|
|
@@ -289,10 +356,10 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
289
356
|
originalText,
|
|
290
357
|
cleanModifiedText,
|
|
291
358
|
serializer,
|
|
292
|
-
author,
|
|
293
|
-
formatHints,
|
|
294
|
-
generateRedlines,
|
|
295
|
-
surgicalTarget
|
|
359
|
+
author,
|
|
360
|
+
formatHints,
|
|
361
|
+
generateRedlines,
|
|
362
|
+
surgicalTarget
|
|
296
363
|
);
|
|
297
364
|
|
|
298
365
|
if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
|
|
@@ -303,22 +370,50 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
303
370
|
}
|
|
304
371
|
if (isTargetList) {
|
|
305
372
|
log('[OxmlEngine] 🎯 Using reconciliation pipeline for list generation');
|
|
306
|
-
const pipeline = new ReconciliationPipeline({
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
});
|
|
373
|
+
const pipeline = new ReconciliationPipeline({
|
|
374
|
+
author,
|
|
375
|
+
generateRedlines,
|
|
376
|
+
revisionIdAllocator
|
|
377
|
+
});
|
|
378
|
+
const result = await pipeline.execute(workingOoxml, sanitizedText, { xmlDoc });
|
|
379
|
+
|
|
380
|
+
if (result.error?.code === 'DIFF_TOKEN_LIMIT') {
|
|
381
|
+
return finalize({ oxml: inputOoxml, hasChanges: false, status: 'error', error: result.error });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (result.isValid && result.ooxml && result.ooxml !== workingOoxml) {
|
|
385
|
+
const includeNumbering = result.includeNumbering === true;
|
|
386
|
+
log(`[OxmlEngine] Wrapping list OOXML with numbering definitions, includeNumbering=${includeNumbering}`);
|
|
387
|
+
const wrapped = wrapInDocumentFragment(result.ooxml, {
|
|
388
|
+
includeNumbering,
|
|
389
|
+
numberingXml: result.numberingXml
|
|
390
|
+
});
|
|
315
391
|
log(`[OxmlEngine] ✅ Wrapped OOXML length: ${wrapped.length}`);
|
|
316
392
|
return finalize({ oxml: wrapped, hasChanges: true });
|
|
317
393
|
}
|
|
318
|
-
return
|
|
394
|
+
return finalizeUnchanged();
|
|
319
395
|
}
|
|
320
396
|
|
|
321
|
-
return finalize(applyReconstructionMode(
|
|
397
|
+
return finalize(applyReconstructionMode(
|
|
398
|
+
xmlDoc,
|
|
399
|
+
originalText,
|
|
400
|
+
cleanModifiedText,
|
|
401
|
+
serializer,
|
|
402
|
+
author,
|
|
403
|
+
formatHints,
|
|
404
|
+
generateRedlines
|
|
405
|
+
));
|
|
406
|
+
} catch (caught) {
|
|
407
|
+
if (isDiffTokenLimitError(caught)) {
|
|
408
|
+
return finalize({
|
|
409
|
+
oxml: inputOoxml,
|
|
410
|
+
hasChanges: false,
|
|
411
|
+
status: 'error',
|
|
412
|
+
error: { code: caught.code, message: caught.message }
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
throw caught;
|
|
416
|
+
}
|
|
322
417
|
}
|
|
323
418
|
|
|
324
419
|
function normalizeTargetText(text) {
|
|
@@ -340,14 +435,12 @@ function textSpanVisibleText(span) {
|
|
|
340
435
|
* @param {string} text - AI response text
|
|
341
436
|
* @returns {string}
|
|
342
437
|
*/
|
|
343
|
-
export function sanitizeAiResponse(text) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return cleaned;
|
|
350
|
-
}
|
|
438
|
+
export function sanitizeAiResponse(text) {
|
|
439
|
+
return String(text ?? '').replace(
|
|
440
|
+
/^(?:Here is the redline:|Here is the text:|Sure, I can help:|Here's the updated text:)[ \t]*\r?\n/i,
|
|
441
|
+
''
|
|
442
|
+
);
|
|
443
|
+
}
|
|
351
444
|
|
|
352
445
|
/**
|
|
353
446
|
* Parses OOXML into a DOM document.
|
|
@@ -355,9 +448,9 @@ export function sanitizeAiResponse(text) {
|
|
|
355
448
|
* @param {string} ooxmlString - OOXML text
|
|
356
449
|
* @returns {Document}
|
|
357
450
|
*/
|
|
358
|
-
export function parseOoxml(ooxmlString) {
|
|
359
|
-
return
|
|
360
|
-
}
|
|
451
|
+
export function parseOoxml(ooxmlString) {
|
|
452
|
+
return parseOoxmlSafe(ooxmlString, 'application/xml').doc;
|
|
453
|
+
}
|
|
361
454
|
|
|
362
455
|
/**
|
|
363
456
|
* Serializes a DOM document to OOXML text.
|