@doxi/core 0.11.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 (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/collab/port.d.ts +46 -0
  4. package/dist/collab/port.d.ts.map +1 -0
  5. package/dist/collab/port.js +11 -0
  6. package/dist/collab/port.js.map +1 -0
  7. package/dist/commands/block-commands.d.ts +62 -0
  8. package/dist/commands/block-commands.d.ts.map +1 -0
  9. package/dist/commands/block-commands.js +208 -0
  10. package/dist/commands/block-commands.js.map +1 -0
  11. package/dist/commands/command.d.ts +13 -0
  12. package/dist/commands/command.d.ts.map +1 -0
  13. package/dist/commands/command.js +13 -0
  14. package/dist/commands/command.js.map +1 -0
  15. package/dist/commands/edit-commands.d.ts +5 -0
  16. package/dist/commands/edit-commands.d.ts.map +1 -0
  17. package/dist/commands/edit-commands.js +147 -0
  18. package/dist/commands/edit-commands.js.map +1 -0
  19. package/dist/commands/image-commands.d.ts +31 -0
  20. package/dist/commands/image-commands.d.ts.map +1 -0
  21. package/dist/commands/image-commands.js +130 -0
  22. package/dist/commands/image-commands.js.map +1 -0
  23. package/dist/commands/index.d.ts +11 -0
  24. package/dist/commands/index.d.ts.map +1 -0
  25. package/dist/commands/index.js +11 -0
  26. package/dist/commands/index.js.map +1 -0
  27. package/dist/commands/keymap.d.ts +34 -0
  28. package/dist/commands/keymap.d.ts.map +1 -0
  29. package/dist/commands/keymap.js +84 -0
  30. package/dist/commands/keymap.js.map +1 -0
  31. package/dist/commands/link-commands.d.ts +54 -0
  32. package/dist/commands/link-commands.d.ts.map +1 -0
  33. package/dist/commands/link-commands.js +151 -0
  34. package/dist/commands/link-commands.js.map +1 -0
  35. package/dist/commands/list-commands.d.ts +42 -0
  36. package/dist/commands/list-commands.d.ts.map +1 -0
  37. package/dist/commands/list-commands.js +316 -0
  38. package/dist/commands/list-commands.js.map +1 -0
  39. package/dist/commands/mark-commands.d.ts +53 -0
  40. package/dist/commands/mark-commands.d.ts.map +1 -0
  41. package/dist/commands/mark-commands.js +181 -0
  42. package/dist/commands/mark-commands.js.map +1 -0
  43. package/dist/commands/selection-commands.d.ts +3 -0
  44. package/dist/commands/selection-commands.d.ts.map +1 -0
  45. package/dist/commands/selection-commands.js +11 -0
  46. package/dist/commands/selection-commands.js.map +1 -0
  47. package/dist/commands/table-commands.d.ts +109 -0
  48. package/dist/commands/table-commands.d.ts.map +1 -0
  49. package/dist/commands/table-commands.js +884 -0
  50. package/dist/commands/table-commands.js.map +1 -0
  51. package/dist/history/history.d.ts +40 -0
  52. package/dist/history/history.d.ts.map +1 -0
  53. package/dist/history/history.js +139 -0
  54. package/dist/history/history.js.map +1 -0
  55. package/dist/history/index.d.ts +2 -0
  56. package/dist/history/index.d.ts.map +1 -0
  57. package/dist/history/index.js +2 -0
  58. package/dist/history/index.js.map +1 -0
  59. package/dist/html/index.d.ts +3 -0
  60. package/dist/html/index.d.ts.map +1 -0
  61. package/dist/html/index.js +3 -0
  62. package/dist/html/index.js.map +1 -0
  63. package/dist/html/parse.d.ts +4 -0
  64. package/dist/html/parse.d.ts.map +1 -0
  65. package/dist/html/parse.js +0 -0
  66. package/dist/html/parse.js.map +1 -0
  67. package/dist/html/serialize.d.ts +4 -0
  68. package/dist/html/serialize.d.ts.map +1 -0
  69. package/dist/html/serialize.js +75 -0
  70. package/dist/html/serialize.js.map +1 -0
  71. package/dist/index.d.ts +13 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +13 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/layout/index.d.ts +6 -0
  76. package/dist/layout/index.d.ts.map +1 -0
  77. package/dist/layout/index.js +6 -0
  78. package/dist/layout/index.js.map +1 -0
  79. package/dist/layout/layout-engine.d.ts +20 -0
  80. package/dist/layout/layout-engine.d.ts.map +1 -0
  81. package/dist/layout/layout-engine.js +198 -0
  82. package/dist/layout/layout-engine.js.map +1 -0
  83. package/dist/layout/measure.d.ts +9 -0
  84. package/dist/layout/measure.d.ts.map +1 -0
  85. package/dist/layout/measure.js +37 -0
  86. package/dist/layout/measure.js.map +1 -0
  87. package/dist/layout/split-paragraph.d.ts +28 -0
  88. package/dist/layout/split-paragraph.d.ts.map +1 -0
  89. package/dist/layout/split-paragraph.js +122 -0
  90. package/dist/layout/split-paragraph.js.map +1 -0
  91. package/dist/layout/split-table.d.ts +46 -0
  92. package/dist/layout/split-table.d.ts.map +1 -0
  93. package/dist/layout/split-table.js +84 -0
  94. package/dist/layout/split-table.js.map +1 -0
  95. package/dist/layout/types.d.ts +73 -0
  96. package/dist/layout/types.d.ts.map +1 -0
  97. package/dist/layout/types.js +36 -0
  98. package/dist/layout/types.js.map +1 -0
  99. package/dist/layout/widow-orphan.d.ts +15 -0
  100. package/dist/layout/widow-orphan.d.ts.map +1 -0
  101. package/dist/layout/widow-orphan.js +14 -0
  102. package/dist/layout/widow-orphan.js.map +1 -0
  103. package/dist/model/content-expr.d.ts +32 -0
  104. package/dist/model/content-expr.d.ts.map +1 -0
  105. package/dist/model/content-expr.js +106 -0
  106. package/dist/model/content-expr.js.map +1 -0
  107. package/dist/model/fragment.d.ts +17 -0
  108. package/dist/model/fragment.d.ts.map +1 -0
  109. package/dist/model/fragment.js +44 -0
  110. package/dist/model/fragment.js.map +1 -0
  111. package/dist/model/index.d.ts +10 -0
  112. package/dist/model/index.d.ts.map +1 -0
  113. package/dist/model/index.js +10 -0
  114. package/dist/model/index.js.map +1 -0
  115. package/dist/model/mark.d.ts +35 -0
  116. package/dist/model/mark.d.ts.map +1 -0
  117. package/dist/model/mark.js +89 -0
  118. package/dist/model/mark.js.map +1 -0
  119. package/dist/model/node-type.d.ts +36 -0
  120. package/dist/model/node-type.d.ts.map +1 -0
  121. package/dist/model/node-type.js +14 -0
  122. package/dist/model/node-type.js.map +1 -0
  123. package/dist/model/node.d.ts +36 -0
  124. package/dist/model/node.d.ts.map +1 -0
  125. package/dist/model/node.js +192 -0
  126. package/dist/model/node.js.map +1 -0
  127. package/dist/model/position.d.ts +66 -0
  128. package/dist/model/position.d.ts.map +1 -0
  129. package/dist/model/position.js +158 -0
  130. package/dist/model/position.js.map +1 -0
  131. package/dist/model/schema.d.ts +28 -0
  132. package/dist/model/schema.d.ts.map +1 -0
  133. package/dist/model/schema.js +195 -0
  134. package/dist/model/schema.js.map +1 -0
  135. package/dist/model/slice.d.ts +26 -0
  136. package/dist/model/slice.d.ts.map +1 -0
  137. package/dist/model/slice.js +56 -0
  138. package/dist/model/slice.js.map +1 -0
  139. package/dist/model/table-grid.d.ts +71 -0
  140. package/dist/model/table-grid.d.ts.map +1 -0
  141. package/dist/model/table-grid.js +130 -0
  142. package/dist/model/table-grid.js.map +1 -0
  143. package/dist/plugin/index.d.ts +3 -0
  144. package/dist/plugin/index.d.ts.map +1 -0
  145. package/dist/plugin/index.js +3 -0
  146. package/dist/plugin/index.js.map +1 -0
  147. package/dist/plugin/plugin-key.d.ts +13 -0
  148. package/dist/plugin/plugin-key.d.ts.map +1 -0
  149. package/dist/plugin/plugin-key.js +13 -0
  150. package/dist/plugin/plugin-key.js.map +1 -0
  151. package/dist/plugin/plugin-state.d.ts +2 -0
  152. package/dist/plugin/plugin-state.d.ts.map +1 -0
  153. package/dist/plugin/plugin-state.js +3 -0
  154. package/dist/plugin/plugin-state.js.map +1 -0
  155. package/dist/plugin/plugin.d.ts +39 -0
  156. package/dist/plugin/plugin.d.ts.map +1 -0
  157. package/dist/plugin/plugin.js +10 -0
  158. package/dist/plugin/plugin.js.map +1 -0
  159. package/dist/schema/default.d.ts +163 -0
  160. package/dist/schema/default.d.ts.map +1 -0
  161. package/dist/schema/default.js +94 -0
  162. package/dist/schema/default.js.map +1 -0
  163. package/dist/schema/index.d.ts +2 -0
  164. package/dist/schema/index.d.ts.map +1 -0
  165. package/dist/schema/index.js +2 -0
  166. package/dist/schema/index.js.map +1 -0
  167. package/dist/serialize/index.d.ts +2 -0
  168. package/dist/serialize/index.d.ts.map +1 -0
  169. package/dist/serialize/index.js +2 -0
  170. package/dist/serialize/index.js.map +1 -0
  171. package/dist/serialize/json.d.ts +15 -0
  172. package/dist/serialize/json.d.ts.map +1 -0
  173. package/dist/serialize/json.js +23 -0
  174. package/dist/serialize/json.js.map +1 -0
  175. package/dist/state/all-selection.d.ts +11 -0
  176. package/dist/state/all-selection.d.ts.map +1 -0
  177. package/dist/state/all-selection.js +17 -0
  178. package/dist/state/all-selection.js.map +1 -0
  179. package/dist/state/cell-selection.d.ts +30 -0
  180. package/dist/state/cell-selection.d.ts.map +1 -0
  181. package/dist/state/cell-selection.js +38 -0
  182. package/dist/state/cell-selection.js.map +1 -0
  183. package/dist/state/editor-state.d.ts +46 -0
  184. package/dist/state/editor-state.d.ts.map +1 -0
  185. package/dist/state/editor-state.js +211 -0
  186. package/dist/state/editor-state.js.map +1 -0
  187. package/dist/state/index.d.ts +7 -0
  188. package/dist/state/index.d.ts.map +1 -0
  189. package/dist/state/index.js +7 -0
  190. package/dist/state/index.js.map +1 -0
  191. package/dist/state/node-selection.d.ts +16 -0
  192. package/dist/state/node-selection.d.ts.map +1 -0
  193. package/dist/state/node-selection.js +51 -0
  194. package/dist/state/node-selection.js.map +1 -0
  195. package/dist/state/selection.d.ts +29 -0
  196. package/dist/state/selection.d.ts.map +1 -0
  197. package/dist/state/selection.js +24 -0
  198. package/dist/state/selection.js.map +1 -0
  199. package/dist/state/text-selection.d.ts +10 -0
  200. package/dist/state/text-selection.d.ts.map +1 -0
  201. package/dist/state/text-selection.js +26 -0
  202. package/dist/state/text-selection.js.map +1 -0
  203. package/dist/transform/attr-step.d.ts +16 -0
  204. package/dist/transform/attr-step.d.ts.map +1 -0
  205. package/dist/transform/attr-step.js +98 -0
  206. package/dist/transform/attr-step.js.map +1 -0
  207. package/dist/transform/index.d.ts +10 -0
  208. package/dist/transform/index.d.ts.map +1 -0
  209. package/dist/transform/index.js +10 -0
  210. package/dist/transform/index.js.map +1 -0
  211. package/dist/transform/mapping.d.ts +44 -0
  212. package/dist/transform/mapping.d.ts.map +1 -0
  213. package/dist/transform/mapping.js +101 -0
  214. package/dist/transform/mapping.js.map +1 -0
  215. package/dist/transform/mark-step.d.ts +27 -0
  216. package/dist/transform/mark-step.d.ts.map +1 -0
  217. package/dist/transform/mark-step.js +146 -0
  218. package/dist/transform/mark-step.js.map +1 -0
  219. package/dist/transform/replace-around-step.d.ts +35 -0
  220. package/dist/transform/replace-around-step.d.ts.map +1 -0
  221. package/dist/transform/replace-around-step.js +144 -0
  222. package/dist/transform/replace-around-step.js.map +1 -0
  223. package/dist/transform/replace-step.d.ts +17 -0
  224. package/dist/transform/replace-step.d.ts.map +1 -0
  225. package/dist/transform/replace-step.js +72 -0
  226. package/dist/transform/replace-step.js.map +1 -0
  227. package/dist/transform/replace.d.ts +18 -0
  228. package/dist/transform/replace.d.ts.map +1 -0
  229. package/dist/transform/replace.js +132 -0
  230. package/dist/transform/replace.js.map +1 -0
  231. package/dist/transform/set-page-meta-step.d.ts +42 -0
  232. package/dist/transform/set-page-meta-step.d.ts.map +1 -0
  233. package/dist/transform/set-page-meta-step.js +75 -0
  234. package/dist/transform/set-page-meta-step.js.map +1 -0
  235. package/dist/transform/step.d.ts +34 -0
  236. package/dist/transform/step.d.ts.map +1 -0
  237. package/dist/transform/step.js +23 -0
  238. package/dist/transform/step.js.map +1 -0
  239. package/dist/transform/transaction.d.ts +20 -0
  240. package/dist/transform/transaction.d.ts.map +1 -0
  241. package/dist/transform/transaction.js +38 -0
  242. package/dist/transform/transaction.js.map +1 -0
  243. package/dist/version.d.ts +2 -0
  244. package/dist/version.d.ts.map +1 -0
  245. package/dist/version.js +5 -0
  246. package/dist/version.js.map +1 -0
  247. package/dist/view/cell-drag.d.ts +33 -0
  248. package/dist/view/cell-drag.d.ts.map +1 -0
  249. package/dist/view/cell-drag.js +177 -0
  250. package/dist/view/cell-drag.js.map +1 -0
  251. package/dist/view/clipboard.d.ts +5 -0
  252. package/dist/view/clipboard.d.ts.map +1 -0
  253. package/dist/view/clipboard.js +97 -0
  254. package/dist/view/clipboard.js.map +1 -0
  255. package/dist/view/default-renderer.d.ts +3 -0
  256. package/dist/view/default-renderer.d.ts.map +1 -0
  257. package/dist/view/default-renderer.js +142 -0
  258. package/dist/view/default-renderer.js.map +1 -0
  259. package/dist/view/dom-spec.d.ts +11 -0
  260. package/dist/view/dom-spec.d.ts.map +1 -0
  261. package/dist/view/dom-spec.js +32 -0
  262. package/dist/view/dom-spec.js.map +1 -0
  263. package/dist/view/editor-view.d.ts +55 -0
  264. package/dist/view/editor-view.d.ts.map +1 -0
  265. package/dist/view/editor-view.js +143 -0
  266. package/dist/view/editor-view.js.map +1 -0
  267. package/dist/view/image-resize.d.ts +37 -0
  268. package/dist/view/image-resize.d.ts.map +1 -0
  269. package/dist/view/image-resize.js +191 -0
  270. package/dist/view/image-resize.js.map +1 -0
  271. package/dist/view/index.d.ts +15 -0
  272. package/dist/view/index.d.ts.map +1 -0
  273. package/dist/view/index.js +15 -0
  274. package/dist/view/index.js.map +1 -0
  275. package/dist/view/input-pipeline.d.ts +24 -0
  276. package/dist/view/input-pipeline.d.ts.map +1 -0
  277. package/dist/view/input-pipeline.js +226 -0
  278. package/dist/view/input-pipeline.js.map +1 -0
  279. package/dist/view/mutation-observer.d.ts +17 -0
  280. package/dist/view/mutation-observer.d.ts.map +1 -0
  281. package/dist/view/mutation-observer.js +62 -0
  282. package/dist/view/mutation-observer.js.map +1 -0
  283. package/dist/view/page-slots.d.ts +56 -0
  284. package/dist/view/page-slots.d.ts.map +1 -0
  285. package/dist/view/page-slots.js +230 -0
  286. package/dist/view/page-slots.js.map +1 -0
  287. package/dist/view/paginator.d.ts +17 -0
  288. package/dist/view/paginator.d.ts.map +1 -0
  289. package/dist/view/paginator.js +93 -0
  290. package/dist/view/paginator.js.map +1 -0
  291. package/dist/view/print.d.ts +42 -0
  292. package/dist/view/print.d.ts.map +1 -0
  293. package/dist/view/print.js +70 -0
  294. package/dist/view/print.js.map +1 -0
  295. package/dist/view/reconcile.d.ts +16 -0
  296. package/dist/view/reconcile.d.ts.map +1 -0
  297. package/dist/view/reconcile.js +158 -0
  298. package/dist/view/reconcile.js.map +1 -0
  299. package/dist/view/renderer.d.ts +31 -0
  300. package/dist/view/renderer.d.ts.map +1 -0
  301. package/dist/view/renderer.js +89 -0
  302. package/dist/view/renderer.js.map +1 -0
  303. package/dist/view/selection-sync.d.ts +35 -0
  304. package/dist/view/selection-sync.d.ts.map +1 -0
  305. package/dist/view/selection-sync.js +324 -0
  306. package/dist/view/selection-sync.js.map +1 -0
  307. package/dist/view/table-resize.d.ts +41 -0
  308. package/dist/view/table-resize.d.ts.map +1 -0
  309. package/dist/view/table-resize.js +216 -0
  310. package/dist/view/table-resize.js.map +1 -0
  311. package/package.json +93 -0
  312. package/styles/base.css +269 -0
  313. package/styles/dark.css +36 -0
  314. package/styles/light.css +13 -0
  315. package/styles/page.css +93 -0
  316. package/styles/print-a4.css +87 -0
  317. package/styles/print.css +88 -0
@@ -0,0 +1,884 @@
1
+ import { Fragment } from '../model/fragment.js';
2
+ import { DxNode } from '../model/node.js';
3
+ import { resolve } from '../model/position.js';
4
+ import { Slice } from '../model/slice.js';
5
+ import { tableGrid } from '../model/table-grid.js';
6
+ import { CellSelection } from '../state/cell-selection.js';
7
+ import { TextSelection } from '../state/text-selection.js';
8
+ import { AttrStep } from '../transform/attr-step.js';
9
+ import { ReplaceStep } from '../transform/replace-step.js';
10
+ /**
11
+ * Table commands.
12
+ *
13
+ * The default schema declares:
14
+ * table: { content: 'table_row+', group: 'block', isolating: true }
15
+ * table_row: { content: 'table_cell+', isolating: true }
16
+ * table_cell: { content: 'block+', isolating: true,
17
+ * attrs: { widthPx: number|null, background: string|null } }
18
+ *
19
+ * Every command sets `selectionOverride` so the cursor lands on an editable
20
+ * inline position after the structural reshuffle.
21
+ */
22
+ function setOverride(tr, pos) {
23
+ ;
24
+ tr.selectionOverride =
25
+ new TextSelection(pos, pos);
26
+ }
27
+ function findTableContext(rp) {
28
+ let cellDepth = -1;
29
+ for (let d = rp.depth; d >= 1; d--) {
30
+ if (rp.node(d).type.name === 'table_cell') {
31
+ cellDepth = d;
32
+ break;
33
+ }
34
+ }
35
+ if (cellDepth < 2)
36
+ return null;
37
+ const rowDepth = cellDepth - 1;
38
+ const tableDepth = cellDepth - 2;
39
+ if (tableDepth < 1)
40
+ return null;
41
+ const table = rp.node(tableDepth);
42
+ if (table.type.name !== 'table')
43
+ return null;
44
+ const rowIdx = rp.index(rowDepth);
45
+ const colIdx = rp.index(cellDepth);
46
+ return {
47
+ tableDepth,
48
+ tableStart: rp.before(tableDepth),
49
+ table,
50
+ rowIdx,
51
+ colIdx,
52
+ };
53
+ }
54
+ function makeEmptyCell(schema) {
55
+ return schema.node('table_cell', null, [schema.node('paragraph', null)]);
56
+ }
57
+ function makeEmptyRow(schema, cols) {
58
+ const cells = [];
59
+ for (let i = 0; i < cols; i++)
60
+ cells.push(makeEmptyCell(schema));
61
+ return schema.node('table_row', null, cells);
62
+ }
63
+ function rowCellCount(row) {
64
+ return row.content.childCount;
65
+ }
66
+ /**
67
+ * Insert a fresh `rows × cols` table at the cursor.
68
+ *
69
+ * If the cursor's top-level block (in its container) is an EMPTY paragraph,
70
+ * REPLACE that paragraph with the table. Otherwise INSERT the table as the
71
+ * next sibling of that block.
72
+ *
73
+ * Each cell starts with a single empty paragraph. Cursor lands inside the
74
+ * first cell's paragraph (parentOffset 0).
75
+ *
76
+ * Returns false if rows < 1 or cols < 1, if the schema doesn't carry
77
+ * table / table_row / table_cell, or if the cursor isn't inside any
78
+ * top-level block.
79
+ */
80
+ export function insertTable(schema, rows, cols) {
81
+ return (state, dispatch) => {
82
+ if (rows < 1 || cols < 1)
83
+ return false;
84
+ if (!schema.nodes.table || !schema.nodes.table_row || !schema.nodes.table_cell) {
85
+ return false;
86
+ }
87
+ const rp = resolve(state.doc, state.selection.from);
88
+ if (rp.depth < 1)
89
+ return false;
90
+ // Build the new table.
91
+ const newRows = [];
92
+ for (let r = 0; r < rows; r++)
93
+ newRows.push(makeEmptyRow(schema, cols));
94
+ const table = schema.node('table', null, newRows);
95
+ // Find the cursor's containing block in its container. We want depth 1
96
+ // unless we're inside a cell-like container (table_cell), in which case
97
+ // we operate on the cell's child block. Mirrors findTopBlockDepth, but
98
+ // we keep the logic local here to avoid pulling helpers that could
99
+ // confuse the table-vs-cell case (we never insert nested tables in v0.3b
100
+ // but the cell case still must work for completeness).
101
+ let topD = 1;
102
+ for (let d = rp.depth; d >= 1; d--) {
103
+ const parent = rp.node(d - 1);
104
+ const parentSpec = parent.type.spec;
105
+ if (parent.type.name === 'doc' || parentSpec.isolating === true) {
106
+ topD = d;
107
+ break;
108
+ }
109
+ }
110
+ const block = rp.node(topD);
111
+ if (!dispatch)
112
+ return true;
113
+ const isEmptyParagraph = block.type.name === 'paragraph' && block.content.size === 0;
114
+ let blockStart;
115
+ let blockEnd;
116
+ let slice;
117
+ if (isEmptyParagraph) {
118
+ blockStart = rp.before(topD);
119
+ blockEnd = rp.after(topD);
120
+ slice = new Slice(Fragment.from([table]), 0, 0);
121
+ }
122
+ else {
123
+ const sibling = rp.after(topD);
124
+ blockStart = sibling;
125
+ blockEnd = sibling;
126
+ slice = new Slice(Fragment.from([table]), 0, 0);
127
+ }
128
+ const tr = state.tr.step(new ReplaceStep(blockStart, blockEnd, slice));
129
+ // Cursor lands in first cell. The table [open] is at `blockStart`.
130
+ // Layout inside table: table[open]=blockStart, row[open]=blockStart+1,
131
+ // cell[open]=blockStart+2, paragraph[open]=blockStart+3, content=blockStart+4.
132
+ setOverride(tr, blockStart + 4);
133
+ dispatch(tr);
134
+ return true;
135
+ };
136
+ }
137
+ /** Insert a fresh empty row above the cursor's row. */
138
+ export function addRowAbove() {
139
+ return (state, dispatch) => {
140
+ const rp = resolve(state.doc, state.selection.from);
141
+ const ctx = findTableContext(rp);
142
+ if (!ctx)
143
+ return false;
144
+ if (!dispatch)
145
+ return true;
146
+ return spliceRow(state, dispatch, rp, ctx, ctx.rowIdx, 'above');
147
+ };
148
+ }
149
+ /** Insert a fresh empty row below the cursor's row. */
150
+ export function addRowBelow() {
151
+ return (state, dispatch) => {
152
+ const rp = resolve(state.doc, state.selection.from);
153
+ const ctx = findTableContext(rp);
154
+ if (!ctx)
155
+ return false;
156
+ if (!dispatch)
157
+ return true;
158
+ return spliceRow(state, dispatch, rp, ctx, ctx.rowIdx + 1, 'below');
159
+ };
160
+ }
161
+ function spliceRow(state, dispatch, _rp, ctx, insertAt, direction) {
162
+ const cols = rowCellCount(ctx.table.content.child(ctx.rowIdx));
163
+ const newRow = makeEmptyRow(state.schema, cols);
164
+ const newRows = [];
165
+ for (let i = 0; i < ctx.table.content.childCount; i++) {
166
+ if (i === insertAt)
167
+ newRows.push(newRow);
168
+ newRows.push(ctx.table.content.child(i));
169
+ }
170
+ if (insertAt === ctx.table.content.childCount)
171
+ newRows.push(newRow);
172
+ const newTable = state.schema.node('table', ctx.table.attrs, newRows);
173
+ const slice = new Slice(Fragment.from([newTable]), 0, 0);
174
+ const tableEnd = ctx.tableStart + ctx.table.nodeSize;
175
+ const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
176
+ // Cursor stays where it was. When inserting above, the cursor's row shifted
177
+ // down by one row's worth of positions; when inserting below, the cursor
178
+ // is unaffected.
179
+ let cursor = state.selection.from;
180
+ if (direction === 'above') {
181
+ cursor += newRow.nodeSize;
182
+ }
183
+ setOverride(tr, cursor);
184
+ dispatch(tr);
185
+ return true;
186
+ }
187
+ /** Insert a fresh empty cell at the cursor's column index in every row. */
188
+ export function addColumnLeft() {
189
+ return (state, dispatch) => {
190
+ const rp = resolve(state.doc, state.selection.from);
191
+ const ctx = findTableContext(rp);
192
+ if (!ctx)
193
+ return false;
194
+ if (!dispatch)
195
+ return true;
196
+ return spliceColumn(state, dispatch, ctx, ctx.colIdx, state.selection.from, 'left');
197
+ };
198
+ }
199
+ /** Insert a fresh empty cell to the right of the cursor's column in every row. */
200
+ export function addColumnRight() {
201
+ return (state, dispatch) => {
202
+ const rp = resolve(state.doc, state.selection.from);
203
+ const ctx = findTableContext(rp);
204
+ if (!ctx)
205
+ return false;
206
+ if (!dispatch)
207
+ return true;
208
+ return spliceColumn(state, dispatch, ctx, ctx.colIdx + 1, state.selection.from, 'right');
209
+ };
210
+ }
211
+ function spliceColumn(state, dispatch, ctx, insertAt, cursor, direction) {
212
+ const newRows = [];
213
+ for (let r = 0; r < ctx.table.content.childCount; r++) {
214
+ const row = ctx.table.content.child(r);
215
+ const newCells = [];
216
+ for (let c = 0; c < row.content.childCount; c++) {
217
+ if (c === insertAt)
218
+ newCells.push(makeEmptyCell(state.schema));
219
+ newCells.push(row.content.child(c));
220
+ }
221
+ if (insertAt === row.content.childCount)
222
+ newCells.push(makeEmptyCell(state.schema));
223
+ newRows.push(state.schema.node('table_row', row.attrs, newCells));
224
+ }
225
+ const newTable = state.schema.node('table', ctx.table.attrs, newRows);
226
+ const slice = new Slice(Fragment.from([newTable]), 0, 0);
227
+ const tableEnd = ctx.tableStart + ctx.table.nodeSize;
228
+ const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
229
+ // The new cell has size 4 (table_cell[open], paragraph[open], paragraph[close], cell[close]).
230
+ // If inserted at-or-before the cursor's column, the cursor shifts by that size.
231
+ const newCellSize = makeEmptyCell(state.schema).nodeSize;
232
+ let newCursor = cursor;
233
+ if (direction === 'left') {
234
+ // insertAt === ctx.colIdx; the new cell sits before each row's existing
235
+ // cell at that index, so the cursor's cell shifts by newCellSize.
236
+ newCursor += newCellSize;
237
+ }
238
+ setOverride(tr, newCursor);
239
+ dispatch(tr);
240
+ return true;
241
+ }
242
+ /**
243
+ * Remove the row containing the cursor. Cursor lands in the next or previous
244
+ * row's same-column cell. If it was the only row, the whole table is replaced
245
+ * by an empty paragraph (cursor inside).
246
+ */
247
+ export function deleteRow() {
248
+ return (state, dispatch) => {
249
+ const rp = resolve(state.doc, state.selection.from);
250
+ const ctx = findTableContext(rp);
251
+ if (!ctx)
252
+ return false;
253
+ if (!dispatch)
254
+ return true;
255
+ // Single-row table → drop the whole table.
256
+ if (ctx.table.content.childCount === 1) {
257
+ return replaceTableWithEmptyParagraph(state, dispatch, ctx);
258
+ }
259
+ const newRows = [];
260
+ for (let r = 0; r < ctx.table.content.childCount; r++) {
261
+ if (r === ctx.rowIdx)
262
+ continue;
263
+ newRows.push(ctx.table.content.child(r));
264
+ }
265
+ const newTable = state.schema.node('table', ctx.table.attrs, newRows);
266
+ const slice = new Slice(Fragment.from([newTable]), 0, 0);
267
+ const tableEnd = ctx.tableStart + ctx.table.nodeSize;
268
+ const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
269
+ // Cursor target: same column in the next row (or previous if this was the
270
+ // last). Compute absolute position of that cell's first inline pos in the
271
+ // NEW table.
272
+ const targetRowIdx = ctx.rowIdx < newRows.length ? ctx.rowIdx : newRows.length - 1;
273
+ const cursor = firstInlineOfCellAt(ctx.tableStart, newTable, targetRowIdx, ctx.colIdx);
274
+ setOverride(tr, cursor);
275
+ dispatch(tr);
276
+ return true;
277
+ };
278
+ }
279
+ /**
280
+ * Remove the column containing the cursor from every row. Cursor lands in the
281
+ * previous column or the next if it was the first. If it was the only column,
282
+ * the whole table is replaced by an empty paragraph.
283
+ */
284
+ export function deleteColumn() {
285
+ return (state, dispatch) => {
286
+ const rp = resolve(state.doc, state.selection.from);
287
+ const ctx = findTableContext(rp);
288
+ if (!ctx)
289
+ return false;
290
+ if (!dispatch)
291
+ return true;
292
+ const firstRow = ctx.table.content.child(0);
293
+ if (firstRow.content.childCount === 1) {
294
+ return replaceTableWithEmptyParagraph(state, dispatch, ctx);
295
+ }
296
+ const newRows = [];
297
+ for (let r = 0; r < ctx.table.content.childCount; r++) {
298
+ const row = ctx.table.content.child(r);
299
+ const cells = [];
300
+ for (let c = 0; c < row.content.childCount; c++) {
301
+ if (c === ctx.colIdx)
302
+ continue;
303
+ cells.push(row.content.child(c));
304
+ }
305
+ newRows.push(state.schema.node('table_row', row.attrs, cells));
306
+ }
307
+ const newTable = state.schema.node('table', ctx.table.attrs, newRows);
308
+ const slice = new Slice(Fragment.from([newTable]), 0, 0);
309
+ const tableEnd = ctx.tableStart + ctx.table.nodeSize;
310
+ const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
311
+ const newColCount = newRows[0].content.childCount;
312
+ const targetColIdx = ctx.colIdx > 0 ? ctx.colIdx - 1 : 0;
313
+ const clampedCol = Math.min(targetColIdx, newColCount - 1);
314
+ const cursor = firstInlineOfCellAt(ctx.tableStart, newTable, ctx.rowIdx, clampedCol);
315
+ setOverride(tr, cursor);
316
+ dispatch(tr);
317
+ return true;
318
+ };
319
+ }
320
+ /**
321
+ * Remove the whole table containing the cursor. The table is replaced by an
322
+ * empty paragraph; cursor lands inside.
323
+ */
324
+ export function deleteTable() {
325
+ return (state, dispatch) => {
326
+ const rp = resolve(state.doc, state.selection.from);
327
+ const ctx = findTableContext(rp);
328
+ if (!ctx)
329
+ return false;
330
+ if (!dispatch)
331
+ return true;
332
+ return replaceTableWithEmptyParagraph(state, dispatch, ctx);
333
+ };
334
+ }
335
+ function replaceTableWithEmptyParagraph(state, dispatch, ctx) {
336
+ const paragraphType = state.schema.nodes.paragraph;
337
+ if (!paragraphType)
338
+ return false;
339
+ const para = state.schema.node('paragraph', null);
340
+ const slice = new Slice(Fragment.from([para]), 0, 0);
341
+ const tableEnd = ctx.tableStart + ctx.table.nodeSize;
342
+ const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
343
+ // Cursor inside the new paragraph: ctx.tableStart points at the [open] of
344
+ // the new paragraph; content starts one position later.
345
+ setOverride(tr, ctx.tableStart + 1);
346
+ dispatch(tr);
347
+ return true;
348
+ }
349
+ /**
350
+ * Move the selection to the first inline position of the next cell in
351
+ * document order. If the cursor is in the last cell of the last row, append
352
+ * a fresh empty row and place the cursor in its first cell — done in a
353
+ * single transaction so undo collapses cleanly and the selection override
354
+ * lines up with the new structure.
355
+ *
356
+ * Returns false when the cursor is not inside any table cell (so the keymap
357
+ * can fall through).
358
+ */
359
+ export function tabToNextCell() {
360
+ return (state, dispatch) => {
361
+ const rp = resolve(state.doc, state.selection.from);
362
+ const ctx = findTableContext(rp);
363
+ if (!ctx)
364
+ return false;
365
+ const row = ctx.table.content.child(ctx.rowIdx);
366
+ const cols = rowCellCount(row);
367
+ const rowCount = ctx.table.content.childCount;
368
+ const isLastCellInRow = ctx.colIdx === cols - 1;
369
+ const isLastRow = ctx.rowIdx === rowCount - 1;
370
+ if (!dispatch)
371
+ return true;
372
+ if (!isLastCellInRow) {
373
+ // Move to next cell in the same row.
374
+ const target = firstInlineOfCellAt(ctx.tableStart, ctx.table, ctx.rowIdx, ctx.colIdx + 1);
375
+ const tr = state.tr;
376
+ setOverride(tr, target);
377
+ dispatch(tr);
378
+ return true;
379
+ }
380
+ if (!isLastRow) {
381
+ // Move to first cell of the next row.
382
+ const target = firstInlineOfCellAt(ctx.tableStart, ctx.table, ctx.rowIdx + 1, 0);
383
+ const tr = state.tr;
384
+ setOverride(tr, target);
385
+ dispatch(tr);
386
+ return true;
387
+ }
388
+ // Last cell of last row → append a new empty row in a single transaction
389
+ // and land the cursor in its first cell.
390
+ const newRow = makeEmptyRow(state.schema, cols);
391
+ const newRows = [];
392
+ for (let i = 0; i < ctx.table.content.childCount; i++) {
393
+ newRows.push(ctx.table.content.child(i));
394
+ }
395
+ newRows.push(newRow);
396
+ const newTable = state.schema.node('table', ctx.table.attrs, newRows);
397
+ const slice = new Slice(Fragment.from([newTable]), 0, 0);
398
+ const tableEnd = ctx.tableStart + ctx.table.nodeSize;
399
+ const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
400
+ const target = firstInlineOfCellAt(ctx.tableStart, newTable, rowCount, 0);
401
+ setOverride(tr, target);
402
+ dispatch(tr);
403
+ return true;
404
+ };
405
+ }
406
+ /**
407
+ * Move the selection to the first inline position of the previous cell.
408
+ * Returns false when the cursor is at the very first cell of the table OR
409
+ * not inside any cell.
410
+ */
411
+ export function tabToPrevCell() {
412
+ return (state, dispatch) => {
413
+ const rp = resolve(state.doc, state.selection.from);
414
+ const ctx = findTableContext(rp);
415
+ if (!ctx)
416
+ return false;
417
+ const isFirstCellInRow = ctx.colIdx === 0;
418
+ const isFirstRow = ctx.rowIdx === 0;
419
+ if (isFirstCellInRow && isFirstRow)
420
+ return false;
421
+ if (!dispatch)
422
+ return true;
423
+ if (!isFirstCellInRow) {
424
+ const target = firstInlineOfCellAt(ctx.tableStart, ctx.table, ctx.rowIdx, ctx.colIdx - 1);
425
+ const tr = state.tr;
426
+ setOverride(tr, target);
427
+ dispatch(tr);
428
+ return true;
429
+ }
430
+ // First cell of a non-first row → last cell of previous row.
431
+ const prevRow = ctx.table.content.child(ctx.rowIdx - 1);
432
+ const target = firstInlineOfCellAt(ctx.tableStart, ctx.table, ctx.rowIdx - 1, rowCellCount(prevRow) - 1);
433
+ const tr = state.tr;
434
+ setOverride(tr, target);
435
+ dispatch(tr);
436
+ return true;
437
+ };
438
+ }
439
+ /**
440
+ * Set the `widthPx` attribute on every cell in `columnIndex` of the table
441
+ * whose [open] token sits at `tableStart` in the doc. Dispatches a single
442
+ * transaction containing one AttrStep per affected cell.
443
+ *
444
+ * Used by the column-resize pointer controller in `view/table-resize.ts`.
445
+ *
446
+ * Returns false if there's no table at `tableStart` or `columnIndex` is
447
+ * out of range in any row.
448
+ */
449
+ export function setCellWidth(tableStart, columnIndex, widthPx) {
450
+ return (state, dispatch) => {
451
+ if (tableStart < 0 || tableStart >= state.doc.content.size)
452
+ return false;
453
+ if (columnIndex < 0)
454
+ return false;
455
+ // Locate the table node whose [open] sits at `tableStart`. The doc tree
456
+ // walk mirrors the AttrStep.apply boundary-resolution logic: we find the
457
+ // child of rp.parent whose offset === rp.parentOffset.
458
+ let rp;
459
+ try {
460
+ rp = resolve(state.doc, tableStart);
461
+ }
462
+ catch {
463
+ return false;
464
+ }
465
+ const parent = rp.parent;
466
+ let offset = 0;
467
+ let table = null;
468
+ for (let i = 0; i < parent.content.childCount; i++) {
469
+ const child = parent.content.child(i);
470
+ if (offset === rp.parentOffset) {
471
+ table = child;
472
+ break;
473
+ }
474
+ offset += child.nodeSize;
475
+ }
476
+ if (!table || table.type.name !== 'table')
477
+ return false;
478
+ // Validate columnIndex is in range for every row, and collect each cell's
479
+ // absolute [open] position in the doc.
480
+ const cellPositions = [];
481
+ // tableStart points at table[open]; row[open] starts at tableStart + 1.
482
+ let pos = tableStart + 1;
483
+ for (let r = 0; r < table.content.childCount; r++) {
484
+ const row = table.content.child(r);
485
+ if (columnIndex >= row.content.childCount)
486
+ return false;
487
+ // pos is row[open]; first cell[open] sits at pos + 1.
488
+ let cellPos = pos + 1;
489
+ for (let c = 0; c < columnIndex; c++) {
490
+ cellPos += row.content.child(c).nodeSize;
491
+ }
492
+ cellPositions.push(cellPos);
493
+ pos += row.nodeSize;
494
+ }
495
+ if (!dispatch)
496
+ return true;
497
+ let tr = state.tr;
498
+ for (const cellPos of cellPositions) {
499
+ tr = tr.step(new AttrStep(cellPos, 'widthPx', widthPx));
500
+ }
501
+ dispatch(tr);
502
+ return true;
503
+ };
504
+ }
505
+ /**
506
+ * Find the model position of the table containing the cell whose [open] sits
507
+ * at `cellPos`, by walking up from the resolved position. Returns the
508
+ * `tableStart` (absolute position of the table [open]) and the table node.
509
+ */
510
+ function findTableForCellPos(doc, cellPos) {
511
+ // cellPos points at the cell's [open]; resolving cellPos + 1 lands us
512
+ // INSIDE the cell. From there, the table is 2 depths up (cell -> row -> table).
513
+ let rp;
514
+ try {
515
+ rp = resolve(doc, cellPos + 1);
516
+ }
517
+ catch {
518
+ return null;
519
+ }
520
+ // Walk up to find the table node.
521
+ for (let d = rp.depth; d >= 1; d--) {
522
+ if (rp.node(d).type.name === 'table') {
523
+ return { tableStart: rp.before(d), table: rp.node(d) };
524
+ }
525
+ }
526
+ return null;
527
+ }
528
+ /**
529
+ * Merge the cells in the current CellSelection into a single cell. The merged
530
+ * master is the top-left cell of the bounding rectangle; its content becomes
531
+ * the concatenation of every selected cell's block content (top-left first,
532
+ * then left-to-right, top-to-bottom). Spans are set so the master covers the
533
+ * original rectangle.
534
+ *
535
+ * Returns false when:
536
+ * - state.selection is not a CellSelection.
537
+ * - the bounding rectangle would orphan a spanned cell at its edge
538
+ * (a cell's master is inside the rect but the cell extends outside,
539
+ * OR a cell straddles the rect boundary).
540
+ * - the rectangle is 1×1 (nothing to merge).
541
+ *
542
+ * After dispatch, the model selection becomes a TextSelection at the first
543
+ * inline position of the merged master cell.
544
+ */
545
+ export function mergeCells() {
546
+ return (state, dispatch) => {
547
+ const sel = state.selection;
548
+ if (!(sel instanceof CellSelection))
549
+ return false;
550
+ // 1) Locate the containing table via the anchor cell's position.
551
+ const located = findTableForCellPos(state.doc, sel.anchorCell);
552
+ if (!located)
553
+ return false;
554
+ const { tableStart, table } = located;
555
+ // 2) Build the grid.
556
+ const grid = tableGrid(table, tableStart);
557
+ // 3) Map the two corner cell positions to (row, col).
558
+ const anchorCell = grid.byPos(sel.anchorCell);
559
+ const headCell = grid.byPos(sel.headCell);
560
+ if (!anchorCell || !headCell)
561
+ return false;
562
+ // 4) Compute the bounding rectangle, EXTENDING it to include any spanned
563
+ // cells whose masters are inside the rect but whose spans push beyond.
564
+ let r0 = Math.min(anchorCell.row, headCell.row);
565
+ let c0 = Math.min(anchorCell.col, headCell.col);
566
+ let r1 = Math.max(anchorCell.row + anchorCell.rowspan - 1, headCell.row + headCell.rowspan - 1);
567
+ let c1 = Math.max(anchorCell.col + anchorCell.colspan - 1, headCell.col + headCell.colspan - 1);
568
+ // 4b) Iteratively widen the rect to include any master whose rectangle
569
+ // overlaps the current rect (covers the "extends beyond" case).
570
+ let changed = true;
571
+ while (changed) {
572
+ changed = false;
573
+ for (const c of grid.cells) {
574
+ const cellR1 = c.row + c.rowspan - 1;
575
+ const cellC1 = c.col + c.colspan - 1;
576
+ const overlaps = c.row <= r1 && cellR1 >= r0 && c.col <= c1 && cellC1 >= c0;
577
+ if (!overlaps)
578
+ continue;
579
+ const intrudes = c.row < r0 || c.col < c0 || cellR1 > r1 || cellC1 > c1;
580
+ if (!intrudes)
581
+ continue;
582
+ // The cell's master is either inside the rect (extending past) or
583
+ // outside the rect (straddling). In the second case we cannot merge
584
+ // without splitting the foreign cell.
585
+ const masterInside = c.row >= r0 && c.col >= c0 && c.row <= r1 && c.col <= c1;
586
+ if (!masterInside) {
587
+ // Straddling — would orphan a span.
588
+ return false;
589
+ }
590
+ // Master inside but extends past: widen the rect to swallow it.
591
+ if (cellR1 > r1) {
592
+ r1 = cellR1;
593
+ changed = true;
594
+ }
595
+ if (cellC1 > c1) {
596
+ c1 = cellC1;
597
+ changed = true;
598
+ }
599
+ }
600
+ }
601
+ // 5) Reject 1x1 (covers same cell or a span=1 cell).
602
+ if (r0 === r1 && c0 === c1) {
603
+ // Only reject if the rect collapses to a single grid slot. Note that a
604
+ // single colspan>1 master also has r0===r1 && c0!==c1 → not rejected.
605
+ return false;
606
+ }
607
+ if (!dispatch)
608
+ return true;
609
+ // 6) Collect cells in the rect in row-major (document) order.
610
+ const rectCells = [];
611
+ for (const c of grid.cells) {
612
+ if (c.row >= r0 && c.row <= r1 && c.col >= c0 && c.col <= c1) {
613
+ rectCells.push(c);
614
+ }
615
+ }
616
+ // Sort by (row, col) — should already match document order for masters.
617
+ rectCells.sort((a, b) => (a.row - b.row) || (a.col - b.col));
618
+ // 7) Build the merged master's content: concatenate every cell's block
619
+ // content (top-left first).
620
+ const mergedBlocks = [];
621
+ for (const c of rectCells) {
622
+ for (let i = 0; i < c.node.content.childCount; i++) {
623
+ mergedBlocks.push(c.node.content.child(i));
624
+ }
625
+ }
626
+ // Guarantee at least one block (cell content must be non-empty).
627
+ if (mergedBlocks.length === 0) {
628
+ mergedBlocks.push(state.schema.node('paragraph', null));
629
+ }
630
+ const masterOriginal = rectCells[0];
631
+ const newMaster = state.schema.node('table_cell', {
632
+ ...masterOriginal.node.attrs,
633
+ colspan: c1 - c0 + 1,
634
+ rowspan: r1 - r0 + 1,
635
+ }, mergedBlocks);
636
+ // 8) Rebuild the table — walk rows; for each row, emit cells. In the
637
+ // rectangle's row range, only emit the master in its starting row;
638
+ // skip every cell whose master is in `rectCells` and which is not the
639
+ // chosen master.
640
+ const mergedCellPositions = new Set();
641
+ for (const c of rectCells)
642
+ mergedCellPositions.add(c.pos);
643
+ // `mergedRow` tracks the row in which we plant the new master.
644
+ const newRows = [];
645
+ for (let r = 0; r < table.content.childCount; r++) {
646
+ const row = table.content.child(r);
647
+ const newCells = [];
648
+ // Walk row's cells; we need their grid coordinates. We can recover them
649
+ // via the grid by matching cell positions.
650
+ // Build per-row cell list from grid.cells filtered by row === r.
651
+ const rowMasters = grid.cells.filter((c) => c.row === r);
652
+ // Sort by col for stable traversal.
653
+ rowMasters.sort((a, b) => a.col - b.col);
654
+ for (const c of rowMasters) {
655
+ if (mergedCellPositions.has(c.pos)) {
656
+ if (c.pos === masterOriginal.pos) {
657
+ // Emit the new merged master in its master row.
658
+ newCells.push(newMaster);
659
+ }
660
+ // else: skip — absorbed into the master.
661
+ continue;
662
+ }
663
+ newCells.push(c.node);
664
+ }
665
+ if (newCells.length === 0) {
666
+ // Row became empty (entire row absorbed by spans of merged cells).
667
+ // This shouldn't happen with our row-master semantics — rows whose
668
+ // cells are all spanned-into from elsewhere keep their non-master
669
+ // grid slots produced by other masters. But guard anyway: ensure
670
+ // table_row content (cell+) is satisfied.
671
+ continue;
672
+ }
673
+ newRows.push(state.schema.node('table_row', row.attrs, newCells));
674
+ }
675
+ const newTable = state.schema.node('table', table.attrs, newRows);
676
+ const slice = new Slice(Fragment.from([newTable]), 0, 0);
677
+ const tableEnd = tableStart + table.nodeSize;
678
+ const tr = state.tr.step(new ReplaceStep(tableStart, tableEnd, slice));
679
+ // 9) Cursor at the first inline of the new master.
680
+ // Compute the master's [open] position in the NEW table.
681
+ const newMasterRowIdx = newRows.findIndex((rr) => {
682
+ for (let i = 0; i < rr.content.childCount; i++) {
683
+ if (rr.content.child(i) === newMaster)
684
+ return true;
685
+ }
686
+ return false;
687
+ });
688
+ let masterColIdx = 0;
689
+ if (newMasterRowIdx >= 0) {
690
+ const rr = newRows[newMasterRowIdx];
691
+ for (let i = 0; i < rr.content.childCount; i++) {
692
+ if (rr.content.child(i) === newMaster) {
693
+ masterColIdx = i;
694
+ break;
695
+ }
696
+ }
697
+ }
698
+ const cursor = firstInlineOfCellAt(tableStart, newTable, Math.max(0, newMasterRowIdx), masterColIdx);
699
+ setOverride(tr, cursor);
700
+ dispatch(tr);
701
+ return true;
702
+ };
703
+ }
704
+ /**
705
+ * If the cursor is inside a cell with colspan > 1 or rowspan > 1, split that
706
+ * cell back into individual 1×1 cells. The original cell keeps its content;
707
+ * the freed grid positions are filled with fresh empty cells (one empty
708
+ * paragraph each).
709
+ *
710
+ * Returns false if cursor isn't in a cell, or the cell already has
711
+ * colspan === 1 AND rowspan === 1.
712
+ */
713
+ export function splitCell() {
714
+ return (state, dispatch) => {
715
+ const rp = resolve(state.doc, state.selection.from);
716
+ const ctx = findTableContext(rp);
717
+ if (!ctx)
718
+ return false;
719
+ // Find this cell in the grid to read its true (row, col, spans).
720
+ const grid = tableGrid(ctx.table, ctx.tableStart);
721
+ // The cell at the cursor is the rowIdx'th master row in the grid row OR
722
+ // we can look up by walking the row's cells. Simpler: compute the cell's
723
+ // [open] position via the same logic as firstInlineOfCellAt, then byPos.
724
+ let cellOpenPos = ctx.tableStart + 1; // row[0][open]
725
+ for (let r = 0; r < ctx.rowIdx; r++) {
726
+ cellOpenPos += ctx.table.content.child(r).nodeSize;
727
+ }
728
+ cellOpenPos += 1; // step inside the row
729
+ const cursorRow = ctx.table.content.child(ctx.rowIdx);
730
+ for (let c = 0; c < ctx.colIdx; c++) {
731
+ cellOpenPos += cursorRow.content.child(c).nodeSize;
732
+ }
733
+ const cellInfo = grid.byPos(cellOpenPos);
734
+ if (!cellInfo)
735
+ return false;
736
+ if (cellInfo.colspan === 1 && cellInfo.rowspan === 1)
737
+ return false;
738
+ if (!dispatch)
739
+ return true;
740
+ // Build the new table. We need to insert empty cells into the appropriate
741
+ // rows to fill the (rowspan × colspan) rectangle the master used to cover,
742
+ // and reset the master's spans to 1.
743
+ const { row: masterRow, col: masterCol, rowspan, colspan } = cellInfo;
744
+ const r0 = masterRow;
745
+ const r1 = masterRow + rowspan - 1;
746
+ const c0 = masterCol;
747
+ const c1 = masterCol + colspan - 1;
748
+ const newMasterNode = state.schema.node('table_cell', { ...cellInfo.node.attrs, colspan: 1, rowspan: 1 },
749
+ // Preserve content (which must be non-empty per schema).
750
+ cellInfo.node.content);
751
+ // For each row in [r0..r1], we need to assemble a new list of cells. We
752
+ // walk the grid's view of that row and determine, for each column in
753
+ // [0..cols-1], which physical cell to emit (skipping spans that aren't
754
+ // the master's slot).
755
+ const newRows = [];
756
+ for (let r = 0; r < ctx.table.content.childCount; r++) {
757
+ const oldRow = ctx.table.content.child(r);
758
+ // Outside the split rectangle: keep the row untouched.
759
+ if (r < r0 || r > r1) {
760
+ newRows.push(oldRow);
761
+ continue;
762
+ }
763
+ // Inside the rectangle: rebuild the row.
764
+ // Step 1: collect every master that BEGINS in this row (existing).
765
+ const rowMasters = grid.cells.filter((c) => c.row === r);
766
+ rowMasters.sort((a, b) => a.col - b.col);
767
+ // Step 2: build the list of cells for this row by scanning columns.
768
+ const newCells = [];
769
+ let col = 0;
770
+ const cols = grid.cols;
771
+ while (col < cols) {
772
+ // What cell occupies grid slot (r, col)?
773
+ const occ = grid.cellAt(r, col);
774
+ const inSplitRect = col >= c0 && col <= c1;
775
+ if (inSplitRect) {
776
+ if (r === r0 && col === c0) {
777
+ // The master's slot — emit the de-spanned master node.
778
+ newCells.push(newMasterNode);
779
+ }
780
+ else {
781
+ // Freed slot — emit a fresh empty cell.
782
+ newCells.push(makeEmptyCell(state.schema));
783
+ }
784
+ col++;
785
+ continue;
786
+ }
787
+ // Outside the split rect: if a master STARTS at (r, col), emit it.
788
+ // Otherwise we're in a span foreign to the split — but in that case
789
+ // the master begins in a different row's column AND the slot is
790
+ // occupied by a master in this same row only if rowspan=1 already.
791
+ // For correctness we only emit when occ is a master beginning at
792
+ // (r, col).
793
+ if (occ && occ.row === r && occ.col === col) {
794
+ newCells.push(occ.node);
795
+ col += occ.colspan;
796
+ }
797
+ else {
798
+ // Slot owned by a span beginning in another row → no cell to emit
799
+ // in THIS row's child list (HTML row layout).
800
+ col++;
801
+ }
802
+ }
803
+ // Note: it is possible newCells is empty if every slot in this row is
804
+ // owned by spans from elsewhere AND the split rect spans the entire
805
+ // row. We always include the split rect's columns above, so this row
806
+ // will always emit at least the columns from c0..c1.
807
+ newRows.push(state.schema.node('table_row', oldRow.attrs, newCells));
808
+ }
809
+ const newTable = state.schema.node('table', ctx.table.attrs, newRows);
810
+ const slice = new Slice(Fragment.from([newTable]), 0, 0);
811
+ const tableEnd = ctx.tableStart + ctx.table.nodeSize;
812
+ const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
813
+ // Cursor: first inline of the (still in place) master cell. Find its
814
+ // index in its row.
815
+ const newMasterRow = newRows[r0];
816
+ let newMasterColIdx = 0;
817
+ for (let i = 0; i < newMasterRow.content.childCount; i++) {
818
+ if (newMasterRow.content.child(i) === newMasterNode) {
819
+ newMasterColIdx = i;
820
+ break;
821
+ }
822
+ }
823
+ const cursor = firstInlineOfCellAt(ctx.tableStart, newTable, r0, newMasterColIdx);
824
+ setOverride(tr, cursor);
825
+ dispatch(tr);
826
+ return true;
827
+ };
828
+ }
829
+ /**
830
+ * Toggle the `header` attr of the table row containing the cursor.
831
+ * Returns false when the cursor isn't inside a table row.
832
+ *
833
+ * The renderer treats header rows specially (either by emitting them
834
+ * inside `<thead>` OR by adding an `is-header` class to the `<tr>`).
835
+ * Either is fine for v0.3c — the toolbar styling on the example apps
836
+ * works against the class.
837
+ */
838
+ export function toggleHeaderRow() {
839
+ return (state, dispatch) => {
840
+ const rp = resolve(state.doc, state.selection.from);
841
+ // Walk up to find the table_row depth.
842
+ let rowDepth = -1;
843
+ for (let d = rp.depth; d >= 1; d--) {
844
+ if (rp.node(d).type.name === 'table_row') {
845
+ rowDepth = d;
846
+ break;
847
+ }
848
+ }
849
+ if (rowDepth < 1)
850
+ return false;
851
+ const row = rp.node(rowDepth);
852
+ const current = row.attrs.header ?? false;
853
+ const rowPos = rp.before(rowDepth);
854
+ if (!dispatch)
855
+ return true;
856
+ const tr = state.tr.step(new AttrStep(rowPos, 'header', !current));
857
+ // Preserve the cursor.
858
+ setOverride(tr, state.selection.from);
859
+ dispatch(tr);
860
+ return true;
861
+ };
862
+ }
863
+ /**
864
+ * Compute the absolute position of the first inline position of the cell at
865
+ * (rowIdx, colIdx) within a freshly-constructed table whose [open] sits at
866
+ * `tableStart`.
867
+ */
868
+ function firstInlineOfCellAt(tableStart, table, rowIdx, colIdx) {
869
+ // tableStart points at the table's [open]; content starts at tableStart+1.
870
+ let pos = tableStart + 1;
871
+ for (let r = 0; r < rowIdx; r++) {
872
+ pos += table.content.child(r).nodeSize;
873
+ }
874
+ // Now `pos` is the [open] of the target row; step inside.
875
+ pos += 1;
876
+ const row = table.content.child(rowIdx);
877
+ for (let c = 0; c < colIdx; c++) {
878
+ pos += row.content.child(c).nodeSize;
879
+ }
880
+ // `pos` is the [open] of the target cell. Step inside the cell, then into
881
+ // the first paragraph's content (one more open token).
882
+ return pos + 2;
883
+ }
884
+ //# sourceMappingURL=table-commands.js.map