@stll/folio-core 0.54.0 → 0.54.2

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.
Files changed (89) hide show
  1. package/dist/ai-edits/apply.d.ts +1 -1
  2. package/dist/ai-edits/apply.js +43 -31
  3. package/dist/ai-edits/headless.d.ts +1 -2
  4. package/dist/ai-edits/headless.js +12 -27
  5. package/dist/ai-edits/read.d.ts +1 -0
  6. package/dist/ai-edits/read.js +7 -2
  7. package/dist/ai-edits/table-mutation-plan.d.ts +2 -1
  8. package/dist/ai-edits/table-mutation-plan.js +39 -1
  9. package/dist/ai-suggestions/apply.d.ts +1 -1
  10. package/dist/ai-suggestions/apply.js +3 -2
  11. package/dist/compare/compare.js +2 -1
  12. package/dist/docx/bookmarkIds.d.ts +11 -0
  13. package/dist/docx/bookmarkIds.js +33 -0
  14. package/dist/docx/documentParser.d.ts +14 -1
  15. package/dist/docx/documentParser.js +17 -4
  16. package/dist/docx/drawingIdNormalization.js +23 -8
  17. package/dist/docx/ensureParaIds.d.ts +5 -0
  18. package/dist/docx/ensureParaIds.js +10 -2
  19. package/dist/docx/listNumberingInstances.js +11 -9
  20. package/dist/docx/noteIds.d.ts +4 -0
  21. package/dist/docx/noteIds.js +13 -0
  22. package/dist/docx/numberingIds.d.ts +14 -0
  23. package/dist/docx/numberingIds.js +16 -0
  24. package/dist/docx/numericIdAllocator.d.ts +12 -0
  25. package/dist/docx/numericIdAllocator.js +33 -0
  26. package/dist/docx/numericIdNormalization.d.ts +16 -0
  27. package/dist/docx/numericIdNormalization.js +174 -0
  28. package/dist/docx/paragraphPropertySource.js +1 -1
  29. package/dist/docx/paragraphTextBoxEnrichment.js +2 -1
  30. package/dist/docx/parser.js +17 -5
  31. package/dist/docx/rasterMime.d.ts +64 -0
  32. package/dist/docx/rasterMime.js +116 -0
  33. package/dist/docx/replyToComment.js +13 -6
  34. package/dist/docx/rezip.js +14 -7
  35. package/dist/docx/selectiveSave.js +10 -0
  36. package/dist/docx/serializer/partNamespaces.js +4 -2
  37. package/dist/docx/serializer/runSerializer.js +5 -4
  38. package/dist/docx/serializer/trackedChangeAttributes.js +7 -1
  39. package/dist/docx/server/build.js +14 -3
  40. package/dist/docx/server/createBilingualDocument.js +10 -9
  41. package/dist/docx/streamingXmlParser.d.ts +10 -1
  42. package/dist/docx/streamingXmlParser.js +88 -19
  43. package/dist/docx/unzip.d.ts +3 -1
  44. package/dist/docx/unzip.js +36 -16
  45. package/dist/docx/verticalMergeProjection.d.ts +17 -0
  46. package/dist/docx/verticalMergeProjection.js +120 -0
  47. package/dist/layout-bridge/convert/fixedTableColumnWidths.d.ts +6 -0
  48. package/dist/layout-bridge/convert/fixedTableColumnWidths.js +23 -0
  49. package/dist/layout-bridge/convert/tableConversion.js +4 -1
  50. package/dist/layout-painter/index.js +2 -5
  51. package/dist/paged-layout/editorScrollRoot.d.ts +15 -0
  52. package/dist/paged-layout/editorScrollRoot.js +72 -0
  53. package/dist/paged-layout/scrollToPmPosition.js +8 -22
  54. package/dist/prosemirror/commands/clearParagraphIndent.d.ts +6 -0
  55. package/dist/prosemirror/commands/clearParagraphIndent.js +25 -0
  56. package/dist/prosemirror/commands/comments.js +6 -1
  57. package/dist/prosemirror/commands/propertyChangeScope.js +4 -1
  58. package/dist/prosemirror/commands/resolveAllTableChanges.js +18 -24
  59. package/dist/prosemirror/commands/resolveParagraphProperties.js +13 -2
  60. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +8 -9
  61. package/dist/prosemirror/commands/tableCellMergeResolution.js +28 -25
  62. package/dist/prosemirror/commands/tableMergeFoldDecisions.d.ts +61 -0
  63. package/dist/prosemirror/commands/tableMergeFoldDecisions.js +79 -0
  64. package/dist/prosemirror/commentIdAllocator.d.ts +15 -22
  65. package/dist/prosemirror/commentIdAllocator.js +50 -25
  66. package/dist/prosemirror/conversion/toProseDoc.d.ts +1 -6
  67. package/dist/prosemirror/conversion/toProseDoc.js +2 -117
  68. package/dist/prosemirror/extensions/core/HistoryExtension.js +97 -5
  69. package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -1
  70. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +22 -28
  71. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +8 -6
  72. package/dist/prosemirror/listNumbering.js +14 -14
  73. package/dist/prosemirror/listRendering.d.ts +4 -2
  74. package/dist/prosemirror/listRendering.js +3 -4
  75. package/dist/prosemirror/paragraphPropertyCarry.js +2 -5
  76. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -19
  77. package/dist/prosemirror/plugins/revisionIds.js +110 -29
  78. package/dist/prosemirror/plugins/suggestionMode.d.ts +2 -2
  79. package/dist/prosemirror/plugins/suggestionMode.js +197 -45
  80. package/dist/prosemirror/plugins/templateDirectives.d.ts +6 -0
  81. package/dist/prosemirror/plugins/templateDirectives.js +21 -7
  82. package/dist/prosemirror/plugins/templatePreviewValues.js +14 -21
  83. package/dist/prosemirror/storyListNumbering.js +11 -9
  84. package/dist/prosemirror/textInput.d.ts +6 -1
  85. package/dist/prosemirror/textInput.js +9 -2
  86. package/dist/prosemirror/utils/visualLineNavigation.js +6 -8
  87. package/dist/utils/mergeDocumentContent.d.ts +2 -2
  88. package/dist/utils/mergeDocumentContent.js +11 -8
  89. package/package.json +3 -3
