@a3s-lab/office 0.48.1 → 0.49.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.
@@ -1,7 +1,7 @@
1
1
  import { __webpack_require__ } from "./0~rslib-runtime.js";
2
2
  import { contentTypeForPart, directChildren, OoxmlPackage, parseXml, firstDescendant, isDocumentParagraphArtBorderStyle, descendants, directChild, xmlContainsAnyElement, attribute, documentRunBorderIsVisible } from "./6968.js";
3
3
  import { normalizeDocumentBookmarkName, docxBookmarkReferenceTarget, DOCUMENT_LEGACY_TEXT_EFFECT_NAMES, normalizeDocumentBookmarkNativeId, supportedDocxDocumentFieldInstruction, supportedDocxBookmarkReferenceInstruction, documentCitationTagsFromInstruction, docxDocumentFieldKind, docxDocumentFieldTarget } from "./4174.js";
4
- import { isSupportedDocxEquationPlacement, inspectDocxPageSize, inspectDocxContentControls, hasInvalidDocxFieldStructure, createDocxTableStyleResolver, inspectDocxRunBorder, inspectDocxEquation, inspectDocxPageMarginSettings, inspectDocxRunShading, supportedDocxIndexField, docxCaptionSequenceKind, inspectDocxPageBorders, docxCaptionBookmark, inspectDocxLegacyTextEffects, inspectDocxPageMargins, inspectDocxPaperSource, createDocxParagraphStyleResolver, inspectDocxRunFonts, supportedDocxTableOfContentsField, docxFieldOccurrences, supportedDocxIndexEntryField, inspectDocxTextBoxes, markDocxParagraphBorders, isSupportedDocxParagraphFormattingChange, docxFieldOccurrenceIsInlineEditable, inspectDocxHiddenText, isSupportedDocxRunFormattingChange, isSupportedDocxNumberingChange, markDocxParagraphShading, docxFieldInstructions } from "./0~3320.js";
4
+ import { isSupportedDocxEquationPlacement, inspectDocxPageSize, inspectDocxContentControls, hasInvalidDocxFieldStructure, createDocxTableStyleResolver, inspectDocxRunBorder, inspectDocxEquation, inspectDocxPageMarginSettings, inspectDocxRunShading, supportedDocxMovePairCount, supportedDocxIndexField, docxCaptionSequenceKind, inspectDocxPageBorders, isSupportedDocxMoveChange, docxCaptionBookmark, inspectDocxLegacyTextEffects, inspectDocxPageMargins, inspectDocxPaperSource, createDocxParagraphStyleResolver, inspectDocxRunFonts, supportedDocxTableOfContentsField, docxFieldOccurrences, supportedDocxIndexEntryField, inspectDocxTextBoxes, markDocxParagraphBorders, isSupportedDocxParagraphFormattingChange, docxFieldOccurrenceIsInlineEditable, inspectDocxHiddenText, isSupportedDocxRunFormattingChange, isSupportedDocxNumberingChange, markDocxParagraphShading, docxFieldInstructions } from "./0~5600.js";
5
5
  import { normalizeDocumentHref } from "./0~work-document-links.js";
6
6
  import { xmlAttributeLocalName, resolveDocxOpenTypeFeatures, inspectDocxProofingLanguages, readDocxBibliography, inspectDocxEmphasisMark, createDocxThemeResolver, DOCX_WORD_2010_NAMESPACE, inspectDocxNoProof, parseDocxParagraphDefaultCollapsed, readDocxImageTransform, DOCX_WORDPROCESSING_NAMESPACES, xmlAttributeNamespace, inspectDocxKerningThresholdHalfPoints } from "./0~3534.js";
7
7
  import { updateSpreadsheetWorksheetCompatibilitySummary, inspectXlsxPivotTables, isSupportedXlsxChartLegend, isSupportedXlsxChartPlotLayout, xlsxChartNodeUsesStackedGrouping, isSupportedXlsxChartSeriesFormatting, readXlsxFormulaFeaturesFromPackage, inspectXlsxHeaderFooterText, emptySpreadsheetWorksheetCompatibilitySummary, isSupportedXlsxWorksheetImageContentType, MAX_XLSX_WORKSHEET_IMAGE_BYTES, xlsxChartSeriesFormattingShapeProperties, diagnoseXlsxProtection, isEditableXlsxPaperSizeCode, isSupportedXlsxCombinationChartNodes } from "./4166.js";
