@stll/folio-core 0.35.1 → 0.37.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 (236) hide show
  1. package/README.md +59 -1
  2. package/dist/ai-edits/apply.js +878 -377
  3. package/dist/ai-edits/block-identity.d.ts +4 -1
  4. package/dist/ai-edits/block-identity.js +4 -1
  5. package/dist/ai-edits/blockRange.js +14 -14
  6. package/dist/ai-edits/clean-text.d.ts +40 -1
  7. package/dist/ai-edits/clean-text.js +52 -2
  8. package/dist/ai-edits/headless.d.ts +3 -1
  9. package/dist/ai-edits/headless.js +141 -73
  10. package/dist/ai-edits/index.d.ts +3 -3
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.js +38 -20
  13. package/dist/ai-edits/snapshot.d.ts +14 -6
  14. package/dist/ai-edits/snapshot.js +105 -20
  15. package/dist/ai-edits/table-cell-mutations.js +1 -1
  16. package/dist/ai-edits/table-row-column-mutations.js +7 -0
  17. package/dist/ai-edits/table-template.d.ts +11 -1
  18. package/dist/ai-edits/table-template.js +49 -17
  19. package/dist/ai-edits/types.d.ts +62 -61
  20. package/dist/ai-edits/word-diff.d.ts +18 -3
  21. package/dist/ai-edits/word-diff.js +556 -67
  22. package/dist/ai-suggestions/conflict.js +1 -4
  23. package/dist/ai-suggestions/text-positions.d.ts +8 -0
  24. package/dist/ai-suggestions/text-positions.js +27 -5
  25. package/dist/compare/__fixtures__/body-sequence.js +3 -4
  26. package/dist/compare/column-alignment.d.ts +13 -13
  27. package/dist/compare/column-alignment.js +78 -39
  28. package/dist/compare/compare.d.ts +4 -3
  29. package/dist/compare/compare.js +48 -21
  30. package/dist/compare/content-alignment.d.ts +95 -0
  31. package/dist/compare/content-alignment.js +1450 -0
  32. package/dist/compare/content-types.d.ts +100 -0
  33. package/dist/compare/content-types.js +0 -0
  34. package/dist/compare/content.d.ts +209 -0
  35. package/dist/compare/content.js +885 -0
  36. package/dist/compare/formatting.d.ts +5 -5
  37. package/dist/compare/formatting.js +6 -4
  38. package/dist/compare/plan.d.ts +6 -1
  39. package/dist/compare/plan.js +171 -566
  40. package/dist/compare/scenario.d.ts +2 -2
  41. package/dist/compare/types.d.ts +3 -3
  42. package/dist/compare/verification.d.ts +8 -9
  43. package/dist/compare/verification.js +72 -50
  44. package/dist/compat/eigenpal.d.ts +5 -3
  45. package/dist/compat/eigenpal.js +3 -2
  46. package/dist/controller/headerFooterEditorManager.js +18 -10
  47. package/dist/controller/hiddenEditorApi.js +1 -1
  48. package/dist/controller/hiddenEditorManager.d.ts +7 -2
  49. package/dist/controller/hiddenEditorManager.js +15 -6
  50. package/dist/controller/layoutPipeline.js +5 -3
  51. package/dist/controller/noteEditorManager.js +12 -7
  52. package/dist/display-list/build/headerFooterPrimitives.js +1 -0
  53. package/dist/display-list/build/storyPrimitives.d.ts +5 -2
  54. package/dist/display-list/build/storyPrimitives.js +60 -3
  55. package/dist/display-list/build/tablePrimitives.js +30 -0
  56. package/dist/display-list/primitives.d.ts +1 -1
  57. package/dist/document-operations.d.ts +8 -4
  58. package/dist/document-operations.js +104 -12
  59. package/dist/docx/blockContentParser.js +6 -27
  60. package/dist/docx/commentParser.js +2 -4
  61. package/dist/docx/commentRangeIntegrity.js +2 -4
  62. package/dist/docx/documentClone.d.ts +2 -0
  63. package/dist/docx/documentClone.js +2 -0
  64. package/dist/docx/ensureParaIds.js +9 -4
  65. package/dist/docx/headerFooterParser.js +2 -4
  66. package/dist/docx/normalizeBaseDirection.js +5 -7
  67. package/dist/docx/numberingParser.d.ts +3 -1
  68. package/dist/docx/numberingParser.js +4 -1
  69. package/dist/docx/paragraphParser.js +34 -35
  70. package/dist/docx/paragraphPropertySource.d.ts +49 -0
  71. package/dist/docx/paragraphPropertySource.js +113 -0
  72. package/dist/docx/paragraphTraversal.js +30 -34
  73. package/dist/docx/rezip.d.ts +0 -8
  74. package/dist/docx/rezip.js +41 -16
  75. package/dist/docx/runConsolidator.js +2 -4
  76. package/dist/docx/runParser.js +6 -17
  77. package/dist/docx/sectionParser.js +1 -15
  78. package/dist/docx/serializer/paragraphSerializer.js +289 -30
  79. package/dist/docx/serializer/runSerializer.js +6 -12
  80. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -10
  81. package/dist/docx/serializer/tableSerializer.js +4 -23
  82. package/dist/docx/serializer/trackedChangeAttributes.d.ts +11 -0
  83. package/dist/docx/serializer/trackedChangeAttributes.js +31 -0
  84. package/dist/docx/server/build.d.ts +1 -1
  85. package/dist/docx/server/createBilingualDocument.js +8 -10
  86. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +22 -13
  87. package/dist/docx/server/validateDocxConformance.js +17 -13
  88. package/dist/docx/tableParser.js +1 -37
  89. package/dist/docx/trackedChangeInfo.d.ts +11 -0
  90. package/dist/docx/trackedChangeInfo.js +38 -0
  91. package/dist/docx/verbatimCapture.d.ts +20 -9
  92. package/dist/docx/verbatimCapture.js +208 -8
  93. package/dist/docx/xmlParser.d.ts +3 -1
  94. package/dist/docx/xmlParser.js +6 -1
  95. package/dist/docx/xmlSafety.d.ts +1 -1
  96. package/dist/docx/xmlSafety.js +157 -0
  97. package/dist/headless-layout.js +5 -4
  98. package/dist/index.d.ts +5 -3
  99. package/dist/index.js +3 -2
  100. package/dist/internal/headlessRevisionResolution.d.ts +23 -0
  101. package/dist/internal/headlessRevisionResolution.js +155 -0
  102. package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
  103. package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
  104. package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
  105. package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
  106. package/dist/internal/pageBreakRunPartition.d.ts +26 -0
  107. package/dist/internal/pageBreakRunPartition.js +35 -0
  108. package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
  109. package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
  110. package/dist/internal/sectionEndpointResolution.d.ts +32 -0
  111. package/dist/internal/sectionEndpointResolution.js +38 -0
  112. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  113. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
  114. package/dist/layout-bridge/convert/headerFooterLayout.js +52 -9
  115. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
  116. package/dist/layout-bridge/convert/toFlowBlocks.js +391 -82
  117. package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
  118. package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
  119. package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
  120. package/dist/layout-bridge/dom/textStreamDom.js +75 -0
  121. package/dist/layout-bridge/headerFooterLayout.js +16 -30
  122. package/dist/layout-engine/index.js +12 -3
  123. package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
  124. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
  125. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  126. package/dist/layout-engine/types.d.ts +7 -0
  127. package/dist/layout-painter/imageLayout.d.ts +1 -1
  128. package/dist/layout-painter/renderTable.js +20 -0
  129. package/dist/markdown/renderBlock.js +7 -10
  130. package/dist/model.d.ts +1 -1
  131. package/dist/paged-layout/rangeProjection.js +6 -9
  132. package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
  133. package/dist/paged-layout/transactionDirtyRange.js +23 -1
  134. package/dist/prosemirror/attrs/index.d.ts +7 -3
  135. package/dist/prosemirror/attrs/index.js +128 -7
  136. package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
  137. package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
  138. package/dist/prosemirror/commands/comments.d.ts +10 -1
  139. package/dist/prosemirror/commands/comments.js +551 -87
  140. package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
  141. package/dist/prosemirror/commands/formatPainter.js +319 -63
  142. package/dist/prosemirror/commands/index.d.ts +2 -2
  143. package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
  144. package/dist/prosemirror/commands/pageBreak.js +18 -36
  145. package/dist/prosemirror/commands/propertyChangeScope.d.ts +27 -3
  146. package/dist/prosemirror/commands/propertyChangeScope.js +85 -14
  147. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -5
  148. package/dist/prosemirror/conversion/fromProseDoc.js +935 -223
  149. package/dist/prosemirror/conversion/runShadingMark.js +2 -2
  150. package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
  151. package/dist/prosemirror/conversion/toProseDoc.js +439 -291
  152. package/dist/prosemirror/extensions/StarterKit.js +4 -0
  153. package/dist/prosemirror/extensions/core/ParagraphExtension.js +54 -6
  154. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -1
  155. package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
  156. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +19 -6
  157. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
  158. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
  159. package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
  160. package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
  161. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
  162. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
  163. package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
  164. package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
  165. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
  166. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
  167. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
  168. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
  169. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  170. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
  171. package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
  172. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
  173. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  174. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
  175. package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
  176. package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
  177. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
  178. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
  179. package/dist/prosemirror/findReplaceSelection.js +8 -3
  180. package/dist/prosemirror/index.d.ts +3 -3
  181. package/dist/prosemirror/listMarker.d.ts +26 -1
  182. package/dist/prosemirror/listMarker.js +108 -14
  183. package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
  184. package/dist/prosemirror/pageBreakRunProjection.js +47 -0
  185. package/dist/prosemirror/paraText.js +6 -0
  186. package/dist/prosemirror/paragraphAlignment.d.ts +12 -0
  187. package/dist/prosemirror/paragraphAlignment.js +13 -0
  188. package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
  189. package/dist/prosemirror/paragraphSpacing.js +125 -0
  190. package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
  191. package/dist/prosemirror/plugins/documentNumbering.js +18 -0
  192. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
  193. package/dist/prosemirror/plugins/pmTextScan.js +14 -4
  194. package/dist/prosemirror/plugins/suggestionMode.js +9 -11
  195. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
  196. package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
  197. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
  198. package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
  199. package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
  200. package/dist/prosemirror/runFormattingProvenance.js +48 -0
  201. package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
  202. package/dist/prosemirror/runFormattingReconciliation.js +54 -0
  203. package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
  204. package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
  205. package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
  206. package/dist/prosemirror/runStyleFormatting.js +105 -0
  207. package/dist/prosemirror/schema/index.d.ts +3 -3
  208. package/dist/prosemirror/schema/marks.d.ts +84 -9
  209. package/dist/prosemirror/schema/marks.js +73 -1
  210. package/dist/prosemirror/schema/nodes.d.ts +30 -6
  211. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
  212. package/dist/prosemirror/styles/resolvedStyleAttrs.js +45 -23
  213. package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
  214. package/dist/prosemirror/styles/styleResolver.js +3 -3
  215. package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
  216. package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
  217. package/dist/prosemirror/tableGridMutation.d.ts +20 -0
  218. package/dist/prosemirror/tableGridMutation.js +30 -0
  219. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
  220. package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
  221. package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
  222. package/dist/prosemirror/validation.js +23 -4
  223. package/dist/redline.js +4 -1
  224. package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
  225. package/dist/render-dom/RenderedDomContext.js +17 -34
  226. package/dist/server.d.ts +4 -4
  227. package/dist/server.js +2 -2
  228. package/dist/style-engine/styleEngine.d.ts +1 -1
  229. package/dist/utils/clipboard.js +1 -1
  230. package/dist/utils/findReplace.js +38 -21
  231. package/dist/utils/mergeDocumentContent.js +3 -3
  232. package/dist/utils/replaceText.js +2 -1
  233. package/dist/version-comparison.d.ts +38 -44
  234. package/dist/version-comparison.js +270 -399
  235. package/dist/watermark/index.js +2 -4
  236. package/package.json +8 -2
