@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.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 (180) hide show
  1. package/README.md +8 -116
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/android/build.gradle +3 -0
  4. package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
  5. package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
  6. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
  7. package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
  8. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
  9. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
  10. package/app.plugin.js +42 -39
  11. package/dist/AtomHost.js +18 -11
  12. package/dist/EditorAddon.js +4 -2
  13. package/dist/EditorAddonNormalization.js +17 -9
  14. package/dist/EditorMentionThemeNormalization.js +6 -3
  15. package/dist/EditorMentionThemeValidation.js +28 -11
  16. package/dist/EditorStyleSheetNormalization.js +85 -38
  17. package/dist/EditorStyleSheetTypes.d.ts +1 -1
  18. package/dist/EditorTheme.d.ts +4 -0
  19. package/dist/EditorTheme.js +5 -3
  20. package/dist/EditorToolbarComponent.d.ts +1 -1
  21. package/dist/EditorToolbarComponent.js +1 -1
  22. package/dist/EditorToolbarRegistry.js +5 -5
  23. package/dist/EditorToolbarTypes.d.ts +12 -12
  24. package/dist/EditorToolbarVisuals.js +1 -1
  25. package/dist/ExternalTextComposition.js +89 -43
  26. package/dist/ImageLoadingPolicy.d.ts +1 -1
  27. package/dist/NativeEditorBoundaryError.d.ts +7 -7
  28. package/dist/NativeEditorBoundaryError.js +9 -5
  29. package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
  30. package/dist/NativeEditorCollaborationTransport.js +28 -15
  31. package/dist/NativeEditorCreateConfig.js +11 -6
  32. package/dist/NativeEditorCreateJson.js +51 -25
  33. package/dist/NativeEditorCreateValidation.js +11 -6
  34. package/dist/NativeEditorDocumentBridge.d.ts +1 -1
  35. package/dist/NativeEditorDocumentBridge.js +32 -18
  36. package/dist/NativeEditorDocumentHandle.d.ts +4 -4
  37. package/dist/NativeEditorDocumentHandle.js +1 -1
  38. package/dist/NativeEditorLocalAwareness.js +25 -13
  39. package/dist/NativeEditorNativeModule.d.ts +8 -1
  40. package/dist/NativeEditorNativeModule.js +7 -5
  41. package/dist/NativeEditorRenderNormalization.js +66 -25
  42. package/dist/NativeEditorResultNormalization.js +53 -29
  43. package/dist/NativeEditorTypes.d.ts +10 -10
  44. package/dist/NativeEditorV2Decimal.js +6 -3
  45. package/dist/NativeProseViewer.js +11 -3
  46. package/dist/ResourceLimits.d.ts +2 -2
  47. package/dist/ResourceLimits.js +7 -2
  48. package/dist/RichTextEditor.js +1 -1
  49. package/dist/RichTextEditorNativeTypes.d.ts +1 -1
  50. package/dist/RichTextEditorNativeView.d.ts +1 -3
  51. package/dist/RichTextEditorSerialization.js +24 -14
  52. package/dist/RichTextEditorTypes.d.ts +1 -1
  53. package/dist/YjsCollaboration.d.ts +7 -7
  54. package/dist/YjsCollaboration.js +42 -26
  55. package/dist/addons.d.ts +4 -4
  56. package/dist/addons.js +3 -3
  57. package/dist/atomInstances.js +6 -3
  58. package/dist/atomUpdates.js +7 -4
  59. package/dist/atoms.js +20 -12
  60. package/dist/attributeValidation.js +55 -24
  61. package/dist/contentExpression.js +80 -42
  62. package/dist/listMarkers.d.ts +1 -1
  63. package/dist/listMarkers.js +1 -1
  64. package/dist/schemaDefinition.d.ts +1 -1
  65. package/dist/schemaDefinition.js +23 -14
  66. package/dist/schemaDocument.js +1 -1
  67. package/dist/schemaDocumentConstruction.js +25 -14
  68. package/dist/schemaNormalization.js +51 -29
  69. package/dist/schemaPresets.d.ts +1 -1
  70. package/dist/schemaPresets.js +9 -4
  71. package/dist/schemaResolution.js +46 -25
  72. package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
  73. package/dist/useEditorToolbarInteractions.d.ts +2 -2
  74. package/dist/useEditorToolbarInteractions.js +45 -23
  75. package/dist/useEditorToolbarItems.d.ts +1 -1
  76. package/dist/useEditorToolbarItems.js +4 -3
  77. package/dist/useEditorToolbarPresentation.d.ts +3 -3
  78. package/dist/useEditorToolbarPresentation.js +4 -4
  79. package/dist/useEditorToolbarState.d.ts +1 -1
  80. package/dist/useEditorToolbarState.js +1 -1
  81. package/dist/useFocusPreservingFrames.js +11 -8
  82. package/dist/useNativeEditor.d.ts +1 -1
  83. package/dist/useNativeEditor.js +21 -11
  84. package/dist/useRichTextEditorCommands.d.ts +2 -2
  85. package/dist/useRichTextEditorCommands.js +59 -29
  86. package/dist/useRichTextEditorEvents.d.ts +3 -3
  87. package/dist/useRichTextEditorEvents.js +75 -32
  88. package/dist/useRichTextEditorMentions.d.ts +4 -4
  89. package/dist/useRichTextEditorMentions.js +48 -29
  90. package/dist/useRichTextEditorPresentation.d.ts +4 -4
  91. package/dist/useRichTextEditorPresentation.js +25 -23
  92. package/dist/useRichTextEditorState.d.ts +3 -3
  93. package/dist/useRichTextEditorState.js +20 -15
  94. package/dist/useRichTextEditorUpdates.d.ts +1 -1
  95. package/dist/useRichTextEditorUpdates.js +99 -41
  96. package/dist/useViewerAtoms.js +63 -36
  97. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  98. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  99. package/ios/EditorInteractionOverlays.swift +114 -122
  100. package/ios/EditorLayoutManager.swift +13 -3
  101. package/ios/EditorStyleSheet.swift +12 -1
  102. package/ios/EditorTextView+Images.swift +32 -10
  103. package/ios/EditorTextView.swift +1 -1
  104. package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
  105. package/ios/NativeToolbar+ButtonStyle.swift +17 -5
  106. package/ios/NativeToolbar+Diagnostics.swift +15 -0
  107. package/ios/RenderBridge+Atoms.swift +0 -1
  108. package/ios/RenderBridge+Attachments.swift +0 -29
  109. package/ios/RichTextEditorView.swift +4 -12
  110. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
  111. package/package.json +11 -3
  112. package/react-native.config.js +1 -1
  113. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  114. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  115. package/rust/android/x86/libeditor_core.so +0 -0
  116. package/rust/android/x86_64/libeditor_core.so +0 -0
  117. package/src/AtomHost.tsx +58 -28
  118. package/src/EditorAddon.ts +18 -8
  119. package/src/EditorAddonNormalization.ts +40 -11
  120. package/src/EditorMentionThemeNormalization.ts +15 -4
  121. package/src/EditorMentionThemeValidation.ts +51 -14
  122. package/src/EditorStyleSheet.ts +1 -0
  123. package/src/EditorStyleSheetNormalization.ts +195 -64
  124. package/src/EditorStyleSheetTypes.ts +1 -1
  125. package/src/EditorTheme.ts +13 -5
  126. package/src/EditorToolbarComponent.tsx +2 -1
  127. package/src/EditorToolbarRegistry.tsx +23 -13
  128. package/src/EditorToolbarTypes.tsx +12 -12
  129. package/src/EditorToolbarVisuals.tsx +6 -3
  130. package/src/EditorUpdateRevision.ts +1 -0
  131. package/src/ExternalTextComposition.ts +259 -49
  132. package/src/ImageLoadingPolicy.ts +3 -1
  133. package/src/NativeEditorBoundaryError.ts +38 -11
  134. package/src/NativeEditorCollaborationTransport.ts +108 -27
  135. package/src/NativeEditorCreateConfig.ts +92 -19
  136. package/src/NativeEditorCreateJson.ts +163 -26
  137. package/src/NativeEditorCreateValidation.ts +33 -12
  138. package/src/NativeEditorDocumentBridge.ts +111 -19
  139. package/src/NativeEditorDocumentHandle.ts +9 -4
  140. package/src/NativeEditorLocalAwareness.ts +70 -15
  141. package/src/NativeEditorNativeModule.ts +13 -9
  142. package/src/NativeEditorRenderNormalization.ts +128 -29
  143. package/src/NativeEditorResultNormalization.ts +143 -32
  144. package/src/NativeEditorTypes.ts +12 -11
  145. package/src/NativeEditorV2Decimal.ts +12 -3
  146. package/src/NativeProseViewer.tsx +46 -16
  147. package/src/ResourceLimits.ts +15 -8
  148. package/src/RichTextEditor.tsx +3 -1
  149. package/src/RichTextEditorNativeTypes.tsx +1 -1
  150. package/src/RichTextEditorNativeView.tsx +2 -8
  151. package/src/RichTextEditorSerialization.tsx +70 -24
  152. package/src/RichTextEditorTypes.tsx +1 -1
  153. package/src/YjsCollaboration.ts +143 -47
  154. package/src/addons.ts +12 -8
  155. package/src/atomInstances.ts +25 -5
  156. package/src/atomUpdates.ts +23 -6
  157. package/src/atoms.ts +85 -18
  158. package/src/attributeValidation.ts +97 -33
  159. package/src/contentExpression.ts +218 -63
  160. package/src/listMarkers.ts +1 -1
  161. package/src/schemaDefinition.ts +82 -20
  162. package/src/schemaDocument.ts +15 -1
  163. package/src/schemaDocumentConstruction.ts +80 -16
  164. package/src/schemaNormalization.ts +155 -41
  165. package/src/schemaPresets.ts +37 -31
  166. package/src/schemaResolution.ts +126 -27
  167. package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
  168. package/src/useEditorToolbarInteractions.tsx +85 -36
  169. package/src/useEditorToolbarItems.tsx +50 -22
  170. package/src/useEditorToolbarPresentation.tsx +81 -58
  171. package/src/useEditorToolbarState.tsx +8 -6
  172. package/src/useFocusPreservingFrames.ts +43 -15
  173. package/src/useNativeEditor.ts +111 -51
  174. package/src/useRichTextEditorCommands.tsx +127 -54
  175. package/src/useRichTextEditorEvents.tsx +144 -42
  176. package/src/useRichTextEditorMentions.tsx +118 -37
  177. package/src/useRichTextEditorPresentation.tsx +128 -114
  178. package/src/useRichTextEditorState.tsx +76 -40
  179. package/src/useRichTextEditorUpdates.tsx +207 -56
  180. package/src/useViewerAtoms.tsx +162 -66
