@stll/folio-core 0.32.2 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
  2. package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
  3. package/dist/ai-edits/apply.d.ts +49 -3
  4. package/dist/ai-edits/apply.js +652 -86
  5. package/dist/ai-edits/blockRange.d.ts +7 -3
  6. package/dist/ai-edits/blockRange.js +14 -6
  7. package/dist/ai-edits/headless.d.ts +39 -5
  8. package/dist/ai-edits/headless.js +76 -17
  9. package/dist/ai-edits/index.d.ts +6 -6
  10. package/dist/ai-edits/index.js +3 -3
  11. package/dist/ai-edits/read.d.ts +4 -0
  12. package/dist/ai-edits/read.js +30 -6
  13. package/dist/ai-edits/scoped-reading.js +4 -3
  14. package/dist/ai-edits/snapshot.d.ts +42 -2
  15. package/dist/ai-edits/snapshot.js +127 -62
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
  17. package/dist/ai-edits/table-row-column-mutations.js +98 -6
  18. package/dist/ai-edits/table-targets.d.ts +12 -3
  19. package/dist/ai-edits/table-targets.js +15 -5
  20. package/dist/ai-edits/types.d.ts +219 -15
  21. package/dist/ai-edits/word-diff.d.ts +54 -11
  22. package/dist/ai-edits/word-diff.js +221 -61
  23. package/dist/compare/__fixtures__/body-sequence.d.ts +56 -0
  24. package/dist/compare/__fixtures__/body-sequence.js +136 -0
  25. package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
  26. package/dist/compare/__fixtures__/nested-table.js +50 -0
  27. package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
  28. package/dist/compare/__fixtures__/numbered-list.js +85 -0
  29. package/dist/compare/column-alignment.d.ts +27 -0
  30. package/dist/compare/column-alignment.js +162 -0
  31. package/dist/compare/compare.d.ts +107 -0
  32. package/dist/compare/compare.js +412 -0
  33. package/dist/compare/formatting.d.ts +29 -0
  34. package/dist/compare/formatting.js +84 -0
  35. package/dist/compare/plan.d.ts +22 -0
  36. package/dist/compare/plan.js +1141 -0
  37. package/dist/compare/reproducible-package.d.ts +21 -0
  38. package/dist/compare/reproducible-package.js +47 -0
  39. package/dist/compare/scenario.d.ts +77 -0
  40. package/dist/compare/scenario.js +263 -0
  41. package/dist/compare/types.d.ts +297 -0
  42. package/dist/compare/types.js +42 -0
  43. package/dist/compare/verification.d.ts +83 -0
  44. package/dist/compare/verification.js +189 -0
  45. package/dist/compat/eigenpal.d.ts +9 -6
  46. package/dist/compat/eigenpal.js +6 -3
  47. package/dist/controller/layoutPipeline.d.ts +13 -0
  48. package/dist/controller/layoutPipeline.js +15 -1
  49. package/dist/display-list/build/buildContext.d.ts +39 -0
  50. package/dist/display-list/build/buildContext.js +31 -0
  51. package/dist/display-list/build/buildDisplayList.d.ts +79 -0
  52. package/dist/display-list/build/buildDisplayList.js +283 -0
  53. package/dist/display-list/build/colors.d.ts +20 -0
  54. package/dist/display-list/build/colors.js +136 -0
  55. package/dist/display-list/build/floatingImages.d.ts +24 -0
  56. package/dist/display-list/build/floatingImages.js +41 -0
  57. package/dist/display-list/build/fontTable.d.ts +31 -0
  58. package/dist/display-list/build/fontTable.js +97 -0
  59. package/dist/display-list/build/furniture.d.ts +86 -0
  60. package/dist/display-list/build/furniture.js +98 -0
  61. package/dist/display-list/build/glyphs.d.ts +55 -0
  62. package/dist/display-list/build/glyphs.js +103 -0
  63. package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
  64. package/dist/display-list/build/headerFooterPrimitives.js +40 -0
  65. package/dist/display-list/build/imagePrimitives.d.ts +35 -0
  66. package/dist/display-list/build/imagePrimitives.js +264 -0
  67. package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
  68. package/dist/display-list/build/pageBorderPrimitives.js +111 -0
  69. package/dist/display-list/build/pageFurniture.d.ts +35 -0
  70. package/dist/display-list/build/pageFurniture.js +141 -0
  71. package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
  72. package/dist/display-list/build/paragraphPrimitives.js +819 -0
  73. package/dist/display-list/build/regions.d.ts +46 -0
  74. package/dist/display-list/build/regions.js +56 -0
  75. package/dist/display-list/build/storyPrimitives.d.ts +29 -0
  76. package/dist/display-list/build/storyPrimitives.js +279 -0
  77. package/dist/display-list/build/strokes.d.ts +19 -0
  78. package/dist/display-list/build/strokes.js +78 -0
  79. package/dist/display-list/build/tablePrimitives.d.ts +38 -0
  80. package/dist/display-list/build/tablePrimitives.js +409 -0
  81. package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
  82. package/dist/display-list/build/textBoxPrimitives.js +129 -0
  83. package/dist/display-list/build/textDecorations.d.ts +29 -0
  84. package/dist/display-list/build/textDecorations.js +29 -0
  85. package/dist/display-list/build/unsupported.d.ts +57 -0
  86. package/dist/display-list/build/unsupported.js +0 -0
  87. package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
  88. package/dist/display-list/build/watermarkPrimitives.js +128 -0
  89. package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
  90. package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
  91. package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
  92. package/dist/display-list/editor/displayListPagePainter.js +63 -0
  93. package/dist/display-list/editor/pageRenderer.d.ts +24 -0
  94. package/dist/display-list/editor/pageRenderer.js +36 -0
  95. package/dist/display-list/primitives.d.ts +95 -0
  96. package/dist/display-list/primitives.js +127 -0
  97. package/dist/display-list/types.d.ts +472 -0
  98. package/dist/display-list/types.js +0 -0
  99. package/dist/document-operations.d.ts +39 -8
  100. package/dist/document-operations.js +169 -16
  101. package/dist/document-stories.d.ts +16 -0
  102. package/dist/document-stories.js +48 -4
  103. package/dist/docx/hyperlinkParser.d.ts +9 -1
  104. package/dist/docx/hyperlinkParser.js +19 -13
  105. package/dist/docx/packageParts.d.ts +35 -0
  106. package/dist/docx/packageParts.js +120 -0
  107. package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
  108. package/dist/docx/paraIdRangeNormalization.js +64 -0
  109. package/dist/docx/paragraphParser.js +101 -26
  110. package/dist/docx/paragraphTraversal.js +1 -1
  111. package/dist/docx/renderedPageBreakNormalization.js +3 -1
  112. package/dist/docx/revisionIdNormalization.d.ts +27 -0
  113. package/dist/docx/revisionIdNormalization.js +121 -0
  114. package/dist/docx/rezip.d.ts +6 -0
  115. package/dist/docx/rezip.js +51 -19
  116. package/dist/docx/selectiveSave.js +2 -2
  117. package/dist/docx/serializer/commentSerializer.js +6 -1
  118. package/dist/docx/serializer/paragraphSerializer.js +73 -20
  119. package/dist/docx/serializer/runSerializer.js +11 -10
  120. package/dist/docx/serializer/tableSerializer.js +26 -10
  121. package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
  122. package/dist/docx/server/createBilingualDocx.js +3 -2
  123. package/dist/docx/streamingXmlParser.d.ts +13 -1
  124. package/dist/docx/streamingXmlParser.js +49 -11
  125. package/dist/docx/unzip.d.ts +1 -2
  126. package/dist/docx/unzip.js +25 -6
  127. package/dist/docx/xmlParser.d.ts +3 -1
  128. package/dist/docx/xmlParser.js +33 -27
  129. package/dist/export-pdf.d.ts +43 -0
  130. package/dist/export-pdf.js +98 -0
  131. package/dist/fonts/headlessMeasure.d.ts +76 -0
  132. package/dist/fonts/headlessMeasure.js +0 -0
  133. package/dist/fonts/sfnt/parse.d.ts +51 -0
  134. package/dist/fonts/sfnt/parse.js +525 -0
  135. package/dist/fonts/sfnt/subset.d.ts +20 -0
  136. package/dist/fonts/sfnt/subset.js +350 -0
  137. package/dist/fonts/sfnt/tables.d.ts +88 -0
  138. package/dist/fonts/sfnt/tables.js +131 -0
  139. package/dist/fonts/sfnt/woff.d.ts +14 -0
  140. package/dist/fonts/sfnt/woff.js +163 -0
  141. package/dist/generated/text_shaper.js +324 -0
  142. package/dist/generated/text_shaper_bg.wasm +0 -0
  143. package/dist/headless-layout.d.ts +75 -0
  144. package/dist/headless-layout.js +350 -0
  145. package/dist/index.d.ts +9 -6
  146. package/dist/index.js +6 -3
  147. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  148. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  149. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  150. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  151. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
  152. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
  153. package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
  154. package/dist/layout-engine/measure/advanceComposition.js +148 -0
  155. package/dist/layout-engine/measure/cache.d.ts +2 -0
  156. package/dist/layout-engine/measure/measureContainer.js +13 -0
  157. package/dist/layout-engine/measure/measureParagraph.js +1 -2
  158. package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
  159. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
  160. package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
  161. package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
  162. package/dist/layout-engine/tableIndentCompatibility.js +15 -0
  163. package/dist/layout-engine/types.d.ts +8 -0
  164. package/dist/layout-painter/renderPage.d.ts +29 -1
  165. package/dist/layout-painter/renderPage.js +47 -19
  166. package/dist/layout-painter/renderParagraph.d.ts +21 -2
  167. package/dist/layout-painter/renderParagraph.js +1 -1
  168. package/dist/layout-painter/renderTable.js +37 -4
  169. package/dist/managers/editorShortcuts.d.ts +33 -1
  170. package/dist/managers/editorShortcuts.js +20 -1
  171. package/dist/markdown/renderRuns.js +7 -2
  172. package/dist/markdown/renderTable.js +5 -1
  173. package/dist/model.d.ts +3 -3
  174. package/dist/model.js +2 -2
  175. package/dist/pdf/contentStream.d.ts +73 -0
  176. package/dist/pdf/contentStream.js +143 -0
  177. package/dist/pdf/fonts.d.ts +96 -0
  178. package/dist/pdf/fonts.js +593 -0
  179. package/dist/pdf/images.d.ts +28 -0
  180. package/dist/pdf/images.js +439 -0
  181. package/dist/pdf/objects.d.ts +93 -0
  182. package/dist/pdf/objects.js +237 -0
  183. package/dist/pdf/pageSpace.d.ts +43 -0
  184. package/dist/pdf/pageSpace.js +57 -0
  185. package/dist/pdf/paint.d.ts +32 -0
  186. package/dist/pdf/paint.js +442 -0
  187. package/dist/pdf/writePdf.d.ts +56 -0
  188. package/dist/pdf/writePdf.js +345 -0
  189. package/dist/prosemirror/commands/comments.js +96 -6
  190. package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
  191. package/dist/prosemirror/conversion/toProseDoc.js +41 -4
  192. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
  193. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
  194. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
  195. package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
  196. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
  197. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
  198. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
  199. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
  200. package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
  201. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
  202. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
  203. package/dist/prosemirror/plugins/suggestionMode.js +1 -0
  204. package/dist/prosemirror/revisionCarriers.js +19 -2
  205. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
  206. package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
  207. package/dist/prosemirror/validation.js +55 -27
  208. package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
  209. package/dist/prosemirror/zeroWidthAnchors.js +23 -0
  210. package/dist/redline.js +12 -79
  211. package/dist/server.d.ts +3 -3
  212. package/dist/server.js +2 -2
  213. package/dist/shaping/placeRun.d.ts +51 -0
  214. package/dist/shaping/placeRun.js +84 -0
  215. package/dist/shaping/shaper.d.ts +92 -0
  216. package/dist/shaping/shaper.js +102 -0
  217. package/dist/types/block-id.d.ts +18 -2
  218. package/dist/types/block-id.js +49 -5
  219. package/dist/types/content.d.ts +2 -2
  220. package/dist/utils/clipboard.js +4 -3
  221. package/dist/utils/fontResolver.js +183 -0
  222. package/dist/utils/htmlComments.d.ts +17 -0
  223. package/dist/utils/htmlComments.js +22 -0
  224. package/dist/utils/scriptSegments.d.ts +12 -1
  225. package/dist/utils/scriptSegments.js +17 -1
  226. package/dist/version-comparison.d.ts +1 -1
  227. package/package.json +7 -3