@@ -86,6 +86,13 @@ type CollapsedLineEdgeCaretTarget = {
86
86
  * cannot decide which span owns a boundary.
87
87
  */
88
88
  declare function findCollapsedLineEdgeCaretTarget(spans: readonly HTMLElement[], pmPos: number): CollapsedLineEdgeCaretTarget | null;
89
+ /**
90
+ * Map an unpainted inline position inside a paragraph to the nearest painted
91
+ * span boundary. Inline metadata and folded fields occupy ProseMirror
92
+ * positions without emitting glyphs; their carets belong at the adjacent text
93
+ * slot, not at the paragraph box's physical edge.
94
+ */
95
+ declare function findParagraphGapCaretTarget(container: ParentNode, pmPos: number): CollapsedLineEdgeCaretTarget | null;
89
96
  declare function getCaretPositionFromDom(container: HTMLElement, pmPos: number, overlayRect: DOMRect): DomCaretPosition | null;
90
97
  //#endregion
91
- export { CollapsedLineEdgeCaretGeometry, CollapsedLineEdgeCaretTarget, DomCaretPosition, DomSelectionRect, clickToPositionDom, findCollapsedLineEdgeCaretTarget, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
98
+ export { CollapsedLineEdgeCaretGeometry, CollapsedLineEdgeCaretTarget, DomCaretPosition, DomSelectionRect, clickToPositionDom, findCollapsedLineEdgeCaretTarget, findParagraphGapCaretTarget, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
@@ -1,4 +1,5 @@
1
1
  import { closestHtmlElement, findHtmlElement, htmlQueryAll, queryHtmlElement } from "../../utils/domGuards.js";
2
+ import { descendantTextNodes, logicalTextOffset, measurePaintedSpanRange, textBoundaryAt, totalTextLength } from "./textStreamDom.js";
2
3
  //#region src/layout-bridge/dom/clickToPositionDom.ts
3
4
  /**
4
5
  * DOM-based Click-to-Position Mapping
@@ -57,28 +58,30 @@ function findPositionInSpan(spanEl, clientX, clientY) {
57
58
  const rect = spanEl.getBoundingClientRect();
58
59
  return clientX < (rect.left + rect.right) / 2 ? pmStart : pmEnd;
59
60
  }
60
- const textNode = spanEl.firstChild;
61
- if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return pmStart;
62
- const text = textNode;
63
- const textLength = text.length;
64
- if (textLength === 0) return pmStart;
61
+ const textNodes = descendantTextNodes(spanEl);
62
+ if (textNodes.length === 0) return pmStart;
63
+ if (totalTextLength(textNodes) === 0) return pmStart;
65
64
  const ownerDoc = spanEl.ownerDocument;
66
- const native = caretOffsetFromPoint(ownerDoc, text, clientX, clientY);
65
+ const native = caretOffsetFromPoint(ownerDoc, textNodes, clientX, clientY);
67
66
  if (native !== null) return pmStart + Math.min(native, pmEnd - pmStart);
68
67
  const range = ownerDoc.createRange();
69
68
  let bestOffset = 0;
70
69
  let bestDistance = Number.POSITIVE_INFINITY;
71
- for (let i = 0; i < textLength; i++) {
72
- range.setStart(text, i);
73
- range.setEnd(text, i + 1);
74
- const rect = range.getBoundingClientRect();
75
- if (rect.width === 0 && rect.height === 0) continue;
76
- const midpoint = rect.left + rect.width / 2;
77
- const distance = Math.abs(clientX - midpoint);
78
- if (distance < bestDistance) {
79
- bestDistance = distance;
80
- bestOffset = clientX < midpoint ? i : i + 1;
70
+ let charsProcessed = 0;
71
+ for (const text of textNodes) {
72
+ for (let i = 0; i < text.length; i++) {
73
+ range.setStart(text, i);
74
+ range.setEnd(text, i + 1);
75
+ const rect = range.getBoundingClientRect();
76
+ if (rect.width === 0 && rect.height === 0) continue;
77
+ const midpoint = rect.left + rect.width / 2;
78
+ const distance = Math.abs(clientX - midpoint);
79
+ if (distance < bestDistance) {
80
+ bestDistance = distance;
81
+ bestOffset = charsProcessed + (clientX < midpoint ? i : i + 1);
82
+ }
81
83
  }
84
+ charsProcessed += text.length;
82
85
  }
83
86
  return pmStart + Math.min(bestOffset, pmEnd - pmStart);
84
87
  }
@@ -89,15 +92,21 @@ function findPositionInSpan(spanEl, clientX, clientY) {
89
92
  * a different node — callers fall back to a glyph-by-glyph
90
93
  * scan in that case.
91
94
  */
92
- function caretOffsetFromPoint(ownerDoc, text, clientX, clientY) {
95
+ function caretOffsetFromPoint(ownerDoc, textNodes, clientX, clientY) {
93
96
  const doc = ownerDoc;
94
97
  if (typeof doc.caretPositionFromPoint === "function") {
95
98
  const pos = doc.caretPositionFromPoint(clientX, clientY);
96
- if (pos && pos.offsetNode === text) return pos.offset;
99
+ if (pos) {
100
+ const offset = logicalTextOffset(textNodes, pos.offsetNode, pos.offset);
101
+ if (offset !== null) return offset;
102
+ }
97
103
  }
98
104
  if (typeof doc.caretRangeFromPoint === "function") {
99
105
  const range = doc.caretRangeFromPoint(clientX, clientY);
100
- if (range && range.startContainer === text) return range.startOffset;
106
+ if (range) {
107
+ const offset = logicalTextOffset(textNodes, range.startContainer, range.startOffset);
108
+ if (offset !== null) return offset;
109
+ }
101
110
  }
102
111
  return null;
103
112
  }
@@ -200,22 +209,12 @@ function getSelectionRectsFromDom(container, from, to, overlayRect) {
200
209
  const rects = [];
201
210
  const spans = htmlQueryAll(container, ".layout-page-content span[data-pm-start][data-pm-end]");
202
211
  for (const spanEl of spans) {
203
- const pmStart = Number(spanEl.dataset["pmStart"]);
204
- if (Number(spanEl.dataset["pmEnd"]) <= from || pmStart >= to) continue;
205
- const textNode = spanEl.firstChild;
206
- if (!textNode || textNode.nodeType !== Node.TEXT_NODE) continue;
207
- const text = textNode;
208
- const ownerDoc = spanEl.ownerDocument;
209
- const startChar = Math.max(0, from - pmStart);
210
- const endChar = Math.min(text.length, to - pmStart);
211
- if (startChar >= endChar) continue;
212
- const range = ownerDoc.createRange();
213
- range.setStart(text, startChar);
214
- range.setEnd(text, endChar);
215
- const clientRects = range.getClientRects();
212
+ const measurement = measurePaintedSpanRange(spanEl, from, to);
213
+ if (measurement.type === "outside") continue;
216
214
  const pageEl = closestHtmlElement(spanEl, ".layout-page");
217
215
  const pageIndex = pageEl ? Number(pageEl.dataset["pageNumber"] || 1) - 1 : 0;
218
- for (const clientRect of Array.from(clientRects)) rects.push({
216
+ const clientRects = measurement.type === "element" ? [measurement.rect] : measurement.rects;
217
+ for (const clientRect of clientRects) rects.push({
219
218
  x: clientRect.left - overlayRect.left,
220
219
  y: clientRect.top - overlayRect.top,
221
220
  width: clientRect.width,
@@ -273,6 +272,70 @@ function findCollapsedLineEdgeCaretTarget(spans, pmPos) {
273
272
  }
274
273
  return null;
275
274
  }
275
+ const getPaintedSpanCaretGeometry = (span, pmPos) => {
276
+ const collapsed = getCollapsedLineEdgeCaretGeometry(span, pmPos);
277
+ if (collapsed) return collapsed;
278
+ const spanRect = span.getBoundingClientRect();
279
+ const lineHeight = closestHtmlElement(span, ".layout-line")?.getBoundingClientRect().height || spanRect.height || 16;
280
+ if (span.classList.contains("layout-run-tab")) return {
281
+ left: pmPos >= Number(span.dataset["pmEnd"]) ? spanRect.right : spanRect.left,
282
+ top: spanRect.top,
283
+ height: lineHeight
284
+ };
285
+ const textNodes = descendantTextNodes(span);
286
+ if (textNodes.length === 0) return {
287
+ left: spanRect.left,
288
+ top: spanRect.top,
289
+ height: lineHeight
290
+ };
291
+ const pmStart = Number(span.dataset["pmStart"]);
292
+ const boundary = textBoundaryAt(textNodes, Math.min(Math.max(0, pmPos - pmStart), totalTextLength(textNodes)), "start");
293
+ if (!boundary) return {
294
+ left: spanRect.left,
295
+ top: spanRect.top,
296
+ height: lineHeight
297
+ };
298
+ const range = span.ownerDocument.createRange();
299
+ range.setStart(boundary.node, boundary.offset);
300
+ range.setEnd(boundary.node, boundary.offset);
301
+ const rangeRect = range.getBoundingClientRect();
302
+ return {
303
+ left: rangeRect.left,
304
+ top: rangeRect.top,
305
+ height: rangeRect.height || lineHeight
306
+ };
307
+ };
308
+ /**
309
+ * Map an unpainted inline position inside a paragraph to the nearest painted
310
+ * span boundary. Inline metadata and folded fields occupy ProseMirror
311
+ * positions without emitting glyphs; their carets belong at the adjacent text
312
+ * slot, not at the paragraph box's physical edge.
313
+ */
314
+ function findParagraphGapCaretTarget(container, pmPos) {
315
+ let best;
316
+ for (const paragraph of htmlQueryAll(container, ".layout-page-content .layout-paragraph")) {
317
+ const paragraphStart = Number(paragraph.dataset["pmStart"]);
318
+ const paragraphEnd = Number(paragraph.dataset["pmEnd"]);
319
+ if (!(pmPos > paragraphStart && pmPos < paragraphEnd)) continue;
320
+ for (const span of htmlQueryAll(paragraph, "span[data-pm-start][data-pm-end]")) {
321
+ const spanStart = Number(span.dataset["pmStart"]);
322
+ const spanEnd = Number(span.dataset["pmEnd"]);
323
+ if (!Number.isFinite(spanStart) || !Number.isFinite(spanEnd)) continue;
324
+ const boundaryPos = pmPos < spanStart ? spanStart : spanEnd;
325
+ const distance = Math.abs(boundaryPos - pmPos);
326
+ if (!best || distance < best.distance) best = {
327
+ span,
328
+ boundaryPos,
329
+ distance
330
+ };
331
+ }
332
+ }
333
+ if (!best) return null;
334
+ return {
335
+ span: best.span,
336
+ geometry: getPaintedSpanCaretGeometry(best.span, best.boundaryPos)
337
+ };
338
+ }
276
339
  function getCaretPositionFromDom(container, pmPos, overlayRect) {
277
340
  const spans = htmlQueryAll(container, ".layout-page-content span[data-pm-start][data-pm-end]");
278
341
  const collapsedTarget = findCollapsedLineEdgeCaretTarget(spans, pmPos);
@@ -306,8 +369,8 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
306
369
  continue;
307
370
  }
308
371
  if (pmPos >= pmStart && pmPos <= pmEnd) {
309
- const textNode = spanEl.firstChild;
310
- if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
372
+ const textNodes = descendantTextNodes(spanEl);
373
+ if (textNodes.length === 0) {
311
374
  const spanRect = spanEl.getBoundingClientRect();
312
375
  const pageEl = closestHtmlElement(spanEl, ".layout-page");
313
376
  const pageIndex = pageEl ? Number(pageEl.dataset["pageNumber"] || 1) - 1 : 0;
@@ -320,11 +383,11 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
320
383
  pageIndex
321
384
  };
322
385
  }
323
- const text = textNode;
324
- const charIndex = Math.min(pmPos - pmStart, text.length);
386
+ const boundary = textBoundaryAt(textNodes, Math.min(pmPos - pmStart, totalTextLength(textNodes)), "start");
387
+ if (!boundary) continue;
325
388
  const range = spanEl.ownerDocument.createRange();
326
- range.setStart(text, charIndex);
327
- range.setEnd(text, charIndex);
389
+ range.setStart(boundary.node, boundary.offset);
390
+ range.setEnd(boundary.node, boundary.offset);
328
391
  const rangeRect = range.getBoundingClientRect();
329
392
  const pageEl = closestHtmlElement(spanEl, ".layout-page");
330
393
  const pageIndex = pageEl ? Number(pageEl.dataset["pageNumber"] || 1) - 1 : 0;
@@ -338,6 +401,17 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
338
401
  };
339
402
  }
340
403
  }
404
+ const gapTarget = findParagraphGapCaretTarget(container, pmPos);
405
+ if (gapTarget) {
406
+ const pageEl = closestHtmlElement(gapTarget.span, ".layout-page");
407
+ const pageIndex = pageEl ? Number(pageEl.dataset["pageNumber"] || 1) - 1 : 0;
408
+ return {
409
+ x: gapTarget.geometry.left - overlayRect.left,
410
+ y: gapTarget.geometry.top - overlayRect.top,
411
+ height: gapTarget.geometry.height,
412
+ pageIndex
413
+ };
414
+ }
341
415
  const paragraphs = htmlQueryAll(container, ".layout-paragraph");
342
416
  for (const pEl of paragraphs) {
343
417
  const pStart = Number(pEl.dataset["pmStart"]);
@@ -360,4 +434,4 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
360
434
  return null;
361
435
  }
362
436
  //#endregion
363
- export { clickToPositionDom, findCollapsedLineEdgeCaretTarget, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
437
+ export { clickToPositionDom, findCollapsedLineEdgeCaretTarget, findParagraphGapCaretTarget, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
@@ -0,0 +1,24 @@
1
+ //#region src/layout-bridge/dom/textStreamDom.d.ts
2
+ type TextBoundary = {
3
+ node: Text;
4
+ offset: number;
5
+ };
6
+ type PaintedSpanRangeMeasurement = {
7
+ type: "outside";
8
+ } | {
9
+ type: "element";
10
+ rect: DOMRect;
11
+ } | {
12
+ type: "text";
13
+ rects: DOMRect[];
14
+ };
15
+ /** Treat every descendant text node as one logical character stream. */
16
+ declare const descendantTextNodes: (root: Node) => Text[];
17
+ declare const totalTextLength: (nodes: readonly Text[]) => number;
18
+ declare const textBoundaryAt: (nodes: readonly Text[], offset: number, edge: "start" | "end") => TextBoundary | null;
19
+ declare const logicalTextOffset: (nodes: readonly Text[], target: Node, offset: number) => number | null;
20
+ declare const createTextStreamRange: (element: HTMLElement, startOffset: number, endOffset: number) => Range | null;
21
+ /** Measure the intersecting portion of one painted position span. */
22
+ declare const measurePaintedSpanRange: (element: HTMLElement, from: number, to: number) => PaintedSpanRangeMeasurement;
23
+ //#endregion
24
+ export { PaintedSpanRangeMeasurement, TextBoundary, createTextStreamRange, descendantTextNodes, logicalTextOffset, measurePaintedSpanRange, textBoundaryAt, totalTextLength };
@@ -0,0 +1,75 @@
1
+ //#region src/layout-bridge/dom/textStreamDom.ts
2
+ const TEXT_NODE_TYPE = 3;
3
+ const isTextNode = (node) => node.nodeType === TEXT_NODE_TYPE;
4
+ /** Treat every descendant text node as one logical character stream. */
5
+ const descendantTextNodes = (root) => {
6
+ const nodes = [];
7
+ const visit = (node) => {
8
+ if (isTextNode(node)) {
9
+ nodes.push(node);
10
+ return;
11
+ }
12
+ for (const child of node.childNodes) visit(child);
13
+ };
14
+ visit(root);
15
+ return nodes;
16
+ };
17
+ const totalTextLength = (nodes) => {
18
+ let length = 0;
19
+ for (const node of nodes) length += node.length;
20
+ return length;
21
+ };
22
+ const textBoundaryAt = (nodes, offset, edge) => {
23
+ let consumed = 0;
24
+ for (const [index, node] of nodes.entries()) {
25
+ const next = consumed + node.length;
26
+ if (offset < next || edge === "end" && offset === next || index === nodes.length - 1) return {
27
+ node,
28
+ offset: Math.max(0, Math.min(node.length, offset - consumed))
29
+ };
30
+ consumed = next;
31
+ }
32
+ return null;
33
+ };
34
+ const logicalTextOffset = (nodes, target, offset) => {
35
+ let consumed = 0;
36
+ for (const text of nodes) {
37
+ if (text === target) return consumed + Math.max(0, Math.min(text.length, offset));
38
+ consumed += text.length;
39
+ }
40
+ return null;
41
+ };
42
+ const createTextStreamRange = (element, startOffset, endOffset) => {
43
+ const nodes = descendantTextNodes(element);
44
+ const length = totalTextLength(nodes);
45
+ const boundedStart = Math.max(0, Math.min(length, startOffset));
46
+ const boundedEnd = Math.max(0, Math.min(length, endOffset));
47
+ const start = textBoundaryAt(nodes, boundedStart, "start");
48
+ const end = textBoundaryAt(nodes, boundedEnd, boundedStart === boundedEnd ? "start" : "end");
49
+ if (!start || !end) return null;
50
+ const range = element.ownerDocument.createRange();
51
+ range.setStart(start.node, start.offset);
52
+ range.setEnd(end.node, end.offset);
53
+ return range;
54
+ };
55
+ /** Measure the intersecting portion of one painted position span. */
56
+ const measurePaintedSpanRange = (element, from, to) => {
57
+ const pmStart = Number(element.dataset["pmStart"]);
58
+ const pmEnd = Number(element.dataset["pmEnd"]);
59
+ if (!Number.isFinite(pmStart) || !Number.isFinite(pmEnd) || pmEnd <= from || pmStart >= to) return { type: "outside" };
60
+ if (element.classList.contains("layout-run-tab")) return {
61
+ type: "element",
62
+ rect: element.getBoundingClientRect()
63
+ };
64
+ const textNodes = descendantTextNodes(element);
65
+ const startOffset = Math.max(0, from - pmStart);
66
+ const endOffset = Math.min(totalTextLength(textNodes), to - pmStart);
67
+ if (startOffset >= endOffset) return { type: "outside" };
68
+ const range = createTextStreamRange(element, startOffset, endOffset);
69
+ return range ? {
70
+ type: "text",
71
+ rects: Array.from(range.getClientRects())
72
+ } : { type: "outside" };
73
+ };
74
+ //#endregion
75
+ export { createTextStreamRange, descendantTextNodes, logicalTextOffset, measurePaintedSpanRange, textBoundaryAt, totalTextLength };
@@ -1,4 +1,5 @@
1
1
  import { getCollapsedLineEdgeCaretGeometry } from "./dom/clickToPositionDom.js";
2
+ import { createTextStreamRange, descendantTextNodes, totalTextLength } from "./dom/textStreamDom.js";
2
3
  //#region src/layout-bridge/headerFooterLayout.ts
3
4
  const hfDomCache = {
4
5
  header: null,
@@ -75,27 +76,15 @@ function computeHfCaretRectFromView(view, section, doc = globalThis.document) {
75
76
  left: collapsedGeometry.left,
76
77
  height: collapsedGeometry.height
77
78
  };
78
- const range = host.ownerDocument.createRange();
79
- const walker = host.ownerDocument.createTreeWalker(span, NodeFilter.SHOW_TEXT);
80
- let remaining = pmPos - start;
81
- let textNode = walker.nextNode();
82
- while (textNode) {
83
- const len = textNode.data.length;
84
- if (remaining <= len) {
85
- try {
86
- range.setStart(textNode, remaining);
87
- range.setEnd(textNode, remaining);
88
- const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
89
- if (rect && rect.height > 0) return {
90
- top: rect.top,
91
- left: rect.left,
92
- height: rect.height
93
- };
94
- } catch {}
95
- break;
96
- }
97
- remaining -= len;
98
- textNode = walker.nextNode();
79
+ const offset = Math.min(pmPos - start, totalTextLength(descendantTextNodes(span)));
80
+ const range = createTextStreamRange(span, offset, offset);
81
+ if (range) {
82
+ const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
83
+ if (rect && rect.height > 0) return {
84
+ top: rect.top,
85
+ left: rect.left,
86
+ height: rect.height
87
+ };
99
88
  }
100
89
  const spanRect = span.getBoundingClientRect();
101
90
  const ratio = (pmPos - start) / Math.max(1, end - start);
@@ -184,7 +173,7 @@ function computeHfSelectionRectsFromView(view, section, doc = globalThis.documen
184
173
  const out = [];
185
174
  const snapshot = getHfDomSnapshot(section, doc);
186
175
  if (!snapshot) return out;
187
- const { host, spans } = snapshot;
176
+ const { spans } = snapshot;
188
177
  for (const spanEl of spans) {
189
178
  const pmStart = Number(spanEl.dataset["pmStart"]);
190
179
  const pmEnd = Number(spanEl.dataset["pmEnd"]);
@@ -200,16 +189,13 @@ function computeHfSelectionRectsFromView(view, section, doc = globalThis.documen
200
189
  });
201
190
  continue;
202
191
  }
203
- let textNode = null;
204
- if (spanEl.firstChild?.nodeType === Node.TEXT_NODE) textNode = spanEl.firstChild;
205
- else if (spanEl.firstChild?.nodeType === Node.ELEMENT_NODE && spanEl.firstChild.tagName === "A" && spanEl.firstChild.firstChild?.nodeType === Node.TEXT_NODE) textNode = spanEl.firstChild.firstChild;
206
- if (!textNode) continue;
192
+ const textNodes = descendantTextNodes(spanEl);
193
+ if (textNodes.length === 0) continue;
207
194
  const startChar = Math.max(0, from - pmStart);
208
- const endChar = Math.min(textNode.length, to - pmStart);
195
+ const endChar = Math.min(totalTextLength(textNodes), to - pmStart);
209
196
  if (startChar >= endChar) continue;
210
- const range = host.ownerDocument.createRange();
211
- range.setStart(textNode, startChar);
212
- range.setEnd(textNode, endChar);
197
+ const range = createTextStreamRange(spanEl, startChar, endChar);
198
+ if (!range) continue;
213
199
  for (const rect of Array.from(range.getClientRects())) out.push({
214
200
  top: rect.top,
215
201
  left: rect.left,
@@ -894,7 +894,7 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
894
894
  kind: "textBox",
895
895
  blockId: block.id,
896
896
  x,
897
- y: state.topMargin + bandTop,
897
+ y: block.position?.vertical?.relativeTo === "page" ? sectionMarginTop + bandTop : state.topMargin + bandTop,
898
898
  width: measure.width,
899
899
  height: measure.height,
900
900
  isPositioned: true,
@@ -916,12 +916,21 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
916
916
  const vertical = block.position.vertical;
917
917
  const anchorBlockId = readTextBoxAnchorBlockId(block);
918
918
  const anchorParagraph = vertical?.relativeTo === "paragraph" && typeof anchorBlockId === "string" ? state.page.fragments.find((fragment) => fragment.kind === "paragraph" && fragment.blockId === anchorBlockId) : void 0;
919
- const y = isPageFrameRelativeAnchor(vertical?.relativeTo) ? state.topMargin + bandTopContentY(vertical, {
919
+ const bandGeometry = {
920
920
  pageHeight: sectionPageHeight,
921
921
  marginTop: sectionMarginTop,
922
922
  marginBottom: sectionMarginBottom,
923
923
  boxHeight: measure.height
924
- }) : (anchorParagraph?.y ?? state.cursorY) + emuToPixels(vertical?.posOffset ?? 0);
924
+ };
925
+ let y;
926
+ if (vertical?.relativeTo === "page") y = sectionMarginTop + bandTopContentY(vertical, bandGeometry);
927
+ else if (isPageFrameRelativeAnchor(vertical?.relativeTo)) y = state.topMargin + bandTopContentY(vertical, {
928
+ pageHeight: sectionPageHeight,
929
+ marginTop: sectionMarginTop,
930
+ marginBottom: sectionMarginBottom,
931
+ boxHeight: measure.height
932
+ });
933
+ else y = (anchorParagraph?.y ?? state.cursorY) + emuToPixels(vertical?.posOffset ?? 0);
925
934
  const fragment = {
926
935
  kind: "textBox",
927
936
  blockId: block.id,
@@ -0,0 +1,17 @@
1
+ import { CellBorderSpec, TableBlock, TableFragment, TableMeasure } from "../types.js";
2
+ import { TableCellPlacements } from "./tableCellGrid.js";
3
+ //#region src/layout-engine/measure/tableFragmentBorderGeometry.d.ts
4
+ type TableFragmentBottomBorder = {
5
+ readonly left: number;
6
+ readonly width: number;
7
+ readonly border: CellBorderSpec;
8
+ };
9
+ type TableFragmentBottomBordersOptions = {
10
+ readonly fragment: TableFragment;
11
+ readonly block: TableBlock;
12
+ readonly measure: TableMeasure;
13
+ readonly placements?: TableCellPlacements;
14
+ };
15
+ declare const tableFragmentBottomBorders: ({ fragment, block, measure, placements: suppliedPlacements }: TableFragmentBottomBordersOptions) => readonly TableFragmentBottomBorder[];
16
+ //#endregion
17
+ export { tableFragmentBottomBorders };
@@ -0,0 +1,24 @@
1
+ import { buildTableCellGrid, buildTableCellPlacements } from "./tableCellGrid.js";
2
+ //#region src/layout-engine/measure/tableFragmentBorderGeometry.ts
3
+ const isVisible = (border) => border !== void 0 && border.style !== "none" && border.style !== "nil";
4
+ const tableFragmentBottomBorders = ({ fragment, block, measure, placements: suppliedPlacements }) => {
5
+ if (fragment.continuesOnNext !== true || fragment.bottomClip === void 0) return [];
6
+ const splitRow = block.rows.at(fragment.toRow - 1);
7
+ if (!splitRow) return [];
8
+ const placements = suppliedPlacements ?? buildTableCellPlacements({
9
+ grid: buildTableCellGrid(block.rows, measure.columnWidths.length),
10
+ columnWidths: measure.columnWidths,
11
+ bidi: block.bidi === true
12
+ });
13
+ return splitRow.cells.flatMap((cell) => {
14
+ const placement = placements.get(cell);
15
+ const border = cell.borders?.bottom;
16
+ return placement && isVisible(border) ? [{
17
+ left: placement.left,
18
+ width: placement.width,
19
+ border
20
+ }] : [];
21
+ });
22
+ };
23
+ //#endregion
24
+ export { tableFragmentBottomBorders };
@@ -34,7 +34,7 @@ const reconcileBreakBeforeBlock = ({ state, block, previousBlock, page, blocksBy
34
34
  state
35
35
  };
36
36
  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));
37
- const forcePageBreak = (renderedBreakNeedsSnap || block.attrs?.keepNext === true || (block.attrs?.spacing?.before ?? 0) > 0 && previousBlock?.kind !== "sectionBreak" || previousBlock?.kind === "table") && !markerAlreadySatisfied && pageHasVisibleBodyContent(page, blocksById);
37
+ const forcePageBreak = (renderedBreakNeedsSnap || (block.attrs?.spacing?.before ?? 0) > 0 && block.attrs?.keepNext !== true && previousBlock?.kind !== "sectionBreak" || previousBlock?.kind === "table") && !markerAlreadySatisfied && pageHasVisibleBodyContent(page, blocksById);
38
38
  const followsAuthoredPageBreak = previousBlock?.kind === "pageBreak";
39
39
  const followsSectionBreak = previousBlock?.kind === "sectionBreak";
40
40
  const followsPageBreakingSection = followsSectionBreak && previousBlock.type !== "continuous";
@@ -748,6 +748,13 @@ type PageHeaderFooterRefs = {
748
748
  type PageBreakBlock = {
749
749
  kind: "pageBreak";
750
750
  id: BlockId;
751
+ /** Revision metadata when the structural boundary is newly authored. */
752
+ isInsertion?: boolean;
753
+ changeAuthor?: string;
754
+ changeDate?: string;
755
+ changeRevisionId?: number;
756
+ isSuggestion?: boolean;
757
+ suggestionId?: string;
751
758
  pmStart?: number;
752
759
  pmEnd?: number;
753
760
  };
@@ -1,5 +1,5 @@
1
- import { FlowBlock, Measure } from "../layout-engine/types.js";
2
1
  import { ImageAttrs } from "../prosemirror/schema/nodes.js";
2
+ import { FlowBlock, Measure } from "../layout-engine/types.js";
3
3
  import { BlockLookup } from "./index.js";
4
4
  import { WrapType } from "../types/wrap.js";
5
5
  //#region src/layout-painter/imageLayout.d.ts
@@ -2,6 +2,7 @@ import { measureParagraph } from "../layout-engine/measure/measureParagraph.js";
2
2
  import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "../layout-engine/measure/tableCellFloating.js";
3
3
  import { createTableCellFlowState, finishTableCellFlow, placeTableCellBlock } from "../layout-engine/measure/tableCellFlow.js";
4
4
  import { buildTableCellGrid, buildTableCellPlacements, getSourceCellAt } from "../layout-engine/measure/tableCellGrid.js";
5
+ import { tableFragmentBottomBorders } from "../layout-engine/measure/tableFragmentBorderGeometry.js";
5
6
  import { resolveTableInlineOffset, resolveTableInlinePlacement } from "../layout-engine/measure/tableInlinePlacement.js";
6
7
  import { isFloatingImageRun, isFloatingTextBoxBlock, resolveTableCellPadding, tableColumnsArePinned } from "../layout-engine/types.js";
7
8
  import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
@@ -727,6 +728,25 @@ function renderTableFragment(fragment, block, measure, context, options = {}) {
727
728
  contentParent.append(rowEl);
728
729
  y += rowMeasure.height;
729
730
  }
731
+ for (const { left, width, border } of tableFragmentBottomBorders({
732
+ fragment,
733
+ block,
734
+ measure,
735
+ placements: cellPlacements
736
+ })) {
737
+ const borderEl = doc.createElement("div");
738
+ borderEl.className = CELL_BOTTOM_BORDER_CLASS;
739
+ borderEl.style.position = "absolute";
740
+ borderEl.style.left = `${left}px`;
741
+ borderEl.style.top = "0";
742
+ borderEl.style.width = `${width}px`;
743
+ borderEl.style.height = `${fragment.height}px`;
744
+ borderEl.style.boxSizing = "border-box";
745
+ borderEl.style.pointerEvents = "none";
746
+ borderEl.style.zIndex = "1";
747
+ applyBorder(borderEl, "bottom", border);
748
+ tableEl.append(borderEl);
749
+ }
730
750
  let handleY = 0;
731
751
  for (let rowIdx = fragment.fromRow; rowIdx < fragment.toRow; rowIdx++) {
732
752
  if (block.rows[rowIdx]?.hidden) continue;
@@ -1,3 +1,4 @@
1
+ import { cloneParagraphWithoutPropertySource } from "../docx/paragraphPropertySource.js";
1
2
  import { renderParagraph } from "./renderParagraph.js";
2
3
  import { renderTable } from "./renderTable.js";
3
4
  //#region src/markdown/renderBlock.ts
@@ -16,17 +17,13 @@ function mergeAcceptedParagraphBreaks(blocks) {
16
17
  for (const block of blocks) {
17
18
  const prev = merged.at(-1);
18
19
  if (prev?.type === "paragraph" && prev.pPrMark?.kind === "del" && block.type === "paragraph") {
19
- const { pPrMark: _resolved, ...base } = prev;
20
20
  const next = block.pPrMark;
21
- const content = [...prev.content, ...block.content];
22
- merged[merged.length - 1] = next ? {
23
- ...base,
24
- content,
25
- pPrMark: next
26
- } : {
27
- ...base,
28
- content
29
- };
21
+ const joined = cloneParagraphWithoutPropertySource(prev, {
22
+ content: [...prev.content, ...block.content],
23
+ ...next ? { pPrMark: next } : {}
24
+ });
25
+ if (!next) Reflect.deleteProperty(joined, "pPrMark");
26
+ merged[merged.length - 1] = joined;
30
27
  continue;
31
28
  }
32
29
  merged.push(block);
package/dist/model.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { document_d_exports } from "./types/document.js";
2
- import { DeriveBlockIdInput, FolioBlockId, currentFolioBlockId, deriveBlankBlockId, deriveBlockId, getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex, isBlankFolioBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
3
2
  import { BORDER_STYLE_VALUES, CONDITIONAL_STYLE_TYPE_VALUES, EMPHASIS_MARK_VALUES, FIELD_TYPE_VALUES, FLOATING_TABLE_X_SPEC_VALUES, FLOATING_TABLE_Y_SPEC_VALUES, FONT_HINT_VALUES, FONT_THEME_VALUES, FRAME_WRAP_VALUES, FRAME_X_ALIGN_VALUES, FRAME_Y_ALIGN_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LEVEL_SUFFIX_VALUES, LINE_SPACING_RULE_VALUES, NUMBER_FORMAT_VALUES, OUTLINE_STYLE_ATTR_VALUES, OUTLINE_STYLE_CSS_ALIASES, OUTLINE_STYLE_CSS_ALIAS_VALUES, OutlineStyleAttr, OutlineStyleCssAlias, PARAGRAPH_ALIGNMENT_VALUES, POSITIONAL_TAB_ALIGNMENT_VALUES, POSITIONAL_TAB_LEADER_VALUES, POSITIONAL_TAB_RELATIVE_TO_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, SHAPE_OUTLINE_STYLE_VALUES, SHAPE_TEXT_ANCHOR_VALUES, SHAPE_TYPE_VALUES, STYLE_TYPE_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES, normalizeShapeTextAnchor } from "./types/documentEnumValues.js";
3
+ import { DeriveBlockIdInput, FolioBlockId, currentFolioBlockId, deriveBlankBlockId, deriveBlockId, getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex, isBlankFolioBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
4
4
  import { types_d_exports } from "./layout-engine/types.js";
5
5
  import { measureTypes_d_exports } from "./layout-engine/measure/measureTypes.js";
6
6
  export * from "@stll/docx-core/model";
@@ -1,4 +1,5 @@
1
1
  import { findBodyPmSpans } from "../layout-bridge/dom/findBodyPmSpans.js";
2
+ import { createTextStreamRange, descendantTextNodes, totalTextLength } from "../layout-bridge/dom/textStreamDom.js";
2
3
  //#region src/paged-layout/rangeProjection.ts
3
4
  /**
4
5
  * Shared range → paged-canvas rectangle projection.
@@ -12,7 +13,6 @@ import { findBodyPmSpans } from "../layout-bridge/dom/findBodyPmSpans.js";
12
13
  * projection only for ranges whose DOM spans aren't mounted yet
13
14
  * (initial paint, off-screen pages).
14
15
  */
15
- const isTextNode = (node) => node?.nodeType === Node.TEXT_NODE;
16
16
  const pageIndexOf = (el) => {
17
17
  const page = el.closest(".layout-page");
18
18
  const raw = page instanceof HTMLElement ? page.dataset["pageNumber"] : void 0;
@@ -35,16 +35,13 @@ const domRectsForRange = (pmSpans, overlayRect, zoom, from, to) => {
35
35
  });
36
36
  continue;
37
37
  }
38
- let textNode = null;
39
- if (isTextNode(spanEl.firstChild)) textNode = spanEl.firstChild;
40
- else if (spanEl.firstChild instanceof HTMLElement && spanEl.firstChild.tagName === "A" && isTextNode(spanEl.firstChild.firstChild)) textNode = spanEl.firstChild.firstChild;
41
- if (!textNode) continue;
38
+ const textNodes = descendantTextNodes(spanEl);
39
+ if (textNodes.length === 0) continue;
42
40
  const startChar = Math.max(0, from - pmStart);
43
- const endChar = Math.min(textNode.length, to - pmStart);
41
+ const endChar = Math.min(totalTextLength(textNodes), to - pmStart);
44
42
  if (startChar >= endChar) continue;
45
- const range = spanEl.ownerDocument.createRange();
46
- range.setStart(textNode, startChar);
47
- range.setEnd(textNode, endChar);
43
+ const range = createTextStreamRange(spanEl, startChar, endChar);
44
+ if (!range) continue;
48
45
  const pageIndex = pageIndexOf(spanEl);
49
46
  for (const rect of Array.from(range.getClientRects())) rects.push({
50
47
  x: (rect.left - overlayRect.left) / zoom,
@@ -2,5 +2,7 @@ import { DirtyRange } from "./incrementalMeasure.js";
2
2
  import { Transaction } from "prosemirror-state";
3
3
  //#region src/paged-layout/transactionDirtyRange.d.ts
4
4
  declare function getTransactionDirtyRange(transaction: Transaction): DirtyRange | null;
5
+ /** Collect every transaction in an applied batch in the final document's coordinates. */
6
+ declare function getTransactionsDirtyRange(transactions: readonly Transaction[]): DirtyRange | null;
5
7
  //#endregion
6
- export { getTransactionDirtyRange };
8
+ export { getTransactionDirtyRange, getTransactionsDirtyRange };