@@ -36,6 +36,7 @@ function sameFrames(
36
36
  left.length === right.length &&
37
37
  left.every((frame, index) => {
38
38
  const candidate = right[index];
39
+
39
40
  return (
40
41
  frame.x === candidate.x &&
41
42
  frame.y === candidate.y &&
@@ -52,9 +53,10 @@ function validFrame(
52
53
  width: number,
53
54
  height: number
54
55
  ): EditorToolbarFrame | null {
55
- if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) {
56
+ if (![ x, y, width, height ].every(Number.isFinite) || width <= 0 || height <= 0) {
56
57
  return null;
57
58
  }
59
+
58
60
  return { x, y, width, height };
59
61
  }
60
62
 
@@ -66,38 +68,49 @@ export function useFocusPreservingFrames(
66
68
  refresh: () => void;
67
69
  } {
68
70
  const normalizedRefs: readonly RichTextEditorFocusPreservingRef[] =
69
- refs == null ? [] : Array.isArray(refs) ? refs : [refs as RichTextEditorFocusPreservingRef];
71
+ refs == null ? [] : Array.isArray(refs) ? refs : [ refs as RichTextEditorFocusPreservingRef ];
72
+
70
73
  const stableRefs = useRef<readonly RichTextEditorFocusPreservingRef[]>(normalizedRefs);
74
+
71
75
  if (!sameRefs(stableRefs.current, normalizedRefs)) {
72
76
  stableRefs.current = normalizedRefs;
73
77
  }
78
+
74
79
  const currentRefs = stableRefs.current;
75
80
 
76
81
  const generation = useRef(0);
82
+
77
83
  const framesByRef = useRef(
78
84
  new Map<RichTextEditorFocusPreservingRef, MeasuredFocusPreservingFrame>()
79
85
  );
80
- const [frames, setFrames] = useState<readonly EditorToolbarFrame[]>(EMPTY_FRAMES);
86
+
87
+ const [ frames, setFrames ] = useState<readonly EditorToolbarFrame[]>(EMPTY_FRAMES);
81
88
 
82
89
  const publish = useCallback(() => {
83
- const nextFrames = currentRefs.flatMap((ref) => {
90
+ const nextFrames = currentRefs.flatMap(ref => {
84
91
  const measurement = framesByRef.current.get(ref);
85
- return measurement == null ? [] : [measurement.frame];
92
+
93
+ return measurement == null ? [] : [ measurement.frame ];
86
94
  });
87
- setFrames((current) => (sameFrames(current, nextFrames) ? current : nextFrames));
88
- }, [currentRefs]);
95
+
96
+ setFrames(current => (sameFrames(current, nextFrames) ? current : nextFrames));
97
+ }, [ currentRefs ]);
89
98
 
90
99
  const refresh = useCallback(() => {
91
100
  const callbackGeneration = ++generation.current;
101
+
92
102
  if (!enabled) {
93
103
  framesByRef.current.clear();
94
- setFrames((current) => (current.length === 0 ? current : EMPTY_FRAMES));
104
+ setFrames(current => (current.length === 0 ? current : EMPTY_FRAMES));
105
+
95
106
  return;
96
107
  }
97
108
 
98
109
  const admittedRefs = new Set(currentRefs);
110
+
99
111
  for (const ref of framesByRef.current.keys()) {
100
112
  const measurement = framesByRef.current.get(ref);
113
+
101
114
  if (
102
115
  !admittedRefs.has(ref) ||
103
116
  ref.current == null ||
@@ -106,49 +119,64 @@ export function useFocusPreservingFrames(
106
119
  framesByRef.current.delete(ref);
107
120
  }
108
121
  }
122
+
109
123
  publish();
110
124
 
111
- currentRefs.forEach((ref) => {
125
+ currentRefs.forEach(ref => {
112
126
  const target = ref.current;
127
+
113
128
  if (target == null || typeof target.measureInWindow !== 'function') {
114
129
  framesByRef.current.delete(ref);
115
130
  publish();
131
+
116
132
  return;
117
133
  }
118
134
 
119
135
  try {
120
136
  target.measureInWindow((x, y, width, height) => {
121
137
  // An older measurement must not restore a removed or replaced target.
122
- if (generation.current !== callbackGeneration || ref.current !== target) return;
138
+ if (generation.current !== callbackGeneration || ref.current !== target) {
139
+ return;
140
+ }
141
+
123
142
  const frame = validFrame(x, y, width, height);
143
+
124
144
  if (frame == null) {
125
145
  framesByRef.current.delete(ref);
126
146
  } else {
127
147
  framesByRef.current.set(ref, { target, frame });
128
148
  }
149
+
129
150
  publish();
130
151
  });
131
152
  } catch {
132
- if (generation.current !== callbackGeneration) return;
153
+ if (generation.current !== callbackGeneration) {
154
+ return;
155
+ }
156
+
133
157
  framesByRef.current.delete(ref);
134
158
  publish();
135
159
  }
136
160
  });
137
- }, [currentRefs, enabled, publish]);
161
+ }, [ currentRefs, enabled, publish ]);
138
162
 
139
163
  useLayoutEffect(() => {
140
164
  refresh();
141
165
  });
142
166
 
143
167
  useEffect(() => {
144
- if (!enabled) return;
168
+ if (!enabled) {
169
+ return;
170
+ }
171
+
145
172
  const subscriptions = [
146
173
  Keyboard.addListener('keyboardDidShow', refresh),
147
174
  Keyboard.addListener('keyboardDidHide', refresh),
148
175
  Keyboard.addListener('keyboardDidChangeFrame', refresh),
149
176
  ];
150
- return () => subscriptions.forEach((subscription) => subscription.remove());
151
- }, [enabled, refresh]);
177
+
178
+ return () => subscriptions.forEach(subscription => subscription.remove());
179
+ }, [ enabled, refresh ]);
152
180
 
153
181
  useEffect(
154
182
  () => () => {
@@ -11,25 +11,7 @@ import {
11
11
  } from './NativeEditorBridge';
12
12
  import { NativeEditorOperationError } from './NativeEditorBoundaryError';
13
13
 
14
- //
15
- // Headless document/control binding over a shared NativeEditorDocumentHandle
16
- // (one native session per handle; the collaboration controller attaches to
17
- // the same one). It mirrors the editor's retained document API — controlled
18
- // `value`/`valueJSON` with the replace/reset history policy, `setContent`,
19
- // `setContentJson`, `getContent`, `getContentJson` — through the v2 engine:
20
- //
21
- // - `valueJSONUpdateMode="replace"` lowers to one undoable local-API
22
- // replacement (`undoableBoundary`); `"reset"` clears history
23
- // (`resetAndClear`). Imperative setContent/setContentJson are undoable;
24
- // clearContent is a non-undoable reset.
25
- // - A room document in `AwaitRemote` renders as not-ready; content getters
26
- // return empty values until Rust promotes an accepted server Step 2.
27
- // - Remote commits arrive through `revisionSignal` (the collaboration
28
- // controller's rendered revision): the binding re-reads the engine and
29
- // emits content callbacks. It never pushes document state anywhere.
30
- // - Controlled applies carry the last rendered engine revision as their base;
31
- // a REVISION_MISMATCH refreshes from the engine and the effect re-applies
32
- // against the fresh revision — never against guessed positions.
14
+ // Controlled writes use the last rendered revision; conflicts refresh before retrying.
33
15
 
34
16
  /** Configuration for {@link useNativeEditorDocument}. */
35
17
  export interface UseNativeEditorDocumentOptions {
@@ -107,7 +89,7 @@ interface V2EngineView {
107
89
  /**
108
90
  * Headless binding to a document handle: the same controlled content,
109
91
  * getters, and history the editor exposes, with no view attached. Use it to
110
- * read or drive a document that is not currently mounted a preview, a
92
+ * read or drive a document that is not currently mounted : a preview, a
111
93
  * background save, a test.
112
94
  *
113
95
  * `RichTextEditor` uses this internally, so mounting an editor on the
@@ -137,7 +119,7 @@ export function useNativeEditorDocument(
137
119
  const onLocalCommitRef = useRef(onLocalCommit);
138
120
  onLocalCommitRef.current = onLocalCommit;
139
121
 
140
- const [engineView, setEngineView] = useState<V2EngineView>({
122
+ const [ engineView, setEngineView ] = useState<V2EngineView>({
141
123
  editorId,
142
124
  ready: false,
143
125
  documentState: null,
@@ -146,26 +128,32 @@ export function useNativeEditorDocument(
146
128
  historyState: DEFAULT_V2_HISTORY_STATE,
147
129
  contentKey: null,
148
130
  });
131
+
149
132
  const currentEditorIdRef = useRef(editorId);
150
133
  currentEditorIdRef.current = editorId;
151
134
  const readyRef = useRef({ editorId, ready: false });
135
+
152
136
  const emittedRef = useRef({
153
137
  editorId,
154
138
  lastRevision: null as string | null,
155
139
  lastContentKey: null as string | null,
156
140
  lastHistoryKey: null as string | null,
157
141
  });
142
+
158
143
  const pendingControlledEchoesRef = useRef({
159
144
  editorId,
160
145
  keys: [] as string[],
161
146
  });
147
+
162
148
  const controlledKindRef = useRef<'html' | 'json' | null>(null);
163
149
  controlledKindRef.current = value != null ? 'html' : valueJSON != null ? 'json' : null;
164
150
  const ignoredControlledEchoRef = useRef<string | null>(null);
165
151
  const lastControlledContentKeyRef = useRef<string | null>(null);
152
+
166
153
  if (readyRef.current.editorId !== editorId) {
167
154
  readyRef.current = { editorId, ready: false };
168
155
  }
156
+
169
157
  if (emittedRef.current.editorId !== editorId) {
170
158
  emittedRef.current = {
171
159
  editorId,
@@ -173,6 +161,7 @@ export function useNativeEditorDocument(
173
161
  lastContentKey: null,
174
162
  lastHistoryKey: null,
175
163
  };
164
+
176
165
  pendingControlledEchoesRef.current = { editorId, keys: [] };
177
166
  ignoredControlledEchoRef.current = null;
178
167
  lastControlledContentKeyRef.current = null;
@@ -181,26 +170,35 @@ export function useNativeEditorDocument(
181
170
  const refresh = useCallback(
182
171
  (emitContentCallbacks: boolean) => {
183
172
  const refreshedEditorId = handle.editorId;
173
+
184
174
  if (handle.isDestroyed || currentEditorIdRef.current !== refreshedEditorId) {
185
175
  return;
186
176
  }
177
+
187
178
  const state = handle.bridge.getState();
188
179
  const ready = state.documentState !== 'AwaitRemote';
189
180
  let contentKey: string | null = null;
190
181
  let snapshotHtml: string | null = null;
191
182
  let snapshotJson: DocumentJSON | null = null;
183
+
192
184
  if (ready) {
193
185
  const snapshot = handle.bridge.getContentSnapshot();
194
186
  snapshotHtml = snapshot.html;
195
187
  snapshotJson = snapshot.json;
196
188
  contentKey = JSON.stringify(snapshot.json);
197
189
  }
198
- if (currentEditorIdRef.current !== refreshedEditorId) return;
190
+
191
+ if (currentEditorIdRef.current !== refreshedEditorId) {
192
+ return;
193
+ }
194
+
199
195
  const historyState: HistoryState = {
200
196
  canUndo: state.canUndo,
201
197
  canRedo: state.canRedo,
202
198
  };
199
+
203
200
  readyRef.current = { editorId: refreshedEditorId, ready };
201
+
204
202
  setEngineView({
205
203
  editorId: refreshedEditorId,
206
204
  ready,
@@ -213,12 +211,14 @@ export function useNativeEditorDocument(
213
211
 
214
212
  const historyKey = `${state.canUndo}/${state.canRedo}`;
215
213
  const emitted = emittedRef.current;
214
+
216
215
  if (
217
216
  currentEditorIdRef.current !== refreshedEditorId ||
218
217
  emitted.editorId !== refreshedEditorId
219
218
  ) {
220
219
  return;
221
220
  }
221
+
222
222
  if (historyKey !== emitted.lastHistoryKey) {
223
223
  emitted.lastHistoryKey = historyKey;
224
224
  onHistoryStateChangeRef.current?.(historyState);
@@ -226,6 +226,7 @@ export function useNativeEditorDocument(
226
226
 
227
227
  if (ready) {
228
228
  const lastRevision = emitted.lastRevision;
229
+
229
230
  if (
230
231
  emitContentCallbacks &&
231
232
  lastRevision != null &&
@@ -239,10 +240,15 @@ export function useNativeEditorDocument(
239
240
  ) {
240
241
  const echoes = pendingControlledEchoesRef.current.keys;
241
242
  echoes.push(`html:${snapshotHtml}`);
242
- if (echoes.length > 32) echoes.shift();
243
+
244
+ if (echoes.length > 32) {
245
+ echoes.shift();
246
+ }
243
247
  }
248
+
244
249
  onContentChangeRef.current?.(snapshotHtml);
245
250
  }
251
+
246
252
  if (snapshotJson != null) {
247
253
  if (
248
254
  controlledKindRef.current === 'json' &&
@@ -250,86 +256,121 @@ export function useNativeEditorDocument(
250
256
  ) {
251
257
  const echoes = pendingControlledEchoesRef.current.keys;
252
258
  echoes.push(`json:${contentKey}`);
253
- if (echoes.length > 32) echoes.shift();
259
+
260
+ if (echoes.length > 32) {
261
+ echoes.shift();
262
+ }
254
263
  }
264
+
255
265
  onContentChangeJSONRef.current?.(snapshotJson);
256
266
  }
257
267
  }
268
+
258
269
  emitted.lastRevision = state.documentRevision;
259
270
  emitted.lastContentKey = contentKey;
260
271
  }
261
272
  },
262
- [handle]
273
+ [ handle ]
263
274
  );
264
275
 
265
276
  useEffect(() => {
266
277
  refresh(true);
267
- }, [refresh, revisionSignal]);
278
+ }, [ refresh, revisionSignal ]);
268
279
 
269
280
  const refreshFromEngine = useCallback(() => {
270
281
  refresh(true);
271
- }, [refresh]);
282
+ }, [ refresh ]);
272
283
 
273
284
  const serializedControlledJson = useMemo(
274
285
  () => (value == null && valueJSON != null ? JSON.stringify(valueJSON) : null),
275
- [value, valueJSON]
286
+ [ value, valueJSON ]
276
287
  );
277
288
 
278
289
  // Controlled document flow: external value/valueJSON changes lower to one
279
290
  // local-API replacement carrying the rendered engine revision as its
280
291
  // base. Content callbacks stay suppressed (controlled sync parity).
281
292
  useEffect(() => {
282
- if (engineView.editorId !== editorId || !engineView.ready || handle.isDestroyed) return;
283
- if (engineView.documentRevision == null) return;
293
+ if (engineView.editorId !== editorId || !engineView.ready || handle.isDestroyed) {
294
+ return;
295
+ }
296
+
297
+ if (engineView.documentRevision == null) {
298
+ return;
299
+ }
300
+
284
301
  const wantsHtml = value != null;
285
302
  const wantsJson = value == null && serializedControlledJson != null;
286
- if (!wantsHtml && !wantsJson) return;
303
+
304
+ if (!wantsHtml && !wantsJson) {
305
+ return;
306
+ }
287
307
 
288
308
  const controlledContentKey = wantsHtml
289
309
  ? `html:${value}`
290
310
  : `json:${serializedControlledJson}`;
311
+
291
312
  const controlledContentChanged =
292
313
  lastControlledContentKeyRef.current !== controlledContentKey;
314
+
293
315
  if (controlledContentChanged) {
294
316
  lastControlledContentKeyRef.current = controlledContentKey;
295
317
  ignoredControlledEchoRef.current = null;
296
318
  }
297
- if (ignoredControlledEchoRef.current === controlledContentKey) return;
298
- if (!controlledContentChanged && pendingControlledEchoesRef.current.keys.length > 0) return;
319
+
320
+ if (ignoredControlledEchoRef.current === controlledContentKey) {
321
+ return;
322
+ }
323
+
324
+ if (!controlledContentChanged && pendingControlledEchoesRef.current.keys.length > 0) {
325
+ return;
326
+ }
299
327
 
300
328
  const snapshot = handle.bridge.getContentSnapshot();
329
+
301
330
  if (currentEditorIdRef.current !== editorId || emittedRef.current.editorId !== editorId) {
302
331
  return;
303
332
  }
333
+
304
334
  const currentJsonKey = JSON.stringify(snapshot.json);
335
+
305
336
  if (wantsHtml && snapshot.html === value) {
306
337
  const echoIndex = pendingControlledEchoesRef.current.keys.indexOf(controlledContentKey);
338
+
307
339
  if (echoIndex >= 0) {
308
340
  pendingControlledEchoesRef.current.keys.splice(0, echoIndex + 1);
309
341
  }
342
+
310
343
  emittedRef.current.lastRevision = engineView.documentRevision;
311
344
  emittedRef.current.lastContentKey = currentJsonKey;
345
+
312
346
  return;
313
347
  }
348
+
314
349
  if (wantsJson && currentJsonKey === serializedControlledJson) {
315
350
  const echoIndex = pendingControlledEchoesRef.current.keys.indexOf(controlledContentKey);
351
+
316
352
  if (echoIndex >= 0) {
317
353
  pendingControlledEchoesRef.current.keys.splice(0, echoIndex + 1);
318
354
  }
355
+
319
356
  emittedRef.current.lastRevision = engineView.documentRevision;
320
357
  emittedRef.current.lastContentKey = currentJsonKey;
358
+
321
359
  return;
322
360
  }
323
361
 
324
362
  const echoIndex = pendingControlledEchoesRef.current.keys.indexOf(controlledContentKey);
363
+
325
364
  if (echoIndex >= 0) {
326
365
  pendingControlledEchoesRef.current.keys.splice(0, echoIndex + 1);
327
366
  ignoredControlledEchoRef.current = controlledContentKey;
367
+
328
368
  return;
329
369
  }
330
370
 
331
371
  try {
332
372
  pendingControlledEchoesRef.current.keys = [];
373
+
333
374
  handle.bridge.applyLocalApi({
334
375
  ...(wantsHtml
335
376
  ? { setHtml: value }
@@ -337,7 +378,11 @@ export function useNativeEditorDocument(
337
378
  history: valueJSONUpdateMode === 'reset' ? 'resetAndClear' : 'undoableBoundary',
338
379
  baseDocumentRevision: engineView.documentRevision,
339
380
  });
340
- if (currentEditorIdRef.current !== editorId) return;
381
+
382
+ if (currentEditorIdRef.current !== editorId) {
383
+ return;
384
+ }
385
+
341
386
  onLocalCommitRef.current?.();
342
387
  refresh(false);
343
388
  } catch (error) {
@@ -345,8 +390,10 @@ export function useNativeEditorDocument(
345
390
  // Refresh from the engine; the effect re-applies against the
346
391
  // fresh revision (never a guessed one).
347
392
  refresh(false);
393
+
348
394
  return;
349
395
  }
396
+
350
397
  throw error;
351
398
  }
352
399
  }, [
@@ -374,32 +421,36 @@ export function useNativeEditorDocument(
374
421
  }
375
422
  );
376
423
  }
424
+
377
425
  return;
378
426
  }
427
+
379
428
  operation();
380
- if (currentEditorIdRef.current !== handle.editorId) return;
429
+
430
+ if (currentEditorIdRef.current !== handle.editorId) {
431
+ return;
432
+ }
433
+
381
434
  onLocalCommitRef.current?.();
382
435
  refresh(true);
383
436
  },
384
- [handle, refresh]
437
+ [ handle, refresh ]
385
438
  );
386
439
 
387
440
  const setContent = useCallback(
388
441
  (html: string) => {
389
442
  mutate(() =>
390
- handle.bridge.replaceDocument({ setHtml: html, history: 'undoableBoundary' })
391
- );
443
+ handle.bridge.replaceDocument({ setHtml: html, history: 'undoableBoundary' }));
392
444
  },
393
- [handle, mutate]
445
+ [ handle, mutate ]
394
446
  );
395
447
 
396
448
  const setContentJson = useCallback(
397
449
  (doc: DocumentJSON) => {
398
450
  mutate(() =>
399
- handle.bridge.replaceDocument({ setJson: doc, history: 'undoableBoundary' })
400
- );
451
+ handle.bridge.replaceDocument({ setJson: doc, history: 'undoableBoundary' }));
401
452
  },
402
- [handle, mutate]
453
+ [ handle, mutate ]
403
454
  );
404
455
 
405
456
  const clearContent = useCallback(() => {
@@ -409,15 +460,15 @@ export function useNativeEditorDocument(
409
460
  history: 'resetAndClear',
410
461
  });
411
462
  });
412
- }, [handle, mutate]);
463
+ }, [ handle, mutate ]);
413
464
 
414
465
  const undo = useCallback(() => {
415
466
  mutate(() => handle.bridge.undo());
416
- }, [handle, mutate]);
467
+ }, [ handle, mutate ]);
417
468
 
418
469
  const redo = useCallback(() => {
419
470
  mutate(() => handle.bridge.redo());
420
- }, [handle, mutate]);
471
+ }, [ handle, mutate ]);
421
472
 
422
473
  const getContent = useCallback((): string => {
423
474
  if (
@@ -427,8 +478,9 @@ export function useNativeEditorDocument(
427
478
  ) {
428
479
  return '';
429
480
  }
481
+
430
482
  return handle.bridge.getDocumentHtml();
431
- }, [handle]);
483
+ }, [ handle ]);
432
484
 
433
485
  const getContentJson = useCallback((): DocumentJSON => {
434
486
  if (
@@ -438,8 +490,9 @@ export function useNativeEditorDocument(
438
490
  ) {
439
491
  return {};
440
492
  }
493
+
441
494
  return handle.bridge.getDocumentJson();
442
- }, [handle]);
495
+ }, [ handle ]);
443
496
 
444
497
  const getIsEmpty = useCallback((): boolean => {
445
498
  if (
@@ -449,8 +502,9 @@ export function useNativeEditorDocument(
449
502
  ) {
450
503
  return true;
451
504
  }
505
+
452
506
  return handle.bridge.renderUpdate().documentIsEmpty;
453
- }, [handle]);
507
+ }, [ handle ]);
454
508
 
455
509
  const getTextContent = useCallback((): string => {
456
510
  if (
@@ -460,8 +514,9 @@ export function useNativeEditorDocument(
460
514
  ) {
461
515
  return '';
462
516
  }
517
+
463
518
  return handle.bridge.getDocumentHtml().replace(/<[^>]+>/g, '');
464
- }, [handle]);
519
+ }, [ handle ]);
465
520
 
466
521
  const canUndo = useCallback((): boolean => {
467
522
  if (
@@ -471,8 +526,9 @@ export function useNativeEditorDocument(
471
526
  ) {
472
527
  return false;
473
528
  }
529
+
474
530
  return handle.bridge.getState().canUndo;
475
- }, [handle]);
531
+ }, [ handle ]);
476
532
 
477
533
  const canRedo = useCallback((): boolean => {
478
534
  if (
@@ -482,10 +538,12 @@ export function useNativeEditorDocument(
482
538
  ) {
483
539
  return false;
484
540
  }
541
+
485
542
  return handle.bridge.getState().canRedo;
486
- }, [handle]);
543
+ }, [ handle ]);
487
544
 
488
545
  let renderedEngineView = engineView;
546
+
489
547
  if (engineView.editorId !== editorId) {
490
548
  if (handle.isDestroyed) {
491
549
  renderedEngineView = {
@@ -499,6 +557,7 @@ export function useNativeEditorDocument(
499
557
  };
500
558
  } else {
501
559
  const state = handle.bridge.getState();
560
+
502
561
  renderedEngineView = {
503
562
  editorId,
504
563
  ready: state.documentState !== 'AwaitRemote',
@@ -509,6 +568,7 @@ export function useNativeEditorDocument(
509
568
  contentKey: null,
510
569
  };
511
570
  }
571
+
512
572
  readyRef.current = { editorId, ready: renderedEngineView.ready };
513
573
  }
514
574