@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
package/dist/4174.js
CHANGED
|
@@ -10096,12 +10096,23 @@ const DocumentChange = Mark.create({
|
|
|
10096
10096
|
parseHTML: (element)=>{
|
|
10097
10097
|
const declared = element.getAttribute('data-change-kind');
|
|
10098
10098
|
if ('formatting' === declared) return 'formatting';
|
|
10099
|
+
if ('move' === declared) return 'move';
|
|
10099
10100
|
return 'del' === element.tagName.toLowerCase() ? 'deletion' : 'insertion';
|
|
10100
10101
|
},
|
|
10101
10102
|
renderHTML: (attributes)=>({
|
|
10102
10103
|
'data-change-kind': attributes.kind
|
|
10103
10104
|
})
|
|
10104
10105
|
},
|
|
10106
|
+
moveRole: {
|
|
10107
|
+
default: '',
|
|
10108
|
+
parseHTML: (element)=>{
|
|
10109
|
+
const role = element.getAttribute('data-change-move-role');
|
|
10110
|
+
return 'from' === role || 'to' === role ? role : '';
|
|
10111
|
+
},
|
|
10112
|
+
renderHTML: (attributes)=>'from' === attributes.moveRole || 'to' === attributes.moveRole ? {
|
|
10113
|
+
'data-change-move-role': attributes.moveRole
|
|
10114
|
+
} : {}
|
|
10115
|
+
},
|
|
10105
10116
|
id: {
|
|
10106
10117
|
default: '',
|
|
10107
10118
|
parseHTML: (element)=>element.getAttribute('data-change-id') ?? '',
|
|
@@ -10189,7 +10200,7 @@ const DocumentChange = Mark.create({
|
|
|
10189
10200
|
];
|
|
10190
10201
|
},
|
|
10191
10202
|
renderHTML ({ mark, HTMLAttributes }) {
|
|
10192
|
-
const tag = 'deletion' === mark.attrs.kind ? 'del' : 'formatting' === mark.attrs.kind ? 'span' : 'ins';
|
|
10203
|
+
const tag = 'deletion' === mark.attrs.kind ? 'del' : 'move' === mark.attrs.kind && 'from' === mark.attrs.moveRole ? 'del' : 'formatting' === mark.attrs.kind ? 'span' : 'ins';
|
|
10193
10204
|
return [
|
|
10194
10205
|
tag,
|
|
10195
10206
|
mergeAttributes(HTMLAttributes, {
|
|
@@ -10289,6 +10300,7 @@ const DocumentChange = Mark.create({
|
|
|
10289
10300
|
function collectDocumentChanges(document1) {
|
|
10290
10301
|
const changes = new Map();
|
|
10291
10302
|
const blockChangeRanges = new Map();
|
|
10303
|
+
const moveTextParts = new Map();
|
|
10292
10304
|
document1.descendants((node, position)=>{
|
|
10293
10305
|
if (('paragraph' === node.type.name || 'heading' === node.type.name) && ('insertion' === node.attrs.blockChangeKind || 'deletion' === node.attrs.blockChangeKind)) {
|
|
10294
10306
|
const kind = node.attrs.blockChangeKind;
|
|
@@ -10365,6 +10377,7 @@ function collectDocumentChanges(document1) {
|
|
|
10365
10377
|
const mark = work_document_changes_documentChangeMark(node.marks);
|
|
10366
10378
|
if (!mark) return;
|
|
10367
10379
|
const kind = changeKind(mark.attrs.kind);
|
|
10380
|
+
const moveRole = documentMoveRole(mark.attrs.moveRole);
|
|
10368
10381
|
const id = work_document_changes_stringAttribute(mark.attrs.id) || `change-at-${position}`;
|
|
10369
10382
|
const key = `${kind}:${id}`;
|
|
10370
10383
|
if (blockChangeRanges.get(key)?.some((range)=>position >= range.from && position + node.nodeSize <= range.to)) return;
|
|
@@ -10372,12 +10385,38 @@ function collectDocumentChanges(document1) {
|
|
|
10372
10385
|
if (current) {
|
|
10373
10386
|
current.from = Math.min(current.from, position);
|
|
10374
10387
|
current.to = Math.max(current.to, position + node.nodeSize);
|
|
10375
|
-
|
|
10388
|
+
if ('move' === kind && moveRole) {
|
|
10389
|
+
const parts = moveTextParts.get(key) ?? {};
|
|
10390
|
+
const previous = parts[moveRole];
|
|
10391
|
+
parts[moveRole] = previous ? {
|
|
10392
|
+
text: previous.text + node.text,
|
|
10393
|
+
from: Math.min(previous.from, position),
|
|
10394
|
+
to: Math.max(previous.to, position + node.nodeSize)
|
|
10395
|
+
} : {
|
|
10396
|
+
text: node.text,
|
|
10397
|
+
from: position,
|
|
10398
|
+
to: position + node.nodeSize
|
|
10399
|
+
};
|
|
10400
|
+
moveTextParts.set(key, parts);
|
|
10401
|
+
if (current.moveRole !== moveRole) delete current.moveRole;
|
|
10402
|
+
} else current.text += node.text;
|
|
10376
10403
|
return;
|
|
10377
10404
|
}
|
|
10405
|
+
if ('move' === kind && moveRole) {
|
|
10406
|
+
const parts = moveTextParts.get(key) ?? {};
|
|
10407
|
+
parts[moveRole] = {
|
|
10408
|
+
text: node.text,
|
|
10409
|
+
from: position,
|
|
10410
|
+
to: position + node.nodeSize
|
|
10411
|
+
};
|
|
10412
|
+
moveTextParts.set(key, parts);
|
|
10413
|
+
}
|
|
10378
10414
|
changes.set(key, {
|
|
10379
10415
|
id,
|
|
10380
10416
|
kind,
|
|
10417
|
+
...moveRole ? {
|
|
10418
|
+
moveRole
|
|
10419
|
+
} : {},
|
|
10381
10420
|
...work_document_changes_stringAttribute(mark.attrs.actorId) ? {
|
|
10382
10421
|
actorId: work_document_changes_stringAttribute(mark.attrs.actorId)
|
|
10383
10422
|
} : {},
|
|
@@ -10385,9 +10424,27 @@ function collectDocumentChanges(document1) {
|
|
|
10385
10424
|
date: work_document_changes_stringAttribute(mark.attrs.date),
|
|
10386
10425
|
from: position,
|
|
10387
10426
|
to: position + node.nodeSize,
|
|
10388
|
-
text: node.text
|
|
10427
|
+
text: node.text ?? ''
|
|
10389
10428
|
});
|
|
10390
10429
|
});
|
|
10430
|
+
for (const [key, parts] of moveTextParts){
|
|
10431
|
+
const change = changes.get(key);
|
|
10432
|
+
if (!change) continue;
|
|
10433
|
+
const source = parts.from;
|
|
10434
|
+
const destination = parts.to;
|
|
10435
|
+
if (source && destination) {
|
|
10436
|
+
change.text = source.text === destination.text ? source.text : `${source.text} → ${destination.text}`;
|
|
10437
|
+
change.from = destination.from;
|
|
10438
|
+
change.to = destination.to;
|
|
10439
|
+
} else {
|
|
10440
|
+
const only = source ?? destination;
|
|
10441
|
+
if (only) {
|
|
10442
|
+
change.text = only.text;
|
|
10443
|
+
change.from = only.from;
|
|
10444
|
+
change.to = only.to;
|
|
10445
|
+
}
|
|
10446
|
+
}
|
|
10447
|
+
}
|
|
10391
10448
|
return Array.from(changes.values()).sort((left, right)=>left.from - right.from);
|
|
10392
10449
|
}
|
|
10393
10450
|
function resolveAllDocumentChanges(document1, decision) {
|
|
@@ -10409,7 +10466,7 @@ function resolveDocumentChangesTransaction(state, tr, type, decision, ids) {
|
|
|
10409
10466
|
const numberingSegments = numberingChangeSegments(state.doc).filter((segment)=>!ids || ids.has(segment.id));
|
|
10410
10467
|
const blockSegments = blockChangeSegments(state.doc).filter((segment)=>!ids || ids.has(segment.id));
|
|
10411
10468
|
if (!segments.length && !paragraphSegments.length && !numberingSegments.length && !blockSegments.length) return 0;
|
|
10412
|
-
if (paragraphSegments.some((segment)=>!parseDocumentParagraphFormatting(segment.before)) || numberingSegments.some((segment)=>!parseDocumentNumberingChange(segment.before)) || 'reject' === decision && segments.some((segment)=>'formatting' === segment.kind && !parseDocumentCharacterFormatting(segment.before))) return 0;
|
|
10469
|
+
if (paragraphSegments.some((segment)=>!parseDocumentParagraphFormatting(segment.before)) || numberingSegments.some((segment)=>!parseDocumentNumberingChange(segment.before)) || 'reject' === decision && segments.some((segment)=>'formatting' === segment.kind && !parseDocumentCharacterFormatting(segment.before)) || segments.some((segment)=>'move' === segment.kind && 'from' !== segment.moveRole && 'to' !== segment.moveRole) || !validMoveSegments(segments)) return 0;
|
|
10413
10470
|
closeHistory(tr);
|
|
10414
10471
|
tr.setMeta(documentChangePluginKey, {
|
|
10415
10472
|
decision
|
|
@@ -10425,6 +10482,12 @@ function resolveDocumentChangesTransaction(state, tr, type, decision, ids) {
|
|
|
10425
10482
|
if ('reject' === decision) formattingRejections.push(segment);
|
|
10426
10483
|
continue;
|
|
10427
10484
|
}
|
|
10485
|
+
if ('move' === segment.kind) {
|
|
10486
|
+
const source = 'from' === segment.moveRole;
|
|
10487
|
+
const removeContent = 'accept' === decision ? source : !source;
|
|
10488
|
+
(removeContent ? contentDeletions : markRemovals).push(segment);
|
|
10489
|
+
continue;
|
|
10490
|
+
}
|
|
10428
10491
|
const removeMark = 'accept' === decision && 'insertion' === segment.kind || 'reject' === decision && 'deletion' === segment.kind;
|
|
10429
10492
|
(removeMark ? markRemovals : contentDeletions).push(segment);
|
|
10430
10493
|
}
|
|
@@ -10507,16 +10570,35 @@ function documentChangeSegments(document1, type) {
|
|
|
10507
10570
|
if (!node.isText) return;
|
|
10508
10571
|
const mark = node.marks.find((candidate)=>candidate.type === type);
|
|
10509
10572
|
if (!mark) return;
|
|
10573
|
+
const moveRole = documentMoveRole(mark.attrs.moveRole);
|
|
10510
10574
|
segments.push({
|
|
10511
10575
|
id: work_document_changes_stringAttribute(mark.attrs.id) || `change-at-${position}`,
|
|
10512
10576
|
kind: changeKind(mark.attrs.kind),
|
|
10577
|
+
...moveRole ? {
|
|
10578
|
+
moveRole
|
|
10579
|
+
} : {},
|
|
10513
10580
|
from: position,
|
|
10514
10581
|
to: position + node.nodeSize,
|
|
10515
|
-
before: work_document_changes_stringAttribute(mark.attrs.before)
|
|
10582
|
+
before: work_document_changes_stringAttribute(mark.attrs.before),
|
|
10583
|
+
text: node.text ?? ''
|
|
10516
10584
|
});
|
|
10517
10585
|
});
|
|
10518
10586
|
return segments;
|
|
10519
10587
|
}
|
|
10588
|
+
function validMoveSegments(segments) {
|
|
10589
|
+
const moves = new Map();
|
|
10590
|
+
for (const segment of segments){
|
|
10591
|
+
if ('move' !== segment.kind) continue;
|
|
10592
|
+
if ('from' !== segment.moveRole && 'to' !== segment.moveRole) return false;
|
|
10593
|
+
const current = moves.get(segment.id) ?? {
|
|
10594
|
+
from: '',
|
|
10595
|
+
to: ''
|
|
10596
|
+
};
|
|
10597
|
+
current[segment.moveRole] += segment.text;
|
|
10598
|
+
moves.set(segment.id, current);
|
|
10599
|
+
}
|
|
10600
|
+
return Array.from(moves.values()).every(({ from, to })=>Boolean(from && to && from === to));
|
|
10601
|
+
}
|
|
10520
10602
|
function paragraphChangeSegments(document1) {
|
|
10521
10603
|
const segments = [];
|
|
10522
10604
|
document1.descendants((node, position)=>{
|
|
@@ -10613,9 +10695,12 @@ function work_document_changes_documentChangeMark(marks) {
|
|
|
10613
10695
|
return marks.find((mark)=>'documentChange' === mark.type.name);
|
|
10614
10696
|
}
|
|
10615
10697
|
function changeKind(value) {
|
|
10616
|
-
if ('deletion' === value || 'formatting' === value) return value;
|
|
10698
|
+
if ('deletion' === value || 'formatting' === value || 'move' === value) return value;
|
|
10617
10699
|
return 'insertion';
|
|
10618
10700
|
}
|
|
10701
|
+
function documentMoveRole(value) {
|
|
10702
|
+
return 'from' === value || 'to' === value ? value : null;
|
|
10703
|
+
}
|
|
10619
10704
|
function paragraphChangeAttribute(field, htmlName) {
|
|
10620
10705
|
const modelName = `paragraphChange${field[0]?.toUpperCase() ?? ''}${field.slice(1)}`;
|
|
10621
10706
|
return {
|
|
@@ -21439,9 +21524,9 @@ function changeIdentity(decision) {
|
|
|
21439
21524
|
return `${decision.changeKind}:${decision.changeId}`;
|
|
21440
21525
|
}
|
|
21441
21526
|
function office_document_collaboration_change_decisions_changeKind(value, shared) {
|
|
21442
|
-
if ('insertion' === value || 'deletion' === value || 'formatting' === value || 'paragraph-formatting' === value || 'numbering' === value) return value;
|
|
21527
|
+
if ('insertion' === value || 'deletion' === value || 'formatting' === value || 'paragraph-formatting' === value || 'numbering' === value || 'move' === value) return value;
|
|
21443
21528
|
if (shared) invalidSharedSidecars('tracked-change decision kind');
|
|
21444
|
-
invalidInputSidecars('an insertion, deletion, formatting, paragraph-formatting, or
|
|
21529
|
+
invalidInputSidecars('an insertion, deletion, formatting, paragraph-formatting, numbering, or move tracked-change kind');
|
|
21445
21530
|
}
|
|
21446
21531
|
function decisionAction(value, shared) {
|
|
21447
21532
|
if ('accept' === value || 'reject' === value) return value;
|
|
@@ -1,20 +1,66 @@
|
|
|
1
1
|
import type { Editor } from '@tiptap/core';
|
|
2
|
+
import type { Transaction } from '@tiptap/pm/state';
|
|
3
|
+
export interface ControlledCompositionRange {
|
|
4
|
+
from: number;
|
|
5
|
+
to: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ControlledCompositionSnapshot {
|
|
8
|
+
id: number;
|
|
9
|
+
range: ControlledCompositionRange | null;
|
|
10
|
+
text: string | null;
|
|
11
|
+
}
|
|
12
|
+
type CompositionSettledHandler = (editor: Editor, snapshot: ControlledCompositionSnapshot) => void;
|
|
2
13
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
14
|
+
* Tracks the browser-owned part of a controlled TipTap composition.
|
|
15
|
+
*
|
|
16
|
+
* WebKit can expose the provisional Latin spelling to ProseMirror, dispatch
|
|
17
|
+
* `compositionend`, and only then deliver the committed CJK text. Merely
|
|
18
|
+
* delaying `onChange` leaves both strings in the document. This controller
|
|
19
|
+
* keeps the original composition range mapped through every transaction and
|
|
20
|
+
* gives the owning editor one bounded opportunity to replace that range with
|
|
21
|
+
* the committed text after the browser has finished its DOM flush.
|
|
8
22
|
*/
|
|
9
23
|
export declare class ControlledEditorComposition {
|
|
10
24
|
private active;
|
|
25
|
+
private committedInputSeen;
|
|
26
|
+
private committedText;
|
|
27
|
+
private compositionData;
|
|
28
|
+
private deadline;
|
|
11
29
|
private generation;
|
|
30
|
+
private awaitingCommittedTransaction;
|
|
31
|
+
private range;
|
|
32
|
+
private rangeFrozen;
|
|
12
33
|
private settling;
|
|
34
|
+
private settleAt;
|
|
35
|
+
private settleHandler;
|
|
36
|
+
private settleEditor;
|
|
37
|
+
private sessionId;
|
|
13
38
|
private timer;
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
/** Start tracking the selection that the browser is about to compose over. */
|
|
40
|
+
start(editor?: Editor | null): void;
|
|
41
|
+
/**
|
|
42
|
+
* Keep the composition range aligned with ProseMirror's current document.
|
|
43
|
+
* This includes the transaction generated when a browser first exposes the
|
|
44
|
+
* provisional pinyin and the transaction generated by a late committed
|
|
45
|
+
* input event.
|
|
46
|
+
*/
|
|
47
|
+
recordTransaction(transaction: Transaction): void;
|
|
48
|
+
/** Observe beforeinput/input without taking ownership of the browser event. */
|
|
49
|
+
noteInput(event: Event): void;
|
|
50
|
+
/**
|
|
51
|
+
* End a composition. The two-argument form is retained for the Markdown and
|
|
52
|
+
* presentation editors; document editing passes compositionend.data as the
|
|
53
|
+
* middle argument so late WebKit input can be distinguished from typing.
|
|
54
|
+
*/
|
|
55
|
+
end(editor: Editor, dataOrHandler: string | null | undefined | CompositionSettledHandler, maybeHandler?: CompositionSettledHandler): void;
|
|
16
56
|
isBlocking(editor?: Editor | null): boolean;
|
|
17
57
|
destroy(): void;
|
|
58
|
+
private selectionRange;
|
|
59
|
+
private currentRangeText;
|
|
60
|
+
private snapshot;
|
|
61
|
+
private scheduleQuietSettlement;
|
|
18
62
|
private scheduleSettlement;
|
|
63
|
+
private clearSettlement;
|
|
19
64
|
private cancelTimer;
|
|
20
65
|
}
|
|
66
|
+
export {};
|
|
@@ -31,9 +31,11 @@ export interface DocumentPaginationPageDescriptorDerivation {
|
|
|
31
31
|
derivedPageCount: number;
|
|
32
32
|
}
|
|
33
33
|
export interface UseDocumentPaginationOptions {
|
|
34
|
+
compositionRevision?: number;
|
|
34
35
|
editor: Editor | null;
|
|
35
36
|
documentRevision: number;
|
|
36
37
|
enabled: boolean;
|
|
38
|
+
isComposing?: () => boolean;
|
|
37
39
|
layoutKey: string;
|
|
38
40
|
page: OfficeKernelPageMetrics;
|
|
39
41
|
selectionVersion: number;
|
|
@@ -49,7 +51,7 @@ export interface UseDocumentPaginationValue {
|
|
|
49
51
|
paginating: boolean;
|
|
50
52
|
resolveFieldContext: WorkDocumentFieldContextResolver | null;
|
|
51
53
|
}
|
|
52
|
-
export declare function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, page, selectionVersion, wasmUrl, layoutFonts, loadedLayoutFontIds, }: UseDocumentPaginationOptions): UseDocumentPaginationValue;
|
|
54
|
+
export declare function useDocumentPagination({ compositionRevision, editor, documentRevision, enabled, isComposing, layoutKey, page, selectionVersion, wasmUrl, layoutFonts, loadedLayoutFontIds, }: UseDocumentPaginationOptions): UseDocumentPaginationValue;
|
|
53
55
|
export declare function createDocumentFieldPaginationContextResolver(pagination: DocumentPaginationResult): WorkDocumentFieldContextResolver;
|
|
54
56
|
export declare function documentResizeObservationPositions(blocks: readonly MeasuredDocumentLayoutBlock[]): ReadonlyMap<HTMLElement, number>;
|
|
55
57
|
export declare function documentResizeObservationTargets(editorDom: HTMLElement, blocks: readonly MeasuredDocumentLayoutBlock[]): {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Mark } from '@tiptap/core';
|
|
2
2
|
import { type Node as ProseMirrorNode } from '@tiptap/pm/model';
|
|
3
|
-
import type { WorkDocumentChangeKind } from './work-types';
|
|
4
|
-
export type { WorkDocumentChangeKind } from './work-types';
|
|
3
|
+
import type { WorkDocumentChangeKind, WorkDocumentMoveRole } from './work-types';
|
|
4
|
+
export type { WorkDocumentChangeKind, WorkDocumentMoveRole, } from './work-types';
|
|
5
5
|
export interface WorkDocumentChangeIdentity {
|
|
6
6
|
id: string;
|
|
7
7
|
actorId?: string;
|
|
@@ -10,6 +10,8 @@ export interface WorkDocumentChangeIdentity {
|
|
|
10
10
|
}
|
|
11
11
|
export interface WorkDocumentChange extends WorkDocumentChangeIdentity {
|
|
12
12
|
kind: WorkDocumentChangeKind;
|
|
13
|
+
/** Present when a change is one side of a native Word move revision. */
|
|
14
|
+
moveRole?: WorkDocumentMoveRole;
|
|
13
15
|
from: number;
|
|
14
16
|
to: number;
|
|
15
17
|
text: string;
|
|
@@ -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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { WorkDocumentChangeIdentity, WorkDocumentChangeKind } from './work-document-changes';
|
|
1
|
+
import type { WorkDocumentChangeIdentity, WorkDocumentChangeKind, WorkDocumentMoveRole } from './work-document-changes';
|
|
2
2
|
export interface ImportedDocxChangeMarker extends WorkDocumentChangeIdentity {
|
|
3
3
|
kind: WorkDocumentChangeKind;
|
|
4
|
+
moveRole?: WorkDocumentMoveRole;
|
|
4
5
|
start: string;
|
|
5
6
|
end: string;
|
|
6
7
|
}
|
|
@@ -10,3 +11,11 @@ export interface ImportedDocxChangeMarkers {
|
|
|
10
11
|
export declare function markDocxTextChanges(document: Document): ImportedDocxChangeMarkers;
|
|
11
12
|
export declare function applyImportedDocxChangeMarkers(document: Document, markers: ImportedDocxChangeMarkers): void;
|
|
12
13
|
export declare function hasImportedDocxChangeMarkers(markers: ImportedDocxChangeMarkers): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Returns whether one move wrapper uses the bounded, text-only Word shape.
|
|
16
|
+
* Range-marker moves and relationship-bearing children intentionally remain on
|
|
17
|
+
* the compatibility path until their structural semantics are modelled.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isSupportedDocxMoveChange(element: Element): boolean;
|
|
20
|
+
/** Counts complete, paired move revisions in a WordprocessingML story. */
|
|
21
|
+
export declare function supportedDocxMovePairCount(document: Document): number;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type DocxMoveRevisionRole = 'from' | 'to';
|
|
2
|
+
export interface DocxMoveRevisionRegistration {
|
|
3
|
+
kind: 'move-from' | 'move-to';
|
|
4
|
+
wireId: number;
|
|
5
|
+
id: number;
|
|
6
|
+
author: string;
|
|
7
|
+
date: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DocxMoveRevisionPatch {
|
|
10
|
+
wireId: number;
|
|
11
|
+
id: number;
|
|
12
|
+
role: DocxMoveRevisionRole;
|
|
13
|
+
author: string;
|
|
14
|
+
date: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Allocates transient revision IDs while the `docx` package creates its
|
|
18
|
+
* ordinary `w:ins`/`w:del` wrappers. The post-pack patcher changes those
|
|
19
|
+
* wrappers to native `w:moveFrom`/`w:moveTo` elements and restores the shared
|
|
20
|
+
* logical revision ID. Negative wire IDs cannot collide with the positive
|
|
21
|
+
* IDs emitted for ordinary tracked changes.
|
|
22
|
+
*/
|
|
23
|
+
export declare class DocxMoveRevisionPatchCollector {
|
|
24
|
+
readonly patches: DocxMoveRevisionPatch[];
|
|
25
|
+
private readonly registrations;
|
|
26
|
+
private nextWireId;
|
|
27
|
+
register(element: HTMLElement, id: number, revisionDate?: string): DocxMoveRevisionRegistration | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts the transient wrappers emitted by `docx` into native Word move
|
|
31
|
+
* revisions. Every registered side must be emitted at least once; a missing
|
|
32
|
+
* side is treated as a programming or package-generation error rather than
|
|
33
|
+
* silently producing a half-move that Word would interpret differently.
|
|
34
|
+
*/
|
|
35
|
+
export declare function patchDocxMoveRevisions(buffer: ArrayBuffer, patches: readonly DocxMoveRevisionPatch[]): Promise<ArrayBuffer>;
|
|
@@ -106,7 +106,9 @@ export interface WorkDocumentContent {
|
|
|
106
106
|
comments?: WorkDocumentComment[];
|
|
107
107
|
bibliography?: WorkDocumentBibliography;
|
|
108
108
|
}
|
|
109
|
-
export type WorkDocumentChangeKind = 'insertion' | 'deletion' | 'formatting' | 'paragraph-formatting' | 'numbering';
|
|
109
|
+
export type WorkDocumentChangeKind = 'insertion' | 'deletion' | 'formatting' | 'paragraph-formatting' | 'numbering' | 'move';
|
|
110
|
+
/** The side of a native Word move revision represented by a text mark. */
|
|
111
|
+
export type WorkDocumentMoveRole = 'from' | 'to';
|
|
110
112
|
export type WorkDocumentChangeDecisionAction = 'accept' | 'reject';
|
|
111
113
|
/**
|
|
112
114
|
* Immutable audit record created when an editor accepts or rejects one
|
package/dist/office-kernel.wasm
CHANGED
|
Binary file
|
package/dist/styles.css
CHANGED
|
@@ -9256,6 +9256,10 @@ button.work-office-collaboration-participant:hover .work-office-collaboration-lo
|
|
|
9256
9256
|
border-left-color: #0f7b78;
|
|
9257
9257
|
}
|
|
9258
9258
|
|
|
9259
|
+
.work-document-change-item.move {
|
|
9260
|
+
border-left-color: #b06a1f;
|
|
9261
|
+
}
|
|
9262
|
+
|
|
9259
9263
|
.work-document-change-summary {
|
|
9260
9264
|
min-width: 0;
|
|
9261
9265
|
color: var(--a3s-ink);
|
|
@@ -9292,6 +9296,11 @@ button.work-office-collaboration-participant:hover .work-office-collaboration-lo
|
|
|
9292
9296
|
background: #e5f5f3;
|
|
9293
9297
|
}
|
|
9294
9298
|
|
|
9299
|
+
.work-document-change-list .work-document-change-item.move .work-document-change-summary > span {
|
|
9300
|
+
color: #8a4f0f;
|
|
9301
|
+
background: #fff3df;
|
|
9302
|
+
}
|
|
9303
|
+
|
|
9295
9304
|
.work-document-change-summary strong {
|
|
9296
9305
|
text-overflow: ellipsis;
|
|
9297
9306
|
white-space: nowrap;
|
|
@@ -283,15 +283,29 @@ DOCX import/export maps that model to strict or transitional `w:pPrChange`,
|
|
|
283
283
|
while Yjs stores the same node attributes and immutable decision audit without a
|
|
284
284
|
parallel transport model.
|
|
285
285
|
|
|
286
|
+
Text-only move revisions are a paired inline concern rather than two unrelated
|
|
287
|
+
insert/delete cards. A `move` identity keeps explicit `from` and `to` roles;
|
|
288
|
+
review navigation targets the destination side, while accept or reject validates
|
|
289
|
+
and resolves both equal-text sides in one transaction. Native DOCX import/export
|
|
290
|
+
maps the pair to strict or transitional `w:moveFrom`/`w:moveTo`, and the Yjs
|
|
291
|
+
sidecar stores one immutable `changeKind: "move"` decision record for the
|
|
292
|
+
complete intent.
|
|
293
|
+
|
|
286
294
|
Document comparison is a pure planning boundary before the editor transaction.
|
|
287
295
|
The current and imported TipTap/ProseMirror trees remain immutable while a
|
|
288
296
|
deterministic weighted block alignment pairs related paragraphs and headings;
|
|
289
297
|
an LCS token diff then produces insertion and deletion marks, while exact mark
|
|
290
298
|
and paragraph-property snapshots produce character- and paragraph-formatting
|
|
291
|
-
revisions.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
308
|
+
reviewable when they already exist.
|
|
295
309
|
|
|
296
310
|
Combine validates rather than guesses. It rejects every imported revision on
|
|
297
311
|
an immutable reviewed snapshot and compares the resulting semantic signature
|
|
@@ -580,7 +594,9 @@ The public formatting fixture also exercises the non-windowed review boundary
|
|
|
580
594
|
with independent character- and paragraph-formatting cards. Focused A3S Test
|
|
581
595
|
suites reject each kind separately, verify that text and the other revision
|
|
582
596
|
remain intact, and prove full paragraph alignment, indentation, spacing, and
|
|
583
|
-
line-height restoration with clean browser diagnostics.
|
|
597
|
+
line-height restoration with clean browser diagnostics. Focused move fixtures
|
|
598
|
+
verify paired source/destination resolution, destination-only navigation,
|
|
599
|
+
immutable collaboration audit, and native DOCX reopen without marker leakage.
|
|
584
600
|
|
|
585
601
|
Spreadsheet now uses a persistent browser Rust/WASM calculation session. The
|
|
586
602
|
editor initializes it with a sparse workbook replacement, sends bounded cell
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a3s-lab/office",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"office",
|
|
@@ -87,12 +87,14 @@
|
|
|
87
87
|
"playground:preview": "rspress preview -c website/rspress.config.ts --host 127.0.0.1 --port 4175",
|
|
88
88
|
"playground:typecheck": "tsc --noEmit -p playground/tsconfig.json",
|
|
89
89
|
"playground:ime:webkit": "playwright test --config playwright.ime.config.ts",
|
|
90
|
+
"playground:testkit": "playwright test --config playwright.testkit.config.ts",
|
|
90
91
|
"playground:visual": "playwright test --config playwright.config.ts",
|
|
91
92
|
"playground:visual:docs-changelog": "playwright test visual-tests/docs-changelog.functional.spec.ts --config playwright.config.ts",
|
|
92
93
|
"playground:visual:home": "playwright test visual-tests/playground-home.functional.spec.ts --config playwright.config.ts",
|
|
93
94
|
"playground:visual:document-emphasis": "playwright test visual-tests/document-emphasis.functional.spec.ts --config playwright.config.ts",
|
|
94
95
|
"playground:visual:document-index": "playwright test visual-tests/document-index.functional.spec.ts --config playwright.config.ts",
|
|
95
96
|
"playground:visual:document-comparison": "playwright test visual-tests/document-comparison.functional.spec.ts --config playwright.config.ts",
|
|
97
|
+
"playground:visual:document-move-revision": "playwright test visual-tests/document-move-revision.functional.spec.ts --config playwright.config.ts",
|
|
96
98
|
"playground:visual:document-hidden-text": "playwright test visual-tests/document-hidden-text.functional.spec.ts --config playwright.config.ts",
|
|
97
99
|
"playground:visual:document-legacy-text-effects": "playwright test visual-tests/document-legacy-text-effects.functional.spec.ts --config playwright.config.ts",
|
|
98
100
|
"playground:visual:document-proofing-languages": "playwright test visual-tests/document-proofing-languages.functional.spec.ts --config playwright.config.ts",
|
|
@@ -140,8 +142,12 @@
|
|
|
140
142
|
"performance:pdf": "bun .a3s-test/performance/generate-fixtures.ts --pdf-only && bun .a3s-test/performance/benchmark-pdf.ts --url http://127.0.0.1:4175/playground/ --runs 3 --timeout-ms 120000",
|
|
141
143
|
"performance:presentation": "bun .a3s-test/performance/generate-fixtures.ts --presentations-only && bun .a3s-test/performance/benchmark-presentation.ts --url http://127.0.0.1:4175/playground/ --runs 3 --timeout-ms 120000 --modes default,content-visibility",
|
|
142
144
|
"test": "rstest",
|
|
143
|
-
"test:e2e": "bun run test:e2e:fixtures &&
|
|
144
|
-
"test:e2e:check": "bun run test:e2e:fixtures &&
|
|
145
|
+
"test:e2e": "bun run test:e2e:fixtures && A3S_TEST_SUITE=all bash scripts/run-a3s-test-web-gate.sh && bun run test:e2e:testkit",
|
|
146
|
+
"test:e2e:check": "bun run test:e2e:fixtures && bun scripts/check-a3s-test-suites.ts",
|
|
147
|
+
"test:e2e:testkit": "bash scripts/run-a3s-testkit-ui-gate.sh",
|
|
148
|
+
"test:e2e:testkit:check": "A3S_TEST_SUITE=office-testkit-ui.acl bun scripts/check-a3s-test-suites.ts",
|
|
149
|
+
"test:e2e:all": "bun run test:e2e",
|
|
150
|
+
"test:e2e:all:check": "bun run test:e2e:check",
|
|
145
151
|
"test:e2e:writer-text-box": "bun run test:e2e:fixtures && A3S_TEST_SUITE=tests/e2e/word-text-box-phone.acl bash scripts/run-a3s-test-web-gate.sh",
|
|
146
152
|
"test:e2e:writer-text-box:check": "a3s-test check tests/e2e/word-text-box-phone.acl --json",
|
|
147
153
|
"test:e2e:writer-content-control": "bun run test:e2e:fixtures && A3S_TEST_SUITE=tests/e2e/word-content-controls-phone.acl bash scripts/run-a3s-test-web-gate.sh",
|
|
@@ -293,6 +299,8 @@
|
|
|
293
299
|
"test:e2e:writer-formatting-revision:check": "a3s-test check tests/e2e/word-formatting-revision.acl --json",
|
|
294
300
|
"test:e2e:writer-paragraph-formatting-revision": "a3s-test run tests/e2e/word-paragraph-formatting-revision.acl --json",
|
|
295
301
|
"test:e2e:writer-paragraph-formatting-revision:check": "a3s-test check tests/e2e/word-paragraph-formatting-revision.acl --json",
|
|
302
|
+
"test:e2e:writer-move-revision": "a3s-test run tests/e2e/word-move-revision.acl --json",
|
|
303
|
+
"test:e2e:writer-move-revision:check": "a3s-test check tests/e2e/word-move-revision.acl --json",
|
|
296
304
|
"test:e2e:writer-status": "a3s-test run tests/e2e/word-status-bar.acl --json",
|
|
297
305
|
"test:e2e:writer-status:check": "a3s-test check tests/e2e/word-status-bar.acl --json",
|
|
298
306
|
"test:wps-layout": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/test-wps-layout-parity.ps1",
|