@a3s-lab/office 0.48.1 → 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/COLLABORATION_ROADMAP.md +14 -6
- package/README.md +33 -0
- package/dist/{0~3320.js → 0~5600.js} +271 -13
- package/dist/0~controlled-editor-composition.js +167 -15
- package/dist/0~document-editor.js +354 -90
- package/dist/0~work-docx-export.js +143 -9
- package/dist/0~work-docx-import.js +5 -120
- package/dist/0~work-office-diagnostics.js +16 -5
- package/dist/4174.js +93 -8
- package/dist/internal/features/work/editors/controlled-editor-composition.d.ts +53 -7
- package/dist/internal/features/work/editors/use-document-pagination.d.ts +3 -1
- package/dist/internal/features/work/work-document-changes.d.ts +4 -2
- 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/dist/internal/features/work/work-docx-change-import.d.ts +10 -1
- package/dist/internal/features/work/work-docx-move-revision-export.d.ts +35 -0
- package/dist/internal/features/work/work-types.d.ts +3 -1
- package/dist/office-kernel.wasm +0 -0
- package/dist/styles.css +9 -0
- package/docs/latest/en/browser-editor-architecture.md +21 -5
- package/package.json +11 -3
|
@@ -1172,6 +1172,7 @@ function documentChangeKindLabel(kind) {
|
|
|
1172
1172
|
if ('formatting' === kind) return '格式';
|
|
1173
1173
|
if ('paragraph-formatting' === kind) return '段落格式';
|
|
1174
1174
|
if ('numbering' === kind) return '编号格式';
|
|
1175
|
+
if ('move' === kind) return '移动';
|
|
1175
1176
|
return '删除';
|
|
1176
1177
|
}
|
|
1177
1178
|
function documentChangeWindowKey(change) {
|
|
@@ -17433,6 +17434,222 @@ function sortJsonValue(value) {
|
|
|
17433
17434
|
sortJsonValue(child)
|
|
17434
17435
|
]));
|
|
17435
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
|
+
}
|
|
17436
17653
|
const MAX_COMPARISON_BLOCKS = 1024;
|
|
17437
17654
|
const MAX_COMPARISON_TEXT = 1000000;
|
|
17438
17655
|
const MAX_BLOCK_ALIGNMENT_CELLS = 1100000;
|
|
@@ -17496,7 +17713,7 @@ function planDocumentCompare(current, revised, options) {
|
|
|
17496
17713
|
const limits = comparisonLimits(currentSections, revisedSections);
|
|
17497
17714
|
if (limits) return unsupportedResult(limits);
|
|
17498
17715
|
const diagnostics = [];
|
|
17499
|
-
const factory =
|
|
17716
|
+
const factory = createComparisonIdentityFactory(comparisonSemanticSignature(current), comparisonSemanticSignature(revised), options);
|
|
17500
17717
|
const comparedSections = [];
|
|
17501
17718
|
for(let sectionIndex = 0; sectionIndex < currentSections.length; sectionIndex += 1){
|
|
17502
17719
|
const currentSection = currentSections[sectionIndex];
|
|
@@ -17565,7 +17782,7 @@ function planDocumentCombine(current, reviewed) {
|
|
|
17565
17782
|
return {
|
|
17566
17783
|
status: 'applied',
|
|
17567
17784
|
document: document1,
|
|
17568
|
-
summary:
|
|
17785
|
+
summary: summarizeComparisonChanges(reviewedChanges),
|
|
17569
17786
|
diagnostics: []
|
|
17570
17787
|
};
|
|
17571
17788
|
}
|
|
@@ -17637,11 +17854,11 @@ function comparePairedBlocks(current, revised, factory) {
|
|
|
17637
17854
|
return current.type.create(attributes, inline);
|
|
17638
17855
|
}
|
|
17639
17856
|
function compareInlineContent(current, revised, factory) {
|
|
17640
|
-
|
|
17641
|
-
|
|
17857
|
+
let currentUnits = inlineUnits(current);
|
|
17858
|
+
let revisedUnits = inlineUnits(revised);
|
|
17642
17859
|
if (!currentUnits || !revisedUnits) return null;
|
|
17643
17860
|
const diff = boundedDocumentSequenceDiff(currentUnits, revisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
|
|
17644
|
-
|
|
17861
|
+
let changes = diff ?? [
|
|
17645
17862
|
{
|
|
17646
17863
|
kind: 'delete',
|
|
17647
17864
|
left: currentUnits,
|
|
@@ -17653,14 +17870,48 @@ function compareInlineContent(current, revised, factory) {
|
|
|
17653
17870
|
right: revisedUnits
|
|
17654
17871
|
}
|
|
17655
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
|
+
}
|
|
17656
17907
|
const nodes = [];
|
|
17657
|
-
for (const change of changes){
|
|
17908
|
+
for (const [stepIndex, change] of changes.entries()){
|
|
17658
17909
|
if ('delete' === change.kind) {
|
|
17659
|
-
|
|
17910
|
+
appendComparisonRevisionUnits(nodes, change.left, 'deletion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, movesByStep.get(stepIndex));
|
|
17660
17911
|
continue;
|
|
17661
17912
|
}
|
|
17662
17913
|
if ('insert' === change.kind) {
|
|
17663
|
-
|
|
17914
|
+
appendComparisonRevisionUnits(nodes, change.right, 'insertion', current.type.schema, factory, withoutReviewMarks, work_document_compare_moves_appendRevisionUnits, movesByStep.get(stepIndex));
|
|
17664
17915
|
continue;
|
|
17665
17916
|
}
|
|
17666
17917
|
let formattingIdentity = null;
|
|
@@ -17668,7 +17919,7 @@ function compareInlineContent(current, revised, factory) {
|
|
|
17668
17919
|
for(let index = 0; index < change.left.length; index += 1){
|
|
17669
17920
|
const left = change.left[index];
|
|
17670
17921
|
const right = change.right[index];
|
|
17671
|
-
if (
|
|
17922
|
+
if (work_document_compare_marksEqual(left.marks, right.marks)) {
|
|
17672
17923
|
nodes.push(current.type.schema.text(left.text, [
|
|
17673
17924
|
...left.marks
|
|
17674
17925
|
]));
|
|
@@ -17692,12 +17943,12 @@ function compareInlineContent(current, revised, factory) {
|
|
|
17692
17943
|
}
|
|
17693
17944
|
const deletion = factory.create('deletion');
|
|
17694
17945
|
const insertion = factory.create('insertion');
|
|
17695
|
-
|
|
17946
|
+
work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17696
17947
|
left
|
|
17697
|
-
], 'deletion', deletion, current.type.schema);
|
|
17698
|
-
|
|
17948
|
+
], 'deletion', deletion, current.type.schema, withoutReviewMarks);
|
|
17949
|
+
work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17699
17950
|
right
|
|
17700
|
-
], 'insertion', insertion, current.type.schema);
|
|
17951
|
+
], 'insertion', insertion, current.type.schema, withoutReviewMarks);
|
|
17701
17952
|
formattingIdentity = null;
|
|
17702
17953
|
formattingSignature = '';
|
|
17703
17954
|
}
|
|
@@ -17712,9 +17963,9 @@ function structuralChangeBlock(block, kind, factory, identityChannel) {
|
|
|
17712
17963
|
textId: block.attrs.textId
|
|
17713
17964
|
};
|
|
17714
17965
|
const nodes = [];
|
|
17715
|
-
for (const unit of inlineUnits(block) ?? [])
|
|
17966
|
+
for (const unit of inlineUnits(block) ?? [])work_document_compare_moves_appendRevisionUnits(nodes, [
|
|
17716
17967
|
unit
|
|
17717
|
-
], kind, identity, block.type.schema);
|
|
17968
|
+
], kind, identity, block.type.schema, withoutReviewMarks);
|
|
17718
17969
|
return block.type.create({
|
|
17719
17970
|
...block.attrs,
|
|
17720
17971
|
...paragraphIdentity,
|
|
@@ -17725,19 +17976,6 @@ function structuralChangeBlock(block, kind, factory, identityChannel) {
|
|
|
17725
17976
|
blockChangeDate: identity.date
|
|
17726
17977
|
}, model_Fragment.fromArray(nodes));
|
|
17727
17978
|
}
|
|
17728
|
-
function appendRevisionUnits(target, units, kind, identity, schema) {
|
|
17729
|
-
for (const unit of units)target.push(schema.text(unit.text, [
|
|
17730
|
-
...withoutReviewMarks(unit.marks),
|
|
17731
|
-
schema.marks.documentChange.create({
|
|
17732
|
-
kind,
|
|
17733
|
-
id: identity.id,
|
|
17734
|
-
actorId: identity.actorId ?? '',
|
|
17735
|
-
author: identity.author,
|
|
17736
|
-
date: identity.date,
|
|
17737
|
-
before: ''
|
|
17738
|
-
})
|
|
17739
|
-
]));
|
|
17740
|
-
}
|
|
17741
17979
|
function comparisonMark(node, kind, identity, before) {
|
|
17742
17980
|
return node.type.schema.marks.documentChange.create({
|
|
17743
17981
|
kind,
|
|
@@ -17821,19 +18059,35 @@ function hasStructuralBlockRevisions(document1) {
|
|
|
17821
18059
|
});
|
|
17822
18060
|
return found;
|
|
17823
18061
|
}
|
|
17824
|
-
function inlineUnits(node) {
|
|
18062
|
+
function inlineUnits(node, attachLeadingWhitespace = false) {
|
|
17825
18063
|
if (!isSimpleTextBlock(node)) return null;
|
|
17826
18064
|
const units = [];
|
|
17827
18065
|
node.forEach((child)=>{
|
|
17828
18066
|
const text = child.text ?? '';
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
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
|
|
17832
18086
|
});
|
|
17833
18087
|
});
|
|
17834
18088
|
return units;
|
|
17835
18089
|
}
|
|
17836
|
-
function
|
|
18090
|
+
function work_document_compare_marksEqual(left, right) {
|
|
17837
18091
|
const normalizedLeft = withoutReviewMarks(left);
|
|
17838
18092
|
const normalizedRight = withoutReviewMarks(right);
|
|
17839
18093
|
return normalizedLeft.length === normalizedRight.length && normalizedLeft.every((mark, index)=>mark.eq(normalizedRight[index]));
|
|
@@ -17919,42 +18173,6 @@ function transferCombineIdentities(current, reviewed) {
|
|
|
17919
18173
|
if ('documentSection' === reviewed.type.name) attributes.id = current.attrs.id;
|
|
17920
18174
|
return reviewed.isText ? reviewed : reviewed.type.create(attributes, model_Fragment.fromArray(content), reviewed.marks);
|
|
17921
18175
|
}
|
|
17922
|
-
function comparisonIdentityFactory(current, revised, options) {
|
|
17923
|
-
const seed = stableHash(`${options.mode}\u0000${comparisonSemanticSignature(current)}\u0000${comparisonSemanticSignature(revised)}`);
|
|
17924
|
-
const author = boundedMetadata(options.author, 256) || 'A3S Work user';
|
|
17925
|
-
const date = normalizedDate(options.date);
|
|
17926
|
-
const summary = emptyComparisonSummary();
|
|
17927
|
-
let sequence = 0;
|
|
17928
|
-
return {
|
|
17929
|
-
summary,
|
|
17930
|
-
create (kind) {
|
|
17931
|
-
sequence += 1;
|
|
17932
|
-
if ('insertion' === kind) summary.insertions += 1;
|
|
17933
|
-
else if ('deletion' === kind) summary.deletions += 1;
|
|
17934
|
-
else if ('formatting' === kind) summary.formatting += 1;
|
|
17935
|
-
else summary.paragraphFormatting += 1;
|
|
17936
|
-
return {
|
|
17937
|
-
id: `compare-${seed}-${kind}-${sequence.toString(36)}`,
|
|
17938
|
-
author,
|
|
17939
|
-
date
|
|
17940
|
-
};
|
|
17941
|
-
},
|
|
17942
|
-
paragraphIdentity (channel) {
|
|
17943
|
-
return {
|
|
17944
|
-
paragraphId: wordParagraphId(`${seed}:${channel}:paragraph`),
|
|
17945
|
-
textId: wordParagraphId(`${seed}:${channel}:text`)
|
|
17946
|
-
};
|
|
17947
|
-
}
|
|
17948
|
-
};
|
|
17949
|
-
}
|
|
17950
|
-
function summarizeChanges(changes) {
|
|
17951
|
-
const summary = emptyComparisonSummary();
|
|
17952
|
-
for (const change of changes)if ('insertion' === change.kind) summary.insertions += 1;
|
|
17953
|
-
else if ('deletion' === change.kind) summary.deletions += 1;
|
|
17954
|
-
else if ('formatting' === change.kind) summary.formatting += 1;
|
|
17955
|
-
else summary.paragraphFormatting += 1;
|
|
17956
|
-
return summary;
|
|
17957
|
-
}
|
|
17958
18176
|
function structuralBlockDiagnostic(block, section, blockIndex) {
|
|
17959
18177
|
return 'paragraph' === block.type.name || 'heading' === block.type.name ? {
|
|
17960
18178
|
code: 'empty-structural-change',
|
|
@@ -17977,14 +18195,6 @@ function unsupportedResult(diagnostic) {
|
|
|
17977
18195
|
]
|
|
17978
18196
|
};
|
|
17979
18197
|
}
|
|
17980
|
-
function emptyComparisonSummary() {
|
|
17981
|
-
return {
|
|
17982
|
-
deletions: 0,
|
|
17983
|
-
formatting: 0,
|
|
17984
|
-
insertions: 0,
|
|
17985
|
-
paragraphFormatting: 0
|
|
17986
|
-
};
|
|
17987
|
-
}
|
|
17988
18198
|
function childNodes(node) {
|
|
17989
18199
|
const children = [];
|
|
17990
18200
|
node.forEach((child)=>{
|
|
@@ -18184,7 +18394,7 @@ function DocumentCompareDialog({ initialMode, restoreFocusTarget, onApplied, onC
|
|
|
18184
18394
|
}
|
|
18185
18395
|
}),
|
|
18186
18396
|
/*#__PURE__*/ jsx("small", {
|
|
18187
|
-
children: "
|
|
18397
|
+
children: "该名称会显示在生成的插入、删除、移动与格式修订中。"
|
|
18188
18398
|
})
|
|
18189
18399
|
]
|
|
18190
18400
|
}),
|
|
@@ -18222,7 +18432,7 @@ function ComparisonBoundary({ mode }) {
|
|
|
18222
18432
|
children: 'compare' === mode ? '确定性比较边界' : '安全合并边界'
|
|
18223
18433
|
}),
|
|
18224
18434
|
/*#__PURE__*/ jsx("p", {
|
|
18225
|
-
children: 'compare' === mode ? '
|
|
18435
|
+
children: 'compare' === mode ? '支持同一分节布局中的段落、标题、文字、格式差异,以及同一段内可安全识别的文本移动;复杂对象或节布局变化会明确停止。' : '审阅副本必须包含修订,且拒绝全部修订后与当前文档一致;现有修订须先处理。'
|
|
18226
18436
|
})
|
|
18227
18437
|
]
|
|
18228
18438
|
});
|
|
@@ -18293,7 +18503,7 @@ function useDocumentComparison({ editor, onApplied }) {
|
|
|
18293
18503
|
}
|
|
18294
18504
|
};
|
|
18295
18505
|
const applied = (result)=>{
|
|
18296
|
-
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);
|
|
18297
18507
|
close();
|
|
18298
18508
|
onApplied();
|
|
18299
18509
|
showToast(`已生成 ${count} 项可审阅修订`, 'success');
|
|
@@ -19872,7 +20082,10 @@ function createDocumentPaginationRunCoordinator({ cancelFrame, onAbort, onCoales
|
|
|
19872
20082
|
};
|
|
19873
20083
|
}
|
|
19874
20084
|
const MAX_DOCUMENT_BLOCK_RESIZE_OBSERVATIONS = 4096;
|
|
19875
|
-
function
|
|
20085
|
+
function neverComposing() {
|
|
20086
|
+
return false;
|
|
20087
|
+
}
|
|
20088
|
+
function useDocumentPagination({ compositionRevision = 0, editor, documentRevision, enabled, isComposing = neverComposing, layoutKey, page, selectionVersion, wasmUrl, layoutFonts, loadedLayoutFontIds }) {
|
|
19876
20089
|
const client = useOfficeKernelClient(wasmUrl, layoutFonts);
|
|
19877
20090
|
const editorMounted = useEditorMounted(editor);
|
|
19878
20091
|
const revision = useRef(0);
|
|
@@ -20011,6 +20224,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20011
20224
|
for (const element of observedElements)observer?.observe(element);
|
|
20012
20225
|
};
|
|
20013
20226
|
const run = async (signal)=>{
|
|
20227
|
+
if (isComposing()) return void measurementRange.ensureDirty();
|
|
20014
20228
|
stopObservingBlocks();
|
|
20015
20229
|
const nextRevision = ++revision.current;
|
|
20016
20230
|
editor.commands.clearDocumentPagination(nextRevision);
|
|
@@ -20040,7 +20254,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20040
20254
|
documentRevision,
|
|
20041
20255
|
paragraphs
|
|
20042
20256
|
}, signal);
|
|
20043
|
-
if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed) return;
|
|
20257
|
+
if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed || isComposing()) return;
|
|
20044
20258
|
textLayoutEngine ||= textLayout.engine;
|
|
20045
20259
|
unsupportedTextLayoutCount += textLayout.unsupportedParagraphIds.length + textLayout.layouts.filter((layout)=>layout.missingGlyphCount > 0).length;
|
|
20046
20260
|
for (const layout of textLayout.layouts)if (0 === layout.missingGlyphCount) {
|
|
@@ -20051,7 +20265,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20051
20265
|
editorDom.dataset.paginationTextEngine = textLayoutEngine;
|
|
20052
20266
|
editorDom.dataset.paginationUnsupportedText = String(unsupportedTextLayoutCount);
|
|
20053
20267
|
} catch (error) {
|
|
20054
|
-
if (signal.aborted || error instanceof DOMException && 'AbortError' === error.name) return;
|
|
20268
|
+
if (signal.aborted || isComposing() || error instanceof DOMException && 'AbortError' === error.name) return;
|
|
20055
20269
|
textLayouts.clear();
|
|
20056
20270
|
fallbackGlyphCount = 0;
|
|
20057
20271
|
editorDom.dataset.paginationTextEngine = 'dom';
|
|
@@ -20069,6 +20283,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20069
20283
|
const snapshot = await measureDocumentLayoutBlocksIncrementally(editor, measurementCache.current, measurementStart, textLayouts, 1000000, {
|
|
20070
20284
|
signal
|
|
20071
20285
|
});
|
|
20286
|
+
if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed || isComposing()) return;
|
|
20072
20287
|
measurementCache.current = snapshot;
|
|
20073
20288
|
editorDom.dataset.paginationBlocks = String(snapshot.blocks.length);
|
|
20074
20289
|
editorDom.dataset.paginationFlows = String(new Set(snapshot.blocks.flatMap((block)=>block.block.flowId ? [
|
|
@@ -20098,7 +20313,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20098
20313
|
pageStyles: snapshot.pageStyles,
|
|
20099
20314
|
blocks: layoutPlan.blocks
|
|
20100
20315
|
}, signal);
|
|
20101
|
-
if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed) return;
|
|
20316
|
+
if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed || isComposing()) return;
|
|
20102
20317
|
const layout = previousPagination && layoutPlan.startPageIndex > 0 ? mergeIncrementalDocumentLayout(previousPagination.layout, partialLayout) : partialLayout;
|
|
20103
20318
|
const blockById = new Map(snapshot.blocks.map((block)=>[
|
|
20104
20319
|
block.block.id,
|
|
@@ -20147,7 +20362,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20147
20362
|
measurementRange.commit(measurementPass);
|
|
20148
20363
|
setPagination(nextPagination);
|
|
20149
20364
|
} catch (error) {
|
|
20150
|
-
if (disposed || signal.aborted || error instanceof DOMException && 'AbortError' === error.name) return;
|
|
20365
|
+
if (disposed || signal.aborted || isComposing() || error instanceof DOMException && 'AbortError' === error.name) return;
|
|
20151
20366
|
editorDom.dataset.paginationState = 'error';
|
|
20152
20367
|
editorDom.dataset.paginationError = error instanceof Error ? `${error.name}: ${error.message}` : 'UnknownError';
|
|
20153
20368
|
editor.commands.clearDocumentPagination(nextRevision);
|
|
@@ -20167,7 +20382,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20167
20382
|
onCoalescedRequest: ()=>updateDiagnostic('coalescedRequests', 'paginationCoalescedRequests'),
|
|
20168
20383
|
onError: (error)=>{
|
|
20169
20384
|
measurementRange.restoreActive();
|
|
20170
|
-
if (disposed || editor.isDestroyed) return;
|
|
20385
|
+
if (disposed || editor.isDestroyed || isComposing()) return;
|
|
20171
20386
|
editorDom.dataset.paginationState = 'error';
|
|
20172
20387
|
editorDom.dataset.paginationError = error instanceof Error ? `${error.name}: ${error.message}` : 'UnknownError';
|
|
20173
20388
|
paginationCache.current = null;
|
|
@@ -20184,6 +20399,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20184
20399
|
run
|
|
20185
20400
|
});
|
|
20186
20401
|
const schedule = (invalidateActive = false)=>{
|
|
20402
|
+
if (isComposing()) return;
|
|
20187
20403
|
coordinator.request({
|
|
20188
20404
|
invalidateActive
|
|
20189
20405
|
});
|
|
@@ -20200,6 +20416,15 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20200
20416
|
markDirty(earliestChangedPosition(transaction));
|
|
20201
20417
|
schedule(true);
|
|
20202
20418
|
};
|
|
20419
|
+
const handleCompositionStart = ()=>{
|
|
20420
|
+
measurementRange.ensureDirty();
|
|
20421
|
+
coordinator.request({
|
|
20422
|
+
invalidateActive: true
|
|
20423
|
+
});
|
|
20424
|
+
};
|
|
20425
|
+
const handleCompositionEnd = ()=>{
|
|
20426
|
+
schedule(true);
|
|
20427
|
+
};
|
|
20203
20428
|
const handleLoadedAsset = (event)=>{
|
|
20204
20429
|
updateDiagnostic('assetTriggers', 'paginationAssetTriggers');
|
|
20205
20430
|
const target = event.target;
|
|
@@ -20219,6 +20444,8 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20219
20444
|
};
|
|
20220
20445
|
const fonts = document.fonts;
|
|
20221
20446
|
editor.on('update', handleDocumentUpdate);
|
|
20447
|
+
editorDom.addEventListener('compositionstart', handleCompositionStart);
|
|
20448
|
+
editorDom.addEventListener('compositionend', handleCompositionEnd);
|
|
20222
20449
|
editorDom.addEventListener('load', handleLoadedAsset, true);
|
|
20223
20450
|
fonts?.addEventListener('loadingdone', handleFontLoading);
|
|
20224
20451
|
window.addEventListener('resize', handleWindowResize);
|
|
@@ -20227,6 +20454,8 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20227
20454
|
disposed = true;
|
|
20228
20455
|
stopObservingBlocks();
|
|
20229
20456
|
editor.off('update', handleDocumentUpdate);
|
|
20457
|
+
editorDom.removeEventListener('compositionstart', handleCompositionStart);
|
|
20458
|
+
editorDom.removeEventListener('compositionend', handleCompositionEnd);
|
|
20230
20459
|
editorDom.removeEventListener('load', handleLoadedAsset, true);
|
|
20231
20460
|
fonts?.removeEventListener('loadingdone', handleFontLoading);
|
|
20232
20461
|
window.removeEventListener('resize', handleWindowResize);
|
|
@@ -20234,6 +20463,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20234
20463
|
};
|
|
20235
20464
|
}, [
|
|
20236
20465
|
client,
|
|
20466
|
+
compositionRevision,
|
|
20237
20467
|
documentRevision,
|
|
20238
20468
|
editor,
|
|
20239
20469
|
editorMounted,
|
|
@@ -20243,6 +20473,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
|
|
|
20243
20473
|
layoutFonts,
|
|
20244
20474
|
loadedLayoutFontKey,
|
|
20245
20475
|
loadedLayoutFontIds,
|
|
20476
|
+
isComposing,
|
|
20246
20477
|
pageKey
|
|
20247
20478
|
]);
|
|
20248
20479
|
const resolveFieldContext = useMemo(()=>pagination ? createDocumentFieldPaginationContextResolver(pagination) : null, [
|
|
@@ -20929,12 +21160,24 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
|
|
|
20929
21160
|
},
|
|
20930
21161
|
handleDOMEvents: {
|
|
20931
21162
|
compositionstart: ()=>{
|
|
20932
|
-
composition.start();
|
|
21163
|
+
composition.start(editorRef.current);
|
|
20933
21164
|
return false;
|
|
20934
21165
|
},
|
|
20935
|
-
|
|
21166
|
+
compositionupdate: ()=>{
|
|
21167
|
+
composition.start(editorRef.current);
|
|
21168
|
+
return false;
|
|
21169
|
+
},
|
|
21170
|
+
beforeinput: (_view, event)=>{
|
|
21171
|
+
composition.noteInput(event);
|
|
21172
|
+
return false;
|
|
21173
|
+
},
|
|
21174
|
+
input: (_view, event)=>{
|
|
21175
|
+
composition.noteInput(event);
|
|
21176
|
+
return false;
|
|
21177
|
+
},
|
|
21178
|
+
compositionend: (_view, event)=>{
|
|
20936
21179
|
const current = editorRef.current;
|
|
20937
|
-
if (current) composition.end(current, (settled)=>settleCompositionRef.current(settled));
|
|
21180
|
+
if (current) composition.end(current, event.data ?? null, (settled, snapshot)=>settleCompositionRef.current(settled, snapshot));
|
|
20938
21181
|
return false;
|
|
20939
21182
|
}
|
|
20940
21183
|
}
|
|
@@ -21010,7 +21253,20 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
|
|
|
21010
21253
|
publishDocumentUpdate,
|
|
21011
21254
|
queueLazyDocumentPublication
|
|
21012
21255
|
]);
|
|
21013
|
-
settleCompositionRef.current = (current)=>{
|
|
21256
|
+
settleCompositionRef.current = (current, snapshot)=>{
|
|
21257
|
+
if (null !== snapshot.text && snapshot.range && !current.isDestroyed) {
|
|
21258
|
+
const maximum = current.state.doc.content.size;
|
|
21259
|
+
const from = Math.max(0, Math.min(maximum, snapshot.range.from));
|
|
21260
|
+
const to = Math.max(0, Math.min(maximum, snapshot.range.to));
|
|
21261
|
+
const start = Math.min(from, to);
|
|
21262
|
+
const end = Math.max(from, to);
|
|
21263
|
+
const currentText = current.state.doc.textBetween(start, end, '\n', '\n');
|
|
21264
|
+
if (currentText !== snapshot.text) {
|
|
21265
|
+
if (trackChangesRef.current) current.commands.replaceDocumentTextWithTrackedChange(start, end, snapshot.text);
|
|
21266
|
+
else current.view.dispatch(current.state.tr.insertText(snapshot.text, start, end).setMeta('composition', snapshot.id).setMeta('uiEvent', 'input'));
|
|
21267
|
+
current.view.dom.dataset.documentCompositionNormalized = 'true';
|
|
21268
|
+
}
|
|
21269
|
+
}
|
|
21014
21270
|
const pending = pendingLazyPublicationRef.current;
|
|
21015
21271
|
if (pending) {
|
|
21016
21272
|
pending.cancel();
|
|
@@ -21065,6 +21321,8 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
|
|
|
21065
21321
|
composition.destroy();
|
|
21066
21322
|
},
|
|
21067
21323
|
onTransaction: ({ appendedTransactions, editor: current, transaction })=>{
|
|
21324
|
+
composition.recordTransaction(transaction);
|
|
21325
|
+
for (const appended of appendedTransactions ?? [])composition.recordTransaction(appended);
|
|
21068
21326
|
if (documentTransactionsOnlyHydrateChunks([
|
|
21069
21327
|
transaction,
|
|
21070
21328
|
...appendedTransactions
|
|
@@ -21429,10 +21687,16 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
|
|
|
21429
21687
|
JSON.stringify(layout.pageMargins),
|
|
21430
21688
|
layout.pageSize
|
|
21431
21689
|
]);
|
|
21690
|
+
const documentCompositionIsActive = useCallback(()=>composition.isBlocking(editor), [
|
|
21691
|
+
composition,
|
|
21692
|
+
editor
|
|
21693
|
+
]);
|
|
21432
21694
|
const pagination = useDocumentPagination({
|
|
21695
|
+
compositionRevision,
|
|
21433
21696
|
editor,
|
|
21434
21697
|
documentRevision: editorInput.revision,
|
|
21435
21698
|
enabled: Boolean(editor && 'page' === viewMode),
|
|
21699
|
+
isComposing: documentCompositionIsActive,
|
|
21436
21700
|
layoutKey: [
|
|
21437
21701
|
layout.breakAfter,
|
|
21438
21702
|
layout.columns.count,
|