@a3s-lab/office 0.49.0 → 0.50.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 +17 -0
- package/dist/0~document-editor.js +289 -80
- package/dist/internal/features/work/work-document-compare-identities.d.ts +28 -0
- package/dist/internal/features/work/work-document-compare-moves.d.ts +60 -0
- package/dist/internal/features/work/work-document-compare.d.ts +2 -13
- package/docs/latest/en/browser-editor-architecture.md +9 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -309,6 +309,23 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
|
|
|
309
309
|
|
|
310
310
|
## Current release
|
|
311
311
|
|
|
312
|
+
Version `0.50.0` extends Writer Compare with a bounded, WPS-referenced text
|
|
313
|
+
move workflow:
|
|
314
|
+
|
|
315
|
+
- Deterministic lexical ranges that move within one simple paragraph or heading
|
|
316
|
+
become one paired `move` review item. Separators travel with the range, and
|
|
317
|
+
accepting, rejecting, undoing, or reopening the DOCX preserves exact source
|
|
318
|
+
and revised text.
|
|
319
|
+
- The Review ribbon and Changes pane expose one localized **移动** card with
|
|
320
|
+
source/destination marks, destination navigation, updated author guidance,
|
|
321
|
+
and move-aware toast counts. Ambiguous duplicates, rich or relationship-bound
|
|
322
|
+
content, cross-paragraph moves, and over-limit inputs remain fail-closed
|
|
323
|
+
boundaries.
|
|
324
|
+
- The WPS 12.0 COM/UIA probe observed `CompareDocuments` returning ordinary
|
|
325
|
+
delete/insert records for the tested reorder. A3S therefore documents its
|
|
326
|
+
paired inference as a bounded local enhancement, with no claim that WPS
|
|
327
|
+
exposed a native move type through that API.
|
|
328
|
+
|
|
312
329
|
Version `0.49.0` adds bounded native Writer move revisions and hardens the
|
|
313
330
|
local PDF workflow:
|
|
314
331
|
|
|
@@ -17434,6 +17434,222 @@ 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
|
+
const fullCandidates = [];
|
|
17500
|
+
for (const [stepIndex, change] of changes.entries()){
|
|
17501
|
+
if ('equal' === change.kind) continue;
|
|
17502
|
+
const units = 'delete' === change.kind ? change.left : change.right;
|
|
17503
|
+
const candidate = inlineMoveCandidate(stepIndex, change.kind, units);
|
|
17504
|
+
if (candidate) fullCandidates.push(candidate);
|
|
17505
|
+
}
|
|
17506
|
+
const fullPairs = pairInlineMoveCandidates(fullCandidates, marksEqual);
|
|
17507
|
+
const consumedSteps = new Set();
|
|
17508
|
+
for (const pair of fullPairs){
|
|
17509
|
+
consumedSteps.add(pair.deletion.stepIndex);
|
|
17510
|
+
consumedSteps.add(pair.insertion.stepIndex);
|
|
17511
|
+
}
|
|
17512
|
+
const tokenCandidates = [];
|
|
17513
|
+
for (const [stepIndex, change] of changes.entries()){
|
|
17514
|
+
if ('equal' === change.kind || consumedSteps.has(stepIndex)) continue;
|
|
17515
|
+
const units = 'delete' === change.kind ? change.left : change.right;
|
|
17516
|
+
tokenCandidates.push(...inlineMoveTokenCandidates(stepIndex, change.kind, units));
|
|
17517
|
+
}
|
|
17518
|
+
return [
|
|
17519
|
+
...fullPairs,
|
|
17520
|
+
...pairInlineMoveCandidates(tokenCandidates, marksEqual)
|
|
17521
|
+
].sort((left, right)=>left.deletion.stepIndex - right.deletion.stepIndex || left.insertion.stepIndex - right.insertion.stepIndex || left.deletion.start - right.deletion.start).slice(0, MAX_INFERRED_MOVES);
|
|
17522
|
+
}
|
|
17523
|
+
function appendComparisonRevisionUnits(target, units, kind, schema, factory, stripReviewMarks, appendRevisionUnits, moves) {
|
|
17524
|
+
if (!moves?.length) return void appendRevisionUnits(target, units, kind, factory.create(kind), schema, stripReviewMarks);
|
|
17525
|
+
const byUnit = new Map();
|
|
17526
|
+
for (const move of [
|
|
17527
|
+
...moves
|
|
17528
|
+
].sort((left, right)=>left.candidate.start - right.candidate.start))for(let index = move.candidate.start; index < move.candidate.end; index += 1){
|
|
17529
|
+
if (byUnit.has(index)) return void appendRevisionUnits(target, units, kind, factory.create(kind), schema, stripReviewMarks);
|
|
17530
|
+
byUnit.set(index, move);
|
|
17531
|
+
}
|
|
17532
|
+
let ordinary = [];
|
|
17533
|
+
const flushOrdinary = ()=>{
|
|
17534
|
+
if (!ordinary.length) return;
|
|
17535
|
+
appendRevisionUnits(target, ordinary, kind, factory.create(kind), schema, stripReviewMarks);
|
|
17536
|
+
ordinary = [];
|
|
17537
|
+
};
|
|
17538
|
+
for (const [index, unit] of units.entries()){
|
|
17539
|
+
const move = byUnit.get(index);
|
|
17540
|
+
if (!move) {
|
|
17541
|
+
ordinary.push(unit);
|
|
17542
|
+
continue;
|
|
17543
|
+
}
|
|
17544
|
+
const start = index === move.candidate.start ? move.candidate.startOffset : 0;
|
|
17545
|
+
const end = index === move.candidate.end - 1 ? move.candidate.endOffset : unit.text.length;
|
|
17546
|
+
const prefix = unit.text.slice(0, start);
|
|
17547
|
+
const marked = unit.text.slice(start, end);
|
|
17548
|
+
const suffix = unit.text.slice(end);
|
|
17549
|
+
if (prefix) ordinary.push({
|
|
17550
|
+
text: prefix,
|
|
17551
|
+
marks: unit.marks
|
|
17552
|
+
});
|
|
17553
|
+
flushOrdinary();
|
|
17554
|
+
if (marked) appendMoveRevisionUnits(target, [
|
|
17555
|
+
{
|
|
17556
|
+
text: marked,
|
|
17557
|
+
marks: unit.marks
|
|
17558
|
+
}
|
|
17559
|
+
], move.role, move.identity, schema, stripReviewMarks);
|
|
17560
|
+
ordinary = suffix ? [
|
|
17561
|
+
{
|
|
17562
|
+
text: suffix,
|
|
17563
|
+
marks: unit.marks
|
|
17564
|
+
}
|
|
17565
|
+
] : [];
|
|
17566
|
+
}
|
|
17567
|
+
flushOrdinary();
|
|
17568
|
+
}
|
|
17569
|
+
function pairInlineMoveCandidates(candidates, marksEqual) {
|
|
17570
|
+
const grouped = new Map();
|
|
17571
|
+
for (const candidate of candidates){
|
|
17572
|
+
const group = grouped.get(candidate.text) ?? {
|
|
17573
|
+
deletion: [],
|
|
17574
|
+
insertion: []
|
|
17575
|
+
};
|
|
17576
|
+
('delete' === candidate.kind ? group.deletion : group.insertion).push(candidate);
|
|
17577
|
+
grouped.set(candidate.text, group);
|
|
17578
|
+
}
|
|
17579
|
+
return Array.from(grouped.values()).filter(({ deletion, insertion })=>1 === deletion.length && 1 === insertion.length && inlineMoveCandidatesEqual(deletion[0], insertion[0], marksEqual)).map(({ deletion, insertion })=>({
|
|
17580
|
+
deletion: deletion[0],
|
|
17581
|
+
insertion: insertion[0]
|
|
17582
|
+
})).sort((left, right)=>left.deletion.stepIndex - right.deletion.stepIndex || left.insertion.stepIndex - right.insertion.stepIndex).slice(0, MAX_INFERRED_MOVES);
|
|
17583
|
+
}
|
|
17584
|
+
function inlineMoveCandidate(stepIndex, kind, units) {
|
|
17585
|
+
let start = 0;
|
|
17586
|
+
while(start < units.length && isWhitespaceUnit(units[start]))start += 1;
|
|
17587
|
+
let end = units.length;
|
|
17588
|
+
while(end > start && isWhitespaceUnit(units[end - 1]))end -= 1;
|
|
17589
|
+
if (start === end) return null;
|
|
17590
|
+
const first = units[start];
|
|
17591
|
+
const last = units[end - 1];
|
|
17592
|
+
const startOffset = first.leadingWhitespaceAttached ? 0 : first.text.length - first.text.trimStart().length;
|
|
17593
|
+
const endOffset = last.leadingWhitespaceAttached ? last.text.length : last.text.trimEnd().length;
|
|
17594
|
+
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('');
|
|
17595
|
+
if (!text || text.length > MAX_INFERRED_MOVE_TEXT || !/[\p{L}\p{N}]/u.test(text)) return null;
|
|
17596
|
+
return {
|
|
17597
|
+
stepIndex,
|
|
17598
|
+
kind,
|
|
17599
|
+
units,
|
|
17600
|
+
start,
|
|
17601
|
+
end,
|
|
17602
|
+
startOffset,
|
|
17603
|
+
endOffset,
|
|
17604
|
+
text
|
|
17605
|
+
};
|
|
17606
|
+
}
|
|
17607
|
+
function inlineMoveTokenCandidates(stepIndex, kind, units) {
|
|
17608
|
+
const candidates = [];
|
|
17609
|
+
for (const [index, unit] of units.entries()){
|
|
17610
|
+
const leadingWhitespace = unit.text.length - unit.text.trimStart().length;
|
|
17611
|
+
const text = unit.text.slice(leadingWhitespace);
|
|
17612
|
+
if (text && !(text.length > MAX_INFERRED_MOVE_TEXT) && /[\p{L}\p{N}]/u.test(text)) candidates.push({
|
|
17613
|
+
stepIndex,
|
|
17614
|
+
kind,
|
|
17615
|
+
units,
|
|
17616
|
+
start: index,
|
|
17617
|
+
end: index + 1,
|
|
17618
|
+
startOffset: leadingWhitespace,
|
|
17619
|
+
endOffset: unit.text.length,
|
|
17620
|
+
text
|
|
17621
|
+
});
|
|
17622
|
+
}
|
|
17623
|
+
return candidates;
|
|
17624
|
+
}
|
|
17625
|
+
function inlineMoveCandidatesEqual(left, right, marksEqual) {
|
|
17626
|
+
const leftUnits = selectedMoveUnits(left);
|
|
17627
|
+
const rightUnits = selectedMoveUnits(right);
|
|
17628
|
+
return leftUnits.length === rightUnits.length && leftUnits.every((unit, index)=>unit.text === rightUnits[index]?.text && marksEqual(unit.marks, rightUnits[index]?.marks ?? []));
|
|
17629
|
+
}
|
|
17630
|
+
function selectedMoveUnits(candidate) {
|
|
17631
|
+
return candidate.units.slice(candidate.start, candidate.end).map((unit, index, selected)=>({
|
|
17632
|
+
text: unit.text.slice(0 === index ? candidate.startOffset : 0, index === selected.length - 1 ? candidate.endOffset : unit.text.length),
|
|
17633
|
+
marks: unit.marks
|
|
17634
|
+
})).filter((unit)=>unit.text.length > 0);
|
|
17635
|
+
}
|
|
17636
|
+
function isWhitespaceUnit(unit) {
|
|
17637
|
+
return '' === unit.text.trim();
|
|
17638
|
+
}
|
|
17639
|
+
function appendMoveRevisionUnits(target, units, role, identity, schema, stripReviewMarks) {
|
|
17640
|
+
for (const unit of units)target.push(schema.text(unit.text, [
|
|
17641
|
+
...stripReviewMarks(unit.marks),
|
|
17642
|
+
schema.marks.documentChange.create({
|
|
17643
|
+
kind: 'move',
|
|
17644
|
+
moveRole: role,
|
|
17645
|
+
id: identity.id,
|
|
17646
|
+
actorId: identity.actorId ?? '',
|
|
17647
|
+
author: identity.author,
|
|
17648
|
+
date: identity.date,
|
|
17649
|
+
before: ''
|
|
17650
|
+
})
|
|
17651
|
+
]));
|
|
17652
|
+
}
|
|
17437
17653
|
const MAX_COMPARISON_BLOCKS = 1024;
|
|
17438
17654
|
const MAX_COMPARISON_TEXT = 1000000;
|
|
17439
17655
|
const MAX_BLOCK_ALIGNMENT_CELLS = 1100000;
|
|
@@ -17497,7 +17713,7 @@ function planDocumentCompare(current, revised, options) {
|
|
|
17497
17713
|
const limits = comparisonLimits(currentSections, revisedSections);
|
|
17498
17714
|
if (limits) return unsupportedResult(limits);
|
|
17499
17715
|
const diagnostics = [];
|
|
17500
|
-
const factory =
|
|
17716
|
+
const factory = createComparisonIdentityFactory(comparisonSemanticSignature(current), comparisonSemanticSignature(revised), options);
|
|
17501
17717
|
const comparedSections = [];
|
|
17502
17718
|
for(let sectionIndex = 0; sectionIndex < currentSections.length; sectionIndex += 1){
|
|
17503
17719
|
const currentSection = currentSections[sectionIndex];
|
|
@@ -17566,7 +17782,7 @@ function planDocumentCombine(current, reviewed) {
|
|
|
17566
17782
|
return {
|
|
17567
17783
|
status: 'applied',
|
|
17568
17784
|
document: document1,
|
|
17569
|
-
summary:
|
|
17785
|
+
summary: summarizeComparisonChanges(reviewedChanges),
|
|
17570
17786
|
diagnostics: []
|
|
17571
17787
|
};
|
|
17572
17788
|
}
|
|
@@ -17638,11 +17854,11 @@ function comparePairedBlocks(current, revised, factory) {
|
|
|
17638
17854
|
return current.type.create(attributes, inline);
|
|
17639
17855
|
}
|
|
17640
17856
|
function compareInlineContent(current, revised, factory) {
|
|
17641
|
-
|
|
17642
|
-
|
|
17857
|
+
let currentUnits = inlineUnits(current);
|
|
17858
|
+
let revisedUnits = inlineUnits(revised);
|
|
17643
17859
|
if (!currentUnits || !revisedUnits) return null;
|
|
17644
17860
|
const diff = boundedDocumentSequenceDiff(currentUnits, revisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
|
|
17645
|
-
|
|
17861
|
+
let changes = diff ?? [
|
|
17646
17862
|
{
|
|
17647
17863
|
kind: 'delete',
|
|
17648
17864
|
left: currentUnits,
|
|
@@ -17654,14 +17870,48 @@ function compareInlineContent(current, revised, factory) {
|
|
|
17654
17870
|
right: revisedUnits
|
|
17655
17871
|
}
|
|
17656
17872
|
];
|
|
17873
|
+
const baseMovePairs = inferInlineMovePairs(changes, work_document_compare_marksEqual);
|
|
17874
|
+
const moveCurrentUnits = inlineUnits(current, true);
|
|
17875
|
+
const moveRevisedUnits = inlineUnits(revised, true);
|
|
17876
|
+
if (moveCurrentUnits && moveRevisedUnits && current.textContent.length + revised.textContent.length <= 131072) {
|
|
17877
|
+
const moveDiff = boundedDocumentSequenceDiff(moveCurrentUnits, moveRevisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
|
|
17878
|
+
if (moveDiff) {
|
|
17879
|
+
const moveChanges = moveDiff;
|
|
17880
|
+
const movePairs = inferInlineMovePairs(moveChanges, work_document_compare_marksEqual);
|
|
17881
|
+
if (movePairs.length >= baseMovePairs.length && movePairs.length > 0) {
|
|
17882
|
+
currentUnits = moveCurrentUnits;
|
|
17883
|
+
revisedUnits = moveRevisedUnits;
|
|
17884
|
+
changes = moveChanges;
|
|
17885
|
+
}
|
|
17886
|
+
}
|
|
17887
|
+
}
|
|
17888
|
+
const movePairs = inferInlineMovePairs(changes, work_document_compare_marksEqual);
|
|
17889
|
+
const movesByStep = new Map();
|
|
17890
|
+
for (const pair of movePairs){
|
|
17891
|
+
const identity = factory.create('move');
|
|
17892
|
+
const deletion = movesByStep.get(pair.deletion.stepIndex) ?? [];
|
|
17893
|
+
deletion.push({
|
|
17894
|
+
role: 'from',
|
|
17895
|
+
candidate: pair.deletion,
|
|
17896
|
+
identity
|
|
17897
|
+
});
|
|
17898
|
+
movesByStep.set(pair.deletion.stepIndex, deletion);
|
|
17899
|
+
const insertion = movesByStep.get(pair.insertion.stepIndex) ?? [];
|
|
17900
|
+
insertion.push({
|
|
17901
|
+
role: 'to',
|
|
17902
|
+
candidate: pair.insertion,
|
|
17903
|
+
identity
|
|
17904
|
+
});
|
|
17905
|
+
movesByStep.set(pair.insertion.stepIndex, insertion);
|
|
17906
|
+
}
|
|
17657
17907
|
const nodes = [];
|
|
17658
|
-
for (const change of changes){
|
|
17908
|
+
for (const [stepIndex, change] of changes.entries()){
|
|
17659
17909
|
if ('delete' === change.kind) {
|
|
17660
|
-
|
|
17910
|
+
appendComparisonRevisionUnits(nodes, change.left, 'deletion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, movesByStep.get(stepIndex));
|
|
17661
17911
|
continue;
|
|
17662
17912
|
}
|
|
17663
17913
|
if ('insert' === change.kind) {
|
|
17664
|
-
|
|
17914
|
+
appendComparisonRevisionUnits(nodes, change.right, 'insertion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, movesByStep.get(stepIndex));
|
|
17665
17915
|
continue;
|
|
17666
17916
|
}
|
|
17667
17917
|
let formattingIdentity = null;
|
|
@@ -17669,7 +17919,7 @@ function compareInlineContent(current, revised, factory) {
|
|
|
17669
17919
|
for(let index = 0; index < change.left.length; index += 1){
|
|
17670
17920
|
const left = change.left[index];
|
|
17671
17921
|
const right = change.right[index];
|
|
17672
|
-
if (
|
|
17922
|
+
if (work_document_compare_marksEqual(left.marks, right.marks)) {
|
|
17673
17923
|
nodes.push(current.type.schema.text(left.text, [
|
|
17674
17924
|
...left.marks
|
|
17675
17925
|
]));
|
|
@@ -17693,12 +17943,12 @@ function compareInlineContent(current, revised, factory) {
|
|
|
17693
17943
|
}
|
|
17694
17944
|
const deletion = factory.create('deletion');
|
|
17695
17945
|
const insertion = factory.create('insertion');
|
|
17696
|
-
|
|
17946
|
+
work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17697
17947
|
left
|
|
17698
|
-
], 'deletion', deletion, current.type.schema);
|
|
17699
|
-
|
|
17948
|
+
], 'deletion', deletion, current.type.schema, withoutReviewMarks);
|
|
17949
|
+
work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17700
17950
|
right
|
|
17701
|
-
], 'insertion', insertion, current.type.schema);
|
|
17951
|
+
], 'insertion', insertion, current.type.schema, withoutReviewMarks);
|
|
17702
17952
|
formattingIdentity = null;
|
|
17703
17953
|
formattingSignature = '';
|
|
17704
17954
|
}
|
|
@@ -17713,9 +17963,9 @@ function structuralChangeBlock(block, kind, factory, identityChannel) {
|
|
|
17713
17963
|
textId: block.attrs.textId
|
|
17714
17964
|
};
|
|
17715
17965
|
const nodes = [];
|
|
17716
|
-
for (const unit of inlineUnits(block) ?? [])
|
|
17966
|
+
for (const unit of inlineUnits(block) ?? [])work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17717
17967
|
unit
|
|
17718
|
-
], kind, identity, block.type.schema);
|
|
17968
|
+
], kind, identity, block.type.schema, withoutReviewMarks);
|
|
17719
17969
|
return block.type.create({
|
|
17720
17970
|
...block.attrs,
|
|
17721
17971
|
...paragraphIdentity,
|
|
@@ -17726,19 +17976,6 @@ function structuralChangeBlock(block, kind, factory, identityChannel) {
|
|
|
17726
17976
|
blockChangeDate: identity.date
|
|
17727
17977
|
}, model_Fragment.fromArray(nodes));
|
|
17728
17978
|
}
|
|
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
17979
|
function comparisonMark(node, kind, identity, before) {
|
|
17743
17980
|
return node.type.schema.marks.documentChange.create({
|
|
17744
17981
|
kind,
|
|
@@ -17822,19 +18059,35 @@ function hasStructuralBlockRevisions(document1) {
|
|
|
17822
18059
|
});
|
|
17823
18060
|
return found;
|
|
17824
18061
|
}
|
|
17825
|
-
function inlineUnits(node) {
|
|
18062
|
+
function inlineUnits(node, attachLeadingWhitespace = false) {
|
|
17826
18063
|
if (!isSimpleTextBlock(node)) return null;
|
|
17827
18064
|
const units = [];
|
|
17828
18065
|
node.forEach((child)=>{
|
|
17829
18066
|
const text = child.text ?? '';
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
18067
|
+
let pendingWhitespace = '';
|
|
18068
|
+
for (const part of text.match(/\s+|[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu) ?? []){
|
|
18069
|
+
if (attachLeadingWhitespace && '' === part.trim()) {
|
|
18070
|
+
pendingWhitespace += part;
|
|
18071
|
+
continue;
|
|
18072
|
+
}
|
|
18073
|
+
units.push({
|
|
18074
|
+
text: attachLeadingWhitespace ? `${pendingWhitespace}${part}` : part,
|
|
18075
|
+
marks: child.marks,
|
|
18076
|
+
...attachLeadingWhitespace ? {
|
|
18077
|
+
leadingWhitespaceAttached: true
|
|
18078
|
+
} : {}
|
|
18079
|
+
});
|
|
18080
|
+
pendingWhitespace = '';
|
|
18081
|
+
}
|
|
18082
|
+
if (attachLeadingWhitespace && pendingWhitespace) units.push({
|
|
18083
|
+
text: pendingWhitespace,
|
|
18084
|
+
marks: child.marks,
|
|
18085
|
+
leadingWhitespaceAttached: true
|
|
17833
18086
|
});
|
|
17834
18087
|
});
|
|
17835
18088
|
return units;
|
|
17836
18089
|
}
|
|
17837
|
-
function
|
|
18090
|
+
function work_document_compare_marksEqual(left, right) {
|
|
17838
18091
|
const normalizedLeft = withoutReviewMarks(left);
|
|
17839
18092
|
const normalizedRight = withoutReviewMarks(right);
|
|
17840
18093
|
return normalizedLeft.length === normalizedRight.length && normalizedLeft.every((mark, index)=>mark.eq(normalizedRight[index]));
|
|
@@ -17920,42 +18173,6 @@ function transferCombineIdentities(current, reviewed) {
|
|
|
17920
18173
|
if ('documentSection' === reviewed.type.name) attributes.id = current.attrs.id;
|
|
17921
18174
|
return reviewed.isText ? reviewed : reviewed.type.create(attributes, model_Fragment.fromArray(content), reviewed.marks);
|
|
17922
18175
|
}
|
|
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
18176
|
function structuralBlockDiagnostic(block, section, blockIndex) {
|
|
17960
18177
|
return 'paragraph' === block.type.name || 'heading' === block.type.name ? {
|
|
17961
18178
|
code: 'empty-structural-change',
|
|
@@ -17978,14 +18195,6 @@ function unsupportedResult(diagnostic) {
|
|
|
17978
18195
|
]
|
|
17979
18196
|
};
|
|
17980
18197
|
}
|
|
17981
|
-
function emptyComparisonSummary() {
|
|
17982
|
-
return {
|
|
17983
|
-
deletions: 0,
|
|
17984
|
-
formatting: 0,
|
|
17985
|
-
insertions: 0,
|
|
17986
|
-
paragraphFormatting: 0
|
|
17987
|
-
};
|
|
17988
|
-
}
|
|
17989
18198
|
function childNodes(node) {
|
|
17990
18199
|
const children = [];
|
|
17991
18200
|
node.forEach((child)=>{
|
|
@@ -18185,7 +18394,7 @@ function DocumentCompareDialog({ initialMode, restoreFocusTarget, onApplied, onC
|
|
|
18185
18394
|
}
|
|
18186
18395
|
}),
|
|
18187
18396
|
/*#__PURE__*/ jsx("small", {
|
|
18188
|
-
children: "
|
|
18397
|
+
children: "该名称会显示在生成的插入、删除、移动与格式修订中。"
|
|
18189
18398
|
})
|
|
18190
18399
|
]
|
|
18191
18400
|
}),
|
|
@@ -18223,7 +18432,7 @@ function ComparisonBoundary({ mode }) {
|
|
|
18223
18432
|
children: 'compare' === mode ? '确定性比较边界' : '安全合并边界'
|
|
18224
18433
|
}),
|
|
18225
18434
|
/*#__PURE__*/ jsx("p", {
|
|
18226
|
-
children: 'compare' === mode ? '
|
|
18435
|
+
children: 'compare' === mode ? '支持同一分节布局中的段落、标题、文字、格式差异,以及同一段内可安全识别的文本移动;复杂对象或节布局变化会明确停止。' : '审阅副本必须包含修订,且拒绝全部修订后与当前文档一致;现有修订须先处理。'
|
|
18227
18436
|
})
|
|
18228
18437
|
]
|
|
18229
18438
|
});
|
|
@@ -18294,7 +18503,7 @@ function useDocumentComparison({ editor, onApplied }) {
|
|
|
18294
18503
|
}
|
|
18295
18504
|
};
|
|
18296
18505
|
const applied = (result)=>{
|
|
18297
|
-
const count = result.summary.insertions + result.summary.deletions + result.summary.formatting + result.summary.paragraphFormatting;
|
|
18506
|
+
const count = result.summary.insertions + result.summary.deletions + result.summary.formatting + result.summary.paragraphFormatting + (result.summary.moves ?? 0);
|
|
18298
18507
|
close();
|
|
18299
18508
|
onApplied();
|
|
18300
18509
|
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,60 @@
|
|
|
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
|
+
stepIndex: number;
|
|
24
|
+
kind: 'delete' | 'insert';
|
|
25
|
+
units: InlineUnit[];
|
|
26
|
+
start: number;
|
|
27
|
+
end: number;
|
|
28
|
+
startOffset: number;
|
|
29
|
+
endOffset: number;
|
|
30
|
+
text: string;
|
|
31
|
+
}
|
|
32
|
+
export interface InlineMovePair {
|
|
33
|
+
deletion: InlineMoveCandidate;
|
|
34
|
+
insertion: InlineMoveCandidate;
|
|
35
|
+
}
|
|
36
|
+
export interface InlineMoveAssignment {
|
|
37
|
+
role: WorkDocumentMoveRole;
|
|
38
|
+
candidate: InlineMoveCandidate;
|
|
39
|
+
identity: WorkDocumentChangeIdentity;
|
|
40
|
+
}
|
|
41
|
+
export interface ComparisonMoveIdentityFactory {
|
|
42
|
+
create(kind: WorkDocumentChangeKind): WorkDocumentChangeIdentity;
|
|
43
|
+
}
|
|
44
|
+
type MarksEqual = (left: readonly ProseMirrorMark[], right: readonly ProseMirrorMark[]) => boolean;
|
|
45
|
+
type StripReviewMarks = (marks: readonly ProseMirrorMark[]) => ProseMirrorMark[];
|
|
46
|
+
type AppendRevisionUnits = (target: ProseMirrorNode[], units: readonly InlineUnit[], kind: 'insertion' | 'deletion', identity: WorkDocumentChangeIdentity, schema: Schema, stripReviewMarks: StripReviewMarks) => void;
|
|
47
|
+
export declare const MAX_INFERRED_MOVE_TEXT = 65536;
|
|
48
|
+
export declare function appendRevisionUnits(target: ProseMirrorNode[], units: readonly InlineUnit[], kind: 'insertion' | 'deletion', identity: WorkDocumentChangeIdentity, schema: Schema, stripReviewMarks: StripReviewMarks): void;
|
|
49
|
+
/**
|
|
50
|
+
* Pairs only deterministic same-text ranges inside one block. Duplicate
|
|
51
|
+
* candidates and ranges whose marks differ are deliberately left as ordinary
|
|
52
|
+
* insertions/deletions so Compare never guesses at an ambiguous move.
|
|
53
|
+
*/
|
|
54
|
+
export declare function inferInlineMovePairs(changes: readonly InlineDiffStep[], marksEqual: MarksEqual): InlineMovePair[];
|
|
55
|
+
/**
|
|
56
|
+
* Splits an ordinary diff step around paired move ranges while retaining the
|
|
57
|
+
* exact whitespace and mark boundaries needed to reconstruct either version.
|
|
58
|
+
*/
|
|
59
|
+
export declare function appendComparisonRevisionUnits(target: ProseMirrorNode[], units: InlineUnit[], kind: 'insertion' | 'deletion', schema: Schema, factory: ComparisonMoveIdentityFactory, stripReviewMarks: StripReviewMarks, appendRevisionUnits: AppendRevisionUnits, moves?: readonly InlineMoveAssignment[]): void;
|
|
60
|
+
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,11 +296,15 @@ 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
|
-
|
|
299
|
+
revisions. A bounded same-paragraph pass may pair a lexical range that occurs
|
|
300
|
+
exactly once in a delete chunk and once in an insert chunk when its marks and
|
|
301
|
+
carried separators match; the result is one `move` identity with source and
|
|
302
|
+
destination roles. Stable semantic signatures seed generated identities, so the
|
|
303
|
+
same inputs and options produce the same ordering and IDs. Only a fully
|
|
304
|
+
admitted plan replaces the mounted document, and that replacement is one
|
|
305
|
+
transaction, one controlled publication, and one Undo record. Duplicate,
|
|
306
|
+
rich, cross-paragraph, and over-limit candidates remain ordinary revisions or
|
|
307
|
+
diagnostics rather than being guessed into moves; native paired moves remain
|
|
304
308
|
reviewable when they already exist.
|
|
305
309
|
|
|
306
310
|
Combine validates rather than guesses. It rejects every imported revision on
|