@stll/folio-core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +39 -0
  2. package/dist/ai-edits/apply.js +18 -4
  3. package/dist/ai-edits/headless.d.ts +31 -4
  4. package/dist/ai-edits/headless.js +148 -35
  5. package/dist/ai-edits/index.d.ts +3 -3
  6. package/dist/ai-edits/index.js +2 -1
  7. package/dist/ai-edits/snapshot.js +40 -3
  8. package/dist/ai-edits/types.d.ts +2 -1
  9. package/dist/compat/eigenpal.d.ts +6 -4
  10. package/dist/compat/eigenpal.js +5 -3
  11. package/dist/controller/latestRequestGate.d.ts +13 -0
  12. package/dist/controller/latestRequestGate.js +16 -0
  13. package/dist/controller/layoutPipeline.js +12 -1
  14. package/dist/document-operations.d.ts +10 -3
  15. package/dist/document-operations.js +29 -14
  16. package/dist/docx/blockContentParser.js +2 -2
  17. package/dist/docx/commentParser.js +1 -1
  18. package/dist/docx/compatibility.d.ts +3 -7
  19. package/dist/docx/compatibility.js +0 -11
  20. package/dist/docx/encryption/agileDecryption.js +1 -1
  21. package/dist/docx/encryption/encryptionInfo.js +1 -1
  22. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  23. package/dist/docx/fontTableParser.d.ts +6 -0
  24. package/dist/docx/fontTableParser.js +72 -0
  25. package/dist/docx/groupDrawingParser.js +7 -2
  26. package/dist/docx/headerFooterParser.js +1 -1
  27. package/dist/docx/normalizeBaseDirection.js +1 -1
  28. package/dist/docx/numberingParser.js +1 -0
  29. package/dist/docx/paragraphParser.js +12 -1
  30. package/dist/docx/parser.d.ts +1 -1
  31. package/dist/docx/parser.js +12 -9
  32. package/dist/docx/rezip.js +70 -5
  33. package/dist/docx/runConsolidator.js +4 -0
  34. package/dist/docx/runParser.js +19 -1
  35. package/dist/docx/selectiveSave.js +3 -3
  36. package/dist/docx/serializer/fontTableSerializer.d.ts +6 -0
  37. package/dist/docx/serializer/fontTableSerializer.js +26 -0
  38. package/dist/docx/serializer/headerFooterSerializer.js +1 -1
  39. package/dist/docx/serializer/paragraphSerializer.js +4 -0
  40. package/dist/docx/serializer/runSerializer.js +7 -0
  41. package/dist/docx/serializer/settingsSerializer.d.ts +6 -0
  42. package/dist/docx/serializer/settingsSerializer.js +16 -0
  43. package/dist/docx/serializer/stylesSerializer.d.ts +6 -0
  44. package/dist/docx/serializer/stylesSerializer.js +56 -0
  45. package/dist/docx/serializer/tableSerializer.js +25 -26
  46. package/dist/docx/serializer/themeSerializer.d.ts +6 -0
  47. package/dist/docx/serializer/themeSerializer.js +36 -0
  48. package/dist/docx/settingsParser.js +46 -0
  49. package/dist/docx/styleParser.js +26 -1
  50. package/dist/docx/tableParser.js +11 -1
  51. package/dist/docx/vmlImageParser.js +1 -0
  52. package/dist/i18n/messages/catalogs.gen.d.ts +5916 -5916
  53. package/dist/i18n/messages/catalogs.gen.js +5910 -5910
  54. package/dist/index.d.ts +6 -4
  55. package/dist/index.js +5 -3
  56. package/dist/layout-bridge/convert/footnoteLayout.d.ts +3 -0
  57. package/dist/layout-bridge/convert/footnoteLayout.js +3 -1
  58. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +3 -0
  59. package/dist/layout-bridge/convert/headerFooterLayout.js +6 -1
  60. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +14 -2
  61. package/dist/layout-bridge/convert/toFlowBlocks.js +51 -14
  62. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  63. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  64. package/dist/layout-engine/index.js +36 -28
  65. package/dist/layout-engine/measure/cache.js +11 -3
  66. package/dist/layout-engine/measure/font-metrics.worker.js +1 -0
  67. package/dist/layout-engine/measure/index.d.ts +2 -1
  68. package/dist/layout-engine/measure/index.js +2 -1
  69. package/dist/layout-engine/measure/lineBreakProvider.d.ts +33 -0
  70. package/dist/layout-engine/measure/lineBreakProvider.js +252 -0
  71. package/dist/layout-engine/measure/lineBreaks.d.ts +7 -2
  72. package/dist/layout-engine/measure/lineBreaks.js +11 -18
  73. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  74. package/dist/layout-engine/measure/listMarkerWidth.js +12 -6
  75. package/dist/layout-engine/measure/measureBlocks.d.ts +1 -2
  76. package/dist/layout-engine/measure/measureBlocks.js +6 -36
  77. package/dist/layout-engine/measure/measureContainer.js +32 -9
  78. package/dist/layout-engine/measure/measureHelpers.js +5 -2
  79. package/dist/layout-engine/measure/measureParagraph.js +335 -72
  80. package/dist/layout-engine/measure/measureTypes.d.ts +2 -1
  81. package/dist/layout-engine/measure/measureWorker.d.ts +2 -2
  82. package/dist/layout-engine/measure/measureWorker.js +8 -19
  83. package/dist/layout-engine/measure/measureWorkerProtocol.d.ts +3 -14
  84. package/dist/layout-engine/measure/tableCellFloating.d.ts +10 -3
  85. package/dist/layout-engine/measure/tableCellFloating.js +37 -29
  86. package/dist/layout-engine/measure/tableCellFlow.d.ts +27 -0
  87. package/dist/layout-engine/measure/tableCellFlow.js +73 -0
  88. package/dist/layout-engine/measure/tableCellGrid.js +1 -1
  89. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +18 -0
  90. package/dist/layout-engine/measure/textMeasurementPolicy.js +24 -0
  91. package/dist/layout-engine/renderedBreakReconciliation.d.ts +6 -1
  92. package/dist/layout-engine/renderedBreakReconciliation.js +34 -11
  93. package/dist/layout-engine/tableRowBreak.d.ts +5 -2
  94. package/dist/layout-engine/tableRowBreak.js +62 -29
  95. package/dist/layout-engine/types.d.ts +40 -7
  96. package/dist/layout-painter/anchoredImagePosition.d.ts +23 -0
  97. package/dist/layout-painter/anchoredImagePosition.js +95 -0
  98. package/dist/layout-painter/borderStroke.d.ts +23 -0
  99. package/dist/layout-painter/borderStroke.js +39 -0
  100. package/dist/layout-painter/renderImage.d.ts +5 -4
  101. package/dist/layout-painter/renderImage.js +18 -4
  102. package/dist/layout-painter/renderPage.d.ts +2 -27
  103. package/dist/layout-painter/renderPage.js +13 -99
  104. package/dist/layout-painter/renderParagraph.js +41 -23
  105. package/dist/layout-painter/renderTable.d.ts +2 -0
  106. package/dist/layout-painter/renderTable.js +223 -43
  107. package/dist/managers/DocumentLoaderManager.d.ts +1 -1
  108. package/dist/managers/DocumentLoaderManager.js +2 -2
  109. package/dist/prosemirror/attrs/index.d.ts +4 -2
  110. package/dist/prosemirror/attrs/index.js +21 -2
  111. package/dist/prosemirror/commands/propertyChangeScope.js +1 -1
  112. package/dist/prosemirror/conversion/fromProseDoc.js +33 -18
  113. package/dist/prosemirror/conversion/toProseDoc.js +50 -18
  114. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  115. package/dist/prosemirror/extensions/core/ParagraphExtension.js +7 -2
  116. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +1 -1
  117. package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
  118. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +1 -0
  119. package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
  120. package/dist/prosemirror/extensions/marks/LanguageExtension.d.ts +7 -0
  121. package/dist/prosemirror/extensions/marks/LanguageExtension.js +30 -0
  122. package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
  123. package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
  124. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +1 -1
  125. package/dist/prosemirror/extensions/marks/markUtils.js +12 -0
  126. package/dist/prosemirror/extensions/nodes/ImageExtension.js +2 -1
  127. package/dist/prosemirror/extensions/nodes/TableExtension.js +4 -2
  128. package/dist/prosemirror/schema/index.d.ts +2 -2
  129. package/dist/prosemirror/schema/marks.d.ts +6 -1
  130. package/dist/prosemirror/schema/nodes.d.ts +13 -11
  131. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  132. package/dist/prosemirror/utils/tabCalculator.d.ts +3 -1
  133. package/dist/prosemirror/utils/tabCalculator.js +12 -10
  134. package/dist/prosemirror/validation.js +4 -1
  135. package/dist/redline.js +13 -3
  136. package/dist/server.d.ts +6 -3
  137. package/dist/server.js +5 -2
  138. package/dist/style-sets/extract.d.ts +35 -0
  139. package/dist/style-sets/extract.js +123 -0
  140. package/dist/style-sets/stellaStyle.d.ts +13 -0
  141. package/dist/style-sets/stellaStyle.js +516 -0
  142. package/dist/style-sets/types.d.ts +31 -0
  143. package/dist/style-sets/types.js +5 -0
  144. package/dist/utils/createDocument.d.ts +12 -2
  145. package/dist/utils/createDocument.js +42 -31
  146. package/dist/utils/fontResolver.js +6 -6
  147. package/dist/utils/formatToStyle.js +1 -1
  148. package/dist/utils/paragraphFormattingMerge.d.ts +14 -3
  149. package/dist/utils/paragraphFormattingMerge.js +14 -4
  150. package/package.json +5 -1
  151. package/dist/docx/capabilities.d.ts +0 -41
  152. package/dist/docx/capabilities.js +0 -322
