@malva-ui/editor 0.1.12 → 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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/fesm2022/malva-ui-editor-ai.mjs +17 -0
  4. package/fesm2022/malva-ui-editor-ai.mjs.map +1 -0
  5. package/fesm2022/malva-ui-editor.mjs +10227 -0
  6. package/fesm2022/malva-ui-editor.mjs.map +1 -0
  7. package/package.json +47 -26
  8. package/types/malva-ui-editor-ai.d.ts +1 -0
  9. package/types/malva-ui-editor.d.ts +2995 -0
  10. package/ai/index.d.ts +0 -12
  11. package/ai/malva-ui-editor-ai.d.ts +0 -5
  12. package/esm2022/ai/index.js +0 -12
  13. package/esm2022/ai/index.js.map +0 -1
  14. package/esm2022/ai/malva-ui-editor-ai.js +0 -5
  15. package/esm2022/ai/malva-ui-editor-ai.js.map +0 -1
  16. package/esm2022/index.js +0 -37
  17. package/esm2022/index.js.map +0 -1
  18. package/esm2022/lib/ai/editor-ai-actions.js +0 -69
  19. package/esm2022/lib/ai/editor-ai-actions.js.map +0 -1
  20. package/esm2022/lib/ai/editor-ai-context.js +0 -612
  21. package/esm2022/lib/ai/editor-ai-context.js.map +0 -1
  22. package/esm2022/lib/ai/editor-ai-menu.js +0 -288
  23. package/esm2022/lib/ai/editor-ai-menu.js.map +0 -1
  24. package/esm2022/lib/ai/editor-ai-review-bar.js +0 -223
  25. package/esm2022/lib/ai/editor-ai-review-bar.js.map +0 -1
  26. package/esm2022/lib/ai/editor-ai-stream.js +0 -677
  27. package/esm2022/lib/ai/editor-ai-stream.js.map +0 -1
  28. package/esm2022/lib/ai/editor-ai-suggestions.js +0 -751
  29. package/esm2022/lib/ai/editor-ai-suggestions.js.map +0 -1
  30. package/esm2022/lib/ai/editor-ai.tokens.js +0 -10
  31. package/esm2022/lib/ai/editor-ai.tokens.js.map +0 -1
  32. package/esm2022/lib/ai/editor-ai.types.js +0 -1
  33. package/esm2022/lib/ai/editor-ai.types.js.map +0 -1
  34. package/esm2022/lib/editor/editor.js +0 -985
  35. package/esm2022/lib/editor/editor.js.map +0 -1
  36. package/esm2022/lib/editor-extension-preflight.js +0 -83
  37. package/esm2022/lib/editor-extension-preflight.js.map +0 -1
  38. package/esm2022/lib/editor-serialization.js +0 -152
  39. package/esm2022/lib/editor-serialization.js.map +0 -1
  40. package/esm2022/lib/editor-toolbar-context.js +0 -259
  41. package/esm2022/lib/editor-toolbar-context.js.map +0 -1
  42. package/esm2022/lib/editor.tokens.js +0 -10
  43. package/esm2022/lib/editor.tokens.js.map +0 -1
  44. package/esm2022/lib/editor.types.js +0 -48
  45. package/esm2022/lib/editor.types.js.map +0 -1
  46. package/esm2022/lib/extensions/editor-block-handle.js +0 -760
  47. package/esm2022/lib/extensions/editor-block-handle.js.map +0 -1
  48. package/esm2022/lib/extensions/editor-extensions.js +0 -321
  49. package/esm2022/lib/extensions/editor-extensions.js.map +0 -1
  50. package/esm2022/lib/extensions/editor-markdown-compatibility.js +0 -162
  51. package/esm2022/lib/extensions/editor-markdown-compatibility.js.map +0 -1
  52. package/esm2022/lib/extensions/editor-upload-placeholder.js +0 -156
  53. package/esm2022/lib/extensions/editor-upload-placeholder.js.map +0 -1
  54. package/esm2022/lib/status/editor-status.js +0 -64
  55. package/esm2022/lib/status/editor-status.js.map +0 -1
  56. package/esm2022/lib/toolbar/editor-alignment.js +0 -203
  57. package/esm2022/lib/toolbar/editor-alignment.js.map +0 -1
  58. package/esm2022/lib/toolbar/editor-block-insert.js +0 -101
  59. package/esm2022/lib/toolbar/editor-block-insert.js.map +0 -1
  60. package/esm2022/lib/toolbar/editor-color-control.js +0 -71
  61. package/esm2022/lib/toolbar/editor-color-control.js.map +0 -1
  62. package/esm2022/lib/toolbar/editor-command-button.js +0 -115
  63. package/esm2022/lib/toolbar/editor-command-button.js.map +0 -1
  64. package/esm2022/lib/toolbar/editor-heading.js +0 -266
  65. package/esm2022/lib/toolbar/editor-heading.js.map +0 -1
  66. package/esm2022/lib/toolbar/editor-highlight.js +0 -84
  67. package/esm2022/lib/toolbar/editor-highlight.js.map +0 -1
  68. package/esm2022/lib/toolbar/editor-image-upload-dialog.js +0 -289
  69. package/esm2022/lib/toolbar/editor-image-upload-dialog.js.map +0 -1
  70. package/esm2022/lib/toolbar/editor-image-upload.js +0 -315
  71. package/esm2022/lib/toolbar/editor-image-upload.js.map +0 -1
  72. package/esm2022/lib/toolbar/editor-inline-marks.js +0 -124
  73. package/esm2022/lib/toolbar/editor-inline-marks.js.map +0 -1
  74. package/esm2022/lib/toolbar/editor-link.js +0 -265
  75. package/esm2022/lib/toolbar/editor-link.js.map +0 -1
  76. package/esm2022/lib/toolbar/editor-list.js +0 -249
  77. package/esm2022/lib/toolbar/editor-list.js.map +0 -1
  78. package/esm2022/lib/toolbar/editor-table.js +0 -292
  79. package/esm2022/lib/toolbar/editor-table.js.map +0 -1
  80. package/esm2022/lib/toolbar/editor-text-color.js +0 -82
  81. package/esm2022/lib/toolbar/editor-text-color.js.map +0 -1
  82. package/esm2022/lib/toolbar/editor-toolbar-widget.js +0 -41
  83. package/esm2022/lib/toolbar/editor-toolbar-widget.js.map +0 -1
  84. package/esm2022/lib/toolbar/editor-toolbar.defs.js +0 -60
  85. package/esm2022/lib/toolbar/editor-toolbar.defs.js.map +0 -1
  86. package/esm2022/lib/toolbar/editor-toolbar.js +0 -826
  87. package/esm2022/lib/toolbar/editor-toolbar.js.map +0 -1
  88. package/esm2022/lib/toolbar/editor-undo-redo.js +0 -72
  89. package/esm2022/lib/toolbar/editor-undo-redo.js.map +0 -1
  90. package/esm2022/lib/toolbar/editor-zoom.js +0 -330
  91. package/esm2022/lib/toolbar/editor-zoom.js.map +0 -1
  92. package/esm2022/lib/upload/editor-image-upload-coordinator.js +0 -480
  93. package/esm2022/lib/upload/editor-image-upload-coordinator.js.map +0 -1
  94. package/esm2022/malva-ui-editor.js +0 -5
  95. package/esm2022/malva-ui-editor.js.map +0 -1
  96. package/index.d.ts +0 -35
  97. package/lib/ai/editor-ai-actions.d.ts +0 -27
  98. package/lib/ai/editor-ai-context.d.ts +0 -307
  99. package/lib/ai/editor-ai-menu.d.ts +0 -142
  100. package/lib/ai/editor-ai-review-bar.d.ts +0 -116
  101. package/lib/ai/editor-ai-stream.d.ts +0 -207
  102. package/lib/ai/editor-ai-suggestions.d.ts +0 -219
  103. package/lib/ai/editor-ai.tokens.d.ts +0 -10
  104. package/lib/ai/editor-ai.types.d.ts +0 -176
  105. package/lib/editor/editor.d.ts +0 -243
  106. package/lib/editor-extension-preflight.d.ts +0 -23
  107. package/lib/editor-serialization.d.ts +0 -74
  108. package/lib/editor-toolbar-context.d.ts +0 -116
  109. package/lib/editor.tokens.d.ts +0 -6
  110. package/lib/editor.types.d.ts +0 -194
  111. package/lib/extensions/editor-block-handle.d.ts +0 -59
  112. package/lib/extensions/editor-extensions.d.ts +0 -165
  113. package/lib/extensions/editor-markdown-compatibility.d.ts +0 -11
  114. package/lib/extensions/editor-upload-placeholder.d.ts +0 -75
  115. package/lib/status/editor-status.d.ts +0 -24
  116. package/lib/toolbar/editor-alignment.d.ts +0 -61
  117. package/lib/toolbar/editor-block-insert.d.ts +0 -26
  118. package/lib/toolbar/editor-color-control.d.ts +0 -32
  119. package/lib/toolbar/editor-command-button.d.ts +0 -29
  120. package/lib/toolbar/editor-heading.d.ts +0 -68
  121. package/lib/toolbar/editor-highlight.d.ts +0 -21
  122. package/lib/toolbar/editor-image-upload-dialog.d.ts +0 -96
  123. package/lib/toolbar/editor-image-upload.d.ts +0 -71
  124. package/lib/toolbar/editor-inline-marks.d.ts +0 -30
  125. package/lib/toolbar/editor-link.d.ts +0 -78
  126. package/lib/toolbar/editor-list.d.ts +0 -67
  127. package/lib/toolbar/editor-table.d.ts +0 -133
  128. package/lib/toolbar/editor-text-color.d.ts +0 -21
  129. package/lib/toolbar/editor-toolbar-widget.d.ts +0 -19
  130. package/lib/toolbar/editor-toolbar.d.ts +0 -157
  131. package/lib/toolbar/editor-toolbar.defs.d.ts +0 -35
  132. package/lib/toolbar/editor-undo-redo.d.ts +0 -26
  133. package/lib/toolbar/editor-zoom.d.ts +0 -108
  134. package/lib/upload/editor-image-upload-coordinator.d.ts +0 -55
  135. package/malva-ui-editor.d.ts +0 -5
