@ansonlai/docx-redline-js 0.8.2 → 0.8.3
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/CHANGELOG.md +586 -553
- package/README.md +901 -890
- package/core/list-targeting.js +12 -10
- package/dist/docx-redline-js.esm.js +154 -28
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +86 -86
- package/dist/docx-redline-js.esm.min.js.map +3 -3
- package/dist/docx-redline.bundle.cjs +154 -28
- package/dist/docx-redline.bundle.cjs.map +3 -3
- package/dist/docx-redline.bundle.js +154 -28
- package/dist/docx-redline.bundle.js.map +3 -3
- package/orchestration/list-structural-fallback.js +530 -518
- package/package.json +1 -1
- package/pipeline/list-generation.js +203 -168
- package/services/document-inspection.js +10 -7
- package/services/document-operation-mutations.js +1597 -1586
- package/services/numbering-helpers.js +108 -0
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OOXML mutation implementations used by the document operation applier.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
6
|
-
import { findReconstructionParagraphRange } from '../engine/reconstruction-mapper.js';
|
|
7
|
-
import { createRevisionMetadata } from '../core/types.js';
|
|
8
|
-
import { containsTrackedChanges, getTrackedChangeAuthors, createWordElement, withOoxmlSourceType } from '../core/word-xml.js';
|
|
9
|
-
import {
|
|
10
|
-
markParagraphMarkInserted,
|
|
11
|
-
markParagraphMarkDeleted,
|
|
12
|
-
snapshotAndAttachRPrChange,
|
|
13
|
-
snapshotAndAttachPPrChange
|
|
14
|
-
} from '../engine/run-builders.js';
|
|
15
|
-
import {
|
|
16
|
-
applyCharacterFormatToRPr,
|
|
17
|
-
checkRunPropertiesChanged,
|
|
18
|
-
applyParagraphPropertiesToPPr,
|
|
19
|
-
checkParagraphPropertiesChanged
|
|
20
|
-
} from '../engine/rpr-helpers.js';
|
|
21
|
-
import { refreshRunPropertyChangeIds } from '../core/revision-cloning.js';
|
|
22
|
-
import { getDefaultAuthor } from '../adapters/config.js';
|
|
1
|
+
/**
|
|
2
|
+
* OOXML mutation implementations used by the document operation applier.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
6
|
+
import { findReconstructionParagraphRange } from '../engine/reconstruction-mapper.js';
|
|
7
|
+
import { createRevisionMetadata } from '../core/types.js';
|
|
8
|
+
import { containsTrackedChanges, getTrackedChangeAuthors, createWordElement, withOoxmlSourceType } from '../core/word-xml.js';
|
|
9
|
+
import {
|
|
10
|
+
markParagraphMarkInserted,
|
|
11
|
+
markParagraphMarkDeleted,
|
|
12
|
+
snapshotAndAttachRPrChange,
|
|
13
|
+
snapshotAndAttachPPrChange
|
|
14
|
+
} from '../engine/run-builders.js';
|
|
15
|
+
import {
|
|
16
|
+
applyCharacterFormatToRPr,
|
|
17
|
+
checkRunPropertiesChanged,
|
|
18
|
+
applyParagraphPropertiesToPPr,
|
|
19
|
+
checkParagraphPropertiesChanged
|
|
20
|
+
} from '../engine/rpr-helpers.js';
|
|
21
|
+
import { refreshRunPropertyChangeIds } from '../core/revision-cloning.js';
|
|
22
|
+
import { getDefaultAuthor } from '../adapters/config.js';
|
|
23
23
|
import { applyRedlineToOxml as applyRedlineToOxmlEngine } from '../engine/oxml-engine.js';
|
|
24
24
|
import {
|
|
25
25
|
getParagraphRestorationRefusal,
|
|
@@ -34,56 +34,57 @@ import {
|
|
|
34
34
|
getRunTextLength,
|
|
35
35
|
splitTrackChangeCarrier
|
|
36
36
|
} from '../engine/surgical-run-splitting.js';
|
|
37
|
-
import { applyHighlightToOoxml } from '../engine/formatting-removal.js';
|
|
38
|
-
import { parseTable as parseMarkdownTable } from '../pipeline/pipeline.js';
|
|
39
|
-
import { injectCommentsIntoOoxml } from './comment-engine.js';
|
|
40
|
-
import {
|
|
41
|
-
getParagraphText as getParagraphTextFromOxml,
|
|
42
|
-
getParagraphId,
|
|
43
|
-
createParagraphFingerprint,
|
|
44
|
-
isMarkdownTableText,
|
|
45
|
-
findContainingWordElement,
|
|
37
|
+
import { applyHighlightToOoxml } from '../engine/formatting-removal.js';
|
|
38
|
+
import { parseTable as parseMarkdownTable } from '../pipeline/pipeline.js';
|
|
39
|
+
import { injectCommentsIntoOoxml } from './comment-engine.js';
|
|
40
|
+
import {
|
|
41
|
+
getParagraphText as getParagraphTextFromOxml,
|
|
42
|
+
getParagraphId,
|
|
43
|
+
createParagraphFingerprint,
|
|
44
|
+
isMarkdownTableText,
|
|
45
|
+
findContainingWordElement,
|
|
46
46
|
resolveTargetParagraphWithSnapshot as resolveTargetParagraphWithSnapshotShared,
|
|
47
47
|
resolveParagraphRangeByRefs,
|
|
48
48
|
getDocumentParagraphNodes,
|
|
49
49
|
validateParagraphBoundaryMutation
|
|
50
50
|
} from '../core/paragraph-targeting.js';
|
|
51
|
-
import {
|
|
52
|
-
synthesizeExpandedListScopeEdit,
|
|
53
|
-
planListInsertionOnlyEdit,
|
|
54
|
-
getParagraphListInfo,
|
|
55
|
-
stripRedundantLeadingListMarkers
|
|
56
|
-
} from '../core/list-targeting.js';
|
|
57
|
-
import {
|
|
58
|
-
synthesizeTableMarkdownFromMultilineCellEdit,
|
|
59
|
-
inferTableReplacementParagraphBlock
|
|
60
|
-
} from '../core/table-targeting.js';
|
|
61
|
-
import {
|
|
62
|
-
buildSingleLineListStructuralFallbackPlan,
|
|
63
|
-
executeSingleLineListStructuralFallback,
|
|
64
|
-
resolveSingleLineListFallbackNumberingAction,
|
|
65
|
-
recordSingleLineListFallbackExplicitSequence,
|
|
66
|
-
clearSingleLineListFallbackExplicitSequence,
|
|
67
|
-
enforceListBindingOnParagraphNodes,
|
|
68
|
-
stripSingleLineListMarkerPrefix
|
|
69
|
-
} from '../orchestration/list-structural-fallback.js';
|
|
70
|
-
import {
|
|
71
|
-
reserveNextNumberingIdPair,
|
|
72
|
-
remapNumberingPayloadForDocument,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
import {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
51
|
+
import {
|
|
52
|
+
synthesizeExpandedListScopeEdit,
|
|
53
|
+
planListInsertionOnlyEdit,
|
|
54
|
+
getParagraphListInfo,
|
|
55
|
+
stripRedundantLeadingListMarkers
|
|
56
|
+
} from '../core/list-targeting.js';
|
|
57
|
+
import {
|
|
58
|
+
synthesizeTableMarkdownFromMultilineCellEdit,
|
|
59
|
+
inferTableReplacementParagraphBlock
|
|
60
|
+
} from '../core/table-targeting.js';
|
|
61
|
+
import {
|
|
62
|
+
buildSingleLineListStructuralFallbackPlan,
|
|
63
|
+
executeSingleLineListStructuralFallback,
|
|
64
|
+
resolveSingleLineListFallbackNumberingAction,
|
|
65
|
+
recordSingleLineListFallbackExplicitSequence,
|
|
66
|
+
clearSingleLineListFallbackExplicitSequence,
|
|
67
|
+
enforceListBindingOnParagraphNodes,
|
|
68
|
+
stripSingleLineListMarkerPrefix
|
|
69
|
+
} from '../orchestration/list-structural-fallback.js';
|
|
70
|
+
import {
|
|
71
|
+
reserveNextNumberingIdPair,
|
|
72
|
+
remapNumberingPayloadForDocument,
|
|
73
|
+
reuseSourceListNumbering,
|
|
74
|
+
overwriteParagraphNumIds,
|
|
75
|
+
extractFirstParagraphNumId,
|
|
76
|
+
buildExplicitDecimalMultilevelNumberingXml
|
|
77
|
+
} from './numbering-helpers.js';
|
|
78
|
+
import {
|
|
79
|
+
extractReplacementNodesFromOoxml,
|
|
80
|
+
normalizeBodySectionOrderStandalone
|
|
81
|
+
} from './standalone-docx-plumbing.js';
|
|
82
|
+
import { prepareRevisionAllocator } from './document-operation-session.js';
|
|
83
|
+
import {
|
|
84
|
+
buildExplicitRangeInsertionEntries,
|
|
85
|
+
deriveSingleParagraphListAdjacencyInsertion,
|
|
86
|
+
deriveSingleParagraphPlainAdjacencyInsertion
|
|
87
|
+
} from './operation-heuristics.js';
|
|
87
88
|
import { resolveTargetFromCapture, ensureParagraphIdsOnImportedNode } from './capture-engine.js';
|
|
88
89
|
import {
|
|
89
90
|
acceptTrackedChangesInOoxml,
|
|
@@ -92,83 +93,83 @@ import {
|
|
|
92
93
|
|
|
93
94
|
const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
94
95
|
const NS_W14 = 'http://schemas.microsoft.com/office/word/2010/wordml';
|
|
95
|
-
|
|
96
|
-
function getCommentIdsInElement(element) {
|
|
97
|
-
const ids = new Set();
|
|
98
|
-
for (const localName of ['commentRangeStart', 'commentRangeEnd', 'commentReference']) {
|
|
99
|
-
for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W, localName) || [])) {
|
|
100
|
-
const id = node.getAttribute('w:id') || node.getAttribute('id');
|
|
101
|
-
if (id !== '') ids.add(id);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return Array.from(ids).sort((a, b) => Number(a) - Number(b) || a.localeCompare(b));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function commentDetailsForIds(ids, detailsById) {
|
|
108
|
-
if (!detailsById || typeof detailsById !== 'object') return [];
|
|
109
|
-
return ids.map(id => detailsById[id]).filter(Boolean);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function resolveMutationDocument(documentXml, options = {}) {
|
|
113
|
-
const operationSession = options?._documentOperationSession || null;
|
|
114
|
-
if (operationSession?.valid && operationSession.document) {
|
|
115
|
-
return {
|
|
116
|
-
xmlDoc: operationSession.document,
|
|
117
|
-
serializer: operationSession.serializer,
|
|
118
|
-
operationSession
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
return {
|
|
122
|
-
xmlDoc: parseOoxmlSafe(documentXml, 'application/xml').doc,
|
|
123
|
-
serializer: createSerializer(),
|
|
124
|
-
operationSession: null
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function completedDocumentXml(xmlDoc, serializer, documentXml, operationSession) {
|
|
129
|
-
return operationSession ? documentXml : serializer.serializeToString(xmlDoc);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}) {
|
|
133
|
-
const result = await applyRedlineToOxmlEngine(oxml, originalText, modifiedText, options);
|
|
134
|
-
if (result?.useNativeApi && typeof result?.oxml !== 'string') {
|
|
135
|
-
return withOoxmlSourceType({
|
|
136
|
-
...result,
|
|
137
|
-
oxml,
|
|
138
|
-
hasChanges: false,
|
|
139
|
-
warnings: [
|
|
140
|
-
...(Array.isArray(result?.warnings) ? result.warnings : []),
|
|
141
|
-
'Standalone mode cannot execute native Word API fallback for this operation.'
|
|
142
|
-
]
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function reconcileMarkdownTableOoxml(oxml, originalText, markdownTable, options = {}) {
|
|
149
|
-
const sourceOoxml = typeof oxml === 'string' ? oxml : '';
|
|
150
|
-
const tableText = typeof markdownTable === 'string' ? markdownTable : String(markdownTable || '');
|
|
151
|
-
let tableData;
|
|
152
|
-
try {
|
|
153
|
-
tableData = parseMarkdownTable(tableText);
|
|
154
|
-
} catch {
|
|
155
|
-
tableData = { headers: [], rows: [] };
|
|
156
|
-
}
|
|
157
|
-
if (!((tableData?.headers?.length || 0) > 0 || (tableData?.rows?.length || 0) > 0)) {
|
|
158
|
-
return {
|
|
159
|
-
oxml: sourceOoxml,
|
|
160
|
-
hasChanges: false,
|
|
161
|
-
isMarkdownTable: false,
|
|
162
|
-
warnings: ['Could not parse Markdown table from input.']
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
...await applyRedlineToOxml(sourceOoxml, originalText || '', tableText, options),
|
|
167
|
-
isMarkdownTable: true,
|
|
168
|
-
tableData
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
96
|
+
|
|
97
|
+
function getCommentIdsInElement(element) {
|
|
98
|
+
const ids = new Set();
|
|
99
|
+
for (const localName of ['commentRangeStart', 'commentRangeEnd', 'commentReference']) {
|
|
100
|
+
for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W, localName) || [])) {
|
|
101
|
+
const id = node.getAttribute('w:id') || node.getAttribute('id');
|
|
102
|
+
if (id !== '') ids.add(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return Array.from(ids).sort((a, b) => Number(a) - Number(b) || a.localeCompare(b));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function commentDetailsForIds(ids, detailsById) {
|
|
109
|
+
if (!detailsById || typeof detailsById !== 'object') return [];
|
|
110
|
+
return ids.map(id => detailsById[id]).filter(Boolean);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveMutationDocument(documentXml, options = {}) {
|
|
114
|
+
const operationSession = options?._documentOperationSession || null;
|
|
115
|
+
if (operationSession?.valid && operationSession.document) {
|
|
116
|
+
return {
|
|
117
|
+
xmlDoc: operationSession.document,
|
|
118
|
+
serializer: operationSession.serializer,
|
|
119
|
+
operationSession
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
xmlDoc: parseOoxmlSafe(documentXml, 'application/xml').doc,
|
|
124
|
+
serializer: createSerializer(),
|
|
125
|
+
operationSession: null
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function completedDocumentXml(xmlDoc, serializer, documentXml, operationSession) {
|
|
130
|
+
return operationSession ? documentXml : serializer.serializeToString(xmlDoc);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}) {
|
|
134
|
+
const result = await applyRedlineToOxmlEngine(oxml, originalText, modifiedText, options);
|
|
135
|
+
if (result?.useNativeApi && typeof result?.oxml !== 'string') {
|
|
136
|
+
return withOoxmlSourceType({
|
|
137
|
+
...result,
|
|
138
|
+
oxml,
|
|
139
|
+
hasChanges: false,
|
|
140
|
+
warnings: [
|
|
141
|
+
...(Array.isArray(result?.warnings) ? result.warnings : []),
|
|
142
|
+
'Standalone mode cannot execute native Word API fallback for this operation.'
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function reconcileMarkdownTableOoxml(oxml, originalText, markdownTable, options = {}) {
|
|
150
|
+
const sourceOoxml = typeof oxml === 'string' ? oxml : '';
|
|
151
|
+
const tableText = typeof markdownTable === 'string' ? markdownTable : String(markdownTable || '');
|
|
152
|
+
let tableData;
|
|
153
|
+
try {
|
|
154
|
+
tableData = parseMarkdownTable(tableText);
|
|
155
|
+
} catch {
|
|
156
|
+
tableData = { headers: [], rows: [] };
|
|
157
|
+
}
|
|
158
|
+
if (!((tableData?.headers?.length || 0) > 0 || (tableData?.rows?.length || 0) > 0)) {
|
|
159
|
+
return {
|
|
160
|
+
oxml: sourceOoxml,
|
|
161
|
+
hasChanges: false,
|
|
162
|
+
isMarkdownTable: false,
|
|
163
|
+
warnings: ['Could not parse Markdown table from input.']
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
...await applyRedlineToOxml(sourceOoxml, originalText || '', tableText, options),
|
|
168
|
+
isMarkdownTable: true,
|
|
169
|
+
tableData
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
172
173
|
function getParagraphText(paragraph) {
|
|
173
174
|
return getParagraphTextFromOxml(paragraph);
|
|
174
175
|
}
|
|
@@ -255,9 +256,9 @@ function resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, opti
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeContext = null, options = {}) {
|
|
258
|
-
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
259
|
-
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
260
|
-
const session = options?._documentOperationSession || null;
|
|
259
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
260
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
261
|
+
const session = options?._documentOperationSession || null;
|
|
261
262
|
const paragraphMetadataIndex = session?.getParagraphMetadataIndex?.() || null;
|
|
262
263
|
|
|
263
264
|
if (options?._compiledSourceId && options?._sourceTargetRegistry) {
|
|
@@ -274,244 +275,244 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
274
275
|
}
|
|
275
276
|
return resolved;
|
|
276
277
|
}
|
|
277
|
-
|
|
278
|
-
if (options?.targetDescriptor?.captureRef) {
|
|
279
|
-
try {
|
|
280
|
-
const resolved = resolveTargetFromCapture(xmlDoc, session, options.targetDescriptor, opType, options);
|
|
281
|
-
if (options?._resolutionCapture && resolved?.paragraph) {
|
|
282
|
-
const paragraph = resolved.paragraph;
|
|
283
|
-
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
278
|
+
|
|
279
|
+
if (options?.targetDescriptor?.captureRef) {
|
|
280
|
+
try {
|
|
281
|
+
const resolved = resolveTargetFromCapture(xmlDoc, session, options.targetDescriptor, opType, options);
|
|
282
|
+
if (options?._resolutionCapture && resolved?.paragraph) {
|
|
283
|
+
const paragraph = resolved.paragraph;
|
|
284
|
+
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
284
285
|
Object.assign(options._resolutionCapture, {
|
|
285
286
|
resolvedBy: resolved.resolvedBy,
|
|
286
287
|
resolvedTarget: resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options)
|
|
287
288
|
});
|
|
288
|
-
}
|
|
289
|
-
return resolved;
|
|
290
|
-
} catch (error) {
|
|
291
|
-
return {
|
|
292
|
-
error: {
|
|
293
|
-
code: typeof error?.code === 'string' && error.code ? error.code : 'TARGET_NOT_FOUND',
|
|
294
|
-
message: error?.message || String(error)
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const resolved = resolveTargetParagraphWithSnapshotShared(xmlDoc, {
|
|
301
|
-
targetText,
|
|
302
|
-
targetRef,
|
|
303
|
-
opType,
|
|
304
|
-
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
305
|
-
targetDescriptor: options?.targetDescriptor || null,
|
|
306
|
-
strictAmbiguity: options?.strictTargets === true,
|
|
307
|
-
paragraphMetadataIndex,
|
|
308
|
-
onInfo,
|
|
309
|
-
onWarn
|
|
310
|
-
});
|
|
311
|
-
if (options?._resolutionCapture && resolved?.paragraph) {
|
|
312
|
-
const paragraph = resolved.paragraph;
|
|
313
|
-
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
289
|
+
}
|
|
290
|
+
return resolved;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
return {
|
|
293
|
+
error: {
|
|
294
|
+
code: typeof error?.code === 'string' && error.code ? error.code : 'TARGET_NOT_FOUND',
|
|
295
|
+
message: error?.message || String(error)
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const resolved = resolveTargetParagraphWithSnapshotShared(xmlDoc, {
|
|
302
|
+
targetText,
|
|
303
|
+
targetRef,
|
|
304
|
+
opType,
|
|
305
|
+
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
306
|
+
targetDescriptor: options?.targetDescriptor || null,
|
|
307
|
+
strictAmbiguity: options?.strictTargets === true,
|
|
308
|
+
paragraphMetadataIndex,
|
|
309
|
+
onInfo,
|
|
310
|
+
onWarn
|
|
311
|
+
});
|
|
312
|
+
if (options?._resolutionCapture && resolved?.paragraph) {
|
|
313
|
+
const paragraph = resolved.paragraph;
|
|
314
|
+
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
314
315
|
Object.assign(options._resolutionCapture, {
|
|
315
316
|
resolvedBy: resolved.resolvedBy,
|
|
316
317
|
resolvedTarget: resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options)
|
|
317
318
|
});
|
|
318
|
-
}
|
|
319
|
-
return resolved;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function extractReplacementNodes(outputOxml) {
|
|
323
|
-
return extractReplacementNodesFromOoxml(outputOxml);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function removeListPackagingSentinel(replacementNodes, warnings) {
|
|
327
|
-
if (!Array.isArray(replacementNodes) || replacementNodes.length === 0) return replacementNodes;
|
|
328
|
-
if (!Array.isArray(warnings) || !warnings.includes('Paragraph expanded to list fragment')) return replacementNodes;
|
|
329
|
-
|
|
330
|
-
const last = replacementNodes[replacementNodes.length - 1];
|
|
331
|
-
if (!last || last.localName !== 'p') return replacementNodes;
|
|
332
|
-
const meaningfulContent = Array.from(last.childNodes || []).some(child => (
|
|
333
|
-
child.nodeType === 1 && child.localName !== 'pPr'
|
|
334
|
-
));
|
|
335
|
-
if (meaningfulContent) return replacementNodes;
|
|
336
|
-
return replacementNodes.slice(0, -1);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function normalizeBodySectionOrder(xmlDoc) {
|
|
340
|
-
normalizeBodySectionOrderStandalone(xmlDoc);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function removeProofErrNodes(paragraph) {
|
|
344
|
-
for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, 'proofErr') || [])) {
|
|
345
|
-
node.parentNode?.removeChild(node);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function preprocessRedlineTargetParagraph(targetParagraph) {
|
|
350
|
-
if (!targetParagraph) return;
|
|
351
|
-
removeProofErrNodes(targetParagraph);
|
|
352
|
-
}
|
|
353
|
-
|
|
319
|
+
}
|
|
320
|
+
return resolved;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function extractReplacementNodes(outputOxml) {
|
|
324
|
+
return extractReplacementNodesFromOoxml(outputOxml);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function removeListPackagingSentinel(replacementNodes, warnings) {
|
|
328
|
+
if (!Array.isArray(replacementNodes) || replacementNodes.length === 0) return replacementNodes;
|
|
329
|
+
if (!Array.isArray(warnings) || !warnings.includes('Paragraph expanded to list fragment')) return replacementNodes;
|
|
330
|
+
|
|
331
|
+
const last = replacementNodes[replacementNodes.length - 1];
|
|
332
|
+
if (!last || last.localName !== 'p') return replacementNodes;
|
|
333
|
+
const meaningfulContent = Array.from(last.childNodes || []).some(child => (
|
|
334
|
+
child.nodeType === 1 && child.localName !== 'pPr'
|
|
335
|
+
));
|
|
336
|
+
if (meaningfulContent) return replacementNodes;
|
|
337
|
+
return replacementNodes.slice(0, -1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function normalizeBodySectionOrder(xmlDoc) {
|
|
341
|
+
normalizeBodySectionOrderStandalone(xmlDoc);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function removeProofErrNodes(paragraph) {
|
|
345
|
+
for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, 'proofErr') || [])) {
|
|
346
|
+
node.parentNode?.removeChild(node);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function preprocessRedlineTargetParagraph(targetParagraph) {
|
|
351
|
+
if (!targetParagraph) return;
|
|
352
|
+
removeProofErrNodes(targetParagraph);
|
|
353
|
+
}
|
|
354
|
+
|
|
354
355
|
function getDirectWordChild(element, localName) {
|
|
355
|
-
if (!element) return null;
|
|
356
|
-
return Array.from(element.childNodes || []).find(
|
|
357
|
-
node => node && node.nodeType === 1 && node.namespaceURI === NS_W && node.localName === localName
|
|
358
|
-
) || null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function computeTableIndexInDocument(xmlDoc, tableElement) {
|
|
362
|
-
if (!xmlDoc || !tableElement) return null;
|
|
363
|
-
const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'tbl'));
|
|
364
|
-
const idx = tables.indexOf(tableElement);
|
|
365
|
-
return idx >= 0 ? idx + 1 : null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function normalizeMultilineTableStructuralPayload(text) {
|
|
369
|
-
return String(text || '')
|
|
370
|
-
.replace(/\r\n/g, '\n')
|
|
371
|
-
.split('\n')
|
|
372
|
-
.map(line => line.trim())
|
|
373
|
-
.filter(Boolean)
|
|
374
|
-
.join('\n');
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function computeTableStructuralDedupeKey(xmlDoc, containingTable, modifiedText) {
|
|
378
|
-
const tableIndex = computeTableIndexInDocument(xmlDoc, containingTable);
|
|
379
|
-
if (!Number.isInteger(tableIndex) || tableIndex < 1) return null;
|
|
380
|
-
const normalizedPayload = normalizeMultilineTableStructuralPayload(modifiedText);
|
|
381
|
-
if (!normalizedPayload) return null;
|
|
382
|
-
return `table:${tableIndex}|payload:${normalizedPayload}`;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function ensureListProperties(xmlDoc, paragraph, ilvl, numId) {
|
|
386
|
-
let pPr = getDirectWordChild(paragraph, 'pPr');
|
|
387
|
-
if (!pPr) {
|
|
388
|
-
pPr = createWordElement(xmlDoc, 'w:pPr');
|
|
389
|
-
paragraph.insertBefore(pPr, paragraph.firstChild);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
let numPr = getDirectWordChild(pPr, 'numPr');
|
|
393
|
-
if (!numPr) {
|
|
394
|
-
numPr = createWordElement(xmlDoc, 'w:numPr');
|
|
395
|
-
pPr.appendChild(numPr);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
let ilvlEl = getDirectWordChild(numPr, 'ilvl');
|
|
399
|
-
if (!ilvlEl) {
|
|
400
|
-
ilvlEl = createWordElement(xmlDoc, 'w:ilvl');
|
|
401
|
-
numPr.appendChild(ilvlEl);
|
|
402
|
-
}
|
|
403
|
-
ilvlEl.setAttribute('w:val', String(Math.max(0, Number.parseInt(ilvl, 10) || 0)));
|
|
404
|
-
|
|
405
|
-
let numIdEl = getDirectWordChild(numPr, 'numId');
|
|
406
|
-
if (!numIdEl) {
|
|
407
|
-
numIdEl = createWordElement(xmlDoc, 'w:numId');
|
|
408
|
-
numPr.appendChild(numIdEl);
|
|
409
|
-
}
|
|
410
|
-
numIdEl.setAttribute('w:val', String(numId));
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMetadata, author, options = {}) {
|
|
414
|
-
const generateRedlines = options.generateRedlines !== false;
|
|
415
|
-
const paragraph = createWordElement(xmlDoc, 'w:p');
|
|
416
|
-
|
|
417
|
-
const anchorPPr = getDirectWordChild(anchorParagraph, 'pPr');
|
|
418
|
-
if (anchorPPr) {
|
|
356
|
+
if (!element) return null;
|
|
357
|
+
return Array.from(element.childNodes || []).find(
|
|
358
|
+
node => node && node.nodeType === 1 && node.namespaceURI === NS_W && node.localName === localName
|
|
359
|
+
) || null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function computeTableIndexInDocument(xmlDoc, tableElement) {
|
|
363
|
+
if (!xmlDoc || !tableElement) return null;
|
|
364
|
+
const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'tbl'));
|
|
365
|
+
const idx = tables.indexOf(tableElement);
|
|
366
|
+
return idx >= 0 ? idx + 1 : null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function normalizeMultilineTableStructuralPayload(text) {
|
|
370
|
+
return String(text || '')
|
|
371
|
+
.replace(/\r\n/g, '\n')
|
|
372
|
+
.split('\n')
|
|
373
|
+
.map(line => line.trim())
|
|
374
|
+
.filter(Boolean)
|
|
375
|
+
.join('\n');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function computeTableStructuralDedupeKey(xmlDoc, containingTable, modifiedText) {
|
|
379
|
+
const tableIndex = computeTableIndexInDocument(xmlDoc, containingTable);
|
|
380
|
+
if (!Number.isInteger(tableIndex) || tableIndex < 1) return null;
|
|
381
|
+
const normalizedPayload = normalizeMultilineTableStructuralPayload(modifiedText);
|
|
382
|
+
if (!normalizedPayload) return null;
|
|
383
|
+
return `table:${tableIndex}|payload:${normalizedPayload}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function ensureListProperties(xmlDoc, paragraph, ilvl, numId) {
|
|
387
|
+
let pPr = getDirectWordChild(paragraph, 'pPr');
|
|
388
|
+
if (!pPr) {
|
|
389
|
+
pPr = createWordElement(xmlDoc, 'w:pPr');
|
|
390
|
+
paragraph.insertBefore(pPr, paragraph.firstChild);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let numPr = getDirectWordChild(pPr, 'numPr');
|
|
394
|
+
if (!numPr) {
|
|
395
|
+
numPr = createWordElement(xmlDoc, 'w:numPr');
|
|
396
|
+
pPr.appendChild(numPr);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let ilvlEl = getDirectWordChild(numPr, 'ilvl');
|
|
400
|
+
if (!ilvlEl) {
|
|
401
|
+
ilvlEl = createWordElement(xmlDoc, 'w:ilvl');
|
|
402
|
+
numPr.appendChild(ilvlEl);
|
|
403
|
+
}
|
|
404
|
+
ilvlEl.setAttribute('w:val', String(Math.max(0, Number.parseInt(ilvl, 10) || 0)));
|
|
405
|
+
|
|
406
|
+
let numIdEl = getDirectWordChild(numPr, 'numId');
|
|
407
|
+
if (!numIdEl) {
|
|
408
|
+
numIdEl = createWordElement(xmlDoc, 'w:numId');
|
|
409
|
+
numPr.appendChild(numIdEl);
|
|
410
|
+
}
|
|
411
|
+
numIdEl.setAttribute('w:val', String(numId));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMetadata, author, options = {}) {
|
|
415
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
416
|
+
const paragraph = createWordElement(xmlDoc, 'w:p');
|
|
417
|
+
|
|
418
|
+
const anchorPPr = getDirectWordChild(anchorParagraph, 'pPr');
|
|
419
|
+
if (anchorPPr) {
|
|
419
420
|
paragraph.appendChild(clonePropertiesWithoutRevisionHistory(anchorPPr));
|
|
420
|
-
}
|
|
421
|
-
ensureListProperties(xmlDoc, paragraph, entry.ilvl, entry.numId);
|
|
422
|
-
|
|
423
|
-
// A new list item is a whole inserted paragraph, not merely inserted text.
|
|
424
|
-
// Tracking its paragraph mark lets Word (and our accept/reject helpers)
|
|
425
|
-
// remove the list paragraph itself on Reject All instead of leaving an
|
|
426
|
-
// empty bullet or number behind.
|
|
427
|
-
if (generateRedlines) {
|
|
428
|
-
markParagraphMarkInserted(xmlDoc, paragraph, author);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const run = createWordElement(xmlDoc, 'w:r');
|
|
432
|
-
const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
|
|
433
|
-
const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
|
|
434
|
-
if (anchorRunPr) {
|
|
421
|
+
}
|
|
422
|
+
ensureListProperties(xmlDoc, paragraph, entry.ilvl, entry.numId);
|
|
423
|
+
|
|
424
|
+
// A new list item is a whole inserted paragraph, not merely inserted text.
|
|
425
|
+
// Tracking its paragraph mark lets Word (and our accept/reject helpers)
|
|
426
|
+
// remove the list paragraph itself on Reject All instead of leaving an
|
|
427
|
+
// empty bullet or number behind.
|
|
428
|
+
if (generateRedlines) {
|
|
429
|
+
markParagraphMarkInserted(xmlDoc, paragraph, author);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const run = createWordElement(xmlDoc, 'w:r');
|
|
433
|
+
const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
|
|
434
|
+
const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
|
|
435
|
+
if (anchorRunPr) {
|
|
435
436
|
run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const textNode = createWordElement(xmlDoc, 'w:t');
|
|
439
|
-
const safeText = String(entry.text || '').trim();
|
|
440
|
-
if (/^\s|\s$/.test(safeText)) textNode.setAttribute('xml:space', 'preserve');
|
|
441
|
-
textNode.textContent = safeText;
|
|
442
|
-
run.appendChild(textNode);
|
|
443
|
-
if (generateRedlines) {
|
|
444
|
-
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc);
|
|
445
|
-
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
446
|
-
ins.setAttribute('w:id', String(metadata.id));
|
|
447
|
-
ins.setAttribute('w:author', metadata.author);
|
|
448
|
-
ins.setAttribute('w:date', metadata.date);
|
|
449
|
-
ins.appendChild(run);
|
|
450
|
-
paragraph.appendChild(ins);
|
|
451
|
-
} else {
|
|
452
|
-
paragraph.appendChild(run);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return paragraph;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function serializeParagraphRangeAsDocument(paragraphs, serializer) {
|
|
459
|
-
const paragraphXml = (paragraphs || [])
|
|
460
|
-
.map(paragraph => serializer.serializeToString(paragraph))
|
|
461
|
-
.join('');
|
|
462
|
-
return `<w:document xmlns:w="${NS_W}"><w:body>${paragraphXml}</w:body></w:document>`;
|
|
463
|
-
}
|
|
464
|
-
function applyExplicitRangeListInsertions({
|
|
465
|
-
xmlDoc,
|
|
466
|
-
explicitRangeParagraphs,
|
|
467
|
-
insertionEntries,
|
|
468
|
-
generateRedlines,
|
|
469
|
-
author
|
|
470
|
-
}) {
|
|
471
|
-
if (!Array.isArray(explicitRangeParagraphs) || explicitRangeParagraphs.length === 0) return false;
|
|
472
|
-
if (!Array.isArray(insertionEntries) || insertionEntries.length === 0) return false;
|
|
473
|
-
|
|
474
|
-
const parent = explicitRangeParagraphs[0].parentNode;
|
|
475
|
-
if (!parent || explicitRangeParagraphs.some(paragraph => paragraph.parentNode !== parent)) return false;
|
|
476
|
-
|
|
477
|
-
const tailInsertionPoint = explicitRangeParagraphs[explicitRangeParagraphs.length - 1].nextSibling;
|
|
478
|
-
for (const entry of insertionEntries) {
|
|
479
|
-
const referenceParagraph = entry.insertBeforeOriginalIndex != null
|
|
480
|
-
? explicitRangeParagraphs[entry.insertBeforeOriginalIndex]
|
|
481
|
-
: explicitRangeParagraphs[explicitRangeParagraphs.length - 1];
|
|
482
|
-
if (!referenceParagraph) return false;
|
|
483
|
-
|
|
484
|
-
const listParagraph = buildInsertedListParagraph(
|
|
485
|
-
xmlDoc,
|
|
486
|
-
referenceParagraph,
|
|
487
|
-
{
|
|
488
|
-
ilvl: entry.ilvl,
|
|
489
|
-
markerType: entry.markerType,
|
|
490
|
-
numId: entry.numId,
|
|
491
|
-
text: entry.text
|
|
492
|
-
},
|
|
493
|
-
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
494
|
-
author,
|
|
495
|
-
{ generateRedlines }
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
if (entry.insertBeforeOriginalIndex != null) {
|
|
499
|
-
parent.insertBefore(listParagraph, referenceParagraph);
|
|
500
|
-
} else {
|
|
501
|
-
parent.insertBefore(listParagraph, tailInsertionPoint);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
normalizeBodySectionOrder(xmlDoc);
|
|
506
|
-
return true;
|
|
507
|
-
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const textNode = createWordElement(xmlDoc, 'w:t');
|
|
440
|
+
const safeText = String(entry.text || '').trim();
|
|
441
|
+
if (/^\s|\s$/.test(safeText)) textNode.setAttribute('xml:space', 'preserve');
|
|
442
|
+
textNode.textContent = safeText;
|
|
443
|
+
run.appendChild(textNode);
|
|
444
|
+
if (generateRedlines) {
|
|
445
|
+
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc);
|
|
446
|
+
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
447
|
+
ins.setAttribute('w:id', String(metadata.id));
|
|
448
|
+
ins.setAttribute('w:author', metadata.author);
|
|
449
|
+
ins.setAttribute('w:date', metadata.date);
|
|
450
|
+
ins.appendChild(run);
|
|
451
|
+
paragraph.appendChild(ins);
|
|
452
|
+
} else {
|
|
453
|
+
paragraph.appendChild(run);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return paragraph;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function serializeParagraphRangeAsDocument(paragraphs, serializer) {
|
|
460
|
+
const paragraphXml = (paragraphs || [])
|
|
461
|
+
.map(paragraph => serializer.serializeToString(paragraph))
|
|
462
|
+
.join('');
|
|
463
|
+
return `<w:document xmlns:w="${NS_W}"><w:body>${paragraphXml}</w:body></w:document>`;
|
|
464
|
+
}
|
|
465
|
+
function applyExplicitRangeListInsertions({
|
|
466
|
+
xmlDoc,
|
|
467
|
+
explicitRangeParagraphs,
|
|
468
|
+
insertionEntries,
|
|
469
|
+
generateRedlines,
|
|
470
|
+
author
|
|
471
|
+
}) {
|
|
472
|
+
if (!Array.isArray(explicitRangeParagraphs) || explicitRangeParagraphs.length === 0) return false;
|
|
473
|
+
if (!Array.isArray(insertionEntries) || insertionEntries.length === 0) return false;
|
|
474
|
+
|
|
475
|
+
const parent = explicitRangeParagraphs[0].parentNode;
|
|
476
|
+
if (!parent || explicitRangeParagraphs.some(paragraph => paragraph.parentNode !== parent)) return false;
|
|
477
|
+
|
|
478
|
+
const tailInsertionPoint = explicitRangeParagraphs[explicitRangeParagraphs.length - 1].nextSibling;
|
|
479
|
+
for (const entry of insertionEntries) {
|
|
480
|
+
const referenceParagraph = entry.insertBeforeOriginalIndex != null
|
|
481
|
+
? explicitRangeParagraphs[entry.insertBeforeOriginalIndex]
|
|
482
|
+
: explicitRangeParagraphs[explicitRangeParagraphs.length - 1];
|
|
483
|
+
if (!referenceParagraph) return false;
|
|
484
|
+
|
|
485
|
+
const listParagraph = buildInsertedListParagraph(
|
|
486
|
+
xmlDoc,
|
|
487
|
+
referenceParagraph,
|
|
488
|
+
{
|
|
489
|
+
ilvl: entry.ilvl,
|
|
490
|
+
markerType: entry.markerType,
|
|
491
|
+
numId: entry.numId,
|
|
492
|
+
text: entry.text
|
|
493
|
+
},
|
|
494
|
+
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
495
|
+
author,
|
|
496
|
+
{ generateRedlines }
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
if (entry.insertBeforeOriginalIndex != null) {
|
|
500
|
+
parent.insertBefore(listParagraph, referenceParagraph);
|
|
501
|
+
} else {
|
|
502
|
+
parent.insertBefore(listParagraph, tailInsertionPoint);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
508
509
|
function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionMetadata, author, options = {}) {
|
|
509
|
-
const generateRedlines = options.generateRedlines !== false;
|
|
510
|
-
const paragraph = createWordElement(xmlDoc, 'w:p');
|
|
511
|
-
const run = createWordElement(xmlDoc, 'w:r');
|
|
512
|
-
const textNode = createWordElement(xmlDoc, 'w:t');
|
|
513
|
-
const safeText = String(text || '');
|
|
514
|
-
if (/^\s|\s$/.test(safeText)) textNode.setAttribute('xml:space', 'preserve');
|
|
510
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
511
|
+
const paragraph = createWordElement(xmlDoc, 'w:p');
|
|
512
|
+
const run = createWordElement(xmlDoc, 'w:r');
|
|
513
|
+
const textNode = createWordElement(xmlDoc, 'w:t');
|
|
514
|
+
const safeText = String(text || '');
|
|
515
|
+
if (/^\s|\s$/.test(safeText)) textNode.setAttribute('xml:space', 'preserve');
|
|
515
516
|
textNode.textContent = safeText;
|
|
516
517
|
run.appendChild(textNode);
|
|
517
518
|
|
|
@@ -520,36 +521,36 @@ function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionMetadata, aut
|
|
|
520
521
|
markParagraphMarkInserted(xmlDoc, paragraph, author);
|
|
521
522
|
}
|
|
522
523
|
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, 'ins');
|
|
523
|
-
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
524
|
-
ins.setAttribute('w:id', String(metadata.id));
|
|
525
|
-
ins.setAttribute('w:author', metadata.author);
|
|
526
|
-
ins.setAttribute('w:date', metadata.date);
|
|
527
|
-
ins.appendChild(run);
|
|
528
|
-
paragraph.appendChild(ins);
|
|
529
|
-
} else {
|
|
530
|
-
paragraph.appendChild(run);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return paragraph;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
|
|
537
|
-
const paragraph = createWordElement(xmlDoc, 'w:p');
|
|
538
|
-
const anchorPPr = getDirectWordChild(anchorParagraph, 'pPr');
|
|
524
|
+
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
525
|
+
ins.setAttribute('w:id', String(metadata.id));
|
|
526
|
+
ins.setAttribute('w:author', metadata.author);
|
|
527
|
+
ins.setAttribute('w:date', metadata.date);
|
|
528
|
+
ins.appendChild(run);
|
|
529
|
+
paragraph.appendChild(ins);
|
|
530
|
+
} else {
|
|
531
|
+
paragraph.appendChild(run);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return paragraph;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
|
|
538
|
+
const paragraph = createWordElement(xmlDoc, 'w:p');
|
|
539
|
+
const anchorPPr = getDirectWordChild(anchorParagraph, 'pPr');
|
|
539
540
|
if (anchorPPr) paragraph.appendChild(clonePropertiesWithoutRevisionHistory(anchorPPr));
|
|
540
|
-
|
|
541
|
-
const run = createWordElement(xmlDoc, 'w:r');
|
|
542
|
-
const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
|
|
543
|
-
const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
|
|
541
|
+
|
|
542
|
+
const run = createWordElement(xmlDoc, 'w:r');
|
|
543
|
+
const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
|
|
544
|
+
const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
|
|
544
545
|
if (anchorRunPr) run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
|
|
545
|
-
|
|
546
|
-
const textNode = createWordElement(xmlDoc, 'w:t');
|
|
547
|
-
textNode.textContent = '';
|
|
548
|
-
run.appendChild(textNode);
|
|
549
|
-
paragraph.appendChild(run);
|
|
550
|
-
return paragraph;
|
|
551
|
-
}
|
|
552
|
-
|
|
546
|
+
|
|
547
|
+
const textNode = createWordElement(xmlDoc, 'w:t');
|
|
548
|
+
textNode.textContent = '';
|
|
549
|
+
run.appendChild(textNode);
|
|
550
|
+
paragraph.appendChild(run);
|
|
551
|
+
return paragraph;
|
|
552
|
+
}
|
|
553
|
+
|
|
553
554
|
function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, author, options = {}) {
|
|
554
555
|
const wrappedParagraph = createWordElement(xmlDoc, 'w:p');
|
|
555
556
|
const pPr = options.sanitizeParagraphProperties === true
|
|
@@ -563,71 +564,71 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
|
|
|
563
564
|
|
|
564
565
|
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
565
566
|
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, 'ins');
|
|
566
|
-
ins.setAttribute('w:id', String(metadata.id));
|
|
567
|
-
ins.setAttribute('w:author', metadata.author);
|
|
568
|
-
ins.setAttribute('w:date', metadata.date);
|
|
569
|
-
|
|
570
|
-
for (const child of Array.from(paragraph.childNodes || [])) {
|
|
571
|
-
if (child?.nodeType === 1 && child.namespaceURI === NS_W && child.localName === 'pPr') continue;
|
|
572
|
-
ins.appendChild(child.cloneNode(true));
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
wrappedParagraph.appendChild(ins);
|
|
576
|
-
return wrappedParagraph;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisionMetadata, author, options = {}) {
|
|
580
|
-
const generateRedlines = options.generateRedlines !== false;
|
|
581
|
-
const serializer = createSerializer();
|
|
582
|
-
const templateParagraph = buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph);
|
|
583
|
-
const templateXml = serializer.serializeToString(templateParagraph);
|
|
584
|
-
const markdownResult = await applyRedlineToOxml(
|
|
585
|
-
templateXml,
|
|
586
|
-
'',
|
|
587
|
-
String(text || ''),
|
|
588
|
-
{
|
|
589
|
-
author,
|
|
590
|
-
generateRedlines: false
|
|
591
|
-
}
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
let sourceParagraph = null;
|
|
595
|
-
if (typeof markdownResult?.oxml === 'string') {
|
|
596
|
-
const extracted = extractReplacementNodes(markdownResult.oxml);
|
|
597
|
-
sourceParagraph = (extracted.replacementNodes || []).find(
|
|
598
|
-
node => node && node.nodeType === 1 && node.namespaceURI === NS_W && node.localName === 'p'
|
|
599
|
-
) || null;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (!sourceParagraph) {
|
|
603
|
-
return buildFallbackInsertedPlainParagraph(
|
|
604
|
-
xmlDoc,
|
|
605
|
-
text,
|
|
567
|
+
ins.setAttribute('w:id', String(metadata.id));
|
|
568
|
+
ins.setAttribute('w:author', metadata.author);
|
|
569
|
+
ins.setAttribute('w:date', metadata.date);
|
|
570
|
+
|
|
571
|
+
for (const child of Array.from(paragraph.childNodes || [])) {
|
|
572
|
+
if (child?.nodeType === 1 && child.namespaceURI === NS_W && child.localName === 'pPr') continue;
|
|
573
|
+
ins.appendChild(child.cloneNode(true));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
wrappedParagraph.appendChild(ins);
|
|
577
|
+
return wrappedParagraph;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisionMetadata, author, options = {}) {
|
|
581
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
582
|
+
const serializer = createSerializer();
|
|
583
|
+
const templateParagraph = buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph);
|
|
584
|
+
const templateXml = serializer.serializeToString(templateParagraph);
|
|
585
|
+
const markdownResult = await applyRedlineToOxml(
|
|
586
|
+
templateXml,
|
|
587
|
+
'',
|
|
588
|
+
String(text || ''),
|
|
589
|
+
{
|
|
590
|
+
author,
|
|
591
|
+
generateRedlines: false
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
let sourceParagraph = null;
|
|
596
|
+
if (typeof markdownResult?.oxml === 'string') {
|
|
597
|
+
const extracted = extractReplacementNodes(markdownResult.oxml);
|
|
598
|
+
sourceParagraph = (extracted.replacementNodes || []).find(
|
|
599
|
+
node => node && node.nodeType === 1 && node.namespaceURI === NS_W && node.localName === 'p'
|
|
600
|
+
) || null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!sourceParagraph) {
|
|
604
|
+
return buildFallbackInsertedPlainParagraph(
|
|
605
|
+
xmlDoc,
|
|
606
|
+
text,
|
|
606
607
|
revisionMetadata,
|
|
607
608
|
author,
|
|
608
609
|
{ ...options, generateRedlines }
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (!generateRedlines) {
|
|
613
|
-
return sourceParagraph;
|
|
614
|
-
}
|
|
615
|
-
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (!generateRedlines) {
|
|
614
|
+
return sourceParagraph;
|
|
615
|
+
}
|
|
616
|
+
|
|
616
617
|
return wrapParagraphContentInInsertion(xmlDoc, sourceParagraph, revisionMetadata, author, options);
|
|
617
|
-
}
|
|
618
|
-
|
|
618
|
+
}
|
|
619
|
+
|
|
619
620
|
function collectNumberingIdsFromNodes(nodes) {
|
|
620
|
-
const ids = new Set();
|
|
621
|
-
for (const node of nodes || []) {
|
|
622
|
-
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.('*', 'numId') || []);
|
|
623
|
-
for (const numIdNode of numIdNodes) {
|
|
624
|
-
const val = numIdNode.getAttribute('w:val') || numIdNode.getAttribute('val');
|
|
625
|
-
if (val != null && val !== '') {
|
|
626
|
-
ids.add(String(val));
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
return Array.from(ids);
|
|
621
|
+
const ids = new Set();
|
|
622
|
+
for (const node of nodes || []) {
|
|
623
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.('*', 'numId') || []);
|
|
624
|
+
for (const numIdNode of numIdNodes) {
|
|
625
|
+
const val = numIdNode.getAttribute('w:val') || numIdNode.getAttribute('val');
|
|
626
|
+
if (val != null && val !== '') {
|
|
627
|
+
ids.add(String(val));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return Array.from(ids);
|
|
631
632
|
}
|
|
632
633
|
|
|
633
634
|
function directDeletionCarrierText(carrier) {
|
|
@@ -1341,308 +1342,308 @@ export async function restoreDeletedParagraphByExactText(
|
|
|
1341
1342
|
...(warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {})
|
|
1342
1343
|
};
|
|
1343
1344
|
}
|
|
1344
|
-
|
|
1345
|
-
async function tryExplicitDecimalHeaderListConversion({
|
|
1346
|
-
xmlDoc,
|
|
1347
|
-
serializer,
|
|
1348
|
-
documentXml,
|
|
1349
|
-
operationSession,
|
|
1350
|
-
targetParagraph,
|
|
1351
|
-
currentParagraphText,
|
|
1352
|
-
modifiedText,
|
|
1353
|
-
author,
|
|
1354
|
-
runtimeContext,
|
|
1355
|
-
revisionIdAllocator,
|
|
1356
|
-
generateRedlines = true,
|
|
1357
|
-
onInfo = () => { },
|
|
1358
|
-
options = {}
|
|
1359
|
-
}) {
|
|
1360
|
-
if (!targetParagraph) return null;
|
|
1361
|
-
const scopedParagraphOxml = serializer.serializeToString(targetParagraph);
|
|
1362
|
-
const explicitPlan = buildSingleLineListStructuralFallbackPlan({
|
|
1363
|
-
oxml: scopedParagraphOxml,
|
|
1364
|
-
originalText: currentParagraphText,
|
|
1365
|
-
modifiedText,
|
|
1366
|
-
allowExistingList: false
|
|
1367
|
-
});
|
|
1368
|
-
if (
|
|
1369
|
-
!explicitPlan ||
|
|
1370
|
-
explicitPlan.numberingKey !== 'numbered:decimal:single' ||
|
|
1371
|
-
!Number.isInteger(explicitPlan.startAt) ||
|
|
1372
|
-
explicitPlan.startAt < 1
|
|
1373
|
-
) {
|
|
1374
|
-
return null;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
const strippedContent = stripSingleLineListMarkerPrefix(explicitPlan.listInput || modifiedText);
|
|
1378
|
-
if (!strippedContent) return null;
|
|
1379
|
-
|
|
1380
|
-
onInfo('[List] Applying explicit numeric header conversion with direct list binding.');
|
|
1381
|
-
const redlineResult = await applyRedlineToOxml(
|
|
1382
|
-
serializer.serializeToString(targetParagraph),
|
|
1383
|
-
currentParagraphText,
|
|
1384
|
-
strippedContent,
|
|
1385
|
-
{
|
|
1386
|
-
author,
|
|
1387
|
-
generateRedlines,
|
|
1388
|
-
_revisionIdAllocator: revisionIdAllocator
|
|
1389
|
-
}
|
|
1390
|
-
);
|
|
1391
|
-
if (!redlineResult?.hasChanges || typeof redlineResult?.oxml !== 'string') return null;
|
|
1392
|
-
|
|
1393
|
-
const extracted = extractReplacementNodes(redlineResult.oxml);
|
|
1394
|
-
const replacementNodes = extracted.replacementNodes;
|
|
1395
|
-
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
1396
|
-
explicitPlan,
|
|
1397
|
-
runtimeContext?.listFallbackSequenceState || null
|
|
1398
|
-
);
|
|
1399
|
-
|
|
1400
|
-
const explicitStart = explicitPlan.startAt;
|
|
1401
|
-
const numberingState = runtimeContext?.numberingIdState || null;
|
|
1402
|
-
let appliedNumId = null;
|
|
1403
|
-
let numberingXml = null;
|
|
1404
|
-
|
|
1405
|
-
if (numberingAction.type === 'explicitReuse' && numberingAction.numId) {
|
|
1406
|
-
appliedNumId = String(numberingAction.numId);
|
|
1407
|
-
onInfo(`[List] Reusing explicit-start list sequence (${numberingAction.numberingKey} -> numId ${appliedNumId}, next ${explicitStart + 1}).`);
|
|
1408
|
-
} else {
|
|
1409
|
-
const reservedPair = reserveNextNumberingIdPair(numberingState);
|
|
1410
|
-
if (!reservedPair) return null;
|
|
1411
|
-
|
|
1412
|
-
appliedNumId = String(reservedPair.numId);
|
|
1413
|
-
numberingXml = buildExplicitDecimalMultilevelNumberingXml(
|
|
1414
|
-
reservedPair.numId,
|
|
1415
|
-
reservedPair.abstractNumId,
|
|
1416
|
-
explicitStart
|
|
1417
|
-
);
|
|
1418
|
-
|
|
1419
|
-
if (numberingAction.type === 'explicitStartNew') {
|
|
1420
|
-
onInfo(`[List] Started explicit-start list sequence (${numberingAction.numberingKey} -> numId ${appliedNumId}).`);
|
|
1421
|
-
}
|
|
1422
|
-
onInfo(`[List] Using isolated explicit-start numbering (start ${explicitStart}, numId ${appliedNumId}, abstractNumId ${reservedPair.abstractNumId}).`);
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
if (explicitPlan.numberingKey && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1426
|
-
runtimeContext.listFallbackSharedNumIdByKey.delete(explicitPlan.numberingKey);
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
if (numberingAction.type === 'explicitStartNew' || numberingAction.type === 'explicitReuse') {
|
|
1430
|
-
recordSingleLineListFallbackExplicitSequence(
|
|
1431
|
-
runtimeContext?.listFallbackSequenceState || null,
|
|
1432
|
-
numberingAction.numberingKey || explicitPlan.numberingKey,
|
|
1433
|
-
appliedNumId,
|
|
1434
|
-
explicitStart
|
|
1435
|
-
);
|
|
1436
|
-
} else {
|
|
1437
|
-
clearSingleLineListFallbackExplicitSequence(
|
|
1438
|
-
runtimeContext?.listFallbackSequenceState || null,
|
|
1439
|
-
numberingAction.numberingKey || explicitPlan.numberingKey
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
enforceListBindingOnParagraphNodes(replacementNodes, {
|
|
1444
|
-
numId: appliedNumId,
|
|
1445
|
-
ilvl: 0,
|
|
1446
|
-
clearParagraphPropertyChanges: true,
|
|
1447
|
-
removeListPropertyNode: true
|
|
1448
|
-
});
|
|
1449
|
-
|
|
1450
|
-
const parent = targetParagraph.parentNode;
|
|
1451
|
-
if (!parent) return null;
|
|
1452
|
-
for (const node of replacementNodes) {
|
|
1453
|
-
const imported = xmlDoc.importNode(node, true);
|
|
1454
|
-
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
1455
|
-
const inserted = parent.insertBefore(imported, targetParagraph);
|
|
1456
|
-
options?._mutationLiveNodes?.push(inserted);
|
|
1457
|
-
}
|
|
1458
|
-
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
1459
|
-
parent.removeChild(targetParagraph);
|
|
1460
|
-
normalizeBodySectionOrder(xmlDoc);
|
|
1461
|
-
if (operationSession?.receiptCollector && numberingXml) {
|
|
1462
|
-
const numIds = collectNumberingIdsFromNodes(options?._mutationLiveNodes);
|
|
1463
|
-
for (const id of numIds) {
|
|
1464
|
-
operationSession.receiptCollector.recordNumbering(id);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
return {
|
|
1468
|
-
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1469
|
-
hasChanges: true,
|
|
1470
|
-
numberingXml
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
async function trySingleParagraphListStructuralFallback({
|
|
1475
|
-
xmlDoc,
|
|
1476
|
-
serializer,
|
|
1477
|
-
documentXml,
|
|
1478
|
-
operationSession,
|
|
1479
|
-
targetParagraph,
|
|
1480
|
-
currentParagraphText,
|
|
1481
|
-
modifiedText,
|
|
1482
|
-
author,
|
|
1483
|
-
runtimeContext,
|
|
1484
|
-
revisionIdAllocator,
|
|
1485
|
-
generateRedlines = true,
|
|
1486
|
-
onInfo = () => { },
|
|
1487
|
-
options = {}
|
|
1488
|
-
}) {
|
|
1489
|
-
if (!targetParagraph) return null;
|
|
1490
|
-
|
|
1491
|
-
const scopedParagraphOxml = serializer.serializeToString(targetParagraph);
|
|
1492
|
-
const fallbackPlan = buildSingleLineListStructuralFallbackPlan({
|
|
1493
|
-
oxml: scopedParagraphOxml,
|
|
1494
|
-
originalText: currentParagraphText,
|
|
1495
|
-
modifiedText,
|
|
1496
|
-
allowExistingList: false
|
|
1497
|
-
});
|
|
1498
|
-
if (!fallbackPlan) return null;
|
|
1499
|
-
|
|
1500
|
-
onInfo('[List] No textual diff but list marker detected; forcing structural list conversion fallback.');
|
|
1501
|
-
const fallbackResult = await executeSingleLineListStructuralFallback(fallbackPlan, {
|
|
1502
|
-
author,
|
|
1503
|
-
generateRedlines,
|
|
1504
|
-
revisionIdAllocator,
|
|
1505
|
-
setAbstractStartOverride: false
|
|
1506
|
-
});
|
|
1507
|
-
if (!fallbackResult?.hasChanges || !fallbackResult?.oxml) {
|
|
1508
|
-
onInfo('[List] Structural list fallback produced no valid OOXML payload.');
|
|
1509
|
-
return null;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
const extracted = extractReplacementNodes(fallbackResult.oxml);
|
|
1513
|
-
let replacementNodes = extracted.replacementNodes;
|
|
1514
|
-
let numberingXml = extracted.numberingXml || fallbackResult?.numberingXml || null;
|
|
1515
|
-
const hasExplicitStartAt = Number.isInteger(fallbackPlan?.startAt) && fallbackPlan.startAt > 0;
|
|
1516
|
-
const numberingKey = fallbackResult?.listStructuralFallbackKey || fallbackPlan?.numberingKey || null;
|
|
1517
|
-
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
1518
|
-
fallbackPlan,
|
|
1519
|
-
runtimeContext?.listFallbackSequenceState || null
|
|
1520
|
-
);
|
|
1521
|
-
if (hasExplicitStartAt) {
|
|
1522
|
-
const explicitStart = fallbackPlan.startAt;
|
|
1523
|
-
let explicitNumIdForBinding = null;
|
|
1524
|
-
const numberingState = runtimeContext?.numberingIdState || null;
|
|
1525
|
-
if (numberingAction.type === 'explicitReuse' && numberingAction.numId) {
|
|
1526
|
-
explicitNumIdForBinding = String(numberingAction.numId);
|
|
1527
|
-
numberingXml = null;
|
|
1528
|
-
onInfo(`[List] Reusing explicit-start list sequence (${numberingAction.numberingKey} -> numId ${explicitNumIdForBinding}, next ${explicitStart + 1}).`);
|
|
1529
|
-
} else if (numberingState) {
|
|
1530
|
-
const reservedPair = reserveNextNumberingIdPair(numberingState);
|
|
1531
|
-
if (!reservedPair) return null;
|
|
1532
|
-
overwriteParagraphNumIds(replacementNodes, reservedPair.numId);
|
|
1533
|
-
explicitNumIdForBinding = String(reservedPair.numId);
|
|
1534
|
-
numberingXml = buildExplicitDecimalMultilevelNumberingXml(
|
|
1535
|
-
reservedPair.numId,
|
|
1536
|
-
reservedPair.abstractNumId,
|
|
1537
|
-
explicitStart
|
|
1538
|
-
);
|
|
1539
|
-
if (numberingAction.type === 'explicitStartNew') {
|
|
1540
|
-
onInfo(`[List] Started explicit-start list sequence (${numberingAction.numberingKey} -> numId ${reservedPair.numId}).`);
|
|
1541
|
-
}
|
|
1542
|
-
onInfo(`[List] Using isolated explicit-start numbering (start ${explicitStart}, numId ${reservedPair.numId}, abstractNumId ${reservedPair.abstractNumId}).`);
|
|
1543
|
-
} else {
|
|
1544
|
-
const generatedNumId = extractFirstParagraphNumId(replacementNodes);
|
|
1545
|
-
explicitNumIdForBinding = generatedNumId ? String(generatedNumId) : null;
|
|
1546
|
-
onInfo(`[List] Using isolated list numbering with explicit start ${explicitStart}${generatedNumId ? ` (numId ${generatedNumId})` : ''}.`);
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
if (numberingKey && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1550
|
-
runtimeContext.listFallbackSharedNumIdByKey.delete(numberingKey);
|
|
1551
|
-
}
|
|
1552
|
-
if (numberingAction.type === 'explicitStartNew' || numberingAction.type === 'explicitReuse') {
|
|
1553
|
-
recordSingleLineListFallbackExplicitSequence(
|
|
1554
|
-
runtimeContext?.listFallbackSequenceState || null,
|
|
1555
|
-
numberingAction.numberingKey || numberingKey,
|
|
1556
|
-
explicitNumIdForBinding,
|
|
1557
|
-
explicitStart
|
|
1558
|
-
);
|
|
1559
|
-
} else {
|
|
1560
|
-
clearSingleLineListFallbackExplicitSequence(
|
|
1561
|
-
runtimeContext?.listFallbackSequenceState || null,
|
|
1562
|
-
numberingAction.numberingKey || numberingKey
|
|
1563
|
-
);
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
if (explicitNumIdForBinding) {
|
|
1567
|
-
enforceListBindingOnParagraphNodes(replacementNodes, {
|
|
1568
|
-
numId: explicitNumIdForBinding,
|
|
1569
|
-
ilvl: 0,
|
|
1570
|
-
clearParagraphPropertyChanges: true,
|
|
1571
|
-
removeListPropertyNode: true
|
|
1572
|
-
});
|
|
1573
|
-
}
|
|
1574
|
-
} else {
|
|
1575
|
-
if (numberingXml && runtimeContext?.numberingIdState) {
|
|
1576
|
-
const normalizedNumbering = remapNumberingPayloadForDocument(numberingXml, replacementNodes, runtimeContext.numberingIdState);
|
|
1577
|
-
replacementNodes = normalizedNumbering.replacementNodes;
|
|
1578
|
-
numberingXml = normalizedNumbering.numberingXml;
|
|
1579
|
-
}
|
|
1580
|
-
clearSingleLineListFallbackExplicitSequence(
|
|
1581
|
-
runtimeContext?.listFallbackSequenceState || null,
|
|
1582
|
-
numberingAction.numberingKey || numberingKey
|
|
1583
|
-
);
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
if (!hasExplicitStartAt && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1587
|
-
const sharedNumId = numberingKey ? runtimeContext.listFallbackSharedNumIdByKey.get(numberingKey) : null;
|
|
1588
|
-
if (sharedNumId) {
|
|
1589
|
-
overwriteParagraphNumIds(replacementNodes, sharedNumId);
|
|
1590
|
-
numberingXml = null;
|
|
1591
|
-
onInfo(`[List] Reusing shared list numbering (${numberingKey} -> numId ${sharedNumId}).`);
|
|
1592
|
-
} else if (numberingKey) {
|
|
1593
|
-
const generatedNumId = extractFirstParagraphNumId(replacementNodes);
|
|
1594
|
-
if (generatedNumId) {
|
|
1595
|
-
runtimeContext.listFallbackSharedNumIdByKey.set(numberingKey, generatedNumId);
|
|
1596
|
-
onInfo(`[List] Captured shared list numbering (${numberingKey} -> numId ${generatedNumId}).`);
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
const parent = targetParagraph.parentNode;
|
|
1602
|
-
if (!parent) return null;
|
|
1603
|
-
for (const node of replacementNodes) {
|
|
1604
|
-
const imported = xmlDoc.importNode(node, true);
|
|
1605
|
-
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
1606
|
-
const inserted = parent.insertBefore(imported, targetParagraph);
|
|
1607
|
-
options?._mutationLiveNodes?.push(inserted);
|
|
1608
|
-
}
|
|
1609
|
-
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
1610
|
-
parent.removeChild(targetParagraph);
|
|
1611
|
-
normalizeBodySectionOrder(xmlDoc);
|
|
1612
|
-
if (operationSession?.receiptCollector && numberingXml) {
|
|
1613
|
-
const numIds = collectNumberingIdsFromNodes(options?._mutationLiveNodes);
|
|
1614
|
-
for (const id of numIds) {
|
|
1615
|
-
operationSession.receiptCollector.recordNumbering(id);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return {
|
|
1619
|
-
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1620
|
-
hasChanges: true,
|
|
1621
|
-
numberingXml
|
|
1622
|
-
};
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
export async function applyToParagraphByExactText(documentXml, targetText, modifiedText, author, targetRef = null, targetEndRef = null, runtimeContext = null, options = {}) {
|
|
1626
|
-
const generateRedlines = options.generateRedlines !== false;
|
|
1627
|
-
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
1628
|
-
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
1629
|
-
const { serializer, xmlDoc, operationSession } = resolveMutationDocument(documentXml, options);
|
|
1630
|
-
if (!xmlDoc) return { documentXml, hasChanges: false, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
1631
|
-
const revisionIdAllocator = operationSession?.revisionIdAllocator || prepareRevisionAllocator(xmlDoc, options);
|
|
1632
|
-
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'redline', runtimeContext, {
|
|
1633
|
-
...options,
|
|
1634
|
-
onInfo,
|
|
1635
|
-
onWarn
|
|
1636
|
-
});
|
|
1637
|
-
if (resolved?.error || !resolved?.paragraph) {
|
|
1638
|
-
return {
|
|
1639
|
-
documentXml,
|
|
1640
|
-
hasChanges: false,
|
|
1641
|
-
numberingXml: null,
|
|
1642
|
-
status: 'error',
|
|
1643
|
-
error: resolved?.error || { code: 'TARGET_NOT_FOUND', message: 'Target paragraph not found.' }
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1345
|
+
|
|
1346
|
+
async function tryExplicitDecimalHeaderListConversion({
|
|
1347
|
+
xmlDoc,
|
|
1348
|
+
serializer,
|
|
1349
|
+
documentXml,
|
|
1350
|
+
operationSession,
|
|
1351
|
+
targetParagraph,
|
|
1352
|
+
currentParagraphText,
|
|
1353
|
+
modifiedText,
|
|
1354
|
+
author,
|
|
1355
|
+
runtimeContext,
|
|
1356
|
+
revisionIdAllocator,
|
|
1357
|
+
generateRedlines = true,
|
|
1358
|
+
onInfo = () => { },
|
|
1359
|
+
options = {}
|
|
1360
|
+
}) {
|
|
1361
|
+
if (!targetParagraph) return null;
|
|
1362
|
+
const scopedParagraphOxml = serializer.serializeToString(targetParagraph);
|
|
1363
|
+
const explicitPlan = buildSingleLineListStructuralFallbackPlan({
|
|
1364
|
+
oxml: scopedParagraphOxml,
|
|
1365
|
+
originalText: currentParagraphText,
|
|
1366
|
+
modifiedText,
|
|
1367
|
+
allowExistingList: false
|
|
1368
|
+
});
|
|
1369
|
+
if (
|
|
1370
|
+
!explicitPlan ||
|
|
1371
|
+
explicitPlan.numberingKey !== 'numbered:decimal:single' ||
|
|
1372
|
+
!Number.isInteger(explicitPlan.startAt) ||
|
|
1373
|
+
explicitPlan.startAt < 1
|
|
1374
|
+
) {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const strippedContent = stripSingleLineListMarkerPrefix(explicitPlan.listInput || modifiedText);
|
|
1379
|
+
if (!strippedContent) return null;
|
|
1380
|
+
|
|
1381
|
+
onInfo('[List] Applying explicit numeric header conversion with direct list binding.');
|
|
1382
|
+
const redlineResult = await applyRedlineToOxml(
|
|
1383
|
+
serializer.serializeToString(targetParagraph),
|
|
1384
|
+
currentParagraphText,
|
|
1385
|
+
strippedContent,
|
|
1386
|
+
{
|
|
1387
|
+
author,
|
|
1388
|
+
generateRedlines,
|
|
1389
|
+
_revisionIdAllocator: revisionIdAllocator
|
|
1390
|
+
}
|
|
1391
|
+
);
|
|
1392
|
+
if (!redlineResult?.hasChanges || typeof redlineResult?.oxml !== 'string') return null;
|
|
1393
|
+
|
|
1394
|
+
const extracted = extractReplacementNodes(redlineResult.oxml);
|
|
1395
|
+
const replacementNodes = extracted.replacementNodes;
|
|
1396
|
+
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
1397
|
+
explicitPlan,
|
|
1398
|
+
runtimeContext?.listFallbackSequenceState || null
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
const explicitStart = explicitPlan.startAt;
|
|
1402
|
+
const numberingState = runtimeContext?.numberingIdState || null;
|
|
1403
|
+
let appliedNumId = null;
|
|
1404
|
+
let numberingXml = null;
|
|
1405
|
+
|
|
1406
|
+
if (numberingAction.type === 'explicitReuse' && numberingAction.numId) {
|
|
1407
|
+
appliedNumId = String(numberingAction.numId);
|
|
1408
|
+
onInfo(`[List] Reusing explicit-start list sequence (${numberingAction.numberingKey} -> numId ${appliedNumId}, next ${explicitStart + 1}).`);
|
|
1409
|
+
} else {
|
|
1410
|
+
const reservedPair = reserveNextNumberingIdPair(numberingState);
|
|
1411
|
+
if (!reservedPair) return null;
|
|
1412
|
+
|
|
1413
|
+
appliedNumId = String(reservedPair.numId);
|
|
1414
|
+
numberingXml = buildExplicitDecimalMultilevelNumberingXml(
|
|
1415
|
+
reservedPair.numId,
|
|
1416
|
+
reservedPair.abstractNumId,
|
|
1417
|
+
explicitStart
|
|
1418
|
+
);
|
|
1419
|
+
|
|
1420
|
+
if (numberingAction.type === 'explicitStartNew') {
|
|
1421
|
+
onInfo(`[List] Started explicit-start list sequence (${numberingAction.numberingKey} -> numId ${appliedNumId}).`);
|
|
1422
|
+
}
|
|
1423
|
+
onInfo(`[List] Using isolated explicit-start numbering (start ${explicitStart}, numId ${appliedNumId}, abstractNumId ${reservedPair.abstractNumId}).`);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (explicitPlan.numberingKey && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1427
|
+
runtimeContext.listFallbackSharedNumIdByKey.delete(explicitPlan.numberingKey);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (numberingAction.type === 'explicitStartNew' || numberingAction.type === 'explicitReuse') {
|
|
1431
|
+
recordSingleLineListFallbackExplicitSequence(
|
|
1432
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
1433
|
+
numberingAction.numberingKey || explicitPlan.numberingKey,
|
|
1434
|
+
appliedNumId,
|
|
1435
|
+
explicitStart
|
|
1436
|
+
);
|
|
1437
|
+
} else {
|
|
1438
|
+
clearSingleLineListFallbackExplicitSequence(
|
|
1439
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
1440
|
+
numberingAction.numberingKey || explicitPlan.numberingKey
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
enforceListBindingOnParagraphNodes(replacementNodes, {
|
|
1445
|
+
numId: appliedNumId,
|
|
1446
|
+
ilvl: 0,
|
|
1447
|
+
clearParagraphPropertyChanges: true,
|
|
1448
|
+
removeListPropertyNode: true
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
const parent = targetParagraph.parentNode;
|
|
1452
|
+
if (!parent) return null;
|
|
1453
|
+
for (const node of replacementNodes) {
|
|
1454
|
+
const imported = xmlDoc.importNode(node, true);
|
|
1455
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
1456
|
+
const inserted = parent.insertBefore(imported, targetParagraph);
|
|
1457
|
+
options?._mutationLiveNodes?.push(inserted);
|
|
1458
|
+
}
|
|
1459
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
1460
|
+
parent.removeChild(targetParagraph);
|
|
1461
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1462
|
+
if (operationSession?.receiptCollector && numberingXml) {
|
|
1463
|
+
const numIds = collectNumberingIdsFromNodes(options?._mutationLiveNodes);
|
|
1464
|
+
for (const id of numIds) {
|
|
1465
|
+
operationSession.receiptCollector.recordNumbering(id);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1470
|
+
hasChanges: true,
|
|
1471
|
+
numberingXml
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
async function trySingleParagraphListStructuralFallback({
|
|
1476
|
+
xmlDoc,
|
|
1477
|
+
serializer,
|
|
1478
|
+
documentXml,
|
|
1479
|
+
operationSession,
|
|
1480
|
+
targetParagraph,
|
|
1481
|
+
currentParagraphText,
|
|
1482
|
+
modifiedText,
|
|
1483
|
+
author,
|
|
1484
|
+
runtimeContext,
|
|
1485
|
+
revisionIdAllocator,
|
|
1486
|
+
generateRedlines = true,
|
|
1487
|
+
onInfo = () => { },
|
|
1488
|
+
options = {}
|
|
1489
|
+
}) {
|
|
1490
|
+
if (!targetParagraph) return null;
|
|
1491
|
+
|
|
1492
|
+
const scopedParagraphOxml = serializer.serializeToString(targetParagraph);
|
|
1493
|
+
const fallbackPlan = buildSingleLineListStructuralFallbackPlan({
|
|
1494
|
+
oxml: scopedParagraphOxml,
|
|
1495
|
+
originalText: currentParagraphText,
|
|
1496
|
+
modifiedText,
|
|
1497
|
+
allowExistingList: false
|
|
1498
|
+
});
|
|
1499
|
+
if (!fallbackPlan) return null;
|
|
1500
|
+
|
|
1501
|
+
onInfo('[List] No textual diff but list marker detected; forcing structural list conversion fallback.');
|
|
1502
|
+
const fallbackResult = await executeSingleLineListStructuralFallback(fallbackPlan, {
|
|
1503
|
+
author,
|
|
1504
|
+
generateRedlines,
|
|
1505
|
+
revisionIdAllocator,
|
|
1506
|
+
setAbstractStartOverride: false
|
|
1507
|
+
});
|
|
1508
|
+
if (!fallbackResult?.hasChanges || !fallbackResult?.oxml) {
|
|
1509
|
+
onInfo('[List] Structural list fallback produced no valid OOXML payload.');
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const extracted = extractReplacementNodes(fallbackResult.oxml);
|
|
1514
|
+
let replacementNodes = extracted.replacementNodes;
|
|
1515
|
+
let numberingXml = extracted.numberingXml || fallbackResult?.numberingXml || null;
|
|
1516
|
+
const hasExplicitStartAt = Number.isInteger(fallbackPlan?.startAt) && fallbackPlan.startAt > 0;
|
|
1517
|
+
const numberingKey = fallbackResult?.listStructuralFallbackKey || fallbackPlan?.numberingKey || null;
|
|
1518
|
+
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
1519
|
+
fallbackPlan,
|
|
1520
|
+
runtimeContext?.listFallbackSequenceState || null
|
|
1521
|
+
);
|
|
1522
|
+
if (hasExplicitStartAt) {
|
|
1523
|
+
const explicitStart = fallbackPlan.startAt;
|
|
1524
|
+
let explicitNumIdForBinding = null;
|
|
1525
|
+
const numberingState = runtimeContext?.numberingIdState || null;
|
|
1526
|
+
if (numberingAction.type === 'explicitReuse' && numberingAction.numId) {
|
|
1527
|
+
explicitNumIdForBinding = String(numberingAction.numId);
|
|
1528
|
+
numberingXml = null;
|
|
1529
|
+
onInfo(`[List] Reusing explicit-start list sequence (${numberingAction.numberingKey} -> numId ${explicitNumIdForBinding}, next ${explicitStart + 1}).`);
|
|
1530
|
+
} else if (numberingState) {
|
|
1531
|
+
const reservedPair = reserveNextNumberingIdPair(numberingState);
|
|
1532
|
+
if (!reservedPair) return null;
|
|
1533
|
+
overwriteParagraphNumIds(replacementNodes, reservedPair.numId);
|
|
1534
|
+
explicitNumIdForBinding = String(reservedPair.numId);
|
|
1535
|
+
numberingXml = buildExplicitDecimalMultilevelNumberingXml(
|
|
1536
|
+
reservedPair.numId,
|
|
1537
|
+
reservedPair.abstractNumId,
|
|
1538
|
+
explicitStart
|
|
1539
|
+
);
|
|
1540
|
+
if (numberingAction.type === 'explicitStartNew') {
|
|
1541
|
+
onInfo(`[List] Started explicit-start list sequence (${numberingAction.numberingKey} -> numId ${reservedPair.numId}).`);
|
|
1542
|
+
}
|
|
1543
|
+
onInfo(`[List] Using isolated explicit-start numbering (start ${explicitStart}, numId ${reservedPair.numId}, abstractNumId ${reservedPair.abstractNumId}).`);
|
|
1544
|
+
} else {
|
|
1545
|
+
const generatedNumId = extractFirstParagraphNumId(replacementNodes);
|
|
1546
|
+
explicitNumIdForBinding = generatedNumId ? String(generatedNumId) : null;
|
|
1547
|
+
onInfo(`[List] Using isolated list numbering with explicit start ${explicitStart}${generatedNumId ? ` (numId ${generatedNumId})` : ''}.`);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if (numberingKey && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1551
|
+
runtimeContext.listFallbackSharedNumIdByKey.delete(numberingKey);
|
|
1552
|
+
}
|
|
1553
|
+
if (numberingAction.type === 'explicitStartNew' || numberingAction.type === 'explicitReuse') {
|
|
1554
|
+
recordSingleLineListFallbackExplicitSequence(
|
|
1555
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
1556
|
+
numberingAction.numberingKey || numberingKey,
|
|
1557
|
+
explicitNumIdForBinding,
|
|
1558
|
+
explicitStart
|
|
1559
|
+
);
|
|
1560
|
+
} else {
|
|
1561
|
+
clearSingleLineListFallbackExplicitSequence(
|
|
1562
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
1563
|
+
numberingAction.numberingKey || numberingKey
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (explicitNumIdForBinding) {
|
|
1568
|
+
enforceListBindingOnParagraphNodes(replacementNodes, {
|
|
1569
|
+
numId: explicitNumIdForBinding,
|
|
1570
|
+
ilvl: 0,
|
|
1571
|
+
clearParagraphPropertyChanges: true,
|
|
1572
|
+
removeListPropertyNode: true
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
} else {
|
|
1576
|
+
if (numberingXml && runtimeContext?.numberingIdState) {
|
|
1577
|
+
const normalizedNumbering = remapNumberingPayloadForDocument(numberingXml, replacementNodes, runtimeContext.numberingIdState);
|
|
1578
|
+
replacementNodes = normalizedNumbering.replacementNodes;
|
|
1579
|
+
numberingXml = normalizedNumbering.numberingXml;
|
|
1580
|
+
}
|
|
1581
|
+
clearSingleLineListFallbackExplicitSequence(
|
|
1582
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
1583
|
+
numberingAction.numberingKey || numberingKey
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (!hasExplicitStartAt && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1588
|
+
const sharedNumId = numberingKey ? runtimeContext.listFallbackSharedNumIdByKey.get(numberingKey) : null;
|
|
1589
|
+
if (sharedNumId) {
|
|
1590
|
+
overwriteParagraphNumIds(replacementNodes, sharedNumId);
|
|
1591
|
+
numberingXml = null;
|
|
1592
|
+
onInfo(`[List] Reusing shared list numbering (${numberingKey} -> numId ${sharedNumId}).`);
|
|
1593
|
+
} else if (numberingKey) {
|
|
1594
|
+
const generatedNumId = extractFirstParagraphNumId(replacementNodes);
|
|
1595
|
+
if (generatedNumId) {
|
|
1596
|
+
runtimeContext.listFallbackSharedNumIdByKey.set(numberingKey, generatedNumId);
|
|
1597
|
+
onInfo(`[List] Captured shared list numbering (${numberingKey} -> numId ${generatedNumId}).`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
const parent = targetParagraph.parentNode;
|
|
1603
|
+
if (!parent) return null;
|
|
1604
|
+
for (const node of replacementNodes) {
|
|
1605
|
+
const imported = xmlDoc.importNode(node, true);
|
|
1606
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
1607
|
+
const inserted = parent.insertBefore(imported, targetParagraph);
|
|
1608
|
+
options?._mutationLiveNodes?.push(inserted);
|
|
1609
|
+
}
|
|
1610
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
1611
|
+
parent.removeChild(targetParagraph);
|
|
1612
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1613
|
+
if (operationSession?.receiptCollector && numberingXml) {
|
|
1614
|
+
const numIds = collectNumberingIdsFromNodes(options?._mutationLiveNodes);
|
|
1615
|
+
for (const id of numIds) {
|
|
1616
|
+
operationSession.receiptCollector.recordNumbering(id);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return {
|
|
1620
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1621
|
+
hasChanges: true,
|
|
1622
|
+
numberingXml
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
export async function applyToParagraphByExactText(documentXml, targetText, modifiedText, author, targetRef = null, targetEndRef = null, runtimeContext = null, options = {}) {
|
|
1627
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
1628
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
1629
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
1630
|
+
const { serializer, xmlDoc, operationSession } = resolveMutationDocument(documentXml, options);
|
|
1631
|
+
if (!xmlDoc) return { documentXml, hasChanges: false, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
1632
|
+
const revisionIdAllocator = operationSession?.revisionIdAllocator || prepareRevisionAllocator(xmlDoc, options);
|
|
1633
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'redline', runtimeContext, {
|
|
1634
|
+
...options,
|
|
1635
|
+
onInfo,
|
|
1636
|
+
onWarn
|
|
1637
|
+
});
|
|
1638
|
+
if (resolved?.error || !resolved?.paragraph) {
|
|
1639
|
+
return {
|
|
1640
|
+
documentXml,
|
|
1641
|
+
hasChanges: false,
|
|
1642
|
+
numberingXml: null,
|
|
1643
|
+
status: 'error',
|
|
1644
|
+
error: resolved?.error || { code: 'TARGET_NOT_FOUND', message: 'Target paragraph not found.' }
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1646
1647
|
const targetParagraph = resolved.paragraph;
|
|
1647
1648
|
if (typeof modifiedText === 'string' && modifiedText.length > 0) {
|
|
1648
1649
|
const resurrectionTarget = inspectForeignDeletedParagraphTarget(targetParagraph, author);
|
|
@@ -1662,37 +1663,37 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1662
1663
|
}
|
|
1663
1664
|
}
|
|
1664
1665
|
preprocessRedlineTargetParagraph(targetParagraph);
|
|
1665
|
-
const currentParagraphText = getParagraphText(targetParagraph);
|
|
1666
|
-
if (modifiedText === '') {
|
|
1667
|
-
const commentIds = getCommentIdsInElement(targetParagraph);
|
|
1668
|
-
if (commentIds.length > 0) {
|
|
1669
|
-
const comments = commentDetailsForIds(commentIds, options._existingCommentDetails);
|
|
1670
|
-
return {
|
|
1671
|
-
documentXml,
|
|
1672
|
-
hasChanges: false,
|
|
1673
|
-
numberingXml: null,
|
|
1674
|
-
status: 'error',
|
|
1675
|
-
error: {
|
|
1676
|
-
code: 'COMMENTED_CONTENT_DELETE',
|
|
1677
|
-
message: 'Refusing to delete a paragraph with existing comments. Resolve or explicitly remove the comments before deleting the paragraph.',
|
|
1678
|
-
commentIds,
|
|
1679
|
-
...(comments.length > 0 ? { comments } : {})
|
|
1680
|
-
}
|
|
1681
|
-
};
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
const boundaryCheck = validateParagraphBoundaryMutation(targetParagraph, modifiedText, options);
|
|
1685
|
-
if (!boundaryCheck.valid) {
|
|
1686
|
-
return {
|
|
1687
|
-
documentXml,
|
|
1688
|
-
hasChanges: false,
|
|
1689
|
-
numberingXml: null,
|
|
1690
|
-
status: 'error',
|
|
1691
|
-
error: {
|
|
1692
|
-
code: boundaryCheck.code,
|
|
1693
|
-
message: boundaryCheck.message
|
|
1694
|
-
}
|
|
1695
|
-
};
|
|
1666
|
+
const currentParagraphText = getParagraphText(targetParagraph);
|
|
1667
|
+
if (modifiedText === '') {
|
|
1668
|
+
const commentIds = getCommentIdsInElement(targetParagraph);
|
|
1669
|
+
if (commentIds.length > 0) {
|
|
1670
|
+
const comments = commentDetailsForIds(commentIds, options._existingCommentDetails);
|
|
1671
|
+
return {
|
|
1672
|
+
documentXml,
|
|
1673
|
+
hasChanges: false,
|
|
1674
|
+
numberingXml: null,
|
|
1675
|
+
status: 'error',
|
|
1676
|
+
error: {
|
|
1677
|
+
code: 'COMMENTED_CONTENT_DELETE',
|
|
1678
|
+
message: 'Refusing to delete a paragraph with existing comments. Resolve or explicitly remove the comments before deleting the paragraph.',
|
|
1679
|
+
commentIds,
|
|
1680
|
+
...(comments.length > 0 ? { comments } : {})
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const boundaryCheck = validateParagraphBoundaryMutation(targetParagraph, modifiedText, options);
|
|
1686
|
+
if (!boundaryCheck.valid) {
|
|
1687
|
+
return {
|
|
1688
|
+
documentXml,
|
|
1689
|
+
hasChanges: false,
|
|
1690
|
+
numberingXml: null,
|
|
1691
|
+
status: 'error',
|
|
1692
|
+
error: {
|
|
1693
|
+
code: boundaryCheck.code,
|
|
1694
|
+
message: boundaryCheck.message
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1696
1697
|
}
|
|
1697
1698
|
const hasRevisions = containsTrackedChanges(targetParagraph);
|
|
1698
1699
|
const existingPolicy = options.existingRevisions || 'merge-same-author';
|
|
@@ -1796,797 +1797,807 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1796
1797
|
&& currentParagraphText === ''
|
|
1797
1798
|
&& !(hasRevisions && existingPolicy === 'merge-same-author')
|
|
1798
1799
|
) {
|
|
1799
|
-
if (generateRedlines) {
|
|
1800
|
-
markParagraphMarkDeleted(xmlDoc, targetParagraph, author, createRevisionMetadata(author, xmlDoc));
|
|
1801
|
-
options?._mutationLiveNodes?.push(targetParagraph);
|
|
1802
|
-
} else {
|
|
1803
|
-
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
1804
|
-
targetParagraph.parentNode?.removeChild(targetParagraph);
|
|
1805
|
-
}
|
|
1806
|
-
normalizeBodySectionOrder(xmlDoc);
|
|
1807
|
-
return {
|
|
1808
|
-
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1809
|
-
hasChanges: true,
|
|
1810
|
-
numberingXml: null
|
|
1811
|
-
};
|
|
1812
|
-
}
|
|
1813
|
-
const containingTable = findContainingWordElement(targetParagraph, 'tbl');
|
|
1814
|
-
const rawTableStructuralCandidate = !!containingTable
|
|
1815
|
-
&& !targetEndRef
|
|
1816
|
-
&& typeof modifiedText === 'string'
|
|
1817
|
-
&& modifiedText.includes('\n')
|
|
1818
|
-
&& !isMarkdownTableText(modifiedText);
|
|
1819
|
-
const rawTableStructuralDedupeKey = rawTableStructuralCandidate
|
|
1820
|
-
? computeTableStructuralDedupeKey(xmlDoc, containingTable, modifiedText)
|
|
1821
|
-
: null;
|
|
1822
|
-
const tableStructuralDedupes = runtimeContext?.tableStructuralRedlineKeys instanceof Set
|
|
1823
|
-
? runtimeContext.tableStructuralRedlineKeys
|
|
1824
|
-
: null;
|
|
1825
|
-
if (rawTableStructuralDedupeKey && tableStructuralDedupes?.has(rawTableStructuralDedupeKey)) {
|
|
1826
|
-
onInfo('[Table] Skipping duplicate table-structural redline for the same table/payload in this turn.');
|
|
1827
|
-
return {
|
|
1828
|
-
documentXml,
|
|
1829
|
-
hasChanges: false,
|
|
1830
|
-
numberingXml: null,
|
|
1831
|
-
warnings: ['Skipped duplicate table-structural redline in the same turn.']
|
|
1832
|
-
};
|
|
1833
|
-
}
|
|
1834
|
-
const synthesizedTableMarkdown = containingTable
|
|
1835
|
-
? synthesizeTableMarkdownFromMultilineCellEdit(targetParagraph, modifiedText, {
|
|
1836
|
-
tableElement: containingTable,
|
|
1837
|
-
currentParagraphText,
|
|
1838
|
-
onInfo,
|
|
1839
|
-
onWarn
|
|
1840
|
-
})
|
|
1841
|
-
: null;
|
|
1842
|
-
let effectiveModifiedText = synthesizedTableMarkdown || modifiedText;
|
|
1843
|
-
const useTableScope = !!containingTable && isMarkdownTableText(effectiveModifiedText);
|
|
1844
|
-
const isTableMarkdownEdit = isMarkdownTableText(effectiveModifiedText);
|
|
1845
|
-
const explicitRangeParagraphs = targetEndRef
|
|
1846
|
-
? resolveParagraphRangeByRefs(xmlDoc, targetRef, targetEndRef, {
|
|
1847
|
-
opType: 'redline',
|
|
1848
|
-
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
1849
|
-
onInfo,
|
|
1850
|
-
onWarn
|
|
1851
|
-
})
|
|
1852
|
-
: (typeof targetText === 'string' && /\r?\n/.test(targetText)
|
|
1853
|
-
? findReconstructionParagraphRange(xmlDoc, targetText)
|
|
1854
|
-
: null);
|
|
1855
|
-
const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
|
|
1856
|
-
if (!useTableScope && hasExplicitRangeScope) {
|
|
1857
|
-
const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
|
|
1858
|
-
if (insertionEntries && insertionEntries.length > 0) {
|
|
1859
|
-
onInfo(`[List] Applying explicit-range insertion-only heuristic (${insertionEntries.length} new item(s)).`);
|
|
1860
|
-
for (const entry of insertionEntries) {
|
|
1861
|
-
onInfo(`[List] Explicit-range insertion: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
1862
|
-
}
|
|
1863
|
-
const applied = applyExplicitRangeListInsertions({
|
|
1864
|
-
xmlDoc,
|
|
1865
|
-
explicitRangeParagraphs,
|
|
1866
|
-
insertionEntries,
|
|
1867
|
-
generateRedlines,
|
|
1868
|
-
author
|
|
1869
|
-
});
|
|
1870
|
-
if (applied) {
|
|
1871
|
-
return {
|
|
1872
|
-
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1873
|
-
hasChanges: true,
|
|
1874
|
-
numberingXml: null
|
|
1875
|
-
};
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
let inferredTableRangeParagraphs = null;
|
|
1880
|
-
if (!explicitRangeParagraphs && !useTableScope && isTableMarkdownEdit) {
|
|
1881
|
-
inferredTableRangeParagraphs = inferTableReplacementParagraphBlock(targetParagraph, {
|
|
1882
|
-
getParagraphText
|
|
1883
|
-
});
|
|
1884
|
-
if (inferredTableRangeParagraphs?.length > 1) {
|
|
1885
|
-
onInfo(`[Table] Heuristic range expansion selected ${inferredTableRangeParagraphs.length} paragraph(s) for replacement.`);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
const targetListInfo = getParagraphListInfo(targetParagraph);
|
|
1889
|
-
if (
|
|
1890
|
-
targetListInfo &&
|
|
1891
|
-
typeof effectiveModifiedText === 'string' &&
|
|
1892
|
-
!effectiveModifiedText.includes('\n')
|
|
1893
|
-
) {
|
|
1894
|
-
const strippedListPrefix = stripRedundantLeadingListMarkers(effectiveModifiedText);
|
|
1895
|
-
if (strippedListPrefix && strippedListPrefix !== effectiveModifiedText.trim()) {
|
|
1896
|
-
onInfo('[List] Stripped redundant manual list marker prefix from single-line list item edit.');
|
|
1897
|
-
effectiveModifiedText = strippedListPrefix;
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
if (useTableScope) {
|
|
1901
|
-
onInfo('[Table] Markdown table edit detected in table cell target; applying reconciliation at table scope.');
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
const bypassSingleParagraphHeuristics = options.explicitStructuredContent === true;
|
|
1905
|
-
|
|
1906
|
-
const adjacencyInsertionCandidate = (
|
|
1907
|
-
!bypassSingleParagraphHeuristics
|
|
1908
|
-
&& !useTableScope
|
|
1909
|
-
&& !hasExplicitRangeScope
|
|
1910
|
-
&& targetListInfo
|
|
1911
|
-
)
|
|
1912
|
-
? deriveSingleParagraphListAdjacencyInsertion(currentParagraphText, effectiveModifiedText)
|
|
1913
|
-
: null;
|
|
1914
|
-
if (adjacencyInsertionCandidate) {
|
|
1915
|
-
onInfo(`[List] Applying single-paragraph list adjacency insertion heuristic (${adjacencyInsertionCandidate.position}).`);
|
|
1916
|
-
const parent = targetParagraph.parentNode;
|
|
1917
|
-
if (!parent) throw new Error('Target paragraph has no parent for adjacency list insertion');
|
|
1918
|
-
|
|
1919
|
-
const listParagraph = buildInsertedListParagraph(
|
|
1920
|
-
xmlDoc,
|
|
1921
|
-
targetParagraph,
|
|
1922
|
-
{
|
|
1923
|
-
ilvl: targetListInfo.ilvl,
|
|
1924
|
-
numId: targetListInfo.numId,
|
|
1925
|
-
markerType: 'numbered',
|
|
1926
|
-
text: adjacencyInsertionCandidate.text
|
|
1927
|
-
},
|
|
1928
|
-
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
1929
|
-
author,
|
|
1930
|
-
{ generateRedlines }
|
|
1931
|
-
);
|
|
1932
|
-
|
|
1933
|
-
const insertionPoint = adjacencyInsertionCandidate.position === 'before'
|
|
1934
|
-
? targetParagraph
|
|
1935
|
-
: targetParagraph.nextSibling;
|
|
1936
|
-
ensureParagraphIdsOnImportedNode(listParagraph, operationSession);
|
|
1937
|
-
parent.insertBefore(listParagraph, insertionPoint);
|
|
1938
|
-
options?._mutationLiveNodes?.push(listParagraph);
|
|
1939
|
-
normalizeBodySectionOrder(xmlDoc);
|
|
1940
|
-
return {
|
|
1941
|
-
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1942
|
-
hasChanges: true,
|
|
1943
|
-
numberingXml: null
|
|
1944
|
-
};
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
const plainAdjacencyInsertionCandidate = (
|
|
1948
|
-
!bypassSingleParagraphHeuristics
|
|
1949
|
-
&& !useTableScope
|
|
1950
|
-
&& !hasExplicitRangeScope
|
|
1951
|
-
&& !targetListInfo
|
|
1952
|
-
)
|
|
1953
|
-
? deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, effectiveModifiedText)
|
|
1954
|
-
: null;
|
|
1955
|
-
if (plainAdjacencyInsertionCandidate) {
|
|
1956
|
-
onInfo(
|
|
1957
|
-
`[Text] Applying single-paragraph plain adjacency insertion heuristic `
|
|
1958
|
-
+ `(${plainAdjacencyInsertionCandidate.position}, count=${plainAdjacencyInsertionCandidate.paragraphs.length}).`
|
|
1959
|
-
);
|
|
1960
|
-
const parent = targetParagraph.parentNode;
|
|
1961
|
-
if (!parent) throw new Error('Target paragraph has no parent for plain adjacency insertion');
|
|
1962
|
-
|
|
1963
|
-
const insertionPoint = plainAdjacencyInsertionCandidate.position === 'before'
|
|
1964
|
-
? targetParagraph
|
|
1965
|
-
: targetParagraph.nextSibling;
|
|
1966
|
-
|
|
1967
|
-
for (const paragraphText of plainAdjacencyInsertionCandidate.paragraphs) {
|
|
1968
|
-
const plainParagraph = await buildInsertedPlainParagraph(
|
|
1969
|
-
xmlDoc,
|
|
1970
|
-
targetParagraph,
|
|
1971
|
-
paragraphText,
|
|
1972
|
-
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
1973
|
-
author,
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
1800
|
+
if (generateRedlines) {
|
|
1801
|
+
markParagraphMarkDeleted(xmlDoc, targetParagraph, author, createRevisionMetadata(author, xmlDoc));
|
|
1802
|
+
options?._mutationLiveNodes?.push(targetParagraph);
|
|
1803
|
+
} else {
|
|
1804
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
1805
|
+
targetParagraph.parentNode?.removeChild(targetParagraph);
|
|
1806
|
+
}
|
|
1807
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1808
|
+
return {
|
|
1809
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1810
|
+
hasChanges: true,
|
|
1811
|
+
numberingXml: null
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
const containingTable = findContainingWordElement(targetParagraph, 'tbl');
|
|
1815
|
+
const rawTableStructuralCandidate = !!containingTable
|
|
1816
|
+
&& !targetEndRef
|
|
1817
|
+
&& typeof modifiedText === 'string'
|
|
1818
|
+
&& modifiedText.includes('\n')
|
|
1819
|
+
&& !isMarkdownTableText(modifiedText);
|
|
1820
|
+
const rawTableStructuralDedupeKey = rawTableStructuralCandidate
|
|
1821
|
+
? computeTableStructuralDedupeKey(xmlDoc, containingTable, modifiedText)
|
|
1822
|
+
: null;
|
|
1823
|
+
const tableStructuralDedupes = runtimeContext?.tableStructuralRedlineKeys instanceof Set
|
|
1824
|
+
? runtimeContext.tableStructuralRedlineKeys
|
|
1825
|
+
: null;
|
|
1826
|
+
if (rawTableStructuralDedupeKey && tableStructuralDedupes?.has(rawTableStructuralDedupeKey)) {
|
|
1827
|
+
onInfo('[Table] Skipping duplicate table-structural redline for the same table/payload in this turn.');
|
|
1828
|
+
return {
|
|
1829
|
+
documentXml,
|
|
1830
|
+
hasChanges: false,
|
|
1831
|
+
numberingXml: null,
|
|
1832
|
+
warnings: ['Skipped duplicate table-structural redline in the same turn.']
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
const synthesizedTableMarkdown = containingTable
|
|
1836
|
+
? synthesizeTableMarkdownFromMultilineCellEdit(targetParagraph, modifiedText, {
|
|
1837
|
+
tableElement: containingTable,
|
|
1838
|
+
currentParagraphText,
|
|
1839
|
+
onInfo,
|
|
1840
|
+
onWarn
|
|
1841
|
+
})
|
|
1842
|
+
: null;
|
|
1843
|
+
let effectiveModifiedText = synthesizedTableMarkdown || modifiedText;
|
|
1844
|
+
const useTableScope = !!containingTable && isMarkdownTableText(effectiveModifiedText);
|
|
1845
|
+
const isTableMarkdownEdit = isMarkdownTableText(effectiveModifiedText);
|
|
1846
|
+
const explicitRangeParagraphs = targetEndRef
|
|
1847
|
+
? resolveParagraphRangeByRefs(xmlDoc, targetRef, targetEndRef, {
|
|
1848
|
+
opType: 'redline',
|
|
1849
|
+
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
1850
|
+
onInfo,
|
|
1851
|
+
onWarn
|
|
1852
|
+
})
|
|
1853
|
+
: (typeof targetText === 'string' && /\r?\n/.test(targetText)
|
|
1854
|
+
? findReconstructionParagraphRange(xmlDoc, targetText)
|
|
1855
|
+
: null);
|
|
1856
|
+
const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
|
|
1857
|
+
if (!useTableScope && hasExplicitRangeScope) {
|
|
1858
|
+
const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
|
|
1859
|
+
if (insertionEntries && insertionEntries.length > 0) {
|
|
1860
|
+
onInfo(`[List] Applying explicit-range insertion-only heuristic (${insertionEntries.length} new item(s)).`);
|
|
1861
|
+
for (const entry of insertionEntries) {
|
|
1862
|
+
onInfo(`[List] Explicit-range insertion: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
1863
|
+
}
|
|
1864
|
+
const applied = applyExplicitRangeListInsertions({
|
|
1865
|
+
xmlDoc,
|
|
1866
|
+
explicitRangeParagraphs,
|
|
1867
|
+
insertionEntries,
|
|
1868
|
+
generateRedlines,
|
|
1869
|
+
author
|
|
1870
|
+
});
|
|
1871
|
+
if (applied) {
|
|
1872
|
+
return {
|
|
1873
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1874
|
+
hasChanges: true,
|
|
1875
|
+
numberingXml: null
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
let inferredTableRangeParagraphs = null;
|
|
1881
|
+
if (!explicitRangeParagraphs && !useTableScope && isTableMarkdownEdit) {
|
|
1882
|
+
inferredTableRangeParagraphs = inferTableReplacementParagraphBlock(targetParagraph, {
|
|
1883
|
+
getParagraphText
|
|
1884
|
+
});
|
|
1885
|
+
if (inferredTableRangeParagraphs?.length > 1) {
|
|
1886
|
+
onInfo(`[Table] Heuristic range expansion selected ${inferredTableRangeParagraphs.length} paragraph(s) for replacement.`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
const targetListInfo = getParagraphListInfo(targetParagraph);
|
|
1890
|
+
if (
|
|
1891
|
+
targetListInfo &&
|
|
1892
|
+
typeof effectiveModifiedText === 'string' &&
|
|
1893
|
+
!effectiveModifiedText.includes('\n')
|
|
1894
|
+
) {
|
|
1895
|
+
const strippedListPrefix = stripRedundantLeadingListMarkers(effectiveModifiedText);
|
|
1896
|
+
if (strippedListPrefix && strippedListPrefix !== effectiveModifiedText.trim()) {
|
|
1897
|
+
onInfo('[List] Stripped redundant manual list marker prefix from single-line list item edit.');
|
|
1898
|
+
effectiveModifiedText = strippedListPrefix;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (useTableScope) {
|
|
1902
|
+
onInfo('[Table] Markdown table edit detected in table cell target; applying reconciliation at table scope.');
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
const bypassSingleParagraphHeuristics = options.explicitStructuredContent === true;
|
|
1906
|
+
|
|
1907
|
+
const adjacencyInsertionCandidate = (
|
|
1908
|
+
!bypassSingleParagraphHeuristics
|
|
1909
|
+
&& !useTableScope
|
|
1910
|
+
&& !hasExplicitRangeScope
|
|
1911
|
+
&& targetListInfo
|
|
1912
|
+
)
|
|
1913
|
+
? deriveSingleParagraphListAdjacencyInsertion(currentParagraphText, effectiveModifiedText)
|
|
1914
|
+
: null;
|
|
1915
|
+
if (adjacencyInsertionCandidate) {
|
|
1916
|
+
onInfo(`[List] Applying single-paragraph list adjacency insertion heuristic (${adjacencyInsertionCandidate.position}).`);
|
|
1917
|
+
const parent = targetParagraph.parentNode;
|
|
1918
|
+
if (!parent) throw new Error('Target paragraph has no parent for adjacency list insertion');
|
|
1919
|
+
|
|
1920
|
+
const listParagraph = buildInsertedListParagraph(
|
|
1921
|
+
xmlDoc,
|
|
1922
|
+
targetParagraph,
|
|
1923
|
+
{
|
|
1924
|
+
ilvl: targetListInfo.ilvl,
|
|
1925
|
+
numId: targetListInfo.numId,
|
|
1926
|
+
markerType: 'numbered',
|
|
1927
|
+
text: adjacencyInsertionCandidate.text
|
|
1928
|
+
},
|
|
1929
|
+
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
1930
|
+
author,
|
|
1931
|
+
{ generateRedlines }
|
|
1932
|
+
);
|
|
1933
|
+
|
|
1934
|
+
const insertionPoint = adjacencyInsertionCandidate.position === 'before'
|
|
1935
|
+
? targetParagraph
|
|
1936
|
+
: targetParagraph.nextSibling;
|
|
1937
|
+
ensureParagraphIdsOnImportedNode(listParagraph, operationSession);
|
|
1938
|
+
parent.insertBefore(listParagraph, insertionPoint);
|
|
1939
|
+
options?._mutationLiveNodes?.push(listParagraph);
|
|
1940
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1941
|
+
return {
|
|
1942
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1943
|
+
hasChanges: true,
|
|
1944
|
+
numberingXml: null
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
const plainAdjacencyInsertionCandidate = (
|
|
1949
|
+
!bypassSingleParagraphHeuristics
|
|
1950
|
+
&& !useTableScope
|
|
1951
|
+
&& !hasExplicitRangeScope
|
|
1952
|
+
&& !targetListInfo
|
|
1953
|
+
)
|
|
1954
|
+
? deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, effectiveModifiedText)
|
|
1955
|
+
: null;
|
|
1956
|
+
if (plainAdjacencyInsertionCandidate) {
|
|
1957
|
+
onInfo(
|
|
1958
|
+
`[Text] Applying single-paragraph plain adjacency insertion heuristic `
|
|
1959
|
+
+ `(${plainAdjacencyInsertionCandidate.position}, count=${plainAdjacencyInsertionCandidate.paragraphs.length}).`
|
|
1960
|
+
);
|
|
1961
|
+
const parent = targetParagraph.parentNode;
|
|
1962
|
+
if (!parent) throw new Error('Target paragraph has no parent for plain adjacency insertion');
|
|
1963
|
+
|
|
1964
|
+
const insertionPoint = plainAdjacencyInsertionCandidate.position === 'before'
|
|
1965
|
+
? targetParagraph
|
|
1966
|
+
: targetParagraph.nextSibling;
|
|
1967
|
+
|
|
1968
|
+
for (const paragraphText of plainAdjacencyInsertionCandidate.paragraphs) {
|
|
1969
|
+
const plainParagraph = await buildInsertedPlainParagraph(
|
|
1970
|
+
xmlDoc,
|
|
1971
|
+
targetParagraph,
|
|
1972
|
+
paragraphText,
|
|
1973
|
+
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
1974
|
+
author,
|
|
1975
|
+
// A wholly inserted paragraph must track its own paragraph mark; otherwise Word's
|
|
1976
|
+
// Reject All removes the text but leaves an empty paragraph behind.
|
|
1977
|
+
{ generateRedlines, trackParagraphMark: true }
|
|
1978
|
+
);
|
|
1979
|
+
const imported = xmlDoc.importNode(plainParagraph, true);
|
|
1980
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
1981
|
+
const inserted = parent.insertBefore(imported, insertionPoint);
|
|
1982
|
+
options?._mutationLiveNodes?.push(inserted);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1986
|
+
return {
|
|
1987
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1988
|
+
hasChanges: true,
|
|
1989
|
+
numberingXml: null
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const insertionOnlyPlan = (!bypassSingleParagraphHeuristics && !useTableScope && !hasExplicitRangeScope)
|
|
1994
|
+
? planListInsertionOnlyEdit(targetParagraph, effectiveModifiedText, {
|
|
1995
|
+
currentParagraphText,
|
|
1996
|
+
onInfo,
|
|
1997
|
+
onWarn
|
|
1998
|
+
})
|
|
1999
|
+
: null;
|
|
2000
|
+
if (insertionOnlyPlan && insertionOnlyPlan.entries.length > 0) {
|
|
2001
|
+
onInfo(`[List] Applying insertion-only list redline heuristic (${insertionOnlyPlan.entries.length} new item(s)).`);
|
|
2002
|
+
for (const entry of insertionOnlyPlan.entries) {
|
|
2003
|
+
onInfo(`[List] Insertion entry resolved: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
2004
|
+
}
|
|
2005
|
+
const parent = targetParagraph.parentNode;
|
|
2006
|
+
if (!parent) throw new Error('Target paragraph has no parent for list insertion');
|
|
2007
|
+
const insertionPoint = targetParagraph.nextSibling;
|
|
2008
|
+
for (const entry of insertionOnlyPlan.entries) {
|
|
2009
|
+
const listParagraph = buildInsertedListParagraph(
|
|
2010
|
+
xmlDoc,
|
|
2011
|
+
targetParagraph,
|
|
2012
|
+
{ ...entry, numId: insertionOnlyPlan.numId },
|
|
2013
|
+
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
2014
|
+
author,
|
|
2015
|
+
{ generateRedlines }
|
|
2016
|
+
);
|
|
2017
|
+
ensureParagraphIdsOnImportedNode(listParagraph, operationSession);
|
|
2018
|
+
parent.insertBefore(listParagraph, insertionPoint);
|
|
2019
|
+
options?._mutationLiveNodes?.push(listParagraph);
|
|
2020
|
+
}
|
|
2021
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
2022
|
+
return {
|
|
2023
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2024
|
+
hasChanges: true,
|
|
2025
|
+
numberingXml: null
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const listScopeEdit = (!bypassSingleParagraphHeuristics && !useTableScope && !hasExplicitRangeScope)
|
|
2030
|
+
? synthesizeExpandedListScopeEdit(targetParagraph, effectiveModifiedText, {
|
|
2031
|
+
currentParagraphText,
|
|
2032
|
+
onInfo,
|
|
2033
|
+
onWarn
|
|
2034
|
+
})
|
|
2035
|
+
: null;
|
|
2036
|
+
const useListScope = !!listScopeEdit && Array.isArray(listScopeEdit.paragraphs) && listScopeEdit.paragraphs.length > 0;
|
|
2037
|
+
if (useListScope) {
|
|
2038
|
+
effectiveModifiedText = listScopeEdit.modifiedText;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
if (!bypassSingleParagraphHeuristics && !useTableScope && !useListScope && !hasExplicitRangeScope) {
|
|
2042
|
+
const explicitHeaderListConversion = await tryExplicitDecimalHeaderListConversion({
|
|
2043
|
+
xmlDoc,
|
|
2044
|
+
serializer,
|
|
2045
|
+
documentXml,
|
|
2046
|
+
operationSession,
|
|
2047
|
+
targetParagraph,
|
|
2048
|
+
currentParagraphText,
|
|
2049
|
+
modifiedText: effectiveModifiedText,
|
|
2050
|
+
author,
|
|
2051
|
+
runtimeContext,
|
|
2052
|
+
revisionIdAllocator,
|
|
2053
|
+
generateRedlines,
|
|
2054
|
+
onInfo,
|
|
2055
|
+
options
|
|
2056
|
+
});
|
|
2057
|
+
if (explicitHeaderListConversion) return explicitHeaderListConversion;
|
|
2058
|
+
|
|
2059
|
+
const listFallback = await trySingleParagraphListStructuralFallback({
|
|
2060
|
+
xmlDoc,
|
|
2061
|
+
serializer,
|
|
2062
|
+
documentXml,
|
|
2063
|
+
operationSession,
|
|
2064
|
+
targetParagraph,
|
|
2065
|
+
currentParagraphText,
|
|
2066
|
+
modifiedText: effectiveModifiedText,
|
|
2067
|
+
author,
|
|
2068
|
+
runtimeContext,
|
|
2069
|
+
revisionIdAllocator,
|
|
2070
|
+
generateRedlines,
|
|
2071
|
+
onInfo,
|
|
2072
|
+
options
|
|
2073
|
+
});
|
|
2074
|
+
if (listFallback) return listFallback;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const originalTextForApply = useListScope
|
|
2078
|
+
? listScopeEdit.originalText
|
|
2079
|
+
: (
|
|
2080
|
+
explicitRangeParagraphs
|
|
2081
|
+
? explicitRangeParagraphs.map(paragraph => getParagraphText(paragraph)).join('\n')
|
|
2082
|
+
: (inferredTableRangeParagraphs
|
|
2083
|
+
? inferredTableRangeParagraphs.map(paragraph => getParagraphText(paragraph)).join('\n')
|
|
2081
2084
|
: (currentParagraphText || targetText))
|
|
2082
2085
|
);
|
|
2083
|
-
const scopedXml = useTableScope
|
|
2084
|
-
? serializer.serializeToString(containingTable)
|
|
2085
|
-
: (
|
|
2086
|
-
useListScope
|
|
2087
|
-
? serializeParagraphRangeAsDocument(listScopeEdit.paragraphs, serializer)
|
|
2088
|
-
: (
|
|
2089
|
-
explicitRangeParagraphs
|
|
2090
|
-
? serializeParagraphRangeAsDocument(explicitRangeParagraphs, serializer)
|
|
2091
|
-
: (inferredTableRangeParagraphs
|
|
2092
|
-
? serializeParagraphRangeAsDocument(inferredTableRangeParagraphs, serializer)
|
|
2093
|
-
: serializer.serializeToString(targetParagraph))
|
|
2094
|
-
)
|
|
2095
|
-
);
|
|
2096
|
-
|
|
2097
|
-
const result = isTableMarkdownEdit
|
|
2098
|
-
? await reconcileMarkdownTableOoxml(scopedXml, originalTextForApply, effectiveModifiedText, {
|
|
2099
|
-
author,
|
|
2100
|
-
generateRedlines,
|
|
2101
|
-
existingRevisions: options.existingRevisions || 'merge-same-author',
|
|
2102
|
-
structuredContent: options.structuredContent !== false,
|
|
2103
|
-
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
2104
|
-
_revisionIdAllocator: revisionIdAllocator,
|
|
2105
|
-
_isolatedTableCell: useTableScope
|
|
2106
|
-
})
|
|
2107
|
-
: await applyRedlineToOxml(scopedXml, originalTextForApply, effectiveModifiedText, {
|
|
2108
|
-
author,
|
|
2109
|
-
generateRedlines,
|
|
2110
|
-
existingRevisions: options.existingRevisions || 'merge-same-author',
|
|
2111
|
-
structuredContent: options.structuredContent !== false,
|
|
2112
|
-
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
2113
|
-
pairReplacements: options.pairReplacements === true,
|
|
2114
|
-
insertionAffinity: options.insertionAffinity || null,
|
|
2115
|
-
_revisionIdAllocator: revisionIdAllocator,
|
|
2116
|
-
_isolatedTableCell: useTableScope
|
|
2117
|
-
});
|
|
2118
|
-
if (!result?.hasChanges) {
|
|
2119
|
-
return {
|
|
2120
|
-
documentXml,
|
|
2121
|
-
hasChanges: false,
|
|
2122
|
-
numberingXml: null,
|
|
2123
|
-
status: result?.status || 'no-op',
|
|
2124
|
-
error: result?.error
|
|
2125
|
-
};
|
|
2126
|
-
}
|
|
2127
|
-
if (result.useNativeApi && !result.oxml) {
|
|
2128
|
-
const warning = 'Format-only fallback requires native Word API; browser demo skipped this operation.';
|
|
2129
|
-
onWarn(`[WARN] ${warning}`);
|
|
2130
|
-
return { documentXml, hasChanges: false, numberingXml: null, warnings: [warning] };
|
|
2131
|
-
}
|
|
2132
|
-
if (typeof result.oxml !== 'string') {
|
|
2133
|
-
throw new Error('Reconciliation engine did not return OOXML for a changed redline operation');
|
|
2134
|
-
}
|
|
2135
|
-
const extracted = extractReplacementNodes(result.oxml);
|
|
2136
|
-
let replacementNodes = removeListPackagingSentinel(extracted.replacementNodes, result.warnings);
|
|
2137
|
-
let numberingXml = extracted.numberingXml;
|
|
2138
|
-
if (numberingXml && runtimeContext?.numberingIdState) {
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
const
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
parent.
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
parent.
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
const
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
}
|
|
2556
|
-
if (!
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
const
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
const
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2086
|
+
const scopedXml = useTableScope
|
|
2087
|
+
? serializer.serializeToString(containingTable)
|
|
2088
|
+
: (
|
|
2089
|
+
useListScope
|
|
2090
|
+
? serializeParagraphRangeAsDocument(listScopeEdit.paragraphs, serializer)
|
|
2091
|
+
: (
|
|
2092
|
+
explicitRangeParagraphs
|
|
2093
|
+
? serializeParagraphRangeAsDocument(explicitRangeParagraphs, serializer)
|
|
2094
|
+
: (inferredTableRangeParagraphs
|
|
2095
|
+
? serializeParagraphRangeAsDocument(inferredTableRangeParagraphs, serializer)
|
|
2096
|
+
: serializer.serializeToString(targetParagraph))
|
|
2097
|
+
)
|
|
2098
|
+
);
|
|
2099
|
+
|
|
2100
|
+
const result = isTableMarkdownEdit
|
|
2101
|
+
? await reconcileMarkdownTableOoxml(scopedXml, originalTextForApply, effectiveModifiedText, {
|
|
2102
|
+
author,
|
|
2103
|
+
generateRedlines,
|
|
2104
|
+
existingRevisions: options.existingRevisions || 'merge-same-author',
|
|
2105
|
+
structuredContent: options.structuredContent !== false,
|
|
2106
|
+
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
2107
|
+
_revisionIdAllocator: revisionIdAllocator,
|
|
2108
|
+
_isolatedTableCell: useTableScope
|
|
2109
|
+
})
|
|
2110
|
+
: await applyRedlineToOxml(scopedXml, originalTextForApply, effectiveModifiedText, {
|
|
2111
|
+
author,
|
|
2112
|
+
generateRedlines,
|
|
2113
|
+
existingRevisions: options.existingRevisions || 'merge-same-author',
|
|
2114
|
+
structuredContent: options.structuredContent !== false,
|
|
2115
|
+
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
2116
|
+
pairReplacements: options.pairReplacements === true,
|
|
2117
|
+
insertionAffinity: options.insertionAffinity || null,
|
|
2118
|
+
_revisionIdAllocator: revisionIdAllocator,
|
|
2119
|
+
_isolatedTableCell: useTableScope
|
|
2120
|
+
});
|
|
2121
|
+
if (!result?.hasChanges) {
|
|
2122
|
+
return {
|
|
2123
|
+
documentXml,
|
|
2124
|
+
hasChanges: false,
|
|
2125
|
+
numberingXml: null,
|
|
2126
|
+
status: result?.status || 'no-op',
|
|
2127
|
+
error: result?.error
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
if (result.useNativeApi && !result.oxml) {
|
|
2131
|
+
const warning = 'Format-only fallback requires native Word API; browser demo skipped this operation.';
|
|
2132
|
+
onWarn(`[WARN] ${warning}`);
|
|
2133
|
+
return { documentXml, hasChanges: false, numberingXml: null, warnings: [warning] };
|
|
2134
|
+
}
|
|
2135
|
+
if (typeof result.oxml !== 'string') {
|
|
2136
|
+
throw new Error('Reconciliation engine did not return OOXML for a changed redline operation');
|
|
2137
|
+
}
|
|
2138
|
+
const extracted = extractReplacementNodes(result.oxml);
|
|
2139
|
+
let replacementNodes = removeListPackagingSentinel(extracted.replacementNodes, result.warnings);
|
|
2140
|
+
let numberingXml = extracted.numberingXml;
|
|
2141
|
+
if (numberingXml && runtimeContext?.numberingIdState && !useTableScope) {
|
|
2142
|
+
const sourceParagraphs = useListScope ? listScopeEdit.paragraphs : (explicitRangeParagraphs || [targetParagraph]);
|
|
2143
|
+
const continued = reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, runtimeContext.numberingIdState);
|
|
2144
|
+
if (continued) {
|
|
2145
|
+
replacementNodes = continued.replacementNodes;
|
|
2146
|
+
numberingXml = null;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
if (numberingXml && runtimeContext?.numberingIdState) {
|
|
2150
|
+
const normalizedNumbering = remapNumberingPayloadForDocument(numberingXml, replacementNodes, runtimeContext.numberingIdState);
|
|
2151
|
+
replacementNodes = normalizedNumbering.replacementNodes;
|
|
2152
|
+
numberingXml = normalizedNumbering.numberingXml;
|
|
2153
|
+
}
|
|
2154
|
+
const scopeNodes = useTableScope
|
|
2155
|
+
? [containingTable]
|
|
2156
|
+
: (
|
|
2157
|
+
useListScope
|
|
2158
|
+
? listScopeEdit.paragraphs
|
|
2159
|
+
: (
|
|
2160
|
+
explicitRangeParagraphs
|
|
2161
|
+
? explicitRangeParagraphs
|
|
2162
|
+
: (inferredTableRangeParagraphs || [targetParagraph])
|
|
2163
|
+
)
|
|
2164
|
+
);
|
|
2165
|
+
const anchorNode = scopeNodes[0];
|
|
2166
|
+
const parent = anchorNode.parentNode;
|
|
2167
|
+
for (const node of replacementNodes) {
|
|
2168
|
+
const imported = xmlDoc.importNode(node, true);
|
|
2169
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
2170
|
+
const inserted = parent.insertBefore(imported, anchorNode);
|
|
2171
|
+
options?._mutationLiveNodes?.push(inserted);
|
|
2172
|
+
}
|
|
2173
|
+
for (const scopeNode of scopeNodes) {
|
|
2174
|
+
if (scopeNode && scopeNode.parentNode === parent) {
|
|
2175
|
+
options?._mutationRemovedNodes?.push(scopeNode);
|
|
2176
|
+
parent.removeChild(scopeNode);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
2180
|
+
if (rawTableStructuralDedupeKey && tableStructuralDedupes && (useTableScope || containingTable)) {
|
|
2181
|
+
tableStructuralDedupes.add(rawTableStructuralDedupeKey);
|
|
2182
|
+
}
|
|
2183
|
+
if (operationSession?.receiptCollector && numberingXml) {
|
|
2184
|
+
const numIds = collectNumberingIdsFromNodes(options?._mutationLiveNodes);
|
|
2185
|
+
for (const id of numIds) {
|
|
2186
|
+
operationSession.receiptCollector.recordNumbering(id);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return {
|
|
2190
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2191
|
+
hasChanges: true,
|
|
2192
|
+
numberingXml,
|
|
2193
|
+
status: 'ok',
|
|
2194
|
+
...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {})
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
export async function applyHighlightToParagraphByExactText(documentXml, targetText, textToHighlight, color, author, targetRef = null, runtimeContext = null, options = {}) {
|
|
2199
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
2200
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
2201
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
2202
|
+
const { serializer, xmlDoc, operationSession } = resolveMutationDocument(documentXml, options);
|
|
2203
|
+
if (!xmlDoc) return { documentXml, hasChanges: false, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
2204
|
+
const revisionIdAllocator = operationSession?.revisionIdAllocator || prepareRevisionAllocator(xmlDoc, options);
|
|
2205
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'highlight', runtimeContext, {
|
|
2206
|
+
...options,
|
|
2207
|
+
onInfo,
|
|
2208
|
+
onWarn
|
|
2209
|
+
});
|
|
2210
|
+
if (resolved?.error || !resolved?.paragraph) {
|
|
2211
|
+
return { documentXml, hasChanges: false, status: 'error', error: resolved?.error || { code: 'TARGET_NOT_FOUND', message: 'Target paragraph not found.' } };
|
|
2212
|
+
}
|
|
2213
|
+
const targetParagraph = resolved.paragraph;
|
|
2214
|
+
const paragraphXml = serializer.serializeToString(targetParagraph);
|
|
2215
|
+
const highlightedXml = applyHighlightToOoxml(paragraphXml, textToHighlight, color, {
|
|
2216
|
+
generateRedlines,
|
|
2217
|
+
author,
|
|
2218
|
+
_revisionIdAllocator: revisionIdAllocator
|
|
2219
|
+
});
|
|
2220
|
+
if (!highlightedXml || highlightedXml === paragraphXml) return { documentXml, hasChanges: false };
|
|
2221
|
+
const { replacementNodes } = extractReplacementNodes(highlightedXml);
|
|
2222
|
+
const parent = targetParagraph.parentNode;
|
|
2223
|
+
for (const node of replacementNodes) {
|
|
2224
|
+
const imported = xmlDoc.importNode(node, true);
|
|
2225
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
2226
|
+
const inserted = parent.insertBefore(imported, targetParagraph);
|
|
2227
|
+
options?._mutationLiveNodes?.push(inserted);
|
|
2228
|
+
}
|
|
2229
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
2230
|
+
parent.removeChild(targetParagraph);
|
|
2231
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
2232
|
+
return {
|
|
2233
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2234
|
+
hasChanges: true
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
export async function applyCommentToParagraphByExactText(documentXml, targetText, textToComment, commentContent, author, targetRef = null, runtimeContext = null, options = {}) {
|
|
2239
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
2240
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
2241
|
+
const { serializer, xmlDoc, operationSession } = resolveMutationDocument(documentXml, options);
|
|
2242
|
+
if (!xmlDoc) return { documentXml, hasChanges: false, commentsXml: null, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
2243
|
+
if (!operationSession) prepareRevisionAllocator(xmlDoc, options);
|
|
2244
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'comment', runtimeContext, {
|
|
2245
|
+
...options,
|
|
2246
|
+
onInfo,
|
|
2247
|
+
onWarn
|
|
2248
|
+
});
|
|
2249
|
+
if (resolved?.error || !resolved?.paragraph) {
|
|
2250
|
+
return { documentXml, hasChanges: false, commentsXml: null, status: 'error', error: resolved?.error || { code: 'TARGET_NOT_FOUND', message: 'Target paragraph not found.' } };
|
|
2251
|
+
}
|
|
2252
|
+
const targetParagraph = resolved.paragraph;
|
|
2253
|
+
const paragraphXml = serializer.serializeToString(targetParagraph);
|
|
2254
|
+
const commentAnchor = typeof textToComment === 'string' && textToComment.length > 0
|
|
2255
|
+
? textToComment
|
|
2256
|
+
: getParagraphText(targetParagraph);
|
|
2257
|
+
const commentResult = injectCommentsIntoOoxml(paragraphXml, [{
|
|
2258
|
+
paragraphIndex: 1,
|
|
2259
|
+
textToFind: commentAnchor,
|
|
2260
|
+
commentContent
|
|
2261
|
+
}], { author, commentIdAllocator: options.commentIdAllocator });
|
|
2262
|
+
if (commentResult.status === 'error' || commentResult.error || !commentResult.commentsApplied) {
|
|
2263
|
+
return {
|
|
2264
|
+
documentXml,
|
|
2265
|
+
hasChanges: false,
|
|
2266
|
+
commentsXml: null,
|
|
2267
|
+
status: 'error',
|
|
2268
|
+
error: commentResult.error || {
|
|
2269
|
+
code: 'ANCHOR_INSERTION_FAILED',
|
|
2270
|
+
message: 'The comment anchor was not applied to the resolved paragraph.'
|
|
2271
|
+
},
|
|
2272
|
+
warnings: commentResult.warnings || []
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
const { replacementNodes } = extractReplacementNodes(commentResult.oxml);
|
|
2276
|
+
const parent = targetParagraph.parentNode;
|
|
2277
|
+
for (const node of replacementNodes) {
|
|
2278
|
+
const imported = xmlDoc.importNode(node, true);
|
|
2279
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
2280
|
+
const inserted = parent.insertBefore(imported, targetParagraph);
|
|
2281
|
+
options?._mutationLiveNodes?.push(inserted);
|
|
2282
|
+
}
|
|
2283
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
2284
|
+
parent.removeChild(targetParagraph);
|
|
2285
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
2286
|
+
if (operationSession?.receiptCollector && Array.isArray(commentResult.placedComments)) {
|
|
2287
|
+
for (const c of commentResult.placedComments) {
|
|
2288
|
+
operationSession.receiptCollector.recordComment(c.id);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2293
|
+
hasChanges: true,
|
|
2294
|
+
commentsXml: commentResult.commentsXml || null,
|
|
2295
|
+
warnings: commentResult.warnings || [],
|
|
2296
|
+
resolvedAnchor: commentResult.resolvedAnchors?.[0] || null
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
function splitRunAtTextOffset(xmlDoc, run, localOffset, revisionIdAllocator) {
|
|
2301
|
+
const run1 = run.cloneNode(true);
|
|
2302
|
+
const run2 = run.cloneNode(true);
|
|
2303
|
+
refreshRunPropertyChangeIds(run2, revisionIdAllocator);
|
|
2304
|
+
|
|
2305
|
+
const isTextPiece = (node) => {
|
|
2306
|
+
if (node.nodeType !== 1) return false;
|
|
2307
|
+
const ln = node.localName || node.nodeName.replace(/^w:/, '');
|
|
2308
|
+
return ['t', 'tab', 'br', 'noBreakHyphen', 'delText'].includes(ln);
|
|
2309
|
+
};
|
|
2310
|
+
|
|
2311
|
+
Array.from(run1.childNodes).forEach(c => { if (isTextPiece(c)) run1.removeChild(c); });
|
|
2312
|
+
Array.from(run2.childNodes).forEach(c => { if (isTextPiece(c)) run2.removeChild(c); });
|
|
2313
|
+
|
|
2314
|
+
let fullText = '';
|
|
2315
|
+
for (const child of Array.from(run.childNodes)) {
|
|
2316
|
+
if (child.nodeType !== 1) continue;
|
|
2317
|
+
const ln = child.localName || child.nodeName.replace(/^w:/, '');
|
|
2318
|
+
if (ln === 't') fullText += child.textContent || '';
|
|
2319
|
+
else if (ln === 'tab') fullText += '\t';
|
|
2320
|
+
else if (ln === 'br') fullText += '\n';
|
|
2321
|
+
else if (ln === 'noBreakHyphen') fullText += '\u2011';
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
const text1 = fullText.slice(0, localOffset);
|
|
2325
|
+
const text2 = fullText.slice(localOffset);
|
|
2326
|
+
|
|
2327
|
+
const appendPieces = (targetRun, text) => {
|
|
2328
|
+
const parts = text.split(/(\t|\n|\u2011)/);
|
|
2329
|
+
for (const part of parts) {
|
|
2330
|
+
if (!part) continue;
|
|
2331
|
+
if (part === '\t') {
|
|
2332
|
+
targetRun.appendChild(createWordElement(xmlDoc, 'w:tab'));
|
|
2333
|
+
} else if (part === '\n') {
|
|
2334
|
+
targetRun.appendChild(createWordElement(xmlDoc, 'w:br'));
|
|
2335
|
+
} else if (part === '\u2011') {
|
|
2336
|
+
targetRun.appendChild(createWordElement(xmlDoc, 'w:noBreakHyphen'));
|
|
2337
|
+
} else {
|
|
2338
|
+
const tEl = createWordElement(xmlDoc, 'w:t');
|
|
2339
|
+
if (/^\s|\s$/.test(part)) tEl.setAttribute('xml:space', 'preserve');
|
|
2340
|
+
tEl.textContent = part;
|
|
2341
|
+
targetRun.appendChild(tEl);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
appendPieces(run1, text1);
|
|
2347
|
+
appendPieces(run2, text2);
|
|
2348
|
+
|
|
2349
|
+
const parent = run.parentNode;
|
|
2350
|
+
parent.insertBefore(run1, run);
|
|
2351
|
+
parent.insertBefore(run2, run);
|
|
2352
|
+
parent.removeChild(run);
|
|
2353
|
+
|
|
2354
|
+
return [run1, run2];
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
export async function applyFormattingToParagraphByExactText(
|
|
2358
|
+
documentXml,
|
|
2359
|
+
targetText,
|
|
2360
|
+
textToFormat,
|
|
2361
|
+
properties = {},
|
|
2362
|
+
author,
|
|
2363
|
+
targetRef = null,
|
|
2364
|
+
runtimeContext = null,
|
|
2365
|
+
options = {}
|
|
2366
|
+
) {
|
|
2367
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
2368
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
2369
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
2370
|
+
const { serializer, xmlDoc, operationSession } = resolveMutationDocument(documentXml, options);
|
|
2371
|
+
if (!xmlDoc) {
|
|
2372
|
+
return {
|
|
2373
|
+
documentXml,
|
|
2374
|
+
hasChanges: false,
|
|
2375
|
+
status: 'error',
|
|
2376
|
+
error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' }
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
const revisionIdAllocator = operationSession?.revisionIdAllocator || prepareRevisionAllocator(xmlDoc, options);
|
|
2380
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'format', runtimeContext, {
|
|
2381
|
+
...options,
|
|
2382
|
+
onInfo,
|
|
2383
|
+
onWarn
|
|
2384
|
+
});
|
|
2385
|
+
if (resolved?.error || !resolved?.paragraph) {
|
|
2386
|
+
return { documentXml, hasChanges: false, status: 'error', error: resolved?.error || { code: 'TARGET_NOT_FOUND', message: 'Target paragraph not found.' } };
|
|
2387
|
+
}
|
|
2388
|
+
const targetParagraph = resolved.paragraph;
|
|
2389
|
+
|
|
2390
|
+
const collectVisibleRuns = () => {
|
|
2391
|
+
const runEntries = [];
|
|
2392
|
+
const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W, 'r'));
|
|
2393
|
+
for (const run of allRuns) {
|
|
2394
|
+
let parent = run.parentNode;
|
|
2395
|
+
let isDel = false;
|
|
2396
|
+
while (parent && parent !== targetParagraph) {
|
|
2397
|
+
if (parent.nodeType === 1 && (parent.localName === 'del' || parent.nodeName === 'w:del')) {
|
|
2398
|
+
isDel = true;
|
|
2399
|
+
break;
|
|
2400
|
+
}
|
|
2401
|
+
parent = parent.parentNode;
|
|
2402
|
+
}
|
|
2403
|
+
if (isDel) continue;
|
|
2404
|
+
|
|
2405
|
+
let runText = '';
|
|
2406
|
+
for (const child of Array.from(run.childNodes)) {
|
|
2407
|
+
if (child.nodeType !== 1) continue;
|
|
2408
|
+
const ln = child.localName || child.nodeName.replace(/^w:/, '');
|
|
2409
|
+
if (ln === 't') runText += child.textContent || '';
|
|
2410
|
+
else if (ln === 'tab') runText += '\t';
|
|
2411
|
+
else if (ln === 'br') runText += '\n';
|
|
2412
|
+
else if (ln === 'noBreakHyphen') runText += '\u2011';
|
|
2413
|
+
}
|
|
2414
|
+
if (runText.length > 0) {
|
|
2415
|
+
runEntries.push({ run, text: runText });
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return runEntries;
|
|
2419
|
+
};
|
|
2420
|
+
|
|
2421
|
+
let runEntries = collectVisibleRuns();
|
|
2422
|
+
let fullParaText = '';
|
|
2423
|
+
let spanRanges = [];
|
|
2424
|
+
for (const entry of runEntries) {
|
|
2425
|
+
const start = fullParaText.length;
|
|
2426
|
+
fullParaText += entry.text;
|
|
2427
|
+
const end = fullParaText.length;
|
|
2428
|
+
spanRanges.push({ ...entry, start, end });
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
const targetDescriptor = options.targetDescriptor;
|
|
2432
|
+
const occurrence = targetDescriptor?.occurrence ?? null;
|
|
2433
|
+
let matchStart = -1;
|
|
2434
|
+
if (occurrence != null && occurrence > 0) {
|
|
2435
|
+
let count = 0;
|
|
2436
|
+
let offset = 0;
|
|
2437
|
+
while (offset <= fullParaText.length - textToFormat.length) {
|
|
2438
|
+
const idx = fullParaText.indexOf(textToFormat, offset);
|
|
2439
|
+
if (idx === -1) break;
|
|
2440
|
+
count++;
|
|
2441
|
+
if (count === occurrence) {
|
|
2442
|
+
matchStart = idx;
|
|
2443
|
+
break;
|
|
2444
|
+
}
|
|
2445
|
+
offset = idx + 1;
|
|
2446
|
+
}
|
|
2447
|
+
} else {
|
|
2448
|
+
matchStart = fullParaText.indexOf(textToFormat);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
if (matchStart === -1) {
|
|
2452
|
+
return {
|
|
2453
|
+
documentXml,
|
|
2454
|
+
hasChanges: false,
|
|
2455
|
+
status: 'error',
|
|
2456
|
+
error: {
|
|
2457
|
+
code: 'TARGET_NOT_FOUND',
|
|
2458
|
+
message: `Text to format was not found in target paragraph: "${textToFormat}".`
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
const matchEnd = matchStart + textToFormat.length;
|
|
2463
|
+
|
|
2464
|
+
// Split runs at matchStart boundary
|
|
2465
|
+
let currentRuns = [];
|
|
2466
|
+
for (const span of spanRanges) {
|
|
2467
|
+
if (span.start < matchStart && span.end > matchStart) {
|
|
2468
|
+
const localOffset = matchStart - span.start;
|
|
2469
|
+
const [r1, r2] = splitRunAtTextOffset(xmlDoc, span.run, localOffset, revisionIdAllocator);
|
|
2470
|
+
currentRuns.push({ run: r1, start: span.start, end: matchStart, text: span.text.slice(0, localOffset) });
|
|
2471
|
+
currentRuns.push({ run: r2, start: matchStart, end: span.end, text: span.text.slice(localOffset) });
|
|
2472
|
+
} else {
|
|
2473
|
+
currentRuns.push(span);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Split runs at matchEnd boundary
|
|
2478
|
+
const finalRuns = [];
|
|
2479
|
+
for (const span of currentRuns) {
|
|
2480
|
+
if (span.start < matchEnd && span.end > matchEnd) {
|
|
2481
|
+
const localOffset = matchEnd - span.start;
|
|
2482
|
+
const [r1, r2] = splitRunAtTextOffset(xmlDoc, span.run, localOffset, revisionIdAllocator);
|
|
2483
|
+
finalRuns.push({ run: r1, start: span.start, end: matchEnd, text: span.text.slice(0, localOffset) });
|
|
2484
|
+
finalRuns.push({ run: r2, start: matchEnd, end: span.end, text: span.text.slice(localOffset) });
|
|
2485
|
+
} else {
|
|
2486
|
+
finalRuns.push(span);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
const targetRuns = finalRuns.filter(span => span.start >= matchStart && span.end <= matchEnd);
|
|
2491
|
+
|
|
2492
|
+
let anyChanged = false;
|
|
2493
|
+
for (const entry of targetRuns) {
|
|
2494
|
+
const rPr = entry.run.getElementsByTagNameNS(NS_W, 'rPr')[0] || null;
|
|
2495
|
+
if (checkRunPropertiesChanged(rPr, properties)) {
|
|
2496
|
+
anyChanged = true;
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
if (!anyChanged) {
|
|
2502
|
+
return { documentXml, hasChanges: false, status: 'no-op' };
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
for (const entry of targetRuns) {
|
|
2506
|
+
const run = entry.run;
|
|
2507
|
+
let rPr = run.getElementsByTagNameNS(NS_W, 'rPr')[0];
|
|
2508
|
+
if (!rPr) {
|
|
2509
|
+
rPr = createWordElement(xmlDoc, 'w:rPr');
|
|
2510
|
+
run.insertBefore(rPr, run.firstChild);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
let insAncestor = run.parentNode;
|
|
2514
|
+
let insideSameAuthorIns = false;
|
|
2515
|
+
while (insAncestor && insAncestor !== targetParagraph) {
|
|
2516
|
+
if (insAncestor.nodeType === 1 && (insAncestor.localName === 'ins' || insAncestor.nodeName === 'w:ins')) {
|
|
2517
|
+
const insAuthor = insAncestor.getAttribute('w:author') || insAncestor.getAttributeNS(NS_W, 'author');
|
|
2518
|
+
if (insAuthor === (author || getDefaultAuthor())) {
|
|
2519
|
+
insideSameAuthorIns = true;
|
|
2520
|
+
}
|
|
2521
|
+
break;
|
|
2522
|
+
}
|
|
2523
|
+
insAncestor = insAncestor.parentNode;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
const coalesce = insideSameAuthorIns && options.formattingRevisionPolicy === 'coalesce-own-insertion';
|
|
2527
|
+
|
|
2528
|
+
if (generateRedlines && !coalesce) {
|
|
2529
|
+
snapshotAndAttachRPrChange(xmlDoc, rPr, author || getDefaultAuthor());
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
applyCharacterFormatToRPr(xmlDoc, rPr, properties);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
2536
|
+
ensureParagraphIdsOnImportedNode(targetParagraph, operationSession);
|
|
2537
|
+
options?._mutationLiveNodes?.push(targetParagraph);
|
|
2538
|
+
|
|
2539
|
+
return {
|
|
2540
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2541
|
+
hasChanges: true,
|
|
2542
|
+
status: 'ok'
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
export async function applyParagraphFormatToParagraphByExactText(
|
|
2547
|
+
documentXml,
|
|
2548
|
+
targetText,
|
|
2549
|
+
properties = {},
|
|
2550
|
+
author,
|
|
2551
|
+
targetRef = null,
|
|
2552
|
+
runtimeContext = null,
|
|
2553
|
+
options = {}
|
|
2554
|
+
) {
|
|
2555
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
2556
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
2557
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
2558
|
+
const { serializer, xmlDoc, operationSession } = resolveMutationDocument(documentXml, options);
|
|
2559
|
+
if (!xmlDoc) {
|
|
2560
|
+
return {
|
|
2561
|
+
documentXml,
|
|
2562
|
+
hasChanges: false,
|
|
2563
|
+
status: 'error',
|
|
2564
|
+
error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' }
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
if (!operationSession) prepareRevisionAllocator(xmlDoc, options);
|
|
2568
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'paragraph-format', runtimeContext, {
|
|
2569
|
+
...options,
|
|
2570
|
+
onInfo,
|
|
2571
|
+
onWarn
|
|
2572
|
+
});
|
|
2573
|
+
if (resolved?.error || !resolved?.paragraph) {
|
|
2574
|
+
return { documentXml, hasChanges: false, status: 'error', error: resolved?.error || { code: 'TARGET_NOT_FOUND', message: 'Target paragraph not found.' } };
|
|
2575
|
+
}
|
|
2576
|
+
const targetParagraph = resolved.paragraph;
|
|
2577
|
+
|
|
2578
|
+
let pPr = targetParagraph.getElementsByTagNameNS(NS_W, 'pPr')[0];
|
|
2579
|
+
if (!pPr) {
|
|
2580
|
+
pPr = createWordElement(xmlDoc, 'w:pPr');
|
|
2581
|
+
targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
const hasChanges = checkParagraphPropertiesChanged(pPr, properties);
|
|
2585
|
+
if (!hasChanges) {
|
|
2586
|
+
return { documentXml, hasChanges: false, status: 'no-op' };
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
if (generateRedlines) {
|
|
2590
|
+
snapshotAndAttachPPrChange(xmlDoc, pPr, author || getDefaultAuthor());
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
applyParagraphPropertiesToPPr(xmlDoc, pPr, properties);
|
|
2594
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
2595
|
+
ensureParagraphIdsOnImportedNode(targetParagraph, operationSession);
|
|
2596
|
+
options?._mutationLiveNodes?.push(targetParagraph);
|
|
2597
|
+
|
|
2598
|
+
return {
|
|
2599
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2600
|
+
hasChanges: true,
|
|
2601
|
+
status: 'ok'
|
|
2602
|
+
};
|
|
2603
|
+
}
|