@@ -2223,6 +2223,14 @@ async function analyzeDocxCompatibility(file, messages, sourcePackage) {
2223
2223
  ...descendants(document, 'ins'),
2224
2224
  ...descendants(document, 'del')
2225
2225
  ];
2226
+ const moveFromRevisions = descendants(document, 'moveFrom');
2227
+ const moveToRevisions = descendants(document, 'moveTo');
2228
+ const moveRevisionCount = moveFromRevisions.length + moveToRevisions.length;
2229
+ const supportedMovePairCount = supportedDocxMovePairCount(document);
2230
+ const supportedMoveRevisionCount = [
2231
+ ...moveFromRevisions,
2232
+ ...moveToRevisions
2233
+ ].filter(isSupportedDocxMoveChange).length;
2226
2234
  const runFormattingRevisions = descendants(document, 'rPrChange');
2227
2235
  const paragraphFormattingRevisions = descendants(document, 'pPrChange');
2228
2236
  const numberingRevisions = descendants(document, 'numberingChange');
@@ -2233,14 +2241,17 @@ async function analyzeDocxCompatibility(file, messages, sourcePackage) {
2233
2241
  if (supportedRunFormattingRevisionCount) issues.push(work_office_diagnostics_issue('docx.revisions.formatting', 'Character-formatting revisions', `${supportedRunFormattingRevisionCount} bounded character-formatting revision(s) preserve author, date, current formatting, and prior bold, italic, underline, strike, text case, hidden text, outline, shadow, emboss, imprint, subscript, superscript, font, size, color, highlight, character borders, character shading, character scale, spacing, kerning threshold, emphasis mark, baseline position, grid state, proofing languages, and explicit proofing state. They remain reviewable in Work and round-trip as native w:rPrChange records.`, 'info'));
2234
2242
  if (supportedParagraphFormattingRevisionCount) issues.push(work_office_diagnostics_issue('docx.revisions.paragraph-formatting', 'Paragraph-formatting revisions', `${supportedParagraphFormattingRevisionCount} bounded paragraph-formatting revision(s) preserve author, date, current formatting, and prior alignment, direction, indentation, spacing, pagination, outline, tab-stop, border, shading, and collapsed state. They remain reviewable in Work and round-trip as native w:pPrChange records.`, 'info'));
2235
2243
  if (supportedNumberingRevisionCount) issues.push(work_office_diagnostics_issue('docx.revisions.numbering', 'Numbering revisions', `${supportedNumberingRevisionCount} bounded ordered-list numbering revision(s) preserve author, date, prior start, and common decimal, letter, or Roman formats. Contiguous list-item records remain reviewable as one Work change and round-trip as native w:numberingChange records.`, 'info'));
2236
- if (textRevisions.some((revision)=>!descendants(revision, 't').length && !descendants(revision, 'delText').length) || supportedRunFormattingRevisionCount !== runFormattingRevisions.length || supportedParagraphFormattingRevisionCount !== paragraphFormattingRevisions.length || supportedNumberingRevisionCount !== numberingRevisions.length || [
2237
- 'moveFrom',
2238
- 'moveTo',
2244
+ if (supportedMovePairCount) issues.push(work_office_diagnostics_issue('docx.revisions.move', 'Move revisions', `${supportedMovePairCount} bounded text move revision(s) preserve author, date, source and destination text, remain reviewable as one atomic Work change, and round-trip as native w:moveFrom and w:moveTo records. Rich content, range markers, and relationship-bound moves remain on the compatibility path.`, 'info'));
2245
+ if (textRevisions.some((revision)=>!descendants(revision, 't').length && !descendants(revision, 'delText').length) || supportedRunFormattingRevisionCount !== runFormattingRevisions.length || supportedParagraphFormattingRevisionCount !== paragraphFormattingRevisions.length || supportedNumberingRevisionCount !== numberingRevisions.length || supportedMoveRevisionCount !== moveRevisionCount || supportedMoveRevisionCount !== 2 * supportedMovePairCount || [
2246
+ 'moveFromRangeStart',
2247
+ 'moveFromRangeEnd',
2248
+ 'moveToRangeStart',
2249
+ 'moveToRangeEnd',
2239
2250
  'tblPrChange',
2240
2251
  'trPrChange',
2241
2252
  'tcPrChange',
2242
2253
  'sectPrChange'
2243
- ].some((name)=>descendants(document, name).length)) issues.push(work_office_diagnostics_issue('docx.revisions.structural', 'Structural revisions', 'Moved content plus unsupported character formatting, paragraph formatting, numbering, section, row, cell, and table-property revisions may be normalized; Work currently reviews body-text insertions/deletions and bounded character-, paragraph-, and ordered-list-numbering subsets.'));
2254
+ ].some((name)=>descendants(document, name).length)) issues.push(work_office_diagnostics_issue('docx.revisions.structural', 'Structural revisions', 'Moved content plus unsupported character formatting, paragraph formatting, numbering, section, row, cell, and table-property revisions may be normalized; Work currently reviews body-text insertions/deletions and bounded text moves, character-, paragraph-, and ordered-list-numbering subsets.'));
2244
2255
  if (captionDiagnostics.hasUnsupportedFields) issues.push(work_office_diagnostics_issue('docx.fields', 'Fields', 'Fields beyond supported body fields, citations, bibliographies, caption SEQ fields, and bookmark or caption REF fields are converted to their current displayed value.'));
2245
2256
  const sectionProperties = descendants(document, 'sectPr');
2246
2257
  const columnProperties = descendants(document, 'cols');
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
- current.text += node.text;
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 numbering tracked-change kind');
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
- * Keeps controlled TipTap editors from publishing or reconciling content while
4
- * the browser and ProseMirror still own an IME composition. The zero-delay
5
- * settlement lets ProseMirror flush WebKit's pending DOM records first; the
6
- * bounded retries cover runtimes that keep `view.composing` true briefly after
7
- * `compositionend` without adding work to ordinary keyboard input.
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
- start(): void;
15
- end(editor: Editor, onSettled: (editor: Editor) => void): void;
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;
@@ -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
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,6 +283,14 @@ 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;
@@ -291,7 +299,9 @@ and paragraph-property snapshots produce character- and paragraph-formatting
291
299
  revisions. Stable semantic signatures seed generated identities, so the same
292
300
  inputs and options produce the same ordering and IDs. Only a fully admitted
293
301
  plan replaces the mounted document, and that replacement is one transaction,
294
- one controlled publication, and one Undo record.
302
+ one controlled publication, and one Undo record. The planner does not infer
303
+ move pairs from arbitrary delete/insert similarities; native paired moves remain
304
+ reviewable when they already exist.
295
305
 
296
306
  Combine validates rather than guesses. It rejects every imported revision on
297
307
  an immutable reviewed snapshot and compares the resulting semantic signature
@@ -580,7 +590,9 @@ The public formatting fixture also exercises the non-windowed review boundary
580
590
  with independent character- and paragraph-formatting cards. Focused A3S Test
581
591
  suites reject each kind separately, verify that text and the other revision
582
592
  remain intact, and prove full paragraph alignment, indentation, spacing, and
583
- line-height restoration with clean browser diagnostics.
593
+ line-height restoration with clean browser diagnostics. Focused move fixtures
594
+ verify paired source/destination resolution, destination-only navigation,
595
+ immutable collaboration audit, and native DOCX reopen without marker leakage.
584
596
 
585
597
  Spreadsheet now uses a persistent browser Rust/WASM calculation session. The
586
598
  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.48.1",
3
+ "version": "0.49.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 && a3s-test run tests/e2e/word-page-color.acl --json && a3s-test run tests/e2e/word-page-setup-phone.acl --json && a3s-test run tests/e2e/word-paragraph-layout-phone.acl --json && a3s-test run tests/e2e/word-list-formatting-phone.acl --json && a3s-test run tests/e2e/word-font-picker-phone.acl --json && a3s-test run tests/e2e/word-comments-drawer.acl --json && a3s-test run tests/e2e/word-comment-windowing.acl --json && a3s-test run tests/e2e/word-citations-phone.acl --json && a3s-test run tests/e2e/word-navigation-search.acl --json && a3s-test run tests/e2e/word-navigation-windowing.acl --json && a3s-test run tests/e2e/word-find-replace-phone.acl --json && a3s-test run tests/e2e/word-page-navigation.acl --json && a3s-test run tests/e2e/word-page-windowing.acl --json && a3s-test run tests/e2e/word-ai-question.acl --json && a3s-test run tests/e2e/word-picture-insert.acl --json && a3s-test run tests/e2e/word-picture-alt-text-phone.acl --json && a3s-test run tests/e2e/word-text-box-phone.acl --json && a3s-test run tests/e2e/word-content-controls-phone.acl --json && a3s-test run tests/e2e/word-track-changes-phone.acl --json && a3s-test run tests/e2e/word-formatting-revision.acl --json && a3s-test run tests/e2e/word-paragraph-formatting-revision.acl --json && a3s-test run tests/e2e/word-numbering-revision.acl --json && a3s-test run tests/e2e/word-document-comparison.acl --json && a3s-test run tests/e2e/word-review-conflict-phone.acl --json && a3s-test run tests/e2e/word-revision-windowing.acl --json && a3s-test run tests/e2e/word-table-phone.acl --json && a3s-test run tests/e2e/word-table-borders.acl --json && a3s-test run tests/e2e/word-theme-table.acl --json && a3s-test run tests/e2e/word-styled-table.acl --json && a3s-test run tests/e2e/word-multi-page-table.acl --json && a3s-test run tests/e2e/word-cross-reference-phone.acl --json && a3s-test run tests/e2e/word-bookmark-links-phone.acl --json && a3s-test run tests/e2e/word-notes-phone.acl --json && a3s-test run tests/e2e/word-fields-phone.acl --json && a3s-test run tests/e2e/spreadsheet-phone-rename.acl --json && a3s-test run tests/e2e/spreadsheet-phone-find.acl --json && a3s-test run tests/e2e/spreadsheet-phone-context-menu.acl --json && a3s-test run tests/e2e/spreadsheet-phone-task-pane.acl --json && a3s-test run tests/e2e/spreadsheet-maximum-sparse.acl --json && a3s-test run tests/e2e/spreadsheet-cell-style.acl --json && a3s-test run tests/e2e/spreadsheet-paste-special.acl --json && a3s-test run tests/e2e/spreadsheet-hyperlink.acl --json && a3s-test run tests/e2e/presentation-cut-paste-focus.acl --json && a3s-test run tests/e2e/presentation-presenter-view.acl --json && a3s-test run tests/e2e/presentation-phone-chart-pane.acl --json && a3s-test run tests/e2e/presentation-phone-comments.acl --json && a3s-test run tests/e2e/pdf-thumbnail-keyboard.acl --json && a3s-test run tests/e2e/pdf-page-drawer-phone.acl --json && a3s-test run tests/e2e/markdown-phone-modes.acl --json && a3s-test run tests/e2e/office-sidebar-breakpoint.acl --json && a3s-test run tests/e2e/office-docs-navigation.acl --json && a3s-test run tests/e2e/collaboration-playground-entry.acl --json && a3s-test run tests/e2e/collaboration-document-comments.acl --command-timeout-ms 120000 --json && a3s-test run tests/e2e/collaboration-document-suggestions.acl --command-timeout-ms 120000 --json && a3s-test run tests/e2e/collaboration-participants.acl --json && a3s-test run tests/e2e/collaboration-pdf-annotations.acl --json && a3s-test run tests/e2e/collaboration-spreadsheet-cells.acl --json && a3s-test run tests/e2e/collaboration-presentation-elements.acl --json",
144
- "test:e2e:check": "bun run test:e2e:fixtures && a3s-test check tests/e2e/word-page-color.acl --json && a3s-test check tests/e2e/word-page-setup-phone.acl --json && a3s-test check tests/e2e/word-paragraph-layout-phone.acl --json && a3s-test check tests/e2e/word-list-formatting-phone.acl --json && a3s-test check tests/e2e/word-font-picker-phone.acl --json && a3s-test check tests/e2e/word-comments-drawer.acl --json && a3s-test check tests/e2e/word-comment-windowing.acl --json && a3s-test check tests/e2e/word-citations-phone.acl --json && a3s-test check tests/e2e/word-navigation-search.acl --json && a3s-test check tests/e2e/word-navigation-windowing.acl --json && a3s-test check tests/e2e/word-find-replace-phone.acl --json && a3s-test check tests/e2e/word-page-navigation.acl --json && a3s-test check tests/e2e/word-page-windowing.acl --json && a3s-test check tests/e2e/word-ai-question.acl --json && a3s-test check tests/e2e/word-picture-insert.acl --json && a3s-test check tests/e2e/word-picture-alt-text-phone.acl --json && a3s-test check tests/e2e/word-text-box-phone.acl --json && a3s-test check tests/e2e/word-content-controls-phone.acl --json && a3s-test check tests/e2e/word-track-changes-phone.acl --json && a3s-test check tests/e2e/word-formatting-revision.acl --json && a3s-test check tests/e2e/word-paragraph-formatting-revision.acl --json && a3s-test check tests/e2e/word-numbering-revision.acl --json && a3s-test check tests/e2e/word-document-comparison.acl --json && a3s-test check tests/e2e/word-review-conflict-phone.acl --json && a3s-test check tests/e2e/word-revision-windowing.acl --json && a3s-test check tests/e2e/word-table-phone.acl --json && a3s-test check tests/e2e/word-table-borders.acl --json && a3s-test check tests/e2e/word-theme-table.acl --json && a3s-test check tests/e2e/word-styled-table.acl --json && a3s-test check tests/e2e/word-multi-page-table.acl --json && a3s-test check tests/e2e/word-cross-reference-phone.acl --json && a3s-test check tests/e2e/word-bookmark-links-phone.acl --json && a3s-test check tests/e2e/word-notes-phone.acl --json && a3s-test check tests/e2e/word-fields-phone.acl --json && a3s-test check tests/e2e/spreadsheet-phone-rename.acl --json && a3s-test check tests/e2e/spreadsheet-phone-find.acl --json && a3s-test check tests/e2e/spreadsheet-phone-context-menu.acl --json && a3s-test check tests/e2e/spreadsheet-phone-task-pane.acl --json && a3s-test check tests/e2e/spreadsheet-maximum-sparse.acl --json && a3s-test check tests/e2e/spreadsheet-cell-style.acl --json && a3s-test check tests/e2e/spreadsheet-paste-special.acl --json && a3s-test check tests/e2e/spreadsheet-hyperlink.acl --json && a3s-test check tests/e2e/presentation-cut-paste-focus.acl --json && a3s-test check tests/e2e/presentation-presenter-view.acl --json && a3s-test check tests/e2e/presentation-phone-chart-pane.acl --json && a3s-test check tests/e2e/presentation-phone-comments.acl --json && a3s-test check tests/e2e/pdf-thumbnail-keyboard.acl --json && a3s-test check tests/e2e/pdf-page-drawer-phone.acl --json && a3s-test check tests/e2e/markdown-phone-modes.acl --json && a3s-test check tests/e2e/office-sidebar-breakpoint.acl --json && a3s-test check tests/e2e/office-docs-navigation.acl --json && a3s-test check tests/e2e/collaboration-playground-entry.acl --json && a3s-test check tests/e2e/collaboration-document-comments.acl --json && a3s-test check tests/e2e/collaboration-document-suggestions.acl --json && a3s-test check tests/e2e/collaboration-participants.acl --json && a3s-test check tests/e2e/collaboration-pdf-annotations.acl --json && a3s-test check tests/e2e/collaboration-spreadsheet-cells.acl --json && a3s-test check tests/e2e/collaboration-presentation-elements.acl --json",
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",