@apollohg/react-native-prose-editor 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +21 -2
  2. package/android/build.gradle +23 -0
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +502 -39
  4. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +56 -28
  5. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +6 -0
  6. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
  7. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
  8. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
  9. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
  10. package/dist/NativeEditorBridge.d.ts +37 -1
  11. package/dist/NativeEditorBridge.js +192 -97
  12. package/dist/NativeRichTextEditor.d.ts +3 -2
  13. package/dist/NativeRichTextEditor.js +164 -56
  14. package/dist/YjsCollaboration.d.ts +2 -0
  15. package/dist/YjsCollaboration.js +142 -20
  16. package/dist/schemas.d.ts +2 -0
  17. package/dist/schemas.js +63 -0
  18. package/ios/EditorCore.xcframework/Info.plist +5 -5
  19. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  20. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  21. package/ios/EditorLayoutManager.swift +3 -3
  22. package/ios/Generated_editor_core.swift +41 -0
  23. package/ios/NativeEditorExpoView.swift +43 -11
  24. package/ios/NativeEditorModule.swift +6 -0
  25. package/ios/PositionBridge.swift +310 -75
  26. package/ios/RenderBridge.swift +362 -27
  27. package/ios/RichTextEditorView.swift +1983 -187
  28. package/ios/editor_coreFFI/editor_coreFFI.h +33 -0
  29. package/package.json +11 -2
  30. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  31. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  32. package/rust/android/x86_64/libeditor_core.so +0 -0
  33. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +63 -0
