@apollohg/react-native-prose-editor 0.1.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 (47) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +143 -0
  3. package/android/build.gradle +39 -0
  4. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  5. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  6. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +131 -0
  7. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
  8. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  9. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
  10. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
  11. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
  12. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
  13. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
  14. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
  15. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
  16. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
  17. package/expo-module.config.json +9 -0
  18. package/ios/EditorAddons.swift +228 -0
  19. package/ios/EditorCore.xcframework/Info.plist +44 -0
  20. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  21. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  22. package/ios/EditorLayoutManager.swift +254 -0
  23. package/ios/EditorTheme.swift +372 -0
  24. package/ios/Generated_editor_core.swift +1143 -0
  25. package/ios/NativeEditorExpoView.swift +1417 -0
  26. package/ios/NativeEditorModule.swift +263 -0
  27. package/ios/PositionBridge.swift +278 -0
  28. package/ios/ReactNativeProseEditor.podspec +49 -0
  29. package/ios/RenderBridge.swift +825 -0
  30. package/ios/RichTextEditorView.swift +1559 -0
  31. package/ios/editor_coreFFI/editor_coreFFI.h +1014 -0
  32. package/ios/editor_coreFFI/module.modulemap +7 -0
  33. package/ios/editor_coreFFI.h +904 -0
  34. package/ios/editor_coreFFI.modulemap +7 -0
  35. package/package.json +66 -0
  36. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  37. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  38. package/rust/android/x86_64/libeditor_core.so +0 -0
  39. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2014 -0
  40. package/src/EditorTheme.ts +130 -0
  41. package/src/EditorToolbar.tsx +620 -0
  42. package/src/NativeEditorBridge.ts +607 -0
  43. package/src/NativeRichTextEditor.tsx +951 -0
  44. package/src/addons.ts +158 -0
  45. package/src/index.ts +63 -0
  46. package/src/schemas.ts +153 -0
  47. package/src/useNativeEditor.ts +173 -0
@@ -0,0 +1,607 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+
3
+ const ERR_DESTROYED = 'NativeEditorBridge: editor has been destroyed';
4
+ const ERR_NATIVE_RESPONSE = 'NativeEditorBridge: invalid JSON response from native module';
5
+
6
+ export interface NativeEditorModule {
7
+ editorCreate(configJson: string): number;
8
+ editorDestroy(editorId: number): void;
9
+ editorSetHtml(editorId: number, html: string): string;
10
+ editorGetHtml(editorId: number): string;
11
+ editorSetJson(editorId: number, json: string): string;
12
+ editorGetJson(editorId: number): string;
13
+ editorReplaceHtml(editorId: number, html: string): string;
14
+ editorReplaceJson(editorId: number, json: string): string;
15
+ editorInsertText(editorId: number, pos: number, text: string): string;
16
+ editorReplaceSelectionText(editorId: number, text: string): string;
17
+ editorDeleteRange(editorId: number, from: number, to: number): string;
18
+ editorSplitBlock(editorId: number, pos: number): string;
19
+ editorInsertContentHtml(editorId: number, html: string): string;
20
+ editorInsertContentJson(editorId: number, json: string): string;
21
+ editorInsertContentJsonAtSelectionScalar(
22
+ editorId: number,
23
+ scalarAnchor: number,
24
+ scalarHead: number,
25
+ json: string
26
+ ): string;
27
+ editorToggleMark(editorId: number, markName: string): string;
28
+ editorSetSelection(editorId: number, anchor: number, head: number): void;
29
+ editorGetSelection(editorId: number): string;
30
+ editorGetCurrentState(editorId: number): string;
31
+ // Scalar-position APIs (used by native views internally)
32
+ editorInsertTextScalar(editorId: number, scalarPos: number, text: string): string;
33
+ editorDeleteScalarRange(editorId: number, scalarFrom: number, scalarTo: number): string;
34
+ editorReplaceTextScalar(editorId: number, scalarFrom: number, scalarTo: number, text: string): string;
35
+ editorSplitBlockScalar(editorId: number, scalarPos: number): string;
36
+ editorDeleteAndSplitScalar(editorId: number, scalarFrom: number, scalarTo: number): string;
37
+ editorSetSelectionScalar(editorId: number, scalarAnchor: number, scalarHead: number): void;
38
+ editorToggleMarkAtSelectionScalar(
39
+ editorId: number,
40
+ scalarAnchor: number,
41
+ scalarHead: number,
42
+ markName: string
43
+ ): string;
44
+ editorWrapInListAtSelectionScalar(
45
+ editorId: number,
46
+ scalarAnchor: number,
47
+ scalarHead: number,
48
+ listType: string
49
+ ): string;
50
+ editorUnwrapFromListAtSelectionScalar(
51
+ editorId: number,
52
+ scalarAnchor: number,
53
+ scalarHead: number
54
+ ): string;
55
+ editorIndentListItemAtSelectionScalar(
56
+ editorId: number,
57
+ scalarAnchor: number,
58
+ scalarHead: number
59
+ ): string;
60
+ editorOutdentListItemAtSelectionScalar(
61
+ editorId: number,
62
+ scalarAnchor: number,
63
+ scalarHead: number
64
+ ): string;
65
+ editorInsertNodeAtSelectionScalar(
66
+ editorId: number,
67
+ scalarAnchor: number,
68
+ scalarHead: number,
69
+ nodeType: string
70
+ ): string;
71
+ editorDocToScalar(editorId: number, docPos: number): number;
72
+ editorScalarToDoc(editorId: number, scalar: number): number;
73
+ editorWrapInList(editorId: number, listType: string): string;
74
+ editorUnwrapFromList(editorId: number): string;
75
+ editorIndentListItem(editorId: number): string;
76
+ editorOutdentListItem(editorId: number): string;
77
+ editorInsertNode(editorId: number, nodeType: string): string;
78
+ editorUndo(editorId: number): string;
79
+ editorRedo(editorId: number): string;
80
+ editorCanUndo(editorId: number): boolean;
81
+ editorCanRedo(editorId: number): boolean;
82
+ }
83
+
84
+ export interface Selection {
85
+ type: 'text' | 'node' | 'all';
86
+ anchor?: number;
87
+ head?: number;
88
+ pos?: number;
89
+ }
90
+
91
+ export interface ListContext {
92
+ ordered: boolean;
93
+ index: number;
94
+ total: number;
95
+ start: number;
96
+ isFirst: boolean;
97
+ isLast: boolean;
98
+ }
99
+
100
+ export interface RenderElement {
101
+ type:
102
+ | 'textRun'
103
+ | 'blockStart'
104
+ | 'blockEnd'
105
+ | 'voidInline'
106
+ | 'voidBlock'
107
+ | 'opaqueInlineAtom'
108
+ | 'opaqueBlockAtom';
109
+ text?: string;
110
+ marks?: string[];
111
+ nodeType?: string;
112
+ depth?: number;
113
+ docPos?: number;
114
+ label?: string;
115
+ listContext?: ListContext;
116
+ }
117
+
118
+ export interface ActiveState {
119
+ marks: Record<string, boolean>;
120
+ nodes: Record<string, boolean>;
121
+ commands: Record<string, boolean>;
122
+ allowedMarks: string[];
123
+ insertableNodes: string[];
124
+ }
125
+
126
+ export interface HistoryState {
127
+ canUndo: boolean;
128
+ canRedo: boolean;
129
+ }
130
+
131
+ export interface EditorUpdate {
132
+ renderElements: RenderElement[];
133
+ selection: Selection;
134
+ activeState: ActiveState;
135
+ historyState: HistoryState;
136
+ }
137
+
138
+ export interface DocumentJSON {
139
+ [key: string]: unknown;
140
+ }
141
+
142
+ export function normalizeActiveState(raw: unknown): ActiveState {
143
+ const obj = (raw as Record<string, unknown>) ?? {};
144
+ return {
145
+ marks: (obj.marks ?? {}) as Record<string, boolean>,
146
+ nodes: (obj.nodes ?? {}) as Record<string, boolean>,
147
+ commands: (obj.commands ?? {}) as Record<string, boolean>,
148
+ allowedMarks: (obj.allowedMarks ?? []) as string[],
149
+ insertableNodes: (obj.insertableNodes ?? []) as string[],
150
+ };
151
+ }
152
+
153
+ function parseRenderElements(json: string): RenderElement[] {
154
+ if (!json || json === '[]') return [];
155
+ try {
156
+ const parsed: unknown = JSON.parse(json);
157
+ if (
158
+ parsed != null &&
159
+ typeof parsed === 'object' &&
160
+ !Array.isArray(parsed) &&
161
+ 'error' in parsed
162
+ ) {
163
+ throw new Error(`NativeEditorBridge: ${(parsed as { error: unknown }).error}`);
164
+ }
165
+ if (!Array.isArray(parsed)) {
166
+ throw new Error(ERR_NATIVE_RESPONSE);
167
+ }
168
+ return parsed as RenderElement[];
169
+ } catch (e) {
170
+ if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
171
+ throw e;
172
+ }
173
+ throw new Error(ERR_NATIVE_RESPONSE);
174
+ }
175
+ }
176
+
177
+ export function parseEditorUpdateJson(json: string): EditorUpdate | null {
178
+ if (!json || json === '') return null;
179
+ try {
180
+ const parsed = JSON.parse(json) as Record<string, unknown>;
181
+ if ('error' in parsed) {
182
+ throw new Error(`NativeEditorBridge: ${parsed.error}`);
183
+ }
184
+ return {
185
+ renderElements: (parsed.renderElements ?? []) as RenderElement[],
186
+ selection: (parsed.selection ?? { type: 'text', anchor: 0, head: 0 }) as Selection,
187
+ activeState: normalizeActiveState(parsed.activeState),
188
+ historyState: (parsed.historyState ?? {
189
+ canUndo: false,
190
+ canRedo: false,
191
+ }) as HistoryState,
192
+ };
193
+ } catch (e) {
194
+ if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
195
+ throw e;
196
+ }
197
+ throw new Error(ERR_NATIVE_RESPONSE);
198
+ }
199
+ }
200
+
201
+ function parseDocumentJSON(json: string): DocumentJSON {
202
+ if (!json || json === '{}') return {};
203
+ try {
204
+ const parsed = JSON.parse(json) as DocumentJSON;
205
+ if (
206
+ parsed != null &&
207
+ typeof parsed === 'object' &&
208
+ 'error' in (parsed as Record<string, unknown>)
209
+ ) {
210
+ throw new Error(
211
+ `NativeEditorBridge: ${(parsed as Record<string, unknown>).error}`
212
+ );
213
+ }
214
+ return parsed;
215
+ } catch (e) {
216
+ if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
217
+ throw e;
218
+ }
219
+ throw new Error(ERR_NATIVE_RESPONSE);
220
+ }
221
+ }
222
+
223
+ let _nativeModule: NativeEditorModule | null = null;
224
+
225
+ function getNativeModule(): NativeEditorModule {
226
+ if (!_nativeModule) {
227
+ _nativeModule = requireNativeModule<NativeEditorModule>('NativeEditor');
228
+ }
229
+ return _nativeModule;
230
+ }
231
+
232
+ /** @internal Reset the cached native module reference. For testing only. */
233
+ export function _resetNativeModuleCache(): void {
234
+ _nativeModule = null;
235
+ }
236
+
237
+ export class NativeEditorBridge {
238
+ private _editorId: number;
239
+ private _destroyed = false;
240
+ private _lastSelection: Selection = { type: 'text', anchor: 0, head: 0 };
241
+
242
+ private constructor(editorId: number) {
243
+ this._editorId = editorId;
244
+ }
245
+
246
+ /** Create a new editor instance backed by the Rust engine. */
247
+ static create(config?: { maxLength?: number; schemaJson?: string }): NativeEditorBridge {
248
+ const configObj: Record<string, unknown> = {};
249
+ if (config?.maxLength != null) configObj.maxLength = config.maxLength;
250
+ if (config?.schemaJson != null) {
251
+ try {
252
+ configObj.schema = JSON.parse(config.schemaJson);
253
+ } catch {
254
+ // Fall back to the default schema when the provided JSON is invalid.
255
+ }
256
+ }
257
+ const id = getNativeModule().editorCreate(JSON.stringify(configObj));
258
+ return new NativeEditorBridge(id);
259
+ }
260
+
261
+ /** The underlying native editor ID. */
262
+ get editorId(): number {
263
+ return this._editorId;
264
+ }
265
+
266
+ /** Whether this bridge has been destroyed. */
267
+ get isDestroyed(): boolean {
268
+ return this._destroyed;
269
+ }
270
+
271
+ /** Destroy the editor instance and free native resources. */
272
+ destroy(): void {
273
+ if (this._destroyed) return;
274
+ this._destroyed = true;
275
+ getNativeModule().editorDestroy(this._editorId);
276
+ }
277
+
278
+ /** Set content from HTML. Returns render elements for display. */
279
+ setHtml(html: string): RenderElement[] {
280
+ this.assertNotDestroyed();
281
+ const json = getNativeModule().editorSetHtml(this._editorId, html);
282
+ return parseRenderElements(json);
283
+ }
284
+
285
+ /** Get content as HTML. */
286
+ getHtml(): string {
287
+ this.assertNotDestroyed();
288
+ return getNativeModule().editorGetHtml(this._editorId);
289
+ }
290
+
291
+ /** Set content from ProseMirror JSON. Returns render elements. */
292
+ setJson(doc: DocumentJSON): RenderElement[] {
293
+ this.assertNotDestroyed();
294
+ const json = getNativeModule().editorSetJson(
295
+ this._editorId,
296
+ JSON.stringify(doc)
297
+ );
298
+ return parseRenderElements(json);
299
+ }
300
+
301
+ /** Get content as ProseMirror JSON. */
302
+ getJson(): DocumentJSON {
303
+ this.assertNotDestroyed();
304
+ const json = getNativeModule().editorGetJson(this._editorId);
305
+ return parseDocumentJSON(json);
306
+ }
307
+
308
+ /** Insert text at a document position. Returns the full update. */
309
+ insertText(pos: number, text: string): EditorUpdate | null {
310
+ this.assertNotDestroyed();
311
+ const json = getNativeModule().editorInsertText(this._editorId, pos, text);
312
+ const update = parseEditorUpdateJson(json);
313
+ if (update) this._lastSelection = update.selection;
314
+ return update;
315
+ }
316
+
317
+ /** Delete a range [from, to). Returns the full update. */
318
+ deleteRange(from: number, to: number): EditorUpdate | null {
319
+ this.assertNotDestroyed();
320
+ const json = getNativeModule().editorDeleteRange(this._editorId, from, to);
321
+ const update = parseEditorUpdateJson(json);
322
+ if (update) this._lastSelection = update.selection;
323
+ return update;
324
+ }
325
+
326
+ /** Replace the current selection with text atomically. */
327
+ replaceSelectionText(text: string): EditorUpdate | null {
328
+ this.assertNotDestroyed();
329
+ const json = getNativeModule().editorReplaceSelectionText(this._editorId, text);
330
+ const update = parseEditorUpdateJson(json);
331
+ if (update) this._lastSelection = update.selection;
332
+ return update;
333
+ }
334
+
335
+ /** Toggle a mark (bold, italic, etc.) on the current selection. */
336
+ toggleMark(markType: string): EditorUpdate | null {
337
+ this.assertNotDestroyed();
338
+ const scalarSelection = this.currentScalarSelection();
339
+ const json = scalarSelection
340
+ ? getNativeModule().editorToggleMarkAtSelectionScalar(
341
+ this._editorId,
342
+ scalarSelection.anchor,
343
+ scalarSelection.head,
344
+ markType
345
+ )
346
+ : getNativeModule().editorToggleMark(this._editorId, markType);
347
+ const update = parseEditorUpdateJson(json);
348
+ if (update) this._lastSelection = update.selection;
349
+ return update;
350
+ }
351
+
352
+ /** Set the document selection by anchor and head positions. */
353
+ setSelection(anchor: number, head: number): void {
354
+ this.assertNotDestroyed();
355
+ getNativeModule().editorSetSelection(this._editorId, anchor, head);
356
+ this._lastSelection = { type: 'text', anchor, head };
357
+ }
358
+
359
+ /** Get the current selection from the Rust engine (synchronous native call).
360
+ * Always returns the live selection, not a stale cache. */
361
+ getSelection(): Selection {
362
+ if (this._destroyed) return { type: 'text', anchor: 0, head: 0 };
363
+ try {
364
+ const json = getNativeModule().editorGetSelection(this._editorId);
365
+ const sel = JSON.parse(json) as Selection;
366
+ this._lastSelection = sel;
367
+ return sel;
368
+ } catch {
369
+ return this._lastSelection;
370
+ }
371
+ }
372
+
373
+ /** Update the cached selection from native events (scalar offsets).
374
+ * Called by the React component when native selection change events arrive. */
375
+ updateSelectionFromNative(anchor: number, head: number): void {
376
+ if (this._destroyed) return;
377
+ this._lastSelection = { type: 'text', anchor, head };
378
+ }
379
+
380
+ /** Get the current full state from Rust (render elements, selection, etc.). */
381
+ getCurrentState(): EditorUpdate | null {
382
+ this.assertNotDestroyed();
383
+ const json = getNativeModule().editorGetCurrentState(this._editorId);
384
+ const update = parseEditorUpdateJson(json);
385
+ if (update) this._lastSelection = update.selection;
386
+ return update;
387
+ }
388
+
389
+ /** Split the block at a position (Enter key). */
390
+ splitBlock(pos: number): EditorUpdate | null {
391
+ this.assertNotDestroyed();
392
+ const json = getNativeModule().editorSplitBlock(this._editorId, pos);
393
+ const update = parseEditorUpdateJson(json);
394
+ if (update) this._lastSelection = update.selection;
395
+ return update;
396
+ }
397
+
398
+ /** Insert HTML content at the current selection. */
399
+ insertContentHtml(html: string): EditorUpdate | null {
400
+ this.assertNotDestroyed();
401
+ const json = getNativeModule().editorInsertContentHtml(this._editorId, html);
402
+ const update = parseEditorUpdateJson(json);
403
+ if (update) this._lastSelection = update.selection;
404
+ return update;
405
+ }
406
+
407
+ /** Insert JSON content at the current selection. */
408
+ insertContentJson(doc: DocumentJSON): EditorUpdate | null {
409
+ this.assertNotDestroyed();
410
+ const json = getNativeModule().editorInsertContentJson(
411
+ this._editorId,
412
+ JSON.stringify(doc)
413
+ );
414
+ const update = parseEditorUpdateJson(json);
415
+ if (update) this._lastSelection = update.selection;
416
+ return update;
417
+ }
418
+
419
+ /** Insert JSON content at an explicit scalar selection. */
420
+ insertContentJsonAtSelectionScalar(
421
+ scalarAnchor: number,
422
+ scalarHead: number,
423
+ doc: DocumentJSON
424
+ ): EditorUpdate | null {
425
+ this.assertNotDestroyed();
426
+ const json = getNativeModule().editorInsertContentJsonAtSelectionScalar(
427
+ this._editorId,
428
+ scalarAnchor,
429
+ scalarHead,
430
+ JSON.stringify(doc)
431
+ );
432
+ const update = parseEditorUpdateJson(json);
433
+ if (update) this._lastSelection = update.selection;
434
+ return update;
435
+ }
436
+
437
+ /** Replace entire document with HTML via transaction (preserves undo history). */
438
+ replaceHtml(html: string): EditorUpdate | null {
439
+ this.assertNotDestroyed();
440
+ const json = getNativeModule().editorReplaceHtml(this._editorId, html);
441
+ const update = parseEditorUpdateJson(json);
442
+ if (update) this._lastSelection = update.selection;
443
+ return update;
444
+ }
445
+
446
+ /** Replace entire document with JSON via transaction (preserves undo history). */
447
+ replaceJson(doc: DocumentJSON): EditorUpdate | null {
448
+ this.assertNotDestroyed();
449
+ const json = getNativeModule().editorReplaceJson(
450
+ this._editorId,
451
+ JSON.stringify(doc)
452
+ );
453
+ const update = parseEditorUpdateJson(json);
454
+ if (update) this._lastSelection = update.selection;
455
+ return update;
456
+ }
457
+
458
+ /** Undo the last operation. Returns update or null if nothing to undo. */
459
+ undo(): EditorUpdate | null {
460
+ this.assertNotDestroyed();
461
+ const json = getNativeModule().editorUndo(this._editorId);
462
+ const update = parseEditorUpdateJson(json);
463
+ if (update) this._lastSelection = update.selection;
464
+ return update;
465
+ }
466
+
467
+ /** Redo the last undone operation. Returns update or null if nothing to redo. */
468
+ redo(): EditorUpdate | null {
469
+ this.assertNotDestroyed();
470
+ const json = getNativeModule().editorRedo(this._editorId);
471
+ const update = parseEditorUpdateJson(json);
472
+ if (update) this._lastSelection = update.selection;
473
+ return update;
474
+ }
475
+
476
+ /** Check if undo is available. */
477
+ canUndo(): boolean {
478
+ this.assertNotDestroyed();
479
+ return getNativeModule().editorCanUndo(this._editorId);
480
+ }
481
+
482
+ /** Check if redo is available. */
483
+ canRedo(): boolean {
484
+ this.assertNotDestroyed();
485
+ return getNativeModule().editorCanRedo(this._editorId);
486
+ }
487
+
488
+ /** Toggle a list type on the current selection. Wraps if not in list, unwraps if already in that list type. */
489
+ toggleList(listType: string): EditorUpdate | null {
490
+ this.assertNotDestroyed();
491
+ const isActive = this.getCurrentState()?.activeState?.nodes?.[listType] === true;
492
+ const scalarSelection = this.currentScalarSelection();
493
+
494
+ const json = isActive
495
+ ? scalarSelection
496
+ ? getNativeModule().editorUnwrapFromListAtSelectionScalar(
497
+ this._editorId,
498
+ scalarSelection.anchor,
499
+ scalarSelection.head
500
+ )
501
+ : getNativeModule().editorUnwrapFromList(this._editorId)
502
+ : scalarSelection
503
+ ? getNativeModule().editorWrapInListAtSelectionScalar(
504
+ this._editorId,
505
+ scalarSelection.anchor,
506
+ scalarSelection.head,
507
+ listType
508
+ )
509
+ : getNativeModule().editorWrapInList(this._editorId, listType);
510
+
511
+ const update = parseEditorUpdateJson(json);
512
+ if (update) this._lastSelection = update.selection;
513
+ return update;
514
+ }
515
+
516
+ /** Unwrap the current list item back to a paragraph. */
517
+ unwrapFromList(): EditorUpdate | null {
518
+ this.assertNotDestroyed();
519
+ const scalarSelection = this.currentScalarSelection();
520
+ const json = scalarSelection
521
+ ? getNativeModule().editorUnwrapFromListAtSelectionScalar(
522
+ this._editorId,
523
+ scalarSelection.anchor,
524
+ scalarSelection.head
525
+ )
526
+ : getNativeModule().editorUnwrapFromList(this._editorId);
527
+ const update = parseEditorUpdateJson(json);
528
+ if (update) this._lastSelection = update.selection;
529
+ return update;
530
+ }
531
+
532
+ /** Indent the current list item into a nested list. */
533
+ indentListItem(): EditorUpdate | null {
534
+ this.assertNotDestroyed();
535
+ const scalarSelection = this.currentScalarSelection();
536
+ const json = scalarSelection
537
+ ? getNativeModule().editorIndentListItemAtSelectionScalar(
538
+ this._editorId,
539
+ scalarSelection.anchor,
540
+ scalarSelection.head
541
+ )
542
+ : getNativeModule().editorIndentListItem(this._editorId);
543
+ const update = parseEditorUpdateJson(json);
544
+ if (update) this._lastSelection = update.selection;
545
+ return update;
546
+ }
547
+
548
+ /** Outdent the current list item to the parent list level. */
549
+ outdentListItem(): EditorUpdate | null {
550
+ this.assertNotDestroyed();
551
+ const scalarSelection = this.currentScalarSelection();
552
+ const json = scalarSelection
553
+ ? getNativeModule().editorOutdentListItemAtSelectionScalar(
554
+ this._editorId,
555
+ scalarSelection.anchor,
556
+ scalarSelection.head
557
+ )
558
+ : getNativeModule().editorOutdentListItem(this._editorId);
559
+ const update = parseEditorUpdateJson(json);
560
+ if (update) this._lastSelection = update.selection;
561
+ return update;
562
+ }
563
+
564
+ /** Insert a void node (e.g. 'horizontalRule') at the current selection. */
565
+ insertNode(nodeType: string): EditorUpdate | null {
566
+ this.assertNotDestroyed();
567
+ const scalarSelection = this.currentScalarSelection();
568
+ const json = scalarSelection
569
+ ? getNativeModule().editorInsertNodeAtSelectionScalar(
570
+ this._editorId,
571
+ scalarSelection.anchor,
572
+ scalarSelection.head,
573
+ nodeType
574
+ )
575
+ : getNativeModule().editorInsertNode(this._editorId, nodeType);
576
+ const update = parseEditorUpdateJson(json);
577
+ if (update) this._lastSelection = update.selection;
578
+ return update;
579
+ }
580
+
581
+ private assertNotDestroyed(): void {
582
+ if (this._destroyed) {
583
+ throw new Error(ERR_DESTROYED);
584
+ }
585
+ }
586
+
587
+ private currentScalarSelection(): { anchor: number; head: number } | null {
588
+ const selection = this._lastSelection;
589
+ const nativeModule = getNativeModule();
590
+
591
+ if (selection.type === 'text') {
592
+ const anchor = selection.anchor ?? 0;
593
+ const head = selection.head ?? anchor;
594
+ return {
595
+ anchor: nativeModule.editorDocToScalar(this._editorId, anchor),
596
+ head: nativeModule.editorDocToScalar(this._editorId, head),
597
+ };
598
+ }
599
+
600
+ if (selection.type === 'node' && typeof selection.pos === 'number') {
601
+ const scalar = nativeModule.editorDocToScalar(this._editorId, selection.pos);
602
+ return { anchor: scalar, head: scalar };
603
+ }
604
+
605
+ return null;
606
+ }
607
+ }