@@ -142,16 +142,10 @@ function flush(current) {
142
142
  const entries = [];
143
143
  for (const entry of current.pending.values()) {
144
144
  if (entries.length >= MAX_BATCH_SIZE) break;
145
- current.pending.delete(makeKey(entry.text, entry.fontCacheKey, entry.letterSpacing));
146
- if (entry.cacheGeneration !== cacheGeneration) continue;
147
- entries.push({
148
- text: entry.text,
149
- font: entry.font,
150
- fontCacheKey: entry.fontCacheKey,
151
- fontFingerprintWidth: entry.fontFingerprintWidth,
152
- letterSpacing: entry.letterSpacing,
153
- horizontalScale: entry.horizontalScale
154
- });
145
+ const { cacheGeneration: entryGeneration, ...request } = entry;
146
+ current.pending.delete(makeKey(request.text, request.fontCacheKey, request.letterSpacing));
147
+ if (entryGeneration !== cacheGeneration) continue;
148
+ entries.push(request);
155
149
  }
156
150
  if (entries.length === 0) {
157
151
  if (current.pending.size > 0) scheduleFlush(current);
@@ -196,20 +190,15 @@ function handleResponse(current, message) {
196
190
  * `OffscreenCanvas`/`Worker`, or when the proxy has been disabled by
197
191
  * a previous error.
198
192
  */
199
- function prefetchMeasurement(text, font, letterSpacing, horizontalScale, fontCacheKey, fontFingerprintWidth) {
193
+ function prefetchMeasurement(request) {
200
194
  if (!isWorkerFontMetricsEnabled()) return;
201
- if (!text) return;
195
+ if (!request.text) return;
202
196
  const current = ensureProxy();
203
197
  if (current === null) return;
204
- const key = makeKey(text, fontCacheKey, letterSpacing);
198
+ const key = makeKey(request.text, request.fontCacheKey, request.letterSpacing);
205
199
  if (current.pending.has(key)) return;
206
200
  current.pending.set(key, {
207
- text,
208
- font,
209
- fontCacheKey,
210
- fontFingerprintWidth,
211
- letterSpacing,
212
- horizontalScale,
201
+ ...request,
213
202
  cacheGeneration: getTextWidthCacheGeneration()
214
203
  });
215
204
  scheduleFlush(current);
@@ -1,18 +1,6 @@
1
+ import { FontKerningMode } from "./textMeasurementPolicy.js";
2
+
1
3
  //#region src/layout-engine/measure/measureWorkerProtocol.d.ts
2
- /**
3
- * Wire format shared between the main-thread proxy
4
- * (`measureWorker.ts`) and the worker entry
5
- * (`font-metrics.worker.ts`).
6
- *
7
- * The protocol is intentionally minimal: a batched request carrying an
8
- * array of measurement inputs, and a matching response with widths in
9
- * the same order. The worker is a pure function — no state, no cache.
10
- * The main thread owns the LRU cache and is the sole authority on what
11
- * is worth measuring.
12
- *
13
- * Keep this file tiny and dependency-free so it can be imported from
14
- * both contexts.
15
- */
16
4
  /**
17
5
  * Single measurement request inside a batch. Mirrors the inputs that
18
6
  * `measureTextWidth` already takes on the main thread.
@@ -40,6 +28,7 @@ type MeasureRequestEntry = {
40
28
  fontFingerprintWidth: number;
41
29
  letterSpacing: number;
42
30
  horizontalScale: number;
31
+ fontKerning: FontKerningMode;
43
32
  };
44
33
  declare const WORKER_FONT_FINGERPRINT_TEXT = "HAMBURGEFONTS ivwqy 0123456789";
45
34
  /**
@@ -1,4 +1,4 @@
1
- import { TableCell, TableCellMeasure } from "../types.js";
1
+ import { ImageRun, TableCell, TableCellMeasure } from "../types.js";
2
2
  import { FloatingImageZone } from "./floatingZones.js";
3
3
 
4
4
  //#region src/layout-engine/measure/tableCellFloating.d.ts
@@ -8,6 +8,11 @@ type TableCellFloatingImage = {
8
8
  height: number;
9
9
  alt?: string;
10
10
  transform?: string;
11
+ opacity?: number;
12
+ cropTop?: number;
13
+ cropRight?: number;
14
+ cropBottom?: number;
15
+ cropLeft?: number;
11
16
  x: number;
12
17
  y: number;
13
18
  side: "left" | "right";
@@ -19,8 +24,10 @@ type TableCellFloatingImage = {
19
24
  pmStart?: number;
20
25
  pmEnd?: number;
21
26
  };
27
+ type TableCellFloatingPosition = Pick<TableCellFloatingImage, "x" | "y" | "side">;
28
+ type ResolveTableCellFloatingPosition = (run: ImageRun, paragraphY: number) => TableCellFloatingPosition;
22
29
  declare function getTableCellContentWidth(cell: TableCell | undefined, cellMeasure: TableCellMeasure): number;
23
- declare function getTableCellFloatingImages(cell: TableCell, cellMeasure: TableCellMeasure, contentWidth: number): TableCellFloatingImage[];
30
+ declare function getTableCellFloatingImages(cell: TableCell, cellMeasure: TableCellMeasure, contentWidth: number, resolvePosition?: ResolveTableCellFloatingPosition): TableCellFloatingImage[];
24
31
  declare function buildTableCellFloatingZones(floatingImages: TableCellFloatingImage[], contentWidth: number): FloatingImageZone[];
25
32
  //#endregion
26
- export { TableCellFloatingImage, buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages };
33
+ export { ResolveTableCellFloatingPosition, TableCellFloatingImage, buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages };
@@ -1,22 +1,22 @@
1
1
  import { emuToPixels } from "../../utils/units.js";
2
2
  import { isFloatingImageRun } from "../types.js";
3
3
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
4
+ import { createTableCellFlowState, placeTableCellBlock } from "./tableCellFlow.js";
4
5
  //#region src/layout-engine/measure/tableCellFloating.ts
5
6
  function getTableCellContentWidth(cell, cellMeasure) {
6
7
  const padLeft = cell?.padding?.left ?? 7;
7
8
  const padRight = cell?.padding?.right ?? 7;
8
9
  return Math.max(0, cellMeasure.width - padLeft - padRight);
9
10
  }
10
- function getTableCellFloatingImages(cell, cellMeasure, contentWidth) {
11
+ function getTableCellFloatingImages(cell, cellMeasure, contentWidth, resolvePosition) {
11
12
  const result = [];
12
- let paragraphY = 0;
13
+ const flowState = createTableCellFlowState();
13
14
  for (let blockIndex = 0; blockIndex < cell.blocks.length; blockIndex++) {
14
15
  const block = cell.blocks[blockIndex];
15
- if (block?.kind !== "paragraph") {
16
- const blockMeasure = cellMeasure.blocks[blockIndex];
17
- if (blockMeasure?.kind === "table") paragraphY += blockMeasure.totalHeight;
18
- continue;
19
- }
16
+ const blockMeasure = cellMeasure.blocks[blockIndex];
17
+ if (!block || !blockMeasure) continue;
18
+ const placement = placeTableCellBlock(flowState, block, blockMeasure);
19
+ if (block.kind !== "paragraph") continue;
20
20
  for (const run of block.runs) {
21
21
  if (run.kind !== "image" || !isFloatingImageRun(run)) continue;
22
22
  const position = run.position;
@@ -24,30 +24,35 @@ function getTableCellFloatingImages(cell, cellMeasure, contentWidth) {
24
24
  const distBottom = run.distBottom ?? 0;
25
25
  const distLeft = run.distLeft ?? 12;
26
26
  const distRight = run.distRight ?? 12;
27
- let side = "left";
28
- let x = 0;
29
- if (position?.horizontal) {
30
- const horizontal = position.horizontal;
31
- if (horizontal.align === "right") {
27
+ let side;
28
+ let x;
29
+ let y;
30
+ if (resolvePosition) ({side, x, y} = resolvePosition(run, placement.contentTop));
31
+ else {
32
+ side = "left";
33
+ x = 0;
34
+ if (position?.horizontal) {
35
+ const horizontal = position.horizontal;
36
+ if (horizontal.align === "right") {
37
+ side = "right";
38
+ x = contentWidth - run.width;
39
+ } else if (horizontal.align === "center") x = (contentWidth - run.width) / 2;
40
+ else if (horizontal.posOffset !== void 0) {
41
+ x = emuToPixels(horizontal.posOffset);
42
+ side = x > contentWidth / 2 ? "right" : "left";
43
+ }
44
+ } else if (run.cssFloat === "right") {
32
45
  side = "right";
33
46
  x = contentWidth - run.width;
34
- } else if (horizontal.align === "left") x = 0;
35
- else if (horizontal.align === "center") x = (contentWidth - run.width) / 2;
36
- else if (horizontal.posOffset !== void 0) {
37
- x = emuToPixels(horizontal.posOffset);
38
- side = x > contentWidth / 2 ? "right" : "left";
39
47
  }
40
- } else if (run.cssFloat === "right") {
41
- side = "right";
42
- x = contentWidth - run.width;
43
- }
44
- let y = paragraphY;
45
- if (position?.vertical) {
46
- const vertical = position.vertical;
47
- if (vertical.posOffset !== void 0) y = paragraphY + emuToPixels(vertical.posOffset);
48
- else if (vertical.align === "top") y = 0;
48
+ y = placement.contentTop;
49
+ if (position?.vertical) {
50
+ const vertical = position.vertical;
51
+ if (vertical.posOffset !== void 0) y = placement.contentTop + emuToPixels(vertical.posOffset);
52
+ else if (vertical.align === "top") y = 0;
53
+ }
54
+ x = Math.max(0, Math.min(x, contentWidth - run.width));
49
55
  }
50
- x = Math.max(0, Math.min(x, contentWidth - run.width));
51
56
  let wrapText = "bothSides";
52
57
  if (run.cssFloat === "left") wrapText = "right";
53
58
  else if (run.cssFloat === "right") wrapText = "left";
@@ -57,6 +62,11 @@ function getTableCellFloatingImages(cell, cellMeasure, contentWidth) {
57
62
  height: run.height,
58
63
  ...run.alt !== void 0 ? { alt: run.alt } : {},
59
64
  ...run.transform !== void 0 ? { transform: run.transform } : {},
65
+ ...run.opacity != null ? { opacity: run.opacity } : {},
66
+ ...run.cropTop != null ? { cropTop: run.cropTop } : {},
67
+ ...run.cropRight != null ? { cropRight: run.cropRight } : {},
68
+ ...run.cropBottom != null ? { cropBottom: run.cropBottom } : {},
69
+ ...run.cropLeft != null ? { cropLeft: run.cropLeft } : {},
60
70
  x,
61
71
  y,
62
72
  side,
@@ -69,8 +79,6 @@ function getTableCellFloatingImages(cell, cellMeasure, contentWidth) {
69
79
  ...run.pmEnd !== void 0 ? { pmEnd: run.pmEnd } : {}
70
80
  });
71
81
  }
72
- const blockMeasure = cellMeasure.blocks[blockIndex];
73
- if (blockMeasure?.kind === "paragraph") paragraphY += blockMeasure.totalHeight;
74
82
  }
75
83
  return result;
76
84
  }
@@ -0,0 +1,27 @@
1
+ import { FlowBlock, Measure } from "../types.js";
2
+
3
+ //#region src/layout-engine/measure/tableCellFlow.d.ts
4
+ type TableCellBlockPlacement = {
5
+ top: number;
6
+ contentTop: number;
7
+ contentHeight: number;
8
+ leadingSpacing: number;
9
+ };
10
+ type TableCellFlowState = {
11
+ height: number;
12
+ previousParagraphWasEmpty: boolean;
13
+ trailingSpacing: number;
14
+ };
15
+ declare const createTableCellFlowState: () => TableCellFlowState;
16
+ declare const finishTableCellFlow: (state: TableCellFlowState) => number;
17
+ declare const getTableCellBlockContentHeight: (block: FlowBlock, measure: Measure) => number;
18
+ /**
19
+ * Place one block in an unpaginated table-cell story.
20
+ *
21
+ * Adjacent paragraph spacing collapses to the larger side. Consecutive
22
+ * authored empty paragraphs remain independent vertical spacers, including
23
+ * at the end of the cell.
24
+ */
25
+ declare const placeTableCellBlock: (state: TableCellFlowState, block: FlowBlock, measure: Measure) => TableCellBlockPlacement;
26
+ //#endregion
27
+ export { TableCellBlockPlacement, TableCellFlowState, createTableCellFlowState, finishTableCellFlow, getTableCellBlockContentHeight, placeTableCellBlock };
@@ -0,0 +1,73 @@
1
+ import { measuredLineRangeHeight } from "../lineFlow.js";
2
+ import { isFloatingTextBoxBlock } from "../types.js";
3
+ import { getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph } from "../paragraphSpacing.js";
4
+ //#region src/layout-engine/measure/tableCellFlow.ts
5
+ const createTableCellFlowState = () => ({
6
+ height: 0,
7
+ previousParagraphWasEmpty: false,
8
+ trailingSpacing: 0
9
+ });
10
+ const finishTableCellFlow = (state) => state.height + state.trailingSpacing;
11
+ const isSuppressedParagraphMeasure = (measure) => measure.totalHeight === 0 && measure.lines.every(({ lineHeight }) => lineHeight === 0);
12
+ const paragraphSpacing = (block, measure) => {
13
+ if (isSuppressedParagraphMeasure(measure)) return {
14
+ before: 0,
15
+ after: 0
16
+ };
17
+ return {
18
+ before: getParagraphSpacingBefore(block),
19
+ after: getParagraphSpacingAfter(block)
20
+ };
21
+ };
22
+ const getTableCellBlockContentHeight = (block, measure) => {
23
+ if (block.kind === "textBox" && isFloatingTextBoxBlock(block)) return 0;
24
+ if (block.kind === "paragraph" && measure.kind === "paragraph") return measuredLineRangeHeight(measure.lines, 0, measure.lines.length);
25
+ if ("totalHeight" in measure) return measure.totalHeight;
26
+ if ("height" in measure) return measure.height;
27
+ return 0;
28
+ };
29
+ /**
30
+ * Place one block in an unpaginated table-cell story.
31
+ *
32
+ * Adjacent paragraph spacing collapses to the larger side. Consecutive
33
+ * authored empty paragraphs remain independent vertical spacers, including
34
+ * at the end of the cell.
35
+ */
36
+ const placeTableCellBlock = (state, block, measure) => {
37
+ const top = state.height;
38
+ const contentHeight = getTableCellBlockContentHeight(block, measure);
39
+ if (block.kind === "paragraph" && measure.kind === "paragraph" && isSuppressedParagraphMeasure(measure)) return {
40
+ top,
41
+ contentTop: top,
42
+ contentHeight: 0,
43
+ leadingSpacing: 0
44
+ };
45
+ if (block.kind !== "paragraph" || measure.kind !== "paragraph") {
46
+ const leadingSpacing = state.trailingSpacing;
47
+ const contentTop = top + leadingSpacing;
48
+ state.height = contentTop + contentHeight;
49
+ state.previousParagraphWasEmpty = false;
50
+ state.trailingSpacing = 0;
51
+ return {
52
+ top,
53
+ contentTop,
54
+ contentHeight,
55
+ leadingSpacing
56
+ };
57
+ }
58
+ const spacing = paragraphSpacing(block, measure);
59
+ const empty = isEmptyParagraph(block);
60
+ const leadingSpacing = empty && state.previousParagraphWasEmpty ? spacing.before + state.trailingSpacing : Math.max(spacing.before, state.trailingSpacing);
61
+ const contentTop = top + leadingSpacing;
62
+ state.height = contentTop + contentHeight;
63
+ state.previousParagraphWasEmpty = empty;
64
+ state.trailingSpacing = spacing.after;
65
+ return {
66
+ top,
67
+ contentTop,
68
+ contentHeight,
69
+ leadingSpacing
70
+ };
71
+ };
72
+ //#endregion
73
+ export { createTableCellFlowState, finishTableCellFlow, getTableCellBlockContentHeight, placeTableCellBlock };
@@ -36,7 +36,7 @@ const getSourceCellColumn = (grid, cell) => grid.sourceColumnsByCell.get(cell);
36
36
  const getTableCellVerticalBorderHeight = (grid, cell, rowIndex) => {
37
37
  const sourceColumn = cell ? getSourceCellColumn(grid, cell) : void 0;
38
38
  const aboveBottom = (sourceColumn === void 0 ? void 0 : getSourceCellAt(grid, rowIndex - 1, sourceColumn))?.borders?.bottom;
39
- const aboveOwnsEdge = aboveBottom !== void 0 && aboveBottom.width !== 0 && aboveBottom.style !== "none" && aboveBottom.style !== "nil";
39
+ const aboveOwnsEdge = aboveBottom !== void 0 && aboveBottom.style !== "none" && aboveBottom.style !== "nil";
40
40
  return (rowIndex === 0 || !aboveOwnsEdge ? cell?.borders?.top?.width ?? 0 : 0) + (cell?.borders?.bottom?.width ?? 0);
41
41
  };
42
42
  const firstAvailableColumn = (occupiedColumns, startingColumn) => {
@@ -0,0 +1,18 @@
1
+ import { RunFormatting } from "../types.js";
2
+ import { FontStyle } from "./measureTypes.js";
3
+
4
+ //#region src/layout-engine/measure/textMeasurementPolicy.d.ts
5
+ declare const FONT_KERNING_MODE: {
6
+ readonly enabled: "normal";
7
+ readonly disabled: "none";
8
+ };
9
+ type FontKerningMode = (typeof FONT_KERNING_MODE)[keyof typeof FONT_KERNING_MODE];
10
+ type RunKerningInput = Pick<RunFormatting, "fontSize" | "kerningMinPt">;
11
+ /** Resolve an authored kerning threshold against the effective run size. */
12
+ declare function getRunFontKerningMode(run: RunKerningInput, fallbackFontSize: number): FontKerningMode;
13
+ /** Convert normalized measurement style into the canvas/CSS kerning mode. */
14
+ declare function getFontKerningMode(style: Pick<FontStyle, "kerning">): FontKerningMode;
15
+ /** Count ordinary ASCII spaces that justification may compress. */
16
+ declare function countCompressibleSpaces(text: string): number;
17
+ //#endregion
18
+ export { FONT_KERNING_MODE, FontKerningMode, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode };
@@ -0,0 +1,24 @@
1
+ //#region src/layout-engine/measure/textMeasurementPolicy.ts
2
+ const FONT_KERNING_MODE = {
3
+ enabled: "normal",
4
+ disabled: "none"
5
+ };
6
+ /** Resolve an authored kerning threshold against the effective run size. */
7
+ function getRunFontKerningMode(run, fallbackFontSize) {
8
+ const threshold = run.kerningMinPt;
9
+ const fontSize = run.fontSize ?? fallbackFontSize;
10
+ if (threshold === void 0 || fontSize < threshold) return FONT_KERNING_MODE.disabled;
11
+ return FONT_KERNING_MODE.enabled;
12
+ }
13
+ /** Convert normalized measurement style into the canvas/CSS kerning mode. */
14
+ function getFontKerningMode(style) {
15
+ return style.kerning ? FONT_KERNING_MODE.enabled : FONT_KERNING_MODE.disabled;
16
+ }
17
+ /** Count ordinary ASCII spaces that justification may compress. */
18
+ function countCompressibleSpaces(text) {
19
+ let count = 0;
20
+ for (const char of text) if (char === " ") count += 1;
21
+ return count;
22
+ }
23
+ //#endregion
24
+ export { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode };
@@ -6,6 +6,10 @@ type RenderedBreakState = {
6
6
  } | {
7
7
  type: "pageAdvance";
8
8
  reason: "ordinary" | "reflowBoundary";
9
+ } | {
10
+ type: "pageAdvance";
11
+ reason: "authoredBoundary";
12
+ boundary: "hardBreak" | "sectionBreak";
9
13
  };
10
14
  declare const INITIAL_RENDERED_BREAK_STATE: RenderedBreakState;
11
15
  type ReconcileBeforeBlockOptions = {
@@ -19,6 +23,7 @@ type ReconcileBeforeBlockOptions = {
19
23
  };
20
24
  type BreakDecision = {
21
25
  forcePageBreak: boolean;
26
+ suppressSpaceBefore: boolean;
22
27
  state: RenderedBreakState;
23
28
  };
24
29
  /**
@@ -46,7 +51,7 @@ type ReconcileAfterBlockOptions = {
46
51
  pageNumberAfter: number;
47
52
  previousPage: Page | undefined;
48
53
  };
49
- /** Record page movement caused by laying out a block, or reset at a structural break. */
54
+ /** Record page movement and carry authored boundaries across pagination-empty paragraphs. */
50
55
  declare const reconcileAfterBlock: ({
51
56
  state,
52
57
  block,
@@ -1,4 +1,3 @@
1
- import { isEmptyParagraph } from "./paragraphSpacing.js";
2
1
  import { continuesNumberedSequence, continuesTabbedParagraphSequence } from "./paragraphSequence.js";
3
2
  //#region src/layout-engine/renderedBreakReconciliation.ts
4
3
  const INITIAL_RENDERED_BREAK_STATE = { type: "noPageAdvance" };
@@ -13,15 +12,24 @@ const INITIAL_RENDERED_BREAK_STATE = { type: "noPageAdvance" };
13
12
  const reconcileBreakBeforeBlock = ({ state, block, previousBlock, page, blocksById, hasExplicitPageBreak, renderedBreakNeedsSnap }) => {
14
13
  if (hasExplicitPageBreak) return {
15
14
  forcePageBreak: true,
15
+ suppressSpaceBefore: false,
16
16
  state: INITIAL_RENDERED_BREAK_STATE
17
17
  };
18
18
  if (block.kind !== "paragraph" || block.attrs?.renderedPageBreakBefore !== true) return {
19
19
  forcePageBreak: false,
20
+ suppressSpaceBefore: false,
20
21
  state
21
22
  };
22
- const markerAlreadySatisfied = pageStartsWithPreviousParagraphContinuation(page, previousBlock) || state.type === "pageAdvance" && state.reason === "reflowBoundary" || state.type === "pageAdvance" && isPaginationEmptyParagraph(block) || state.type === "pageAdvance" && (continuesNumberedSequence(previousBlock, block) || continuesTabbedParagraphSequence(previousBlock, block));
23
+ const markerAlreadySatisfied = pageStartsWithPreviousParagraphContinuation(page, previousBlock) || state.type === "pageAdvance" && state.reason === "authoredBoundary" || state.type === "pageAdvance" && state.reason === "reflowBoundary" || state.type === "pageAdvance" && isPaginationEmptyParagraph(block) || state.type === "pageAdvance" && (continuesNumberedSequence(previousBlock, block) || continuesTabbedParagraphSequence(previousBlock, block));
24
+ const forcePageBreak = (renderedBreakNeedsSnap || block.attrs?.keepNext === true) && !markerAlreadySatisfied && pageHasVisibleBodyContent(page, blocksById);
25
+ const followsAuthoredPageBreak = previousBlock?.kind === "pageBreak";
26
+ const followsSectionBreak = previousBlock?.kind === "sectionBreak";
27
+ const followsPageBreakingSection = followsSectionBreak && previousBlock.type !== "continuous";
28
+ const followsCarriedSectionBoundary = state.type === "pageAdvance" && state.reason === "authoredBoundary" && state.boundary === "sectionBreak";
29
+ const preserveSectionLeadingSpacing = (followsSectionBreak || followsCarriedSectionBoundary) && block.attrs?.spacingExplicit?.before === true;
23
30
  return {
24
- forcePageBreak: renderedBreakNeedsSnap && !markerAlreadySatisfied && pageHasVisibleBodyContent(page, blocksById),
31
+ forcePageBreak,
32
+ suppressSpaceBefore: (forcePageBreak || markerAlreadySatisfied || followsAuthoredPageBreak || followsPageBreakingSection) && !preserveSectionLeadingSpacing,
25
33
  state: INITIAL_RENDERED_BREAK_STATE
26
34
  };
27
35
  };
@@ -29,19 +37,38 @@ const recordReflowBoundary = (state, crossedPageBoundary) => crossedPageBoundary
29
37
  type: "pageAdvance",
30
38
  reason: "reflowBoundary"
31
39
  } : state;
32
- /** Record page movement caused by laying out a block, or reset at a structural break. */
40
+ /** Record page movement and carry authored boundaries across pagination-empty paragraphs. */
33
41
  const reconcileAfterBlock = ({ state, block, pageNumberBefore, pageNumberAfter, previousPage }) => {
34
- if (isStructuralBreak(block)) return INITIAL_RENDERED_BREAK_STATE;
35
- if (!isVisibleBodyBlock(block) || pageNumberAfter <= pageNumberBefore) return state;
42
+ if (isStructuralBreak(block)) {
43
+ if (pageNumberAfter <= pageNumberBefore) {
44
+ if (state.type === "pageAdvance" && state.reason === "authoredBoundary") return block.kind === "sectionBreak" ? {
45
+ ...state,
46
+ boundary: "sectionBreak"
47
+ } : state;
48
+ return INITIAL_RENDERED_BREAK_STATE;
49
+ }
50
+ return {
51
+ type: "pageAdvance",
52
+ reason: "authoredBoundary",
53
+ boundary: block.kind === "sectionBreak" ? "sectionBreak" : "hardBreak"
54
+ };
55
+ }
56
+ if (block.kind === "paragraph" && isPaginationEmptyParagraph(block)) return state;
57
+ if (pageNumberAfter <= pageNumberBefore) return state.type === "pageAdvance" && state.reason === "authoredBoundary" ? INITIAL_RENDERED_BREAK_STATE : state;
36
58
  return {
37
59
  type: "pageAdvance",
38
60
  reason: previousPage?.fragments.some(({ blockId }) => blockId === block.id) ? "reflowBoundary" : "ordinary"
39
61
  };
40
62
  };
41
- const isPaginationEmptyParagraph = (block) => block.runs.every((run) => {
63
+ const isPaginationEmptyParagraph = (block) => !hasVisibleEmptyParagraphDecoration(block) && block.runs.every((run) => {
42
64
  if (run.kind === "text") return run.text.trim().length === 0;
43
65
  return run.kind === "tab" || run.kind === "lineBreak";
44
66
  });
67
+ const hasVisibleEmptyParagraphDecoration = (block) => {
68
+ if (block.attrs?.listMarker !== void 0 && block.attrs.listMarker.length > 0 && block.attrs.listMarkerHidden !== true) return true;
69
+ if (block.attrs?.shading) return true;
70
+ return Object.values(block.attrs?.borders ?? {}).some((border) => border !== void 0 && border.style !== "none" && border.style !== "nil" && border.width !== 0);
71
+ };
45
72
  const pageHasVisibleBodyContent = (page, blocksById) => {
46
73
  for (const fragment of page.fragments) {
47
74
  if (fragment.kind !== "paragraph") return true;
@@ -55,9 +82,5 @@ const pageStartsWithPreviousParagraphContinuation = (page, previousBlock) => {
55
82
  return page.fragments.some((fragment) => fragment.kind === "paragraph" && String(fragment.blockId) === String(previousBlock.id) && fragment.continuesFromPrev === true);
56
83
  };
57
84
  const isStructuralBreak = (block) => block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak";
58
- const isVisibleBodyBlock = (block) => {
59
- if (block.kind === "paragraph") return !isEmptyParagraph(block);
60
- return !isStructuralBreak(block);
61
- };
62
85
  //#endregion
63
86
  export { INITIAL_RENDERED_BREAK_STATE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
@@ -9,10 +9,13 @@ type TableRowBreakInfo = {
9
9
  * at which a break is clean. Always includes the row's full height as the
10
10
  * final boundary.
11
11
  */
12
- breakOffsets: number[][];
12
+ breakOffsets: number[][]; /** Suppressible leading paragraph whitespace after each matching break offset. */
13
+ continuationSkips: number[][];
13
14
  };
14
15
  /** Build break geometry for a table from its block + measure. */
15
16
  declare function buildTableRowBreakInfo(block: TableBlock, measure: TableMeasure): TableRowBreakInfo;
17
+ /** Leading empty-paragraph whitespace to consume when a row resumes after `offset`. */
18
+ declare function getRowContinuationSkip(info: TableRowBreakInfo, rowIndex: number, offset: number): number;
16
19
  /**
17
20
  * Given a row and how much of it has already been placed (`fromOffset`), return
18
21
  * how many more px can be placed ending on a whole line, without exceeding
@@ -20,4 +23,4 @@ declare function buildTableRowBreakInfo(block: TableBlock, measure: TableMeasure
20
23
  */
21
24
  declare function snapRowBreak(info: TableRowBreakInfo, rowIndex: number, fromOffset: number, maxSlice: number): number;
22
25
  //#endregion
23
- export { TableRowBreakInfo, buildTableRowBreakInfo, snapRowBreak };
26
+ export { TableRowBreakInfo, buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak };