@rtif-sdk/web 1.0.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/block-drag-handler.d.ts +189 -0
  4. package/dist/block-drag-handler.d.ts.map +1 -0
  5. package/dist/block-drag-handler.js +745 -0
  6. package/dist/block-drag-handler.js.map +1 -0
  7. package/dist/block-renderer.d.ts +402 -0
  8. package/dist/block-renderer.d.ts.map +1 -0
  9. package/dist/block-renderer.js +424 -0
  10. package/dist/block-renderer.js.map +1 -0
  11. package/dist/clipboard.d.ts +178 -0
  12. package/dist/clipboard.d.ts.map +1 -0
  13. package/dist/clipboard.js +432 -0
  14. package/dist/clipboard.js.map +1 -0
  15. package/dist/command-bus.d.ts +113 -0
  16. package/dist/command-bus.d.ts.map +1 -0
  17. package/dist/command-bus.js +70 -0
  18. package/dist/command-bus.js.map +1 -0
  19. package/dist/composition.d.ts +220 -0
  20. package/dist/composition.d.ts.map +1 -0
  21. package/dist/composition.js +271 -0
  22. package/dist/composition.js.map +1 -0
  23. package/dist/content-extraction.d.ts +69 -0
  24. package/dist/content-extraction.d.ts.map +1 -0
  25. package/dist/content-extraction.js +228 -0
  26. package/dist/content-extraction.js.map +1 -0
  27. package/dist/content-handler-file.d.ts +40 -0
  28. package/dist/content-handler-file.d.ts.map +1 -0
  29. package/dist/content-handler-file.js +91 -0
  30. package/dist/content-handler-file.js.map +1 -0
  31. package/dist/content-handler-image.d.ts +82 -0
  32. package/dist/content-handler-image.d.ts.map +1 -0
  33. package/dist/content-handler-image.js +120 -0
  34. package/dist/content-handler-image.js.map +1 -0
  35. package/dist/content-handler-url.d.ts +129 -0
  36. package/dist/content-handler-url.d.ts.map +1 -0
  37. package/dist/content-handler-url.js +244 -0
  38. package/dist/content-handler-url.js.map +1 -0
  39. package/dist/content-handlers.d.ts +67 -0
  40. package/dist/content-handlers.d.ts.map +1 -0
  41. package/dist/content-handlers.js +263 -0
  42. package/dist/content-handlers.js.map +1 -0
  43. package/dist/content-pipeline.d.ts +383 -0
  44. package/dist/content-pipeline.d.ts.map +1 -0
  45. package/dist/content-pipeline.js +232 -0
  46. package/dist/content-pipeline.js.map +1 -0
  47. package/dist/cursor-nav.d.ts +149 -0
  48. package/dist/cursor-nav.d.ts.map +1 -0
  49. package/dist/cursor-nav.js +230 -0
  50. package/dist/cursor-nav.js.map +1 -0
  51. package/dist/cursor-rect.d.ts +65 -0
  52. package/dist/cursor-rect.d.ts.map +1 -0
  53. package/dist/cursor-rect.js +98 -0
  54. package/dist/cursor-rect.js.map +1 -0
  55. package/dist/drop-indicator.d.ts +108 -0
  56. package/dist/drop-indicator.d.ts.map +1 -0
  57. package/dist/drop-indicator.js +236 -0
  58. package/dist/drop-indicator.js.map +1 -0
  59. package/dist/editor.d.ts +41 -0
  60. package/dist/editor.d.ts.map +1 -0
  61. package/dist/editor.js +710 -0
  62. package/dist/editor.js.map +1 -0
  63. package/dist/floating-toolbar.d.ts +93 -0
  64. package/dist/floating-toolbar.d.ts.map +1 -0
  65. package/dist/floating-toolbar.js +159 -0
  66. package/dist/floating-toolbar.js.map +1 -0
  67. package/dist/index.d.ts +62 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +119 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/input-bridge.d.ts +273 -0
  72. package/dist/input-bridge.d.ts.map +1 -0
  73. package/dist/input-bridge.js +884 -0
  74. package/dist/input-bridge.js.map +1 -0
  75. package/dist/link-popover.d.ts +38 -0
  76. package/dist/link-popover.d.ts.map +1 -0
  77. package/dist/link-popover.js +278 -0
  78. package/dist/link-popover.js.map +1 -0
  79. package/dist/mark-renderer.d.ts +275 -0
  80. package/dist/mark-renderer.d.ts.map +1 -0
  81. package/dist/mark-renderer.js +210 -0
  82. package/dist/mark-renderer.js.map +1 -0
  83. package/dist/perf.d.ts +145 -0
  84. package/dist/perf.d.ts.map +1 -0
  85. package/dist/perf.js +260 -0
  86. package/dist/perf.js.map +1 -0
  87. package/dist/plugin-kit.d.ts +265 -0
  88. package/dist/plugin-kit.d.ts.map +1 -0
  89. package/dist/plugin-kit.js +234 -0
  90. package/dist/plugin-kit.js.map +1 -0
  91. package/dist/plugins/alignment-plugin.d.ts +68 -0
  92. package/dist/plugins/alignment-plugin.d.ts.map +1 -0
  93. package/dist/plugins/alignment-plugin.js +98 -0
  94. package/dist/plugins/alignment-plugin.js.map +1 -0
  95. package/dist/plugins/block-utils.d.ts +113 -0
  96. package/dist/plugins/block-utils.d.ts.map +1 -0
  97. package/dist/plugins/block-utils.js +191 -0
  98. package/dist/plugins/block-utils.js.map +1 -0
  99. package/dist/plugins/blockquote-plugin.d.ts +39 -0
  100. package/dist/plugins/blockquote-plugin.d.ts.map +1 -0
  101. package/dist/plugins/blockquote-plugin.js +88 -0
  102. package/dist/plugins/blockquote-plugin.js.map +1 -0
  103. package/dist/plugins/bold-plugin.d.ts +37 -0
  104. package/dist/plugins/bold-plugin.d.ts.map +1 -0
  105. package/dist/plugins/bold-plugin.js +48 -0
  106. package/dist/plugins/bold-plugin.js.map +1 -0
  107. package/dist/plugins/callout-plugin.d.ts +100 -0
  108. package/dist/plugins/callout-plugin.d.ts.map +1 -0
  109. package/dist/plugins/callout-plugin.js +200 -0
  110. package/dist/plugins/callout-plugin.js.map +1 -0
  111. package/dist/plugins/code-block-plugin.d.ts +62 -0
  112. package/dist/plugins/code-block-plugin.d.ts.map +1 -0
  113. package/dist/plugins/code-block-plugin.js +176 -0
  114. package/dist/plugins/code-block-plugin.js.map +1 -0
  115. package/dist/plugins/code-plugin.d.ts +37 -0
  116. package/dist/plugins/code-plugin.d.ts.map +1 -0
  117. package/dist/plugins/code-plugin.js +48 -0
  118. package/dist/plugins/code-plugin.js.map +1 -0
  119. package/dist/plugins/embed-plugin.d.ts +90 -0
  120. package/dist/plugins/embed-plugin.d.ts.map +1 -0
  121. package/dist/plugins/embed-plugin.js +147 -0
  122. package/dist/plugins/embed-plugin.js.map +1 -0
  123. package/dist/plugins/font-family-plugin.d.ts +58 -0
  124. package/dist/plugins/font-family-plugin.d.ts.map +1 -0
  125. package/dist/plugins/font-family-plugin.js +57 -0
  126. package/dist/plugins/font-family-plugin.js.map +1 -0
  127. package/dist/plugins/font-size-plugin.d.ts +57 -0
  128. package/dist/plugins/font-size-plugin.d.ts.map +1 -0
  129. package/dist/plugins/font-size-plugin.js +56 -0
  130. package/dist/plugins/font-size-plugin.js.map +1 -0
  131. package/dist/plugins/heading-plugin.d.ts +52 -0
  132. package/dist/plugins/heading-plugin.d.ts.map +1 -0
  133. package/dist/plugins/heading-plugin.js +114 -0
  134. package/dist/plugins/heading-plugin.js.map +1 -0
  135. package/dist/plugins/hr-plugin.d.ts +33 -0
  136. package/dist/plugins/hr-plugin.d.ts.map +1 -0
  137. package/dist/plugins/hr-plugin.js +75 -0
  138. package/dist/plugins/hr-plugin.js.map +1 -0
  139. package/dist/plugins/image-plugin.d.ts +115 -0
  140. package/dist/plugins/image-plugin.d.ts.map +1 -0
  141. package/dist/plugins/image-plugin.js +199 -0
  142. package/dist/plugins/image-plugin.js.map +1 -0
  143. package/dist/plugins/indent-plugin.d.ts +62 -0
  144. package/dist/plugins/indent-plugin.d.ts.map +1 -0
  145. package/dist/plugins/indent-plugin.js +128 -0
  146. package/dist/plugins/indent-plugin.js.map +1 -0
  147. package/dist/plugins/index.d.ts +45 -0
  148. package/dist/plugins/index.d.ts.map +1 -0
  149. package/dist/plugins/index.js +42 -0
  150. package/dist/plugins/index.js.map +1 -0
  151. package/dist/plugins/italic-plugin.d.ts +37 -0
  152. package/dist/plugins/italic-plugin.d.ts.map +1 -0
  153. package/dist/plugins/italic-plugin.js +48 -0
  154. package/dist/plugins/italic-plugin.js.map +1 -0
  155. package/dist/plugins/link-plugin.d.ts +129 -0
  156. package/dist/plugins/link-plugin.d.ts.map +1 -0
  157. package/dist/plugins/link-plugin.js +212 -0
  158. package/dist/plugins/link-plugin.js.map +1 -0
  159. package/dist/plugins/list-plugin.d.ts +53 -0
  160. package/dist/plugins/list-plugin.d.ts.map +1 -0
  161. package/dist/plugins/list-plugin.js +309 -0
  162. package/dist/plugins/list-plugin.js.map +1 -0
  163. package/dist/plugins/mark-utils.d.ts +173 -0
  164. package/dist/plugins/mark-utils.d.ts.map +1 -0
  165. package/dist/plugins/mark-utils.js +425 -0
  166. package/dist/plugins/mark-utils.js.map +1 -0
  167. package/dist/plugins/mention-plugin.d.ts +191 -0
  168. package/dist/plugins/mention-plugin.d.ts.map +1 -0
  169. package/dist/plugins/mention-plugin.js +295 -0
  170. package/dist/plugins/mention-plugin.js.map +1 -0
  171. package/dist/plugins/strikethrough-plugin.d.ts +37 -0
  172. package/dist/plugins/strikethrough-plugin.d.ts.map +1 -0
  173. package/dist/plugins/strikethrough-plugin.js +48 -0
  174. package/dist/plugins/strikethrough-plugin.js.map +1 -0
  175. package/dist/plugins/text-color-plugin.d.ts +57 -0
  176. package/dist/plugins/text-color-plugin.d.ts.map +1 -0
  177. package/dist/plugins/text-color-plugin.js +56 -0
  178. package/dist/plugins/text-color-plugin.js.map +1 -0
  179. package/dist/plugins/underline-plugin.d.ts +37 -0
  180. package/dist/plugins/underline-plugin.d.ts.map +1 -0
  181. package/dist/plugins/underline-plugin.js +48 -0
  182. package/dist/plugins/underline-plugin.js.map +1 -0
  183. package/dist/presets.d.ts +95 -0
  184. package/dist/presets.d.ts.map +1 -0
  185. package/dist/presets.js +159 -0
  186. package/dist/presets.js.map +1 -0
  187. package/dist/renderer.d.ts +125 -0
  188. package/dist/renderer.d.ts.map +1 -0
  189. package/dist/renderer.js +415 -0
  190. package/dist/renderer.js.map +1 -0
  191. package/dist/scroll-to-cursor.d.ts +25 -0
  192. package/dist/scroll-to-cursor.d.ts.map +1 -0
  193. package/dist/scroll-to-cursor.js +59 -0
  194. package/dist/scroll-to-cursor.js.map +1 -0
  195. package/dist/selection-sync.d.ts +159 -0
  196. package/dist/selection-sync.d.ts.map +1 -0
  197. package/dist/selection-sync.js +527 -0
  198. package/dist/selection-sync.js.map +1 -0
  199. package/dist/shortcut-handler.d.ts +98 -0
  200. package/dist/shortcut-handler.d.ts.map +1 -0
  201. package/dist/shortcut-handler.js +155 -0
  202. package/dist/shortcut-handler.js.map +1 -0
  203. package/dist/toolbar.d.ts +103 -0
  204. package/dist/toolbar.d.ts.map +1 -0
  205. package/dist/toolbar.js +134 -0
  206. package/dist/toolbar.js.map +1 -0
  207. package/dist/trigger-manager.d.ts +205 -0
  208. package/dist/trigger-manager.d.ts.map +1 -0
  209. package/dist/trigger-manager.js +466 -0
  210. package/dist/trigger-manager.js.map +1 -0
  211. package/dist/types.d.ts +216 -0
  212. package/dist/types.d.ts.map +1 -0
  213. package/dist/types.js +2 -0
  214. package/dist/types.js.map +1 -0
  215. package/package.json +30 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Determine the container element where span children should be placed
