@ansonlai/docx-redline-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +176 -0
- package/ARCHITECTURE.md +121 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/adapters/config.js +43 -0
- package/adapters/logger.js +89 -0
- package/adapters/xml-adapter.js +74 -0
- package/core/list-targeting.js +398 -0
- package/core/ooxml-identifiers.js +15 -0
- package/core/paragraph-offset-policy.js +50 -0
- package/core/paragraph-targeting.js +501 -0
- package/core/table-targeting.js +233 -0
- package/core/types.js +204 -0
- package/core/xml-query.js +99 -0
- package/dist/docx-redline-js.esm.js +8801 -0
- package/dist/docx-redline-js.esm.js.map +7 -0
- package/dist/docx-redline-js.esm.min.js +195 -0
- package/dist/docx-redline-js.esm.min.js.map +7 -0
- package/engine/format-application.js +358 -0
- package/engine/format-extraction.js +232 -0
- package/engine/format-paragraph-targeting.js +208 -0
- package/engine/format-span-application.js +178 -0
- package/engine/formatting-removal.js +330 -0
- package/engine/oxml-engine.js +279 -0
- package/engine/reconstruction-mapper.js +270 -0
- package/engine/reconstruction-mode.js +38 -0
- package/engine/reconstruction-writer.js +276 -0
- package/engine/rpr-helpers.js +194 -0
- package/engine/run-builders.js +235 -0
- package/engine/surgical-mode.js +520 -0
- package/engine/table-cell-context.js +151 -0
- package/engine/table-mode.js +172 -0
- package/index.js +308 -0
- package/orchestration/list-markdown.js +141 -0
- package/orchestration/list-parsing.js +73 -0
- package/orchestration/list-structural-fallback.js +530 -0
- package/orchestration/redline-operation-converter.js +141 -0
- package/orchestration/route-plan.js +160 -0
- package/package.json +76 -0
- package/pipeline/content-analysis.js +107 -0
- package/pipeline/diff-engine.js +204 -0
- package/pipeline/ingestion-export.js +255 -0
- package/pipeline/ingestion-paragraph.js +351 -0
- package/pipeline/ingestion-table.js +169 -0
- package/pipeline/ingestion-xml.js +39 -0
- package/pipeline/ingestion.js +8 -0
- package/pipeline/list-generation.js +280 -0
- package/pipeline/list-markers.js +77 -0
- package/pipeline/markdown-processor.js +160 -0
- package/pipeline/patching.js +408 -0
- package/pipeline/pipeline.js +326 -0
- package/pipeline/serialization.js +395 -0
- package/services/browser-demo-prompt-context.js +345 -0
- package/services/comment-builders.js +60 -0
- package/services/comment-engine.js +248 -0
- package/services/comment-locator.js +197 -0
- package/services/comment-package.js +113 -0
- package/services/numbering-helpers.js +416 -0
- package/services/numbering-service.js +290 -0
- package/services/package-builder.js +147 -0
- package/services/standalone-docx-plumbing.js +443 -0
- package/services/standalone-operation-runner.js +1169 -0
- package/services/table-reconciliation.js +344 -0
- package/standalone.js +5 -0
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone document-operation runner for redline/highlight/comment operations.
|
|
3
|
+
*
|
|
4
|
+
* This module centralizes the browser-demo operation bridge so host layers can
|
|
5
|
+
* stay focused on UI + prompt orchestration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createParser, createSerializer } from '../adapters/xml-adapter.js';
|
|
9
|
+
import {
|
|
10
|
+
applyRedlineToOxml,
|
|
11
|
+
reconcileMarkdownTableOoxml,
|
|
12
|
+
applyHighlightToOoxml,
|
|
13
|
+
injectCommentsIntoOoxml,
|
|
14
|
+
getParagraphText as getParagraphTextFromOxml,
|
|
15
|
+
isMarkdownTableText,
|
|
16
|
+
findContainingWordElement,
|
|
17
|
+
resolveTargetParagraphWithSnapshot as resolveTargetParagraphWithSnapshotShared,
|
|
18
|
+
buildSingleLineListStructuralFallbackPlan,
|
|
19
|
+
executeSingleLineListStructuralFallback,
|
|
20
|
+
resolveSingleLineListFallbackNumberingAction,
|
|
21
|
+
recordSingleLineListFallbackExplicitSequence,
|
|
22
|
+
clearSingleLineListFallbackExplicitSequence,
|
|
23
|
+
enforceListBindingOnParagraphNodes,
|
|
24
|
+
synthesizeTableMarkdownFromMultilineCellEdit,
|
|
25
|
+
synthesizeExpandedListScopeEdit,
|
|
26
|
+
planListInsertionOnlyEdit,
|
|
27
|
+
getParagraphListInfo,
|
|
28
|
+
stripRedundantLeadingListMarkers,
|
|
29
|
+
stripSingleLineListMarkerPrefix,
|
|
30
|
+
normalizeWhitespaceForTargeting,
|
|
31
|
+
reserveNextNumberingIdPair,
|
|
32
|
+
remapNumberingPayloadForDocument,
|
|
33
|
+
overwriteParagraphNumIds,
|
|
34
|
+
extractFirstParagraphNumId,
|
|
35
|
+
buildExplicitDecimalMultilevelNumberingXml,
|
|
36
|
+
inferTableReplacementParagraphBlock,
|
|
37
|
+
resolveParagraphRangeByRefs,
|
|
38
|
+
extractReplacementNodesFromOoxml,
|
|
39
|
+
normalizeBodySectionOrderStandalone
|
|
40
|
+
} from '../standalone.js';
|
|
41
|
+
|
|
42
|
+
const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
43
|
+
|
|
44
|
+
function getParagraphText(paragraph) {
|
|
45
|
+
return getParagraphTextFromOxml(paragraph);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeContext = null, options = {}) {
|
|
49
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => {};
|
|
50
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => {};
|
|
51
|
+
return resolveTargetParagraphWithSnapshotShared(xmlDoc, {
|
|
52
|
+
targetText,
|
|
53
|
+
targetRef,
|
|
54
|
+
opType,
|
|
55
|
+
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
56
|
+
onInfo,
|
|
57
|
+
onWarn
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractReplacementNodes(outputOxml) {
|
|
62
|
+
return extractReplacementNodesFromOoxml(outputOxml);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeBodySectionOrder(xmlDoc) {
|
|
66
|
+
normalizeBodySectionOrderStandalone(xmlDoc);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getDirectWordChild(element, localName) {
|
|
70
|
+
if (!element) return null;
|
|
71
|
+
return Array.from(element.childNodes || []).find(
|
|
72
|
+
node => node && node.nodeType === 1 && node.namespaceURI === NS_W && node.localName === localName
|
|
73
|
+
) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function computeTableIndexInDocument(xmlDoc, tableElement) {
|
|
77
|
+
if (!xmlDoc || !tableElement) return null;
|
|
78
|
+
const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'tbl'));
|
|
79
|
+
const idx = tables.indexOf(tableElement);
|
|
80
|
+
return idx >= 0 ? idx + 1 : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeMultilineTableStructuralPayload(text) {
|
|
84
|
+
return String(text || '')
|
|
85
|
+
.replace(/\r\n/g, '\n')
|
|
86
|
+
.split('\n')
|
|
87
|
+
.map(line => line.trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function computeTableStructuralDedupeKey(xmlDoc, containingTable, modifiedText) {
|
|
93
|
+
const tableIndex = computeTableIndexInDocument(xmlDoc, containingTable);
|
|
94
|
+
if (!Number.isInteger(tableIndex) || tableIndex < 1) return null;
|
|
95
|
+
const normalizedPayload = normalizeMultilineTableStructuralPayload(modifiedText);
|
|
96
|
+
if (!normalizedPayload) return null;
|
|
97
|
+
return `table:${tableIndex}|payload:${normalizedPayload}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getNextTrackedChangeId(xmlDoc) {
|
|
101
|
+
let maxId = 999;
|
|
102
|
+
const revisionNodes = [
|
|
103
|
+
...Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'ins')),
|
|
104
|
+
...Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'del'))
|
|
105
|
+
];
|
|
106
|
+
for (const node of revisionNodes) {
|
|
107
|
+
const raw = node.getAttribute('w:id') || node.getAttribute('id') || '';
|
|
108
|
+
const parsed = Number.parseInt(raw, 10);
|
|
109
|
+
if (Number.isFinite(parsed)) maxId = Math.max(maxId, parsed);
|
|
110
|
+
}
|
|
111
|
+
return maxId + 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ensureListProperties(xmlDoc, paragraph, ilvl, numId) {
|
|
115
|
+
let pPr = getDirectWordChild(paragraph, 'pPr');
|
|
116
|
+
if (!pPr) {
|
|
117
|
+
pPr = xmlDoc.createElementNS(NS_W, 'w:pPr');
|
|
118
|
+
paragraph.insertBefore(pPr, paragraph.firstChild);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let numPr = getDirectWordChild(pPr, 'numPr');
|
|
122
|
+
if (!numPr) {
|
|
123
|
+
numPr = xmlDoc.createElementNS(NS_W, 'w:numPr');
|
|
124
|
+
pPr.appendChild(numPr);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let ilvlEl = getDirectWordChild(numPr, 'ilvl');
|
|
128
|
+
if (!ilvlEl) {
|
|
129
|
+
ilvlEl = xmlDoc.createElementNS(NS_W, 'w:ilvl');
|
|
130
|
+
numPr.appendChild(ilvlEl);
|
|
131
|
+
}
|
|
132
|
+
ilvlEl.setAttribute('w:val', String(Math.max(0, Number.parseInt(ilvl, 10) || 0)));
|
|
133
|
+
|
|
134
|
+
let numIdEl = getDirectWordChild(numPr, 'numId');
|
|
135
|
+
if (!numIdEl) {
|
|
136
|
+
numIdEl = xmlDoc.createElementNS(NS_W, 'w:numId');
|
|
137
|
+
numPr.appendChild(numIdEl);
|
|
138
|
+
}
|
|
139
|
+
numIdEl.setAttribute('w:val', String(numId));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionId, author, dateIso, options = {}) {
|
|
143
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
144
|
+
const paragraph = xmlDoc.createElementNS(NS_W, 'w:p');
|
|
145
|
+
|
|
146
|
+
const anchorPPr = getDirectWordChild(anchorParagraph, 'pPr');
|
|
147
|
+
if (anchorPPr) {
|
|
148
|
+
paragraph.appendChild(anchorPPr.cloneNode(true));
|
|
149
|
+
}
|
|
150
|
+
ensureListProperties(xmlDoc, paragraph, entry.ilvl, entry.numId);
|
|
151
|
+
|
|
152
|
+
const run = xmlDoc.createElementNS(NS_W, 'w:r');
|
|
153
|
+
const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
|
|
154
|
+
const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
|
|
155
|
+
if (anchorRunPr) {
|
|
156
|
+
run.appendChild(anchorRunPr.cloneNode(true));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const textNode = xmlDoc.createElementNS(NS_W, 'w:t');
|
|
160
|
+
const safeText = String(entry.text || '').trim();
|
|
161
|
+
if (/^\s|\s$/.test(safeText)) textNode.setAttribute('xml:space', 'preserve');
|
|
162
|
+
textNode.textContent = safeText;
|
|
163
|
+
run.appendChild(textNode);
|
|
164
|
+
if (generateRedlines) {
|
|
165
|
+
const ins = xmlDoc.createElementNS(NS_W, 'w:ins');
|
|
166
|
+
ins.setAttribute('w:id', String(revisionId));
|
|
167
|
+
ins.setAttribute('w:author', author || 'Browser Demo AI');
|
|
168
|
+
ins.setAttribute('w:date', dateIso);
|
|
169
|
+
ins.appendChild(run);
|
|
170
|
+
paragraph.appendChild(ins);
|
|
171
|
+
} else {
|
|
172
|
+
paragraph.appendChild(run);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return paragraph;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function serializeParagraphRangeAsDocument(paragraphs, serializer) {
|
|
179
|
+
const paragraphXml = (paragraphs || [])
|
|
180
|
+
.map(paragraph => serializer.serializeToString(paragraph))
|
|
181
|
+
.join('');
|
|
182
|
+
return `<w:document xmlns:w="${NS_W}"><w:body>${paragraphXml}</w:body></w:document>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const LIST_LINE_REGEX = /^(\s*)((?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|[ivxlcIVXLC]+\.|[-*+\u2022]))\s+(.*)$/;
|
|
186
|
+
const INLINE_LIST_MARKER_REGEX = /(?:^|\s)(?:\d+(?:\.\d+)*\.?|[A-Za-z]\.|[ivxlcIVXLC]+\.)\s+/g;
|
|
187
|
+
|
|
188
|
+
function parseOutlineLevelFromMarker(marker) {
|
|
189
|
+
const normalized = String(marker || '').trim();
|
|
190
|
+
if (!/^\d+(?:\.\d+)+\.?$/.test(normalized)) return null;
|
|
191
|
+
const parts = normalized.replace(/\.$/, '').split('.');
|
|
192
|
+
return Math.max(0, parts.length - 1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseModifiedListLines(modifiedText) {
|
|
196
|
+
const lines = String(modifiedText || '')
|
|
197
|
+
.split(/\r?\n/g)
|
|
198
|
+
.map(line => line.trimEnd())
|
|
199
|
+
.filter(line => line.trim().length > 0);
|
|
200
|
+
if (lines.length < 2) return null;
|
|
201
|
+
|
|
202
|
+
const parsed = [];
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
const markerMatch = line.match(LIST_LINE_REGEX);
|
|
205
|
+
if (!markerMatch) return null;
|
|
206
|
+
const marker = markerMatch[2];
|
|
207
|
+
const markerType = /^[-*+\u2022]$/.test(marker) ? 'bullet' : 'numbered';
|
|
208
|
+
parsed.push({
|
|
209
|
+
marker,
|
|
210
|
+
markerType,
|
|
211
|
+
level: Math.floor((markerMatch[1] || '').length / 2),
|
|
212
|
+
outlineLevel: markerType === 'numbered' ? parseOutlineLevelFromMarker(marker) : null,
|
|
213
|
+
text: stripRedundantLeadingListMarkers(markerMatch[3])
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return parsed.length >= 2 ? parsed : null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildExplicitRangeInsertionEntries(explicitRangeParagraphs, modifiedText) {
|
|
220
|
+
if (!Array.isArray(explicitRangeParagraphs) || explicitRangeParagraphs.length === 0) return null;
|
|
221
|
+
const parsedLines = parseModifiedListLines(modifiedText);
|
|
222
|
+
if (!parsedLines) return null;
|
|
223
|
+
|
|
224
|
+
const originalTexts = explicitRangeParagraphs.map(paragraph =>
|
|
225
|
+
normalizeWhitespaceForTargeting(getParagraphText(paragraph))
|
|
226
|
+
);
|
|
227
|
+
const modifiedTexts = parsedLines.map(item => normalizeWhitespaceForTargeting(item.text));
|
|
228
|
+
if (originalTexts.some(text => !text) || modifiedTexts.some(text => !text)) return null;
|
|
229
|
+
|
|
230
|
+
const listInfos = explicitRangeParagraphs.map(paragraph => getParagraphListInfo(paragraph));
|
|
231
|
+
if (listInfos.some(info => !info || !info.numId)) return null;
|
|
232
|
+
const baselineNumId = String(listInfos[0].numId);
|
|
233
|
+
if (listInfos.some(info => String(info.numId) !== baselineNumId)) return null;
|
|
234
|
+
|
|
235
|
+
const matchedPairs = [];
|
|
236
|
+
let originalIndex = 0;
|
|
237
|
+
for (let modifiedIndex = 0; modifiedIndex < modifiedTexts.length && originalIndex < originalTexts.length; modifiedIndex += 1) {
|
|
238
|
+
if (modifiedTexts[modifiedIndex] === originalTexts[originalIndex]) {
|
|
239
|
+
matchedPairs.push({ originalIndex, modifiedIndex });
|
|
240
|
+
originalIndex += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (originalIndex !== originalTexts.length) return null;
|
|
244
|
+
|
|
245
|
+
const matchedModifiedIndexes = new Set(matchedPairs.map(pair => pair.modifiedIndex));
|
|
246
|
+
const insertedIndexes = [];
|
|
247
|
+
for (let idx = 0; idx < parsedLines.length; idx += 1) {
|
|
248
|
+
if (!matchedModifiedIndexes.has(idx)) insertedIndexes.push(idx);
|
|
249
|
+
}
|
|
250
|
+
if (insertedIndexes.length === 0) return null;
|
|
251
|
+
|
|
252
|
+
const baseIndentLevel = parsedLines[0]?.level || 0;
|
|
253
|
+
return insertedIndexes.map(modifiedIndex => {
|
|
254
|
+
const nextMatch = matchedPairs.find(pair => pair.modifiedIndex > modifiedIndex) || null;
|
|
255
|
+
const prevMatch = [...matchedPairs].reverse().find(pair => pair.modifiedIndex < modifiedIndex) || null;
|
|
256
|
+
const referenceMatch = nextMatch || prevMatch;
|
|
257
|
+
if (!referenceMatch) return null;
|
|
258
|
+
const referenceListInfo = listInfos[referenceMatch.originalIndex] || listInfos[0];
|
|
259
|
+
if (!referenceListInfo) return null;
|
|
260
|
+
|
|
261
|
+
const entry = parsedLines[modifiedIndex];
|
|
262
|
+
const relativeLevel = Math.max(0, entry.level - baseIndentLevel);
|
|
263
|
+
const explicitOutlineLevel = Number.isInteger(entry.outlineLevel) ? entry.outlineLevel : null;
|
|
264
|
+
return {
|
|
265
|
+
text: entry.text,
|
|
266
|
+
markerType: entry.markerType,
|
|
267
|
+
ilvl: explicitOutlineLevel != null
|
|
268
|
+
? explicitOutlineLevel
|
|
269
|
+
: Math.max(0, (referenceListInfo.ilvl || 0) + relativeLevel),
|
|
270
|
+
numId: String(referenceListInfo.numId),
|
|
271
|
+
insertBeforeOriginalIndex: nextMatch ? nextMatch.originalIndex : null
|
|
272
|
+
};
|
|
273
|
+
}).filter(Boolean);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function applyExplicitRangeListInsertions({
|
|
277
|
+
xmlDoc,
|
|
278
|
+
explicitRangeParagraphs,
|
|
279
|
+
insertionEntries,
|
|
280
|
+
generateRedlines,
|
|
281
|
+
author
|
|
282
|
+
}) {
|
|
283
|
+
if (!Array.isArray(explicitRangeParagraphs) || explicitRangeParagraphs.length === 0) return false;
|
|
284
|
+
if (!Array.isArray(insertionEntries) || insertionEntries.length === 0) return false;
|
|
285
|
+
|
|
286
|
+
const parent = explicitRangeParagraphs[0].parentNode;
|
|
287
|
+
if (!parent || explicitRangeParagraphs.some(paragraph => paragraph.parentNode !== parent)) return false;
|
|
288
|
+
|
|
289
|
+
const tailInsertionPoint = explicitRangeParagraphs[explicitRangeParagraphs.length - 1].nextSibling;
|
|
290
|
+
const dateIso = generateRedlines ? new Date().toISOString() : null;
|
|
291
|
+
let revisionId = generateRedlines ? getNextTrackedChangeId(xmlDoc) : null;
|
|
292
|
+
|
|
293
|
+
for (const entry of insertionEntries) {
|
|
294
|
+
const referenceParagraph = entry.insertBeforeOriginalIndex != null
|
|
295
|
+
? explicitRangeParagraphs[entry.insertBeforeOriginalIndex]
|
|
296
|
+
: explicitRangeParagraphs[explicitRangeParagraphs.length - 1];
|
|
297
|
+
if (!referenceParagraph) return false;
|
|
298
|
+
|
|
299
|
+
const listParagraph = buildInsertedListParagraph(
|
|
300
|
+
xmlDoc,
|
|
301
|
+
referenceParagraph,
|
|
302
|
+
{
|
|
303
|
+
ilvl: entry.ilvl,
|
|
304
|
+
markerType: entry.markerType,
|
|
305
|
+
numId: entry.numId,
|
|
306
|
+
text: entry.text
|
|
307
|
+
},
|
|
308
|
+
revisionId,
|
|
309
|
+
author,
|
|
310
|
+
dateIso,
|
|
311
|
+
{ generateRedlines }
|
|
312
|
+
);
|
|
313
|
+
if (generateRedlines) revisionId += 1;
|
|
314
|
+
|
|
315
|
+
if (entry.insertBeforeOriginalIndex != null) {
|
|
316
|
+
parent.insertBefore(listParagraph, referenceParagraph);
|
|
317
|
+
} else {
|
|
318
|
+
parent.insertBefore(listParagraph, tailInsertionPoint);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function countWords(text) {
|
|
327
|
+
return String(text || '')
|
|
328
|
+
.trim()
|
|
329
|
+
.split(/\s+/)
|
|
330
|
+
.filter(Boolean)
|
|
331
|
+
.length;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function hasMultipleInlineListMarkers(text) {
|
|
335
|
+
const source = String(text || '');
|
|
336
|
+
if (!source) return false;
|
|
337
|
+
|
|
338
|
+
let count = 0;
|
|
339
|
+
const regex = new RegExp(INLINE_LIST_MARKER_REGEX.source, INLINE_LIST_MARKER_REGEX.flags);
|
|
340
|
+
while (regex.exec(source)) {
|
|
341
|
+
count += 1;
|
|
342
|
+
if (count >= 2) return true;
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function deriveSingleParagraphListAdjacencyInsertion(currentParagraphText, modifiedText) {
|
|
348
|
+
const rawCurrent = String(currentParagraphText || '').trim();
|
|
349
|
+
const rawModified = String(modifiedText || '').trim();
|
|
350
|
+
if (!rawCurrent || !rawModified || rawModified === rawCurrent) return null;
|
|
351
|
+
if (rawModified.includes('\n')) return null;
|
|
352
|
+
const normalizedCurrent = normalizeWhitespaceForTargeting(rawCurrent);
|
|
353
|
+
|
|
354
|
+
const minWords = 6;
|
|
355
|
+
const sanitizeCandidate = text => stripRedundantLeadingListMarkers(String(text || '').trim()).trim();
|
|
356
|
+
const buildCandidate = (position, text) => {
|
|
357
|
+
const cleanedText = sanitizeCandidate(text);
|
|
358
|
+
const cleanedNormalized = normalizeWhitespaceForTargeting(cleanedText);
|
|
359
|
+
if (!cleanedText || cleanedText === rawCurrent) return null;
|
|
360
|
+
if (countWords(cleanedText) < minWords) return null;
|
|
361
|
+
if (hasMultipleInlineListMarkers(cleanedText)) return null;
|
|
362
|
+
if (normalizedCurrent && cleanedNormalized.includes(normalizedCurrent)) return null;
|
|
363
|
+
return { position, text: cleanedText };
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (rawModified.endsWith(rawCurrent)) {
|
|
367
|
+
const prefix = rawModified.slice(0, rawModified.length - rawCurrent.length);
|
|
368
|
+
const candidate = buildCandidate('before', prefix);
|
|
369
|
+
if (candidate) return candidate;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (rawModified.startsWith(rawCurrent)) {
|
|
373
|
+
const suffix = rawModified.slice(rawCurrent.length);
|
|
374
|
+
const candidate = buildCandidate('after', suffix);
|
|
375
|
+
if (candidate) return candidate;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const normalizedModified = normalizeWhitespaceForTargeting(rawModified);
|
|
379
|
+
if (!normalizedCurrent || normalizedCurrent === normalizedModified) return null;
|
|
380
|
+
|
|
381
|
+
if (normalizedModified.endsWith(normalizedCurrent)) {
|
|
382
|
+
const prefix = normalizedModified.slice(0, normalizedModified.length - normalizedCurrent.length);
|
|
383
|
+
const candidate = buildCandidate('before', prefix);
|
|
384
|
+
if (candidate) return candidate;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (normalizedModified.startsWith(normalizedCurrent)) {
|
|
388
|
+
const suffix = normalizedModified.slice(normalizedCurrent.length);
|
|
389
|
+
const candidate = buildCandidate('after', suffix);
|
|
390
|
+
if (candidate) return candidate;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, modifiedText) {
|
|
397
|
+
const rawCurrent = String(currentParagraphText || '').trim();
|
|
398
|
+
const rawModified = String(modifiedText || '');
|
|
399
|
+
if (!rawCurrent || !rawModified || !rawModified.includes('\n')) return null;
|
|
400
|
+
|
|
401
|
+
const lines = rawModified
|
|
402
|
+
.split(/\r?\n/g)
|
|
403
|
+
.map(line => String(line || '').trim())
|
|
404
|
+
.filter(Boolean);
|
|
405
|
+
if (lines.length < 2) return null;
|
|
406
|
+
|
|
407
|
+
const normalize = value => normalizeWhitespaceForTargeting(String(value || ''));
|
|
408
|
+
const normalizedCurrent = normalize(rawCurrent);
|
|
409
|
+
const normalizedFirst = normalize(lines[0]);
|
|
410
|
+
const normalizedLast = normalize(lines[lines.length - 1]);
|
|
411
|
+
|
|
412
|
+
if (normalizedLast === normalizedCurrent) {
|
|
413
|
+
const paragraphs = lines.slice(0, -1).map(line => String(line || '').trim()).filter(Boolean);
|
|
414
|
+
if (paragraphs.length > 0) {
|
|
415
|
+
return { position: 'before', paragraphs };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (normalizedFirst === normalizedCurrent) {
|
|
420
|
+
const paragraphs = lines.slice(1).map(line => String(line || '').trim()).filter(Boolean);
|
|
421
|
+
if (paragraphs.length > 0) {
|
|
422
|
+
return { position: 'after', paragraphs };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionId, author, dateIso, options = {}) {
|
|
430
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
431
|
+
const paragraph = xmlDoc.createElementNS(NS_W, 'w:p');
|
|
432
|
+
const run = xmlDoc.createElementNS(NS_W, 'w:r');
|
|
433
|
+
const textNode = xmlDoc.createElementNS(NS_W, 'w:t');
|
|
434
|
+
const safeText = String(text || '');
|
|
435
|
+
if (/^\s|\s$/.test(safeText)) textNode.setAttribute('xml:space', 'preserve');
|
|
436
|
+
textNode.textContent = safeText;
|
|
437
|
+
run.appendChild(textNode);
|
|
438
|
+
|
|
439
|
+
if (generateRedlines) {
|
|
440
|
+
const ins = xmlDoc.createElementNS(NS_W, 'w:ins');
|
|
441
|
+
ins.setAttribute('w:id', String(revisionId));
|
|
442
|
+
ins.setAttribute('w:author', author || 'Browser Demo AI');
|
|
443
|
+
ins.setAttribute('w:date', dateIso);
|
|
444
|
+
ins.appendChild(run);
|
|
445
|
+
paragraph.appendChild(ins);
|
|
446
|
+
} else {
|
|
447
|
+
paragraph.appendChild(run);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return paragraph;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
|
|
454
|
+
const paragraph = xmlDoc.createElementNS(NS_W, 'w:p');
|
|
455
|
+
const anchorPPr = getDirectWordChild(anchorParagraph, 'pPr');
|
|
456
|
+
if (anchorPPr) paragraph.appendChild(anchorPPr.cloneNode(true));
|
|
457
|
+
|
|
458
|
+
const run = xmlDoc.createElementNS(NS_W, 'w:r');
|
|
459
|
+
const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
|
|
460
|
+
const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
|
|
461
|
+
if (anchorRunPr) run.appendChild(anchorRunPr.cloneNode(true));
|
|
462
|
+
|
|
463
|
+
const textNode = xmlDoc.createElementNS(NS_W, 'w:t');
|
|
464
|
+
textNode.textContent = '';
|
|
465
|
+
run.appendChild(textNode);
|
|
466
|
+
paragraph.appendChild(run);
|
|
467
|
+
return paragraph;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionId, author, dateIso) {
|
|
471
|
+
const wrappedParagraph = xmlDoc.createElementNS(NS_W, 'w:p');
|
|
472
|
+
const pPr = getDirectWordChild(paragraph, 'pPr');
|
|
473
|
+
if (pPr) wrappedParagraph.appendChild(pPr.cloneNode(true));
|
|
474
|
+
|
|
475
|
+
const ins = xmlDoc.createElementNS(NS_W, 'w:ins');
|
|
476
|
+
ins.setAttribute('w:id', String(revisionId));
|
|
477
|
+
ins.setAttribute('w:author', author || 'Browser Demo AI');
|
|
478
|
+
ins.setAttribute('w:date', dateIso);
|
|
479
|
+
|
|
480
|
+
for (const child of Array.from(paragraph.childNodes || [])) {
|
|
481
|
+
if (child?.nodeType === 1 && child.namespaceURI === NS_W && child.localName === 'pPr') continue;
|
|
482
|
+
ins.appendChild(child.cloneNode(true));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
wrappedParagraph.appendChild(ins);
|
|
486
|
+
return wrappedParagraph;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisionId, author, dateIso, options = {}) {
|
|
490
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
491
|
+
const serializer = createSerializer();
|
|
492
|
+
const templateParagraph = buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph);
|
|
493
|
+
const templateXml = serializer.serializeToString(templateParagraph);
|
|
494
|
+
const markdownResult = await applyRedlineToOxml(
|
|
495
|
+
templateXml,
|
|
496
|
+
'',
|
|
497
|
+
String(text || ''),
|
|
498
|
+
{
|
|
499
|
+
author,
|
|
500
|
+
generateRedlines: false
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
let sourceParagraph = null;
|
|
505
|
+
if (typeof markdownResult?.oxml === 'string') {
|
|
506
|
+
const extracted = extractReplacementNodes(markdownResult.oxml);
|
|
507
|
+
sourceParagraph = (extracted.replacementNodes || []).find(
|
|
508
|
+
node => node && node.nodeType === 1 && node.namespaceURI === NS_W && node.localName === 'p'
|
|
509
|
+
) || null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!sourceParagraph) {
|
|
513
|
+
return buildFallbackInsertedPlainParagraph(
|
|
514
|
+
xmlDoc,
|
|
515
|
+
text,
|
|
516
|
+
revisionId,
|
|
517
|
+
author,
|
|
518
|
+
dateIso,
|
|
519
|
+
{ generateRedlines }
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!generateRedlines) {
|
|
524
|
+
return sourceParagraph;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return wrapParagraphContentInInsertion(xmlDoc, sourceParagraph, revisionId, author, dateIso);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function tryExplicitDecimalHeaderListConversion({
|
|
531
|
+
xmlDoc,
|
|
532
|
+
serializer,
|
|
533
|
+
targetParagraph,
|
|
534
|
+
currentParagraphText,
|
|
535
|
+
modifiedText,
|
|
536
|
+
author,
|
|
537
|
+
runtimeContext,
|
|
538
|
+
generateRedlines = true,
|
|
539
|
+
onInfo = () => {}
|
|
540
|
+
}) {
|
|
541
|
+
if (!targetParagraph) return null;
|
|
542
|
+
const scopedParagraphOxml = serializer.serializeToString(targetParagraph);
|
|
543
|
+
const explicitPlan = buildSingleLineListStructuralFallbackPlan({
|
|
544
|
+
oxml: scopedParagraphOxml,
|
|
545
|
+
originalText: currentParagraphText,
|
|
546
|
+
modifiedText,
|
|
547
|
+
allowExistingList: false
|
|
548
|
+
});
|
|
549
|
+
if (
|
|
550
|
+
!explicitPlan ||
|
|
551
|
+
explicitPlan.numberingKey !== 'numbered:decimal:single' ||
|
|
552
|
+
!Number.isInteger(explicitPlan.startAt) ||
|
|
553
|
+
explicitPlan.startAt < 1
|
|
554
|
+
) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const strippedContent = stripSingleLineListMarkerPrefix(explicitPlan.listInput || modifiedText);
|
|
559
|
+
if (!strippedContent) return null;
|
|
560
|
+
|
|
561
|
+
onInfo('[List] Applying explicit numeric header conversion with direct list binding.');
|
|
562
|
+
const redlineResult = await applyRedlineToOxml(
|
|
563
|
+
serializer.serializeToString(targetParagraph),
|
|
564
|
+
currentParagraphText,
|
|
565
|
+
strippedContent,
|
|
566
|
+
{
|
|
567
|
+
author,
|
|
568
|
+
generateRedlines
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
if (!redlineResult?.hasChanges || typeof redlineResult?.oxml !== 'string') return null;
|
|
572
|
+
|
|
573
|
+
const extracted = extractReplacementNodes(redlineResult.oxml);
|
|
574
|
+
const replacementNodes = extracted.replacementNodes;
|
|
575
|
+
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
576
|
+
explicitPlan,
|
|
577
|
+
runtimeContext?.listFallbackSequenceState || null
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
const explicitStart = explicitPlan.startAt;
|
|
581
|
+
const numberingState = runtimeContext?.numberingIdState || null;
|
|
582
|
+
let appliedNumId = null;
|
|
583
|
+
let numberingXml = null;
|
|
584
|
+
|
|
585
|
+
if (numberingAction.type === 'explicitReuse' && numberingAction.numId) {
|
|
586
|
+
appliedNumId = String(numberingAction.numId);
|
|
587
|
+
onInfo(`[List] Reusing explicit-start list sequence (${numberingAction.numberingKey} -> numId ${appliedNumId}, next ${explicitStart + 1}).`);
|
|
588
|
+
} else {
|
|
589
|
+
const reservedPair = reserveNextNumberingIdPair(numberingState);
|
|
590
|
+
if (!reservedPair) return null;
|
|
591
|
+
|
|
592
|
+
appliedNumId = String(reservedPair.numId);
|
|
593
|
+
numberingXml = buildExplicitDecimalMultilevelNumberingXml(
|
|
594
|
+
reservedPair.numId,
|
|
595
|
+
reservedPair.abstractNumId,
|
|
596
|
+
explicitStart
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
if (numberingAction.type === 'explicitStartNew') {
|
|
600
|
+
onInfo(`[List] Started explicit-start list sequence (${numberingAction.numberingKey} -> numId ${appliedNumId}).`);
|
|
601
|
+
}
|
|
602
|
+
onInfo(`[List] Using isolated explicit-start numbering (start ${explicitStart}, numId ${appliedNumId}, abstractNumId ${reservedPair.abstractNumId}).`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (explicitPlan.numberingKey && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
606
|
+
runtimeContext.listFallbackSharedNumIdByKey.delete(explicitPlan.numberingKey);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (numberingAction.type === 'explicitStartNew' || numberingAction.type === 'explicitReuse') {
|
|
610
|
+
recordSingleLineListFallbackExplicitSequence(
|
|
611
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
612
|
+
numberingAction.numberingKey || explicitPlan.numberingKey,
|
|
613
|
+
appliedNumId,
|
|
614
|
+
explicitStart
|
|
615
|
+
);
|
|
616
|
+
} else {
|
|
617
|
+
clearSingleLineListFallbackExplicitSequence(
|
|
618
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
619
|
+
numberingAction.numberingKey || explicitPlan.numberingKey
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
enforceListBindingOnParagraphNodes(replacementNodes, {
|
|
624
|
+
numId: appliedNumId,
|
|
625
|
+
ilvl: 0,
|
|
626
|
+
clearParagraphPropertyChanges: true,
|
|
627
|
+
removeListPropertyNode: true
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const parent = targetParagraph.parentNode;
|
|
631
|
+
if (!parent) return null;
|
|
632
|
+
for (const node of replacementNodes) parent.insertBefore(xmlDoc.importNode(node, true), targetParagraph);
|
|
633
|
+
parent.removeChild(targetParagraph);
|
|
634
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
635
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function trySingleParagraphListStructuralFallback({
|
|
639
|
+
xmlDoc,
|
|
640
|
+
serializer,
|
|
641
|
+
targetParagraph,
|
|
642
|
+
currentParagraphText,
|
|
643
|
+
modifiedText,
|
|
644
|
+
author,
|
|
645
|
+
runtimeContext,
|
|
646
|
+
generateRedlines = true,
|
|
647
|
+
onInfo = () => {}
|
|
648
|
+
}) {
|
|
649
|
+
if (!targetParagraph) return null;
|
|
650
|
+
|
|
651
|
+
const scopedParagraphOxml = serializer.serializeToString(targetParagraph);
|
|
652
|
+
const fallbackPlan = buildSingleLineListStructuralFallbackPlan({
|
|
653
|
+
oxml: scopedParagraphOxml,
|
|
654
|
+
originalText: currentParagraphText,
|
|
655
|
+
modifiedText,
|
|
656
|
+
allowExistingList: false
|
|
657
|
+
});
|
|
658
|
+
if (!fallbackPlan) return null;
|
|
659
|
+
|
|
660
|
+
onInfo('[List] No textual diff but list marker detected; forcing structural list conversion fallback.');
|
|
661
|
+
const fallbackResult = await executeSingleLineListStructuralFallback(fallbackPlan, {
|
|
662
|
+
author,
|
|
663
|
+
generateRedlines,
|
|
664
|
+
setAbstractStartOverride: false
|
|
665
|
+
});
|
|
666
|
+
if (!fallbackResult?.hasChanges || !fallbackResult?.oxml) {
|
|
667
|
+
onInfo('[List] Structural list fallback produced no valid OOXML payload.');
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const extracted = extractReplacementNodes(fallbackResult.oxml);
|
|
672
|
+
let replacementNodes = extracted.replacementNodes;
|
|
673
|
+
let numberingXml = extracted.numberingXml || fallbackResult?.numberingXml || null;
|
|
674
|
+
const hasExplicitStartAt = Number.isInteger(fallbackPlan?.startAt) && fallbackPlan.startAt > 0;
|
|
675
|
+
const numberingKey = fallbackResult?.listStructuralFallbackKey || fallbackPlan?.numberingKey || null;
|
|
676
|
+
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
677
|
+
fallbackPlan,
|
|
678
|
+
runtimeContext?.listFallbackSequenceState || null
|
|
679
|
+
);
|
|
680
|
+
if (hasExplicitStartAt) {
|
|
681
|
+
const explicitStart = fallbackPlan.startAt;
|
|
682
|
+
let explicitNumIdForBinding = null;
|
|
683
|
+
const numberingState = runtimeContext?.numberingIdState || null;
|
|
684
|
+
if (numberingAction.type === 'explicitReuse' && numberingAction.numId) {
|
|
685
|
+
explicitNumIdForBinding = String(numberingAction.numId);
|
|
686
|
+
numberingXml = null;
|
|
687
|
+
onInfo(`[List] Reusing explicit-start list sequence (${numberingAction.numberingKey} -> numId ${explicitNumIdForBinding}, next ${explicitStart + 1}).`);
|
|
688
|
+
} else if (numberingState) {
|
|
689
|
+
const reservedPair = reserveNextNumberingIdPair(numberingState);
|
|
690
|
+
if (!reservedPair) return null;
|
|
691
|
+
overwriteParagraphNumIds(replacementNodes, reservedPair.numId);
|
|
692
|
+
explicitNumIdForBinding = String(reservedPair.numId);
|
|
693
|
+
numberingXml = buildExplicitDecimalMultilevelNumberingXml(
|
|
694
|
+
reservedPair.numId,
|
|
695
|
+
reservedPair.abstractNumId,
|
|
696
|
+
explicitStart
|
|
697
|
+
);
|
|
698
|
+
if (numberingAction.type === 'explicitStartNew') {
|
|
699
|
+
onInfo(`[List] Started explicit-start list sequence (${numberingAction.numberingKey} -> numId ${reservedPair.numId}).`);
|
|
700
|
+
}
|
|
701
|
+
onInfo(`[List] Using isolated explicit-start numbering (start ${explicitStart}, numId ${reservedPair.numId}, abstractNumId ${reservedPair.abstractNumId}).`);
|
|
702
|
+
} else {
|
|
703
|
+
const generatedNumId = extractFirstParagraphNumId(replacementNodes);
|
|
704
|
+
explicitNumIdForBinding = generatedNumId ? String(generatedNumId) : null;
|
|
705
|
+
onInfo(`[List] Using isolated list numbering with explicit start ${explicitStart}${generatedNumId ? ` (numId ${generatedNumId})` : ''}.`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (numberingKey && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
709
|
+
runtimeContext.listFallbackSharedNumIdByKey.delete(numberingKey);
|
|
710
|
+
}
|
|
711
|
+
if (numberingAction.type === 'explicitStartNew' || numberingAction.type === 'explicitReuse') {
|
|
712
|
+
recordSingleLineListFallbackExplicitSequence(
|
|
713
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
714
|
+
numberingAction.numberingKey || numberingKey,
|
|
715
|
+
explicitNumIdForBinding,
|
|
716
|
+
explicitStart
|
|
717
|
+
);
|
|
718
|
+
} else {
|
|
719
|
+
clearSingleLineListFallbackExplicitSequence(
|
|
720
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
721
|
+
numberingAction.numberingKey || numberingKey
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (explicitNumIdForBinding) {
|
|
726
|
+
enforceListBindingOnParagraphNodes(replacementNodes, {
|
|
727
|
+
numId: explicitNumIdForBinding,
|
|
728
|
+
ilvl: 0,
|
|
729
|
+
clearParagraphPropertyChanges: true,
|
|
730
|
+
removeListPropertyNode: true
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
if (numberingXml && runtimeContext?.numberingIdState) {
|
|
735
|
+
const normalizedNumbering = remapNumberingPayloadForDocument(numberingXml, replacementNodes, runtimeContext.numberingIdState);
|
|
736
|
+
replacementNodes = normalizedNumbering.replacementNodes;
|
|
737
|
+
numberingXml = normalizedNumbering.numberingXml;
|
|
738
|
+
}
|
|
739
|
+
clearSingleLineListFallbackExplicitSequence(
|
|
740
|
+
runtimeContext?.listFallbackSequenceState || null,
|
|
741
|
+
numberingAction.numberingKey || numberingKey
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (!hasExplicitStartAt && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
746
|
+
const sharedNumId = numberingKey ? runtimeContext.listFallbackSharedNumIdByKey.get(numberingKey) : null;
|
|
747
|
+
if (sharedNumId) {
|
|
748
|
+
overwriteParagraphNumIds(replacementNodes, sharedNumId);
|
|
749
|
+
numberingXml = null;
|
|
750
|
+
onInfo(`[List] Reusing shared list numbering (${numberingKey} -> numId ${sharedNumId}).`);
|
|
751
|
+
} else if (numberingKey) {
|
|
752
|
+
const generatedNumId = extractFirstParagraphNumId(replacementNodes);
|
|
753
|
+
if (generatedNumId) {
|
|
754
|
+
runtimeContext.listFallbackSharedNumIdByKey.set(numberingKey, generatedNumId);
|
|
755
|
+
onInfo(`[List] Captured shared list numbering (${numberingKey} -> numId ${generatedNumId}).`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const parent = targetParagraph.parentNode;
|
|
761
|
+
if (!parent) return null;
|
|
762
|
+
for (const node of replacementNodes) parent.insertBefore(xmlDoc.importNode(node, true), targetParagraph);
|
|
763
|
+
parent.removeChild(targetParagraph);
|
|
764
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
765
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function applyToParagraphByExactText(documentXml, targetText, modifiedText, author, targetRef = null, targetEndRef = null, runtimeContext = null, options = {}) {
|
|
769
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
770
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => {};
|
|
771
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => {};
|
|
772
|
+
const parser = createParser();
|
|
773
|
+
const serializer = createSerializer();
|
|
774
|
+
const xmlDoc = parser.parseFromString(documentXml, 'application/xml');
|
|
775
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'redline', runtimeContext, { onInfo, onWarn });
|
|
776
|
+
const targetParagraph = resolved.paragraph;
|
|
777
|
+
const currentParagraphText = getParagraphText(targetParagraph).trim();
|
|
778
|
+
const containingTable = findContainingWordElement(targetParagraph, 'tbl');
|
|
779
|
+
const rawTableStructuralCandidate = !!containingTable
|
|
780
|
+
&& !targetEndRef
|
|
781
|
+
&& typeof modifiedText === 'string'
|
|
782
|
+
&& modifiedText.includes('\n')
|
|
783
|
+
&& !isMarkdownTableText(modifiedText);
|
|
784
|
+
const rawTableStructuralDedupeKey = rawTableStructuralCandidate
|
|
785
|
+
? computeTableStructuralDedupeKey(xmlDoc, containingTable, modifiedText)
|
|
786
|
+
: null;
|
|
787
|
+
const tableStructuralDedupes = runtimeContext?.tableStructuralRedlineKeys instanceof Set
|
|
788
|
+
? runtimeContext.tableStructuralRedlineKeys
|
|
789
|
+
: null;
|
|
790
|
+
if (rawTableStructuralDedupeKey && tableStructuralDedupes?.has(rawTableStructuralDedupeKey)) {
|
|
791
|
+
onInfo('[Table] Skipping duplicate table-structural redline for the same table/payload in this turn.');
|
|
792
|
+
return {
|
|
793
|
+
documentXml,
|
|
794
|
+
hasChanges: false,
|
|
795
|
+
numberingXml: null,
|
|
796
|
+
warnings: ['Skipped duplicate table-structural redline in the same turn.']
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const synthesizedTableMarkdown = containingTable
|
|
800
|
+
? synthesizeTableMarkdownFromMultilineCellEdit(targetParagraph, modifiedText, {
|
|
801
|
+
tableElement: containingTable,
|
|
802
|
+
currentParagraphText,
|
|
803
|
+
onInfo,
|
|
804
|
+
onWarn
|
|
805
|
+
})
|
|
806
|
+
: null;
|
|
807
|
+
let effectiveModifiedText = synthesizedTableMarkdown || modifiedText;
|
|
808
|
+
const useTableScope = !!containingTable && isMarkdownTableText(effectiveModifiedText);
|
|
809
|
+
const isTableMarkdownEdit = isMarkdownTableText(effectiveModifiedText);
|
|
810
|
+
const explicitRangeParagraphs = targetEndRef
|
|
811
|
+
? resolveParagraphRangeByRefs(xmlDoc, targetRef, targetEndRef, {
|
|
812
|
+
opType: 'redline',
|
|
813
|
+
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
814
|
+
onInfo,
|
|
815
|
+
onWarn
|
|
816
|
+
})
|
|
817
|
+
: null;
|
|
818
|
+
const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
|
|
819
|
+
if (!useTableScope && hasExplicitRangeScope) {
|
|
820
|
+
const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
|
|
821
|
+
if (insertionEntries && insertionEntries.length > 0) {
|
|
822
|
+
onInfo(`[List] Applying explicit-range insertion-only heuristic (${insertionEntries.length} new item(s)).`);
|
|
823
|
+
for (const entry of insertionEntries) {
|
|
824
|
+
onInfo(`[List] Explicit-range insertion: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
825
|
+
}
|
|
826
|
+
const applied = applyExplicitRangeListInsertions({
|
|
827
|
+
xmlDoc,
|
|
828
|
+
explicitRangeParagraphs,
|
|
829
|
+
insertionEntries,
|
|
830
|
+
generateRedlines,
|
|
831
|
+
author
|
|
832
|
+
});
|
|
833
|
+
if (applied) {
|
|
834
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml: null };
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
let inferredTableRangeParagraphs = null;
|
|
839
|
+
if (!explicitRangeParagraphs && !useTableScope && isTableMarkdownEdit) {
|
|
840
|
+
inferredTableRangeParagraphs = inferTableReplacementParagraphBlock(targetParagraph, {
|
|
841
|
+
getParagraphText
|
|
842
|
+
});
|
|
843
|
+
if (inferredTableRangeParagraphs?.length > 1) {
|
|
844
|
+
onInfo(`[Table] Heuristic range expansion selected ${inferredTableRangeParagraphs.length} paragraph(s) for replacement.`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const targetListInfo = getParagraphListInfo(targetParagraph);
|
|
848
|
+
if (
|
|
849
|
+
targetListInfo &&
|
|
850
|
+
typeof effectiveModifiedText === 'string' &&
|
|
851
|
+
!effectiveModifiedText.includes('\n')
|
|
852
|
+
) {
|
|
853
|
+
const strippedListPrefix = stripRedundantLeadingListMarkers(effectiveModifiedText);
|
|
854
|
+
if (strippedListPrefix && strippedListPrefix !== effectiveModifiedText.trim()) {
|
|
855
|
+
onInfo('[List] Stripped redundant manual list marker prefix from single-line list item edit.');
|
|
856
|
+
effectiveModifiedText = strippedListPrefix;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (useTableScope) {
|
|
860
|
+
onInfo('[Table] Markdown table edit detected in table cell target; applying reconciliation at table scope.');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const adjacencyInsertionCandidate = (!useTableScope && !hasExplicitRangeScope && targetListInfo)
|
|
864
|
+
? deriveSingleParagraphListAdjacencyInsertion(currentParagraphText, effectiveModifiedText)
|
|
865
|
+
: null;
|
|
866
|
+
if (adjacencyInsertionCandidate) {
|
|
867
|
+
onInfo(`[List] Applying single-paragraph list adjacency insertion heuristic (${adjacencyInsertionCandidate.position}).`);
|
|
868
|
+
const parent = targetParagraph.parentNode;
|
|
869
|
+
if (!parent) throw new Error('Target paragraph has no parent for adjacency list insertion');
|
|
870
|
+
|
|
871
|
+
const dateIso = generateRedlines ? new Date().toISOString() : null;
|
|
872
|
+
const revisionId = generateRedlines ? getNextTrackedChangeId(xmlDoc) : null;
|
|
873
|
+
const listParagraph = buildInsertedListParagraph(
|
|
874
|
+
xmlDoc,
|
|
875
|
+
targetParagraph,
|
|
876
|
+
{
|
|
877
|
+
ilvl: targetListInfo.ilvl,
|
|
878
|
+
numId: targetListInfo.numId,
|
|
879
|
+
markerType: 'numbered',
|
|
880
|
+
text: adjacencyInsertionCandidate.text
|
|
881
|
+
},
|
|
882
|
+
revisionId,
|
|
883
|
+
author,
|
|
884
|
+
dateIso,
|
|
885
|
+
{ generateRedlines }
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
const insertionPoint = adjacencyInsertionCandidate.position === 'before'
|
|
889
|
+
? targetParagraph
|
|
890
|
+
: targetParagraph.nextSibling;
|
|
891
|
+
parent.insertBefore(listParagraph, insertionPoint);
|
|
892
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
893
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml: null };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const plainAdjacencyInsertionCandidate = (!useTableScope && !hasExplicitRangeScope && !targetListInfo)
|
|
897
|
+
? deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, effectiveModifiedText)
|
|
898
|
+
: null;
|
|
899
|
+
if (plainAdjacencyInsertionCandidate) {
|
|
900
|
+
onInfo(
|
|
901
|
+
`[Text] Applying single-paragraph plain adjacency insertion heuristic `
|
|
902
|
+
+ `(${plainAdjacencyInsertionCandidate.position}, count=${plainAdjacencyInsertionCandidate.paragraphs.length}).`
|
|
903
|
+
);
|
|
904
|
+
const parent = targetParagraph.parentNode;
|
|
905
|
+
if (!parent) throw new Error('Target paragraph has no parent for plain adjacency insertion');
|
|
906
|
+
|
|
907
|
+
const dateIso = generateRedlines ? new Date().toISOString() : null;
|
|
908
|
+
let revisionId = generateRedlines ? getNextTrackedChangeId(xmlDoc) : null;
|
|
909
|
+
const insertionPoint = plainAdjacencyInsertionCandidate.position === 'before'
|
|
910
|
+
? targetParagraph
|
|
911
|
+
: targetParagraph.nextSibling;
|
|
912
|
+
|
|
913
|
+
for (const paragraphText of plainAdjacencyInsertionCandidate.paragraphs) {
|
|
914
|
+
const plainParagraph = await buildInsertedPlainParagraph(
|
|
915
|
+
xmlDoc,
|
|
916
|
+
targetParagraph,
|
|
917
|
+
paragraphText,
|
|
918
|
+
revisionId,
|
|
919
|
+
author,
|
|
920
|
+
dateIso,
|
|
921
|
+
{ generateRedlines }
|
|
922
|
+
);
|
|
923
|
+
parent.insertBefore(xmlDoc.importNode(plainParagraph, true), insertionPoint);
|
|
924
|
+
if (generateRedlines) revisionId += 1;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
928
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml: null };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const insertionOnlyPlan = (!useTableScope && !hasExplicitRangeScope)
|
|
932
|
+
? planListInsertionOnlyEdit(targetParagraph, effectiveModifiedText, {
|
|
933
|
+
currentParagraphText,
|
|
934
|
+
onInfo,
|
|
935
|
+
onWarn
|
|
936
|
+
})
|
|
937
|
+
: null;
|
|
938
|
+
if (insertionOnlyPlan && insertionOnlyPlan.entries.length > 0) {
|
|
939
|
+
onInfo(`[List] Applying insertion-only list redline heuristic (${insertionOnlyPlan.entries.length} new item(s)).`);
|
|
940
|
+
for (const entry of insertionOnlyPlan.entries) {
|
|
941
|
+
onInfo(`[List] Insertion entry resolved: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
942
|
+
}
|
|
943
|
+
const parent = targetParagraph.parentNode;
|
|
944
|
+
if (!parent) throw new Error('Target paragraph has no parent for list insertion');
|
|
945
|
+
const insertionPoint = targetParagraph.nextSibling;
|
|
946
|
+
const dateIso = generateRedlines ? new Date().toISOString() : null;
|
|
947
|
+
let revisionId = generateRedlines ? getNextTrackedChangeId(xmlDoc) : null;
|
|
948
|
+
for (const entry of insertionOnlyPlan.entries) {
|
|
949
|
+
const listParagraph = buildInsertedListParagraph(
|
|
950
|
+
xmlDoc,
|
|
951
|
+
targetParagraph,
|
|
952
|
+
{ ...entry, numId: insertionOnlyPlan.numId },
|
|
953
|
+
revisionId,
|
|
954
|
+
author,
|
|
955
|
+
dateIso,
|
|
956
|
+
{ generateRedlines }
|
|
957
|
+
);
|
|
958
|
+
if (generateRedlines) revisionId += 1;
|
|
959
|
+
parent.insertBefore(listParagraph, insertionPoint);
|
|
960
|
+
}
|
|
961
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
962
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml: null };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const listScopeEdit = (!useTableScope && !hasExplicitRangeScope)
|
|
966
|
+
? synthesizeExpandedListScopeEdit(targetParagraph, effectiveModifiedText, {
|
|
967
|
+
currentParagraphText,
|
|
968
|
+
onInfo,
|
|
969
|
+
onWarn
|
|
970
|
+
})
|
|
971
|
+
: null;
|
|
972
|
+
const useListScope = !!listScopeEdit && Array.isArray(listScopeEdit.paragraphs) && listScopeEdit.paragraphs.length > 0;
|
|
973
|
+
if (useListScope) {
|
|
974
|
+
effectiveModifiedText = listScopeEdit.modifiedText;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (!useTableScope && !useListScope && !hasExplicitRangeScope) {
|
|
978
|
+
const explicitHeaderListConversion = await tryExplicitDecimalHeaderListConversion({
|
|
979
|
+
xmlDoc,
|
|
980
|
+
serializer,
|
|
981
|
+
targetParagraph,
|
|
982
|
+
currentParagraphText,
|
|
983
|
+
modifiedText: effectiveModifiedText,
|
|
984
|
+
author,
|
|
985
|
+
runtimeContext,
|
|
986
|
+
generateRedlines,
|
|
987
|
+
onInfo
|
|
988
|
+
});
|
|
989
|
+
if (explicitHeaderListConversion) return explicitHeaderListConversion;
|
|
990
|
+
|
|
991
|
+
const listFallback = await trySingleParagraphListStructuralFallback({
|
|
992
|
+
xmlDoc,
|
|
993
|
+
serializer,
|
|
994
|
+
targetParagraph,
|
|
995
|
+
currentParagraphText,
|
|
996
|
+
modifiedText: effectiveModifiedText,
|
|
997
|
+
author,
|
|
998
|
+
runtimeContext,
|
|
999
|
+
generateRedlines,
|
|
1000
|
+
onInfo
|
|
1001
|
+
});
|
|
1002
|
+
if (listFallback) return listFallback;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const originalTextForApply = useListScope
|
|
1006
|
+
? listScopeEdit.originalText
|
|
1007
|
+
: (
|
|
1008
|
+
explicitRangeParagraphs
|
|
1009
|
+
? explicitRangeParagraphs.map(paragraph => getParagraphText(paragraph)).join('\n')
|
|
1010
|
+
: (inferredTableRangeParagraphs
|
|
1011
|
+
? inferredTableRangeParagraphs.map(paragraph => getParagraphText(paragraph)).join('\n')
|
|
1012
|
+
: (currentParagraphText || targetText))
|
|
1013
|
+
);
|
|
1014
|
+
const scopedXml = useTableScope
|
|
1015
|
+
? serializer.serializeToString(containingTable)
|
|
1016
|
+
: (
|
|
1017
|
+
useListScope
|
|
1018
|
+
? serializeParagraphRangeAsDocument(listScopeEdit.paragraphs, serializer)
|
|
1019
|
+
: (
|
|
1020
|
+
explicitRangeParagraphs
|
|
1021
|
+
? serializeParagraphRangeAsDocument(explicitRangeParagraphs, serializer)
|
|
1022
|
+
: (inferredTableRangeParagraphs
|
|
1023
|
+
? serializeParagraphRangeAsDocument(inferredTableRangeParagraphs, serializer)
|
|
1024
|
+
: serializer.serializeToString(targetParagraph))
|
|
1025
|
+
)
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
const result = isTableMarkdownEdit
|
|
1029
|
+
? await reconcileMarkdownTableOoxml(scopedXml, originalTextForApply, effectiveModifiedText, {
|
|
1030
|
+
author,
|
|
1031
|
+
generateRedlines,
|
|
1032
|
+
_isolatedTableCell: useTableScope
|
|
1033
|
+
})
|
|
1034
|
+
: await applyRedlineToOxml(scopedXml, originalTextForApply, effectiveModifiedText, {
|
|
1035
|
+
author,
|
|
1036
|
+
generateRedlines,
|
|
1037
|
+
_isolatedTableCell: useTableScope
|
|
1038
|
+
});
|
|
1039
|
+
if (!result?.hasChanges) return { documentXml, hasChanges: false, numberingXml: null };
|
|
1040
|
+
if (result.useNativeApi && !result.oxml) {
|
|
1041
|
+
const warning = 'Format-only fallback requires native Word API; browser demo skipped this operation.';
|
|
1042
|
+
onWarn(`[WARN] ${warning}`);
|
|
1043
|
+
return { documentXml, hasChanges: false, numberingXml: null, warnings: [warning] };
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof result.oxml !== 'string') {
|
|
1046
|
+
throw new Error('Reconciliation engine did not return OOXML for a changed redline operation');
|
|
1047
|
+
}
|
|
1048
|
+
const extracted = extractReplacementNodes(result.oxml);
|
|
1049
|
+
let replacementNodes = extracted.replacementNodes;
|
|
1050
|
+
let numberingXml = extracted.numberingXml;
|
|
1051
|
+
if (numberingXml && runtimeContext?.numberingIdState) {
|
|
1052
|
+
const normalizedNumbering = remapNumberingPayloadForDocument(numberingXml, replacementNodes, runtimeContext.numberingIdState);
|
|
1053
|
+
replacementNodes = normalizedNumbering.replacementNodes;
|
|
1054
|
+
numberingXml = normalizedNumbering.numberingXml;
|
|
1055
|
+
}
|
|
1056
|
+
const scopeNodes = useTableScope
|
|
1057
|
+
? [containingTable]
|
|
1058
|
+
: (
|
|
1059
|
+
useListScope
|
|
1060
|
+
? listScopeEdit.paragraphs
|
|
1061
|
+
: (
|
|
1062
|
+
explicitRangeParagraphs
|
|
1063
|
+
? explicitRangeParagraphs
|
|
1064
|
+
: (inferredTableRangeParagraphs || [targetParagraph])
|
|
1065
|
+
)
|
|
1066
|
+
);
|
|
1067
|
+
const anchorNode = scopeNodes[0];
|
|
1068
|
+
const parent = anchorNode.parentNode;
|
|
1069
|
+
for (const node of replacementNodes) parent.insertBefore(xmlDoc.importNode(node, true), anchorNode);
|
|
1070
|
+
for (const scopeNode of scopeNodes) {
|
|
1071
|
+
if (scopeNode && scopeNode.parentNode === parent) parent.removeChild(scopeNode);
|
|
1072
|
+
}
|
|
1073
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1074
|
+
if (rawTableStructuralDedupeKey && tableStructuralDedupes && (useTableScope || containingTable)) {
|
|
1075
|
+
tableStructuralDedupes.add(rawTableStructuralDedupeKey);
|
|
1076
|
+
}
|
|
1077
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, numberingXml };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function applyHighlightToParagraphByExactText(documentXml, targetText, textToHighlight, color, author, targetRef = null, runtimeContext = null, options = {}) {
|
|
1081
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
1082
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => {};
|
|
1083
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => {};
|
|
1084
|
+
const parser = createParser();
|
|
1085
|
+
const serializer = createSerializer();
|
|
1086
|
+
const xmlDoc = parser.parseFromString(documentXml, 'application/xml');
|
|
1087
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'highlight', runtimeContext, { onInfo, onWarn });
|
|
1088
|
+
const targetParagraph = resolved.paragraph;
|
|
1089
|
+
const paragraphXml = serializer.serializeToString(targetParagraph);
|
|
1090
|
+
const highlightedXml = applyHighlightToOoxml(paragraphXml, textToHighlight, color, { generateRedlines, author });
|
|
1091
|
+
if (!highlightedXml || highlightedXml === paragraphXml) return { documentXml, hasChanges: false };
|
|
1092
|
+
const { replacementNodes } = extractReplacementNodes(highlightedXml);
|
|
1093
|
+
const parent = targetParagraph.parentNode;
|
|
1094
|
+
for (const node of replacementNodes) parent.insertBefore(xmlDoc.importNode(node, true), targetParagraph);
|
|
1095
|
+
parent.removeChild(targetParagraph);
|
|
1096
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1097
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async function applyCommentToParagraphByExactText(documentXml, targetText, textToComment, commentContent, author, targetRef = null, runtimeContext = null, options = {}) {
|
|
1101
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => {};
|
|
1102
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => {};
|
|
1103
|
+
const parser = createParser();
|
|
1104
|
+
const serializer = createSerializer();
|
|
1105
|
+
const xmlDoc = parser.parseFromString(documentXml, 'application/xml');
|
|
1106
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'comment', runtimeContext, { onInfo, onWarn });
|
|
1107
|
+
const targetParagraph = resolved.paragraph;
|
|
1108
|
+
const paragraphXml = serializer.serializeToString(targetParagraph);
|
|
1109
|
+
const commentResult = injectCommentsIntoOoxml(paragraphXml, [{ paragraphIndex: 1, textToFind: textToComment, commentContent }], { author });
|
|
1110
|
+
if (!commentResult.commentsApplied) return { documentXml, hasChanges: false, commentsXml: null, warnings: commentResult.warnings || [] };
|
|
1111
|
+
const { replacementNodes } = extractReplacementNodes(commentResult.oxml);
|
|
1112
|
+
const parent = targetParagraph.parentNode;
|
|
1113
|
+
for (const node of replacementNodes) parent.insertBefore(xmlDoc.importNode(node, true), targetParagraph);
|
|
1114
|
+
parent.removeChild(targetParagraph);
|
|
1115
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1116
|
+
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, commentsXml: commentResult.commentsXml || null, warnings: commentResult.warnings || [] };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Applies one structured operation (`redline`, `highlight`, or `comment`) to
|
|
1121
|
+
* a full `word/document.xml` payload.
|
|
1122
|
+
*
|
|
1123
|
+
* @param {string} documentXml
|
|
1124
|
+
* @param {Object} op
|
|
1125
|
+
* @param {string} author
|
|
1126
|
+
* @param {Object|null} [runtimeContext=null]
|
|
1127
|
+
* @param {{
|
|
1128
|
+
* generateRedlines?: boolean,
|
|
1129
|
+
* onInfo?: (message: string) => void,
|
|
1130
|
+
* onWarn?: (message: string) => void
|
|
1131
|
+
* }} [options={}]
|
|
1132
|
+
* @returns {Promise<{ documentXml: string, hasChanges: boolean, numberingXml?: string|null, commentsXml?: string|null, warnings?: string[] }>}
|
|
1133
|
+
*/
|
|
1134
|
+
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
1135
|
+
if (op?.type === 'highlight') {
|
|
1136
|
+
return applyHighlightToParagraphByExactText(
|
|
1137
|
+
documentXml,
|
|
1138
|
+
op.target,
|
|
1139
|
+
op.textToHighlight,
|
|
1140
|
+
op.color,
|
|
1141
|
+
author,
|
|
1142
|
+
op.targetRef,
|
|
1143
|
+
runtimeContext,
|
|
1144
|
+
options
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
if (op?.type === 'comment') {
|
|
1148
|
+
return applyCommentToParagraphByExactText(
|
|
1149
|
+
documentXml,
|
|
1150
|
+
op.target,
|
|
1151
|
+
op.textToComment,
|
|
1152
|
+
op.commentContent,
|
|
1153
|
+
author,
|
|
1154
|
+
op.targetRef,
|
|
1155
|
+
runtimeContext,
|
|
1156
|
+
options
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
return applyToParagraphByExactText(
|
|
1160
|
+
documentXml,
|
|
1161
|
+
op?.target,
|
|
1162
|
+
op?.modified,
|
|
1163
|
+
author,
|
|
1164
|
+
op?.targetRef,
|
|
1165
|
+
op?.targetEndRef,
|
|
1166
|
+
runtimeContext,
|
|
1167
|
+
options
|
|
1168
|
+
);
|
|
1169
|
+
}
|