smartrte-core 0.2.1 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +23 -0
  3. package/dist/command.d.ts +8 -8
  4. package/dist/editor.d.ts +32 -0
  5. package/dist/editor.js +121 -0
  6. package/dist/foundation/annotations/index.d.ts +2 -0
  7. package/dist/foundation/annotations/index.js +2 -0
  8. package/dist/foundation/annotations/range.d.ts +54 -0
  9. package/dist/foundation/annotations/range.js +158 -0
  10. package/dist/foundation/annotations/types.d.ts +33 -0
  11. package/dist/foundation/annotations/types.js +1 -0
  12. package/dist/foundation/atom/commands.d.ts +11 -0
  13. package/dist/foundation/atom/commands.js +118 -0
  14. package/dist/foundation/atom/composition.d.ts +8 -0
  15. package/dist/foundation/atom/composition.js +22 -0
  16. package/dist/foundation/atom/declarations.d.ts +2 -0
  17. package/dist/foundation/atom/declarations.js +17 -0
  18. package/dist/foundation/atom/formats.d.ts +24 -0
  19. package/dist/foundation/atom/formats.js +105 -0
  20. package/dist/foundation/atom/index.d.ts +10 -0
  21. package/dist/foundation/atom/index.js +10 -0
  22. package/dist/foundation/atom/lifecycle.d.ts +12 -0
  23. package/dist/foundation/atom/lifecycle.js +41 -0
  24. package/dist/foundation/atom/persistence.d.ts +3 -0
  25. package/dist/foundation/atom/persistence.js +20 -0
  26. package/dist/foundation/atom/plugin.d.ts +20 -0
  27. package/dist/foundation/atom/plugin.js +55 -0
  28. package/dist/foundation/atom/schema.d.ts +3 -0
  29. package/dist/foundation/atom/schema.js +29 -0
  30. package/dist/foundation/atom/security.d.ts +10 -0
  31. package/dist/foundation/atom/security.js +20 -0
  32. package/dist/foundation/atom/selection.d.ts +3 -0
  33. package/dist/foundation/atom/selection.js +4 -0
  34. package/dist/foundation/atom/types.d.ts +47 -0
  35. package/dist/foundation/atom/types.js +1 -0
  36. package/dist/foundation/block/commands.d.ts +17 -0
  37. package/dist/foundation/block/commands.js +197 -0
  38. package/dist/foundation/block/formats.d.ts +33 -0
  39. package/dist/foundation/block/formats.js +57 -0
  40. package/dist/foundation/block/index.d.ts +5 -0
  41. package/dist/foundation/block/index.js +5 -0
  42. package/dist/foundation/block/input.d.ts +19 -0
  43. package/dist/foundation/block/input.js +67 -0
  44. package/dist/foundation/block/plugin.d.ts +26 -0
  45. package/dist/foundation/block/plugin.js +79 -0
  46. package/dist/foundation/block/schema.d.ts +49 -0
  47. package/dist/foundation/block/schema.js +27 -0
  48. package/dist/foundation/block/types.d.ts +40 -0
  49. package/dist/foundation/block/types.js +1 -0
  50. package/dist/foundation/boundaries.d.ts +11 -0
  51. package/dist/foundation/boundaries.js +117 -0
  52. package/dist/foundation/clipboard/detection.d.ts +3 -0
  53. package/dist/foundation/clipboard/detection.js +29 -0
  54. package/dist/foundation/clipboard/diagnostics.d.ts +24 -0
  55. package/dist/foundation/clipboard/diagnostics.js +59 -0
  56. package/dist/foundation/clipboard/index.d.ts +8 -0
  57. package/dist/foundation/clipboard/index.js +8 -0
  58. package/dist/foundation/clipboard/insertion.d.ts +22 -0
  59. package/dist/foundation/clipboard/insertion.js +210 -0
  60. package/dist/foundation/clipboard/normalizers.d.ts +8 -0
  61. package/dist/foundation/clipboard/normalizers.js +229 -0
  62. package/dist/foundation/clipboard/pipeline.d.ts +13 -0
  63. package/dist/foundation/clipboard/pipeline.js +58 -0
  64. package/dist/foundation/clipboard/sanitizer.d.ts +3 -0
  65. package/dist/foundation/clipboard/sanitizer.js +58 -0
  66. package/dist/foundation/clipboard/serialization.d.ts +5 -0
  67. package/dist/foundation/clipboard/serialization.js +33 -0
  68. package/dist/foundation/clipboard/transfer.d.ts +11 -0
  69. package/dist/foundation/clipboard/transfer.js +108 -0
  70. package/dist/foundation/clipboard/types.d.ts +65 -0
  71. package/dist/foundation/clipboard/types.js +1 -0
  72. package/dist/foundation/collab/index.d.ts +3 -0
  73. package/dist/foundation/collab/index.js +3 -0
  74. package/dist/foundation/collab/rebase.d.ts +28 -0
  75. package/dist/foundation/collab/rebase.js +82 -0
  76. package/dist/foundation/collab/transform.d.ts +73 -0
  77. package/dist/foundation/collab/transform.js +251 -0
  78. package/dist/foundation/collab/types.d.ts +52 -0
  79. package/dist/foundation/collab/types.js +1 -0
  80. package/dist/foundation/comments/fromSelection.d.ts +16 -0
  81. package/dist/foundation/comments/fromSelection.js +24 -0
  82. package/dist/foundation/comments/index.d.ts +3 -0
  83. package/dist/foundation/comments/index.js +3 -0
  84. package/dist/foundation/comments/rebase.d.ts +31 -0
  85. package/dist/foundation/comments/rebase.js +43 -0
  86. package/dist/foundation/comments/types.d.ts +37 -0
  87. package/dist/foundation/comments/types.js +1 -0
  88. package/dist/foundation/diff/documentDiff.d.ts +12 -0
  89. package/dist/foundation/diff/documentDiff.js +148 -0
  90. package/dist/foundation/diff/index.d.ts +3 -0
  91. package/dist/foundation/diff/index.js +3 -0
  92. package/dist/foundation/diff/textDiff.d.ts +12 -0
  93. package/dist/foundation/diff/textDiff.js +114 -0
  94. package/dist/foundation/diff/types.d.ts +74 -0
  95. package/dist/foundation/diff/types.js +1 -0
  96. package/dist/foundation/editor.d.ts +138 -0
  97. package/dist/foundation/editor.js +398 -0
  98. package/dist/foundation/formats/codec.d.ts +45 -0
  99. package/dist/foundation/formats/codec.js +1 -0
  100. package/dist/foundation/formats/docx/export.d.ts +4 -0
  101. package/dist/foundation/formats/docx/export.js +285 -0
  102. package/dist/foundation/formats/docx/import.d.ts +9 -0
  103. package/dist/foundation/formats/docx/import.js +37 -0
  104. package/dist/foundation/formats/docx/nestingGuard.d.ts +40 -0
  105. package/dist/foundation/formats/docx/nestingGuard.js +66 -0
  106. package/dist/foundation/formats/docx/portableAtoms.d.ts +4 -0
  107. package/dist/foundation/formats/docx/portableAtoms.js +66 -0
  108. package/dist/foundation/formats/docx/styledImport.d.ts +10 -0
  109. package/dist/foundation/formats/docx/styledImport.js +205 -0
  110. package/dist/foundation/formats/featureCodecs.d.ts +37 -0
  111. package/dist/foundation/formats/featureCodecs.js +69 -0
  112. package/dist/foundation/formats/fidelity.d.ts +31 -0
  113. package/dist/foundation/formats/fidelity.js +123 -0
  114. package/dist/foundation/formats/index.d.ts +7 -0
  115. package/dist/foundation/formats/index.js +7 -0
  116. package/dist/foundation/formats/pdf/format.d.ts +29 -0
  117. package/dist/foundation/formats/pdf/format.js +190 -0
  118. package/dist/foundation/history.d.ts +20 -0
  119. package/dist/foundation/history.js +121 -0
  120. package/dist/foundation/identity.d.ts +8 -0
  121. package/dist/foundation/identity.js +30 -0
  122. package/dist/foundation/index.d.ts +30 -0
  123. package/dist/foundation/index.js +30 -0
  124. package/dist/foundation/list/commands.d.ts +28 -0
  125. package/dist/foundation/list/commands.js +422 -0
  126. package/dist/foundation/list/formats.d.ts +21 -0
  127. package/dist/foundation/list/formats.js +813 -0
  128. package/dist/foundation/list/index.d.ts +8 -0
  129. package/dist/foundation/list/index.js +8 -0
  130. package/dist/foundation/list/input.d.ts +35 -0
  131. package/dist/foundation/list/input.js +320 -0
  132. package/dist/foundation/list/plugin.d.ts +21 -0
  133. package/dist/foundation/list/plugin.js +71 -0
  134. package/dist/foundation/list/presets.d.ts +16 -0
  135. package/dist/foundation/list/presets.js +21 -0
  136. package/dist/foundation/list/schema.d.ts +35 -0
  137. package/dist/foundation/list/schema.js +29 -0
  138. package/dist/foundation/list/shadow.d.ts +54 -0
  139. package/dist/foundation/list/shadow.js +75 -0
  140. package/dist/foundation/list/shadowCorpus.d.ts +10 -0
  141. package/dist/foundation/list/shadowCorpus.js +41 -0
  142. package/dist/foundation/list/types.d.ts +50 -0
  143. package/dist/foundation/list/types.js +1 -0
  144. package/dist/foundation/mapping.d.ts +11 -0
  145. package/dist/foundation/mapping.js +40 -0
  146. package/dist/foundation/marks/canonical.d.ts +9 -0
  147. package/dist/foundation/marks/canonical.js +72 -0
  148. package/dist/foundation/marks/commands.d.ts +21 -0
  149. package/dist/foundation/marks/commands.js +136 -0
  150. package/dist/foundation/marks/dom.d.ts +2 -0
  151. package/dist/foundation/marks/dom.js +46 -0
  152. package/dist/foundation/marks/editor.d.ts +8 -0
  153. package/dist/foundation/marks/editor.js +50 -0
  154. package/dist/foundation/marks/formats.d.ts +37 -0
  155. package/dist/foundation/marks/formats.js +49 -0
  156. package/dist/foundation/marks/hardBreak.d.ts +8 -0
  157. package/dist/foundation/marks/hardBreak.js +80 -0
  158. package/dist/foundation/marks/index.d.ts +11 -0
  159. package/dist/foundation/marks/index.js +11 -0
  160. package/dist/foundation/marks/normalization.d.ts +2 -0
  161. package/dist/foundation/marks/normalization.js +49 -0
  162. package/dist/foundation/marks/plugin.d.ts +14 -0
  163. package/dist/foundation/marks/plugin.js +63 -0
  164. package/dist/foundation/marks/resolveMarkRun.d.ts +8 -0
  165. package/dist/foundation/marks/resolveMarkRun.js +47 -0
  166. package/dist/foundation/marks/schema.d.ts +167 -0
  167. package/dist/foundation/marks/schema.js +50 -0
  168. package/dist/foundation/marks/stored.d.ts +2 -0
  169. package/dist/foundation/marks/stored.js +31 -0
  170. package/dist/foundation/marks/types.d.ts +40 -0
  171. package/dist/foundation/marks/types.js +1 -0
  172. package/dist/foundation/modelDom.d.ts +38 -0
  173. package/dist/foundation/modelDom.js +327 -0
  174. package/dist/foundation/normalization.d.ts +22 -0
  175. package/dist/foundation/normalization.js +132 -0
  176. package/dist/foundation/operations.d.ts +28 -0
  177. package/dist/foundation/operations.js +568 -0
  178. package/dist/foundation/plugin/builtins.d.ts +10 -0
  179. package/dist/foundation/plugin/builtins.js +27 -0
  180. package/dist/foundation/plugin/dispatch.d.ts +33 -0
  181. package/dist/foundation/plugin/dispatch.js +34 -0
  182. package/dist/foundation/plugin/index.d.ts +4 -0
  183. package/dist/foundation/plugin/index.js +4 -0
  184. package/dist/foundation/plugin/registry.d.ts +36 -0
  185. package/dist/foundation/plugin/registry.js +115 -0
  186. package/dist/foundation/plugin/types.d.ts +141 -0
  187. package/dist/foundation/plugin/types.js +13 -0
  188. package/dist/foundation/positions.d.ts +20 -0
  189. package/dist/foundation/positions.js +121 -0
  190. package/dist/foundation/schema.d.ts +57 -0
  191. package/dist/foundation/schema.js +371 -0
  192. package/dist/foundation/schemaBuilder.d.ts +41 -0
  193. package/dist/foundation/schemaBuilder.js +96 -0
  194. package/dist/foundation/scope/index.d.ts +2 -0
  195. package/dist/foundation/scope/index.js +2 -0
  196. package/dist/foundation/scope/resolveScope.d.ts +13 -0
  197. package/dist/foundation/scope/resolveScope.js +860 -0
  198. package/dist/foundation/scope/types.d.ts +122 -0
  199. package/dist/foundation/scope/types.js +1 -0
  200. package/dist/foundation/security/urlPolicy.d.ts +21 -0
  201. package/dist/foundation/security/urlPolicy.js +75 -0
  202. package/dist/foundation/structural/contentTarget.d.ts +21 -0
  203. package/dist/foundation/structural/contentTarget.js +51 -0
  204. package/dist/foundation/structural/index.d.ts +2 -0
  205. package/dist/foundation/structural/index.js +2 -0
  206. package/dist/foundation/structural/move.d.ts +7 -0
  207. package/dist/foundation/structural/move.js +35 -0
  208. package/dist/foundation/suggestions/index.d.ts +3 -0
  209. package/dist/foundation/suggestions/index.js +3 -0
  210. package/dist/foundation/suggestions/inline.d.ts +64 -0
  211. package/dist/foundation/suggestions/inline.js +116 -0
  212. package/dist/foundation/suggestions/structural.d.ts +40 -0
  213. package/dist/foundation/suggestions/structural.js +47 -0
  214. package/dist/foundation/suggestions/types.d.ts +19 -0
  215. package/dist/foundation/suggestions/types.js +1 -0
  216. package/dist/foundation/surface/index.d.ts +3 -0
  217. package/dist/foundation/surface/index.js +3 -0
  218. package/dist/foundation/surface/input.d.ts +127 -0
  219. package/dist/foundation/surface/input.js +1761 -0
  220. package/dist/foundation/surface/renderer.d.ts +84 -0
  221. package/dist/foundation/surface/renderer.js +974 -0
  222. package/dist/foundation/surface/types.d.ts +48 -0
  223. package/dist/foundation/surface/types.js +1 -0
  224. package/dist/foundation/table/commands.d.ts +31 -0
  225. package/dist/foundation/table/commands.js +470 -0
  226. package/dist/foundation/table/formats.d.ts +27 -0
  227. package/dist/foundation/table/formats.js +40 -0
  228. package/dist/foundation/table/grid.d.ts +8 -0
  229. package/dist/foundation/table/grid.js +178 -0
  230. package/dist/foundation/table/index.d.ts +6 -0
  231. package/dist/foundation/table/index.js +6 -0
  232. package/dist/foundation/table/plugin.d.ts +23 -0
  233. package/dist/foundation/table/plugin.js +119 -0
  234. package/dist/foundation/table/schema.d.ts +2 -0
  235. package/dist/foundation/table/schema.js +38 -0
  236. package/dist/foundation/table/selection.d.ts +7 -0
  237. package/dist/foundation/table/selection.js +35 -0
  238. package/dist/foundation/table/types.d.ts +109 -0
  239. package/dist/foundation/table/types.js +1 -0
  240. package/dist/foundation/transactions.d.ts +41 -0
  241. package/dist/foundation/transactions.js +90 -0
  242. package/dist/foundation/types.d.ts +261 -0
  243. package/dist/foundation/types.js +1 -0
  244. package/dist/foundation/versioning/diffVersions.d.ts +5 -0
  245. package/dist/foundation/versioning/diffVersions.js +3 -0
  246. package/dist/foundation/versioning/index.d.ts +2 -0
  247. package/dist/foundation/versioning/index.js +2 -0
  248. package/dist/foundation/versioning/types.d.ts +17 -0
  249. package/dist/foundation/versioning/types.js +1 -0
  250. package/dist/history.d.ts +20 -0
  251. package/dist/history.js +165 -0
  252. package/dist/html/compatibility.js +7 -1
  253. package/dist/index.d.ts +1 -14
  254. package/dist/index.js +9 -14
  255. package/dist/legacy/index.d.ts +45 -0
  256. package/dist/legacy/index.js +45 -0
  257. package/dist/legacyCommands/alignment.d.ts +20 -0
  258. package/dist/{commands → legacyCommands}/alignment.js +17 -0
  259. package/dist/legacyCommands/blocks.d.ts +14 -0
  260. package/dist/legacyCommands/blocks.js +105 -0
  261. package/dist/legacyCommands/checklist.d.ts +11 -0
  262. package/dist/legacyCommands/checklist.js +45 -0
  263. package/dist/legacyCommands/code.d.ts +15 -0
  264. package/dist/{commands → legacyCommands}/code.js +25 -1
  265. package/dist/legacyCommands/formula.d.ts +7 -0
  266. package/dist/legacyCommands/formula.js +67 -0
  267. package/dist/legacyCommands/inlineAtoms.d.ts +7 -0
  268. package/dist/legacyCommands/inlineAtoms.js +58 -0
  269. package/dist/legacyCommands/list.d.ts +38 -0
  270. package/dist/legacyCommands/list.js +477 -0
  271. package/dist/{commands → legacyCommands}/marks.d.ts +3 -0
  272. package/dist/{commands → legacyCommands}/marks.js +28 -11
  273. package/dist/legacyCommands/media.d.ts +31 -0
  274. package/dist/legacyCommands/media.js +205 -0
  275. package/dist/legacyCommands/move.d.ts +14 -0
  276. package/dist/legacyCommands/move.js +91 -0
  277. package/dist/legacyCommands/table.d.ts +91 -0
  278. package/dist/legacyCommands/table.js +547 -0
  279. package/dist/listPresets.d.ts +12 -0
  280. package/dist/listPresets.js +6 -0
  281. package/dist/listScope.d.ts +27 -0
  282. package/dist/listScope.js +104 -0
  283. package/dist/markdown/compatibility.d.ts +2 -0
  284. package/dist/markdown/compatibility.js +94 -2
  285. package/dist/marks.d.ts +5 -5
  286. package/dist/model.d.ts +65 -9
  287. package/dist/plugin.d.ts +15 -0
  288. package/dist/plugin.js +31 -0
  289. package/dist/plugins/alignment.d.ts +3 -0
  290. package/dist/plugins/alignment.js +6 -0
  291. package/dist/plugins/basicFormatting.d.ts +9 -0
  292. package/dist/plugins/basicFormatting.js +24 -0
  293. package/dist/plugins/blockType.d.ts +3 -0
  294. package/dist/plugins/blockType.js +6 -0
  295. package/dist/plugins/blockquote.d.ts +3 -0
  296. package/dist/plugins/blockquote.js +6 -0
  297. package/dist/plugins/checklist.d.ts +3 -0
  298. package/dist/plugins/checklist.js +10 -0
  299. package/dist/plugins/codeBlock.d.ts +3 -0
  300. package/dist/plugins/codeBlock.js +6 -0
  301. package/dist/plugins/formula.d.ts +3 -0
  302. package/dist/plugins/formula.js +9 -0
  303. package/dist/plugins/list.d.ts +3 -0
  304. package/dist/plugins/list.js +10 -0
  305. package/dist/plugins/media.d.ts +3 -0
  306. package/dist/plugins/media.js +12 -0
  307. package/dist/plugins/move.d.ts +3 -0
  308. package/dist/plugins/move.js +9 -0
  309. package/dist/plugins/table.d.ts +3 -0
  310. package/dist/plugins/table.js +22 -0
  311. package/dist/preset.d.ts +12 -0
  312. package/dist/preset.js +40 -0
  313. package/dist/schema.d.ts +22 -0
  314. package/dist/schema.js +333 -0
  315. package/dist/security/urlPolicy.d.ts +1 -17
  316. package/dist/security/urlPolicy.js +3 -69
  317. package/dist/selection.d.ts +1 -1
  318. package/dist/selectionMapping.d.ts +4 -0
  319. package/dist/selectionMapping.js +222 -0
  320. package/dist/table.d.ts +3 -3
  321. package/dist/transaction.d.ts +35 -12
  322. package/dist/transaction.js +174 -28
  323. package/dist/tree.d.ts +8 -2
  324. package/dist/tree.js +44 -0
  325. package/package.json +27 -8
  326. package/dist/commands/alignment.d.ts +0 -18
  327. package/dist/commands/code.d.ts +0 -13
  328. package/dist/commands/list.d.ts +0 -20
  329. package/dist/commands/list.js +0 -52