3
+ * for a given block element.
4
+ *
5
+ * For composite blocks (e.g., callout) whose renderer defines
6
+ * `getContentContainer`, spans are rendered inside a nested content
7
+ * element rather than directly in the block element. For all other
8
+ * block types the block element itself is the span container.
9
+ *
10
+ * @param blockEl - The block DOM element
11
+ * @param blockType - The RTIF block type string
12
+ * @param blockRenderers - Optional registry of block renderers
13
+ * @returns The element that should contain span children
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const container = getSpanContainer(blockEl, 'callout', registry);
18
+ * // For callout: returns the .rtif-callout-content div
19
+ * // For text: returns blockEl itself
20
+ * ```
21
+ */
22
+ function getSpanContainer(blockEl, blockType, blockRenderers) {
23
+ if (!blockRenderers)
24
+ return blockEl;
25
+ const renderer = blockRenderers.get(blockType);
26
+ if (renderer?.getContentContainer) {
27
+ return renderer.getContentContainer(blockEl) ?? blockEl;
28
+ }
29
+ return blockEl;
30
+ }
31
+ /**
32
+ * Create a DOM element for an RTIF span.
33
+ *
34
+ * When a `markRenderers` registry is provided and the span has a mark with a
35
+ * registered {@link SpanReplacer}, the replacer's `createElement()` is called
36
+ * to produce a fully custom element. Otherwise, a default `<span>` is created
37
+ * with mark renderers applied.
38
+ *
39
+ * Non-empty spans contain a single Text node. Empty spans (text === '')
40
+ * contain a `<br>` element so the block has layout height in the browser.
41
+ *
42
+ * @param span - The RTIF span to render
43
+ * @param markRenderers - Optional registry of mark renderers for visual styling
44
+ * @returns An `HTMLElement` with `data-rtif-span` attribute and appropriate child node
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const el = createSpanElement({ text: 'hello' });
49
+ * // <span data-rtif-span>hello</span>
50
+ *
51
+ * const boldEl = createSpanElement(
52
+ * { text: 'bold', marks: { bold: true } },
53
+ * markRegistry,
54
+ * );
55
+ * // <span data-rtif-span class="rtif-bold">bold</span>
56
+ * ```
57
+ */
58
+ export function createSpanElement(span, markRenderers) {
59
+ // Check for span replacer before creating default element
60
+ if (markRenderers && span.marks) {
61
+ for (const markType of Object.keys(span.marks)) {
62
+ if (span.marks[markType] == null)
63
+ continue;
64
+ const replacer = markRenderers.getReplacer(markType);
65
+ if (replacer) {
66
+ const el = replacer.createElement(span.text, span.marks);
67
+ el.setAttribute('data-rtif-span', '');
68
+ return el;
69
+ }
70
+ }
71
+ }
72
+ const el = document.createElement('span');
73
+ el.setAttribute('data-rtif-span', '');
74
+ // Apply mark rendering
75
+ if (markRenderers) {
76
+ markRenderers.applyAll(el, span.marks);
77
+ }
78
+ if (span.text === '') {
79
+ el.appendChild(document.createElement('br'));
80
+ }
81
+ else {
82
+ el.appendChild(document.createTextNode(span.text));
83
+ }
84
+ return el;
85
+ }
86
+ /**
87
+ * Create a DOM block element for an RTIF block.
88
+ *
89
+ * Each block renders as a `<div>` with `data-rtif-block` set to the block ID
90
+ * and `data-block-type` set to the block type. Child span elements are created
91
+ * for each span in the block.
92
+ *
93
+ * When a `blockRenderers` registry is provided, the renderer for the block's
94
+ * type is applied to the element (adding CSS classes, ARIA roles, etc.).
95
+ *
96
+ * @param block - The RTIF block to render
97
+ * @param markRenderers - Optional registry of mark renderers for visual styling
98
+ * @param blockRenderers - Optional registry of block renderers for block-type styling
99
+ * @returns A `<div data-rtif-block="{id}" data-block-type="{type}">` element
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const el = createBlockElement({
104
+ * id: 'b1',
105
+ * type: 'text',
106
+ * spans: [{ text: 'Hello world' }],
107
+ * });
108
+ * // <div data-rtif-block="b1" data-block-type="text">
109
+ * // <span data-rtif-span>Hello world</span>
110
+ * // </div>
111
+ * ```
112
+ */
113
+ export function createBlockElement(block, markRenderers, blockRenderers) {
114
+ const el = document.createElement('div');
115
+ el.setAttribute('data-rtif-block', block.id);
116
+ el.setAttribute('data-block-type', block.type);
117
+ if (blockRenderers) {
118
+ blockRenderers.applyToElement(el, block);
119
+ }
120
+ const container = getSpanContainer(el, block.type, blockRenderers);
121
+ for (const span of block.spans) {
122
+ container.appendChild(createSpanElement(span, markRenderers));
123
+ }
124
+ return el;
125
+ }
126
+ /**
127
+ * Find a block DOM element within a root by its RTIF block ID.
128
+ *
129
+ * @param root - The root element to search within
130
+ * @param blockId - The block ID to find (matches `data-rtif-block` attribute)
131
+ * @returns The matching element, or `null` if not found
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * const blockEl = findBlockElement(root, 'b1');
136
+ * if (blockEl) {
137
+ * console.log(blockEl.textContent);
138
+ * }
139
+ * ```
140
+ */
141
+ export function findBlockElement(root, blockId) {
142
+ return root.querySelector(`[data-rtif-block="${blockId}"]`);
143
+ }
144
+ /**
145
+ * Perform a full initial render of an RTIF document into a root element.
146
+ *
147
+ * Clears all existing content in the root, then creates block and span
148
+ * elements for the entire document. Use this for the first render; use
149
+ * {@link reconcile} for subsequent updates.
150
+ *
151
+ * @param root - The root DOM element (typically `contenteditable`)
152
+ * @param doc - The RTIF document to render
153
+ * @param markRenderers - Optional registry of mark renderers for visual styling
154
+ * @param blockRenderers - Optional registry of block renderers for block-type styling
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const root = document.getElementById('editor')!;
159
+ * renderInitial(root, {
160
+ * version: 1,
161
+ * blocks: [{ id: 'b1', type: 'text', spans: [{ text: 'Hello' }] }],
162
+ * });
163
+ * ```
164
+ */
165
+ export function renderInitial(root, doc, markRenderers, blockRenderers) {
166
+ // Clear all existing content without using innerHTML
167
+ while (root.firstChild) {
168
+ root.removeChild(root.firstChild);
169
+ }
170
+ for (const block of doc.blocks) {
171
+ root.appendChild(createBlockElement(block, markRenderers, blockRenderers));
172
+ }
173
+ }
174
+ /**
175
+ * Update the content of a span DOM element to match an RTIF span.
176
+ *
177
+ * Handles the transition between empty spans (`<br>`) and text spans
178
+ * (Text node) efficiently by reusing existing Text nodes when possible.
179
+ *
180
+ * @param spanEl - The existing span DOM element
181
+ * @param span - The RTIF span with the desired content
182
+ */
183
+ function reconcileSpanContent(spanEl, span) {
184
+ if (span.text === '') {
185
+ // Empty span: must contain a <br>
186
+ while (spanEl.firstChild) {
187
+ spanEl.removeChild(spanEl.firstChild);
188
+ }
189
+ spanEl.appendChild(document.createElement('br'));
190
+ }
191
+ else {
192
+ // Non-empty span: must contain a Text node
193
+ const firstChild = spanEl.firstChild;
194
+ if (firstChild && firstChild.nodeType === Node.TEXT_NODE) {
195
+ // Reuse existing text node, just update value
196
+ firstChild.nodeValue = span.text;
197
+ // Remove any extra children (shouldn't happen, but be safe)
198
+ while (spanEl.childNodes.length > 1) {
199
+ spanEl.removeChild(spanEl.lastChild);
200
+ }
201
+ }
202
+ else {
203
+ // Replace all children with a text node
204
+ while (spanEl.firstChild) {
205
+ spanEl.removeChild(spanEl.firstChild);
206
+ }
207
+ spanEl.appendChild(document.createTextNode(span.text));
208
+ }
209
+ }
210
+ }
211
+ /**
212
+ * Reconcile the span children of a block element to match an RTIF block.
213
+ *
214
+ * Compares spans by reference identity to skip unchanged spans. When a span
215
+ * has changed, checks whether marks also changed — if so, replaces the
216
+ * entire span element to reflect new mark styling; otherwise, updates only
217
+ * the text content.
218
+ *
219
+ * @param container - The DOM element containing span children (may be the
220
+ * block element itself or a content container for composite blocks)
221
+ * @param prevBlock - The previous RTIF block state
222
+ * @param nextBlock - The new RTIF block state
223
+ * @param markRenderers - Optional registry of mark renderers
224
+ */
225
+ function reconcileBlockSpans(container, prevBlock, nextBlock, markRenderers) {
226
+ const prevSpans = prevBlock.spans;
227
+ const nextSpans = nextBlock.spans;
228
+ const spanElements = container.children;
229
+ // Update or replace existing spans
230
+ const minLen = Math.min(prevSpans.length, nextSpans.length);
231
+ for (let i = 0; i < minLen; i++) {
232
+ const prevSpan = prevSpans[i];
233
+ const nextSpan = nextSpans[i];
234
+ // Skip if same reference
235
+ if (prevSpan === nextSpan) {
236
+ continue;
237
+ }
238
+ const spanEl = spanElements[i];
239
+ if (spanEl) {
240
+ // If marks changed, try replacer update() before full replacement
241
+ if (prevSpan.marks !== nextSpan.marks) {
242
+ let handled = false;
243
+ if (markRenderers && nextSpan.marks) {
244
+ for (const markType of Object.keys(nextSpan.marks)) {
245
+ if (nextSpan.marks[markType] == null)
246
+ continue;
247
+ const replacer = markRenderers.getReplacer(markType);
248
+ if (replacer?.update) {
249
+ handled = replacer.update(spanEl, nextSpan.text, nextSpan.marks);
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ if (!handled) {
255
+ const newSpanEl = createSpanElement(nextSpan, markRenderers);
256
+ container.replaceChild(newSpanEl, spanEl);
257
+ }
258
+ }
259
+ else {
260
+ // Only text changed — check replacer update() for text-only changes too
261
+ let handled = false;
262
+ if (markRenderers && nextSpan.marks) {
263
+ for (const markType of Object.keys(nextSpan.marks)) {
264
+ if (nextSpan.marks[markType] == null)
265
+ continue;
266
+ const replacer = markRenderers.getReplacer(markType);
267
+ if (replacer?.update) {
268
+ handled = replacer.update(spanEl, nextSpan.text, nextSpan.marks);
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ if (!handled) {
274
+ reconcileSpanContent(spanEl, nextSpan);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ // Remove extra spans (next has fewer)
280
+ while (spanElements.length > nextSpans.length) {
281
+ container.removeChild(container.lastElementChild);
282
+ }
283
+ // Add new spans (next has more)
284
+ for (let i = prevSpans.length; i < nextSpans.length; i++) {
285
+ const nextSpan = nextSpans[i];
286
+ container.appendChild(createSpanElement(nextSpan, markRenderers));
287
+ }
288
+ }
289
+ /**
290
+ * Efficiently reconcile the DOM to match a new RTIF document state.
291
+ *
292
+ * Only touches blocks that have changed (detected by object reference
293
+ * comparison). Handles block additions, removals, reordering, and
294
+ * span-level content updates within changed blocks. When a block's type
295
+ * or attrs change (by reference), the entire block element is replaced
296
+ * to ensure block-renderer styling is re-applied cleanly.
297
+ *
298
+ * **Critical**: When `composingBlockId` is set (IME composition active),
299
+ * the block with that ID is skipped entirely to avoid disrupting the
300
+ * browser's native composition rendering.
301
+ *
302
+ * @param root - The root DOM element containing block elements
303
+ * @param prevDoc - The previous RTIF document state
304
+ * @param nextDoc - The new RTIF document state to reconcile toward
305
+ * @param composingBlockId - Block ID currently under IME composition, or `null`
306
+ * @param markRenderers - Optional registry of mark renderers for visual styling
307
+ * @param blockRenderers - Optional registry of block renderers for block-type styling
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * // After an engine dispatch produces a new document:
312
+ * reconcile(root, prevDoc, nextDoc, composingBlockId, markRenderers, blockRenderers);
313
+ * ```
314
+ */
315
+ export function reconcile(root, prevDoc, nextDoc, composingBlockId, markRenderers, blockRenderers) {
316
+ // Fast path: identical document reference
317
+ if (prevDoc === nextDoc) {
318
+ return;
319
+ }
320
+ // Build a lookup of previous blocks by ID for reference comparison
321
+ const prevBlockMap = new Map();
322
+ for (const block of prevDoc.blocks) {
323
+ prevBlockMap.set(block.id, block);
324
+ }
325
+ // Build a map of existing DOM block elements by ID
326
+ const existingElements = new Map();
327
+ for (let i = 0; i < root.children.length; i++) {
328
+ const child = root.children[i];
329
+ const blockId = child.getAttribute('data-rtif-block');
330
+ if (blockId) {
331
+ existingElements.set(blockId, child);
332
+ }
333
+ }
334
+ // Track which block IDs are in the next document
335
+ const nextBlockIds = new Set();
336
+ // Walk next blocks in order, using a reference node cursor to minimize moves
337
+ let referenceNode = root.firstChild;
338
+ for (const nextBlock of nextDoc.blocks) {
339
+ nextBlockIds.add(nextBlock.id);
340
+ let blockEl = existingElements.get(nextBlock.id) ?? null;
341
+ if (blockEl) {
342
+ // Block element already exists in the DOM
343
+ const prevBlock = prevBlockMap.get(nextBlock.id);
344
+ // Skip composing block entirely
345
+ if (nextBlock.id === composingBlockId) {
346
+ // Just make sure it's in the right position
347
+ if (blockEl !== referenceNode) {
348
+ root.insertBefore(blockEl, referenceNode);
349
+ }
350
+ else {
351
+ referenceNode = referenceNode.nextSibling;
352
+ }
353
+ continue;
354
+ }
355
+ // Check if block changed by reference
356
+ if (prevBlock !== nextBlock) {
357
+ // If block type or attrs changed, replace entire block element
358
+ // to ensure block-renderer styling is re-applied cleanly.
359
+ // This mirrors how mark changes replace span elements.
360
+ if (prevBlock && (prevBlock.type !== nextBlock.type || prevBlock.attrs !== nextBlock.attrs)) {
361
+ const newBlockEl = createBlockElement(nextBlock, markRenderers, blockRenderers);
362
+ root.replaceChild(newBlockEl, blockEl);
363
+ // Update our reference so position tracking remains correct
364
+ existingElements.set(nextBlock.id, newBlockEl);
365
+ blockEl = newBlockEl;
366
+ // replaceChild keeps position; advance referenceNode past replaced element
367
+ referenceNode = newBlockEl.nextSibling;
368
+ continue;
369
+ }
370
+ // Update block-type attribute if changed
371
+ if (blockEl.getAttribute('data-block-type') !== nextBlock.type) {
372
+ blockEl.setAttribute('data-block-type', nextBlock.type);
373
+ }
374
+ // Reconcile spans within this block
375
+ if (prevBlock) {
376
+ const container = getSpanContainer(blockEl, nextBlock.type, blockRenderers);
377
+ reconcileBlockSpans(container, prevBlock, nextBlock, markRenderers);
378
+ }
379
+ else {
380
+ // No previous block data — full re-render of span content
381
+ const container = getSpanContainer(blockEl, nextBlock.type, blockRenderers);
382
+ while (container.firstChild) {
383
+ container.removeChild(container.firstChild);
384
+ }
385
+ for (const span of nextBlock.spans) {
386
+ container.appendChild(createSpanElement(span, markRenderers));
387
+ }
388
+ }
389
+ }
390
+ // Ensure correct position
391
+ if (blockEl !== referenceNode) {
392
+ root.insertBefore(blockEl, referenceNode);
393
+ }
394
+ else {
395
+ referenceNode = referenceNode.nextSibling;
396
+ }
397
+ }
398
+ else {
399
+ // New block — create element and insert at correct position
400
+ blockEl = createBlockElement(nextBlock, markRenderers, blockRenderers);
401
+ root.insertBefore(blockEl, referenceNode);
402
+ }
403
+ }
404
+ // Remove any block elements that are no longer in the document
405
+ const toRemove = [];
406
+ for (const [blockId, el] of existingElements) {
407
+ if (!nextBlockIds.has(blockId)) {
408
+ toRemove.push(el);
409
+ }
410
+ }
411
+ for (const el of toRemove) {
412
+ root.removeChild(el);
413
+ }
414
+ }
415
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Scroll-to-cursor utility.
3
+ *
4
+ * After every operation that moves the cursor (typing, Enter, paste, undo),
5
+ * the implementation MUST ensure the cursor is visible within the scrollable
6
+ * viewport (platform-requirements §12.1).
7
+ *
8
+ * @module
9
+ */
10
+ /**
11
+ * Scroll the viewport so the current cursor (DOM focus point) is visible.
12
+ *
13
+ * Uses the browser's native `scrollIntoView` on the element nearest the
14
+ * caret. Gracefully no-ops when there is no selection, the focus is outside
15
+ * the editor root, or layout information is unavailable (e.g. jsdom).
16
+ *
17
+ * @param root - The contenteditable root element
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * scrollToCursor(editorRoot);
22
+ * ```
23
+ */
24
+ export declare function scrollToCursor(root: HTMLElement): void;
25
+ //# sourceMappingURL=scroll-to-cursor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll-to-cursor.d.ts","sourceRoot":"","sources":["../src/scroll-to-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAqCtD"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Scroll-to-cursor utility.
3
+ *
4
+ * After every operation that moves the cursor (typing, Enter, paste, undo),
5
+ * the implementation MUST ensure the cursor is visible within the scrollable
6
+ * viewport (platform-requirements §12.1).
7
+ *
8
+ * @module
9
+ */
10
+ /**
11
+ * Scroll the viewport so the current cursor (DOM focus point) is visible.
12
+ *
13
+ * Uses the browser's native `scrollIntoView` on the element nearest the
14
+ * caret. Gracefully no-ops when there is no selection, the focus is outside
15
+ * the editor root, or layout information is unavailable (e.g. jsdom).
16
+ *
17
+ * @param root - The contenteditable root element
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * scrollToCursor(editorRoot);
22
+ * ```
23
+ */
24
+ export function scrollToCursor(root) {
25
+ const sel = root.ownerDocument.defaultView?.getSelection();
26
+ if (!sel || sel.rangeCount === 0)
27
+ return;
28
+ const { focusNode, focusOffset } = sel;
29
+ if (!focusNode)
30
+ return;
31
+ // Ensure the focus is inside the editor root
32
+ if (!root.contains(focusNode))
33
+ return;
34
+ // Create a collapsed range at the focus point
35
+ const range = root.ownerDocument.createRange();
36
+ try {
37
+ range.setStart(focusNode, focusOffset);
38
+ range.collapse(true);
39
+ }
40
+ catch {
41
+ // Invalid offset — bail
42
+ return;
43
+ }
44
+ // getBoundingClientRect may not exist in non-browser environments (jsdom)
45
+ if (typeof range.getBoundingClientRect !== 'function')
46
+ return;
47
+ const rect = range.getBoundingClientRect();
48
+ // Zero-dimension rect means no layout (jsdom, detached element, etc.)
49
+ if (rect.width === 0 && rect.height === 0)
50
+ return;
51
+ // Find the nearest element ancestor to call scrollIntoView on
52
+ const element = focusNode.nodeType === Node.ELEMENT_NODE
53
+ ? focusNode
54
+ : focusNode.parentElement;
55
+ if (!element)
56
+ return;
57
+ element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
58
+ }
59
+ //# sourceMappingURL=scroll-to-cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll-to-cursor.js","sourceRoot":"","sources":["../src/scroll-to-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,IAAiB;IAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,YAAY,EAAE,CAAC;IAC3D,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,KAAK,CAAC;QAAE,OAAO;IAEzC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,GAAG,CAAC;IACvC,IAAI,CAAC,SAAS;QAAE,OAAO;IAEvB,6CAA6C;IAC7C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO;IAEtC,8CAA8C;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAC;IAC/C,IAAI,CAAC;QACH,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QACvC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,wBAAwB;QACxB,OAAO;IACT,CAAC;IAED,0EAA0E;IAC1E,IAAI,OAAO,KAAK,CAAC,qBAAqB,KAAK,UAAU;QAAE,OAAO;IAE9D,MAAM,IAAI,GAAG,KAAK,CAAC,qBAAqB,EAAE,CAAC;IAE3C,sEAAsE;IACtE,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAElD,8DAA8D;IAC9D,MAAM,OAAO,GACX,SAAS,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY;QACtC,CAAC,CAAE,SAAqB;QACxB,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,OAAO,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;AAClE,CAAC"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Bidirectional DOM Selection <-> RTIF Selection synchronization.
3
+ *
4
+ * Converts between the browser's Selection API (anchorNode/offset,
5
+ * focusNode/offset) and RTIF's absolute integer offset Selection
6
+ * ({anchor: {offset}, focus: {offset}}).
7
+ *
8
+ * Relies on the DOM structure produced by the renderer:
9
+ * - Root: `[data-rtif-root]` contenteditable div
10
+ * - Blocks: `div[data-rtif-block="{id}"]`
11
+ * - Spans: `span[data-rtif-span]` with Text node or `<br>` children
12
+ */
13
+ import type { Document, Selection } from '@rtif-sdk/core';
14
+ import type { BlockOffsetEntry, DomPoint } from './types.js';
15
+ /**
16
+ * Build an array of block offset entries for fast DOM <-> RTIF conversion.
17
+ *
18
+ * Each entry records the block's ID, its absolute start offset in the
19
+ * document, and its text length. Block N+1 starts at
20
+ * `block[N].startOffset + block[N].length + 1` (the +1 is the virtual `\n`
21
+ * separator between consecutive blocks).
22
+ *
23
+ * @param doc - The RTIF document
24
+ * @returns Array of offset entries, one per block, in document order
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const doc = { version: 1, blocks: [
29
+ * { id: 'b1', type: 'text', spans: [{ text: 'hello' }] },
30
+ * { id: 'b2', type: 'text', spans: [{ text: 'world' }] },
31
+ * ]};
32
+ * buildBlockOffsetCache(doc);
33
+ * // => [
34
+ * // { blockId: 'b1', startOffset: 0, length: 5 },
35
+ * // { blockId: 'b2', startOffset: 6, length: 5 },
36
+ * // ]
37
+ * ```
38
+ */
39
+ export declare function buildBlockOffsetCache(doc: Document): BlockOffsetEntry[];
40
+ /**
41
+ * Convert a DOM position (node + offset) to an absolute RTIF document offset.
42
+ *
43
+ * Walks up from the given node to find the containing block element
44
+ * (`[data-rtif-block]`), then walks the block's span children to compute the
45
+ * local character offset. Adds the block's start offset from the cache.
46
+ *
47
+ * @param root - The editor's root element (`[data-rtif-root]`)
48
+ * @param node - The DOM node containing the position
49
+ * @param domOffset - The offset within the node (character offset for Text nodes,
50
+ * child index for Element nodes)
51
+ * @param cache - Pre-built block offset cache from `buildBlockOffsetCache`
52
+ * @returns The absolute RTIF offset, or `null` if the node is outside the
53
+ * editor or not inside a recognized block
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const textNode = blockEl.querySelector('[data-rtif-span]')!.firstChild!;
58
+ * const rtifOffset = domPointToRtifOffset(root, textNode, 3, cache);
59
+ * ```
60
+ */
61
+ export declare function domPointToRtifOffset(root: HTMLElement, node: Node, domOffset: number, cache: BlockOffsetEntry[]): number | null;
62
+ /**
63
+ * Convert an absolute RTIF document offset to a DOM point (node + offset).
64
+ *
65
+ * Uses `resolve()` from `@rtif-sdk/core` to find the block index and local offset,
66
+ * then walks the block's span elements in the DOM to locate the correct text
67
+ * node and character position.
68
+ *
69
+ * @param root - The editor's root element (`[data-rtif-root]`)
70
+ * @param doc - The current RTIF document (must match the DOM structure)
71
+ * @param offset - The absolute RTIF document offset
72
+ * @returns A DOM point, or `null` if the DOM is out of sync with the document
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const point = rtifOffsetToDomPoint(root, doc, 5);
77
+ * if (point) {
78
+ * console.log(point.node, point.offset);
79
+ * }
80
+ * ```
81
+ */
82
+ export declare function rtifOffsetToDomPoint(root: HTMLElement, doc: Document, offset: number): DomPoint | null;
83
+ /**
84
+ * Read the browser's current DOM selection and convert it to an RTIF Selection.
85
+ *
86
+ * Returns `null` if:
87
+ * - There is no selection or no ranges
88
+ * - The selection is outside the editor root
89
+ * - Either anchor or focus cannot be resolved to an RTIF offset
90
+ *
91
+ * @param root - The editor's root element (`[data-rtif-root]`)
92
+ * @param cache - Pre-built block offset cache from `buildBlockOffsetCache`
93
+ * @returns The RTIF Selection, or `null` if the selection cannot be resolved
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const sel = readDomSelection(root, cache);
98
+ * if (sel) {
99
+ * engine.dispatch({ type: 'set_selection', selection: sel });
100
+ * }
101
+ * ```
102
+ */
103
+ export declare function readDomSelection(root: HTMLElement, cache: BlockOffsetEntry[]): Selection | null;
104
+ /**
105
+ * Set the browser's DOM selection to match an RTIF Selection.
106
+ *
107
+ * Converts the RTIF anchor and focus offsets to DOM points, then calls
108
+ * `setBaseAndExtent()` on the window's Selection object.
109
+ *
110
+ * Sets the suppression flag to `true` before modifying the selection, to
111
+ * prevent the `selectionchange` event handler from triggering a feedback
112
+ * loop. The flag is reset via `requestAnimationFrame`.
113
+ *
114
+ * If either offset cannot be resolved to a DOM point (e.g., DOM is out of
115
+ * sync), the function is a no-op.
116
+ *
117
+ * @param root - The editor's root element (`[data-rtif-root]`)
118
+ * @param doc - The current RTIF document
119
+ * @param selection - The RTIF selection to apply to the DOM
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * setDomSelection(root, doc, { anchor: { offset: 0 }, focus: { offset: 5 } });
124
+ * ```
125
+ */
126
+ export declare function setDomSelection(root: HTMLElement, doc: Document, selection: Selection): void;
127
+ /**
128
+ * Returns `true` if the selection-sync layer is currently suppressing
129
+ * `selectionchange` events to avoid feedback loops.
130
+ *
131
+ * The `selectionchange` handler in the input bridge should check this flag
132
+ * and skip processing when it returns `true`.
133
+ *
134
+ * @returns Whether selectionchange events should be suppressed
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * document.addEventListener('selectionchange', () => {
139
+ * if (isSuppressed()) return;
140
+ * handleUserSelectionChange();
141
+ * });
142
+ * ```
143
+ */
144
+ export declare function isSuppressed(): boolean;
145
+ /**
146
+ * Manually reset the suppression flag to `false`.
147
+ *
148
+ * Primarily useful for testing, where `requestAnimationFrame` may not run.
149
+ * Production code should not need to call this.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * afterEach(() => {
154
+ * resetSuppression();
155
+ * });
156
+ * ```
157
+ */
158
+ export declare function resetSuppression(): void;
159
+ //# sourceMappingURL=selection-sync.d.ts.map