@ansonlai/docx-redline-js 0.8.4 → 0.10.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/ARCHITECTURE.md +679 -460
- package/CHANGELOG.md +208 -1
- package/README.md +152 -16
- package/core/current-properties.js +136 -0
- package/core/paragraph-text.js +211 -209
- package/core/types.js +9 -3
- package/core/word-revision-semantics.js +256 -0
- package/core/word-xml.js +160 -118
- package/dist/docx-redline-js.esm.js +4338 -796
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +111 -108
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4463 -921
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4463 -921
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +49 -7
- package/docs/SKILL_AUTHORING.md +169 -150
- package/docs/TESTING.md +1146 -1059
- package/docs/schemas/document-operations.schema.json +55 -2
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +21 -22
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-writer.js +67 -10
- package/engine/rpr-helpers.js +42 -0
- package/engine/run-builders.js +0 -73
- package/engine/surgical-diff-application.js +604 -605
- package/engine/table-mode.js +63 -63
- package/index.d.ts +53 -2
- package/index.js +13 -2
- package/node/cli-help.js +1 -0
- package/node/cli.js +21 -3
- package/orchestration/list-structural-fallback.js +67 -9
- package/package.json +1 -1
- package/pipeline/diff-engine.js +130 -1
- package/pipeline/ingestion-export.js +227 -243
- package/pipeline/ingestion-paragraph.js +93 -98
- package/pipeline/list-generation.js +415 -419
- package/pipeline/markdown-processor.js +5 -2
- package/pipeline/pipeline.js +206 -204
- package/scripts/export-validation-fixtures.mjs +19 -6
- package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
- package/scripts/lib/word-coverage-catalogue.mjs +7 -2
- package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
- package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
- package/scripts/word-com-differential.ps1 +443 -335
- package/scripts/word-com-visual-suite.ps1 +2 -2
- package/scripts/word-revision-semantics-matrix.ps1 +108 -0
- package/services/batch-operation-orchestrator.js +23 -3
- package/services/capture-engine.js +228 -226
- package/services/comment-replies.js +1 -1
- package/services/document-operation-applier.js +214 -0
- package/services/document-operation-contract.js +76 -0
- package/services/document-operation-mutations.js +688 -51
- package/services/error-recovery.js +17 -0
- package/services/inspection-comparison.js +82 -0
- package/services/list-conversion-diagnostics.js +296 -0
- package/services/list-format-change.js +586 -0
- package/services/list-format-mutation.js +416 -0
- package/services/list-format.js +305 -0
- package/services/numbering-allocation.js +642 -0
- package/services/numbering-helpers.js +354 -524
- package/services/operation-batch-compiler.js +183 -11
- package/services/operation-preflight.js +43 -33
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +38 -5
- package/services/revision-fidelity-verifier.js +484 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +182 -5
- package/services/table-reconciliation.js +59 -7
|
@@ -3,15 +3,27 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
6
|
+
import {
|
|
7
|
+
resolveListDiagnosticPolicy,
|
|
8
|
+
detectUnboundListMarker,
|
|
9
|
+
buildUnboundListMarkerDiagnostic,
|
|
10
|
+
detectRequestedListFormatMismatch,
|
|
11
|
+
buildListFormatDiagnostic,
|
|
12
|
+
findUnknownParagraphFormatKeys,
|
|
13
|
+
PARAGRAPH_FORMAT_REVISION_CONFLICT,
|
|
14
|
+
PARAGRAPH_NUMBERING_NOT_FOUND,
|
|
15
|
+
buildUnknownParagraphFormatDiagnostic,
|
|
16
|
+
diagnosticWarningText
|
|
17
|
+
} from './list-conversion-diagnostics.js';
|
|
18
|
+
import { getAllReferencedNumIdElements, getCurrentChild } from '../core/current-properties.js';
|
|
6
19
|
import { findReconstructionParagraphRange } from '../engine/reconstruction-mapper.js';
|
|
7
20
|
import { createRevisionMetadata } from '../core/types.js';
|
|
8
|
-
import { containsTrackedChanges, getTrackedChangeAuthors, createWordElement, withOoxmlSourceType } from '../core/word-xml.js';
|
|
21
|
+
import { containsTrackedChanges, getTrackedChangeAuthors, hasUnattributedTrackedChange, createWordElement, resolveExistingRevisionsPolicy, withOoxmlSourceType } from '../core/word-xml.js';
|
|
9
22
|
import {
|
|
10
|
-
markParagraphMarkInserted,
|
|
11
|
-
markParagraphMarkDeleted,
|
|
12
23
|
snapshotAndAttachRPrChange,
|
|
13
24
|
snapshotAndAttachPPrChange
|
|
14
25
|
} from '../engine/run-builders.js';
|
|
26
|
+
import { markParagraphInserted, markParagraphDeleted } from '../engine/paragraph-mark-revisions.js';
|
|
15
27
|
import {
|
|
16
28
|
applyCharacterFormatToRPr,
|
|
17
29
|
checkRunPropertiesChanged,
|
|
@@ -71,11 +83,19 @@ import {
|
|
|
71
83
|
import {
|
|
72
84
|
reserveNextNumberingIdPair,
|
|
73
85
|
remapNumberingPayloadForDocument,
|
|
74
|
-
reuseSourceListNumbering,
|
|
75
86
|
overwriteParagraphNumIds,
|
|
76
87
|
extractFirstParagraphNumId,
|
|
77
88
|
buildExplicitDecimalMultilevelNumberingXml
|
|
78
89
|
} from './numbering-helpers.js';
|
|
90
|
+
import {
|
|
91
|
+
allocateNumberingForReplacement,
|
|
92
|
+
checkNumberingBinding,
|
|
93
|
+
continueBatchListSequence,
|
|
94
|
+
mergeNumberingParts
|
|
95
|
+
} from './numbering-allocation.js';
|
|
96
|
+
import { convertPlainParagraphsToList, numIdIsLive, sequenceMapOf } from './list-format-mutation.js';
|
|
97
|
+
import { changeListParagraphsFormat, joinListParagraphs } from './list-format-change.js';
|
|
98
|
+
import { LIST_FORMAT_UNCHANGED, normalizeListFormatRequest } from './list-format.js';
|
|
79
99
|
import {
|
|
80
100
|
extractReplacementNodesFromOoxml,
|
|
81
101
|
normalizeBodySectionOrderStandalone
|
|
@@ -324,6 +344,52 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
324
344
|
return resolved;
|
|
325
345
|
}
|
|
326
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Zero-based document-order positions `{ from, to }` of an operation's target (and its targetEnd, when it
|
|
349
|
+
* is a range), resolved against the current DOM without mutating, capturing or warning. Used by the
|
|
350
|
+
* revision fidelity verifier to scope accept-first baselines and the accepted-intent check exactly.
|
|
351
|
+
* Returns null when the target or its end cannot be resolved.
|
|
352
|
+
*/
|
|
353
|
+
export function resolveOperationSourceRange(xmlDoc, targetText, targetRef, targetEndRef, runtimeContext, options = {}) {
|
|
354
|
+
const quiet = { ...options, _resolutionCapture: null, onInfo: () => { }, onWarn: () => { } };
|
|
355
|
+
const positionOf = paragraph => {
|
|
356
|
+
if (!paragraph) return null;
|
|
357
|
+
const position = getDocumentParagraphNodes(xmlDoc).indexOf(paragraph);
|
|
358
|
+
return position >= 0 ? position : null;
|
|
359
|
+
};
|
|
360
|
+
let start;
|
|
361
|
+
try {
|
|
362
|
+
start = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'redline', runtimeContext, quiet);
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const from = positionOf(start?.paragraph);
|
|
367
|
+
if (from === null) return null;
|
|
368
|
+
const descriptor = options.targetEndDescriptor || null;
|
|
369
|
+
let endParagraph = null;
|
|
370
|
+
if (options._compiledSourceEndId && options._sourceTargetRegistry) {
|
|
371
|
+
endParagraph = options._sourceTargetRegistry.resolve(options._compiledSourceEndId, xmlDoc)?.paragraph || null;
|
|
372
|
+
} else if (descriptor) {
|
|
373
|
+
endParagraph = resolveTargetParagraph(xmlDoc, descriptor.text || '', descriptor.index, 'redline', runtimeContext, {
|
|
374
|
+
...quiet,
|
|
375
|
+
targetDescriptor: descriptor,
|
|
376
|
+
_compiledSourceId: null
|
|
377
|
+
})?.paragraph || null;
|
|
378
|
+
} else if (targetEndRef) {
|
|
379
|
+
endParagraph = resolveTargetParagraphWithSnapshotShared(xmlDoc, {
|
|
380
|
+
targetRef: targetEndRef,
|
|
381
|
+
opType: 'redline',
|
|
382
|
+
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
383
|
+
onInfo: () => { },
|
|
384
|
+
onWarn: () => { }
|
|
385
|
+
})?.paragraph || null;
|
|
386
|
+
} else {
|
|
387
|
+
return { from, to: from };
|
|
388
|
+
}
|
|
389
|
+
const to = positionOf(endParagraph);
|
|
390
|
+
return to !== null && to >= from ? { from, to } : null;
|
|
391
|
+
}
|
|
392
|
+
|
|
327
393
|
function extractReplacementNodes(outputOxml) {
|
|
328
394
|
return extractReplacementNodesFromOoxml(outputOxml);
|
|
329
395
|
}
|
|
@@ -430,7 +496,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
|
|
|
430
496
|
// remove the list paragraph itself on Reject All instead of leaving an
|
|
431
497
|
// empty bullet or number behind.
|
|
432
498
|
if (generateRedlines) {
|
|
433
|
-
|
|
499
|
+
markParagraphInserted(paragraph, { author });
|
|
434
500
|
}
|
|
435
501
|
|
|
436
502
|
const run = createWordElement(xmlDoc, 'w:r');
|
|
@@ -522,7 +588,7 @@ function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionMetadata, aut
|
|
|
522
588
|
|
|
523
589
|
if (generateRedlines) {
|
|
524
590
|
if (options.trackParagraphMark === true) {
|
|
525
|
-
|
|
591
|
+
markParagraphInserted(paragraph, { author });
|
|
526
592
|
}
|
|
527
593
|
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, 'ins');
|
|
528
594
|
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
@@ -563,7 +629,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
|
|
|
563
629
|
if (pPr) wrappedParagraph.appendChild(pPr);
|
|
564
630
|
if (options.paragraphId) wrappedParagraph.setAttributeNS(NS_W14, 'w14:paraId', options.paragraphId);
|
|
565
631
|
if (options.trackParagraphMark === true) {
|
|
566
|
-
|
|
632
|
+
markParagraphInserted(wrappedParagraph, { author });
|
|
567
633
|
}
|
|
568
634
|
|
|
569
635
|
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
@@ -624,8 +690,9 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisi
|
|
|
624
690
|
function collectNumberingIdsFromNodes(nodes) {
|
|
625
691
|
const ids = new Set();
|
|
626
692
|
for (const node of nodes || []) {
|
|
627
|
-
|
|
628
|
-
|
|
693
|
+
// Deliberately historical-inclusive: the numbering-part merge must define every id the content can
|
|
694
|
+
// reference, including ids held only by pPrChange snapshots (Reject restores them).
|
|
695
|
+
for (const numIdNode of getAllReferencedNumIdElements(node)) {
|
|
629
696
|
const val = numIdNode.getAttribute('w:val') || numIdNode.getAttribute('val');
|
|
630
697
|
if (val != null && val !== '') {
|
|
631
698
|
ids.add(String(val));
|
|
@@ -1383,20 +1450,6 @@ async function tryExplicitDecimalHeaderListConversion({
|
|
|
1383
1450
|
if (!strippedContent) return null;
|
|
1384
1451
|
|
|
1385
1452
|
onInfo('[List] Applying explicit numeric header conversion with direct list binding.');
|
|
1386
|
-
const redlineResult = await applyRedlineToOxml(
|
|
1387
|
-
serializer.serializeToString(targetParagraph),
|
|
1388
|
-
currentParagraphText,
|
|
1389
|
-
strippedContent,
|
|
1390
|
-
{
|
|
1391
|
-
author,
|
|
1392
|
-
generateRedlines,
|
|
1393
|
-
_revisionIdAllocator: revisionIdAllocator
|
|
1394
|
-
}
|
|
1395
|
-
);
|
|
1396
|
-
if (!redlineResult?.hasChanges || typeof redlineResult?.oxml !== 'string') return null;
|
|
1397
|
-
|
|
1398
|
-
const extracted = extractReplacementNodes(redlineResult.oxml);
|
|
1399
|
-
const replacementNodes = extracted.replacementNodes;
|
|
1400
1453
|
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
1401
1454
|
explicitPlan,
|
|
1402
1455
|
runtimeContext?.listFallbackSequenceState || null
|
|
@@ -1427,6 +1480,22 @@ async function tryExplicitDecimalHeaderListConversion({
|
|
|
1427
1480
|
onInfo(`[List] Using isolated explicit-start numbering (start ${explicitStart}, numId ${appliedNumId}, abstractNumId ${reservedPair.abstractNumId}).`);
|
|
1428
1481
|
}
|
|
1429
1482
|
|
|
1483
|
+
// Redline only after numbering reservation succeeded so an aborted conversion never records receipt revisions.
|
|
1484
|
+
const redlineResult = await applyRedlineToOxml(
|
|
1485
|
+
serializer.serializeToString(targetParagraph),
|
|
1486
|
+
currentParagraphText,
|
|
1487
|
+
strippedContent,
|
|
1488
|
+
{
|
|
1489
|
+
author,
|
|
1490
|
+
generateRedlines,
|
|
1491
|
+
_revisionIdAllocator: revisionIdAllocator
|
|
1492
|
+
}
|
|
1493
|
+
);
|
|
1494
|
+
if (!redlineResult?.hasChanges || typeof redlineResult?.oxml !== 'string') return null;
|
|
1495
|
+
|
|
1496
|
+
const extracted = extractReplacementNodes(redlineResult.oxml);
|
|
1497
|
+
const replacementNodes = extracted.replacementNodes;
|
|
1498
|
+
|
|
1430
1499
|
if (explicitPlan.numberingKey && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1431
1500
|
runtimeContext.listFallbackSharedNumIdByKey.delete(explicitPlan.numberingKey);
|
|
1432
1501
|
}
|
|
@@ -1449,7 +1518,8 @@ async function tryExplicitDecimalHeaderListConversion({
|
|
|
1449
1518
|
numId: appliedNumId,
|
|
1450
1519
|
ilvl: 0,
|
|
1451
1520
|
clearParagraphPropertyChanges: true,
|
|
1452
|
-
removeListPropertyNode: true
|
|
1521
|
+
removeListPropertyNode: true,
|
|
1522
|
+
trackPropertyChange: generateRedlines ? { author, revisionIdAllocator } : null
|
|
1453
1523
|
});
|
|
1454
1524
|
|
|
1455
1525
|
const parent = targetParagraph.parentNode;
|
|
@@ -1476,6 +1546,145 @@ async function tryExplicitDecimalHeaderListConversion({
|
|
|
1476
1546
|
};
|
|
1477
1547
|
}
|
|
1478
1548
|
|
|
1549
|
+
const FALLBACK_STYLE_TO_LIST_FORMAT = Object.freeze({
|
|
1550
|
+
upperAlpha: 'upperLetter',
|
|
1551
|
+
lowerAlpha: 'lowerLetter',
|
|
1552
|
+
upperRoman: 'upperRoman',
|
|
1553
|
+
lowerRoman: 'lowerRoman',
|
|
1554
|
+
bullet: 'bullet'
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Text-equal marker conversion ("A. Header" -> "A. Header") of a plain paragraph, done in place through the
|
|
1559
|
+
* list-format convert-plain core: the typed marker becomes a tracked deletion and the binding a tracked pPrChange
|
|
1560
|
+
* in the same paragraph. This replaces the whole-paragraph delete/insert for letters, roman numerals and bullets,
|
|
1561
|
+
* which lost w14:paraId, bookmarks and direct formatting and reviewed as one deleted and one inserted line. Shapes
|
|
1562
|
+
* the core refuses (marker inside a hyperlink, field or revision; changed marker punctuation) keep the structural
|
|
1563
|
+
* fallback below, which is still reject-exact.
|
|
1564
|
+
*/
|
|
1565
|
+
function tryInPlaceMarkerListConversion({
|
|
1566
|
+
xmlDoc,
|
|
1567
|
+
serializer,
|
|
1568
|
+
documentXml,
|
|
1569
|
+
operationSession,
|
|
1570
|
+
targetParagraph,
|
|
1571
|
+
currentParagraphText,
|
|
1572
|
+
modifiedText,
|
|
1573
|
+
author,
|
|
1574
|
+
runtimeContext,
|
|
1575
|
+
revisionIdAllocator,
|
|
1576
|
+
generateRedlines = true,
|
|
1577
|
+
onInfo = () => { },
|
|
1578
|
+
options = {}
|
|
1579
|
+
}) {
|
|
1580
|
+
if (!targetParagraph || getParagraphListInfo(targetParagraph)) return null;
|
|
1581
|
+
if (normalizeWhitespaceForTargeting(currentParagraphText) !== normalizeWhitespaceForTargeting(modifiedText)) return null;
|
|
1582
|
+
const plan = buildSingleLineListStructuralFallbackPlan({
|
|
1583
|
+
oxml: serializer.serializeToString(targetParagraph),
|
|
1584
|
+
originalText: currentParagraphText,
|
|
1585
|
+
modifiedText,
|
|
1586
|
+
allowExistingList: false
|
|
1587
|
+
});
|
|
1588
|
+
const format = plan ? FALLBACK_STYLE_TO_LIST_FORMAT[plan.numberingStyle] : null;
|
|
1589
|
+
if (!format) return null;
|
|
1590
|
+
const allParagraphs = getDocumentParagraphNodes(xmlDoc);
|
|
1591
|
+
const conversion = convertPlainParagraphsToList(xmlDoc, [targetParagraph], normalizeListFormatRequest({
|
|
1592
|
+
mode: 'convert-plain',
|
|
1593
|
+
format,
|
|
1594
|
+
strip: 'marker'
|
|
1595
|
+
}), {
|
|
1596
|
+
author: author || getDefaultAuthor(),
|
|
1597
|
+
generateRedlines,
|
|
1598
|
+
revisionIdAllocator,
|
|
1599
|
+
runtimeContext,
|
|
1600
|
+
stylesXml: runtimeContext?.stylesXml || options.stylesXml || null,
|
|
1601
|
+
numberingXml: runtimeContext?.numberingXml || options.numberingXml || null,
|
|
1602
|
+
paragraphIndexOf: paragraph => allParagraphs.indexOf(paragraph) + 1
|
|
1603
|
+
});
|
|
1604
|
+
if (!conversion.ok) {
|
|
1605
|
+
onInfo(`[List] In-place marker conversion declined (${conversion.error.code}); using the structural fallback.`);
|
|
1606
|
+
return null;
|
|
1607
|
+
}
|
|
1608
|
+
onInfo(`[List] Converted typed ${format} marker in place (numId ${conversion.items[0].numId}).`);
|
|
1609
|
+
ensureParagraphIdsOnImportedNode(targetParagraph, operationSession);
|
|
1610
|
+
options?._mutationLiveNodes?.push(targetParagraph);
|
|
1611
|
+
for (const numId of conversion.allocatedNumIds) operationSession?.receiptCollector?.recordNumbering(numId);
|
|
1612
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
1613
|
+
return {
|
|
1614
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1615
|
+
hasChanges: true,
|
|
1616
|
+
numberingXml: conversion.numberingXml
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// CT_PPrBase child order (ECMA-376), used to place copied direct properties on the new list paragraph.
|
|
1621
|
+
const PPR_CHILD_ORDER = [
|
|
1622
|
+
'pStyle', 'keepNext', 'keepLines', 'pageBreakBefore', 'framePr', 'widowControl', 'numPr', 'suppressLineNumbers',
|
|
1623
|
+
'pBdr', 'shd', 'tabs', 'suppressAutoHyphens', 'kinsoku', 'wordWrap', 'overflowPunct', 'topLinePunct',
|
|
1624
|
+
'autoSpaceDE', 'autoSpaceDN', 'bidi', 'adjustRightInd', 'snapToGrid', 'spacing', 'ind', 'contextualSpacing',
|
|
1625
|
+
'mirrorIndents', 'suppressOverlap', 'jc', 'textDirection', 'textAlignment', 'textboxTightWrap', 'outlineLvl',
|
|
1626
|
+
'divId', 'cnfStyle', 'rPr', 'sectPr', 'pPrChange'
|
|
1627
|
+
];
|
|
1628
|
+
const PPR_NOT_COPIED = new Set(['pStyle', 'numPr', 'rPr', 'sectPr', 'pPrChange']);
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* The list structural fallback rebuilds the paragraph as a tracked-deleted source paragraph plus a new list
|
|
1632
|
+
* paragraph from text alone. The deleted paragraph must keep the source `pPr` (Reject All restores it, so the
|
|
1633
|
+
* Reject All verifier sees no property change), the new paragraph carries the source's direct properties that the
|
|
1634
|
+
* list binding does not own (`keepNext`, spacing, alignment...), and the paragraph that survives Accept All
|
|
1635
|
+
* keeps the source `w14:paraId` / `w14:textId`.
|
|
1636
|
+
*/
|
|
1637
|
+
function restoreSourceIdentityAndPropertiesOnFallback(xmlDoc, replacementNodes, targetParagraph) {
|
|
1638
|
+
const paragraphs = replacementNodes.filter(node => node?.nodeType === 1 && node.localName === 'p');
|
|
1639
|
+
const markOf = (paragraph, kind) => {
|
|
1640
|
+
const rPr = getDirectWordChild(getDirectWordChild(paragraph, 'pPr'), 'rPr');
|
|
1641
|
+
return !!rPr && !!getDirectWordChild(rPr, kind);
|
|
1642
|
+
};
|
|
1643
|
+
const deleted = paragraphs.find(paragraph => markOf(paragraph, 'del'));
|
|
1644
|
+
const created = paragraphs.find(paragraph => !markOf(paragraph, 'del'));
|
|
1645
|
+
const sourcePPr = getDirectWordChild(targetParagraph, 'pPr');
|
|
1646
|
+
if (deleted && sourcePPr) {
|
|
1647
|
+
const restored = xmlDoc.importNode(sourcePPr, true);
|
|
1648
|
+
const mark = getDirectWordChild(getDirectWordChild(deleted, 'pPr'), 'rPr');
|
|
1649
|
+
const delMarker = mark && getDirectWordChild(mark, 'del');
|
|
1650
|
+
if (delMarker) {
|
|
1651
|
+
let rPr = getDirectWordChild(restored, 'rPr');
|
|
1652
|
+
if (!rPr) {
|
|
1653
|
+
rPr = createWordElement(xmlDoc, 'w:rPr');
|
|
1654
|
+
const successor = getDirectWordChild(restored, 'sectPr') || getDirectWordChild(restored, 'pPrChange');
|
|
1655
|
+
restored.insertBefore(rPr, successor || null);
|
|
1656
|
+
}
|
|
1657
|
+
Array.from(rPr.childNodes).forEach(child => {
|
|
1658
|
+
if (child.nodeType === 1 && (child.localName === 'ins' || child.localName === 'del')) rPr.removeChild(child);
|
|
1659
|
+
});
|
|
1660
|
+
rPr.appendChild(xmlDoc.importNode(delMarker, true));
|
|
1661
|
+
deleted.replaceChild(restored, getDirectWordChild(deleted, 'pPr'));
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (created && sourcePPr) {
|
|
1665
|
+
let createdPPr = getDirectWordChild(created, 'pPr');
|
|
1666
|
+
if (!createdPPr) {
|
|
1667
|
+
createdPPr = createWordElement(xmlDoc, 'w:pPr');
|
|
1668
|
+
created.insertBefore(createdPPr, created.firstChild || null);
|
|
1669
|
+
}
|
|
1670
|
+
for (const child of Array.from(sourcePPr.childNodes)) {
|
|
1671
|
+
if (child.nodeType !== 1 || PPR_NOT_COPIED.has(child.localName)) continue;
|
|
1672
|
+
if (getDirectWordChild(createdPPr, child.localName)) continue;
|
|
1673
|
+
const rank = PPR_CHILD_ORDER.indexOf(child.localName);
|
|
1674
|
+
const successor = Array.from(createdPPr.childNodes).find(node => (
|
|
1675
|
+
node.nodeType === 1 && PPR_CHILD_ORDER.indexOf(node.localName) > rank
|
|
1676
|
+
));
|
|
1677
|
+
createdPPr.insertBefore(xmlDoc.importNode(child, true), successor || null);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (created) {
|
|
1681
|
+
for (const name of ['paraId', 'textId']) {
|
|
1682
|
+
const value = targetParagraph.getAttributeNS?.(NS_W14, name) || targetParagraph.getAttribute?.(`w14:${name}`);
|
|
1683
|
+
if (value) created.setAttributeNS(NS_W14, `w14:${name}`, value);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1479
1688
|
async function trySingleParagraphListStructuralFallback({
|
|
1480
1689
|
xmlDoc,
|
|
1481
1690
|
serializer,
|
|
@@ -1517,7 +1726,11 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1517
1726
|
const extracted = extractReplacementNodes(fallbackResult.oxml);
|
|
1518
1727
|
let replacementNodes = extracted.replacementNodes;
|
|
1519
1728
|
let numberingXml = extracted.numberingXml || fallbackResult?.numberingXml || null;
|
|
1520
|
-
const
|
|
1729
|
+
const hasPositiveStart = Number.isInteger(fallbackPlan?.startAt) && fallbackPlan.startAt > 0;
|
|
1730
|
+
// Only decimal starts use the explicit multilevel-decimal definition. Letter and roman starts (`C.`, `iv.`)
|
|
1731
|
+
// keep the generated definition, whose level-0 startOverride is already applied by the fallback executor.
|
|
1732
|
+
const hasExplicitStartAt = hasPositiveStart && fallbackPlan?.numberingStyle === 'decimal';
|
|
1733
|
+
const hasNonDecimalStart = hasPositiveStart && !hasExplicitStartAt;
|
|
1521
1734
|
const numberingKey = fallbackResult?.listStructuralFallbackKey || fallbackPlan?.numberingKey || null;
|
|
1522
1735
|
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
1523
1736
|
fallbackPlan,
|
|
@@ -1573,7 +1786,8 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1573
1786
|
numId: explicitNumIdForBinding,
|
|
1574
1787
|
ilvl: 0,
|
|
1575
1788
|
clearParagraphPropertyChanges: true,
|
|
1576
|
-
removeListPropertyNode: true
|
|
1789
|
+
removeListPropertyNode: true,
|
|
1790
|
+
trackPropertyChange: generateRedlines ? { author, revisionIdAllocator } : null
|
|
1577
1791
|
});
|
|
1578
1792
|
}
|
|
1579
1793
|
} else {
|
|
@@ -1588,7 +1802,23 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1588
1802
|
);
|
|
1589
1803
|
}
|
|
1590
1804
|
|
|
1591
|
-
if (!hasExplicitStartAt &&
|
|
1805
|
+
if (!hasExplicitStartAt && numberingXml && runtimeContext) {
|
|
1806
|
+
// Separate operations of one batch that convert continuing markers (`A.` then `B.`) join one numbering
|
|
1807
|
+
// instance, through the same sequence state `list-format convert-plain` uses.
|
|
1808
|
+
const batch = continueBatchListSequence({
|
|
1809
|
+
numberingXml,
|
|
1810
|
+
replacementNodes,
|
|
1811
|
+
sequence: sequenceMapOf(runtimeContext),
|
|
1812
|
+
isNumIdLive: numId => numIdIsLive(xmlDoc, numId)
|
|
1813
|
+
});
|
|
1814
|
+
if (batch) {
|
|
1815
|
+
replacementNodes = batch.replacementNodes;
|
|
1816
|
+
numberingXml = null;
|
|
1817
|
+
onInfo(`[List] Continuing batch list numbering (numId ${batch.numId}).`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
if (!hasExplicitStartAt && !hasNonDecimalStart && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1592
1822
|
const sharedNumId = numberingKey ? runtimeContext.listFallbackSharedNumIdByKey.get(numberingKey) : null;
|
|
1593
1823
|
if (sharedNumId) {
|
|
1594
1824
|
overwriteParagraphNumIds(replacementNodes, sharedNumId);
|
|
@@ -1605,6 +1835,7 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1605
1835
|
|
|
1606
1836
|
const parent = targetParagraph.parentNode;
|
|
1607
1837
|
if (!parent) return null;
|
|
1838
|
+
if (generateRedlines) restoreSourceIdentityAndPropertiesOnFallback(xmlDoc, replacementNodes, targetParagraph);
|
|
1608
1839
|
for (const node of replacementNodes) {
|
|
1609
1840
|
const imported = xmlDoc.importNode(node, true);
|
|
1610
1841
|
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
@@ -1627,6 +1858,43 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1627
1858
|
};
|
|
1628
1859
|
}
|
|
1629
1860
|
|
|
1861
|
+
function listDiagnosticFailure(documentXml, diagnostic) {
|
|
1862
|
+
const { severity: _severity, ...error } = diagnostic;
|
|
1863
|
+
return { documentXml, hasChanges: false, numberingXml: null, status: 'error', error };
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function listDiagnosticFields(diagnostics) {
|
|
1867
|
+
return diagnostics.length > 0
|
|
1868
|
+
? { diagnostics, warnings: diagnostics.map(diagnosticWarningText) }
|
|
1869
|
+
: {};
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Compares the markers of a list edit with the numbering definition the source paragraphs keep.
|
|
1874
|
+
* Returns a diagnostic (warning or error severity per policy) or null.
|
|
1875
|
+
*/
|
|
1876
|
+
function diagnoseListFormatNotApplied({ sourceParagraphs, modifiedText, runtimeContext, options }) {
|
|
1877
|
+
const policy = resolveListDiagnosticPolicy(options);
|
|
1878
|
+
if (policy === 'off') return null;
|
|
1879
|
+
const sourceNumberingXml = runtimeContext?.numberingIdState?.sourceNumberingXml;
|
|
1880
|
+
if (!sourceNumberingXml) return null;
|
|
1881
|
+
const sourceItems = [];
|
|
1882
|
+
for (const paragraph of sourceParagraphs || []) {
|
|
1883
|
+
const info = getParagraphListInfo(paragraph);
|
|
1884
|
+
if (info) sourceItems.push({ text: getParagraphText(paragraph), numId: info.numId, ilvl: info.ilvl });
|
|
1885
|
+
}
|
|
1886
|
+
const found = detectRequestedListFormatMismatch({ modifiedText, sourceItems, sourceNumberingXml });
|
|
1887
|
+
if (!found) return null;
|
|
1888
|
+
return buildListFormatDiagnostic(found.mismatch, {
|
|
1889
|
+
policy,
|
|
1890
|
+
numId: found.numId,
|
|
1891
|
+
level: found.level,
|
|
1892
|
+
marker: found.marker,
|
|
1893
|
+
targetText: found.targetText,
|
|
1894
|
+
operationIndex: options?._operationIndex
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1630
1898
|
export async function applyToParagraphByExactText(documentXml, targetText, modifiedText, author, targetRef = null, targetEndRef = null, runtimeContext = null, options = {}) {
|
|
1631
1899
|
const generateRedlines = options.generateRedlines !== false;
|
|
1632
1900
|
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
@@ -1699,6 +1967,12 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1699
1967
|
}
|
|
1700
1968
|
};
|
|
1701
1969
|
}
|
|
1970
|
+
// 'auto' resolves against the target paragraph here and again against the whole
|
|
1971
|
+
// range once a targetEnd range is resolved below.
|
|
1972
|
+
const autoExistingRevisions = options.existingRevisions === 'auto';
|
|
1973
|
+
if (autoExistingRevisions) {
|
|
1974
|
+
options = { ...options, existingRevisions: resolveExistingRevisionsPolicy('auto', targetParagraph, author) };
|
|
1975
|
+
}
|
|
1702
1976
|
const hasRevisions = containsTrackedChanges(targetParagraph);
|
|
1703
1977
|
const existingPolicy = options.existingRevisions || 'merge-same-author';
|
|
1704
1978
|
if (
|
|
@@ -1709,7 +1983,9 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1709
1983
|
if (existingPolicy === 'merge-same-author' || existingPolicy === 'slice-cross-author') {
|
|
1710
1984
|
const authors = getTrackedChangeAuthors(targetParagraph);
|
|
1711
1985
|
const opAuthor = String(author || '').trim().toLowerCase();
|
|
1712
|
-
const allSame = authors.length > 0
|
|
1986
|
+
const allSame = authors.length > 0
|
|
1987
|
+
&& !hasUnattributedTrackedChange(targetParagraph)
|
|
1988
|
+
&& authors.every(a => a.trim().toLowerCase() === opAuthor);
|
|
1713
1989
|
if (!allSame && existingPolicy === 'merge-same-author') {
|
|
1714
1990
|
return {
|
|
1715
1991
|
documentXml,
|
|
@@ -1718,7 +1994,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1718
1994
|
status: 'error',
|
|
1719
1995
|
error: {
|
|
1720
1996
|
code: 'EXISTING_REVISIONS',
|
|
1721
|
-
message: `Target paragraph contains tracked changes from another author (${authors
|
|
1997
|
+
message: `Target paragraph contains tracked changes from another author (${[...authors, ...(hasUnattributedTrackedChange(targetParagraph) ? ['unattributed'] : [])].join(', ')}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
|
|
1722
1998
|
revisionAuthors: authors,
|
|
1723
1999
|
currentPolicy: existingPolicy
|
|
1724
2000
|
}
|
|
@@ -1802,7 +2078,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1802
2078
|
&& !(hasRevisions && existingPolicy === 'merge-same-author')
|
|
1803
2079
|
) {
|
|
1804
2080
|
if (generateRedlines) {
|
|
1805
|
-
|
|
2081
|
+
markParagraphDeleted(targetParagraph, { author });
|
|
1806
2082
|
options?._mutationLiveNodes?.push(targetParagraph);
|
|
1807
2083
|
} else {
|
|
1808
2084
|
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
@@ -1847,7 +2123,31 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1847
2123
|
let effectiveModifiedText = synthesizedTableMarkdown || modifiedText;
|
|
1848
2124
|
const useTableScope = !!containingTable && isMarkdownTableText(effectiveModifiedText);
|
|
1849
2125
|
const isTableMarkdownEdit = isMarkdownTableText(effectiveModifiedText);
|
|
1850
|
-
|
|
2126
|
+
// A targetEnd given as a compact handle (paragraphId + fingerprint, no numeric index) has no targetEndRef;
|
|
2127
|
+
// its range comes from the batch-compiled end or the end descriptor, never silently dropped.
|
|
2128
|
+
let compiledEndRange = null;
|
|
2129
|
+
if (!targetEndRef && (options._compiledSourceEndId || options.targetEndDescriptor)) {
|
|
2130
|
+
const rangeResolution = resolveOperationParagraphRange(xmlDoc, targetText, targetRef, targetEndRef, 'redline', runtimeContext, {
|
|
2131
|
+
...options,
|
|
2132
|
+
_resolutionCapture: null
|
|
2133
|
+
});
|
|
2134
|
+
if (rangeResolution.error) {
|
|
2135
|
+
return { documentXml, hasChanges: false, status: 'error', error: rangeResolution.error };
|
|
2136
|
+
}
|
|
2137
|
+
if (rangeResolution.paragraphs.length > 1) {
|
|
2138
|
+
const container = rangeResolution.paragraphs[0].parentNode;
|
|
2139
|
+
if (rangeResolution.paragraphs.some(paragraph => paragraph.parentNode !== container)) {
|
|
2140
|
+
return {
|
|
2141
|
+
documentXml,
|
|
2142
|
+
hasChanges: false,
|
|
2143
|
+
status: 'error',
|
|
2144
|
+
error: { code: 'TARGET_RANGE_INVALID', message: 'A redline target range must stay inside one container (body, table cell, header, ...); it spans a table or other container boundary. Edit the paragraphs on each side separately.' }
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
compiledEndRange = rangeResolution.paragraphs;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
const explicitRangeParagraphs = compiledEndRange ? compiledEndRange : targetEndRef
|
|
1851
2151
|
? resolveParagraphRangeByRefs(xmlDoc, targetRef, targetEndRef, {
|
|
1852
2152
|
opType: 'redline',
|
|
1853
2153
|
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
@@ -1858,6 +2158,9 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1858
2158
|
? findReconstructionParagraphRange(xmlDoc, targetText)
|
|
1859
2159
|
: null);
|
|
1860
2160
|
const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
|
|
2161
|
+
if (autoExistingRevisions && hasExplicitRangeScope) {
|
|
2162
|
+
options = { ...options, existingRevisions: resolveExistingRevisionsPolicy('auto', explicitRangeParagraphs, author) };
|
|
2163
|
+
}
|
|
1861
2164
|
if (!useTableScope && hasExplicitRangeScope) {
|
|
1862
2165
|
const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
|
|
1863
2166
|
if (insertionEntries && insertionEntries.length > 0) {
|
|
@@ -1865,6 +2168,13 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1865
2168
|
for (const entry of insertionEntries) {
|
|
1866
2169
|
onInfo(`[List] Explicit-range insertion: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
1867
2170
|
}
|
|
2171
|
+
const rangeFormatDiagnostic = diagnoseListFormatNotApplied({
|
|
2172
|
+
sourceParagraphs: explicitRangeParagraphs,
|
|
2173
|
+
modifiedText: effectiveModifiedText,
|
|
2174
|
+
runtimeContext,
|
|
2175
|
+
options
|
|
2176
|
+
});
|
|
2177
|
+
if (rangeFormatDiagnostic?.severity === 'error') return listDiagnosticFailure(documentXml, rangeFormatDiagnostic);
|
|
1868
2178
|
const applied = applyExplicitRangeListInsertions({
|
|
1869
2179
|
xmlDoc,
|
|
1870
2180
|
explicitRangeParagraphs,
|
|
@@ -1876,7 +2186,8 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1876
2186
|
return {
|
|
1877
2187
|
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1878
2188
|
hasChanges: true,
|
|
1879
|
-
numberingXml: null
|
|
2189
|
+
numberingXml: null,
|
|
2190
|
+
...listDiagnosticFields(rangeFormatDiagnostic ? [rangeFormatDiagnostic] : [])
|
|
1880
2191
|
};
|
|
1881
2192
|
}
|
|
1882
2193
|
}
|
|
@@ -2021,8 +2332,11 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2021
2332
|
// Word cannot end the body (or a container) with a table: it would add an untracked paragraph
|
|
2022
2333
|
// there that Reject All leaves behind. Supply the paragraph ourselves with a tracked-inserted
|
|
2023
2334
|
// mark so Reject All removes it and the source paragraph count is restored.
|
|
2024
|
-
|
|
2025
|
-
|
|
2335
|
+
// Skip whitespace/comment nodes of pretty-printed documents when deciding what follows the table.
|
|
2336
|
+
let followingElement = followingNode;
|
|
2337
|
+
while (followingElement && followingElement.nodeType !== 1) followingElement = followingElement.nextSibling;
|
|
2338
|
+
const endsContainer = !followingElement
|
|
2339
|
+
|| (followingElement.namespaceURI === NS_W && followingElement.localName === 'sectPr');
|
|
2026
2340
|
if (endsContainer) {
|
|
2027
2341
|
const trailing = createWordElement(xmlDoc, 'w:p');
|
|
2028
2342
|
const trailingPPr = clonePropertiesWithoutRevisionHistory(getDirectWordChild(targetParagraph, 'pPr'));
|
|
@@ -2032,7 +2346,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2032
2346
|
}
|
|
2033
2347
|
trailing.appendChild(trailingPPr);
|
|
2034
2348
|
}
|
|
2035
|
-
if (generateRedlines)
|
|
2349
|
+
if (generateRedlines) markParagraphInserted(trailing, { author });
|
|
2036
2350
|
ensureParagraphIdsOnImportedNode(trailing, operationSession);
|
|
2037
2351
|
options?._mutationLiveNodes?.push(parent.insertBefore(trailing, followingNode));
|
|
2038
2352
|
}
|
|
@@ -2098,6 +2412,13 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2098
2412
|
})
|
|
2099
2413
|
: null;
|
|
2100
2414
|
if (insertionOnlyPlan && insertionOnlyPlan.entries.length > 0) {
|
|
2415
|
+
const insertionFormatDiagnostic = diagnoseListFormatNotApplied({
|
|
2416
|
+
sourceParagraphs: [targetParagraph],
|
|
2417
|
+
modifiedText: effectiveModifiedText,
|
|
2418
|
+
runtimeContext,
|
|
2419
|
+
options
|
|
2420
|
+
});
|
|
2421
|
+
if (insertionFormatDiagnostic?.severity === 'error') return listDiagnosticFailure(documentXml, insertionFormatDiagnostic);
|
|
2101
2422
|
onInfo(`[List] Applying insertion-only list redline heuristic (${insertionOnlyPlan.entries.length} new item(s)).`);
|
|
2102
2423
|
for (const entry of insertionOnlyPlan.entries) {
|
|
2103
2424
|
onInfo(`[List] Insertion entry resolved: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
@@ -2122,7 +2443,8 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2122
2443
|
return {
|
|
2123
2444
|
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2124
2445
|
hasChanges: true,
|
|
2125
|
-
numberingXml: null
|
|
2446
|
+
numberingXml: null,
|
|
2447
|
+
...listDiagnosticFields(insertionFormatDiagnostic ? [insertionFormatDiagnostic] : [])
|
|
2126
2448
|
};
|
|
2127
2449
|
}
|
|
2128
2450
|
|
|
@@ -2138,6 +2460,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2138
2460
|
effectiveModifiedText = listScopeEdit.modifiedText;
|
|
2139
2461
|
}
|
|
2140
2462
|
|
|
2463
|
+
let unboundMarkerDiagnostic = null;
|
|
2141
2464
|
if (!bypassSingleParagraphHeuristics && !useTableScope && !useListScope && !hasExplicitRangeScope) {
|
|
2142
2465
|
const explicitHeaderListConversion = await tryExplicitDecimalHeaderListConversion({
|
|
2143
2466
|
xmlDoc,
|
|
@@ -2156,6 +2479,23 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2156
2479
|
});
|
|
2157
2480
|
if (explicitHeaderListConversion) return explicitHeaderListConversion;
|
|
2158
2481
|
|
|
2482
|
+
const inPlaceConversion = tryInPlaceMarkerListConversion({
|
|
2483
|
+
xmlDoc,
|
|
2484
|
+
serializer,
|
|
2485
|
+
documentXml,
|
|
2486
|
+
operationSession,
|
|
2487
|
+
targetParagraph,
|
|
2488
|
+
currentParagraphText,
|
|
2489
|
+
modifiedText: effectiveModifiedText,
|
|
2490
|
+
author,
|
|
2491
|
+
runtimeContext,
|
|
2492
|
+
revisionIdAllocator,
|
|
2493
|
+
generateRedlines,
|
|
2494
|
+
onInfo,
|
|
2495
|
+
options
|
|
2496
|
+
});
|
|
2497
|
+
if (inPlaceConversion) return inPlaceConversion;
|
|
2498
|
+
|
|
2159
2499
|
const listFallback = await trySingleParagraphListStructuralFallback({
|
|
2160
2500
|
xmlDoc,
|
|
2161
2501
|
serializer,
|
|
@@ -2172,6 +2512,25 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2172
2512
|
options
|
|
2173
2513
|
});
|
|
2174
2514
|
if (listFallback) return listFallback;
|
|
2515
|
+
|
|
2516
|
+
// Both structural fallbacks declined. A plain paragraph whose new text starts a list marker
|
|
2517
|
+
// would be written as literal text: report that instead of returning a silent success.
|
|
2518
|
+
const unboundPolicy = resolveListDiagnosticPolicy(options);
|
|
2519
|
+
if (unboundPolicy !== 'off' && !targetListInfo) {
|
|
2520
|
+
const requestedMarker = detectUnboundListMarker({
|
|
2521
|
+
originalText: currentParagraphText,
|
|
2522
|
+
modifiedText: effectiveModifiedText,
|
|
2523
|
+
targetIsListItem: false
|
|
2524
|
+
});
|
|
2525
|
+
if (requestedMarker) {
|
|
2526
|
+
unboundMarkerDiagnostic = buildUnboundListMarkerDiagnostic(requestedMarker, {
|
|
2527
|
+
policy: unboundPolicy,
|
|
2528
|
+
targetText: currentParagraphText,
|
|
2529
|
+
operationIndex: options?._operationIndex
|
|
2530
|
+
});
|
|
2531
|
+
if (unboundPolicy === 'error') return listDiagnosticFailure(documentXml, unboundMarkerDiagnostic);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2175
2534
|
}
|
|
2176
2535
|
|
|
2177
2536
|
const originalTextForApply = useListScope
|
|
@@ -2216,6 +2575,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2216
2575
|
pairReplacements: options.pairReplacements === true,
|
|
2217
2576
|
insertionAffinity: options.insertionAffinity || null,
|
|
2218
2577
|
literalTextEdit: options.literalTextEdit === true,
|
|
2578
|
+
plainTextContent: options.plainTextContent === true,
|
|
2219
2579
|
_revisionIdAllocator: revisionIdAllocator,
|
|
2220
2580
|
_isolatedTableCell: useTableScope
|
|
2221
2581
|
});
|
|
@@ -2239,18 +2599,16 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2239
2599
|
const extracted = extractReplacementNodes(result.oxml);
|
|
2240
2600
|
let replacementNodes = removeListPackagingSentinel(extracted.replacementNodes, result.warnings);
|
|
2241
2601
|
let numberingXml = extracted.numberingXml;
|
|
2242
|
-
if (numberingXml && runtimeContext?.numberingIdState && !useTableScope) {
|
|
2243
|
-
const sourceParagraphs = useListScope ? listScopeEdit.paragraphs : (explicitRangeParagraphs || [targetParagraph]);
|
|
2244
|
-
const continued = reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, runtimeContext.numberingIdState);
|
|
2245
|
-
if (continued) {
|
|
2246
|
-
replacementNodes = continued.replacementNodes;
|
|
2247
|
-
numberingXml = null;
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
2602
|
if (numberingXml && runtimeContext?.numberingIdState) {
|
|
2251
|
-
const
|
|
2252
|
-
|
|
2253
|
-
|
|
2603
|
+
const allocated = allocateNumberingForReplacement({
|
|
2604
|
+
numberingXml,
|
|
2605
|
+
replacementNodes,
|
|
2606
|
+
sourceParagraphs: useListScope ? listScopeEdit.paragraphs : (explicitRangeParagraphs || [targetParagraph]),
|
|
2607
|
+
numberingIdState: runtimeContext.numberingIdState,
|
|
2608
|
+
allowContinueSource: !useTableScope
|
|
2609
|
+
});
|
|
2610
|
+
replacementNodes = allocated.replacementNodes;
|
|
2611
|
+
numberingXml = allocated.numberingXml;
|
|
2254
2612
|
}
|
|
2255
2613
|
const scopeNodes = useTableScope
|
|
2256
2614
|
? [containingTable]
|
|
@@ -2292,7 +2650,10 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2292
2650
|
hasChanges: true,
|
|
2293
2651
|
numberingXml,
|
|
2294
2652
|
status: 'ok',
|
|
2295
|
-
...(
|
|
2653
|
+
...(unboundMarkerDiagnostic ? {
|
|
2654
|
+
diagnostics: [unboundMarkerDiagnostic],
|
|
2655
|
+
warnings: [...(Array.isArray(result.warnings) ? result.warnings : []), diagnosticWarningText(unboundMarkerDiagnostic)]
|
|
2656
|
+
} : (Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}))
|
|
2296
2657
|
};
|
|
2297
2658
|
}
|
|
2298
2659
|
|
|
@@ -2699,13 +3060,63 @@ export async function applyParagraphFormatToParagraphByExactText(
|
|
|
2699
3060
|
targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
|
|
2700
3061
|
}
|
|
2701
3062
|
|
|
3063
|
+
const unknownPropertyKeys = findUnknownParagraphFormatKeys(properties);
|
|
3064
|
+
const propertyDiagnostics = unknownPropertyKeys.length > 0
|
|
3065
|
+
? [buildUnknownParagraphFormatDiagnostic(unknownPropertyKeys)]
|
|
3066
|
+
: [];
|
|
3067
|
+
if (properties?.numbering) {
|
|
3068
|
+
const binding = checkNumberingBinding(
|
|
3069
|
+
runtimeContext?.numberingIdState,
|
|
3070
|
+
properties.numbering.numId,
|
|
3071
|
+
Number(properties.numbering.level) || 0
|
|
3072
|
+
);
|
|
3073
|
+
if (!binding.ok) {
|
|
3074
|
+
return {
|
|
3075
|
+
documentXml,
|
|
3076
|
+
hasChanges: false,
|
|
3077
|
+
status: 'error',
|
|
3078
|
+
error: {
|
|
3079
|
+
code: PARAGRAPH_NUMBERING_NOT_FOUND,
|
|
3080
|
+
message: `${binding.message} paragraph-format.numbering binds to an existing list; it never creates one.`,
|
|
3081
|
+
field: 'properties.numbering',
|
|
3082
|
+
numId: Number(properties.numbering.numId),
|
|
3083
|
+
level: Number(properties.numbering.level) || 0,
|
|
3084
|
+
reason: binding.reason,
|
|
3085
|
+
recovery: {
|
|
3086
|
+
action: 'inspect_numbering',
|
|
3087
|
+
hint: 'Inspect a paragraph already in the target list and pass its list.numId and list.level. To start a new list, use a list-change/redline operation.'
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
2702
3093
|
const hasChanges = checkParagraphPropertiesChanged(pPr, properties);
|
|
2703
3094
|
if (!hasChanges) {
|
|
2704
|
-
return { documentXml, hasChanges: false, status: 'no-op' };
|
|
3095
|
+
return { documentXml, hasChanges: false, status: 'no-op', ...listDiagnosticFields(propertyDiagnostics) };
|
|
2705
3096
|
}
|
|
2706
3097
|
|
|
2707
3098
|
if (generateRedlines) {
|
|
2708
|
-
|
|
3099
|
+
const authorUsed = author || getDefaultAuthor();
|
|
3100
|
+
const pendingChange = getCurrentChild(pPr, 'pPrChange');
|
|
3101
|
+
const pendingAuthor = pendingChange?.getAttribute('w:author') ?? pendingChange?.getAttribute('author') ?? '';
|
|
3102
|
+
if (pendingChange && pendingAuthor !== authorUsed) {
|
|
3103
|
+
return {
|
|
3104
|
+
documentXml,
|
|
3105
|
+
hasChanges: false,
|
|
3106
|
+
status: 'error',
|
|
3107
|
+
error: {
|
|
3108
|
+
code: PARAGRAPH_FORMAT_REVISION_CONFLICT,
|
|
3109
|
+
message: `Target paragraph already has a pending paragraph-property change by "${pendingAuthor || 'unattributed'}". Overwriting its snapshot would change what Reject All restores.`,
|
|
3110
|
+
pendingAuthor,
|
|
3111
|
+
recovery: {
|
|
3112
|
+
action: 'resolve_pending_revision',
|
|
3113
|
+
hint: 'Ask the user to accept or reject the existing paragraph formatting change first; this tool does not resolve revisions by other reviewers.'
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
// A same-author pending snapshot already records the earliest "before" state; keep it.
|
|
3119
|
+
if (!pendingChange) snapshotAndAttachPPrChange(xmlDoc, pPr, authorUsed);
|
|
2709
3120
|
}
|
|
2710
3121
|
|
|
2711
3122
|
applyParagraphPropertiesToPPr(xmlDoc, pPr, properties);
|
|
@@ -2716,6 +3127,232 @@ export async function applyParagraphFormatToParagraphByExactText(
|
|
|
2716
3127
|
return {
|
|
2717
3128
|
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2718
3129
|
hasChanges: true,
|
|
2719
|
-
status: 'ok'
|
|
3130
|
+
status: 'ok',
|
|
3131
|
+
...listDiagnosticFields(propertyDiagnostics)
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* Resolves an operation's target range (target plus optional targetEnd) to paragraphs in document order, the
|
|
3137
|
+
* same way restore does: batch-compiled source ids first, then legacy refs, then an end descriptor.
|
|
3138
|
+
*/
|
|
3139
|
+
function resolveOperationParagraphRange(xmlDoc, targetText, targetRef, targetEndRef, opType, runtimeContext, options) {
|
|
3140
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeContext, options);
|
|
3141
|
+
if (resolved?.error || !resolved?.paragraph) {
|
|
3142
|
+
return { error: resolved?.error || { code: 'TARGET_NOT_FOUND', message: 'Target paragraph not found.' } };
|
|
3143
|
+
}
|
|
3144
|
+
const first = resolved.paragraph;
|
|
3145
|
+
const hasEnd = options._compiledSourceEndId || targetEndRef != null || options.targetEndDescriptor;
|
|
3146
|
+
if (!hasEnd) return { paragraphs: [first] };
|
|
3147
|
+
let paragraphs = null;
|
|
3148
|
+
if (options._compiledSourceEndId || options.targetEndDescriptor) {
|
|
3149
|
+
const endResolved = resolveTargetParagraph(
|
|
3150
|
+
xmlDoc,
|
|
3151
|
+
options.targetEndDescriptor?.text || '',
|
|
3152
|
+
options._compiledSourceEndId ? targetEndRef : (options.targetEndDescriptor?.index ?? targetEndRef),
|
|
3153
|
+
opType,
|
|
3154
|
+
runtimeContext,
|
|
3155
|
+
{
|
|
3156
|
+
...options,
|
|
3157
|
+
...(options.targetEndDescriptor ? { targetDescriptor: options.targetEndDescriptor } : {}),
|
|
3158
|
+
_compiledSourceId: options._compiledSourceEndId || null,
|
|
3159
|
+
_resolutionCapture: null
|
|
3160
|
+
}
|
|
3161
|
+
);
|
|
3162
|
+
if (endResolved?.error) return { error: endResolved.error };
|
|
3163
|
+
const all = endResolved?.paragraph ? getDocumentParagraphNodes(xmlDoc) : [];
|
|
3164
|
+
const startIndex = all.indexOf(first);
|
|
3165
|
+
const endIndex = all.indexOf(endResolved?.paragraph);
|
|
3166
|
+
paragraphs = startIndex >= 0 && endIndex >= startIndex ? all.slice(startIndex, endIndex + 1) : null;
|
|
3167
|
+
} else {
|
|
3168
|
+
paragraphs = resolveParagraphRangeByRefs(xmlDoc, targetRef, targetEndRef, {
|
|
3169
|
+
opType,
|
|
3170
|
+
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
3171
|
+
onInfo: options.onInfo,
|
|
3172
|
+
onWarn: options.onWarn
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
if (!Array.isArray(paragraphs) || paragraphs.length === 0 || paragraphs[0] !== first) {
|
|
3176
|
+
return { error: { code: 'TARGET_RANGE_INVALID', message: 'targetEnd must resolve to the target paragraph or a paragraph after it.' } };
|
|
3177
|
+
}
|
|
3178
|
+
return { paragraphs };
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
/**
|
|
3182
|
+
* `list-format` (`convert-plain`): binds plain paragraphs to a new list in place; see services/list-format-mutation.js.
|
|
3183
|
+
*/
|
|
3184
|
+
export async function applyListFormatToParagraphByExactText(
|
|
3185
|
+
documentXml,
|
|
3186
|
+
targetText,
|
|
3187
|
+
list,
|
|
3188
|
+
author,
|
|
3189
|
+
targetRef = null,
|
|
3190
|
+
targetEndRef = null,
|
|
3191
|
+
runtimeContext = null,
|
|
3192
|
+
options = {}
|
|
3193
|
+
) {
|
|
3194
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
3195
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
3196
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
3197
|
+
const { serializer, xmlDoc, operationSession } = resolveMutationDocument(documentXml, options);
|
|
3198
|
+
if (!xmlDoc) {
|
|
3199
|
+
return { documentXml, hasChanges: false, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
3200
|
+
}
|
|
3201
|
+
const revisionIdAllocator = operationSession?.revisionIdAllocator || prepareRevisionAllocator(xmlDoc, options);
|
|
3202
|
+
const range = resolveOperationParagraphRange(xmlDoc, targetText, targetRef, targetEndRef, 'list-format', runtimeContext, {
|
|
3203
|
+
...options,
|
|
3204
|
+
onInfo,
|
|
3205
|
+
onWarn
|
|
3206
|
+
});
|
|
3207
|
+
if (range.error) return { documentXml, hasChanges: false, status: 'error', error: range.error };
|
|
3208
|
+
const paragraphs = range.paragraphs;
|
|
3209
|
+
if (paragraphs.some(paragraph => paragraph.parentNode !== paragraphs[0].parentNode)) {
|
|
3210
|
+
return {
|
|
3211
|
+
documentXml,
|
|
3212
|
+
hasChanges: false,
|
|
3213
|
+
status: 'error',
|
|
3214
|
+
error: {
|
|
3215
|
+
code: 'TARGET_RANGE_INVALID',
|
|
3216
|
+
message: 'A list-format range must stay inside one container (the body, or one table cell).'
|
|
3217
|
+
}
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
const allParagraphs = getDocumentParagraphNodes(xmlDoc);
|
|
3221
|
+
const request = normalizeListFormatRequest(list);
|
|
3222
|
+
if (request.mode === 'change-format' || request.mode === 'join') {
|
|
3223
|
+
return applyListRebinding(xmlDoc, paragraphs, request, {
|
|
3224
|
+
documentXml, serializer, operationSession, runtimeContext, options, author, generateRedlines, onInfo, onWarn,
|
|
3225
|
+
paragraphIndexOf: paragraph => {
|
|
3226
|
+
const index = allParagraphs.indexOf(paragraph);
|
|
3227
|
+
return index >= 0 ? index + 1 : null;
|
|
3228
|
+
}
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
const conversion = convertPlainParagraphsToList(xmlDoc, paragraphs, request, {
|
|
3232
|
+
author: author || getDefaultAuthor(),
|
|
3233
|
+
generateRedlines,
|
|
3234
|
+
revisionIdAllocator,
|
|
3235
|
+
runtimeContext,
|
|
3236
|
+
stylesXml: runtimeContext?.stylesXml || options.stylesXml || null,
|
|
3237
|
+
numberingXml: runtimeContext?.numberingXml || options.numberingXml || null,
|
|
3238
|
+
paragraphIndexOf: paragraph => allParagraphs.indexOf(paragraph) + 1
|
|
3239
|
+
});
|
|
3240
|
+
if (!conversion.ok) return { documentXml, hasChanges: false, status: 'error', error: conversion.error };
|
|
3241
|
+
|
|
3242
|
+
for (const item of conversion.items) {
|
|
3243
|
+
ensureParagraphIdsOnImportedNode(item.paragraph, operationSession);
|
|
3244
|
+
options?._mutationLiveNodes?.push(item.paragraph);
|
|
3245
|
+
}
|
|
3246
|
+
for (const numId of conversion.allocatedNumIds) operationSession?.receiptCollector?.recordNumbering(numId);
|
|
3247
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
3248
|
+
onInfo(`[List] list-format convert-plain bound ${conversion.items.length} paragraph(s) as ${conversion.format} (numId ${[...new Set(conversion.items.map(item => item.numId))].join(', ')}).`);
|
|
3249
|
+
|
|
3250
|
+
return {
|
|
3251
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
3252
|
+
hasChanges: true,
|
|
3253
|
+
status: 'ok',
|
|
3254
|
+
numberingXml: conversion.numberingXml,
|
|
3255
|
+
list: {
|
|
3256
|
+
mode: 'convert-plain',
|
|
3257
|
+
format: conversion.format,
|
|
3258
|
+
level: conversion.level,
|
|
3259
|
+
strip: conversion.strip,
|
|
3260
|
+
items: conversion.items.map(item => ({
|
|
3261
|
+
paragraphIndex: item.paragraphIndex,
|
|
3262
|
+
paragraphId: getParagraphId(item.paragraph),
|
|
3263
|
+
marker: item.marker,
|
|
3264
|
+
value: item.value,
|
|
3265
|
+
numId: item.numId
|
|
3266
|
+
})),
|
|
3267
|
+
skipped: conversion.skipped.map(item => ({ paragraphIndex: item.paragraphIndex, reason: item.reason }))
|
|
3268
|
+
}
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
/**
|
|
3273
|
+
* `list-format` `change-format` / `join`: rebinds existing list items (see services/list-format-change.js). The
|
|
3274
|
+
* definitions are read from the source numbering part plus the parts earlier operations of this batch allocated,
|
|
3275
|
+
* so a later operation can change a list an earlier one created or rebound.
|
|
3276
|
+
*/
|
|
3277
|
+
function applyListRebinding(xmlDoc, paragraphs, request, ctx) {
|
|
3278
|
+
const { documentXml, serializer, operationSession, runtimeContext, options, author, generateRedlines, onInfo, paragraphIndexOf } = ctx;
|
|
3279
|
+
const refused = error => ({ documentXml, hasChanges: false, status: 'error', error });
|
|
3280
|
+
const sourceNumberingXml = runtimeContext?.numberingIdState?.sourceNumberingXml
|
|
3281
|
+
|| runtimeContext?.numberingXml
|
|
3282
|
+
|| options.numberingXml
|
|
3283
|
+
|| '';
|
|
3284
|
+
const pendingParts = Array.isArray(operationSession?.numberingXmlParts) ? operationSession.numberingXmlParts : [];
|
|
3285
|
+
const numberingXml = pendingParts.length > 0 ? mergeNumberingParts(sourceNumberingXml, pendingParts) : sourceNumberingXml;
|
|
3286
|
+
const shared = {
|
|
3287
|
+
author: author || getDefaultAuthor(),
|
|
3288
|
+
generateRedlines,
|
|
3289
|
+
runtimeContext,
|
|
3290
|
+
stylesXml: runtimeContext?.stylesXml || options.stylesXml || null,
|
|
3291
|
+
numberingXml,
|
|
3292
|
+
paragraphIndexOf
|
|
3293
|
+
};
|
|
3294
|
+
|
|
3295
|
+
let outcome;
|
|
3296
|
+
if (request.mode === 'join') {
|
|
3297
|
+
const descriptor = options.continueFromDescriptor || null;
|
|
3298
|
+
const resolved = descriptor
|
|
3299
|
+
? resolveTargetParagraph(xmlDoc, descriptor.text || '', descriptor.index ?? null, 'list-format', runtimeContext, {
|
|
3300
|
+
...options,
|
|
3301
|
+
targetDescriptor: descriptor,
|
|
3302
|
+
_compiledSourceId: options._compiledContinueFromId || null,
|
|
3303
|
+
_compiledResolvedBy: null,
|
|
3304
|
+
_resolutionCapture: null
|
|
3305
|
+
})
|
|
3306
|
+
: null;
|
|
3307
|
+
if (!resolved?.paragraph) {
|
|
3308
|
+
return refused({
|
|
3309
|
+
code: resolved?.error?.code || 'TARGET_NOT_FOUND',
|
|
3310
|
+
message: `list.continueFrom: ${resolved?.error?.message || 'paragraph not found.'}`,
|
|
3311
|
+
field: 'list.continueFrom'
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
outcome = joinListParagraphs(xmlDoc, paragraphs, resolved.paragraph, request, shared);
|
|
3315
|
+
} else {
|
|
3316
|
+
outcome = changeListParagraphsFormat(xmlDoc, paragraphs, request, shared);
|
|
3317
|
+
}
|
|
3318
|
+
if (!outcome.ok) return refused(outcome.error);
|
|
3319
|
+
if (outcome.unchanged) {
|
|
3320
|
+
const warning = `${LIST_FORMAT_UNCHANGED}: ${outcome.message}`;
|
|
3321
|
+
ctx.onWarn?.(warning);
|
|
3322
|
+
return { documentXml, hasChanges: false, status: 'ok', warnings: [warning] };
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
for (const item of outcome.items) {
|
|
3326
|
+
ensureParagraphIdsOnImportedNode(item.paragraph, operationSession);
|
|
3327
|
+
options?._mutationLiveNodes?.push(item.paragraph);
|
|
3328
|
+
}
|
|
3329
|
+
for (const numId of outcome.allocatedNumIds) operationSession?.receiptCollector?.recordNumbering(numId);
|
|
3330
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
3331
|
+
const outside = outcome.items.filter(item => !item.inTarget).length;
|
|
3332
|
+
onInfo(`[List] list-format ${outcome.mode} (${outcome.scope}) rebound ${outcome.items.length} paragraph(s) from numId ${outcome.sourceNumId} to numId ${outcome.numId}${outside ? ` (${outside} outside the target range)` : ''}.`);
|
|
3333
|
+
|
|
3334
|
+
return {
|
|
3335
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
3336
|
+
hasChanges: true,
|
|
3337
|
+
status: 'ok',
|
|
3338
|
+
numberingXml: outcome.numberingXml,
|
|
3339
|
+
list: {
|
|
3340
|
+
mode: outcome.mode,
|
|
3341
|
+
scope: outcome.scope,
|
|
3342
|
+
sourceNumId: outcome.sourceNumId,
|
|
3343
|
+
numId: outcome.numId,
|
|
3344
|
+
...(outcome.mode === 'change-format'
|
|
3345
|
+
? { format: outcome.format, level: outcome.level, lvlText: outcome.lvlText, levelStarts: outcome.levelStarts }
|
|
3346
|
+
: { continueFrom: outcome.continueFrom }),
|
|
3347
|
+
items: outcome.items.map(item => ({
|
|
3348
|
+
paragraphIndex: item.paragraphIndex,
|
|
3349
|
+
paragraphId: getParagraphId(item.paragraph),
|
|
3350
|
+
level: item.level,
|
|
3351
|
+
numId: item.numId,
|
|
3352
|
+
sourceNumId: item.sourceNumId,
|
|
3353
|
+
inTarget: item.inTarget
|
|
3354
|
+
})),
|
|
3355
|
+
skipped: outcome.skipped.map(item => ({ paragraphIndex: item.paragraphIndex, reason: item.reason }))
|
|
3356
|
+
}
|
|
2720
3357
|
};
|
|
2721
3358
|
}
|