@a3s-lab/office 0.49.0 → 0.51.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/README.md +34 -0
- package/dist/0~document-editor.js +426 -105
- package/dist/internal/features/work/work-document-compare-identities.d.ts +28 -0
- package/dist/internal/features/work/work-document-compare-moves.d.ts +72 -0
- package/dist/internal/features/work/work-document-compare.d.ts +2 -13
- package/docs/latest/en/browser-editor-architecture.md +13 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -309,6 +309,40 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
|
|
|
309
309
|
|
|
310
310
|
## Current release
|
|
311
311
|
|
|
312
|
+
Version `0.51.0` extends Writer Compare with bounded, WPS-referenced
|
|
313
|
+
cross-paragraph text moves:
|
|
314
|
+
|
|
315
|
+
- A unique lexical range can move between aligned simple text paragraphs or
|
|
316
|
+
headings in the same section and become one paired `move` review item.
|
|
317
|
+
Separators travel with the range; accepting, rejecting, undoing, or reopening
|
|
318
|
+
the DOCX preserves exact source and revised text.
|
|
319
|
+
- The Review ribbon and Changes pane keep one localized **移动** card with
|
|
320
|
+
source/destination marks, destination navigation, author guidance, and
|
|
321
|
+
move-aware toast counts. Duplicate or mark-mismatched candidates, section
|
|
322
|
+
boundaries, rich or relationship-bound content, tables, and over-limit input
|
|
323
|
+
remain fail-closed ordinary revisions or diagnostics.
|
|
324
|
+
- The WPS 12.0 COM/UIA probe observed `CompareDocuments` returning ordinary
|
|
325
|
+
delete/insert records for the tested reorder. A3S therefore documents this
|
|
326
|
+
paired inference as a bounded local enhancement, with no claim that WPS
|
|
327
|
+
exposed a native move type through that API.
|
|
328
|
+
|
|
329
|
+
Version `0.50.0` extends Writer Compare with a bounded, WPS-referenced text
|
|
330
|
+
move workflow:
|
|
331
|
+
|
|
332
|
+
- Deterministic lexical ranges that move within one simple paragraph or heading
|
|
333
|
+
become one paired `move` review item. Separators travel with the range, and
|
|
334
|
+
accepting, rejecting, undoing, or reopening the DOCX preserves exact source
|
|
335
|
+
and revised text.
|
|
336
|
+
- The Review ribbon and Changes pane expose one localized **移动** card with
|
|
337
|
+
source/destination marks, destination navigation, updated author guidance,
|
|
338
|
+
and move-aware toast counts. Ambiguous duplicates, rich or relationship-bound
|
|
339
|
+
content, cross-paragraph moves, and over-limit inputs remain fail-closed
|
|
340
|
+
boundaries.
|
|
341
|
+
- The WPS 12.0 COM/UIA probe observed `CompareDocuments` returning ordinary
|
|
342
|
+
delete/insert records for the tested reorder. A3S therefore documents its
|
|
343
|
+
paired inference as a bounded local enhancement, with no claim that WPS
|
|
344
|
+
exposed a native move type through that API.
|
|
345
|
+
|
|
312
346
|
Version `0.49.0` adds bounded native Writer move revisions and hardens the
|
|
313
347
|
local PDF workflow:
|
|
314
348
|
|
|
@@ -17434,6 +17434,245 @@ function sortJsonValue(value) {
|
|
|
17434
17434
|
sortJsonValue(child)
|
|
17435
17435
|
]));
|
|
17436
17436
|
}
|
|
17437
|
+
function createComparisonIdentityFactory(currentSignature, revisedSignature, options) {
|
|
17438
|
+
const seed = stableHash(`${options.mode}\u0000${currentSignature}\u0000${revisedSignature}`);
|
|
17439
|
+
const author = boundedMetadata(options.author, 256) || 'A3S Work user';
|
|
17440
|
+
const date = normalizedDate(options.date);
|
|
17441
|
+
const summary = emptyComparisonSummary();
|
|
17442
|
+
let sequence = 0;
|
|
17443
|
+
return {
|
|
17444
|
+
summary,
|
|
17445
|
+
create (kind) {
|
|
17446
|
+
sequence += 1;
|
|
17447
|
+
if ('insertion' === kind) summary.insertions += 1;
|
|
17448
|
+
else if ('deletion' === kind) summary.deletions += 1;
|
|
17449
|
+
else if ('formatting' === kind) summary.formatting += 1;
|
|
17450
|
+
else if ('move' === kind) summary.moves = (summary.moves ?? 0) + 1;
|
|
17451
|
+
else summary.paragraphFormatting += 1;
|
|
17452
|
+
return {
|
|
17453
|
+
id: `compare-${seed}-${kind}-${sequence.toString(36)}`,
|
|
17454
|
+
author,
|
|
17455
|
+
date
|
|
17456
|
+
};
|
|
17457
|
+
},
|
|
17458
|
+
paragraphIdentity (channel) {
|
|
17459
|
+
return {
|
|
17460
|
+
paragraphId: wordParagraphId(`${seed}:${channel}:paragraph`),
|
|
17461
|
+
textId: wordParagraphId(`${seed}:${channel}:text`)
|
|
17462
|
+
};
|
|
17463
|
+
}
|
|
17464
|
+
};
|
|
17465
|
+
}
|
|
17466
|
+
function summarizeComparisonChanges(changes) {
|
|
17467
|
+
const summary = emptyComparisonSummary();
|
|
17468
|
+
for (const change of changes)if ('insertion' === change.kind) summary.insertions += 1;
|
|
17469
|
+
else if ('deletion' === change.kind) summary.deletions += 1;
|
|
17470
|
+
else if ('formatting' === change.kind) summary.formatting += 1;
|
|
17471
|
+
else if ('move' === change.kind) summary.moves = (summary.moves ?? 0) + 1;
|
|
17472
|
+
else summary.paragraphFormatting += 1;
|
|
17473
|
+
return summary;
|
|
17474
|
+
}
|
|
17475
|
+
function emptyComparisonSummary() {
|
|
17476
|
+
return {
|
|
17477
|
+
deletions: 0,
|
|
17478
|
+
formatting: 0,
|
|
17479
|
+
insertions: 0,
|
|
17480
|
+
paragraphFormatting: 0
|
|
17481
|
+
};
|
|
17482
|
+
}
|
|
17483
|
+
const MAX_INFERRED_MOVE_TEXT = 65536;
|
|
17484
|
+
const MAX_INFERRED_MOVES = 256;
|
|
17485
|
+
function work_document_compare_moves_appendRevisionUnits(target, units, kind, identity, schema, stripReviewMarks) {
|
|
17486
|
+
for (const unit of units)target.push(schema.text(unit.text, [
|
|
17487
|
+
...stripReviewMarks(unit.marks),
|
|
17488
|
+
schema.marks.documentChange.create({
|
|
17489
|
+
kind,
|
|
17490
|
+
id: identity.id,
|
|
17491
|
+
actorId: identity.actorId ?? '',
|
|
17492
|
+
author: identity.author,
|
|
17493
|
+
date: identity.date,
|
|
17494
|
+
before: ''
|
|
17495
|
+
})
|
|
17496
|
+
]));
|
|
17497
|
+
}
|
|
17498
|
+
function inferInlineMovePairs(changes, marksEqual) {
|
|
17499
|
+
return inferInlineMovePairsAcrossScopes([
|
|
17500
|
+
{
|
|
17501
|
+
scope: '',
|
|
17502
|
+
changes
|
|
17503
|
+
}
|
|
17504
|
+
], marksEqual);
|
|
17505
|
+
}
|
|
17506
|
+
function inferInlineMovePairsAcrossScopes(comparisons, marksEqual) {
|
|
17507
|
+
const fullCandidates = [];
|
|
17508
|
+
for (const comparison of comparisons)for (const [stepIndex, change] of comparison.changes.entries()){
|
|
17509
|
+
if ('equal' === change.kind) continue;
|
|
17510
|
+
const units = 'delete' === change.kind ? change.left : change.right;
|
|
17511
|
+
const candidate = inlineMoveCandidate(stepIndex, change.kind, units, comparison.scope);
|
|
17512
|
+
if (candidate) fullCandidates.push(candidate);
|
|
17513
|
+
}
|
|
17514
|
+
const fullPairs = pairInlineMoveCandidates(fullCandidates, marksEqual);
|
|
17515
|
+
const consumedSteps = new Set();
|
|
17516
|
+
for (const pair of fullPairs){
|
|
17517
|
+
consumedSteps.add(moveStepKey(pair.deletion));
|
|
17518
|
+
consumedSteps.add(moveStepKey(pair.insertion));
|
|
17519
|
+
}
|
|
17520
|
+
const tokenCandidates = [];
|
|
17521
|
+
for (const comparison of comparisons)for (const [stepIndex, change] of comparison.changes.entries()){
|
|
17522
|
+
if ('equal' === change.kind || consumedSteps.has(moveStepKey({
|
|
17523
|
+
scope: comparison.scope,
|
|
17524
|
+
stepIndex
|
|
17525
|
+
}))) continue;
|
|
17526
|
+
const units = 'delete' === change.kind ? change.left : change.right;
|
|
17527
|
+
tokenCandidates.push(...inlineMoveTokenCandidates(stepIndex, change.kind, units, comparison.scope));
|
|
17528
|
+
}
|
|
17529
|
+
return [
|
|
17530
|
+
...fullPairs,
|
|
17531
|
+
...pairInlineMoveCandidates(tokenCandidates, marksEqual)
|
|
17532
|
+
].sort(compareMovePairs).slice(0, MAX_INFERRED_MOVES);
|
|
17533
|
+
}
|
|
17534
|
+
function appendComparisonRevisionUnits(target, units, kind, schema, factory, stripReviewMarks, appendRevisionUnits, moves) {
|
|
17535
|
+
if (!moves?.length) return void appendRevisionUnits(target, units, kind, factory.create(kind), schema, stripReviewMarks);
|
|
17536
|
+
const byUnit = new Map();
|
|
17537
|
+
for (const move of [
|
|
17538
|
+
...moves
|
|
17539
|
+
].sort((left, right)=>left.candidate.start - right.candidate.start))for(let index = move.candidate.start; index < move.candidate.end; index += 1){
|
|
17540
|
+
if (byUnit.has(index)) return void appendRevisionUnits(target, units, kind, factory.create(kind), schema, stripReviewMarks);
|
|
17541
|
+
byUnit.set(index, move);
|
|
17542
|
+
}
|
|
17543
|
+
let ordinary = [];
|
|
17544
|
+
const flushOrdinary = ()=>{
|
|
17545
|
+
if (!ordinary.length) return;
|
|
17546
|
+
appendRevisionUnits(target, ordinary, kind, factory.create(kind), schema, stripReviewMarks);
|
|
17547
|
+
ordinary = [];
|
|
17548
|
+
};
|
|
17549
|
+
for (const [index, unit] of units.entries()){
|
|
17550
|
+
const move = byUnit.get(index);
|
|
17551
|
+
if (!move) {
|
|
17552
|
+
ordinary.push(unit);
|
|
17553
|
+
continue;
|
|
17554
|
+
}
|
|
17555
|
+
const start = index === move.candidate.start ? move.candidate.startOffset : 0;
|
|
17556
|
+
const end = index === move.candidate.end - 1 ? move.candidate.endOffset : unit.text.length;
|
|
17557
|
+
const prefix = unit.text.slice(0, start);
|
|
17558
|
+
const marked = unit.text.slice(start, end);
|
|
17559
|
+
const suffix = unit.text.slice(end);
|
|
17560
|
+
if (prefix) ordinary.push({
|
|
17561
|
+
text: prefix,
|
|
17562
|
+
marks: unit.marks
|
|
17563
|
+
});
|
|
17564
|
+
flushOrdinary();
|
|
17565
|
+
if (marked) appendMoveRevisionUnits(target, [
|
|
17566
|
+
{
|
|
17567
|
+
text: marked,
|
|
17568
|
+
marks: unit.marks
|
|
17569
|
+
}
|
|
17570
|
+
], move.role, move.identity, schema, stripReviewMarks);
|
|
17571
|
+
ordinary = suffix ? [
|
|
17572
|
+
{
|
|
17573
|
+
text: suffix,
|
|
17574
|
+
marks: unit.marks
|
|
17575
|
+
}
|
|
17576
|
+
] : [];
|
|
17577
|
+
}
|
|
17578
|
+
flushOrdinary();
|
|
17579
|
+
}
|
|
17580
|
+
function pairInlineMoveCandidates(candidates, marksEqual) {
|
|
17581
|
+
const grouped = new Map();
|
|
17582
|
+
for (const candidate of candidates){
|
|
17583
|
+
const group = grouped.get(candidate.text) ?? {
|
|
17584
|
+
deletion: [],
|
|
17585
|
+
insertion: []
|
|
17586
|
+
};
|
|
17587
|
+
('delete' === candidate.kind ? group.deletion : group.insertion).push(candidate);
|
|
17588
|
+
grouped.set(candidate.text, group);
|
|
17589
|
+
}
|
|
17590
|
+
return Array.from(grouped.values()).filter(({ deletion, insertion })=>1 === deletion.length && 1 === insertion.length && inlineMoveCandidatesEqual(deletion[0], insertion[0], marksEqual)).map(({ deletion, insertion })=>({
|
|
17591
|
+
deletion: deletion[0],
|
|
17592
|
+
insertion: insertion[0]
|
|
17593
|
+
})).sort(compareMovePairs).slice(0, MAX_INFERRED_MOVES);
|
|
17594
|
+
}
|
|
17595
|
+
function inlineMoveCandidate(stepIndex, kind, units, scope = '') {
|
|
17596
|
+
let start = 0;
|
|
17597
|
+
while(start < units.length && isWhitespaceUnit(units[start]))start += 1;
|
|
17598
|
+
let end = units.length;
|
|
17599
|
+
while(end > start && isWhitespaceUnit(units[end - 1]))end -= 1;
|
|
17600
|
+
if (start === end) return null;
|
|
17601
|
+
const first = units[start];
|
|
17602
|
+
const last = units[end - 1];
|
|
17603
|
+
const startOffset = first.leadingWhitespaceAttached ? 0 : first.text.length - first.text.trimStart().length;
|
|
17604
|
+
const endOffset = last.leadingWhitespaceAttached ? last.text.length : last.text.trimEnd().length;
|
|
17605
|
+
const text = units.slice(start, end).map((unit, index, selected)=>unit.text.slice(0 === index ? startOffset : 0, index === selected.length - 1 ? endOffset : unit.text.length)).join('');
|
|
17606
|
+
if (!text || text.length > MAX_INFERRED_MOVE_TEXT || !/[\p{L}\p{N}]/u.test(text)) return null;
|
|
17607
|
+
return {
|
|
17608
|
+
...scope ? {
|
|
17609
|
+
scope
|
|
17610
|
+
} : {},
|
|
17611
|
+
stepIndex,
|
|
17612
|
+
kind,
|
|
17613
|
+
units,
|
|
17614
|
+
start,
|
|
17615
|
+
end,
|
|
17616
|
+
startOffset,
|
|
17617
|
+
endOffset,
|
|
17618
|
+
text
|
|
17619
|
+
};
|
|
17620
|
+
}
|
|
17621
|
+
function inlineMoveTokenCandidates(stepIndex, kind, units, scope = '') {
|
|
17622
|
+
const candidates = [];
|
|
17623
|
+
for (const [index, unit] of units.entries()){
|
|
17624
|
+
const leadingWhitespace = unit.text.length - unit.text.trimStart().length;
|
|
17625
|
+
const text = unit.text.slice(leadingWhitespace);
|
|
17626
|
+
if (text && !(text.length > MAX_INFERRED_MOVE_TEXT) && /[\p{L}\p{N}]/u.test(text)) candidates.push({
|
|
17627
|
+
...scope ? {
|
|
17628
|
+
scope
|
|
17629
|
+
} : {},
|
|
17630
|
+
stepIndex,
|
|
17631
|
+
kind,
|
|
17632
|
+
units,
|
|
17633
|
+
start: index,
|
|
17634
|
+
end: index + 1,
|
|
17635
|
+
startOffset: leadingWhitespace,
|
|
17636
|
+
endOffset: unit.text.length,
|
|
17637
|
+
text
|
|
17638
|
+
});
|
|
17639
|
+
}
|
|
17640
|
+
return candidates;
|
|
17641
|
+
}
|
|
17642
|
+
function moveStepKey(candidate) {
|
|
17643
|
+
return `${candidate.scope ?? ''}\u0000${candidate.stepIndex}`;
|
|
17644
|
+
}
|
|
17645
|
+
function compareMovePairs(left, right) {
|
|
17646
|
+
return (left.deletion.scope ?? '').localeCompare(right.deletion.scope ?? '') || left.deletion.stepIndex - right.deletion.stepIndex || (left.insertion.scope ?? '').localeCompare(right.insertion.scope ?? '') || left.insertion.stepIndex - right.insertion.stepIndex || left.deletion.start - right.deletion.start;
|
|
17647
|
+
}
|
|
17648
|
+
function inlineMoveCandidatesEqual(left, right, marksEqual) {
|
|
17649
|
+
const leftUnits = selectedMoveUnits(left);
|
|
17650
|
+
const rightUnits = selectedMoveUnits(right);
|
|
17651
|
+
return leftUnits.length === rightUnits.length && leftUnits.every((unit, index)=>unit.text === rightUnits[index]?.text && marksEqual(unit.marks, rightUnits[index]?.marks ?? []));
|
|
17652
|
+
}
|
|
17653
|
+
function selectedMoveUnits(candidate) {
|
|
17654
|
+
return candidate.units.slice(candidate.start, candidate.end).map((unit, index, selected)=>({
|
|
17655
|
+
text: unit.text.slice(0 === index ? candidate.startOffset : 0, index === selected.length - 1 ? candidate.endOffset : unit.text.length),
|
|
17656
|
+
marks: unit.marks
|
|
17657
|
+
})).filter((unit)=>unit.text.length > 0);
|
|
17658
|
+
}
|
|
17659
|
+
function isWhitespaceUnit(unit) {
|
|
17660
|
+
return '' === unit.text.trim();
|
|
17661
|
+
}
|
|
17662
|
+
function appendMoveRevisionUnits(target, units, role, identity, schema, stripReviewMarks) {
|
|
17663
|
+
for (const unit of units)target.push(schema.text(unit.text, [
|
|
17664
|
+
...stripReviewMarks(unit.marks),
|
|
17665
|
+
schema.marks.documentChange.create({
|
|
17666
|
+
kind: 'move',
|
|
17667
|
+
moveRole: role,
|
|
17668
|
+
id: identity.id,
|
|
17669
|
+
actorId: identity.actorId ?? '',
|
|
17670
|
+
author: identity.author,
|
|
17671
|
+
date: identity.date,
|
|
17672
|
+
before: ''
|
|
17673
|
+
})
|
|
17674
|
+
]));
|
|
17675
|
+
}
|
|
17437
17676
|
const MAX_COMPARISON_BLOCKS = 1024;
|
|
17438
17677
|
const MAX_COMPARISON_TEXT = 1000000;
|
|
17439
17678
|
const MAX_BLOCK_ALIGNMENT_CELLS = 1100000;
|
|
@@ -17497,7 +17736,7 @@ function planDocumentCompare(current, revised, options) {
|
|
|
17497
17736
|
const limits = comparisonLimits(currentSections, revisedSections);
|
|
17498
17737
|
if (limits) return unsupportedResult(limits);
|
|
17499
17738
|
const diagnostics = [];
|
|
17500
|
-
const factory =
|
|
17739
|
+
const factory = createComparisonIdentityFactory(comparisonSemanticSignature(current), comparisonSemanticSignature(revised), options);
|
|
17501
17740
|
const comparedSections = [];
|
|
17502
17741
|
for(let sectionIndex = 0; sectionIndex < currentSections.length; sectionIndex += 1){
|
|
17503
17742
|
const currentSection = currentSections[sectionIndex];
|
|
@@ -17566,7 +17805,7 @@ function planDocumentCombine(current, reviewed) {
|
|
|
17566
17805
|
return {
|
|
17567
17806
|
status: 'applied',
|
|
17568
17807
|
document: document1,
|
|
17569
|
-
summary:
|
|
17808
|
+
summary: summarizeComparisonChanges(reviewedChanges),
|
|
17570
17809
|
diagnostics: []
|
|
17571
17810
|
};
|
|
17572
17811
|
}
|
|
@@ -17582,40 +17821,129 @@ function compareSectionBlocks(currentSection, revisedSection, sectionIndex, fact
|
|
|
17582
17821
|
});
|
|
17583
17822
|
return null;
|
|
17584
17823
|
}
|
|
17585
|
-
const
|
|
17824
|
+
const slots = [];
|
|
17586
17825
|
for (const [blockIndex, step] of alignment.entries()){
|
|
17587
17826
|
if ('equal' === step.kind) {
|
|
17588
|
-
|
|
17827
|
+
slots.push({
|
|
17828
|
+
kind: 'node',
|
|
17829
|
+
node: step.left
|
|
17830
|
+
});
|
|
17589
17831
|
continue;
|
|
17590
17832
|
}
|
|
17591
17833
|
if ('delete' === step.kind) {
|
|
17592
17834
|
const deleted = structuralChangeBlock(step.left, 'deletion', factory, `section-${sectionIndex}-delete-${blockIndex}`);
|
|
17593
|
-
if (deleted)
|
|
17835
|
+
if (deleted) slots.push({
|
|
17836
|
+
kind: 'node',
|
|
17837
|
+
node: deleted
|
|
17838
|
+
});
|
|
17594
17839
|
else diagnostics.push(structuralBlockDiagnostic(step.left, sectionIndex, blockIndex));
|
|
17595
17840
|
continue;
|
|
17596
17841
|
}
|
|
17597
17842
|
if ('insert' === step.kind) {
|
|
17598
17843
|
const inserted = structuralChangeBlock(step.right, 'insertion', factory, `section-${sectionIndex}-insert-${blockIndex}`);
|
|
17599
|
-
if (inserted)
|
|
17844
|
+
if (inserted) slots.push({
|
|
17845
|
+
kind: 'node',
|
|
17846
|
+
node: inserted
|
|
17847
|
+
});
|
|
17600
17848
|
else diagnostics.push(structuralBlockDiagnostic(step.right, sectionIndex, blockIndex));
|
|
17601
17849
|
continue;
|
|
17602
17850
|
}
|
|
17603
|
-
const
|
|
17604
|
-
if (
|
|
17605
|
-
|
|
17851
|
+
const prepared = preparePairedBlock(step.left, step.right, `section-${sectionIndex}-block-${blockIndex}`);
|
|
17852
|
+
if (prepared) {
|
|
17853
|
+
slots.push({
|
|
17854
|
+
kind: 'paired',
|
|
17855
|
+
comparison: prepared
|
|
17856
|
+
});
|
|
17606
17857
|
continue;
|
|
17607
17858
|
}
|
|
17608
17859
|
const deleted = structuralChangeBlock(step.left, 'deletion', factory, `section-${sectionIndex}-replace-delete-${blockIndex}`);
|
|
17609
17860
|
const inserted = structuralChangeBlock(step.right, 'insertion', factory, `section-${sectionIndex}-replace-insert-${blockIndex}`);
|
|
17610
|
-
if (deleted
|
|
17611
|
-
|
|
17861
|
+
if (deleted) slots.push({
|
|
17862
|
+
kind: 'node',
|
|
17863
|
+
node: deleted
|
|
17864
|
+
});
|
|
17865
|
+
if (inserted) slots.push({
|
|
17866
|
+
kind: 'node',
|
|
17867
|
+
node: inserted
|
|
17868
|
+
});
|
|
17869
|
+
if (!deleted || !inserted) diagnostics.push(structuralBlockDiagnostic(step.left, sectionIndex, blockIndex));
|
|
17612
17870
|
}
|
|
17613
|
-
|
|
17871
|
+
const pairedSlots = slots.filter((slot)=>'paired' === slot.kind);
|
|
17872
|
+
const initialComparisons = pairedSlots.map(({ comparison })=>({
|
|
17873
|
+
scope: comparison.scope,
|
|
17874
|
+
changes: comparison.inline.variant.changes
|
|
17875
|
+
}));
|
|
17876
|
+
const initialMovePairs = inferInlineMovePairsAcrossScopes(initialComparisons, work_document_compare_marksEqual);
|
|
17877
|
+
const attachedComparisons = pairedSlots.map(({ comparison })=>({
|
|
17878
|
+
scope: comparison.scope,
|
|
17879
|
+
changes: comparison.inline.attached?.changes ?? comparison.inline.variant.changes
|
|
17880
|
+
}));
|
|
17881
|
+
const attachedMovePairs = inferInlineMovePairsAcrossScopes(attachedComparisons, work_document_compare_marksEqual);
|
|
17882
|
+
const pairedByScope = new Map(pairedSlots.map((slot)=>[
|
|
17883
|
+
slot.comparison.scope,
|
|
17884
|
+
slot.comparison
|
|
17885
|
+
]));
|
|
17886
|
+
const attachedScopes = new Set();
|
|
17887
|
+
for (const pair of [
|
|
17888
|
+
...initialMovePairs,
|
|
17889
|
+
...attachedMovePairs
|
|
17890
|
+
]){
|
|
17891
|
+
const deletionScope = pair.deletion.scope ?? '';
|
|
17892
|
+
const insertionScope = pair.insertion.scope ?? '';
|
|
17893
|
+
if (deletionScope === insertionScope) continue;
|
|
17894
|
+
const deletionBlock = pairedByScope.get(deletionScope);
|
|
17895
|
+
const insertionBlock = pairedByScope.get(insertionScope);
|
|
17896
|
+
if (deletionBlock?.inline.attached && insertionBlock?.inline.attached) {
|
|
17897
|
+
attachedScopes.add(deletionScope);
|
|
17898
|
+
attachedScopes.add(insertionScope);
|
|
17899
|
+
}
|
|
17900
|
+
}
|
|
17901
|
+
const selectedComparisons = pairedSlots.map(({ comparison })=>({
|
|
17902
|
+
scope: comparison.scope,
|
|
17903
|
+
changes: (attachedScopes.has(comparison.scope) ? comparison.inline.attached : comparison.inline.variant)?.changes ?? comparison.inline.variant.changes
|
|
17904
|
+
}));
|
|
17905
|
+
const movePairs = inferInlineMovePairsAcrossScopes(selectedComparisons, work_document_compare_marksEqual);
|
|
17906
|
+
const useAttachedVariants = attachedScopes.size > 0 && movePairs.length >= initialMovePairs.length;
|
|
17907
|
+
const movesByScope = new Map();
|
|
17908
|
+
for (const pair of useAttachedVariants ? movePairs : initialMovePairs){
|
|
17909
|
+
const identity = factory.create('move');
|
|
17910
|
+
appendMoveAssignment(movesByScope, pair.deletion, 'from', identity);
|
|
17911
|
+
appendMoveAssignment(movesByScope, pair.insertion, 'to', identity);
|
|
17912
|
+
}
|
|
17913
|
+
return slots.flatMap((slot)=>{
|
|
17914
|
+
if ('node' === slot.kind) return [
|
|
17915
|
+
slot.node
|
|
17916
|
+
];
|
|
17917
|
+
const compared = renderPairedBlock(slot.comparison, factory, movesByScope.get(slot.comparison.scope), useAttachedVariants && attachedScopes.has(slot.comparison.scope) ? slot.comparison.inline.attached ?? slot.comparison.inline.variant : slot.comparison.inline.variant);
|
|
17918
|
+
return compared ? [
|
|
17919
|
+
compared
|
|
17920
|
+
] : [];
|
|
17921
|
+
});
|
|
17922
|
+
}
|
|
17923
|
+
function appendMoveAssignment(target, candidate, role, identity) {
|
|
17924
|
+
const scope = candidate.scope ?? '';
|
|
17925
|
+
const assignments = target.get(scope) ?? [];
|
|
17926
|
+
assignments.push({
|
|
17927
|
+
role,
|
|
17928
|
+
candidate,
|
|
17929
|
+
identity
|
|
17930
|
+
});
|
|
17931
|
+
target.set(scope, assignments);
|
|
17614
17932
|
}
|
|
17615
|
-
function
|
|
17933
|
+
function preparePairedBlock(current, revised, scope) {
|
|
17616
17934
|
if (!isSimpleTextBlock(current) || !isSimpleTextBlock(revised)) return null;
|
|
17617
17935
|
if (current.type !== revised.type || blockStructuralSignature(current) !== blockStructuralSignature(revised)) return null;
|
|
17618
|
-
const
|
|
17936
|
+
const comparison = prepareInlineComparison(current, revised);
|
|
17937
|
+
return comparison ? {
|
|
17938
|
+
current,
|
|
17939
|
+
revised,
|
|
17940
|
+
scope,
|
|
17941
|
+
inline: comparison
|
|
17942
|
+
} : null;
|
|
17943
|
+
}
|
|
17944
|
+
function renderPairedBlock(comparison, factory, moves, variant) {
|
|
17945
|
+
const { current, revised } = comparison;
|
|
17946
|
+
const inline = renderInlineComparison(current, variant, factory, moves);
|
|
17619
17947
|
if (!inline) return null;
|
|
17620
17948
|
const before = serializeDocumentParagraphFormatting(current.attrs);
|
|
17621
17949
|
const after = serializeDocumentParagraphFormatting(revised.attrs);
|
|
@@ -17637,31 +17965,65 @@ function comparePairedBlocks(current, revised, factory) {
|
|
|
17637
17965
|
}
|
|
17638
17966
|
return current.type.create(attributes, inline);
|
|
17639
17967
|
}
|
|
17640
|
-
function
|
|
17641
|
-
const
|
|
17642
|
-
const
|
|
17643
|
-
if (!
|
|
17644
|
-
const diff = boundedDocumentSequenceDiff(
|
|
17645
|
-
const
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17968
|
+
function prepareInlineComparison(current, revised) {
|
|
17969
|
+
const baseCurrentUnits = inlineUnits(current);
|
|
17970
|
+
const baseRevisedUnits = inlineUnits(revised);
|
|
17971
|
+
if (!baseCurrentUnits || !baseRevisedUnits) return null;
|
|
17972
|
+
const diff = boundedDocumentSequenceDiff(baseCurrentUnits, baseRevisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
|
|
17973
|
+
const baseVariant = {
|
|
17974
|
+
currentUnits: baseCurrentUnits,
|
|
17975
|
+
revisedUnits: baseRevisedUnits,
|
|
17976
|
+
changes: diff ?? [
|
|
17977
|
+
{
|
|
17978
|
+
kind: 'delete',
|
|
17979
|
+
left: baseCurrentUnits,
|
|
17980
|
+
right: []
|
|
17981
|
+
},
|
|
17982
|
+
{
|
|
17983
|
+
kind: 'insert',
|
|
17984
|
+
left: [],
|
|
17985
|
+
right: baseRevisedUnits
|
|
17986
|
+
}
|
|
17987
|
+
]
|
|
17988
|
+
};
|
|
17989
|
+
const variant = baseVariant;
|
|
17990
|
+
const baseMovePairs = inferInlineMovePairs(variant.changes, work_document_compare_marksEqual);
|
|
17991
|
+
const moveCurrentUnits = inlineUnits(current, true);
|
|
17992
|
+
const moveRevisedUnits = inlineUnits(revised, true);
|
|
17993
|
+
if (moveCurrentUnits && moveRevisedUnits && current.textContent.length + revised.textContent.length <= 131072) {
|
|
17994
|
+
const moveDiff = boundedDocumentSequenceDiff(moveCurrentUnits, moveRevisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
|
|
17995
|
+
if (moveDiff) {
|
|
17996
|
+
const moveChanges = moveDiff;
|
|
17997
|
+
const moveVariant = {
|
|
17998
|
+
currentUnits: moveCurrentUnits,
|
|
17999
|
+
revisedUnits: moveRevisedUnits,
|
|
18000
|
+
changes: moveChanges
|
|
18001
|
+
};
|
|
18002
|
+
const movePairs = inferInlineMovePairs(moveChanges, work_document_compare_marksEqual);
|
|
18003
|
+
if (movePairs.length >= baseMovePairs.length && movePairs.length > 0) return {
|
|
18004
|
+
variant: moveVariant,
|
|
18005
|
+
attached: moveVariant
|
|
18006
|
+
};
|
|
18007
|
+
return {
|
|
18008
|
+
variant,
|
|
18009
|
+
attached: moveVariant
|
|
18010
|
+
};
|
|
17655
18011
|
}
|
|
17656
|
-
|
|
18012
|
+
}
|
|
18013
|
+
return {
|
|
18014
|
+
variant
|
|
18015
|
+
};
|
|
18016
|
+
}
|
|
18017
|
+
function renderInlineComparison(current, variant, factory, moves) {
|
|
18018
|
+
const { changes } = variant;
|
|
17657
18019
|
const nodes = [];
|
|
17658
|
-
for (const change of changes){
|
|
18020
|
+
for (const [stepIndex, change] of changes.entries()){
|
|
17659
18021
|
if ('delete' === change.kind) {
|
|
17660
|
-
|
|
18022
|
+
appendComparisonRevisionUnits(nodes, change.left, 'deletion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, moves?.filter((move)=>move.candidate.stepIndex === stepIndex));
|
|
17661
18023
|
continue;
|
|
17662
18024
|
}
|
|
17663
18025
|
if ('insert' === change.kind) {
|
|
17664
|
-
|
|
18026
|
+
appendComparisonRevisionUnits(nodes, change.right, 'insertion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, moves?.filter((move)=>move.candidate.stepIndex === stepIndex));
|
|
17665
18027
|
continue;
|
|
17666
18028
|
}
|
|
17667
18029
|
let formattingIdentity = null;
|
|
@@ -17669,7 +18031,7 @@ function compareInlineContent(current, revised, factory) {
|
|
|
17669
18031
|
for(let index = 0; index < change.left.length; index += 1){
|
|
17670
18032
|
const left = change.left[index];
|
|
17671
18033
|
const right = change.right[index];
|
|
17672
|
-
if (
|
|
18034
|
+
if (work_document_compare_marksEqual(left.marks, right.marks)) {
|
|
17673
18035
|
nodes.push(current.type.schema.text(left.text, [
|
|
17674
18036
|
...left.marks
|
|
17675
18037
|
]));
|
|
@@ -17693,12 +18055,12 @@ function compareInlineContent(current, revised, factory) {
|
|
|
17693
18055
|
}
|
|
17694
18056
|
const deletion = factory.create('deletion');
|
|
17695
18057
|
const insertion = factory.create('insertion');
|
|
17696
|
-
|
|
18058
|
+
work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17697
18059
|
left
|
|
17698
|
-
], 'deletion', deletion, current.type.schema);
|
|
17699
|
-
|
|
18060
|
+
], 'deletion', deletion, current.type.schema, withoutReviewMarks);
|
|
18061
|
+
work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17700
18062
|
right
|
|
17701
|
-
], 'insertion', insertion, current.type.schema);
|
|
18063
|
+
], 'insertion', insertion, current.type.schema, withoutReviewMarks);
|
|
17702
18064
|
formattingIdentity = null;
|
|
17703
18065
|
formattingSignature = '';
|
|
17704
18066
|
}
|
|
@@ -17713,9 +18075,9 @@ function structuralChangeBlock(block, kind, factory, identityChannel) {
|
|
|
17713
18075
|
textId: block.attrs.textId
|
|
17714
18076
|
};
|
|
17715
18077
|
const nodes = [];
|
|
17716
|
-
for (const unit of inlineUnits(block) ?? [])
|
|
18078
|
+
for (const unit of inlineUnits(block) ?? [])work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17717
18079
|
unit
|
|
17718
|
-
], kind, identity, block.type.schema);
|
|
18080
|
+
], kind, identity, block.type.schema, withoutReviewMarks);
|
|
17719
18081
|
return block.type.create({
|
|
17720
18082
|
...block.attrs,
|
|
17721
18083
|
...paragraphIdentity,
|
|
@@ -17726,19 +18088,6 @@ function structuralChangeBlock(block, kind, factory, identityChannel) {
|
|
|
17726
18088
|
blockChangeDate: identity.date
|
|
17727
18089
|
}, model_Fragment.fromArray(nodes));
|
|
17728
18090
|
}
|
|
17729
|
-
function appendRevisionUnits(target, units, kind, identity, schema) {
|
|
17730
|
-
for (const unit of units)target.push(schema.text(unit.text, [
|
|
17731
|
-
...withoutReviewMarks(unit.marks),
|
|
17732
|
-
schema.marks.documentChange.create({
|
|
17733
|
-
kind,
|
|
17734
|
-
id: identity.id,
|
|
17735
|
-
actorId: identity.actorId ?? '',
|
|
17736
|
-
author: identity.author,
|
|
17737
|
-
date: identity.date,
|
|
17738
|
-
before: ''
|
|
17739
|
-
})
|
|
17740
|
-
]));
|
|
17741
|
-
}
|
|
17742
18091
|
function comparisonMark(node, kind, identity, before) {
|
|
17743
18092
|
return node.type.schema.marks.documentChange.create({
|
|
17744
18093
|
kind,
|
|
@@ -17822,19 +18171,35 @@ function hasStructuralBlockRevisions(document1) {
|
|
|
17822
18171
|
});
|
|
17823
18172
|
return found;
|
|
17824
18173
|
}
|
|
17825
|
-
function inlineUnits(node) {
|
|
18174
|
+
function inlineUnits(node, attachLeadingWhitespace = false) {
|
|
17826
18175
|
if (!isSimpleTextBlock(node)) return null;
|
|
17827
18176
|
const units = [];
|
|
17828
18177
|
node.forEach((child)=>{
|
|
17829
18178
|
const text = child.text ?? '';
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
18179
|
+
let pendingWhitespace = '';
|
|
18180
|
+
for (const part of text.match(/\s+|[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu) ?? []){
|
|
18181
|
+
if (attachLeadingWhitespace && '' === part.trim()) {
|
|
18182
|
+
pendingWhitespace += part;
|
|
18183
|
+
continue;
|
|
18184
|
+
}
|
|
18185
|
+
units.push({
|
|
18186
|
+
text: attachLeadingWhitespace ? `${pendingWhitespace}${part}` : part,
|
|
18187
|
+
marks: child.marks,
|
|
18188
|
+
...attachLeadingWhitespace ? {
|
|
18189
|
+
leadingWhitespaceAttached: true
|
|
18190
|
+
} : {}
|
|
18191
|
+
});
|
|
18192
|
+
pendingWhitespace = '';
|
|
18193
|
+
}
|
|
18194
|
+
if (attachLeadingWhitespace && pendingWhitespace) units.push({
|
|
18195
|
+
text: pendingWhitespace,
|
|
18196
|
+
marks: child.marks,
|
|
18197
|
+
leadingWhitespaceAttached: true
|
|
17833
18198
|
});
|
|
17834
18199
|
});
|
|
17835
18200
|
return units;
|
|
17836
18201
|
}
|
|
17837
|
-
function
|
|
18202
|
+
function work_document_compare_marksEqual(left, right) {
|
|
17838
18203
|
const normalizedLeft = withoutReviewMarks(left);
|
|
17839
18204
|
const normalizedRight = withoutReviewMarks(right);
|
|
17840
18205
|
return normalizedLeft.length === normalizedRight.length && normalizedLeft.every((mark, index)=>mark.eq(normalizedRight[index]));
|
|
@@ -17920,42 +18285,6 @@ function transferCombineIdentities(current, reviewed) {
|
|
|
17920
18285
|
if ('documentSection' === reviewed.type.name) attributes.id = current.attrs.id;
|
|
17921
18286
|
return reviewed.isText ? reviewed : reviewed.type.create(attributes, model_Fragment.fromArray(content), reviewed.marks);
|
|
17922
18287
|
}
|
|
17923
|
-
function comparisonIdentityFactory(current, revised, options) {
|
|
17924
|
-
const seed = stableHash(`${options.mode}\u0000${comparisonSemanticSignature(current)}\u0000${comparisonSemanticSignature(revised)}`);
|
|
17925
|
-
const author = boundedMetadata(options.author, 256) || 'A3S Work user';
|
|
17926
|
-
const date = normalizedDate(options.date);
|
|
17927
|
-
const summary = emptyComparisonSummary();
|
|
17928
|
-
let sequence = 0;
|
|
17929
|
-
return {
|
|
17930
|
-
summary,
|
|
17931
|
-
create (kind) {
|
|
17932
|
-
sequence += 1;
|
|
17933
|
-
if ('insertion' === kind) summary.insertions += 1;
|
|
17934
|
-
else if ('deletion' === kind) summary.deletions += 1;
|
|
17935
|
-
else if ('formatting' === kind) summary.formatting += 1;
|
|
17936
|
-
else summary.paragraphFormatting += 1;
|
|
17937
|
-
return {
|
|
17938
|
-
id: `compare-${seed}-${kind}-${sequence.toString(36)}`,
|
|
17939
|
-
author,
|
|
17940
|
-
date
|
|
17941
|
-
};
|
|
17942
|
-
},
|
|
17943
|
-
paragraphIdentity (channel) {
|
|
17944
|
-
return {
|
|
17945
|
-
paragraphId: wordParagraphId(`${seed}:${channel}:paragraph`),
|
|
17946
|
-
textId: wordParagraphId(`${seed}:${channel}:text`)
|
|
17947
|
-
};
|
|
17948
|
-
}
|
|
17949
|
-
};
|
|
17950
|
-
}
|
|
17951
|
-
function summarizeChanges(changes) {
|
|
17952
|
-
const summary = emptyComparisonSummary();
|
|
17953
|
-
for (const change of changes)if ('insertion' === change.kind) summary.insertions += 1;
|
|
17954
|
-
else if ('deletion' === change.kind) summary.deletions += 1;
|
|
17955
|
-
else if ('formatting' === change.kind) summary.formatting += 1;
|
|
17956
|
-
else summary.paragraphFormatting += 1;
|
|
17957
|
-
return summary;
|
|
17958
|
-
}
|
|
17959
18288
|
function structuralBlockDiagnostic(block, section, blockIndex) {
|
|
17960
18289
|
return 'paragraph' === block.type.name || 'heading' === block.type.name ? {
|
|
17961
18290
|
code: 'empty-structural-change',
|
|
@@ -17978,14 +18307,6 @@ function unsupportedResult(diagnostic) {
|
|
|
17978
18307
|
]
|
|
17979
18308
|
};
|
|
17980
18309
|
}
|
|
17981
|
-
function emptyComparisonSummary() {
|
|
17982
|
-
return {
|
|
17983
|
-
deletions: 0,
|
|
17984
|
-
formatting: 0,
|
|
17985
|
-
insertions: 0,
|
|
17986
|
-
paragraphFormatting: 0
|
|
17987
|
-
};
|
|
17988
|
-
}
|
|
17989
18310
|
function childNodes(node) {
|
|
17990
18311
|
const children = [];
|
|
17991
18312
|
node.forEach((child)=>{
|
|
@@ -18185,7 +18506,7 @@ function DocumentCompareDialog({ initialMode, restoreFocusTarget, onApplied, onC
|
|
|
18185
18506
|
}
|
|
18186
18507
|
}),
|
|
18187
18508
|
/*#__PURE__*/ jsx("small", {
|
|
18188
|
-
children: "
|
|
18509
|
+
children: "该名称会显示在生成的插入、删除、移动与格式修订中。"
|
|
18189
18510
|
})
|
|
18190
18511
|
]
|
|
18191
18512
|
}),
|
|
@@ -18223,7 +18544,7 @@ function ComparisonBoundary({ mode }) {
|
|
|
18223
18544
|
children: 'compare' === mode ? '确定性比较边界' : '安全合并边界'
|
|
18224
18545
|
}),
|
|
18225
18546
|
/*#__PURE__*/ jsx("p", {
|
|
18226
|
-
children: 'compare' === mode ? '
|
|
18547
|
+
children: 'compare' === mode ? '支持同一分节布局中的段落、标题、文字、格式差异,以及同一段内可安全识别的文本移动;复杂对象或节布局变化会明确停止。' : '审阅副本必须包含修订,且拒绝全部修订后与当前文档一致;现有修订须先处理。'
|
|
18227
18548
|
})
|
|
18228
18549
|
]
|
|
18229
18550
|
});
|
|
@@ -18294,7 +18615,7 @@ function useDocumentComparison({ editor, onApplied }) {
|
|
|
18294
18615
|
}
|
|
18295
18616
|
};
|
|
18296
18617
|
const applied = (result)=>{
|
|
18297
|
-
const count = result.summary.insertions + result.summary.deletions + result.summary.formatting + result.summary.paragraphFormatting;
|
|
18618
|
+
const count = result.summary.insertions + result.summary.deletions + result.summary.formatting + result.summary.paragraphFormatting + (result.summary.moves ?? 0);
|
|
18298
18619
|
close();
|
|
18299
18620
|
onApplied();
|
|
18300
18621
|
showToast(`已生成 ${count} 项可审阅修订`, 'success');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { WorkDocumentChange, WorkDocumentChangeIdentity } from './work-document-changes';
|
|
2
|
+
import type { WorkDocumentChangeKind } from './work-types';
|
|
3
|
+
export type DocumentComparisonMode = 'compare' | 'combine';
|
|
4
|
+
export interface DocumentComparisonSummary {
|
|
5
|
+
insertions: number;
|
|
6
|
+
deletions: number;
|
|
7
|
+
formatting: number;
|
|
8
|
+
paragraphFormatting: number;
|
|
9
|
+
/** Number of paired text moves inferred by Compare (when non-zero). */
|
|
10
|
+
moves?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface DocumentComparisonOptions {
|
|
13
|
+
mode: DocumentComparisonMode;
|
|
14
|
+
author: string;
|
|
15
|
+
date: string;
|
|
16
|
+
sourceName: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ComparisonIdentityFactory {
|
|
19
|
+
create(kind: WorkDocumentChangeKind, before?: string): WorkDocumentChangeIdentity;
|
|
20
|
+
paragraphIdentity(channel: string): {
|
|
21
|
+
paragraphId: string;
|
|
22
|
+
textId: string;
|
|
23
|
+
};
|
|
24
|
+
summary: DocumentComparisonSummary;
|
|
25
|
+
}
|
|
26
|
+
export declare function createComparisonIdentityFactory(currentSignature: string, revisedSignature: string, options: DocumentComparisonOptions): ComparisonIdentityFactory;
|
|
27
|
+
export declare function summarizeComparisonChanges(changes: readonly WorkDocumentChange[]): DocumentComparisonSummary;
|
|
28
|
+
export declare function emptyComparisonSummary(): DocumentComparisonSummary;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Mark as ProseMirrorMark, Node as ProseMirrorNode, Schema } from '@tiptap/pm/model';
|
|
2
|
+
import type { WorkDocumentChangeIdentity, WorkDocumentChangeKind } from './work-document-changes';
|
|
3
|
+
import type { WorkDocumentMoveRole } from './work-types';
|
|
4
|
+
export interface InlineUnit {
|
|
5
|
+
text: string;
|
|
6
|
+
marks: readonly ProseMirrorMark[];
|
|
7
|
+
leadingWhitespaceAttached?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type InlineDiffStep = {
|
|
10
|
+
kind: 'equal';
|
|
11
|
+
left: InlineUnit[];
|
|
12
|
+
right: InlineUnit[];
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'delete';
|
|
15
|
+
left: InlineUnit[];
|
|
16
|
+
right: [];
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'insert';
|
|
19
|
+
left: [];
|
|
20
|
+
right: InlineUnit[];
|
|
21
|
+
};
|
|
22
|
+
export interface InlineMoveCandidate {
|
|
23
|
+
/** Stable comparison scope (for example, the containing paragraph). */
|
|
24
|
+
scope?: string;
|
|
25
|
+
stepIndex: number;
|
|
26
|
+
kind: 'delete' | 'insert';
|
|
27
|
+
units: InlineUnit[];
|
|
28
|
+
start: number;
|
|
29
|
+
end: number;
|
|
30
|
+
startOffset: number;
|
|
31
|
+
endOffset: number;
|
|
32
|
+
text: string;
|
|
33
|
+
}
|
|
34
|
+
export interface InlineMovePair {
|
|
35
|
+
deletion: InlineMoveCandidate;
|
|
36
|
+
insertion: InlineMoveCandidate;
|
|
37
|
+
}
|
|
38
|
+
export interface InlineMoveComparison {
|
|
39
|
+
scope: string;
|
|
40
|
+
changes: readonly InlineDiffStep[];
|
|
41
|
+
}
|
|
42
|
+
export interface InlineMoveAssignment {
|
|
43
|
+
role: WorkDocumentMoveRole;
|
|
44
|
+
candidate: InlineMoveCandidate;
|
|
45
|
+
identity: WorkDocumentChangeIdentity;
|
|
46
|
+
}
|
|
47
|
+
export interface ComparisonMoveIdentityFactory {
|
|
48
|
+
create(kind: WorkDocumentChangeKind): WorkDocumentChangeIdentity;
|
|
49
|
+
}
|
|
50
|
+
type MarksEqual = (left: readonly ProseMirrorMark[], right: readonly ProseMirrorMark[]) => boolean;
|
|
51
|
+
type StripReviewMarks = (marks: readonly ProseMirrorMark[]) => ProseMirrorMark[];
|
|
52
|
+
type AppendRevisionUnits = (target: ProseMirrorNode[], units: readonly InlineUnit[], kind: 'insertion' | 'deletion', identity: WorkDocumentChangeIdentity, schema: Schema, stripReviewMarks: StripReviewMarks) => void;
|
|
53
|
+
export declare const MAX_INFERRED_MOVE_TEXT = 65536;
|
|
54
|
+
export declare function appendRevisionUnits(target: ProseMirrorNode[], units: readonly InlineUnit[], kind: 'insertion' | 'deletion', identity: WorkDocumentChangeIdentity, schema: Schema, stripReviewMarks: StripReviewMarks): void;
|
|
55
|
+
/**
|
|
56
|
+
* Pairs deterministic same-text ranges. A comparison may contain several
|
|
57
|
+
* simple blocks, which lets Compare represent a bounded move between
|
|
58
|
+
* paragraphs while still rejecting duplicate or mark-mismatched candidates.
|
|
59
|
+
*/
|
|
60
|
+
export declare function inferInlineMovePairs(changes: readonly InlineDiffStep[], marksEqual: MarksEqual): InlineMovePair[];
|
|
61
|
+
/**
|
|
62
|
+
* Infers moves across a bounded set of simple-block diffs. Candidate scopes
|
|
63
|
+
* are retained on each side so callers can put the resulting marks back into
|
|
64
|
+
* the right paragraph without flattening the document tree.
|
|
65
|
+
*/
|
|
66
|
+
export declare function inferInlineMovePairsAcrossScopes(comparisons: readonly InlineMoveComparison[], marksEqual: MarksEqual): InlineMovePair[];
|
|
67
|
+
/**
|
|
68
|
+
* Splits an ordinary diff step around paired move ranges while retaining the
|
|
69
|
+
* exact whitespace and mark boundaries needed to reconstruct either version.
|
|
70
|
+
*/
|
|
71
|
+
export declare function appendComparisonRevisionUnits(target: ProseMirrorNode[], units: InlineUnit[], kind: 'insertion' | 'deletion', schema: Schema, factory: ComparisonMoveIdentityFactory, stripReviewMarks: StripReviewMarks, appendRevisionUnits: AppendRevisionUnits, moves?: readonly InlineMoveAssignment[]): void;
|
|
72
|
+
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Content, type Editor } from '@tiptap/core';
|
|
2
|
-
|
|
2
|
+
import { type DocumentComparisonOptions, type DocumentComparisonSummary } from './work-document-compare-identities';
|
|
3
|
+
export type { DocumentComparisonMode, DocumentComparisonOptions, DocumentComparisonSummary, } from './work-document-compare-identities';
|
|
3
4
|
export type DocumentComparisonDiagnosticCode = 'changed-complex-structure' | 'combine-baseline-mismatch' | 'combine-resolution-invalid' | 'combine-structural-revisions' | 'combine-without-revisions' | 'comparison-limit-exceeded' | 'current-revisions-present' | 'empty-structural-change' | 'invalid-revised-content' | 'revised-revisions-present' | 'section-layout-mismatch' | 'unsupported-inline-review-state';
|
|
4
5
|
export interface DocumentComparisonDiagnostic {
|
|
5
6
|
code: DocumentComparisonDiagnosticCode;
|
|
@@ -7,18 +8,6 @@ export interface DocumentComparisonDiagnostic {
|
|
|
7
8
|
section?: number;
|
|
8
9
|
block?: number;
|
|
9
10
|
}
|
|
10
|
-
export interface DocumentComparisonSummary {
|
|
11
|
-
insertions: number;
|
|
12
|
-
deletions: number;
|
|
13
|
-
formatting: number;
|
|
14
|
-
paragraphFormatting: number;
|
|
15
|
-
}
|
|
16
|
-
export interface DocumentComparisonOptions {
|
|
17
|
-
mode: DocumentComparisonMode;
|
|
18
|
-
author: string;
|
|
19
|
-
date: string;
|
|
20
|
-
sourceName: string;
|
|
21
|
-
}
|
|
22
11
|
export interface DocumentComparisonApplyResult {
|
|
23
12
|
status: 'applied' | 'unchanged' | 'unsupported';
|
|
24
13
|
summary: DocumentComparisonSummary;
|
|
@@ -296,12 +296,19 @@ The current and imported TipTap/ProseMirror trees remain immutable while a
|
|
|
296
296
|
deterministic weighted block alignment pairs related paragraphs and headings;
|
|
297
297
|
an LCS token diff then produces insertion and deletion marks, while exact mark
|
|
298
298
|
and paragraph-property snapshots produce character- and paragraph-formatting
|
|
299
|
-
revisions.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
299
|
+
revisions. A bounded move pass may pair a lexical range that occurs exactly
|
|
300
|
+
once in a delete chunk and once in an insert chunk when its marks and carried
|
|
301
|
+
separators match. The pass retains a stable paragraph scope, so it can pair
|
|
302
|
+
aligned simple text blocks within one document section without flattening the
|
|
303
|
+
document tree; section boundaries, rich blocks, and structural objects remain
|
|
304
|
+
outside this inference. The result is one `move` identity with source and
|
|
305
|
+
destination roles. Stable semantic signatures seed generated identities, so the
|
|
306
|
+
same inputs and options produce the same ordering and IDs. Only a fully
|
|
307
|
+
admitted plan replaces the mounted document, and that replacement is one
|
|
308
|
+
transaction, one controlled publication, and one Undo record. Duplicate,
|
|
309
|
+
mark-mismatched, section-boundary, rich, and over-limit candidates remain
|
|
310
|
+
ordinary revisions or diagnostics rather than being guessed into moves; native
|
|
311
|
+
paired moves remain reviewable when they already exist.
|
|
305
312
|
|
|
306
313
|
Combine validates rather than guesses. It rejects every imported revision on
|
|
307
314
|
an immutable reviewed snapshot and compares the resulting semantic signature
|