@@ -1,7 +1,46 @@
1
- import { deriveBlockId } from "../types/block-id.js";
1
+ import { deriveBlankBlockId, deriveBlockId } from "../types/block-id.js";
2
2
  import { buildCleanBlockText } from "./clean-text.js";
3
+ import { TableMap } from "prosemirror-tables";
3
4
  //#region src/ai-edits/snapshot.ts
4
5
  const normalizeFolioAIBlockText = (text) => text.replace(/\s+/gu, " ").trim();
6
+ /**
7
+ * Whether a block carries text a reader would see.
8
+ *
9
+ * The snapshot holds every paragraph, blank ones included: an empty cell is
10
+ * part of a table's shape, an empty row is a row, and a comparison that cannot
11
+ * address them cannot describe what changed around them. A reading surface
12
+ * wants the opposite — a model shown a document should see its content, not a
13
+ * line per blank paragraph.
14
+ *
15
+ * So the two needs are split here rather than in the walk: the snapshot is
16
+ * complete, and every surface that reads it for a person or a model states
17
+ * that it wants content by calling this. One helper, so "what counts as
18
+ * content" has a single answer; five inline emptiness checks would drift.
19
+ */
20
+ const isFolioAIContentBlock = ({ text }) => normalizeFolioAIBlockText(text).length > 0;
21
+ /**
22
+ * The block that content appended to the end of a story hangs from: the last
23
+ * paragraph at body level.
24
+ *
25
+ * Not simply the last block. A story's last block is often inside a table, and
26
+ * a paragraph cannot be appended after one: the insertion escapes to the
27
+ * table's boundary, where the block it was anchored to is not adjacent to it
28
+ * and its paragraph mark ends a different container's paragraph. Neither
29
+ * container-edge rule then applies and the addition cannot be rejected
30
+ * cleanly.
31
+ *
32
+ * A body always ends with a paragraph — a table may not be the last child of a
33
+ * body or a cell, so a package that ends in a table carries a trailing, often
34
+ * empty, paragraph after it — so this is only `null` for a story with no
35
+ * body-level paragraph at all, which is a malformed document.
36
+ */
37
+ const trailingBodyBlockId = ({ blocks }) => {
38
+ for (let index = blocks.length - 1; index >= 0; index--) {
39
+ const block = blocks[index];
40
+ if (block !== void 0 && block.table === void 0) return block.id;
41
+ }
42
+ return null;
43
+ };
5
44
  const hashFolioAIBlockText = (text) => {
6
45
  let hash = 5381;
7
46
  for (const character of text) hash = (hash * 33 + (character.codePointAt(0) ?? 0)) % 2147483647;
@@ -19,63 +58,102 @@ const createFolioAITextRangeHandle = ({ blockId, text, startOffset, endOffset })
19
58
  };