@@ -1,612 +0,0 @@
1
- import { LiveAnnouncer } from '@angular/cdk/a11y';
2
- import { computed, inject, Injectable, InjectionToken, signal, } from '@angular/core';
3
- import { MLV_EDITOR_I18N, MlvI18nResolverService } from '@malva-ui/i18n';
4
- import { MLV_EDITOR_TOOLBAR_CONTEXT } from '../editor-toolbar-context';
5
- import { runMlvEditorAiStream } from './editor-ai-stream';
6
- import { applyMlvEditorAiSuggestions } from './editor-ai-suggestions';
7
- import { MLV_EDITOR_AI_PROVIDER } from './editor-ai.tokens';
8
- import * as i0 from "@angular/core";
9
- /**
10
- * @internal English ICU fallback for the review-started announcement, used
11
- * when no `MLV_EDITOR_I18N` token is provided — the same template the English
12
- * pack ships.
13
- */
14
- const REVIEW_STARTED_FALLBACK = '{count, plural, one {# AI suggestion ready for review} other {# AI suggestions ready for review}}';
15
- /**
16
- * @internal English ICU fallbacks for the current-suggestion descriptions,
17
- * used when no `MLV_EDITOR_I18N` token is provided — the same templates the
18
- * English pack ships.
19
- */
20
- const CURRENT_SUGGESTION_FALLBACKS = {
21
- aiCurrentSuggestionReplace: 'Suggestion {index} of {count}: replaces "{oldText}" with "{newText}"',
22
- aiCurrentSuggestionInsert: 'Suggestion {index} of {count}: inserts "{newText}"',
23
- aiCurrentSuggestionDelete: 'Suggestion {index} of {count}: removes "{oldText}"',
24
- };
25
- /**
26
- * @internal Supplies the owning `mlv-editor`'s `aiProvider` input signal to
27
- * the per-editor AI context. The input wins over `MLV_EDITOR_AI_PROVIDER`,
28
- * mirroring the image-uploader precedence rule. Deliberately excluded from
29
- * the public barrels.
30
- */
31
- export const MLV_EDITOR_AI_INPUT_PROVIDER = new InjectionToken('MLV_EDITOR_AI_INPUT_PROVIDER');
32
- /**
33
- * Per-editor AI command state provided by `mlv-editor`.
34
- *
35
- * The context owns provider resolution, request shaping, and the streaming
36
- * and review session lifecycles; the framework-free engines
37
- * ({@link runMlvEditorAiStream}, {@link applyMlvEditorAiSuggestions}) own
38
- * document mutation. Every mutation is
39
- * gated by the toolbar context's guard, so readonly and disabled editors
40
- * refuse to start AI work — controls stay visible while a provider exists
41
- * (presence) and disable with the editor (executability), matching the
42
- * toolbar's presence-vs-executability rule.
43
- *
44
- * Failures never throw or reject: they surface as recoverable
45
- * {@link MlvEditorError} emissions through the owning editor's `editorError`
46
- * output. Cancellation stays silent.
47
- */
48
- export class MlvEditorAiContext {
49
- /** @private Guarded command surface and error channel of the owning editor. */
50
- _toolbar = inject(MLV_EDITOR_TOOLBAR_CONTEXT);
51
- /** @private Application-level provider used when the input supplies none. */
52
- _injectedProvider = inject(MLV_EDITOR_AI_PROVIDER, {
53
- optional: true,
54
- });
55
- /** @private The owning editor's `aiProvider` input; wins over the token. */
56
- _inputProvider = inject(MLV_EDITOR_AI_INPUT_PROVIDER, {
57
- optional: true,
58
- });
59
- /** @private Announces streaming lifecycle politely; decorations are aria-hidden. */
60
- _liveAnnouncer = inject(LiveAnnouncer);
61
- /** @private Optional translated announcement strings. */
62
- _i18n = inject(MLV_EDITOR_I18N, { optional: true });
63
- /** @private Resolves the ICU review-count announcement template. */
64
- _resolver = inject(MlvI18nResolverService);
65
- /** @private Writable lifecycle state behind the readonly public signal. */
66
- _status = signal('idle', /* @ts-ignore */
67
- ...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
68
- /** @private Writable pending-suggestion projection behind the readonly signal. */
69
- _suggestions = signal([], /* @ts-ignore */
70
- ...(ngDevMode ? [{ debugName: "_suggestions" }] : /* istanbul ignore next */ []));
71
- /**
72
- * @private Latest streaming session. Retained after settlement so
73
- * {@link restoreCheckpoint} can undo a committed transform; replaced when
74
- * the next transform starts.
75
- */
76
- _session = null;
77
- /** @private Abort control for the in-flight provider request, if any. */
78
- _abortController = null;
79
- /**
80
- * @private Active review wiring: the engine session plus the teardown that
81
- * detaches the editor `transaction`/`destroy` listeners mirroring it into
82
- * {@link suggestions}. `null` outside `'reviewing'`.
83
- */
84
- _review = null;
85
- /**
86
- * @private Last suggestion id {@link revealSuggestion} announced. Repeated
87
- * reveals of the same suggestion (the review bar re-runs its outline effect
88
- * on unrelated signal updates) must not re-announce it.
89
- */
90
- _revealedId = null;
91
- /** Lifecycle of the AI surface for this editor. */
92
- status = this._status.asReadonly();
93
- /**
94
- * Pending suggestions of the active review session, in document order.
95
- * Empty outside `'reviewing'`. Refreshed after every editor transaction, so
96
- * accept/reject calls, intersecting user edits, and an undo of the
97
- * application are all reflected immediately. Document ranges deliberately
98
- * stay internal — they remap on every transaction and would be stale the
99
- * moment a consumer stored them.
100
- */
101
- suggestions = this._suggestions.asReadonly();
102
- /**
103
- * Whether a review session with unresolved suggestions is active. Hosts
104
- * gate saves on this: during `'reviewing'` the document serializes in its
105
- * accepted-by-default shape (inserted text present, removed text absent).
106
- */
107
- hasPendingSuggestions = computed(() => this._suggestions().length > 0, /* @ts-ignore */
108
- ...(ngDevMode ? [{ debugName: "hasPendingSuggestions" }] : /* istanbul ignore next */ []));
109
- /**
110
- * Whether an AI provider is resolvable for this editor, either through the
111
- * `aiProvider` input or `MLV_EDITOR_AI_PROVIDER`. AI UI modules hide
112
- * entirely without a provider.
113
- */
114
- hasProvider = computed(() => this._resolveProvider() !== null, /* @ts-ignore */
115
- ...(ngDevMode ? [{ debugName: "hasProvider" }] : /* istanbul ignore next */ []));
116
- /**
117
- * Runs one selection transform through the resolved provider and lands the
118
- * result according to the output mode: `'replace-selection'` and
119
- * `'insert-below'` stream into the document, while `'review'` collects the
120
- * complete result without writing and then applies it over the selection
121
- * captured at start as reviewable tracked suggestions — status moves to
122
- * `'reviewing'` and the suggestion surface
123
- * ({@link suggestions}, {@link acceptSuggestion}, …) takes over. A review
124
- * result identical to the region applies nothing: status returns straight
125
- * to `'idle'` with a polite no-changes announcement and no error.
126
- *
127
- * The returned promise resolves after the session settles and never
128
- * rejects; every failure is reported as a recoverable error through the
129
- * editor's `editorError` output instead:
130
- *
131
- * - a transform already running, or a review still pending
132
- * (`configuration`) — concurrent calls are refused rather than queued,
133
- * so one editor never hosts two writers and pending suggestions are
134
- * never silently replaced;
135
- * - a readonly, disabled, or absent editor (`configuration`) — the toolbar
136
- * guard refuses to start, and a `'review'` run re-checks the same guard
137
- * after collection: an editor that became readonly or disabled mid-run
138
- * refuses to apply the collected suggestions (checkpoint untouched — a
139
- * review collection never wrote);
140
- * - no resolvable provider (`configuration`);
141
- * - a provider that throws or whose stream fails (`'ai-transport'`) — the
142
- * checkpoint is restored;
143
- * - unusable output (`'ai-result'`) — the checkpoint is restored. Both
144
- * restore paths are no-ops for `'review'`, which never wrote.
145
- *
146
- * Start, successful finish, and cancellation are announced through the CDK
147
- * `LiveAnnouncer` at `'polite'`; a successful `'review'` run announces the
148
- * pending suggestion count instead of the generic finish. A failure
149
- * announces nothing because the error channel already reports it.
150
- *
151
- * @param kind Transform requested from the provider.
152
- * @param options Optional instruction and output mode.
153
- */
154
- async runTransform(kind, options = {}) {
155
- if (this._status() !== 'idle') {
156
- this._reportConfiguration(this._status() === 'reviewing'
157
- ? 'An AI review is pending for this editor: resolve the suggestions before starting another transform.'
158
- : 'An AI transform is already running for this editor.');
159
- return;
160
- }
161
- if (!this._toolbar.can(() => true)) {
162
- this._reportConfiguration('AI transforms require an editable editor: readonly and disabled editors refuse to start.');
163
- return;
164
- }
165
- const editor = this._toolbar.editor();
166
- if (!editor)
167
- return;
168
- const provider = this._resolveProvider();
169
- if (!provider) {
170
- this._reportConfiguration('No AI provider is configured. Supply the aiProvider input or provide MLV_EDITOR_AI_PROVIDER.');
171
- return;
172
- }
173
- const output = options.output ?? 'replace-selection';
174
- // Review applies over the selection captured at start. External edits
175
- // abandon the collection session, so a committed run still sees exactly
176
- // this document region.
177
- const reviewFrom = editor.state.selection.from;
178
- const reviewTo = editor.state.selection.to;
179
- const controller = new AbortController();
180
- const request = {
181
- kind,
182
- ...(options.instruction === undefined
183
- ? {}
184
- : { instruction: options.instruction }),
185
- context: {
186
- selection: this._serializeSelection(editor),
187
- document: this._serializeDocument(editor),
188
- },
189
- signal: controller.signal,
190
- };
191
- let chunks;
192
- try {
193
- chunks = provider.stream(request);
194
- }
195
- catch (cause) {
196
- this._toolbar.reportError({
197
- code: 'ai-transport',
198
- message: 'The AI provider failed to start streaming.',
199
- recoverable: true,
200
- cause,
201
- });
202
- return;
203
- }
204
- this._abortController = controller;
205
- this._status.set('running');
206
- this._announce('aiStreamingStarted', 'AI generation started');
207
- const session = runMlvEditorAiStream(editor, { chunks, output });
208
- this._session = session;
209
- const result = await session.done;
210
- if (this._abortController === controller)
211
- this._abortController = null;
212
- // Cancellation, abandonment, and failure all release the provider: the
213
- // request signal aborts so a still-producing transport stops.
214
- if (result.status !== 'committed')
215
- controller.abort();
216
- switch (result.status) {
217
- case 'committed':
218
- if (output === 'review') {
219
- // The guard passed at start, but the editor may have become
220
- // readonly or disabled while the result was collected. Applying
221
- // suggestions is a document mutation, so it must re-pass the
222
- // toolbar guard here; on refusal the checkpoint is untouched — a
223
- // review collection never wrote.
224
- if (!this._toolbar.can(() => true)) {
225
- this._status.set('idle');
226
- this._reportConfiguration('The editor became readonly or disabled while the AI review result was collected: the suggestions were not applied.');
227
- break;
228
- }
229
- this._startReview(editor, reviewFrom, reviewTo, result.text);
230
- }
231
- else {
232
- this._status.set('idle');
233
- this._announce('aiStreamingFinished', 'AI generation finished');
234
- }
235
- break;
236
- case 'cancelled':
237
- case 'abandoned':
238
- this._status.set('idle');
239
- this._announce('aiStreamingCancelled', 'AI generation cancelled');
240
- break;
241
- case 'failed':
242
- this._status.set('idle');
243
- this._toolbar.reportError({
244
- code: result.error ?? 'ai-result',
245
- message: result.error === 'ai-transport'
246
- ? 'The AI provider stream failed.'
247
- : 'The AI provider produced no usable output.',
248
- recoverable: true,
249
- });
250
- break;
251
- }
252
- }
253
- /**
254
- * Aborts the in-flight request and rolls the document back to the
255
- * checkpoint captured when the transform started.
256
- *
257
- * The provider's `AbortSignal` aborts, the streaming session restores the
258
- * checkpoint, and the transform resolves silently — no error is emitted
259
- * and only the cancellation announcement is made. A no-op while idle.
260
- */
261
- cancel() {
262
- this._abortController?.abort();
263
- this._abortController = null;
264
- this._session?.cancel();
265
- }
266
- /**
267
- * Restores the document to the checkpoint of the most recent transform.
268
- *
269
- * While a transform is running this behaves exactly like {@link cancel}.
270
- * After a committed transform it reverts the commit in one history-visible
271
- * step, so the restore is itself undoable. While `'reviewing'`, restoring
272
- * reverts the applied suggestions wholesale, which drops the suggestion
273
- * set and ends the review. The restore is a document mutation and
274
- * therefore passes the toolbar guard: readonly and disabled editors refuse
275
- * it. A no-op when no transform has run.
276
- */
277
- restoreCheckpoint() {
278
- if (this._status() === 'running') {
279
- this.cancel();
280
- return;
281
- }
282
- const session = this._session;
283
- if (!session)
284
- return;
285
- this._toolbar.run(() => session.restoreCheckpoint());
286
- }
287
- /**
288
- * Accepts one pending suggestion: the applied change stays in the document
289
- * and its decorations drop. Delegates to the review engine — a pure
290
- * plugin-state update that creates no history entry. Announces the
291
- * acceptance politely; when the last suggestion resolves the review ends
292
- * and {@link status} returns to `'idle'`.
293
- *
294
- * Refused silently — decorations stay visible, nothing changes — while no
295
- * review is active, the id is unknown, or the editor is readonly or
296
- * disabled (the same guard rule that blocks every other AI mutation).
297
- *
298
- * @param id Identifier from {@link suggestions}.
299
- */
300
- acceptSuggestion(id) {
301
- const session = this._review?.session;
302
- if (!session || !this._toolbar.can(() => true))
303
- return;
304
- if (session.accept(id)) {
305
- this._announce('aiSuggestionAccepted', 'AI suggestion accepted');
306
- }
307
- }
308
- /**
309
- * Rejects one pending suggestion: the original content is restored over
310
- * the suggestion's range in exactly one history-visible step and its
311
- * decorations drop. Announces the rejection politely; when the last
312
- * suggestion resolves the review ends and {@link status} returns to
313
- * `'idle'`.
314
- *
315
- * Refused silently while no review is active, the id is unknown, or the
316
- * editor is readonly or disabled — the restore is a document mutation and
317
- * runs through the toolbar guard.
318
- *
319
- * @param id Identifier from {@link suggestions}.
320
- */
321
- rejectSuggestion(id) {
322
- const session = this._review?.session;
323
- if (!session)
324
- return;
325
- if (this._toolbar.run(() => session.reject(id))) {
326
- this._announce('aiSuggestionRejected', 'AI suggestion rejected');
327
- }
328
- }
329
- /**
330
- * Accepts every pending suggestion in one plugin-state update — no
331
- * document step, no history entry — then ends the review and returns
332
- * {@link status} to `'idle'`. Announced politely. Refused silently while
333
- * no review is active or the editor is readonly or disabled.
334
- */
335
- acceptAll() {
336
- const session = this._review?.session;
337
- if (!session || !this._toolbar.can(() => true))
338
- return;
339
- if (session.acceptAll()) {
340
- this._announce('aiAllSuggestionsAccepted', 'All AI suggestions accepted');
341
- }
342
- }
343
- /**
344
- * Rejects every pending suggestion in exactly one history-visible step,
345
- * restoring the original content of the whole reviewed region, then ends
346
- * the review and returns {@link status} to `'idle'`. Announced politely.
347
- * Refused silently while no review is active or the editor is readonly or
348
- * disabled — the restore is a document mutation and runs through the
349
- * toolbar guard.
350
- */
351
- rejectAll() {
352
- const session = this._review?.session;
353
- if (!session)
354
- return;
355
- if (this._toolbar.run(() => session.rejectAll())) {
356
- this._announce('aiAllSuggestionsRejected', 'All AI suggestions rejected');
357
- }
358
- }
359
- /**
360
- * Outlines one pending suggestion as the current one under review and
361
- * scrolls its document range into view; `null` clears the outline (review
362
- * surfaces call that on teardown so no marker strands).
363
- *
364
- * The outline decorations are `aria-hidden`, so revealing a suggestion also
365
- * announces its {@link describeSuggestion} description politely — index,
366
- * count, and the removed/added text — the non-visual equivalent the spec
367
- * requires. Each suggestion is announced once per reveal: repeating the
368
- * current id moves nothing and re-announces nothing.
369
- *
370
- * Otherwise a pure view affordance: the outline is a decoration-only
371
- * plugin-state update, so no document step and no history entry is created
372
- * — which is why, unlike accept/reject, it stays available in readonly
373
- * editors, whose pending reviews remain visible and navigable. Refused
374
- * silently while no review is active or the id is unknown. The review bar
375
- * drives this from its navigation cursor.
376
- *
377
- * @param id Identifier from {@link suggestions}, or `null` to clear.
378
- */
379
- revealSuggestion(id) {
380
- const session = this._review?.session;
381
- if (!session || !session.setCurrent(id))
382
- return;
383
- if (id === null) {
384
- this._revealedId = null;
385
- return;
386
- }
387
- const editor = this._toolbar.editor();
388
- if (editor) {
389
- const target = session
390
- .suggestions()
391
- .find((suggestion) => suggestion.id === id);
392
- if (target)
393
- this._scrollPositionIntoView(editor, target.range.from);
394
- }
395
- if (this._revealedId !== id) {
396
- this._revealedId = id;
397
- const description = this.describeSuggestion(id);
398
- if (description) {
399
- void this._liveAnnouncer.announce(description, 'polite');
400
- }
401
- }
402
- }
403
- /**
404
- * Human-readable description of one pending suggestion — its one-based
405
- * position in the pending list plus what it replaces, inserts, or removes —
406
- * resolved through the `aiCurrentSuggestionReplace`/`-Insert`/`-Delete`
407
- * ICU templates.
408
- *
409
- * This is the non-visual equivalent of the `aria-hidden` suggestion
410
- * decorations: {@link revealSuggestion} announces it, and the review bar
411
- * additionally exposes it to its accept/reject controls through
412
- * `aria-describedby`, so the change under decision is inspectable without
413
- * sight of the decorations. Returns `null` while no review is active or
414
- * for an unknown id.
415
- *
416
- * @param id Identifier from {@link suggestions}.
417
- */
418
- describeSuggestion(id) {
419
- const pending = this._suggestions();
420
- const index = pending.findIndex((suggestion) => suggestion.id === id);
421
- if (index < 0)
422
- return null;
423
- const suggestion = pending[index];
424
- const key = suggestion.kind === 'insert'
425
- ? 'aiCurrentSuggestionInsert'
426
- : suggestion.kind === 'delete'
427
- ? 'aiCurrentSuggestionDelete'
428
- : 'aiCurrentSuggestionReplace';
429
- const template = this._i18n?.()[key] ?? CURRENT_SUGGESTION_FALLBACKS[key];
430
- return this._resolver.resolve({ [key]: template }, key, {
431
- index: index + 1,
432
- count: pending.length,
433
- oldText: suggestion.oldText,
434
- newText: suggestion.newText,
435
- });
436
- }
437
- /**
438
- * @private Scrolls the rendered element at one document position into view.
439
- * Best effort: position resolution can fail transiently mid-transaction and
440
- * jsdom offers no `scrollIntoView`, so failure never fails the reveal.
441
- */
442
- _scrollPositionIntoView(editor, position) {
443
- try {
444
- const { node } = editor.view.domAtPos(position);
445
- const element = node instanceof HTMLElement ? node : node.parentElement;
446
- if (element && typeof element.scrollIntoView === 'function') {
447
- element.scrollIntoView({ block: 'nearest' });
448
- }
449
- }
450
- catch {
451
- // The outline already moved; scrolling is a progressive enhancement.
452
- }
453
- }
454
- /**
455
- * @private Routes a committed review collection through the suggestion
456
- * engine. An engine refusal — the replacement is identical to the region,
457
- * or otherwise nothing reviewable results — is the no-changes case: status
458
- * returns to `'idle'` with a polite announcement, no error, and no
459
- * document change. Otherwise the context enters `'reviewing'`, mirrors the
460
- * engine's suggestion set into {@link suggestions} after every editor
461
- * transaction (the session has no change notification of its own), and
462
- * announces the pending count.
463
- */
464
- _startReview(editor, from, to, replacementMarkdown) {
465
- const session = applyMlvEditorAiSuggestions(editor, {
466
- from,
467
- to,
468
- replacementMarkdown,
469
- });
470
- if (!session) {
471
- this._status.set('idle');
472
- this._announce('aiReviewNoChanges', 'The AI suggested no changes');
473
- return;
474
- }
475
- const synchronize = () => {
476
- if (this._review?.session !== session)
477
- return;
478
- const pending = this._projectSuggestions(session);
479
- this._suggestions.set(pending);
480
- if (pending.length === 0)
481
- this._endReview();
482
- };
483
- const onDestroy = () => {
484
- if (this._review?.session === session)
485
- this._endReview();
486
- };
487
- editor.on('transaction', synchronize);
488
- editor.on('destroy', onDestroy);
489
- this._revealedId = null;
490
- this._review = {
491
- session,
492
- detach: () => {
493
- editor.off('transaction', synchronize);
494
- editor.off('destroy', onDestroy);
495
- },
496
- };
497
- const pending = this._projectSuggestions(session);
498
- this._suggestions.set(pending);
499
- if (pending.length === 0) {
500
- // Defensive: the engine returns null rather than an empty session.
501
- this._endReview();
502
- this._announce('aiReviewNoChanges', 'The AI suggested no changes');
503
- return;
504
- }
505
- this._status.set('reviewing');
506
- this._announceReviewStarted(pending.length);
507
- }
508
- /**
509
- * @private Ends the review session exactly once: detaches the editor
510
- * listeners, clears the suggestion projection, and returns to `'idle'`.
511
- * Runs when the suggestion set empties — accept/reject calls, intersecting
512
- * edits dropping the last suggestion, or an undo of the application — and
513
- * when the editor is destroyed.
514
- */
515
- _endReview() {
516
- const review = this._review;
517
- if (!review)
518
- return;
519
- this._review = null;
520
- this._revealedId = null;
521
- review.detach();
522
- this._suggestions.set([]);
523
- this._status.set('idle');
524
- }
525
- /** @private Projects engine suggestions into the range-free public shape. */
526
- _projectSuggestions(session) {
527
- return session.suggestions().map(({ id, kind, oldText, newText }) => ({
528
- id,
529
- kind,
530
- oldText,
531
- newText,
532
- }));
533
- }
534
- /** @private Announces the pending review count politely through the ICU template. */
535
- _announceReviewStarted(count) {
536
- const template = this._i18n?.().aiReviewStarted ?? REVIEW_STARTED_FALLBACK;
537
- void this._liveAnnouncer.announce(this._resolver.resolve({ aiReviewStarted: template }, 'aiReviewStarted', {
538
- count,
539
- }), 'polite');
540
- }
541
- /** @private Resolves the active provider; the input wins over the token. */
542
- _resolveProvider() {
543
- return this._inputProvider?.() ?? this._injectedProvider ?? null;
544
- }
545
- /** @private Reports a recoverable configuration refusal through the editor. */
546
- _reportConfiguration(message) {
547
- this._toolbar.reportError({
548
- code: 'configuration',
549
- message,
550
- recoverable: true,
551
- });
552
- }
553
- /** @private Announces one localized AI lifecycle message politely. */
554
- _announce(key, fallback) {
555
- void this._liveAnnouncer.announce(this._i18n?.()[key] ?? fallback, 'polite');
556
- }
557
- /**
558
- * @private Serializes the current selection as Markdown, or `null` when the
559
- * selection is empty. Uses the editor's Markdown manager — the same
560
- * serialization path as `getMarkdown()` — over the selection sliced into
561
- * its own document; a replacement extension set without Markdown falls back
562
- * to plain text so providers still receive usable context.
563
- */
564
- _serializeSelection(editor) {
565
- const { from, to } = editor.state.selection;
566
- if (from === to)
567
- return null;
568
- const manager = editor.markdown;
569
- if (manager) {
570
- try {
571
- const markdown = manager.serialize(editor.state.doc.cut(from, to).toJSON());
572
- if (markdown.length > 0)
573
- return markdown;
574
- }
575
- catch {
576
- // Fall back to plain text below.
577
- }
578
- }
579
- const text = editor.state.doc.textBetween(from, to, '\n\n');
580
- return text.length > 0 ? text : null;
581
- }
582
- /**
583
- * @private Serializes the whole document as Markdown, or `null` when the
584
- * document is semantically empty. The document window is currently the full
585
- * document; a bounded window is a later refinement. Falls back to plain
586
- * text when the extension set carries no Markdown manager.
587
- */
588
- _serializeDocument(editor) {
589
- if (editor.isEmpty)
590
- return null;
591
- if (typeof editor.getMarkdown === 'function' && editor.markdown) {
592
- try {
593
- const markdown = editor.getMarkdown();
594
- if (markdown.length > 0)
595
- return markdown;
596
- }
597
- catch {
598
- // Fall back to plain text below.
599
- }
600
- }
601
- const text = editor.getText();
602
- return text.length > 0 ? text : null;
603
- }
604
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MlvEditorAiContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
605
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MlvEditorAiContext });
606
- }
607
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MlvEditorAiContext, decorators: [{
608
- type: Injectable
609
- }] });
610
- /** Injects the AI command state belonging to the nearest `mlv-editor`. */
611
- export const MLV_EDITOR_AI_CONTEXT = new InjectionToken('MLV_EDITOR_AI_CONTEXT');
612
- //# sourceMappingURL=editor-ai-context.js.map