@ansonlai/docx-redline-js 0.8.3 → 0.9.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 +221 -12
- package/CHANGELOG.md +803 -586
- package/README.md +1018 -901
- 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/dist/docx-redline-js.esm.js +4551 -967
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +112 -106
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4690 -1106
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4690 -1106
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +23 -4
- package/docs/SKILL_AUTHORING.md +165 -150
- package/docs/TESTING.md +1142 -1059
- package/docs/schemas/document-operations.schema.json +221 -168
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +567 -560
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-mode.js +183 -183
- 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 +51 -1
- package/index.js +47 -3
- package/node/cli-help.js +1 -0
- package/node/cli.js +992 -974
- 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/pipeline.js +206 -204
- package/pipeline/serialization.js +111 -94
- 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 +26 -0
- package/services/capture-engine.js +228 -226
- package/services/document-operation-applier.js +215 -0
- package/services/document-operation-contract.js +628 -537
- package/services/document-operation-mutations.js +789 -46
- package/services/error-recovery.js +17 -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 +612 -436
- package/services/operation-heuristics.js +197 -173
- package/services/operation-preflight.js +1 -0
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +634 -594
- package/services/revision-fidelity-verifier.js +498 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +606 -427
- package/services/table-reconciliation.js +103 -49
|
@@ -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
21
|
import { containsTrackedChanges, getTrackedChangeAuthors, createWordElement, 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,
|
|
@@ -42,6 +54,7 @@ import {
|
|
|
42
54
|
getParagraphId,
|
|
43
55
|
createParagraphFingerprint,
|
|
44
56
|
isMarkdownTableText,
|
|
57
|
+
normalizeWhitespaceForTargeting,
|
|
45
58
|
findContainingWordElement,
|
|
46
59
|
resolveTargetParagraphWithSnapshot as resolveTargetParagraphWithSnapshotShared,
|
|
47
60
|
resolveParagraphRangeByRefs,
|
|
@@ -70,11 +83,19 @@ import {
|
|
|
70
83
|
import {
|
|
71
84
|
reserveNextNumberingIdPair,
|
|
72
85
|
remapNumberingPayloadForDocument,
|
|
73
|
-
reuseSourceListNumbering,
|
|
74
86
|
overwriteParagraphNumIds,
|
|
75
87
|
extractFirstParagraphNumId,
|
|
76
88
|
buildExplicitDecimalMultilevelNumberingXml
|
|
77
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';
|
|
78
99
|
import {
|
|
79
100
|
extractReplacementNodesFromOoxml,
|
|
80
101
|
normalizeBodySectionOrderStandalone
|
|
@@ -82,9 +103,12 @@ import {
|
|
|
82
103
|
import { prepareRevisionAllocator } from './document-operation-session.js';
|
|
83
104
|
import {
|
|
84
105
|
buildExplicitRangeInsertionEntries,
|
|
106
|
+
deriveParagraphThenTableAppend,
|
|
85
107
|
deriveSingleParagraphListAdjacencyInsertion,
|
|
86
108
|
deriveSingleParagraphPlainAdjacencyInsertion
|
|
87
109
|
} from './operation-heuristics.js';
|
|
110
|
+
import { generateTableOoxml } from './table-reconciliation.js';
|
|
111
|
+
import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
|
|
88
112
|
import { resolveTargetFromCapture, ensureParagraphIdsOnImportedNode } from './capture-engine.js';
|
|
89
113
|
import {
|
|
90
114
|
acceptTrackedChangesInOoxml,
|
|
@@ -320,6 +344,52 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
320
344
|
return resolved;
|
|
321
345
|
}
|
|
322
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
|
+
|
|
323
393
|
function extractReplacementNodes(outputOxml) {
|
|
324
394
|
return extractReplacementNodesFromOoxml(outputOxml);
|
|
325
395
|
}
|
|
@@ -426,7 +496,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
|
|
|
426
496
|
// remove the list paragraph itself on Reject All instead of leaving an
|
|
427
497
|
// empty bullet or number behind.
|
|
428
498
|
if (generateRedlines) {
|
|
429
|
-
|
|
499
|
+
markParagraphInserted(paragraph, { author });
|
|
430
500
|
}
|
|
431
501
|
|
|
432
502
|
const run = createWordElement(xmlDoc, 'w:r');
|
|
@@ -518,7 +588,7 @@ function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionMetadata, aut
|
|
|
518
588
|
|
|
519
589
|
if (generateRedlines) {
|
|
520
590
|
if (options.trackParagraphMark === true) {
|
|
521
|
-
|
|
591
|
+
markParagraphInserted(paragraph, { author });
|
|
522
592
|
}
|
|
523
593
|
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, 'ins');
|
|
524
594
|
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
@@ -559,7 +629,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
|
|
|
559
629
|
if (pPr) wrappedParagraph.appendChild(pPr);
|
|
560
630
|
if (options.paragraphId) wrappedParagraph.setAttributeNS(NS_W14, 'w14:paraId', options.paragraphId);
|
|
561
631
|
if (options.trackParagraphMark === true) {
|
|
562
|
-
|
|
632
|
+
markParagraphInserted(wrappedParagraph, { author });
|
|
563
633
|
}
|
|
564
634
|
|
|
565
635
|
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
@@ -620,8 +690,9 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text, revisi
|
|
|
620
690
|
function collectNumberingIdsFromNodes(nodes) {
|
|
621
691
|
const ids = new Set();
|
|
622
692
|
for (const node of nodes || []) {
|
|
623
|
-
|
|
624
|
-
|
|
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)) {
|
|
625
696
|
const val = numIdNode.getAttribute('w:val') || numIdNode.getAttribute('val');
|
|
626
697
|
if (val != null && val !== '') {
|
|
627
698
|
ids.add(String(val));
|
|
@@ -1379,20 +1450,6 @@ async function tryExplicitDecimalHeaderListConversion({
|
|
|
1379
1450
|
if (!strippedContent) return null;
|
|
1380
1451
|
|
|
1381
1452
|
onInfo('[List] Applying explicit numeric header conversion with direct list binding.');
|
|
1382
|
-
const redlineResult = await applyRedlineToOxml(
|
|
1383
|
-
serializer.serializeToString(targetParagraph),
|
|
1384
|
-
currentParagraphText,
|
|
1385
|
-
strippedContent,
|
|
1386
|
-
{
|
|
1387
|
-
author,
|
|
1388
|
-
generateRedlines,
|
|
1389
|
-
_revisionIdAllocator: revisionIdAllocator
|
|
1390
|
-
}
|
|
1391
|
-
);
|
|
1392
|
-
if (!redlineResult?.hasChanges || typeof redlineResult?.oxml !== 'string') return null;
|
|
1393
|
-
|
|
1394
|
-
const extracted = extractReplacementNodes(redlineResult.oxml);
|
|
1395
|
-
const replacementNodes = extracted.replacementNodes;
|
|
1396
1453
|
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
1397
1454
|
explicitPlan,
|
|
1398
1455
|
runtimeContext?.listFallbackSequenceState || null
|
|
@@ -1423,6 +1480,22 @@ async function tryExplicitDecimalHeaderListConversion({
|
|
|
1423
1480
|
onInfo(`[List] Using isolated explicit-start numbering (start ${explicitStart}, numId ${appliedNumId}, abstractNumId ${reservedPair.abstractNumId}).`);
|
|
1424
1481
|
}
|
|
1425
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
|
+
|
|
1426
1499
|
if (explicitPlan.numberingKey && runtimeContext?.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1427
1500
|
runtimeContext.listFallbackSharedNumIdByKey.delete(explicitPlan.numberingKey);
|
|
1428
1501
|
}
|
|
@@ -1445,7 +1518,8 @@ async function tryExplicitDecimalHeaderListConversion({
|
|
|
1445
1518
|
numId: appliedNumId,
|
|
1446
1519
|
ilvl: 0,
|
|
1447
1520
|
clearParagraphPropertyChanges: true,
|
|
1448
|
-
removeListPropertyNode: true
|
|
1521
|
+
removeListPropertyNode: true,
|
|
1522
|
+
trackPropertyChange: generateRedlines ? { author, revisionIdAllocator } : null
|
|
1449
1523
|
});
|
|
1450
1524
|
|
|
1451
1525
|
const parent = targetParagraph.parentNode;
|
|
@@ -1472,6 +1546,145 @@ async function tryExplicitDecimalHeaderListConversion({
|
|
|
1472
1546
|
};
|
|
1473
1547
|
}
|
|
1474
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
|
+
|
|
1475
1688
|
async function trySingleParagraphListStructuralFallback({
|
|
1476
1689
|
xmlDoc,
|
|
1477
1690
|
serializer,
|
|
@@ -1513,7 +1726,11 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1513
1726
|
const extracted = extractReplacementNodes(fallbackResult.oxml);
|
|
1514
1727
|
let replacementNodes = extracted.replacementNodes;
|
|
1515
1728
|
let numberingXml = extracted.numberingXml || fallbackResult?.numberingXml || null;
|
|
1516
|
-
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;
|
|
1517
1734
|
const numberingKey = fallbackResult?.listStructuralFallbackKey || fallbackPlan?.numberingKey || null;
|
|
1518
1735
|
const numberingAction = resolveSingleLineListFallbackNumberingAction(
|
|
1519
1736
|
fallbackPlan,
|
|
@@ -1569,7 +1786,8 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1569
1786
|
numId: explicitNumIdForBinding,
|
|
1570
1787
|
ilvl: 0,
|
|
1571
1788
|
clearParagraphPropertyChanges: true,
|
|
1572
|
-
removeListPropertyNode: true
|
|
1789
|
+
removeListPropertyNode: true,
|
|
1790
|
+
trackPropertyChange: generateRedlines ? { author, revisionIdAllocator } : null
|
|
1573
1791
|
});
|
|
1574
1792
|
}
|
|
1575
1793
|
} else {
|
|
@@ -1584,7 +1802,23 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1584
1802
|
);
|
|
1585
1803
|
}
|
|
1586
1804
|
|
|
1587
|
-
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) {
|
|
1588
1822
|
const sharedNumId = numberingKey ? runtimeContext.listFallbackSharedNumIdByKey.get(numberingKey) : null;
|
|
1589
1823
|
if (sharedNumId) {
|
|
1590
1824
|
overwriteParagraphNumIds(replacementNodes, sharedNumId);
|
|
@@ -1601,6 +1835,7 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1601
1835
|
|
|
1602
1836
|
const parent = targetParagraph.parentNode;
|
|
1603
1837
|
if (!parent) return null;
|
|
1838
|
+
if (generateRedlines) restoreSourceIdentityAndPropertiesOnFallback(xmlDoc, replacementNodes, targetParagraph);
|
|
1604
1839
|
for (const node of replacementNodes) {
|
|
1605
1840
|
const imported = xmlDoc.importNode(node, true);
|
|
1606
1841
|
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
@@ -1623,6 +1858,43 @@ async function trySingleParagraphListStructuralFallback({
|
|
|
1623
1858
|
};
|
|
1624
1859
|
}
|
|
1625
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
|
+
|
|
1626
1898
|
export async function applyToParagraphByExactText(documentXml, targetText, modifiedText, author, targetRef = null, targetEndRef = null, runtimeContext = null, options = {}) {
|
|
1627
1899
|
const generateRedlines = options.generateRedlines !== false;
|
|
1628
1900
|
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
@@ -1798,7 +2070,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1798
2070
|
&& !(hasRevisions && existingPolicy === 'merge-same-author')
|
|
1799
2071
|
) {
|
|
1800
2072
|
if (generateRedlines) {
|
|
1801
|
-
|
|
2073
|
+
markParagraphDeleted(targetParagraph, { author });
|
|
1802
2074
|
options?._mutationLiveNodes?.push(targetParagraph);
|
|
1803
2075
|
} else {
|
|
1804
2076
|
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
@@ -1843,7 +2115,31 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1843
2115
|
let effectiveModifiedText = synthesizedTableMarkdown || modifiedText;
|
|
1844
2116
|
const useTableScope = !!containingTable && isMarkdownTableText(effectiveModifiedText);
|
|
1845
2117
|
const isTableMarkdownEdit = isMarkdownTableText(effectiveModifiedText);
|
|
1846
|
-
|
|
2118
|
+
// A targetEnd given as a compact handle (paragraphId + fingerprint, no numeric index) has no targetEndRef;
|
|
2119
|
+
// its range comes from the batch-compiled end or the end descriptor, never silently dropped.
|
|
2120
|
+
let compiledEndRange = null;
|
|
2121
|
+
if (!targetEndRef && (options._compiledSourceEndId || options.targetEndDescriptor)) {
|
|
2122
|
+
const rangeResolution = resolveOperationParagraphRange(xmlDoc, targetText, targetRef, targetEndRef, 'redline', runtimeContext, {
|
|
2123
|
+
...options,
|
|
2124
|
+
_resolutionCapture: null
|
|
2125
|
+
});
|
|
2126
|
+
if (rangeResolution.error) {
|
|
2127
|
+
return { documentXml, hasChanges: false, status: 'error', error: rangeResolution.error };
|
|
2128
|
+
}
|
|
2129
|
+
if (rangeResolution.paragraphs.length > 1) {
|
|
2130
|
+
const container = rangeResolution.paragraphs[0].parentNode;
|
|
2131
|
+
if (rangeResolution.paragraphs.some(paragraph => paragraph.parentNode !== container)) {
|
|
2132
|
+
return {
|
|
2133
|
+
documentXml,
|
|
2134
|
+
hasChanges: false,
|
|
2135
|
+
status: 'error',
|
|
2136
|
+
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.' }
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
compiledEndRange = rangeResolution.paragraphs;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const explicitRangeParagraphs = compiledEndRange ? compiledEndRange : targetEndRef
|
|
1847
2143
|
? resolveParagraphRangeByRefs(xmlDoc, targetRef, targetEndRef, {
|
|
1848
2144
|
opType: 'redline',
|
|
1849
2145
|
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
@@ -1861,6 +2157,13 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1861
2157
|
for (const entry of insertionEntries) {
|
|
1862
2158
|
onInfo(`[List] Explicit-range insertion: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
1863
2159
|
}
|
|
2160
|
+
const rangeFormatDiagnostic = diagnoseListFormatNotApplied({
|
|
2161
|
+
sourceParagraphs: explicitRangeParagraphs,
|
|
2162
|
+
modifiedText: effectiveModifiedText,
|
|
2163
|
+
runtimeContext,
|
|
2164
|
+
options
|
|
2165
|
+
});
|
|
2166
|
+
if (rangeFormatDiagnostic?.severity === 'error') return listDiagnosticFailure(documentXml, rangeFormatDiagnostic);
|
|
1864
2167
|
const applied = applyExplicitRangeListInsertions({
|
|
1865
2168
|
xmlDoc,
|
|
1866
2169
|
explicitRangeParagraphs,
|
|
@@ -1872,7 +2175,8 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1872
2175
|
return {
|
|
1873
2176
|
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
1874
2177
|
hasChanges: true,
|
|
1875
|
-
numberingXml: null
|
|
2178
|
+
numberingXml: null,
|
|
2179
|
+
...listDiagnosticFields(rangeFormatDiagnostic ? [rangeFormatDiagnostic] : [])
|
|
1876
2180
|
};
|
|
1877
2181
|
}
|
|
1878
2182
|
}
|
|
@@ -1945,6 +2249,105 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1945
2249
|
};
|
|
1946
2250
|
}
|
|
1947
2251
|
|
|
2252
|
+
// Retained paragraph followed by a Markdown table: keep the source paragraph in place (so its own
|
|
2253
|
+
// content, formatting and revisions are untouched), apply any requested inline formatting to it,
|
|
2254
|
+
// and insert the table after it instead of rewriting the paragraph as a deletion plus a copy.
|
|
2255
|
+
const tableAppendCandidate = (
|
|
2256
|
+
!useTableScope
|
|
2257
|
+
&& !hasExplicitRangeScope
|
|
2258
|
+
&& !targetListInfo
|
|
2259
|
+
&& !containingTable
|
|
2260
|
+
&& !targetEndRef
|
|
2261
|
+
)
|
|
2262
|
+
? deriveParagraphThenTableAppend(effectiveModifiedText)
|
|
2263
|
+
: null;
|
|
2264
|
+
const tableAppendParagraph = tableAppendCandidate
|
|
2265
|
+
? preprocessMarkdown(tableAppendCandidate.paragraphMarkdown)
|
|
2266
|
+
: null;
|
|
2267
|
+
if (
|
|
2268
|
+
tableAppendCandidate
|
|
2269
|
+
&& normalizeWhitespaceForTargeting(tableAppendParagraph.cleanText) === normalizeWhitespaceForTargeting(currentParagraphText)
|
|
2270
|
+
) {
|
|
2271
|
+
const tableData = parseMarkdownTable(tableAppendCandidate.tableText);
|
|
2272
|
+
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
2273
|
+
onInfo('[Table] Applying retained-paragraph table append heuristic.');
|
|
2274
|
+
const parent = targetParagraph.parentNode;
|
|
2275
|
+
if (!parent) throw new Error('Target paragraph has no parent for table append');
|
|
2276
|
+
const followingNode = targetParagraph.nextSibling;
|
|
2277
|
+
if (tableAppendParagraph.formatHints.length > 0) {
|
|
2278
|
+
const formatted = await applyRedlineToOxml(
|
|
2279
|
+
serializer.serializeToString(targetParagraph),
|
|
2280
|
+
currentParagraphText || targetText,
|
|
2281
|
+
tableAppendCandidate.paragraphMarkdown,
|
|
2282
|
+
{
|
|
2283
|
+
author,
|
|
2284
|
+
generateRedlines,
|
|
2285
|
+
existingRevisions: options.existingRevisions || 'merge-same-author',
|
|
2286
|
+
structuredContent: false,
|
|
2287
|
+
_revisionIdAllocator: revisionIdAllocator
|
|
2288
|
+
}
|
|
2289
|
+
);
|
|
2290
|
+
if (formatted?.status === 'error') {
|
|
2291
|
+
return {
|
|
2292
|
+
documentXml,
|
|
2293
|
+
hasChanges: false,
|
|
2294
|
+
numberingXml: null,
|
|
2295
|
+
status: 'error',
|
|
2296
|
+
error: formatted.error
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
if (formatted?.hasChanges) {
|
|
2300
|
+
if (typeof formatted.oxml !== 'string') {
|
|
2301
|
+
throw new Error('Reconciliation engine did not return OOXML for a changed paragraph format');
|
|
2302
|
+
}
|
|
2303
|
+
for (const node of extractReplacementNodes(formatted.oxml).replacementNodes) {
|
|
2304
|
+
const imported = xmlDoc.importNode(node, true);
|
|
2305
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
2306
|
+
options?._mutationLiveNodes?.push(parent.insertBefore(imported, targetParagraph));
|
|
2307
|
+
}
|
|
2308
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
2309
|
+
parent.removeChild(targetParagraph);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
const tableOoxml = generateTableOoxml(tableData, {
|
|
2313
|
+
generateRedlines,
|
|
2314
|
+
author,
|
|
2315
|
+
revisionIdAllocator,
|
|
2316
|
+
trackAsBlock: true
|
|
2317
|
+
});
|
|
2318
|
+
for (const node of extractReplacementNodes(tableOoxml).replacementNodes) {
|
|
2319
|
+
options?._mutationLiveNodes?.push(parent.insertBefore(xmlDoc.importNode(node, true), followingNode));
|
|
2320
|
+
}
|
|
2321
|
+
// Word cannot end the body (or a container) with a table: it would add an untracked paragraph
|
|
2322
|
+
// there that Reject All leaves behind. Supply the paragraph ourselves with a tracked-inserted
|
|
2323
|
+
// mark so Reject All removes it and the source paragraph count is restored.
|
|
2324
|
+
// Skip whitespace/comment nodes of pretty-printed documents when deciding what follows the table.
|
|
2325
|
+
let followingElement = followingNode;
|
|
2326
|
+
while (followingElement && followingElement.nodeType !== 1) followingElement = followingElement.nextSibling;
|
|
2327
|
+
const endsContainer = !followingElement
|
|
2328
|
+
|| (followingElement.namespaceURI === NS_W && followingElement.localName === 'sectPr');
|
|
2329
|
+
if (endsContainer) {
|
|
2330
|
+
const trailing = createWordElement(xmlDoc, 'w:p');
|
|
2331
|
+
const trailingPPr = clonePropertiesWithoutRevisionHistory(getDirectWordChild(targetParagraph, 'pPr'));
|
|
2332
|
+
if (trailingPPr) {
|
|
2333
|
+
for (const child of Array.from(trailingPPr.childNodes)) {
|
|
2334
|
+
if (child.nodeType === 1 && ['sectPr', 'numPr'].includes(child.localName)) trailingPPr.removeChild(child);
|
|
2335
|
+
}
|
|
2336
|
+
trailing.appendChild(trailingPPr);
|
|
2337
|
+
}
|
|
2338
|
+
if (generateRedlines) markParagraphInserted(trailing, { author });
|
|
2339
|
+
ensureParagraphIdsOnImportedNode(trailing, operationSession);
|
|
2340
|
+
options?._mutationLiveNodes?.push(parent.insertBefore(trailing, followingNode));
|
|
2341
|
+
}
|
|
2342
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
2343
|
+
return {
|
|
2344
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2345
|
+
hasChanges: true,
|
|
2346
|
+
numberingXml: null
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
1948
2351
|
const plainAdjacencyInsertionCandidate = (
|
|
1949
2352
|
!bypassSingleParagraphHeuristics
|
|
1950
2353
|
&& !useTableScope
|
|
@@ -1998,6 +2401,13 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
1998
2401
|
})
|
|
1999
2402
|
: null;
|
|
2000
2403
|
if (insertionOnlyPlan && insertionOnlyPlan.entries.length > 0) {
|
|
2404
|
+
const insertionFormatDiagnostic = diagnoseListFormatNotApplied({
|
|
2405
|
+
sourceParagraphs: [targetParagraph],
|
|
2406
|
+
modifiedText: effectiveModifiedText,
|
|
2407
|
+
runtimeContext,
|
|
2408
|
+
options
|
|
2409
|
+
});
|
|
2410
|
+
if (insertionFormatDiagnostic?.severity === 'error') return listDiagnosticFailure(documentXml, insertionFormatDiagnostic);
|
|
2001
2411
|
onInfo(`[List] Applying insertion-only list redline heuristic (${insertionOnlyPlan.entries.length} new item(s)).`);
|
|
2002
2412
|
for (const entry of insertionOnlyPlan.entries) {
|
|
2003
2413
|
onInfo(`[List] Insertion entry resolved: ilvl=${entry.ilvl}, markerType=${entry.markerType}, text="${String(entry.text || '').slice(0, 80)}${String(entry.text || '').length > 80 ? '…' : ''}"`);
|
|
@@ -2022,7 +2432,8 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2022
2432
|
return {
|
|
2023
2433
|
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2024
2434
|
hasChanges: true,
|
|
2025
|
-
numberingXml: null
|
|
2435
|
+
numberingXml: null,
|
|
2436
|
+
...listDiagnosticFields(insertionFormatDiagnostic ? [insertionFormatDiagnostic] : [])
|
|
2026
2437
|
};
|
|
2027
2438
|
}
|
|
2028
2439
|
|
|
@@ -2038,6 +2449,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2038
2449
|
effectiveModifiedText = listScopeEdit.modifiedText;
|
|
2039
2450
|
}
|
|
2040
2451
|
|
|
2452
|
+
let unboundMarkerDiagnostic = null;
|
|
2041
2453
|
if (!bypassSingleParagraphHeuristics && !useTableScope && !useListScope && !hasExplicitRangeScope) {
|
|
2042
2454
|
const explicitHeaderListConversion = await tryExplicitDecimalHeaderListConversion({
|
|
2043
2455
|
xmlDoc,
|
|
@@ -2056,6 +2468,23 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2056
2468
|
});
|
|
2057
2469
|
if (explicitHeaderListConversion) return explicitHeaderListConversion;
|
|
2058
2470
|
|
|
2471
|
+
const inPlaceConversion = tryInPlaceMarkerListConversion({
|
|
2472
|
+
xmlDoc,
|
|
2473
|
+
serializer,
|
|
2474
|
+
documentXml,
|
|
2475
|
+
operationSession,
|
|
2476
|
+
targetParagraph,
|
|
2477
|
+
currentParagraphText,
|
|
2478
|
+
modifiedText: effectiveModifiedText,
|
|
2479
|
+
author,
|
|
2480
|
+
runtimeContext,
|
|
2481
|
+
revisionIdAllocator,
|
|
2482
|
+
generateRedlines,
|
|
2483
|
+
onInfo,
|
|
2484
|
+
options
|
|
2485
|
+
});
|
|
2486
|
+
if (inPlaceConversion) return inPlaceConversion;
|
|
2487
|
+
|
|
2059
2488
|
const listFallback = await trySingleParagraphListStructuralFallback({
|
|
2060
2489
|
xmlDoc,
|
|
2061
2490
|
serializer,
|
|
@@ -2072,6 +2501,25 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2072
2501
|
options
|
|
2073
2502
|
});
|
|
2074
2503
|
if (listFallback) return listFallback;
|
|
2504
|
+
|
|
2505
|
+
// Both structural fallbacks declined. A plain paragraph whose new text starts a list marker
|
|
2506
|
+
// would be written as literal text: report that instead of returning a silent success.
|
|
2507
|
+
const unboundPolicy = resolveListDiagnosticPolicy(options);
|
|
2508
|
+
if (unboundPolicy !== 'off' && !targetListInfo) {
|
|
2509
|
+
const requestedMarker = detectUnboundListMarker({
|
|
2510
|
+
originalText: currentParagraphText,
|
|
2511
|
+
modifiedText: effectiveModifiedText,
|
|
2512
|
+
targetIsListItem: false
|
|
2513
|
+
});
|
|
2514
|
+
if (requestedMarker) {
|
|
2515
|
+
unboundMarkerDiagnostic = buildUnboundListMarkerDiagnostic(requestedMarker, {
|
|
2516
|
+
policy: unboundPolicy,
|
|
2517
|
+
targetText: currentParagraphText,
|
|
2518
|
+
operationIndex: options?._operationIndex
|
|
2519
|
+
});
|
|
2520
|
+
if (unboundPolicy === 'error') return listDiagnosticFailure(documentXml, unboundMarkerDiagnostic);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2075
2523
|
}
|
|
2076
2524
|
|
|
2077
2525
|
const originalTextForApply = useListScope
|
|
@@ -2115,6 +2563,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2115
2563
|
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
2116
2564
|
pairReplacements: options.pairReplacements === true,
|
|
2117
2565
|
insertionAffinity: options.insertionAffinity || null,
|
|
2566
|
+
literalTextEdit: options.literalTextEdit === true,
|
|
2118
2567
|
_revisionIdAllocator: revisionIdAllocator,
|
|
2119
2568
|
_isolatedTableCell: useTableScope
|
|
2120
2569
|
});
|
|
@@ -2138,18 +2587,16 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2138
2587
|
const extracted = extractReplacementNodes(result.oxml);
|
|
2139
2588
|
let replacementNodes = removeListPackagingSentinel(extracted.replacementNodes, result.warnings);
|
|
2140
2589
|
let numberingXml = extracted.numberingXml;
|
|
2141
|
-
if (numberingXml && runtimeContext?.numberingIdState && !useTableScope) {
|
|
2142
|
-
const sourceParagraphs = useListScope ? listScopeEdit.paragraphs : (explicitRangeParagraphs || [targetParagraph]);
|
|
2143
|
-
const continued = reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, runtimeContext.numberingIdState);
|
|
2144
|
-
if (continued) {
|
|
2145
|
-
replacementNodes = continued.replacementNodes;
|
|
2146
|
-
numberingXml = null;
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
2590
|
if (numberingXml && runtimeContext?.numberingIdState) {
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
|
|
2591
|
+
const allocated = allocateNumberingForReplacement({
|
|
2592
|
+
numberingXml,
|
|
2593
|
+
replacementNodes,
|
|
2594
|
+
sourceParagraphs: useListScope ? listScopeEdit.paragraphs : (explicitRangeParagraphs || [targetParagraph]),
|
|
2595
|
+
numberingIdState: runtimeContext.numberingIdState,
|
|
2596
|
+
allowContinueSource: !useTableScope
|
|
2597
|
+
});
|
|
2598
|
+
replacementNodes = allocated.replacementNodes;
|
|
2599
|
+
numberingXml = allocated.numberingXml;
|
|
2153
2600
|
}
|
|
2154
2601
|
const scopeNodes = useTableScope
|
|
2155
2602
|
? [containingTable]
|
|
@@ -2191,7 +2638,10 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
2191
2638
|
hasChanges: true,
|
|
2192
2639
|
numberingXml,
|
|
2193
2640
|
status: 'ok',
|
|
2194
|
-
...(
|
|
2641
|
+
...(unboundMarkerDiagnostic ? {
|
|
2642
|
+
diagnostics: [unboundMarkerDiagnostic],
|
|
2643
|
+
warnings: [...(Array.isArray(result.warnings) ? result.warnings : []), diagnosticWarningText(unboundMarkerDiagnostic)]
|
|
2644
|
+
} : (Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}))
|
|
2195
2645
|
};
|
|
2196
2646
|
}
|
|
2197
2647
|
|
|
@@ -2429,7 +2879,12 @@ export async function applyFormattingToParagraphByExactText(
|
|
|
2429
2879
|
}
|
|
2430
2880
|
|
|
2431
2881
|
const targetDescriptor = options.targetDescriptor;
|
|
2432
|
-
|
|
2882
|
+
// `textOccurrence` is the dedicated in-paragraph selector. Legacy calls without it keep
|
|
2883
|
+
// reading `target.occurrence` (which also selects the N-th matching paragraph).
|
|
2884
|
+
const explicitTextOccurrence = Number.isInteger(options.textOccurrence) && options.textOccurrence > 0
|
|
2885
|
+
? options.textOccurrence
|
|
2886
|
+
: null;
|
|
2887
|
+
const occurrence = explicitTextOccurrence ?? targetDescriptor?.occurrence ?? null;
|
|
2433
2888
|
let matchStart = -1;
|
|
2434
2889
|
if (occurrence != null && occurrence > 0) {
|
|
2435
2890
|
let count = 0;
|
|
@@ -2448,6 +2903,18 @@ export async function applyFormattingToParagraphByExactText(
|
|
|
2448
2903
|
matchStart = fullParaText.indexOf(textToFormat);
|
|
2449
2904
|
}
|
|
2450
2905
|
|
|
2906
|
+
if (matchStart === -1 && explicitTextOccurrence != null && fullParaText.includes(textToFormat)) {
|
|
2907
|
+
return {
|
|
2908
|
+
documentXml,
|
|
2909
|
+
hasChanges: false,
|
|
2910
|
+
status: 'error',
|
|
2911
|
+
error: {
|
|
2912
|
+
code: 'PATCH_SOURCE_NOT_FOUND',
|
|
2913
|
+
message: `Occurrence ${explicitTextOccurrence} of text to format was not found in target paragraph: "${textToFormat}".`
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2451
2918
|
if (matchStart === -1) {
|
|
2452
2919
|
return {
|
|
2453
2920
|
documentXml,
|
|
@@ -2581,13 +3048,63 @@ export async function applyParagraphFormatToParagraphByExactText(
|
|
|
2581
3048
|
targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
|
|
2582
3049
|
}
|
|
2583
3050
|
|
|
3051
|
+
const unknownPropertyKeys = findUnknownParagraphFormatKeys(properties);
|
|
3052
|
+
const propertyDiagnostics = unknownPropertyKeys.length > 0
|
|
3053
|
+
? [buildUnknownParagraphFormatDiagnostic(unknownPropertyKeys)]
|
|
3054
|
+
: [];
|
|
3055
|
+
if (properties?.numbering) {
|
|
3056
|
+
const binding = checkNumberingBinding(
|
|
3057
|
+
runtimeContext?.numberingIdState,
|
|
3058
|
+
properties.numbering.numId,
|
|
3059
|
+
Number(properties.numbering.level) || 0
|
|
3060
|
+
);
|
|
3061
|
+
if (!binding.ok) {
|
|
3062
|
+
return {
|
|
3063
|
+
documentXml,
|
|
3064
|
+
hasChanges: false,
|
|
3065
|
+
status: 'error',
|
|
3066
|
+
error: {
|
|
3067
|
+
code: PARAGRAPH_NUMBERING_NOT_FOUND,
|
|
3068
|
+
message: `${binding.message} paragraph-format.numbering binds to an existing list; it never creates one.`,
|
|
3069
|
+
field: 'properties.numbering',
|
|
3070
|
+
numId: Number(properties.numbering.numId),
|
|
3071
|
+
level: Number(properties.numbering.level) || 0,
|
|
3072
|
+
reason: binding.reason,
|
|
3073
|
+
recovery: {
|
|
3074
|
+
action: 'inspect_numbering',
|
|
3075
|
+
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.'
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
2584
3081
|
const hasChanges = checkParagraphPropertiesChanged(pPr, properties);
|
|
2585
3082
|
if (!hasChanges) {
|
|
2586
|
-
return { documentXml, hasChanges: false, status: 'no-op' };
|
|
3083
|
+
return { documentXml, hasChanges: false, status: 'no-op', ...listDiagnosticFields(propertyDiagnostics) };
|
|
2587
3084
|
}
|
|
2588
3085
|
|
|
2589
3086
|
if (generateRedlines) {
|
|
2590
|
-
|
|
3087
|
+
const authorUsed = author || getDefaultAuthor();
|
|
3088
|
+
const pendingChange = getCurrentChild(pPr, 'pPrChange');
|
|
3089
|
+
const pendingAuthor = pendingChange?.getAttribute('w:author') ?? pendingChange?.getAttribute('author') ?? '';
|
|
3090
|
+
if (pendingChange && pendingAuthor !== authorUsed) {
|
|
3091
|
+
return {
|
|
3092
|
+
documentXml,
|
|
3093
|
+
hasChanges: false,
|
|
3094
|
+
status: 'error',
|
|
3095
|
+
error: {
|
|
3096
|
+
code: PARAGRAPH_FORMAT_REVISION_CONFLICT,
|
|
3097
|
+
message: `Target paragraph already has a pending paragraph-property change by "${pendingAuthor || 'unattributed'}". Overwriting its snapshot would change what Reject All restores.`,
|
|
3098
|
+
pendingAuthor,
|
|
3099
|
+
recovery: {
|
|
3100
|
+
action: 'resolve_pending_revision',
|
|
3101
|
+
hint: 'Ask the user to accept or reject the existing paragraph formatting change first; this tool does not resolve revisions by other reviewers.'
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
// A same-author pending snapshot already records the earliest "before" state; keep it.
|
|
3107
|
+
if (!pendingChange) snapshotAndAttachPPrChange(xmlDoc, pPr, authorUsed);
|
|
2591
3108
|
}
|
|
2592
3109
|
|
|
2593
3110
|
applyParagraphPropertiesToPPr(xmlDoc, pPr, properties);
|
|
@@ -2598,6 +3115,232 @@ export async function applyParagraphFormatToParagraphByExactText(
|
|
|
2598
3115
|
return {
|
|
2599
3116
|
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
2600
3117
|
hasChanges: true,
|
|
2601
|
-
status: 'ok'
|
|
3118
|
+
status: 'ok',
|
|
3119
|
+
...listDiagnosticFields(propertyDiagnostics)
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/**
|
|
3124
|
+
* Resolves an operation's target range (target plus optional targetEnd) to paragraphs in document order, the
|
|
3125
|
+
* same way restore does: batch-compiled source ids first, then legacy refs, then an end descriptor.
|
|
3126
|
+
*/
|
|
3127
|
+
function resolveOperationParagraphRange(xmlDoc, targetText, targetRef, targetEndRef, opType, runtimeContext, options) {
|
|
3128
|
+
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeContext, options);
|
|
3129
|
+
if (resolved?.error || !resolved?.paragraph) {
|
|
3130
|
+
return { error: resolved?.error || { code: 'TARGET_NOT_FOUND', message: 'Target paragraph not found.' } };
|
|
3131
|
+
}
|
|
3132
|
+
const first = resolved.paragraph;
|
|
3133
|
+
const hasEnd = options._compiledSourceEndId || targetEndRef != null || options.targetEndDescriptor;
|
|
3134
|
+
if (!hasEnd) return { paragraphs: [first] };
|
|
3135
|
+
let paragraphs = null;
|
|
3136
|
+
if (options._compiledSourceEndId || options.targetEndDescriptor) {
|
|
3137
|
+
const endResolved = resolveTargetParagraph(
|
|
3138
|
+
xmlDoc,
|
|
3139
|
+
options.targetEndDescriptor?.text || '',
|
|
3140
|
+
options._compiledSourceEndId ? targetEndRef : (options.targetEndDescriptor?.index ?? targetEndRef),
|
|
3141
|
+
opType,
|
|
3142
|
+
runtimeContext,
|
|
3143
|
+
{
|
|
3144
|
+
...options,
|
|
3145
|
+
...(options.targetEndDescriptor ? { targetDescriptor: options.targetEndDescriptor } : {}),
|
|
3146
|
+
_compiledSourceId: options._compiledSourceEndId || null,
|
|
3147
|
+
_resolutionCapture: null
|
|
3148
|
+
}
|
|
3149
|
+
);
|
|
3150
|
+
if (endResolved?.error) return { error: endResolved.error };
|
|
3151
|
+
const all = endResolved?.paragraph ? getDocumentParagraphNodes(xmlDoc) : [];
|
|
3152
|
+
const startIndex = all.indexOf(first);
|
|
3153
|
+
const endIndex = all.indexOf(endResolved?.paragraph);
|
|
3154
|
+
paragraphs = startIndex >= 0 && endIndex >= startIndex ? all.slice(startIndex, endIndex + 1) : null;
|
|
3155
|
+
} else {
|
|
3156
|
+
paragraphs = resolveParagraphRangeByRefs(xmlDoc, targetRef, targetEndRef, {
|
|
3157
|
+
opType,
|
|
3158
|
+
targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
|
|
3159
|
+
onInfo: options.onInfo,
|
|
3160
|
+
onWarn: options.onWarn
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
if (!Array.isArray(paragraphs) || paragraphs.length === 0 || paragraphs[0] !== first) {
|
|
3164
|
+
return { error: { code: 'TARGET_RANGE_INVALID', message: 'targetEnd must resolve to the target paragraph or a paragraph after it.' } };
|
|
3165
|
+
}
|
|
3166
|
+
return { paragraphs };
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
/**
|
|
3170
|
+
* `list-format` (`convert-plain`): binds plain paragraphs to a new list in place; see services/list-format-mutation.js.
|
|
3171
|
+
*/
|
|
3172
|
+
export async function applyListFormatToParagraphByExactText(
|
|
3173
|
+
documentXml,
|
|
3174
|
+
targetText,
|
|
3175
|
+
list,
|
|
3176
|
+
author,
|
|
3177
|
+
targetRef = null,
|
|
3178
|
+
targetEndRef = null,
|
|
3179
|
+
runtimeContext = null,
|
|
3180
|
+
options = {}
|
|
3181
|
+
) {
|
|
3182
|
+
const generateRedlines = options.generateRedlines !== false;
|
|
3183
|
+
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
3184
|
+
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
3185
|
+
const { serializer, xmlDoc, operationSession } = resolveMutationDocument(documentXml, options);
|
|
3186
|
+
if (!xmlDoc) {
|
|
3187
|
+
return { documentXml, hasChanges: false, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
3188
|
+
}
|
|
3189
|
+
const revisionIdAllocator = operationSession?.revisionIdAllocator || prepareRevisionAllocator(xmlDoc, options);
|
|
3190
|
+
const range = resolveOperationParagraphRange(xmlDoc, targetText, targetRef, targetEndRef, 'list-format', runtimeContext, {
|
|
3191
|
+
...options,
|
|
3192
|
+
onInfo,
|
|
3193
|
+
onWarn
|
|
3194
|
+
});
|
|
3195
|
+
if (range.error) return { documentXml, hasChanges: false, status: 'error', error: range.error };
|
|
3196
|
+
const paragraphs = range.paragraphs;
|
|
3197
|
+
if (paragraphs.some(paragraph => paragraph.parentNode !== paragraphs[0].parentNode)) {
|
|
3198
|
+
return {
|
|
3199
|
+
documentXml,
|
|
3200
|
+
hasChanges: false,
|
|
3201
|
+
status: 'error',
|
|
3202
|
+
error: {
|
|
3203
|
+
code: 'TARGET_RANGE_INVALID',
|
|
3204
|
+
message: 'A list-format range must stay inside one container (the body, or one table cell).'
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
const allParagraphs = getDocumentParagraphNodes(xmlDoc);
|
|
3209
|
+
const request = normalizeListFormatRequest(list);
|
|
3210
|
+
if (request.mode === 'change-format' || request.mode === 'join') {
|
|
3211
|
+
return applyListRebinding(xmlDoc, paragraphs, request, {
|
|
3212
|
+
documentXml, serializer, operationSession, runtimeContext, options, author, generateRedlines, onInfo, onWarn,
|
|
3213
|
+
paragraphIndexOf: paragraph => {
|
|
3214
|
+
const index = allParagraphs.indexOf(paragraph);
|
|
3215
|
+
return index >= 0 ? index + 1 : null;
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
const conversion = convertPlainParagraphsToList(xmlDoc, paragraphs, request, {
|
|
3220
|
+
author: author || getDefaultAuthor(),
|
|
3221
|
+
generateRedlines,
|
|
3222
|
+
revisionIdAllocator,
|
|
3223
|
+
runtimeContext,
|
|
3224
|
+
stylesXml: runtimeContext?.stylesXml || options.stylesXml || null,
|
|
3225
|
+
numberingXml: runtimeContext?.numberingXml || options.numberingXml || null,
|
|
3226
|
+
paragraphIndexOf: paragraph => allParagraphs.indexOf(paragraph) + 1
|
|
3227
|
+
});
|
|
3228
|
+
if (!conversion.ok) return { documentXml, hasChanges: false, status: 'error', error: conversion.error };
|
|
3229
|
+
|
|
3230
|
+
for (const item of conversion.items) {
|
|
3231
|
+
ensureParagraphIdsOnImportedNode(item.paragraph, operationSession);
|
|
3232
|
+
options?._mutationLiveNodes?.push(item.paragraph);
|
|
3233
|
+
}
|
|
3234
|
+
for (const numId of conversion.allocatedNumIds) operationSession?.receiptCollector?.recordNumbering(numId);
|
|
3235
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
3236
|
+
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(', ')}).`);
|
|
3237
|
+
|
|
3238
|
+
return {
|
|
3239
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
3240
|
+
hasChanges: true,
|
|
3241
|
+
status: 'ok',
|
|
3242
|
+
numberingXml: conversion.numberingXml,
|
|
3243
|
+
list: {
|
|
3244
|
+
mode: 'convert-plain',
|
|
3245
|
+
format: conversion.format,
|
|
3246
|
+
level: conversion.level,
|
|
3247
|
+
strip: conversion.strip,
|
|
3248
|
+
items: conversion.items.map(item => ({
|
|
3249
|
+
paragraphIndex: item.paragraphIndex,
|
|
3250
|
+
paragraphId: getParagraphId(item.paragraph),
|
|
3251
|
+
marker: item.marker,
|
|
3252
|
+
value: item.value,
|
|
3253
|
+
numId: item.numId
|
|
3254
|
+
})),
|
|
3255
|
+
skipped: conversion.skipped.map(item => ({ paragraphIndex: item.paragraphIndex, reason: item.reason }))
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
/**
|
|
3261
|
+
* `list-format` `change-format` / `join`: rebinds existing list items (see services/list-format-change.js). The
|
|
3262
|
+
* definitions are read from the source numbering part plus the parts earlier operations of this batch allocated,
|
|
3263
|
+
* so a later operation can change a list an earlier one created or rebound.
|
|
3264
|
+
*/
|
|
3265
|
+
function applyListRebinding(xmlDoc, paragraphs, request, ctx) {
|
|
3266
|
+
const { documentXml, serializer, operationSession, runtimeContext, options, author, generateRedlines, onInfo, paragraphIndexOf } = ctx;
|
|
3267
|
+
const refused = error => ({ documentXml, hasChanges: false, status: 'error', error });
|
|
3268
|
+
const sourceNumberingXml = runtimeContext?.numberingIdState?.sourceNumberingXml
|
|
3269
|
+
|| runtimeContext?.numberingXml
|
|
3270
|
+
|| options.numberingXml
|
|
3271
|
+
|| '';
|
|
3272
|
+
const pendingParts = Array.isArray(operationSession?.numberingXmlParts) ? operationSession.numberingXmlParts : [];
|
|
3273
|
+
const numberingXml = pendingParts.length > 0 ? mergeNumberingParts(sourceNumberingXml, pendingParts) : sourceNumberingXml;
|
|
3274
|
+
const shared = {
|
|
3275
|
+
author: author || getDefaultAuthor(),
|
|
3276
|
+
generateRedlines,
|
|
3277
|
+
runtimeContext,
|
|
3278
|
+
stylesXml: runtimeContext?.stylesXml || options.stylesXml || null,
|
|
3279
|
+
numberingXml,
|
|
3280
|
+
paragraphIndexOf
|
|
3281
|
+
};
|
|
3282
|
+
|
|
3283
|
+
let outcome;
|
|
3284
|
+
if (request.mode === 'join') {
|
|
3285
|
+
const descriptor = options.continueFromDescriptor || null;
|
|
3286
|
+
const resolved = descriptor
|
|
3287
|
+
? resolveTargetParagraph(xmlDoc, descriptor.text || '', descriptor.index ?? null, 'list-format', runtimeContext, {
|
|
3288
|
+
...options,
|
|
3289
|
+
targetDescriptor: descriptor,
|
|
3290
|
+
_compiledSourceId: options._compiledContinueFromId || null,
|
|
3291
|
+
_compiledResolvedBy: null,
|
|
3292
|
+
_resolutionCapture: null
|
|
3293
|
+
})
|
|
3294
|
+
: null;
|
|
3295
|
+
if (!resolved?.paragraph) {
|
|
3296
|
+
return refused({
|
|
3297
|
+
code: resolved?.error?.code || 'TARGET_NOT_FOUND',
|
|
3298
|
+
message: `list.continueFrom: ${resolved?.error?.message || 'paragraph not found.'}`,
|
|
3299
|
+
field: 'list.continueFrom'
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
outcome = joinListParagraphs(xmlDoc, paragraphs, resolved.paragraph, request, shared);
|
|
3303
|
+
} else {
|
|
3304
|
+
outcome = changeListParagraphsFormat(xmlDoc, paragraphs, request, shared);
|
|
3305
|
+
}
|
|
3306
|
+
if (!outcome.ok) return refused(outcome.error);
|
|
3307
|
+
if (outcome.unchanged) {
|
|
3308
|
+
const warning = `${LIST_FORMAT_UNCHANGED}: ${outcome.message}`;
|
|
3309
|
+
ctx.onWarn?.(warning);
|
|
3310
|
+
return { documentXml, hasChanges: false, status: 'ok', warnings: [warning] };
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
for (const item of outcome.items) {
|
|
3314
|
+
ensureParagraphIdsOnImportedNode(item.paragraph, operationSession);
|
|
3315
|
+
options?._mutationLiveNodes?.push(item.paragraph);
|
|
3316
|
+
}
|
|
3317
|
+
for (const numId of outcome.allocatedNumIds) operationSession?.receiptCollector?.recordNumbering(numId);
|
|
3318
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
3319
|
+
const outside = outcome.items.filter(item => !item.inTarget).length;
|
|
3320
|
+
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)` : ''}.`);
|
|
3321
|
+
|
|
3322
|
+
return {
|
|
3323
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
3324
|
+
hasChanges: true,
|
|
3325
|
+
status: 'ok',
|
|
3326
|
+
numberingXml: outcome.numberingXml,
|
|
3327
|
+
list: {
|
|
3328
|
+
mode: outcome.mode,
|
|
3329
|
+
scope: outcome.scope,
|
|
3330
|
+
sourceNumId: outcome.sourceNumId,
|
|
3331
|
+
numId: outcome.numId,
|
|
3332
|
+
...(outcome.mode === 'change-format'
|
|
3333
|
+
? { format: outcome.format, level: outcome.level, lvlText: outcome.lvlText, levelStarts: outcome.levelStarts }
|
|
3334
|
+
: { continueFrom: outcome.continueFrom }),
|
|
3335
|
+
items: outcome.items.map(item => ({
|
|
3336
|
+
paragraphIndex: item.paragraphIndex,
|
|
3337
|
+
paragraphId: getParagraphId(item.paragraph),
|
|
3338
|
+
level: item.level,
|
|
3339
|
+
numId: item.numId,
|
|
3340
|
+
sourceNumId: item.sourceNumId,
|
|
3341
|
+
inTarget: item.inTarget
|
|
3342
|
+
})),
|
|
3343
|
+
skipped: outcome.skipped.map(item => ({ paragraphIndex: item.paragraphIndex, reason: item.reason }))
|
|
3344
|
+
}
|
|
2602
3345
|
};
|
|
2603
3346
|
}
|