20
59
  };
21
60
  const TABLE_ROW_NODE_NAME = "tableRow";
61
+ const TABLE_ROLE_TABLE = "table";
62
+ const TABLE_ROLE_ROW = "row";
22
63
  /**
23
- * True when `pos`'s nearest `tableRow` ancestor is marked `hidden` (OOXML
24
- * `w:trPr/w:hidden` — Word never renders the row). A textblock inside such a
25
- * row must not surface its text to the AI/agent snapshot: an attacker DOCX
26
- * could otherwise smuggle prompt-injection instructions into a row that no
27
- * human ever sees.
64
+ * True for a `tableRow` marked `hidden` (OOXML `w:trPr/w:hidden` — Word never
65
+ * renders the row). Nothing inside such a row may surface its text to the
66
+ * AI/agent snapshot: an attacker DOCX could otherwise smuggle prompt-injection
67
+ * instructions into a row that no human ever sees. The walk skips the row's
68
+ * whole subtree, so a table nested inside a hidden row is hidden with it.
28
69
  */
29
- const isInHiddenTableRow = (doc, pos) => {
30
- const $pos = doc.resolve(pos);
31
- for (let depth = $pos.depth; depth > 0; depth--) {
32
- const ancestor = $pos.node(depth);
33
- if (ancestor.type.name === TABLE_ROW_NODE_NAME) return ancestor.attrs["hidden"] === true;
70
+ const isHiddenTableRow = (node) => node.type.name === TABLE_ROW_NODE_NAME && node.attrs["hidden"] === true;
71
+ /**
72
+ * Locate the block inside its innermost enclosing table, or `undefined` when
73
+ * it sits outside every table.
74
+ */
75
+ const getTableLocation = ({ path, blockIndex, tableIndexByStart }) => {
76
+ for (let cellDepth = path.length - 1; cellDepth > 1; cellDepth--) {
77
+ const cell = path[cellDepth];
78
+ if (!cell) continue;
79
+ const role = cell.node.type.spec["tableRole"];
80
+ if (role !== "cell" && role !== "header_cell") continue;
81
+ const row = path[cellDepth - 1];
82
+ const table = path[cellDepth - 2];
83
+ if (!row || !table || row.node.type.spec["tableRole"] !== TABLE_ROLE_ROW || table.node.type.spec["tableRole"] !== TABLE_ROLE_TABLE) return;
84
+ const tableIndex = tableIndexByStart.get(table.start);
85
+ const outerTable = path.find((entry) => entry.node.type.spec["tableRole"] === TABLE_ROLE_TABLE);
86
+ const outerTableIndex = outerTable === void 0 ? void 0 : tableIndexByStart.get(outerTable.start);
87
+ if (tableIndex === void 0 || outerTableIndex === void 0) return;
88
+ const rectangle = TableMap.get(table.node).findCell(cell.start - table.start - 1);
89
+ return {
90
+ outerTableIndex,
91
+ tableIndex,
92
+ rowIndex: row.index,
93
+ cellIndex: cell.index,
94
+ gridColumnIndex: rectangle.left,
95
+ columnSpan: rectangle.right - rectangle.left,
96
+ rowSpan: rectangle.bottom - rectangle.top,
97
+ paragraphIndex: blockIndex
98
+ };
34
99
  }
35
- return false;
36
100
  };
37
101
  const createFolioAIEditSnapshot = (doc) => {
38
102
  const draftBlocks = [];
39
103
  const hashCounts = /* @__PURE__ */ new Map();
40
104
  const usedBlockIds = /* @__PURE__ */ new Set();
41
- const emptyAnchorState = {
42
- candidate: null,
43
- textblockCount: 0
44
- };
105
+ const tableIndexByStart = /* @__PURE__ */ new Map();
106
+ const path = [];
45
107
  let blockIndex = 0;
46
- doc.descendants((node, pos, parent) => {
47
- if (!node.isTextblock) return true;
48
- if (isInHiddenTableRow(doc, pos)) return false;
49
- const { text } = buildCleanBlockText(node, pos);
50
- const normalizedText = normalizeFolioAIBlockText(text);
51
- if (normalizedText.length === 0) {
52
- if (parent?.type !== doc.type) return true;
53
- emptyAnchorState.textblockCount++;
54
- if (emptyAnchorState.candidate === null) {
55
- const paraIdAttr = node.attrs["paraId"];
56
- emptyAnchorState.candidate = {
57
- from: pos,
58
- to: pos + node.nodeSize,
59
- paraId: typeof paraIdAttr === "string" && paraIdAttr.length > 0 ? paraIdAttr : null
60
- };
61
- }
108
+ let blankIndex = 0;
109
+ doc.descendants((node, pos, _parent, index) => {
110
+ while (pos >= (path.at(-1)?.end ?? Number.POSITIVE_INFINITY)) path.pop();
111
+ if (!node.isTextblock) {
112
+ if (isHiddenTableRow(node)) return false;
113
+ if (node.type.spec["tableRole"] === TABLE_ROLE_TABLE) tableIndexByStart.set(pos, tableIndexByStart.size);
114
+ if (!node.isLeaf) path.push({
115
+ node,
116
+ start: pos,
117
+ end: pos + node.nodeSize,
118
+ index
119
+ });
62
120
  return true;
63
121
  }
122
+ const { text } = buildCleanBlockText(node, pos);
123
+ const normalizedText = normalizeFolioAIBlockText(text);
64
124
  const textHash = hashFolioAIBlockText(normalizedText);
65
125
  hashCounts.set(textHash, (hashCounts.get(textHash) ?? 0) + 1);
66
- blockIndex++;
67
126
  const paraIdAttr = node.attrs["paraId"];
68
- const id = deriveBlockId({
69
- paraId: typeof paraIdAttr === "string" && paraIdAttr.length > 0 ? paraIdAttr : null,
70
- index: blockIndex,
71
- taken: usedBlockIds
72
- });
127
+ const paraId = typeof paraIdAttr === "string" && paraIdAttr.length > 0 ? paraIdAttr : null;
128
+ const isBlank = normalizedText.length === 0;
129
+ let id;
130
+ if (isBlank) {
131
+ blankIndex++;
132
+ id = deriveBlankBlockId({
133
+ paraId,
134
+ index: blankIndex,
135
+ taken: usedBlockIds
136
+ });
137
+ } else {
138
+ blockIndex++;
139
+ id = deriveBlockId({
140
+ paraId,
141
+ index: blockIndex,
142
+ taken: usedBlockIds
143
+ });
144
+ }
73
145
  usedBlockIds.add(id);
74
146
  const headingLevel = getHeadingLevel(node);
75
147
  const kind = getBlockKind(node, headingLevel);
76
148
  const displayLabel = getDisplayLabel(node);
77
149
  const styleId = getStyleId(node);
150
+ const listLevel = getListLevel(node);
78
151
  const previewRuns = getPreviewRuns(node);
152
+ const table = getTableLocation({
153
+ path,
154
+ blockIndex: index,
155
+ tableIndexByStart
156
+ });
79
157
  draftBlocks.push({
80
158
  block: {
81
159
  id,
@@ -84,7 +162,9 @@ const createFolioAIEditSnapshot = (doc) => {
84
162
  ...headingLevel !== void 0 && { headingLevel },
85
163
  ...displayLabel !== void 0 && { displayLabel },
86
164
  ...styleId !== void 0 && { styleId },
87
- ...previewRuns !== void 0 && { previewRuns }
165
+ ...listLevel !== void 0 && { listLevel },
166
+ ...previewRuns !== void 0 && { previewRuns },
167
+ ...table !== void 0 && { table }
88
168
  },
89
169
  anchor: {
90
170
  id,
@@ -106,30 +186,9 @@ const createFolioAIEditSnapshot = (doc) => {
106
186
  hashOccurrenceCount: hashCounts.get(draft.anchor.textHash) ?? 0
107
187
  };
108
188
  }
109
- const emptyAnchorCandidate = emptyAnchorState.candidate;
110
- if (blocks.length > 0 || emptyAnchorCandidate === null) return {
111
- blocks,
112
- anchors
113
- };
114
- const emptyDocumentAnchorId = deriveBlockId({
115
- paraId: emptyAnchorCandidate.paraId,
116
- index: 1,
117
- taken: usedBlockIds
118
- });
119
- const normalizedText = "";
120
- anchors[emptyDocumentAnchorId] = {
121
- id: emptyDocumentAnchorId,
122
- from: emptyAnchorCandidate.from,
123
- to: emptyAnchorCandidate.to,
124
- text: "",
125
- normalizedText,
126
- textHash: hashFolioAIBlockText(normalizedText),
127
- hashOccurrenceCount: emptyAnchorState.textblockCount
128
- };
129
189
  return {
130
190
  blocks,
131
- anchors,
132
- emptyDocumentAnchorId
191
+ anchors
133
192
  };
134
193
  };
135
194
  const getBlockKind = (node, headingLevel) => {
@@ -153,6 +212,12 @@ const getDisplayLabel = (node) => {
153
212
  const styleId = node.attrs["styleId"];
154
213
  if (typeof styleId === "string" && /^heading/iu.test(styleId)) return styleId;
155
214
  };
215
+ const getListLevel = (node) => {
216
+ const numPr = node.attrs["numPr"];
217
+ if (typeof numPr !== "object" || numPr === null || !("ilvl" in numPr)) return;
218
+ const { ilvl } = numPr;
219
+ return typeof ilvl === "number" && Number.isInteger(ilvl) && ilvl >= 0 ? ilvl : void 0;
220
+ };
156
221
  const getStyleId = (node) => {
157
222
  const styleId = node.attrs["styleId"];
158
223
  return typeof styleId === "string" && styleId.length > 0 ? styleId : void 0;
@@ -267,4 +332,4 @@ const getColorFromAttrs = (attrs) => {
267
332
  const samePreviewRunStyle = (run, style) => run.bold === style.bold && run.italic === style.italic && run.underline === style.underline && run.strike === style.strike && run.fontFamily === style.fontFamily && run.fontSizePt === style.fontSizePt && run.color === style.color;
268
333
  const isUnstyledPreviewRun = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
269
334
  //#endregion
270
- export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText };
335
+ export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
@@ -9,6 +9,17 @@ type TableRowInsertion = {
9
9
  rowType: Node["type"];
10
10
  cells: readonly Node[];
11
11
  rowspanUpdates: readonly number[];
12
+ /**
13
+ * The table's column count, which is not `cells.length`. One cell can occupy
14
+ * several columns (a `colspan`), and one column can have no cell in this row
15
+ * at all (a `rowspan` from a row above reaches down into it).
16
+ *
17
+ * It is what `cellTexts` is SIZED against — the most cells any row of this
18
+ * table could have — while the texts themselves fill the new row's cells in
19
+ * order. Sizing against `cells.length` refused a row the table can hold
20
+ * whenever a span made this row narrower than the table.
21
+ */
22
+ columnCount: number;
12
23
  };
13
24
  type TableRowDeletion = Omit<TableRowTarget, "table">;
14
25
  type TableColumnInsertion = {
@@ -74,5 +85,17 @@ type ApplyTableRowDeletionOptions = {
74
85
  revision: TableStructureRevision | null;
75
86
  };
76
87
  declare const applyTableRowDeletion: ({ tr, deletion, revision }: ApplyTableRowDeletionOptions) => TableRowColumnMutationResult;
88
+ /**
89
+ * Split one cell's text into the paragraphs it describes, one string each.
90
+ *
91
+ * A cell holds paragraphs, not lines: a line break starts a new one, and a
92
+ * blank line is a blank paragraph, exactly as `text: ""` is for an insertion.
93
+ *
94
+ * This is deliberately not the prose rule `insertAfterBlock` follows, where a
95
+ * blank line between two model-written clauses is formatting noise and is
96
+ * dropped. A cell's text is read off a cell that HAS those paragraphs, so
97
+ * dropping one would lose a block that was there.
98
+ */
99
+ declare const splitCellParagraphTexts: (text: string) => string[];
77
100
  //#endregion
78
- export { TableColumnDeletion, TableColumnInsertion, TableRowDeletion, TableRowInsertion, TableStructureRevision, applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey };
101
+ export { TableColumnDeletion, TableColumnInsertion, TableRowDeletion, TableRowInsertion, TableStructureRevision, applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, splitCellParagraphTexts };
@@ -46,8 +46,74 @@ const applyTableRowInsertion = ({ tr, insertion, cellTexts, revision }) => {
46
46
  const rowAttrs = revision ? { trIns: revision } : null;
47
47
  const row = insertion.rowType.create(rowAttrs, insertion.cells);
48
48
  tr.insert(insertion.rowPosition, populateTableRow(row, cellTexts));
49
+ if (revision) markTableRowContent({
50
+ tr,
51
+ rowPosition: insertion.rowPosition,
52
+ kind: "insertion",
53
+ revision
54
+ });
49
55
  return applied(tr, revision);
50
56
  };
57
+ /**
58
+ * Mark the runs inside a row whose structural revision was just written.
59
+ *
60
+ * Word records a tracked row insertion or deletion twice: on the row
61
+ * (`w:trPr/w:ins` | `w:trPr/w:del`) AND around every run in its cells
62
+ * (`w:ins` | `w:del`, the latter with `w:delText`). A consumer that reads only
63
+ * run-level revisions — which is most of them — keeps a deleted row's text on
64
+ * accept and an inserted row's text on reject when the row marker stands
65
+ * alone, so the two must always be written together.
66
+ *
67
+ * Two kinds of run stay unmarked. A run that already carries a revision keeps
68
+ * it, because OOXML nests `w:ins`/`w:del` but the editable model holds one
69
+ * wrapper per run and overwriting would drop the earlier revision. A cell that
70
+ * spans into the row below survives the deletion — `removeRow` moves it down
71
+ * and shortens its span — so marking its text would delete content the
72
+ * accepted document must still hold.
73
+ */
74
+ const markTableRowContent = ({ tr, rowPosition, kind, revision }) => {
75
+ const row = tr.doc.nodeAt(rowPosition);
76
+ const markType = tr.doc.type.schema.marks[kind];
77
+ if (!row || row.type.spec["tableRole"] !== "row" || !markType) return;
78
+ const mark = markType.create({
79
+ revisionId: revision.revisionId,
80
+ author: revision.author,
81
+ date: revision.date,
82
+ ...revision.initials != null && { initials: revision.initials },
83
+ ...revision.provenance != null && { provenance: revision.provenance },
84
+ ...revision.suggestionId != null && { suggestionId: revision.suggestionId }
85
+ });
86
+ const contentFrom = rowPosition + 1;
87
+ const markable = [];
88
+ let wholeRowMarkable = true;
89
+ row.descendants((node, offset) => {
90
+ if (isRowSpanningCell(node)) {
91
+ wholeRowMarkable = false;
92
+ return false;
93
+ }
94
+ if (!node.isInline) return true;
95
+ if (node.marks.some(isTrackedChangeMark)) {
96
+ wholeRowMarkable = false;
97
+ return false;
98
+ }
99
+ const from = contentFrom + offset;
100
+ markable.push({
101
+ from,
102
+ to: from + node.nodeSize
103
+ });
104
+ return false;
105
+ });
106
+ if (wholeRowMarkable) {
107
+ tr.addMark(contentFrom, rowPosition + row.nodeSize - 1, mark);
108
+ return;
109
+ }
110
+ for (const range of markable) tr.addMark(range.from, range.to, mark);
111
+ };
112
+ const isTrackedChangeMark = (mark) => mark.type.name === "insertion" || mark.type.name === "deletion";
113
+ const isRowSpanningCell = (node) => {
114
+ const role = node.type.spec["tableRole"];
115
+ return (role === "cell" || role === "header_cell") && Number(node.attrs["rowspan"]) > 1;
116
+ };
51
117
  const applyTableColumnInsertion = ({ tr, insertion, cellTexts, revision }) => {
52
118
  const tablePosition = tr.mapping.map(insertion.tablePosition, 1);
53
119
  const table = tr.doc.nodeAt(tablePosition);
@@ -123,6 +189,12 @@ const applyTableRowDeletion = ({ tr, deletion, revision }) => {
123
189
  const row = tr.doc.nodeAt(rowPosition);
124
190
  if (!row || row.type.spec["tableRole"] !== "row" || row.attrs["trIns"] != null || row.attrs["trDel"] != null) return { type: "unsupported" };
125
191
  tr.setNodeAttribute(rowPosition, "trDel", revision);
192
+ markTableRowContent({
193
+ tr,
194
+ rowPosition,
195
+ kind: "deletion",
196
+ revision
197
+ });
126
198
  markStructuralChange(tr);
127
199
  return applied(tr, revision);
128
200
  }
@@ -194,22 +266,42 @@ const buildTableRowInsertion = ({ map, table, tableStart, rowIndex }) => {
194
266
  rowPosition,
195
267
  rowType: tableNodeTypes(table.type.schema).row,
196
268
  cells,
197
- rowspanUpdates
269
+ rowspanUpdates,
270
+ columnCount: map.width
198
271
  };
199
272
  };
273
+ const CELL_LINE_BREAK_PATTERN = /\r\n|\r|\n/;
274
+ /**
275
+ * Split one cell's text into the paragraphs it describes, one string each.
276
+ *
277
+ * A cell holds paragraphs, not lines: a line break starts a new one, and a
278
+ * blank line is a blank paragraph, exactly as `text: ""` is for an insertion.
279
+ *
280
+ * This is deliberately not the prose rule `insertAfterBlock` follows, where a
281
+ * blank line between two model-written clauses is formatting noise and is
282
+ * dropped. A cell's text is read off a cell that HAS those paragraphs, so
283
+ * dropping one would lose a block that was there.
284
+ */
285
+ const splitCellParagraphTexts = (text) => text.split(CELL_LINE_BREAK_PATTERN);
286
+ /**
287
+ * Fill the new row's cells from `cellTexts`, in order: the nth text goes in
288
+ * the nth cell the row actually has, which is how a caller reading the row
289
+ * through the block snapshot counts them (`table.cellIndex`). Cells the caller
290
+ * did not name stay empty.
291
+ */
200
292
  const populateTableRow = (row, cellTexts) => {
201
293
  if (cellTexts === void 0) return row;
202
294
  const cells = [];
203
295
  row.forEach((cell, _offset, index) => {
296
+ const text = cellTexts[index] ?? "";
204
297
  const paragraph = cell.firstChild;
205
298
  if (!paragraph?.isTextblock) {
206
299
  cells.push(cell);
207
300
  return;
208
301
  }
209
- const text = cellTexts[index] ?? "";
210
- const content = text.length > 0 ? row.type.schema.text(text) : null;
211
- const nextParagraph = paragraph.type.create(stripBlockIdentityAttrs(paragraph.attrs), content);
212
- cells.push(cell.type.create(cell.attrs, nextParagraph));
302
+ const attrs = stripBlockIdentityAttrs(paragraph.attrs);
303
+ const paragraphs = splitCellParagraphTexts(text).map((line) => paragraph.type.create(attrs, line.length > 0 ? row.type.schema.text(line) : null));
304
+ cells.push(cell.type.create(cell.attrs, paragraphs));
213
305
  });
214
306
  return row.type.create(row.attrs, cells);
215
307
  };
@@ -297,4 +389,4 @@ const getTableColumnDeletionCellPositions = ({ map, table, columnIndex }) => {
297
389
  return [...positions];
298
390
  };
299
391
  //#endregion
300
- export { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey };
392
+ export { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, splitCellParagraphTexts };
@@ -22,10 +22,19 @@ type TableCellTarget = {
22
22
  topRowIndex: number;
23
23
  bottomRowIndex: number;
24
24
  };
25
- /** Resolve the nearest enclosing table boundary for block-adjacent insertions. */
26
- declare const findEnclosingTableBoundary: (doc: Node, blockFrom: number) => TableBoundary | null;
25
+ /**
26
+ * The boundary a block-adjacent insertion escapes to: the outermost table the
27
+ * anchor sits in, so the new block lands as a peer of that table at document
28
+ * level.
29
+ *
30
+ * The nearest table is the wrong answer once tables nest. Escaping one level
31
+ * out of a table inside a cell leaves the insertion in the enclosing cell,
32
+ * which is still "inside a table" and is never what a block-level insertion
33
+ * meant.
34
+ */
35
+ declare const findOutermostTableBoundary: (doc: Node, blockFrom: number) => TableBoundary | null;
27
36
  declare const findEnclosingTableRow: (doc: Node, blockFrom: number) => TableRowTarget | null;
28
37
  declare const findEnclosingTableCell: (doc: Node, blockFrom: number) => TableCellTarget | null;
29
38
  declare const tableRectangleCutsMergedCell: (map: TableMap, rectangle: TableRectangle) => boolean;
30
39
  //#endregion
31
- export { TableBoundary, TableCellTarget, TableRowTarget, findEnclosingTableBoundary, findEnclosingTableCell, findEnclosingTableRow, tableRectangleCutsMergedCell };
40
+ export { TableBoundary, TableCellTarget, TableRowTarget, findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell };
@@ -1,13 +1,23 @@
1
1
  import { TableMap } from "prosemirror-tables";
2
2
  //#region src/ai-edits/table-targets.ts
3
- /** Resolve the nearest enclosing table boundary for block-adjacent insertions. */
4
- const findEnclosingTableBoundary = (doc, blockFrom) => {
3
+ /**
4
+ * The boundary a block-adjacent insertion escapes to: the outermost table the
5
+ * anchor sits in, so the new block lands as a peer of that table at document
6
+ * level.
7
+ *
8
+ * The nearest table is the wrong answer once tables nest. Escaping one level
9
+ * out of a table inside a cell leaves the insertion in the enclosing cell,
10
+ * which is still "inside a table" and is never what a block-level insertion
11
+ * meant.
12
+ */
13
+ const findOutermostTableBoundary = (doc, blockFrom) => {
5
14
  const resolved = doc.resolve(blockFrom);
6
- for (let depth = resolved.depth; depth > 0; depth--) if (resolved.node(depth).type.name === "table") return {
15
+ let boundary = null;
16
+ for (let depth = resolved.depth; depth > 0; depth--) if (resolved.node(depth).type.name === "table") boundary = {
7
17
  before: resolved.before(depth),
8
18
  after: resolved.after(depth)
9
19
  };
10
- return null;
20
+ return boundary;
11
21
  };
12
22
  const findEnclosingTableRow = (doc, blockFrom) => {
13
23
  const resolved = doc.resolve(blockFrom);
@@ -67,4 +77,4 @@ const tableRectangleCutsMergedCell = (map, rectangle) => {
67
77
  return false;
68
78
  };
69
79
  //#endregion
70
- export { findEnclosingTableBoundary, findEnclosingTableCell, findEnclosingTableRow, tableRectangleCutsMergedCell };
80
+ export { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell };