@malva-ui/editor 0.1.13 → 0.1.14

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 (134) hide show
  1. package/README.md +5 -5
  2. package/fesm2022/malva-ui-editor-ai.mjs +17 -0
  3. package/fesm2022/malva-ui-editor-ai.mjs.map +1 -0
  4. package/fesm2022/malva-ui-editor.mjs +10227 -0
  5. package/fesm2022/malva-ui-editor.mjs.map +1 -0
  6. package/package.json +35 -26
  7. package/types/malva-ui-editor-ai.d.ts +1 -0
  8. package/types/malva-ui-editor.d.ts +2995 -0
  9. package/ai/index.d.ts +0 -12
  10. package/ai/malva-ui-editor-ai.d.ts +0 -5
  11. package/esm2022/ai/index.js +0 -12
  12. package/esm2022/ai/index.js.map +0 -1
  13. package/esm2022/ai/malva-ui-editor-ai.js +0 -5
  14. package/esm2022/ai/malva-ui-editor-ai.js.map +0 -1
  15. package/esm2022/index.js +0 -37
  16. package/esm2022/index.js.map +0 -1
  17. package/esm2022/lib/ai/editor-ai-actions.js +0 -69
  18. package/esm2022/lib/ai/editor-ai-actions.js.map +0 -1
  19. package/esm2022/lib/ai/editor-ai-context.js +0 -612
  20. package/esm2022/lib/ai/editor-ai-context.js.map +0 -1
  21. package/esm2022/lib/ai/editor-ai-menu.js +0 -288
  22. package/esm2022/lib/ai/editor-ai-menu.js.map +0 -1
  23. package/esm2022/lib/ai/editor-ai-review-bar.js +0 -223
  24. package/esm2022/lib/ai/editor-ai-review-bar.js.map +0 -1
  25. package/esm2022/lib/ai/editor-ai-stream.js +0 -677
  26. package/esm2022/lib/ai/editor-ai-stream.js.map +0 -1
  27. package/esm2022/lib/ai/editor-ai-suggestions.js +0 -751
  28. package/esm2022/lib/ai/editor-ai-suggestions.js.map +0 -1
  29. package/esm2022/lib/ai/editor-ai.tokens.js +0 -10
  30. package/esm2022/lib/ai/editor-ai.tokens.js.map +0 -1
  31. package/esm2022/lib/ai/editor-ai.types.js +0 -1
  32. package/esm2022/lib/ai/editor-ai.types.js.map +0 -1
  33. package/esm2022/lib/editor/editor.js +0 -985
  34. package/esm2022/lib/editor/editor.js.map +0 -1
  35. package/esm2022/lib/editor-extension-preflight.js +0 -83
  36. package/esm2022/lib/editor-extension-preflight.js.map +0 -1
  37. package/esm2022/lib/editor-serialization.js +0 -152
  38. package/esm2022/lib/editor-serialization.js.map +0 -1
  39. package/esm2022/lib/editor-toolbar-context.js +0 -259
  40. package/esm2022/lib/editor-toolbar-context.js.map +0 -1
  41. package/esm2022/lib/editor.tokens.js +0 -10
  42. package/esm2022/lib/editor.tokens.js.map +0 -1
  43. package/esm2022/lib/editor.types.js +0 -48
  44. package/esm2022/lib/editor.types.js.map +0 -1
  45. package/esm2022/lib/extensions/editor-block-handle.js +0 -760
  46. package/esm2022/lib/extensions/editor-block-handle.js.map +0 -1
  47. package/esm2022/lib/extensions/editor-extensions.js +0 -321
  48. package/esm2022/lib/extensions/editor-extensions.js.map +0 -1
  49. package/esm2022/lib/extensions/editor-markdown-compatibility.js +0 -162
  50. package/esm2022/lib/extensions/editor-markdown-compatibility.js.map +0 -1
  51. package/esm2022/lib/extensions/editor-upload-placeholder.js +0 -156
  52. package/esm2022/lib/extensions/editor-upload-placeholder.js.map +0 -1
  53. package/esm2022/lib/status/editor-status.js +0 -64
  54. package/esm2022/lib/status/editor-status.js.map +0 -1
  55. package/esm2022/lib/toolbar/editor-alignment.js +0 -203
  56. package/esm2022/lib/toolbar/editor-alignment.js.map +0 -1
  57. package/esm2022/lib/toolbar/editor-block-insert.js +0 -101
  58. package/esm2022/lib/toolbar/editor-block-insert.js.map +0 -1
  59. package/esm2022/lib/toolbar/editor-color-control.js +0 -71
  60. package/esm2022/lib/toolbar/editor-color-control.js.map +0 -1
  61. package/esm2022/lib/toolbar/editor-command-button.js +0 -115
  62. package/esm2022/lib/toolbar/editor-command-button.js.map +0 -1
  63. package/esm2022/lib/toolbar/editor-heading.js +0 -266
  64. package/esm2022/lib/toolbar/editor-heading.js.map +0 -1
  65. package/esm2022/lib/toolbar/editor-highlight.js +0 -84
  66. package/esm2022/lib/toolbar/editor-highlight.js.map +0 -1
  67. package/esm2022/lib/toolbar/editor-image-upload-dialog.js +0 -289
  68. package/esm2022/lib/toolbar/editor-image-upload-dialog.js.map +0 -1
  69. package/esm2022/lib/toolbar/editor-image-upload.js +0 -315
  70. package/esm2022/lib/toolbar/editor-image-upload.js.map +0 -1
  71. package/esm2022/lib/toolbar/editor-inline-marks.js +0 -124
  72. package/esm2022/lib/toolbar/editor-inline-marks.js.map +0 -1
  73. package/esm2022/lib/toolbar/editor-link.js +0 -265
  74. package/esm2022/lib/toolbar/editor-link.js.map +0 -1
  75. package/esm2022/lib/toolbar/editor-list.js +0 -249
  76. package/esm2022/lib/toolbar/editor-list.js.map +0 -1
  77. package/esm2022/lib/toolbar/editor-table.js +0 -292
  78. package/esm2022/lib/toolbar/editor-table.js.map +0 -1
  79. package/esm2022/lib/toolbar/editor-text-color.js +0 -82
  80. package/esm2022/lib/toolbar/editor-text-color.js.map +0 -1
  81. package/esm2022/lib/toolbar/editor-toolbar-widget.js +0 -41
  82. package/esm2022/lib/toolbar/editor-toolbar-widget.js.map +0 -1
  83. package/esm2022/lib/toolbar/editor-toolbar.defs.js +0 -60
  84. package/esm2022/lib/toolbar/editor-toolbar.defs.js.map +0 -1
  85. package/esm2022/lib/toolbar/editor-toolbar.js +0 -826
  86. package/esm2022/lib/toolbar/editor-toolbar.js.map +0 -1
  87. package/esm2022/lib/toolbar/editor-undo-redo.js +0 -72
  88. package/esm2022/lib/toolbar/editor-undo-redo.js.map +0 -1
  89. package/esm2022/lib/toolbar/editor-zoom.js +0 -330
  90. package/esm2022/lib/toolbar/editor-zoom.js.map +0 -1
  91. package/esm2022/lib/upload/editor-image-upload-coordinator.js +0 -480
  92. package/esm2022/lib/upload/editor-image-upload-coordinator.js.map +0 -1
  93. package/esm2022/malva-ui-editor.js +0 -5
  94. package/esm2022/malva-ui-editor.js.map +0 -1
  95. package/index.d.ts +0 -35
  96. package/lib/ai/editor-ai-actions.d.ts +0 -27
  97. package/lib/ai/editor-ai-context.d.ts +0 -307
  98. package/lib/ai/editor-ai-menu.d.ts +0 -142
  99. package/lib/ai/editor-ai-review-bar.d.ts +0 -116
  100. package/lib/ai/editor-ai-stream.d.ts +0 -207
  101. package/lib/ai/editor-ai-suggestions.d.ts +0 -219
  102. package/lib/ai/editor-ai.tokens.d.ts +0 -10
  103. package/lib/ai/editor-ai.types.d.ts +0 -176
  104. package/lib/editor/editor.d.ts +0 -243
  105. package/lib/editor-extension-preflight.d.ts +0 -23
  106. package/lib/editor-serialization.d.ts +0 -74
  107. package/lib/editor-toolbar-context.d.ts +0 -116
  108. package/lib/editor.tokens.d.ts +0 -6
  109. package/lib/editor.types.d.ts +0 -194
  110. package/lib/extensions/editor-block-handle.d.ts +0 -59
  111. package/lib/extensions/editor-extensions.d.ts +0 -165
  112. package/lib/extensions/editor-markdown-compatibility.d.ts +0 -11
  113. package/lib/extensions/editor-upload-placeholder.d.ts +0 -75
  114. package/lib/status/editor-status.d.ts +0 -24
  115. package/lib/toolbar/editor-alignment.d.ts +0 -61
  116. package/lib/toolbar/editor-block-insert.d.ts +0 -26
  117. package/lib/toolbar/editor-color-control.d.ts +0 -32
  118. package/lib/toolbar/editor-command-button.d.ts +0 -29
  119. package/lib/toolbar/editor-heading.d.ts +0 -68
  120. package/lib/toolbar/editor-highlight.d.ts +0 -21
  121. package/lib/toolbar/editor-image-upload-dialog.d.ts +0 -96
  122. package/lib/toolbar/editor-image-upload.d.ts +0 -71
  123. package/lib/toolbar/editor-inline-marks.d.ts +0 -30
  124. package/lib/toolbar/editor-link.d.ts +0 -78
  125. package/lib/toolbar/editor-list.d.ts +0 -67
  126. package/lib/toolbar/editor-table.d.ts +0 -133
  127. package/lib/toolbar/editor-text-color.d.ts +0 -21
  128. package/lib/toolbar/editor-toolbar-widget.d.ts +0 -19
  129. package/lib/toolbar/editor-toolbar.d.ts +0 -157
  130. package/lib/toolbar/editor-toolbar.defs.d.ts +0 -35
  131. package/lib/toolbar/editor-undo-redo.d.ts +0 -26
  132. package/lib/toolbar/editor-zoom.d.ts +0 -108
  133. package/lib/upload/editor-image-upload-coordinator.d.ts +0 -55
  134. package/malva-ui-editor.d.ts +0 -5
@@ -1,677 +0,0 @@
1
- import { closeHistory } from '@tiptap/pm/history';
2
- import { Node as ProseMirrorNode } from '@tiptap/pm/model';
3
- import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state';
4
- import { Decoration, DecorationSet } from '@tiptap/pm/view';
5
- /**
6
- * CSS class of the inline decoration marking the active streaming region.
7
- *
8
- * The class is applied through a ProseMirror decoration held in plugin state,
9
- * never through document content, so it can appear in no HTML, Markdown, or
10
- * JSON serialization.
11
- */
12
- export const MLV_EDITOR_AI_STREAMING_CLASS = 'mlv-editor__ai-streaming';
13
- /**
14
- * CSS class of the one-shot inline decoration over each flush's newly
15
- * revealed slice of the streaming region.
16
- *
17
- * Created exactly once per revealed delta and only mapped afterwards — never
18
- * rebuilt for old ranges — so a one-shot CSS entrance animation runs once per
19
- * chunk and earlier text never re-animates. Removed with the region
20
- * decoration on commit, cancel, and abandonment, and excluded from every
21
- * serialization the same way.
22
- */
23
- export const MLV_EDITOR_AI_STREAMING_CHUNK_CLASS = 'mlv-editor__ai-streaming-chunk';
24
- /**
25
- * CSS class of the `aria-hidden` widget decoration the streaming engine
26
- * keeps at the current insertion tip while a writing session is active.
27
- *
28
- * View-layer only: it exists in no HTML, Markdown, or JSON serialization and
29
- * is removed when the session settles (commit, cancel, abandonment, failure).
30
- * Collect-only `'review'` sessions never render it — they never write.
31
- */
32
- export const MLV_EDITOR_AI_CARET_CLASS = 'mlv-editor__ai-caret';
33
- /**
34
- * @internal Key identifying the per-session streaming plugin. Every
35
- * transaction the session dispatches carries metadata under this key, which
36
- * is also how the external-transaction listener tells session writes apart
37
- * from foreign document changes.
38
- */
39
- const MLV_EDITOR_AI_STREAM_KEY = new PluginKey('mlvEditorAiStream');
40
- /** @internal Metadata clearing the region on restore and commit transactions. */
41
- const MLV_EDITOR_AI_CLEAR_METADATA = {
42
- type: 'clear',
43
- };
44
- /** @internal Empty plugin state shared by init and every clear. */
45
- const MLV_EDITOR_AI_EMPTY_PLUGIN_STATE = {
46
- region: null,
47
- decorations: DecorationSet.empty,
48
- };
49
- /**
50
- * @internal Default {@link MlvEditorAiStreamOptions.revealCharsPerFrame}:
51
- * about four characters per frame reads as fast, steady typing at 60 fps
52
- * without the per-chunk lurch of uncapped writes.
53
- */
54
- const MLV_EDITOR_AI_DEFAULT_REVEAL_CAP = 4;
55
- /**
56
- * @internal Spec marker identifying the caret widget inside the decoration
57
- * set, so each flush can remove the previous caret before re-anchoring it at
58
- * the new insertion tip.
59
- */
60
- const MLV_EDITOR_AI_CARET_SPEC = 'mlvEditorAiCaret';
61
- /**
62
- * @internal Spec marker identifying the whole-region tint decoration, which
63
- * is rebuilt on every flush because its range grows — unlike the per-chunk
64
- * decorations, which are created once and only mapped.
65
- */
66
- const MLV_EDITOR_AI_REGION_SPEC = 'mlvEditorAiRegion';
67
- /**
68
- * @internal Normalizes the reveal cap: `Infinity` is honoured as "uncapped",
69
- * unusable values fall back to the default, fractions floor.
70
- */
71
- const normalizeRevealCap = (value) => {
72
- if (value === Number.POSITIVE_INFINITY)
73
- return value;
74
- if (value === undefined || !Number.isFinite(value) || value < 1) {
75
- return MLV_EDITOR_AI_DEFAULT_REVEAL_CAP;
76
- }
77
- return Math.floor(value);
78
- };
79
- /**
80
- * @internal Builds the caret widget decoration at the given insertion tip.
81
- * The element is created lazily by the view (browser-only, so module scope
82
- * stays SSR-safe) and is `aria-hidden`: the caret is a purely visual pulse,
83
- * not content. `side: 1` keeps it after the text already revealed. The `key`
84
- * lets ProseMirror treat consecutive carets at one position as identical.
85
- */
86
- const createCaretDecoration = (tip) => Decoration.widget(tip, () => {
87
- const caret = document.createElement('span');
88
- caret.className = MLV_EDITOR_AI_CARET_CLASS;
89
- caret.setAttribute('aria-hidden', 'true');
90
- return caret;
91
- }, {
92
- key: MLV_EDITOR_AI_CARET_SPEC,
93
- side: 1,
94
- ignoreSelection: true,
95
- [MLV_EDITOR_AI_CARET_SPEC]: true,
96
- });
97
- /**
98
- * @internal One active session per editor. Starting a new session cancels the
99
- * previous one first, so the shared plugin key can never be registered twice.
100
- */
101
- const activeSessions = new WeakMap();
102
- /**
103
- * @internal Default frame scheduler: `requestAnimationFrame` where available,
104
- * otherwise a short timeout so streaming still progresses under server
105
- * rendering or a test DOM without a frame API. Feature-detected at call time —
106
- * module scope stays SSR-safe.
107
- */
108
- const defaultScheduler = (flush) => {
109
- if (typeof requestAnimationFrame === 'function' &&
110
- typeof cancelAnimationFrame === 'function') {
111
- const handle = requestAnimationFrame(() => flush());
112
- return () => cancelAnimationFrame(handle);
113
- }
114
- const handle = setTimeout(flush, 16);
115
- return () => clearTimeout(handle);
116
- };
117
- /**
118
- * @internal Creates the per-session ProseMirror plugin holding the streaming
119
- * region and rendering it as an inline decoration. Decorations are view-layer
120
- * state, so the region reaches no HTML, Markdown, or JSON serialization —
121
- * the upload-placeholder precedent.
122
- *
123
- * The plugin also claims Escape while it is registered — which is exactly the
124
- * session's lifetime — so a keyboard user whose focus is in the content
125
- * region can cancel a running transform without reaching for the toolbar
126
- * stop button.
127
- *
128
- * @param onEscape Invoked when Escape is pressed inside the content region.
129
- */
130
- function createStreamPlugin(onEscape) {
131
- return new Plugin({
132
- key: MLV_EDITOR_AI_STREAM_KEY,
133
- state: {
134
- init: () => MLV_EDITOR_AI_EMPTY_PLUGIN_STATE,
135
- apply: (transaction, value) => {
136
- const metadata = transaction.getMeta(MLV_EDITOR_AI_STREAM_KEY);
137
- if (metadata?.type === 'clear')
138
- return MLV_EDITOR_AI_EMPTY_PLUGIN_STATE;
139
- // Existing chunk decorations are only ever mapped — never rebuilt —
140
- // so their DOM survives later flushes and the one-shot entrance
141
- // animation cannot re-run over earlier text.
142
- let decorations = transaction.docChanged
143
- ? value.decorations.map(transaction.mapping, transaction.doc)
144
- : value.decorations;
145
- if (metadata?.type === 'set') {
146
- // The region tint (its range grew) and the caret (it moved to the
147
- // new tip) are replaced; the newly revealed slice gets its own
148
- // freshly created one-shot chunk decoration.
149
- decorations = decorations.remove(decorations.find(undefined, undefined, (spec) => spec[MLV_EDITOR_AI_CARET_SPEC] === true ||
150
- spec[MLV_EDITOR_AI_REGION_SPEC] === true));
151
- const added = [];
152
- if (metadata.from < metadata.to) {
153
- added.push(Decoration.inline(metadata.from, metadata.to, { class: MLV_EDITOR_AI_STREAMING_CLASS }, { [MLV_EDITOR_AI_REGION_SPEC]: true }));
154
- }
155
- if (metadata.chunkFrom < metadata.chunkTo) {
156
- added.push(Decoration.inline(metadata.chunkFrom, metadata.chunkTo, {
157
- class: MLV_EDITOR_AI_STREAMING_CHUNK_CLASS,
158
- }));
159
- }
160
- added.push(createCaretDecoration(metadata.to));
161
- return {
162
- region: { from: metadata.from, to: metadata.to },
163
- decorations: decorations.add(transaction.doc, added),
164
- };
165
- }
166
- if (!value.region || !transaction.docChanged) {
167
- return decorations === value.decorations
168
- ? value
169
- : { region: value.region, decorations };
170
- }
171
- // Appended transactions from other plugins move the region too; the
172
- // session re-reads this mapped state after every dispatch.
173
- return {
174
- region: {
175
- from: transaction.mapping.map(value.region.from, -1),
176
- to: transaction.mapping.map(value.region.to, 1),
177
- },
178
- decorations,
179
- };
180
- },
181
- },
182
- props: {
183
- decorations: (state) => MLV_EDITOR_AI_STREAM_KEY.getState(state)?.decorations ?? null,
184
- handleKeyDown: (_view, event) => {
185
- if (event.key !== 'Escape')
186
- return false;
187
- onEscape();
188
- return true;
189
- },
190
- },
191
- });
192
- }
193
- /**
194
- * Runs one AI streaming session against a live Tiptap editor.
195
- *
196
- * Framework-free: the runner needs only the editor instance and an async
197
- * iterable of Markdown/text chunks — no Angular, no provider token, no
198
- * network. Interim writes render the chunks as plain text inside the target
199
- * region; the final commit parses the accumulated Markdown through the
200
- * editor's Markdown manager, so the committed document carries real
201
- * structure rather than literal Markdown syntax. An editor without a
202
- * Markdown manager commits plain text — the same fallback the Angular
203
- * context uses for request serialization. The `'review'` output mode is
204
- * collect-only: no interim write, no decoration, and no final commit ever
205
- * touches the document — a `'committed'` result just carries the complete
206
- * text for the caller to route through the suggestion engine. Every other
207
- * lifecycle rule below (Escape, cancellation, abandonment, transport and
208
- * result errors) applies to it unchanged.
209
- *
210
- * Lifecycle:
211
- *
212
- * - A checkpoint (document JSON) and the target coordinates are captured when
213
- * the session starts, before the first mutating step.
214
- * - Chunks are buffered and written at most once per animation frame, and
215
- * each flush reveals at most {@link MlvEditorAiStreamOptions.revealCharsPerFrame}
216
- * characters — the remainder carries over to self-scheduled follow-up
217
- * frames, smoothing bursty providers into a steady typing cadence. Every
218
- * interim write dispatches with `addToHistory: false`, so no partial state
219
- * ever becomes an undo step.
220
- * - Each flush's newly revealed slice carries a one-shot
221
- * {@link MLV_EDITOR_AI_STREAMING_CHUNK_CLASS} entrance decoration (created
222
- * once, then only mapped), and an `aria-hidden`
223
- * {@link MLV_EDITOR_AI_CARET_CLASS} widget marks the insertion tip while
224
- * the session writes. Both are view-layer decorations: removed when the
225
- * session settles and excluded from every serialization, exactly like the
226
- * region tint.
227
- * - On successful completion the session silently reverts to the checkpoint
228
- * and commits the complete content as exactly one history-visible step
229
- * (closed against the previous history group): one undo restores the
230
- * pre-session document.
231
- * - {@link MlvEditorAiStreamHandle.cancel} restores the checkpoint and
232
- * resolves with `'cancelled'` — silently, with no error code. Pressing
233
- * Escape inside the content region triggers the same cancellation through
234
- * the session plugin's key handler.
235
- * - A thrown or rejected chunk iteration restores the checkpoint and resolves
236
- * `'failed'` with `'ai-transport'`. Empty or whitespace-only total output
237
- * restores the checkpoint and resolves `'failed'` with `'ai-result'`.
238
- * - **Abandonment**: any external transaction — one whose root carries no
239
- * session metadata but changes the document (directly or through appended
240
- * transactions) — ends the session immediately following the drag-lifecycle
241
- * precedent. The session stops consuming and writing, removes its
242
- * decoration, and resolves `'abandoned'` with `error: null` and
243
- * `aborted: true`. **Nothing is restored**: inverting the checkpoint
244
- * through foreign steps is unsafe, so the document is left exactly as the
245
- * external edit made it, including any interim session writes (which were
246
- * history-invisible and therefore cannot be undone individually).
247
- * - Destroying the editor mid-session settles it as `'abandoned'` without
248
- * touching the DOM.
249
- *
250
- * The session never throws and {@link MlvEditorAiStreamHandle.done} never
251
- * rejects. Starting a new session on an editor cancels the session already
252
- * running there.
253
- *
254
- * @param editor Live Tiptap editor the session writes into.
255
- * @param options Chunk source, output mode, and optional frame scheduler.
256
- * @returns Handle exposing completion, cancellation, and checkpoint restore.
257
- */
258
- export function runMlvEditorAiStream(editor, options) {
259
- const scheduler = options.scheduler ?? defaultScheduler;
260
- const output = options.output;
261
- const revealCap = normalizeRevealCap(options.revealCharsPerFrame);
262
- const iterator = options.chunks[Symbol.asyncIterator]();
263
- let resolveDone;
264
- const done = new Promise((resolve) => {
265
- resolveDone = resolve;
266
- });
267
- /** Resolves the consumption loop's race when the session settles early. */
268
- let notifyInterrupt = () => undefined;
269
- const interrupted = new Promise((resolve) => {
270
- notifyInterrupt = () => resolve('interrupted');
271
- });
272
- let settled = false;
273
- let hostDestroyed = false;
274
- let registered = false;
275
- let mutated = false;
276
- let accumulated = '';
277
- /** Characters of `accumulated` already written into the document. */
278
- let written = 0;
279
- /**
280
- * Characters the first (region-bootstrapping) flush wrote. Marks the exact
281
- * document point where prosemirror-history's deferred mapping pins the
282
- * `to` positions of earlier undo items (ReplaceStep maps `to` with assoc
283
- * -1, so later appends never move them past it); the precise revert must
284
- * split its steps at this point rather than replace across it.
285
- */
286
- let firstWritten = 0;
287
- let region = null;
288
- let scheduled = false;
289
- let cancelScheduled = null;
290
- /** Politely closes the chunk producer; its cleanup must not break settlement. */
291
- const closeIterator = () => {
292
- try {
293
- const closing = iterator.return?.();
294
- if (closing)
295
- void Promise.resolve(closing).catch(() => undefined);
296
- }
297
- catch {
298
- // A producer throwing from return() has no bearing on the session result.
299
- }
300
- };
301
- if (editor.isDestroyed) {
302
- settled = true;
303
- closeIterator();
304
- resolveDone({ status: 'abandoned', error: null, aborted: true, text: '' });
305
- return {
306
- done,
307
- cancel: () => undefined,
308
- restoreCheckpoint: () => false,
309
- };
310
- }
311
- // One session per editor: the shared plugin key must never register twice,
312
- // and two writers over one region cannot both be correct.
313
- activeSessions.get(editor)?.();
314
- // Captured before the first mutating step. External document changes abandon
315
- // the session, and only session writes mutate otherwise, so these stay valid
316
- // until the session itself writes.
317
- const checkpoint = editor.state.doc.toJSON();
318
- const selectionFrom = editor.state.selection.from;
319
- const selectionTo = editor.state.selection.to;
320
- const $to = editor.state.selection.$to;
321
- /** Gap after the selection's top-level block, where `'insert-below'` writes. */
322
- const insertAt = $to.depth === 0 ? $to.pos : $to.after(1);
323
- /** @internal Parses the checkpoint back into a document node. */
324
- const checkpointDoc = () => ProseMirrorNode.fromJSON(editor.schema, checkpoint);
325
- /**
326
- * @internal Tries to revert only the session-written region back to the
327
- * checkpoint content. Preferred over replacing the whole document because
328
- * a whole-document replace map collapses every position inside it, which
329
- * would corrupt the deferred mapping prosemirror-history applies to undo
330
- * items recorded before the session. Returns whether the built steps are
331
- * usable; the caller still verifies the result equals the checkpoint.
332
- */
333
- const buildPreciseRevert = (tr, target) => {
334
- if (!region)
335
- return false;
336
- try {
337
- if (output === 'replace-selection') {
338
- // Earlier undo items' `to` positions sit pinned at the end of the
339
- // first interim write (see `firstWritten`). Splitting the revert
340
- // there keeps every step boundary exactly on that point, so the
341
- // deferred mapping prosemirror-history applies to pre-session undo
342
- // items never crosses the interior of a replaced range — which would
343
- // silently corrupt those items.
344
- const firstEnd = Math.min(region.from + firstWritten, region.to);
345
- if (firstEnd < region.to)
346
- tr.delete(firstEnd, region.to);
347
- tr.replace(region.from, firstEnd, target.slice(selectionFrom, selectionTo));
348
- }
349
- else {
350
- // The streamed paragraph wraps the region by exactly one position on
351
- // each side; deleting it removes everything the session inserted.
352
- tr.delete(Math.max(0, region.from - 1), Math.min(tr.doc.content.size, region.to + 1));
353
- }
354
- return true;
355
- }
356
- catch {
357
- return false;
358
- }
359
- };
360
- /**
361
- * @internal Restores the checkpoint: precisely when the region-local revert
362
- * verifiably reproduces it, otherwise by replacing the whole document.
363
- * History-visible restores close the current history group so they undo on
364
- * their own; invisible restores also hide from history entirely.
365
- */
366
- const restoreDoc = (historyVisible) => {
367
- const target = checkpointDoc();
368
- let tr = editor.state.tr;
369
- if (!buildPreciseRevert(tr, target) || !tr.doc.eq(target)) {
370
- tr = editor.state.tr;
371
- tr.replaceWith(0, tr.doc.content.size, target.content);
372
- }
373
- tr.setSelection(TextSelection.near(tr.doc.resolve(Math.min(selectionFrom, tr.doc.content.size))));
374
- tr.setMeta(MLV_EDITOR_AI_STREAM_KEY, MLV_EDITOR_AI_CLEAR_METADATA);
375
- if (historyVisible)
376
- closeHistory(tr);
377
- else
378
- tr.setMeta('addToHistory', false);
379
- editor.view.dispatch(tr);
380
- };
381
- /**
382
- * @internal Commits the streamed Markdown as parsed rich content through
383
- * the editor's Markdown manager, as one history-visible step against the
384
- * checkpoint document. A single plain paragraph replaces the selection
385
- * inline, so a one-line result never splits the surrounding block; every
386
- * other result lands as the block structure the Markdown describes.
387
- * Returns `false` — without dispatching a content change — when no manager
388
- * is registered, parsing fails or yields nothing, or the insertion is
389
- * refused; the caller then commits plain text instead.
390
- */
391
- const commitParsedMarkdown = (text) => {
392
- const manager = editor.markdown;
393
- if (!manager)
394
- return false;
395
- let blocks;
396
- try {
397
- const parsed = manager.parse(text);
398
- if (!Array.isArray(parsed.content) || parsed.content.length === 0) {
399
- return false;
400
- }
401
- blocks = parsed.content;
402
- }
403
- catch {
404
- return false;
405
- }
406
- const inline = output === 'replace-selection' &&
407
- blocks.length === 1 &&
408
- blocks[0].type === 'paragraph'
409
- ? (blocks[0].content ?? [])
410
- : null;
411
- const content = inline ?? blocks;
412
- if (content.length === 0)
413
- return false;
414
- try {
415
- return editor
416
- .chain()
417
- .command(({ tr }) => {
418
- tr.setMeta(MLV_EDITOR_AI_STREAM_KEY, MLV_EDITOR_AI_CLEAR_METADATA);
419
- closeHistory(tr);
420
- return true;
421
- })
422
- .insertContentAt(output === 'replace-selection'
423
- ? { from: selectionFrom, to: selectionTo }
424
- : insertAt, content)
425
- .run();
426
- }
427
- catch {
428
- return false;
429
- }
430
- };
431
- /**
432
- * @internal Commits the complete streamed text as one history-visible step
433
- * against the checkpoint document. `closeHistory` prevents the step from
434
- * merging into a directly preceding user edit's undo group. Markdown
435
- * structure is committed through {@link commitParsedMarkdown}; the inline
436
- * path below is the plain-text fallback for editors without a Markdown
437
- * manager.
438
- */
439
- const commitFinal = (text) => {
440
- if (commitParsedMarkdown(text))
441
- return;
442
- const tr = editor.state.tr;
443
- let end;
444
- if (output === 'replace-selection') {
445
- tr.insertText(text, selectionFrom, selectionTo);
446
- end = tr.mapping.map(selectionTo, 1);
447
- }
448
- else {
449
- const paragraph = editor.schema.nodes['paragraph'];
450
- if (paragraph) {
451
- tr.insert(insertAt, paragraph.create(null, editor.schema.text(text)));
452
- end = insertAt + 1 + text.length;
453
- }
454
- else {
455
- tr.insertText(text, insertAt, insertAt);
456
- end = tr.mapping.map(insertAt, 1);
457
- }
458
- }
459
- tr.setSelection(TextSelection.near(tr.doc.resolve(Math.min(end, tr.doc.content.size))));
460
- tr.setMeta(MLV_EDITOR_AI_STREAM_KEY, MLV_EDITOR_AI_CLEAR_METADATA);
461
- closeHistory(tr);
462
- editor.view.dispatch(tr);
463
- };
464
- /**
465
- * @internal Settles the session exactly once: detaches every listener,
466
- * cancels pending frames, closes the producer, performs the requested
467
- * restore/commit, unregisters the plugin, and resolves `done`. Wrapped so a
468
- * failing dispatch can never surface as a thrown error — the session's
469
- * contract is that it never throws.
470
- */
471
- const settle = (status, error, behavior) => {
472
- if (settled)
473
- return;
474
- settled = true;
475
- editor.off('transaction', onTransaction);
476
- editor.off('destroy', onDestroy);
477
- cancelScheduled?.();
478
- cancelScheduled = null;
479
- scheduled = false;
480
- closeIterator();
481
- try {
482
- if (!hostDestroyed && !editor.isDestroyed) {
483
- if (behavior.restore && mutated)
484
- restoreDoc(false);
485
- if (behavior.commit)
486
- commitFinal(accumulated);
487
- if (registered)
488
- editor.unregisterPlugin(MLV_EDITOR_AI_STREAM_KEY);
489
- }
490
- }
491
- catch {
492
- // Settlement must complete even when a final dispatch fails; the result
493
- // below still reports the session outcome.
494
- }
495
- if (activeSessions.get(editor) === cancel)
496
- activeSessions.delete(editor);
497
- notifyInterrupt();
498
- resolveDone({
499
- status,
500
- error,
501
- aborted: status === 'cancelled' || status === 'abandoned',
502
- text: accumulated,
503
- });
504
- };
505
- /**
506
- * @internal Abandons on any external document change: the root transaction
507
- * carries no session metadata and the document changed, either directly or
508
- * through transactions other plugins appended to that root. Session-rooted
509
- * events are skipped wholesale — reactions other plugins append to a
510
- * session write (for example a trailing paragraph) belong to the session.
511
- */
512
- const onTransaction = ({ transaction, appendedTransactions, }) => {
513
- if (settled)
514
- return;
515
- if (transaction.getMeta(MLV_EDITOR_AI_STREAM_KEY) !== undefined)
516
- return;
517
- const docChanged = transaction.docChanged ||
518
- appendedTransactions.some((appended) => appended.docChanged);
519
- if (docChanged)
520
- settle('abandoned', null, { restore: false, commit: false });
521
- };
522
- /** @internal A destroyed editor leaves nothing to restore or unregister. */
523
- const onDestroy = () => {
524
- hostDestroyed = true;
525
- settle('abandoned', null, { restore: false, commit: false });
526
- };
527
- /**
528
- * @internal Writes the next paced slice of the accumulated text into the
529
- * target region. The first write bootstraps the region from the captured
530
- * target; later writes append at the region's end — appends only, so the
531
- * per-chunk entrance decorations over earlier text keep their positions and
532
- * DOM. Each flush reveals at most `revealCap` characters; a remaining
533
- * buffer schedules its own follow-up frame (carry-over), which is what
534
- * turns bursty chunks into a steady cadence. Region boundaries are mapped
535
- * through the transaction and then re-read from plugin state, so appended
536
- * transactions cannot desynchronize them.
537
- */
538
- const flush = () => {
539
- if (settled || hostDestroyed || editor.isDestroyed)
540
- return;
541
- const pending = accumulated.length - written;
542
- if (pending <= 0)
543
- return;
544
- const count = Math.min(pending, revealCap);
545
- const delta = accumulated.slice(written, written + count);
546
- try {
547
- const tr = editor.state.tr;
548
- let next;
549
- let chunkFrom;
550
- if (region) {
551
- tr.insertText(delta, region.to, region.to);
552
- chunkFrom = tr.mapping.map(region.to, -1);
553
- next = {
554
- from: tr.mapping.map(region.from, -1),
555
- to: tr.mapping.map(region.to, 1),
556
- };
557
- }
558
- else if (output === 'replace-selection') {
559
- tr.insertText(delta, selectionFrom, selectionTo);
560
- next = {
561
- from: tr.mapping.map(selectionFrom, -1),
562
- to: tr.mapping.map(selectionTo, 1),
563
- };
564
- chunkFrom = next.from;
565
- }
566
- else {
567
- const paragraph = editor.schema.nodes['paragraph'];
568
- if (paragraph) {
569
- tr.insert(insertAt, paragraph.create(null, editor.schema.text(delta)));
570
- next = { from: insertAt + 1, to: insertAt + 1 + delta.length };
571
- }
572
- else {
573
- tr.insertText(delta, insertAt, insertAt);
574
- next = {
575
- from: tr.mapping.map(insertAt, -1),
576
- to: tr.mapping.map(insertAt, 1),
577
- };
578
- }
579
- chunkFrom = next.from;
580
- }
581
- tr.setMeta('addToHistory', false);
582
- tr.setMeta(MLV_EDITOR_AI_STREAM_KEY, {
583
- type: 'set',
584
- from: next.from,
585
- to: next.to,
586
- chunkFrom,
587
- chunkTo: next.to,
588
- });
589
- editor.view.dispatch(tr);
590
- mutated = true;
591
- if (written === 0)
592
- firstWritten = count;
593
- written += count;
594
- region = MLV_EDITOR_AI_STREAM_KEY.getState(editor.state)?.region ?? next;
595
- // Carry-over: reveal the rest of the buffer on later frames.
596
- if (written < accumulated.length)
597
- scheduleFlush();
598
- }
599
- catch {
600
- // The target region rejected the write; the output is unusable here.
601
- settle('failed', 'ai-result', { restore: true, commit: false });
602
- }
603
- };
604
- /** @internal Schedules at most one flush per frame while chunks arrive. */
605
- const scheduleFlush = () => {
606
- if (settled || scheduled)
607
- return;
608
- scheduled = true;
609
- const cancelFrame = scheduler(() => {
610
- scheduled = false;
611
- cancelScheduled = null;
612
- flush();
613
- });
614
- // A synchronously invoked scheduler has already flushed by now.
615
- cancelScheduled = scheduled ? cancelFrame : null;
616
- };
617
- const cancel = () => settle('cancelled', null, { restore: true, commit: false });
618
- const restoreCheckpoint = () => {
619
- if (hostDestroyed || editor.isDestroyed)
620
- return false;
621
- const changed = !editor.state.doc.eq(checkpointDoc());
622
- if (!settled) {
623
- cancel();
624
- return changed;
625
- }
626
- if (!changed)
627
- return false;
628
- try {
629
- restoreDoc(true);
630
- }
631
- catch {
632
- return false;
633
- }
634
- return true;
635
- };
636
- editor.registerPlugin(createStreamPlugin(() => cancel()));
637
- registered = true;
638
- editor.on('transaction', onTransaction);
639
- editor.on('destroy', onDestroy);
640
- activeSessions.set(editor, cancel);
641
- void (async () => {
642
- for (;;) {
643
- if (settled)
644
- return;
645
- const step = await Promise.race([
646
- interrupted,
647
- iterator.next().then((result) => result, () => 'thrown'),
648
- ]);
649
- if (settled || step === 'interrupted')
650
- return;
651
- if (step === 'thrown') {
652
- settle('failed', 'ai-transport', { restore: true, commit: false });
653
- return;
654
- }
655
- if (step.done) {
656
- if (accumulated.trim().length === 0) {
657
- settle('failed', 'ai-result', { restore: true, commit: false });
658
- }
659
- else {
660
- // Collect-only review sessions never wrote, so there is nothing to
661
- // restore (`mutated` stayed false) and nothing to commit.
662
- settle('committed', null, {
663
- restore: true,
664
- commit: output !== 'review',
665
- });
666
- }
667
- return;
668
- }
669
- accumulated += step.value;
670
- // Review sessions only accumulate; a flush would write the document.
671
- if (output !== 'review')
672
- scheduleFlush();
673
- }
674
- })();
675
- return { done, cancel, restoreCheckpoint };
676
- }
677
- //# sourceMappingURL=editor-ai-stream.js.map