@a3s-lab/office 0.8.1 → 0.9.1

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.
@@ -247,9 +247,9 @@ comments, and delete-vs-edit cases converge and survive PPTX round trips.
247
247
 
248
248
  ### Phase 4: Spreadsheet
249
249
 
250
- Status: browser collaboration foundation and native conflict-local cell
251
- mutations implemented; structural operations, native recalculation parity, and
252
- XLSX concurrency coverage are pending.
250
+ Status: browser collaboration foundation plus native conflict-local single-cell
251
+ and atomic batch-cell mutations implemented; structural operations, native
252
+ recalculation parity, and XLSX concurrency coverage are pending.
253
253
 
254
254
  - Stable sheet/named-range order arrays, ID-keyed records, and append-only
255
255
  creation claims avoid a serialized workbook or dense worksheet root.
@@ -282,11 +282,16 @@ XLSX concurrency coverage are pending.
282
282
  and an empty sheet's first write stays sparse. Native restart, real CLI/MCP
283
283
  subprocesses, and browser Yjs duplicate/reordered delivery cover the same
284
284
  update contract.
285
+ - `spreadsheet-batch-cells` translates one paste, fill, or other bounded user
286
+ gesture into 1 to 4,096 distinct coordinates in one sheet and one Yjs
287
+ transaction. Set/create changes retain recursive optimistic guards; delete
288
+ changes require an exact complete observed cell. Every change is preflighted
289
+ against one shared sheet snapshot, so one conflict rejects the entire batch
290
+ without partial fields, presence markers, dense dimensions, or durable log
291
+ growth. Single-cell mutations use this same kernel.
285
292
 
286
293
  Remaining:
287
294
 
288
- - Translate one user gesture into one transaction; batch paste/fill/sort/filter
289
- without emitting a document-sized replacement.
290
295
  - Add stable structural operations and reference transforms for row/column
291
296
  insertion/deletion, merged ranges, tables, sort, and named references.
292
297
  - Treat calculated values as derived once browser and native recalculation are
@@ -461,9 +466,10 @@ are pending.
461
466
  without replacing the full document.
462
467
  - Spreadsheet cell mutations create or recursively patch one zero-based
463
468
  coordinate after matching the caller's observed cell, or delete it only after
464
- an exact complete-cell match. They preserve the browser's field-addressed
465
- representation, dense/sparse projection mode, and atomic fail-closed
466
- semantics without replacing a worksheet or workbook.
469
+ an exact complete-cell match. `spreadsheet-batch-cells` applies 1 to 4,096
470
+ distinct changes from one sheet snapshot in one transaction. They preserve
471
+ the browser's field-addressed representation, dense/sparse projection mode,
472
+ and atomic fail-closed semantics without replacing a worksheet or workbook.
467
473
  - Presentation scene-element mutations create, update, move, or tombstone one
468
474
  stable object inside a slide, master, or layout. Canonical creation claims
469
475
  prevent conflicting same-ID reuse, optimistic top-level field guards merge
package/README.md CHANGED
@@ -144,7 +144,7 @@ The images below are committed visual-regression baselines from the real
144
144
  - **Automation outside the browser** — The native Rust CLI, standard MCP
145
145
  server, and Office Skill share bounded mutation contracts. Coding agents can
146
146
  also keep a durable Yrs replica, exchange standard Yjs v1 updates and state
147
- vectors, perform authorized typed Markdown, Document, Spreadsheet cell,
147
+ vectors, perform authorized typed Markdown, Document, Spreadsheet cell/batch,
148
148
  Presentation scene-element and z-order, and PDF annotation, form-value, and
149
149
  review changes, including attributable Document selection comments, replies,
150
150
  text suggestions, and atomic final decisions, retain browser/native actor
@@ -415,7 +415,10 @@ interaction model.
415
415
  edit/preview/PDF typography and page-chrome placement. Editing, read-only
416
416
  preview, and browser PDF export share one live pagination result. Tail edits
417
417
  reuse the stable physical-page prefix, including resolved first/odd/even
418
- headers, footers, page numbers, and navigation descriptors.
418
+ headers, footers, page numbers, and navigation descriptors. Browser DOM
419
+ measurement runs in bounded 32 ms slices and yields only between top-level
420
+ blocks; document edits abort stale passes before a snapshot is committed, so
421
+ long repagination work does not monopolize the browser event loop.
419
422
  Real DOCX fixtures provide deterministic A3S Test evidence for inherited
420
423
  table styling across edit and preview, a centered 62.5% table with table and
421
424
  cell margin overrides, all four Table Properties tabs at 390 px, exact
@@ -1109,6 +1112,13 @@ cargo run -p a3s-office-cli -- collab mutate .a3s/plan.replica \
1109
1112
  --operation-id edit-45 \
1110
1113
  --mutation '{"type":"spreadsheet-set-cell","sheetId":"sheet-data","row":1,"column":0,"expectedCell":{"v":10,"m":"10"},"nextCell":{"v":12,"m":"12","f":"=6*2"}}' --json
1111
1114
 
1115
+ # Apply one bounded Spreadsheet gesture atomically. Coordinates must be unique;
1116
+ # nextCell:null is an exact guarded delete.
1117
+ cargo run -p a3s-office-cli -- collab mutate .a3s/plan.replica \
1118
+ --artifact-id plan --kind spreadsheet --actor-id agent-7 --mode edit \
1119
+ --operation-id paste-46 \
1120
+ --mutation '{"type":"spreadsheet-batch-cells","sheetId":"sheet-data","changes":[{"row":1,"column":0,"expectedCell":{"v":12,"m":"12","f":"=6*2"},"nextCell":{"v":14,"m":"14","f":"=7*2"}},{"row":1,"column":1,"expectedCell":null,"nextCell":{"v":20,"m":"20"}},{"row":2,"column":0,"expectedCell":{"v":"obsolete","m":"obsolete"},"nextCell":null}]}' --json
1121
+
1112
1122
  # Update one Presentation scene element after matching the complete observed
1113
1123
  # record. Create/delete use presentation-create/delete-element.
1114
1124
  cargo run -p a3s-office-cli -- collab mutate .a3s/deck.replica \
@@ -1197,7 +1207,11 @@ write only changed leaves, so unrelated concurrent value, formula, style,
1197
1207
  hyperlink, note, and metadata edits merge. Use `expectedCell: null` only when
1198
1208
  creating an observed blank coordinate. Spreadsheet deletion requires the exact
1199
1209
  complete current cell, dense projections keep their dimensions, and sparse or
1200
- empty sheets remain sparse. Presentation creation writes one complete element
1210
+ empty sheets remain sparse. `spreadsheet-batch-cells` applies 1 to 4,096
1211
+ distinct coordinates in one sheet after checking every guard against the same
1212
+ snapshot, then writes fields, presence markers, and dense dimensions in one
1213
+ Yjs transaction. One conflict rejects the complete gesture without a durable
1214
+ event. Presentation creation writes one complete element
1201
1215
  plus a canonical immutable claim inside a slide, master, or layout and can
1202
1216
  place it after a stable active element. Presentation update compares complete
1203
1217
  expected/current/next elements and writes only changed top-level fields, so
@@ -1309,6 +1323,8 @@ without hard-coded return URLs.
1309
1323
 
1310
1324
  - [Live Playground](https://a3s-lab.github.io/Office/)
1311
1325
  - [Documentation center](https://a3s-lab.github.io/Office/docs/)
1326
+ - [A3S Office 0.9.1 documentation](https://a3s-lab.github.io/Office/docs/0.9.1/)
1327
+ - [A3S Office 0.9.0 documentation](https://a3s-lab.github.io/Office/docs/0.9.0/)
1312
1328
  - [A3S Office 0.8.1 documentation](https://a3s-lab.github.io/Office/docs/0.8.1/)
1313
1329
  - [A3S Office 0.8.0 documentation](https://a3s-lab.github.io/Office/docs/0.8.0/)
1314
1330
  - [A3S Office 0.7.3 documentation](https://a3s-lab.github.io/Office/docs/0.7.3/)
@@ -19,7 +19,7 @@ import { TableMap, isInTable, selectedRect } from "@tiptap/pm/tables";
19
19
  import { useOfficeTaskPaneEscape, normalizeOfficeFontFamily, OfficeCheckbox, useOfficeDialog, useOfficeTaskPaneModal, CommittedOfficeNumberField, officeFontFamilyLabel, officeFontFamilies, CollectionState, createOfficeKernelClient, OfficeNumberField } from "./0~7048.js";
20
20
  import { office_text_field_OfficeTextArea, Popover, officeOverlayPortalRoot, button_Button, useOfficePublishPresenceLocation, OfficeTiptapPresenceLayer, icon_button_IconButton, office_text_field_OfficeTextField, useDialogFocusScope, useOfficeEditorWheelZoom, office_text_field_OfficeFileInput } from "./0~6090.js";
21
21
  import { WorkOfficePreviewBar, isWorkspaceContextMenuKeyboardEvent, Tabs, WorkOfficeZoomControls, WorkOfficeStatusBar, Dialog, WorkOfficeRibbon, workspaceContextMenuPosition, OfficeSelect, WorkspaceContextMenu, WorkOfficeRibbonGroup, WorkOfficeRibbonButton } from "./0~4595.js";
22
- import { documentTableSizing, documentTableRowOptions, documentCommentViews, documentParagraphSpacing, documentPageBordersVisible, DOCUMENT_PAGE_BORDER_EDGES, normalizeDocumentColumns, clampDocumentMargin, normalizeDocumentTabStops, DOCUMENT_BOOKMARK_DUPLICATE_MESSAGE, createWorkDocumentModel, nextDocumentTabAlignment, documentBookmarkNameExists, normalizedTabPosition, DocumentImage, collectDocumentChanges, documentCitationCount, isDocumentParagraphArtBorderStyle, resolveDocumentPageMargins, updateDocumentPageMarginMode, resolveDocumentPageBorders, canSetDocumentTableRowRepeatHeader, MAX_DOCUMENT_IMAGE_RELATIVE_HEIGHT, retainAnchoredDocumentComments, measureDocumentLayoutBlocks, DocumentTableRowIdentity, work_document_page_margins_twipsToMillimeters, DEFAULT_DOCUMENT_TABLE_CELL_FORMAT, resolveWorkDocumentEditorInput, collectDocumentTextLayoutParagraphs, millimetersToPixels as work_document_layout_millimetersToPixels, documentBulletListStyle, readOfficeDocumentCollaboration as readWorkOfficeDocumentCollaboration, documentTabLeaderLabel, documentOrderedListState, normalizeDocumentPageChrome, documentPageMarginsForLayout, documentTableCellFormat, updateDocumentCustomPageMillimeters, documentCommentDraftRange, normalizeDocumentPageGeometry, documentParagraphTabStops, normalizeDocumentImageLayer, documentImageProperties, documentParagraphIndent, documentTableHorizontalAlignment, isValidDocumentCitationTag, canInsertDocumentComment, sanitizeDocumentPageChromeHtml, pageTwipsToMillimeters, normalizeDocumentPageBorders, DocumentParagraphFormatting, activeDocumentSection, documentTextLayoutBatches, normalizeDocumentImageWrapSide, normalizeDocumentHtml, documentParagraphDirection, documentParagraphPagination, updateDocumentPageOrientation, documentCitationStyleDetails, documentInitialSectionLayout, documentPageChromeLegacyFields, setCustomDocumentColumns, collectDocumentCommentAnchors, documentCitationStyle, updateDocumentPageMarginMillimeters, canChangeDocumentIndent, validateDocumentBookmarkName, DEFAULT_DOCUMENT_TABLE_CELL_MARGINS, DocumentSuperscript, documentSectionById, DOCUMENT_TABLE_STYLE_OPTIONS, updateDocumentColumnWidth, updateDocumentPaperSizePreset, DocumentEquation, documentPageMetrics, normalizeDocumentParagraphIndent, updateDocumentGutterPosition, mountWorkLiveDocumentCapture, activeDocumentBookmark, createDocumentBibliography, DocumentSubscript, DocumentParagraphIdentity, syncDocumentContentFromHtml, updateDocumentPageChromeVariant, editorDocumentCaptionTargets, resolveDocumentPageChrome, updateDocumentMirrorMargins, documentImageLayoutOptions, MAX_DOCUMENT_NUMBERING_START, resolveDocumentPageSize, activeDocumentTableStyle, editorDocumentBookmarkReferenceTargets, createOfficeDocumentCollaborationBinding as createWorkOfficeDocumentCollaborationBinding, createWorkDocumentExtensions } from "./8928.js";
22
+ import { documentTableSizing, documentTableRowOptions, documentCommentViews, documentParagraphSpacing, documentPageBordersVisible, DOCUMENT_PAGE_BORDER_EDGES, normalizeDocumentColumns, clampDocumentMargin, normalizeDocumentTabStops, DOCUMENT_BOOKMARK_DUPLICATE_MESSAGE, createWorkDocumentModel, nextDocumentTabAlignment, documentBookmarkNameExists, normalizedTabPosition, DocumentImage, collectDocumentChanges, documentCitationCount, isDocumentParagraphArtBorderStyle, resolveDocumentPageMargins, updateDocumentPageMarginMode, resolveDocumentPageBorders, canSetDocumentTableRowRepeatHeader, MAX_DOCUMENT_IMAGE_RELATIVE_HEIGHT, retainAnchoredDocumentComments, DocumentTableRowIdentity, work_document_page_margins_twipsToMillimeters, DEFAULT_DOCUMENT_TABLE_CELL_FORMAT, resolveWorkDocumentEditorInput, collectDocumentTextLayoutParagraphs, millimetersToPixels as work_document_layout_millimetersToPixels, documentBulletListStyle, readOfficeDocumentCollaboration as readWorkOfficeDocumentCollaboration, documentTabLeaderLabel, documentOrderedListState, normalizeDocumentPageChrome, documentPageMarginsForLayout, documentTableCellFormat, updateDocumentCustomPageMillimeters, documentCommentDraftRange, normalizeDocumentPageGeometry, documentParagraphTabStops, normalizeDocumentImageLayer, documentImageProperties, documentParagraphIndent, documentTableHorizontalAlignment, isValidDocumentCitationTag, canInsertDocumentComment, sanitizeDocumentPageChromeHtml, pageTwipsToMillimeters, normalizeDocumentPageBorders, DocumentParagraphFormatting, activeDocumentSection, documentTextLayoutBatches, normalizeDocumentImageWrapSide, normalizeDocumentHtml, documentParagraphDirection, documentParagraphPagination, updateDocumentPageOrientation, documentCitationStyleDetails, documentInitialSectionLayout, documentPageChromeLegacyFields, setCustomDocumentColumns, collectDocumentCommentAnchors, documentCitationStyle, updateDocumentPageMarginMillimeters, canChangeDocumentIndent, validateDocumentBookmarkName, DEFAULT_DOCUMENT_TABLE_CELL_MARGINS, DocumentSuperscript, documentSectionById, DOCUMENT_TABLE_STYLE_OPTIONS, updateDocumentColumnWidth, updateDocumentPaperSizePreset, DocumentEquation, documentPageMetrics, normalizeDocumentParagraphIndent, updateDocumentGutterPosition, mountWorkLiveDocumentCapture, activeDocumentBookmark, createDocumentBibliography, DocumentSubscript, DocumentParagraphIdentity, syncDocumentContentFromHtml, updateDocumentPageChromeVariant, editorDocumentCaptionTargets, resolveDocumentPageChrome, updateDocumentMirrorMargins, documentImageLayoutOptions, MAX_DOCUMENT_NUMBERING_START, resolveDocumentPageSize, activeDocumentTableStyle, editorDocumentBookmarkReferenceTargets, createOfficeDocumentCollaborationBinding as createWorkOfficeDocumentCollaborationBinding, measureDocumentLayoutBlocksIncrementally, createWorkDocumentExtensions } from "./8928.js";
23
23
  import { createOfficeId as createWorkId } from "./5184.js";
24
24
  import { normalizeDocumentHref, DOCUMENT_LINK_VALIDATION_MESSAGE } from "./0~work-document-links.js";
25
25
  import { OfficeColorPicker, isOfficeShortcutBlocked } from "./0~3635.js";
@@ -13382,6 +13382,44 @@ function incrementalBoundaryBreak(previous, next) {
13382
13382
  spacerHeight: remainingBodyHeight + previous.page.marginBottom + previous.page.pageGap + next.page.marginTop
13383
13383
  };
13384
13384
  }
13385
+ function createDocumentPaginationMeasurementRange() {
13386
+ let activePass = null;
13387
+ let dirtyFrom = 0;
13388
+ const restore = (pass)=>{
13389
+ if (activePass !== pass) return;
13390
+ dirtyFrom = Math.min(dirtyFrom, pass.from);
13391
+ activePass = null;
13392
+ };
13393
+ return {
13394
+ begin: ()=>{
13395
+ const pass = {
13396
+ from: Number.isFinite(dirtyFrom) ? dirtyFrom : 0
13397
+ };
13398
+ activePass = pass;
13399
+ dirtyFrom = 1 / 0;
13400
+ return pass;
13401
+ },
13402
+ commit: (pass)=>{
13403
+ if (activePass === pass) activePass = null;
13404
+ },
13405
+ ensureDirty: ()=>{
13406
+ if (Number.isFinite(dirtyFrom)) return;
13407
+ dirtyFrom = activePass?.from ?? 0;
13408
+ },
13409
+ invalidate: (position)=>{
13410
+ const normalizedPosition = Number.isFinite(position) ? Math.max(0, position) : 0;
13411
+ dirtyFrom = Math.min(dirtyFrom, normalizedPosition);
13412
+ },
13413
+ reset: ()=>{
13414
+ activePass = null;
13415
+ dirtyFrom = 0;
13416
+ },
13417
+ restore,
13418
+ restoreActive: ()=>{
13419
+ if (activePass) restore(activePass);
13420
+ }
13421
+ };
13422
+ }
13385
13423
  function createDocumentPaginationRunCoordinator({ cancelFrame, onAbort, onCoalescedRequest, onError, onRunFinish, onRunStart, requestFrame, run }) {
13386
13424
  let activeController = null;
13387
13425
  let disposed = false;
@@ -13455,7 +13493,9 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
13455
13493
  const paginationCache = useRef(null);
13456
13494
  const measurementEditor = useRef(null);
13457
13495
  const measurementLayoutKey = useRef('');
13458
- const dirtyMeasurementFrom = useRef(0);
13496
+ const measurementRangeRef = useRef(null);
13497
+ measurementRangeRef.current ??= createDocumentPaginationMeasurementRange();
13498
+ const measurementRange = measurementRangeRef.current;
13459
13499
  const observedDocumentRevision = useRef(documentRevision);
13460
13500
  const [pagination, setPagination] = useState(null);
13461
13501
  const pageKey = pageMetricsKey(page);
@@ -13470,10 +13510,10 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
13470
13510
  paginationCache.current = null;
13471
13511
  measurementEditor.current = editor;
13472
13512
  measurementLayoutKey.current = measurementKey;
13473
- dirtyMeasurementFrom.current = 0;
13513
+ measurementRange.reset();
13474
13514
  }
13475
13515
  if (observedDocumentRevision.current !== documentRevision) {
13476
- if (!Number.isFinite(dirtyMeasurementFrom.current)) dirtyMeasurementFrom.current = 0;
13516
+ measurementRange.ensureDirty();
13477
13517
  observedDocumentRevision.current = documentRevision;
13478
13518
  }
13479
13519
  if (!editor || !editorMounted || editor.isDestroyed || !enabled || !client) {
@@ -13518,7 +13558,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
13518
13558
  if (!enabled) {
13519
13559
  measurementCache.current = null;
13520
13560
  paginationCache.current = null;
13521
- dirtyMeasurementFrom.current = 0;
13561
+ measurementRange.reset();
13522
13562
  }
13523
13563
  setPagination(null);
13524
13564
  return;
@@ -13586,143 +13626,155 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
13586
13626
  editorDom.dataset.paginationReusedPageChrome = '0';
13587
13627
  editorDom.dataset.paginationDerivedPageChrome = '0';
13588
13628
  delete editorDom.dataset.paginationError;
13589
- const measurementStart = dirtyMeasurementFrom.current;
13590
- dirtyMeasurementFrom.current = 1 / 0;
13591
- const textLayoutCollection = collectDocumentTextLayoutParagraphs(editor, layoutFonts, loadedLayoutFontIds, measurementCache.current, measurementStart);
13592
- editorDom.dataset.paginationTextCandidates = String(textLayoutCollection.paragraphs.length);
13593
- editorDom.dataset.paginationTextRuns = String(textLayoutCollection.paragraphs.reduce((count, paragraph)=>count + paragraph.runs.length, 0));
13594
- const textLayouts = new Map();
13595
- let fallbackGlyphCount = 0;
13596
- if (textLayoutCollection.paragraphs.length) {
13597
- editorDom.dataset.paginationState = 'shaping';
13598
- try {
13599
- let textLayoutEngine = '';
13600
- let unsupportedTextLayoutCount = 0;
13601
- const textLayoutBatches = documentTextLayoutBatches(textLayoutCollection.paragraphs);
13602
- editorDom.dataset.paginationTextBatches = String(textLayoutBatches.length);
13603
- for (const [batchIndex, paragraphs] of textLayoutBatches.entries()){
13604
- editorDom.dataset.paginationTextBatch = String(batchIndex + 1);
13605
- const textLayout = await client.textLayout({
13606
- revision: nextRevision,
13607
- documentRevision,
13608
- paragraphs
13609
- }, signal);
13610
- if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed) return;
13611
- textLayoutEngine ||= textLayout.engine;
13612
- unsupportedTextLayoutCount += textLayout.unsupportedParagraphIds.length + textLayout.layouts.filter((layout)=>layout.missingGlyphCount > 0).length;
13613
- for (const layout of textLayout.layouts)if (0 === layout.missingGlyphCount) {
13614
- textLayouts.set(layout.id, layout);
13615
- fallbackGlyphCount += layout.fallbackGlyphCount;
13629
+ const measurementPass = measurementRange.begin();
13630
+ const measurementStart = measurementPass.from;
13631
+ try {
13632
+ const textLayoutCollection = collectDocumentTextLayoutParagraphs(editor, layoutFonts, loadedLayoutFontIds, measurementCache.current, measurementStart);
13633
+ editorDom.dataset.paginationTextCandidates = String(textLayoutCollection.paragraphs.length);
13634
+ editorDom.dataset.paginationTextRuns = String(textLayoutCollection.paragraphs.reduce((count, paragraph)=>count + paragraph.runs.length, 0));
13635
+ const textLayouts = new Map();
13636
+ let fallbackGlyphCount = 0;
13637
+ if (textLayoutCollection.paragraphs.length) {
13638
+ editorDom.dataset.paginationState = 'shaping';
13639
+ try {
13640
+ let textLayoutEngine = '';
13641
+ let unsupportedTextLayoutCount = 0;
13642
+ const textLayoutBatches = documentTextLayoutBatches(textLayoutCollection.paragraphs);
13643
+ editorDom.dataset.paginationTextBatches = String(textLayoutBatches.length);
13644
+ for (const [batchIndex, paragraphs] of textLayoutBatches.entries()){
13645
+ editorDom.dataset.paginationTextBatch = String(batchIndex + 1);
13646
+ const textLayout = await client.textLayout({
13647
+ revision: nextRevision,
13648
+ documentRevision,
13649
+ paragraphs
13650
+ }, signal);
13651
+ if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed) return;
13652
+ textLayoutEngine ||= textLayout.engine;
13653
+ unsupportedTextLayoutCount += textLayout.unsupportedParagraphIds.length + textLayout.layouts.filter((layout)=>layout.missingGlyphCount > 0).length;
13654
+ for (const layout of textLayout.layouts)if (0 === layout.missingGlyphCount) {
13655
+ textLayouts.set(layout.id, layout);
13656
+ fallbackGlyphCount += layout.fallbackGlyphCount;
13657
+ }
13616
13658
  }
13659
+ editorDom.dataset.paginationTextEngine = textLayoutEngine;
13660
+ editorDom.dataset.paginationUnsupportedText = String(unsupportedTextLayoutCount);
13661
+ } catch (error) {
13662
+ if (signal.aborted || error instanceof DOMException && 'AbortError' === error.name) return;
13663
+ textLayouts.clear();
13664
+ fallbackGlyphCount = 0;
13665
+ editorDom.dataset.paginationTextEngine = 'dom';
13666
+ editorDom.dataset.paginationUnsupportedText = String(textLayoutCollection.paragraphs.length);
13617
13667
  }
13618
- editorDom.dataset.paginationTextEngine = textLayoutEngine;
13619
- editorDom.dataset.paginationUnsupportedText = String(unsupportedTextLayoutCount);
13620
- } catch (error) {
13621
- if (signal.aborted || error instanceof DOMException && 'AbortError' === error.name) return;
13622
- textLayouts.clear();
13623
- fallbackGlyphCount = 0;
13668
+ } else {
13624
13669
  editorDom.dataset.paginationTextEngine = 'dom';
13625
- editorDom.dataset.paginationUnsupportedText = String(textLayoutCollection.paragraphs.length);
13670
+ editorDom.dataset.paginationUnsupportedText = '0';
13671
+ editorDom.dataset.paginationTextBatch = '0';
13672
+ editorDom.dataset.paginationTextBatches = '0';
13626
13673
  }
13627
- } else {
13628
- editorDom.dataset.paginationTextEngine = 'dom';
13629
- editorDom.dataset.paginationUnsupportedText = '0';
13630
- editorDom.dataset.paginationTextBatch = '0';
13631
- editorDom.dataset.paginationTextBatches = '0';
13632
- }
13633
- editorDom.dataset.paginationShapedParagraphs = String(textLayouts.size);
13634
- editorDom.dataset.paginationFallbackGlyphs = String(fallbackGlyphCount);
13635
- editorDom.dataset.paginationShapedRuns = String(textLayoutCollection.paragraphs.reduce((count, paragraph)=>count + (textLayouts.has(paragraph.id) ? paragraph.runs.length : 0), 0));
13636
- const snapshot = measureDocumentLayoutBlocks(editor, measurementCache.current, measurementStart, textLayouts);
13637
- measurementCache.current = snapshot;
13638
- editorDom.dataset.paginationBlocks = String(snapshot.blocks.length);
13639
- editorDom.dataset.paginationFlows = String(new Set(snapshot.blocks.flatMap((block)=>block.block.flowId ? [
13640
- block.block.flowId
13641
- ] : [])).size);
13642
- editorDom.dataset.paginationMeasuredBlocks = String(snapshot.measuredBlockCount);
13643
- editorDom.dataset.paginationReusedBlocks = String(snapshot.reusedBlockCount);
13644
- if (snapshot.unsupportedLayout || !snapshot.blocks.length) {
13645
- observeBlocks(snapshot.blocks);
13646
- editorDom.dataset.paginationState = snapshot.unsupportedLayout ? 'unsupported' : 'empty';
13647
- setPagination(null);
13648
- paginationCache.current = null;
13649
- return;
13650
- }
13651
- const previousPagination = paginationCache.current;
13652
- const layoutPlan = planIncrementalDocumentLayout(previousPagination, snapshot.blocks, measurementStart);
13653
- editorDom.dataset.paginationLaidOutBlocks = String(layoutPlan.blocks.length);
13654
- editorDom.dataset.paginationReusedPages = String(layoutPlan.reusedPageCount);
13655
- try {
13656
- editorDom.dataset.paginationState = 'layout';
13657
- const partialLayout = await client.layout({
13658
- revision: nextRevision,
13659
- documentRevision,
13660
- startPageIndex: layoutPlan.startPageIndex,
13661
- page,
13662
- pageStyles: snapshot.pageStyles,
13663
- blocks: layoutPlan.blocks
13664
- }, signal);
13665
- if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed) return;
13666
- const layout = previousPagination && layoutPlan.startPageIndex > 0 ? mergeIncrementalDocumentLayout(previousPagination.layout, partialLayout) : partialLayout;
13667
- const blockById = new Map(snapshot.blocks.map((block)=>[
13668
- block.block.id,
13669
- block
13670
- ]));
13671
- const pageDescriptorDerivation = deriveDocumentPaginationPageDescriptors(layout, snapshot.blocks, editor.state.doc, previousPagination?.pages, layoutPlan.reusedPageCount);
13672
- const pages = pageDescriptorDerivation.pages;
13673
- editorDom.dataset.paginationReusedPageChrome = String(pageDescriptorDerivation.reusedPageCount);
13674
- editorDom.dataset.paginationDerivedPageChrome = String(pageDescriptorDerivation.derivedPageCount);
13675
- const pageByIndex = new Map(pages.map((descriptor)=>[
13676
- descriptor.pageIndex,
13677
- descriptor
13678
- ]));
13679
- editor.commands.applyDocumentPagination(nextRevision, layout.breaks.flatMap((pageBreak)=>{
13680
- const block = blockById.get(pageBreak.beforeBlockId);
13681
- return block ? [
13682
- {
13683
- ...pageBreak,
13684
- previousPage: pageByIndex.get(pageBreak.pageIndex - 1)?.page ?? layout.pages[pageBreak.pageIndex - 1]?.page ?? page,
13685
- nextPage: pageByIndex.get(pageBreak.pageIndex)?.page ?? layout.pages[pageBreak.pageIndex]?.page ?? page,
13686
- position: block.from,
13687
- inlineOffsetLeft: block.inlineOffsetLeft,
13688
- inlineOffsetRight: block.inlineOffsetRight,
13689
- previousPageChrome: visualPageChrome(pageByIndex.get(pageBreak.pageIndex - 1)),
13690
- nextPageChrome: visualPageChrome(pageByIndex.get(pageBreak.pageIndex)),
13691
- tableBreak: block.tableBreak
13692
- }
13693
- ] : [];
13694
- }));
13695
- observeBlocks(snapshot.blocks);
13696
- editorDom.dataset.paginationEngine = layout.engine;
13697
- editorDom.dataset.paginationDocumentRevision = String(layout.documentRevision);
13698
- editorDom.dataset.paginationPages = String(layout.pages.length);
13699
- editorDom.dataset.paginationState = 'ready';
13700
- delete editorDom.dataset.paginationError;
13701
- const nextPagination = {
13702
- layout,
13703
- blocks: snapshot.blocks,
13704
- pages,
13705
- pageByBlockId: new Map(layout.pages.flatMap((page)=>page.placements.map((placement)=>[
13706
- placement.blockId,
13707
- page.index + 1
13708
- ])))
13709
- };
13710
- paginationCache.current = nextPagination;
13711
- setPagination(nextPagination);
13712
- } catch (error) {
13713
- if (disposed || signal.aborted || error instanceof DOMException && 'AbortError' === error.name) return;
13714
- editorDom.dataset.paginationState = 'error';
13715
- editorDom.dataset.paginationError = error instanceof Error ? `${error.name}: ${error.message}` : 'UnknownError';
13716
- editor.commands.clearDocumentPagination(nextRevision);
13717
- paginationCache.current = null;
13718
- setPagination(null);
13674
+ editorDom.dataset.paginationShapedParagraphs = String(textLayouts.size);
13675
+ editorDom.dataset.paginationFallbackGlyphs = String(fallbackGlyphCount);
13676
+ editorDom.dataset.paginationShapedRuns = String(textLayoutCollection.paragraphs.reduce((count, paragraph)=>count + (textLayouts.has(paragraph.id) ? paragraph.runs.length : 0), 0));
13677
+ const snapshot = await measureDocumentLayoutBlocksIncrementally(editor, measurementCache.current, measurementStart, textLayouts, 1000000, {
13678
+ signal
13679
+ });
13680
+ measurementCache.current = snapshot;
13681
+ editorDom.dataset.paginationBlocks = String(snapshot.blocks.length);
13682
+ editorDom.dataset.paginationFlows = String(new Set(snapshot.blocks.flatMap((block)=>block.block.flowId ? [
13683
+ block.block.flowId
13684
+ ] : [])).size);
13685
+ editorDom.dataset.paginationMeasuredBlocks = String(snapshot.measuredBlockCount);
13686
+ editorDom.dataset.paginationReusedBlocks = String(snapshot.reusedBlockCount);
13687
+ if (snapshot.unsupportedLayout || !snapshot.blocks.length) {
13688
+ observeBlocks(snapshot.blocks);
13689
+ editorDom.dataset.paginationState = snapshot.unsupportedLayout ? 'unsupported' : 'empty';
13690
+ setPagination(null);
13691
+ paginationCache.current = null;
13692
+ measurementRange.commit(measurementPass);
13693
+ return;
13694
+ }
13695
+ const previousPagination = paginationCache.current;
13696
+ const layoutPlan = planIncrementalDocumentLayout(previousPagination, snapshot.blocks, measurementStart);
13697
+ editorDom.dataset.paginationLaidOutBlocks = String(layoutPlan.blocks.length);
13698
+ editorDom.dataset.paginationReusedPages = String(layoutPlan.reusedPageCount);
13699
+ try {
13700
+ editorDom.dataset.paginationState = 'layout';
13701
+ const partialLayout = await client.layout({
13702
+ revision: nextRevision,
13703
+ documentRevision,
13704
+ startPageIndex: layoutPlan.startPageIndex,
13705
+ page,
13706
+ pageStyles: snapshot.pageStyles,
13707
+ blocks: layoutPlan.blocks
13708
+ }, signal);
13709
+ if (disposed || signal.aborted || nextRevision !== revision.current || editor.isDestroyed) return;
13710
+ const layout = previousPagination && layoutPlan.startPageIndex > 0 ? mergeIncrementalDocumentLayout(previousPagination.layout, partialLayout) : partialLayout;
13711
+ const blockById = new Map(snapshot.blocks.map((block)=>[
13712
+ block.block.id,
13713
+ block
13714
+ ]));
13715
+ const pageDescriptorDerivation = deriveDocumentPaginationPageDescriptors(layout, snapshot.blocks, editor.state.doc, previousPagination?.pages, layoutPlan.reusedPageCount);
13716
+ const pages = pageDescriptorDerivation.pages;
13717
+ editorDom.dataset.paginationReusedPageChrome = String(pageDescriptorDerivation.reusedPageCount);
13718
+ editorDom.dataset.paginationDerivedPageChrome = String(pageDescriptorDerivation.derivedPageCount);
13719
+ const pageByIndex = new Map(pages.map((descriptor)=>[
13720
+ descriptor.pageIndex,
13721
+ descriptor
13722
+ ]));
13723
+ editor.commands.applyDocumentPagination(nextRevision, layout.breaks.flatMap((pageBreak)=>{
13724
+ const block = blockById.get(pageBreak.beforeBlockId);
13725
+ return block ? [
13726
+ {
13727
+ ...pageBreak,
13728
+ previousPage: pageByIndex.get(pageBreak.pageIndex - 1)?.page ?? layout.pages[pageBreak.pageIndex - 1]?.page ?? page,
13729
+ nextPage: pageByIndex.get(pageBreak.pageIndex)?.page ?? layout.pages[pageBreak.pageIndex]?.page ?? page,
13730
+ position: block.from,
13731
+ inlineOffsetLeft: block.inlineOffsetLeft,
13732
+ inlineOffsetRight: block.inlineOffsetRight,
13733
+ previousPageChrome: visualPageChrome(pageByIndex.get(pageBreak.pageIndex - 1)),
13734
+ nextPageChrome: visualPageChrome(pageByIndex.get(pageBreak.pageIndex)),
13735
+ tableBreak: block.tableBreak
13736
+ }
13737
+ ] : [];
13738
+ }));
13739
+ observeBlocks(snapshot.blocks);
13740
+ editorDom.dataset.paginationEngine = layout.engine;
13741
+ editorDom.dataset.paginationDocumentRevision = String(layout.documentRevision);
13742
+ editorDom.dataset.paginationPages = String(layout.pages.length);
13743
+ editorDom.dataset.paginationState = 'ready';
13744
+ delete editorDom.dataset.paginationError;
13745
+ const nextPagination = {
13746
+ layout,
13747
+ blocks: snapshot.blocks,
13748
+ pages,
13749
+ pageByBlockId: new Map(layout.pages.flatMap((page)=>page.placements.map((placement)=>[
13750
+ placement.blockId,
13751
+ page.index + 1
13752
+ ])))
13753
+ };
13754
+ paginationCache.current = nextPagination;
13755
+ measurementRange.commit(measurementPass);
13756
+ setPagination(nextPagination);
13757
+ } catch (error) {
13758
+ if (disposed || signal.aborted || error instanceof DOMException && 'AbortError' === error.name) return;
13759
+ editorDom.dataset.paginationState = 'error';
13760
+ editorDom.dataset.paginationError = error instanceof Error ? `${error.name}: ${error.message}` : 'UnknownError';
13761
+ editor.commands.clearDocumentPagination(nextRevision);
13762
+ paginationCache.current = null;
13763
+ setPagination(null);
13764
+ }
13765
+ } finally{
13766
+ measurementRange.restore(measurementPass);
13719
13767
  }
13720
13768
  };
13721
13769
  const coordinator = createDocumentPaginationRunCoordinator({
13722
13770
  cancelFrame: cancelAnimationFrame,
13723
- onAbort: ()=>updateDiagnostic('aborts', 'paginationAborts'),
13771
+ onAbort: ()=>{
13772
+ measurementRange.restoreActive();
13773
+ updateDiagnostic('aborts', 'paginationAborts');
13774
+ },
13724
13775
  onCoalescedRequest: ()=>updateDiagnostic('coalescedRequests', 'paginationCoalescedRequests'),
13725
13776
  onError: (error)=>{
13777
+ measurementRange.restoreActive();
13726
13778
  if (disposed || editor.isDestroyed) return;
13727
13779
  editorDom.dataset.paginationState = 'error';
13728
13780
  editorDom.dataset.paginationError = error instanceof Error ? `${error.name}: ${error.message}` : 'UnknownError';
@@ -13745,7 +13797,7 @@ function useDocumentPagination({ editor, documentRevision, enabled, layoutKey, p
13745
13797
  });
13746
13798
  };
13747
13799
  const markDirty = (position)=>{
13748
- dirtyMeasurementFrom.current = Math.min(dirtyMeasurementFrom.current, Math.max(0, position));
13800
+ measurementRange.invalidate(position);
13749
13801
  };
13750
13802
  const handleDocumentUpdate = ({ transaction })=>{
13751
13803
  updateDiagnostic('documentTriggers', 'paginationDocumentTriggers');
@@ -14029,7 +14081,10 @@ function pageMetricsKey(page) {
14029
14081
  page.headerHeight,
14030
14082
  page.footerHeight,
14031
14083
  page.pageGap
14032
- ].join(':');
14084
+ ].map(canonicalPageMetric).join(':');
14085
+ }
14086
+ function canonicalPageMetric(value) {
14087
+ return Math.round(1000000 * value) / 1000000;
14033
14088
  }
14034
14089
  function earliestChangedPosition(transaction) {
14035
14090
  let earliest = 1 / 0;
package/dist/8928.js CHANGED
@@ -7672,7 +7672,7 @@ function documentSectionNodeAttributes(layout, id) {
7672
7672
  documentGridLinePitch: normalizedDocumentGrid(layout.documentGrid)?.linePitch ?? null
7673
7673
  };
7674
7674
  }
7675
- function documentSectionLayoutFromNodeAttributes(attributes, fallback) {
7675
+ function work_document_section_documentSectionLayoutFromNodeAttributes(attributes, fallback) {
7676
7676
  const base = fallback ?? {
7677
7677
  pageSize: 'a4',
7678
7678
  orientation: 'portrait',
@@ -7765,7 +7765,7 @@ function documentSectionDomAttributes(layout, id) {
7765
7765
  };
7766
7766
  }
7767
7767
  function documentSectionLayoutFromElement(element, fallback) {
7768
- return documentSectionLayoutFromNodeAttributes({
7768
+ return work_document_section_documentSectionLayoutFromNodeAttributes({
7769
7769
  pageSize: element.dataset.sectionPageSize,
7770
7770
  orientation: element.dataset.sectionOrientation,
7771
7771
  marginTop: numberValue(element.dataset.sectionMarginTop),
@@ -7911,7 +7911,7 @@ function activeDocumentSectionFromState(state) {
7911
7911
  count: sections.length,
7912
7912
  position,
7913
7913
  node,
7914
- layout: documentSectionLayoutFromNodeAttributes(node.attrs)
7914
+ layout: work_document_section_documentSectionLayoutFromNodeAttributes(node.attrs)
7915
7915
  };
7916
7916
  }
7917
7917
  function updateActiveDocumentSection(layout, commandContext) {
@@ -7933,7 +7933,7 @@ function updateDocumentSection(sectionId, layout, commandContext) {
7933
7933
  const globals = documentPageMarginsForLayout(next);
7934
7934
  for (const [index, candidate] of directDocumentSections(state.doc).entries()){
7935
7935
  const id = documentSectionId(candidate.node, index);
7936
- const current = documentSectionLayoutFromNodeAttributes(candidate.node.attrs);
7936
+ const current = work_document_section_documentSectionLayoutFromNodeAttributes(candidate.node.attrs);
7937
7937
  const synchronized = synchronizeDocumentPageMarginGlobals(id === sectionId ? next : current, globals);
7938
7938
  commandContext.tr.setNodeMarkup(candidate.position, void 0, documentSectionNodeAttributes(synchronized, id));
7939
7939
  }
@@ -8008,7 +8008,7 @@ function documentSectionByIdInDocument(document1, sectionId) {
8008
8008
  count: sections.length,
8009
8009
  position: section.position,
8010
8010
  node: section.node,
8011
- layout: documentSectionLayoutFromNodeAttributes(section.node.attrs)
8011
+ layout: work_document_section_documentSectionLayoutFromNodeAttributes(section.node.attrs)
8012
8012
  };
8013
8013
  }
8014
8014
  function directDocumentSections(document1) {
@@ -12967,30 +12967,37 @@ function sanitizeRepeatedDocumentTableElement(clone) {
12967
12967
  pageBreak.remove();
12968
12968
  });
12969
12969
  }
12970
- function measureDocumentLayoutBlocks(editor, previous = null, dirtyFrom = 0, textLayouts = new Map(), maximumFragmentedTableRowHeight = 1000000) {
12970
+ const DOCUMENT_LAYOUT_MEASUREMENT_SLICE_MS = 32;
12971
+ async function measureDocumentLayoutBlocksIncrementally(editor, previous = null, dirtyFrom = 0, textLayouts = new Map(), maximumFragmentedTableRowHeight = 1000000, options = {}) {
12971
12972
  const blocks = [];
12972
12973
  const pageStyleByMetrics = new Map();
12973
12974
  const counts = {
12974
12975
  measured: 0,
12975
12976
  reused: 0
12976
12977
  };
12978
+ const checkpoint = options.checkpoint ?? createDocumentLayoutMeasurementCheckpoint();
12979
+ throwIfDocumentMeasurementAborted(options.signal);
12977
12980
  let unsupportedLayout = false;
12978
- editor.state.doc.forEach((section, sectionPosition, sectionIndex)=>{
12979
- if ('documentSection' !== section.type.name) return;
12980
- const layout = documentSectionLayoutFromNodeAttributes(section.attrs);
12981
- if (layout.columns.count > 1) unsupportedLayout = true;
12982
- const page = documentPageMetrics(layout);
12983
- const metricsKey = documentPageMetricsKey(page);
12984
- let pageStyle = pageStyleByMetrics.get(metricsKey);
12985
- if (!pageStyle) {
12986
- pageStyle = {
12987
- id: `document-page-style-${pageStyleByMetrics.size + 1}`,
12988
- page
12989
- };
12990
- pageStyleByMetrics.set(metricsKey, pageStyle);
12981
+ let sectionPosition = 0;
12982
+ for(let sectionIndex = 0; sectionIndex < editor.state.doc.childCount; sectionIndex += 1){
12983
+ const section = editor.state.doc.child(sectionIndex);
12984
+ if ('documentSection' === section.type.name) {
12985
+ const layout = work_document_section_documentSectionLayoutFromNodeAttributes(section.attrs);
12986
+ if (layout.columns.count > 1) unsupportedLayout = true;
12987
+ const page = documentPageMetrics(layout);
12988
+ const metricsKey = documentPageMetricsKey(page);
12989
+ let pageStyle = pageStyleByMetrics.get(metricsKey);
12990
+ if (!pageStyle) {
12991
+ pageStyle = {
12992
+ id: `document-page-style-${pageStyleByMetrics.size + 1}`,
12993
+ page
12994
+ };
12995
+ pageStyleByMetrics.set(metricsKey, pageStyle);
12996
+ }
12997
+ await measureSectionBlocksIncrementally(editor, section, sectionPosition, sectionIndex, layout, pageStyle, previous?.blocks ?? [], dirtyFrom, textLayouts, Math.min(maximumFragmentedTableRowHeight, documentPageBodyHeight(pageStyle.page)), counts, blocks, checkpoint, options.signal);
12991
12998
  }
12992
- measureSectionBlocks(editor, section, sectionPosition, sectionIndex, layout, pageStyle, previous?.blocks ?? [], dirtyFrom, textLayouts, Math.min(maximumFragmentedTableRowHeight, documentPageBodyHeight(pageStyle.page)), counts, blocks);
12993
- });
12999
+ sectionPosition += section.nodeSize;
13000
+ }
12994
13001
  return {
12995
13002
  blocks,
12996
13003
  pageStyles: [
@@ -13022,63 +13029,74 @@ function documentPageMetrics(layout, physicalPage = 1) {
13022
13029
  function documentPageBodyHeight(page) {
13023
13030
  return Math.max(1, page.height - page.marginTop - page.marginBottom);
13024
13031
  }
13025
- function measureSectionBlocks(editor, section, sectionPosition, sectionIndex, layout, pageStyle, previous, dirtyFrom, textLayouts, maximumFragmentedTableRowHeight, counts, result) {
13032
+ async function measureSectionBlocksIncrementally(editor, section, sectionPosition, sectionIndex, layout, pageStyle, previous, dirtyFrom, textLayouts, maximumFragmentedTableRowHeight, counts, result, checkpoint, signal) {
13026
13033
  const sectionBlocks = [];
13027
- section.forEach((node, offset, index)=>{
13028
- const position = sectionPosition + offset + 1;
13029
- const element = elementForNode(editor, position);
13030
- if (!element) return;
13031
- const id = documentBlockId(sectionPosition, index, position);
13032
- const paragraphPagination = documentNodeParagraphPagination(node);
13033
- if (isDocumentListNode(node)) {
13034
- const reused = reusableDocumentListLayoutBlocks(previous, id, position + node.nodeSize, dirtyFrom);
13035
- if (reused.length) {
13036
- counts.reused += reused.length;
13037
- sectionBlocks.push(...reused);
13038
- return;
13039
- }
13040
- const listBlocks = measureDocumentListBlocks(editor, node, element, id, position, textLayouts, maximumFragmentedTableRowHeight);
13041
- if (listBlocks.length) {
13042
- counts.measured += listBlocks.length;
13043
- sectionBlocks.push(...listBlocks);
13044
- return;
13045
- }
13046
- }
13047
- const reused = reusableDocumentLayoutBlocks(previous, id, element, position + node.nodeSize, dirtyFrom);
13034
+ let offset = 0;
13035
+ for(let index = 0; index < section.childCount; index += 1){
13036
+ await checkpoint(signal);
13037
+ throwIfDocumentMeasurementAborted(signal);
13038
+ const node = section.child(index);
13039
+ measureSectionBlock(editor, node, offset, index, sectionPosition, previous, dirtyFrom, textLayouts, maximumFragmentedTableRowHeight, counts, sectionBlocks);
13040
+ offset += node.nodeSize;
13041
+ }
13042
+ finishMeasuredSection(section, sectionPosition, sectionIndex, layout, pageStyle, sectionBlocks, result);
13043
+ }
13044
+ function measureSectionBlock(editor, node, offset, index, sectionPosition, previous, dirtyFrom, textLayouts, maximumFragmentedTableRowHeight, counts, sectionBlocks) {
13045
+ const position = sectionPosition + offset + 1;
13046
+ const element = elementForNode(editor, position);
13047
+ if (!element) return;
13048
+ const id = documentBlockId(sectionPosition, index, position);
13049
+ const paragraphPagination = documentNodeParagraphPagination(node);
13050
+ if (isDocumentListNode(node)) {
13051
+ const reused = reusableDocumentListLayoutBlocks(previous, id, position + node.nodeSize, dirtyFrom);
13048
13052
  if (reused.length) {
13049
13053
  counts.reused += reused.length;
13050
13054
  sectionBlocks.push(...reused);
13051
13055
  return;
13052
13056
  }
13053
- if ('table' === node.type.name) {
13054
- const tableRows = measureDocumentTableRows(editor, node, element, id, position, maximumFragmentedTableRowHeight);
13055
- if (tableRows.length) {
13056
- counts.measured += tableRows.length;
13057
- sectionBlocks.push(...tableRows);
13058
- return;
13059
- }
13057
+ const listBlocks = measureDocumentListBlocks(editor, node, element, id, position, textLayouts, maximumFragmentedTableRowHeight);
13058
+ if (listBlocks.length) {
13059
+ counts.measured += listBlocks.length;
13060
+ sectionBlocks.push(...listBlocks);
13061
+ return;
13060
13062
  }
13061
- const lineFragments = measureParagraphLineFragments(editor, node, element, id, position, position + node.nodeSize, paragraphPagination, textLayouts.get(id));
13062
- if (lineFragments.length > 1) {
13063
- counts.measured += lineFragments.length;
13064
- sectionBlocks.push(...lineFragments);
13063
+ }
13064
+ const reused = reusableDocumentLayoutBlocks(previous, id, element, position + node.nodeSize, dirtyFrom);
13065
+ if (reused.length) {
13066
+ counts.reused += reused.length;
13067
+ sectionBlocks.push(...reused);
13068
+ return;
13069
+ }
13070
+ if ('table' === node.type.name) {
13071
+ const tableRows = measureDocumentTableRows(editor, node, element, id, position, maximumFragmentedTableRowHeight);
13072
+ if (tableRows.length) {
13073
+ counts.measured += tableRows.length;
13074
+ sectionBlocks.push(...tableRows);
13065
13075
  return;
13066
13076
  }
13067
- counts.measured += 1;
13068
- sectionBlocks.push(measuredDocumentBlock({
13069
- block: {
13070
- id,
13071
- height: documentBlockFlowHeight(node, element),
13072
- breakBefore: paragraphPagination.pageBreakBefore,
13073
- breakAfter: 'pageBreak' === node.type.name,
13074
- keepTogether: paragraphPagination.keepLines || shouldKeepDocumentBlockTogether(node),
13075
- keepWithNext: paragraphPagination.keepWithNext
13076
- },
13077
- element,
13078
- from: position,
13079
- to: position + node.nodeSize
13080
- }));
13081
- });
13077
+ }
13078
+ const lineFragments = measureParagraphLineFragments(editor, node, element, id, position, position + node.nodeSize, paragraphPagination, textLayouts.get(id));
13079
+ if (lineFragments.length > 1) {
13080
+ counts.measured += lineFragments.length;
13081
+ sectionBlocks.push(...lineFragments);
13082
+ return;
13083
+ }
13084
+ counts.measured += 1;
13085
+ sectionBlocks.push(measuredDocumentBlock({
13086
+ block: {
13087
+ id,
13088
+ height: documentBlockFlowHeight(node, element),
13089
+ breakBefore: paragraphPagination.pageBreakBefore,
13090
+ breakAfter: 'pageBreak' === node.type.name,
13091
+ keepTogether: paragraphPagination.keepLines || shouldKeepDocumentBlockTogether(node),
13092
+ keepWithNext: paragraphPagination.keepWithNext
13093
+ },
13094
+ element,
13095
+ from: position,
13096
+ to: position + node.nodeSize
13097
+ }));
13098
+ }
13099
+ function finishMeasuredSection(section, sectionPosition, sectionIndex, layout, pageStyle, sectionBlocks, result) {
13082
13100
  const last = sectionBlocks.at(-1);
13083
13101
  if (last && 'continuous' !== layout.breakAfter && 'nextColumn' !== layout.breakAfter) last.block.breakAfter = true;
13084
13102
  const sectionMetadata = {
@@ -13095,6 +13113,41 @@ function measureSectionBlocks(editor, section, sectionPosition, sectionIndex, la
13095
13113
  }
13096
13114
  result.push(...sectionBlocks);
13097
13115
  }
13116
+ function createDocumentLayoutMeasurementCheckpoint() {
13117
+ let deadline = documentMeasurementNow() + DOCUMENT_LAYOUT_MEASUREMENT_SLICE_MS;
13118
+ return async (signal)=>{
13119
+ throwIfDocumentMeasurementAborted(signal);
13120
+ if (documentMeasurementNow() < deadline) return;
13121
+ await yieldDocumentMeasurement();
13122
+ throwIfDocumentMeasurementAborted(signal);
13123
+ deadline = documentMeasurementNow() + DOCUMENT_LAYOUT_MEASUREMENT_SLICE_MS;
13124
+ };
13125
+ }
13126
+ function documentMeasurementNow() {
13127
+ return globalThis.performance?.now?.() ?? Date.now();
13128
+ }
13129
+ function yieldDocumentMeasurement() {
13130
+ if ("u" > typeof MessageChannel) return new Promise((resolve)=>{
13131
+ const channel = new MessageChannel();
13132
+ channel.port1.addEventListener('message', ()=>{
13133
+ channel.port1.close();
13134
+ channel.port2.close();
13135
+ resolve();
13136
+ }, {
13137
+ once: true
13138
+ });
13139
+ channel.port1.start();
13140
+ channel.port2.postMessage(void 0);
13141
+ });
13142
+ return new Promise((resolve)=>globalThis.setTimeout(resolve, 0));
13143
+ }
13144
+ function throwIfDocumentMeasurementAborted(signal) {
13145
+ if (!signal?.aborted) return;
13146
+ if (signal.reason instanceof Error) throw signal.reason;
13147
+ const error = new Error('Document layout measurement was aborted');
13148
+ error.name = 'AbortError';
13149
+ throw error;
13150
+ }
13098
13151
  function documentPageMetricsKey(page) {
13099
13152
  return [
13100
13153
  page.width,
@@ -13337,7 +13390,7 @@ const DocumentSection = core_Node.create({
13337
13390
  ];
13338
13391
  },
13339
13392
  renderHTML ({ node, HTMLAttributes }) {
13340
- const layout = documentSectionLayoutFromNodeAttributes(node.attrs);
13393
+ const layout = work_document_section_documentSectionLayoutFromNodeAttributes(node.attrs);
13341
13394
  const id = 'string' == typeof node.attrs.id && node.attrs.id ? node.attrs.id : 'document-section';
13342
13395
  const columns = layout.columns;
13343
13396
  const page = documentPageMetrics(layout);
@@ -17415,4 +17468,4 @@ function capturePages(surface) {
17415
17468
  width: surface.pageWidth
17416
17469
  }));
17417
17470
  }
17418
- export { DEFAULT_DOCUMENT_TABLE_CELL_FORMAT, DEFAULT_DOCUMENT_TABLE_CELL_MARGINS, DEFAULT_DOCUMENT_TABLE_GEOMETRY, DOCUMENT_BOOKMARK_DUPLICATE_MESSAGE, DOCUMENT_PAGE_BORDER_EDGES, DOCUMENT_PAGE_MARGIN_KEYS, DOCUMENT_PARAGRAPH_BORDER_EDGES, DOCUMENT_PARAGRAPH_BORDER_STYLES, DOCUMENT_PARAGRAPH_SHADING_PATTERNS, DOCUMENT_TABLE_ROW_ID_ATTRIBUTE, DOCUMENT_TABLE_ROW_TEXT_ID_ATTRIBUTE, DOCUMENT_TABLE_STYLE_OPTIONS, DocumentEquation, DocumentImage, DocumentParagraphFormatting, DocumentParagraphIdentity, DocumentSubscript, DocumentSuperscript, DocumentTableRowIdentity, DocxThemePatchCollector, MAX_DOCUMENT_IMAGE_RELATIVE_HEIGHT, MAX_DOCUMENT_NUMBERING_START, activeDocumentBookmark, activeDocumentSection, activeDocumentTableStyle, applyDocumentImageCropToElement, applyDocumentImageIdentityToElement, applyDocumentImageLayerToElement, applyDocumentImageWrapContourToElement, applyDocumentPageGeometry, applyDocumentParagraphIdentityToElement, applyDocumentTableGeometryToElement, applyDocumentTableRowIdentityToElement, canChangeDocumentIndent, canInsertDocumentComment, canSetDocumentTableRowRepeatHeader, clampDocumentMargin, collectDocumentChanges, collectDocumentCommentAnchors, collectDocumentNotes, collectDocumentTextLayoutParagraphs, createDocumentBibliography, createDocumentEquationElement, createDocumentImageIdentityRegistry, createDocumentNoteElement, createDocumentParagraphIdentityRegistry, createWorkDocumentExtensions, createWorkDocumentModel, createWorkDocumentModelFromContent, createWorkOfficeDocumentCollaborationBinding as createOfficeDocumentCollaborationBinding, defaultDocumentImageWrapContour, documentAutoLineHeight, documentBookmarkNameExists, documentBookmarkReferenceInstruction, documentBulletListStyle, documentCaptionKind, documentCaptionLabel, documentCitationCount, documentCitationInstruction, documentCitationStyle, documentCitationStyleDetails, documentCitationTags, documentCitationTagsFromInstruction, documentCommentDraftRange, documentCommentViews, documentContentLayoutProperties, documentEquationFromElement, documentEquationText, documentImageCropFromElement, documentImageIdentityFromElement, documentImageLayerFromElement, documentImageLayoutFromElement, documentImageLayoutOptions, documentImagePositionFromElement, documentImageProperties, documentImageWrapContourFromElement, documentInitialSectionLayout, documentModelForContent, documentNoteKey, documentNoteKind, documentOrderedListState, documentPageBordersVisible, documentPageChromeLegacyFields, documentPageGeometryForLayout, documentPageHorizontalMarginTwips, documentPageMarginBody, documentPageMarginsForLayout, documentPageMetrics, documentParagraphBordersDomAttributes, documentParagraphDirection, documentParagraphIdentityFromElement, documentParagraphIndent, documentParagraphPagination, documentParagraphShadingDomAttributes, documentParagraphSpacing, documentParagraphTabStops, documentSectionById, documentSectionDomAttributes, documentSections, documentTabLeaderLabel, documentTableBordersFromElement, documentTableCellFormat, documentTableCellMarginOverridesFromElement, documentTableColumnPercentagesFromElement, documentTableGeometryFromElement, documentTableHorizontalAlignment, documentTableRowIdentityFromElement, documentTableRowOptions, documentTableSizing, documentTextLayoutBatches, documentWordLineHeightFactor, docxBookmarkReferenceTarget, docxDocumentFieldKind, editorDocumentBookmarkReferenceTargets, editorDocumentCaptionTargets, initializeWorkOfficeDocumentCollaboration as initializeOfficeDocumentCollaboration, isContourImageLayout, isDocumentParagraphArtBorderStyle, isValidDocumentCitationTag, materializeWorkDocumentContent, measureDocumentLayoutBlocks, millimetersToPixels, mountWorkLiveDocumentCapture, nextDocumentTabAlignment, normalizeDocumentBookmarkName, normalizeDocumentBookmarkNativeId, normalizeDocumentBookmarkReferencesHtml, normalizeDocumentBookmarksHtml, normalizeDocumentCaptionsHtml, normalizeDocumentCitationsHtml, normalizeDocumentColumns, normalizeDocumentEquation, normalizeDocumentFieldsHtml, normalizeDocumentHtml, normalizeDocumentImageAlignment, normalizeDocumentImageCrop, normalizeDocumentImageIdentity, normalizeDocumentImageLayer, normalizeDocumentImageLayoutOptions, normalizeDocumentImagePosition, normalizeDocumentImageWrapContour, normalizeDocumentImageWrapSide, normalizeDocumentNotesHtml, normalizeDocumentPageBorders, normalizeDocumentPageChrome, normalizeDocumentPageGeometry, normalizeDocumentPageMargins, normalizeDocumentPaperSource, normalizeDocumentParagraphBorder, normalizeDocumentParagraphBorders, normalizeDocumentParagraphId, normalizeDocumentParagraphIdentity, normalizeDocumentParagraphIndent, normalizeDocumentTabStops, normalizeDocumentTableBorderStyle, normalizeDocumentTableBorderWidth, normalizeDocumentTableRowHeightRule, normalizeDocumentTableRowIdentity, normalizeDocumentTableVerticalAlign, normalizeTableColor, normalizedTabPosition, pageTwipsToMillimeters, parseDocumentParagraphBordersElement, parseDocumentParagraphShadingElement, parseDocxThemeReference, patchDocxThemeReferences, positionWorkLiveDocumentCapture, readWorkOfficeDocumentCollaboration as readOfficeDocumentCollaboration, renderDocumentAutoLineHeight, renderDocumentTableBorders, renderDocumentTableCellMarginOverrides, resolveDocumentPageBorders, resolveDocumentPageChrome, resolveDocumentPageMargins, resolveDocumentPageSize, resolveWorkDocumentEditorInput, retainAnchoredDocumentComments, sanitizeDocumentPageChromeHtml, serializeDocumentTabStops, serializeDocxThemeReference, serializeWorkDocumentNode, setCustomDocumentColumns, supportedDocxBookmarkReferenceInstruction, syncDocumentContentFromHtml, uniqueDocumentImageIdentity, uniqueDocumentParagraphIdentity, updateDocumentColumnWidth, updateDocumentCustomPageMillimeters, updateDocumentGutterPosition, updateDocumentMirrorMargins, updateDocumentPageChromeVariant, updateDocumentPageMarginMillimeters, updateDocumentPageMarginMode, updateDocumentPageOrientation, updateDocumentPaperSizePreset, validateDocumentBookmarkName, workDocumentSchema, workOfficeDocumentCollaborationFragment as officeDocumentCollaborationFragment, work_document_page_margins_twipsToMillimeters, wrapsBesideImage };
17471
+ export { DEFAULT_DOCUMENT_TABLE_CELL_FORMAT, DEFAULT_DOCUMENT_TABLE_CELL_MARGINS, DEFAULT_DOCUMENT_TABLE_GEOMETRY, DOCUMENT_BOOKMARK_DUPLICATE_MESSAGE, DOCUMENT_PAGE_BORDER_EDGES, DOCUMENT_PAGE_MARGIN_KEYS, DOCUMENT_PARAGRAPH_BORDER_EDGES, DOCUMENT_PARAGRAPH_BORDER_STYLES, DOCUMENT_PARAGRAPH_SHADING_PATTERNS, DOCUMENT_TABLE_ROW_ID_ATTRIBUTE, DOCUMENT_TABLE_ROW_TEXT_ID_ATTRIBUTE, DOCUMENT_TABLE_STYLE_OPTIONS, DocumentEquation, DocumentImage, DocumentParagraphFormatting, DocumentParagraphIdentity, DocumentSubscript, DocumentSuperscript, DocumentTableRowIdentity, DocxThemePatchCollector, MAX_DOCUMENT_IMAGE_RELATIVE_HEIGHT, MAX_DOCUMENT_NUMBERING_START, activeDocumentBookmark, activeDocumentSection, activeDocumentTableStyle, applyDocumentImageCropToElement, applyDocumentImageIdentityToElement, applyDocumentImageLayerToElement, applyDocumentImageWrapContourToElement, applyDocumentPageGeometry, applyDocumentParagraphIdentityToElement, applyDocumentTableGeometryToElement, applyDocumentTableRowIdentityToElement, canChangeDocumentIndent, canInsertDocumentComment, canSetDocumentTableRowRepeatHeader, clampDocumentMargin, collectDocumentChanges, collectDocumentCommentAnchors, collectDocumentNotes, collectDocumentTextLayoutParagraphs, createDocumentBibliography, createDocumentEquationElement, createDocumentImageIdentityRegistry, createDocumentNoteElement, createDocumentParagraphIdentityRegistry, createWorkDocumentExtensions, createWorkDocumentModel, createWorkDocumentModelFromContent, createWorkOfficeDocumentCollaborationBinding as createOfficeDocumentCollaborationBinding, defaultDocumentImageWrapContour, documentAutoLineHeight, documentBookmarkNameExists, documentBookmarkReferenceInstruction, documentBulletListStyle, documentCaptionKind, documentCaptionLabel, documentCitationCount, documentCitationInstruction, documentCitationStyle, documentCitationStyleDetails, documentCitationTags, documentCitationTagsFromInstruction, documentCommentDraftRange, documentCommentViews, documentContentLayoutProperties, documentEquationFromElement, documentEquationText, documentImageCropFromElement, documentImageIdentityFromElement, documentImageLayerFromElement, documentImageLayoutFromElement, documentImageLayoutOptions, documentImagePositionFromElement, documentImageProperties, documentImageWrapContourFromElement, documentInitialSectionLayout, documentModelForContent, documentNoteKey, documentNoteKind, documentOrderedListState, documentPageBordersVisible, documentPageChromeLegacyFields, documentPageGeometryForLayout, documentPageHorizontalMarginTwips, documentPageMarginBody, documentPageMarginsForLayout, documentPageMetrics, documentParagraphBordersDomAttributes, documentParagraphDirection, documentParagraphIdentityFromElement, documentParagraphIndent, documentParagraphPagination, documentParagraphShadingDomAttributes, documentParagraphSpacing, documentParagraphTabStops, documentSectionById, documentSectionDomAttributes, documentSections, documentTabLeaderLabel, documentTableBordersFromElement, documentTableCellFormat, documentTableCellMarginOverridesFromElement, documentTableColumnPercentagesFromElement, documentTableGeometryFromElement, documentTableHorizontalAlignment, documentTableRowIdentityFromElement, documentTableRowOptions, documentTableSizing, documentTextLayoutBatches, documentWordLineHeightFactor, docxBookmarkReferenceTarget, docxDocumentFieldKind, editorDocumentBookmarkReferenceTargets, editorDocumentCaptionTargets, initializeWorkOfficeDocumentCollaboration as initializeOfficeDocumentCollaboration, isContourImageLayout, isDocumentParagraphArtBorderStyle, isValidDocumentCitationTag, materializeWorkDocumentContent, measureDocumentLayoutBlocksIncrementally, millimetersToPixels, mountWorkLiveDocumentCapture, nextDocumentTabAlignment, normalizeDocumentBookmarkName, normalizeDocumentBookmarkNativeId, normalizeDocumentBookmarkReferencesHtml, normalizeDocumentBookmarksHtml, normalizeDocumentCaptionsHtml, normalizeDocumentCitationsHtml, normalizeDocumentColumns, normalizeDocumentEquation, normalizeDocumentFieldsHtml, normalizeDocumentHtml, normalizeDocumentImageAlignment, normalizeDocumentImageCrop, normalizeDocumentImageIdentity, normalizeDocumentImageLayer, normalizeDocumentImageLayoutOptions, normalizeDocumentImagePosition, normalizeDocumentImageWrapContour, normalizeDocumentImageWrapSide, normalizeDocumentNotesHtml, normalizeDocumentPageBorders, normalizeDocumentPageChrome, normalizeDocumentPageGeometry, normalizeDocumentPageMargins, normalizeDocumentPaperSource, normalizeDocumentParagraphBorder, normalizeDocumentParagraphBorders, normalizeDocumentParagraphId, normalizeDocumentParagraphIdentity, normalizeDocumentParagraphIndent, normalizeDocumentTabStops, normalizeDocumentTableBorderStyle, normalizeDocumentTableBorderWidth, normalizeDocumentTableRowHeightRule, normalizeDocumentTableRowIdentity, normalizeDocumentTableVerticalAlign, normalizeTableColor, normalizedTabPosition, pageTwipsToMillimeters, parseDocumentParagraphBordersElement, parseDocumentParagraphShadingElement, parseDocxThemeReference, patchDocxThemeReferences, positionWorkLiveDocumentCapture, readWorkOfficeDocumentCollaboration as readOfficeDocumentCollaboration, renderDocumentAutoLineHeight, renderDocumentTableBorders, renderDocumentTableCellMarginOverrides, resolveDocumentPageBorders, resolveDocumentPageChrome, resolveDocumentPageMargins, resolveDocumentPageSize, resolveWorkDocumentEditorInput, retainAnchoredDocumentComments, sanitizeDocumentPageChromeHtml, serializeDocumentTabStops, serializeDocxThemeReference, serializeWorkDocumentNode, setCustomDocumentColumns, supportedDocxBookmarkReferenceInstruction, syncDocumentContentFromHtml, uniqueDocumentImageIdentity, uniqueDocumentParagraphIdentity, updateDocumentColumnWidth, updateDocumentCustomPageMillimeters, updateDocumentGutterPosition, updateDocumentMirrorMargins, updateDocumentPageChromeVariant, updateDocumentPageMarginMillimeters, updateDocumentPageMarginMode, updateDocumentPageOrientation, updateDocumentPaperSizePreset, validateDocumentBookmarkName, workDocumentSchema, workOfficeDocumentCollaborationFragment as officeDocumentCollaborationFragment, work_document_page_margins_twipsToMillimeters, wrapsBesideImage };
@@ -0,0 +1,18 @@
1
+ export interface DocumentPaginationMeasurementPass {
2
+ readonly from: number;
3
+ }
4
+ export interface DocumentPaginationMeasurementRange {
5
+ begin: () => DocumentPaginationMeasurementPass;
6
+ commit: (pass: DocumentPaginationMeasurementPass) => void;
7
+ ensureDirty: () => void;
8
+ invalidate: (position: number) => void;
9
+ reset: () => void;
10
+ restore: (pass: DocumentPaginationMeasurementPass) => void;
11
+ restoreActive: () => void;
12
+ }
13
+ /**
14
+ * Tracks the earliest document position that the next pagination pass must
15
+ * measure. An active pass consumes that position temporarily, then either
16
+ * commits it or restores it when cooperative work is aborted.
17
+ */
18
+ export declare function createDocumentPaginationMeasurementRange(): DocumentPaginationMeasurementRange;
@@ -2,7 +2,18 @@ import type { Editor } from '@tiptap/core';
2
2
  import type { OfficeKernelPageMetrics, OfficeKernelTextLayoutParagraphResult } from '../../kernel/office-kernel-protocol';
3
3
  import type { DocumentPaginationSnapshot } from './work-document-pagination-types';
4
4
  import type { WorkDocumentSectionLayout } from './work-types';
5
+ export interface IncrementalDocumentLayoutMeasurementOptions {
6
+ checkpoint?: (signal?: AbortSignal) => Promise<void>;
7
+ signal?: AbortSignal;
8
+ }
5
9
  export declare function measureDocumentLayoutBlocks(editor: Editor, previous?: DocumentPaginationSnapshot | null, dirtyFrom?: number, textLayouts?: ReadonlyMap<string, OfficeKernelTextLayoutParagraphResult>, maximumFragmentedTableRowHeight?: number): DocumentPaginationSnapshot;
10
+ /**
11
+ * Measures the same canonical editor DOM as `measureDocumentLayoutBlocks`
12
+ * while yielding between top-level blocks. The browser keeps input, media,
13
+ * and animation tasks schedulable during large-document pagination without
14
+ * weakening the layout result or replacing the controlled editor model.
15
+ */
16
+ export declare function measureDocumentLayoutBlocksIncrementally(editor: Editor, previous?: DocumentPaginationSnapshot | null, dirtyFrom?: number, textLayouts?: ReadonlyMap<string, OfficeKernelTextLayoutParagraphResult>, maximumFragmentedTableRowHeight?: number, options?: IncrementalDocumentLayoutMeasurementOptions): Promise<DocumentPaginationSnapshot>;
6
17
  export declare function documentPageMetrics(layout: WorkDocumentSectionLayout, physicalPage?: number): OfficeKernelPageMetrics;
7
18
  export declare function documentPageBodyHeight(page: OfficeKernelPageMetrics): number;
8
19
  export declare function documentPaginationSurfaceHeight(pageCount: number, page: OfficeKernelPageMetrics): number;
@@ -2,7 +2,8 @@ import { Extension } from '@tiptap/core';
2
2
  import type { DocumentPaginationVisualBreak } from './work-document-pagination-types';
3
3
  export { createDocumentLineFragments, createShapedDocumentLineFragments, findDocumentLineStartOffset, } from './work-document-line-measurement';
4
4
  export { reusableDocumentLayoutBlocks } from './work-document-pagination-dom';
5
- export { documentPageBodyHeight, documentPageMetrics, documentPaginationSurfaceHeight, measureDocumentLayoutBlocks, } from './work-document-pagination-measurement';
5
+ export { documentPageBodyHeight, documentPageMetrics, documentPaginationSurfaceHeight, measureDocumentLayoutBlocks, measureDocumentLayoutBlocksIncrementally, } from './work-document-pagination-measurement';
6
+ export type { IncrementalDocumentLayoutMeasurementOptions } from './work-document-pagination-measurement';
6
7
  export type { DocumentPaginationSection, DocumentPaginationSnapshot, DocumentPaginationVisualBreak, DocumentPaginationVisualPageChrome, DocumentTableCellBoundary, DocumentTableCellFragmentMeasurement, DocumentTableCellPageBreak, DocumentTablePaginationBreak, DocumentTableRowFragmentPlan, DocumentTextLayoutCollection, MeasuredDocumentLayoutBlock, } from './work-document-pagination-types';
7
8
  export { createDocumentTableRowFragmentPlan } from './work-document-table-pagination';
8
9
  export { collectDocumentTextLayoutParagraphs, collectDocumentTextLayoutRuns, documentTextLayoutBatches, } from './work-document-text-layout';
Binary file
@@ -844,6 +844,13 @@ text validation, UTF-16 offset mapping, shaping, and their tests live in
844
844
  focused modules. This keeps the fallback and WASM paths independently
845
845
  testable without duplicating their public protocol.
846
846
 
847
+ Live DOM measurement is cooperatively scheduled in bounded 32 ms slices. It
848
+ yields only between top-level ProseMirror blocks, so one block is always
849
+ measured atomically against its rendered element. Document invalidation aborts
850
+ the stale pass before it can replace the reusable snapshot, while the existing
851
+ single-flight coordinator coalesces one fresh run. The synchronous measurement
852
+ path remains available for deterministic callers and parity tests.
853
+
847
854
  Layout requests carry a bounded, deduplicated page-style table and let each
848
855
  measured block reference its section geometry. A geometry transition opens a
849
856
  new physical page in both the JavaScript fallback and Rust/WASM kernel, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",