@@ -0,0 +1,48 @@
1
+ import type { FoundationEditor } from "../editor.js";
2
+ import type { ModelDomMapping, SmartDocument, SmartSelection } from "../types.js";
3
+ import type { ClipboardDiagnosticReport } from "../clipboard/diagnostics.js";
4
+ export interface CanonicalSubtreeRenderer {
5
+ readonly mapping: ModelDomMapping;
6
+ readonly composingNodeId: string | null;
7
+ readonly domWriteCount: number;
8
+ readonly composingDomWriteCount: number;
9
+ render(document: SmartDocument, selection: SmartSelection): void;
10
+ beginComposition(nodeId: string): void;
11
+ endComposition(): void;
12
+ resetWriteCounters(): void;
13
+ destroy(): void;
14
+ }
15
+ export interface CanonicalInputPipeline {
16
+ readonly editor: FoundationEditor;
17
+ readonly renderer: CanonicalSubtreeRenderer;
18
+ readonly unhandledInputTypes: readonly string[];
19
+ handleBeforeInput(event: InputEvent): void;
20
+ handleKeyDown(event: KeyboardEvent): void;
21
+ handleCompositionStart(event: CompositionEvent): void;
22
+ handleCompositionUpdate(event: CompositionEvent): void;
23
+ handleCompositionEnd(event: CompositionEvent): void;
24
+ handlePaste(event: ClipboardEvent): void;
25
+ handleCopy(event: ClipboardEvent): void;
26
+ handleCut(event: ClipboardEvent): void;
27
+ handleDrop(event: DragEvent): void;
28
+ syncSelectionFromDom(): void;
29
+ /**
30
+ * Phase 12a §2.3 ambient track-changes mode: when enabled, ordinary
31
+ * typing and same-owner Backspace/Delete become live "suggestion" marks
32
+ * (see suggestions/inline.ts) instead of directly editing the document -
33
+ * exactly what most people mean by "track changes"/"suggesting mode" in
34
+ * Word/Google Docs, as opposed to the explicit per-action Suggest
35
+ * deletion/Suggest insertion toolbar commands, which remain available
36
+ * regardless of this mode. Scoped to insertion and same-owner deletion
37
+ * only - cross-paragraph deletion and the cross-block Backspace/Delete
38
+ * merge (deleteAcrossBlock) still edit directly even when enabled; see
39
+ * docs/PHASE_ROADMAP_8B_12B.md's Phase 12a status note for why.
40
+ */
41
+ setTrackChanges(enabled: boolean, authorId?: string): void;
42
+ destroy(): void;
43
+ }
44
+ export interface CanonicalInputPipelineOptions {
45
+ onFiles?: (files: readonly File[], position: SmartSelection) => void;
46
+ /** Privacy-safe clipboard telemetry; reports hashes and structure, never text. */
47
+ onClipboardDiagnostic?: (report: ClipboardDiagnosticReport) => void;
48
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import type { CellAttributesParams, ColumnParams, ColumnWidthParams, HeaderParams, InsertTableParams, MoveTableAxisParams, RowHeightParams, RowParams, SplitCellParams, TableCommand } from "./types.js";
2
+ export declare const insertTableCommand: TableCommand<InsertTableParams>;
3
+ export declare const removeTableCommand: TableCommand<Record<string, never>>;
4
+ export declare const insertTableRowCommand: TableCommand<RowParams>;
5
+ export declare const removeTableRowCommand: TableCommand<RowParams>;
6
+ export declare const insertTableColumnCommand: TableCommand<ColumnParams>;
7
+ export declare const removeTableColumnCommand: TableCommand<ColumnParams>;
8
+ export declare const mergeTableCellsCommand: TableCommand<Record<string, never>>;
9
+ export declare const splitTableCellCommand: TableCommand<SplitCellParams>;
10
+ export declare const setTableHeaderCommand: TableCommand<HeaderParams>;
11
+ export declare const setTableCellAttributesCommand: TableCommand<CellAttributesParams>;
12
+ export declare const setTableColumnWidthCommand: TableCommand<ColumnWidthParams>;
13
+ export declare const setTableRowHeightCommand: TableCommand<RowHeightParams>;
14
+ export declare const moveTableRowCommand: TableCommand<MoveTableAxisParams>;
15
+ export declare const moveTableColumnCommand: TableCommand<MoveTableAxisParams>;
16
+ export declare const tableCommands: {
17
+ readonly "table.insert": TableCommand<InsertTableParams>;
18
+ readonly "table.remove": TableCommand<Record<string, never>>;
19
+ readonly "table.insertRow": TableCommand<RowParams>;
20
+ readonly "table.removeRow": TableCommand<RowParams>;
21
+ readonly "table.insertColumn": TableCommand<ColumnParams>;
22
+ readonly "table.removeColumn": TableCommand<ColumnParams>;
23
+ readonly "table.mergeCells": TableCommand<Record<string, never>>;
24
+ readonly "table.splitCell": TableCommand<SplitCellParams>;
25
+ readonly "table.setHeader": TableCommand<HeaderParams>;
26
+ readonly "table.setCellAttributes": TableCommand<CellAttributesParams>;
27
+ readonly "table.setColumnWidth": TableCommand<ColumnWidthParams>;
28
+ readonly "table.setRowHeight": TableCommand<RowHeightParams>;
29
+ readonly "table.moveRow": TableCommand<MoveTableAxisParams>;
30
+ readonly "table.moveColumn": TableCommand<MoveTableAxisParams>;
31
+ };
@@ -0,0 +1,470 @@
1
+ import { cloneNode, isTextNode } from "../identity.js";
2
+ import { moveContiguousSiblings } from "../structural/move.js";
3
+ import { occupancyGridFor } from "./grid.js";
4
+ const tableScope = (scope) => scope.kind === "table-grid" ? scope : null;
5
+ const blockScope = (scope) => scope.kind === "block-range" ? scope : null;
6
+ const cleanAttrs = (attrs) => Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== undefined));
7
+ const emptyCell = (cellId, paragraphId, header = false, styleAttrs = {}) => ({
8
+ type: "table_cell", id: cellId, attrs: { rowspan: 1, colspan: 1, header, ...styleAttrs },
9
+ children: [{ type: "paragraph", id: paragraphId, children: [] }],
10
+ });
11
+ /**
12
+ * A newly-inserted row/column defaulted to blank cell styling regardless of
13
+ * neighboring cells - not a broken inheritance attempt, there was never one.
14
+ * New cells now default to the immediately adjacent existing row/column's
15
+ * per-cell style so "Add row"/"Add column" continues a styled table's
16
+ * look instead of visibly breaking it. Structural attrs (rowspan/colspan/
17
+ * header) are handled separately and are deliberately excluded here.
18
+ */
19
+ const CELL_STYLE_ATTR_KEYS = ["background", "borders", "borderTop", "borderRight", "borderBottom", "borderLeft", "textColor", "verticalAlign"];
20
+ const cellStyleAttrs = (node) => {
21
+ const attrs = node?.attrs;
22
+ if (!attrs)
23
+ return {};
24
+ return Object.fromEntries(CELL_STYLE_ATTR_KEYS.filter((key) => attrs[key] !== undefined).map((key) => [key, attrs[key]]));
25
+ };
26
+ /**
27
+ * Cell placeholders are empty paragraphs inserted to keep a cell editable.
28
+ * They must not become stacked lines when several cells are merged. Atomic
29
+ * content remains meaningful even when it has no text descendants.
30
+ */
31
+ const hasMeaningfulContent = (node, ctx) => {
32
+ if (isTextNode(node))
33
+ return node.text.length > 0;
34
+ if (ctx.schema.nodes[node.type]?.atomic)
35
+ return true;
36
+ return (node.children || []).some((child) => hasMeaningfulContent(child, ctx));
37
+ };
38
+ const locateNode = (nodeId, ctx) => {
39
+ const resolved = ctx.positions.positionOf(nodeId);
40
+ const node = resolved?.parent.children?.[resolved.pos.offset];
41
+ if (!resolved || !node || isTextNode(node) || node.id !== nodeId)
42
+ throw new Error(`Unknown or stale node ID "${nodeId}".`);
43
+ return { node, parentPos: { path: [...resolved.pos.path], offset: resolved.pos.offset } };
44
+ };
45
+ const locateTable = (scope, ctx) => {
46
+ const selected = tableScope(scope);
47
+ if (!selected)
48
+ return null;
49
+ const located = locateNode(selected.tableId, ctx);
50
+ if (located.node.type !== "table")
51
+ return null;
52
+ return { scope: selected, table: located.node, parentPos: located.parentPos };
53
+ };
54
+ const consumeEmptyCells = (cellIds, paragraphIds, count, header = false, styleAttrsFor) => {
55
+ if ((cellIds?.length || 0) < count || (paragraphIds?.length || 0) < count)
56
+ throw new Error(`Table command requires ${count} caller-provided cell and paragraph IDs.`);
57
+ return Array.from({ length: count }, (_, index) => emptyCell(cellIds[index], paragraphIds[index], header, styleAttrsFor?.(index)));
58
+ };
59
+ export const insertTableCommand = (_document, scope, params, ctx) => {
60
+ const selected = blockScope(scope);
61
+ const firstId = selected?.blockIds[0];
62
+ if (!firstId || params.rows < 1 || params.columns < 1)
63
+ return [];
64
+ const target = locateNode(firstId, ctx);
65
+ const total = params.rows * params.columns;
66
+ if (params.ids.rowIds.length < params.rows || params.ids.cellIds.length < total || params.ids.paragraphIds.length < total) {
67
+ throw new Error("table.insert requires caller-provided IDs for every row, cell, and paragraph.");
68
+ }
69
+ let cellIndex = 0;
70
+ const table = {
71
+ type: "table", id: params.ids.tableId, attrs: { columnWidths: Array(params.columns).fill(120), layout: "fixed" },
72
+ children: Array.from({ length: params.rows }, (_, row) => ({
73
+ type: "table_row", id: params.ids.rowIds[row], children: Array.from({ length: params.columns }, () => {
74
+ const current = cellIndex++;
75
+ return emptyCell(params.ids.cellIds[current], params.ids.paragraphIds[current], Boolean(params.withHeader && row === 0));
76
+ }),
77
+ })),
78
+ };
79
+ return [{ type: "insertNode", pos: { ...target.parentPos, offset: target.parentPos.offset + (params.placement === "after" ? 1 : 0) }, node: table }];
80
+ };
81
+ export const removeTableCommand = (_document, scope, _params, ctx) => {
82
+ const target = locateTable(scope, ctx);
83
+ return target ? [{ type: "removeNode", pos: target.parentPos, node: target.table }] : [];
84
+ };
85
+ export const insertTableRowCommand = (_document, scope, params, ctx) => {
86
+ const target = locateTable(scope, ctx);
87
+ if (!target)
88
+ return [];
89
+ const grid = occupancyGridFor(target.table);
90
+ const base = params.rowIndex ?? (params.position === "before" ? target.scope.rect.top : target.scope.rect.bottom + 1);
91
+ const index = Math.max(0, Math.min(grid.rows, base));
92
+ // A crossing anchor is always in a row strictly before `index` (its rowspan
93
+ // reaches past `index`), so its own row position is never shifted by this
94
+ // insertion - only its rowspan attribute needs to grow.
95
+ const crossing = grid.anchors.filter((cell) => cell.top < index && cell.bottom > index);
96
+ const covered = new Set();
97
+ crossing.forEach((cell) => { for (let col = cell.left; col < cell.right; col += 1)
98
+ covered.add(col); });
99
+ const columnsNeedingCell = Array.from({ length: grid.columns }, (_, column) => column).filter((column) => !covered.has(column));
100
+ const header = index === 0 && grid.anchors.filter((cell) => cell.top === 0).every((cell) => cell.node.attrs?.header === true);
101
+ // Inherit style from the adjacent existing row - the one immediately
102
+ // before the insertion point (the "last row" for the common append
103
+ // case), or immediately after it if inserting before every existing row.
104
+ const adjacentRow = index > 0 ? index - 1 : (grid.rows > 0 ? 0 : null);
105
+ const rowCells = consumeEmptyCells(params.cellIds, params.paragraphIds, columnsNeedingCell.length, header, adjacentRow === null ? undefined : (cursor) => cellStyleAttrs(grid.at(adjacentRow, columnsNeedingCell[cursor])?.node));
106
+ if (!params.rowId)
107
+ throw new Error("table.insertRow requires a caller-provided row ID.");
108
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
109
+ const newRow = { type: "table_row", id: params.rowId, children: rowCells };
110
+ const operations = [{ type: "insertNode", pos: { path: tablePath, offset: index }, node: newRow }];
111
+ crossing.forEach((cell) => {
112
+ const before = cell.node.attrs || {};
113
+ const after = { ...before, rowspan: (cell.bottom - cell.top) + 1 };
114
+ operations.push({ type: "setNodeAttributes", pos: { path: [...tablePath, cell.top, cell.childIndex], offset: 0 }, before, after });
115
+ });
116
+ return operations;
117
+ };
118
+ export const removeTableRowCommand = (_document, scope, params, ctx) => {
119
+ const target = locateTable(scope, ctx);
120
+ if (!target)
121
+ return [];
122
+ const grid = occupancyGridFor(target.table);
123
+ const index = params.rowIndex ?? target.scope.rect.top;
124
+ if (index < 0 || index >= grid.rows)
125
+ return [];
126
+ if (grid.rows === 1)
127
+ return [{ type: "removeNode", pos: target.parentPos, node: target.table }];
128
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
129
+ const doomedRow = target.table.children?.[index];
130
+ if (!doomedRow || isTextNode(doomedRow))
131
+ return [];
132
+ const operations = [];
133
+ // An anchor physically inside the doomed row whose span reaches further
134
+ // down must relocate into the next row (which becomes this row's new
135
+ // occupant) before the doomed row itself is removed - the row node cannot
136
+ // simply be deleted out from under a cell that still needs to exist.
137
+ const relocating = grid.anchors.filter((cell) => cell.top === index && cell.bottom - cell.top > 1)
138
+ .sort((a, b) => a.childIndex - b.childIndex);
139
+ const destinationRow = index + 1;
140
+ const destinationAnchors = grid.anchors.filter((cell) => cell.top === destinationRow).sort((a, b) => a.left - b.left);
141
+ relocating.forEach((cell, movedBefore) => {
142
+ const sourceOffset = cell.childIndex - movedBefore;
143
+ const destOffset = destinationAnchors.filter((anchor) => anchor.left < cell.left).length;
144
+ operations.push({ type: "moveNode", from: { path: [...tablePath, index], offset: sourceOffset }, to: { path: [...tablePath, destinationRow], offset: destOffset }, nodeId: cell.cellId });
145
+ destinationAnchors.splice(destOffset, 0, cell);
146
+ const before = cell.node.attrs || {};
147
+ const after = { ...before, rowspan: (cell.bottom - cell.top) - 1 };
148
+ operations.push({ type: "setNodeAttributes", pos: { path: [...tablePath, destinationRow, destOffset], offset: 0 }, before, after });
149
+ });
150
+ // Anchors before the doomed row whose span crosses into it just shrink.
151
+ grid.anchors.filter((cell) => cell.top < index && cell.bottom > index).forEach((cell) => {
152
+ const before = cell.node.attrs || {};
153
+ const after = { ...before, rowspan: (cell.bottom - cell.top) - 1 };
154
+ operations.push({ type: "setNodeAttributes", pos: { path: [...tablePath, cell.top, cell.childIndex], offset: 0 }, before, after });
155
+ });
156
+ const relocatedIds = new Set(relocating.map((cell) => cell.cellId));
157
+ const remainingChildren = (doomedRow.children || []).filter((child) => isTextNode(child) || !relocatedIds.has(child.id));
158
+ operations.push({ type: "removeNode", pos: { path: tablePath, offset: index }, node: { ...doomedRow, children: remainingChildren } });
159
+ return operations;
160
+ };
161
+ export const insertTableColumnCommand = (_document, scope, params, ctx) => {
162
+ const target = locateTable(scope, ctx);
163
+ if (!target)
164
+ return [];
165
+ const grid = occupancyGridFor(target.table);
166
+ const base = params.columnIndex ?? (params.position === "before" ? target.scope.rect.left : target.scope.rect.right + 1);
167
+ const index = Math.max(0, Math.min(grid.columns, base));
168
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
169
+ const crossing = grid.anchors.filter((cell) => cell.left < index && cell.right > index);
170
+ const crossingIds = new Set(crossing.map((cell) => cell.cellId));
171
+ const rowsNeedingCell = Array.from({ length: grid.rows }, (_, row) => row).filter((row) => {
172
+ const occupant = grid.at(row, Math.min(index, Math.max(0, grid.columns - 1)));
173
+ return !(occupant && crossingIds.has(occupant.cellId));
174
+ });
175
+ // Inherit style from the adjacent existing column - the one immediately
176
+ // before the insertion point (the "last column" for the common append
177
+ // case), or immediately after it if inserting before every existing
178
+ // column.
179
+ const adjacentColumn = index > 0 ? index - 1 : (grid.columns > 0 ? 0 : null);
180
+ const cells = consumeEmptyCells(params.cellIds, params.paragraphIds, rowsNeedingCell.length, false, adjacentColumn === null ? undefined : (cursor) => cellStyleAttrs(grid.at(rowsNeedingCell[cursor], adjacentColumn)?.node));
181
+ const operations = [];
182
+ rowsNeedingCell.forEach((row, cellIndex) => {
183
+ // A row's insertion offset is however many of its own anchors already
184
+ // sit left of the new column - no other op in this batch touches row
185
+ // structure, so this position is stable regardless of op order.
186
+ const offset = grid.anchors.filter((cell) => cell.top === row && cell.left < index).length;
187
+ operations.push({ type: "insertNode", pos: { path: [...tablePath, row], offset }, node: cells[cellIndex] });
188
+ });
189
+ crossing.forEach((cell) => {
190
+ const before = cell.node.attrs || {};
191
+ const after = { ...before, colspan: (cell.right - cell.left) + 1 };
192
+ operations.push({ type: "setNodeAttributes", pos: { path: [...tablePath, cell.top, cell.childIndex], offset: 0 }, before, after });
193
+ });
194
+ const beforeAttrs = target.table.attrs || {};
195
+ // Only extend columnWidths when the table already has real per-column
196
+ // data. Fabricating a full Array(columns).fill(120) for a table that
197
+ // never had columnWidths (e.g. pasted content with no real per-<col>
198
+ // pixel width, correctly left unset per docs/bugs/table-shrinks-after-
199
+ // paste.md's all-or-nothing rule) makes the renderer pin the table's
200
+ // width to that fabricated sum instead of leaving it at its natural
201
+ // width - the same fabricated-fallback pattern docs/bugs/
202
+ // table-resize-shrinks-table-with-no-prior-columnwidths.md fixed for
203
+ // resize, recurring here for insert.
204
+ if (Array.isArray(beforeAttrs.columnWidths)) {
205
+ const widths = [...beforeAttrs.columnWidths];
206
+ widths.splice(index, 0, widths[Math.max(0, index - 1)] || 120);
207
+ operations.push({ type: "setNodeAttributes", pos: { path: tablePath, offset: 0 }, before: beforeAttrs, after: { ...beforeAttrs, columnWidths: widths } });
208
+ }
209
+ return operations;
210
+ };
211
+ export const removeTableColumnCommand = (_document, scope, params, ctx) => {
212
+ const target = locateTable(scope, ctx);
213
+ if (!target)
214
+ return [];
215
+ const grid = occupancyGridFor(target.table);
216
+ const index = params.columnIndex ?? target.scope.rect.left;
217
+ if (index < 0 || index >= grid.columns)
218
+ return [];
219
+ if (grid.columns === 1)
220
+ return [{ type: "removeNode", pos: target.parentPos, node: target.table }];
221
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
222
+ const operations = [];
223
+ grid.anchors.filter((cell) => index >= cell.left && index < cell.right).forEach((cell) => {
224
+ const colspan = cell.right - cell.left;
225
+ if (colspan === 1) {
226
+ operations.push({ type: "removeNode", pos: { path: [...tablePath, cell.top], offset: cell.childIndex }, node: cell.node });
227
+ }
228
+ else {
229
+ const before = cell.node.attrs || {};
230
+ operations.push({ type: "setNodeAttributes", pos: { path: [...tablePath, cell.top, cell.childIndex], offset: 0 }, before, after: { ...before, colspan: colspan - 1 } });
231
+ }
232
+ });
233
+ const beforeAttrs = target.table.attrs || {};
234
+ const widths = Array.isArray(beforeAttrs.columnWidths) ? beforeAttrs.columnWidths.filter((_, column) => column !== index) : undefined;
235
+ operations.push({ type: "setNodeAttributes", pos: { path: tablePath, offset: 0 }, before: beforeAttrs, after: cleanAttrs({ ...beforeAttrs, columnWidths: widths }) });
236
+ return operations;
237
+ };
238
+ export const mergeTableCellsCommand = (_document, scope, _params, ctx) => {
239
+ const target = locateTable(scope, ctx);
240
+ if (!target || !target.scope.rectangular)
241
+ return [];
242
+ const rect = { top: target.scope.rect.top, left: target.scope.rect.left, bottom: target.scope.rect.bottom + 1, right: target.scope.rect.right + 1 };
243
+ const grid = occupancyGridFor(target.table);
244
+ if (!grid.isRectangular(rect) || rect.bottom - rect.top === 1 && rect.right - rect.left === 1)
245
+ return [];
246
+ const selected = grid.anchorsIn(rect).sort((a, b) => a.top - b.top || a.left - b.left);
247
+ const headers = new Set(selected.map((cell) => cell.node.attrs?.header === true));
248
+ if (headers.size > 1)
249
+ return [];
250
+ const anchor = grid.at(rect.top, rect.left);
251
+ if (!anchor || anchor.top !== rect.top || anchor.left !== rect.left)
252
+ return [];
253
+ const merged = {
254
+ ...anchor.node,
255
+ attrs: { ...(anchor.node.attrs || {}), rowspan: rect.bottom - rect.top, colspan: rect.right - rect.left },
256
+ children: (() => {
257
+ // Each source cell's content becomes its own block in the merged
258
+ // cell - never concatenated onto one line with another cell's
259
+ // content, however short both are. A prior version of this command
260
+ // combined short single-paragraph cells into one shared paragraph to
261
+ // avoid a merged row growing taller than its original single line;
262
+ // that traded away a worse defect (a horizontal merge of "A"/"B"/"C"/
263
+ // "D" cells silently reading as the single run "ABCD") for a cosmetic
264
+ // one. A merged cell's height legitimately growing to fit multiple
265
+ // real lines of content is expected, not a bug - see
266
+ // docs/bugs/table-merge-concatenates-cell-content.md.
267
+ const content = selected.flatMap((cell) => {
268
+ const children = cell.node.children || [];
269
+ // Preserve all blocks from a non-empty cell, including intentional
270
+ // blank lines. Only an entirely empty source cell is discarded.
271
+ return children.some((child) => hasMeaningfulContent(child, ctx)) ? children.map(cloneNode) : [];
272
+ });
273
+ // A merged region made entirely of empty cells still needs one editable
274
+ // block, but never one placeholder block per source cell.
275
+ return content.length ? content : anchor.node.children?.[0] ? [cloneNode(anchor.node.children[0])] : [];
276
+ })(),
277
+ };
278
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
279
+ const operations = [];
280
+ // Cells other than the anchor are dropped. Several can share a row (e.g.
281
+ // merging a 2x2 rectangle), so within each row they must be removed
282
+ // highest-offset-first - applyOperations mutates one session sequentially,
283
+ // and a lower removal first would invalidate a still-pending higher offset.
284
+ const byRow = new Map();
285
+ selected.filter((cell) => cell.cellId !== anchor.cellId).forEach((cell) => {
286
+ byRow.set(cell.top, [...(byRow.get(cell.top) || []), cell]);
287
+ });
288
+ byRow.forEach((cells, row) => {
289
+ [...cells].sort((a, b) => b.childIndex - a.childIndex).forEach((cell) => {
290
+ // mergedInto marks this as a semantic merge, not a plain deletion -
291
+ // an annotation anchored to this cell can snap to the anchor cell
292
+ // instead of orphaning. See annotations/range.ts.
293
+ operations.push({ type: "removeNode", pos: { path: [...tablePath, row], offset: cell.childIndex }, node: cell.node, mergedInto: anchor.node.id });
294
+ });
295
+ });
296
+ // The anchor's replaceNode must be emitted last: its merged content can
297
+ // clone a node id (e.g. a sole surviving paragraph) from a cell removed
298
+ // above. Forward that is invisible - one batch, checked once at the end -
299
+ // but undo replays operations one at a time, so the clone must not exist
300
+ // yet when the removal's inverse re-creates the original with that id.
301
+ operations.push({ type: "replaceNode", pos: { path: [...tablePath, anchor.top], offset: anchor.childIndex }, before: anchor.node, after: merged });
302
+ return operations;
303
+ };
304
+ export const splitTableCellCommand = (_document, scope, params, ctx) => {
305
+ const target = locateTable(scope, ctx);
306
+ if (!target)
307
+ return [];
308
+ const grid = occupancyGridFor(target.table);
309
+ const anchor = grid.at(target.scope.rect.top, target.scope.rect.left);
310
+ if (!anchor || anchor.bottom - anchor.top === 1 && anchor.right - anchor.left === 1)
311
+ return [];
312
+ const count = (anchor.bottom - anchor.top) * (anchor.right - anchor.left) - 1;
313
+ const cells = consumeEmptyCells(params.cellIds, params.paragraphIds, count, anchor.node.attrs?.header === true);
314
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
315
+ const before = anchor.node.attrs || {};
316
+ const operations = [{
317
+ type: "setNodeAttributes",
318
+ pos: { path: [...tablePath, anchor.top, anchor.childIndex], offset: 0 },
319
+ before,
320
+ after: cleanAttrs({ ...before, rowspan: 1, colspan: 1 }),
321
+ }];
322
+ let cursor = 0;
323
+ for (let row = anchor.top; row < anchor.bottom; row += 1) {
324
+ // Every column in this row's split range was previously covered only by
325
+ // the anchor's own span - the sole exception is the anchor's own row,
326
+ // where the (unmoved) anchor cell itself already occupies the leftmost
327
+ // slot, so new siblings land immediately after it.
328
+ const baseOffset = row === anchor.top
329
+ ? anchor.childIndex + 1
330
+ : grid.anchors.filter((cell) => cell.top === row && cell.left < anchor.left).length;
331
+ let inserted = 0;
332
+ for (let column = anchor.left; column < anchor.right; column += 1) {
333
+ if (row === anchor.top && column === anchor.left)
334
+ continue;
335
+ operations.push({ type: "insertNode", pos: { path: [...tablePath, row], offset: baseOffset + inserted }, node: cells[cursor++] });
336
+ inserted += 1;
337
+ }
338
+ }
339
+ return operations;
340
+ };
341
+ export const setTableHeaderCommand = (_document, scope, params, ctx) => {
342
+ const target = locateTable(scope, ctx);
343
+ if (!target)
344
+ return [];
345
+ const grid = occupancyGridFor(target.table);
346
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
347
+ return grid.anchors.flatMap((cell) => {
348
+ const inRows = cell.top <= target.scope.rect.bottom;
349
+ const inColumns = cell.left <= target.scope.rect.right;
350
+ const header = params.target === "none" ? false : params.target === "row" ? inRows : params.target === "column" ? inColumns : inRows || inColumns;
351
+ const before = cell.node.attrs || {};
352
+ if ((before.header === true) === header)
353
+ return [];
354
+ return [{ type: "setNodeAttributes", pos: { path: [...tablePath, cell.top, cell.childIndex], offset: 0 }, before, after: { ...before, header } }];
355
+ });
356
+ };
357
+ export const setTableCellAttributesCommand = (_document, scope, params, ctx) => {
358
+ const target = locateTable(scope, ctx);
359
+ if (!target)
360
+ return [];
361
+ return target.scope.cellIds.flatMap((cellId) => {
362
+ const located = locateNode(cellId, ctx);
363
+ const before = located.node.attrs || {};
364
+ const after = cleanAttrs({ ...before, ...params.attrs });
365
+ if (JSON.stringify(before) === JSON.stringify(after))
366
+ return [];
367
+ return [{ type: "setNodeAttributes", pos: { path: [...located.parentPos.path, located.parentPos.offset], offset: 0 }, before, after }];
368
+ });
369
+ };
370
+ export const setTableColumnWidthCommand = (_document, scope, params, ctx) => {
371
+ const target = locateTable(scope, ctx);
372
+ if (!target || !Number.isFinite(params.width) || params.width < 20)
373
+ return [];
374
+ const grid = occupancyGridFor(target.table);
375
+ if (params.index < 0 || params.index >= grid.columns)
376
+ return [];
377
+ const before = target.table.attrs || {};
378
+ // Prefer an explicit seed (a caller that already measured real rendered
379
+ // widths, e.g. mid-drag) over the table's own columnWidths, over a
380
+ // fabricated 120 - see ColumnWidthParams' doc comment.
381
+ const seed = Array.isArray(params.widths) ? params.widths : Array.isArray(before.columnWidths) ? before.columnWidths : [];
382
+ const widths = Array.from({ length: grid.columns }, (_, index) => {
383
+ const value = seed[index];
384
+ return Number.isFinite(value) && value > 0 ? value : 120;
385
+ });
386
+ widths[params.index] = params.width;
387
+ const after = { ...before, columnWidths: widths };
388
+ if (JSON.stringify(before) === JSON.stringify(after))
389
+ return [];
390
+ return [{ type: "setNodeAttributes", pos: { path: [...target.parentPos.path, target.parentPos.offset], offset: 0 }, before, after }];
391
+ };
392
+ export const setTableRowHeightCommand = (_document, scope, params, ctx) => {
393
+ const target = locateTable(scope, ctx);
394
+ if (!target || !Number.isFinite(params.height) || params.height < 20)
395
+ return [];
396
+ const row = target.table.children?.[params.index];
397
+ if (!row || isTextNode(row))
398
+ return [];
399
+ const before = row.attrs || {};
400
+ const after = { ...before, height: params.height };
401
+ if (JSON.stringify(before) === JSON.stringify(after))
402
+ return [];
403
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
404
+ return [{ type: "setNodeAttributes", pos: { path: [...tablePath, params.index], offset: 0 }, before, after }];
405
+ };
406
+ export const moveTableRowCommand = (_document, scope, params, ctx) => {
407
+ const target = locateTable(scope, ctx);
408
+ if (!target || !["up", "down"].includes(params.direction))
409
+ return [];
410
+ const rows = (target.table.children || []).filter((row) => !isTextNode(row) && row.type === "table_row");
411
+ const index = params.index ?? target.scope.rect.top;
412
+ const to = index + (params.direction === "up" ? -1 : 1);
413
+ const grid = occupancyGridFor(target.table);
414
+ if (to < 0 || to >= grid.rows || grid.anchors.some((cell) => cell.top <= index && cell.bottom > index + 1 || cell.top <= to && cell.bottom > to + 1))
415
+ return [];
416
+ const row = rows[index];
417
+ return row ? moveContiguousSiblings([row.id], params.direction, ctx) : [];
418
+ };
419
+ export const moveTableColumnCommand = (_document, scope, params, ctx) => {
420
+ const target = locateTable(scope, ctx);
421
+ if (!target || !["left", "right"].includes(params.direction))
422
+ return [];
423
+ const grid = occupancyGridFor(target.table);
424
+ const from = params.index ?? target.scope.rect.left;
425
+ const to = from + (params.direction === "left" ? -1 : 1);
426
+ if (to < 0 || to >= grid.columns)
427
+ return [];
428
+ if (grid.anchors.some((cell) => (cell.left <= from && cell.right > from + 1) || (cell.left <= to && cell.right > to + 1)))
429
+ return [];
430
+ const tablePath = [...target.parentPos.path, target.parentPos.offset];
431
+ const operations = [];
432
+ // Column identity is derived purely from physical ordering, never stored.
433
+ // A row whose own anchor sits on only one side of the swap needs no op at
434
+ // all here: once the paired anchor's row (elsewhere, for a rowspan cell)
435
+ // is reordered, the grid walk re-derives this row's column for free.
436
+ for (let row = 0; row < grid.rows; row += 1) {
437
+ const atFrom = grid.anchors.find((cell) => cell.top === row && cell.left === from);
438
+ const atTo = grid.anchors.find((cell) => cell.top === row && cell.left === to);
439
+ if (!atFrom || !atTo)
440
+ continue;
441
+ const [earlier, later] = atFrom.childIndex < atTo.childIndex ? [atFrom, atTo] : [atTo, atFrom];
442
+ operations.push({
443
+ type: "moveNode",
444
+ from: { path: [...tablePath, row], offset: earlier.childIndex },
445
+ to: { path: [...tablePath, row], offset: later.childIndex },
446
+ nodeId: earlier.cellId,
447
+ });
448
+ }
449
+ const beforeAttrs = target.table.attrs || {};
450
+ const widths = Array.isArray(beforeAttrs.columnWidths) ? [...beforeAttrs.columnWidths] : Array(grid.columns).fill(120);
451
+ [widths[from], widths[to]] = [widths[to], widths[from]];
452
+ operations.push({ type: "setNodeAttributes", pos: { path: tablePath, offset: 0 }, before: beforeAttrs, after: { ...beforeAttrs, columnWidths: widths } });
453
+ return operations;
454
+ };
455
+ export const tableCommands = {
456
+ "table.insert": insertTableCommand,
457
+ "table.remove": removeTableCommand,
458
+ "table.insertRow": insertTableRowCommand,
459
+ "table.removeRow": removeTableRowCommand,
460
+ "table.insertColumn": insertTableColumnCommand,
461
+ "table.removeColumn": removeTableColumnCommand,
462
+ "table.mergeCells": mergeTableCellsCommand,
463
+ "table.splitCell": splitTableCellCommand,
464
+ "table.setHeader": setTableHeaderCommand,
465
+ "table.setCellAttributes": setTableCellAttributesCommand,
466
+ "table.setColumnWidth": setTableColumnWidthCommand,
467
+ "table.setRowHeight": setTableRowHeightCommand,
468
+ "table.moveRow": moveTableRowCommand,
469
+ "table.moveColumn": moveTableColumnCommand,
470
+ };
@@ -0,0 +1,27 @@
1
+ import type { SmartDocument } from "../types.js";
2
+ export declare const parseCanonicalTableHtml: (html: string) => SmartDocument;
3
+ export declare const serializeCanonicalTableHtml: (document: SmartDocument, options?: {
4
+ clean?: boolean;
5
+ fragment?: boolean;
6
+ renderFormulaHtml?: boolean;
7
+ }) => string;
8
+ /** GFM is intentionally lossy: anchor content survives and covered coordinates are emitted empty. */
9
+ export declare const serializeCanonicalTableMarkdown: (document: SmartDocument) => string;
10
+ export interface CanonicalDocxTableCell {
11
+ readonly cellId: string;
12
+ readonly row: number;
13
+ readonly column: number;
14
+ readonly gridSpan: number;
15
+ readonly verticalMerge: "restart" | "continue" | null;
16
+ readonly header: boolean;
17
+ readonly text: string;
18
+ }
19
+ export interface CanonicalDocxTable {
20
+ readonly tableId: string;
21
+ readonly columnWidths: readonly number[];
22
+ readonly cells: readonly CanonicalDocxTableCell[];
23
+ }
24
+ /** Semantic DOCX table data maps to gridCol/gridSpan/vMerge/tcPr in the exporter. */
25
+ export declare const canonicalTablesToDocx: (document: SmartDocument) => CanonicalDocxTable[];
26
+ /** PDF fidelity is visual; page overflow and splitting are renderer-owned. */
27
+ export declare const canonicalTablePdfText: (document: SmartDocument) => string;
@@ -0,0 +1,40 @@
1
+ import { isTextNode } from "../identity.js";
2
+ import { parseCanonicalListHtml, serializeCanonicalListHtml, serializeCanonicalListMarkdown } from "../list/formats.js";
3
+ import { occupancyGridFor } from "./grid.js";
4
+ import { foundationSchema, repair } from "../schema.js";
5
+ export const parseCanonicalTableHtml = (html) => repair(parseCanonicalListHtml(html), foundationSchema).doc;
6
+ export const serializeCanonicalTableHtml = serializeCanonicalListHtml;
7
+ /** GFM is intentionally lossy: anchor content survives and covered coordinates are emitted empty. */
8
+ export const serializeCanonicalTableMarkdown = serializeCanonicalListMarkdown;
9
+ const textOf = (node) => (node.children || []).map((child) => isTextNode(child) ? child.text : textOf(child)).join("\n");
10
+ /** Semantic DOCX table data maps to gridCol/gridSpan/vMerge/tcPr in the exporter. */
11
+ export const canonicalTablesToDocx = (document) => {
12
+ const tables = [];
13
+ const visit = (node) => {
14
+ if (node.type === "table") {
15
+ const grid = occupancyGridFor(node);
16
+ const cells = [];
17
+ grid.anchors.forEach((anchor) => {
18
+ cells.push({ cellId: anchor.cellId, row: anchor.top, column: anchor.left,
19
+ gridSpan: anchor.right - anchor.left, verticalMerge: anchor.bottom - anchor.top > 1 ? "restart" : null,
20
+ header: anchor.node.attrs?.header === true, text: textOf(anchor.node) });
21
+ for (let row = anchor.top + 1; row < anchor.bottom; row += 1)
22
+ cells.push({
23
+ cellId: anchor.cellId, row, column: anchor.left, gridSpan: anchor.right - anchor.left,
24
+ verticalMerge: "continue", header: anchor.node.attrs?.header === true, text: "",
25
+ });
26
+ });
27
+ tables.push({ tableId: node.id,
28
+ columnWidths: Array.isArray(node.attrs?.columnWidths) ? node.attrs.columnWidths : Array(grid.columns).fill(120), cells });
29
+ }
30
+ (node.children || []).forEach((child) => { if (!isTextNode(child))
31
+ visit(child); });
32
+ };
33
+ visit(document);
34
+ return tables;
35
+ };
36
+ /** PDF fidelity is visual; page overflow and splitting are renderer-owned. */
37
+ export const canonicalTablePdfText = (document) => canonicalTablesToDocx(document)
38
+ .map((table) => table.cells.filter((cell) => cell.verticalMerge !== "continue")
39
+ .map((cell) => `[${cell.row},${cell.column}] ${cell.text}`).join("\n"))
40
+ .join("\n\n");
@@ -0,0 +1,8 @@
1
+ import type { SmartElementNode } from "../types.js";
2
+ import type { OccupancyGrid, TableGeometryIssue } from "./types.js";
3
+ export declare const occupancyGridFor: (table: SmartElementNode) => OccupancyGrid;
4
+ export declare const validateTableGeometry: (table: SmartElementNode) => TableGeometryIssue[];
5
+ export declare const repairTableGeometry: (table: SmartElementNode) => {
6
+ table: SmartElementNode;
7
+ repairs: TableGeometryIssue[];
8
+ };