@@ -2,6 +2,7 @@ import { bytesToDataUrl } from "../utils/base64.js";
2
2
  import { compressionRatioLimitFor, countCentralDirectoryRecords, createInflationBudget, exceedsCompressionRatio, getZipEntrySizes, inflateEntryWithinLimits, isStoredZipEntry } from "./archiveInflation.js";
3
3
  import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./encryption/containerFormat.js";
4
4
  import { openDocxBuffer } from "./encryption/openEncryptedDocx.js";
5
+ import { RASTER_MIME_TYPES, detectRasterMimeType } from "./rasterMime.js";
5
6
  import { decodeXmlBytes } from "./xmlEncoding.js";
6
7
  import { FOLIO_XML_RESOURCE_LIMITS, assertXmlResourceLimits, createXmlPackageBudget } from "./xmlResourceLimits.js";
7
8
  import JSZip from "jszip";
@@ -38,12 +39,7 @@ var DocxSecurityError = class extends Error {
38
39
  }
39
40
  };
40
41
  const DEFAULT_ALLOWED_MEDIA_MIME_TYPES = /* @__PURE__ */ new Set([
41
- "image/png",
42
- "image/jpeg",
43
- "image/gif",
44
- "image/bmp",
45
- "image/tiff",
46
- "image/webp",
42
+ ...RASTER_MIME_TYPES,
47
43
  "image/x-emf",
48
44
  "image/x-wmf"
49
45
  ]);
@@ -227,11 +223,12 @@ async function unzipDocx(buffer, options = {}, { verifyUnreadEntries = true } =
227
223
  return null;
228
224
  }
229
225
  const binaryContent = result.bytes.buffer;
230
- if (!isMediaContentAllowed(binaryContent, mimeType)) return null;
226
+ const effectiveMimeType = RASTER_MIME_TYPES.has(mimeType) ? detectRasterMimeType(binaryContent) : mimeType;
227
+ if (!effectiveMimeType || !limits.allowedMediaMimeTypes.has(effectiveMimeType) || !isMediaContentAllowed(binaryContent, effectiveMimeType)) return null;
231
228
  return {
232
229
  type: "media",
233
230
  path,
234
- mimeType,
231
+ mimeType: effectiveMimeType,
235
232
  content: binaryContent
236
233
  };
237
234
  });
@@ -274,6 +271,26 @@ async function unzipDocx(buffer, options = {}, { verifyUnreadEntries = true } =
274
271
  }
275
272
  return content;
276
273
  }
274
+ /** Keep extracted projections and the source ZIP aligned after parser normalization. */
275
+ const replaceRawDocxXmlParts = async (content, parts) => {
276
+ let identitiesChanged = false;
277
+ for (const [path, xmlContent] of parts) {
278
+ if (content.allXml.get(path) === xmlContent) continue;
279
+ indexXmlContent(content, {
280
+ type: "xml",
281
+ path,
282
+ lowerPath: path.toLowerCase(),
283
+ content: xmlContent
284
+ });
285
+ content.originalZip.file(path, xmlContent);
286
+ identitiesChanged = true;
287
+ }
288
+ if (identitiesChanged) content.originalBuffer = await content.originalZip.generateAsync({
289
+ type: "arraybuffer",
290
+ compression: "DEFLATE",
291
+ compressionOptions: { level: 1 }
292
+ });
293
+ };
277
294
  /**
278
295
  * Preflight every XML part the unzip retains, not a named few.
279
296
  *
@@ -291,6 +308,14 @@ function assignXmlContent(content, { path, lowerPath, content: xmlContent }, lim
291
308
  partPath: path,
292
309
  budget
293
310
  });
311
+ indexXmlContent(content, {
312
+ type: "xml",
313
+ path,
314
+ lowerPath,
315
+ content: xmlContent
316
+ });
317
+ }
318
+ const indexXmlContent = (content, { path, lowerPath, content: xmlContent }) => {
294
319
  content.allXml.set(path, xmlContent);
295
320
  if (lowerPath === "word/document.xml") content.documentXml = xmlContent;
296
321
  else if (lowerPath === "word/styles.xml") content.stylesXml = xmlContent;
@@ -317,7 +342,7 @@ function assignXmlContent(content, { path, lowerPath, content: xmlContent }, lim
317
342
  const filename = path.split("/").pop() || path;
318
343
  content.footers.set(filename, xmlContent);
319
344
  }
320
- }
345
+ };
321
346
  function inflationLimitError(limit, path) {
322
347
  switch (limit) {
323
348
  case "declared-size": return new DocxSecurityError(`DOCX entry inflates past its declared size: ${path}`);
@@ -499,14 +524,9 @@ function isEntryTooLarge(declaredSize, maxBytes) {
499
524
  return declaredSize !== null && declaredSize > maxBytes;
500
525
  }
501
526
  function isMediaContentAllowed(data, mimeType) {
527
+ if (RASTER_MIME_TYPES.has(mimeType)) return detectRasterMimeType(data) === mimeType;
502
528
  const bytes = new Uint8Array(data);
503
529
  switch (mimeType) {
504
- case "image/png": return bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
505
- case "image/jpeg": return bytes[0] === 255 && bytes[1] === 216;
506
- case "image/gif": return bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56;
507
- case "image/bmp": return bytes[0] === 66 && bytes[1] === 77;
508
- case "image/webp": return bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80;
509
- case "image/tiff": return bytes[0] === 73 && bytes[1] === 73 || bytes[0] === 77 && bytes[1] === 77;
510
530
  case "image/x-emf":
511
531
  case "image/emf": return bytes.length >= 44 && bytes[0] === 1 && bytes[40] === 32 && bytes[41] === 69 && bytes[42] === 77 && bytes[43] === 70;
512
532
  case "image/x-wmf":
@@ -605,4 +625,4 @@ function getContentSummary(content) {
605
625
  };
606
626
  }
607
627
  //#endregion
608
- export { DocxSecurityError, extractFile, getContentSummary, getEntryUncompressedSize, getFileList, getMediaMimeType, hasFile, mediaToDataUrl, unzipDocx };
628
+ export { DocxSecurityError, extractFile, getContentSummary, getEntryUncompressedSize, getFileList, getMediaMimeType, hasFile, mediaToDataUrl, replaceRawDocxXmlParts, unzipDocx };
@@ -0,0 +1,17 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ //#region src/docx/verticalMergeProjection.d.ts
3
+ /** The reader and revision resolver share which continuation cells remain visible. */
4
+ type RowSpanInfo = {
5
+ rowSpan: number;
6
+ skip: boolean;
7
+ preserveVMergeRestart?: boolean;
8
+ continuationCells?: document_d_exports.TableCell[];
9
+ };
10
+ declare function calculateRowSpans(table: document_d_exports.Table): Map<string, RowSpanInfo>;
11
+ /**
12
+ * Whether a cell carries content a vertical-merge continuation must not hide.
13
+ * The reader keeps such a continuation as a cell of its own.
14
+ */
15
+ declare function tableCellHasMeaningfulContent(cell: document_d_exports.TableCell): boolean;
16
+ //#endregion
17
+ export { RowSpanInfo, calculateRowSpans, tableCellHasMeaningfulContent };
@@ -0,0 +1,120 @@
1
+ import { isCellMergeContinuation } from "./tableParser.js";
2
+ //#region src/docx/verticalMergeProjection.ts
3
+ function calculateRowSpans(table) {
4
+ const result = /* @__PURE__ */ new Map();
5
+ const numRows = table.rows.length;
6
+ const activeMerges = /* @__PURE__ */ new Map();
7
+ for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
8
+ const row = table.rows[rowIndex];
9
+ if (row.cells.length === 0) {
10
+ clearActiveVerticalMerges(activeMerges, result);
11
+ continue;
12
+ }
13
+ let colIndex = row.formatting?.gridBefore ?? 0;
14
+ const rowCells = row.cells.map((cell) => {
15
+ const colspan = cell.formatting?.gridSpan ?? 1;
16
+ const vMerge = cell.formatting?.vMerge;
17
+ const isMergeContinuation = isCellMergeContinuation(cell);
18
+ const startRow = isMergeContinuation ? activeMerges.get(colIndex) : void 0;
19
+ const info = {
20
+ cell,
21
+ colIndex,
22
+ colspan,
23
+ vMerge,
24
+ isMergeContinuation,
25
+ startRow,
26
+ hasMeaningfulContent: tableCellHasMeaningfulContent(cell),
27
+ shouldSkip: isMergeContinuation && startRow !== void 0
28
+ };
29
+ colIndex += colspan;
30
+ return info;
31
+ });
32
+ const rowWouldBeEmpty = rowCells.length > 0 && rowCells.every((cell) => cell.shouldSkip);
33
+ for (const cellInfo of rowCells) {
34
+ const { colIndex: cellColIndex, vMerge, isMergeContinuation, startRow, hasMeaningfulContent } = cellInfo;
35
+ const key = `${rowIndex}-${cellColIndex}`;
36
+ if (vMerge === "restart") {
37
+ closeVerticalMerge(activeMerges, result, cellColIndex);
38
+ activeMerges.set(cellColIndex, rowIndex);
39
+ result.set(key, {
40
+ rowSpan: 1,
41
+ skip: false
42
+ });
43
+ } else if (isMergeContinuation) {
44
+ if (startRow === void 0 || rowWouldBeEmpty || hasMeaningfulContent) {
45
+ result.set(key, {
46
+ rowSpan: 1,
47
+ skip: false
48
+ });
49
+ if ((rowWouldBeEmpty || hasMeaningfulContent) && startRow !== void 0) {
50
+ const restartCell = result.get(`${startRow}-${cellColIndex}`);
51
+ if (restartCell) restartCell.preserveVMergeRestart = true;
52
+ activeMerges.delete(cellColIndex);
53
+ }
54
+ continue;
55
+ }
56
+ const startKey = `${startRow}-${cellColIndex}`;
57
+ const startCell = result.get(startKey);
58
+ if (startCell) {
59
+ startCell.rowSpan++;
60
+ startCell.continuationCells ??= [];
61
+ startCell.continuationCells.push(cellInfo.cell);
62
+ }
63
+ result.set(key, {
64
+ rowSpan: 1,
65
+ skip: true
66
+ });
67
+ } else {
68
+ closeVerticalMerge(activeMerges, result, cellColIndex);
69
+ result.set(key, {
70
+ rowSpan: 1,
71
+ skip: false
72
+ });
73
+ }
74
+ }
75
+ }
76
+ clearActiveVerticalMerges(activeMerges, result);
77
+ return result;
78
+ }
79
+ /**
80
+ * End the vertical merge active in one column.
81
+ *
82
+ * A `w:vMerge="restart"` is carried through the editor by its cell's rowspan,
83
+ * which only exists once a continuation joins it. A merge that closes with a
84
+ * rowspan of 1 — a restart with no continuation, one interrupted by a plain
85
+ * cell, one whose continuation is in a row a revision removed — has nothing
86
+ * but this flag to say the cell was a merge origin, and dropping it changes
87
+ * the table's visible structure.
88
+ */
89
+ function closeVerticalMerge(activeMerges, result, colIndex) {
90
+ const startRow = activeMerges.get(colIndex);
91
+ if (startRow === void 0) return;
92
+ const restartCell = result.get(`${startRow}-${colIndex}`);
93
+ if (restartCell && restartCell.rowSpan === 1) restartCell.preserveVMergeRestart = true;
94
+ activeMerges.delete(colIndex);
95
+ }
96
+ function clearActiveVerticalMerges(activeMerges, result) {
97
+ for (const colIndex of [...activeMerges.keys()]) closeVerticalMerge(activeMerges, result, colIndex);
98
+ }
99
+ /**
100
+ * Whether a cell carries content a vertical-merge continuation must not hide.
101
+ * The reader keeps such a continuation as a cell of its own.
102
+ */
103
+ function tableCellHasMeaningfulContent(cell) {
104
+ return cell.content.some(blockHasMeaningfulContent);
105
+ }
106
+ function blockHasMeaningfulContent(block) {
107
+ if (block.type === "preservedBlock") return true;
108
+ if (block.type === "bookmarkStart" || block.type === "bookmarkEnd") return true;
109
+ if (block.type === "table") return block.rows.some((row) => row.cells.some((cell) => tableCellHasMeaningfulContent(cell)));
110
+ if (block.type === "blockSdt" || block.type === "blockCustomXml") return true;
111
+ return block.content.some(paragraphContentHasMeaningfulContent);
112
+ }
113
+ function paragraphContentHasMeaningfulContent(content) {
114
+ if (content.type === "run") return content.content.length > 0;
115
+ if (content.type === "hyperlink") return content.children.some(paragraphContentHasMeaningfulContent);
116
+ if (content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo") return content.content.some(paragraphContentHasMeaningfulContent);
117
+ return true;
118
+ }
119
+ //#endregion
120
+ export { calculateRowSpans, tableCellHasMeaningfulContent };
@@ -0,0 +1,6 @@
1
+ import { Node } from "prosemirror-model";
2
+ //#region src/layout-bridge/convert/fixedTableColumnWidths.d.ts
3
+ /** Resolve absolute cell preferences for an unbounded fixed-layout grid (§17.4.53). */
4
+ declare const fixedTableColumnWidths: (table: Node, grid: readonly number[]) => readonly number[];
5
+ //#endregion
6
+ export { fixedTableColumnWidths };
@@ -0,0 +1,23 @@
1
+ import { expectTableCellAttrs, expectTableRowAttrs } from "../../prosemirror/attrs/index.js";
2
+ //#region src/layout-bridge/convert/fixedTableColumnWidths.ts
3
+ /** Resolve absolute cell preferences for an unbounded fixed-layout grid (§17.4.53). */
4
+ const fixedTableColumnWidths = (table, grid) => {
5
+ const preferred = /* @__PURE__ */ new Map();
6
+ for (let rowIndex = 0; rowIndex < table.childCount; rowIndex++) {
7
+ const row = table.child(rowIndex);
8
+ const rowAttrs = expectTableRowAttrs(row);
9
+ if (rowAttrs.trIns || rowAttrs.trDel || rowAttrs.hidden) return grid;
10
+ let column = Math.max(0, rowAttrs._originalFormatting?.gridBefore ?? 0);
11
+ for (let cellIndex = 0; cellIndex < row.childCount; cellIndex++) {
12
+ const attrs = expectTableCellAttrs(row.child(cellIndex));
13
+ if (attrs._omittedGridSlot) continue;
14
+ const width = attrs._authoredWidth;
15
+ if (attrs.colspan !== 1 || attrs.rowspan !== 1 || attrs._preserveVMergeRestart || attrs._originalFormatting?.vMerge !== void 0 || !width || width.type !== "dxa" || !Number.isFinite(width.value) || width.value <= 0) return grid;
16
+ if (column < grid.length) preferred.set(column, Math.max(preferred.get(column) ?? 0, width.value));
17
+ column++;
18
+ }
19
+ }
20
+ return grid.map((width, column) => preferred.get(column) ?? width);
21
+ };
22
+ //#endregion
23
+ export { fixedTableColumnWidths };
@@ -3,6 +3,7 @@ import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
3
3
  import { expectParagraphAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs } from "../../prosemirror/attrs/index.js";
4
4
  import { textBoxHostParagraph } from "../../prosemirror/textBoxHostParagraph.js";
5
5
  import { normalizeShapeTextAnchor } from "../../types/documentEnumValues.js";
6
+ import { fixedTableColumnWidths } from "./fixedTableColumnWidths.js";
6
7
  import { convertBorderSpecToLayout } from "./flowBorders.js";
7
8
  import { nextBlockId, twipsToPixels } from "./flowConversionShared.js";
8
9
  import { countPageBreakRuns, hasSingleLeadingProjectedPageBreak } from "./pageBreakSplitting.js";
@@ -183,7 +184,9 @@ function convertTable(node, startPos, options) {
183
184
  if (child.type.name === "tableRow") rows.push(convertTableRow(child, offset, options, rowContext));
184
185
  offset += child.nodeSize;
185
186
  });
186
- let columnWidths = attrs.columnWidths?.map(twipsToPixels);
187
+ const columnWidthsTwips = attrs.columnWidths;
188
+ const hasTableWidth = (attrs.widthType === "dxa" || attrs.widthType === "pct") && (attrs.width ?? 0) > 0;
189
+ let columnWidths = (columnWidthsTwips && attrs._originalFormatting?.layout === "fixed" && !hasTableWidth ? fixedTableColumnWidths(node, columnWidthsTwips) : columnWidthsTwips)?.map(twipsToPixels);
187
190
  const width = attrs.width;
188
191
  const widthType = attrs.widthType;
189
192
  if (!columnWidths && rows.length > 0) {
@@ -1,4 +1,4 @@
1
- import { prefersReducedMotionBehavior } from "../paged-layout/scrollNavigation.js";
1
+ import { scrollEditorElementIntoView } from "../paged-layout/editorScrollRoot.js";
2
2
  import { createDefaultRegistry } from "./registry/modules.js";
3
3
  import { createFeatureRegistry } from "./registry/registry.js";
4
4
  import { FRAGMENT_CLASS_NAMES, renderFragment } from "./renderFragment.js";
@@ -167,10 +167,7 @@ var LayoutPainter = class {
167
167
  */
168
168
  scrollToPage(pageNumber) {
169
169
  const state = this.pageStates.find((s) => s.pageNumber === pageNumber);
170
- if (state?.element) state.element.scrollIntoView({
171
- behavior: prefersReducedMotionBehavior(),
172
- block: "start"
173
- });
170
+ if (state?.element) scrollEditorElementIntoView(state.element, { block: "start" });
174
171
  }
175
172
  };
176
173
  /**
@@ -0,0 +1,15 @@
1
+ //#region src/paged-layout/editorScrollRoot.d.ts
2
+ declare const getEditorScrollRoot: (element: HTMLElement | null) => HTMLElement | null;
3
+ /** Resolve the root even when a caller holds a painted descendant or viewport. */
4
+ declare const scrollEditorTo: (element: HTMLElement | null, options: ScrollToOptions) => void;
5
+ declare const scrollEditorBy: (element: HTMLElement | null, delta: number) => void;
6
+ type ScrollEditorElementOptions = {
7
+ block?: "start" | "center" | "end" | "nearest";
8
+ behavior?: ScrollBehavior;
9
+ /** Space to leave between the target and the visible root edge. */
10
+ margin?: number;
11
+ };
12
+ /** Scroll only the marked root, including when the target is inside a scaled viewport. */
13
+ declare const scrollEditorElementIntoView: (element: HTMLElement | null, { block, behavior, margin }?: ScrollEditorElementOptions) => void;
14
+ //#endregion
15
+ export { getEditorScrollRoot, scrollEditorBy, scrollEditorElementIntoView, scrollEditorTo };
@@ -0,0 +1,72 @@
1
+ import { prefersReducedMotionBehavior } from "./scrollNavigation.js";
2
+ //#region src/paged-layout/editorScrollRoot.ts
3
+ /** The marked editor owns scrolling; navigation must never move host ancestors. */
4
+ const EDITOR_SCROLL_ROOT_SELECTOR = "[data-folio-scroll]";
5
+ const getEditorScrollRoot = (element) => element?.closest(EDITOR_SCROLL_ROOT_SELECTOR) ?? null;
6
+ /** Resolve the root even when a caller holds a painted descendant or viewport. */
7
+ const scrollEditorTo = (element, options) => {
8
+ getEditorScrollRoot(element)?.scrollTo(options);
9
+ };
10
+ const scrollEditorBy = (element, delta) => {
11
+ const root = getEditorScrollRoot(element);
12
+ if (root) scrollEditorTo(root, {
13
+ top: root.scrollTop + delta,
14
+ behavior: "instant"
15
+ });
16
+ };
17
+ const nearestScrollOffset = ({ targetStart, targetEnd, viewportStart, viewportEnd }) => {
18
+ const above = targetStart < viewportStart;
19
+ const below = targetEnd > viewportEnd;
20
+ if (above && below) return 0;
21
+ const targetSize = targetEnd - targetStart;
22
+ const viewportSize = viewportEnd - viewportStart;
23
+ if (above && targetSize <= viewportSize || below && targetSize > viewportSize) return targetStart - viewportStart;
24
+ if (below && targetSize <= viewportSize || above && targetSize > viewportSize) return targetEnd - viewportEnd;
25
+ return 0;
26
+ };
27
+ /** Scroll only the marked root, including when the target is inside a scaled viewport. */
28
+ const scrollEditorElementIntoView = (element, { block = "center", behavior = prefersReducedMotionBehavior(), margin = 0 } = {}) => {
29
+ const root = getEditorScrollRoot(element);
30
+ if (!root || !element) return;
31
+ const targetRect = element.getBoundingClientRect();
32
+ const rootRect = root.getBoundingClientRect();
33
+ const visibleTop = rootRect.top + root.clientTop;
34
+ const targetTop = targetRect.top - visibleTop + root.scrollTop;
35
+ const targetBottom = targetRect.bottom - visibleTop + root.scrollTop;
36
+ let top;
37
+ switch (block) {
38
+ case "start":
39
+ top = targetTop - margin;
40
+ break;
41
+ case "center":
42
+ top = targetTop - (root.clientHeight - targetRect.height) / 2;
43
+ break;
44
+ case "end":
45
+ top = targetBottom - root.clientHeight + margin;
46
+ break;
47
+ case "nearest":
48
+ top = root.scrollTop + nearestScrollOffset({
49
+ targetStart: targetTop,
50
+ targetEnd: targetBottom,
51
+ viewportStart: root.scrollTop + margin,
52
+ viewportEnd: root.scrollTop + root.clientHeight - margin
53
+ });
54
+ break;
55
+ default: return block;
56
+ }
57
+ const visibleLeft = rootRect.left + root.clientLeft;
58
+ const left = root.scrollLeft + nearestScrollOffset({
59
+ targetStart: targetRect.left,
60
+ targetEnd: targetRect.right,
61
+ viewportStart: visibleLeft,
62
+ viewportEnd: visibleLeft + root.clientWidth
63
+ });
64
+ if (top === root.scrollTop && left === root.scrollLeft) return;
65
+ scrollEditorTo(root, {
66
+ top: Math.max(0, top),
67
+ left: Math.max(0, left),
68
+ behavior
69
+ });
70
+ };
71
+ //#endregion
72
+ export { getEditorScrollRoot, scrollEditorBy, scrollEditorElementIntoView, scrollEditorTo };
@@ -1,6 +1,7 @@
1
1
  import { findBodyPmAnchor, findBodyPmAnchors } from "../layout-bridge/dom/findBodyPmSpans.js";
2
2
  import { findPageShellForPmPos } from "../layout-painter/renderPage.js";
3
- import { isValidPmScrollPosition, prefersReducedMotionBehavior } from "./scrollNavigation.js";
3
+ import { getEditorScrollRoot, scrollEditorElementIntoView } from "./editorScrollRoot.js";
4
+ import { isValidPmScrollPosition } from "./scrollNavigation.js";
4
5
  //#region src/paged-layout/scrollToPmPosition.ts
5
6
  /** Class on the pages container `PagedEditor` paints into. */
6
7
  const PAGES_CONTAINER_CLASS = "paged-editor__pages";
@@ -9,10 +10,7 @@ const scrollPagesToPmPosition = (pageContainer, pmPos) => {
9
10
  if (!isValidPmScrollPosition(pmPos)) return;
10
11
  const exact = findBodyPmAnchor(pageContainer, pmPos);
11
12
  if (exact) {
12
- exact.scrollIntoView({
13
- behavior: prefersReducedMotionBehavior(),
14
- block: "center"
15
- });
13
+ scrollEditorElementIntoView(exact);
16
14
  return;
17
15
  }
18
16
  let runMatch = null;
@@ -27,28 +25,19 @@ const scrollPagesToPmPosition = (pageContainer, pmPos) => {
27
25
  }
28
26
  }
29
27
  if (runMatch) {
30
- runMatch.scrollIntoView({
31
- behavior: prefersReducedMotionBehavior(),
32
- block: "center"
33
- });
28
+ scrollEditorElementIntoView(runMatch);
34
29
  return;
35
30
  }
36
31
  const shellHit = findPageShellForPmPos(pageContainer, pmPos);
37
32
  if (!shellHit) return;
38
33
  const { element: shell } = shellHit;
39
- shell.scrollIntoView({
40
- behavior: prefersReducedMotionBehavior(),
41
- block: "center"
42
- });
34
+ scrollEditorElementIntoView(shell);
43
35
  let attempts = 0;
44
36
  const refine = () => {
45
37
  attempts++;
46
38
  const exactInShell = findBodyPmAnchor(shell, pmPos);
47
39
  if (exactInShell) {
48
- exactInShell.scrollIntoView({
49
- behavior: prefersReducedMotionBehavior(),
50
- block: "center"
51
- });
40
+ scrollEditorElementIntoView(exactInShell);
52
41
  return;
53
42
  }
54
43
  let bestEl = null;
@@ -68,10 +57,7 @@ const scrollPagesToPmPosition = (pageContainer, pmPos) => {
68
57
  }
69
58
  }
70
59
  if (bestEl) {
71
- bestEl.scrollIntoView({
72
- behavior: prefersReducedMotionBehavior(),
73
- block: "center"
74
- });
60
+ scrollEditorElementIntoView(bestEl);
75
61
  return;
76
62
  }
77
63
  if (attempts < 20) requestAnimationFrame(refine);
@@ -86,7 +72,7 @@ const scrollPagesToPmPosition = (pageContainer, pmPos) => {
86
72
  * callers can fall back to coordinate-based scrolling.
87
73
  */
88
74
  const scrollFolioPositionIntoView = (view, pmPos) => {
89
- const pageContainer = view.dom.closest("[data-folio-scroll]")?.querySelector(`.paged-editor__pages`) ?? null;
75
+ const pageContainer = getEditorScrollRoot(view.dom)?.querySelector(`.paged-editor__pages`) ?? null;
90
76
  if (!pageContainer) return false;
91
77
  scrollPagesToPmPosition(pageContainer, pmPos);
92
78
  return true;
@@ -0,0 +1,6 @@
1
+ import { Command } from "prosemirror-state";
2
+ //#region src/prosemirror/commands/clearParagraphIndent.d.ts
3
+ /** Clear indentation at paragraph start before Backspace joins paragraphs. */
4
+ declare const clearIndentOnBackspace: Command;
5
+ //#endregion
6
+ export { clearIndentOnBackspace };
@@ -0,0 +1,25 @@
1
+ import { expectParagraphAttrs } from "../attrs/index.js";
2
+ import { TextSelection } from "prosemirror-state";
3
+ //#region src/prosemirror/commands/clearParagraphIndent.ts
4
+ /** Clear indentation at paragraph start before Backspace joins paragraphs. */
5
+ const clearIndentOnBackspace = (state, dispatch) => {
6
+ if (!(state.selection instanceof TextSelection)) return false;
7
+ const { $cursor } = state.selection;
8
+ if (!$cursor || $cursor.parentOffset !== 0 || $cursor.parent.type.name !== "paragraph") return false;
9
+ const attrs = expectParagraphAttrs($cursor.parent);
10
+ const hasFirstLine = typeof attrs.indentFirstLine === "number" && attrs.indentFirstLine > 0;
11
+ const hasIndentLeft = typeof attrs.indentLeft === "number" && attrs.indentLeft > 0;
12
+ if (!hasFirstLine && !attrs.hangingIndent && !hasIndentLeft) return false;
13
+ if (dispatch) {
14
+ const position = $cursor.before();
15
+ dispatch(state.tr.setNodeMarkup(position, void 0, {
16
+ ...attrs,
17
+ indentFirstLine: null,
18
+ hangingIndent: null,
19
+ indentLeft: null
20
+ }).scrollIntoView());
21
+ }
22
+ return true;
23
+ };
24
+ //#endregion
25
+ export { clearIndentOnBackspace };
@@ -287,7 +287,12 @@ function resolveChange(from, to, mode, revisionIds) {
287
287
  const cell = tr.doc.nodeAt(mappedPos);
288
288
  if (!cell || cell.type.name !== "tableCell" && cell.type.name !== "tableHeader") continue;
289
289
  if (op.type === "merge") {
290
- const resolved = op.source === "collapsed" ? resolveCollapsedTableCellMerge(tr, mappedPos, op.mode, op.revisionSet) : resolveVisibleTableCellMerge(tr, mappedPos, op.mode);
290
+ const resolved = op.source === "collapsed" ? resolveCollapsedTableCellMerge(tr, mappedPos, op.mode, op.revisionSet) : resolveVisibleTableCellMerge({
291
+ tr,
292
+ cellPos: mappedPos,
293
+ mode: op.mode,
294
+ revisionSet: op.revisionSet
295
+ });
291
296
  if (!resolved) {
292
297
  failedTableCellMergeResolution = true;
293
298
  break;
@@ -1,3 +1,4 @@
1
+ import { isStyleSourcedParagraphNumbering } from "../../internal/paragraphFormattingSerialization.js";
1
2
  import { expectParagraphAttrs } from "../attrs/index.js";
2
3
  import { readParagraphNumberingAttr } from "../numberingAttr.js";
3
4
  import { directParagraphAlignment } from "../paragraphAlignment.js";
@@ -126,7 +127,9 @@ const paragraphPropertiesSnapshot = (node) => {
126
127
  const attrs = expectParagraphAttrs(node);
127
128
  const snapshot = {};
128
129
  for (const key of PPR_CHANGE_SCOPED_ATTR_KEYS) {
129
- if (key === "alignment" || PPR_SPACING_ATTR_KEYS.has(key)) continue;
130
+ if (key === "alignment" || PPR_SPACING_ATTR_KEYS.has(key) || key === "numPr" && isStyleSourcedParagraphNumbering(attrs.numPr, attrs.numPrFromStyle)) continue;
131
+ if (styleSuppliedParagraphValue(attrs, key)) continue;
132
+ if (key === "hangingIndent" && styleSuppliedParagraphValue(attrs, "indentFirstLine")) continue;
130
133
  if (styleSuppliedParagraphValue(attrs, key)) continue;
131
134
  if (key === "hangingIndent" && styleSuppliedParagraphValue(attrs, "indentFirstLine")) continue;
132
135
  const value = attrs[key];
@@ -5,7 +5,8 @@ import { nodePropertyRevisionSites, propertyRevisionRecords } from "../revisionC
5
5
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
6
6
  import { isTableCellRetainedInReviewView } from "../tableCellRevisionVisibility.js";
7
7
  import { resolveAllNodePropertyChangeAttrs } from "./resolveNodePropertyChangeAttrs.js";
8
- import { acceptedMergeFoldsIntoCellAbove, createRestoredTableCell, hasMatchingCollapsedTableCellMerge, tableCellContinuationCells, tableCellContinuationFromNode, tableCellContinuationPayload } from "./tableCellMergeResolution.js";
8
+ import { createRestoredTableCell, hasMatchingCollapsedTableCellMerge, tableCellContinuationCells, tableCellContinuationFromNode, tableCellContinuationPayload } from "./tableCellMergeResolution.js";
9
+ import { resolvedTableMergeOriginAttrs, resolvedVisibleTableCellMergeAttrs, tableMergeFoldDecisions } from "./tableMergeFoldDecisions.js";
9
10
  import { panic } from "better-result";
10
11
  import { TableMap } from "prosemirror-tables";
11
12
  import { StepMap } from "prosemirror-transform";
@@ -358,6 +359,12 @@ const compactColumnWidths = (widths, removals) => {
358
359
  /** Resolve all cell edits in reverse order against one grid and mutable slots. */
359
360
  const resolvePureTableMerges = ({ table, mode, rowMetadata, tablePropertyTouched, sourceTable }) => {
360
361
  const map = TableMap.get(table);
362
+ const mergeDecisions = tableMergeFoldDecisions({
363
+ table,
364
+ mode,
365
+ revisionSet: null
366
+ });
367
+ if (mergeDecisions.invalid.size > 0) return failedTableResolution(table);
361
368
  const cellRectangle = indexedTableCells(map);
362
369
  const rows = Array.from({ length: table.childCount }, (_, index) => table.child(index));
363
370
  const rowCells = rows.map(() => /* @__PURE__ */ new Map());
@@ -383,7 +390,7 @@ const resolvePureTableMerges = ({ table, mode, rowMetadata, tablePropertyTouched
383
390
  for (let coveredRow = rectangle.top; coveredRow < rectangle.bottom; coveredRow++) rowOccupancy[coveredRow] = elementAt(rowOccupancy, coveredRow) + (rectangle.right - rectangle.left);
384
391
  const initial = elementAt(elementAt(rowMetadata, rowIndex).cells, cellIndex);
385
392
  const entry = {
386
- node,
393
+ node: mergeDecisions.origins.has(cellOffset) ? node.type.create(resolvedTableMergeOriginAttrs(node), node.content, node.marks) : node,
387
394
  position: cellOffset,
388
395
  left,
389
396
  row: rowIndex,
@@ -438,28 +445,15 @@ const resolvePureTableMerges = ({ table, mode, rowMetadata, tablePropertyTouched
438
445
  });
439
446
  }
440
447
  } else if (marker?.kind === "merge") {
441
- const joinsAbove = mode === "accept" ? isTableCellMergeRevisionContinuation(marker.verticalMerge) && acceptedMergeFoldsIntoCellAbove(entry.node) : isTableCellMergeRevisionContinuation(marker.verticalMergeOriginal);
442
- if (mode === "accept" && !joinsAbove) {
443
- entry.touched = true;
444
- entry.node = entry.node.type.create({
445
- ...entry.node.attrs,
446
- cellMarker: null
447
- }, entry.node.content, entry.node.marks);
448
- } else if (!joinsAbove) {
449
- entry.touched = true;
450
- const original = entry.node.attrs["_originalFormatting"];
451
- let formatting = original;
452
- if (typeof original === "object" && original !== null) {
453
- const copy = { ...original };
454
- delete copy["vMerge"];
455
- formatting = copy;
456
- }
457
- entry.node = entry.node.type.create({
458
- ...entry.node.attrs,
459
- cellMarker: null,
460
- _originalFormatting: formatting
461
- }, entry.node.content, entry.node.marks);
462
- } else {
448
+ const joinsAbove = mergeDecisions.folds.get(entry.position);
449
+ if (joinsAbove === void 0) return panic("Merge resolution lost its cell");
450
+ if (mode === "reject" && isTableCellMergeRevisionContinuation(marker.verticalMergeOriginal) && cellRectangle(entry.position).top === 0) return failedTableResolution(table);
451
+ if (!joinsAbove) entry.node = entry.node.type.create(resolvedVisibleTableCellMergeAttrs({
452
+ cell: entry.node,
453
+ mode,
454
+ origin: mergeDecisions.origins.has(entry.position)
455
+ }), entry.node.content, entry.node.marks);
456
+ else {
463
457
  const rectangle = cellRectangle(entry.position);
464
458
  if (rectangle.top === 0) return failedTableResolution(table);
465
459
  const abovePosition = map.map[(rectangle.top - 1) * map.width + rectangle.left];
@@ -1,5 +1,7 @@
1
+ import { resolveParagraphNumbering } from "../../docx/numberingReference.js";
1
2
  import { expectParagraphAttrs } from "../attrs/index.js";
2
3
  import { rejectedListRenderingPatch } from "../listRendering.js";
4
+ import { paragraphNumberingAttr } from "../numberingAttr.js";
3
5
  import { styleResolvedParagraphFormatting } from "../paragraphFormattingProvenance.js";
4
6
  import { resolveParagraphDefaultTextFormatting } from "../styles/paragraphStyleCascade.js";
5
7
  import { paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, removeParagraphPropertyChanges, sectionRejectProperties } from "./propertyChangeScope.js";
@@ -38,11 +40,20 @@ const resolveParagraphChangeAttrs = ({ node, boundaryCovered, mode, revisionSet,
38
40
  ...previousFormattingFromStyle,
39
41
  ...resolvedFromStyle
40
42
  };
43
+ let styleNumbering = previousFormattingFromStyle?.numPr;
44
+ if (!styleResolver && sameStyle) styleNumbering = currentAttrs.numPrFromStyle ?? void 0;
45
+ const inheritedNumbering = styleNumbering == null ? null : paragraphNumberingAttr(resolveParagraphNumbering(styleNumbering));
46
+ const recordedNumbering = rejection.previousFormatting?.numPr;
47
+ const restoredNumbering = recordedNumbering ?? inheritedNumbering;
41
48
  Object.assign(nextAttrs, paragraphRejectAttrPatch(rejection.previousFormatting, inheritedFormatting), rejectedListRenderingPatch({
42
49
  current: expectParagraphAttrs(node),
43
50
  previousFormatting: rejection.previousFormatting,
44
- numbering
45
- }));
51
+ numbering,
52
+ restoredNumbering
53
+ }), {
54
+ numPr: restoredNumbering,
55
+ numPrFromStyle: recordedNumbering == null ? inheritedNumbering : null
56
+ });
46
57
  const restoredFormatting = paragraphRejectOriginalFormatting(rejection.previousFormatting, node.attrs["_originalFormatting"]);
47
58
  nextAttrs["_originalFormatting"] = restoredFormatting;
48
59
  if (styleResolver) nextAttrs["defaultTextFormatting"] = resolveParagraphDefaultTextFormatting(rejection.previousFormatting?.styleId, restoredFormatting ?? void 0, styleResolver) ?? null;