@@ -1,3 +1,4 @@
1
+ import { type SchemaDefinition } from './schemas';
1
2
  export interface NativeEditorModule {
2
3
  editorCreate(configJson: string): number;
3
4
  editorDestroy(editorId: number): void;
@@ -17,6 +18,7 @@ export interface NativeEditorModule {
17
18
  editorGetHtml(editorId: number): string;
18
19
  editorSetJson(editorId: number, json: string): string;
19
20
  editorGetJson(editorId: number): string;
21
+ editorGetContentSnapshot(editorId: number): string;
20
22
  editorReplaceHtml(editorId: number, html: string): string;
21
23
  editorReplaceJson(editorId: number, json: string): string;
22
24
  editorInsertText(editorId: number, pos: number, text: string): string;
@@ -33,6 +35,7 @@ export interface NativeEditorModule {
33
35
  editorToggleHeading(editorId: number, level: number): string;
34
36
  editorSetSelection(editorId: number, anchor: number, head: number): void;
35
37
  editorGetSelection(editorId: number): string;
38
+ editorGetSelectionState(editorId: number): string;
36
39
  editorGetCurrentState(editorId: number): string;
37
40
  editorInsertTextScalar(editorId: number, scalarPos: number, text: string): string;
38
41
  editorDeleteScalarRange(editorId: number, scalarFrom: number, scalarTo: number): string;
@@ -92,6 +95,11 @@ export interface RenderElement {
92
95
  attrs?: Record<string, unknown>;
93
96
  listContext?: ListContext;
94
97
  }
98
+ interface RenderBlocksPatch {
99
+ startIndex: number;
100
+ deleteCount: number;
101
+ renderBlocks: RenderElement[][];
102
+ }
95
103
  export interface ActiveState {
96
104
  marks: Record<string, boolean>;
97
105
  markAttrs: Record<string, Record<string, unknown>>;
@@ -106,9 +114,16 @@ export interface HistoryState {
106
114
  }
107
115
  export interface EditorUpdate {
108
116
  renderElements: RenderElement[];
117
+ renderBlocks?: RenderElement[][];
118
+ renderPatch?: RenderBlocksPatch;
109
119
  selection: Selection;
110
120
  activeState: ActiveState;
111
121
  historyState: HistoryState;
122
+ documentVersion?: number;
123
+ }
124
+ export interface ContentSnapshot {
125
+ html: string;
126
+ json: DocumentJSON;
112
127
  }
113
128
  export interface DocumentJSON {
114
129
  [key: string]: unknown;
@@ -127,7 +142,7 @@ export interface CollaborationResult {
127
142
  }
128
143
  export type EncodedCollaborationStateInput = Uint8Array | readonly number[] | string;
129
144
  export declare function normalizeActiveState(raw: unknown): ActiveState;
130
- export declare function parseEditorUpdateJson(json: string): EditorUpdate | null;
145
+ export declare function parseEditorUpdateJson(json: string, previousRenderBlocks?: RenderElement[][]): EditorUpdate | null;
131
146
  export declare function encodeCollaborationStateBase64(encodedState: EncodedCollaborationStateInput): string;
132
147
  export declare function decodeCollaborationStateBase64(base64: string): Uint8Array;
133
148
  export declare function parseCollaborationResultJson(json: string): CollaborationResult;
@@ -135,8 +150,13 @@ export declare function parseCollaborationResultJson(json: string): Collaboratio
135
150
  export declare function _resetNativeModuleCache(): void;
136
151
  export declare class NativeEditorBridge {
137
152
  private _editorId;
153
+ private _schema?;
138
154
  private _destroyed;
139
155
  private _lastSelection;
156
+ private _documentVersion;
157
+ private _cachedHtml;
158
+ private _cachedJsonString;
159
+ private _renderBlocksCache;
140
160
  private constructor();
141
161
  /** Create a new editor instance backed by the Rust engine. */
142
162
  static create(config?: {
@@ -156,8 +176,14 @@ export declare class NativeEditorBridge {
156
176
  getHtml(): string;
157
177
  /** Set content from ProseMirror JSON. Returns render elements. */
158
178
  setJson(doc: DocumentJSON): RenderElement[];
179
+ /** Set content from a serialized ProseMirror JSON string. Returns render elements. */
180
+ setJsonString(jsonString: string): RenderElement[];
181
+ /** Get content as raw ProseMirror JSON string. */
182
+ getJsonString(): string;
159
183
  /** Get content as ProseMirror JSON. */
160
184
  getJson(): DocumentJSON;
185
+ /** Get both HTML and JSON content in one native roundtrip. */
186
+ getContentSnapshot(): ContentSnapshot;
161
187
  /** Insert text at a document position. Returns the full update. */
162
188
  insertText(pos: number, text: string): EditorUpdate | null;
163
189
  /** Delete a range [from, to). Returns the full update. */
@@ -184,6 +210,8 @@ export declare class NativeEditorBridge {
184
210
  updateSelectionFromNative(anchor: number, head: number): void;
185
211
  /** Get the current full state from Rust (render elements, selection, etc.). */
186
212
  getCurrentState(): EditorUpdate | null;
213
+ /** Get the current selection-related state without render elements. */
214
+ getSelectionState(): EditorUpdate | null;
187
215
  /** Split the block at a position (Enter key). */
188
216
  splitBlock(pos: number): EditorUpdate | null;
189
217
  /** Insert HTML content at the current selection. */
@@ -196,6 +224,8 @@ export declare class NativeEditorBridge {
196
224
  replaceHtml(html: string): EditorUpdate | null;
197
225
  /** Replace entire document with JSON via transaction (preserves undo history). */
198
226
  replaceJson(doc: DocumentJSON): EditorUpdate | null;
227
+ /** Replace entire document with a serialized JSON transaction. */
228
+ replaceJsonString(jsonString: string): EditorUpdate | null;
199
229
  /** Undo the last operation. Returns update or null if nothing to undo. */
200
230
  undo(): EditorUpdate | null;
201
231
  /** Redo the last undone operation. Returns update or null if nothing to redo. */
@@ -214,6 +244,10 @@ export declare class NativeEditorBridge {
214
244
  outdentListItem(): EditorUpdate | null;
215
245
  /** Insert a void node (e.g. 'horizontalRule') at the current selection. */
216
246
  insertNode(nodeType: string): EditorUpdate | null;
247
+ parseUpdateJson(json: string): EditorUpdate | null;
248
+ private noteUpdate;
249
+ private parseAndNoteUpdate;
250
+ private invalidateContentCaches;
217
251
  private assertNotDestroyed;
218
252
  private currentScalarSelection;
219
253
  }
@@ -224,6 +258,7 @@ export declare class NativeCollaborationBridge {
224
258
  static create(config?: {
225
259
  clientId?: number;
226
260
  fragmentName?: string;
261
+ schema?: SchemaDefinition;
227
262
  initialDocumentJson?: DocumentJSON;
228
263
  initialEncodedState?: EncodedCollaborationStateInput;
229
264
  localAwareness?: Record<string, unknown>;
@@ -244,3 +279,4 @@ export declare class NativeCollaborationBridge {
244
279
  clearLocalAwareness(): CollaborationResult;
245
280
  private assertNotDestroyed;
246
281
  }
282
+ export {};
@@ -8,6 +8,7 @@ exports.decodeCollaborationStateBase64 = decodeCollaborationStateBase64;
8
8
  exports.parseCollaborationResultJson = parseCollaborationResultJson;
9
9
  exports._resetNativeModuleCache = _resetNativeModuleCache;
10
10
  const expo_modules_core_1 = require("expo-modules-core");
11
+ const schemas_1 = require("./schemas");
11
12
  const ERR_DESTROYED = 'NativeEditorBridge: editor has been destroyed';
12
13
  const ERR_NATIVE_RESPONSE = 'NativeEditorBridge: invalid JSON response from native module';
13
14
  const ERR_INVALID_ENCODED_STATE = 'NativeEditorBridge: invalid encoded collaboration state';
@@ -46,7 +47,7 @@ function parseRenderElements(json) {
46
47
  throw new Error(ERR_NATIVE_RESPONSE);
47
48
  }
48
49
  }
49
- function parseEditorUpdateJson(json) {
50
+ function parseEditorUpdateJson(json, previousRenderBlocks) {
50
51
  if (!json || json === '')
51
52
  return null;
52
53
  try {
@@ -54,14 +55,72 @@ function parseEditorUpdateJson(json) {
54
55
  if ('error' in parsed) {
55
56
  throw new Error(`NativeEditorBridge: ${parsed.error}`);
56
57
  }
58
+ const renderBlocks = Array.isArray(parsed.renderBlocks)
59
+ ? parsed.renderBlocks
60
+ : applyRenderBlocksPatch(previousRenderBlocks, parsed.renderPatch != null && typeof parsed.renderPatch === 'object'
61
+ ? parsed.renderPatch
62
+ : undefined);
63
+ const renderPatch = parsed.renderPatch != null && typeof parsed.renderPatch === 'object'
64
+ ? parsed.renderPatch
65
+ : undefined;
57
66
  return {
58
- renderElements: (parsed.renderElements ?? []),
67
+ renderElements: Array.isArray(parsed.renderElements)
68
+ ? parsed.renderElements
69
+ : flattenRenderBlocks(renderBlocks),
70
+ renderBlocks,
71
+ renderPatch,
59
72
  selection: (parsed.selection ?? { type: 'text', anchor: 0, head: 0 }),
60
73
  activeState: normalizeActiveState(parsed.activeState),
61
74
  historyState: (parsed.historyState ?? {
62
75
  canUndo: false,
63
76
  canRedo: false,
64
77
  }),
78
+ documentVersion: typeof parsed.documentVersion === 'number' ? parsed.documentVersion : undefined,
79
+ };
80
+ }
81
+ catch (e) {
82
+ if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
83
+ throw e;
84
+ }
85
+ throw new Error(ERR_NATIVE_RESPONSE);
86
+ }
87
+ }
88
+ function flattenRenderBlocks(renderBlocks) {
89
+ if (!renderBlocks || renderBlocks.length === 0) {
90
+ return [];
91
+ }
92
+ return renderBlocks.flat();
93
+ }
94
+ function applyRenderBlocksPatch(previousRenderBlocks, renderPatch) {
95
+ if (!previousRenderBlocks || !renderPatch) {
96
+ return undefined;
97
+ }
98
+ const { startIndex, deleteCount, renderBlocks } = renderPatch;
99
+ if (!Number.isInteger(startIndex) ||
100
+ !Number.isInteger(deleteCount) ||
101
+ startIndex < 0 ||
102
+ deleteCount < 0 ||
103
+ startIndex > previousRenderBlocks.length ||
104
+ startIndex + deleteCount > previousRenderBlocks.length) {
105
+ return undefined;
106
+ }
107
+ return [
108
+ ...previousRenderBlocks.slice(0, startIndex),
109
+ ...renderBlocks,
110
+ ...previousRenderBlocks.slice(startIndex + deleteCount),
111
+ ];
112
+ }
113
+ function parseContentSnapshotJson(json) {
114
+ try {
115
+ const parsed = JSON.parse(json);
116
+ if ('error' in parsed) {
117
+ throw new Error(`NativeEditorBridge: ${parsed.error}`);
118
+ }
119
+ return {
120
+ html: typeof parsed.html === 'string' ? parsed.html : '',
121
+ json: parsed.json != null && typeof parsed.json === 'object'
122
+ ? parsed.json
123
+ : {},
65
124
  };
66
125
  }
67
126
  catch (e) {
@@ -175,6 +234,16 @@ function encodeCollaborationStateBase64(encodedState) {
175
234
  function decodeCollaborationStateBase64(base64) {
176
235
  return base64ToBytes(base64);
177
236
  }
237
+ function normalizeDocumentJsonString(jsonString, schema) {
238
+ try {
239
+ const parsed = JSON.parse(jsonString);
240
+ const normalized = (0, schemas_1.normalizeDocumentJson)(parsed, schema);
241
+ return normalized === parsed ? jsonString : JSON.stringify(normalized);
242
+ }
243
+ catch {
244
+ return jsonString;
245
+ }
246
+ }
178
247
  function parseCollaborationResultJson(json) {
179
248
  if (!json || json === '') {
180
249
  return {
@@ -217,14 +286,20 @@ function _resetNativeModuleCache() {
217
286
  _nativeModule = null;
218
287
  }
219
288
  class NativeEditorBridge {
220
- constructor(editorId) {
289
+ constructor(editorId, schema) {
221
290
  this._destroyed = false;
222
291
  this._lastSelection = { type: 'text', anchor: 0, head: 0 };
292
+ this._documentVersion = 0;
293
+ this._cachedHtml = null;
294
+ this._cachedJsonString = null;
295
+ this._renderBlocksCache = null;
223
296
  this._editorId = editorId;
297
+ this._schema = schema;
224
298
  }
225
299
  /** Create a new editor instance backed by the Rust engine. */
226
300
  static create(config) {
227
301
  const configObj = {};
302
+ let parsedSchema;
228
303
  if (config?.maxLength != null)
229
304
  configObj.maxLength = config.maxLength;
230
305
  if (config?.allowBase64Images != null) {
@@ -232,14 +307,15 @@ class NativeEditorBridge {
232
307
  }
233
308
  if (config?.schemaJson != null) {
234
309
  try {
235
- configObj.schema = JSON.parse(config.schemaJson);
310
+ parsedSchema = JSON.parse(config.schemaJson);
311
+ configObj.schema = parsedSchema;
236
312
  }
237
313
  catch {
238
314
  // Fall back to the default schema when the provided JSON is invalid.
239
315
  }
240
316
  }
241
317
  const id = getNativeModule().editorCreate(JSON.stringify(configObj));
242
- return new NativeEditorBridge(id);
318
+ return new NativeEditorBridge(id, parsedSchema);
243
319
  }
244
320
  /** The underlying native editor ID. */
245
321
  get editorId() {
@@ -254,57 +330,89 @@ class NativeEditorBridge {
254
330
  if (this._destroyed)
255
331
  return;
256
332
  this._destroyed = true;
333
+ this._renderBlocksCache = null;
257
334
  getNativeModule().editorDestroy(this._editorId);
258
335
  }
259
336
  /** Set content from HTML. Returns render elements for display. */
260
337
  setHtml(html) {
261
338
  this.assertNotDestroyed();
339
+ this.invalidateContentCaches();
340
+ this._renderBlocksCache = null;
262
341
  const json = getNativeModule().editorSetHtml(this._editorId, html);
263
342
  return parseRenderElements(json);
264
343
  }
265
344
  /** Get content as HTML. */
266
345
  getHtml() {
267
346
  this.assertNotDestroyed();
268
- return getNativeModule().editorGetHtml(this._editorId);
347
+ if (this._cachedHtml?.version === this._documentVersion) {
348
+ return this._cachedHtml.value;
349
+ }
350
+ const html = getNativeModule().editorGetHtml(this._editorId);
351
+ this._cachedHtml = { version: this._documentVersion, value: html };
352
+ return html;
269
353
  }
270
354
  /** Set content from ProseMirror JSON. Returns render elements. */
271
355
  setJson(doc) {
356
+ return this.setJsonString(JSON.stringify((0, schemas_1.normalizeDocumentJson)(doc, this._schema)));
357
+ }
358
+ /** Set content from a serialized ProseMirror JSON string. Returns render elements. */
359
+ setJsonString(jsonString) {
272
360
  this.assertNotDestroyed();
273
- const json = getNativeModule().editorSetJson(this._editorId, JSON.stringify(doc));
361
+ this.invalidateContentCaches();
362
+ this._renderBlocksCache = null;
363
+ const normalizedJsonString = normalizeDocumentJsonString(jsonString, this._schema);
364
+ const json = getNativeModule().editorSetJson(this._editorId, normalizedJsonString);
274
365
  return parseRenderElements(json);
275
366
  }
367
+ /** Get content as raw ProseMirror JSON string. */
368
+ getJsonString() {
369
+ this.assertNotDestroyed();
370
+ if (this._cachedJsonString?.version === this._documentVersion) {
371
+ return this._cachedJsonString.value;
372
+ }
373
+ const json = getNativeModule().editorGetJson(this._editorId);
374
+ this._cachedJsonString = { version: this._documentVersion, value: json };
375
+ return json;
376
+ }
276
377
  /** Get content as ProseMirror JSON. */
277
378
  getJson() {
379
+ return parseDocumentJSON(this.getJsonString());
380
+ }
381
+ /** Get both HTML and JSON content in one native roundtrip. */
382
+ getContentSnapshot() {
278
383
  this.assertNotDestroyed();
279
- const json = getNativeModule().editorGetJson(this._editorId);
280
- return parseDocumentJSON(json);
384
+ if (this._cachedHtml?.version === this._documentVersion &&
385
+ this._cachedJsonString?.version === this._documentVersion) {
386
+ return {
387
+ html: this._cachedHtml.value,
388
+ json: parseDocumentJSON(this._cachedJsonString.value),
389
+ };
390
+ }
391
+ const snapshot = parseContentSnapshotJson(getNativeModule().editorGetContentSnapshot(this._editorId));
392
+ this._cachedHtml = { version: this._documentVersion, value: snapshot.html };
393
+ this._cachedJsonString = {
394
+ version: this._documentVersion,
395
+ value: JSON.stringify(snapshot.json),
396
+ };
397
+ return snapshot;
281
398
  }
282
399
  /** Insert text at a document position. Returns the full update. */
283
400
  insertText(pos, text) {
284
401
  this.assertNotDestroyed();
285
402
  const json = getNativeModule().editorInsertText(this._editorId, pos, text);
286
- const update = parseEditorUpdateJson(json);
287
- if (update)
288
- this._lastSelection = update.selection;
289
- return update;
403
+ return this.parseAndNoteUpdate(json);
290
404
  }
291
405
  /** Delete a range [from, to). Returns the full update. */
292
406
  deleteRange(from, to) {
293
407
  this.assertNotDestroyed();
294
408
  const json = getNativeModule().editorDeleteRange(this._editorId, from, to);
295
- const update = parseEditorUpdateJson(json);
296
- if (update)
297
- this._lastSelection = update.selection;
298
- return update;
409
+ return this.parseAndNoteUpdate(json);
299
410
  }
300
411
  /** Replace the current selection with text atomically. */
301
412
  replaceSelectionText(text) {
302
413
  this.assertNotDestroyed();
303
414
  const json = getNativeModule().editorReplaceSelectionText(this._editorId, text);
304
- const update = parseEditorUpdateJson(json);
305
- if (update)
306
- this._lastSelection = update.selection;
307
- return update;
415
+ return this.parseAndNoteUpdate(json);
308
416
  }
309
417
  /** Toggle a mark (bold, italic, etc.) on the current selection. */
310
418
  toggleMark(markType) {
@@ -313,10 +421,7 @@ class NativeEditorBridge {
313
421
  const json = scalarSelection
314
422
  ? getNativeModule().editorToggleMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType)
315
423
  : getNativeModule().editorToggleMark(this._editorId, markType);
316
- const update = parseEditorUpdateJson(json);
317
- if (update)
318
- this._lastSelection = update.selection;
319
- return update;
424
+ return this.parseAndNoteUpdate(json);
320
425
  }
321
426
  /** Set a mark with attrs on the current selection. */
322
427
  setMark(markType, attrs) {
@@ -326,10 +431,7 @@ class NativeEditorBridge {
326
431
  const json = scalarSelection
327
432
  ? getNativeModule().editorSetMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType, attrsJson)
328
433
  : getNativeModule().editorSetMark(this._editorId, markType, attrsJson);
329
- const update = parseEditorUpdateJson(json);
330
- if (update)
331
- this._lastSelection = update.selection;
332
- return update;
434
+ return this.parseAndNoteUpdate(json);
333
435
  }
334
436
  /** Remove a mark from the current selection. */
335
437
  unsetMark(markType) {
@@ -338,10 +440,7 @@ class NativeEditorBridge {
338
440
  const json = scalarSelection
339
441
  ? getNativeModule().editorUnsetMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType)
340
442
  : getNativeModule().editorUnsetMark(this._editorId, markType);
341
- const update = parseEditorUpdateJson(json);
342
- if (update)
343
- this._lastSelection = update.selection;
344
- return update;
443
+ return this.parseAndNoteUpdate(json);
345
444
  }
346
445
  /** Toggle blockquote wrapping for the current block selection. */
347
446
  toggleBlockquote() {
@@ -350,10 +449,7 @@ class NativeEditorBridge {
350
449
  const json = scalarSelection
351
450
  ? getNativeModule().editorToggleBlockquoteAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
352
451
  : getNativeModule().editorToggleBlockquote(this._editorId);
353
- const update = parseEditorUpdateJson(json);
354
- if (update)
355
- this._lastSelection = update.selection;
356
- return update;
452
+ return this.parseAndNoteUpdate(json);
357
453
  }
358
454
  /** Toggle a heading level on the current block selection. */
359
455
  toggleHeading(level) {
@@ -365,10 +461,7 @@ class NativeEditorBridge {
365
461
  const json = scalarSelection
366
462
  ? getNativeModule().editorToggleHeadingAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, level)
367
463
  : getNativeModule().editorToggleHeading(this._editorId, level);
368
- const update = parseEditorUpdateJson(json);
369
- if (update)
370
- this._lastSelection = update.selection;
371
- return update;
464
+ return this.parseAndNoteUpdate(json);
372
465
  }
373
466
  /** Set the document selection by anchor and head positions. */
374
467
  setSelection(anchor, head) {
@@ -402,82 +495,66 @@ class NativeEditorBridge {
402
495
  getCurrentState() {
403
496
  this.assertNotDestroyed();
404
497
  const json = getNativeModule().editorGetCurrentState(this._editorId);
405
- const update = parseEditorUpdateJson(json);
406
- if (update)
407
- this._lastSelection = update.selection;
408
- return update;
498
+ return this.parseAndNoteUpdate(json);
499
+ }
500
+ /** Get the current selection-related state without render elements. */
501
+ getSelectionState() {
502
+ this.assertNotDestroyed();
503
+ const json = getNativeModule().editorGetSelectionState(this._editorId);
504
+ return this.parseAndNoteUpdate(json);
409
505
  }
410
506
  /** Split the block at a position (Enter key). */
411
507
  splitBlock(pos) {
412
508
  this.assertNotDestroyed();
413
509
  const json = getNativeModule().editorSplitBlock(this._editorId, pos);
414
- const update = parseEditorUpdateJson(json);
415
- if (update)
416
- this._lastSelection = update.selection;
417
- return update;
510
+ return this.parseAndNoteUpdate(json);
418
511
  }
419
512
  /** Insert HTML content at the current selection. */
420
513
  insertContentHtml(html) {
421
514
  this.assertNotDestroyed();
422
515
  const json = getNativeModule().editorInsertContentHtml(this._editorId, html);
423
- const update = parseEditorUpdateJson(json);
424
- if (update)
425
- this._lastSelection = update.selection;
426
- return update;
516
+ return this.parseAndNoteUpdate(json);
427
517
  }
428
518
  /** Insert JSON content at the current selection. */
429
519
  insertContentJson(doc) {
430
520
  this.assertNotDestroyed();
431
521
  const json = getNativeModule().editorInsertContentJson(this._editorId, JSON.stringify(doc));
432
- const update = parseEditorUpdateJson(json);
433
- if (update)
434
- this._lastSelection = update.selection;
435
- return update;
522
+ return this.parseAndNoteUpdate(json);
436
523
  }
437
524
  /** Insert JSON content at an explicit scalar selection. */
438
525
  insertContentJsonAtSelectionScalar(scalarAnchor, scalarHead, doc) {
439
526
  this.assertNotDestroyed();
440
527
  const json = getNativeModule().editorInsertContentJsonAtSelectionScalar(this._editorId, scalarAnchor, scalarHead, JSON.stringify(doc));
441
- const update = parseEditorUpdateJson(json);
442
- if (update)
443
- this._lastSelection = update.selection;
444
- return update;
528
+ return this.parseAndNoteUpdate(json);
445
529
  }
446
530
  /** Replace entire document with HTML via transaction (preserves undo history). */
447
531
  replaceHtml(html) {
448
532
  this.assertNotDestroyed();
449
533
  const json = getNativeModule().editorReplaceHtml(this._editorId, html);
450
- const update = parseEditorUpdateJson(json);
451
- if (update)
452
- this._lastSelection = update.selection;
453
- return update;
534
+ return this.parseAndNoteUpdate(json);
454
535
  }
455
536
  /** Replace entire document with JSON via transaction (preserves undo history). */
456
537
  replaceJson(doc) {
538
+ return this.replaceJsonString(JSON.stringify((0, schemas_1.normalizeDocumentJson)(doc, this._schema)));
539
+ }
540
+ /** Replace entire document with a serialized JSON transaction. */
541
+ replaceJsonString(jsonString) {
457
542
  this.assertNotDestroyed();
458
- const json = getNativeModule().editorReplaceJson(this._editorId, JSON.stringify(doc));
459
- const update = parseEditorUpdateJson(json);
460
- if (update)
461
- this._lastSelection = update.selection;
462
- return update;
543
+ const normalizedJsonString = normalizeDocumentJsonString(jsonString, this._schema);
544
+ const json = getNativeModule().editorReplaceJson(this._editorId, normalizedJsonString);
545
+ return this.parseAndNoteUpdate(json);
463
546
  }
464
547
  /** Undo the last operation. Returns update or null if nothing to undo. */
465
548
  undo() {
466
549
  this.assertNotDestroyed();
467
550
  const json = getNativeModule().editorUndo(this._editorId);
468
- const update = parseEditorUpdateJson(json);
469
- if (update)
470
- this._lastSelection = update.selection;
471
- return update;
551
+ return this.parseAndNoteUpdate(json);
472
552
  }
473
553
  /** Redo the last undone operation. Returns update or null if nothing to redo. */
474
554
  redo() {
475
555
  this.assertNotDestroyed();
476
556
  const json = getNativeModule().editorRedo(this._editorId);
477
- const update = parseEditorUpdateJson(json);
478
- if (update)
479
- this._lastSelection = update.selection;
480
- return update;
557
+ return this.parseAndNoteUpdate(json);
481
558
  }
482
559
  /** Check if undo is available. */
483
560
  canUndo() {
@@ -501,10 +578,7 @@ class NativeEditorBridge {
501
578
  : scalarSelection
502
579
  ? getNativeModule().editorWrapInListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, listType)
503
580
  : getNativeModule().editorWrapInList(this._editorId, listType);
504
- const update = parseEditorUpdateJson(json);
505
- if (update)
506
- this._lastSelection = update.selection;
507
- return update;
581
+ return this.parseAndNoteUpdate(json);
508
582
  }
509
583
  /** Unwrap the current list item back to a paragraph. */
510
584
  unwrapFromList() {
@@ -513,10 +587,7 @@ class NativeEditorBridge {
513
587
  const json = scalarSelection
514
588
  ? getNativeModule().editorUnwrapFromListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
515
589
  : getNativeModule().editorUnwrapFromList(this._editorId);
516
- const update = parseEditorUpdateJson(json);
517
- if (update)
518
- this._lastSelection = update.selection;
519
- return update;
590
+ return this.parseAndNoteUpdate(json);
520
591
  }
521
592
  /** Indent the current list item into a nested list. */
522
593
  indentListItem() {
@@ -525,10 +596,7 @@ class NativeEditorBridge {
525
596
  const json = scalarSelection
526
597
  ? getNativeModule().editorIndentListItemAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
527
598
  : getNativeModule().editorIndentListItem(this._editorId);
528
- const update = parseEditorUpdateJson(json);
529
- if (update)
530
- this._lastSelection = update.selection;
531
- return update;
599
+ return this.parseAndNoteUpdate(json);
532
600
  }
533
601
  /** Outdent the current list item to the parent list level. */
534
602
  outdentListItem() {
@@ -537,10 +605,7 @@ class NativeEditorBridge {
537
605
  const json = scalarSelection
538
606
  ? getNativeModule().editorOutdentListItemAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
539
607
  : getNativeModule().editorOutdentListItem(this._editorId);
540
- const update = parseEditorUpdateJson(json);
541
- if (update)
542
- this._lastSelection = update.selection;
543
- return update;
608
+ return this.parseAndNoteUpdate(json);
544
609
  }
545
610
  /** Insert a void node (e.g. 'horizontalRule') at the current selection. */
546
611
  insertNode(nodeType) {
@@ -549,11 +614,41 @@ class NativeEditorBridge {
549
614
  const json = scalarSelection
550
615
  ? getNativeModule().editorInsertNodeAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, nodeType)
551
616
  : getNativeModule().editorInsertNode(this._editorId, nodeType);
552
- const update = parseEditorUpdateJson(json);
553
- if (update)
554
- this._lastSelection = update.selection;
617
+ return this.parseAndNoteUpdate(json);
618
+ }
619
+ parseUpdateJson(json) {
620
+ this.assertNotDestroyed();
621
+ return this.parseAndNoteUpdate(json);
622
+ }
623
+ noteUpdate(update) {
624
+ if (!update) {
625
+ return;
626
+ }
627
+ this._lastSelection = update.selection;
628
+ if (update.renderBlocks) {
629
+ this._renderBlocksCache = update.renderBlocks;
630
+ }
631
+ if (typeof update.documentVersion !== 'number') {
632
+ this.invalidateContentCaches();
633
+ return;
634
+ }
635
+ if (update.documentVersion !== this._documentVersion) {
636
+ this._documentVersion = update.documentVersion;
637
+ this.invalidateContentCaches();
638
+ }
639
+ }
640
+ parseAndNoteUpdate(json) {
641
+ let update = parseEditorUpdateJson(json, this._renderBlocksCache ?? undefined);
642
+ if (update?.renderPatch && !update.renderBlocks) {
643
+ update = parseEditorUpdateJson(getNativeModule().editorGetCurrentState(this._editorId), this._renderBlocksCache ?? undefined);
644
+ }
645
+ this.noteUpdate(update);
555
646
  return update;
556
647
  }
648
+ invalidateContentCaches() {
649
+ this._cachedHtml = null;
650
+ this._cachedJsonString = null;
651
+ }
557
652
  assertNotDestroyed() {
558
653
  if (this._destroyed) {
559
654
  throw new Error(ERR_DESTROYED);
@@ -4,8 +4,7 @@ import { type ActiveState, type DocumentJSON, type HistoryState, type Selection
4
4
  import { type EditorToolbarHeadingLevel, type EditorToolbarItem } from './EditorToolbar';
5
5
  import { type EditorTheme } from './EditorTheme';
6
6
  import { type EditorAddons } from './addons';
7
- import { type SchemaDefinition } from './schemas';
8
- import { type ImageNodeAttributes } from './schemas';
7
+ import { type ImageNodeAttributes, type SchemaDefinition } from './schemas';
9
8
  export type NativeRichTextEditorHeightBehavior = 'fixed' | 'autoGrow';
10
9
  export type NativeRichTextEditorToolbarPlacement = 'keyboard' | 'inline';
11
10
  export interface RemoteSelectionDecoration {
@@ -38,6 +37,8 @@ export interface NativeRichTextEditorProps {
38
37
  value?: string;
39
38
  /** Controlled ProseMirror JSON content. Ignored if value is set. */
40
39
  valueJSON?: DocumentJSON;
40
+ /** Optional stable revision hint for `valueJSON` to avoid reserializing equal docs on rerender. */
41
+ valueJSONRevision?: string | number;
41
42
  /** Schema definition. Defaults to tiptapSchema if not provided. */
42
43
  schema?: SchemaDefinition;
43
44
  /** Placeholder text shown when editor is empty. */